i18n(ar): register Arabic as the first right-to-left language

- Add ar to LOCALES (picker name العربية) and RTL_LOCALES, so <html dir>
  turns rtl; any ar-* tag maps to it.
- Intl formatters get ar-u-nu-latn: numbers and dates keep Western digits
  like the catalog, whatever the browser's ICU picks for Arabic regions.
- HUD quantities keep a space before an Arabic unit (2069.0 كم), as in
  Chinese, instead of running digits into the word.
- Codes and product names that stay Latin go to same-as-english.json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-28 12:55:20 +04:00
1 parent 94dc2736e1
commit 0b928b6810
8 files changed
+239 -8

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { formatDate, formatNumber, getLocale, setLocale, t, tn } from './index.js';
import {
applyDocumentLanguage,
intlLocale,
LOCALE_STORAGE_KEY,
resolveLocale,
} from './locale.js';
import { hudQuantity, LOCALIZED_LOCALITY } from '../hudLocalityFormat.js';
import { formatCockpitDistance } from '../ui/cockpitPresentation.js';
import { READOUT_WORDS } from '../layers/recentImagery/display.js';
test('Arabic resolves from URL, saved preference and browser language', () => {
assert.equal(
resolveLocale({
location: { search: '?lang=ar-EG' },
storage: { getItem: () => 'en' },
languages: ['en'],
}),
'ar',
);
assert.equal(
resolveLocale({
location: { search: '' },
storage: {
getItem: (key) => (key === LOCALE_STORAGE_KEY ? 'ar' : null),
},
languages: ['en'],
}),
'ar',
);
assert.equal(
resolveLocale({
location: { search: '' },
storage: { getItem: () => null },
languages: ['ar-SA', 'en'],
}),
'ar',
);
});
test('Arabic turns the page right-to-left; other languages stay left-to-right', () => {
const doc = { documentElement: {} };
applyDocumentLanguage(doc, 'ar');
assert.deepEqual(doc.documentElement, { lang: 'ar', dir: 'rtl' });
applyDocumentLanguage(doc, 'zh-CN');
assert.deepEqual(doc.documentElement, { lang: 'zh-CN', dir: 'ltr' });
});
test('Arabic formats with Western digits whatever ICU defaults to', () => {
assert.equal(intlLocale('ar'), 'ar-u-nu-latn');
assert.equal(intlLocale('ru'), 'ru');
const previous = getLocale();
try {
setLocale('ar');
assert.equal(formatNumber(1234.5), '1,234.5');
assert.equal(
formatDate(Date.UTC(2026, 7, 26), {
day: 'numeric',
month: 'long',
year: 'numeric',
timeZone: 'UTC',
}),
'26 أغسطس 2026',
);
assert.doesNotMatch(formatNumber(9876543210), /[٠-٩]/);
} finally {
setLocale(previous);
}
});
test('Arabic uses all six plural forms and spaced HUD units', () => {
const previous = getLocale();
try {
setLocale('ar');
assert.deepEqual(
[0, 1, 2, 3, 11, 100].map((n) =>
tn(n, '{count} SOURCE', '{count} SOURCES'),
),
[
'0 مصدر',
'مصدر واحد (1)',
'مصدران (2)',
'3 مصادر',
'11 مصدرًا',
'100 مصدر',
],
);
assert.equal(hudQuantity(2069, 'km', 1), '2069.0 كم');
assert.equal(formatCockpitDistance(14000), '14 كم');
assert.equal(
LOCALIZED_LOCALITY.sector({ latDeg: 21.33, lonDeg: -77.5 }),
'القطاع 21.33ش 77.50غ',
);
assert.equal(READOUT_WORDS.daysAgo(9), 'قبل 9 أيام');
assert.equal(t('SAVE KEYS'), 'حفظ المفاتيح');
} finally {
setLocale(previous);
}
});