mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
i18n(ar): keep values whole in right-to-left text
Fixes from the Arabic screenshot review: - t() isolates every interpolated value in RTL locales (U+2068…U+2069), so a time (10/28 09:00 UTC), a code or an English name keeps its order inside an Arabic sentence; coordinates (30.326° ش) are isolated too. No-op in left-to-right languages. - The cockpit position readout reads right to left again now that its coordinates are isolated; gauges, tapes and roll counters stay LTR. - The start window's arrows point along the reading direction; FM seek and the glyph-only mini transport stay left to right, the worded PREV / PLAY / NEXT row follows the page; the Bhote Koshi clock stays a number. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
33985138ef
commit
4c3e366211
5 files changed
+49
-23
No files matched your search
@@ -71,13 +71,16 @@ test('Arabic formats with Western digits whatever ICU defaults to', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** The text without the bidi isolates t() puts around values in RTL. */
|
||||||
|
const plain = (text) => text.replace(/[\u2068\u2069]/g, '');
|
||||||
|
|
||||||
test('Arabic uses all six plural forms and spaced HUD units', () => {
|
test('Arabic uses all six plural forms and spaced HUD units', () => {
|
||||||
const previous = getLocale();
|
const previous = getLocale();
|
||||||
try {
|
try {
|
||||||
setLocale('ar');
|
setLocale('ar');
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
[0, 1, 2, 3, 11, 100].map((n) =>
|
[0, 1, 2, 3, 11, 100].map((n) =>
|
||||||
tn(n, '{count} SOURCE', '{count} SOURCES'),
|
plain(tn(n, '{count} SOURCE', '{count} SOURCES')),
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
'0 مصدر',
|
'0 مصدر',
|
||||||
@@ -96,11 +99,16 @@ test('Arabic uses all six plural forms and spaced HUD units', () => {
|
|||||||
'Sentinel-2 · \u206830\u00a0م\u2069',
|
'Sentinel-2 · \u206830\u00a0م\u2069',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
LOCALIZED_LOCALITY.sector({ latDeg: 21.33, lonDeg: -77.5 }),
|
plain(LOCALIZED_LOCALITY.sector({ latDeg: 21.33, lonDeg: -77.5 })),
|
||||||
'القطاع 21.33ش 77.50غ',
|
'القطاع 21.33ش 77.50غ',
|
||||||
);
|
);
|
||||||
assert.equal(READOUT_WORDS.daysAgo(9), 'قبل 9 أيام');
|
assert.equal(READOUT_WORDS.daysAgo(9), 'قبل 9 أيام');
|
||||||
assert.equal(t('SAVE KEYS'), 'حفظ المفاتيح');
|
assert.equal(t('SAVE KEYS'), 'حفظ المفاتيح');
|
||||||
|
// Each value is isolated, so a time keeps its order inside Arabic text.
|
||||||
|
assert.equal(
|
||||||
|
t('Issued: {time}', { time: '10/28 00:00 UTC' }),
|
||||||
|
'صدر: \u206810/28 00:00 UTC\u2069',
|
||||||
|
);
|
||||||
setLocale('ru');
|
setLocale('ru');
|
||||||
assert.equal(formatCockpitDistance(14000), '14 КМ');
|
assert.equal(formatCockpitDistance(14000), '14 КМ');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+7
-3
@@ -81,9 +81,13 @@ const PLACEHOLDER_PATTERN = /\{([A-Za-z0-9_]+)\}/g;
|
|||||||
* stays visible instead of silently vanishing. */
|
* stays visible instead of silently vanishing. */
|
||||||
function interpolate(pattern, values) {
|
function interpolate(pattern, values) {
|
||||||
if (!values) return pattern;
|
if (!values) return pattern;
|
||||||
return pattern.replace(PLACEHOLDER_PATTERN, (literal, name) =>
|
// In a right-to-left page each value is isolated: a time, a code or an
|
||||||
Object.hasOwn(values, name) ? String(values[name]) : literal,
|
// English name keeps its own order instead of mixing with the sentence.
|
||||||
);
|
return pattern.replace(PLACEHOLDER_PATTERN, (literal, name) => {
|
||||||
|
if (!Object.hasOwn(values, name)) return literal;
|
||||||
|
const value = String(values[name]);
|
||||||
|
return value === '' ? value : isolate(value);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A catalog string with something to show; an empty one counts as missing. */
|
/** A catalog string with something to show; an empty one counts as missing. */
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { formatNumber, t } from '../../i18n/index.js';
|
import { formatNumber, isolate, t } from '../../i18n/index.js';
|
||||||
import {
|
import {
|
||||||
satelliteShortName,
|
satelliteShortName,
|
||||||
fireDetectionKey,
|
fireDetectionKey,
|
||||||
@@ -513,7 +513,7 @@ export function createModel({
|
|||||||
lon >= 0
|
lon >= 0
|
||||||
? t('{value}°E', { value: amount(lon) })
|
? t('{value}°E', { value: amount(lon) })
|
||||||
: t('{value}°W', { value: amount(lon) });
|
: t('{value}°W', { value: amount(lon) });
|
||||||
return `${latPart} ${lonPart}`;
|
return `${isolate(latPart)} ${isolate(lonPart)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Millisecond delta → "<1h" / "Xh" / "Xd", or '' for invalid input. */
|
/** Millisecond delta → "<1h" / "Xh" / "Xd", or '' for invalid input. */
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/** Pure formatting and fixed presentation constants used by Cockpit components. */
|
/** Pure formatting and fixed presentation constants used by Cockpit components. */
|
||||||
import { formatNumber, getLocale, t, msg } from '../i18n/index.js';
|
import { formatNumber, getLocale, isolate, t, msg } from '../i18n/index.js';
|
||||||
import { formatQuantity } from './unitLabels.js';
|
import { formatQuantity } from './unitLabels.js';
|
||||||
|
|
||||||
export const COCKPIT_HEADING_SLEW_DPS = 28;
|
export const COCKPIT_HEADING_SLEW_DPS = 28;
|
||||||
@@ -124,18 +124,19 @@ export function formatCockpitCoordinate(value, positive, negative) {
|
|||||||
maximumFractionDigits: 3,
|
maximumFractionDigits: 3,
|
||||||
});
|
});
|
||||||
const code = value >= 0 ? positive : negative;
|
const code = value >= 0 ? positive : negative;
|
||||||
// Not «30,203°С»: after a degree sign «С» reads as Celsius.
|
// Not «30,203°С»: after a degree sign «С» reads as Celsius. Isolated, so
|
||||||
|
// an Arabic hemisphere letter does not pull the next number into its run.
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case 'N':
|
case 'N':
|
||||||
return t('{value}°N', { value: amount });
|
return isolate(t('{value}°N', { value: amount }));
|
||||||
case 'S':
|
case 'S':
|
||||||
return t('{value}°S', { value: amount });
|
return isolate(t('{value}°S', { value: amount }));
|
||||||
case 'E':
|
case 'E':
|
||||||
return t('{value}°E', { value: amount });
|
return isolate(t('{value}°E', { value: amount }));
|
||||||
case 'W':
|
case 'W':
|
||||||
return t('{value}°W', { value: amount });
|
return isolate(t('{value}°W', { value: amount }));
|
||||||
default:
|
default:
|
||||||
return `${amount}°${compassPointLabel(code)}`;
|
return isolate(`${amount}°${compassPointLabel(code)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-9
@@ -68,16 +68,22 @@ html:lang(ar) :is(#intel-hud, #cockpit-hud) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* The right rail sits on the left: its expand/collapse arrows point the
|
/* The right rail sits on the left: its expand/collapse arrows point the
|
||||||
other way. */
|
other way, and the start window's "go" arrows point along the reading
|
||||||
html[dir='rtl'] #right-context-rail .panel-collapse-btn {
|
direction. */
|
||||||
|
html[dir='rtl'] #right-context-rail .panel-collapse-btn,
|
||||||
|
html[dir='rtl'] .first-run-arrow {
|
||||||
transform: scaleX(-1);
|
transform: scaleX(-1);
|
||||||
}
|
}
|
||||||
|
html[dir='rtl'] .first-run-choices button:hover .first-run-arrow,
|
||||||
|
html[dir='rtl'] .first-run-choices button:focus-visible .first-run-arrow {
|
||||||
|
transform: translateX(-2px) scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
/* The cockpit is an aircraft panel, not page chrome: its windows keep their
|
/* The cockpit is an aircraft panel, not page chrome: its windows keep their
|
||||||
physical sides, so the data-layer stack goes back to the left edge instead
|
physical sides, so the data-layer stack goes back to the left edge instead
|
||||||
of landing on the utility controls. Gauges, tapes, the roll counters and the
|
of landing on the utility controls. Gauges, tapes and the roll counters stay
|
||||||
position readout stay left to right (speed left, altitude right, -10 not
|
left to right (speed left, altitude right, -10 not 10-); the cards keep
|
||||||
10-); the cards keep reading right to left. */
|
reading right to left. */
|
||||||
html[dir='rtl'] body.cockpit-mode #left-panel-stack {
|
html[dir='rtl'] body.cockpit-mode #left-panel-stack {
|
||||||
left: var(--left-stack-x);
|
left: var(--left-stack-x);
|
||||||
right: auto;
|
right: auto;
|
||||||
@@ -90,7 +96,6 @@ html[dir='rtl']
|
|||||||
.cockpit-pitch-rail,
|
.cockpit-pitch-rail,
|
||||||
.cockpit-altitude-rim,
|
.cockpit-altitude-rim,
|
||||||
.cockpit-roll-arc,
|
.cockpit-roll-arc,
|
||||||
.cockpit-position-readout,
|
|
||||||
.cockpit-vision-controls
|
.cockpit-vision-controls
|
||||||
) {
|
) {
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
@@ -110,9 +115,17 @@ html[dir='rtl']
|
|||||||
unicode-bidi: plaintext;
|
unicode-bidi: plaintext;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Physical controls keep their left-to-right sense: media transport
|
/* Clock readouts (~0:05 / 5:19) are numbers, not prose. */
|
||||||
(previous / play / next) and the radio dial. */
|
html[dir='rtl'] #bhote-koshi-event-panel [data-role='time'] {
|
||||||
|
direction: ltr;
|
||||||
|
unicode-bidi: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Physical controls keep their left-to-right sense: the glyph-only media
|
||||||
|
transport (◀◀ ▶ ▶▶), FM seek down/up and the radio dial. The worded
|
||||||
|
PREV / PLAY / NEXT row follows the reading direction like any other
|
||||||
|
button row. */
|
||||||
html[dir='rtl']
|
html[dir='rtl']
|
||||||
:is(.radio-transport, .context-radio-mini-transport, .radio-tuner) {
|
:is(.context-radio-mini-transport, .sdr-seek-row, .radio-tuner) {
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user