Merge pull request #1473 from bilawalsidhu/german/hal-641-it

Italian in the language picker: 2730 strings, Italian plural forms, labels
shortened to fit the panels, and the it style guide. Based on the translation
by @abiyeenzo (#912).

Co-authored-by: Enzo Abyie <[email protected]>
This commit is contained in:
German KhokhlovandEnzo Abyie authored and GitHub committed 2026-10-29 17:38:34 +04:00
commit 15da935b8f
15 files changed
+3433 -15

No files matched your search

+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
@@ -3782,7 +3782,7 @@ test:track` 43 tracking invariants · headless QA harnesses under
### Interface localization (October 2026)
Shipped locales: **en** (source and fallback), **es**, **ru**, **uk**, **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:
[`docs/TRANSLATORS.md`](./TRANSLATORS.md).
+2 -2
View File
@@ -1,6 +1,6 @@
# Translating God's Eye View
Shipped languages: **English** (source and fallback), **Spanish** (`es`), **Russian** (`ru`), **Ukrainian** (`uk`), **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
@@ -63,4 +63,4 @@ These are contracts, not copy: layer ids and registry keys, status enums and oth
## 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), [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.
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 … |
+43
View File
@@ -440,6 +440,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -481,6 +482,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -512,6 +514,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -535,6 +538,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -553,6 +557,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -574,6 +579,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -602,6 +608,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -638,6 +645,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -656,6 +664,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -816,6 +825,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -939,6 +949,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1008,6 +1019,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1058,6 +1070,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1122,6 +1135,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1153,6 +1167,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1185,6 +1200,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1217,6 +1233,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1252,6 +1269,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1296,6 +1314,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1334,6 +1353,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1386,6 +1406,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1417,6 +1438,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1467,6 +1489,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1532,6 +1555,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1563,6 +1587,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1605,6 +1630,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1629,6 +1655,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1687,6 +1714,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -1751,6 +1779,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -2636,6 +2665,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -3222,6 +3252,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -3334,6 +3365,7 @@
"src/data/cameraArrival.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -3359,6 +3391,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -3397,6 +3430,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -3425,6 +3459,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -3518,6 +3553,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -3575,6 +3611,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -3831,6 +3868,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -3876,6 +3914,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -3931,6 +3970,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -3949,6 +3989,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -4010,6 +4051,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
@@ -4208,6 +4250,7 @@
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
+2
View File
@@ -37,6 +37,8 @@ test('normalizeLocale folds regional variants and rejects unshipped tags', () =>
assert.equal(normalizeLocale('uk-UA'), 'uk');
assert.equal(normalizeLocale('en_GB'), 'en');
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.
assert.equal(normalizeLocale('zh'), 'zh-CN');
assert.equal(normalizeLocale('zh-CN'), 'zh-CN');
+2
View File
@@ -17,6 +17,7 @@ import {
writeStoredLocale,
} from './locale.js';
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 uk from './locales/uk.json' with { type: 'json' };
import zhCN from './locales/zh-CN.json' with { type: 'json' };
@@ -24,6 +25,7 @@ import zhHant from './locales/zh-Hant.json' with { type: 'json' };
const CATALOGS = Object.freeze({
es,
it,
ru,
uk,
'zh-CN': zhCN,
+2
View File
@@ -11,6 +11,7 @@ export const DEFAULT_LOCALE = 'en';
export const LOCALES = Object.freeze([
'en',
'es',
'it',
'ru',
'uk',
'zh-CN',
@@ -21,6 +22,7 @@ export const LOCALES = Object.freeze([
export const LOCALE_NAMES = Object.freeze({
en: 'English',
es: 'Español',
it: 'Italiano',
ru: 'Русский',
uk: 'Українська',
'zh-CN': '中文(简体)',
File diff suppressed because it is too large. Load diff
+169
View File
@@ -124,6 +124,175 @@
"ΔN",
"▲ {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": [
"AIS: --",
"AIS: {name}",
@@ -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.equal(scene.appliedShotPacks[0].id, 'bhote-koshi-nepal-evidence-pack');
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._selectedShotId, scene.shots[0].id);
} 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
* typed. Without `shot`, the title alone decides.
* @param {string} title
* @param {{sourcePackId?: string}} [shot]
* @param {{id?: string}} [scene] The shot's scene, for projects saved before
* shots recorded their source pack.
* @param {{id?: string, sourcePackId?: string}} [shot]
* @param {{id?: string, appliedShotPacks?: Array<{id?: string, shotBindings?: Object<string, string>}>}} [scene]
* The shot's scene, for projects saved before shots recorded their source
* pack.
* @returns {string}
*/
export function displayShotTitle(title, shot = undefined, scene = undefined) {
const text = typeof title === 'string' ? title : '';
const fromRecipe = shot
? Boolean(shot.sourcePackId) || recipeDeclaresShotTitle(scene?.id, text)
? Boolean(shot.sourcePackId) || recipeDeclaresShotTitle(scene, shot, text)
: true;
if (fromRecipe && BUILTIN_SHOT_TITLES.has(text)) return t(text);
const numbered = /^Shot ([1-9]\d*)$/.exec(text);
return numbered ? t('Shot {n}', { n: numbered[1] }) : text;
}
/** Whether the built-in recipe behind a scene declares a shot with this title. */
function recipeDeclaresShotTitle(sceneId, title) {
const recipe = sceneId ? getSceneRecipeById(sceneId) : null;
return Boolean(
recipe?.cameraPath?.some((keyframe) => keyframe?.title === title),
/** Whether the built-in recipe behind a scene declares this shot's title. */
function recipeDeclaresShotTitle(scene, shot, title) {
const recipe = scene?.id ? getSceneRecipeById(scene.id) : null;
if (!recipe) return false;
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 assert from 'node:assert/strict';
import {
SCENE_APPEND_RECIPES,
SCENE_RECIPES,
displaySceneTitle,
displayShotTitle,
@@ -141,3 +142,40 @@ test('the scene context of a shot carries its display title by provenance', asyn
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,
);
}
});
+3 -3
View File
@@ -40,7 +40,7 @@ test('the current language comes first, the rest by their own names', () => {
nativeNames: LOCALE_NAMES,
}),
),
['ru', 'uk', 'en', 'es', 'zh-CN', 'zh-Hant'],
['ru', 'uk', 'en', 'es', 'it', 'zh-CN', 'zh-Hant'],
);
assert.deepEqual(
codes(
@@ -50,7 +50,7 @@ test('the current language comes first, the rest by their own names', () => {
nativeNames: LOCALE_NAMES,
}),
),
['es', 'en', 'ru', 'uk', 'zh-CN', 'zh-Hant'],
['es', 'en', 'it', 'ru', 'uk', 'zh-CN', 'zh-Hant'],
);
assert.deepEqual(
codes(
@@ -60,7 +60,7 @@ test('the current language comes first, the rest by their own names', () => {
nativeNames: LOCALE_NAMES,
}),
),
['en', 'es', 'ru', 'uk', 'zh-CN', 'zh-Hant'],
['en', 'es', 'it', 'ru', 'uk', 'zh-CN', 'zh-Hant'],
);
const rows = buildLocaleOptions({
locales: MANY,