i18n(he): complete and ship Hebrew

- Add the full catalog (2940 entries keyed by the English text; 75 plural
  entries carry one/two/other per Intl.PluralRules('he')), reviewed for
  meaning, terms, separators and label length.
- Register he: he-IL and he map to it; it joins RTL_LOCALES so <html
  dir=rtl> mirrors the chrome through logical properties and rtl.css.
- rtl.css: Noto Sans Hebrew (loaded with the other Google fonts) leads the
  mono and sans stacks, tracking released like Arabic, HUDs keep JetBrains
  Mono for Latin telemetry.
- same-as-english: instrument codes and HUD readouts stay as printed
  (HDG/BRG/CRS/FOV/CRT/NVG/FLIR/CAL/SRC, MW units, Sentinel-2, km/h...).
- docs/translations/he.md: style, plurals and glossary.
- docs: TRANSLATORS.md and CURRENT-STATE.md updated; normalizeLocale test
  covers he-IL.
This commit is contained in:
Meir Michanie committed 2026-11-09 14:41:36 -06:00
1 parent e5da073d33
commit 584732c3ec
12 files changed
+3509 -15

No files matched your search

+14 -10
View File
@@ -4589,7 +4589,9 @@ test:track` 43 tracking invariants · headless QA harnesses under
### Interface localization (October 2026) ### Interface localization (October 2026)
Shipped locales: **en** (source and fallback), **ar**, **de**, **es**, **fr**, **it**, **ja**, **pt** (Brazilian Portuguese), **ru**, **uk**, **zh-CN** and **zh-Hant**. The code lives in Shipped locales: **en** (source and fallback), **ar**, **de**, **es**, **fr**, **he**,
**it**, **ja**, **pt** (Brazilian Portuguese), **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).
@@ -4603,19 +4605,21 @@ Shipped locales: **en** (source and fallback), **ar**, **de**, **es**, **fr**, *
`navigator.languages` → English. Any `pt` tag (`pt-BR`, `pt-PT`) selects `pt`. Chinese tags default to `zh-CN`; Traditional Chinese tags `navigator.languages` → English. Any `pt` tag (`pt-BR`, `pt-PT`) selects `pt`. Chinese tags default to `zh-CN`; Traditional Chinese tags
select `zh-Hant`. Explicit `Hans` selects `zh-CN` regardless of region. `?lang=` is not stored, and share links drop select `zh-Hant`. Explicit `Hans` selects `zh-CN` regardless of region. `?lang=` is not stored, and share links drop
it, so a recipient keeps their own language. The globe picker in the it, so a recipient keeps their own language. The globe picker in the
bottom-right corner (bottom-left in Arabic) (`src/ui/localePicker.js`) stores the choice, drops bottom-right corner (bottom-left in Arabic and Hebrew)
(`src/ui/localePicker.js`) stores the choice, drops
`?lang=` (keeps it only when storage is blocked) and switches in place: `?lang=` (keeps it only when storage is blocked) and switches in place:
views re-render, code that published translated text words it again through views re-render, code that published translated text words it again through
`onLocaleChange`, and `<html lang>`/`dir` and the page's `data-i18n` markup `onLocaleChange`, and `<html lang>`/`dir` and the page's `data-i18n` markup
follow. Nothing is lost; a toast already on screen keeps its words. follow. Nothing is lost; a toast already on screen keeps its words.
- **Right to left.** Arabic (`ar`) is in `RTL_LOCALES`, so `<html dir="rtl">`: - **Right to left.** Arabic (`ar`) and Hebrew (`he`) are in `RTL_LOCALES`, so
the title, style indicator, both panel stacks and their icon rails swap `<html dir="rtl">`: the title, style indicator, both panel stacks and their
sides through logical properties (`inset-inline-*`, `margin-inline-*`), the icon rails swap sides through logical properties (`inset-inline-*`,
dock and corner row reverse their order, and `src/ui/styles/rtl.css` holds `margin-inline-*`), the dock and corner row reverse their order, and
the rest: no letter-spacing on Arabic text, Noto Sans Arabic in the font `src/ui/styles/rtl.css` holds the rest: no letter-spacing on Arabic and
stacks, the intel HUD kept as a left-to-right instrument overlay, FM seek Hebrew text, Noto Sans Arabic and Noto Sans Hebrew in the font stacks, the
and the radio dial kept left to right. Intl formatters get `ar-u-nu-latn`, intel HUD kept as a left-to-right instrument overlay, FM seek and the radio
so numbers and dates keep Western digits. dial kept left to right. Intl formatters get `ar-u-nu-latn`, so numbers and
dates keep Western digits.
- **Gate.** `npm run i18n` (and `src/i18n/catalog.test.mjs` under `npm test`) - **Gate.** `npm run i18n` (and `src/i18n/catalog.test.mjs` under `npm test`)
extracts every message and fails on missing, empty, unused, extracts every message and fails on missing, empty, unused,
placeholder-mismatched, still-English (plural forms included) or placeholder-mismatched, still-English (plural forms included) or
+2 -2
View File
@@ -1,6 +1,6 @@
# Translating God's Eye View # Translating God's Eye View
Shipped languages: **English** (source and fallback), **Arabic** (`ar`), **German** (`de`), **Spanish** (`es`), **French** (`fr`), **Italian** (`it`), **Japanese** (`ja`), **Brazilian Portuguese** (`pt`), **Russian** (`ru`), **Ukrainian** (`uk`), **Simplified Chinese** (`zh-CN`) and **Traditional Chinese** (`zh-Hant`). Shipped languages: **English** (source and fallback), **Arabic** (`ar`), **German** (`de`), **Spanish** (`es`), **French** (`fr`), **Hebrew** (`he`), **Italian** (`it`), **Japanese** (`ja`), **Brazilian Portuguese** (`pt`), **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`: [Arabic](./translations/ar.md), [German](./translations/de.md), [Spanish](./translations/es.md), [French](./translations/fr.md), [Italian](./translations/it.md), [Japanese](./translations/ja.md), [Brazilian Portuguese](./translations/pt.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`: [Arabic](./translations/ar.md), [German](./translations/de.md), [Spanish](./translations/es.md), [French](./translations/fr.md), [Hebrew](./translations/he.md), [Italian](./translations/it.md), [Japanese](./translations/ja.md), [Brazilian Portuguese](./translations/pt.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.
+65
View File
@@ -0,0 +1,65 @@
# Hebrew (`he`)
Catalog: `src/i18n/locales/he.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
## Style
- Address the user with a masculine imperative, Google style: הסתר, הצג, נסה שוב, התקרב. Menu items and tooltips take the noun form: הסרת {name}, הרחבת {name}, שינוי הצבע של {name}.
- Sentence case: Hebrew has no case forms, so UPPERCASE English is normal sentence style here; short HUD labels stay short, favor an abbreviation over a wrapped line.
- Keep the `·`, `—`, `→`, `↗`, `…` separators. Latin codes read inside RTL text because the app isolates interpolated values; leave them alone.
- Units follow the catalog: ק"מ, מ׳, קמ"ש, שנ', דק', שע'; instrument chips keep the Latin unit (MW, KM, FT, KTS, kn) and codes (HDG, FOV, CAL, SRC, REC) exactly as in English — see `same-as-english.json`.
## Digits and direction
- Western digits 0–9 only, like every catalog: "5 מטוסים", "26.08.2026". Dates in scene labels use dd.mm.yyyy.
- `{direction}` values are adverbs (שמאלה, ימינה, ישר); maneuver templates are written to place them after the verb: פנה {direction} אל {road}.
- Values inserted by t() are isolated (U+2068…U+2069); do not add manual RTL marks — write the sentence as natural Hebrew and let isolation do the rest.
## Plurals
A `tn()` entry needs three forms under `Intl.PluralRules('he')`:
| Form | Counts | Example |
| ------- | ------------------------- | -------------- |
| `one` | 1 | 1 מצלמה |
| `two` | 2 | 2 מצלמות |
| `other` | 0, 3, 10, 100, fractions… | 5 מצלמות |
With {count} shown as a digit, `two` and `other` share the plural noun (2 מצלמות, 5 מצלמות); `one` takes the singular (1 מצלמה). For constructs that resist agreement with a noun (אין, הכול, זמין), phrase around the number: "{count} תחנות מדווחות" is fine, but prefer "ממתין: {count}" over a mismatched genitive chain.
## Glossary
| English | עברית |
| ---------------------------------------------- | ----------------------------------------------------- |
| layer / data layer | שכבה / שכבת נתונים |
| feed | פיד |
| source / data source / Provider Settings | מקור / מקור נתונים / הגדרות הספקים |
| LIVE / live data / LIVE TRACK | חי / נתונים חיים / מעקב חי |
| SIMULATED | מדומה |
| stale / degraded / unavailable | מיושן / מופחת / לא זמין |
| key (API key) / POWER UP | מפתח / אתחול |
| globe / map / map source / basemap | גלובוס / מפה / מקור מפה / מפת בסיס |
| aircraft / flight / military flight | מטוס (כלי טיס in class lists) / טיסה / טיסה צבאית |
| vessel / ship / ships | אונייה / אוניות |
| satellite | לוויין |
| launch (rocket) / space mission | שיגור / משימת חלל |
| contact / tracking | מגע / עקיבה |
| installation / site | מתקן / אתר |
| camera / CCTV camera / ALPR camera | מצלמה / מצלמת CCTV / מצלמת ALPR |
| earthquake / active fire / cyclone | רעידת אדמה / שרפה פעילה / ציקלון |
| scene / shot | סצנה / שוט |
| briefing / panel / dock | דיווח / פאנל / עגינה |
| visual preset / style | ערכת ראייה / סגנון |
| Scope (circular viewport mask) / keyhole | סקופ / חור המנעול |
| heading (compass °) / course | כיוון / מסלול |
| HDG / BRG / CRS / FOV / DEST / SPD / RANGE | בלי שינוי — קודי מכשיר |
| advisory (NHC/CPHC cyclone) | ייעוץ; Advisory center — מרכז הייעוץ |
| Street View (Google) | Street View, בלי תרגום |
| Street Level / Street Level layer | Street Level, בלי תרגום (שם שכבה) |
| WHOT/BHOT, Ironbow, CRT, NVG, FLIR | בלי שינוי — קודים ושמות מצבים |
| PREV / NEXT | הקודם / הבא |
| anchor / data pack / asset bundle (scene) | עוגן / חבילת נתונים / חבילת נכסים |
| witness / geolocated source | עד ראייה / מקור נתון גיאוגרפית |
| contained (wildland fire) / FRP | הכלה / FRP, בלי שינוי |
Latin product, provider and dataset names stay as printed: Google Maps, Cesium ion, OpenSky, ADS-B, AIS, KartaView, Mapillary, Panoramax, Overpass, InciWeb, iNaturalist, Acartia, NASA FIRMS, NOAA, USGS, CelesTrak, TomTom, Sentinel-2, Landsat, VIIRS, HLS, S2 GhostMaps, TeleGeography.
+1 -1
View File
@@ -21,7 +21,7 @@
<link rel="stylesheet" href="/style.css" /> <link rel="stylesheet" href="/style.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&family=Noto+Sans+Arabic:wght@400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&family=Noto+Sans+Arabic:wght@400;500;600;700&family=Noto+Sans+Hebrew:wght@400;500;600;700&display=swap" rel="stylesheet" />
<!-- Icon font subset to the glyphs this app renders. Keep `icon_names` in sync <!-- Icon font subset to the glyphs this app renders. Keep `icon_names` in sync
with the sources: src/materialSymbolsSubset.test.mjs fails when a source with the sources: src/materialSymbolsSubset.test.mjs fails when a source
names a glyph this list is missing, because a missing glyph does not draw names a glyph this list is missing, because a missing glyph does not draw
+46
View File
@@ -73,6 +73,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -651,6 +652,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -699,6 +701,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -740,6 +743,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -783,6 +787,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -810,6 +815,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -840,6 +846,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -877,6 +884,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -946,6 +954,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -973,6 +982,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -1175,6 +1185,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -1367,6 +1378,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -1453,6 +1465,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -1514,6 +1527,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -1591,6 +1605,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -1628,6 +1643,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -1669,6 +1685,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -1709,6 +1726,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -1757,6 +1775,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -1809,6 +1828,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -1856,6 +1876,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -1919,6 +1940,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -1956,6 +1978,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -2013,6 +2036,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -2084,6 +2108,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -2124,6 +2149,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -2174,6 +2200,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -2204,6 +2231,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -2268,6 +2296,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -2346,6 +2375,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -3295,6 +3325,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -4031,6 +4062,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -4214,6 +4246,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -4247,6 +4280,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -4301,6 +4335,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -4339,6 +4374,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -4445,6 +4481,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -4518,6 +4555,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -4809,6 +4847,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -4864,6 +4903,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -4927,6 +4967,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -4951,6 +4992,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -5021,6 +5063,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -5070,6 +5113,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -5275,6 +5319,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
@@ -5321,6 +5366,7 @@
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
"src/i18n/locales/fr.json", "src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json", "src/i18n/locales/it.json",
"src/i18n/locales/ja.json", "src/i18n/locales/ja.json",
"src/i18n/locales/pt.json", "src/i18n/locales/pt.json",
+3
View File
@@ -41,6 +41,9 @@ test('normalizeLocale folds regional variants and rejects unshipped tags', () =>
assert.equal(normalizeLocale('de'), 'de'); assert.equal(normalizeLocale('de'), 'de');
assert.equal(normalizeLocale('de-AT'), 'de'); assert.equal(normalizeLocale('de-AT'), 'de');
assert.equal(normalizeLocale('de_CH'), 'de'); assert.equal(normalizeLocale('de_CH'), 'de');
// Hebrew keeps Western digits like its catalog; regional tags fold to he.
assert.equal(normalizeLocale('he'), 'he');
assert.equal(normalizeLocale('he-IL'), 'he');
assert.equal(normalizeLocale('fr'), 'fr'); assert.equal(normalizeLocale('fr'), 'fr');
assert.equal(normalizeLocale('fr-CA'), 'fr'); assert.equal(normalizeLocale('fr-CA'), 'fr');
assert.equal(normalizeLocale('FR_be'), 'fr'); assert.equal(normalizeLocale('FR_be'), 'fr');
+2
View File
@@ -22,6 +22,7 @@ import ar from './locales/ar.json' with { type: 'json' };
import de from './locales/de.json' with { type: 'json' }; import de from './locales/de.json' with { type: 'json' };
import es from './locales/es.json' with { type: 'json' }; import es from './locales/es.json' with { type: 'json' };
import fr from './locales/fr.json' with { type: 'json' }; import fr from './locales/fr.json' with { type: 'json' };
import he from './locales/he.json' with { type: 'json' };
import it from './locales/it.json' with { type: 'json' }; import it from './locales/it.json' with { type: 'json' };
import ja from './locales/ja.json' with { type: 'json' }; import ja from './locales/ja.json' with { type: 'json' };
import pt from './locales/pt.json' with { type: 'json' }; import pt from './locales/pt.json' with { type: 'json' };
@@ -35,6 +36,7 @@ const CATALOGS = Object.freeze({
de, de,
es, es,
fr, fr,
he,
it, it,
ja, ja,
pt, pt,
+4 -2
View File
@@ -14,6 +14,7 @@ export const LOCALES = Object.freeze([
'de', 'de',
'es', 'es',
'fr', 'fr',
'he',
'it', 'it',
'ja', 'ja',
'pt', 'pt',
@@ -30,6 +31,7 @@ export const LOCALE_NAMES = Object.freeze({
de: 'Deutsch', de: 'Deutsch',
es: 'Español', es: 'Español',
fr: 'Français', fr: 'Français',
he: 'עברית',
it: 'Italiano', it: 'Italiano',
ja: '日本語', ja: '日本語',
pt: 'Português (Brasil)', pt: 'Português (Brasil)',
@@ -40,7 +42,7 @@ export const LOCALE_NAMES = Object.freeze({
}); });
/** Right-to-left locales. */ /** Right-to-left locales. */
const RTL_LOCALES = Object.freeze(['ar']); const RTL_LOCALES = Object.freeze(['ar', 'he']);
/** Whether a shipped locale reads right to left. */ /** Whether a shipped locale reads right to left. */
export function isRtlLocale(locale) { export function isRtlLocale(locale) {
@@ -62,7 +64,7 @@ export function intlLocale(locale) {
/** /**
* Map any language tag to a shipped locale: 'ru-RU' and 'RU' → 'ru', * Map any language tag to a shipped locale: 'ru-RU' and 'RU' → 'ru',
* 'ar-EG' and 'ar-SA' → 'ar', * 'ar-EG' and 'ar-SA' → 'ar', 'he-IL' and 'he' → 'he',
* 'de-AT' and 'de_CH' → 'de', 'ja-JP' → 'ja', * 'de-AT' and 'de_CH' → 'de', 'ja-JP' → 'ja',
* 'pt-BR' and 'pt-PT' → 'pt' (Brazilian Portuguese), * 'pt-BR' and 'pt-PT' → 'pt' (Brazilian Portuguese),
* 'zh', 'zh-CN' and 'zh-Hans-SG' → 'zh-CN'; * 'zh', 'zh-CN' and 'zh-Hans-SG' → 'zh-CN';
File diff suppressed because it is too large. Load diff
+89
View File
@@ -1189,5 +1189,94 @@
"acartia.io ↗", "acartia.io ↗",
"iNaturalist ↗", "iNaturalist ↗",
"iNaturalist" "iNaturalist"
],
"he": [
"{speed} kn",
"GEO",
"SAR",
"EARTH WATCH",
"CRT",
"NVG",
"FLIR",
"AIS: --",
"REC",
"HDG {value}°",
"BRG {value}°",
"BRG —",
"CRS {value}°",
"FOV {value}°",
"CAL {value}",
"SRC {value}",
"▲ {frp} MW",
"≥ {value} MW",
"Intel Picture",
"Kinetic 30d",
"Border Crisis",
"Iran Picture",
"Iran Kinetic",
"Yemen FLOT",
"Latest Drop",
"1090 MHz",
"978 MHz UAT",
"ALT {altitude} FT · GS {speed} KT · TRK {track}",
"V/S {rate} FPM",
"Sentinel-2",
"Landsat 8/9",
"Google Street View",
"GOOGLE STREET VIEW",
"AIS: {name}",
"{type} SPD: {speed} HDG: {heading}",
"POS: {time}Z",
"{source} HTTP {status}",
"NNE",
"NE",
"ENE",
"ESE",
"SE",
"SSE",
"SSW",
"SW",
"WSW",
"WNW",
"NW",
"NNW",
"WHOT/BHOT",
"HDG",
"FOV",
"HGT",
"ΔN",
"ΔE",
"CAL · {badge}",
"DEST ---°",
"SQUAWK {code} · {status}",
"DEST {bearing}",
"Space Missions",
"CCTV",
"S2 GhostMaps",
"Street Level",
"STREET LEVEL",
"km/h",
"mph",
"hPa",
"kt",
"kts",
"ft",
"km",
"min",
"dB",
"LEVEL",
"KTS",
"FT",
"FLT",
"MIL",
"WX",
"MSG/S",
"FM MHZ",
"Acartia",
"CC BY 4.0",
"Salish Sea",
"iNaturalist",
"Be Whale Wise",
"MIC"
] ]
} }
+4
View File
@@ -52,6 +52,7 @@ test('the current language comes first, the rest by their own names', () => {
'fr', 'fr',
'it', 'it',
'pt', 'pt',
'he',
'ar', 'ar',
'zh-CN', 'zh-CN',
'zh-Hant', 'zh-Hant',
@@ -75,6 +76,7 @@ test('the current language comes first, the rest by their own names', () => {
'pt', 'pt',
'ru', 'ru',
'uk', 'uk',
'he',
'ar', 'ar',
'zh-CN', 'zh-CN',
'zh-Hant', 'zh-Hant',
@@ -98,6 +100,7 @@ test('the current language comes first, the rest by their own names', () => {
'pt', 'pt',
'ru', 'ru',
'uk', 'uk',
'he',
'ar', 'ar',
'zh-CN', 'zh-CN',
'zh-Hant', 'zh-Hant',
@@ -122,6 +125,7 @@ test('the current language comes first, the rest by their own names', () => {
'pt', 'pt',
'ru', 'ru',
'uk', 'uk',
'he',
'zh-CN', 'zh-CN',
'zh-Hant', 'zh-Hant',
'ja', 'ja',
+37
View File
@@ -60,6 +60,43 @@ html:lang(ar) :is(#intel-hud, #cockpit-hud) {
Tahoma, monospace; Tahoma, monospace;
} }
/* Hebrew: JetBrains Mono and Inter also lack Hebrew glyphs. Noto Sans Hebrew
(loaded with the other Google fonts; its files download only when the page
shows Hebrew) leads the stack so the body text keeps one proportional
Hebrew face across platforms. Hebrew letters do not join like Arabic, but
tracking still reads as gaps inside a word, so the body is released from it
the same way; Latin marked translate="no" keeps its tracking. The intel and
cockpit HUDs keep JetBrains Mono for their Latin telemetry. */
html:lang(he) {
--font-mono:
'Noto Sans Hebrew', 'JetBrains Mono', 'SF Mono', 'Fira Code', Tahoma,
monospace;
--font-sans:
'Inter', 'Noto Sans Hebrew', -apple-system, BlinkMacSystemFont, Tahoma,
sans-serif;
}
html:lang(he)
:is(
#military-awareness-panel,
.military-awareness-controls button,
.military-awareness-standby button,
.military-awareness-compass-label,
.military-awareness-compass-heading,
.military-awareness-direction-label,
.tr3b-toggle
) {
font-family: var(--font-mono);
}
html:lang(he) :is(#intel-hud, #cockpit-hud) {
--font-mono:
'JetBrains Mono', 'SF Mono', 'Fira Code', 'Noto Sans Hebrew', Tahoma,
monospace;
}
html:lang(he) body
:not(#intel-hud, #intel-hud *, [translate='no'], [translate='no'] *) {
letter-spacing: 0 !important;
}
/* The Display/Context rail moves to the left edge. It keeps a physical /* The Display/Context rail moves to the left edge. It keeps a physical
`right` in layers.css because the attribution-clearance model `right` in layers.css because the attribution-clearance model
(creditAttribution.test.mjs) reads that rule; the swap only applies above (creditAttribution.test.mjs) reads that rule; the swap only applies above