i18n(ar): address code review findings

Mirror the dock trays and their pin, the compact Radio window and the
phone top-bar actions in right-to-left; keep the Cyber cockpit's Data
Layers stack on the left edge. Isolate the Space Missions counter,
landing coordinates and CCTV summary parts, format their numbers with
the locale's numerals, decide pack shot titles by the marker binding,
and place Bhote Koshi media cards between the panels on either side.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-29 15:20:47 +04:00
1 parent 8c291d8a0c
commit 04481ef202
17 files changed
+313 -55

No files matched your search

+14 -1
View File
@@ -18,6 +18,16 @@ import { flyToDataSource } from './flyToDataSource.js';
// One color per file, in import order, from the whiteboard palette.
const COLORS = ['#39d0ff', '#ffb547', '#5dff9f', '#ff6b6b', '#8be9ff'];
/** An import error's values, with its counts and limits in local numerals. */
function localizedValues(values) {
return Object.fromEntries(
Object.entries(values || {}).map(([name, value]) => [
name,
typeof value === 'number' ? formatNumber(value) : value,
]),
);
}
/**
* Wire the Import control.
* @param {{viewer: object, load?: Function, document?: Document}} deps
@@ -303,7 +313,10 @@ export function initGeoFileImport({
ok: false,
error:
error instanceof GeoImportError || error?.name === 'GeoImportError'
? t(error.template || error.message, error.values)
? t(
error.template || error.message,
localizedValues(error.values),
)
: t('That file could not be read.'),
});
continue;
+18
View File
@@ -729,3 +729,21 @@ test('the map-object count in a row title keeps digit grouping', async () => {
assert.match(row.children[1].title, /12,345 map objects/);
t.tool.destroy();
});
test('a refusal shows its counts in the language’s numerals, not the browser’s', async () => {
const t = setup({
load: async () => {
throw new GeoImportError(
'That file has 60,000 map objects; the limit is 50,000.',
'That file has {count} map objects; the limit is {limit}.',
{ count: 60000, limit: 50000 },
);
},
});
const [out] = await t.tool.importFiles([file('huge.kml')]);
assert.equal(
out.error,
'That file has 60,000 map objects; the limit is 50,000.',
);
t.tool.destroy();
});
+98
View File
@@ -0,0 +1,98 @@
// Right-to-left layout pins: chrome that mirrors in Arabic must mirror on
// both sides of a pair, or one half lands on the other or off the screen.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
const sheet = (name) =>
readFileSync(new URL(`./styles/${name}`, import.meta.url), 'utf8');
/** The declarations of the first rule whose selector is exactly `selector`. */
function ruleBody(css, selector) {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const match = new RegExp(`(?:^|\\n)\\s*${escaped}\\s*\\{([^}]*)\\}`).exec(
css,
);
assert.ok(match, `no rule for ${selector}`);
return match[1];
}
test('the dock trays open from their own wing in right-to-left', () => {
const rtl = sheet('rtl.css');
const location = ruleBody(
rtl,
"html[dir='rtl'] #command-dock #location-bar .dock-popover-content",
);
assert.match(
location,
/right: calc\(\(var\(--dock-unified-width\) - var\(--dock-location-open\)\) \/ 2\);/,
);
assert.match(location, /left: auto;/);
assert.match(location, /transform-origin: bottom right;/);
const presets = ruleBody(
rtl,
"html[dir='rtl'] #command-dock #control-panel .dock-popover-content",
);
assert.match(
presets,
/left: calc\(\(var\(--dock-unified-width\) - var\(--dock-presets-open\)\) \/ 2\);/,
);
assert.match(presets, /right: auto;/);
assert.match(presets, /transform-origin: bottom left;/);
// The narrow band re-centres both trays on the mobile tray width.
const narrow = /@media \(max-width: 900px\) \{([\s\S]*?)\n\}/.exec(rtl);
assert.ok(narrow, 'no max-width:900px block in rtl.css');
assert.match(
narrow[1],
/#location-bar \.dock-popover-content \{\s*right: calc\(\s*\(var\(--dock-unified-width\) - var\(--dock-popover-mobile-width\)\) \/ 2\s*\);/,
);
assert.match(
narrow[1],
/#control-panel \.dock-popover-content \{\s*left: calc\(\s*\(var\(--dock-unified-width\) - var\(--dock-popover-mobile-width\)\) \/ 2\s*\);/,
);
});
test('the Cyber cockpit keeps Data Layers on the left edge in every language', () => {
const body = ruleBody(
sheet('cyber.css'),
":root[data-ui-theme='cyber'] body.cockpit-mode #left-panel-stack",
);
// inset-inline-start would become right: 30px in Arabic and beat
// rtl.css's reset, landing the stack on the utility controls.
assert.doesNotMatch(body, /inset-inline-start/);
assert.match(body, /left: 30px;/);
assert.match(body, /right: auto;/);
});
test('the compact Radio window opens toward the screen in right-to-left', () => {
const layers = ruleBody(sheet('layers.css'), '.context-radio-mini');
assert.match(layers, /inset-inline-end: -38px;/);
assert.doesNotMatch(layers, /(?:^|\s)right:/);
const responsive = /\.context-radio-mini \{([^}]*)\}/.exec(
sheet('responsive.css'),
);
assert.ok(responsive);
assert.match(responsive[1], /inset-inline-end: -38px;/);
assert.doesNotMatch(responsive[1], /(?:^|\s)right:/);
});
test('phone top-bar actions mirror with the logo', () => {
const mobile = sheet('mobile-shell.css');
const actions = ruleBody(mobile, 'body.mobile-shell #top-center-actions');
assert.match(actions, /inset-inline-start: auto;/);
assert.match(actions, /inset-inline-end: 12px;/);
assert.doesNotMatch(actions, /(?:^|\s)right:/);
const corner = ruleBody(mobile, 'body.mobile-shell #corner-actions');
assert.match(corner, /inset-inline-end: 12px;/);
assert.doesNotMatch(corner, /(?:^|\s)right:/);
});
test('the Space Missions counter reads as a number pair in right-to-left', () => {
const counters =
/html\[dir='rtl'\]\s*:is\(([^)]*\[data-role='beat-index'\][^)]*)\)\s*\{([^}]*)\}/.exec(
sheet('rtl.css'),
);
assert.ok(counters, 'no left-to-right counter rule in rtl.css');
assert.match(counters[1], /\[data-mission-index\]/);
assert.match(counters[2], /direction: ltr;/);
});
+4 -1
View File
@@ -2721,7 +2721,10 @@
:root[data-ui-theme='cyber'] body.cockpit-mode #left-panel-stack {
top: clamp(164px, 19vh, 204px) !important;
bottom: auto;
inset-inline-start: 30px;
/* The cockpit keeps its physical sides in every language (rtl.css): the
utility controls hold the right edge. */
right: auto;
left: 30px;
width: var(--cyber-panel-width);
padding-inline-start: 0;
border-inline-start: 0;
+1 -1
View File
@@ -1059,7 +1059,7 @@
position: absolute;
z-index: 5;
top: calc(100% + 20px);
right: -38px;
inset-inline-end: -38px;
display: none;
width: 244px;
box-sizing: border-box;
+4 -3
View File
@@ -84,7 +84,7 @@ body.mobile-shell[data-mobile-sheet] #mobile-sheet-backdrop:not([hidden]) {
background: rgba(0, 0, 0, 0.18);
}
/* ── Top bar: logo left, globe actions right ───────────────────────────── */
/* ── Top bar: logo at the start, globe actions at the end ──────────────── */
body.mobile-shell #title-bar {
top: calc(var(--mobile-top-inset) + 10px);
inset-inline-start: 14px;
@@ -101,7 +101,8 @@ body.mobile-shell #title-bar .subtitle {
body.mobile-shell #top-center-actions {
top: calc(var(--mobile-top-inset) + 10px);
right: 12px;
inset-inline-start: auto;
inset-inline-end: 12px;
}
/* ── HUD: one compact readout under the top bar ────────────────────────── */
@@ -401,7 +402,7 @@ body.mobile-shell:not(.cockpit-mode) #view-switcher {
body.mobile-shell #corner-actions {
top: calc(var(--mobile-top-inset) + 60px);
right: 12px;
inset-inline-end: 12px;
bottom: auto;
}
+1 -1
View File
@@ -229,7 +229,7 @@
width: 100%;
}
.context-radio-mini {
right: -38px;
inset-inline-end: -38px;
width: min(244px, calc(100vw - 32px));
}
+33 -1
View File
@@ -77,6 +77,37 @@ html:lang(ar) :is(#intel-hud, #cockpit-hud) {
}
}
/* The dock's wings swap sides (Location on the right, Visual presets on the
left), so each slide-up tray is anchored from the wing's outer edge and
centred on the whole dock from there, as it is in left-to-right. */
html[dir='rtl'] #command-dock #location-bar .dock-popover-content {
right: calc((var(--dock-unified-width) - var(--dock-location-open)) / 2);
left: auto;
transform-origin: bottom right;
}
html[dir='rtl'] #command-dock #control-panel .dock-popover-content {
right: auto;
left: calc((var(--dock-unified-width) - var(--dock-presets-open)) / 2);
transform-origin: bottom left;
}
@media (max-width: 900px) {
html[dir='rtl'] #command-dock #location-bar .dock-popover-content {
right: calc(
(var(--dock-unified-width) - var(--dock-popover-mobile-width)) / 2
);
}
html[dir='rtl'] #command-dock #control-panel .dock-popover-content {
left: calc(
(var(--dock-unified-width) - var(--dock-popover-mobile-width)) / 2
);
}
}
/* The tray pin sits on the tray's top end corner: top-left here. */
html[dir='rtl'] #command-dock :is(#location-bar, #control-panel) .dock-pin-btn {
right: auto;
left: -1rem;
}
/* The right rail sits on the left: its expand/collapse arrows point the
other way, and the start window's "go" arrows point along the reading
direction. */
@@ -165,7 +196,8 @@ html[dir='rtl'] .gev-anno-text {
html[dir='rtl']
:is(
#bhote-koshi-event-panel [data-role='beat-index'],
#cockpit-brief-position
#cockpit-brief-position,
[data-mission-index]
) {
direction: ltr;
unicode-bidi: isolate;