forked from MapComplete/MapComplete
		
	Add translation buttons
This commit is contained in:
		
							parent
							
								
									592bc4ae0b
								
							
						
					
					
						commit
						2c7fb556dc
					
				
					 31 changed files with 442 additions and 150 deletions
				
			
		|  | @ -11,6 +11,7 @@ import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater"; | ||||||
| import {Changes} from "../Osm/Changes"; | import {Changes} from "../Osm/Changes"; | ||||||
| import ChangeToElementsActor from "../Actors/ChangeToElementsActor"; | import ChangeToElementsActor from "../Actors/ChangeToElementsActor"; | ||||||
| import PendingChangesUploader from "../Actors/PendingChangesUploader"; | 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, |  * The part of the state which keeps track of user-related stuff, e.g. the OSM-connection, | ||||||
|  | @ -36,6 +37,8 @@ export default class UserRelatedState extends ElementsState { | ||||||
|      */ |      */ | ||||||
|     public favouriteLayers: UIEventSource<string[]>; |     public favouriteLayers: UIEventSource<string[]>; | ||||||
| 
 | 
 | ||||||
|  |     public readonly isTranslator : UIEventSource<boolean>; | ||||||
|  |      | ||||||
|     constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) { |     constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) { | ||||||
|         super(layoutToUse); |         super(layoutToUse); | ||||||
| 
 | 
 | ||||||
|  | @ -50,6 +53,21 @@ export default class UserRelatedState extends ElementsState { | ||||||
|             osmConfiguration: <'osm' | 'osm-test'>this.featureSwitchApiURL.data, |             osmConfiguration: <'osm' | 'osm-test'>this.featureSwitchApiURL.data, | ||||||
|             attemptLogin: options?.attemptLogin |             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) |         this.changes = new Changes(this, layoutToUse?.isLeftRightSensitive() ?? false) | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ export default class ExtraLinkConfig { | ||||||
| 
 | 
 | ||||||
|     constructor(configJson: ExtraLinkConfigJson, context) { |     constructor(configJson: ExtraLinkConfigJson, context) { | ||||||
|         this.icon = configJson.icon |         this.icon = configJson.icon | ||||||
|         this.text = Translations.T(configJson.text) |         this.text = Translations.T(configJson.text, "themes:"+context+".text") | ||||||
|         this.href = configJson.href |         this.href = configJson.href | ||||||
|         this.newTab = configJson.newTab |         this.newTab = configJson.newTab | ||||||
|         this.requirements = new Set(configJson.requirements) |         this.requirements = new Set(configJson.requirements) | ||||||
|  |  | ||||||
|  | @ -38,7 +38,7 @@ export default class FilterConfig { | ||||||
|         this.id = json.id; |         this.id = json.id; | ||||||
|         let defaultSelection : number = undefined |         let defaultSelection : number = undefined | ||||||
|         this.options = json.options.map((option, i) => { |         this.options = json.options.map((option, i) => { | ||||||
|             const ctx = `${context}.options[${i}]`; |             const ctx = `${context}.options.${i}`; | ||||||
|             const question = Translations.T( |             const question = Translations.T( | ||||||
|                 option.question, |                 option.question, | ||||||
|                 `${ctx}.question` |                 `${ctx}.question` | ||||||
|  |  | ||||||
|  | @ -72,6 +72,7 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|         official: boolean = true |         official: boolean = true | ||||||
|     ) { |     ) { | ||||||
|         context = context + "." + json.id; |         context = context + "." + json.id; | ||||||
|  |         const translationContext = "layers:"+json.id | ||||||
|         super(json, context) |         super(json, context) | ||||||
|         this.id = json.id; |         this.id = json.id; | ||||||
| 
 | 
 | ||||||
|  | @ -125,7 +126,7 @@ export default class LayerConfig extends WithContextLoader { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         this.allowSplit = json.allowSplit ?? false; |         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}]`))) |         this.units = (json.units ?? []).map(((unitJson, i) => Unit.fromJson(unitJson, `${context}.unit[${i}]`))) | ||||||
| 
 | 
 | ||||||
|         if (json.description !== undefined) { |         if (json.description !== undefined) { | ||||||
|  | @ -136,7 +137,7 @@ export default class LayerConfig extends WithContextLoader { | ||||||
| 
 | 
 | ||||||
|         this.description = Translations.T( |         this.description = Translations.T( | ||||||
|             json.description, |             json.description, | ||||||
|             context + ".description" |             translationContext + ".description" | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -211,9 +212,9 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const config: PresetConfig = { |             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)), |                 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, |                 preciseInput: preciseInput, | ||||||
|                 exampleImages: pr.exampleImages |                 exampleImages: pr.exampleImages | ||||||
|             } |             } | ||||||
|  | @ -258,7 +259,7 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|             this.filters = [] |             this.filters = [] | ||||||
|         } else { |         } else { | ||||||
|             this.filters = (<FilterConfigJson[]>json.filter ?? []).map((option, i) => { |             this.filters = (<FilterConfigJson[]>json.filter ?? []).map((option, i) => { | ||||||
|                 return new FilterConfig(option, `${context}.filter-[${i}]`) |                 return new FilterConfig(option, `layers:${this.id}.filter.${i}`) | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -67,7 +67,11 @@ export default class LayoutConfig { | ||||||
|                 throw "The id of a theme should match [a-z0-9-_]*: " + json.id |                 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.maintainer = json.maintainer; | ||||||
|         this.credits = json.credits; |         this.credits = json.credits; | ||||||
|         this.version = json.version; |         this.version = json.version; | ||||||
|  | @ -99,10 +103,10 @@ export default class LayoutConfig { | ||||||
|                 throw "Got undefined layers for " + json.id + " at " + context |                 throw "Got undefined layers for " + json.id + " at " + context | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         this.title = new Translation(json.title, context + ".title"); |         this.title = new Translation(json.title, "themes:"+context + ".title"); | ||||||
|         this.description = new Translation(json.description, context + ".description"); |         this.description = new Translation(json.description, "themes:"+context + ".description"); | ||||||
|         this.shortDescription = json.shortDescription === undefined ? this.description.FirstSentence() : new Translation(json.shortDescription, context + ".shortdescription"); |         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, context + ".descriptionTail"); |         this.descriptionTail = json.descriptionTail === undefined ? undefined : new Translation(json.descriptionTail, "themes:"+context + ".descriptionTail"); | ||||||
|         this.icon = json.icon; |         this.icon = json.icon; | ||||||
|         this.socialImage = json.socialImage ?? LayoutConfig.defaultSocialImage; |         this.socialImage = json.socialImage ?? LayoutConfig.defaultSocialImage; | ||||||
|         if (this.socialImage === "") { |         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}", |             href: "https://mapcomplete.osm.be/{theme}.html?lat={lat}&lon={lon}&z={zoom}&language={language}", | ||||||
|             newTab: true, |             newTab: true, | ||||||
|             requirements: ["iframe","no-welcome-message"] |             requirements: ["iframe","no-welcome-message"] | ||||||
|         }, context) |         }, context+".extraLink") | ||||||
|      |      | ||||||
| 
 | 
 | ||||||
|         this.clustering = { |         this.clustering = { | ||||||
|  |  | ||||||
|  | @ -54,7 +54,6 @@ export default class TagRenderingConfig { | ||||||
|         if (json === undefined) { |         if (json === undefined) { | ||||||
|             throw "Initing a TagRenderingConfig with undefined in " + context; |             throw "Initing a TagRenderingConfig with undefined in " + context; | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|         if (json === "questions") { |         if (json === "questions") { | ||||||
|             // Very special value
 |             // Very special value
 | ||||||
|             this.render = null; |             this.render = null; | ||||||
|  | @ -70,9 +69,23 @@ export default class TagRenderingConfig { | ||||||
|             json = "" + json |             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") { |         if (typeof json === "string") { | ||||||
|             this.render = Translations.T(json, context + ".render"); |             this.render = Translations.T(json, translationKey + ".render"); | ||||||
|             this.multiAnswer = false; |             this.multiAnswer = false; | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  | @ -86,8 +99,8 @@ export default class TagRenderingConfig { | ||||||
| 
 | 
 | ||||||
|         this.group = json.group ?? ""; |         this.group = json.group ?? ""; | ||||||
|         this.labels = json.labels ?? [] |         this.labels = json.labels ?? [] | ||||||
|         this.render = Translations.T(json.render, context + ".render"); |         this.render = Translations.T(json.render, translationKey + ".render"); | ||||||
|         this.question = Translations.T(json.question, context + ".question"); |         this.question = Translations.T(json.question, translationKey + ".question"); | ||||||
|         this.condition = TagUtils.Tag(json.condition ?? {"and": []}, `${context}.condition`); |         this.condition = TagUtils.Tag(json.condition ?? {"and": []}, `${context}.condition`); | ||||||
|         if (json.freeform) { |         if (json.freeform) { | ||||||
| 
 | 
 | ||||||
|  | @ -101,7 +114,7 @@ export default class TagRenderingConfig { | ||||||
|                 const typeDescription = Translations.t.validation[type]?.description |                 const typeDescription = Translations.t.validation[type]?.description | ||||||
|                 placeholder = Translations.T(json.freeform.key+" ("+type+")") |                 placeholder = Translations.T(json.freeform.key+" ("+type+")") | ||||||
|                 if(typeDescription !== undefined){ |                 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) => { |             this.mappings = json.mappings.map((mapping, i) => { | ||||||
| 
 | 
 | ||||||
|                 const ctx = `${context}.mapping[${i}]` |                 const ctx = `${translationKey}.mappings.${i}` | ||||||
|                 if (mapping.then === undefined) { |                 if (mapping.then === undefined) { | ||||||
|                     throw `${ctx}: Invalid mapping: if without body` |                     throw `${ctx}: Invalid mapping: if without body` | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  | @ -16,6 +16,7 @@ export default class Link extends BaseUIElement { | ||||||
|         if (this._embeddedShow === undefined) { |         if (this._embeddedShow === undefined) { | ||||||
|             throw "Error: got a link where embeddedShow is undefined" |             throw "Error: got a link where embeddedShow is undefined" | ||||||
|         } |         } | ||||||
|  |         this.onClick(() => {}) | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										33
									
								
								UI/Base/LinkToWeblate.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								UI/Base/LinkToWeblate.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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" | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -23,6 +23,7 @@ import Constants from "../../Models/Constants"; | ||||||
| import ContributorCount from "../../Logic/ContributorCount"; | import ContributorCount from "../../Logic/ContributorCount"; | ||||||
| import Img from "../Base/Img"; | import Img from "../Base/Img"; | ||||||
| import {Translation} from "../i18n/Translation"; | import {Translation} from "../i18n/Translation"; | ||||||
|  | import TranslatorsPanel from "./TranslatorsPanel"; | ||||||
| 
 | 
 | ||||||
| export class OpenIdEditor extends VariableUiElement { | export class OpenIdEditor extends VariableUiElement { | ||||||
|     constructor(state: { locationControl: UIEventSource<Loc> }, iconStyle?: string, objectId?: string) { |     constructor(state: { locationControl: UIEventSource<Loc> }, iconStyle?: string, objectId?: string) { | ||||||
|  | @ -110,7 +111,8 @@ export default class CopyrightPanel extends Combine { | ||||||
|         featurePipeline: FeaturePipeline, |         featurePipeline: FeaturePipeline, | ||||||
|         currentBounds: UIEventSource<BBox>, |         currentBounds: UIEventSource<BBox>, | ||||||
|         locationControl: UIEventSource<Loc>, |         locationControl: UIEventSource<Loc>, | ||||||
|         osmConnection: OsmConnection |         osmConnection: OsmConnection, | ||||||
|  |         isTranslator: UIEventSource<boolean> | ||||||
|     }) { |     }) { | ||||||
| 
 | 
 | ||||||
|         const t = Translations.t.general.attribution |         const t = Translations.t.general.attribution | ||||||
|  | @ -131,25 +133,21 @@ export default class CopyrightPanel extends Combine { | ||||||
|             }), |             }), | ||||||
|             new OpenIdEditor(state, iconStyle), |             new OpenIdEditor(state, iconStyle), | ||||||
|             new OpenMapillary(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) |         const iconAttributions = layoutToUse.usedImages.map(CopyrightPanel.IconAttribution) | ||||||
| 
 | 
 | ||||||
|         let maintainer: BaseUIElement = undefined |         let maintainer: BaseUIElement = undefined | ||||||
|         if (layoutToUse.maintainer !== undefined && layoutToUse.maintainer !== "" && layoutToUse.maintainer.toLowerCase() !== "mapcomplete") { |         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 |         const contributions = new ContributorCount(state).Contributors | ||||||
| 
 | 
 | ||||||
|         super([ |         const dataContributors =  new VariableUiElement(contributions.map(contributions => { | ||||||
|             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 => { |  | ||||||
|                 if (contributions === undefined) { |                 if (contributions === undefined) { | ||||||
|                     return "" |                     return "" | ||||||
|                 } |                 } | ||||||
|  | @ -170,20 +168,29 @@ export default class CopyrightPanel extends Combine { | ||||||
|                 const contribs = links.join(", ") |                 const contribs = links.join(", ") | ||||||
| 
 | 
 | ||||||
|                 if (hiddenCount <= 0) { |                 if (hiddenCount <= 0) { | ||||||
|                     return Translations.t.general.attribution.mapContributionsBy.Subs({ |                     return t.mapContributionsBy.Subs({ | ||||||
|                         contributors: contribs |                         contributors: contribs | ||||||
|                     }) |                     }) | ||||||
|                 } else { |                 } else { | ||||||
|                     return Translations.t.general.attribution.mapContributionsByAndHidden.Subs({ |                     return t.mapContributionsByAndHidden.Subs({ | ||||||
|                         contributors: contribs, |                         contributors: contribs, | ||||||
|                         hiddenCount: hiddenCount |                         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), |             new Title(t.iconAttribution.title, 3), | ||||||
|             ...iconAttributions |             ...iconAttributions | ||||||
|         ].map(e => e?.SetClass("mt-4"))); |         ].map(e => e?.SetClass("mt-4"))); | ||||||
|  | @ -213,9 +220,9 @@ export default class CopyrightPanel extends Combine { | ||||||
| 
 | 
 | ||||||
|     private static IconAttribution(iconPath: string): BaseUIElement { |     private static IconAttribution(iconPath: string): BaseUIElement { | ||||||
|         if (iconPath.startsWith("http")) { |         if (iconPath.startsWith("http")) { | ||||||
|             try{ |             try { | ||||||
|                 iconPath = "." + new URL(iconPath).pathname; |                 iconPath = "." + new URL(iconPath).pathname; | ||||||
|             }catch(e){ |             } catch (e) { | ||||||
|                 console.warn(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 Img(iconPath).SetClass("w-12 min-h-12 mr-2 mb-2"), | ||||||
|             new Combine([ |             new Combine([ | ||||||
|                 new FixedUiElement(license.authors.join("; ")).SetClass("font-bold"), |                 new FixedUiElement(license.authors.join("; ")).SetClass("font-bold"), | ||||||
|                     license.license, |                 license.license, | ||||||
|                     new Combine([    ...sources.map(lnk => { |                 new Combine([...sources.map(lnk => { | ||||||
|                             let sourceLinkContent = lnk; |                     let sourceLinkContent = lnk; | ||||||
|                             try { |                     try { | ||||||
|                                 sourceLinkContent = new URL(lnk).hostname |                         sourceLinkContent = new URL(lnk).hostname | ||||||
|                             } catch { |                     } catch { | ||||||
|                                 console.error("Not a valid URL:", lnk) |                         console.error("Not a valid URL:", lnk) | ||||||
|                             } |                     } | ||||||
|                             return new Link(sourceLinkContent, lnk, true).SetClass("mr-2 mb-2"); |                     return new Link(sourceLinkContent, lnk, true).SetClass("mr-2 mb-2"); | ||||||
|                         })]).SetClass("flex flex-wrap") |                 })]).SetClass("flex flex-wrap") | ||||||
|             ]).SetClass("flex flex-col").SetStyle("width: calc(100% - 50px - 0.5em); min-width: 12rem;") |             ]).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") |         ]).SetClass("flex flex-wrap border-b border-gray-300 m-2 border-box") | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -101,9 +101,7 @@ export default class FilterView extends VariableUiElement { | ||||||
|             iconStyle |             iconStyle | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         const name: Translation = Translations.WT( |         const name: Translation = filteredLayer.layerDef.name.Clone() | ||||||
|             filteredLayer.layerDef.name |  | ||||||
|         ); |  | ||||||
| 
 | 
 | ||||||
|         const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3"); |         const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3"); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -83,9 +83,7 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { | ||||||
|                 new Combine( |                 new Combine( | ||||||
|                     [ |                     [ | ||||||
|                         Translations.t.general.openStreetMapIntro.SetClass("link-underline"), |                         Translations.t.general.openStreetMapIntro.SetClass("link-underline"), | ||||||
|                         Translations.t.general.attribution.attributionTitle, |  | ||||||
|                         new CopyrightPanel(state) |                         new CopyrightPanel(state) | ||||||
| 
 |  | ||||||
|                     ] |                     ] | ||||||
|                 ) |                 ) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ import BaseUIElement from "../BaseUIElement"; | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import Loc from "../../Models/Loc"; | 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 UserRelatedState from "../../Logic/State/UserRelatedState"; | ||||||
| import Toggle from "../Input/Toggle"; | import Toggle from "../Input/Toggle"; | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
|  | @ -53,7 +53,8 @@ export default class MoreScreen extends Combine { | ||||||
|             icon: string, |             icon: string, | ||||||
|             title: any, |             title: any, | ||||||
|             shortDescription: any, |             shortDescription: any, | ||||||
|             definition?: any |             definition?: any, | ||||||
|  |             mustHaveLanguage?: boolean | ||||||
|         }, isCustom: boolean = false |         }, isCustom: boolean = false | ||||||
|     ): |     ): | ||||||
|         BaseUIElement { |         BaseUIElement { | ||||||
|  | @ -109,7 +110,7 @@ export default class MoreScreen extends Combine { | ||||||
|         return new SubtleButton(layout.icon, |         return new SubtleButton(layout.icon, | ||||||
|             new Combine([ |             new Combine([ | ||||||
|                 `<dt class='text-lg leading-6 font-medium text-gray-900 group-hover:text-blue-800'>`, |                 `<dt class='text-lg leading-6 font-medium text-gray-900 group-hover:text-blue-800'>`, | ||||||
|                 new Translation(layout.title), |                 new Translation(layout.title, !isCustom && !layout.mustHaveLanguage ? "themes:"+layout.id+".title" : undefined), | ||||||
|                 `</dt>`, |                 `</dt>`, | ||||||
|                 `<dd class='mt-1 text-base text-gray-500 group-hover:text-blue-900 overflow-ellipsis'>`, |                 `<dd class='mt-1 text-base text-gray-500 group-hover:text-blue-900 overflow-ellipsis'>`, | ||||||
|                 new Translation(layout.shortDescription)?.SetClass("subtle") ?? "", |                 new Translation(layout.shortDescription)?.SetClass("subtle") ?? "", | ||||||
|  | @ -142,9 +143,10 @@ export default class MoreScreen extends Combine { | ||||||
|                 icon: string, |                 icon: string, | ||||||
|                 title: any, |                 title: any, | ||||||
|                 shortDescription: any, |                 shortDescription: any, | ||||||
|                 definition?: any |                 definition?: any, | ||||||
|  |                 isOfficial: boolean | ||||||
|             } = JSON.parse(str) |             } = JSON.parse(str) | ||||||
| 
 |             value.isOfficial = false | ||||||
|             return MoreScreen.createLinkButton(state, value, true) |             return MoreScreen.createLinkButton(state, value, true) | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.debug("Could not parse unofficial theme information for " + id, "The json is: ", str, e) |             console.debug("Could not parse unofficial theme information for " + id, "The json is: ", str, e) | ||||||
|  |  | ||||||
|  | @ -117,7 +117,7 @@ export default class SimpleAddUI extends Toggle { | ||||||
|                         selectedPreset.setData(undefined) |                         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, |                     return new ConfirmLocationOfPoint(state, filterViewIsOpened, preset, | ||||||
|                         message, |                         message, | ||||||
|                         state.LastClickLocation.data, |                         state.LastClickLocation.data, | ||||||
|  | @ -184,12 +184,13 @@ export default class SimpleAddUI extends Toggle { | ||||||
| 
 | 
 | ||||||
|     private static CreatePresetSelectButton(preset: PresetInfo) { |     private static CreatePresetSelectButton(preset: PresetInfo) { | ||||||
| 
 | 
 | ||||||
|  |         const title = Translations.t.general.add.addNew.Subs({ | ||||||
|  |             category: preset.name | ||||||
|  |         }, preset.name["context"]) | ||||||
|         return new SubtleButton( |         return new SubtleButton( | ||||||
|             preset.icon(), |             preset.icon(), | ||||||
|             new Combine([ |             new Combine([ | ||||||
|                 Translations.t.general.add.addNew.Subs({ |                 title.SetClass("font-bold"), | ||||||
|                     category: preset.name |  | ||||||
|                 }).SetClass("font-bold"), |  | ||||||
|                 Translations.WT(preset.description)?.FirstSentence() |                 Translations.WT(preset.description)?.FirstSentence() | ||||||
|             ]).SetClass("flex flex-col") |             ]).SetClass("flex flex-col") | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
							
								
								
									
										124
									
								
								UI/BigComponents/TranslatorsPanel.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								UI/BigComponents/TranslatorsPanel.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<boolean>) { | ||||||
|  |         const t = Translations.t.translations | ||||||
|  |         const completeness = new Map<string, number>() | ||||||
|  |         let total = 0 | ||||||
|  |         const untranslated = new Map<string, string[]>() | ||||||
|  |         Utils.WalkObject(layout, (o, path) => { | ||||||
|  |             const translation = <Translation><any>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<boolean> }, 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") | ||||||
|  |          | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -842,7 +842,7 @@ export default class SpecialVisualizations { | ||||||
| 
 | 
 | ||||||
|                         return new LoginToggle( |                         return new LoginToggle( | ||||||
|                             new Combine([ |                             new Combine([ | ||||||
|                                 new Title("Add a comment"), |                                 new Title(t.addAComment), | ||||||
|                                 textField, |                                 textField, | ||||||
|                                 new Combine([ |                                 new Combine([ | ||||||
|                                     stateButtons.SetClass("sm:mr-2"), |                                     stateButtons.SetClass("sm:mr-2"), | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ import Combine from "./Base/Combine"; | ||||||
| import BaseUIElement from "./BaseUIElement"; | import BaseUIElement from "./BaseUIElement"; | ||||||
| import {DefaultGuiState} from "./DefaultGuiState"; | import {DefaultGuiState} from "./DefaultGuiState"; | ||||||
| import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; | import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; | ||||||
|  | import LinkToWeblate from "./Base/LinkToWeblate"; | ||||||
| 
 | 
 | ||||||
| export class SubstitutedTranslation extends VariableUiElement { | export class SubstitutedTranslation extends VariableUiElement { | ||||||
| 
 | 
 | ||||||
|  | @ -34,6 +35,8 @@ export class SubstitutedTranslation extends VariableUiElement { | ||||||
|             ) |             ) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  |         const linkToWeblate = new LinkToWeblate(translation.context, translation.translations) | ||||||
|  |          | ||||||
|         super( |         super( | ||||||
|             Locale.language.map(language => { |             Locale.language.map(language => { | ||||||
|                 let txt = translation?.textFor(language); |                 let txt = translation?.textFor(language); | ||||||
|  | @ -44,7 +47,7 @@ export class SubstitutedTranslation extends VariableUiElement { | ||||||
|                     txt = txt.replace(new RegExp(`{${key}}`, "g"), `{${key}()}`) |                     txt = txt.replace(new RegExp(`{${key}}`, "g"), `{${key}()}`) | ||||||
|                 }) |                 }) | ||||||
| 
 | 
 | ||||||
|                 return new Combine(SubstitutedTranslation.ExtractSpecialComponents(txt, extraMappings).map( |                 const allElements = SubstitutedTranslation.ExtractSpecialComponents(txt, extraMappings).map( | ||||||
|                     proto => { |                     proto => { | ||||||
|                         if (proto.fixed !== undefined) { |                         if (proto.fixed !== undefined) { | ||||||
|                             return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags))); |                             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) |                             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") |                             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 | ||||||
|  |                 ) | ||||||
|             }) |             }) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -39,16 +39,12 @@ export default class WikidataPreviewBox extends VariableUiElement { | ||||||
|         { |         { | ||||||
|             property: "P569", |             property: "P569", | ||||||
|             requires: WikidataPreviewBox.isHuman, |             requires: WikidataPreviewBox.isHuman, | ||||||
|             display: new Translation({ |             display: Translations.t.general.wikipedia.previewbox.born | ||||||
|                 "*": "Born: {value}" |  | ||||||
|             }) |  | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             property: "P570", |             property: "P570", | ||||||
|             requires: WikidataPreviewBox.isHuman, |             requires: WikidataPreviewBox.isHuman, | ||||||
|             display: new Translation({ |             display:Translations.t.general.wikipedia.previewbox.died | ||||||
|                 "*": "Died: {value}" |  | ||||||
|             }) |  | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ import {QueryParameters} from "../../Logic/Web/QueryParameters"; | ||||||
| export default class Locale { | export default class Locale { | ||||||
| 
 | 
 | ||||||
|     public static language: UIEventSource<string> = Locale.setup(); |     public static language: UIEventSource<string> = Locale.setup(); | ||||||
|  |     public static showLinkToWeblate: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||||
|      |      | ||||||
|     private static setup() { |     private static setup() { | ||||||
|         const source = LocalStorageSource.Get('language', "en"); |         const source = LocalStorageSource.Get('language', "en"); | ||||||
|  |  | ||||||
|  | @ -1,15 +1,21 @@ | ||||||
| import Locale from "./Locale"; | import Locale from "./Locale"; | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | 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 { | export class Translation extends BaseUIElement { | ||||||
| 
 | 
 | ||||||
|     public static forcedLanguage = undefined; |     public static forcedLanguage = undefined; | ||||||
| 
 | 
 | ||||||
|     public readonly translations: object |     public readonly translations: object | ||||||
|  |     context?: string; | ||||||
| 
 | 
 | ||||||
|     constructor(translations: object, context?: string) { |     constructor(translations: object, context?: string) { | ||||||
|         super() |         super() | ||||||
|  |         this.context = context; | ||||||
|         if (translations === undefined) { |         if (translations === undefined) { | ||||||
|             console.error("Translation without content at "+context) |             console.error("Translation without content at "+context) | ||||||
|             throw `Translation without content (${context})` |             throw `Translation without content (${context})` | ||||||
|  | @ -101,13 +107,35 @@ export class Translation extends BaseUIElement { | ||||||
|     InnerConstructElement(): HTMLElement { |     InnerConstructElement(): HTMLElement { | ||||||
|         const el = document.createElement("span") |         const el = document.createElement("span") | ||||||
|         const self = this |         const self = this | ||||||
|  |          | ||||||
|  |         | ||||||
|         Locale.language.addCallbackAndRun(_ => { |         Locale.language.addCallbackAndRun(_ => { | ||||||
|             if (self.isDestroyed) { |             if (self.isDestroyed) { | ||||||
|                 return true |                 return true | ||||||
|             } |             } | ||||||
|             el.innerHTML = this.txt |             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[] { |     public SupportedLanguages(): string[] { | ||||||
|  | @ -131,11 +159,25 @@ export class Translation extends BaseUIElement { | ||||||
|         return this.SupportedLanguages().map(lng => this.translations[lng]); |         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 = {}; |         const newTranslations = {}; | ||||||
|         for (const lang in this.translations) { |         for (const lang in this.translations) { | ||||||
|             if (!this.translations.hasOwnProperty(lang)) { |             if (!this.translations.hasOwnProperty(lang)) { | ||||||
|  | @ -143,37 +185,10 @@ export class Translation extends BaseUIElement { | ||||||
|             } |             } | ||||||
|             newTranslations[lang] = f(this.translations[lang], lang); |             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. |      * Replaces the given string with the given text in the language. | ||||||
|      * Other substitutions are left in place |      * Other substitutions are left in place | ||||||
|  | @ -190,7 +205,7 @@ export class Translation extends BaseUIElement { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public Clone() { |     public Clone() { | ||||||
|         return new Translation(this.translations) |         return new Translation(this.translations, this.context) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     FirstSentence() { |     FirstSentence() { | ||||||
|  | @ -256,4 +271,6 @@ export class Translation extends BaseUIElement { | ||||||
|     AsMarkdown(): string { |     AsMarkdown(): string { | ||||||
|         return this.txt |         return this.txt | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  | 
 | ||||||
| } | } | ||||||
							
								
								
									
										31
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										31
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -513,6 +513,37 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|         return cp |         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<K, V>(dict: Map<K, V>, k: K, v: () => V) { |     static getOrSetDefault<K, V>(dict: Map<K, V>, k: K, v: () => V) { | ||||||
|         let found = dict.get(k); |         let found = dict.get(k); | ||||||
|         if (found !== undefined) { |         if (found !== undefined) { | ||||||
|  |  | ||||||
|  | @ -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"}]} | {"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"}]} | ||||||
|  | @ -298,18 +298,13 @@ | ||||||
|       }, |       }, | ||||||
|       "id": "bike_shop-email" |       "id": "bike_shop-email" | ||||||
|     }, |     }, | ||||||
|     { |     "opening_hours", | ||||||
|       "render": "{opening_hours_table(opening_hours)}", |  | ||||||
|       "question": "When is this shop opened?", |  | ||||||
|       "freeform": { |  | ||||||
|         "key": "opening_hours", |  | ||||||
|         "type": "opening_hours" |  | ||||||
|       }, |  | ||||||
|       "id": "bike_shop-opening_hours" |  | ||||||
|     }, |  | ||||||
|     "description", |     "description", | ||||||
|     { |     { | ||||||
|       "render": "Enkel voor {access}", |       "render": { | ||||||
|  |         "en": "Only accessible to {access}", | ||||||
|  |         "nl": "Enkel voor {access}" | ||||||
|  |       }, | ||||||
|       "freeform": { |       "freeform": { | ||||||
|         "key": "access" |         "key": "access" | ||||||
|       }, |       }, | ||||||
|  |  | ||||||
|  | @ -123,7 +123,7 @@ | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           "id": "uk_addresses_import_button", |           "id": "uk_addresses_import_button", | ||||||
|           "render":{ |           "render": { | ||||||
|             "special": { |             "special": { | ||||||
|               "type": "import_button", |               "type": "import_button", | ||||||
|               "targetLayer": "address", |               "targetLayer": "address", | ||||||
|  |  | ||||||
|  | @ -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"}]} | {"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"}]} | ||||||
|  | @ -1040,6 +1040,10 @@ video { | ||||||
|   height: 50%; |   height: 50%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .h-4 { | ||||||
|  |   height: 1rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .h-screen { | .h-screen { | ||||||
|   height: 100vh; |   height: 100vh; | ||||||
| } | } | ||||||
|  | @ -1060,10 +1064,6 @@ video { | ||||||
|   height: 4rem; |   height: 4rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .h-4 { |  | ||||||
|   height: 1rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .h-0 { | .h-0 { | ||||||
|   height: 0px; |   height: 0px; | ||||||
| } | } | ||||||
|  | @ -1132,6 +1132,10 @@ video { | ||||||
|   width: 0px; |   width: 0px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .w-4 { | ||||||
|  |   width: 1rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .w-screen { | .w-screen { | ||||||
|   width: 100vw; |   width: 100vw; | ||||||
| } | } | ||||||
|  | @ -1140,10 +1144,6 @@ video { | ||||||
|   width: 2.75rem; |   width: 2.75rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .w-4 { |  | ||||||
|   width: 1rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .w-16 { | .w-16 { | ||||||
|   width: 4rem; |   width: 4rem; | ||||||
| } | } | ||||||
|  | @ -1412,6 +1412,11 @@ video { | ||||||
|   border-color: rgba(0, 0, 0, var(--tw-border-opacity)); |   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 { | .border-gray-300 { | ||||||
|   --tw-border-opacity: 1; |   --tw-border-opacity: 1; | ||||||
|   border-color: rgba(209, 213, 219, var(--tw-border-opacity)); |   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-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 { | .border-gray-200 { | ||||||
|   --tw-border-opacity: 1; |   --tw-border-opacity: 1; | ||||||
|   border-color: rgba(229, 231, 235, var(--tw-border-opacity)); |   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)); |   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 { | .bg-gray-400 { | ||||||
|   --tw-bg-opacity: 1; |   --tw-bg-opacity: 1; | ||||||
|   background-color: rgba(156, 163, 175, var(--tw-bg-opacity)); |   background-color: rgba(156, 163, 175, var(--tw-bg-opacity)); | ||||||
|  | @ -1518,6 +1523,10 @@ video { | ||||||
|   padding-left: 1rem; |   padding-left: 1rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .pl-1 { | ||||||
|  |   padding-left: 0.25rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .pl-2 { | .pl-2 { | ||||||
|   padding-left: 0.5rem; |   padding-left: 0.5rem; | ||||||
| } | } | ||||||
|  | @ -1534,10 +1543,6 @@ video { | ||||||
|   padding-bottom: 0.25rem; |   padding-bottom: 0.25rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .pl-1 { |  | ||||||
|   padding-left: 0.25rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .pr-1 { | .pr-1 { | ||||||
|   padding-right: 0.25rem; |   padding-right: 0.25rem; | ||||||
| } | } | ||||||
|  | @ -1908,6 +1913,10 @@ svg, img { | ||||||
|   display: none; |   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 { | .mapcontrol svg path { | ||||||
|   fill: var(--subtle-detail-color-contrast) !important; |   fill: var(--subtle-detail-color-contrast) !important; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -151,6 +151,10 @@ svg, img { | ||||||
|     display: none; |     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 { | .mapcontrol svg path { | ||||||
|     fill: var(--subtle-detail-color-contrast) !important; |     fill: var(--subtle-detail-color-contrast) !important; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -246,6 +246,10 @@ | ||||||
|             "loading": "Loading Wikipedia...", |             "loading": "Loading Wikipedia...", | ||||||
|             "noResults": "Nothing found for <i>{search}</i>", |             "noResults": "Nothing found for <i>{search}</i>", | ||||||
|             "noWikipediaPage": "This Wikidata item has no corresponding Wikipedia page yet.", |             "noWikipediaPage": "This Wikidata item has no corresponding Wikipedia page yet.", | ||||||
|  |             "previewbox": { | ||||||
|  |                 "born": "Born: {value}", | ||||||
|  |                 "died": "Died: {value}" | ||||||
|  |             }, | ||||||
|             "searchWikidata": "Search on Wikidata", |             "searchWikidata": "Search on Wikidata", | ||||||
|             "wikipediaboxTitle": "Wikipedia" |             "wikipediaboxTitle": "Wikipedia" | ||||||
|         } |         } | ||||||
|  | @ -359,6 +363,7 @@ | ||||||
|         "autoApply": "When changing the attributes {attr_names}, these attributes will automatically be changed on {count} other objects too" |         "autoApply": "When changing the attributes {attr_names}, these attributes will automatically be changed on {count} other objects too" | ||||||
|     }, |     }, | ||||||
|     "notes": { |     "notes": { | ||||||
|  |         "addAComment": "Add a comment", | ||||||
|         "addComment": "Add comment", |         "addComment": "Add comment", | ||||||
|         "addCommentAndClose": "Add comment and close", |         "addCommentAndClose": "Add comment and close", | ||||||
|         "addCommentPlaceholder": "Add a comment...", |         "addCommentPlaceholder": "Add a comment...", | ||||||
|  | @ -525,6 +530,15 @@ | ||||||
|         "split": "Split", |         "split": "Split", | ||||||
|         "splitTitle": "Choose on the map where to split this road" |         "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": { |     "validation": { | ||||||
|         "color": { |         "color": { | ||||||
|             "description": "A color or hexcode" |             "description": "A color or hexcode" | ||||||
|  |  | ||||||
|  | @ -1062,6 +1062,9 @@ | ||||||
|                 }, |                 }, | ||||||
|                 "question": "Are there tools here to repair your own bike?" |                 "question": "Are there tools here to repair your own bike?" | ||||||
|             }, |             }, | ||||||
|  |             "bike_shop-access": { | ||||||
|  |                 "render": "Only accessible to {access}" | ||||||
|  |             }, | ||||||
|             "bike_shop-email": { |             "bike_shop-email": { | ||||||
|                 "question": "What is the email address of {name}?" |                 "question": "What is the email address of {name}?" | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  | @ -1062,6 +1062,9 @@ | ||||||
|                 }, |                 }, | ||||||
|                 "question": "Biedt deze winkel gereedschap aan om je fiets zelf te herstellen?" |                 "question": "Biedt deze winkel gereedschap aan om je fiets zelf te herstellen?" | ||||||
|             }, |             }, | ||||||
|  |             "bike_shop-access": { | ||||||
|  |                 "render": "Enkel voor {access}" | ||||||
|  |             }, | ||||||
|             "bike_shop-email": { |             "bike_shop-email": { | ||||||
|                 "question": "Wat is het email-adres van {name}?" |                 "question": "Wat is het email-adres van {name}?" | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ import {DesugaringContext} from "../Models/ThemeConfig/Conversion/Conversion"; | ||||||
| 
 | 
 | ||||||
| class LayerOverviewUtils { | 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<string, any>(); |         const perId = new Map<string, any>(); | ||||||
|         for (const theme of themes) { |         for (const theme of themes) { | ||||||
|             const data = { |             const data = { | ||||||
|  | @ -27,7 +27,8 @@ class LayerOverviewUtils { | ||||||
|                 title: theme.title, |                 title: theme.title, | ||||||
|                 shortDescription: theme.shortDescription, |                 shortDescription: theme.shortDescription, | ||||||
|                 icon: theme.icon, |                 icon: theme.icon, | ||||||
|                 hideFromOverview: theme.hideFromOverview |                 hideFromOverview: theme.hideFromOverview, | ||||||
|  |                 mustHaveLanguage: theme.mustHaveLanguage | ||||||
|             } |             } | ||||||
|             perId.set(theme.id, data); |             perId.set(theme.id, data); | ||||||
|         } |         } | ||||||
|  | @ -73,6 +74,7 @@ class LayerOverviewUtils { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             questions[key].id = key; |             questions[key].id = key; | ||||||
|  |             questions[key]["source"] = "shared-questions" | ||||||
|             dict.set(key, <TagRenderingConfigJson>questions[key]) |             dict.set(key, <TagRenderingConfigJson>questions[key]) | ||||||
|         } |         } | ||||||
|         for (const key in icons["default"]) { |         for (const key in icons["default"]) { | ||||||
|  | @ -218,7 +220,8 @@ class LayerOverviewUtils { | ||||||
|             return { |             return { | ||||||
|                 ...t, |                 ...t, | ||||||
|                 hideFromOverview: t.hideFromOverview ?? false, |                 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; |         return fixed; | ||||||
|  |  | ||||||
|  | @ -244,11 +244,11 @@ function isTranslation(tr: any): boolean { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Converts a translation object into something that can be added to the 'generated translations' |  * Converts a translation object into something that can be added to the 'generated translations'. | ||||||
|  * @param obj |  *  | ||||||
|  * @param depth |  * 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)) { |     if (isTranslation(obj)) { | ||||||
|         return `new Translation( ${JSON.stringify(obj)} )` |         return `new Translation( ${JSON.stringify(obj)} )` | ||||||
|  | @ -259,15 +259,24 @@ function transformTranslation(obj: any, depth = 1) { | ||||||
|         if (key === "#") { |         if (key === "#") { | ||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|         if (key.match("^[a-zA-Z0-9_]*$") === null) { |         if (key.match("^[a-zA-Z0-9_]*$") === null) { | ||||||
|             throw "Invalid character in key: " + key |             throw "Invalid character in key: " + key | ||||||
|         } |         } | ||||||
|         const value = obj[key] |         let value = obj[key] | ||||||
| 
 | 
 | ||||||
|         if (isTranslation(value)) { |         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 { |         } 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}}`; |     return `{${values}}`; | ||||||
|  | @ -305,11 +314,11 @@ function formatFile(path) { | ||||||
|  */ |  */ | ||||||
| function genTranslations() { | function genTranslations() { | ||||||
|     const translations = JSON.parse(fs.readFileSync("./assets/generated/translations.json", "utf-8")) |     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`; |     let module = `import {Translation} from "../../UI/i18n/Translation"\n\nexport default class CompiledTranslations {\n\n`; | ||||||
|     module += " public static t = " + transformed; |     module += " public static t = " + transformed; | ||||||
|     module += "}" |     module += "\n    }" | ||||||
| 
 | 
 | ||||||
|     fs.writeFileSync("./assets/generated/CompiledTranslations.ts", module); |     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").validateStrict("./langs") | ||||||
| TranslationPart.fromDirectory("./langs/layers").validateStrict("layers") | TranslationPart.fromDirectory("./langs/layers").validateStrict("layers") | ||||||
| TranslationPart.fromDirectory("./langs/themes").validateStrict("themes") | TranslationPart.fromDirectory("./langs/themes").validateStrict("themes") | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue