Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bin/strings-pull-project.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ OUTPUT_DIR=src/code/utils/lang
# nn: Norwegian Nynorsk
# pl: Polish Polski
# pt-BR: Brazilian Portuguese
# sl: Slovenian
# th: Thai
# 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 ]]
Expand Down
165 changes: 165 additions & 0 deletions src/code/utils/lang/sl.json
Original file line number Diff line number Diff line change
@@ -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}.<br><br>Č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.<br><br>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}'.<br><br>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}.<br><br>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}.<br><br>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"
}
41 changes: 40 additions & 1 deletion src/code/utils/translate.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import fs from "fs"
import path from "path"
import { kBundledLanguageKeys } from "./translate"
import enUS from "./lang/en-US.json"
import sl from "./lang/sl.json"
import translate, { kBundledLanguageKeys } from "./translate"

describe("bundled languages", () => {
it("stay in sync with the POEditor pull script", () => {
Expand All @@ -21,3 +23,40 @@ describe("bundled languages", () => {
expect(bundledNonEnglish).toEqual(pulledLangs)
})
})

describe("translate() language fallback", () => {
it("uses the requested language when it is bundled", () => {
// 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", () => {
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")
})
})
14 changes: 13 additions & 1 deletion src/code/utils/translate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -91,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
Expand All @@ -114,7 +121,12 @@ const translate = function(key: string, vars?: Record<string, string>, 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)
Expand Down
Loading