From c066da4e223e987968d214df0d2c89368b45c81c Mon Sep 17 00:00:00 2001 From: Kirk Swenson Date: Wed, 26 Aug 2026 16:18:47 -0700 Subject: [PATCH 1/3] feat: add Slovenian (sl) translation [CFM-19] Slovenian is complete in CFM's POEditor project (163/163 keys, none empty) but was never bundled. CODAP v3 cannot offer Slovenian until CFM does: translate() falls back to the raw key rather than English, so a Slovenian user would see ~MENUBAR.UNTITLED_DOCUMENT in the title bar. - pull src/code/utils/lang/sl.json from POEditor project 125177 - register it in the languageFiles list in translate.ts - add "sl" to LANGUAGES in bin/strings-pull-project.sh translate.test.ts asserts the bundled list stays in sync with the pull script, so both halves are required together. Unblocks CODAP-1484. (cherry picked from commit 4e1960ec23a99e5e0e7de08b01ccef1a2a700cd6) --- bin/strings-pull-project.sh | 3 +- src/code/utils/lang/sl.json | 165 ++++++++++++++++++++++++++++++++++++ src/code/utils/translate.ts | 2 + 3 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/code/utils/lang/sl.json diff --git a/bin/strings-pull-project.sh b/bin/strings-pull-project.sh index 15109542..52b2b829 100755 --- a/bin/strings-pull-project.sh +++ b/bin/strings-pull-project.sh @@ -14,10 +14,11 @@ OUTPUT_DIR=src/code/utils/lang # nn: Norwegian Nynorsk # pl: Polish Polski # pt-BR: Brazilian Portuguese +# sl: Slovenian # tr: Turkish # zh-Hans: Simplified Chinese # zh-TW: Traditional Chinese (Taiwan) -LANGUAGES=("de" "el" "es" "fa" "fr" "he" "ja" "ko" "nb" "nl" "nn" "pl" "pt-BR" "th" "tr" "zh-Hans" "zh-TW") +LANGUAGES=("de" "el" "es" "fa" "fr" "he" "ja" "ko" "nb" "nl" "nn" "pl" "pt-BR" "sl" "th" "tr" "zh-Hans" "zh-TW") # argument processing from https://stackoverflow.com/a/14203146 while [[ $# -gt 1 ]] diff --git a/src/code/utils/lang/sl.json b/src/code/utils/lang/sl.json new file mode 100644 index 00000000..7e552ce5 --- /dev/null +++ b/src/code/utils/lang/sl.json @@ -0,0 +1,165 @@ +{ + "~MENUBAR.UNTITLED_DOCUMENT": "Neimenovani dokument", + "~MENU.NEW": "Nov", + "~MENU.OPEN": "Odpri ...", + "~MENU.CLOSE": "Zapri", + "~MENU.IMPORT_DATA": "Uvozi podatke ...", + "~MENU.SAVE": "Shrani", + "~MENU.SAVE_AS": "Shrani kot ...", + "~MENU.EXPORT_AS": "Izvozi datoteko kot ...", + "~MENU.CREATE_COPY": "Ustvari kopijo", + "~MENU.SHARE": "Deli ...", + "~MENU.SHARE_GET_LINK": "Pridobi povezavo do skupnega pogleda", + "~MENU.SHARE_UPDATE": "Posodobi deljeni pogled", + "~MENU.DOWNLOAD": "Prenesi", + "~MENU.RENAME": "Preimenuj", + "~MENU.REVERT_TO": "Vrni se na ...", + "~MENU.REVERT_TO_LAST_OPENED": "Nedavno odprto stanje", + "~MENU.REVERT_TO_SHARED_VIEW": "Deljeni pogled", + "~DIALOG.SAVE": "Shrani", + "~DIALOG.SAVE_AS": "Shrani kot ...", + "~DIALOG.EXPORT_AS": "Izvozi datoteko kot ...", + "~DIALOG.CREATE_COPY": "Ustvari kopijo ...", + "~DIALOG.OPEN": "Odprto", + "~DIALOG.DOWNLOAD": "Prenesi", + "~DIALOG.RENAME": "Preimenuj", + "~DIALOG.SHARED": "Deli", + "~DIALOG.IMPORT_DATA": "Uvoz podatkov", + "~PROVIDER.LOCAL_STORAGE": "Lokalno shranjevanje", + "~PROVIDER.READ_ONLY": "Samo za branje", + "~PROVIDER.GOOGLE_DRIVE": "Google Drive", + "~PROVIDER.DOCUMENT_STORE": "Concord Cloud", + "~PROVIDER.LOCAL_FILE": "Lokalna datoteka", + "~FILE_STATUS.SAVING": "Shranjevanje ...", + "~FILE_STATUS.SAVED": "Vse spremembe so shranjene", + "~FILE_STATUS.SAVED_TO_PROVIDER": "Vse spremembe so shranjene v %{providerName}", + "~FILE_STATUS.UNSAVED": "Neshranjeno", + "~FILE_DIALOG.FILENAME": "Ime datoteke", + "~FILE_DIALOG.OPEN": "Odpri", + "~FILE_DIALOG.SAVE": "Shrani", + "~FILE_DIALOG.CANCEL": "Prekliči", + "~FILE_DIALOG.REMOVE": "Izbriši", + "~FILE_DIALOG.REMOVE_CONFIRM": "Ali si prepričan, da želiš izbrisati %{filename}?", + "~FILE_DIALOG.REMOVED_TITLE": "Izbrisana datoteka", + "~FILE_DIALOG.REMOVED_MESSAGE": "%{filename} je bil izbrisan", + "~FILE_DIALOG.LOADING": "Nalaganje ...", + "~FILE_DIALOG.LOAD_FOLDER_ERROR": "*** Napaka pri nalaganju vsebine mape ***", + "~FILE_DIALOG.DOWNLOAD": "Prenesi", + "~DOWNLOAD_DIALOG.DOWNLOAD": "Prenesi", + "~DOWNLOAD_DIALOG.CANCEL": "Prekliči", + "~DOWNLOAD_DIALOG.INCLUDE_SHARE_INFO": "Vključi informacije o skupni rabi v preneseno datoteko", + "~RENAME_DIALOG.RENAME": "Preimenuj", + "~RENAME_DIALOG.CANCEL": "Prekliči", + "~SHARE_DIALOG.COPY": "Kopiraj", + "~SHARE_DIALOG.VIEW": "Ogled", + "~SHARE_DIALOG.CLOSE": "Zapri", + "~SHARE_DIALOG.COPY_SUCCESS": "Informacije so bile kopirane v odložišče.", + "~SHARE_DIALOG.COPY_ERROR": "Žal podatkov ni bilo mogoče kopirati v odložišče.", + "~SHARE_DIALOG.COPY_TITLE": "Kopiraj rezultat", + "~SHARE_DIALOG.LONGEVITY_WARNING": "Deljena kopija tega dokumenta bo shranjena eno leto od zadnjega dostopa.", + "~SHARE_UPDATE.TITLE": "Posodobljeno v skupni rabi", + "~SHARE_UPDATE.MESSAGE": "Skupni pogled je bil uspešno posodobljen. Posodobitve lahko trajajo do 1 minuto.", + "~CONFIRM.OPEN_FILE": "Imaš neshranjene spremembe. Si prepričan, da želiš odpreti nov dokument?", + "~CONFIRM.NEW_FILE": "Imaš neshranjene spremembe. Si prepričan, da želiš ustvariti nov dokument?", + "~CONFIRM.AUTHORIZE_OPEN": "Za odpiranje dokumenta je potrebna avtorizacija. Ali želiš nadaljevati z avtorizacijo?", + "~CONFIRM.AUTHORIZE_SAVE": "Za shranjevanje dokumenta je potrebna avtorizacija. Ali želiš nadaljevati z avtorizacijo?", + "~CONFIRM.CLOSE_FILE": "Imaš neshranjene spremembe. Si prepričan, da želiš zapreti dokument?", + "~CONFIRM.REVERT_TO_LAST_OPENED": "Ali si prepričan, da želiš dokument vrniti v stanje, v katerem je bil nazadnje odprt?", + "~CONFIRM.REVERT_TO_SHARED_VIEW": "Ali ste prepričani, da želite dokument povrniti v stanje, v katerem je bil nazadnje deljen?", + "~CONFIRM_DIALOG.TITLE": "Ste prepričani?", + "~CONFIRM_DIALOG.YES": "Da", + "~CONFIRM_DIALOG.NO": "Ne", + "~LOCAL_FILE_DIALOG.DROP_FILE_HERE": "Spustite datoteko sem ali kliknite tukaj, da izberete datoteko.", + "~LOCAL_FILE_DIALOG.MULTIPLE_FILES_SELECTED": "Odprete lahko samo eno datoteko.", + "~LOCAL_FILE_DIALOG.MULTIPLE_FILES_DROPPED": "Ne morete spustiti več kot ene datoteke.", + "~IMPORT.LOCAL_FILE": "Lokalna datoteka", + "~IMPORT.URL": "URL-naslov", + "~IMPORT_URL.MULTIPLE_URLS_DROPPED": "Odprete lahko samo en URL.", + "~IMPORT_URL.PLEASE_ENTER_URL": "Vnesi URL za uvoz.", + "~URL_TAB.DROP_URL_HERE": "Spusti URL tukaj ali vnesi URL spodaj", + "~URL_TAB.IMPORT": "Uvozi", + "~CLIENT_ERROR.TITLE": "Napaka", + "~ALERT_DIALOG.TITLE": "Opozorilo", + "~ALERT_DIALOG.CLOSE": "Zapri", + "~ALERT.NO_PROVIDER": "Navedenega dokumenta ni bilo mogoče odpreti, ker ustrezni ponudnik ni na voljo.", + "~GOOGLE_DRIVE.LOGIN_BUTTON_LABEL": "Prijavi se z Googlom", + "~GOOGLE_DRIVE.CONNECTING_MESSAGE": "Povezovanje z Googlom ...", + "~GOOGLE_DRIVE.ERROR_MISSING_CLIENTID": "V možnostih ponudnika GoogleDrive manjka zahtevani ID stranke", + "~DOCSTORE.LOAD_403_ERROR": "Nimaš dovoljenja za nalaganje datoteke %{filename}.

Če uporabljaš dokument, ki ga je delil kdo drug, je morda njegova skupna raba preklicana.", + "~DOCSTORE.LOAD_SHARED_404_ERROR": "Zahtevanega dokumenta v skupni rabi ni mogoče naložiti.

Morda datoteka ni bila v skupni rabi?", + "~DOCSTORE.LOAD_404_ERROR": "Datoteke %{filename} ni mogoče naložiti", + "~DOCSTORE.SAVE_403_ERROR": "Nimaš dovoljenja za shranjevanje datoteke '%{filename}'.

Morda se boš moral znova prijaviti.", + "~DOCSTORE.SAVE_DUPLICATE_ERROR": "Ni mogoče ustvariti %{filename}. Datoteka že obstaja.", + "~DOCSTORE.SAVE_ERROR_WITH_MESSAGE": "Ni mogoče shraniti %{filename}: [%{message}]", + "~DOCSTORE.SAVE_ERROR": "Datoteke %{filename} ni mogoče shraniti.", + "~DOCSTORE.REMOVE_403_ERROR": "Nimaš dovoljenja za odstranitev datoteke %{filename}.

Morda se boš moral znova prijaviti.", + "~DOCSTORE.REMOVE_ERROR": "Datoteke %{filename} ni mogoče odstraniti.", + "~DOCSTORE.RENAME_403_ERROR": "Nimaš dovoljenja za preimenovanje datoteke %{filename}.

Morda se boš moral znova prijaviti.", + "~DOCSTORE.RENAME_ERROR": "Datoteke %{filename} ni mogoče preimenovati.", + "~CONCORD_CLOUD_DEPRECATION.CONFIRM_SAVE_TITLE": "Concord Cloud opozorilo", + "~CONCORD_CLOUD_DEPRECATION.ALERT_SAVE_TITLE": "Concord Cloud opozorilo", + "~CONCORD_CLOUD_DEPRECATION.CONFIRM_SAVE_ELSEWHERE": "Shrani drugam", + "~CONCORD_CLOUD_DEPRECATION.CONFIRM_DO_IT_LATER": "Bom to naredil kasneje", + "~CONCORD_CLOUD_DEPRECATION.SHUT_DOWN_MESSAGE": "Concord Cloud je bil zaprt!", + "~CONCORD_CLOUD_DEPRECATION.PLEASE_SAVE_ELSEWHERE": "Prosimo, shranite dokumente na drugo mesto.", + "~SHARE_DIALOG.SHARE_STATE": "Deljeni pogled: ", + "~SHARE_DIALOG.SHARE_STATE_ENABLED": "emogočeno", + "~SHARE_DIALOG.SHARE_STATE_DISABLED": "onemogočeno", + "~SHARE_DIALOG.ENABLE_SHARING": "Omogoči skupno rabo", + "~SHARE_DIALOG.STOP_SHARING": "Ustavi deljenje", + "~SHARE_DIALOG.UPDATE_SHARING": "Posodobi deljeni pogled", + "~SHARE_DIALOG.PREVIEW_SHARING": "Predogled deljenega pogleda", + "~SHARE_DIALOG.ENABLE_SHARING_MESSAGE": "Ko je omogočena skupna raba, se ustvari povezava do kopije trenutnega pogleda. Z odpiranjem te povezave ima vsak uporabnik svojo kopijo za interakcijo.", + "~SHARE_DIALOG.LINK_TAB": "Povezava", + "~SHARE_DIALOG.LINK_MESSAGE": "Z uporabo spodnje povezave lahko drugim zagotovite njihovo kopijo skupnega pogleda. ", + "~SHARE_DIALOG.EMBED_TAB": "Vdelaj", + "~SHARE_DIALOG.EMBED_MESSAGE": "Vdelana koda za vključitev na spletne strani ali drugo spletno vsebino", + "~SHARE_DIALOG.LARA_MESSAGE": "Uporabi to povezavo pri ustvarjanju dejavnosti v LARA", + "~SHARE_DIALOG.LARA_CODAP_URL": "URL strežnika CODAP:", + "~SHARE_DIALOG.LARA_FULLSCREEN_BUTTON_AND_SCALING": "Gumb za celozaslonski način in skaliranje", + "~SHARE_DIALOG.LARA_DISPLAY_VISIBILITY_TOGGLES": "Preklopi vidnost podatkov na grafih", + "~CONFIRM.CHANGE_LANGUAGE": "Imaš neshranjene spremembe. Si prepričan, da želiš spremeniti jezik?", + "~FILE_STATUS.FAILURE": "Ponovni poskus ...", + "~SHARE_DIALOG.PLEASE_WAIT": "Počakaj, da ustvarimo skupno povezavo …", + "~GOOGLE_DRIVE.ERROR_CONNECTING_MESSAGE": "Napaka pri povezovanju z Googlom!", + "~GOOGLE_DRIVE.ERROR_MISSING_APIKEY": "V možnostih ponudnika GoogleDrive manjka zahtevani APIKey", + "~GOOGLE_DRIVE.UNABLE_TO_UPLOAD": "Datoteke ni mogoče naložiti", + "~GOOGLE_DRIVE.UNABLE_TO_UPLOAD_MSG": "Datoteke ni mogoče naložiti: %{message}", + "~SHARE_DIALOG.INTERACTIVE_API_MESSAGE": "Pri ustvarjanju dejavnosti za predvajalnik dejavnosti uporabi to povezavo", + "~DIALOG.SELECT_INTERACTIVE_STATE.USE_THIS_VERSION": "Uporabi to različico", + "~DIALOG.SELECT_INTERACTIVE_STATE.CLICK_TO_PREVIEW": "Klikni za predogled", + "~DIALOG.SELECT_INTERACTIVE_STATE.UPDATED_AT": "Posodobljeno ob", + "~DIALOG.SELECT_INTERACTIVE_STATE.PAGE": "Stran", + "~DIALOG.SELECT_INTERACTIVE_STATE.ACTIVITY": "Dejavnost", + "~DIALOG.SELECT_INTERACTIVE_STATE.CURRENT_VS_LINKED": "Druga stran vsebuje novejše podatke. Katere želiš uporabiti?", + "~DIALOG.SELECT_INTERACTIVE_STATE.LINKED_VS_LINKED": "Obstajata dve možnosti za nadaljevanje dela. Katero različico želite uporabiti?", + "~DIALOG.SELECT_INTERACTIVE_STATE.TITLE": "Kaj bi radi počeli?", + "~DIALOG.SELECT_INTERACTIVE_STATE.PREVIEW_INFO": "To je predogled vaših podatkov samo za branje. Kliknite kjer koli, da ga zaprete.", + "~GOOGLE_DRIVE.MISSING_SCOPES_MESSAGE": "Poskusite se znova prijaviti in označite vsa polja, ko se prikaže poziv v pojavnem oknu.", + "~FILE_DIALOG.FILTER": "Filtriraj rezultate ...", + "~GOOGLE_DRIVE.USERNAME_LABEL": "Uporabniško ime:", + "~GOOGLE_DRIVE.SELECT_DIFFERENT_ACCOUNT": "Izberite drug Google Račun", + "~GOOGLE_DRIVE.MY_DRIVE": "Moj pogon", + "~GOOGLE_DRIVE.SHARED_DRIVES": "Deljeni diski", + "~GOOGLE_DRIVE.SHARED_WITH_ME": "Deljeno z mano", + "~FILE_STATUS.CONTINUE_SAVE": "Še naprej bomo poskušali shraniti tvoje spremembe.", + "~GOOGLE_DRIVE.ERROR_MISSING_APPID": "V možnostih ponudnika GoogleDrive manjka zahtevani ID aplikacije", + "~FILE_DIALOG.OVERWRITE_CONFIRM": "Ali si prepričan, da želiš prepisati %{filename}?", + "~GOOGLE_DRIVE.REOPEN_DRIVE": "Ponovno odpri pogon", + "~GOOGLE_DRIVE.QUICK_SAVE": "Hitro shranjevanje na moj disk", + "~GOOGLE_DRIVE.PICK_FOLDER": "Shranite v izbrano mapo", + "~GOOGLE_DRIVE.PICK_FILE": "Shranite čez obstoječo datoteko", + "~GOOGLE_DRIVE.SELECT_A_FILE": "Izberite datoteko", + "~GOOGLE_DRIVE.SELECT_A_FOLDER": "Izberi mapo", + "~GOOGLE_DRIVE.STARRED": "Označeno z zvezdico", + "~GOOGLE_DRIVE.SELECT_VALID_FILE": "Izberi veljavno datoteko za to aplikacijo", + "~MENUBAR.APP_IFRAME_TITLE": "Uporaba", + "~MENUBAR.TOOLBAR_LABEL": "Menijska vrstica", + "~MENUBAR.ABOUT_APP": "O aplikaciji %{appName}", + "~MENUBAR.ABOUT": "O tej aplikaciji", + "~MENUBAR.RENAME_DOCUMENT": "Ime dokumenta", + "~MENUBAR.RENAME_FILENAME": "Preimenuj %{filename}", + "~FILE_DIALOG.CLEAR_FILTER": "Počistite filter", + "~FILE_DIALOG.FILE_NOT_FOUND": "%{filename} ni bilo mogoče najti", + "~GOOGLE_DRIVE.UNABLE_TO_LOAD": "Datoteke ni mogoče naložiti" +} \ No newline at end of file diff --git a/src/code/utils/translate.ts b/src/code/utils/translate.ts index 6019d772..e082d3df 100644 --- a/src/code/utils/translate.ts +++ b/src/code/utils/translate.ts @@ -22,6 +22,7 @@ import nl from './lang/nl.json' import nn from './lang/nn.json' import pl from "./lang/pl.json" import ptBR from './lang/pt-BR.json' +import sl from './lang/sl.json' import th from './lang/th.json' import tr from './lang/tr.json' import zhHans from './lang/zh-Hans.json' @@ -48,6 +49,7 @@ const languageFiles: LanguageFileEntry[] = [ {key: 'nn', contents: nn}, // Norwegian Nynorsk {key: 'pl', contents: pl}, // Polish Polski {key: 'pt-BR', contents: ptBR}, // Brazilian Portuguese + {key: 'sl', contents: sl}, // Slovenian {key: 'th', contents: th}, // Thai {key: 'tr', contents: tr}, // Turkish {key: 'zh-Hans', contents: zhHans}, // Simplified Chinese From 6593780e4b889354d3711d5611307afa5548bc4b Mon Sep 17 00:00:00 2001 From: Kirk Swenson Date: Wed, 26 Aug 2026 16:43:52 -0700 Subject: [PATCH 2/3] fix: fall back to English rather than the raw key [CFM-20] translate() resolved a string as translations[lang][key] and fell back to the key itself when that was missing. An unrecognized language therefore rendered every string as its own key -- a consumer selecting a language CFM lacks saw ~MENUBAR.UNTITLED_DOCUMENT rather than "Untitled Document". The same applied per key: a key missing from an otherwise-present locale also rendered as the key. Fall back to en-US before falling back to the key. A key that exists in no locale still renders as itself, so nothing that works today changes. This is what made Slovenian unshippable in CODAP 3.1.0: a 98%-translated locale could not ship at all, because the failure mode for the residual 2% was raw keys rather than English. Tests cover all three acceptance cases; the three fallback tests were confirmed to fail against the previous implementation. (cherry picked from commit 14936d2f08e28badf07d75c1e3f9e98cdd400bd9) --- src/code/utils/translate.test.ts | 35 +++++++++++++++++++++++++++++++- src/code/utils/translate.ts | 12 ++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/code/utils/translate.test.ts b/src/code/utils/translate.test.ts index 67bb0658..ae0a3077 100644 --- a/src/code/utils/translate.test.ts +++ b/src/code/utils/translate.test.ts @@ -1,6 +1,6 @@ import fs from "fs" import path from "path" -import { kBundledLanguageKeys } from "./translate" +import translate, { kBundledLanguageKeys } from "./translate" describe("bundled languages", () => { it("stay in sync with the POEditor pull script", () => { @@ -21,3 +21,36 @@ describe("bundled languages", () => { expect(bundledNonEnglish).toEqual(pulledLangs) }) }) + +describe("translate() language fallback", () => { + it("uses the requested language when it is bundled", () => { + expect(translate("~MENU.NEW", {}, "sl")).toBe("Nov") + }) + + it("falls back to English when the requested language is not bundled", () => { + expect(translate("~MENUBAR.UNTITLED_DOCUMENT", {}, "xx")).toBe("Untitled Document") + }) + + it("falls back to English for a key missing from an otherwise-bundled language", () => { + jest.isolateModules(() => { + // a deliberately incomplete locale; the real sl.json has all 163 keys + jest.doMock("./lang/sl.json", () => ({ "~MENU.NEW": "Nov" })) + // eslint-disable-next-line @typescript-eslint/no-var-requires + const isolatedModule = require("./translate") + const isolated: typeof translate = isolatedModule["default"] + expect(isolated("~MENU.NEW", {}, "sl")).toBe("Nov") + expect(isolated("~MENU.OPEN", {}, "sl")).toBe("Open ...") + }) + jest.dontMock("./lang/sl.json") + }) + + it("renders the key itself when it exists in no language", () => { + expect(translate("~NO.SUCH.KEY", {}, "sl")).toBe("~NO.SUCH.KEY") + expect(translate("~NO.SUCH.KEY", {}, "xx")).toBe("~NO.SUCH.KEY") + }) + + it("still interpolates variables into a fallback translation", () => { + expect(translate("~FILE_DIALOG.REMOVED_MESSAGE", { filename: "data.codap" }, "xx")) + .toBe("data.codap was deleted") + }) +}) diff --git a/src/code/utils/translate.ts b/src/code/utils/translate.ts index e082d3df..2f09e8fc 100644 --- a/src/code/utils/translate.ts +++ b/src/code/utils/translate.ts @@ -93,6 +93,11 @@ languageFiles.forEach(function(lang) { } }) +// CFM owns en-US, so it is the most complete locale and serves as the fallback +// for an unknown language or an untranslated key. Keys in `translations` are +// lower-cased, so this must be too. +const kFallbackLanguage = 'en-us' + const lang = (urlParams as any).lang || getPageLanguage() || getFirstBrowserLanguage() const baseLang = getBaseLanguage(lang || '') // CODAP/SproutCore lower cases language in documentElement @@ -116,7 +121,12 @@ const translate = function(key: string, vars?: Record, lang?: st if (vars == null) { vars = {} } if (lang == null) { lang = gCurrentLanguage } lang = lang.toLowerCase() - let translation = translations[lang] != null ? translations[lang][key] : undefined + // Fall back to English before falling back to the key, so that an unknown + // language or a key missing from an otherwise-present locale degrades to + // English rather than exposing a raw key such as + // `~MENUBAR.UNTITLED_DOCUMENT` in the UI. A key that exists in no locale + // still renders as itself. + let translation = translations[lang]?.[key] ?? translations[kFallbackLanguage]?.[key] if ((translation == null)) { translation = key } return translation.replace(varRegExp, function(match: string, key: string) { return Object.prototype.hasOwnProperty.call(vars, key) From 51c6e9ecfbded28fbb7fa19a658c8f690c1b1cbb Mon Sep 17 00:00:00 2001 From: Kirk Swenson Date: Wed, 26 Aug 2026 21:52:18 -0700 Subject: [PATCH 3/3] test: compare against imported JSON instead of a literal [CFM-19] Addresses review feedback on #434. The bundled-language assertion hardcoded "Nov". sl.json is regenerated by every POEditor pull, so a translator reworking that string would fail a test about fallback behavior. Compare against the imported JSON instead, with a not.toBe against the English value so the assertion still proves Slovenian was selected rather than falling through. Left the English-valued assertions as literals: en-US is not pulled from POEditor (LANGUAGES excludes it; en-US.json is generated locally from en-US-master.json), so they carry no churn risk. The "Nov" inside the isolateModules block is the mock's own literal, not sl.json data, and the interpolation test would have to reimplement translate()'s substitution to avoid its literal. Also adds the missing `th` comment in the pull script's language list, which had 17 comments against 18 entries. (cherry picked from commit 36ca5129fa87b3fd250bce3caed061b0693ed434) --- bin/strings-pull-project.sh | 1 + src/code/utils/translate.test.ts | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/bin/strings-pull-project.sh b/bin/strings-pull-project.sh index 52b2b829..28f284d1 100755 --- a/bin/strings-pull-project.sh +++ b/bin/strings-pull-project.sh @@ -15,6 +15,7 @@ OUTPUT_DIR=src/code/utils/lang # pl: Polish Polski # pt-BR: Brazilian Portuguese # sl: Slovenian +# th: Thai # tr: Turkish # zh-Hans: Simplified Chinese # zh-TW: Traditional Chinese (Taiwan) diff --git a/src/code/utils/translate.test.ts b/src/code/utils/translate.test.ts index ae0a3077..490085a5 100644 --- a/src/code/utils/translate.test.ts +++ b/src/code/utils/translate.test.ts @@ -1,5 +1,7 @@ import fs from "fs" import path from "path" +import enUS from "./lang/en-US.json" +import sl from "./lang/sl.json" import translate, { kBundledLanguageKeys } from "./translate" describe("bundled languages", () => { @@ -24,7 +26,11 @@ describe("bundled languages", () => { describe("translate() language fallback", () => { it("uses the requested language when it is bundled", () => { - expect(translate("~MENU.NEW", {}, "sl")).toBe("Nov") + // compared against the imported JSON rather than a literal, so a future + // POEditor pull that reworks the Slovenian string doesn't fail a test + // about fallback behavior + expect(translate("~MENU.NEW", {}, "sl")).toBe(sl["~MENU.NEW"]) + expect(translate("~MENU.NEW", {}, "sl")).not.toBe(enUS["~MENU.NEW"]) }) it("falls back to English when the requested language is not bundled", () => {