mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Merge pull request #1298 from bilawalsidhu/german/hal-643-zh-cn
i18n: Simplified Chinese (zh-CN) Add a complete Simplified Chinese translation and language picker support. Improve Chinese typography, date formatting, and localized readouts. Include translation guidelines and regression tests. Based on translations by abiyeenzo (#914) and EDIT-Aeterna (#979). Co-authored-by: German Khokhlov <[email protected]> Co-authored-by: Enzo Abyie <[email protected]> Co-authored-by: EDIT-Aeterna <[email protected]>
This commit is contained in:
26 files changed
+3310
-58
No files matched your search
@@ -0,0 +1,4 @@
|
||||
- Add Simplified Chinese (zh-CN) to the language picker, with localized dates
|
||||
and readable Chinese labels. Based on translations by @abiyeenzo (#914)
|
||||
and @EDIT-Aeterna (#979).
|
||||
(German, @gekh, #1298).
|
||||
@@ -3538,7 +3538,7 @@ test:track` 43 tracking invariants · headless QA harnesses under
|
||||
|
||||
### Interface localization (October 2026)
|
||||
|
||||
Shipped locales: **en** (source and fallback), **es** and **ru**. The code lives in
|
||||
Shipped locales: **en** (source and fallback), **es**, **ru** and **zh-CN**. The code lives in
|
||||
`src/i18n/`. How to mark UI text: [`docs/I18N.md`](./I18N.md); translator workflow:
|
||||
[`docs/TRANSLATORS.md`](./TRANSLATORS.md).
|
||||
|
||||
@@ -3549,7 +3549,8 @@ Shipped locales: **en** (source and fallback), **es** and **ru**. The code lives
|
||||
value). English has no catalog; every other locale is
|
||||
`src/i18n/locales/<code>.json`, and a missing entry falls back to English.
|
||||
- **Locale choice.** `?lang=` → stored choice (`gev:locale:v1`) →
|
||||
`navigator.languages` → English. `?lang=` is not stored, and share links drop
|
||||
`navigator.languages` → English. Chinese tags map to `zh-CN`; tags containing
|
||||
`Hant`, `TW`, `HK` or `MO` are skipped. `?lang=` is not stored, and share links drop
|
||||
it, so a recipient keeps their own language. The globe picker in the
|
||||
bottom-right corner (`src/ui/localePicker.js`) stores the choice, drops
|
||||
`?lang=` (keeps it only when storage is blocked) and reloads. It asks first
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Translating God's Eye View
|
||||
|
||||
Shipped languages: **English** (source and fallback), **Spanish** (`es`) and **Russian** (`ru`).
|
||||
Shipped languages: **English** (source and fallback), **Spanish** (`es`), **Russian** (`ru`) and **Simplified Chinese** (`zh-CN`).
|
||||
|
||||
## 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). Read it before translating, and add a term there when you settle on one.
|
||||
Each language keeps its glossary, plural forms, units and style in its own file, `docs/translations/<code>.md`: [Spanish](./translations/es.md), [Russian](./translations/ru.md), [Simplified Chinese](./translations/zh-CN.md). Read it before translating, and add a term there when you settle on one.
|
||||
@@ -0,0 +1,77 @@
|
||||
# Simplified Chinese (`zh-CN`)
|
||||
|
||||
Catalog: `src/i18n/locales/zh-CN.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
|
||||
|
||||
Translation sources: abiyeenzo (#914) and EDIT-Aeterna (#979).
|
||||
|
||||
## Style
|
||||
|
||||
- Mainland usage and Simplified characters. Address the user neutrally, without 您/你 where a plain imperative works: 「点击地图」, 「重试」.
|
||||
- Chinese has no case: an UPPERCASE English HUD label becomes the same short Chinese label. Keep labels as short as the English: 「实时」, 「过期」, 「关」.
|
||||
- Full-width punctuation in Chinese text: ,。:;?!()“”. A dash is 「——」 without spaces; keep `·` as the HUD separator and `…` for ellipsis.
|
||||
- One space between Chinese and Latin letters or digits: 「AIS 船舶」, 「24 小时」, 「{count} 个」. No space next to full-width punctuation.
|
||||
- Units: km, m, km/h, m/s, kn, ft, MW, °, % stay as written; time words are 小时, 分钟, 秒 after a number (「5 分钟前」).
|
||||
- Product names, provider names and codes stay as they are: Google, Cesium, CelesTrak, NOAA, FIRMS, AIS, ADS-B, GNSS, CCTV, HUD, CRT, NVG, FLIR, WHOT/BHOT, Ironbow, CONUS.
|
||||
|
||||
## Plurals
|
||||
|
||||
`Intl.PluralRules('zh-CN')` has one category, `other`: a `tn()` entry has only that form. Use a measure word next to the number where Chinese needs one: 「{count} 个密钥」, 「{count} 架飞机」, 「{count} 艘船」.
|
||||
|
||||
## Glossary
|
||||
|
||||
| English | 中文 |
|
||||
| ------------------------------------- | --------------------------- |
|
||||
| layer / data layer | 图层 / 数据图层 |
|
||||
| feed | 数据源 |
|
||||
| source / data source | 来源 / 数据源 |
|
||||
| LIVE (status) / live data | 实时 / 实时数据 |
|
||||
| SIMULATED | 模拟 |
|
||||
| stale / degraded / unavailable | 过期 / 降级 / 不可用 |
|
||||
| retry | 重试 |
|
||||
| key (API key) / POWER UP | 密钥(API 密钥)/ 增强 |
|
||||
| Provider Settings | 服务商设置 |
|
||||
| globe / map / map source / basemap | 地球 / 地图 / 地图源 / 底图 |
|
||||
| aircraft / flight / military flights | 飞机 / 航班 / 军用航班 |
|
||||
| vessel, ship | 船舶 |
|
||||
| satellite | 卫星 |
|
||||
| launch (rocket) / space mission | 发射 / 太空任务 |
|
||||
| contact (tracked object) | 目标 |
|
||||
| installation, site | 设施 |
|
||||
| camera / CCTV camera | 摄像头 / CCTV 摄像头 |
|
||||
| earthquake / active fire | 地震 / 活跃火点 |
|
||||
| scene / shot | 场景 / 镜头 |
|
||||
| briefing / panel / dock | 简报 / 面板 / 停靠栏 |
|
||||
| preset / visual preset | 预设 / 视觉预设 |
|
||||
| Display / HUD layout | 显示 / HUD 布局 |
|
||||
| Tactical / Operator / Minimal / Cyber | 战术 / 操作员 / 精简 / 赛博 |
|
||||
| Scope (circular viewport mask) / keyhole | 视域 |
|
||||
| Clean UI | 纯净界面 |
|
||||
| cockpit | 座舱 |
|
||||
| heading / altitude / ground speed | 航向 / 高度 / 地速 |
|
||||
| map heading / North up | 方位 / 正北朝上 |
|
||||
| Street Level / Street View (Google) | 街景 / 街景(Google) |
|
||||
| Recent Imagery | 近期影像 |
|
||||
| Global Context / Context (panel) | 全局态势 / 态势 |
|
||||
| radio (station) | 电台 |
|
||||
| transit / bikeshare | 公共交通 / 共享单车 |
|
||||
| Previous / Next | 上一个 / 下一个 |
|
||||
| compass points (N, NNE, …) | 北, 北东北, … |
|
||||
| weather field / FIELD (selector) | 气象场 / 要素 |
|
||||
| weather model / forecast lead hours | 模式 / 预报时效 |
|
||||
| valid (forecast time) | 有效时间 |
|
||||
| scene cloud / mosaic | 整景云量 / 镶嵌图 |
|
||||
| pin (Recent Imagery) | 固定 |
|
||||
| THIS (voice pointer) | 所指 |
|
||||
| outline / trace an outline | 轮廓 / 绘制轮廓 |
|
||||
| mark (map annotation) | 标注 |
|
||||
| camera badge | 摄像头标记 |
|
||||
| provider / embedded provider controls | 服务商 / 平台控件 |
|
||||
| share (a view or link) | 分享 |
|
||||
| AI agent | AI 智能体 |
|
||||
| stage (rocket) / downrange | 子级 / 航程 |
|
||||
| capture shot / camera move | 保存镜头 / 运镜 |
|
||||
| story beat / evidence beat | 事件节点 / 证据节点 |
|
||||
| camera pose | 位姿 |
|
||||
| forecast uncertainty cone | 预报不确定范围 |
|
||||
| seed (catalog data) | 预置 |
|
||||
| frame (bring into view) | 将…纳入视野 |
|
||||
@@ -437,7 +437,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -462,7 +463,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -478,7 +480,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -507,6 +510,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/unitLabels.js",
|
||||
"src/ui/keyGuidance.js",
|
||||
"src/ui/statusText.js",
|
||||
@@ -526,7 +530,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -541,7 +546,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -560,6 +566,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/unitLabels.js",
|
||||
"src/ui/statusBadge.js"
|
||||
],
|
||||
@@ -585,6 +592,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/layerNames.js"
|
||||
],
|
||||
"external": []
|
||||
@@ -618,6 +626,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/unitLabels.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
@@ -633,6 +642,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/unitLabels.js"
|
||||
],
|
||||
"external": []
|
||||
@@ -785,6 +795,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/unitLabels.js",
|
||||
"src/ui/keyGuidance.js",
|
||||
"src/ui/layerNames.js",
|
||||
@@ -904,6 +915,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/unitLabels.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
@@ -969,6 +981,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/unitLabels.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
@@ -1014,7 +1027,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -1074,7 +1088,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -1103,6 +1118,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/unitLabels.js",
|
||||
"src/layers/recentImagery/display.js"
|
||||
],
|
||||
@@ -1131,7 +1147,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -1162,6 +1179,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/unitLabels.js"
|
||||
],
|
||||
"external": ["cesium", "satellite.js"]
|
||||
@@ -1193,7 +1211,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -1235,6 +1254,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/layers/installations/classLabel.js"
|
||||
],
|
||||
"external": ["cesium", "@mapbox/vector-tile", "pbf"]
|
||||
@@ -1269,6 +1289,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/unitLabels.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
@@ -1317,7 +1338,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["cesium", "pbf", "@mapbox/vector-tile"]
|
||||
},
|
||||
@@ -1345,7 +1367,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -1392,7 +1415,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["hls.js", "cesium"]
|
||||
},
|
||||
@@ -1453,6 +1477,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/layers/streetLevel/providers/mapillary/policy.js"
|
||||
],
|
||||
"external": ["cesium", "pbf", "@mapbox/vector-tile", "mapillary-js"]
|
||||
@@ -1480,7 +1505,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["cesium", "@mapbox/vector-tile", "pbf"]
|
||||
},
|
||||
@@ -1519,7 +1545,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -1540,7 +1567,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -1595,7 +1623,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -1651,6 +1680,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/statusText.js",
|
||||
"src/voice/providerCommands.js",
|
||||
"src/voice/providerCapabilities.js",
|
||||
@@ -2512,6 +2542,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/unitLabels.js",
|
||||
"src/layers/recentImagery/display.js",
|
||||
"src/ui/keyGuidance.js",
|
||||
@@ -3087,6 +3118,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/unitLabels.js",
|
||||
"src/layers/recentImagery/display.js",
|
||||
"src/layers/installations/classLabel.js",
|
||||
@@ -3193,6 +3225,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/index.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
@@ -3213,7 +3246,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -3247,7 +3281,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -3270,7 +3305,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -3361,6 +3397,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/unitLabels.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
@@ -3415,6 +3452,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/statusText.js",
|
||||
"src/voice/providerCapabilities.js"
|
||||
],
|
||||
@@ -3667,7 +3705,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["cesium", "egm96-universal", "pbf"]
|
||||
},
|
||||
@@ -3709,7 +3748,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["cesium", "pbf"]
|
||||
},
|
||||
@@ -3761,7 +3801,8 @@
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json"
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -3777,6 +3818,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/statusText.js",
|
||||
"src/ui/layerNames.js"
|
||||
],
|
||||
@@ -3969,6 +4011,7 @@
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
"src/i18n/locales/ru.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/ui/statusText.js",
|
||||
"src/ui/layerNames.js"
|
||||
],
|
||||
|
||||
@@ -412,14 +412,18 @@ const IMAGERY_LABEL_NAMES = Object.freeze([
|
||||
[/^(\d{4}) POST-EVENT$/, msg('{year} POST-EVENT')],
|
||||
]);
|
||||
|
||||
/** Day, short month and year in the active language: 16 OCT 2021, 16 ОКТ. 2021. */
|
||||
/** Day, short month and year in the active language: 16 OCT 2021, 16 ОКТ. 2021, 2021年10月16日. */
|
||||
function imageryDateText(observedAt) {
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(observedAt || ''));
|
||||
if (!match) return '';
|
||||
const date = new Date(Date.UTC(+match[1], +match[2] - 1, +match[3]));
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
const part = (options) => formatDate(date, { timeZone: 'UTC', ...options });
|
||||
return `${part({ day: 'numeric' })} ${part({ month: 'short' })} ${part({ year: 'numeric' })}`.toUpperCase();
|
||||
const year = part({ year: 'numeric' });
|
||||
// Year-first languages (2021年10月16日) keep their own order and units.
|
||||
const whole = part({ day: 'numeric', month: 'short', year: 'numeric' });
|
||||
if (whole.startsWith(year)) return whole;
|
||||
return `${part({ day: 'numeric' })} ${part({ month: 'short' })} ${year}`.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1029,7 +1033,7 @@ function createPanel(event, handlers) {
|
||||
<span>${escapeMarkup(t('GEOLOCATIONS · {credit}', { credit: 'GEO GEORGE SHADRACH' }))}</span>
|
||||
<button type="button" data-action="geolocation-map">${escapeMarkup(t('↗ OPEN PUBLIC MAP'))}</button>
|
||||
</div>
|
||||
<p class="bhote-event-caveat"><span data-role="caveat"></span><span data-role="imagery-cloud-note"> ${escapeMarkup(t('Clouds are preserved from the source imagery.'))}</span></p>
|
||||
<p class="bhote-event-caveat"><span data-role="caveat"></span><span data-role="imagery-cloud-note">${escapeMarkup(t(' Clouds are preserved from the source imagery.'))}</span></p>
|
||||
`;
|
||||
|
||||
const refs = collectPanelReferences(panel);
|
||||
|
||||
@@ -15,7 +15,9 @@ export function hudQuantity(value, unit, digits = 0) {
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
});
|
||||
return `${number}${unitLabel(unit).toUpperCase()}`;
|
||||
const label = unitLabel(unit);
|
||||
if (getLocale() === 'zh-CN') return label ? `${number} ${label}` : number;
|
||||
return `${number}${label.toUpperCase()}`;
|
||||
}
|
||||
|
||||
/** One hemisphere-tagged coordinate, e.g. `21.33N` / `21,33С`. */
|
||||
@@ -35,7 +37,16 @@ export const LOCALIZED_LOCALITY = Object.freeze({
|
||||
}),
|
||||
sector: ({ latDeg, lonDeg }) =>
|
||||
t('SECTOR {lat} {lon}', {
|
||||
lat: hudCoordinate(latDeg, t('N'), t('S')),
|
||||
lon: hudCoordinate(lonDeg, t('E'), t('W')),
|
||||
// Hemisphere letters, not compass words: 21.33N, never 21.33北.
|
||||
lat: hudCoordinate(
|
||||
latDeg,
|
||||
t('N', { context: 'hemisphere' }),
|
||||
t('S', { context: 'hemisphere' }),
|
||||
),
|
||||
lon: hudCoordinate(
|
||||
lonDeg,
|
||||
t('E', { context: 'hemisphere' }),
|
||||
t('W', { context: 'hemisphere' }),
|
||||
),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { getLocale, setLocale } from './index.js';
|
||||
import { createModel } from '../layers/firms/model.js';
|
||||
import { createQueries } from '../layers/firms/queries.js';
|
||||
|
||||
const cases = [
|
||||
[0, '不到 1 分钟前', '<1m ago'],
|
||||
[120000, '2 分钟前', '2m ago'],
|
||||
[7200000, '2 小时前', '2h ago'],
|
||||
];
|
||||
|
||||
for (const [age, zh, en] of cases) {
|
||||
test(`FIRMS live age ${age} ms has one relative-time suffix`, (context) => {
|
||||
const now = 1800000000000;
|
||||
context.mock.method(Date, 'now', () => now);
|
||||
const previousLocale = getLocale();
|
||||
const layerState = {
|
||||
_loading: false,
|
||||
_stale: false,
|
||||
_keyRequired: false,
|
||||
_error: null,
|
||||
_lastUpdate: now - age,
|
||||
_count: 1,
|
||||
_cellCount: 0,
|
||||
_fires: [],
|
||||
};
|
||||
const model = createModel({ services: { anchors: {} } });
|
||||
const { methods } = createQueries({
|
||||
layerState,
|
||||
components: { model },
|
||||
config: {},
|
||||
});
|
||||
try {
|
||||
setLocale('en');
|
||||
assert.equal(methods.getStats().loadingLabel, `LIVE · updated ${en}`);
|
||||
setLocale('zh-CN');
|
||||
assert.equal(methods.getStats().loadingLabel, `实时 · 更新于 ${zh}`);
|
||||
} finally {
|
||||
setLocale(previousLocale);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -35,6 +35,12 @@ test('normalizeLocale folds regional variants and rejects unshipped tags', () =>
|
||||
assert.equal(normalizeLocale(' RU '), 'ru');
|
||||
assert.equal(normalizeLocale('en_GB'), 'en');
|
||||
assert.equal(normalizeLocale('xx'), null);
|
||||
// Simplified Chinese by default; Traditional tags wait for zh-TW.
|
||||
assert.equal(normalizeLocale('zh'), 'zh-CN');
|
||||
assert.equal(normalizeLocale('zh-CN'), 'zh-CN');
|
||||
assert.equal(normalizeLocale('zh-Hans-SG'), 'zh-CN');
|
||||
assert.equal(normalizeLocale('zh-TW'), null);
|
||||
assert.equal(normalizeLocale('zh-Hant-HK'), null);
|
||||
assert.equal(normalizeLocale(undefined), null);
|
||||
});
|
||||
|
||||
|
||||
+2
-1
@@ -18,8 +18,9 @@ import {
|
||||
} from './locale.js';
|
||||
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' };
|
||||
|
||||
const CATALOGS = Object.freeze({ es, ru });
|
||||
const CATALOGS = Object.freeze({ es, ru, 'zh-CN': zhCN });
|
||||
|
||||
let currentLocale = DEFAULT_LOCALE;
|
||||
|
||||
|
||||
+10
-3
@@ -8,25 +8,32 @@ 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']);
|
||||
export const LOCALES = Object.freeze(['en', 'es', 'ru', 'zh-CN']);
|
||||
|
||||
/** Each locale named in its own language, for the selector. */
|
||||
export const LOCALE_NAMES = Object.freeze({
|
||||
en: 'English',
|
||||
es: 'Español',
|
||||
ru: 'Русский',
|
||||
'zh-CN': '简体中文',
|
||||
});
|
||||
|
||||
/** Right-to-left locales. */
|
||||
const RTL_LOCALES = Object.freeze([]);
|
||||
|
||||
/**
|
||||
* 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',
|
||||
* 'zh', 'zh-CN' and 'zh-Hans-SG' → 'zh-CN'.
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function normalizeLocale(candidate) {
|
||||
if (typeof candidate !== 'string') return null;
|
||||
const primary = candidate.trim().toLowerCase().split(/[-_]/)[0];
|
||||
const [primary, ...rest] = candidate.trim().toLowerCase().split(/[-_]/);
|
||||
// Traditional Chinese readers are not given Simplified: no zh-TW yet.
|
||||
if (primary === 'zh')
|
||||
return rest.some((part) => ['hant', 'tw', 'hk', 'mo'].includes(part))
|
||||
? null
|
||||
: 'zh-CN';
|
||||
return LOCALES.includes(primary) ? primary : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1086,7 +1086,6 @@
|
||||
"FIELD REPORTS · {count}": "INFORMES DE CAMPO · {count}",
|
||||
"GEOLOCATIONS · {credit}": "GEOLOCALIZACIONES · {credit}",
|
||||
"↗ OPEN PUBLIC MAP": "↗ ABRIR MAPA PÚBLICO",
|
||||
"Clouds are preserved from the source imagery.": "Las nubes provienen de las imágenes de origen.",
|
||||
"Show the Nepal context callout without a border or country fill": "Muestra la etiqueta de contexto de Nepal sin frontera ni relleno del país",
|
||||
"Introduce Nepal, draw its border, then reveal the Bhote Koshi incident": "Presenta Nepal, dibuja su frontera y luego revela el incidente del Bhote Koshi",
|
||||
"CITIES": "CIUDADES",
|
||||
@@ -2937,5 +2936,12 @@
|
||||
"Gemini access was refused. Check the API key and model access.": "Se denegó el acceso a Gemini. Comprueba la clave de API y el acceso al modelo.",
|
||||
"Gemini Live is unavailable for the configured model.": "Gemini Live no está disponible para el modelo configurado.",
|
||||
"Token request is too large": "La solicitud de credencial es demasiado grande",
|
||||
"Use an empty JSON token request": "Usa una solicitud de credencial con un objeto JSON vacío"
|
||||
"Use an empty JSON token request": "Usa una solicitud de credencial con un objeto JSON vacío",
|
||||
" Clouds are preserved from the source imagery.": " Las nubes provienen de las imágenes de origen.",
|
||||
"hemisphere|N": "N",
|
||||
"hemisphere|S": "S",
|
||||
"hemisphere|E": "E",
|
||||
"hemisphere|W": "O",
|
||||
"OK": "OK",
|
||||
"{category} ({count})": "{category} ({count})"
|
||||
}
|
||||
@@ -1100,7 +1100,6 @@
|
||||
"FIELD REPORTS · {count}": "СООБЩЕНИЯ С МЕСТА · {count}",
|
||||
"GEOLOCATIONS · {credit}": "ГЕОЛОКАЦИИ · {credit}",
|
||||
"↗ OPEN PUBLIC MAP": "↗ ОТКРЫТЬ ПУБЛИЧНУЮ КАРТУ",
|
||||
"Clouds are preserved from the source imagery.": "Облака оставлены такими, как на исходных снимках.",
|
||||
"Show the Nepal context callout without a border or country fill": "Показать выноску с контекстом Непала без границы и заливки страны",
|
||||
"Introduce Nepal, draw its border, then reveal the Bhote Koshi incident": "Представить Непал, нарисовать его границу, затем показать инцидент на Bhote Koshi",
|
||||
"CITIES": "ГОРОДА",
|
||||
@@ -3004,5 +3003,12 @@
|
||||
"Gemini access was refused. Check the API key and model access.": "Доступ к Gemini запрещён. Проверьте ключ API и доступ к модели.",
|
||||
"Gemini Live is unavailable for the configured model.": "Gemini Live недоступен для выбранной модели.",
|
||||
"Token request is too large": "Запрос токена слишком велик",
|
||||
"Use an empty JSON token request": "Используйте запрос токена с пустым объектом JSON"
|
||||
"Use an empty JSON token request": "Используйте запрос токена с пустым объектом JSON",
|
||||
" Clouds are preserved from the source imagery.": " Облака оставлены такими, как на исходных снимках.",
|
||||
"hemisphere|N": "С",
|
||||
"hemisphere|S": "Ю",
|
||||
"hemisphere|E": "В",
|
||||
"hemisphere|W": "З",
|
||||
"OK": "НОРМА",
|
||||
"{category} ({count})": "{category} ({count})"
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -63,6 +63,7 @@
|
||||
"NVG",
|
||||
"Noir",
|
||||
"Normal",
|
||||
"OK",
|
||||
"POS: {time}Z",
|
||||
"Plan",
|
||||
"Pop",
|
||||
@@ -98,6 +99,9 @@
|
||||
"fire confidence|nominal",
|
||||
"ft",
|
||||
"hPa",
|
||||
"hemisphere|E",
|
||||
"hemisphere|N",
|
||||
"hemisphere|S",
|
||||
"installation class|BASE",
|
||||
"km",
|
||||
"km/h",
|
||||
@@ -133,5 +137,60 @@
|
||||
"REC",
|
||||
"VS",
|
||||
"WHOT/BHOT"
|
||||
],
|
||||
"zh-CN": [
|
||||
"1090 MHz",
|
||||
"978 MHz UAT",
|
||||
"AIS: --",
|
||||
"AIS: {name}",
|
||||
"BRG {value}°",
|
||||
"BRG —",
|
||||
"CAL {value}",
|
||||
"CAL · {badge}",
|
||||
"CCTV",
|
||||
"CRS {value}°",
|
||||
"CRT",
|
||||
"FLIR",
|
||||
"FOV",
|
||||
"FOV {value}°",
|
||||
"FT",
|
||||
"GEO",
|
||||
"HDG",
|
||||
"HDG {value}°",
|
||||
"House",
|
||||
"Ironbow",
|
||||
"KTS",
|
||||
"Landsat 8/9",
|
||||
"MM",
|
||||
"NVG",
|
||||
"REC",
|
||||
"SRC {value}",
|
||||
"Sentinel-2",
|
||||
"Techno",
|
||||
"Trance",
|
||||
"USB {rate} msg/s",
|
||||
"WHOT/BHOT",
|
||||
"dB",
|
||||
"ft",
|
||||
"hPa",
|
||||
"hemisphere|E",
|
||||
"hemisphere|N",
|
||||
"hemisphere|S",
|
||||
"hemisphere|W",
|
||||
"km",
|
||||
"km/h",
|
||||
"kt",
|
||||
"kts",
|
||||
"mph",
|
||||
"{city} CCTV",
|
||||
"{rate} msg/s",
|
||||
"{source} HTTP {status}",
|
||||
"{speed} kn",
|
||||
"{type} SPD: {speed} HDG: {heading}",
|
||||
"{value} KM",
|
||||
"{value} KM/S",
|
||||
"ΔE",
|
||||
"ΔN",
|
||||
"▲ {frp} MW"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { setLocale, t } from './index.js';
|
||||
import { hudQuantity, LOCALIZED_LOCALITY } from '../hudLocalityFormat.js';
|
||||
import { formatCockpitDistance } from '../ui/cockpitPresentation.js';
|
||||
import { cctvSourceStatusLabel } from '../ui/cctvFrames.js';
|
||||
import { READOUT_WORDS } from '../layers/recentImagery/display.js';
|
||||
import { createModel } from '../layers/firms/model.js';
|
||||
import { createQueries } from '../layers/firms/queries.js';
|
||||
import { renderRadioState } from '../ui/radioPresentation.js';
|
||||
|
||||
test('Chinese HUD and cockpit units keep their case without spacing coordinates', () => {
|
||||
try {
|
||||
setLocale('zh-CN');
|
||||
assert.equal(hudQuantity(0, 'km'), '0 km');
|
||||
assert.equal(hudQuantity(2069, 'km', 1), '2069.0 km');
|
||||
assert.equal(formatCockpitDistance(14000), '14\u00a0km');
|
||||
assert.equal(LOCALIZED_LOCALITY.sector({ latDeg: 21.33, lonDeg: -77.5 }),
|
||||
'扇区 21.33N 77.50W');
|
||||
setLocale('en');
|
||||
assert.equal(hudQuantity(0, 'km'), '0KM');
|
||||
assert.equal(hudQuantity(2069, 'km', 1), '2069.0KM');
|
||||
assert.equal(formatCockpitDistance(14000), '14\u00a0KM');
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
});
|
||||
|
||||
test('FIRMS stale cache age uses localized durations while English stays compact', () => {
|
||||
const now = Date.now();
|
||||
const layerState = { _stale: true };
|
||||
const components = {
|
||||
model: createModel({ services: { anchors: {} } }),
|
||||
};
|
||||
const { methods } = createQueries({ layerState, components, config: {} });
|
||||
try {
|
||||
for (const [hours, zh, en] of [[0.5, '不到 1 小时', '<1h'], [2, '2 小时', '2h'], [72, '3 天', '3d']]) {
|
||||
layerState._lastUpdate = now - hours * 3600000;
|
||||
setLocale('zh-CN');
|
||||
const expected = t('STALE · cached {age}', { age: zh });
|
||||
assert.equal(methods.getStats().loadingLabel, expected);
|
||||
assert.equal(methods.getStats().error, expected);
|
||||
setLocale('en');
|
||||
assert.equal(methods.getStats().loadingLabel, `STALE · cached ${en}`);
|
||||
}
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
});
|
||||
|
||||
test('Recent Imagery spaces numeric Chinese ages and retains natural relative dates', () => {
|
||||
try {
|
||||
setLocale('zh-CN');
|
||||
assert.equal(READOUT_WORDS.daysAgo(9), '9 天前');
|
||||
assert.equal(READOUT_WORDS.daysAgo(0), '今天');
|
||||
assert.equal(READOUT_WORDS.daysAgo(1), '昨天');
|
||||
setLocale('en');
|
||||
assert.equal(READOUT_WORDS.daysAgo(9), '9 days ago');
|
||||
assert.equal(READOUT_WORDS.daysAgo(0), 'today');
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
});
|
||||
|
||||
test('CCTV translates the healthy status and preserves unknown provider codes', () => {
|
||||
try {
|
||||
for (const [locale, expected] of [['zh-CN', '正常'], ['ru', 'НОРМА'], ['es', 'OK'], ['en', 'OK']]) {
|
||||
setLocale(locale);
|
||||
assert.equal(cctvSourceStatusLabel('ok'), expected);
|
||||
assert.equal(cctvSourceStatusLabel('CUSTOM'), 'CUSTOM');
|
||||
}
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
});
|
||||
|
||||
test('radio option text and accessible name use the same localized parentheses', () => {
|
||||
const originalDocument = globalThis.document;
|
||||
const element = () => ({
|
||||
style: {}, dataset: {}, attributes: {},
|
||||
classList: { contains: () => true, toggle() {} },
|
||||
setAttribute(name, value) { this.attributes[name] = value; },
|
||||
replaceChildren(...children) { this.children = children; },
|
||||
});
|
||||
globalThis.document = { createElement: element, getElementById: () => null };
|
||||
try {
|
||||
for (const [locale, label, expected] of [['zh-CN', '全部', '全部(750)'], ['en', 'All', 'All (750)']]) {
|
||||
setLocale(locale);
|
||||
const host = {
|
||||
_radioPanel: element(), _radioFilter: element(),
|
||||
_syncContextRadioLauncherState() {},
|
||||
actions: { getLifecycle: () => null, scheduleLayout() {} },
|
||||
};
|
||||
renderRadioState.call(host, {
|
||||
enabled: true, categories: [{ id: 'all', label, count: 750, color: '' }],
|
||||
filter: 'all', filteredCount: 750, stationCount: 750, volume: 1,
|
||||
});
|
||||
const option = host._radioFilter.children[0];
|
||||
assert.equal(option.textContent, `● ${expected}`);
|
||||
assert.equal(option.attributes['aria-label'], expected);
|
||||
}
|
||||
} finally {
|
||||
globalThis.document = originalDocument;
|
||||
setLocale('en');
|
||||
}
|
||||
});
|
||||
@@ -591,6 +591,7 @@ export function createModel({
|
||||
formatLatLon,
|
||||
formatFrp,
|
||||
formatAge,
|
||||
formatAgeLabel,
|
||||
formatAgoMinutes,
|
||||
confidenceBucket,
|
||||
};
|
||||
|
||||
@@ -64,7 +64,8 @@ export function createQueries({
|
||||
const staleText = layerState._lastUpdate
|
||||
? t('STALE · cached {age}', {
|
||||
age:
|
||||
components.model.formatAge(now - layerState._lastUpdate) || '<1h',
|
||||
components.model.formatAgeLabel(now - layerState._lastUpdate) ||
|
||||
t('<1h', { context: 'age' }),
|
||||
})
|
||||
: t('STALE');
|
||||
let loadingLabel = '';
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
* day's readout. The model (`model.js`) stays English and i18n-free because AI
|
||||
* tool services import it outside the browser; this module is page-only.
|
||||
*/
|
||||
import { formatDate, formatRelativeTime, t } from '../../i18n/index.js';
|
||||
import {
|
||||
formatDate,
|
||||
formatRelativeTime,
|
||||
getLocale,
|
||||
t,
|
||||
} from '../../i18n/index.js';
|
||||
import { formatQuantity } from '../../ui/unitLabels.js';
|
||||
import { dayTime } from './model.js';
|
||||
|
||||
@@ -26,7 +31,10 @@ export const displayMetres = (value) => formatQuantity(value, 'm');
|
||||
/** The readout's words in the page's language; the model keeps them English. */
|
||||
export const READOUT_WORDS = Object.freeze({
|
||||
date: (day) => displayDay(day, LONG_DAY),
|
||||
daysAgo: (delta) => formatRelativeTime(delta > 0 ? -delta : 0, 'day'),
|
||||
daysAgo: (delta) => {
|
||||
const text = formatRelativeTime(delta > 0 ? -delta : 0, 'day');
|
||||
return getLocale() === 'zh-CN' ? text.replace(/(\d)(?=天)/g, '$1 ') : text;
|
||||
},
|
||||
// "Sentinel-2" never breaks at its hyphen: a word joiner follows it
|
||||
// (JetBrains Mono has no U+2011 non-breaking hyphen).
|
||||
sensor: (sensor) =>
|
||||
|
||||
@@ -20,6 +20,7 @@ const SOURCE_KIND_LABELS = Object.freeze({
|
||||
});
|
||||
|
||||
const SOURCE_STATUS_LABELS = Object.freeze({
|
||||
ok: msg('OK'),
|
||||
degraded: msg('DEGRADED'),
|
||||
error: msg('ERROR'),
|
||||
unknown: msg('UNKNOWN'),
|
||||
@@ -52,7 +53,7 @@ export function cctvSourceKindLabel(kind) {
|
||||
: key.toUpperCase();
|
||||
}
|
||||
|
||||
/** "DEGRADED" / "ПЕРЕБОИ"; OK and unknown codes stay as uppercase codes. */
|
||||
/** "DEGRADED" / "ПЕРЕБОИ"; unknown codes stay as uppercase codes. */
|
||||
export function cctvSourceStatusLabel(status) {
|
||||
const key = String(status || 'unknown').toLowerCase();
|
||||
return SOURCE_STATUS_LABELS[key]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Contact readouts and nearby-signal presentation for the Cockpit controller. */
|
||||
import * as Cesium from 'cesium';
|
||||
import { formatNumber, t, tn } from '../i18n/index.js';
|
||||
import { formatNumber, getLocale, t, tn } from '../i18n/index.js';
|
||||
import { unitLabel } from './unitLabels.js';
|
||||
import {
|
||||
bearingBetweenCoordinates,
|
||||
@@ -15,7 +15,7 @@ const LOCALIZED_CONTEXT_SCOPE = Object.freeze({
|
||||
window: ({ label, radiusKm }) =>
|
||||
t('{label} · {radius} AIR/SEA WINDOW', {
|
||||
label,
|
||||
radius: `${radiusKm === null ? '—' : formatNumber(radiusKm)}\u00a0${unitLabel('km').toUpperCase()}`,
|
||||
radius: `${radiusKm === null ? '—' : formatNumber(radiusKm)}\u00a0${getLocale() === 'zh-CN' ? unitLabel('km') : unitLabel('km').toUpperCase()}`,
|
||||
}),
|
||||
installations: ({ scope, coverage }) =>
|
||||
t('{scope} · INSTALLATIONS {coverage}', { scope, coverage }),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Pure formatting and fixed presentation constants used by Cockpit components. */
|
||||
import { formatNumber, t, msg } from '../i18n/index.js';
|
||||
import { formatNumber, getLocale, t, msg } from '../i18n/index.js';
|
||||
import { formatQuantity } from './unitLabels.js';
|
||||
|
||||
export const COCKPIT_HEADING_SLEW_DPS = 28;
|
||||
@@ -145,10 +145,11 @@ export function formatCockpitCoordinate(value, positive, negative) {
|
||||
*/
|
||||
export function formatCockpitDistance(distanceM) {
|
||||
const digits = distanceM < 10000 ? 1 : 0;
|
||||
return formatQuantity(distanceM / 1000, 'km', {
|
||||
const distance = formatQuantity(distanceM / 1000, 'km', {
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
}).toUpperCase();
|
||||
});
|
||||
return getLocale() === 'zh-CN' ? distance : distance.toUpperCase();
|
||||
}
|
||||
|
||||
export function formatCockpitWindDirection(value) {
|
||||
|
||||
@@ -40,7 +40,7 @@ test('the current language comes first, the rest by their own names', () => {
|
||||
nativeNames: LOCALE_NAMES,
|
||||
}),
|
||||
),
|
||||
['ru', 'en', 'es'],
|
||||
['ru', 'en', 'es', 'zh-CN'],
|
||||
);
|
||||
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'],
|
||||
['es', 'en', 'ru', 'zh-CN'],
|
||||
);
|
||||
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'],
|
||||
['en', 'es', 'ru', 'zh-CN'],
|
||||
);
|
||||
const rows = buildLocaleOptions({
|
||||
locales: MANY,
|
||||
@@ -263,7 +263,7 @@ test('keyboard model: open on the current language, move, choose, close', () =>
|
||||
assert.deepEqual(model.keydown({ key: 'ArrowDown' }), { handled: true });
|
||||
assert.equal(model.activeOption.code, 'en');
|
||||
model.keydown({ key: 'End' });
|
||||
assert.equal(model.activeOption.code, 'es');
|
||||
assert.equal(model.activeOption.code, 'zh-CN');
|
||||
model.keydown({ key: 'Home' });
|
||||
assert.equal(model.activeOption.code, 'ru');
|
||||
model.keydown({ key: 'ArrowUp' });
|
||||
|
||||
@@ -114,13 +114,14 @@ export function renderRadioState(state) {
|
||||
...state.categories.map((category) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = category.id;
|
||||
option.textContent = `● ${category.label} (${category.count})`;
|
||||
const label = t('{category} ({count})', {
|
||||
category: category.label,
|
||||
count: category.count,
|
||||
});
|
||||
option.textContent = `● ${label}`;
|
||||
option.dataset.radioColor = category.color;
|
||||
option.style.color = category.color;
|
||||
option.setAttribute(
|
||||
'aria-label',
|
||||
`${category.label} (${category.count})`,
|
||||
);
|
||||
option.setAttribute('aria-label', label);
|
||||
return option;
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/* Tight tracking keeps Chinese labels readable; preserve Latin tracking for
|
||||
intel HUD readouts and content marked translate="no", including its containers. */
|
||||
html:lang(zh)
|
||||
body
|
||||
:not(
|
||||
#intel-hud,
|
||||
#intel-hud *,
|
||||
[translate='no'],
|
||||
[translate='no'] *,
|
||||
:has([translate='no'])
|
||||
) {
|
||||
letter-spacing: 0.02em !important;
|
||||
}
|
||||
|
||||
/* Keep small Chinese labels legible at a minimum of 9px. */
|
||||
html:lang(zh) #command-dock .gev-mic-label,
|
||||
html:lang(zh) #command-dock #gev-voice-detail,
|
||||
html:lang(zh) .gev-voice-tier-btn,
|
||||
html:lang(zh) .panel-layer-toggle {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
/* A two-glyph button label must not break between its glyphs. */
|
||||
html:lang(zh) .pp-mode-btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Keep unit symbols in their catalog case, including the cockpit readouts. */
|
||||
html:lang(zh) #intel-hud *,
|
||||
html:lang(zh) #cockpit-hud * {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/* Move short travel modes (步行 / 骑行) to the next line as whole words. */
|
||||
html:lang(zh) [data-layer-id='directions'] .data-toggle-meta {
|
||||
word-break: keep-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Leave room for the complete sensor name and resolution on two lines. */
|
||||
html:lang(zh) .recent-imagery-readout .ri-strip {
|
||||
height: 131px;
|
||||
}
|
||||
html:lang(zh) .recent-imagery-readout .ri-card {
|
||||
height: 123px;
|
||||
grid-template-rows: 42px 13px 22px 11px 18px;
|
||||
}
|
||||
html:lang(zh) .recent-imagery-readout .ri-card-sensor {
|
||||
white-space: normal;
|
||||
text-overflow: clip;
|
||||
}
|
||||
Reference in new issue
Block a user