Files
gods-eye-view/src/i18n/locale.js
T
German KhokhlovandClaude Opus 5.5 c4cc499d58 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]>
2026-10-21 12:52:34 +04:00

96 lines
3.0 KiB
JavaScript

// Which locale the page runs in: ?lang= → stored choice → browser languages →
// English. Only shipped locales are ever selected.
/** Durable preference key. Matches the repo convention gev:<name>:v1. */
export const LOCALE_STORAGE_KEY = 'gev:locale:v1';
/** Source language and fallback. */
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']);
/** 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',
* 'zh', 'zh-CN' and 'zh-Hans-SG' → 'zh-CN'.
* @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')
return rest.some((part) => ['hant', 'tw', 'hk', 'mo'].includes(part))
? null
: 'zh-CN';
return LOCALES.includes(primary) ? primary : null;
}
function queryLocale(location) {
try {
return normalizeLocale(new URLSearchParams(location?.search).get('lang'));
} catch {
return null;
}
}
function readStoredLocale(storage) {
try {
const store = storage !== undefined ? storage : globalThis.localStorage;
return normalizeLocale(store?.getItem?.(LOCALE_STORAGE_KEY));
} catch {
return null;
}
}
/** Store the choice; false when storage is unavailable. */
export function writeStoredLocale(locale, storage) {
const normalized = normalizeLocale(locale) || DEFAULT_LOCALE;
try {
const store = storage !== undefined ? storage : globalThis.localStorage;
if (typeof store?.setItem !== 'function') return false;
store.setItem(LOCALE_STORAGE_KEY, normalized);
return true;
} catch {
return false;
}
}
/** Resolve the boot locale. Every input is injectable for tests. */
export function resolveLocale({
location = globalThis.location,
storage,
languages = globalThis.navigator?.languages,
} = {}) {
const fromQuery = queryLocale(location);
if (fromQuery) return fromQuery;
const stored = readStoredLocale(storage);
if (stored) return stored;
for (const candidate of Array.isArray(languages) ? languages : []) {
const normalized = normalizeLocale(candidate);
if (normalized) return normalized;
}
return DEFAULT_LOCALE;
}
/** Set <html lang> and <html dir>. */
export function applyDocumentLanguage(doc, locale = DEFAULT_LOCALE) {
const root = doc?.documentElement;
if (!root) return false;
const normalized = normalizeLocale(locale) || DEFAULT_LOCALE;
root.lang = normalized;
root.dir = RTL_LOCALES.includes(normalized) ? 'rtl' : 'ltr';
return true;
}