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

This commit is contained in:
Said Turksever committed 2026-10-30 15:17:15 +00:00
commit 828417f98b
162 files changed
+22045 -1988

No files matched your search

+77 -2
View File
@@ -5,6 +5,7 @@ import {
applyDocumentTranslations,
formatDate,
formatNumber,
formatRelativeTime,
msg,
persistLocaleAndReload,
setLocale,
@@ -33,8 +34,23 @@ function memoryStorage(initial = {}) {
test('normalizeLocale folds regional variants and rejects unshipped tags', () => {
assert.equal(normalizeLocale('ru-RU'), 'ru');
assert.equal(normalizeLocale(' RU '), 'ru');
assert.equal(normalizeLocale('uk'), 'uk');
assert.equal(normalizeLocale('uk-UA'), 'uk');
assert.equal(normalizeLocale('en_GB'), 'en');
assert.equal(normalizeLocale('de'), 'de');
assert.equal(normalizeLocale('de-AT'), 'de');
assert.equal(normalizeLocale('de_CH'), 'de');
assert.equal(normalizeLocale('fr'), 'fr');
assert.equal(normalizeLocale('fr-CA'), 'fr');
assert.equal(normalizeLocale('FR_be'), 'fr');
assert.equal(normalizeLocale('pt-BR'), 'pt');
assert.equal(normalizeLocale('pt_PT'), 'pt');
assert.equal(normalizeLocale('xx'), null);
assert.equal(normalizeLocale('it-IT'), 'it');
assert.equal(normalizeLocale('it_CH'), 'it');
assert.equal(normalizeLocale('ja'), 'ja');
assert.equal(normalizeLocale('ja-JP'), 'ja');
assert.equal(normalizeLocale('JA_jp'), 'ja');
// Explicit script tags take precedence over regional defaults.
assert.equal(normalizeLocale('zh'), 'zh-CN');
assert.equal(normalizeLocale('zh-CN'), 'zh-CN');
@@ -68,7 +84,7 @@ test('resolveLocale: ?lang= wins, then the stored choice, then browser languages
resolveLocale({
location: { search: '?lang=xx' },
storage: memoryStorage(),
languages: ['de-DE', 'ru-RU'],
languages: ['ko-KR', 'ru-RU'],
}),
'ru',
);
@@ -76,7 +92,15 @@ test('resolveLocale: ?lang= wins, then the stored choice, then browser languages
resolveLocale({
location: { search: '' },
storage: memoryStorage(),
languages: ['de'],
languages: ['de-DE', 'ru-RU'],
}),
'de',
);
assert.equal(
resolveLocale({
location: { search: '' },
storage: memoryStorage(),
languages: ['ko'],
}),
'en',
);
@@ -142,6 +166,36 @@ test('tn() picks English and Russian plural forms', () => {
assert.match(label(21), / 21 КЛЮЧ(?![А-ЯЁ])/);
});
test('tn() picks the Portuguese one, many and other forms', () => {
const label = (count) =>
tn(
count,
'POWER UP · {count} KEY WAITING',
'POWER UP · {count} KEYS WAITING',
);
setLocale('pt');
assert.equal(label(0), 'ATIVAR · 0 CHAVE PENDENTE');
assert.equal(label(1), 'ATIVAR · 1 CHAVE PENDENTE');
assert.equal(label(3), 'ATIVAR · 3 CHAVES PENDENTES');
assert.equal(label(1_000_000), 'ATIVAR · 1000000 DE CHAVES PENDENTES');
});
test('tn() picks the four Ukrainian plural forms', () => {
const label = (count) =>
tn(
count,
'POWER UP · {count} KEY WAITING',
'POWER UP · {count} KEYS WAITING',
);
setLocale('uk');
assert.match(label(1), / 1 КЛЮЧ(?![А-ЯҐЄІЇ])/);
assert.match(label(3), / 3 КЛЮЧІ(?![А-ЯҐЄІЇ])/);
assert.match(label(5), / 5 КЛЮЧІВ(?![А-ЯҐЄІЇ])/);
assert.match(label(11), / 11 КЛЮЧІВ(?![А-ЯҐЄІЇ])/);
assert.match(label(21), / 21 КЛЮЧ(?![А-ЯҐЄІЇ])/);
assert.match(label(1.5), / 1\.5 КЛЮЧА(?![А-ЯҐЄІЇ])/);
});
test('t() context separates identical English with different meanings', () => {
assert.equal(t('CLEAR', { context: 'action' }), 'CLEAR');
setLocale('ru');
@@ -164,7 +218,13 @@ test('Intl formatters follow the active locale', () => {
assert.equal(formatNumber(1234.5), '1,234.5');
setLocale('ru');
assert.equal(formatDate(date, options), '15 января 2026 г.');
assert.equal(formatRelativeTime(-5, 'minute'), '5 минут назад');
assert.equal(formatNumber(1234.5), '1 234,5');
// Japanese keeps a number and its unit together, as the catalog does.
setLocale('ja');
assert.equal(formatDate(date, options), '2026年1月15日');
assert.equal(formatRelativeTime(-5, 'minute'), '5分前');
assert.equal(formatRelativeTime(-3, 'day'), '3日前');
});
function fakeElement({ text = '', attributes = {} } = {}) {
@@ -200,6 +260,21 @@ test('applyDocumentTranslations translates opted-in text and attributes in place
assert.equal(unknown.textContent, 'No such message');
});
test('applyDocumentTranslations reads data-i18n-context for element text', () => {
const option = fakeElement({
text: 'All',
attributes: { 'data-i18n-context': 'radio tag' },
});
const doc = {
querySelectorAll: (selector) =>
selector === '[data-i18n]' ? [option] : [],
};
setLocale('pt');
applyDocumentTranslations(doc);
assert.equal(option.textContent, t('All', { context: 'radio tag' }));
assert.notEqual(option.textContent, t('All'));
});
test('persistLocaleAndReload keeps the choice in ?lang when storage is blocked', () => {
const calls = [];
const location = {
+33 -5
View File
@@ -16,12 +16,29 @@ import {
normalizeLocale,
writeStoredLocale,
} from './locale.js';
import de from './locales/de.json' with { type: 'json' };
import es from './locales/es.json' with { type: 'json' };
import fr from './locales/fr.json' with { type: 'json' };
import it from './locales/it.json' with { type: 'json' };
import ja from './locales/ja.json' with { type: 'json' };
import pt from './locales/pt.json' with { type: 'json' };
import ru from './locales/ru.json' with { type: 'json' };
import uk from './locales/uk.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, 'zh-Hant': zhHant });
const CATALOGS = Object.freeze({
de,
es,
fr,
it,
ja,
pt,
ru,
uk,
'zh-CN': zhCN,
'zh-Hant': zhHant,
});
let currentLocale = DEFAULT_LOCALE;
@@ -201,13 +218,19 @@ export function formatUtcDayMonth(value) {
/**
* Locale-aware relative time (Intl.RelativeTimeFormat), e.g.
* formatRelativeTime(-5, 'minute') → '5 minutes ago' / '5 минут назад'.
* formatRelativeTime(-5, 'minute') → '5 minutes ago' / '5 минут назад' /
* '5分前'.
*/
export function formatRelativeTime(value, unit = 'second', options) {
return cachedFormatter('relative', Intl.RelativeTimeFormat, {
const text = cachedFormatter('relative', Intl.RelativeTimeFormat, {
numeric: 'auto',
...options,
}).format(value, unit);
// CLDR spaces Japanese (5 分前); the ja catalog writes the number and its
// unit together (5分前), so the two must not differ on one screen.
return currentLocale === 'ja'
? text.replace(/(?<=\d) (?=\p{Script=Han})/gu, '')
: text;
}
/** Collapse markup whitespace so a message reads the same in HTML and JS. */
@@ -221,7 +244,9 @@ function normalizeMarkupText(text) {
* Markup opt-in: the element's own English text or attribute is the source.
* <span data-i18n>Normal</span>
* <button title="Pin" data-i18n-title>…</button>
* Also data-i18n-aria-label and data-i18n-placeholder.
* Also data-i18n-aria-label and data-i18n-placeholder. data-i18n-context
* gives the element's text a context, as t(text, { context }) does:
* <option data-i18n data-i18n-context="radio tag">All</option>
*/
const DOCUMENT_TRANSLATION_TARGETS = Object.freeze([
['data-i18n', null],
@@ -246,7 +271,10 @@ export function applyDocumentTranslations(doc) {
? element.getAttribute(attribute)
: normalizeMarkupText(element.textContent);
if (!source) continue;
const text = t(source);
const context = attribute
? null
: element.getAttribute?.('data-i18n-context');
const text = context ? t(source, { context }) : t(source);
if (text === source) continue;
if (attribute) element.setAttribute(attribute, text);
else element.textContent = text;
+21
View File
@@ -0,0 +1,21 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { setLocale } from './index.js';
import {
cockpitUnitCase,
formatCockpitDistance,
} from '../ui/cockpitPresentation.js';
test('Japanese cockpit distances keep the unit in its own case', () => {
try {
setLocale('ja');
assert.equal(formatCockpitDistance(14000), '14 km');
assert.equal(formatCockpitDistance(9500), '9.5 km');
assert.equal(cockpitUnitCase('km'), 'km');
setLocale('en');
assert.equal(formatCockpitDistance(14000), '14 KM');
assert.equal(cockpitUnitCase('km'), 'KM');
} finally {
setLocale('en');
}
});
+22
View File
@@ -0,0 +1,22 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { setLocale } from './index.js';
import { hudQuantity, LOCALIZED_LOCALITY } from '../hudLocalityFormat.js';
import { formatCockpitDistance } from '../ui/cockpitPresentation.js';
// revmain-opus-ja-i1-1: ja has no case, so HUD units keep the catalog case
// (km), as the cockpit already does and docs/translations/ja.md requires.
test('Japanese HUD units keep their case, like the cockpit', () => {
try {
setLocale('ja');
assert.equal(formatCockpitDistance(14000), '14 km');
assert.equal(hudQuantity(13, 'km'), '13 km');
assert.equal(hudQuantity(1.5, 'km', 1), '1.5 km');
assert.equal(
LOCALIZED_LOCALITY.near({ poi: 'Shibuya', city: 'Tokyo', distKm: 13 }),
'SHIBUYA(東京)付近 13 km',
);
} finally {
setLocale('en');
}
});
+21 -1
View File
@@ -8,13 +8,31 @@ 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', 'zh-Hant']);
export const LOCALES = Object.freeze([
'en',
'de',
'es',
'fr',
'it',
'ja',
'pt',
'ru',
'uk',
'zh-CN',
'zh-Hant',
]);
/** Each locale named in its own language, for the selector. */
export const LOCALE_NAMES = Object.freeze({
en: 'English',
de: 'Deutsch',
es: 'Español',
fr: 'Français',
it: 'Italiano',
ja: '日本語',
pt: 'Português (Brasil)',
ru: 'Русский',
uk: 'Українська',
'zh-CN': '中文(简体)',
'zh-Hant': '中文(繁體)',
});
@@ -24,6 +42,8 @@ const RTL_LOCALES = Object.freeze([]);
/**
* Map any language tag to a shipped locale: 'ru-RU' and 'RU' → 'ru',
* 'de-AT' and 'de_CH' → 'de', 'ja-JP' → 'ja',
* 'pt-BR' and 'pt-PT' → 'pt' (Brazilian Portuguese),
* 'zh', 'zh-CN' and 'zh-Hans-SG' → 'zh-CN';
* 'zh-Hant', 'zh-TW', 'zh-HK' and 'zh-MO' → 'zh-Hant'.
* @returns {string|null}
File diff suppressed because it is too large. Load diff
+7 -15
View File
@@ -66,8 +66,6 @@
"Anime": "Anime",
"Apply high-contrast monochrome film-noir grading.": "Aplica una gradación monocroma de alto contraste estilo film noir.",
"Noir": "Noir",
"Add a cold, snowy whiteout treatment to the scene.": "Añade a la escena un tratamiento frío de ventisca blanca.",
"Snow": "Nieve",
"MAP SOURCE": "FUENTE DEL MAPA",
"Map source": "Fuente del mapa",
"Style": "Estilo",
@@ -76,7 +74,6 @@
"Layout": "Disposición",
"HUD layout": "Disposición del HUD",
"Tactical": "Táctica",
"Operator": "De operador",
"Minimal": "Mínima",
"Detection Overlay (D)": "Superposición de detección (D)",
"Detection overlay": "Superposición de detección",
@@ -198,21 +195,13 @@
"AVAILABLE MISSIONS": "MISIONES DISPONIBLES",
"SELECT A MISSION TO INSPECT": "SELECCIONA UNA MISIÓN PARA INSPECCIONAR",
"LOADING 30-DAY MISSION INDEX": "CARGANDO ÍNDICE DE MISIONES DE 30 DÍAS",
"Open compact Radio controls": "Abrir los controles compactos de Radio",
"Compact Radio controls": "Controles compactos de Radio",
"RADIO": "RADIO",
"RADIO READY": "RADIO LISTA",
"Open detailed Radio controls": "Abrir los controles detallados de Radio",
"Close compact Radio controls": "Cerrar los controles compactos de Radio",
"ENABLE": "ACTIVAR",
"Previous filtered radio station": "Emisora de radio filtrada anterior",
"Previous station": "Emisora anterior",
"Play selected radio station": "Reproducir la emisora de radio seleccionada",
"Play": "Reproducir",
"Next filtered radio station": "Siguiente emisora de radio filtrada",
"Next station": "Siguiente emisora",
"VOLUME": "VOLUMEN",
"Compact Radio volume": "Volumen de la Radio compacta",
"Cockpit display and Radio controls": "Controles de visualización y Radio de la cabina",
"Expand Cockpit display options": "Expandir las opciones de visualización de la cabina",
"Cockpit display options": "Opciones de visualización de la cabina",
@@ -357,8 +346,6 @@
"selected": "seleccionada",
"nearest": "más cercana",
"{action} {target} radio station": "{action} la emisora de radio {target}",
"RADIO STATE UNCERTAIN": "ESTADO DE RADIO SIN CONFIRMAR",
"SYNCING DIRECTORY": "SINCRONIZANDO DIRECTORIO",
"SYNCING": "SINCRONIZANDO",
"station": "emisora",
"Ready": "Listo",
@@ -382,6 +369,11 @@
"many": "{count} de cámaras cargadas · activa CCTV para activarlas",
"other": "{count} cámaras cargadas · activa CCTV para activarlas"
},
"{count} more cameras outside this view · pan or zoom out": {
"one": "{count} cámara más fuera de esta vista · desplaza o aleja",
"many": "{count} de cámaras más fuera de esta vista · desplaza o aleja",
"other": "{count} cámaras más fuera de esta vista · desplaza o aleja"
},
"No summary available.": "No hay resumen disponible.",
"Celestial ring — available in Normal style": "Anillo celestial — disponible en el estilo Normal",
"Detection overlay: {mode}": "Superposición de detección: {mode}",
@@ -2230,6 +2222,8 @@
"GOOGLE_MAPS_API_KEY is not set": "GOOGLE_MAPS_API_KEY no está configurada",
"The Google Maps JavaScript API could not load": "No se pudo cargar la API de Maps JavaScript de Google",
"Weather request timed out": "La petición del clima agotó el tiempo de espera",
"Cyclone request timed out": "La petición de ciclones agotó el tiempo de espera",
"Malformed cyclone snapshot": "Instantánea de ciclones mal formada",
"Malformed weather manifest": "Manifiesto del clima mal formado",
"Weather HTTP {status}": "Clima: HTTP {status}",
"Weather image HTTP {status}": "Imagen del clima: HTTP {status}",
@@ -2445,8 +2439,6 @@
"Exit cockpit to fly to a route": "Sal de la cabina para volar a una ruta",
"Exit cockpit to fly to a file": "Sal de la cabina para volar a un archivo",
"Exit cockpit to fly to a scene": "Sal de la cabina para volar a una escena",
"Go to expanded Radio section": "Ir a la sección de Radio expandida",
"Expand Radio section in Context": "Expandir la sección de Radio en Contexto",
"New scene name": "Nombre de la nueva escena",
"Delete scene \"{scene}\" and all shots?": "¿Eliminar la escena \"{scene}\" y todos sus planos?",
"Detailed outline unavailable": "Contorno detallado no disponible",
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+8 -15
View File
@@ -342,6 +342,12 @@
"many": "Загружено {count} камер · включите CCTV, чтобы активировать",
"other": "Загружено {count} камеры · включите CCTV, чтобы активировать"
},
"{count} more cameras outside this view · pan or zoom out": {
"one": "Ещё {count} камера вне этого вида · сдвиньте или отдалите",
"few": "Ещё {count} камеры вне этого вида · сдвиньте или отдалите",
"many": "Ещё {count} камер вне этого вида · сдвиньте или отдалите",
"other": "Ещё {count} камеры вне этого вида · сдвиньте или отдалите"
},
"Enable CCTV to load camera intersections": "Включите CCTV, чтобы загрузить камеры на перекрёстках",
"Enable CCTV to start camera-linked intelligence summaries.": "Включите CCTV, чтобы запускать сводки по камерам.",
"No summary available.": "Сводка недоступна.",
@@ -416,7 +422,6 @@
"Thermal": "Тепловизор",
"Anime": "Аниме",
"Noir": "Нуар",
"Snow": "Снег",
"Current cockpit vision style: {style}. Activate for next style.": "Текущий стиль обзора кабины: {style}. Активируйте, чтобы переключить на следующий.",
"Current style: {style} — click for next": "Текущий стиль: {style} — нажмите для следующего",
"AIRCRAFT": "САМОЛЁТ",
@@ -553,8 +558,6 @@
"Expand Cockpit display options": "Развернуть параметры экрана кабины",
"Collapse Cockpit Radio controls": "Свернуть управление радио кабины",
"Expand Cockpit Radio controls": "Развернуть управление радио кабины",
"Close compact Radio controls": "Закрыть компактное управление радио",
"Open compact Radio controls": "Открыть компактное управление радио",
"DIRECTORY BAND": "ДИАПАЗОН КАТАЛОГА",
"{category} BAND": "ДИАПАЗОН {category}",
"NO STATION SELECTED": "СТАНЦИЯ НЕ ВЫБРАНА",
@@ -568,9 +571,6 @@
"selected": "выбранную",
"nearest": "ближайшую",
"{action} {target} radio station": "{action} {target} радиостанцию",
"RADIO STATE UNCERTAIN": "СОСТОЯНИЕ РАДИО НЕОПРЕДЕЛЁННО",
"SYNCING DIRECTORY": "СИНХРОНИЗАЦИЯ КАТАЛОГА",
"RADIO READY": "РАДИО ГОТОВО",
"SYNCING": "СИНХРОНИЗАЦИЯ",
"READY": "ГОТОВО",
"station": "станция",
@@ -690,7 +690,6 @@
"Simulate FLIR-style thermal contrast. Turn up Ironbow for color.": "Имитирует тепловой контраст в стиле FLIR. Для цвета увеличьте Ironbow.",
"Apply bright cel-shaded color and illustrated outlines.": "Применяет яркие cel-shading-цвета и рисованные контуры.",
"Apply high-contrast monochrome film-noir grading.": "Применяет высококонтрастную монохромную цветокоррекцию в стиле нуар.",
"Add a cold, snowy whiteout treatment to the scene.": "Добавляет в сцену холодный снежный эффект «белой мглы».",
"Expand LOCATION": "Развернуть панель «Место»",
"Collapse panel": "Свернуть панель",
"Pin location tray": "Закрепить панель места",
@@ -710,9 +709,6 @@
"FLIR": "FLIR",
"Style": "Стиль",
"STATION SITE": "САЙТ СТАНЦИИ",
"Open detailed Radio controls": "Открыть подробное управление радио",
"Previous station": "Предыдущая станция",
"Next station": "Следующая станция",
"Expand panel": "Развернуть панель",
"CONTACTS": "КОНТАКТЫ",
"Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "Перебирает ближайшие контакты выбранного типа — самолёты, суда, объекты. Спутники отслеживаются отдельно.",
@@ -724,13 +720,11 @@
"Next filtered station": "Следующая отфильтрованная станция",
"STOP": "СТОП",
"Stop radio playback": "Остановить воспроизведение радио",
"Compact Radio controls": "Компактное управление радио",
"Context mode": "Режим контекста",
"Contact Context actions": "Действия контекста контакта",
"Available Space Missions": "Доступные космические миссии",
"LOADING 30-DAY MISSION INDEX": "ЗАГРУЗКА ИНДЕКСА МИССИЙ ЗА 30 ДНЕЙ",
"Radio playback": "Воспроизведение радио",
"Compact Radio volume": "Громкость компактного радио",
"Radio volume": "Громкость радио",
"STATION TAG": "ТЕГ СТАНЦИИ",
"DRAG TO TUNE": "ТЯНИТЕ ДЛЯ НАСТРОЙКИ",
@@ -777,7 +771,6 @@
"Fade": "Затухание",
"Outside": "Снаружи",
"Tactical": "Тактический",
"Operator": "Операторский",
"Minimal": "Минимальный",
"HUD layout": "Макет HUD",
"Layout": "Макет",
@@ -2266,6 +2259,8 @@
"GOOGLE_MAPS_API_KEY is not set": "GOOGLE_MAPS_API_KEY не задан",
"The Google Maps JavaScript API could not load": "Не удалось загрузить Google Maps JavaScript API",
"Weather request timed out": "Истекло время ожидания запроса погоды",
"Cyclone request timed out": "Истекло время ожидания запроса циклонов",
"Malformed cyclone snapshot": "Некорректный снимок циклонов",
"Malformed weather manifest": "Некорректный манифест погоды",
"Weather HTTP {status}": "Погода: HTTP {status}",
"Weather image HTTP {status}": "Снимок погоды: HTTP {status}",
@@ -2486,8 +2481,6 @@
"Exit cockpit to fly to a route": "Выйдите из кабины, чтобы перелететь к маршруту",
"Exit cockpit to fly to a file": "Выйдите из кабины, чтобы перелететь к файлу",
"Exit cockpit to fly to a scene": "Выйдите из кабины, чтобы перелететь к сцене",
"Go to expanded Radio section": "Перейти к развёрнутому разделу радио",
"Expand Radio section in Context": "Развернуть раздел «Радио» в панели «Контекст»",
"New scene name": "Название новой сцены",
"Delete scene \"{scene}\" and all shots?": "Удалить сцену «{scene}» и все кадры?",
"Detailed outline unavailable": "Подробный контур недоступен",
File diff suppressed because it is too large. Load diff
+5 -15
View File
@@ -1203,6 +1203,8 @@
"Some weather tiles unavailable": "部分天气瓦片不可用",
"Malformed weather manifest": "天气清单格式错误",
"Weather request timed out": "天气请求超时",
"Cyclone request timed out": "气旋请求超时",
"Malformed cyclone snapshot": "气旋快照格式错误",
"Temperature field unavailable": "温度场不可用",
"Mean sea level pressure field unavailable": "平均海平面气压场不可用",
"Wind upstream unavailable": "风场上游数据源不可用",
@@ -1551,6 +1553,9 @@
"{count} cameras loaded · enable CCTV to activate": {
"other": "已加载 {count} 个摄像头 · 启用 CCTV 以激活"
},
"{count} more cameras outside this view · pan or zoom out": {
"other": "此视图外还有 {count} 个摄像头 · 平移或缩小"
},
"Enable CCTV to load camera intersections": "启用 CCTV 以加载路口摄像头",
"Enable CCTV to start camera-linked intelligence summaries.": "启用 CCTV 以开始生成与摄像头关联的情报摘要。",
"No summary available.": "暂无摘要。",
@@ -1624,7 +1629,6 @@
"Thermal": "热成像",
"Anime": "动漫",
"Noir": "黑白",
"Snow": "雪景",
"Current cockpit vision style: {style}. Activate for next style.": "当前座舱视觉样式:{style}。点击可切换到下一个样式。",
"Current style: {style} — click for next": "当前样式:{style}——点击切换到下一个",
"Collapse Contact panel": "收起目标面板",
@@ -1861,10 +1865,6 @@
"STATION UNAVAILABLE": "电台不可用",
"Station unavailable after directory refresh": "目录刷新后该电台不可用",
"Radio could not {action} cleanly": "电台未能正常{action}",
"Go to expanded Radio section": "前往展开的电台区域",
"Expand Radio section in Context": "在态势面板中展开电台区域",
"Close compact Radio controls": "关闭精简电台控件",
"Open compact Radio controls": "打开精简电台控件",
"SYNC": "同步",
"RECONCILE": "核对",
"DISABLE": "停用",
@@ -1886,9 +1886,6 @@
"selected": "所选",
"nearest": "最近",
"{action} {target} radio station": "{action}{target}电台",
"RADIO STATE UNCERTAIN": "电台状态不确定",
"SYNCING DIRECTORY": "正在同步目录",
"RADIO READY": "电台就绪",
"SYNCING": "同步中",
"READY": "就绪",
"station": "电台",
@@ -2536,7 +2533,6 @@
"Simulate FLIR-style thermal contrast. Turn up Ironbow for color.": "模拟 FLIR 风格的热成像对比。调高 Ironbow 可显示色彩。",
"Apply bright cel-shaded color and illustrated outlines.": "应用明亮的赛璐珞色彩和插画轮廓线。",
"Apply high-contrast monochrome film-noir grading.": "应用高对比度的黑白电影风格调色。",
"Add a cold, snowy whiteout treatment to the scene.": "为场景添加寒冷的暴雪白化效果。",
"MAP SOURCE": "地图源",
"Map source": "地图源",
"Style": "样式",
@@ -2552,11 +2548,6 @@
"RECENT IMAGERY": "近期影像",
"Imagery days in the box": "视野内的影像天数",
"CONTEXT": "态势",
"Compact Radio controls": "精简电台控件",
"Open detailed Radio controls": "打开详细电台控件",
"Previous station": "上一个电台",
"Next station": "下一个电台",
"Compact Radio volume": "精简电台音量",
"Expand panel": "展开面板",
"Context mode": "态势模式",
"CONTACTS": "目标",
@@ -2617,7 +2608,6 @@
"Layout": "布局",
"HUD layout": "HUD 布局",
"Tactical": "战术",
"Operator": "操作员",
"Minimal": "精简",
"Cyber": "赛博",
"Sonar": "声呐",
+5 -15
View File
@@ -1198,6 +1198,8 @@
"Some weather tiles unavailable": "部分天氣瓦片不可用",
"Malformed weather manifest": "天氣清單格式錯誤",
"Weather request timed out": "天氣請求超時",
"Cyclone request timed out": "氣旋請求超時",
"Malformed cyclone snapshot": "氣旋快照格式錯誤",
"Temperature field unavailable": "溫度場不可用",
"Mean sea level pressure field unavailable": "平均海平面氣壓場不可用",
"Wind upstream unavailable": "風場上游數據源不可用",
@@ -1541,6 +1543,9 @@
"{count} cameras loaded · enable CCTV to activate": {
"other": "已加載 {count} 個攝像頭 · 啟用 CCTV 以激活"
},
"{count} more cameras outside this view · pan or zoom out": {
"other": "此視圖外還有 {count} 個攝像頭 · 平移或縮小"
},
"Enable CCTV to load camera intersections": "啟用 CCTV 以加載路口攝像頭",
"Enable CCTV to start camera-linked intelligence summaries.": "啟用 CCTV 以開始生成與攝像頭關聯的情報摘要。",
"No summary available.": "暫無摘要。",
@@ -1614,7 +1619,6 @@
"Thermal": "熱成像",
"Anime": "動漫",
"Noir": "黑白",
"Snow": "雪景",
"Current cockpit vision style: {style}. Activate for next style.": "當前座艙視覺樣式:{style}。點擊可切換到下一個樣式。",
"Current style: {style} — click for next": "當前樣式:{style}——點擊切換到下一個",
"Collapse Contact panel": "收起目標面板",
@@ -1849,10 +1853,6 @@
"STATION UNAVAILABLE": "電臺不可用",
"Station unavailable after directory refresh": "目錄刷新後該電臺不可用",
"Radio could not {action} cleanly": "電臺未能正常{action}",
"Go to expanded Radio section": "前往展開的電臺區域",
"Expand Radio section in Context": "在態勢面板中展開電臺區域",
"Close compact Radio controls": "關閉精簡電臺控件",
"Open compact Radio controls": "打開精簡電臺控件",
"SYNC": "同步",
"RECONCILE": "核對",
"DISABLE": "停用",
@@ -1874,9 +1874,6 @@
"selected": "所選",
"nearest": "最近",
"{action} {target} radio station": "{action}{target}電臺",
"RADIO STATE UNCERTAIN": "電臺狀態不確定",
"SYNCING DIRECTORY": "正在同步目錄",
"RADIO READY": "電臺就緒",
"SYNCING": "同步中",
"READY": "就緒",
"station": "電臺",
@@ -2524,7 +2521,6 @@
"Simulate FLIR-style thermal contrast. Turn up Ironbow for color.": "模擬 FLIR 風格的熱成像對比。調高 Ironbow 可顯示色彩。",
"Apply bright cel-shaded color and illustrated outlines.": "應用明亮的賽璐珞色彩和插畫輪廓線。",
"Apply high-contrast monochrome film-noir grading.": "應用高對比度的黑白電影風格調色。",
"Add a cold, snowy whiteout treatment to the scene.": "為場景添加寒冷的暴雪白化效果。",
"MAP SOURCE": "地圖源",
"Map source": "地圖源",
"Style": "樣式",
@@ -2540,11 +2536,6 @@
"RECENT IMAGERY": "近期影像",
"Imagery days in the box": "視野內的影像天數",
"CONTEXT": "態勢",
"Compact Radio controls": "精簡電臺控件",
"Open detailed Radio controls": "打開詳細電臺控件",
"Previous station": "上一個電臺",
"Next station": "下一個電臺",
"Compact Radio volume": "精簡電臺音量",
"Expand panel": "展開面板",
"Context mode": "態勢模式",
"CONTACTS": "目標",
@@ -2605,7 +2596,6 @@
"Layout": "佈局",
"HUD layout": "HUD 佈局",
"Tactical": "戰術",
"Operator": "操作員",
"Minimal": "精簡",
"Cyber": "賽博",
"Sonar": "聲吶",
+704
View File
@@ -1,4 +1,170 @@
{
"de": [
"1090 MHz",
"978 MHz UAT",
"AIS: --",
"AIS: {name}",
"ANIME",
"AUTO",
"Alternative",
"Ambient",
"Anime",
"Austin",
"BRG {value}°",
"BRG —",
"Bloom",
"Blues",
"Bus",
"CCTV",
"COCKPIT",
"CRS {value}°",
"CRT",
"Country",
"Cyan",
"Cyber",
"DEST ---°",
"DEST {bearing}",
"Dance",
"Details",
"Dubai",
"EXPORT",
"EXPORT {slot}",
"FALLBACK",
"FILTER",
"FLIR",
"FOV",
"FOV {value}°",
"FT",
"Folk",
"Funk",
"GEO",
"GOOGLE STREET VIEW",
"Global",
"Global · 60°S–60°N",
"Google Street View",
"HDG",
"HDG {value}°",
"Hip-Hop",
"House",
"Hybrid",
"Indie",
"Ironbow",
"Jazz",
"LIVE",
"Landsat 8/9",
"Latin",
"Layout",
"London",
"Los Angeles",
"MIL",
"MINI",
"MISSION",
"MM",
"MONITOR",
"Metal",
"Minimal",
"NAME",
"NASA FIRMS · LIVE",
"NAV",
"NEWS",
"NNW",
"NOIR",
"NORMAL",
"NVG",
"NW",
"New York",
"Noir",
"Normal",
"OK",
"ORBIT",
"ORBIT · ",
"Oldies",
"OpenStreetMap + optional Google Maps Places",
"Operator",
"PAUSE",
"PLAY",
"POS: LIVE",
"POS: {time}Z",
"POSITION {age}",
"PROJ MONITOR",
"Paris",
"Pause",
"Pin",
"Plan",
"Pop",
"Punk",
"RADIO",
"REC",
"REGION",
"RESET",
"Radio",
"Reggae",
"Rock",
"S2 GhostMaps",
"SAR",
"SIG",
"SNAPSHOT",
"SQUAWK {code} · {status}",
"SSW",
"STANDARD",
"START",
"STATION",
"STATUS",
"STATUS · ",
"STD",
"SW",
"SYNC",
"San Francisco",
"Scanlines",
"Scope",
"Sentinel-2",
"Sonar",
"Soul",
"Station",
"Station {id}",
"T minus {seconds}",
"TAGS · {tags}",
"TANKER",
"TEMP",
"Talk",
"Tallinn",
"Techno",
"Trance",
"Turboprop",
"UPSTREAM",
"Upstream HTTP {status}",
"Vignette",
"WHOT/BHOT",
"WIND",
"WNW",
"WSW",
"Washington DC",
"Wind",
"dB",
"feed status|live",
"fire behavior|Minimal",
"ft",
"hPa",
"km",
"km/h",
"min",
"mph",
"radio|NEWS",
"radio|TALK",
"view band|GLOBAL",
"{city} CCTV",
"{county} County",
"{source} HTTP {status}",
"{speed} kn",
"{type} SPD: {speed} HDG: {heading}",
"{value} KM",
"{value} KM DOWNRANGE",
"{value} KM/H",
"{value} KM/S",
"ΔN",
"Ⅱ PAUSE",
"▲ {frp} MW"
],
"es": [
"1090 MHz",
"978 MHz UAT",
@@ -125,6 +291,531 @@
"ΔN",
"▲ {frp} MW"
],
"fr": [
"1090 MHz",
"978 MHz UAT",
"ALTITUDE",
"ALTITUDE · FT",
"ANIME",
"AUTO",
"Action",
"Allocation",
"Altitude {altitude}",
"Ambient",
"Anime",
"Austin",
"Aviation / Marine",
"BRG {value}°",
"BRG —",
"Bloom",
"Blues",
"Bus",
"CAL {value}",
"CAL · {badge}",
"CALIBRATION",
"CARGO",
"CAUSE",
"CCTV",
"CCTV OFF",
"CCTV ON",
"COCKPIT",
"COMMERCIAL",
"CONTACT",
"CONTACTS",
"CONTACTS · 250 KM",
"CRS {value}°",
"CRT",
"Country",
"Cyan",
"Cyber",
"Cyclones · NHC / CPHC",
"DENSE",
"DENSE ···",
"DENSE ✕",
"DEST ---°",
"DEST {bearing}",
"DESTINATION",
"Dance",
"Drone",
"ENE",
"ESE",
"EUROPE",
"FLIR",
"FM MHZ",
"FOV",
"FOV {value}°",
"FT",
"Ferry",
"Folk",
"Funk",
"GAIN",
"GEO",
"GOOGLE STREET VIEW",
"Gain",
"Google Street View",
"Grain",
"HDG",
"HDG {value}°",
"Hip-Hop",
"House",
"IMAGE",
"Indie",
"Ironbow",
"Jazz",
"LOCAL",
"Landsat 8/9",
"Landsat 8/9 via HLS",
"Los Angeles",
"MIC",
"MIL",
"MINI",
"MISSION",
"MM",
"MSG/S",
"Metal",
"Minimal",
"Minutes",
"Mode",
"NAV",
"NE",
"NNE",
"NOIR",
"NORMAL",
"NVG",
"New York",
"Noir",
"Normal",
"OFF",
"OK",
"ON",
"ON/OFF",
"Oldies",
"PAUSE",
"POSITION {age}",
"PROJ OFF",
"PROJECTION OFF",
"PROJECTION ON",
"PROPAGATION",
"Paris",
"Pause",
"Plan",
"Pop",
"Punk",
"RADIO",
"REC",
"Radio",
"Reggae",
"Rock",
"S2 GhostMaps",
"SAR",
"SE",
"SIG",
"SOURCE",
"SQUAWK {code} · {status}",
"SRC {value}",
"SSE",
"STANDARD",
"STATION",
"STD",
"San Francisco",
"Satellites",
"Saturation",
"Sonar",
"Soul",
"Source",
"Station",
"Station {id}",
"Style",
"TAGS · {tags}",
"TEMP",
"TYPE",
"Tallinn",
"Techno",
"Tokyo",
"Train",
"Trance",
"USB {rate} msg/s",
"VOLUME",
"VS",
"WHOT/BHOT",
"audio {state}",
"dB",
"ft",
"hPa",
"image",
"installation class|BASE",
"installation class|INSTALLATION",
"km",
"km/h",
"max {value} MW",
"min",
"mini",
"mph",
"nominal",
"satellite",
"standard",
"station",
"voice|OFF",
"{count} MSGS",
"{count} SOURCES",
"{count} messages",
"{n} cyclones",
"{n} satellites",
"{rate} msg/s",
"{value} KM",
"{value} KM/H",
"{value} KM/S",
"ΔE",
"ΔN",
"Ⅱ PAUSE",
"▲ {frp} MW"
],
"it": [
"1090 MHz",
"978 MHz UAT",
"AFRICA",
"AIS: --",
"AIS: {name}",
"ALT {altitude} FT · GS {speed} KT · TRK {track}",
"ANIME",
"ASIA",
"AUTO",
"AUTO HOP OFF",
"AUTO HOP ON",
"Alternative",
"Ambient",
"Anime",
"Area",
"Austin",
"BRG {value}°",
"BRG —",
"Bloom",
"Blues",
"Business jet",
"CAL {value}",
"CAL · {badge}",
"CARGO",
"CCTV",
"CCTV OFF",
"CCTV ON",
"CH {index} / {total}",
"COCKPIT",
"CRS {value}°",
"CRT",
"Country",
"Cyber",
"DEST ---°",
"DEST {bearing}",
"DISPLAY",
"Dance",
"Drone",
"Dubai",
"ENE",
"ESE",
"FLIR",
"FM MHZ",
"FOCUS",
"FOV",
"FOV {value}°",
"FT",
"Folk",
"Funk",
"GEO",
"GOOGLE STREET VIEW",
"Google Street View",
"HDG",
"HDG {value}°",
"Hip-Hop",
"House",
"Indie",
"Ironbow",
"Jazz",
"KTS",
"LIVE",
"Landsat 8/9",
"Layout",
"Los Angeles",
"MIC",
"MIL",
"MINI",
"MM",
"MONITOR",
"MSG/S",
"Metal",
"NASA FIRMS · LIVE",
"NAV",
"NE",
"NNE",
"NOIR",
"NVG",
"New York",
"Noir",
"OCEANIA",
"OFF",
"OK",
"ON",
"ON/OFF",
"Oldies",
"PAYLOAD",
"PLAY",
"POS: LIVE",
"POS: {time}Z",
"PRECIP",
"Pop",
"Punk",
"RADIO",
"REC",
"RESET",
"RESET CAL",
"Radio",
"Reggae",
"Rock",
"S2 GhostMaps",
"SAR",
"SE",
"SIG",
"SNAPSHOT",
"SQUAWK {code} · {status}",
"SSE",
"STANDARD",
"STD",
"STOP",
"SYNC",
"San Francisco",
"Scope",
"Sentinel-2",
"Sonar",
"Soul",
"TEMP",
"TIMEOUT",
"Talk",
"Tallinn",
"Techno",
"Tokyo",
"Tram",
"Trance",
"UPSTREAM",
"USB {rate} msg/s",
"Upstream HTTP {status}",
"V/S {rate} FPM",
"VIEWSHED",
"VIEWSHED ON",
"VOLUME",
"VS",
"WHOT/BHOT",
"Washington DC",
"World",
"audio {state}",
"dB",
"feed status|live",
"ft",
"hPa",
"installation class|BASE",
"km",
"km/h",
"max {value} MW",
"min",
"mini",
"mph",
"radio|TALK",
"satellite",
"standard",
"view band|METRO",
"voice|OFF",
"{count} MSGS",
"{count} feeds live",
"{count} scenes",
"{rate} msg/s",
"{scene} / {pack}: {status}. File: {path}. {attribution} · {license}",
"{source} HTTP {status}",
"{speed} kn",
"{speed}KT",
"{value} KM",
"{value} KM DOWNRANGE",
"{value} KM/H",
"{value} KM/S",
"{value} conf",
"ΔE",
"ΔN",
"▲ {frp} MW"
],
"ja": [
"1090 MHz",
"978 MHz UAT",
"AIS: --",
"AIS: {name}",
"BRG {value}°",
"BRG —",
"CAL {value}",
"CAL · {badge}",
"CCTV",
"CH {index} / {total}",
"CRS {value}°",
"CRT",
"FLIR",
"FLT",
"FM MHZ",
"FOV",
"FOV {value}°",
"FT",
"GEO",
"HDG",
"HDG {value}°",
"Ironbow",
"Landsat 8/9",
"MSG/S",
"NVG",
"POS: {time}Z",
"REC",
"S2 GhostMaps",
"SRC {value}",
"Sentinel-2",
"USB {rate} msg/s",
"WHOT/BHOT",
"dB",
"ft",
"hPa",
"km",
"km/h",
"kt",
"mph",
"{city} CCTV",
"{rate} msg/s",
"{source} HTTP {status}",
"{speed} kn",
"{type} SPD: {speed} HDG: {heading}",
"ΔE",
"ΔN",
"▲ {frp} MW"
],
"pt": [
"1090 MHz",
"978 MHz UAT",
"AIS: --",
"AIS: {name}",
"ALT {altitude} FT · GS {speed} KT · TRK {track}",
"ALTITUDE",
"ALTITUDE · FT",
"ANIME",
"AUTO",
"Altitude {altitude}",
"Anime",
"Austin",
"BIKE",
"BRG {value}°",
"BRG —",
"Bloom",
"Blues",
"CAL {value}",
"CAL · {badge}",
"CCTV",
"CRS {value}°",
"CRT",
"Country",
"DEST ---°",
"DEST {bearing}",
"Dance",
"Drone",
"Dubai",
"ENE",
"ESE",
"FLIR",
"FLT",
"FM MHZ",
"FOV",
"FOV {value}°",
"FT",
"Folk",
"Funk",
"GEO",
"GOOGLE STREET VIEW",
"Global",
"Global · 60°S–60°N",
"Google Street View",
"HDG",
"HDG {value}°",
"Hip-Hop",
"House",
"Indie",
"Ironbow",
"Jazz",
"LOCAL",
"Landsat 8/9",
"Landsat 8/9 via HLS",
"Layout",
"Los Angeles",
"MIC",
"MIL",
"MINI",
"MM",
"MONITOR",
"MSG/S",
"Metal",
"NAV",
"NE",
"NNE",
"NOIR",
"NORMAL",
"NVG",
"Noir",
"Normal",
"OCEANIA",
"OK",
"POS: {time}Z",
"PRECIP",
"Paris",
"Pop",
"Punk",
"RANGE",
"REC",
"Reggae",
"Rock",
"S2 GhostMaps",
"SAR",
"SE",
"SIG",
"SNAPSHOT",
"SQUAWK {code} · {status}",
"SSE",
"STATUS",
"STATUS · ",
"Sentinel-2",
"Sentinel-2 via HLS",
"Sonar",
"Soul",
"TAGS · {tags}",
"TEMP",
"Tallinn",
"Techno",
"Trance",
"USB {rate} msg/s",
"V/S {rate}",
"VISUAL",
"VOLUME",
"VS",
"WHOT/BHOT",
"WX",
"dB",
"fire confidence|nominal",
"ft",
"hPa",
"installation class|BASE",
"km",
"km/h",
"kt",
"min",
"mini",
"mph",
"nominal",
"view band|GLOBAL",
"{rate} msg/s",
"{type} SPD: {speed} HDG: {heading}",
"{value} KM",
"{value} KM/H",
"{value} KM/S",
"ΔN",
"▲ {frp} MW"
],
"ru": [
"AIS: --",
"AIS: {name}",
@@ -143,6 +834,19 @@
"VS",
"WHOT/BHOT"
],
"uk": [
"AIS: --",
"AIS: {name}",
"CCTV",
"CRT",
"FLIR",
"Ironbow",
"Landsat 8/9",
"NVG",
"REC",
"S2 GhostMaps",
"WHOT/BHOT"
],
"zh-CN": [
"1090 MHz",
"978 MHz UAT",
+3 -1
View File
@@ -17,6 +17,9 @@ test('Chinese HUD and cockpit units keep their case without spacing coordinates'
assert.equal(formatCockpitDistance(14000), '14\u00a0km');
assert.equal(LOCALIZED_LOCALITY.sector({ latDeg: 21.33, lonDeg: -77.5 }),
'扇区 21.33N 77.50W');
setLocale('ja');
assert.equal(hudQuantity(13, 'km'), '13 km');
assert.equal(hudQuantity(1.5, 'km', 1), '1.5 km');
setLocale('en');
assert.equal(hudQuantity(0, 'km'), '0KM');
assert.equal(hudQuantity(2069, 'km', 1), '2069.0KM');
@@ -88,7 +91,6 @@ test('radio option text and accessible name use the same localized parentheses',
setLocale(locale);
const host = {
_radioPanel: element(), _radioFilter: element(),
_syncContextRadioLauncherState() {},
actions: { getLifecycle: () => null, scheduleLayout() {} },
};
renderRadioState.call(host, {