fix(i18n): second screenshot review: remaining English, numbers and wraps

From the re-shot Russian screenshots:
- Translated: weather frame/imagery status errors, HUD summary provenance
  ("НЕДОСТУПНО: СУДА AIS ОНЛАЙН"), Bhote Koshi map labels, cockpit signal
  distances (КМ). The HUD summary fields are separated with " · " in every
  language, so the style name and view band no longer read as one phrase.
- Numbers follow the locale: cockpit altitude ("26 175"), precipitation.
- No breaks inside codes and dates: ADS-B and similar in layer rows,
  Sentinel-2, cyclone issue dates (word joiner; the UI font has no U+2011),
  no-break space after label colons in cyclone details.
- Layout: count lines under ALPR, short unavailable chip (context
  "status chip"), roomier draw segments, tighter CCTV calibration chips,
  two-line beat titles and media card titles, wrapping weather status.
- Wording: ru advisory is «сводка» (was «бюллетень»; "Advisory center" is
  «Центр циклона по сводке»), Street View is «Просмотр улиц», OPACITY
  «НЕПРОЗР.», synced «синхр.», Area «Зона»; the import dialog no longer says
  "this scene" for several scenes. Glossary updated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-13 19:13:12 +04:00
1 parent 2b6ccad732
commit 54dce5b3bd
32 files changed
+1204 -346

No files matched your search

+206 -53
View File
@@ -4,7 +4,9 @@ import { afterEach } from 'node:test';
import { formatAwarenessLabel } from './data/militaryAwarenessEngine.js';
import { CockpitViewController } from './ui/cockpitController.js';
const previousDocument = globalThis.document;
afterEach(() => { globalThis.document = previousDocument; });
afterEach(() => {
globalThis.document = previousDocument;
});
// Model native focus loss on removal AND on ordinary DOM moves. Keeping an
// object reference alone is insufficient if reconciliation disconnects it.
@@ -22,23 +24,60 @@ function fixture() {
this.className = '';
this._text = '';
this.classList = {
add: (...names) => { this.className = [...new Set([...this.className.split(/\s+/).filter(Boolean), ...names])].join(' '); },
add: (...names) => {
this.className = [
...new Set([
...this.className.split(/\s+/).filter(Boolean),
...names,
]),
].join(' ');
},
};
}
get isConnected() { return this === document.body || Boolean(this.parentNode?.isConnected); }
get firstElementChild() { return this.children[0] || null; }
get nextElementSibling() { return this.parentNode?.children[this.parentNode.children.indexOf(this) + 1] || null; }
get textContent() { return this._text + this.children.map((node) => node.textContent).join(''); }
set textContent(value) { this.replaceChildren(); this._text = String(value); }
setAttribute(name, value) { this.attributes.set(name, String(value)); }
getAttribute(name) { return this.attributes.get(name) ?? null; }
contains(node) { return this === node || this.children.some((child) => child.contains(node)); }
append(...nodes) { for (const node of nodes) this.insertBefore(node, null); }
get isConnected() {
return this === document.body || Boolean(this.parentNode?.isConnected);
}
get firstElementChild() {
return this.children[0] || null;
}
get nextElementSibling() {
return (
this.parentNode?.children[this.parentNode.children.indexOf(this) + 1] ||
null
);
}
get textContent() {
return (
this._text + this.children.map((node) => node.textContent).join('')
);
}
set textContent(value) {
this.replaceChildren();
this._text = String(value);
}
setAttribute(name, value) {
this.attributes.set(name, String(value));
}
getAttribute(name) {
return this.attributes.get(name) ?? null;
}
contains(node) {
return (
this === node || this.children.some((child) => child.contains(node))
);
}
append(...nodes) {
for (const node of nodes) this.insertBefore(node, null);
}
insertBefore(node, anchor) {
assert.ok(anchor === null || anchor.parentNode === this, 'insert anchor belongs to its parent');
assert.ok(
anchor === null || anchor.parentNode === this,
'insert anchor belongs to its parent',
);
if (node === anchor) return node;
node.remove();
const index = anchor === null ? this.children.length : this.children.indexOf(anchor);
const index =
anchor === null ? this.children.length : this.children.indexOf(anchor);
this.children.splice(index, 0, node);
node.parentNode = this;
return node;
@@ -49,7 +88,10 @@ function fixture() {
document.activeElement = document.body;
focusLosses += 1;
}
this.parentNode.children.splice(this.parentNode.children.indexOf(this), 1);
this.parentNode.children.splice(
this.parentNode.children.indexOf(this),
1,
);
this.parentNode = null;
}
replaceChildren(...nodes) {
@@ -59,20 +101,32 @@ function fixture() {
}
matches(selector) {
if (selector === 'button[data-signal-layer][data-signal-id]') {
return this.tagName === 'BUTTON' && this.dataset.signalLayer != null && this.dataset.signalId != null;
return (
this.tagName === 'BUTTON' &&
this.dataset.signalLayer != null &&
this.dataset.signalId != null
);
}
return selector.startsWith('.') ? this.className.split(/\s+/).includes(selector.slice(1))
return selector.startsWith('.')
? this.className.split(/\s+/).includes(selector.slice(1))
: this.tagName === selector.toUpperCase();
}
closest(selector) {
for (let node = this; node; node = node.parentNode) if (node.matches(selector)) return node;
for (let node = this; node; node = node.parentNode)
if (node.matches(selector)) return node;
return null;
}
querySelectorAll(selector) {
return this.children.flatMap((child) => [ ...(child.matches(selector) ? [child] : []), ...child.querySelectorAll(selector) ]);
return this.children.flatMap((child) => [
...(child.matches(selector) ? [child] : []),
...child.querySelectorAll(selector),
]);
}
focus() {
assert.ok(this.isConnected, 'a focus destination must still be connected');
assert.ok(
this.isConnected,
'a focus destination must still be connected',
);
focusCalls += 1;
document.activeElement = this;
}
@@ -85,7 +139,11 @@ function fixture() {
controller.services = { formatAwarenessLabel };
controller.signalList = new Node('ol');
controller.signalToggle = new Node('button');
controller.briefTabs = [new Node('button'), new Node('button'), new Node('button')];
controller.briefTabs = [
new Node('button'),
new Node('button'),
new Node('button'),
];
controller.briefPageIndex = 0;
controller.signalItems = [];
controller.signalSignatures = new Map();
@@ -93,64 +151,131 @@ function fixture() {
const display = new Node('button');
const radio = new Node('button');
const outside = new Node('button');
document.body.append(controller.signalToggle, controller.signalList, ...controller.briefTabs, display, radio, outside);
document.body.append(
controller.signalToggle,
controller.signalList,
...controller.briefTabs,
display,
radio,
outside,
);
const selected = [];
controller.services.militaryAwarenessLayer = { focusTarget: (...args) => selected.push(args) };
const click = event => controller.handleSignalClick(event);
controller.services.militaryAwarenessLayer = {
focusTarget: (...args) => selected.push(args),
};
const click = (event) => controller.handleSignalClick(event);
const buttons = () => controller.signalList.querySelectorAll('button');
const tab = () => {
const nodes = document.body.querySelectorAll('button');
const index = nodes.indexOf(document.activeElement);
nodes[(index + 1) % nodes.length].focus();
};
return { controller, document, buttons, display, radio, outside, selected, click, tab,
losses: () => focusLosses, focusCalls: () => focusCalls };
return {
controller,
document,
buttons,
display,
radio,
outside,
selected,
click,
tab,
losses: () => focusLosses,
focusCalls: () => focusCalls,
};
}
const item = (id, overrides = {}) => ({
key: `flight:flights:${id}`, tone: 'nearby', title: `Flight ${id}`, detail: 'COMMERCIAL FLIGHT · 2 KM',
target: { layerId: 'flights', id }, timestamp: 1_700_000_000_000, ...overrides,
key: `flight:flights:${id}`,
tone: 'nearby',
title: `Flight ${id}`,
detail: 'COMMERCIAL FLIGHT · 2 KM',
target: { layerId: 'flights', id },
timestamp: 1_700_000_000_000,
...overrides,
});
const render = (f, items) => { f.controller.signalItems = items; f.controller.renderCockpitSignals(); };
const render = (f, items) => {
f.controller.signalItems = items;
f.controller.renderCockpitSignals();
};
const snapshot = (distanceM = 2000) => ({
evaluatedAt: 1_700_000_000_000,
subject: { layerId: 'flights', id: 'current', label: 'CURRENT' },
cohorts: [{ id: 'flights', count: 1, nearest: [{ id: 'nearby', label: 'NEARBY', distanceM }] }],
cohorts: [
{
id: 'flights',
count: 1,
nearest: [{ id: 'nearby', label: 'NEARBY', distanceM }],
},
],
});
test('actual Context refresh retains the focused signal button across unchanged and changed details', () => {
const f = fixture();
f.controller.updateCockpitSignals(snapshot(), 0);
const target = f.buttons()[1]; target.focus();
const target = f.buttons()[1];
target.focus();
const calls = f.focusCalls();
for (let tick = 0; tick < 8; tick += 1) f.controller.updateCockpitSignals(snapshot(2500), 0);
for (let tick = 0; tick < 8; tick += 1)
f.controller.updateCockpitSignals(snapshot(2500), 0);
assert.equal(f.buttons()[1], target);
assert.equal(f.document.activeElement, target);
assert.equal(f.losses(), 0);
assert.equal(f.focusCalls(), calls, 'refresh must preserve focus without repeatedly restoring it');
assert.match(f.controller.signalList.textContent, /2\.5 KM/);
assert.equal(
f.focusCalls(),
calls,
'refresh must preserve focus without repeatedly restoring it',
);
assert.match(f.controller.signalList.textContent, /2\.5\u00a0KM/);
});
test('signal content and accessible name update without replacing the active target', () => {
const f = fixture(); render(f, [item('a')]);
const target = f.buttons()[0]; target.focus();
render(f, [item('a', { title: 'RENAMED', tone: 'track', detail: 'CURRENT', timestamp: 1_700_000_001_000 })]);
const f = fixture();
render(f, [item('a')]);
const target = f.buttons()[0];
target.focus();
render(f, [
item('a', {
title: 'RENAMED',
tone: 'track',
detail: 'CURRENT',
timestamp: 1_700_000_001_000,
}),
]);
assert.equal(f.buttons()[0], target);
assert.equal(target.getAttribute('aria-label'), 'Select flight RENAMED');
assert.match(target.textContent, /RENAMED/);
assert.match(f.controller.signalList.textContent, /CURRENT/);
assert.equal(f.controller.signalList.children[0].className, 'track actionable');
assert.equal(
f.controller.signalList.children[0].className,
'track actionable',
);
assert.equal(f.document.activeElement, target);
assert.equal(f.losses(), 0);
});
test('signal ranking changes move surrounding rows without disconnecting the focused row', () => {
const f = fixture(); render(f, ['a', 'b', 'c', 'd'].map((id) => item(id)));
const target = f.buttons()[1]; target.focus();
const f = fixture();
render(
f,
['a', 'b', 'c', 'd'].map((id) => item(id)),
);
const target = f.buttons()[1];
target.focus();
const calls = f.focusCalls();
for (const order of [['d', 'c', 'b', 'a'], ['b', 'c', 'd', 'a'], ['a', 'd', 'c', 'b']]) {
render(f, order.map((id) => item(id)));
assert.deepEqual(f.buttons().map((button) => button.dataset.signalId), order);
for (const order of [
['d', 'c', 'b', 'a'],
['b', 'c', 'd', 'a'],
['a', 'd', 'c', 'b'],
]) {
render(
f,
order.map((id) => item(id)),
);
assert.deepEqual(
f.buttons().map((button) => button.dataset.signalId),
order,
);
assert.equal(f.document.activeElement, target);
assert.equal(f.losses(), 0);
}
@@ -158,37 +283,65 @@ test('signal ranking changes move surrounding rows without disconnecting the foc
});
test('a new pushed signal preserves existing focus and delegated selection reads the current target', () => {
const f = fixture(); render(f, [item('a'), item('b')]);
const target = f.buttons()[1]; target.focus();
f.controller.pushCockpitSignal('status', 'warning', 'INPUT UNKNOWN', 'SOURCE UNAVAILABLE');
const f = fixture();
render(f, [item('a'), item('b')]);
const target = f.buttons()[1];
target.focus();
f.controller.pushCockpitSignal(
'status',
'warning',
'INPUT UNKNOWN',
'SOURCE UNAVAILABLE',
);
assert.equal(f.document.activeElement, target);
assert.equal(f.losses(), 0);
const event = { target: target.children[0], prevented: false, preventDefault() { this.prevented = true; } };
const event = {
target: target.children[0],
prevented: false,
preventDefault() {
this.prevented = true;
},
};
f.click(event);
assert.equal(event.prevented, true);
assert.deepEqual(f.selected, [['flights', 'b', { origin: 'user' }]]);
});
test('removing the focused identity uses the existing briefing tab once and permits forward traversal', () => {
const f = fixture(); render(f, [item('a'), item('b')]);
const removed = f.buttons()[1]; removed.focus();
const f = fixture();
render(f, [item('a'), item('b')]);
const removed = f.buttons()[1];
removed.focus();
const calls = f.focusCalls();
render(f, [item('a')]);
assert.equal(removed.isConnected, false);
assert.equal(f.document.activeElement, f.controller.briefTabs[0]);
assert.equal(f.focusCalls(), calls + 1);
f.tab(); f.tab(); f.tab();
f.tab();
f.tab();
f.tab();
assert.equal(f.document.activeElement, f.display);
render(f, [item('a', { detail: 'UPDATED' })]);
assert.equal(f.document.activeElement, f.display, 'later refresh must not pull focus back');
assert.equal(
f.document.activeElement,
f.display,
'later refresh must not pull focus back',
);
f.tab();
assert.equal(f.document.activeElement, f.radio);
});
test('a changed target cannot reuse a focused action for another flight', () => {
const f = fixture(); render(f, [item('a', { key: 'shared-status-key' })]);
const removed = f.buttons()[0]; removed.focus();
render(f, [item('b', { key: 'shared-status-key', target: { layerId: 'military', id: 'b' } })]);
const f = fixture();
render(f, [item('a', { key: 'shared-status-key' })]);
const removed = f.buttons()[0];
removed.focus();
render(f, [
item('b', {
key: 'shared-status-key',
target: { layerId: 'military', id: 'b' },
}),
]);
const target = f.buttons()[0];
assert.notEqual(target, removed);
assert.equal(removed.isConnected, false);