mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge remote-tracking branch 'upstream/main' into pr1401-review
This commit is contained in:
commit
81680c5a15
151 files changed
+3865
-634
No files matched your search
@@ -16,7 +16,8 @@ export function hudQuantity(value, unit, digits = 0) {
|
||||
maximumFractionDigits: digits,
|
||||
});
|
||||
const label = unitLabel(unit);
|
||||
if (getLocale() === 'zh-CN') return label ? `${number} ${label}` : number;
|
||||
if (getLocale().startsWith('zh-'))
|
||||
return label ? `${number} ${label}` : number;
|
||||
return `${number}${label.toUpperCase()}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -33,7 +33,9 @@ export const READOUT_WORDS = Object.freeze({
|
||||
date: (day) => displayDay(day, LONG_DAY),
|
||||
daysAgo: (delta) => {
|
||||
const text = formatRelativeTime(delta > 0 ? -delta : 0, 'day');
|
||||
return getLocale() === 'zh-CN' ? text.replace(/(\d)(?=天)/g, '$1 ') : text;
|
||||
return getLocale().startsWith('zh-')
|
||||
? 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).
|
||||
|
||||
@@ -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${getLocale() === 'zh-CN' ? unitLabel('km') : unitLabel('km').toUpperCase()}`,
|
||||
radius: `${radiusKm === null ? '—' : formatNumber(radiusKm)}\u00a0${getLocale().startsWith('zh-') ? unitLabel('km') : unitLabel('km').toUpperCase()}`,
|
||||
}),
|
||||
installations: ({ scope, coverage }) =>
|
||||
t('{scope} · INSTALLATIONS {coverage}', { scope, coverage }),
|
||||
|
||||
@@ -149,7 +149,7 @@ export function formatCockpitDistance(distanceM) {
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
});
|
||||
return getLocale() === 'zh-CN' ? distance : distance.toUpperCase();
|
||||
return getLocale().startsWith('zh-') ? distance : distance.toUpperCase();
|
||||
}
|
||||
|
||||
export function formatCockpitWindDirection(value) {
|
||||
|
||||
@@ -20,6 +20,11 @@ export const LOCALE_SEARCH_THRESHOLD = 8;
|
||||
/** Type-ahead keystrokes closer together than this extend one prefix. */
|
||||
export const TYPEAHEAD_RESET_MS = 500;
|
||||
|
||||
const CHINESE_LABELS = {
|
||||
'zh-CN': 'Chinese (Simplified)',
|
||||
'zh-Hant': 'Chinese (Traditional)',
|
||||
};
|
||||
|
||||
/**
|
||||
* Text folded for search: no diacritics, lower case. 'Español' → 'espanol',
|
||||
* 'Ёжик' → 'ежик'.
|
||||
@@ -464,12 +469,13 @@ export function mountLocalePicker({
|
||||
item.dataset.locale = option.code;
|
||||
const name = element(documentRef, 'span', 'locale-picker-native');
|
||||
name.textContent = option.nativeName;
|
||||
// Every row reads the same: the language named in itself, then its code.
|
||||
// The interface-language name only helps the search find it.
|
||||
// Chinese variants are distinguished by script rather than region.
|
||||
const meta = element(documentRef, 'span', 'locale-picker-meta');
|
||||
const codeLabel = element(documentRef, 'span', 'locale-picker-option-code');
|
||||
codeLabel.setAttribute('translate', 'no');
|
||||
codeLabel.textContent = option.code.toUpperCase();
|
||||
codeLabel.textContent =
|
||||
CHINESE_LABELS[option.code] || option.code.toUpperCase();
|
||||
if (CHINESE_LABELS[option.code]) codeLabel.lang = 'en';
|
||||
meta.append(codeLabel);
|
||||
const text = element(documentRef, 'span', 'locale-picker-text');
|
||||
text.append(name, meta);
|
||||
|
||||
@@ -40,7 +40,7 @@ test('the current language comes first, the rest by their own names', () => {
|
||||
nativeNames: LOCALE_NAMES,
|
||||
}),
|
||||
),
|
||||
['ru', 'en', 'es', 'zh-CN'],
|
||||
['ru', 'en', 'es', 'zh-CN', 'zh-Hant'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
codes(
|
||||
@@ -50,7 +50,7 @@ test('the current language comes first, the rest by their own names', () => {
|
||||
nativeNames: LOCALE_NAMES,
|
||||
}),
|
||||
),
|
||||
['es', 'en', 'ru', 'zh-CN'],
|
||||
['es', 'en', 'ru', 'zh-CN', 'zh-Hant'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
codes(
|
||||
@@ -60,7 +60,7 @@ test('the current language comes first, the rest by their own names', () => {
|
||||
nativeNames: LOCALE_NAMES,
|
||||
}),
|
||||
),
|
||||
['en', 'es', 'ru', 'zh-CN'],
|
||||
['en', 'es', 'ru', 'zh-CN', 'zh-Hant'],
|
||||
);
|
||||
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, 'zh-CN');
|
||||
assert.equal(model.activeOption.code, 'zh-Hant');
|
||||
model.keydown({ key: 'Home' });
|
||||
assert.equal(model.activeOption.code, 'ru');
|
||||
model.keydown({ key: 'ArrowUp' });
|
||||
|
||||
@@ -168,8 +168,9 @@ function presentViewer(state) {
|
||||
const { street } = state;
|
||||
const right = [];
|
||||
if (street.isPano) right.push('360°');
|
||||
// 359.5° and above rounds to north; modulo keeps it from printing 360°.
|
||||
if (Number.isFinite(street.bearing))
|
||||
right.push(`${Math.round(street.bearing)}°`);
|
||||
right.push(`${Math.round(street.bearing) % 360}°`);
|
||||
if (street.capturedAt)
|
||||
right.push(formatDate(street.capturedAt, street.capturedAtPrecision));
|
||||
const left = [];
|
||||
|
||||
@@ -273,6 +273,14 @@ test('viewer caption reads "Image by" left, date right, and links to Mapillary',
|
||||
assert.equal(view.viewer.open, true);
|
||||
});
|
||||
|
||||
test('a bearing that rounds to north reads 0°, not 360°', () => {
|
||||
const caption = (bearing) =>
|
||||
presentStreetLevelPanel(snapshot({ street: { open: true, bearing } }))
|
||||
.viewer.captionRight;
|
||||
assert.equal(caption(359.4), '359°');
|
||||
assert.equal(caption(359.6), '0°');
|
||||
});
|
||||
|
||||
test('an image without a creator name leaves the left caption empty', () => {
|
||||
const view = presentStreetLevelPanel(
|
||||
snapshot({
|
||||
|
||||
Reference in new issue
Block a user