Merge branch 'main' into sameh/firms-window-label

This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-11-10 10:27:01 -08:00
commit bb45aacb24
18 files changed
+3735 -37

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
name: Announce merged PRs in Discord
on:
pull_request_target:
types: [closed]
permissions: {}
jobs:
announce:
if: github.repository == 'bilawalsidhu/gods-eye-view' && github.event.pull_request.merged == true
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
# This uses the trusted base workflow and never checks out or runs PR code.
- name: Post to Discord contributions
shell: bash
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_CONTRIBUTIONS_WEBHOOK_URL }}
run: |
if [[ ! "$DISCORD_WEBHOOK_URL" =~ ^https://discord\.com/api/webhooks/[0-9]+/[A-Za-z0-9_-]+$ ]]; then
echo '::error::Set DISCORD_CONTRIBUTIONS_WEBHOOK_URL to the Discord channel webhook URL.'
exit 1
fi
payload=$(jq -ce '
.pull_request | {
allowed_mentions: {parse: []},
embeds: [{
title: ("Merged #" + (.number | tostring) + ": " + (.title | .[0:115])),
url: .html_url,
footer: {text: ("By " + .user.login + " · into " + (.base.ref | .[0:100]))}
}]
}
' "$GITHUB_EVENT_PATH")
curl --silent --show-error --fail --output /dev/null \
--connect-timeout 10 --max-time 20 --retry 3 --retry-max-time 50 \
--header 'Content-Type: application/json' --data-binary "$payload" \
--url "$DISCORD_WEBHOOK_URL?wait=true"
+5
View File
@@ -0,0 +1,5 @@
- Add Hebrew (he), the second right-to-left language: the title, rails, dock
and corner row mirror, numbers keep Western digits, and plurals use the
one/two/other Hebrew forms. Noto Sans Hebrew joins the font stacks and the
style, plural and glossary notes arrive in docs/translations/he.md
(Hebrew, @meirm, #1594).
+14 -10
View File
@@ -4647,7 +4647,9 @@ test:track` 43 tracking invariants · headless QA harnesses under
### 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:
[`docs/TRANSLATORS.md`](./TRANSLATORS.md).
@@ -4661,19 +4663,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
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
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:
views re-render, code that published translated text words it again through
`onLocaleChange`, and `<html lang>`/`dir` and the page's `data-i18n` markup
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">`:
the title, style indicator, both panel stacks and their icon rails swap
sides through logical properties (`inset-inline-*`, `margin-inline-*`), the
dock and corner row reverse their order, and `src/ui/styles/rtl.css` holds
the rest: no letter-spacing on Arabic text, Noto Sans Arabic in the font
stacks, the intel HUD kept as a left-to-right instrument overlay, FM seek
and the radio dial kept left to right. Intl formatters get `ar-u-nu-latn`,
so numbers and dates keep Western digits.
- **Right to left.** Arabic (`ar`) and Hebrew (`he`) are in `RTL_LOCALES`, so
`<html dir="rtl">`: the title, style indicator, both panel stacks and their
icon rails swap sides through logical properties (`inset-inline-*`,
`margin-inline-*`), the dock and corner row reverse their order, and
`src/ui/styles/rtl.css` holds the rest: no letter-spacing on Arabic and
Hebrew text, Noto Sans Arabic and Noto Sans Hebrew in the font stacks, the
intel HUD kept as a left-to-right instrument overlay, FM seek and the radio
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`)
extracts every message and fails on missing, empty, unused,
placeholder-mismatched, still-English (plural forms included) or
+5 -4
View File
@@ -1,6 +1,6 @@
# 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
@@ -45,9 +45,10 @@ Edit `src/i18n/locales/<code>.json` and run `npm run i18n`. To see your change,
1. Create `src/i18n/locales/<code>.json`, fill it from `npm run i18n -- --missing <code>` (start from `{}`), and translate every entry.
2. Import it in `src/i18n/index.js` and add it to `CATALOGS`.
3. Add the code to `LOCALES` and its own-language name to `LOCALE_NAMES` in `src/i18n/locale.js`. Right-to-left languages also go in `RTL_LOCALES`; the chrome mirrors through logical CSS properties (`inset-inline-*`, `margin-inline-*`), and `src/ui/styles/rtl.css` holds what those cannot express (see the Arabic notes). If the language should keep Western digits, map it in `INTL_TAGS` in `locale.js`.
4. Add `src/i18n/locales/<code>.json` next to `ru.json` in `scripts/package-boundaries.json`.
4. Add `src/i18n/locales/<code>.json` next to `ru.json` in `scripts/package-boundaries.json` and in `scripts/format-scope.json`.
5. Write `docs/translations/<code>.md`: glossary, plural forms, units and style (copy the structure of `ru.md`).
6. Run `npm run i18n` and `npm test`, then check the panels in the app at narrow widths.
6. If the language is not written in Latin letters, also add it to `NON_LATIN_LOCALES` in `src/hudSummaryResponse.js` — that guard falls back to the translated text when a summary provider answers in English.
7. Run `npm run i18n` and `npm test`, then check the panels in the app at narrow widths.
## Rules
@@ -63,4 +64,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`: [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.
+64
View File
@@ -0,0 +1,64 @@
# 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.
- Units follow the catalog: ק"מ, מ׳, קמ"ש, שנ׳, דק׳, שע׳; an abbreviation always ends in the geresh (׳). Instrument readouts and formatted shared-unit labels keep the Latin units (km, NM, FT, KTS, MW, KM/S); catalog prose outside those readouts keeps ק"מ/מ׳. Codes like HDG, FOV, CAL, SRC, REC stay 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 and fractions below 1 | 1 מצלמה · 0.5 |
| `two` | 2 | 2 מצלמות |
| `other` | 0, 3, 10, 100, integers… | 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) | עוגן / חבילת נתונים / חבילת נכסים |
| forecast (weather) | תחזית |
| witness / geolocated source | עד ראייה / מקור מאוכן |
| contained (wildland fire) / FRP | הכלה / FRP, בלי שינוי |
+1 -1
View File
@@ -21,7 +21,7 @@
<link rel="stylesheet" href="/style.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<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
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
+1
View File
@@ -1294,6 +1294,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
+46
View File
@@ -73,6 +73,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -656,6 +657,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -704,6 +706,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -745,6 +748,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -788,6 +792,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -815,6 +820,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -845,6 +851,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -882,6 +889,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -951,6 +959,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -978,6 +987,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1180,6 +1190,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1372,6 +1383,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1459,6 +1471,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1521,6 +1534,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1598,6 +1612,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1635,6 +1650,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1676,6 +1692,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1716,6 +1733,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1764,6 +1782,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1816,6 +1835,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1863,6 +1883,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1926,6 +1947,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1963,6 +1985,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -2020,6 +2043,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -2091,6 +2115,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -2132,6 +2157,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -2182,6 +2208,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -2212,6 +2239,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -2276,6 +2304,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -2354,6 +2383,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -3304,6 +3334,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4042,6 +4073,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4226,6 +4258,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4259,6 +4292,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4313,6 +4347,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4351,6 +4386,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4457,6 +4493,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4530,6 +4567,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4822,6 +4860,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4877,6 +4916,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4940,6 +4980,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4964,6 +5005,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -5034,6 +5076,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -5083,6 +5126,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -5290,6 +5334,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -5336,6 +5381,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
+1
View File
@@ -143,6 +143,7 @@ for (const [locale, summary] of [
['ja', '東京上空のLive Flights'],
['zh-CN', '北京航班stale数据'],
['ru', 'Остин Live-Flights частично'],
['he', 'תל אביב stale טיסות'],
]) {
test(`English flush against other text is an echo: ${summary}`, () => {
assert.ok(hudSummaryHasEnglish(summary, locale, ['stale', 'Live Flights']));
+1 -1
View File
@@ -110,7 +110,7 @@ const SUMMARY_LANGUAGES = Object.freeze({
const UNSPACED_LOCALES = /^(?:ja|zh)(?:-|$)/;
/** Shipped locales not written in Latin letters. */
const NON_LATIN_LOCALES = /^(?:ar|ja|ru|uk|zh)(?:-|$)/;
const NON_LATIN_LOCALES = /^(?:ar|ja|ru|uk|zh|he)(?:-|$)/;
/** Scripts whose letters extend a word; any other neighbour ends it. */
const WORD_SCRIPTS = [
+6
View File
@@ -41,6 +41,12 @@ test('normalizeLocale folds regional variants and rejects unshipped tags', () =>
assert.equal(normalizeLocale('de'), 'de');
assert.equal(normalizeLocale('de-AT'), 'de');
assert.equal(normalizeLocale('de_CH'), 'de');
// Hebrew keeps Western digits like its catalog; regional tags fold to he,
// and the legacy tag iw is still what some browsers and OSes label Hebrew.
assert.equal(normalizeLocale('he'), 'he');
assert.equal(normalizeLocale('he-IL'), 'he');
assert.equal(normalizeLocale('iw'), 'he');
assert.equal(normalizeLocale('iw-IL'), 'he');
assert.equal(normalizeLocale('fr'), 'fr');
assert.equal(normalizeLocale('fr-CA'), '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 es from './locales/es.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 ja from './locales/ja.json' with { type: 'json' };
import pt from './locales/pt.json' with { type: 'json' };
@@ -35,6 +36,7 @@ const CATALOGS = Object.freeze({
de,
es,
fr,
he,
it,
ja,
pt,
+6 -2
View File
@@ -14,6 +14,7 @@ export const LOCALES = Object.freeze([
'de',
'es',
'fr',
'he',
'it',
'ja',
'pt',
@@ -30,6 +31,7 @@ export const LOCALE_NAMES = Object.freeze({
de: 'Deutsch',
es: 'Español',
fr: 'Français',
he: 'עברית',
it: 'Italiano',
ja: '日本語',
pt: 'Português (Brasil)',
@@ -40,7 +42,7 @@ export const LOCALE_NAMES = Object.freeze({
});
/** 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. */
export function isRtlLocale(locale) {
@@ -62,7 +64,8 @@ export function intlLocale(locale) {
/**
* 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', and the
* legacy tag 'iw'/'iw-IL' → 'he',
* 'de-AT' and 'de_CH' → 'de', 'ja-JP' → 'ja',
* 'pt-BR' and 'pt-PT' → 'pt' (Brazilian Portuguese),
* 'zh', 'zh-CN' and 'zh-Hans-SG' → 'zh-CN';
@@ -78,6 +81,7 @@ export function normalizeLocale(candidate) {
? 'zh-Hant'
: 'zh-CN';
}
if (primary === 'iw') return 'he';
return LOCALES.includes(primary) ? primary : null;
}
File diff suppressed because it is too large. Load diff
+97
View File
@@ -1190,5 +1190,102 @@
"acartia.io ↗",
"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}",
"CCTV",
"S2 GhostMaps",
"Street Level",
"STREET LEVEL",
"km/h",
"mph",
"hPa",
"kt",
"kts",
"ft",
"km",
"min",
"dB",
"KTS",
"FT",
"FLT",
"MIL",
"WX",
"MSG/S",
"FM MHZ",
"Acartia",
"CC BY 4.0",
"Salish Sea",
"iNaturalist",
"Be Whale Wise",
"MIC",
"STARLINK",
"STARLINK ···",
"STARLINK ✕",
"{range} NM",
"NORAD {id}",
"EPOCH {time}",
"TLE",
"{value} KM",
"{value} KM/S",
"Ironbow"
]
}
+18 -10
View File
@@ -7,9 +7,10 @@
*/
/** Shared visual tokens used by every world-overlay source. */
// Map-card fonts: JetBrains Mono has no Arabic glyphs and the generic
// monospace fallback is a fixed-pitch Arabic face that stretches every word;
// Arabic text falls through to Noto Sans Arabic (loaded with the page fonts).
// Map-card fonts: JetBrains Mono has no Arabic or Hebrew glyphs and the
// generic monospace fallback is a fixed-pitch Arabic face that stretches every
// word; Arabic falls through to Noto Sans Arabic, Hebrew to Noto Sans Hebrew
// (both loaded with the page fonts).
export const WORLD_OVERLAY_STYLE = Object.freeze({
background: 'rgba(4, 12, 16, 0.82)',
selectedBackground: 'rgba(5, 18, 24, 0.94)',
@@ -19,13 +20,20 @@ export const WORLD_OVERLAY_STYLE = Object.freeze({
detail: 'rgba(147, 161, 173, 0.92)',
leader: 'rgba(147, 213, 228, 0.58)',
accent: '#6be8ff',
fontLabel: '500 10px "JetBrains Mono", "Noto Sans Arabic", monospace',
fontTrack: '600 10px "JetBrains Mono", "Noto Sans Arabic", monospace',
fontTitle: '600 12px "JetBrains Mono", "Noto Sans Arabic", monospace',
fontDetail: '500 10.5px "JetBrains Mono", "Noto Sans Arabic", monospace',
fontSelected: '600 13px "JetBrains Mono", "Noto Sans Arabic", monospace',
fontTrackedTitle: '600 13px "JetBrains Mono", "Noto Sans Arabic", monospace',
fontTrackedDetail: '500 11px "JetBrains Mono", "Noto Sans Arabic", monospace',
fontLabel:
'500 10px "JetBrains Mono", "Noto Sans Arabic", "Noto Sans Hebrew", monospace',
fontTrack:
'600 10px "JetBrains Mono", "Noto Sans Arabic", "Noto Sans Hebrew", monospace',
fontTitle:
'600 12px "JetBrains Mono", "Noto Sans Arabic", "Noto Sans Hebrew", monospace',
fontDetail:
'500 10.5px "JetBrains Mono", "Noto Sans Arabic", "Noto Sans Hebrew", monospace',
fontSelected:
'600 13px "JetBrains Mono", "Noto Sans Arabic", "Noto Sans Hebrew", monospace',
fontTrackedTitle:
'600 13px "JetBrains Mono", "Noto Sans Arabic", "Noto Sans Hebrew", monospace',
fontTrackedDetail:
'500 11px "JetBrains Mono", "Noto Sans Arabic", "Noto Sans Hebrew", monospace',
radius: 4,
anchorDotRadius: 3.2,
anchorDotStrokeWidth: 1,
+4
View File
@@ -52,6 +52,7 @@ test('the current language comes first, the rest by their own names', () => {
'fr',
'it',
'pt',
'he',
'ar',
'zh-CN',
'zh-Hant',
@@ -75,6 +76,7 @@ test('the current language comes first, the rest by their own names', () => {
'pt',
'ru',
'uk',
'he',
'ar',
'zh-CN',
'zh-Hant',
@@ -98,6 +100,7 @@ test('the current language comes first, the rest by their own names', () => {
'pt',
'ru',
'uk',
'he',
'ar',
'zh-CN',
'zh-Hant',
@@ -122,6 +125,7 @@ test('the current language comes first, the rest by their own names', () => {
'pt',
'ru',
'uk',
'he',
'zh-CN',
'zh-Hant',
'ja',
+31 -9
View File
@@ -1,13 +1,15 @@
/* Right-to-left languages (Arabic). The chrome mirrors through logical
properties in the component sheets (inset-inline-*, margin-inline-*); this
sheet holds only what logical properties cannot express. */
/* Right-to-left languages (Arabic, Hebrew). The chrome mirrors through
logical properties in the component sheets (inset-inline-*,
margin-inline-*); this sheet holds only what logical properties cannot
express. */
/* Arabic letters join: letter-spacing tears every word into separate glyphs.
Hebrew letters do not join, but tracking only spaces their words out.
Latin marked translate="no" and the intel HUD's Latin telemetry keep their
tracking; the HUD's translated lines are released below. Unlike cjk.css, a
container that merely holds a translate="no" span is not spared: its own
Arabic text would come apart. */
html:lang(ar)
text would come apart. */
html:is(:lang(ar), :lang(he))
body
:not(#intel-hud, #intel-hud *, [translate='no'], [translate='no'] *) {
letter-spacing: 0 !important;
@@ -30,8 +32,7 @@ html[dir='rtl'] #intel-hud :is(.hud-mode, .hud-summary-label, .hud-summary) {
letter apart. Noto Sans Arabic (loaded with the other Google fonts; its
files download only when the page shows Arabic) leads the mono stack, so
the spaces between Arabic words come from it too and not from the
fixed-pitch Latin face. The intel and cockpit HUDs keep JetBrains Mono for
their Latin telemetry and column-aligned digits. */
fixed-pitch Latin face. */
html:lang(ar) {
--font-mono:
'Noto Sans Arabic', 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Geeza Pro',
@@ -40,9 +41,23 @@ html:lang(ar) {
'Inter', 'Noto Sans Arabic', -apple-system, BlinkMacSystemFont, 'Geeza Pro',
Tahoma, sans-serif;
}
/* 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. */
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;
}
/* The Global Context lines and their buttons set ui-monospace through the
`font` shorthand; route them through the Arabic-aware stack too. */
html:lang(ar)
`font` shorthand; in both RTL languages the words come from the page's
own font stack. */
html:is(:lang(ar), :lang(he))
:is(
#military-awareness-panel,
.military-awareness-controls button,
@@ -54,11 +69,18 @@ html:lang(ar)
) {
font-family: var(--font-mono);
}
/* The intel and cockpit HUDs keep JetBrains Mono leading for their Latin
telemetry and column-aligned digits, with the language's face behind it. */
html:lang(ar) :is(#intel-hud, #cockpit-hud) {
--font-mono:
'JetBrains Mono', 'SF Mono', 'Fira Code', 'Noto Sans Arabic', 'Geeza Pro',
Tahoma, monospace;
}
html:lang(he) :is(#intel-hud, #cockpit-hud) {
--font-mono:
'JetBrains Mono', 'SF Mono', 'Fira Code', 'Noto Sans Hebrew', Tahoma,
monospace;
}
/* The Display/Context rail moves to the left edge. It keeps a physical
`right` in layers.css because the attribution-clearance model