mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
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:
1 parent
94dc2736e1
commit
0b928b6810
8 files changed
+239
-8
No files matched your search
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -47,6 +47,9 @@ test('normalizeLocale folds regional variants and rejects unshipped tags', () =>
|
||||
assert.equal(normalizeLocale('zh-Hant-CN'), 'zh-Hant');
|
||||
assert.equal(normalizeLocale('zh_HK'), 'zh-Hant');
|
||||
assert.equal(normalizeLocale('zh-MO'), 'zh-Hant');
|
||||
assert.equal(normalizeLocale('ar'), 'ar');
|
||||
assert.equal(normalizeLocale('ar-EG'), 'ar');
|
||||
assert.equal(normalizeLocale(' AR_sa '), 'ar');
|
||||
assert.equal(normalizeLocale(undefined), null);
|
||||
});
|
||||
|
||||
@@ -102,6 +105,10 @@ test('applyDocumentLanguage sets lang and dir', () => {
|
||||
const doc = { documentElement: {} };
|
||||
applyDocumentLanguage(doc, 'ru-RU');
|
||||
assert.deepEqual(doc.documentElement, { lang: 'ru', dir: 'ltr' });
|
||||
applyDocumentLanguage(doc, 'ar-EG');
|
||||
assert.deepEqual(doc.documentElement, { lang: 'ar', dir: 'rtl' });
|
||||
applyDocumentLanguage(doc, 'en');
|
||||
assert.deepEqual(doc.documentElement, { lang: 'en', dir: 'ltr' });
|
||||
});
|
||||
|
||||
test('t() returns English text in English and translates in Russian', () => {
|
||||
|
||||
+10
-2
@@ -13,15 +13,23 @@
|
||||
// `npm run i18n` keeps the catalogs in step with the code. Guide: docs/I18N.md.
|
||||
import {
|
||||
DEFAULT_LOCALE,
|
||||
intlLocale,
|
||||
normalizeLocale,
|
||||
writeStoredLocale,
|
||||
} from './locale.js';
|
||||
import ar from './locales/ar.json' with { type: 'json' };
|
||||
import es from './locales/es.json' with { type: 'json' };
|
||||
import ru from './locales/ru.json' with { type: 'json' };
|
||||
import zhCN from './locales/zh-CN.json' with { type: 'json' };
|
||||
import zhHant from './locales/zh-Hant.json' with { type: 'json' };
|
||||
|
||||
const CATALOGS = Object.freeze({ es, ru, 'zh-CN': zhCN, 'zh-Hant': zhHant });
|
||||
const CATALOGS = Object.freeze({
|
||||
ar,
|
||||
es,
|
||||
ru,
|
||||
'zh-CN': zhCN,
|
||||
'zh-Hant': zhHant,
|
||||
});
|
||||
|
||||
let currentLocale = DEFAULT_LOCALE;
|
||||
|
||||
@@ -157,7 +165,7 @@ function cachedFormatter(kind, Constructor, options) {
|
||||
const cacheKey = `${kind}|${currentLocale}|${options ? JSON.stringify(options) : ''}`;
|
||||
let formatter = formatterCache.get(cacheKey);
|
||||
if (!formatter) {
|
||||
formatter = new Constructor(currentLocale, options);
|
||||
formatter = new Constructor(intlLocale(currentLocale), options);
|
||||
formatterCache.set(cacheKey, formatter);
|
||||
}
|
||||
return formatter;
|
||||
|
||||
+24
-2
@@ -8,11 +8,19 @@ export const LOCALE_STORAGE_KEY = 'gev:locale:v1';
|
||||
export const DEFAULT_LOCALE = 'en';
|
||||
|
||||
/** Shipped locales; order does not matter. Adding one: docs/TRANSLATORS.md. */
|
||||
export const LOCALES = Object.freeze(['en', 'es', 'ru', 'zh-CN', 'zh-Hant']);
|
||||
export const LOCALES = Object.freeze([
|
||||
'en',
|
||||
'ar',
|
||||
'es',
|
||||
'ru',
|
||||
'zh-CN',
|
||||
'zh-Hant',
|
||||
]);
|
||||
|
||||
/** Each locale named in its own language, for the selector. */
|
||||
export const LOCALE_NAMES = Object.freeze({
|
||||
en: 'English',
|
||||
ar: 'العربية',
|
||||
es: 'Español',
|
||||
ru: 'Русский',
|
||||
'zh-CN': '中文(简体)',
|
||||
@@ -20,10 +28,24 @@ export const LOCALE_NAMES = Object.freeze({
|
||||
});
|
||||
|
||||
/** Right-to-left locales. */
|
||||
const RTL_LOCALES = Object.freeze([]);
|
||||
const RTL_LOCALES = Object.freeze(['ar']);
|
||||
|
||||
/**
|
||||
* Tags for Intl formatters where the locale code alone is not enough. Arabic
|
||||
* keeps Western digits (0–9) like its catalog: ICU picks Arabic-Indic digits
|
||||
* (٠–٩) for some Arabic regions and browsers, and a HUD that mixes both
|
||||
* misreads.
|
||||
*/
|
||||
const INTL_TAGS = Object.freeze({ ar: 'ar-u-nu-latn' });
|
||||
|
||||
/** The tag to hand Intl.NumberFormat, DateTimeFormat and the like. */
|
||||
export function intlLocale(locale) {
|
||||
return INTL_TAGS[locale] ?? locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map any language tag to a shipped locale: 'ru-RU' and 'RU' → 'ru',
|
||||
* 'ar-EG' and 'ar-SA' → 'ar',
|
||||
* 'zh', 'zh-CN' and 'zh-Hans-SG' → 'zh-CN';
|
||||
* 'zh-Hant', 'zh-TW', 'zh-HK' and 'zh-MO' → 'zh-Hant'.
|
||||
* @returns {string|null}
|
||||
|
||||
@@ -1,4 +1,38 @@
|
||||
{
|
||||
"ar": [
|
||||
"1090 MHz",
|
||||
"978 MHz UAT",
|
||||
"AIS: --",
|
||||
"AIS: {name}",
|
||||
"BRG {value}°",
|
||||
"BRG —",
|
||||
"CAL {value}",
|
||||
"CAL · {badge}",
|
||||
"CCTV",
|
||||
"CRS {value}°",
|
||||
"CRT",
|
||||
"FLIR",
|
||||
"FOV",
|
||||
"FOV {value}°",
|
||||
"GEO",
|
||||
"GOOGLE STREET VIEW",
|
||||
"Google Street View",
|
||||
"HDG",
|
||||
"HDG {value}°",
|
||||
"Ironbow",
|
||||
"Landsat 8/9",
|
||||
"NVG",
|
||||
"REC",
|
||||
"S2 GhostMaps",
|
||||
"SQUAWK {code} · {status}",
|
||||
"SRC {value}",
|
||||
"Sentinel-2",
|
||||
"WHOT/BHOT",
|
||||
"dB",
|
||||
"hPa",
|
||||
"{source} HTTP {status}",
|
||||
"▲ {frp} MW"
|
||||
],
|
||||
"es": [
|
||||
"1090 MHz",
|
||||
"978 MHz UAT",
|
||||
|
||||
Reference in new issue
Block a user