i18n(de): complete and ship German

- Translate the remaining 1,868 strings in a neutral infinitive style (no
  Sie/du), with German compounds, decimal commas and CLDR one/other plurals.
- Review the #908 entries: knots are kn everywhere, the avionics codes BRG,
  CRS and DEST stay English like HDG and SPD, expand/collapse is
  ausklappen/einklappen, the radio toggle reads EIN/AUS, density SPARSE is
  DÜNN, the Thermal style is Wärmebild, German quotes in the shot prompt.
- Register de: catalog, LOCALES, LOCALE_NAMES (Deutsch), package boundaries,
  same-as-english (codes, product names, music genres, units).
- Tests: picker order and keyboard model with Deutsch, de/de-AT/de_CH
  normalization, browser de-DE resolves to German.
- docs/translations/de.md: style, plurals, units and glossary; TRANSLATORS.md
  and CURRENT-STATE.md list German.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-28 12:53:49 +04:00
1 parent 7d7c535dc7
commit dfa7ac98a7
10 files changed
+2409 -30

No files matched your search

+1 -1
View File
@@ -3733,7 +3733,7 @@ test:track` 43 tracking invariants · headless QA harnesses under
### 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), **de**, **es**, **ru**, **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`), **Simplified Chinese** (`zh-CN`) and **Traditional Chinese** (`zh-Hant`).
Shipped languages: **English** (source and fallback), **German** (`de`), **Spanish** (`es`), **Russian** (`ru`), **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), [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`: [German](./translations/de.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.
+90
View File
@@ -0,0 +1,90 @@
# German (`de`)
Catalog: `src/i18n/locales/de.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
Translation source: abiyeenzo (#908).
## Style
- Standard German that reads naturally in Germany, Austria and Switzerland; ß where it belongs (Straße, schließen), no Swiss ss.
- No direct address: neither «Sie» nor «du». Actions and hints use the neutral infinitive: «Erneut versuchen», «Kamera anklicken zum Aktivieren», «Zuerst eine Aufnahme wählen», «Mikrofonzugriff erlauben und erneut versuchen». "Your" becomes the article or «eigene»: «Eigener RTL-SDR-Empfänger», «Die IP-Adresse ist für diesen Sender sichtbar».
- Nouns are capitalized, as German requires; otherwise sentence case, not Title Case: "Live Flights" → «Live-Flüge», "Provider Settings" → «Anbieter-Einstellungen». UPPERCASE English stays UPPERCASE: «LIVE-DATEN AUS», «KEINE ENTWARNUNG». In UPPERCASE, ß is written SS: «SCHLIESSEN», «STRASSENEBENE».
- Compounds are written as one word or with a hyphen next to a name, code or English term: «Datenebene», «Wetterkacheln», «TomTom-Schlüssel», «AIS-Schiffe», «Street-View-Fallback». Do not split them with spaces.
- Short labels, chips and HUD readouts stay short. German runs about a third longer than English, so prefer a short word or a standard abbreviation with a period over a wrapped line: REICHW., GESCHW., NIEDERSCHL., SONST., NACHR., VERST., VGL., KAL.
- The avionics codes HDG, BRG, CRS, DEST, SPD, ALT, GS, TRK, V/S, FOV and SQUAWK stay in English, as in the cockpit. Words used in a sentence are translated: heading → «Kurs», bearing → «Peilung», range → «Reichweite».
- A dash between clauses is a spaced en dash « – »; a colon is fine where it explains: «Feed ausgefallen – langsame Wiederholung», «SIMULIERT: Verkehrsdienst nicht erreichbar». Quotes are „…“.
- Keyboard keys are named as on German keyboards where they differ: Leertaste, Rücktaste, Umschalt; Esc, Enter, Tab and letter keys stay.
- Decimal comma in fixed text: «M4,4», «28,2710° N». Thousands separator is a period: «1.000 km». Formatted numbers come from `Intl` and are already German.
- Units: km, m, km/h, m/s, kn (knots, also in the cockpit), ft (feet), hPa, MW, h, min, s, T (days, in short readouts); in UPPERCASE text KM, KM/H, KN, FT, MIN. A space between number and unit: «{value} m», «{count} min», «50 %». `°` sticks to the number.
- "ago" goes before the time: «vor {count} min», «vor {value}». When a placeholder already holds a localized "ago" text (`{age}` in `STALE · cached {age}`, `{ago}`), do not add «vor» again.
- Compass points use O for Ost: N, NNO, NO, ONO, O, OSO, SO, SSO, S, SSW, SW, WSW, W, WNW, NW, NNW; hemisphere letters N, S, O, W.
- Routing: direction words are lowercase adverbs (links, leicht rechts, scharf links, geradeaus), so a sentence never starts with one: «Auf {road} {direction} abbiegen», «Dann {direction} abbiegen», «Weiter {direction}». Roundabout exits are ordinals with a period: «die {exit} Ausfahrt» → «die 2. Ausfahrt».
## Plurals
A `tn()` entry needs two forms. `Intl.PluralRules('de')` picks:
| Form | Counts | Example |
| ------- | --------------------------- | --------- |
| `one` | 1 | 1 Kamera |
| `other` | everything else: 0, 2, 1,5… | 3 Kameras |
Keep the verb in agreement: «{count} Station meldet» / «{count} Stationen melden». Abbreviated units and codes do not change: «{count} NACHR.», «{count} EMPFANGEN».
## Glossary
| English | Deutsch |
| ---------------------------------------------- | -------------------------------------------------------------------- |
| layer / data layer | Ebene / Datenebene |
| feed / source / data source | Feed / Quelle / Datenquelle |
| LIVE (status) / live data | LIVE / Live-Daten |
| SIMULATED | SIMULIERT |
| stale / degraded / unavailable | veraltet / eingeschränkt / nicht verfügbar |
| retry / retrying | neuer Versuch / erneut versuchen |
| fallback | Fallback |
| key (API key) / POWER UP / POWERED UP | Schlüssel / STARTKLAR / HOCHGEFAHREN |
| Provider Settings | Anbieter-Einstellungen |
| ON / OFF (state chips) | EIN / AUS |
| enable / disable | aktivieren / deaktivieren |
| globe / map / map source / map stack / basemap | Globus / Karte / Kartenquelle / Kartenstapel / Basiskarte |
| aircraft / flight / military flights | Luftfahrzeug (HUD and short labels: Flugzeug) / Flug / Militärflüge |
| vessel, ship | Schiff |
| satellite / shell (Starlink) / DENSE | Satellit / Schale / DICHT |
| launch (rocket) / launch site / space mission | Start / Startplatz / Raummission |
| ascent / replay / payload / stage / downrange | Aufstieg / Replay, Wiedergabe / Nutzlast / Stufe / Downrange |
| contact / tracking | Kontakt / Verfolgung |
| installation / site | Anlage / Standort |
| mapped / observed | kartiert / beobachtet |
| camera / CCTV camera / camera badge | Kamera / CCTV-Kamera / Kamera-Markierung |
| earthquake / active fire / fire perimeter | Erdbeben / aktiver Brand / Brandgrenze |
| cyclone / advisory (NHC/CPHC) / cone | Wirbelsturm / Warnung / Kegel |
| radio station | Sender |
| broadcast FM | UKW |
| road / flow (traffic) | Straße / Verkehrsfluss |
| transit / bike share | ÖPNV / Bikesharing |
| scene / shot / scene recipe | Szene / Aufnahme / Szenenrezept |
| capture shot / camera move | Aufnahme speichern / Kameraflug |
| anchor / data pack / bundle | Anker / Datenpaket / Bundle |
| story beat / evidence beat | Story-Schritt / Beweisschritt |
| briefing / panel / dock | Briefing / Bereich / Dock |
| cockpit | Cockpit |
| preset / visual preset | Preset / visuelles Preset |
| Scope (circular viewport mask) / keyhole | Scope / Schlüsselloch |
| feather / fade | Weichzeichnung / Ausblenden |
| overlay / detection overlay | Overlay / Erkennungs-Overlay |
| Clean UI | Reine Ansicht |
| Night vision / Thermal / Snow (cockpit styles) | Nachtsicht / Wärmebild / Schnee |
| heading / altitude / ground speed | Kurs / Höhe / Geschwindigkeit über Grund (short: GESCHW. ÜBER GRUND) |
| HDG / BRG / CRS / DEST / SPD / FOV | unchanged |
| RANGE / PITCH / HGT (CCTV calibration chips) | REICHW. / NEIGUNG / HÖHE |
| PREV / NEXT | ZURÜCK / WEITER |
| L / R (relative bearing) | L / R |
| SIG / NEWS / LOCAL (briefing tabs) | SIG / NEWS / LOKAL |
| NOT AN ALL-CLEAR | KEINE ENTWARNUNG |
| Street Level / Street View (Google) | Straßenebene / Street View |
| Recent Imagery / box / pin (a day) | Aktuelle Bilddaten / Bereich / fixieren |
| frame (bring into view) | ins Bild nehmen |
| mark (map annotation) / outline | Markierung / Umriss |
| voice (control) / voice session | Sprachsteuerung / Sprachsitzung |
| YOU / THIS / HERE (voice card labels) | EINGABE / DIES / HIER |
| click / click to … | anklicken / … klicken |
+43
View File
@@ -439,6 +439,7 @@
"src/ui/slider.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -479,6 +480,7 @@
"src/localRequestGate.mjs",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -509,6 +511,7 @@
"src/data/layerSnapshot.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -531,6 +534,7 @@
"src/app/stateChannel.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -548,6 +552,7 @@
"src/ui/radioTunerModel.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -568,6 +573,7 @@
"src/ui/cctvPresentation.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -595,6 +601,7 @@
"src/ui/panelRailGeometry.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -630,6 +637,7 @@
"src/ui/panelRailGeometry.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -647,6 +655,7 @@
"src/ui/actionButton.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -806,6 +815,7 @@
"src/data/cameraArrival.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -928,6 +938,7 @@
"src/data/cameraArrival.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -996,6 +1007,7 @@
"src/data/modelEligibility.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1045,6 +1057,7 @@
"src/data/modelEligibility.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1108,6 +1121,7 @@
"src/data/inputOwnership.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1138,6 +1152,7 @@
"src/maps/imageryHost.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1169,6 +1184,7 @@
"src/data/inputOwnership.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1200,6 +1216,7 @@
"src/data/inputOwnership.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1234,6 +1251,7 @@
"src/data/inputOwnership.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1277,6 +1295,7 @@
"src/data/infrastructureOverlayEntry.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1314,6 +1333,7 @@
"src/ui/panelRailGeometry.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1365,6 +1385,7 @@
"src/layers/traffic/roadModes.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1395,6 +1416,7 @@
"src/data/cameraSensitivity.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1444,6 +1466,7 @@
"src/layers/cctv/sourcePolicy.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1508,6 +1531,7 @@
"scripts/qa-street-level.mjs",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1538,6 +1562,7 @@
"src/layers/alpr/floors.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1579,6 +1604,7 @@
"src/data/inputOwnership.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1602,6 +1628,7 @@
"src/data/inputOwnership.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1659,6 +1686,7 @@
"src/maps/imageryHost.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -1722,6 +1750,7 @@
"src/voice/pointerReticle.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -2605,6 +2634,7 @@
"src/app/embed.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -3190,6 +3220,7 @@
"src/sources/infrastructureData.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -3301,6 +3332,7 @@
"src/app/stateChannel.js",
"src/data/cameraArrival.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -3325,6 +3357,7 @@
"src/ui/uiLifetime.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -3362,6 +3395,7 @@
"src/i18n/marker.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -3389,6 +3423,7 @@
"src/ui/uiLifetime.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -3481,6 +3516,7 @@
"src/data/routeSteps.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -3537,6 +3573,7 @@
"src/voice/pointerReticle.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -3792,6 +3829,7 @@
"src/sources/capability.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -3836,6 +3874,7 @@
"src/sources/transitHistory.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -3890,6 +3929,7 @@
"src/layers/perimeters/inciweb.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -3907,6 +3947,7 @@
"src/layers/gnss/records.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -3967,6 +4008,7 @@
"src/i18n/marker.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
@@ -4164,6 +4206,7 @@
"src/i18n/marker.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/zh-CN.json",
+13 -2
View File
@@ -34,6 +34,9 @@ test('normalizeLocale folds regional variants and rejects unshipped tags', () =>
assert.equal(normalizeLocale('ru-RU'), 'ru');
assert.equal(normalizeLocale(' RU '), 'ru');
assert.equal(normalizeLocale('en_GB'), 'en');
assert.equal(normalizeLocale('de'), 'de');
assert.equal(normalizeLocale('de-AT'), 'de');
assert.equal(normalizeLocale('de_CH'), 'de');
assert.equal(normalizeLocale('xx'), null);
// Explicit script tags take precedence over regional defaults.
assert.equal(normalizeLocale('zh'), 'zh-CN');
@@ -68,7 +71,7 @@ test('resolveLocale: ?lang= wins, then the stored choice, then browser languages
resolveLocale({
location: { search: '?lang=xx' },
storage: memoryStorage(),
languages: ['de-DE', 'ru-RU'],
languages: ['ko-KR', 'ru-RU'],
}),
'ru',
);
@@ -76,7 +79,15 @@ test('resolveLocale: ?lang= wins, then the stored choice, then browser languages
resolveLocale({
location: { search: '' },
storage: memoryStorage(),
languages: ['de'],
languages: ['de-DE', 'ru-RU'],
}),
'de',
);
assert.equal(
resolveLocale({
location: { search: '' },
storage: memoryStorage(),
languages: ['ko'],
}),
'en',
);
+8 -1
View File
@@ -16,12 +16,19 @@ import {
normalizeLocale,
writeStoredLocale,
} from './locale.js';
import de from './locales/de.json' with { type: 'json' };
import es from './locales/es.json' with { type: 'json' };
import ru from './locales/ru.json' with { type: 'json' };
import zhCN from './locales/zh-CN.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({
de,
es,
ru,
'zh-CN': zhCN,
'zh-Hant': zhHant,
});
let currentLocale = DEFAULT_LOCALE;
+10 -1
View File
@@ -8,11 +8,19 @@ export const LOCALE_STORAGE_KEY = 'gev:locale:v1';
export const DEFAULT_LOCALE = 'en';
/** 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',
'de',
'es',
'ru',
'zh-CN',
'zh-Hant',
]);
/** Each locale named in its own language, for the selector. */
export const LOCALE_NAMES = Object.freeze({
en: 'English',
de: 'Deutsch',
es: 'Español',
ru: 'Русский',
'zh-CN': '中文(简体)',
@@ -24,6 +32,7 @@ const RTL_LOCALES = Object.freeze([]);
/**
* Map any language tag to a shipped locale: 'ru-RU' and 'RU' → 'ru',
* 'de-AT' and 'de_CH' → 'de',
* 'zh', 'zh-CN' and 'zh-Hans-SG' → 'zh-CN';
* 'zh-Hant', 'zh-TW', 'zh-HK' and 'zh-MO' → 'zh-Hant'.
* @returns {string|null}
+2072 -18
View File
File diff suppressed because it is too large. Load diff
+164
View File
@@ -1,4 +1,168 @@
{
"de": [
"1090 MHz",
"978 MHz UAT",
"AIS: --",
"AIS: {name}",
"ANIME",
"AUTO",
"Alternative",
"Ambient",
"Anime",
"Austin",
"BRG {value}°",
"BRG —",
"Bloom",
"Blues",
"Bus",
"CCTV",
"COCKPIT",
"CRS {value}°",
"CRT",
"Country",
"Cyan",
"Cyber",
"DEST ---°",
"DEST {bearing}",
"Dance",
"Details",
"Dubai",
"FALLBACK",
"FILTER",
"FLIR",
"FOV",
"FOV {value}°",
"FT",
"Folk",
"Funk",
"GEO",
"GOOGLE STREET VIEW",
"Global",
"Global · 60°S–60°N",
"Google Street View",
"HDG",
"HDG {value}°",
"Hip-Hop",
"House",
"Hybrid",
"Indie",
"Ironbow",
"Jazz",
"LIVE",
"Landsat 8/9",
"Latin",
"Layout",
"London",
"Los Angeles",
"MIL",
"MINI",
"MISSION",
"MM",
"MONITOR",
"Metal",
"Minimal",
"NAME",
"NASA FIRMS · LIVE",
"NAV",
"NEWS",
"NNW",
"NOIR",
"NORMAL",
"NVG",
"NW",
"New York",
"Noir",
"Normal",
"OK",
"ORBIT",
"ORBIT · ",
"Oldies",
"OpenStreetMap + optional Google Maps Places",
"Operator",
"PAUSE",
"PLAY",
"POS: LIVE",
"POS: {time}Z",
"POSITION {age}",
"PROJ MONITOR",
"Paris",
"Pause",
"Pin",
"Plan",
"Pop",
"Punk",
"RADIO",
"REC",
"REGION",
"RESET",
"Radio",
"Reggae",
"Rock",
"S2 GhostMaps",
"SAR",
"SIG",
"SNAPSHOT",
"SQUAWK {code} · {status}",
"SSW",
"STANDARD",
"START",
"STATION",
"STATUS",
"STATUS · ",
"STD",
"SW",
"SYNC",
"San Francisco",
"Scanlines",
"Scope",
"Sentinel-2",
"Sonar",
"Soul",
"Station",
"Station {id}",
"T minus {seconds}",
"TAGS · {tags}",
"TANKER",
"TEMP",
"Talk",
"Tallinn",
"Techno",
"Trance",
"Turboprop",
"UPSTREAM",
"Upstream HTTP {status}",
"Vignette",
"WHOT/BHOT",
"WIND",
"WNW",
"WSW",
"Washington DC",
"Wind",
"dB",
"feed status|live",
"fire behavior|Minimal",
"ft",
"hPa",
"km",
"km/h",
"min",
"mph",
"radio|NEWS",
"radio|TALK",
"view band|GLOBAL",
"{city} CCTV",
"{county} County",
"{source} HTTP {status}",
"{speed} kn",
"{type} SPD: {speed} HDG: {heading}",
"{value} KM",
"{value} KM DOWNRANGE",
"{value} KM/H",
"{value} KM/S",
"ΔN",
"Ⅱ PAUSE",
"▲ {frp} MW"
],
"es": [
"1090 MHz",
"978 MHz UAT",
+6 -5
View File
@@ -40,7 +40,7 @@ test('the current language comes first, the rest by their own names', () => {
nativeNames: LOCALE_NAMES,
}),
),
['ru', 'en', 'es', 'zh-CN', 'zh-Hant'],
['ru', 'de', 'en', 'es', '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', 'zh-CN', 'zh-Hant'],
['es', 'de', 'en', 'ru', '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', 'zh-CN', 'zh-Hant'],
['en', 'de', 'es', 'ru', 'zh-CN', 'zh-Hant'],
);
const rows = buildLocaleOptions({
locales: MANY,
@@ -95,6 +95,7 @@ test('each row gets the interface-language name for search, unless it only repea
const byCode = Object.fromEntries(rows.map((row) => [row.code, row]));
assert.equal(byCode.en.localName, 'английский');
assert.equal(byCode.es.localName, 'испанский');
assert.equal(byCode.de.localName, 'немецкий');
assert.equal(byCode.ru.localName, '', 'русский only repeats Русский');
const english = buildLocaleOptions({
locales: LOCALES,
@@ -261,7 +262,7 @@ test('keyboard model: open on the current language, move, choose, close', () =>
model.openList();
assert.equal(model.activeOption.code, 'ru');
assert.deepEqual(model.keydown({ key: 'ArrowDown' }), { handled: true });
assert.equal(model.activeOption.code, 'en');
assert.equal(model.activeOption.code, 'de');
model.keydown({ key: 'End' });
assert.equal(model.activeOption.code, 'zh-Hant');
model.keydown({ key: 'Home' });
@@ -275,7 +276,7 @@ test('keyboard model: open on the current language, move, choose, close', () =>
model.keydown({ key: 'ArrowDown' });
assert.deepEqual(model.keydown({ key: ' ' }), {
handled: true,
choose: 'en',
choose: 'de',
});
assert.deepEqual(model.keydown({ key: 'Escape' }), {
handled: true,