Merge develop

This commit is contained in:
Pieter Vander Vennet 2022-04-19 01:39:03 +02:00
commit ccf9c4b5f6
50 changed files with 1427 additions and 766 deletions

View file

@ -4,6 +4,7 @@ import Link from "./Link";
import Svg from "../../Svg";
export default class LinkToWeblate extends VariableUiElement {
private static URI: any;
constructor(context: string, availableTranslations: object) {
super( Locale.language.map(ln => {
if (Locale.showLinkToWeblate.data === false) {
@ -36,4 +37,10 @@ export default class LinkToWeblate extends VariableUiElement {
const baseUrl = "https://hosted.weblate.org/translate/mapcomplete/"
return baseUrl + category + "/" + language + "/?offset=1&q=context%3A%3D%22" + key + "%22"
}
public static hrefToWeblateZen(language: string, category: string, searchKey: string): string{
const baseUrl = "https://hosted.weblate.org/zen/mapcomplete/"
// ?offset=1&q=+state%3A%3Ctranslated+context%3Acampersite&sort_by=-priority%2Cposition&checksum=
return baseUrl + category + "/" + language + "?offset=1&q=+state%3A%3Ctranslated+context%3A"+encodeURIComponent(searchKey)+"&sort_by=-priority%2Cposition&checksum="
}
}

View file

@ -49,7 +49,7 @@ export default abstract class BaseUIElement {
*/
public SetClass(clss: string) {
if (clss == undefined) {
return
return this
}
const all = clss.split(" ").map(clsName => clsName.trim());
let recordedChange = false;

View file

@ -14,7 +14,7 @@ export default class AddNewMarker extends Combine {
let last = undefined;
for (const filteredLayer of filteredLayers) {
const layer = filteredLayer.layerDef;
if(layer.name === undefined){
if(layer.name === undefined && !filteredLayer.isDisplayed.data){
continue
}
for (const preset of filteredLayer.layerDef.presets) {

View file

@ -22,7 +22,7 @@ import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import Constants from "../../Models/Constants";
import ContributorCount from "../../Logic/ContributorCount";
import Img from "../Base/Img";
import {Translation} from "../i18n/Translation";
import {TypedTranslation} from "../i18n/Translation";
import TranslatorsPanel from "./TranslatorsPanel";
export class OpenIdEditor extends VariableUiElement {
@ -198,7 +198,7 @@ export default class CopyrightPanel extends Combine {
this.SetStyle("max-width:100%; width: 40rem; margin-left: 0.75rem; margin-right: 0.5rem")
}
private static CodeContributors(contributors, translation: Translation): BaseUIElement {
private static CodeContributors(contributors, translation: TypedTranslation<{contributors, hiddenCount}>): BaseUIElement {
const total = contributors.contributors.length;
let filtered = [...contributors.contributors]

View file

@ -90,10 +90,10 @@ export default class MoreScreen extends Combine {
}
let hash = ""
if(layout.definition !== undefined){
hash = "#"+btoa(JSON.stringify(layout.definition))
if (layout.definition !== undefined) {
hash = "#" + btoa(JSON.stringify(layout.definition))
}
const linkText = currentLocation?.map(currentLocation => {
const params = [
["z", currentLocation?.zoom],
@ -106,11 +106,10 @@ export default class MoreScreen extends Combine {
}) ?? new UIEventSource<string>(`${linkPrefix}`)
return new SubtleButton(layout.icon,
new Combine([
`<dt class='text-lg leading-6 font-medium text-gray-900 group-hover:text-blue-800'>`,
new Translation(layout.title, !isCustom && !layout.mustHaveLanguage ? "themes:"+layout.id+".title" : undefined),
new Translation(layout.title, !isCustom && !layout.mustHaveLanguage ? "themes:" + layout.id + ".title" : undefined),
`</dt>`,
`<dd class='mt-1 text-base text-gray-500 group-hover:text-blue-900 overflow-ellipsis'>`,
new Translation(layout.shortDescription)?.SetClass("subtle") ?? "",
@ -128,15 +127,13 @@ export default class MoreScreen extends Combine {
}
private static createUnofficialButtonFor(state: UserRelatedState, id: string): BaseUIElement {
const allPreferences = state.osmConnection.preferencesHandler.preferences.data;
const length = Number(allPreferences[id + "-length"])
let str = "";
for (let i = 0; i < length; i++) {
str += allPreferences[id + "-" + i]
}
if(str === undefined || str === "undefined"){
const pref = state.osmConnection.GetLongPreference(id)
const str = pref.data
if (str === undefined || str === "undefined" || str === "") {
pref.setData(null)
return undefined
}
try {
const value: {
id: string
@ -149,7 +146,8 @@ export default class MoreScreen extends Combine {
value.isOfficial = false
return MoreScreen.createLinkButton(state, value, true)
} catch (e) {
console.debug("Could not parse unofficial theme information for " + id, "The json is: ", str, e)
console.warn("Removing theme " + id + " as it could not be parsed from the preferences")
pref.setData(null)
return undefined
}
}
@ -163,16 +161,14 @@ export default class MoreScreen extends Combine {
for (const key in allPreferences) {
if (key.startsWith(prefix) && key.endsWith("-combined-length")) {
const id = key.substring(0, key.length - "-length".length)
const id = key.substring("mapcomplete-".length, key.length - "-combined-length".length)
ids.push(id)
}
}
return ids
});
var stableIds = UIEventSource.ListStabilized<string>(currentIds)
return new VariableUiElement(
stableIds.map(ids => {
const allThemes: BaseUIElement[] = []
@ -182,12 +178,11 @@ export default class MoreScreen extends Combine {
allThemes.push(link.SetClass(buttonClass))
}
}
if (allThemes.length <= 0) {
return undefined;
}
return new Combine([
Translations.t.general.customThemeIntro.Clone(),
Translations.t.general.customThemeIntro,
new Combine(allThemes).SetClass(themeListClasses)
]);
}));

View file

@ -208,15 +208,20 @@ export default class SimpleAddUI extends Toggle {
const allButtons = [];
for (const layer of state.filteredLayers.data) {
if (layer.isDisplayed.data === false && !state.featureSwitchFilter.data) {
// The layer is not displayed and we cannot enable the layer control -> we skip
continue;
if (layer.isDisplayed.data === false) {
// The layer is not displayed...
if(!state.featureSwitchFilter.data){
// ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway
continue;
}
if (layer.layerDef.name === undefined) {
// this layer can never be toggled on in any case, so we skip the presets
continue;
}
}
if (layer.layerDef.name === undefined) {
// this is a parlty hidden layer
continue;
}
const presets = layer.layerDef.presets;
for (const preset of presets) {

View file

@ -14,7 +14,9 @@ import Title from "../Base/Title";
import {UIEventSource} from "../../Logic/UIEventSource";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import * as native_languages from "../../assets/language_native.json"
import * as used_languages from "../../assets/generated/used_languages.json"
import BaseUIElement from "../BaseUIElement";
class TranslatorsPanelContent extends Combine {
constructor(layout: LayoutConfig, isTranslator: UIEventSource<boolean>) {
@ -24,36 +26,53 @@ class TranslatorsPanelContent extends Combine {
const seed = t.completeness
for (const ln of Array.from(completeness.keys())) {
if(ln === "*"){
if (ln === "*") {
continue
}
if (seed.translations[ln] === undefined) {
seed.translations[ln] = seed.translations["en"]
}
}
const completenessTr = {}
const completenessPercentage = {}
seed.SupportedLanguages().forEach(ln => {
completenessTr[ln] = ""+(completeness.get(ln) ?? 0)
completenessPercentage[ln] = ""+Math.round(100 * (completeness.get(ln) ?? 0) / total)
completenessTr[ln] = "" + (completeness.get(ln) ?? 0)
completenessPercentage[ln] = "" + Math.round(100 * (completeness.get(ln) ?? 0) / total)
})
const missingTranslationsFor = (ln: string) => Utils.NoNull(untranslated.get(ln) ?? [])
.filter(ctx => ctx.indexOf(":") >= 0)
.map(ctx => ctx.replace(/note_import_[a-zA-Z0-9_]*/, "note_import"))
.map(context => new Link(context, LinkToWeblate.hrefToWeblate(ln, context), true))
function missingTranslationsFor(language: string): BaseUIElement[] {
// e.g. "themes:<themename>.layers.0.tagRenderings..., or "layers:<layername>.description
const missingKeys = Utils.NoNull(untranslated.get(language) ?? [])
.filter(ctx => ctx.indexOf(":") >= 0)
.map(ctx => ctx.replace(/note_import_[a-zA-Z0-9_]*/, "note_import"))
const hasMissingTheme = missingKeys.some(k => k.startsWith("themes:"))
const missingLayers = Utils.Dedup( missingKeys.filter(k => k.startsWith("layers:"))
.map(k => k.slice("layers:".length).split(".")[0]))
console.log("Getting untranslated string for",language,"raw:",missingKeys,"hasMissingTheme:",hasMissingTheme,"missingLayers:",missingLayers)
return [
hasMissingTheme ? new Link("themes:" + layout.id + ".* (zen mode)", LinkToWeblate.hrefToWeblateZen(language, "themes", layout.id), true) : undefined,
...missingLayers.map(id => new Link("layer:" + id + ".* (zen mode)", LinkToWeblate.hrefToWeblateZen(language, "layers", id), true)),
...missingKeys.map(context => new Link(context, LinkToWeblate.hrefToWeblate(language, context), true))
]
}
//
//
// "translationCompleteness": "Translations for {theme} in {language} are at {percentage}: {translated} out of {total}",
const translated = seed.Subs({total, theme: layout.title,
const translated = seed.Subs({
total, theme: layout.title,
percentage: new Translation(completenessPercentage),
translated: new Translation(completenessTr)
translated: new Translation(completenessTr),
language: seed.OnEveryLanguage((_, lng) => native_languages[lng] ?? lng)
})
super([
new Title(
Translations.t.translations.activateButton,
Translations.t.translations.activateButton,
),
new Toggle(t.isTranslator.SetClass("thanks block"), undefined, isTranslator),
t.help,
@ -63,15 +82,18 @@ class TranslatorsPanelContent extends Combine {
.onClick(() => {
Locale.showLinkToWeblate.setData(false)
}),
new VariableUiElement(Locale.language.map(ln => {
new VariableUiElement(Locale.language.map(ln => {
const missing = missingTranslationsFor(ln)
if (missing.length === 0) {
return undefined
}
let title = Translations.t.translations.allMissing;
if(untranslated.get(ln) !== undefined){
title = Translations.t.translations.missing.Subs({count: untranslated.get(ln).length})
}
return new Toggleable(
new Title(Translations.t.translations.missing.Subs({count: missing.length})),
new Title(title),
new Combine(missing).SetClass("flex flex-col")
)
}))
@ -83,38 +105,37 @@ class TranslatorsPanelContent extends Combine {
export default class TranslatorsPanel extends Toggle {
constructor(state: { layoutToUse: LayoutConfig, isTranslator: UIEventSource<boolean> }, iconStyle?: string) {
const t = Translations.t.translations
super(
new Lazy(() => new TranslatorsPanelContent(state.layoutToUse, state.isTranslator)
new Lazy(() => new TranslatorsPanelContent(state.layoutToUse, state.isTranslator)
).SetClass("flex flex-col"),
new SubtleButton(Svg.translate_ui().SetStyle(iconStyle), t.activateButton).onClick(() => Locale.showLinkToWeblate.setData(true)),
Locale.showLinkToWeblate
Locale.showLinkToWeblate
)
this.SetClass("hidden-on-mobile")
}
public static MissingTranslationsFor(layout: LayoutConfig) : {completeness: Map<string, number>, untranslated: Map<string, string[]>, total: number} {
public static MissingTranslationsFor(layout: LayoutConfig): { completeness: Map<string, number>, untranslated: Map<string, string[]>, total: number } {
let total = 0
const completeness = new Map<string, number>()
const untranslated = new Map<string, string[]>()
Utils.WalkObject(layout, (o, path) => {
const translation = <Translation><any>o;
if(translation.translations["*"] !== undefined){
if (translation.translations["*"] !== undefined) {
return
}
if(translation.context === undefined || translation.context.indexOf(":") < 0){
if (translation.context === undefined || translation.context.indexOf(":") < 0) {
// no source given - lets ignore
return
}
for (const lang of translation.SupportedLanguages()) {
completeness.set(lang, 1 + (completeness.get(lang) ?? 0))
}
layout.title.SupportedLanguages().forEach(ln => {
total ++
used_languages.languages.forEach(ln => {
const trans = translation.translations
if (trans["*"] !== undefined) {
return;
@ -124,11 +145,11 @@ export default class TranslatorsPanel extends Toggle {
untranslated.set(ln, [])
}
untranslated.get(ln).push(translation.context)
}else{
completeness.set(ln, 1 + (completeness.get(ln) ?? 0))
}
})
if(translation.translations["*"] === undefined){
total++
}
}, o => {
if (o === undefined || o === null) {
return false;

View file

@ -248,7 +248,7 @@ export default class TagRenderingQuestion extends Combine {
const inputEl = new InputElementMap<number[], TagsFilter>(
checkBoxes,
(t0, t1) => {
return t0?.isEquivalent(t1) ?? false
return t0?.shadows(t1) ?? false
},
(indices) => {
if (indices.length === 0) {
@ -370,7 +370,7 @@ export default class TagRenderingQuestion extends Combine {
return new FixedInputElement(
TagRenderingQuestion.GenerateMappingContent(mapping, tagsSource, state),
tagging,
(t0, t1) => t1.isEquivalent(t0));
(t0, t1) => t1.shadows(t0));
}
private static GenerateMappingContent(mapping: {
@ -450,7 +450,7 @@ export default class TagRenderingQuestion extends Combine {
})
let inputTagsFilter: InputElement<TagsFilter> = new InputElementMap(
input, (a, b) => a === b || (a?.isEquivalent(b) ?? false),
input, (a, b) => a === b || (a?.shadows(b) ?? false),
pickString, toString
);

View file

@ -25,7 +25,7 @@ export default class ReviewElement extends VariableUiElement {
SingleReview.GenStars(avg),
new Link(
revs.length === 1 ? Translations.t.reviews.title_singular.Clone() :
Translations.t.reviews.title.Clone()
Translations.t.reviews.title
.Subs({count: "" + revs.length}),
`https://mangrove.reviews/search?sub=${encodeURIComponent(subject)}`,
true

View file

@ -1,7 +1,7 @@
import {VariableUiElement} from "../Base/VariableUIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata";
import {Translation} from "../i18n/Translation";
import {Translation, TypedTranslation} from "../i18n/Translation";
import {FixedUiElement} from "../Base/FixedUiElement";
import Loading from "../Base/Loading";
import Translations from "../i18n/Translations";
@ -22,7 +22,7 @@ export default class WikidataPreviewBox extends VariableUiElement {
private static extraProperties: {
requires?: { p: number, q?: number }[],
property: string,
display: Translation | Map<string, string | (() => BaseUIElement) /*If translation: Subs({value: * }) */>
display: TypedTranslation<{value}> | Map<string, string | (() => BaseUIElement) /*If translation: Subs({value: * }) */>
}[] = [
{
requires: WikidataPreviewBox.isHuman,

View file

@ -1,9 +1,6 @@
import Locale from "./Locale";
import {Utils} from "../../Utils";
import BaseUIElement from "../BaseUIElement";
import Link from "../Base/Link";
import Svg from "../../Svg";
import {VariableUiElement} from "../Base/VariableUIElement";
import LinkToWeblate from "../Base/LinkToWeblate";
export class Translation extends BaseUIElement {
@ -164,25 +161,7 @@ export class Translation extends BaseUIElement {
public AllValues(): string[] {
return this.SupportedLanguages().map(lng => this.translations[lng]);
}
/**
* Substitutes text in a translation.
* If a translation is passed, it'll be fused
*
* // Should replace simple keys
* new Translation({"en": "Some text {key}"}).Subs({key: "xyz"}).textFor("en") // => "Some text xyz"
*
* // Should fuse translations
* const subpart = new Translation({"en": "subpart","nl":"onderdeel"})
* const tr = new Translation({"en": "Full sentence with {part}", nl: "Volledige zin met {part}"})
* const subbed = tr.Subs({part: subpart})
* subbed.textFor("en") // => "Full sentence with subpart"
* subbed.textFor("nl") // => "Volledige zin met onderdeel"
*/
public Subs(text: any, context?: string): Translation {
return this.OnEveryLanguage((template, lang) => Utils.SubstituteKeys(template, text, lang), context)
}
public OnEveryLanguage(f: (s: string, language: string) => string, context?: string): Translation {
const newTranslations = {};
for (const lang in this.translations) {
@ -278,5 +257,28 @@ export class Translation extends BaseUIElement {
return this.txt
}
}
export class TypedTranslation<T> extends Translation {
constructor(translations: object, context?: string) {
super(translations, context);
}
/**
* Substitutes text in a translation.
* If a translation is passed, it'll be fused
*
* // Should replace simple keys
* new TypedTranslation<object>({"en": "Some text {key}"}).Subs({key: "xyz"}).textFor("en") // => "Some text xyz"
*
* // Should fuse translations
* const subpart = new Translation({"en": "subpart","nl":"onderdeel"})
* const tr = new TypedTranslation<object>({"en": "Full sentence with {part}", nl: "Volledige zin met {part}"})
* const subbed = tr.Subs({part: subpart})
* subbed.textFor("en") // => "Full sentence with subpart"
* subbed.textFor("nl") // => "Volledige zin met onderdeel"
*/
Subs(text: T, context?: string): Translation {
return this.OnEveryLanguage((template, lang) => Utils.SubstituteKeys(template, text, lang), context)
}
}

View file

@ -1,5 +1,5 @@
import {FixedUiElement} from "../Base/FixedUiElement";
import {Translation} from "./Translation";
import {Translation, TypedTranslation} from "./Translation";
import BaseUIElement from "../BaseUIElement";
import * as known_languages from "../../assets/generated/used_languages.json"
import CompiledTranslations from "../../assets/generated/CompiledTranslations";
@ -22,7 +22,7 @@ export default class Translations {
return s;
}
static T(t: string | any, context = undefined): Translation {
static T(t: string | any, context = undefined): TypedTranslation<object> {
if (t === undefined || t === null) {
return undefined;
}
@ -30,17 +30,17 @@ export default class Translations {
t = "" + t
}
if (typeof t === "string") {
return new Translation({"*": t}, context);
return new TypedTranslation({"*": t}, context);
}
if (t.render !== undefined) {
const msg = "Creating a translation, but this object contains a 'render'-field. Use the translation directly"
console.error(msg, t);
throw msg
}
if (t instanceof Translation) {
if (t instanceof TypedTranslation) {
return t;
}
return new Translation(t, context);
return new TypedTranslation(t, context);
}
/**