mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
i18n: fix the LGTM review findings and tidy the branch texts
- i18n scanner: lastError sink, no false sinks on defaults and ternaries - shared src/i18n/marker.js for modules that only mark strings - wind field messages move to wind/fields.js; CITY_POIS imports from cityPois.js - drop review IDs, change history and stale references from comments and docs Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0ed05568f7
commit
20f4cf3763
178 files changed
+918
-948
No files matched your search
+11
-41
@@ -715,10 +715,8 @@ export class StyleManager extends ShellFacade {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mounts the interface language picker in the bottom-right corner
|
||||
* (src/ui/localePicker.js). Choosing another language stores it and
|
||||
* reloads, so nothing re-renders live; it asks first when the reload would
|
||||
* lose work (see _chooseLocale).
|
||||
* Mounts the language picker (src/ui/localePicker.js). A choice reloads the
|
||||
* page; _confirmLocaleReload asks first when that would lose work.
|
||||
* @returns {void}
|
||||
*/
|
||||
_initLocaleSelector() {
|
||||
@@ -776,11 +774,9 @@ export class StyleManager extends ShellFacade {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives every Display-panel row label one shared column, sized in
|
||||
* characters to the longest label of the active language, so the slider
|
||||
* tracks of separate rows start at the same x. The labels are monospace and
|
||||
* often hidden (collapsed rows have no layout), so this counts characters
|
||||
* instead of measuring; the CSS turns the count into a width.
|
||||
* Sizes one Display-panel label column to the longest label in the active
|
||||
* language, so slider tracks line up. Counts characters (labels are
|
||||
* monospace and often hidden); CSS turns the count into a width.
|
||||
* @returns {void}
|
||||
*/
|
||||
_sizeDisplayLabelColumn() {
|
||||
@@ -800,11 +796,8 @@ export class StyleManager extends ShellFacade {
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites a header's trailing text node in place, preserving the leading
|
||||
* dock-label-icon span that a textContent write would destroy (the phase-2
|
||||
* "key-only" headers: .panel-title / .pp-header-label share their element
|
||||
* with the icon, so no data-i18n attribute could target them — see
|
||||
* docs/TRANSLATORS.md, runtime-only static sites).
|
||||
* Rewrites a header's last text node, keeping the leading icon span that a
|
||||
* textContent write would destroy.
|
||||
* @param {Element|null} element
|
||||
* @param {string} text
|
||||
* @returns {void}
|
||||
@@ -821,9 +814,8 @@ export class StyleManager extends ShellFacade {
|
||||
}
|
||||
|
||||
/**
|
||||
* Localizes the key-only dock/panel headers at boot: VISUAL PRESETS,
|
||||
* LOCATION, and the DISPLAY rail label. English output is byte-identical to
|
||||
* the static markup these replace.
|
||||
* Translates at boot the header texts that share their element with an
|
||||
* icon or a <br> (data-i18n-runtime in the templates).
|
||||
* @returns {void}
|
||||
*/
|
||||
_applyRuntimeStaticHeaderText() {
|
||||
@@ -835,22 +827,7 @@ export class StyleManager extends ShellFacade {
|
||||
document.querySelector('#location-bar .location-toolbar-label'),
|
||||
t('LOCATION'),
|
||||
);
|
||||
this._setHeaderText(
|
||||
document.querySelector('#pp-toggles .pp-header-label'),
|
||||
t('DISPLAY'),
|
||||
);
|
||||
// The cockpit context kicker ('CONTACT') is visible on the map itself, and
|
||||
// the briefing kicker shares its element with the live-dot <i>; both are
|
||||
// phase-2 key-only sites (see docs/TRANSLATORS.md). The briefing
|
||||
// text node keeps renderBriefPage's leading-space convention.
|
||||
this._setHeaderText(
|
||||
document.querySelector('.cockpit-context-kicker'),
|
||||
t('CONTACT'),
|
||||
);
|
||||
// The phase-2 key-only standby description: one span holds BOTH mode
|
||||
// descriptions split by a literal <br>, so no attribute write was possible
|
||||
// without destroying the markup (docs/TRANSLATORS.md). Split into
|
||||
// two keys and write them at boot, preserving the <br>.
|
||||
// One span holds both mode descriptions around a <br>; keep the <br>.
|
||||
const standbyDescription = document.querySelector(
|
||||
'#context-mode-standby span',
|
||||
);
|
||||
@@ -865,6 +842,7 @@ export class StyleManager extends ShellFacade {
|
||||
),
|
||||
);
|
||||
}
|
||||
// Leading space after the live dot, as showBriefPage writes it.
|
||||
const briefKicker = document.querySelector('#cockpit-brief-kicker');
|
||||
if (briefKicker) {
|
||||
this._setHeaderText(briefKicker, ` ${t('LIVE SIGNALS')}`);
|
||||
@@ -880,14 +858,6 @@ export class StyleManager extends ShellFacade {
|
||||
_initMapStackControl() {
|
||||
if (!this.mapStackController) return;
|
||||
|
||||
// Key-only i18n sites (mapStackChips.test.mjs pins the static markup
|
||||
// verbatim, so no data-i18n* attribute can be added): localize at this
|
||||
// runtime write instead. The locale resolves before UI init and never
|
||||
// changes without a reload, so an init-time write is sufficient.
|
||||
if (this._mapSourceLabel)
|
||||
this._mapSourceLabel.textContent = t('MAP SOURCE');
|
||||
this._mapStackChips.setAttribute('aria-label', t('Map source'));
|
||||
|
||||
this._mapSourceControls?.destroy();
|
||||
this._mapSourceControls = createMapSourceControls({
|
||||
container: this._mapStackChips,
|
||||
|
||||
@@ -12,9 +12,8 @@ import {
|
||||
formatCockpitWindDirection,
|
||||
} from './cockpitPresentation.js';
|
||||
|
||||
// regionalModel.weatherCodeLabel lives in the shared node/browser source graph,
|
||||
// so it returns stable English labels; localize them here, at the cockpit
|
||||
// presentation boundary (docs/TRANSLATORS.md).
|
||||
// regionalModel.weatherCodeLabel is shared with node code and returns
|
||||
// English; translate it here.
|
||||
const WEATHER_CONDITION_KEYS = Object.freeze({
|
||||
'CONDITIONS UNKNOWN': msg('CONDITIONS UNKNOWN'),
|
||||
CLEAR: msg('CLEAR'),
|
||||
@@ -79,9 +78,8 @@ export function setBriefAutoRotate(enabled) {
|
||||
: COCKPIT_BRIEF_CYCLE_OFF_HELP;
|
||||
this.briefAutoToggle.setAttribute('aria-label', label);
|
||||
this.briefAutoToggle.title = help;
|
||||
// The English writes above are pinned verbatim by cockpitMarkup.test.mjs;
|
||||
// these t() re-writes are same-value overrides that localize the toggle
|
||||
// under other locales (byte-identical under 'en').
|
||||
// cockpitMarkup.test.mjs pins the English writes above; these translate
|
||||
// them.
|
||||
const labelKey = this.briefAutoRotateEnabled
|
||||
? msg('CYCLE ON')
|
||||
: msg('CYCLE OFF');
|
||||
|
||||
@@ -146,8 +146,6 @@ export function updateHud(
|
||||
this.position.textContent = `${lat} · ${lon}`;
|
||||
}
|
||||
if (this.aircraftMeta) {
|
||||
// State-sibling keys at the writer: each feed state translates
|
||||
// independently so no state can leak English under other locales.
|
||||
const feedState = this.surfaceAcquiring
|
||||
? t('ACQUIRING SURFACE')
|
||||
: this.surfaceFallback
|
||||
|
||||
@@ -55,7 +55,6 @@ export const COCKPIT_BRIEF_PAGES = [
|
||||
id: 'news',
|
||||
kickerKey: msg('REGIONAL NEWS'),
|
||||
subtitleKey: msg('LATEST LOCATION-MATCHED REPORTING'),
|
||||
// The RSS provider name stays English; the rest of the line translates.
|
||||
sourceKey: msg('{source} · LOCATION QUERY · RECENT'),
|
||||
sourceName: 'GOOGLE NEWS RSS', // i18n-ignore: provider name
|
||||
},
|
||||
@@ -63,7 +62,7 @@ export const COCKPIT_BRIEF_PAGES = [
|
||||
id: 'local',
|
||||
kickerKey: msg('LOCAL INFO'),
|
||||
subtitleKey: msg('PLACE / CONDITIONS / POSITION'),
|
||||
// Data-source attribution is a keep-English boundary string.
|
||||
// Attribution stays English.
|
||||
source: 'NATURAL EARTH · OPEN-METEO · UTC',
|
||||
},
|
||||
];
|
||||
@@ -117,7 +116,7 @@ export function compassPointLabel(code) {
|
||||
: code;
|
||||
}
|
||||
|
||||
/** `30.203°N` in the active language: `30,203°С`. */
|
||||
/** `30.203°N` in the active language: `30,203° с. ш.` in Russian. */
|
||||
export function formatCockpitCoordinate(value, positive, negative) {
|
||||
const amount = formatNumber(Math.abs(value), {
|
||||
useGrouping: false,
|
||||
@@ -125,8 +124,7 @@ export function formatCockpitCoordinate(value, positive, negative) {
|
||||
maximumFractionDigits: 3,
|
||||
});
|
||||
const code = value >= 0 ? positive : negative;
|
||||
// Russian writes the hemisphere out ("30,203° с. ш."): a lone «С» after a
|
||||
// degree sign reads as Celsius.
|
||||
// Not «30,203°С»: after a degree sign «С» reads as Celsius.
|
||||
switch (code) {
|
||||
case 'N':
|
||||
return t('{value}°N', { value: amount });
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/** Compose UI controls with the application's existing engines and layer instances. */
|
||||
import { StyleManager as ApplicationShell } from './applicationShell.js';
|
||||
import { LocationSearch } from './location.js';
|
||||
import { CITY_POIS } from '../cityPois.js';
|
||||
import {
|
||||
CITY_POIS,
|
||||
GLOBE_VIEW,
|
||||
flyToGlobeView,
|
||||
flyToPresetLocation,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// The blocked-visibility toast speaks the UI language: the Space Missions
|
||||
// guard reason (English data from contextModePolicy.js) is re-composed with
|
||||
// the layer name localized, a known lifecycle sentence is looked up, and an
|
||||
// unknown provider message passes through.
|
||||
// The blocked-visibility toast reason in the UI language.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { setLocale } from '../i18n/index.js';
|
||||
|
||||
@@ -12,11 +12,9 @@ import {
|
||||
import { localizeLayerName } from './layerNames.js';
|
||||
|
||||
/**
|
||||
* A blocked-visibility reason in the UI language. The lifecycle keeps reasons
|
||||
* English (they also reach voice and tool callers): the Space Missions guard
|
||||
* is re-composed from its template with the layer name localized, any other
|
||||
* known sentence ("Data source unavailable") is looked up as is, and an
|
||||
* unknown one — a live provider message — passes through.
|
||||
* A blocked-visibility reason in the UI language. Reasons stay English (voice
|
||||
* and tools read them): the Space Missions refusal is rebuilt with the layer
|
||||
* name localized, a known sentence is looked up, anything else passes.
|
||||
*/
|
||||
export function localizeVisibilityBlockReason(reason) {
|
||||
if (typeof reason !== 'string' || !reason) return reason;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// K-49: the CCTV "ACQUIRING FRAME" caption lives in CSS `content:` and the
|
||||
// The CCTV "ACQUIRING FRAME" caption lives in CSS `content:` and the
|
||||
// i18n scanner (scripts/i18n.mjs) never reads CSS, so es/ru show it in English.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -13,7 +13,7 @@ const LOCALES = path.join(HERE, '..', 'i18n', 'locales');
|
||||
// street-level.css ' · ESC' is a key cap. Neither is UI copy.
|
||||
const DECORATIVE = new Set(['cyber.css', 'street-level.css']);
|
||||
|
||||
test('K-49: no user-visible text in CSS content: outside decorative themes', () => {
|
||||
test('no user-visible text in CSS content: outside decorative themes', () => {
|
||||
const hits = [];
|
||||
for (const file of readdirSync(STYLES)) {
|
||||
if (!file.endsWith('.css') || DECORATIVE.has(file)) continue;
|
||||
@@ -26,7 +26,7 @@ test('K-49: no user-visible text in CSS content: outside decorative themes', ()
|
||||
assert.deepEqual(hits, []);
|
||||
});
|
||||
|
||||
test('K-49: the CCTV acquisition caption has es and ru translations', () => {
|
||||
test('the CCTV acquisition caption has es and ru translations', () => {
|
||||
for (const lang of ['es', 'ru']) {
|
||||
const dict = JSON.parse(readFileSync(path.join(LOCALES, `${lang}.json`), 'utf8'));
|
||||
assert.ok('ACQUIRING FRAME' in dict, `${lang}.json lacks "ACQUIRING FRAME"`);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// K-47: Flights / Military feed failure texts reach the DATA LAYERS row in the
|
||||
// Flights / Military feed failure texts reach the DATA LAYERS row in the
|
||||
// UI language; the feeds keep them in English.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -40,7 +40,7 @@ const cases = [
|
||||
['malformed rows', 'Malformed adsb.lol aircraft rows'],
|
||||
];
|
||||
|
||||
test('en: feed failure texts read as before', () => {
|
||||
test('en: feed failure texts stay English', () => {
|
||||
setLocale('en');
|
||||
for (const [, message] of cases)
|
||||
assert.equal(
|
||||
|
||||
@@ -212,8 +212,7 @@ const rows = (els) => els['geo-import-list'].children;
|
||||
const action = (row, name) =>
|
||||
row.children.find((c) => c.dataset.action === name);
|
||||
|
||||
// K-27. Before the other tests: the tools they leave behind would answer the
|
||||
// same registry.
|
||||
// Runs first: tools left by later tests would answer the same registry.
|
||||
test('imported files make the language switch ask first', async () => {
|
||||
const t = setup();
|
||||
assert.deepEqual(describeUnsavedWork(), []);
|
||||
|
||||
@@ -130,8 +130,6 @@ export class LayerBindings {
|
||||
]) {
|
||||
const layer = this._dataManager?.layers?.get(id)?.module;
|
||||
if (typeof layer?.attachShellServices !== 'function') continue;
|
||||
// The Cockpit refusal names the destination: a cyclone layer flies to
|
||||
// a storm, the others to their coverage area.
|
||||
const noun = id === 'weather-cyclones' ? 'cyclone' : 'weather';
|
||||
layer.attachShellServices({
|
||||
runNavigation: (navigate) =>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { APPLICATION_LAYER_METADATA } from '../app/constructCatalog.js';
|
||||
import { LAYER_NAME_KEYS, PANEL_LABEL_KEYS } from './layerNames.js';
|
||||
import { setLocale, t } from '../i18n/index.js';
|
||||
import { FEED_SOURCE, COMBINED_SOURCE } from '../layers/localAdsb/status.js';
|
||||
import {
|
||||
LAYER_NAME_KEYS,
|
||||
PANEL_LABEL_KEYS,
|
||||
sourceLabel,
|
||||
} from './layerNames.js';
|
||||
|
||||
// Layers whose module name is a proper name and stays as it is in every
|
||||
// language: the Bhote Koshi pair belong to a scripted scene.
|
||||
@@ -31,3 +37,17 @@ test('the name tables list only catalog layers', () => {
|
||||
].filter((id) => !catalogIds.includes(id));
|
||||
assert.deepEqual(unknown, []);
|
||||
});
|
||||
|
||||
for (const locale of ['es', 'ru']) {
|
||||
test(`${locale}: Local ADS-B source names translate without their own entry`, () => {
|
||||
setLocale(locale);
|
||||
try {
|
||||
for (const source of [FEED_SOURCE, COMBINED_SOURCE]) {
|
||||
assert.notEqual(t(source), source, 'the catalog has the entry');
|
||||
assert.equal(sourceLabel(source), t(source));
|
||||
}
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
});
|
||||
}
|
||||
+12
-27
@@ -4,9 +4,6 @@ import { layerFeedState } from '../data/feedState.js';
|
||||
export { layerFeedState } from '../data/feedState.js';
|
||||
import { GUIDANCE_STATUSES } from '../loadingFeedback.js';
|
||||
import { keyGuidanceText, keyRequirementText } from './keyGuidance.js';
|
||||
// Layer and source names at this presentation boundary: registry ids and
|
||||
// English module names stay as they are; a layer without an entry renders its
|
||||
// module name, and the panel's own English names win (PANEL_LABEL_KEYS).
|
||||
import {
|
||||
LAYER_NAME_KEYS,
|
||||
PANEL_LABEL_KEYS as PANEL_LABELS,
|
||||
@@ -14,7 +11,7 @@ import {
|
||||
} from './layerNames.js';
|
||||
export { localizeLayerName } from './layerNames.js';
|
||||
import { createWeatherPanel } from './weatherPanel.js';
|
||||
import { translateFeedError, translateLoadingLabel } from './statusText.js';
|
||||
import { translateFeedError, translateStatusLine } from './statusText.js';
|
||||
import {
|
||||
formatNumber,
|
||||
getLocale,
|
||||
@@ -23,9 +20,8 @@ import {
|
||||
translateKnown,
|
||||
} from '../i18n/index.js';
|
||||
|
||||
// Feed-state enum VALUES are machine identifiers (layerFeedState contract, QA
|
||||
// hooks, CSS feed-* classes); only the final label value translates, at this
|
||||
// presentation boundary. See docs/TRANSLATORS.md keep-English boundary.
|
||||
// Feed-state values are machine ids (QA hooks, CSS feed-* classes); only the
|
||||
// label translates.
|
||||
const FEED_STATE_LABEL_KEYS = Object.freeze({
|
||||
nominal: msg('ON'),
|
||||
loading: msg('LOADING'),
|
||||
@@ -39,15 +35,6 @@ const FEED_STATE_LABEL_KEYS = Object.freeze({
|
||||
function lifecycleStateLabel(lifecycleState) {
|
||||
return t(lifecycleState === 'enabling' ? msg('ENABLING') : msg('DISABLING'));
|
||||
}
|
||||
const FEED_STATE_LABELS = Object.freeze({
|
||||
nominal: 'ON',
|
||||
loading: 'LOADING',
|
||||
degraded: 'DEGRADED',
|
||||
stale: 'STALE',
|
||||
partial: 'PARTIAL',
|
||||
fallback: 'FALLBACK',
|
||||
unavailable: 'UNAVAILABLE',
|
||||
});
|
||||
|
||||
// Presentation order is independent of catalog registration and startup order.
|
||||
const PANEL_GROUPS = [
|
||||
@@ -115,7 +102,7 @@ function englishPanelLabel(layer) {
|
||||
return PANEL_LABELS[layer.id] || LAYER_NAME_KEYS[layer.id] || layer.name;
|
||||
}
|
||||
|
||||
// The panel's own English names win, as before i18n; module names otherwise.
|
||||
// PANEL_LABELS win over layer-name keys; otherwise the module's own name.
|
||||
function panelLabel(layer) {
|
||||
if (PANEL_LABELS[layer.id]) return t(PANEL_LABELS[layer.id]);
|
||||
const key = LAYER_NAME_KEYS[layer.id];
|
||||
@@ -485,12 +472,10 @@ export class LayerPanel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a row's count. A count that is a phrase carrying its own number
|
||||
* ("24 nearby · 2 on screen") moves to its own line under the name outside
|
||||
* English: beside the toggle it squeezes a longer translated name to a word
|
||||
* per line. English keeps the count beside the toggle. The "—" placeholder
|
||||
* is marked so it can give way to a wide status chip (translations only,
|
||||
* radio.css).
|
||||
* Write a row's count. Outside English a count phrase ("24 nearby · 2 on
|
||||
* screen") gets its own line, or it squeezes a longer translated name to a
|
||||
* word per line. "—" is marked so a wide status chip can replace it
|
||||
* (radio.css).
|
||||
*/
|
||||
_syncCount(row, count, stats) {
|
||||
const text = this._layerCountText(stats);
|
||||
@@ -693,14 +678,14 @@ export class LayerPanel {
|
||||
if (stats.loading) {
|
||||
const loadingLabel =
|
||||
typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()
|
||||
? translateLoadingLabel(stats.loadingLabel)
|
||||
? translateStatusLine(stats.loadingLabel.trim())
|
||||
: t('loading...');
|
||||
return t('{source} · {loadingLabel}', { source, loadingLabel });
|
||||
}
|
||||
if (feedState === 'fallback') {
|
||||
const detail =
|
||||
typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()
|
||||
? translateLoadingLabel(stats.loadingLabel)
|
||||
? translateStatusLine(stats.loadingLabel.trim())
|
||||
: stats.coverage || ago;
|
||||
return t('{state} · {source} · {detail}', {
|
||||
state: stateLabel,
|
||||
@@ -745,7 +730,7 @@ export class LayerPanel {
|
||||
if (typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()) {
|
||||
return t('{source} · {loadingLabel}', {
|
||||
source,
|
||||
loadingLabel: translateLoadingLabel(stats.loadingLabel),
|
||||
loadingLabel: translateStatusLine(stats.loadingLabel.trim()),
|
||||
});
|
||||
}
|
||||
return t('{source} · {ago}', { source, ago });
|
||||
@@ -767,7 +752,7 @@ export class LayerPanel {
|
||||
button.classList.toggle('enabling', layer.lifecycleState === 'enabling');
|
||||
button.classList.toggle('disabling', layer.lifecycleState === 'disabling');
|
||||
button.classList.toggle('lifecycle-uncertain', uncertain);
|
||||
for (const state of Object.keys(FEED_STATE_LABELS)) {
|
||||
for (const state of Object.keys(FEED_STATE_LABEL_KEYS)) {
|
||||
button.classList.toggle(
|
||||
`feed-${state}`,
|
||||
(layer.enabled || unavailable) && !uncertain && feedState === state,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
test('Local ADS-B guidance hint is translated in the layer row (K-20)', async () => {
|
||||
test('Local ADS-B guidance hint is translated in the layer row', async () => {
|
||||
const { setLocale, t } = await import('../i18n/index.js');
|
||||
const { LayerPanel } = await import('./layerPanel.js');
|
||||
const { localAdsbStatus } = await import('../layers/localAdsb/status.js');
|
||||
|
||||
@@ -37,7 +37,7 @@ test('an unknown feed status is shown as given', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// B6: a feed status agrees with "fuente" in Spanish; the shared "invalid" key
|
||||
// A feed status agrees with "fuente" in Spanish; the shared "invalid" key
|
||||
// (any other masculine noun) is left alone.
|
||||
test('es: an invalid decoder feed reads "no válida"', () => {
|
||||
setLocale('es');
|
||||
|
||||
@@ -86,7 +86,7 @@ for (const locale of ['es', 'ru']) {
|
||||
});
|
||||
}
|
||||
|
||||
test('FM stop message after internet radio start reads as before in English', async (t) => {
|
||||
test('FM stop message after internet radio start stays English', async (t) => {
|
||||
const view = await stopLocalFmForInternetRadio(t, 'en');
|
||||
assert.equal(view.statusText, 'Local FM stopped: internet radio started');
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
// C14: the SDR status line and stats write numbers in the UI language, like
|
||||
// The SDR status line and stats write numbers in the UI language, like
|
||||
// the frequency next to them ("Сканирование 101,1 МГц…").
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
* Interface language picker: a HUD button in the bottom-right corner that
|
||||
* opens a listbox of the shipped languages.
|
||||
*
|
||||
* The ordering, search filter, keyboard model and choice rule are pure
|
||||
* functions below, testable without a DOM (localePicker.test.mjs).
|
||||
* mountLocalePicker() binds them to the #locale-picker markup; the shell only
|
||||
* mounts it and decides what choosing another language does (confirm, reload).
|
||||
* The logic is pure and DOM-free; mountLocalePicker() only renders it. The
|
||||
* shell decides what a choice does (confirm, reload).
|
||||
*
|
||||
* The list follows the ARIA listbox pattern: the button opens it, focus moves
|
||||
* into the list (or into the search field, which is a combobox, once there are
|
||||
|
||||
@@ -84,7 +84,7 @@ test('the current language comes first, the rest by their own names', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('each row names the language in the interface language too, unless that only repeats it', () => {
|
||||
test('each row gets the interface-language name for search, unless it only repeats the native one', () => {
|
||||
const local = createLocalNameReader('ru');
|
||||
const rows = buildLocaleOptions({
|
||||
locales: LOCALES,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// K-27: the language picker reloads the page, so it asks first while the
|
||||
// The language picker reloads the page, so it asks first while the
|
||||
// reload would lose work (unsaved CCTV calibration, the open cockpit, anything
|
||||
// a module registers). Choosing the language already active does nothing.
|
||||
import { test } from 'node:test';
|
||||
|
||||
@@ -5,10 +5,8 @@ import { localizePlaceName } from './placeNames.js';
|
||||
const POI_KEYS = ['Q', 'W', 'E', 'R', 'T'];
|
||||
|
||||
/**
|
||||
* locationStatus.js sentinel outputs used as localized-rewrite triggers for
|
||||
* the collapsed LOCATION mini-status. Built by calling the module with no
|
||||
* real data, so the comparisons track that module's copy without restating
|
||||
* English strings here. Resolved destinations (place names) stay verbatim.
|
||||
* locationStatus.js placeholder lines, built by the module itself; renderStatus
|
||||
* swaps them for translated text. Place names are shown as they are.
|
||||
*/
|
||||
const LOCATION_MINI_EMPTY = Object.freeze(locationMiniStatus());
|
||||
const LOCATION_MINI_SEARCHED_PLACEHOLDER = Object.freeze(
|
||||
@@ -154,9 +152,6 @@ export class LocationControls {
|
||||
}
|
||||
: record;
|
||||
const lines = locationMiniStatus(state);
|
||||
// The initial/placeholder states get catalog keys at the writer so a
|
||||
// non-English boot never leaks the English sentinels back over the
|
||||
// localized static readout (resolved place names stay verbatim data).
|
||||
this.elements.statusCity.textContent =
|
||||
lines.city === LOCATION_MINI_EMPTY.city
|
||||
? t('📍 Location: --')
|
||||
|
||||
@@ -318,8 +318,7 @@ export class PanelChrome {
|
||||
btn.title = t(titleKey, { name: panelName });
|
||||
btn.setAttribute('aria-label', t(titleKey, { name: panelName }));
|
||||
if (panelEl.id === 'radio-panel') {
|
||||
// Expand state reuses the layers.* keys seeded on the static button;
|
||||
// the collapse state is runtime-only, so it owns cockpit.* keys.
|
||||
// Radio keeps the wording of its static button (context.html).
|
||||
btn.title = t(
|
||||
collapsed ? msg('Expand Radio') : msg('Collapse Radio'),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Preset place names as the page shows them.
|
||||
*
|
||||
* The preset cities in src/locations.js keep their English names in data:
|
||||
* The preset cities in src/cityPois.js keep their English names in data:
|
||||
* those are what voice tools, the AI model, share links and lookups match on.
|
||||
* This helper translates a known English name at a presentation boundary
|
||||
* only; any other place (a geocoded address, a name the model chose) is shown
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { setLocale } from '../i18n/index.js';
|
||||
import { CITY_POIS } from '../locations.js';
|
||||
import { CITY_POIS } from '../cityPois.js';
|
||||
import { KNOWN_PLACE_NAMES, localizePlaceName } from './placeNames.js';
|
||||
import { LocationControls } from './locationControls.js';
|
||||
|
||||
|
||||
@@ -362,10 +362,8 @@ export function bindRadioControls() {
|
||||
origin: 'user',
|
||||
notificationToken,
|
||||
}),
|
||||
// Lifecycle toast copy: the English template below is pinned
|
||||
// verbatim by contextSessionOrdering.test.mjs, so it stays the
|
||||
// argument and localizedMessage rewrites it at the toast boundary
|
||||
// (identical value under 'en').
|
||||
// contextSessionOrdering.test.mjs pins this English argument; the
|
||||
// toast shows localizedMessage.
|
||||
`Radio could not ${enabling ? 'start' : 'stop'} cleanly`,
|
||||
{
|
||||
localizedMessage: t('Radio could not {action} cleanly', {
|
||||
|
||||
@@ -32,7 +32,6 @@ export function renderRadioState(state) {
|
||||
}
|
||||
this._syncContextRadioLauncherState();
|
||||
this._radioLayerState?.classList.toggle('active', enabled);
|
||||
// Lifecycle enum values map to status-chip keys; counts stay machine.
|
||||
const lifecycleLabel =
|
||||
lifecycleState === 'enabling' ? t('ENABLING') : t('DISABLING');
|
||||
if (this._radioLayerState) {
|
||||
|
||||
@@ -62,7 +62,7 @@ for (const locale of ['es', 'ru']) {
|
||||
});
|
||||
}
|
||||
|
||||
test('radio playback errors read as before in English', () => {
|
||||
test('radio playback errors stay English', () => {
|
||||
setLocale('en');
|
||||
for (const message of PLAYBACK_ERRORS) {
|
||||
assert.equal(render(message), message);
|
||||
|
||||
@@ -37,9 +37,9 @@ const LAYER_NOTICES = new Set([
|
||||
msg('Point the camera at the ground to use the view'),
|
||||
msg('That point cannot anchor a box'),
|
||||
]);
|
||||
const TOO_LARGE = /^(Box|View) is ([\d,]+) km wide · limit ([\d,]+) km$/;
|
||||
// Refusals with a value in them (layers/recentImagery/model.js and
|
||||
// imageryBoxTool.js); fixed refusals there are msg() text for translateKnown.
|
||||
const TOO_LARGE = /^(Box|View) is ([\d,]+) km wide · limit ([\d,]+) km$/;
|
||||
const POLAR = /^Imagery stops at ±([\d.]+)° latitude$/;
|
||||
const POINTER_BUSY = /^(.+) is using the pointer — close it first$/;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// K-43: box-tool and model refusals on the Recent Imagery notice line are
|
||||
// Box-tool and model refusals on the Recent Imagery notice line are
|
||||
// shown in the UI language; the layer keeps them in English.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -35,7 +35,7 @@ const SHOWN_EN = new Map([
|
||||
],
|
||||
]);
|
||||
|
||||
test('K-43 en: refusals read as before', () => {
|
||||
test('en: refusals stay English', () => {
|
||||
setLocale('en');
|
||||
for (const text of REFUSALS)
|
||||
assert.equal(localizeNotice(text), SHOWN_EN.get(text) ?? text);
|
||||
@@ -83,7 +83,7 @@ test('E3/E12: the pointer owner is named by its tool, never by its id', () => {
|
||||
});
|
||||
|
||||
for (const locale of ['es', 'ru']) {
|
||||
test(`K-43 ${locale}: box-tool and model refusals are translated`, () => {
|
||||
test(`${locale}: box-tool and model refusals are translated`, () => {
|
||||
setLocale(locale);
|
||||
try {
|
||||
for (const text of REFUSALS)
|
||||
|
||||
@@ -87,11 +87,8 @@ export function createSceneDialog(title, onClose) {
|
||||
}
|
||||
/**
|
||||
* The browser draws a file input's button and "no file" text in its own
|
||||
* language. Keep the native input itself as the whole control — stretched
|
||||
* over the row below, fully transparent — so clicking, keyboard focus and
|
||||
* dropping files or folders from the desktop all stay native (a shrunken
|
||||
* input would only accept drops on its own 1 px), and draw a translated
|
||||
* button and selection line underneath it.
|
||||
* language. Stretch the native input, transparent, over the whole row (so
|
||||
* click, focus and drops stay native) and draw translated text under it.
|
||||
*/
|
||||
function fileChooser(wrapper, node) {
|
||||
wrapper.style.position = 'relative';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Share-tracking restore notices are ready-made sentences per policy: a label
|
||||
// dropped into a template cannot take the case (ru) or article (es) the
|
||||
// sentence needs. English stays exactly as it was composed before.
|
||||
// dropped into a template cannot take the case (ru) or article (es) needed.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { setLocale } from '../i18n/index.js';
|
||||
@@ -11,7 +10,7 @@ const { flights, military, satellites } = SHARE_TRACKING_RESTORE_POLICIES;
|
||||
|
||||
test.afterEach(() => setLocale('en'));
|
||||
|
||||
test('en: the notices read as before for every policy and outcome', () => {
|
||||
test('en: the notices for every policy and outcome', () => {
|
||||
setLocale('en');
|
||||
const notice = (policy, classification) =>
|
||||
shareTrackingRestoreNotice({ ...policy, classification });
|
||||
|
||||
@@ -54,7 +54,6 @@ export function readShellElements(document = globalThis.document) {
|
||||
_scopeFeatherValue: document.getElementById('scope-feather-value'),
|
||||
_mapStackChips: document.getElementById('map-stack-chips'),
|
||||
_mapStackStatus: document.getElementById('map-stack-status'),
|
||||
_mapSourceLabel: document.getElementById('map-source-label'),
|
||||
_cleanViewBtn: document.getElementById('clean-view-toggle'),
|
||||
_cleanViewExitBtn: document.getElementById('clean-view-exit'),
|
||||
_dataPanel: document.getElementById('data-panel'),
|
||||
|
||||
@@ -13,11 +13,9 @@ import { getLocale, t, msg } from '../i18n/index.js';
|
||||
import { localizeLayerName } from './layerPanel.js';
|
||||
import { translateFeedError } from './statusText.js';
|
||||
|
||||
// loadingFeedback.js composes the shared status copy inside the portable
|
||||
// node/browser graph (the server-side HUD summary reads the same modules), so
|
||||
// its labels stay English there. This chip is the presentation boundary: map
|
||||
// the closed label vocabulary to the catalog, and pass anything unrecognized
|
||||
// (a live provider error) through verbatim (docs/TRANSLATORS.md).
|
||||
// The server also loads loadingFeedback.js (through the HUD summary), so its
|
||||
// labels stay English. Translate the known ones; pass anything else (a live
|
||||
// provider error) through.
|
||||
const STATUS_LABEL_KEYS = Object.freeze({
|
||||
'LOADING LIVE DATA': msg('LOADING LIVE DATA'),
|
||||
'LOAD COMPLETE': msg('LOAD COMPLETE'),
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// The global loading chip translates the closed status vocabulary of
|
||||
// loadingFeedback.js at the presentation boundary. The ALPR retry detail keeps
|
||||
// its layer-name prefix in every language (it is what tells the chip apart
|
||||
// from a mapped-sites retry), and every mapped-site failure reason label has a
|
||||
// catalog entry, not only the default one.
|
||||
// The global loading chip in the UI language: the status vocabulary, the ALPR
|
||||
// retry prefix and every mapped-site failure reason.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { setLocale, t } from '../i18n/index.js';
|
||||
|
||||
+14
-12
@@ -28,7 +28,7 @@ const FEED_ERRORS = new Set([
|
||||
msg('OpenSky auth failed'),
|
||||
]);
|
||||
const FEED_ERROR_PATTERNS = [
|
||||
// Street Level coverage errors (streetLevel/providers/kartaview/source.js),
|
||||
// Street Level provider errors (providers/{kartaview,panoramax}/source.js),
|
||||
// ahead of the generic "<source> HTTP <status>".
|
||||
[
|
||||
/^KartaView answered HTTP (\d+)$/,
|
||||
@@ -133,9 +133,11 @@ export function translateFeedError(text) {
|
||||
if (typeof text !== 'string') return text;
|
||||
const whole = feedErrorText(text);
|
||||
if (whole !== null) return whole;
|
||||
// feedErrorText has already looked the text up in the catalog.
|
||||
if (!text.includes(STATUS_SEPARATOR)) return statusPatternText(text) ?? text;
|
||||
return text
|
||||
.split(STATUS_SEPARATOR)
|
||||
.map((piece) => feedErrorText(piece) ?? translateStatusPiece(piece))
|
||||
.map((piece) => feedErrorText(piece) ?? statusPatternText(piece) ?? piece)
|
||||
.join(STATUS_SEPARATOR);
|
||||
}
|
||||
|
||||
@@ -206,15 +208,20 @@ const STATUS_PIECE_PATTERNS = [
|
||||
],
|
||||
];
|
||||
|
||||
/** One piece of a status line in the UI language, or as it came. */
|
||||
export function translateStatusPiece(piece) {
|
||||
const known = translateKnown(piece);
|
||||
if (known !== piece) return known;
|
||||
/** A status piece by its shape in the UI language, or null. */
|
||||
function statusPatternText(piece) {
|
||||
for (const [pattern, format] of STATUS_PIECE_PATTERNS) {
|
||||
const match = pattern.exec(piece);
|
||||
if (match) return format(match);
|
||||
}
|
||||
return piece;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** One piece of a status line in the UI language, or as it came. */
|
||||
export function translateStatusPiece(piece) {
|
||||
const known = translateKnown(piece);
|
||||
if (known !== piece) return known;
|
||||
return statusPatternText(piece) ?? piece;
|
||||
}
|
||||
|
||||
/** A status line of " · " joined pieces in the UI language, piece by piece. */
|
||||
@@ -224,8 +231,3 @@ export function translateStatusLine(text) {
|
||||
.map(translateStatusPiece)
|
||||
.join(STATUS_SEPARATOR);
|
||||
}
|
||||
|
||||
/** A loading label is English data, at times several joined by " · ". */
|
||||
export function translateLoadingLabel(label) {
|
||||
return translateStatusLine(label.trim());
|
||||
}
|
||||
@@ -2,15 +2,8 @@
|
||||
// their shape; English shows exactly as it came.
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { setLocale, t, translateKnown } from '../i18n/index.js';
|
||||
import { sourceLabel } from './layerNames.js';
|
||||
import {
|
||||
translateFeedError,
|
||||
translateLoadingLabel,
|
||||
translateStatusLine,
|
||||
} from './statusText.js';
|
||||
import { FEED_SOURCE, COMBINED_SOURCE } from '../layers/localAdsb/status.js';
|
||||
import { readResponseTextCapped } from '../sources/httpBody.js';
|
||||
import { setLocale, t } from '../i18n/index.js';
|
||||
import { translateFeedError, translateStatusLine } from './statusText.js';
|
||||
|
||||
function inLocale(locale, run) {
|
||||
setLocale(locale);
|
||||
@@ -41,7 +34,6 @@ test('en: status lines and errors show as they came', () => {
|
||||
]) {
|
||||
assert.equal(translateFeedError(text), text);
|
||||
assert.equal(translateStatusLine(text), text);
|
||||
assert.equal(translateLoadingLabel(` ${text} `), text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -75,24 +67,11 @@ for (const locale of ['es', 'ru']) {
|
||||
});
|
||||
});
|
||||
|
||||
test(`${locale}: an oversized upstream response is translated`, async () => {
|
||||
const response = new Response('x'.repeat(10), {
|
||||
headers: { 'content-length': '10' },
|
||||
});
|
||||
const error = await readResponseTextCapped(response, 4).catch((e) => e);
|
||||
assert.equal(error.message, 'Upstream response too large');
|
||||
test(`${locale}: a known error shows its catalog entry`, () => {
|
||||
inLocale(locale, () => {
|
||||
assert.notEqual(translateKnown(error.message), error.message);
|
||||
assert.equal(translateFeedError(error.message), t(error.message));
|
||||
});
|
||||
});
|
||||
|
||||
test(`${locale}: Local ADS-B source names translate without their own entry`, () => {
|
||||
inLocale(locale, () => {
|
||||
for (const source of [FEED_SOURCE, COMBINED_SOURCE]) {
|
||||
assert.notEqual(t(source), source, 'the catalog has the entry');
|
||||
assert.equal(sourceLabel(source), t(source));
|
||||
}
|
||||
const text = 'Upstream response too large';
|
||||
assert.notEqual(t(text), text, 'the catalog has the entry');
|
||||
assert.equal(translateFeedError(text), t(text));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -97,8 +97,8 @@ function presentStatus(state) {
|
||||
: { text: t('OFF'), tone: '', pressed, title };
|
||||
}
|
||||
|
||||
// Provider refusals with a number in them stay English data
|
||||
// (providers/kartaview/source.js); they go back onto a placeholder here.
|
||||
// Provider refusals with a number stay English data (kartaview and panoramax
|
||||
// source.js); they go back onto a placeholder here.
|
||||
const ERROR_PATTERNS = [
|
||||
[
|
||||
/^KartaView answered HTTP (\d+)$/,
|
||||
|
||||
+5
-10
@@ -70,17 +70,13 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Spanish labels run ~25% longer than English and overflow the nowrap
|
||||
.scene-btn rows inside the fixed-width CCTV panel (e.g. ENFOCAR ·
|
||||
COBERTURA DESACTIVADA · AUTO HOP DESACTIVADO). Let the rows wrap under
|
||||
es only; the English layout stays pixel-identical. */
|
||||
/* Spanish labels overflow the nowrap .scene-btn row of the fixed-width
|
||||
panel; wrap the row under es only, English stays as is. */
|
||||
html[lang='es'] .cctv-controls {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* A translated label longer than its button wraps inside the button instead
|
||||
of running past the panel edge. English is excluded so its labels (AUTO HOP
|
||||
OFF) stay on one line. */
|
||||
/* A label longer than its button wraps inside it, not past the panel edge. */
|
||||
html:not([lang='en']) .cctv-controls .scene-btn {
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
@@ -389,9 +385,8 @@ html:not([lang='en']) .cctv-controls .scene-btn {
|
||||
text-shadow: 0 0 10px rgba(0, 212, 255, 0.18);
|
||||
}
|
||||
|
||||
/* Longer translated labels and units: tighter calibration chips keep the
|
||||
seven values to two rows so SAVE / RESET stay in view, and the camera
|
||||
select uses the compact mono face. English keeps its original look. */
|
||||
/* Tighter calibration chips keep the seven values to two rows, so
|
||||
SAVE / RESET stay in view. */
|
||||
html:not([lang='en']) .cctv-cal-readout {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@@ -788,10 +788,8 @@ html:not([lang='en']) .panel-collapse-btn {
|
||||
}
|
||||
|
||||
/* One label column across the Display rows: --pp-label-chars is the longest
|
||||
label of the active language (set by applicationShell), and the labels are
|
||||
monospace, so every slider track starts at the same x whatever the
|
||||
language. 0.125em is the 1px letter-spacing at 8px. English keeps its
|
||||
own label widths. */
|
||||
label of the active language (set in applicationShell.js); the labels are
|
||||
monospace. 0.125em is the 1px letter-spacing at 8px. */
|
||||
html:not([lang='en']) .pp-slider-row > .pp-slider-mini-label {
|
||||
flex: 0 0 auto;
|
||||
min-width: calc(var(--pp-label-chars, 0) * (1ch + 0.125em));
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// K-44 (+ refront-opus-1): layout rules written for long translations must not change the
|
||||
// English rendering ("English renders as before"). Every rule below is read
|
||||
// from the stylesheet without a `html:not([lang='en'])` guard, so it applies
|
||||
// to English too; the expected values are the ones on main (9beadbda).
|
||||
// Layout rules written for long translations must not change the English
|
||||
// rendering: every rule checked here has no `html:not([lang='en'])` guard,
|
||||
// so it applies to English too.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
@@ -33,7 +32,7 @@ function declaration(block, prop) {
|
||||
return m ? m[1].replace(/\s+/g, ' ').trim() : undefined;
|
||||
}
|
||||
|
||||
test('K-44: layer status chip keeps its English width (82px)', () => {
|
||||
test('layer status chip keeps its English width (82px)', () => {
|
||||
const rules = unguardedRules(
|
||||
css('radio.css'),
|
||||
`.data-toggle-btn.feed-loading, .data-toggle-btn.feed-degraded,
|
||||
@@ -44,14 +43,14 @@ test('K-44: layer status chip keeps its English width (82px)', () => {
|
||||
assert.equal(declaration(rules[0], 'min-width'), '82px');
|
||||
});
|
||||
|
||||
test('K-44: Directions steps keep one line with an ellipsis in English', () => {
|
||||
test('Directions steps keep one line with an ellipsis in English', () => {
|
||||
const [rule] = unguardedRules(css('radio.css'), '.data-row-list-text');
|
||||
assert.ok(rule);
|
||||
assert.equal(declaration(rule, 'white-space'), 'nowrap');
|
||||
assert.equal(declaration(rule, 'text-overflow'), 'ellipsis');
|
||||
});
|
||||
|
||||
test('K-44: count-line layout is not reachable from an unguarded selector', () => {
|
||||
test('count-line layout is not reachable from an unguarded selector', () => {
|
||||
const source = css('radio.css');
|
||||
assert.equal(
|
||||
unguardedRules(source, '.data-toggle-row--count-line .data-count').length,
|
||||
@@ -59,7 +58,7 @@ test('K-44: count-line layout is not reachable from an unguarded selector', () =
|
||||
);
|
||||
});
|
||||
|
||||
test('K-44: weather status line keeps its fixed English height', () => {
|
||||
test('weather status line keeps its fixed English height', () => {
|
||||
const [rule] = unguardedRules(
|
||||
css('weather.css'),
|
||||
".weather-card [data-line-id='status']",
|
||||
@@ -68,7 +67,7 @@ test('K-44: weather status line keeps its fixed English height', () => {
|
||||
assert.equal(declaration(rule, 'height'), '1.45em');
|
||||
});
|
||||
|
||||
test('K-44: cockpit number separators keep the English cell width', () => {
|
||||
test('cockpit number separators keep the English cell width', () => {
|
||||
assert.equal(
|
||||
unguardedRules(css('cockpit.css'), '.cockpit-roll-token.is-separator')
|
||||
.length,
|
||||
@@ -76,7 +75,7 @@ test('K-44: cockpit number separators keep the English cell width', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('K-44: Bhote Koshi callout keeps its English footer layout', () => {
|
||||
test('Bhote Koshi callout keeps its English footer layout', () => {
|
||||
const source = css('bhote-koshi.css');
|
||||
const [strong] = unguardedRules(
|
||||
source,
|
||||
@@ -87,7 +86,7 @@ test('K-44: Bhote Koshi callout keeps its English footer layout', () => {
|
||||
assert.equal(declaration(link, 'grid-row'), '1 / span 2');
|
||||
});
|
||||
|
||||
test('K-44: SCENES actions stay on one line in English (refront-opus-1)', () => {
|
||||
test('SCENES actions stay on one line in English', () => {
|
||||
assert.equal(
|
||||
unguardedRules(css('scenes.css'), '#scene-panel .scene-controls .scene-btn')
|
||||
.length,
|
||||
@@ -97,7 +96,7 @@ test('K-44: SCENES actions stay on one line in English (refront-opus-1)', () =>
|
||||
assert.equal(declaration(select, 'white-space'), undefined);
|
||||
});
|
||||
|
||||
test('K-44: Display and parameter rows keep their English layout', () => {
|
||||
test('Display and parameter rows keep their English layout', () => {
|
||||
const source = css('controls.css');
|
||||
assert.equal(
|
||||
unguardedRules(source, '.pp-slider-row > .pp-slider-mini-label').length,
|
||||
@@ -110,7 +109,7 @@ test('K-44: Display and parameter rows keep their English layout', () => {
|
||||
assert.equal(declaration(color, 'flex'), '0 0 74px');
|
||||
});
|
||||
|
||||
test('K-44: weather setting rows keep the 56px English label column', () => {
|
||||
test('weather setting rows keep the 56px English label column', () => {
|
||||
const source = css('weather.css');
|
||||
assert.equal(unguardedRules(source, '.rail-card-settings').length, 0);
|
||||
const [row] = unguardedRules(source, '.rail-card-setting');
|
||||
@@ -120,14 +119,14 @@ test('K-44: weather setting rows keep the 56px English label column', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('K-44: cockpit brief controls keep their English sizes', () => {
|
||||
test('cockpit brief controls keep their English sizes', () => {
|
||||
const source = css('cockpit.css');
|
||||
const [auto] = unguardedRules(source, '#cockpit-brief-auto');
|
||||
assert.equal(declaration(auto, 'width'), '50px');
|
||||
assert.equal(unguardedRules(source, '.cockpit-brief-actions > *').length, 0);
|
||||
});
|
||||
|
||||
test('K-44: the count-line class is set only outside English', async () => {
|
||||
test('the count-line class is set only outside English', async () => {
|
||||
const { setLocale } = await import('../../i18n/index.js');
|
||||
const { LayerPanel } = await import('../layerPanel.js');
|
||||
const classes = () => {
|
||||
@@ -153,7 +152,7 @@ test('K-44: the count-line class is set only outside English', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('K-44: Recent Imagery readout keeps its English layout', () => {
|
||||
test('Recent Imagery readout keeps its English layout', () => {
|
||||
const source = css('recent-imagery.css');
|
||||
const one = (selector) => {
|
||||
const rules = unguardedRules(source, selector);
|
||||
@@ -211,7 +210,7 @@ test('K-44: Recent Imagery readout keeps its English layout', () => {
|
||||
assert.equal(declaration(swap, 'min-width'), undefined);
|
||||
});
|
||||
|
||||
test('N-13: CCTV control buttons stay on one line in English', () => {
|
||||
test('CCTV control buttons stay on one line in English', () => {
|
||||
assert.equal(
|
||||
unguardedRules(css('cctv.css'), '.cctv-controls .scene-btn').length,
|
||||
0,
|
||||
|
||||
@@ -362,10 +362,9 @@ html:not([lang='en'])
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
/* In a translation the slot rows share the bar's columns (subgrid), so the
|
||||
tag column fits the longest tag in either row and both rows stay aligned.
|
||||
A translated slot line (date · source · resolution · state) is longer:
|
||||
let it take a second line rather than cut off the state at the end. */
|
||||
/* The slot rows share the bar's columns (subgrid), so the tag column fits
|
||||
the longest tag and both rows align; a long slot line (date · source ·
|
||||
resolution · state) takes a second line instead of losing the state. */
|
||||
html:not([lang='en']) .recent-imagery-readout .ri-selection {
|
||||
grid-template-columns: minmax(44px, max-content) minmax(0, 1fr) 22px;
|
||||
column-gap: 6px;
|
||||
|
||||
@@ -163,10 +163,16 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* A translated scene name needs the width: the select takes its own row,
|
||||
NEW / DEL share the next one. */
|
||||
html:not([lang='en']) #scene-select {
|
||||
flex: 1 1 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* A translated action longer than its button wraps inside the button instead
|
||||
@@ -468,15 +474,3 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
|
||||
.director-sharing-dialog::backdrop {
|
||||
background: rgba(0, 8, 14, 0.65);
|
||||
}
|
||||
|
||||
html:not([lang='en']) #scene-select {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* A translated scene name needs the width: the select takes its own row and
|
||||
NEW / DELETE share the next one. */
|
||||
html:not([lang='en']) #scene-select {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
@@ -216,8 +216,7 @@ button.sl-status:focus-visible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Longer translations: rather than squeeze the hint into a narrow column
|
||||
beside the button, put the button on its own line when both do not fit. */
|
||||
/* When the hint and the button do not both fit, the button takes its own line. */
|
||||
html:not([lang='en']) .sl-meta-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -83,14 +83,11 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The dock heading row is only as wide as the voice column, so the cost
|
||||
cluster keeps its size and the state label gives way: a long translated
|
||||
state (ПОДКЛЮЧЕНИЕ, CONECTANDO) ends in an ellipsis instead of pushing the
|
||||
cost past the card edge. The kicker already collapses to zero width here;
|
||||
taking it out of the row returns its gap to the state label. The row also
|
||||
reaches left over the gutter beside the mic ring: at heading height the
|
||||
ring has already curved away. English fits as it is, so this applies to
|
||||
the other languages only. */
|
||||
/* The heading row is only as wide as the voice column: the cost cluster
|
||||
keeps its size and a long state (ПОДКЛЮЧЕНИЕ, CONECTANDO) ends in an
|
||||
ellipsis. Dropping the kicker returns its gap to the state label; the
|
||||
negative margin reaches over the gutter beside the mic ring, which has
|
||||
curved away at heading height. */
|
||||
html:not([lang='en']) #command-dock .gev-voice-heading {
|
||||
gap: 0.2rem;
|
||||
margin-left: -0.6rem;
|
||||
@@ -123,10 +120,9 @@ html:not([lang='en']) #command-dock .gev-voice-cost-value {
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
/* Live hints ("Ask or command", "Requesting microphone") and the tier switch
|
||||
("СТАНДАРТ — со следующей сессии") run longer in other languages; give them
|
||||
a second line from the spare space under the readout, without the tracking
|
||||
that would push the tier line onto a third, rather than cutting them. */
|
||||
/* Live hints and the tier switch ("СТАНДАРТ — со следующей сессии") run
|
||||
longer: a second line from the spare space under the readout, with no
|
||||
tracking so the tier line does not need a third. */
|
||||
html:not([lang='en'])
|
||||
#command-dock
|
||||
#gev-voice-control:is(
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
both the chip and this dialog outright when /api/setup/status is not
|
||||
there to answer (prod builds, LAN visitors). Rows are rendered from
|
||||
that payload, so the key registry lives in src/keySetupCore.mjs alone. -->
|
||||
<!-- Bottom-right corner, one HUD row: the POWER UP chip (while keys are
|
||||
missing) and, in the very corner, the interface language picker that
|
||||
src/ui/localePicker.js renders into #locale-picker. -->
|
||||
<!-- Bottom-right HUD row: the POWER UP chip (while keys are missing) and
|
||||
the language picker that src/ui/localePicker.js renders into #locale-picker. -->
|
||||
<div id="corner-actions">
|
||||
<button id="key-setup-chip" type="button" hidden aria-haspopup="dialog" aria-controls="key-setup">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">bolt</span>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Vector tile and weather image failures reach the DATA LAYERS row and the
|
||||
// ALPR retry chip in the UI language; the sources keep them in English, and
|
||||
// English shows them as before.
|
||||
// ALPR retry chip in the UI language; the sources keep them in English.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { setLocale } from '../i18n/index.js';
|
||||
@@ -41,7 +40,7 @@ test('the vector tile source fails with the catalog key', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('en: tile and weather image failures read as before', () => {
|
||||
test('en: tile and weather image failures stay English', () => {
|
||||
setLocale('en');
|
||||
assert.match(
|
||||
row('alpr-cameras', 'Vector tile metadata unavailable'),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// K-38: refusals and failures that source modules produce in English reach
|
||||
// Refusals and failures that source modules produce in English reach
|
||||
// the page translated — through a placeholder at the point of display, or
|
||||
// through a catalog entry when the text is stored as data.
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -30,7 +30,7 @@ function snapshot(error) {
|
||||
}
|
||||
|
||||
for (const locale of ['es', 'ru']) {
|
||||
test(`K-38 (${locale}): the nearest-image refusal is built in the language and kept by the panel`, () => {
|
||||
test(`${locale}: the nearest-image refusal is built in the language and kept by the panel`, () => {
|
||||
setLocale(locale);
|
||||
try {
|
||||
// What streetLevel/index.js openNearest writes into street.error.
|
||||
@@ -50,7 +50,7 @@ for (const locale of ['es', 'ru']) {
|
||||
}
|
||||
});
|
||||
|
||||
test(`K-38 (${locale}): the route proxy's refusals are shown translated`, () => {
|
||||
test(`${locale}: the route proxy's refusals are shown translated`, () => {
|
||||
setLocale(locale);
|
||||
try {
|
||||
const shown = t('Routing failed: {error}', {
|
||||
@@ -78,7 +78,7 @@ for (const locale of ['es', 'ru']) {
|
||||
}
|
||||
});
|
||||
|
||||
test(`K-38 (${locale}): a weather manifest timeout is shown translated`, async () => {
|
||||
test(`${locale}: a weather manifest timeout is shown translated`, async () => {
|
||||
const source = createWeatherSource({
|
||||
fetchImpl: (_url, { signal }) =>
|
||||
new Promise((_resolve, reject) =>
|
||||
|
||||
@@ -657,7 +657,7 @@ test('a change of open card scrolls its header into view once; refreshes never m
|
||||
assert.deepEqual(intoView, [], 'ancestor scrollers are never moved');
|
||||
view.destroy();
|
||||
});
|
||||
test('history time keeps the hour whatever the length of the localized date (K-6)', () => {
|
||||
test('history time keeps the hour whatever the length of the localized date', () => {
|
||||
for (const locale of ['en', 'es', 'ru']) {
|
||||
setLocale(locale);
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user