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 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, | ||||
|  | @ -36,6 +37,8 @@ export default class UserRelatedState extends ElementsState { | |||
|      */ | ||||
|     public favouriteLayers: UIEventSource<string[]>; | ||||
| 
 | ||||
|     public readonly isTranslator : UIEventSource<boolean>; | ||||
|      | ||||
|     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) | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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` | ||||
|  |  | |||
|  | @ -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 = (<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 | ||||
|             } | ||||
|         } | ||||
|         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 = { | ||||
|  |  | |||
|  | @ -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` | ||||
|                 } | ||||
|  |  | |||
|  | @ -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(() => {}) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										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 Img from "../Base/Img"; | ||||
| import {Translation} from "../i18n/Translation"; | ||||
| import TranslatorsPanel from "./TranslatorsPanel"; | ||||
| 
 | ||||
| export class OpenIdEditor extends VariableUiElement { | ||||
|     constructor(state: { locationControl: UIEventSource<Loc> }, iconStyle?: string, objectId?: string) { | ||||
|  | @ -110,7 +111,8 @@ export default class CopyrightPanel extends Combine { | |||
|         featurePipeline: FeaturePipeline, | ||||
|         currentBounds: UIEventSource<BBox>, | ||||
|         locationControl: UIEventSource<Loc>, | ||||
|         osmConnection: OsmConnection | ||||
|         osmConnection: OsmConnection, | ||||
|         isTranslator: UIEventSource<boolean> | ||||
|     }) { | ||||
| 
 | ||||
|         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") | ||||
|     } | ||||
|  |  | |||
|  | @ -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"); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) | ||||
| 
 | ||||
|                     ] | ||||
|                 ) | ||||
|         } | ||||
|  |  | |||
|  | @ -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([ | ||||
|                 `<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>`, | ||||
|                 `<dd class='mt-1 text-base text-gray-500 group-hover:text-blue-900 overflow-ellipsis'>`, | ||||
|                 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) | ||||
|  |  | |||
|  | @ -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") | ||||
|         ) | ||||
|  |  | |||
							
								
								
									
										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( | ||||
|                             new Combine([ | ||||
|                                 new Title("Add a comment"), | ||||
|                                 new Title(t.addAComment), | ||||
|                                 textField, | ||||
|                                 new Combine([ | ||||
|                                     stateButtons.SetClass("sm:mr-2"), | ||||
|  |  | |||
|  | @ -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 | ||||
|                 ) | ||||
|             }) | ||||
|         ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|         } | ||||
|     ] | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ import {QueryParameters} from "../../Logic/Web/QueryParameters"; | |||
| export default class Locale { | ||||
| 
 | ||||
|     public static language: UIEventSource<string> = Locale.setup(); | ||||
|     public static showLinkToWeblate: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||
|      | ||||
|     private static setup() { | ||||
|         const source = LocalStorageSource.Get('language', "en"); | ||||
|  |  | |||
|  | @ -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 | ||||
|     } | ||||
|      | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										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 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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) { | ||||
|         let found = dict.get(k); | ||||
|         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" | ||||
|     }, | ||||
|     { | ||||
|       "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" | ||||
|       }, | ||||
|  |  | |||
|  | @ -123,7 +123,7 @@ | |||
|         }, | ||||
|         { | ||||
|           "id": "uk_addresses_import_button", | ||||
|           "render":{ | ||||
|           "render": { | ||||
|             "special": { | ||||
|               "type": "import_button", | ||||
|               "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%; | ||||
| } | ||||
| 
 | ||||
| .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; | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -246,6 +246,10 @@ | |||
|             "loading": "Loading Wikipedia...", | ||||
|             "noResults": "Nothing found for <i>{search}</i>", | ||||
|             "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" | ||||
|  |  | |||
|  | @ -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}?" | ||||
|             }, | ||||
|  |  | |||
|  | @ -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}?" | ||||
|             }, | ||||
|  |  | |||
|  | @ -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<string, any>(); | ||||
|         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, <TagRenderingConfigJson>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; | ||||
|  |  | |||
|  | @ -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") | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue