From 1f9a453bc41400b06557dba0dc6a0ae4e2ccca87 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 1 Nov 2026 20:20:09 -0800 Subject: [PATCH] i18n(ar): own the Arabic catalog in the application package; mirror select chevrons The application package boundary owns src/i18n/locales/ar.json beside the other catalogs, and the style.css shim test checks that the shim holds only import statements rather than counting its lines, so adding rtl.css to it does not read as rules. Selects pad the chevron's side with padding-inline-end, as do the Radio filter, the SDR gain select and the voice timeout select, which set their own padding; rtl.css moves the chevron to the left, where an Arabic field's text ends. A rail legend's range keeps left-to-right order under its colour ramp, which always runs low to high from left to right. --- scripts/package-boundaries.json | 1 + src/annotations/drawTool.test.mjs | 9 ++++---- src/ui/rtlLayout.test.mjs | 35 +++++++++++++++++++++++++++++++ src/ui/styles/form-field.css | 4 ++-- src/ui/styles/radio.css | 6 ++++-- src/ui/styles/rtl.css | 15 +++++++++++++ src/ui/styles/voice-card.css | 3 ++- 7 files changed, 64 insertions(+), 9 deletions(-) diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 6c312deef..c8783bde9 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -60,6 +60,7 @@ "src/ui/scenePresentation.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/de.json", "src/i18n/locales/es.json", "src/i18n/locales/fr.json", diff --git a/src/annotations/drawTool.test.mjs b/src/annotations/drawTool.test.mjs index d955d2d6f..b4e25abf8 100644 --- a/src/annotations/drawTool.test.mjs +++ b/src/annotations/drawTool.test.mjs @@ -112,10 +112,11 @@ test('the DISPLAY rail styles live in their current owner, not the shim', () => // these rules lived in the root style.css. That file is an import shim now, // and putting rules back into it would silently undo the split. const shim = read('style.css'); - assert.ok( - shim.split('\n').length < 40, - 'style.css is an import shim, not a stylesheet', - ); + const rules = shim + .replace(/\/\*[\s\S]*?\*\//g, '') + .split('\n') + .filter((line) => line.trim() && !/^@import '[^']+';$/.test(line.trim())); + assert.deepEqual(rules, [], 'style.css is an import shim, not a stylesheet'); assert.doesNotMatch( shim, /draw-hint|pp-text-input/, diff --git a/src/ui/rtlLayout.test.mjs b/src/ui/rtlLayout.test.mjs index ea9056a6f..a28ddcaa5 100644 --- a/src/ui/rtlLayout.test.mjs +++ b/src/ui/rtlLayout.test.mjs @@ -111,3 +111,38 @@ test('the Space Missions counter reads as a number pair in right-to-left', () => assert.match(counters[1], /\[data-mission-index\]/); assert.match(counters[2], /direction: ltr;/); }); + +test('select chevrons and legend ranges follow the field and the ramp in right-to-left', () => { + const field = sheet('form-field.css'); + assert.match( + ruleBody(field, 'select.gev-field'), + /padding-inline-end: 22px;/, + ); + assert.match( + ruleBody(field, 'select.gev-field--compact'), + /padding-inline-end: 18px;/, + ); + // Selects that set their own padding keep the chevron's room at the end. + for (const [name, selector, inline] of [ + ['radio.css', '#radio-filter', '8px 24px'], + ['radio.css', '.sdr-gain-row select', '7px 22px'], + ['voice-card.css', '#gev-voice-timeout-select', '0.45rem 22px'], + ]) { + const body = ruleBody(sheet(name), selector); + assert.match(body, new RegExp(`padding-inline: ${inline};`)); + assert.doesNotMatch(body, /(?:^|\s)padding(?:-right)?:/); + } + const rtl = sheet('rtl.css'); + assert.match( + ruleBody(rtl, "html[dir='rtl'] select.gev-field"), + /background-position: left 8px center;/, + ); + assert.match( + ruleBody(rtl, "html[dir='rtl'] select.gev-field--compact"), + /background-position: left 6px center;/, + ); + assert.match( + ruleBody(rtl, "html[dir='rtl'] .rail-card-scale"), + /direction: ltr;/, + ); +}); diff --git a/src/ui/styles/form-field.css b/src/ui/styles/form-field.css index 81462ba21..11e7d5a2d 100644 --- a/src/ui/styles/form-field.css +++ b/src/ui/styles/form-field.css @@ -19,7 +19,7 @@ select.gev-field { -webkit-appearance: none; appearance: none; - padding-right: 22px; + padding-inline-end: 22px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='rgba(255,255,255,0.62)' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; @@ -45,7 +45,7 @@ select.gev-field { } select.gev-field--compact { - padding-right: 18px; + padding-inline-end: 18px; background-position: right 6px center; } diff --git a/src/ui/styles/radio.css b/src/ui/styles/radio.css index 4d6df2230..e491c7409 100644 --- a/src/ui/styles/radio.css +++ b/src/ui/styles/radio.css @@ -49,7 +49,8 @@ #radio-filter { /* Radio owns its compact density and arrow space; shared fields paint the palette and disabled state, including this intentional theme exception. */ - padding: 7px 24px 7px 8px; + padding-block: 7px; + padding-inline: 8px 24px; border-radius: 5px; font: 9px/1.2 var(--font-mono); --gev-field-border-color: rgba(65, 220, 231, 0.28); @@ -1128,7 +1129,8 @@ html:not([lang='en']) .data-row-list-text { .sdr-gain-row select { flex: 1; min-width: 0; - padding: 5px 22px 5px 7px; + padding-block: 5px; + padding-inline: 7px 22px; } .sdr-adsb-stats { display: grid; diff --git a/src/ui/styles/rtl.css b/src/ui/styles/rtl.css index e5e3c629d..10b5c13bd 100644 --- a/src/ui/styles/rtl.css +++ b/src/ui/styles/rtl.css @@ -205,3 +205,18 @@ html[dir='rtl'] html[dir='rtl'] #radio-tuner-value { unicode-bidi: plaintext; } + +/* The select chevron sits at the end of the field, where the text stops + (form-field.css pads that side through padding-inline-end). */ +html[dir='rtl'] select.gev-field { + background-position: left 8px center; +} +html[dir='rtl'] select.gev-field--compact { + background-position: left 6px center; +} + +/* A legend's range is printed under its colour ramp, which always runs low + to high from left to right; the numbers keep that order. */ +html[dir='rtl'] .rail-card-scale { + direction: ltr; +} diff --git a/src/ui/styles/voice-card.css b/src/ui/styles/voice-card.css index dfda6d9c7..637a4e88a 100644 --- a/src/ui/styles/voice-card.css +++ b/src/ui/styles/voice-card.css @@ -553,7 +553,8 @@ body.recording-mode .gev-pointer-reticle { #gev-voice-timeout-select { flex: 1 1 auto; min-width: 0; - padding: 0 22px 0 0.45rem; + padding-block: 0; + padding-inline: 0.45rem 22px; } #gev-voice-timeout-input { flex: 0 0 4.5rem;