Merge branch 'main' into fix/remove-operator-hud-layout

This commit is contained in:
Said Turksever authored and GitHub committed 2026-10-29 15:39:45 +00:00
commit 8348402a56
14 files changed
+3442 -9

No files matched your search

+3
View File
@@ -36,6 +36,9 @@ test('normalizeLocale folds regional variants and rejects unshipped tags', () =>
assert.equal(normalizeLocale('uk'), 'uk');
assert.equal(normalizeLocale('uk-UA'), 'uk');
assert.equal(normalizeLocale('en_GB'), 'en');
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);
+2
View File
@@ -17,6 +17,7 @@ import {
writeStoredLocale,
} from './locale.js';
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 pt from './locales/pt.json' with { type: 'json' };
import ru from './locales/ru.json' with { type: 'json' };
@@ -26,6 +27,7 @@ import zhHant from './locales/zh-Hant.json' with { type: 'json' };
const CATALOGS = Object.freeze({
es,
fr,
it,
pt,
ru,
+2
View File
@@ -11,6 +11,7 @@ export const DEFAULT_LOCALE = 'en';
export const LOCALES = Object.freeze([
'en',
'es',
'fr',
'it',
'pt',
'ru',
@@ -23,6 +24,7 @@ export const LOCALES = Object.freeze([
export const LOCALE_NAMES = Object.freeze({
en: 'English',
es: 'Español',
fr: 'Français',
it: 'Italiano',
pt: 'Português (Brasil)',
ru: 'Русский',
File diff suppressed because it is too large. Load diff
+179
View File
@@ -124,6 +124,185 @@
"Δ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",
@@ -0,0 +1,58 @@
// The CCTV HUD summary shows the CAL badge, source kind and view band in the
// UI language; English keeps its codes, and unknown source codes stay codes.
import test from 'node:test';
import assert from 'node:assert/strict';
import { setLocale } from '../../i18n/index.js';
import { createPresentation } from './presentation.js';
function summary(sourceKind) {
const camera = {
id: 'c1',
city: 'Austin',
name: 'Cam',
rangeM: 100,
fovDeg: 60,
feedType: 'image',
};
const record = { camera };
const presentation = createPresentation({
state: {
_records: [record],
_healthById: new Map(sourceKind ? [['c1', { sourceKind }]] : []),
_showProjection: false,
_coverageMode: 'sector',
},
services: {},
parts: {
selection: { getActiveRecord: () => record },
model: {
sectorAreaKm2: () => 1,
currentViewContext: () => 'street:1:2',
},
geometry: { coverageNeighborCount: () => 0 },
calibration: { deriveCalBadge: () => 'raw-prior' },
},
source: {},
});
return presentation.buildSummaryText();
}
test('en: CAL, SRC and CONTEXT keep their English codes', () => {
setLocale('en');
assert.match(summary('upstream'), /CAL RAW PRIOR · SRC UPSTREAM/);
assert.match(summary(null), /SRC IMAGE · STREET CONTEXT/);
assert.match(summary('upstream-image'), /SRC UPSTREAM-IMAGE/);
});
test('fr: CAL, SRC and CONTEXT values are translated', () => {
setLocale('fr');
try {
assert.match(
summary('upstream'),
/CAL ESTIM\. INITIALE · SRC AMONT · CONTEXTE RUE/,
);
assert.match(summary('upstream-image'), /SRC UPSTREAM-IMAGE/);
} finally {
setLocale('en');
}
});
+18 -5
View File
@@ -31,6 +31,9 @@ const MANY = Object.keys(NAMES);
const codes = (rows) => rows.map((row) => row.code);
// Few enough languages that the picker has no search field.
const UNSEARCHED = ['en', 'es', 'ru', 'uk', 'zh-CN', 'zh-Hant'];
test('the current language comes first, the rest by their own names', () => {
assert.deepEqual(
codes(
@@ -40,7 +43,7 @@ test('the current language comes first, the rest by their own names', () => {
nativeNames: LOCALE_NAMES,
}),
),
['ru', 'uk', 'en', 'es', 'it', 'pt', 'zh-CN', 'zh-Hant'],
['ru', 'uk', 'en', 'es', 'fr', 'it', 'pt', 'zh-CN', 'zh-Hant'],
);
assert.deepEqual(
codes(
@@ -50,7 +53,7 @@ test('the current language comes first, the rest by their own names', () => {
nativeNames: LOCALE_NAMES,
}),
),
['es', 'en', 'it', 'pt', 'ru', 'uk', 'zh-CN', 'zh-Hant'],
['es', 'en', 'fr', 'it', 'pt', 'ru', 'uk', 'zh-CN', 'zh-Hant'],
);
assert.deepEqual(
codes(
@@ -60,7 +63,7 @@ test('the current language comes first, the rest by their own names', () => {
nativeNames: LOCALE_NAMES,
}),
),
['en', 'es', 'it', 'pt', 'ru', 'uk', 'zh-CN', 'zh-Hant'],
['en', 'es', 'fr', 'it', 'pt', 'ru', 'uk', 'zh-CN', 'zh-Hant'],
);
const rows = buildLocaleOptions({
locales: MANY,
@@ -95,6 +98,7 @@ test('each row gets the interface-language name for search, unless it only repea
const byCode = Object.fromEntries(rows.map((row) => [row.code, row]));
assert.equal(byCode.en.localName, 'английский');
assert.equal(byCode.es.localName, 'испанский');
assert.equal(byCode.fr.localName, 'французский');
assert.equal(byCode.ru.localName, '', 'русский only repeats Русский');
const english = buildLocaleOptions({
locales: LOCALES,
@@ -103,6 +107,7 @@ test('each row gets the interface-language name for search, unless it only repea
localName: createLocalNameReader('en'),
});
assert.equal(english.find((row) => row.code === 'es').localName, 'Spanish');
assert.equal(english.find((row) => row.code === 'fr').localName, 'French');
});
test('without Intl.DisplayNames the interface-language name is simply left out', () => {
@@ -165,7 +170,10 @@ test('search folds case and diacritics, and matches either name or the code', ()
test('the search field appears only above the threshold', () => {
assert.equal(LOCALE_SEARCH_THRESHOLD, 8);
assert.equal(createLocalePickerModel({ current: 'en' }).searchable, false);
assert.equal(
createLocalePickerModel({ current: 'en' }).searchable,
LOCALES.length > LOCALE_SEARCH_THRESHOLD,
);
const eight = MANY.slice(0, 8);
assert.equal(
createLocalePickerModel({
@@ -250,6 +258,7 @@ test('type-ahead cycles by first letter and refines with more letters', () => {
test('keyboard model: open on the current language, move, choose, close', () => {
const model = createLocalePickerModel({
locales: UNSEARCHED,
current: 'ru',
localName: createLocalNameReader('ru'),
});
@@ -293,7 +302,11 @@ test('keyboard model: open on the current language, move, choose, close', () =>
test('keyboard model: type-ahead resets after a pause and ignores shortcuts', () => {
let clock = 0;
const model = createLocalePickerModel({ current: 'en', now: () => clock });
const model = createLocalePickerModel({
locales: UNSEARCHED,
current: 'en',
now: () => clock,
});
model.openList();
assert.deepEqual(model.keydown({ key: 'e' }), { handled: true });
assert.equal(model.activeOption.code, 'es');
+4 -1
View File
@@ -963,7 +963,10 @@ export function progressStepLabel(step, label) {
: step
? spokenLabel(step, 24)
: t('Working');
return label ? `${base}: ${spokenLabel(label, 32)}` : base;
// The join is a catalog string so each language spaces its colon.
return label
? t('{name}: {state}', { name: base, state: spokenLabel(label, 32) })
: base;
}
/**
+13
View File
@@ -9,6 +9,7 @@ import {
spokenLabel,
} from './speech.js';
import { countWords, findHedges } from './speechLint.js';
import { setLocale } from '../i18n/index.js';
function envelope(name, result, args) {
return attachVoiceResult(name, result, args);
@@ -459,3 +460,15 @@ test('labels are bounded and cleaned before they are spoken', () => {
'background steps stay on screen',
);
});
test('progress step labels join through the catalog colon', () => {
setLocale('fr');
try {
assert.equal(
progressStepLabel('resolve', 'Capitol'),
'Recherche de lieux\u202f: Capitol',
);
} finally {
setLocale('en');
}
});