Add typings to translations, move Subs into 'TypedTranslations', cleanup of duplicate parts in translation files, fix #752

This commit is contained in:
pietervdvn 2022-04-13 01:19:28 +02:00
parent f5d5f304ae
commit e391c1ce20
12 changed files with 64 additions and 318 deletions

View file

@ -1,4 +1,4 @@
import {Translation} from "../../UI/i18n/Translation";
import {Translation, TypedTranslation} from "../../UI/i18n/Translation";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import Translations from "../../UI/i18n/Translations";
import {TagUtils} from "../../Logic/Tags/TagUtils";
@ -22,8 +22,8 @@ export default class TagRenderingConfig {
public readonly id: string;
public readonly group: string;
public readonly render?: Translation;
public readonly question?: Translation;
public readonly render?: TypedTranslation<object>;
public readonly question?: TypedTranslation<object>;
public readonly condition?: TagsFilter;
public readonly configuration_warnings: string[] = []
@ -43,7 +43,7 @@ export default class TagRenderingConfig {
public readonly mappings?: {
readonly if: TagsFilter,
readonly ifnot?: TagsFilter,
readonly then: Translation,
readonly then: TypedTranslation<object>,
readonly icon: string,
readonly iconClass: string
readonly hideInAnswer: boolean | TagsFilter
@ -110,12 +110,13 @@ export default class TagRenderingConfig {
}
const type = json.freeform.type ?? "string"
let placeholder = Translations.T(json.freeform.placeholder)
let placeholder: Translation = Translations.T(json.freeform.placeholder)
if (placeholder === undefined) {
const typeDescription = Translations.t.validation[type]?.description
placeholder = Translations.T(json.freeform.key+" ("+type+")")
if(typeDescription !== undefined){
placeholder = placeholder.Subs({[type]: typeDescription})
placeholder = Translations.T(json.freeform.key+" ("+type+")").Subs({[type]: typeDescription})
}else{
placeholder = Translations.T(json.freeform.key+" ("+type+")")
}
}
@ -383,7 +384,7 @@ export default class TagRenderingConfig {
let freeformKeyDefined = this.freeform?.key !== undefined;
let usedFreeformValues = new Set<string>()
// We run over all the mappings first, to check if the mapping matches
const applicableMappings: { then: Translation, img?: string }[] = Utils.NoNull((this.mappings ?? [])?.map(mapping => {
const applicableMappings: { then: TypedTranslation<any>, img?: string }[] = Utils.NoNull((this.mappings ?? [])?.map(mapping => {
if (mapping.if === undefined) {
return mapping;
}
@ -404,7 +405,7 @@ export default class TagRenderingConfig {
const leftovers = freeformValues.filter(v => !usedFreeformValues.has(v))
for (const leftover of leftovers) {
applicableMappings.push({then:
this.render.replace("{"+this.freeform.key+"}", leftover)
new TypedTranslation<object>(this.render.replace("{"+this.freeform.key+"}", leftover).translations)
})
}
}
@ -412,7 +413,7 @@ export default class TagRenderingConfig {
return applicableMappings
}
public GetRenderValue(tags: any, defltValue: any = undefined): Translation {
public GetRenderValue(tags: any, defltValue: any = undefined): TypedTranslation<any> {
return this.GetRenderValueWithImage(tags, defltValue).then
}
@ -421,7 +422,7 @@ export default class TagRenderingConfig {
* Not compatible with multiAnswer - use GetRenderValueS instead in that case
* @constructor
*/
public GetRenderValueWithImage(tags: any, defltValue: any = undefined): { then: Translation, icon?: string } {
public GetRenderValueWithImage(tags: any, defltValue: any = undefined): { then: TypedTranslation<any>, icon?: string } {
if (this.mappings !== undefined && !this.multiAnswer) {
for (const mapping of this.mappings) {
if (mapping.if === undefined) {

View file

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

View file

@ -14,7 +14,7 @@ import Title from "../Base/Title";
import {UIEventSource} from "../../Logic/UIEventSource";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import * as native_languages from "../../assets/language_native.json"
class TranslatorsPanelContent extends Combine {
constructor(layout: LayoutConfig, isTranslator: UIEventSource<boolean>) {
@ -48,7 +48,8 @@ class TranslatorsPanelContent extends Combine {
// "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)
translated: new Translation(completenessTr),
language: seed.OnEveryLanguage((_, lng) => native_languages[lng])
})
super([

View file

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

View file

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

View file

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

View file

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

View file

@ -42,11 +42,6 @@
"getStartedLogin": "Entra no OpenStreetMap para comezar",
"getStartedNewAccount": " ou <a href='https://www.openstreetmap.org/user/new' target='_blank'>crea unha nova conta</a>",
"goToInbox": "Abrir mensaxes",
"index": {
"intro": "O MapComplete é un visor e editor do OpenStreetMap, que te amosa información sobre un tema específico.",
"pickTheme": "Escolle un tema para comezar.",
"title": "Benvido ao MapComplete"
},
"layerSelection": {
"title": "Seleccionar capas",
"zoomInToSeeThisLayer": "Achégate para ver esta capa"

View file

@ -97,12 +97,6 @@
"backToMapcomplete": "Terug naar het themaoverzicht",
"backgroundMap": "Achtergrondkaart",
"cancel": "Annuleren",
"centerMessage": {
"loadingData": "Data wordt geladen…",
"ready": "Klaar!",
"retrying": "Data inladen mislukt. Opnieuw proberen over {count} seconden…",
"zoomIn": "Zoom in om de data te zien en te bewerken"
},
"confirm": "Bevestigen",
"customThemeIntro": "<h3>Onofficiële thema's</h3>De onderstaande thema's heb je eerder bezocht en zijn gemaakt door andere OpenStreetMappers.",
"download": {
@ -132,12 +126,6 @@
"histogram": {
"error_loading": "Kan het histogram niet laden"
},
"index": {
"#": "Deze teksten worden getoond boven de themaknoppen als er geen thema is geladen",
"intro": "MapComplete is een OpenStreetMap applicatie waar informatie over een specifiek thema bekeken en aangepast kan worden.",
"pickTheme": "Kies hieronder een thema om te beginnen.",
"title": "Welkom bij MapComplete"
},
"layerSelection": {
"title": "Selecteer lagen",
"zoomInToSeeThisLayer": "Vergroot de kaart om deze laag te zien"
@ -199,22 +187,6 @@
"readYourMessages": "Gelieve eerst je berichten op OpenStreetMap te lezen alvorens nieuwe punten toe te voegen.",
"removeLocationHistory": "Verwijder de geschiedenis aan locaties",
"returnToTheMap": "Ga terug naar de kaart",
"reviews": {
"affiliated_reviewer_warning": "(Review door betrokkene)",
"attribution": "De beoordelingen worden voorzien door <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> en zijn beschikbaar onder de<a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0-licentie</a>.",
"i_am_affiliated": "<span>Ik ben persoonlijk betrokken</span><br/><span class='subtle'>Vink aan indien je de oprichter, maker, werknemer, ... of dergelijke bent</span>",
"name_required": "De naam van dit object moet gekend zijn om een review te kunnen maken",
"no_rating": "Geen score bekend",
"no_reviews_yet": "Er zijn nog geen beoordelingen. Wees de eerste om een beoordeling te schrijven en help open data en het bedrijf!",
"plz_login": "Meld je aan om een beoordeling te geven",
"posting_as": "Ingelogd als",
"saved": "<span class='thanks'>Bedankt om je beoordeling te delen!</span>",
"saving_review": "Opslaan…",
"title": "{count} beoordelingen",
"title_singular": "Eén beoordeling",
"tos": "Als je je review publiceert, ga je akkoord met de <a href='https://mangrove.reviews/terms' target='_blank'>de gebruiksvoorwaarden en privacy policy van Mangrove.reviews</a>",
"write_a_comment": "Schrijf een beoordeling…"
},
"save": "Opslaan",
"search": {
"error": "Niet gelukt...",

View file

@ -35,25 +35,6 @@
"cancel": "Anuluj",
"customThemeIntro": "<h3>Motywy własne</h3>Są to wcześniej odwiedzone motywy stworzone przez użytkowników.",
"fewChangesBefore": "Proszę odpowiedzieć na kilka pytań dotyczących istniejących punktów przed dodaniem nowego punktu.",
"general": {
"about": "Łatwo edytuj i dodaj OpenStreetMap dla określonego motywu",
"aboutMapcomplete": "<h3>O MapComplete</h3><p>Dzięki MapComplete możesz wzbogacić OpenStreetMap o informacje na <b>pojedynczy temat.</b> Odpowiedz na kilka pytań, a w ciągu kilku minut Twój wkład będzie dostępny na całym świecie! Opiekun <b>tematu</b> definiuje elementy, pytania i języki dla tematu.</p><h3>Dowiedz się więcej</h3><p>MapComplete zawsze <b>oferuje następny krok</b>, by dowiedzieć się więcej o OpenStreetMap.</p><ul><li>Po osadzeniu na stronie internetowej, element iframe łączy się z pełnoekranowym MapComplete</li><li>Wersja pełnoekranowa oferuje informacje o OpenStreetMap</li><li>Przeglądanie działa bez logowania, ale edycja wymaga loginu OSM.</li><li>Jeżeli nie jesteś zalogowany, zostaniesz poproszony o zalogowanie się</li><li>Po udzieleniu odpowiedzi na jedno pytanie, możesz dodać nowe punkty do mapy</li><li>Po chwili wyświetlane są rzeczywiste tagi OSM, które później linkują do wiki</li></ul><p></p><br><p>Zauważyłeś <b>problem</b>? Czy masz <b>prośbę o dodanie jakiejś funkcji</b>? Chcesz <b>pomóc w tłumaczeniu</b>? Udaj się do <a href=\"https://github.com/pietervdvn/MapComplete\" target=\"_blank\">kodu źródłowego</a> lub <a href=\"https://github.com/pietervdvn/MapComplete/issues\" target=\"_blank\">issue trackera.</a> </p><p> Chcesz zobaczyć <b>swoje postępy</b>? Śledź liczbę edycji na <a href=\"{osmcha_link}\" target=\"_blank\">OsmCha</a>.</p>",
"add": {
"addNew": "Dodaj nową {category} tutaj",
"confirmButton": "Dodaj tutaj {category}.<br><div class=\"alert\">Twój dodatek jest widoczny dla wszystkich</div>",
"confirmIntro": "<h3>Czy dodać tutaj {title}?</h3> Punkt, który tutaj utworzysz, będzie <b>widoczny dla wszystkich</b>. Proszę, dodawaj rzeczy do mapy tylko wtedy, gdy naprawdę istnieją. Wiele aplikacji korzysta z tych danych.",
"intro": "Kliknąłeś gdzieś, gdzie nie są jeszcze znane żadne dane.<br>",
"layerNotEnabled": "Warstwa {layer} nie jest włączona. Włącz tę warstwę, aby dodać punkt",
"openLayerControl": "Otwórz okno sterowania warstwą",
"pleaseLogin": "<a class=\"activate-osm-authentication\">Zaloguj się, aby dodać nowy punkt</a>",
"stillLoading": "Dane wciąż się ładują. Poczekaj chwilę, zanim dodasz nowy punkt.",
"title": "Czy dodać nowy punkt?",
"zoomInFurther": "Powiększ jeszcze bardziej, aby dodać punkt."
},
"backgroundMap": "Tło mapy",
"cancel": "Anuluj",
"customThemeIntro": "<h3>Motywy własne</h3>Są to wcześniej odwiedzone motywy stworzone przez użytkowników.",
"fewChangesBefore": "Proszę odpowiedzieć na kilka pytań dotyczących istniejących punktów przed dodaniem nowego punktu.",
"getStartedLogin": "Zaloguj się za pomocą OpenStreetMap, aby rozpocząć",
"getStartedNewAccount": " lub <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">utwórz nowe konto</a>",
"goToInbox": "Otwórz skrzynkę odbiorczą",
@ -141,99 +122,6 @@
},
"welcomeBack": "Jesteś zalogowany, witaj z powrotem!"
},
"getStartedLogin": "Zaloguj się za pomocą OpenStreetMap, aby rozpocząć",
"getStartedNewAccount": " lub <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">utwórz nowe konto</a>",
"goToInbox": "Otwórz skrzynkę odbiorczą",
"index": {
"#": "Te teksty są wyświetlane nad przyciskami motywu, gdy nie jest załadowany żaden motyw",
"intro": "MapComplete to przeglądarka i edytor OpenStreetMap, który pokazuje informacje podzielone według tematu.",
"pickTheme": "Wybierz temat poniżej, aby rozpocząć.",
"title": "Witaj w MapComplete"
},
"layerSelection": {
"title": "Wybierz warstwy",
"zoomInToSeeThisLayer": "Powiększ, aby zobaczyć tę warstwę"
},
"loginToStart": "Zaloguj się, aby odpowiedzieć na to pytanie",
"loginWithOpenStreetMap": "Zaloguj z OpenStreetMap",
"nameInlineQuestion": "Nazwa tej {category} to $$$",
"noNameCategory": "{category} bez nazwy",
"noTagsSelected": "Nie wybrano tagów",
"number": "numer",
"oneSkippedQuestion": "Jedno pytanie zostało pominięte",
"opening_hours": {
"closed_permanently": "Zamknięte na nieokreślony czas",
"closed_until": "Zamknięte do {date}",
"error_loading": "Błąd: nie można zwizualizować tych godzin otwarcia.",
"not_all_rules_parsed": "Godziny otwarcia tego sklepu są skomplikowane. Następujące reguły są ignorowane w elemencie wejściowym:",
"openTill": "do",
"open_24_7": "Otwarte przez całą dobę",
"open_during_ph": "W czasie świąt państwowych udogodnienie to jest",
"opensAt": "z",
"ph_closed": "zamknięte",
"ph_not_known": " ",
"ph_open": "otwarte"
},
"osmLinkTooltip": "Zobacz ten obiekt na OpenStreetMap, aby uzyskać historię i więcej opcji edycji",
"pickLanguage": "Wybierz język: ",
"questions": {
"emailIs": "Adres e-mail {category} to <a href=\"mailto:{email}\" target=\"_blank\">{email}</a>",
"emailOf": "Jaki jest adres e-mail {category}?",
"phoneNumberIs": "Numer telefonu {category} to <a target=\"_blank\">{phone}</a>",
"phoneNumberOf": "Jaki jest numer telefonu do {category}?",
"websiteIs": "Strona internetowa: <a href=\"{website}\" target=\"_blank\">{website}</a>",
"websiteOf": "Jaka jest strona internetowa {category}?"
},
"readYourMessages": "Przeczytaj wszystkie wiadomości OpenStreetMap przed dodaniem nowego punktu.",
"returnToTheMap": "Wróć do mapy",
"save": "Zapisz",
"search": {
"error": "Coś poszło nie tak…",
"nothing": "Nic nie znaleziono…",
"search": "Wyszukaj lokalizację",
"searching": "Szukanie…"
},
"sharescreen": {
"addToHomeScreen": "<h3> Dodaj do ekranu głównego</h3>Możesz łatwo dodać tę stronę do ekranu głównego smartfona, aby poczuć się jak w domu. Kliknij przycisk \"Dodaj do ekranu głównego\" na pasku adresu URL, aby to zrobić.",
"copiedToClipboard": "Link został skopiowany do schowka",
"editThemeDescription": "Dodaj lub zmień pytania do tego motywu mapy",
"editThisTheme": "Edytuj ten motyw",
"embedIntro": "<h3>Umieść na swojej stronie internetowej</h3>Proszę, umieść tę mapę na swojej stronie internetowej. <br>Zachęcamy cię do tego - nie musisz nawet pytać o zgodę. <br>Jest ona darmowa i zawsze będzie. Im więcej osób jej używa, tym bardziej staje się wartościowa.",
"fsAddNew": "Włącz przycisk \"Dodaj nowe POI\"",
"fsGeolocation": "Włącz przycisk „Zlokalizuj mnie” (tylko na urządzeniach mobilnych)",
"fsIncludeCurrentBackgroundMap": "Dołącz bieżący wybór tła <b>{name}</b>",
"fsIncludeCurrentLayers": "Uwzględnij wybór bieżącej warstwy",
"fsIncludeCurrentLocation": "Uwzględnij bieżącą lokalizację",
"fsLayerControlToggle": "Zacznij od rozwiniętej kontroli warstw",
"fsLayers": "Włącz kontrolę warstw",
"fsSearch": "Włącz pasek wyszukiwania",
"fsUserbadge": "Włącz przycisk logowania",
"fsWelcomeMessage": "Pokaż wyskakujące okienko wiadomości powitalnej i powiązane zakładki",
"intro": "<h3> Udostępnij tę mapę</h3> Udostępnij tę mapę, kopiując poniższy link i wysyłając ją do przyjaciół i rodziny:",
"thanksForSharing": "Dzięki za udostępnienie!"
},
"skip": "Pomiń to pytanie",
"skippedQuestions": "Niektóre pytania są pominięte",
"weekdays": {
"abbreviations": {
"friday": "Pt",
"monday": "Pn",
"saturday": "Sob",
"sunday": "Niedz",
"thursday": "Czw",
"tuesday": "Wt",
"wednesday": "Śr"
},
"friday": "Piątek",
"monday": "Poniedziałek",
"saturday": "Sobota",
"sunday": "Niedziela",
"thursday": "Czwartek",
"tuesday": "Wtorek",
"wednesday": "Środa"
},
"welcomeBack": "Jesteś zalogowany, witaj z powrotem!"
},
"image": {
"addPicture": "Dodaj zdjęcie",
"ccb": "na licencji CC-BY",

View file

@ -90,130 +90,6 @@
"title": "下載可視的資料"
},
"fewChangesBefore": "請先回答有關既有節點的問題再來新增新節點。",
"general": {
"about": "相當容易編輯,而且能為開放街圖新增特定主題",
"aboutMapcomplete": "<h3>關於 MapComplete</h3><p>使用 MapComplete 你可以藉由<b>單一主題</b>豐富開放街圖的圖資。回答幾個問題,然後幾分鐘之內你的貢獻立刻就傳遍全球!<b>主題維護者</b>定議主題的元素、問題與語言。</p><h3>發現更多</h3><p>MapComplete 總是提供學習更多開放街圖<b>下一步的知識</b>。</p><ul><li>當你內嵌網站,網頁內嵌會連結到全螢幕的 MapComplete</li><li>全螢幕的版本提供關於開放街圖的資訊</li><li>不登入檢視成果,但是要編輯則需登入 OSM。</li><li>如果你沒有登入,你會被要求先登入</li><li>當你回答單一問題時,你可以在地圖新增新的節點</li><li>過了一陣子,實際的 OSM-標籤會顯示,之後會連結到 wiki</li></ul><p></p><br><p>你有注意到<b>問題</b>嗎?你想請求<b>功能</b>嗎?想要<b>幫忙翻譯</b>嗎?來到<a href=\"https://github.com/pietervdvn/MapComplete\" target=\"_blank\">原始碼</a>或是<a href=\"https://github.com/pietervdvn/MapComplete/issues\" target=\"_blank\">問題追蹤器。</a></p><p>想要看到<b>你的進度</b>嗎?到<a href=\"{osmcha_link}\" target=\"_blank\">OsmCha</a>追蹤編輯數。</p>",
"add": {
"addNew": "在這裡新增新的 {category}",
"confirmButton": "在此新增 {category}。<br><div class=\"alert\">大家都可以看到您新增的內容</div>",
"confirmIntro": "<h3>在這裡新增 {title} </h3>你在這裡新增的節點<b>所有人都看得到</b>。請只有在確定有物件存在的情形下才新增上去,許多應用程式都使用這份資料。",
"intro": "您點擊處目前未有已知的資料。<br>",
"layerNotEnabled": "圖層 {layer} 目前無法使用,請先啟用這圖層再加新的節點",
"openLayerControl": "開啟圖層控制框",
"pleaseLogin": "<a class=\"activate-osm-authentication\">請先登入來新增節點</a>",
"stillLoading": "目前仍在載入資料,請稍後再來新增節點。",
"title": "新增新的節點?",
"zoomInFurther": "放大來新增新的節點。"
},
"attribution": {
"attributionContent": "<p>所有資料由<a href=\"https://osm.org\" target=\"_blank\">開放街圖</a>提供,在<a href=\"https://osm.org/copyright\" target=\"_blank\">開放資料庫授權條款</a>之下自由再利用。</p>",
"attributionTitle": "署名通知",
"codeContributionsBy": "MapComplete 是由 {contributors} 和其他 <a href=\"https://github.com/pietervdvn/MapComplete/graphs/contributors\" target=\"_blank\">{hiddenCount} 位貢獻者</a>構建而成",
"iconAttribution": {
"title": "使用的圖示"
},
"mapContributionsBy": "目前檢視的資料由 {contributors} 貢獻編輯",
"mapContributionsByAndHidden": "目前顯到的資料是由 {contributors} 和其他 {hiddenCount} 位貢獻者編輯貢獻",
"themeBy": "由 {author} 維護主題"
},
"backgroundMap": "背景地圖",
"cancel": "取消",
"customThemeIntro": "<h3>客製化主題</h3>觀看這些先前使用者創造的主題。",
"fewChangesBefore": "請先回答有關既有節點的問題再來新增新節點。",
"getStartedLogin": "登入開放街圖帳號來開始",
"getStartedNewAccount": " 或是 <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">註冊新帳號</a>",
"goToInbox": "開啟訊息框",
"layerSelection": {
"title": "選擇圖層",
"zoomInToSeeThisLayer": "放大來看這個圖層"
},
"loginToStart": "登入之後來回答這問題",
"loginWithOpenStreetMap": "用開放街圖帳號登入",
"morescreen": {
"createYourOwnTheme": "從零開始建立你的 MapComplete 主題",
"intro": "<h3>看更多主題地圖?</h3>您喜歡蒐集地理資料嗎?<br>還有更多主題。",
"requestATheme": "如果你有客製化要求,請到問題追踪器那邊提出要求",
"streetcomplete": "行動裝置另有類似的應用程式 <a class=\"underline hover:text-blue-800\" href=\"https://play.google.com/store/apps/details?id=de.westnordost.streetcomplete\" target=\"_blank\">StreetComplete</a>。"
},
"nameInlineQuestion": "這個 {category} 的名稱是 $$$",
"noNameCategory": "{category} 沒有名稱",
"noTagsSelected": "沒有選取標籤",
"number": "號碼",
"oneSkippedQuestion": "跳過一個問題",
"openStreetMapIntro": "<h3>開放的地圖</h3><p>如果有一份地圖,任何人都能自由使用與編輯,單一的地圖能夠儲存所有地理相關資訊?這樣不就很酷嗎?接著,所有的網站使用不同的、範圍小的,不相容的地圖 (通常也都過時了),也就不再需要了。</p><p><b><a href=\"https://OpenStreetMap.org\" target=\"_blank\">開放街圖</a></b>就是這樣的地圖,人人都能免費這些圖資 (只要<a href=\"https://osm.org/copyright\" target=\"_blank\">署名與公開變動這資料</a>)。只要遵循這些,任何人都能自由新增新資料與修正錯誤,這些網站也都使用開放街圖,資料也都來自開放街圖,你的答案與修正也會加到開放街圖上面。</p><p>許多人與應用程式已經採用開放街圖了:<a href=\"https://organicmaps.app//\" target=\"_blank\">Organic Maps</a>、<a href=\"https://osmAnd.net\" target=\"_blank\">OsmAnd</a>,還有 Facebook、Instagram蘋果地圖、Bing 地圖(部分)採用開放街圖。如果你在開放街圖上變動資料,也會同時影響這些應用 - 在他們下次更新資料之後!</p>",
"opening_hours": {
"closed_permanently": "不清楚關閉多久了",
"closed_until": "{date} 起關閉",
"error_loading": "錯誤:無法視覺化開放時間。",
"not_all_rules_parsed": "這間店的開放時間相當複雜,在輸入元素時忽略接下來的規則:",
"openTill": "結束時間",
"open_24_7": "24小時營業",
"open_during_ph": "國定假日的時候,這個場所是",
"opensAt": "開始時間",
"ph_closed": "無營業",
"ph_not_known": " ",
"ph_open": "有營業"
},
"osmLinkTooltip": "在開放街圖歷史和更多編輯選項下面來檢視這物件",
"pickLanguage": "選擇語言: ",
"questions": {
"emailIs": "{category} 的電子郵件地址是<a href=\"mailto:{email}\" target=\"_blank\">{email}</a>",
"emailOf": "{category} 的電子郵件地址是?",
"phoneNumberIs": "此 {category} 的電話號碼為 <a target=\"_blank\">{phone}</a>",
"phoneNumberOf": "{category} 的電話號碼是?",
"websiteIs": "網站:<a href=\"{website}\" target=\"_blank\">{website}</a>",
"websiteOf": "{category} 的網站網址是?"
},
"readYourMessages": "請先閱讀開放街圖訊息之前再來新增新節點。",
"returnToTheMap": "回到地圖",
"save": "儲存",
"search": {
"error": "有狀況發生了…",
"nothing": "沒有找到…",
"search": "搜尋地點",
"searching": "搜尋中…"
},
"sharescreen": {
"addToHomeScreen": "<h3>新增到您的主畫面</h3>您可以輕易將這網站新增到您智慧型手機的主畫面,在網址列點選「新增到主畫面按鈕」來做這件事情。",
"copiedToClipboard": "複製連結到簡貼簿",
"editThemeDescription": "新增或改變這個地圖的問題",
"editThisTheme": "編輯這個主題",
"embedIntro": "<h3>嵌入到你的網站</h3>請考慮將這份地圖嵌入您的網站。<br>地圖毋須額外授權,非常歡迎您多加利用。<br>一切都是免費的,而且之後也是免費的,越有更多人使用,則越顯得它的價值。",
"fsAddNew": "啟用'新增新的興趣點'按鈕",
"fsGeolocation": "啟用'地理定位自身'按鈕 (只有行動版本)",
"fsIncludeCurrentBackgroundMap": "包含目前背景選擇<b>{name}</b>",
"fsIncludeCurrentLayers": "包含目前選擇圖層",
"fsIncludeCurrentLocation": "包含目前位置",
"fsLayerControlToggle": "開始時擴展圖層控制",
"fsLayers": "啟用圖層控制",
"fsSearch": "啟用搜尋列",
"fsUserbadge": "啟用登入按鈕",
"fsWelcomeMessage": "顯示歡迎訊息以及相關頁籤",
"intro": "<h3>分享這地圖</h3>複製下面的連結來向朋友與家人分享這份地圖:",
"thanksForSharing": "感謝分享!"
},
"skip": "跳過這問題",
"skippedQuestions": "有些問題已經跳過了",
"weekdays": {
"abbreviations": {
"friday": "星期五",
"monday": "星期一",
"saturday": "星期六",
"sunday": "星期日",
"thursday": "星期四",
"tuesday": "星期二",
"wednesday": "星期三"
},
"friday": "星期五",
"monday": "星期一",
"saturday": "星期六",
"sunday": "星期日",
"thursday": "星期四",
"tuesday": "星期二",
"wednesday": "星期三"
},
"welcomeBack": "你已經登入了,歡迎回來!"
},
"getStartedLogin": "登入開放街圖帳號來開始",
"getStartedNewAccount": " 或是 <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">註冊新帳號</a>",
"goToInbox": "開啟訊息框",

View file

@ -2,8 +2,6 @@ import * as fs from "fs";
import {readFileSync, writeFileSync} from "fs";
import {Utils} from "../Utils";
import ScriptUtils from "./ScriptUtils";
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
import TranslatorsPanel from "../UI/BigComponents/TranslatorsPanel";
const knownLanguages = ["en", "nl", "de", "fr", "es", "gl", "ca"];
@ -275,7 +273,20 @@ function transformTranslation(obj: any, path: string[] = [], languageWhitelist :
}
value = nv;
}
values += `${Utils.Times((_) => " ", path.length + 1)}get ${key}() { return new Translation(${JSON.stringify(value)}, "core:${path.join(".")}.${key}") },
if(value["en"] === undefined){
throw `At ${path.join(".")}: Missing 'en' translation for ${JSON.stringify(value)}`
}
const subParts : string[] = value["en"].match(/{[^}]*}/g)
let expr = `return new Translation(${JSON.stringify(value)}, "core:${path.join(".")}.${key}")`
if(subParts !== null){
// convert '{to_substitute}' into 'to_substitute'
const types = Utils.Dedup( subParts.map(tp => tp.substring(1, tp.length - 1)))
expr = `return new TypedTranslation<{ ${types.join(", ")} }>(${JSON.stringify(value)}, "core:${path.join(".")}.${key}")`
}
values += `${Utils.Times((_) => " ", path.length + 1)}get ${key}() { ${expr} },
`
} else {
values += (Utils.Times((_) => " ", path.length + 1)) + key + ": " + transformTranslation(value, [...path, key], languageWhitelist) + ",\n"
@ -318,7 +329,7 @@ function genTranslations() {
const translations = JSON.parse(fs.readFileSync("./assets/generated/translations.json", "utf-8"))
const transformed = transformTranslation(translations);
let module = `import {Translation} from "../../UI/i18n/Translation"\n\nexport default class CompiledTranslations {\n\n`;
let module = `import {Translation, TypedTranslation} from "../../UI/i18n/Translation"\n\nexport default class CompiledTranslations {\n\n`;
module += " public static t = " + transformed;
module += "\n }"