Merge remote-tracking branch 'upstream/main' into pr1405-fix

This commit is contained in:
Said Turksever committed 2026-10-28 14:06:40 +00:00
commit 9659f38fe6
151 files changed
+3865 -634

No files matched your search

+6 -4
View File
@@ -5,12 +5,12 @@ 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'],
[0, '不到 1 分钟前', '<1m ago', '不到 1 分鐘前'],
[120000, '2 分钟前', '2m ago', '2 分鐘前'],
[7200000, '2 小时前', '2h ago', '2 小時前'],
];
for (const [age, zh, en] of cases) {
for (const [age, zh, en, hant] of cases) {
test(`FIRMS live age ${age} ms has one relative-time suffix`, (context) => {
const now = 1800000000000;
context.mock.method(Date, 'now', () => now);
@@ -36,6 +36,8 @@ for (const [age, zh, en] of cases) {
assert.equal(methods.getStats().loadingLabel, `LIVE · updated ${en}`);
setLocale('zh-CN');
assert.equal(methods.getStats().loadingLabel, `实时 · 更新于 ${zh}`);
setLocale('zh-Hant');
assert.equal(methods.getStats().loadingLabel, `實時 · 更新於 ${hant}`);
} finally {
setLocale(previousLocale);
}
+9 -3
View File
@@ -35,12 +35,18 @@ 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.
// Explicit script tags take precedence over regional defaults.
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('zh-Hant'), 'zh-Hant');
assert.equal(normalizeLocale(' ZH_hant '), 'zh-Hant');
assert.equal(normalizeLocale('zh-TW'), 'zh-Hant');
assert.equal(normalizeLocale('zh-Hant-HK'), 'zh-Hant');
assert.equal(normalizeLocale('zh-Hans-HK'), 'zh-CN');
assert.equal(normalizeLocale('zh-Hant-CN'), 'zh-Hant');
assert.equal(normalizeLocale('zh_HK'), 'zh-Hant');
assert.equal(normalizeLocale('zh-MO'), 'zh-Hant');
assert.equal(normalizeLocale(undefined), null);
});
+2 -1
View File
@@ -19,8 +19,9 @@ import {
import es from './locales/es.json' with { type: 'json' };
import ru from './locales/ru.json' with { type: 'json' };
import zhCN from './locales/zh-CN.json' with { type: 'json' };
import zhHant from './locales/zh-Hant.json' with { type: 'json' };
const CATALOGS = Object.freeze({ es, ru, 'zh-CN': zhCN });
const CATALOGS = Object.freeze({ es, ru, 'zh-CN': zhCN, 'zh-Hant': zhHant });
let currentLocale = DEFAULT_LOCALE;
+9 -6
View File
@@ -8,14 +8,15 @@ export const LOCALE_STORAGE_KEY = 'gev:locale:v1';
export const DEFAULT_LOCALE = 'en';
/** Shipped locales; order does not matter. Adding one: docs/TRANSLATORS.md. */
export const LOCALES = Object.freeze(['en', 'es', 'ru', 'zh-CN']);
export const LOCALES = Object.freeze(['en', 'es', 'ru', 'zh-CN', 'zh-Hant']);
/** Each locale named in its own language, for the selector. */
export const LOCALE_NAMES = Object.freeze({
en: 'English',
es: 'Español',
ru: 'Русский',
'zh-CN': '简体中文',
'zh-CN': '中文(简体)',
'zh-Hant': '中文(繁體)',
});
/** Right-to-left locales. */
@@ -23,17 +24,19 @@ const RTL_LOCALES = Object.freeze([]);
/**
* Map any language tag to a shipped locale: 'ru-RU' and 'RU' → 'ru',
* 'zh', 'zh-CN' and 'zh-Hans-SG' → 'zh-CN'.
* 'zh', 'zh-CN' and 'zh-Hans-SG' → 'zh-CN';
* 'zh-Hant', 'zh-TW', 'zh-HK' and 'zh-MO' → 'zh-Hant'.
* @returns {string|null}
*/
export function normalizeLocale(candidate) {
if (typeof candidate !== 'string') return null;
const [primary, ...rest] = candidate.trim().toLowerCase().split(/[-_]/);
// Traditional Chinese readers are not given Simplified: no zh-TW yet.
if (primary === 'zh')
if (primary === 'zh') {
if (rest.includes('hans')) return 'zh-CN';
return rest.some((part) => ['hant', 'tw', 'hk', 'mo'].includes(part))
? null
? 'zh-Hant'
: 'zh-CN';
}
return LOCALES.includes(primary) ? primary : null;
}
File diff suppressed because it is too large. Load diff
+57
View File
@@ -198,5 +198,62 @@
"ΔE",
"ΔN",
"▲ {frp} MW"
],
"zh-Hant": [
"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}",
"S2 GhostMaps",
"S2 · {name}",
"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"
]
}
+80
View File
@@ -0,0 +1,80 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { setLocale, t, tn, getLocale } from './index.js';
import {
resolveLocale,
LOCALE_STORAGE_KEY,
writeStoredLocale,
applyDocumentLanguage,
} from './locale.js';
import { hudQuantity, LOCALIZED_LOCALITY } from '../hudLocalityFormat.js';
import { formatCockpitDistance } from '../ui/cockpitPresentation.js';
import { READOUT_WORDS } from '../layers/recentImagery/display.js';
import { cctvSourceStatusLabel } from '../ui/cctvFrames.js';
test('Traditional Chinese resolves from URL, saved preference and browser language', () => {
assert.equal(
resolveLocale({
location: { search: '?lang=zh-TW' },
storage: { getItem: () => 'en' },
languages: ['en'],
}),
'zh-Hant',
);
assert.equal(
resolveLocale({
location: { search: '' },
storage: {
getItem: (key) => (key === LOCALE_STORAGE_KEY ? 'zh-TW' : null),
},
languages: ['en'],
}),
'zh-Hant',
);
assert.equal(
resolveLocale({
location: { search: '' },
storage: { getItem: () => null },
languages: ['zh-Hant', 'en'],
}),
'zh-Hant',
);
});
test('Traditional Chinese persists and exposes the script locale for legacy choices', () => {
const stored = new Map();
assert.equal(
writeStoredLocale('zh-TW', {
setItem: (key, value) => stored.set(key, value),
}),
true,
);
assert.equal(stored.get(LOCALE_STORAGE_KEY), 'zh-Hant');
const doc = { documentElement: {} };
assert.equal(applyDocumentLanguage(doc, 'zh-TW'), true);
assert.equal(doc.documentElement.lang, 'zh-Hant');
assert.equal(doc.documentElement.dir, 'ltr');
});
test('Traditional Chinese readouts preserve unit case, coordinates and natural relative dates', () => {
const previous = getLocale();
try {
setLocale('zh-Hant');
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',
);
assert.equal(READOUT_WORDS.daysAgo(9), '9 天前');
assert.equal(READOUT_WORDS.daysAgo(0), '今天');
assert.equal(cctvSourceStatusLabel('ok'), '正常');
assert.equal(t('SAVE KEYS'), '保存密鑰');
assert.equal(
tn(2, 'Click {count} more point.', 'Click {count} more points.'),
'再點擊 2 個點。',
);
} finally {
setLocale(previous);
}
});