mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
i18n(zh-CN): complete and ship Simplified Chinese
- Translate the remaining 910 strings and review the whole catalog (meaning, terms, punctuation, label length) against the screens. - Register zh-CN: any zh tag maps to it except Traditional ones (Hant, TW, HK, MO), which keep English until a Traditional catalog ships. - Hemisphere letters get their own context so the HUD sector line reads 21.33N, not a compass word; the Bhote Koshi imagery dates follow year-first languages (2021年10月16日). - cjk.css: tight tracking for Chinese text and a 9px floor on the dock voice labels and layer buttons; Latin HUD readouts keep their look. - docs/translations/zh-CN.md: style, plurals and glossary. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
db16d8ed4c
commit
c4cc499d58
16 files changed
+1422
-199
No files matched your search
@@ -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);
|
||||
|
||||
@@ -35,7 +35,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' }),
|
||||
),
|
||||
}),
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1096,7 +1096,7 @@
|
||||
"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.",
|
||||
" 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",
|
||||
@@ -2890,5 +2890,9 @@
|
||||
"USB work abandoned after a stalled transfer": "Trabajo USB cancelado tras una transferencia bloqueada",
|
||||
"SDR decoder did not start; receiver stopped": "El decodificador SDR no arrancó; receptor detenido",
|
||||
"SDR decoder stopped responding; receiver stopped": "El decodificador SDR dejó de responder; receptor detenido",
|
||||
"SDR decoder fell behind; receiver stopped": "El decodificador SDR se quedó atrás; receptor detenido"
|
||||
"SDR decoder fell behind; receiver stopped": "El decodificador SDR se quedó atrás; receptor detenido",
|
||||
"hemisphere|N": "N",
|
||||
"hemisphere|S": "S",
|
||||
"hemisphere|E": "E",
|
||||
"hemisphere|W": "O"
|
||||
}
|
||||
@@ -1110,7 +1110,7 @@
|
||||
"FIELD REPORTS · {count}": "СООБЩЕНИЯ С МЕСТА · {count}",
|
||||
"GEOLOCATIONS · {credit}": "ГЕОЛОКАЦИИ · {credit}",
|
||||
"↗ OPEN PUBLIC MAP": "↗ ОТКРЫТЬ ПУБЛИЧНУЮ КАРТУ",
|
||||
"Clouds are preserved from the source imagery.": "Облака оставлены такими, как на исходных снимках.",
|
||||
" 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": "ГОРОДА",
|
||||
@@ -2957,5 +2957,9 @@
|
||||
"USB work abandoned after a stalled transfer": "Работа с USB прервана: передача данных зависла",
|
||||
"SDR decoder did not start; receiver stopped": "Декодер SDR не запустился; приёмник остановлен",
|
||||
"SDR decoder stopped responding; receiver stopped": "Декодер SDR перестал отвечать; приёмник остановлен",
|
||||
"SDR decoder fell behind; receiver stopped": "Декодер SDR не успевает; приёмник остановлен"
|
||||
"SDR decoder fell behind; receiver stopped": "Декодер SDR не успевает; приёмник остановлен",
|
||||
"hemisphere|N": "С",
|
||||
"hemisphere|S": "Ю",
|
||||
"hemisphere|E": "В",
|
||||
"hemisphere|W": "З"
|
||||
}
|
||||
+1151
-155
File diff suppressed because it is too large.
Load diff
@@ -98,6 +98,9 @@
|
||||
"fire confidence|nominal",
|
||||
"ft",
|
||||
"hPa",
|
||||
"hemisphere|E",
|
||||
"hemisphere|N",
|
||||
"hemisphere|S",
|
||||
"installation class|BASE",
|
||||
"km",
|
||||
"km/h",
|
||||
@@ -133,5 +136,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"
|
||||
]
|
||||
}
|
||||
@@ -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' });
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/* Chinese text. The UI sets Latin capitals with wide tracking; Chinese set
|
||||
that way falls apart into single glyphs, so its tracking stays tight. The
|
||||
intel HUD readouts (codes, coordinates) and text kept in English on purpose
|
||||
(translate="no", and whatever holds it) keep the Latin look. */
|
||||
html:lang(zh)
|
||||
body
|
||||
:not(
|
||||
#intel-hud,
|
||||
#intel-hud *,
|
||||
[translate='no'],
|
||||
[translate='no'] *,
|
||||
:has([translate='no'])
|
||||
) {
|
||||
letter-spacing: 0.02em !important;
|
||||
}
|
||||
|
||||
/* Below about 9px a Chinese glyph blurs: lift the dock's voice labels and the
|
||||
panel layer buttons to that floor. */
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user