From 0b928b6810a046c598c89d1bd04e314fbe2dcbd5 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Wed, 28 Oct 2026 12:55:20 +0400 Subject: [PATCH] i18n(ar): register Arabic as the first right-to-left language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ar to LOCALES (picker name العربية) and RTL_LOCALES, so 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 --- scripts/package-boundaries.json | 43 ++++++++++++++ src/hudLocalityFormat.js | 8 ++- src/i18n/ar-display.test.mjs | 101 ++++++++++++++++++++++++++++++++ src/i18n/i18n.test.mjs | 7 +++ src/i18n/index.js | 12 +++- src/i18n/locale.js | 26 +++++++- src/i18n/same-as-english.json | 34 +++++++++++ src/ui/localePicker.test.mjs | 16 ++++- 8 files changed, 239 insertions(+), 8 deletions(-) create mode 100644 src/i18n/ar-display.test.mjs diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 9f06f73f6..3470c1e8f 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -439,6 +439,7 @@ "src/ui/slider.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -479,6 +480,7 @@ "src/localRequestGate.mjs", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -509,6 +511,7 @@ "src/data/layerSnapshot.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -531,6 +534,7 @@ "src/app/stateChannel.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -548,6 +552,7 @@ "src/ui/radioTunerModel.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -568,6 +573,7 @@ "src/ui/cctvPresentation.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -595,6 +601,7 @@ "src/ui/panelRailGeometry.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -630,6 +637,7 @@ "src/ui/panelRailGeometry.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -647,6 +655,7 @@ "src/ui/actionButton.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -806,6 +815,7 @@ "src/data/cameraArrival.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -928,6 +938,7 @@ "src/data/cameraArrival.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -996,6 +1007,7 @@ "src/data/modelEligibility.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1045,6 +1057,7 @@ "src/data/modelEligibility.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1108,6 +1121,7 @@ "src/data/inputOwnership.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1138,6 +1152,7 @@ "src/maps/imageryHost.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1169,6 +1184,7 @@ "src/data/inputOwnership.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1200,6 +1216,7 @@ "src/data/inputOwnership.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1234,6 +1251,7 @@ "src/data/inputOwnership.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1277,6 +1295,7 @@ "src/data/infrastructureOverlayEntry.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1314,6 +1333,7 @@ "src/ui/panelRailGeometry.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1365,6 +1385,7 @@ "src/layers/traffic/roadModes.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1395,6 +1416,7 @@ "src/data/cameraSensitivity.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1444,6 +1466,7 @@ "src/layers/cctv/sourcePolicy.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1508,6 +1531,7 @@ "scripts/qa-street-level.mjs", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1538,6 +1562,7 @@ "src/layers/alpr/floors.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1579,6 +1604,7 @@ "src/data/inputOwnership.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1602,6 +1628,7 @@ "src/data/inputOwnership.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1659,6 +1686,7 @@ "src/maps/imageryHost.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -1722,6 +1750,7 @@ "src/voice/pointerReticle.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -2605,6 +2634,7 @@ "src/app/embed.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -3190,6 +3220,7 @@ "src/sources/infrastructureData.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -3301,6 +3332,7 @@ "src/app/stateChannel.js", "src/data/cameraArrival.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -3325,6 +3357,7 @@ "src/ui/uiLifetime.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -3362,6 +3395,7 @@ "src/i18n/marker.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -3389,6 +3423,7 @@ "src/ui/uiLifetime.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -3481,6 +3516,7 @@ "src/data/routeSteps.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -3537,6 +3573,7 @@ "src/voice/pointerReticle.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -3792,6 +3829,7 @@ "src/sources/capability.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -3836,6 +3874,7 @@ "src/sources/transitHistory.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -3890,6 +3929,7 @@ "src/layers/perimeters/inciweb.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -3907,6 +3947,7 @@ "src/layers/gnss/records.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -3967,6 +4008,7 @@ "src/i18n/marker.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", @@ -4164,6 +4206,7 @@ "src/i18n/marker.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/ar.json", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", "src/i18n/locales/zh-CN.json", diff --git a/src/hudLocalityFormat.js b/src/hudLocalityFormat.js index 697075a82..23eea0ddd 100644 --- a/src/hudLocalityFormat.js +++ b/src/hudLocalityFormat.js @@ -8,6 +8,12 @@ import { formatNumber, getLocale, t } from './i18n/index.js'; import { unitLabel } from './ui/unitLabels.js'; import { localizePlaceName } from './ui/placeNames.js'; +/** + * Scripts without letter case keep a space before the unit: `13 km`, `13 كم`. + * Arabic would otherwise run the digits into the word. + */ +const UNCASED_UNITS = /^(?:zh-|ar$)/; + /** An upper-case HUD quantity such as `13KM` / `13КМ`, in the active language. */ export function hudQuantity(value, unit, digits = 0) { const number = formatNumber(value, { @@ -16,7 +22,7 @@ export function hudQuantity(value, unit, digits = 0) { maximumFractionDigits: digits, }); const label = unitLabel(unit); - if (getLocale().startsWith('zh-')) + if (UNCASED_UNITS.test(getLocale())) return label ? `${number} ${label}` : number; return `${number}${label.toUpperCase()}`; } diff --git a/src/i18n/ar-display.test.mjs b/src/i18n/ar-display.test.mjs new file mode 100644 index 000000000..c481fc70d --- /dev/null +++ b/src/i18n/ar-display.test.mjs @@ -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); + } +}); diff --git a/src/i18n/i18n.test.mjs b/src/i18n/i18n.test.mjs index 1b421f2ff..225034b85 100644 --- a/src/i18n/i18n.test.mjs +++ b/src/i18n/i18n.test.mjs @@ -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', () => { diff --git a/src/i18n/index.js b/src/i18n/index.js index 6fc4becc9..8e1b24839 100644 --- a/src/i18n/index.js +++ b/src/i18n/index.js @@ -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; diff --git a/src/i18n/locale.js b/src/i18n/locale.js index 55ed71163..23a3e0349 100644 --- a/src/i18n/locale.js +++ b/src/i18n/locale.js @@ -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} diff --git a/src/i18n/same-as-english.json b/src/i18n/same-as-english.json index 83c8897d8..e55624554 100644 --- a/src/i18n/same-as-english.json +++ b/src/i18n/same-as-english.json @@ -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", diff --git a/src/ui/localePicker.test.mjs b/src/ui/localePicker.test.mjs index 057642827..630d95492 100644 --- a/src/ui/localePicker.test.mjs +++ b/src/ui/localePicker.test.mjs @@ -40,7 +40,7 @@ test('the current language comes first, the rest by their own names', () => { nativeNames: LOCALE_NAMES, }), ), - ['ru', 'en', 'es', 'zh-CN', 'zh-Hant'], + ['ru', 'en', 'es', 'ar', 'zh-CN', 'zh-Hant'], ); assert.deepEqual( codes( @@ -50,7 +50,7 @@ test('the current language comes first, the rest by their own names', () => { nativeNames: LOCALE_NAMES, }), ), - ['es', 'en', 'ru', 'zh-CN', 'zh-Hant'], + ['es', 'en', 'ru', 'ar', 'zh-CN', 'zh-Hant'], ); assert.deepEqual( codes( @@ -60,7 +60,17 @@ test('the current language comes first, the rest by their own names', () => { nativeNames: LOCALE_NAMES, }), ), - ['en', 'es', 'ru', 'zh-CN', 'zh-Hant'], + ['en', 'es', 'ru', 'ar', 'zh-CN', 'zh-Hant'], + ); + assert.deepEqual( + codes( + buildLocaleOptions({ + locales: LOCALES, + current: 'ar', + nativeNames: LOCALE_NAMES, + }), + ), + ['ar', 'en', 'es', 'ru', 'zh-CN', 'zh-Hant'], ); const rows = buildLocaleOptions({ locales: MANY,