Merge branch 'main' into fix/remove-operator-hud-layout

This commit is contained in:
German Khokhlov authored and GitHub committed 2026-10-29 17:45:40 +04:00
commit 4ed06fac22
19 files changed
+6715 -20

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
- Add Ukrainian (uk) to the language picker: the whole interface in Ukrainian,
with its four plural forms and Cyrillic HUD units. Based on the Ukrainian
translation by @meirm (#312) (German, @gekh, #1470).
+3
View File
@@ -0,0 +1,3 @@
- Add Italian (it) to the language picker: the whole interface in Italian,
with Italian plural forms and labels shortened to fit the panels. Based on
the translation by @abiyeenzo (#912) (German, @gekh, #1473).
+1 -1
View File
@@ -3789,7 +3789,7 @@ test:track` 43 tracking invariants · headless QA harnesses under
### Interface localization (October 2026) ### Interface localization (October 2026)
Shipped locales: **en** (source and fallback), **es**, **ru**, **zh-CN** and **zh-Hant**. The code lives in Shipped locales: **en** (source and fallback), **es**, **it**, **ru**, **uk**, **zh-CN** and **zh-Hant**. The code lives in
`src/i18n/`. How to mark UI text: [`docs/I18N.md`](./I18N.md); translator workflow: `src/i18n/`. How to mark UI text: [`docs/I18N.md`](./I18N.md); translator workflow:
[`docs/TRANSLATORS.md`](./TRANSLATORS.md). [`docs/TRANSLATORS.md`](./TRANSLATORS.md).
+2 -2
View File
@@ -1,6 +1,6 @@
# Translating God's Eye View # Translating God's Eye View
Shipped languages: **English** (source and fallback), **Spanish** (`es`), **Russian** (`ru`), **Simplified Chinese** (`zh-CN`) and **Traditional Chinese** (`zh-Hant`). Shipped languages: **English** (source and fallback), **Spanish** (`es`), **Italian** (`it`), **Russian** (`ru`), **Ukrainian** (`uk`), **Simplified Chinese** (`zh-CN`) and **Traditional Chinese** (`zh-Hant`).
## How it works ## How it works
@@ -63,4 +63,4 @@ These are contracts, not copy: layer ids and registry keys, status enums and oth
## Language notes ## Language notes
Each language keeps its glossary, plural forms, units and style in its own file, `docs/translations/<code>.md`: [Spanish](./translations/es.md), [Russian](./translations/ru.md), [Simplified Chinese](./translations/zh-CN.md), [Traditional Chinese](./translations/zh-Hant.md). Read it before translating, and add a term there when you settle on one. Each language keeps its glossary, plural forms, units and style in its own file, `docs/translations/<code>.md`: [Spanish](./translations/es.md), [Italian](./translations/it.md), [Russian](./translations/ru.md), [Ukrainian](./translations/uk.md), [Simplified Chinese](./translations/zh-CN.md), [Traditional Chinese](./translations/zh-Hant.md). Read it before translating, and add a term there when you settle on one.
+108
View File
@@ -0,0 +1,108 @@
# Italian (`it`)
Catalog: `src/i18n/locales/it.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
Translation source: abiyeenzo (#912).
## Style
- Standard Italian as in Italian software UIs. Address the user with informal "tu" imperatives: «Aggiungi», «Attiva», «Scegli», «Incolla», «Riprova». Tooltips that name an action use the same imperative: «Espandi il pannello», «Ripristina la camera».
- Sentence case, not Title Case: "Live Flights" → «Voli live», "Space Missions" → «Missioni spaziali». UPPERCASE English stays UPPERCASE: «LIVE», «NON DISPONIBILE».
- Quotes inside Italian text are straight double quotes: «Eliminare lo scatto "{shot}"?». Use the accented capitals (È, À) in UPPERCASE text: «VELOCITÀ», «CITTÀ».
- Errors read as a statement, not a past participle on its own: "Search failed" → «Ricerca non riuscita»; "Could not …" → «Impossibile …». A dash that joins a cause and a hint becomes a colon: «Chiave API rifiutata: controlla AISSTREAM_API_KEY». The same goes for a label and its hint in a tooltip: «Direzione (bussola °): clic per digitare».
- Short labels, chips and HUD readouts stay short. A standard abbreviation with a period beats a wrapped line: REGOLAZ., RILEV., INCL., ALT., VEL., STIM., PROIEZ., ESP.; PREC / SUCC for PREV / NEXT and PREVIOUS / NEXT.
- The HUD codes HDG, BRG, CRS, FOV and DEST stay in English, as in cockpit avionics. RANGE is «PORTATA». Words used in a sentence are translated: heading → «prua» (aircraft, ships) or «direzione» (camera, buses, trams, trains), course → «rotta».
- Keyboard keys stay in English: Space, Enter, Esc, Tab, Shift, Backspace, H, 1–9 («Tieni premuto Space per parlare», «Premi Enter per riprovare»).
- Decimal comma in numbers written in the text, magnitudes included: «28,2710° N», «M5,2». Numbers formatted by the code follow `Intl` and need nothing.
- Amounts: thousands, millions and billions are written out or abbreviated in Italian, not with K/M/B: «{amount} mila $», «{amount} mln $», «{amount} mld $».
- Units: km, m, km/h, m/s, kn (knots), ft (feet), h, min, s; in UPPERCASE text KM, KM/S, KT, FT, MIN, H. "m" is always metres, so minutes are «min» («5 min fa»). MW stays MW. `°` and `%` stay.
- "ago" goes after the time: «{count} min fa». When a placeholder already holds a localized "ago" text (`{age}`, `{ago}`), do not add «fa» again.
- A feature name used as a sentence subject gets a head noun so the verb agrees: «La modalità Missioni spaziali isola…», «Il pannello Impostazioni provider è disattivato…», «Il livello {layer} è attivo…», «Lo strumento {name} è attivo…». A short card title with a placeholder name puts the state after a colon: «{label}: attivato». The same goes for a placeholder list of names: «Attivazione recente: {layers}». A count in a plain `t()` entry (no plural forms) that can be 1 goes after a label, so nothing has to agree with it: «Record accettati: {accepted} su {raw}».
- English loanwords that Italian UIs use as they are stay in English: feed, live, cockpit, briefing, preset, overlay, layout, snapshot, tile, provider, timeout, payload, bloom, scope.
## Plurals
A `tn()` entry needs three forms. `Intl.PluralRules('it')` picks:
| Form | Counts | Example |
| ------- | -------------------------------------- | ----------------------- |
| `one` | 1 | 1 telecamera |
| `many` | 1 000 000, 2 000 000… (exact millions) | 1 000 000 di telecamere |
| `other` | everything else: 0, 2–999 999, 1,5… | 3 telecamere |
The `many` form is the `other` text with «di» after the number when a noun follows: «{count} di telecamere caricate», «{count} DI CHIAVI IN ATTESA». When no noun follows the number («{count} vicine», «{count} RICEVUTI»), `many` repeats `other`. Nouns that do not change in the plural keep one form for all three (`{count} file`, `{count} data center`), with «di» in `many`.
## Glossary
| English | Italiano |
| --------------------------------------------- | ------------------------------------------------------------ |
| layer / data layer | livello / livello dati |
| feed / source / data source | feed / fonte / fonte dati |
| LIVE (status) / live data / live (feed state) | LIVE / dati live / live |
| SIMULATED | SIMULATO |
| stale / degraded / unavailable | obsoleto / degradato / non disponibile |
| retry / retrying | riprova / nuovo tentativo |
| rate limited | limite di richieste (raggiunto) |
| key (API key) / POWER UP / POWERED UP | chiave / ACCENDI / ACCESO |
| Provider Settings | Impostazioni provider |
| ON / OFF (state chips) | ON / OFF |
| enable / disable | attivare / disattivare |
| globe / map / map source / map stack | globo / mappa / fonte mappa / stack mappa |
| basemap / box (Recent Imagery area) | mappa di base / riquadro |
| aircraft, plane / flight / military flights | aereo / volo / voli militari |
| vessel, ship | nave |
| satellite / shell (Starlink) / DENSE | satellite / shell / DENSO |
| SPARSE / BALANCED / DENSE (detection modes) | RADO / BILANCIATO / DENSO (agree with «overlay») |
| broadband (Starlink shell) | a banda larga |
| VISUAL (CelesTrak group) | VISIBILI |
| launch (rocket) / launch site / space mission | lancio / sito di lancio / missione spaziale |
| ascent / replay / payload / stage | salita / replay / payload / stadio |
| contact / tracking | contatto / tracciamento |
| installation / site | installazione / sito |
| mapped / observed | mappato / osservato |
| camera (view) / camera, CCTV camera | camera / telecamera, telecamera CCTV |
| earthquake / active fire / fire perimeter | terremoto / incendio attivo / perimetro dell'incendio |
| lightning strike / cloud (cover, %) | fulmine / nuvole («non singoli fulmini», «20% di nuvole») |
| incident (wildfire, Type N) / scene incident | evento (Evento di tipo N, Incendio senza nome) / evento |
| trigger record (USGS card) / OPEN SOURCE link | scheda / APRI LA FONTE |
| advisory (NHC/CPHC cyclone) / cone | bollettino / cono di incertezza |
| forecast lead hours (cyclone points) | scadenze di previsione in ore |
| AIR / SEA (radio category) / ground station | ARIA / MARE / stazione di terra |
| radio station / broadcaster | stazione radio / emittente |
| CH (radio channel readout) | CH (unchanged) |
| road / flow (traffic) | strada / flusso |
| transit / bike share | trasporto pubblico / bike sharing |
| last report (vehicle position) | ultima segnalazione |
| scene / shot / scene recipe | scena / scatto / ricetta della scena |
| anchor / data pack / bundle (scene) | ancora / pacchetto dati / pacchetto |
| story beat | tappa del racconto |
| briefing / panel / dock | briefing / pannello / dock |
| cockpit | cockpit |
| preset / visual preset | preset / preset visivo |
| Tactical / Operator / Minimal / Cyber | Tattico / Operatore / Minimale / Cyber |
| Scope (circular viewport mask) / keyhole | scope / foro |
| feather / fade | sfumatura / dissolvenza |
| detection overlay | overlay di rilevamento |
| pin (Draw tool) / mark | segnaposto / segno |
| pin (Recent Imagery) | fissare |
| Draw / Directions (tools) | Disegno / Indicazioni |
| Street Level (layer) / Street View (Google) | Vista stradale / Street View |
| Recent Imagery | Immagini recenti |
| Global Context / Context (panel) | Contesto globale / Contesto |
| heading, course / altitude / ground speed | prua, rotta / altitudine / velocità al suolo (VEL. AL SUOLO) |
| altitude in short HUD captions / Alt unknown | QUOTA (QUOTA · FT) / Quota sconosciuta |
| fallback (FALLBACK, Street View fallback) | riserva / di riserva (SUPERFICIE DI RISERVA) |
| forecast track / advisory center (cyclones) | traccia prevista / centro del ciclone da bollettino |
| witness source / authored (scene) | fonte testimoniale / predefinito, definito dall'autore |
| explore manually | esplora manualmente |
| HDG / BRG / CRS / FOV / DEST | HDG / BRG / CRS / FOV / DEST (unchanged) |
| RANGE / PITCH / HGT (CCTV calibration) | PORTATA / INCL. / ALT. |
| PREV / NEXT | PREC / SUCC |
| Previous / Next (button text) | Precedente / Successivo |
| Focus {target} (action, aria label) | Inquadra {target} (the FOCUS button stays FOCUS) |
| L / R (relative bearing) | S / D |
| SIG / NEWS / LOCAL (briefing tabs) | SIG / NOTIZIE / LOCALE |
| compass points (N, NNE, …, W, NW) | N, NNE, …, O, NO (west is «O», ovest) |
| NOT AN ALL-CLEAR | NON È UN VIA LIBERA |
| weather field / model / valid / issued | campo / modello / valida / emessa |
| click / click to … | fai clic / clic per … |
+106
View File
@@ -0,0 +1,106 @@
# Ukrainian (`uk`)
Catalog: `src/i18n/locales/uk.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
Translation source: meirm (#312).
## Style
- Address the user with polite imperatives: «Натисніть», «Увімкніть», «Спробуйте ще раз».
- Sentence case, not Title Case: "Live Flights" → «Рейси онлайн».
- Ukrainian norms, not Russian calques: «у радіусі», «за посиланням», «надто довгий», «потім», «вигляд» (a camera view: «вигляд згори»), «стеження» (tracking); prepositions alternate у/в and і/й by euphony.
- The apostrophe is always ʼ (U+02BC): обʼєкт, звʼязок, памʼять. Not ' or ’.
- Quotes inside Ukrainian text are «ялинки»; a button or layer name inside a sentence goes in them: Увімкніть «Вуличні знімки».
- In short labels, a standard abbreviation with a period beats a wrapped line: ПОПЕР., ДАЛЬН., КАЛІБР.
- Units: км, м, км/год, м/с, вуз (knots), фт (feet), год, хв, с, МГц, дБ, гПа; in UPPERCASE text КМ, М, КМ/С, ВУЗ, ФТ. MW is МВт in any case. `°` and `%` stay. Decimal comma in coordinates written as text: 28,2710° пн. ш.
- Coordinates: пн. ш. / пд. ш. / сх. д. / зх. д.; the HUD hemisphere letters are Пн, Пд, Сх, Зх.
## Plurals
A `tn()` entry needs four forms. `Intl.PluralRules('uk')` picks:
| Form | Counts | Example |
| ------- | ------------------------ | ---------- |
| `one` | 1, 21, 31, 101… | 1 камера |
| `few` | 2–4, 22–24, 32–34… | 3 камери |
| `many` | 0, 5–20, 25–30, 111–114… | 5 камер |
| `other` | fractions: 1.5, 2.25… | 1,5 камери |
`other` takes the genitive singular: 1,5 кадру, 1,5 літака, 1,5 ключа. When a word next to a number cannot agree in every form, move the number after a colon instead: «поруч: {count}», «відповідей: {responses}». The same trick keeps place names and tool names out of a preposition's case: «звідки: {origin}», «Активно: {name}».
## Glossary
| English | Українська |
| ----------------------------------------------- | ---------------------------------------------------------------- |
| layer / data layer | шар / шар даних |
| feed | потік даних (short: потік) |
| source / data source | джерело / джерело даних |
| LIVE (status) / live data | ОНЛАЙН / у реальному часі (status chip: ДАНІ ОНЛАЙН) |
| SIMULATED / SYNTHETIC | ІМІТАЦІЯ |
| stale / degraded / unavailable | застарів / з перебоями (chip: ПЕРЕБОЇ) / недоступний |
| retry | повтор |
| key (API key) / POWER UP | ключ / ЗАПУСК |
| Provider Settings / provider | Налаштування постачальників / постачальник |
| globe / map / map source / basemap | глобус / карта / джерело карти / підкладка |
| aircraft / flight / military flights | літак (short HUD: борт) / рейс / військові рейси |
| vessel, ship / vessels | судно / судна (gen. pl. суден) |
| satellite | супутник |
| launch (rocket) / space mission | запуск / космічна місія |
| ascent (rocket) / booster FLIGHT {n} | виведення / ПОЛІТ {n} (not рейс) |
| contact / tracking | контакт / стеження |
| installation, site / mapped | обʼєкт / нанесений (обʼєкт, контакт) |
| camera / CCTV camera | камера / камера CCTV |
| earthquake / active fire / fire perimeter | землетрус / активна пожежа / контур пожежі |
| advisory (NHC/CPHC cyclone) | зведення |
| scene / shot / scene recipe | сцена / кадр / рецепт сцени |
| briefing / panel / dock | брифінг / панель / док |
| preset / visual preset | пресет / візуальний пресет |
| Scope, keyhole (circular viewport mask) | приціл |
| overlay (detection overlay) / world overlay | накладка / накладка на глобусі |
| heading / altitude / ground speed | курс / висота / шляхова швидкість |
| map bearing, map heading | азимут карти |
| HDG / BRG / CRS, TRK / FOV / DEST / SPD / RANGE | КУРС / ПЕЛ. / ШК (шляховий кут) / ОГЛЯД / ПРИЗН. / ШВИД / ДАЛЬН. |
| Street View (Google) | Перегляд вулиць (as in Ukrainian Google Maps) |
| Street Level (layer) | Вуличні знімки |
| imagery / recent imagery | знімки / свіжі знімки |
| radar reflectivity / lightning density | радіолокаційна відбивність / щільність блискавок |
| infrared | інфрачервоний (short: ІЧ) |
| receiver / gain / decoder | приймач / підсилення / декодер |
| broadcaster | мовник |
| CONUS, WHOT/BHOT, Ironbow, CRT, NVG, FLIR | без перекладу |
| PREV / NEXT (HUD) / Previous / Next | ПОПЕР. / ДАЛІ / Попередній / Наступний |
| anchor / data pack / bundle (scene) | якір / пакет даних / архів |
| box (Recent Imagery area) | область |
| camera move | переліт |
| SIG / NEWS / LOCAL (briefing tabs) | СИГН. / НОВИНИ / МІСЦ. |
| compass points (N, NNE, NE, …) | Пн, ПнПнСх, ПнСх, … |
| L / R (relative bearing) | Л / П |
| click / tap | клацнути / натиснути |
| session (voice, OAuth) | сеанс |
| sequence (street-level photos) | послідовність |
| search (place search) | пошук місця |
| pitch (CCTV camera pose) | нахил |
| traffic (road traffic layer) | дорожній рух |
| phosphor (CRT) | люмінофор |
| view (map extent) / view centre / in view | видима область (карти) / центр карти / у кадрі |
| select (verb) / selected | вибрати / вибраний (not обрати) |
| ping (cockpit proximity contacts) | контакт |
| merge (turn-by-turn) / on ramp | влитися в потік / вʼїзд (off ramp: зʼїзд) |
| watch (Earth/Orbital Watch) | спостереження |
| timeout | тайм-аут |
| icon | значок (not іконка) |
| run (scene playback) / RUN LOG | відтворення / ЖУРНАЛ ВІДТВОРЕННЯ (запуск is a rocket launch) |
| Preview (voice panel replies) | відповіді |
| Marine (radio tag) / pleasure craft | море (not флот) / прогулянкове судно |
| emergency (ADS-B) / Emergency (radio tag) | аварійний стан / надзвичайні ситуації |
| confidence (FIRMS fire detection) | достовірність (short: достов.) |
| thermal (FLIR-style imaging) | тепловий (not термальний) |
| map stack | набір карт |
| stage (rocket) re-entry / recovery | вхід ступеня в атмосферу / повернення |
| ON / OFF (toggle chips) | УВІМК. / ВИМК. |
| Clean UI / EXIT CLEAN VIEW | чистий інтерфейс / ПОВЕРНУТИ ІНТЕРФЕЙС |
| full globe view | вигляд всього глобуса |
| inputs (cockpit context chip) | потоки (not вхідні дані) |
| downrange (rocket stage) | дальність |
| resolution (spatial) | роздільна здатність (not роздільність) |
| shared follow expired | посилання на стеження застаріло |
+86
View File
@@ -440,7 +440,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -480,7 +482,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -510,7 +514,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js", "src/ui/unitLabels.js",
@@ -532,7 +538,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -549,7 +557,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -569,7 +579,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js", "src/ui/unitLabels.js",
@@ -596,7 +608,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/layerNames.js" "src/ui/layerNames.js"
@@ -631,7 +645,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js" "src/ui/unitLabels.js"
@@ -648,7 +664,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js" "src/ui/unitLabels.js"
@@ -807,7 +825,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js", "src/ui/unitLabels.js",
@@ -929,7 +949,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js" "src/ui/unitLabels.js"
@@ -997,7 +1019,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js" "src/ui/unitLabels.js"
@@ -1046,7 +1070,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -1109,7 +1135,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -1139,7 +1167,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js", "src/ui/unitLabels.js",
@@ -1170,7 +1200,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -1201,7 +1233,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js" "src/ui/unitLabels.js"
@@ -1235,7 +1269,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -1278,7 +1314,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/layers/installations/classLabel.js" "src/layers/installations/classLabel.js"
@@ -1315,7 +1353,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js" "src/ui/unitLabels.js"
@@ -1366,7 +1406,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -1396,7 +1438,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -1445,7 +1489,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -1509,7 +1555,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/layers/streetLevel/providers/mapillary/policy.js" "src/layers/streetLevel/providers/mapillary/policy.js"
@@ -1539,7 +1587,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -1580,7 +1630,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -1603,7 +1655,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -1660,7 +1714,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -1723,7 +1779,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/statusText.js", "src/ui/statusText.js",
@@ -2607,7 +2665,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js", "src/ui/unitLabels.js",
@@ -3192,7 +3252,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js", "src/ui/unitLabels.js",
@@ -3303,7 +3365,9 @@
"src/data/cameraArrival.js", "src/data/cameraArrival.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/i18n/index.js" "src/i18n/index.js"
@@ -3327,7 +3391,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -3364,7 +3430,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/scopeMaskPolicy.js", "src/scopeMaskPolicy.js",
@@ -3391,7 +3459,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -3483,7 +3553,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js" "src/ui/unitLabels.js"
@@ -3539,7 +3611,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/statusText.js", "src/ui/statusText.js",
@@ -3794,7 +3868,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -3838,7 +3914,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -3892,7 +3970,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json"
], ],
@@ -3909,7 +3989,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/statusText.js", "src/ui/statusText.js",
@@ -3969,7 +4051,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/data/geoImportImages.js", "src/data/geoImportImages.js",
@@ -4166,7 +4250,9 @@
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/statusText.js", "src/ui/statusText.js",
+20
View File
@@ -33,8 +33,12 @@ function memoryStorage(initial = {}) {
test('normalizeLocale folds regional variants and rejects unshipped tags', () => { test('normalizeLocale folds regional variants and rejects unshipped tags', () => {
assert.equal(normalizeLocale('ru-RU'), 'ru'); assert.equal(normalizeLocale('ru-RU'), 'ru');
assert.equal(normalizeLocale(' RU '), 'ru'); assert.equal(normalizeLocale(' RU '), 'ru');
assert.equal(normalizeLocale('uk'), 'uk');
assert.equal(normalizeLocale('uk-UA'), 'uk');
assert.equal(normalizeLocale('en_GB'), 'en'); assert.equal(normalizeLocale('en_GB'), 'en');
assert.equal(normalizeLocale('xx'), null); assert.equal(normalizeLocale('xx'), null);
assert.equal(normalizeLocale('it-IT'), 'it');
assert.equal(normalizeLocale('it_CH'), 'it');
// Explicit script tags take precedence over regional defaults. // Explicit script tags take precedence over regional defaults.
assert.equal(normalizeLocale('zh'), 'zh-CN'); assert.equal(normalizeLocale('zh'), 'zh-CN');
assert.equal(normalizeLocale('zh-CN'), 'zh-CN'); assert.equal(normalizeLocale('zh-CN'), 'zh-CN');
@@ -142,6 +146,22 @@ test('tn() picks English and Russian plural forms', () => {
assert.match(label(21), / 21 КЛЮЧ(?![А-ЯЁ])/); assert.match(label(21), / 21 КЛЮЧ(?![А-ЯЁ])/);
}); });
test('tn() picks the four Ukrainian plural forms', () => {
const label = (count) =>
tn(
count,
'POWER UP · {count} KEY WAITING',
'POWER UP · {count} KEYS WAITING',
);
setLocale('uk');
assert.match(label(1), / 1 КЛЮЧ(?![А-ЯҐЄІЇ])/);
assert.match(label(3), / 3 КЛЮЧІ(?![А-ЯҐЄІЇ])/);
assert.match(label(5), / 5 КЛЮЧІВ(?![А-ЯҐЄІЇ])/);
assert.match(label(11), / 11 КЛЮЧІВ(?![А-ЯҐЄІЇ])/);
assert.match(label(21), / 21 КЛЮЧ(?![А-ЯҐЄІЇ])/);
assert.match(label(1.5), / 1\.5 КЛЮЧА(?![А-ЯҐЄІЇ])/);
});
test('t() context separates identical English with different meanings', () => { test('t() context separates identical English with different meanings', () => {
assert.equal(t('CLEAR', { context: 'action' }), 'CLEAR'); assert.equal(t('CLEAR', { context: 'action' }), 'CLEAR');
setLocale('ru'); setLocale('ru');
+10 -1
View File
@@ -17,11 +17,20 @@ import {
writeStoredLocale, writeStoredLocale,
} from './locale.js'; } from './locale.js';
import es from './locales/es.json' with { type: 'json' }; import es from './locales/es.json' with { type: 'json' };
import it from './locales/it.json' with { type: 'json' };
import ru from './locales/ru.json' with { type: 'json' }; import ru from './locales/ru.json' with { type: 'json' };
import uk from './locales/uk.json' with { type: 'json' };
import zhCN from './locales/zh-CN.json' with { type: 'json' }; import zhCN from './locales/zh-CN.json' with { type: 'json' };
import zhHant from './locales/zh-Hant.json' with { type: 'json' }; import zhHant from './locales/zh-Hant.json' with { type: 'json' };
const CATALOGS = Object.freeze({ es, ru, 'zh-CN': zhCN, 'zh-Hant': zhHant }); const CATALOGS = Object.freeze({
es,
it,
ru,
uk,
'zh-CN': zhCN,
'zh-Hant': zhHant,
});
let currentLocale = DEFAULT_LOCALE; let currentLocale = DEFAULT_LOCALE;
+11 -1
View File
@@ -8,13 +8,23 @@ export const LOCALE_STORAGE_KEY = 'gev:locale:v1';
export const DEFAULT_LOCALE = 'en'; export const DEFAULT_LOCALE = 'en';
/** Shipped locales; order does not matter. Adding one: docs/TRANSLATORS.md. */ /** Shipped locales; order does not matter. Adding one: docs/TRANSLATORS.md. */
export const LOCALES = Object.freeze(['en', 'es', 'ru', 'zh-CN', 'zh-Hant']); export const LOCALES = Object.freeze([
'en',
'es',
'it',
'ru',
'uk',
'zh-CN',
'zh-Hant',
]);
/** Each locale named in its own language, for the selector. */ /** Each locale named in its own language, for the selector. */
export const LOCALE_NAMES = Object.freeze({ export const LOCALE_NAMES = Object.freeze({
en: 'English', en: 'English',
es: 'Español', es: 'Español',
it: 'Italiano',
ru: 'Русский', ru: 'Русский',
uk: 'Українська',
'zh-CN': '中文(简体)', 'zh-CN': '中文(简体)',
'zh-Hant': '中文(繁體)', 'zh-Hant': '中文(繁體)',
}); });
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+182
View File
@@ -124,6 +124,175 @@
"ΔN", "ΔN",
"▲ {frp} MW" "▲ {frp} MW"
], ],
"it": [
"1090 MHz",
"978 MHz UAT",
"AFRICA",
"AIS: --",
"AIS: {name}",
"ALT {altitude} FT · GS {speed} KT · TRK {track}",
"ANIME",
"ASIA",
"AUTO",
"AUTO HOP OFF",
"AUTO HOP ON",
"Alternative",
"Ambient",
"Anime",
"Area",
"Austin",
"BRG {value}°",
"BRG —",
"Bloom",
"Blues",
"Business jet",
"CAL {value}",
"CAL · {badge}",
"CARGO",
"CCTV",
"CCTV OFF",
"CCTV ON",
"CH {index} / {total}",
"COCKPIT",
"CRS {value}°",
"CRT",
"Country",
"Cyber",
"DEST ---°",
"DEST {bearing}",
"DISPLAY",
"Dance",
"Drone",
"Dubai",
"ENE",
"ESE",
"FLIR",
"FM MHZ",
"FOCUS",
"FOV",
"FOV {value}°",
"FT",
"Folk",
"Funk",
"GEO",
"GOOGLE STREET VIEW",
"Google Street View",
"HDG",
"HDG {value}°",
"Hip-Hop",
"House",
"Indie",
"Ironbow",
"Jazz",
"KTS",
"LIVE",
"Landsat 8/9",
"Layout",
"Los Angeles",
"MIC",
"MIL",
"MINI",
"MM",
"MONITOR",
"MSG/S",
"Metal",
"NASA FIRMS · LIVE",
"NAV",
"NE",
"NNE",
"NOIR",
"NVG",
"New York",
"Noir",
"OCEANIA",
"OFF",
"OK",
"ON",
"ON/OFF",
"Oldies",
"PAYLOAD",
"PLAY",
"POS: LIVE",
"POS: {time}Z",
"PRECIP",
"Pop",
"Punk",
"RADIO",
"REC",
"RESET",
"RESET CAL",
"Radio",
"Reggae",
"Rock",
"S2 GhostMaps",
"SAR",
"SE",
"SIG",
"SNAPSHOT",
"SQUAWK {code} · {status}",
"SSE",
"STANDARD",
"STD",
"STOP",
"SYNC",
"San Francisco",
"Scope",
"Sentinel-2",
"Sonar",
"Soul",
"TEMP",
"TIMEOUT",
"Talk",
"Tallinn",
"Techno",
"Tokyo",
"Tram",
"Trance",
"UPSTREAM",
"USB {rate} msg/s",
"Upstream HTTP {status}",
"V/S {rate} FPM",
"VIEWSHED",
"VIEWSHED ON",
"VOLUME",
"VS",
"WHOT/BHOT",
"Washington DC",
"World",
"audio {state}",
"dB",
"feed status|live",
"ft",
"hPa",
"installation class|BASE",
"km",
"km/h",
"max {value} MW",
"min",
"mini",
"mph",
"radio|TALK",
"satellite",
"standard",
"view band|METRO",
"voice|OFF",
"{count} MSGS",
"{count} feeds live",
"{count} scenes",
"{rate} msg/s",
"{scene} / {pack}: {status}. File: {path}. {attribution} · {license}",
"{source} HTTP {status}",
"{speed} kn",
"{speed}KT",
"{value} KM",
"{value} KM DOWNRANGE",
"{value} KM/H",
"{value} KM/S",
"{value} conf",
"ΔE",
"ΔN",
"▲ {frp} MW"
],
"ru": [ "ru": [
"AIS: --", "AIS: --",
"AIS: {name}", "AIS: {name}",
@@ -142,6 +311,19 @@
"VS", "VS",
"WHOT/BHOT" "WHOT/BHOT"
], ],
"uk": [
"AIS: --",
"AIS: {name}",
"CCTV",
"CRT",
"FLIR",
"Ironbow",
"Landsat 8/9",
"NVG",
"REC",
"S2 GhostMaps",
"WHOT/BHOT"
],
"zh-CN": [ "zh-CN": [
"1090 MHz", "1090 MHz",
"978 MHz UAT", "978 MHz UAT",
@@ -0,0 +1,20 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { SCENE_RECIPES, displayShotTitle } from './recipes.js';
import { setLocale } from '../i18n/index.js';
test('an operator-typed title in the built-in Nepal scene stays as typed', () => {
const scene = SCENE_RECIPES.find((r) => r.id === 'bhote-koshi-nepal-scene');
assert.ok(scene, 'built-in bootstrap scene exists');
for (const locale of ['ru', 'es', 'it', 'zh-CN', 'zh-Hant']) {
setLocale(locale);
const title = 'Global Incident Context';
// A shot captured by the operator: no sourcePackId, renamed to this title.
const shown = displayShotTitle(
title,
{ id: 'shot-user', title },
{ id: scene.id },
);
assert.equal(shown, title, `${locale}: got ${shown}`);
}
});
+4
View File
@@ -712,6 +712,10 @@ test('a legacy three-shot Nepal browser project bootstraps to the current 25-sho
assert.deepEqual(scene.shots.slice(0, 3).map(({ camera }) => camera), originalCameras); assert.deepEqual(scene.shots.slice(0, 3).map(({ camera }) => camera), originalCameras);
assert.equal(scene.appliedShotPacks[0].id, 'bhote-koshi-nepal-evidence-pack'); assert.equal(scene.appliedShotPacks[0].id, 'bhote-koshi-nepal-evidence-pack');
assert.equal(scene.appliedShotPacks[0].version, 18); assert.equal(scene.appliedShotPacks[0].version, 18);
// displayShotTitle trusts these bindings to localize the bootstrap shots.
for (const shot of scene.shots.slice(0, 8)) {
assert.equal(scene.appliedShotPacks[0].shotBindings[shot.title], shot.id);
}
assert.equal(director._selectedSceneId, scene.id); assert.equal(director._selectedSceneId, scene.id);
assert.equal(director._selectedShotId, scene.shots[0].id); assert.equal(director._selectedShotId, scene.shots[0].id);
} finally { } finally {
+28 -9
View File
@@ -1061,25 +1061,44 @@ export function displaySceneTitle(scene) {
* (`sourcePackId`), so a typed title that matches a built-in one stays as * (`sourcePackId`), so a typed title that matches a built-in one stays as
* typed. Without `shot`, the title alone decides. * typed. Without `shot`, the title alone decides.
* @param {string} title * @param {string} title
* @param {{sourcePackId?: string}} [shot] * @param {{id?: string, sourcePackId?: string}} [shot]
* @param {{id?: string}} [scene] The shot's scene, for projects saved before * @param {{id?: string, appliedShotPacks?: Array<{id?: string, shotBindings?: Object<string, string>}>}} [scene]
* shots recorded their source pack. * The shot's scene, for projects saved before shots recorded their source
* pack.
* @returns {string} * @returns {string}
*/ */
export function displayShotTitle(title, shot = undefined, scene = undefined) { export function displayShotTitle(title, shot = undefined, scene = undefined) {
const text = typeof title === 'string' ? title : ''; const text = typeof title === 'string' ? title : '';
const fromRecipe = shot const fromRecipe = shot
? Boolean(shot.sourcePackId) || recipeDeclaresShotTitle(scene?.id, text) ? Boolean(shot.sourcePackId) || recipeDeclaresShotTitle(scene, shot, text)
: true; : true;
if (fromRecipe && BUILTIN_SHOT_TITLES.has(text)) return t(text); if (fromRecipe && BUILTIN_SHOT_TITLES.has(text)) return t(text);
const numbered = /^Shot ([1-9]\d*)$/.exec(text); const numbered = /^Shot ([1-9]\d*)$/.exec(text);
return numbered ? t('Shot {n}', { n: numbered[1] }) : text; return numbered ? t('Shot {n}', { n: numbered[1] }) : text;
} }
/** Whether the built-in recipe behind a scene declares a shot with this title. */ /** Whether the built-in recipe behind a scene declares this shot's title. */
function recipeDeclaresShotTitle(sceneId, title) { function recipeDeclaresShotTitle(scene, shot, title) {
const recipe = sceneId ? getSceneRecipeById(sceneId) : null; const recipe = scene?.id ? getSceneRecipeById(scene.id) : null;
return Boolean( if (!recipe) return false;
recipe?.cameraPath?.some((keyframe) => keyframe?.title === title), const declares = (path) =>
Array.isArray(path) && path.some((keyframe) => keyframe?.title === title);
if (declares(recipe.cameraPath)) return true;
// A bootstrap recipe seeds legacy "Shot N" keyframes; the upgrade replaces
// them with the append recipe's bootstrap shots, which carry no source pack.
// The pack marker binds each of those titles to its shot id, so a shot the
// operator later gave the same title stays as typed.
return SCENE_APPEND_RECIPES.some(
(append) =>
append.legacySceneBootstrap?.targetSceneTitle === recipe.title &&
declares(append.legacySceneBootstrap.cameraPath) &&
Boolean(shot?.id) &&
(Array.isArray(scene.appliedShotPacks)
? scene.appliedShotPacks
: []
).some(
(marker) =>
marker?.id === append.id && marker.shotBindings?.[title] === shot.id,
),
); );
} }
+38
View File
@@ -4,6 +4,7 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { import {
SCENE_APPEND_RECIPES,
SCENE_RECIPES, SCENE_RECIPES,
displaySceneTitle, displaySceneTitle,
displayShotTitle, displayShotTitle,
@@ -141,3 +142,40 @@ test('the scene context of a shot carries its display title by provenance', asyn
title, title,
); );
}); });
test('bootstrap shots of a built-in scene localize without a source pack', () => {
setLocale('ru');
const append = SCENE_APPEND_RECIPES.find(
(recipe) => recipe.legacySceneBootstrap?.cameraPath?.length,
);
if (!append) return;
const bootstrap = append.legacySceneBootstrap;
const scene = SCENE_RECIPES.find(
(recipe) => recipe.title === bootstrap.targetSceneTitle,
);
if (!scene) return;
for (const { title } of bootstrap.cameraPath) {
// The upgrade copies these keyframes into the scene without sourcePackId
// and binds each title to its shot in the pack marker.
const upgraded = {
id: scene.id,
appliedShotPacks: [
{ id: append.id, version: 1, shotBindings: { [title]: 'shot-old' } },
],
};
assert.equal(
displayShotTitle(title, { id: 'shot-old', title }, upgraded),
displayShotTitle(title),
);
// Another shot the operator gave the same title stays as typed.
assert.equal(
displayShotTitle(title, { id: 'shot-user', title }, upgraded),
title,
);
// The same title in an operator's own scene stays as typed.
assert.equal(
displayShotTitle(title, { id: 'shot-user', title }, { id: 'scene-user' }),
title,
);
}
});
+6 -5
View File
@@ -40,7 +40,7 @@ test('the current language comes first, the rest by their own names', () => {
nativeNames: LOCALE_NAMES, nativeNames: LOCALE_NAMES,
}), }),
), ),
['ru', 'en', 'es', 'zh-CN', 'zh-Hant'], ['ru', 'uk', 'en', 'es', 'it', 'zh-CN', 'zh-Hant'],
); );
assert.deepEqual( assert.deepEqual(
codes( codes(
@@ -50,7 +50,7 @@ test('the current language comes first, the rest by their own names', () => {
nativeNames: LOCALE_NAMES, nativeNames: LOCALE_NAMES,
}), }),
), ),
['es', 'en', 'ru', 'zh-CN', 'zh-Hant'], ['es', 'en', 'it', 'ru', 'uk', 'zh-CN', 'zh-Hant'],
); );
assert.deepEqual( assert.deepEqual(
codes( codes(
@@ -60,7 +60,7 @@ test('the current language comes first, the rest by their own names', () => {
nativeNames: LOCALE_NAMES, nativeNames: LOCALE_NAMES,
}), }),
), ),
['en', 'es', 'ru', 'zh-CN', 'zh-Hant'], ['en', 'es', 'it', 'ru', 'uk', 'zh-CN', 'zh-Hant'],
); );
const rows = buildLocaleOptions({ const rows = buildLocaleOptions({
locales: MANY, locales: MANY,
@@ -261,7 +261,8 @@ test('keyboard model: open on the current language, move, choose, close', () =>
model.openList(); model.openList();
assert.equal(model.activeOption.code, 'ru'); assert.equal(model.activeOption.code, 'ru');
assert.deepEqual(model.keydown({ key: 'ArrowDown' }), { handled: true }); assert.deepEqual(model.keydown({ key: 'ArrowDown' }), { handled: true });
assert.equal(model.activeOption.code, 'en'); // Russian collation puts the other Cyrillic name next.
assert.equal(model.activeOption.code, 'uk');
model.keydown({ key: 'End' }); model.keydown({ key: 'End' });
assert.equal(model.activeOption.code, 'zh-Hant'); assert.equal(model.activeOption.code, 'zh-Hant');
model.keydown({ key: 'Home' }); model.keydown({ key: 'Home' });
@@ -275,7 +276,7 @@ test('keyboard model: open on the current language, move, choose, close', () =>
model.keydown({ key: 'ArrowDown' }); model.keydown({ key: 'ArrowDown' });
assert.deepEqual(model.keydown({ key: ' ' }), { assert.deepEqual(model.keydown({ key: ' ' }), {
handled: true, handled: true,
choose: 'en', choose: 'uk',
}); });
assert.deepEqual(model.keydown({ key: 'Escape' }), { assert.deepEqual(model.keydown({ key: 'Escape' }), {
handled: true, handled: true,
+2 -1
View File
@@ -730,7 +730,8 @@ html:not([lang='en']) .cockpit-roll-token.is-separator {
} }
.cockpit-vision-controls { .cockpit-vision-controls {
display: grid; display: grid;
grid-template-columns: 48px 58px 48px; /* The current style grows for a long translated name (ЗВИЧАЙНИЙ). */
grid-template-columns: 48px minmax(58px, max-content) 48px;
align-items: stretch; align-items: stretch;
gap: 2px; gap: 2px;
margin-top: 2px; margin-top: 2px;