diff --git a/Logic/State/UserRelatedState.ts b/Logic/State/UserRelatedState.ts index 6d6f8ae89..3dafd0c68 100644 --- a/Logic/State/UserRelatedState.ts +++ b/Logic/State/UserRelatedState.ts @@ -11,7 +11,8 @@ import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater"; import {Changes} from "../Osm/Changes"; import ChangeToElementsActor from "../Actors/ChangeToElementsActor"; import PendingChangesUploader from "../Actors/PendingChangesUploader"; - +import * as translators from "../../assets/translators.json" + /** * The part of the state which keeps track of user-related stuff, e.g. the OSM-connection, * which layers they enabled, ... @@ -36,6 +37,8 @@ export default class UserRelatedState extends ElementsState { */ public favouriteLayers: UIEventSource; + public readonly isTranslator : UIEventSource; + constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) { super(layoutToUse); @@ -50,6 +53,21 @@ export default class UserRelatedState extends ElementsState { osmConfiguration: <'osm' | 'osm-test'>this.featureSwitchApiURL.data, attemptLogin: options?.attemptLogin }) + this.isTranslator = this.osmConnection.userDetails.map(ud => { + if(!ud.loggedIn){ + return false; + } + const name= ud.name.toLowerCase().replace(/\s+/g, '') + return translators.contributors.some(c => c.contributor.toLowerCase().replace(/\s+/g, '') === name) + }) + this.isTranslator.addCallbackAndRunD(ud => { + if(ud){ + Locale.showLinkToWeblate.setData(true) + } + }); + + QueryParameters.GetBooleanQueryParameter("fs-translation-mode",false,"If set, will show the translation buttons") + .addCallbackAndRunD(tr => Locale.showLinkToWeblate.setData(Locale.showLinkToWeblate.data || tr)) this.changes = new Changes(this, layoutToUse?.isLeftRightSensitive() ?? false) @@ -57,7 +75,7 @@ export default class UserRelatedState extends ElementsState { new ChangeToElementsActor(this.changes, this.allElements) new PendingChangesUploader(this.changes, this.selectedElement); - + this.mangroveIdentity = new MangroveIdentity( this.osmConnection.GetLongPreference("identity", "mangrove") ); diff --git a/Models/ThemeConfig/ExtraLinkConfig.ts b/Models/ThemeConfig/ExtraLinkConfig.ts index 641029f6f..9c792ab11 100644 --- a/Models/ThemeConfig/ExtraLinkConfig.ts +++ b/Models/ThemeConfig/ExtraLinkConfig.ts @@ -11,7 +11,7 @@ export default class ExtraLinkConfig { constructor(configJson: ExtraLinkConfigJson, context) { this.icon = configJson.icon - this.text = Translations.T(configJson.text) + this.text = Translations.T(configJson.text, "themes:"+context+".text") this.href = configJson.href this.newTab = configJson.newTab this.requirements = new Set(configJson.requirements) diff --git a/Models/ThemeConfig/FilterConfig.ts b/Models/ThemeConfig/FilterConfig.ts index b26e43deb..a76a67a23 100644 --- a/Models/ThemeConfig/FilterConfig.ts +++ b/Models/ThemeConfig/FilterConfig.ts @@ -38,7 +38,7 @@ export default class FilterConfig { this.id = json.id; let defaultSelection : number = undefined this.options = json.options.map((option, i) => { - const ctx = `${context}.options[${i}]`; + const ctx = `${context}.options.${i}`; const question = Translations.T( option.question, `${ctx}.question` diff --git a/Models/ThemeConfig/LayerConfig.ts b/Models/ThemeConfig/LayerConfig.ts index 8a080f00f..a74a90c57 100644 --- a/Models/ThemeConfig/LayerConfig.ts +++ b/Models/ThemeConfig/LayerConfig.ts @@ -72,6 +72,7 @@ export default class LayerConfig extends WithContextLoader { official: boolean = true ) { context = context + "." + json.id; + const translationContext = "layers:"+json.id super(json, context) this.id = json.id; @@ -125,7 +126,7 @@ export default class LayerConfig extends WithContextLoader { this.allowSplit = json.allowSplit ?? false; - this.name = Translations.T(json.name, context + ".name"); + this.name = Translations.T(json.name, translationContext + ".name"); this.units = (json.units ?? []).map(((unitJson, i) => Unit.fromJson(unitJson, `${context}.unit[${i}]`))) if (json.description !== undefined) { @@ -136,7 +137,7 @@ export default class LayerConfig extends WithContextLoader { this.description = Translations.T( json.description, - context + ".description" + translationContext + ".description" ); @@ -211,9 +212,9 @@ export default class LayerConfig extends WithContextLoader { } const config: PresetConfig = { - title: Translations.T(pr.title, `${context}.presets[${i}].title`), + title: Translations.T(pr.title, `${translationContext}.presets.${i}.title`), tags: pr.tags.map((t) => TagUtils.SimpleTag(t)), - description: Translations.T(pr.description, `${context}.presets[${i}].description`), + description: Translations.T(pr.description, `${translationContext}.presets.${i}.description`), preciseInput: preciseInput, exampleImages: pr.exampleImages } @@ -258,7 +259,7 @@ export default class LayerConfig extends WithContextLoader { this.filters = [] } else { this.filters = (json.filter ?? []).map((option, i) => { - return new FilterConfig(option, `${context}.filter-[${i}]`) + return new FilterConfig(option, `layers:${this.id}.filter.${i}`) }); } diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index be970107a..402f28892 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -67,7 +67,11 @@ export default class LayoutConfig { throw "The id of a theme should match [a-z0-9-_]*: " + json.id } } - context = (context ?? "") + "." + this.id; + if(context === undefined){ + context = this.id + }else{ + context = context + "." + this.id; + } this.maintainer = json.maintainer; this.credits = json.credits; this.version = json.version; @@ -99,10 +103,10 @@ export default class LayoutConfig { throw "Got undefined layers for " + json.id + " at " + context } } - this.title = new Translation(json.title, context + ".title"); - this.description = new Translation(json.description, context + ".description"); - this.shortDescription = json.shortDescription === undefined ? this.description.FirstSentence() : new Translation(json.shortDescription, context + ".shortdescription"); - this.descriptionTail = json.descriptionTail === undefined ? undefined : new Translation(json.descriptionTail, context + ".descriptionTail"); + this.title = new Translation(json.title, "themes:"+context + ".title"); + this.description = new Translation(json.description, "themes:"+context + ".description"); + this.shortDescription = json.shortDescription === undefined ? this.description.FirstSentence() : new Translation(json.shortDescription, "themes:"+context + ".shortdescription"); + this.descriptionTail = json.descriptionTail === undefined ? undefined : new Translation(json.descriptionTail, "themes:"+context + ".descriptionTail"); this.icon = json.icon; this.socialImage = json.socialImage ?? LayoutConfig.defaultSocialImage; if (this.socialImage === "") { @@ -125,7 +129,7 @@ export default class LayoutConfig { href: "https://mapcomplete.osm.be/{theme}.html?lat={lat}&lon={lon}&z={zoom}&language={language}", newTab: true, requirements: ["iframe","no-welcome-message"] - }, context) + }, context+".extraLink") this.clustering = { diff --git a/Models/ThemeConfig/TagRenderingConfig.ts b/Models/ThemeConfig/TagRenderingConfig.ts index 967b0cd0e..b1fcc78aa 100644 --- a/Models/ThemeConfig/TagRenderingConfig.ts +++ b/Models/ThemeConfig/TagRenderingConfig.ts @@ -54,7 +54,6 @@ export default class TagRenderingConfig { if (json === undefined) { throw "Initing a TagRenderingConfig with undefined in " + context; } - if (json === "questions") { // Very special value this.render = null; @@ -70,9 +69,23 @@ export default class TagRenderingConfig { json = "" + json } + let translationKey = context; + if(json["id"] !== undefined){ + const layerId = context.split(".")[0] + if(json["source"]){ + let src = json["source"]+":" + if(json["source"] === "shared-questions"){ + src += "shared_questions." + } + translationKey = `${src}${json["id"] ?? ""}` + }else{ + translationKey = `layers:${layerId}.tagRenderings.${json["id"] ?? ""}` + } + } + if (typeof json === "string") { - this.render = Translations.T(json, context + ".render"); + this.render = Translations.T(json, translationKey + ".render"); this.multiAnswer = false; return; } @@ -86,8 +99,8 @@ export default class TagRenderingConfig { this.group = json.group ?? ""; this.labels = json.labels ?? [] - this.render = Translations.T(json.render, context + ".render"); - this.question = Translations.T(json.question, context + ".question"); + this.render = Translations.T(json.render, translationKey + ".render"); + this.question = Translations.T(json.question, translationKey + ".question"); this.condition = TagUtils.Tag(json.condition ?? {"and": []}, `${context}.condition`); if (json.freeform) { @@ -101,7 +114,7 @@ export default class TagRenderingConfig { const typeDescription = Translations.t.validation[type]?.description placeholder = Translations.T(json.freeform.key+" ("+type+")") if(typeDescription !== undefined){ - placeholder = placeholder.Fuse(typeDescription, type) + placeholder = placeholder.Subs({[type]: typeDescription}) } } @@ -155,7 +168,7 @@ export default class TagRenderingConfig { this.mappings = json.mappings.map((mapping, i) => { - const ctx = `${context}.mapping[${i}]` + const ctx = `${translationKey}.mappings.${i}` if (mapping.then === undefined) { throw `${ctx}: Invalid mapping: if without body` } diff --git a/UI/Base/Link.ts b/UI/Base/Link.ts index d0c40faa4..d33679312 100644 --- a/UI/Base/Link.ts +++ b/UI/Base/Link.ts @@ -16,6 +16,7 @@ export default class Link extends BaseUIElement { if (this._embeddedShow === undefined) { throw "Error: got a link where embeddedShow is undefined" } + this.onClick(() => {}) } diff --git a/UI/Base/LinkToWeblate.ts b/UI/Base/LinkToWeblate.ts new file mode 100644 index 000000000..e2914ebd6 --- /dev/null +++ b/UI/Base/LinkToWeblate.ts @@ -0,0 +1,33 @@ +import {VariableUiElement} from "./VariableUIElement"; +import Locale from "../i18n/Locale"; +import Link from "./Link"; +import Svg from "../../Svg"; + +export default class LinkToWeblate extends VariableUiElement { + constructor(context: string, availableTranslations: object) { + super( Locale.language.map(ln => { + if (Locale.showLinkToWeblate.data === false) { + return undefined; + } + if(availableTranslations["*"] !== undefined){ + return undefined + } + const icon = Svg.translate_svg() + .SetClass("rounded-full border border-gray-400 inline-block w-4 h-4 m-1 weblate-link self-center") + if(availableTranslations[ln] === undefined){ + icon.SetClass("bg-red-400") + } + return new Link(icon, + LinkToWeblate.hrefToWeblate(ln, context), true) + } ,[Locale.showLinkToWeblate])); + this.SetClass("enable-links hidden-on-mobile") + } + + public static hrefToWeblate(language: string, contextKey: string): string{ + const [category, ...rest] = contextKey.split(":") + const key = rest.join(":") + + const baseUrl = "https://hosted.weblate.org/translate/mapcomplete/" + return baseUrl + category + "/" + language + "/?offset=1&q=context%3A%3D%22" + key + "%22" + } +} \ No newline at end of file diff --git a/UI/BigComponents/CopyrightPanel.ts b/UI/BigComponents/CopyrightPanel.ts index 73475b647..c1cadcf8b 100644 --- a/UI/BigComponents/CopyrightPanel.ts +++ b/UI/BigComponents/CopyrightPanel.ts @@ -23,6 +23,7 @@ import Constants from "../../Models/Constants"; import ContributorCount from "../../Logic/ContributorCount"; import Img from "../Base/Img"; import {Translation} from "../i18n/Translation"; +import TranslatorsPanel from "./TranslatorsPanel"; export class OpenIdEditor extends VariableUiElement { constructor(state: { locationControl: UIEventSource }, iconStyle?: string, objectId?: string) { @@ -110,7 +111,8 @@ export default class CopyrightPanel extends Combine { featurePipeline: FeaturePipeline, currentBounds: UIEventSource, locationControl: UIEventSource, - osmConnection: OsmConnection + osmConnection: OsmConnection, + isTranslator: UIEventSource }) { const t = Translations.t.general.attribution @@ -131,25 +133,21 @@ export default class CopyrightPanel extends Combine { }), new OpenIdEditor(state, iconStyle), new OpenMapillary(state, iconStyle), - new OpenJosm(state, iconStyle) + new OpenJosm(state, iconStyle), + new TranslatorsPanel(state, iconStyle) + ] const iconAttributions = layoutToUse.usedImages.map(CopyrightPanel.IconAttribution) let maintainer: BaseUIElement = undefined if (layoutToUse.maintainer !== undefined && layoutToUse.maintainer !== "" && layoutToUse.maintainer.toLowerCase() !== "mapcomplete") { - maintainer = Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.maintainer}) + maintainer = t.themeBy.Subs({author: layoutToUse.maintainer}) } const contributions = new ContributorCount(state).Contributors - super([ - Translations.t.general.attribution.attributionContent, - new FixedUiElement("MapComplete " + Constants.vNumber).SetClass("font-bold"), - maintainer, - new Combine(actionButtons).SetClass("block w-full"), - new FixedUiElement(layoutToUse.credits), - new VariableUiElement(contributions.map(contributions => { + const dataContributors = new VariableUiElement(contributions.map(contributions => { if (contributions === undefined) { return "" } @@ -170,20 +168,29 @@ export default class CopyrightPanel extends Combine { const contribs = links.join(", ") if (hiddenCount <= 0) { - return Translations.t.general.attribution.mapContributionsBy.Subs({ + return t.mapContributionsBy.Subs({ contributors: contribs }) } else { - return Translations.t.general.attribution.mapContributionsByAndHidden.Subs({ + return t.mapContributionsByAndHidden.Subs({ contributors: contribs, hiddenCount: hiddenCount }); } - })), - CopyrightPanel.CodeContributors(contributors, Translations.t.general.attribution.codeContributionsBy), - CopyrightPanel.CodeContributors(translators, Translations.t.general.attribution.translatedBy), + })) + + super([ + new Title(t.attributionTitle), + t.attributionContent, + maintainer, + new FixedUiElement(layoutToUse.credits), + dataContributors, + CopyrightPanel.CodeContributors(contributors, t.codeContributionsBy), + CopyrightPanel.CodeContributors(translators, t.translatedBy), + new FixedUiElement("MapComplete " + Constants.vNumber).SetClass("font-bold"), + new Combine(actionButtons).SetClass("block w-full"), new Title(t.iconAttribution.title, 3), ...iconAttributions ].map(e => e?.SetClass("mt-4"))); @@ -213,9 +220,9 @@ export default class CopyrightPanel extends Combine { private static IconAttribution(iconPath: string): BaseUIElement { if (iconPath.startsWith("http")) { - try{ + try { iconPath = "." + new URL(iconPath).pathname; - }catch(e){ + } catch (e) { console.warn(e) } } @@ -234,16 +241,16 @@ export default class CopyrightPanel extends Combine { new Img(iconPath).SetClass("w-12 min-h-12 mr-2 mb-2"), new Combine([ new FixedUiElement(license.authors.join("; ")).SetClass("font-bold"), - license.license, - new Combine([ ...sources.map(lnk => { - let sourceLinkContent = lnk; - try { - sourceLinkContent = new URL(lnk).hostname - } catch { - console.error("Not a valid URL:", lnk) - } - return new Link(sourceLinkContent, lnk, true).SetClass("mr-2 mb-2"); - })]).SetClass("flex flex-wrap") + license.license, + new Combine([...sources.map(lnk => { + let sourceLinkContent = lnk; + try { + sourceLinkContent = new URL(lnk).hostname + } catch { + console.error("Not a valid URL:", lnk) + } + return new Link(sourceLinkContent, lnk, true).SetClass("mr-2 mb-2"); + })]).SetClass("flex flex-wrap") ]).SetClass("flex flex-col").SetStyle("width: calc(100% - 50px - 0.5em); min-width: 12rem;") ]).SetClass("flex flex-wrap border-b border-gray-300 m-2 border-box") } diff --git a/UI/BigComponents/FilterView.ts b/UI/BigComponents/FilterView.ts index 9c83a9889..e707211a0 100644 --- a/UI/BigComponents/FilterView.ts +++ b/UI/BigComponents/FilterView.ts @@ -101,9 +101,7 @@ export default class FilterView extends VariableUiElement { iconStyle ); - const name: Translation = Translations.WT( - filteredLayer.layerDef.name - ); + const name: Translation = filteredLayer.layerDef.name.Clone() const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3"); diff --git a/UI/BigComponents/FullWelcomePaneWithTabs.ts b/UI/BigComponents/FullWelcomePaneWithTabs.ts index b9e149800..7daa95088 100644 --- a/UI/BigComponents/FullWelcomePaneWithTabs.ts +++ b/UI/BigComponents/FullWelcomePaneWithTabs.ts @@ -83,9 +83,7 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { new Combine( [ Translations.t.general.openStreetMapIntro.SetClass("link-underline"), - Translations.t.general.attribution.attributionTitle, new CopyrightPanel(state) - ] ) } diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index 1d3820cbe..4fa8d418a 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -9,7 +9,7 @@ import BaseUIElement from "../BaseUIElement"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import {UIEventSource} from "../../Logic/UIEventSource"; import Loc from "../../Models/Loc"; -import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection"; +import {OsmConnection} from "../../Logic/Osm/OsmConnection"; import UserRelatedState from "../../Logic/State/UserRelatedState"; import Toggle from "../Input/Toggle"; import {Utils} from "../../Utils"; @@ -53,7 +53,8 @@ export default class MoreScreen extends Combine { icon: string, title: any, shortDescription: any, - definition?: any + definition?: any, + mustHaveLanguage?: boolean }, isCustom: boolean = false ): BaseUIElement { @@ -109,7 +110,7 @@ export default class MoreScreen extends Combine { return new SubtleButton(layout.icon, new Combine([ `
`, - new Translation(layout.title), + new Translation(layout.title, !isCustom && !layout.mustHaveLanguage ? "themes:"+layout.id+".title" : undefined), `
`, `
`, new Translation(layout.shortDescription)?.SetClass("subtle") ?? "", @@ -142,9 +143,10 @@ export default class MoreScreen extends Combine { icon: string, title: any, shortDescription: any, - definition?: any + definition?: any, + isOfficial: boolean } = JSON.parse(str) - + 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) diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index 87e8730ef..2e8371a0a 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -117,7 +117,7 @@ export default class SimpleAddUI extends Toggle { selectedPreset.setData(undefined) } - const message = Translations.t.general.add.addNew.Subs({category: preset.name}); + const message = Translations.t.general.add.addNew.Subs({category: preset.name}, preset.name["context"]); return new ConfirmLocationOfPoint(state, filterViewIsOpened, preset, message, state.LastClickLocation.data, @@ -184,12 +184,13 @@ export default class SimpleAddUI extends Toggle { private static CreatePresetSelectButton(preset: PresetInfo) { + const title = Translations.t.general.add.addNew.Subs({ + category: preset.name + }, preset.name["context"]) return new SubtleButton( preset.icon(), new Combine([ - Translations.t.general.add.addNew.Subs({ - category: preset.name - }).SetClass("font-bold"), + title.SetClass("font-bold"), Translations.WT(preset.description)?.FirstSentence() ]).SetClass("flex flex-col") ) diff --git a/UI/BigComponents/TranslatorsPanel.ts b/UI/BigComponents/TranslatorsPanel.ts new file mode 100644 index 000000000..87f6118fa --- /dev/null +++ b/UI/BigComponents/TranslatorsPanel.ts @@ -0,0 +1,124 @@ +import Toggle from "../Input/Toggle"; +import Lazy from "../Base/Lazy"; +import {Utils} from "../../Utils"; +import Translations from "../i18n/Translations"; +import Combine from "../Base/Combine"; +import Locale from "../i18n/Locale"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import {Translation} from "../i18n/Translation"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import Link from "../Base/Link"; +import LinkToWeblate from "../Base/LinkToWeblate"; +import Toggleable from "../Base/Toggleable"; +import Title from "../Base/Title"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {SubtleButton} from "../Base/SubtleButton"; +import Svg from "../../Svg"; + + +class TranslatorsPanelContent extends Combine { + constructor(layout: LayoutConfig, isTranslator: UIEventSource) { + const t = Translations.t.translations + const completeness = new Map() + let total = 0 + const untranslated = new Map() + Utils.WalkObject(layout, (o, path) => { + const translation = o; + for (const lang of translation.SupportedLanguages()) { + completeness.set(lang, 1 + (completeness.get(lang) ?? 0)) + } + layout.title.SupportedLanguages().forEach(ln => { + const trans = translation.translations + if (trans["*"] !== undefined) { + return; + } + if (trans[ln] === undefined) { + if (!untranslated.has(ln)) { + untranslated.set(ln, []) + } + untranslated.get(ln).push(translation.context) + } + }) + if(translation.translations["*"] === undefined){ + total++ + } + }, o => { + if (o === undefined || o === null) { + return false; + } + return o instanceof Translation; + }) + + + const seed = t.completeness + for (const ln of Array.from(completeness.keys())) { + 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) + }) + + // "translationCompleteness": "Translations for {theme} in {language} are at {percentage}: {translated} out of {total}", + const translated = seed.Subs({total, theme: layout.title, + percentage: new Translation(completenessPercentage), + translated: new Translation(completenessTr) + }) + + 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)) + + const disable = new SubtleButton(undefined, t.deactivate) + .onClick(() => { + Locale.showLinkToWeblate.setData(false) + }) + + super([ + new Title( + Translations.t.translations.activateButton, + ), + new Toggle(t.isTranslator.SetClass("thanks block"), undefined, isTranslator), + t.help, + translated, + disable, + new VariableUiElement(Locale.language.map(ln => { + + const missing = missingTranslationsFor(ln) + if (missing.length === 0) { + return undefined + } + return new Toggleable( + new Title(Translations.t.translations.missing.Subs({count: missing.length})), + new Combine(missing).SetClass("flex flex-col") + ) + })) + ]) + + } +} + +export default class TranslatorsPanel extends Toggle { + + + constructor(state: { layoutToUse: LayoutConfig, isTranslator: UIEventSource }, iconStyle?: string) { + const t = Translations.t.translations + super( + 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 + ) + this.SetClass("hidden-on-mobile") + + } +} diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 92ee0dd11..046bd28f6 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -842,7 +842,7 @@ export default class SpecialVisualizations { return new LoginToggle( new Combine([ - new Title("Add a comment"), + new Title(t.addAComment), textField, new Combine([ stateButtons.SetClass("sm:mr-2"), diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index 59d7ae50b..bccb6e4e2 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -9,6 +9,7 @@ import Combine from "./Base/Combine"; import BaseUIElement from "./BaseUIElement"; import {DefaultGuiState} from "./DefaultGuiState"; import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; +import LinkToWeblate from "./Base/LinkToWeblate"; export class SubstitutedTranslation extends VariableUiElement { @@ -34,6 +35,8 @@ export class SubstitutedTranslation extends VariableUiElement { ) }) + const linkToWeblate = new LinkToWeblate(translation.context, translation.translations) + super( Locale.language.map(language => { let txt = translation?.textFor(language); @@ -44,7 +47,7 @@ export class SubstitutedTranslation extends VariableUiElement { txt = txt.replace(new RegExp(`{${key}}`, "g"), `{${key}()}`) }) - return new Combine(SubstitutedTranslation.ExtractSpecialComponents(txt, extraMappings).map( + const allElements = SubstitutedTranslation.ExtractSpecialComponents(txt, extraMappings).map( proto => { if (proto.fixed !== undefined) { return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags))); @@ -56,8 +59,12 @@ export class SubstitutedTranslation extends VariableUiElement { console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e) return new FixedUiElement(`Could not generate special rendering for ${viz.func.funcName}(${viz.args.join(", ")}) ${e}`).SetStyle("alert") } - } - )) + }); + allElements.push(linkToWeblate) + + return new Combine( + allElements + ) }) ) diff --git a/UI/Wikipedia/WikidataPreviewBox.ts b/UI/Wikipedia/WikidataPreviewBox.ts index 7a94c4e02..a5f125e5c 100644 --- a/UI/Wikipedia/WikidataPreviewBox.ts +++ b/UI/Wikipedia/WikidataPreviewBox.ts @@ -39,16 +39,12 @@ export default class WikidataPreviewBox extends VariableUiElement { { property: "P569", requires: WikidataPreviewBox.isHuman, - display: new Translation({ - "*": "Born: {value}" - }) + display: Translations.t.general.wikipedia.previewbox.born }, { property: "P570", requires: WikidataPreviewBox.isHuman, - display: new Translation({ - "*": "Died: {value}" - }) + display:Translations.t.general.wikipedia.previewbox.died } ] diff --git a/UI/i18n/Locale.ts b/UI/i18n/Locale.ts index 7c71fcbe1..47e99c110 100644 --- a/UI/i18n/Locale.ts +++ b/UI/i18n/Locale.ts @@ -7,7 +7,8 @@ import {QueryParameters} from "../../Logic/Web/QueryParameters"; export default class Locale { public static language: UIEventSource = Locale.setup(); - + public static showLinkToWeblate: UIEventSource = new UIEventSource(false); + private static setup() { const source = LocalStorageSource.Get('language', "en"); if (!Utils.runningFromConsole) { diff --git a/UI/i18n/Translation.ts b/UI/i18n/Translation.ts index 7c7027709..8e3af95f0 100644 --- a/UI/i18n/Translation.ts +++ b/UI/i18n/Translation.ts @@ -1,15 +1,21 @@ 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 { public static forcedLanguage = undefined; public readonly translations: object + context?: string; constructor(translations: object, context?: string) { super() + this.context = context; if (translations === undefined) { console.error("Translation without content at "+context) throw `Translation without content (${context})` @@ -101,13 +107,35 @@ export class Translation extends BaseUIElement { InnerConstructElement(): HTMLElement { const el = document.createElement("span") const self = this + + Locale.language.addCallbackAndRun(_ => { if (self.isDestroyed) { return true } el.innerHTML = this.txt }) - return el; + + if (self.translations["*"] !== undefined || self.context === undefined || self.context?.indexOf(":") < 0) { + return el; + } + + const linkToWeblate = new LinkToWeblate(self.context, self.translations) + + const wrapper = document.createElement("span") + wrapper.appendChild(el) + wrapper.classList.add("flex") + Locale.showLinkToWeblate.addCallbackAndRun(doShow => { + + if (!doShow) { + return; + } + wrapper.appendChild(linkToWeblate.ConstructElement()) + return true; + }) + + + return wrapper ; } public SupportedLanguages(): string[] { @@ -131,11 +159,25 @@ export class Translation extends BaseUIElement { return this.SupportedLanguages().map(lng => this.translations[lng]); } - public Subs(text: any): Translation { - return this.OnEveryLanguage((template, lang) => Utils.SubstituteKeys(template, text, lang)) + /** + * 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): Translation { + public OnEveryLanguage(f: (s: string, language: string) => string, context?: string): Translation { const newTranslations = {}; for (const lang in this.translations) { if (!this.translations.hasOwnProperty(lang)) { @@ -143,37 +185,10 @@ export class Translation extends BaseUIElement { } newTranslations[lang] = f(this.translations[lang], lang); } - return new Translation(newTranslations); + return new Translation(newTranslations, context ?? this.context); } - - /** - * - * Given a translation such as `{en: "How much of bicycle_types are rented here}` (which is this translation) - * and a translation object `{ en: "electrical bikes" }`, plus the translation specification `bicycle_types`, will return - * a new translation: - * `{en: "How much electrical bikes are rented here?"}` - * - * @param translationObject - * @param stringToReplace - * @constructor - */ - public Fuse(translationObject: Translation, stringToReplace: string): Translation{ - const translations = this.translations - const newTranslations = {} - for (const lang in translations) { - const target = translationObject.textFor(lang) - if(target === undefined){ - continue - } - if(typeof target !== "string"){ - throw "Invalid object in Translation.fuse: translationObject['"+lang+"'] is not a string, it is: "+JSON.stringify(target) - } - newTranslations[lang] = this.translations[lang].replaceAll(stringToReplace, target) - } - return new Translation(newTranslations) - } - + /** * Replaces the given string with the given text in the language. * Other substitutions are left in place @@ -190,7 +205,7 @@ export class Translation extends BaseUIElement { } public Clone() { - return new Translation(this.translations) + return new Translation(this.translations, this.context) } FirstSentence() { @@ -256,4 +271,6 @@ export class Translation extends BaseUIElement { AsMarkdown(): string { return this.txt } + + } \ No newline at end of file diff --git a/Utils.ts b/Utils.ts index 0a993b06c..1aa02822b 100644 --- a/Utils.ts +++ b/Utils.ts @@ -513,6 +513,37 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be return cp } + /** + * Walks an object recursively. Will hang on objects with loops + */ + static WalkObject(json: any, collect: (v: number | string | boolean | undefined, path: string[]) => any, isLeaf: (object) => boolean = undefined, path = []) { + if (json === undefined) { + return; + } + const jtp = typeof json + if (isLeaf !== undefined) { + if (jtp !== "object") { + return + } + + if (isLeaf(json)) { + return collect(json, path) + } + } else if (jtp === "boolean" || jtp === "string" || jtp === "number") { + return collect(json,path) + } + if (Array.isArray(json)) { + return json.map((sub,i) => { + return Utils.WalkObject(sub, collect, isLeaf,[...path, i]); + }) + } + + for (const key in json) { + Utils.WalkObject(json[key], collect, isLeaf, [...path,key]) + } + } + + static getOrSetDefault(dict: Map, k: K, v: () => V) { let found = dict.get(k); if (found !== undefined) { diff --git a/assets/contributors.json b/assets/contributors.json index dc33c379b..743b68e76 100644 --- a/assets/contributors.json +++ b/assets/contributors.json @@ -1 +1 @@ -{"contributors":[{"commits":3145,"contributor":"Pieter Vander Vennet"},{"commits":64,"contributor":"Robin van der Linde"},{"commits":38,"contributor":"Tobias"},{"commits":33,"contributor":"Christian Neumann"},{"commits":31,"contributor":"Win Olario"},{"commits":31,"contributor":"Pieter Fiers"},{"commits":26,"contributor":"karelleketers"},{"commits":24,"contributor":"Ward"},{"commits":20,"contributor":"Joost"},{"commits":19,"contributor":"Sebastian Kürten"},{"commits":18,"contributor":"Arno Deceuninck"},{"commits":17,"contributor":"pgm-chardelv1"},{"commits":16,"contributor":"Hosted Weblate"},{"commits":15,"contributor":"ToastHawaii"},{"commits":13,"contributor":"riQQ"},{"commits":13,"contributor":"Nicole"},{"commits":12,"contributor":"Tobias Jordans"},{"commits":12,"contributor":"Bavo Vanderghote"},{"commits":10,"contributor":"LiamSimons"},{"commits":8,"contributor":"dependabot[bot]"},{"commits":8,"contributor":"Midgard"},{"commits":7,"contributor":"RobJN"},{"commits":7,"contributor":"Mateusz Konieczny"},{"commits":7,"contributor":"Flo Edelmann"},{"commits":7,"contributor":"Binnette"},{"commits":7,"contributor":"yopaseopor"},{"commits":6,"contributor":"pelderson"},{"commits":5,"contributor":"David Haberthür"},{"commits":4,"contributor":"Ward Beyens"},{"commits":3,"contributor":"Léo Villeveygoux"},{"commits":2,"contributor":"arrival-spring"},{"commits":2,"contributor":"Strubbl"},{"commits":2,"contributor":"RayBB"},{"commits":2,"contributor":"Charlotte Delvaux"},{"commits":2,"contributor":"Supaplex"},{"commits":2,"contributor":"pbarban"},{"commits":2,"contributor":"graveelius"},{"commits":2,"contributor":"Stanislas Gueniffey"},{"commits":1,"contributor":"Jiří Podhorecký"},{"commits":1,"contributor":"Mark Rogerson"},{"commits":1,"contributor":"nicole_s"},{"commits":1,"contributor":"SC"},{"commits":1,"contributor":"Raphael Das Gupta"},{"commits":1,"contributor":"Nikolay Korotkiy"},{"commits":1,"contributor":"Seppe Santens"},{"commits":1,"contributor":"root"},{"commits":1,"contributor":"Allan Nordhøy"},{"commits":1,"contributor":"快乐的老鼠宝宝"},{"commits":1,"contributor":"Sebastian"},{"commits":1,"contributor":"Hiroshi Miura"},{"commits":1,"contributor":"riiga"},{"commits":1,"contributor":"Vinicius"},{"commits":1,"contributor":"Alexey Shabanov"},{"commits":1,"contributor":"Polgár Sándor"},{"commits":1,"contributor":"SiegbjornSitumeang"},{"commits":1,"contributor":"Marco"},{"commits":1,"contributor":"mozita"},{"commits":1,"contributor":"Schouppe Joost"},{"commits":1,"contributor":"Thibault Molleman"},{"commits":1,"contributor":"Noémie"},{"commits":1,"contributor":"Tomas Fiers"},{"commits":1,"contributor":"tbowdecl97"}]} \ No newline at end of file +{"contributors":[{"commits":3421,"contributor":"Pieter Vander Vennet"},{"commits":86,"contributor":"Robin van der Linde"},{"commits":39,"contributor":"Tobias"},{"commits":33,"contributor":"Christian Neumann"},{"commits":31,"contributor":"Win Olario"},{"commits":31,"contributor":"Pieter Fiers"},{"commits":26,"contributor":"karelleketers"},{"commits":24,"contributor":"Ward"},{"commits":20,"contributor":"Joost"},{"commits":19,"contributor":"Sebastian Kürten"},{"commits":18,"contributor":"riQQ"},{"commits":18,"contributor":"Arno Deceuninck"},{"commits":17,"contributor":"pgm-chardelv1"},{"commits":16,"contributor":"Hosted Weblate"},{"commits":15,"contributor":"ToastHawaii"},{"commits":13,"contributor":"Nicole"},{"commits":12,"contributor":"Tobias Jordans"},{"commits":12,"contributor":"Bavo Vanderghote"},{"commits":10,"contributor":"LiamSimons"},{"commits":8,"contributor":"dependabot[bot]"},{"commits":8,"contributor":"Midgard"},{"commits":7,"contributor":"RobJN"},{"commits":7,"contributor":"Mateusz Konieczny"},{"commits":7,"contributor":"Flo Edelmann"},{"commits":7,"contributor":"Binnette"},{"commits":7,"contributor":"yopaseopor"},{"commits":6,"contributor":"pelderson"},{"commits":5,"contributor":"David Haberthür"},{"commits":4,"contributor":"Ward Beyens"},{"commits":3,"contributor":"Weblate (bot)"},{"commits":3,"contributor":"Léo Villeveygoux"},{"commits":2,"contributor":"Codain"},{"commits":2,"contributor":"arrival-spring"},{"commits":2,"contributor":"Strubbl"},{"commits":2,"contributor":"RayBB"},{"commits":2,"contributor":"Charlotte Delvaux"},{"commits":2,"contributor":"Supaplex"},{"commits":2,"contributor":"pbarban"},{"commits":2,"contributor":"graveelius"},{"commits":2,"contributor":"Stanislas Gueniffey"},{"commits":1,"contributor":"Štefan Baebler"},{"commits":1,"contributor":"Jiří Podhorecký"},{"commits":1,"contributor":"Mark Rogerson"},{"commits":1,"contributor":"nicole_s"},{"commits":1,"contributor":"SC"},{"commits":1,"contributor":"Raphael Das Gupta"},{"commits":1,"contributor":"Nikolay Korotkiy"},{"commits":1,"contributor":"Seppe Santens"},{"commits":1,"contributor":"root"},{"commits":1,"contributor":"Allan Nordhøy"},{"commits":1,"contributor":"快乐的老鼠宝宝"},{"commits":1,"contributor":"Sebastian"},{"commits":1,"contributor":"Hiroshi Miura"},{"commits":1,"contributor":"riiga"},{"commits":1,"contributor":"Vinicius"},{"commits":1,"contributor":"Alexey Shabanov"},{"commits":1,"contributor":"Polgár Sándor"},{"commits":1,"contributor":"SiegbjornSitumeang"},{"commits":1,"contributor":"Marco"},{"commits":1,"contributor":"mozita"},{"commits":1,"contributor":"Schouppe Joost"},{"commits":1,"contributor":"Thibault Molleman"},{"commits":1,"contributor":"Noémie"},{"commits":1,"contributor":"Tomas Fiers"},{"commits":1,"contributor":"tbowdecl97"}]} \ No newline at end of file diff --git a/assets/layers/bike_shop/bike_shop.json b/assets/layers/bike_shop/bike_shop.json index b77f8df94..32b242157 100644 --- a/assets/layers/bike_shop/bike_shop.json +++ b/assets/layers/bike_shop/bike_shop.json @@ -298,18 +298,13 @@ }, "id": "bike_shop-email" }, - { - "render": "{opening_hours_table(opening_hours)}", - "question": "When is this shop opened?", - "freeform": { - "key": "opening_hours", - "type": "opening_hours" - }, - "id": "bike_shop-opening_hours" - }, + "opening_hours", "description", { - "render": "Enkel voor {access}", + "render": { + "en": "Only accessible to {access}", + "nl": "Enkel voor {access}" + }, "freeform": { "key": "access" }, diff --git a/assets/themes/uk_addresses/uk_addresses.json b/assets/themes/uk_addresses/uk_addresses.json index c7d1cc552..80bccb856 100644 --- a/assets/themes/uk_addresses/uk_addresses.json +++ b/assets/themes/uk_addresses/uk_addresses.json @@ -123,7 +123,7 @@ }, { "id": "uk_addresses_import_button", - "render":{ + "render": { "special": { "type": "import_button", "targetLayer": "address", diff --git a/assets/translators.json b/assets/translators.json index b2d361f78..7713d90b9 100644 --- a/assets/translators.json +++ b/assets/translators.json @@ -1 +1 @@ -{"contributors":[{"commits":60,"contributor":"danieldegroot2"},{"commits":41,"contributor":"kjon"},{"commits":29,"contributor":"Artem"},{"commits":23,"contributor":"Pieter Vander Vennet"},{"commits":22,"contributor":"Supaplex"},{"commits":22,"contributor":"Marco"},{"commits":22,"contributor":"Allan Nordhøy"},{"commits":21,"contributor":"Babos Gábor"},{"commits":21,"contributor":"Anonymous"},{"commits":15,"contributor":"WaldiS"},{"commits":14,"contributor":"J. Lavoie"},{"commits":13,"contributor":"SC"},{"commits":10,"contributor":"Reza Almanda"},{"commits":9,"contributor":"Jacque Fresco"},{"commits":8,"contributor":"LeJun"},{"commits":8,"contributor":"Irina"},{"commits":6,"contributor":"Nikolay Korotkiy"},{"commits":6,"contributor":"William Weber Berrutti"},{"commits":6,"contributor":"lvgx"},{"commits":5,"contributor":"Piotr"},{"commits":5,"contributor":"Robin van der Linde"},{"commits":5,"contributor":"seppesantens"},{"commits":5,"contributor":"Vinicius"},{"commits":5,"contributor":"Alexey Shabanov"},{"commits":4,"contributor":"Jeff Huang"},{"commits":4,"contributor":"Joost"},{"commits":4,"contributor":"Adolfo Jayme Barrientos"},{"commits":4,"contributor":"Polgár Sándor"},{"commits":4,"contributor":"David Haberthür"},{"commits":4,"contributor":"phlostically"},{"commits":4,"contributor":"Jan Zabel"},{"commits":4,"contributor":"Fabio Bettani"},{"commits":3,"contributor":"Sasha"},{"commits":3,"contributor":"Jose Luis Infante"},{"commits":3,"contributor":"Francois"},{"commits":3,"contributor":"Eduardo Addad de Oliveira"},{"commits":3,"contributor":"Wiktor Przybylski"},{"commits":3,"contributor":"Erik Palm"},{"commits":3,"contributor":"vankos"},{"commits":3,"contributor":"JCGF-OSM"},{"commits":3,"contributor":"Hiroshi Miura"},{"commits":3,"contributor":"SiegbjornSitumeang"},{"commits":2,"contributor":"わたなべけんご"},{"commits":2,"contributor":"Mateusz Konieczny"},{"commits":2,"contributor":"Kristoffer Grundström"},{"commits":2,"contributor":"el_libre como el chaval"},{"commits":2,"contributor":"Sebastian Kürten"},{"commits":2,"contributor":"Damian Tokarski"},{"commits":2,"contributor":"mic140"},{"commits":2,"contributor":"Heiko"},{"commits":2,"contributor":"Leo Alcaraz"},{"commits":1,"contributor":"sparky-oxford"},{"commits":1,"contributor":"jcn706"},{"commits":1,"contributor":"whatismoss"},{"commits":1,"contributor":"LePirlouit"},{"commits":1,"contributor":"SoftwareByRedline"},{"commits":1,"contributor":"plic ploc"},{"commits":1,"contributor":"Janina Ellinghaus"},{"commits":1,"contributor":"ssantos"},{"commits":1,"contributor":"Andre Fajar N"},{"commits":1,"contributor":"Ahen Purwakarta"},{"commits":1,"contributor":"Luna Jernberg"},{"commits":1,"contributor":"Rodrigo Tavares"},{"commits":1,"contributor":"liimee"},{"commits":1,"contributor":"Michał Targoński"},{"commits":1,"contributor":"Sean Young"},{"commits":1,"contributor":"Damian Pułka"},{"commits":1,"contributor":"Iváns"},{"commits":1,"contributor":"快乐的老鼠宝宝"},{"commits":1,"contributor":"Eric Armijo"},{"commits":1,"contributor":"Beardhatcode"},{"commits":1,"contributor":"riiga"},{"commits":1,"contributor":"Carlos Ramos Carreño"}]} \ No newline at end of file +{"contributors":[{"commits":60,"contributor":"danieldegroot2"},{"commits":43,"contributor":"kjon"},{"commits":29,"contributor":"Artem"},{"commits":26,"contributor":"Pieter Vander Vennet"},{"commits":25,"contributor":"Babos Gábor"},{"commits":22,"contributor":"Supaplex"},{"commits":22,"contributor":"Marco"},{"commits":22,"contributor":"Allan Nordhøy"},{"commits":21,"contributor":"Anonymous"},{"commits":15,"contributor":"WaldiS"},{"commits":14,"contributor":"Reza Almanda"},{"commits":14,"contributor":"J. Lavoie"},{"commits":13,"contributor":"SC"},{"commits":10,"contributor":"Robin van der Linde"},{"commits":9,"contributor":"Jacque Fresco"},{"commits":8,"contributor":"Joost"},{"commits":8,"contributor":"LeJun"},{"commits":8,"contributor":"Irina"},{"commits":6,"contributor":"Štefan Baebler"},{"commits":6,"contributor":"seppesantens"},{"commits":6,"contributor":"Nikolay Korotkiy"},{"commits":6,"contributor":"William Weber Berrutti"},{"commits":6,"contributor":"lvgx"},{"commits":5,"contributor":"Romain de Bossoreille"},{"commits":5,"contributor":"Piotr"},{"commits":5,"contributor":"Vinicius"},{"commits":5,"contributor":"Alexey Shabanov"},{"commits":4,"contributor":"Jeff Huang"},{"commits":4,"contributor":"Adolfo Jayme Barrientos"},{"commits":4,"contributor":"Polgár Sándor"},{"commits":4,"contributor":"David Haberthür"},{"commits":4,"contributor":"phlostically"},{"commits":4,"contributor":"Jan Zabel"},{"commits":4,"contributor":"Fabio Bettani"},{"commits":3,"contributor":"Sasha"},{"commits":3,"contributor":"Jose Luis Infante"},{"commits":3,"contributor":"Francois"},{"commits":3,"contributor":"Eduardo Addad de Oliveira"},{"commits":3,"contributor":"Wiktor Przybylski"},{"commits":3,"contributor":"Erik Palm"},{"commits":3,"contributor":"vankos"},{"commits":3,"contributor":"JCGF-OSM"},{"commits":3,"contributor":"Hiroshi Miura"},{"commits":3,"contributor":"SiegbjornSitumeang"},{"commits":2,"contributor":"MeblIkea"},{"commits":2,"contributor":"快乐的老鼠宝宝"},{"commits":2,"contributor":"わたなべけんご"},{"commits":2,"contributor":"Mateusz Konieczny"},{"commits":2,"contributor":"Kristoffer Grundström"},{"commits":2,"contributor":"el_libre como el chaval"},{"commits":2,"contributor":"Sebastian Kürten"},{"commits":2,"contributor":"Damian Tokarski"},{"commits":2,"contributor":"mic140"},{"commits":2,"contributor":"Heiko"},{"commits":2,"contributor":"Leo Alcaraz"},{"commits":1,"contributor":"Falk Rund"},{"commits":1,"contributor":"pdassori"},{"commits":1,"contributor":"sparky-oxford"},{"commits":1,"contributor":"jcn706"},{"commits":1,"contributor":"whatismoss"},{"commits":1,"contributor":"LePirlouit"},{"commits":1,"contributor":"SoftwareByRedline"},{"commits":1,"contributor":"plic ploc"},{"commits":1,"contributor":"Janina Ellinghaus"},{"commits":1,"contributor":"ssantos"},{"commits":1,"contributor":"Andre Fajar N"},{"commits":1,"contributor":"Ahen Purwakarta"},{"commits":1,"contributor":"Luna Jernberg"},{"commits":1,"contributor":"Rodrigo Tavares"},{"commits":1,"contributor":"liimee"},{"commits":1,"contributor":"Michał Targoński"},{"commits":1,"contributor":"Sean Young"},{"commits":1,"contributor":"Damian Pułka"},{"commits":1,"contributor":"Iváns"},{"commits":1,"contributor":"Eric Armijo"},{"commits":1,"contributor":"Beardhatcode"},{"commits":1,"contributor":"riiga"},{"commits":1,"contributor":"Carlos Ramos Carreño"}]} \ No newline at end of file diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index db8ca3cc6..b0527f323 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -1040,6 +1040,10 @@ video { height: 50%; } +.h-4 { + height: 1rem; +} + .h-screen { height: 100vh; } @@ -1060,10 +1064,6 @@ video { height: 4rem; } -.h-4 { - height: 1rem; -} - .h-0 { height: 0px; } @@ -1132,6 +1132,10 @@ video { width: 0px; } +.w-4 { + width: 1rem; +} + .w-screen { width: 100vw; } @@ -1140,10 +1144,6 @@ video { width: 2.75rem; } -.w-4 { - width: 1rem; -} - .w-16 { width: 4rem; } @@ -1412,6 +1412,11 @@ video { border-color: rgba(0, 0, 0, var(--tw-border-opacity)); } +.border-gray-400 { + --tw-border-opacity: 1; + border-color: rgba(156, 163, 175, var(--tw-border-opacity)); +} + .border-gray-300 { --tw-border-opacity: 1; border-color: rgba(209, 213, 219, var(--tw-border-opacity)); @@ -1422,11 +1427,6 @@ video { border-color: rgba(252, 165, 165, var(--tw-border-opacity)); } -.border-gray-400 { - --tw-border-opacity: 1; - border-color: rgba(156, 163, 175, var(--tw-border-opacity)); -} - .border-gray-200 { --tw-border-opacity: 1; border-color: rgba(229, 231, 235, var(--tw-border-opacity)); @@ -1441,6 +1441,11 @@ video { background-color: rgba(255, 255, 255, var(--tw-bg-opacity)); } +.bg-red-400 { + --tw-bg-opacity: 1; + background-color: rgba(248, 113, 113, var(--tw-bg-opacity)); +} + .bg-gray-400 { --tw-bg-opacity: 1; background-color: rgba(156, 163, 175, var(--tw-bg-opacity)); @@ -1518,6 +1523,10 @@ video { padding-left: 1rem; } +.pl-1 { + padding-left: 0.25rem; +} + .pl-2 { padding-left: 0.5rem; } @@ -1534,10 +1543,6 @@ video { padding-bottom: 0.25rem; } -.pl-1 { - padding-left: 0.25rem; -} - .pr-1 { padding-right: 0.25rem; } @@ -1908,6 +1913,10 @@ svg, img { display: none; } +.weblate-link { + /* Weblate-links are the little translation icon next to translatable sentences. Due to their special nature, they are exempt from some rules */ +} + .mapcontrol svg path { fill: var(--subtle-detail-color-contrast) !important; } diff --git a/index.css b/index.css index 921261d45..77c7b0160 100644 --- a/index.css +++ b/index.css @@ -151,6 +151,10 @@ svg, img { display: none; } +.weblate-link { + /* Weblate-links are the little translation icon next to translatable sentences. Due to their special nature, they are exempt from some rules */ +} + .mapcontrol svg path { fill: var(--subtle-detail-color-contrast) !important; } diff --git a/langs/en.json b/langs/en.json index d99a40519..64792e271 100644 --- a/langs/en.json +++ b/langs/en.json @@ -246,6 +246,10 @@ "loading": "Loading Wikipedia...", "noResults": "Nothing found for {search}", "noWikipediaPage": "This Wikidata item has no corresponding Wikipedia page yet.", + "previewbox": { + "born": "Born: {value}", + "died": "Died: {value}" + }, "searchWikidata": "Search on Wikidata", "wikipediaboxTitle": "Wikipedia" } @@ -359,6 +363,7 @@ "autoApply": "When changing the attributes {attr_names}, these attributes will automatically be changed on {count} other objects too" }, "notes": { + "addAComment": "Add a comment", "addComment": "Add comment", "addCommentAndClose": "Add comment and close", "addCommentPlaceholder": "Add a comment...", @@ -525,6 +530,15 @@ "split": "Split", "splitTitle": "Choose on the map where to split this road" }, + "translations": { + "activateButton": "Help to translate MapComplete", + "completeness": "Translations for {theme} in {language} are at {percentage}%: {translated} strings out of {total} are translated", + "deactivate": "Disable translation buttons", + "help": "Click the 'translate'-icon next to a string to enter or update a piece of text. You need a Weblate-account for this. Create one with your OSM-username to automatically unlock translation mode.", + "isTranslator": "Translation mode is active as your username matches the name of a previous translator", + "missing": "{count} untranslated strings", + "notImmediate": "Translations are not updated directly. This typically takes a few days" + }, "validation": { "color": { "description": "A color or hexcode" diff --git a/langs/layers/en.json b/langs/layers/en.json index 81b2589b9..98da61451 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -1062,6 +1062,9 @@ }, "question": "Are there tools here to repair your own bike?" }, + "bike_shop-access": { + "render": "Only accessible to {access}" + }, "bike_shop-email": { "question": "What is the email address of {name}?" }, diff --git a/langs/layers/nl.json b/langs/layers/nl.json index 892b8d487..894bf5b82 100644 --- a/langs/layers/nl.json +++ b/langs/layers/nl.json @@ -1062,6 +1062,9 @@ }, "question": "Biedt deze winkel gereedschap aan om je fiets zelf te herstellen?" }, + "bike_shop-access": { + "render": "Enkel voor {access}" + }, "bike_shop-email": { "question": "Wat is het email-adres van {name}?" }, diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index 5c8b4836b..d9faf2d9a 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -19,7 +19,7 @@ import {DesugaringContext} from "../Models/ThemeConfig/Conversion/Conversion"; class LayerOverviewUtils { - writeSmallOverview(themes: { id: string, title: any, shortDescription: any, icon: string, hideFromOverview: boolean }[]) { + writeSmallOverview(themes: { id: string, title: any, shortDescription: any, icon: string, hideFromOverview: boolean, mustHaveLanguage: boolean }[]) { const perId = new Map(); for (const theme of themes) { const data = { @@ -27,7 +27,8 @@ class LayerOverviewUtils { title: theme.title, shortDescription: theme.shortDescription, icon: theme.icon, - hideFromOverview: theme.hideFromOverview + hideFromOverview: theme.hideFromOverview, + mustHaveLanguage: theme.mustHaveLanguage } perId.set(theme.id, data); } @@ -73,6 +74,7 @@ class LayerOverviewUtils { continue } questions[key].id = key; + questions[key]["source"] = "shared-questions" dict.set(key, questions[key]) } for (const key in icons["default"]) { @@ -218,7 +220,8 @@ class LayerOverviewUtils { return { ...t, hideFromOverview: t.hideFromOverview ?? false, - shortDescription: t.shortDescription ?? new Translation(t.description).FirstSentence().translations + shortDescription: t.shortDescription ?? new Translation(t.description).FirstSentence().translations, + mustHaveLanguage: t.mustHaveLanguage?.length > 0 } })); return fixed; diff --git a/scripts/generateTranslations.ts b/scripts/generateTranslations.ts index a458af85c..29b750167 100644 --- a/scripts/generateTranslations.ts +++ b/scripts/generateTranslations.ts @@ -244,11 +244,11 @@ function isTranslation(tr: any): boolean { } /** - * Converts a translation object into something that can be added to the 'generated translations' - * @param obj - * @param depth + * Converts a translation object into something that can be added to the 'generated translations'. + * + * To debug the 'compiledTranslations', add a languageWhiteList to only generate a single language */ -function transformTranslation(obj: any, depth = 1) { +function transformTranslation(obj: any, path: string[] = [], languageWhitelist : string[] = undefined) { if (isTranslation(obj)) { return `new Translation( ${JSON.stringify(obj)} )` @@ -259,15 +259,24 @@ function transformTranslation(obj: any, depth = 1) { if (key === "#") { continue; } + if (key.match("^[a-zA-Z0-9_]*$") === null) { throw "Invalid character in key: " + key } - const value = obj[key] + let value = obj[key] if (isTranslation(value)) { - values += (Utils.Times((_) => " ", depth)) + "get " + key + "() { return new Translation(" + JSON.stringify(value) + ") }" + ",\n" + if(languageWhitelist !== undefined){ + const nv = {} + for (const ln of languageWhitelist) { + nv[ln] = value[ln] + } + value = nv; + } + values += `${Utils.Times((_) => " ", path.length + 1)}get ${key}() { return new Translation(${JSON.stringify(value)}, "core:${path.join(".")}.${key}") }, +` } else { - values += (Utils.Times((_) => " ", depth)) + key + ": " + transformTranslation(value, depth + 1) + ",\n" + values += (Utils.Times((_) => " ", path.length + 1)) + key + ": " + transformTranslation(value, [...path, key], languageWhitelist) + ",\n" } } return `{${values}}`; @@ -305,11 +314,11 @@ function formatFile(path) { */ function genTranslations() { const translations = JSON.parse(fs.readFileSync("./assets/generated/translations.json", "utf-8")) - const transformed = transformTranslation(translations); + const transformed = transformTranslation(translations); let module = `import {Translation} from "../../UI/i18n/Translation"\n\nexport default class CompiledTranslations {\n\n`; module += " public static t = " + transformed; - module += "}" + module += "\n }" fs.writeFileSync("./assets/generated/CompiledTranslations.ts", module); @@ -541,7 +550,7 @@ for (const path of allTranslationFiles) { } -// SOme validation +// Some validation TranslationPart.fromDirectory("./langs").validateStrict("./langs") TranslationPart.fromDirectory("./langs/layers").validateStrict("layers") TranslationPart.fromDirectory("./langs/themes").validateStrict("themes")