إنتقل إلى المحتوى الرئيسي

13 — i18n runtime and localization tooling for Rasd Forms (Arabic-first, multi-script)

Research note, gap #3. Accessed 2026-08-15. Complements the earlier notes on RTL layout mechanics, Arabic fonts and XLSForm label::Arabic (ar) columns; this note covers the runtime formatting/message layer, bidi handling, the library's own UI-string catalogs, and translation workflow/tooling.

Method: web search budget was exhausted before this note started, so every fact below was pulled by fetching primary pages directly (Hermes docs/issues, FormatJS, Unicode/CLDR, MDN, RN docs/issues, Kobo/ODK/Enketo/SurveyJS/RJSF/JSON Forms docs, bundlephobia API, CLDR JSON, OASIS, fonttools, Weblate/Crowdin, DeepL/Azure). Items I could not confirm from a primary page are marked [unverified].

Summary​

  1. Hermes (RN 0.84+ ships "Hermes V1" by default, RN 0.87 is current, Aug 2026) still implements only Intl.Collator, Intl.NumberFormat, Intl.DateTimeFormat, Intl.getCanonicalLocales and the toLocale* methods; Intl.PluralRules, RelativeTimeFormat, ListFormat, DisplayNames, Locale, Segmenter, DateTimeFormat.formatToParts/formatRange are absent, and issue #1462 (PluralRules, opened Jul 2024) is still open with no maintainer response [1][2][3][14][15].
  2. Polyfilling Intl.PluralRules the FormatJS way costs ~150 KB gzip on Hermes because it requires @formatjs/intl-locale (73.5 KB gz) + @formatjs/intl-getcanonicallocales (59.8 KB gz) + the polyfill itself (14.5 KB gz) [5][9]. Rasd should instead ship compiled CLDR plural functions per locale (make-plural style, <1 KB/locale) and never depend on Intl.PluralRules at runtime.
  3. Unicode MessageFormat 2.0 is now a stable spec (LDML Part 9, version 48.2), and the messageformat@4.0.0 npm package (8.2 KB gz, zero deps) implements LDML 48.2 (March 2026) [10][11][12]. ICU MF1 via intl-messageformat@11.2.13 is 9.9 KB gz and needs Intl.PluralRules [7][9]. Recommendation: form-authored strings get a deliberately tiny placeholder subset ({name} + {count, plural, …}), library chrome uses the same mini-format; do not embed a full MF1/MF2 engine in core.
  4. Arabic has six CLDR plural categories (zero/one/two/few 3–10/many 11–99/other); most other target languages (ckb, ku, fa, ps, ur, so, ha, am, bn, ti) are one/other; Burmese has only other; Ukrainian follows the Russian one/few/many/other pattern; French/Spanish/Portuguese have many for exact millions since CLDR 38 [13].
  5. Digits are a per-locale, per-region decision, not "Arabic = Arabic-Indic": CLDR default numbering system is arab for ar, ar-JO, ar-LB, ckb; latn for ar-MA, ar-AE, ur; arabext for fa, ps; beng for bn [45]. Rasd needs an explicit numberingSystem setting per form/locale (default latn for data-entry fields, locale-native for display) with -u-nu- overrides.
  6. Web bidi is solved with markup, not control characters: dir="auto"/<bdi> for user-provided values, unicode-bidi: isolate (default for dir), unicode-bidi: plaintext for free-text inputs, dir="ltr" islands for phone numbers/IDs/codes, U+2066–2069 isolates only inside attribute strings [20]. On RN, Text inherits Android first-strong/iOS writingDirection:'auto', writingDirection is iOS-only and buggy in TextInput (#54399, RN 0.81, open) and the Yoga direction layout style is the way to flip a subtree without I18nManager.forceRTL() restarts [16][17][19].
  7. Every comparable product ships its own chrome dictionary and lets hosts override it: SurveyJS (73 locale files, getLocaleStrings()/setupLocale({rtl}), community PRs + Azure AI Translator gap-filling), RJSF (translateString(TranslatableString, params)), JSON Forms (i18n.translate(key, defaultMessage, ctx) + translateError), Enketo (i18next JSON, Transifex, ~24 languages incl. Arabic/Persian), ODK Central (vue-i18n JSON5 → Transifex Structured JSON, tx pull) [30][31][33][35][36][37]. None of them ship Kurdish, Somali, Amharic, Bengali, Pashto, Dari, Tigrinya or Hausa — a real differentiator for Rasd [35].
  8. Kobo/ODK/XLSForm multi-language: label::Language (code) for label/hint/guidance_hint/constraint_message/required_message/image/audio/video, default_language in settings, no fallback — a missing cell renders blank, and Enketo/Kobo decide RTL from the first visible text of the translation, applied as data-dir on the language <option> [26][27][28][32].
  9. TMS/MT: Weblate is GPL-3+ with hosted plans from EUR 47/mo (10k strings) and a free Libre plan (=160k-string plan) for public OSS; Crowdin is free for qualifying OSS; Weblate has ICU MessageFormat, missing-plurals, placeholder and max-length checks; XLIFF 2.1 (OASIS, 13 Feb 2018) carries srcDir/trgDir bidi attributes and segment states [48][49][50][51][52]. Azure AI Translator F0 = 2 M chars/month free; DeepL lists Arabic, Ukrainian, Persian, Urdu, Kurdish (KMR & CKB), Bengali, Burmese, Pashto, Hausa, Swahili as supported [46][47]. Kobo has no built-in form-translation MT [26].
  10. Arabic search/sort: Intl.Collator('ar', {sensitivity:'base', ignorePunctuation:true}) is available on all targets incl. Hermes; pair it with a ~30-line NFKC + tashkeel-strip + alef/yaa/taa-marbuta folding normalizer (the npm ecosystem is thin: arabic-normalize@1.0.1, anynum@1.0.1) [1][55].

1. Intl support matrix and polyfill decisions​

Engines: Web = evergreen Chrome/Firefox/Safari; Android WebView = same V8 as Chrome on the device (auto-updated on Android 5+, but low-end/unmanaged devices are often years behind); Hermes iOS (NSFoundation-backed); Hermes Android (ICU4J-backed, quality depends on OS ICU/CLDR: Android 7 = ICU 56/CLDR 28, Android 9 = ICU 60/CLDR 32, Android 11 = ICU 66/CLDR 36) [1].

APIWeb (evergreen)Android WebView (old)Hermes iOSHermes AndroidRasd decision
Intl.NumberFormat (format, numberingSystem, -u-nu-)yesyes (Chrome 24+)yes, but no notation:'compact'/'engineering', compactDisplay, signDisplay [1]yes, incl. formatToParts [1]rely natively; avoid compact/signDisplay in core
Intl.DateTimeFormatyesyesyes, no numberingSystem, formatMatcher [1]yes, no dayPeriod, fractionalSecondDigits, formatMatcher [1]rely natively; test calendar:'islamic-umalqura' per device [unverified on Hermes]
DateTimeFormat.formatToParts/formatRangeyesformatToParts yes; formatRange Chrome 76+no [1]no [1]do not use in core
Intl.PluralRulesyes (Baseline since Sep 2019) [22]Chrome 63+no [1][2]no [1][2]ship compiled CLDR plural functions per locale (no polyfill)
Intl.Collator (sensitivity, ignorePunctuation, numeric)yesyesyes [1]yes [1]rely natively (option coverage on Hermes not documented — test)
Intl.RelativeTimeFormatyesChrome 71+nonooptional feature module; @formatjs/intl-relativetimeformat 9.4 KB gz [9]
Intl.ListFormatyesChrome 72+nonotiny in-house list join per locale (", " / " و ") ; polyfill 9.0 KB gz if needed [9]
Intl.Locale / getCanonicalLocalesyesChrome 74+Locale no; getCanonicalLocales yes [1]sameown BCP 47 parser (~40 lines); never pull @formatjs/intl-locale (73.5 KB gz) [9]
Intl.DisplayNamesyes (Baseline Apr 2021; Chrome 81/FF 86/Safari 14.1) [24]Chrome 81+nonoship language names in catalog metadata (nativeName), no polyfill
Intl.SegmenterBaseline Apr 2024 [23]Chrome 87+nononot needed
Temporalnot Baseline (Aug 2026) [25]nononodo not depend; polyfill temporal-polyfill@1.0.4 is 19.8 KB gz, @js-temporal/polyfill@0.5.1 45.2 KB gz [9]

Bundle-cost table (bundlephobia, min+gzip, 2026-08-15) [9]: @formatjs/intl-getcanonicallocales@3.2.11 59.8 KB · @formatjs/intl-locale@5.3.10 73.5 KB · @formatjs/intl-pluralrules@6.3.13 14.5 KB · @formatjs/intl-numberformat@9.4.0 22.2 KB (+ locale data) · @formatjs/intl-datetimeformat@7.6.0 20.9 KB (+ tz/locale data) · @formatjs/intl-relativetimeformat@12.3.13 9.4 KB · @formatjs/intl-listformat@8.3.13 9.0 KB · @formatjs/intl-displaynames@7.3.13 9.5 KB · @formatjs/intl-localematcher@0.8.13 7.4 KB · @umalqura/core@0.0.7 6.1 KB · temporal-polyfill@1.0.4 19.8 KB.

Practical notes: FormatJS says the shouldPolyfill() detection "runs very slowly" on Android and recommends polyfill-force imports on RN [5]; Lingui's RN guide likewise tells you to import @formatjs/intl-pluralrules/polyfill-force and @formatjs/intl-locale [38]. That is exactly the ~150 KB gz path Rasd should avoid. Decision: core depends only on Intl.NumberFormat, Intl.DateTimeFormat, Intl.Collator (present everywhere incl. Hermes) plus Rasd's own compiled plural rules and BCP 47 parser; everything else is an opt-in @rasd/i18n-extras module.

2. Message-format libraries (comparison)​

LibraryVersion (Aug 2026)min+gzFormatPlurals need Intl.PluralRules?RN/HermesNotes
intl-messageformat (FormatJS)11.2.139.9 KB (+2 deps)ICU MF1 (plural/select/selectordinal, skeletons, rich tags)yes [7]works with polyfills [6]mature; formatters cache; AST pre-parse
messageformat (MF2)4.0.08.2 KB, 0 depsUnicode MF2 (LDML 48.2, Mar 2026) [12]uses Intl.PluralRules for :number selection [unverified on Hermes]untestedpolyfill for proposed Intl.MessageFormat; @messageformat/xliff (alpha) MF2↔XLIFF 2
i18next (+i18next-icu)26.3.6 (+2.4.4)13.5 KB (+2.0 KB)key/value JSON v4 _zero/_one/_two/_few/_many/_otheryes, "no fallback mechanism" without it (v24+) [40]needs polyfill on Hermes [40]huge ecosystem, TMS support everywhere
@lingui/core6.6.02.1 KBICU MF1 in PO/JSON, compiled at buildyes (Lingui RN docs mandate FormatJS polyfills) [38]RN ≥0.76/Expo ≥52 supportedbest DX for app authors, heavier toolchain for a library
Paraglide JSv2tree-shaken per messageinlang format (+ICU1, i18next, XLIFF plugins)plural formatter uses Intl.PluralRules [39]RN not mentioned [39]compiler-first; not suited to runtime form-authored strings
typesafe-i18n5.x~1 KBown template syntaxrelies on Intl PluralRules [41]not documentedtiny, single-maintainer

Sizes from bundlephobia [9]. Recommendation: none of these becomes a hard core dependency. Rasd defines a "Rasd Mini-Message" subset that is syntactically valid ICU MF1 and trivially convertible to MF2: {name} variables, {count, plural, zero{…} one{…} two{…} few{…} many{…} other{…}} (with #), {gender, select, …}, and ' escaping. Ship a ~2 KB parser/formatter; provide @rasd/i18n-icu (wraps intl-messageformat) and @rasd/i18n-mf2 (wraps messageformat) adapters for hosts that want the full grammar. Because the subset is MF1-valid, TMS tools (Weblate ICU check, Crowdin, Lokalise) validate it out of the box [50].

3. Plural rules for target languages (CLDR 48) [13]​

LanguageCategoriesRule sketch
Arabic arzero, one, two, few, many, otherzero n=0; one n=1; two n=2; few n%100∈3..10; many n%100∈11..99; other (100, 1000…)
Kurdish ckb (Sorani), ku (Kurmanji), Pashto ps, Hausa ha, Nepali neone, otherone n=1
Persian/Dari fa, Amharic am, Bengali bn, Hindi hione, otherone i=0 or n=1 (0 is "one")
Urdu ur, English en, Swahili sw, Somali soone, otherone i=1 and v=0
Tigrinya tione, otherone n=0..1 [from CLDR memory — chart fetch did not return ti]
French frone, many, otherone i=0,1; many e=0 & i%1e6=0
Spanish esone, many, otherone n=1; many exact millions
Portuguese ptone, many, otherone i=0..1
Ukrainian uk, Russian ru, Polish pl (structure)one, few, many, otherone v=0 & i%10=1 & i%100≠11; few i%10∈2..4 & i%100∉12..14; many rest integers
Burmese my, Japanese, Chinese, Vietnameseothernone

Design consequences: (a) authoring UI must show all six Arabic boxes and only the categories that exist for each locale; (b) validation must flag a translation that lacks a category the locale requires (Weblate's "Missing plurals" does this server-side [50]); (c) exact-value keys (=0, =1) should be allowed in the mini-format because Persian/French "one" covers 0.

4. Numbers, digits, dates and calendars​

Numbering systems (CLDR default per locale) [45]: ar, ar-JO, ar-LB, ckb → arab (٠١٢٣٤٥٦٧٨٩); ar-MA (and Maghreb), ar-AE, ur → latn; fa, ps → arabext (۰۱۲۳۴۵۶۷۸۹); bn → beng; my → mymr. So new Intl.NumberFormat('ar-JO') prints Arabic-Indic digits while ar-AE/ar-MA print Western digits — a WFP Jordan/Lebanon/Syria form and a UNHCR Morocco form differ by default. -u-nu-latn / {numberingSystem:'latn'} overrides work in V8/JSC and Hermes NumberFormat on both platforms; Hermes iOS DateTimeFormat ignores numberingSystem [1], so Hijri/Gregorian date strings on iOS will follow NSFoundation's default digits for the locale.

Field practice [unverified — no primary UN style guide could be fetched]: In Kobo/Enketo and ODK Collect, numeric answers are captured with the device keyboard and stored as ASCII digits; Arabic questionnaires from Levant/Iraq operations are commonly displayed with Western digits inside data-entry widgets and Arabic-Indic digits mostly in printed/PDF questionnaires. MDN also warns type="number" is wrong for IDs/phones; use type="text" inputmode="numeric" pattern="\d*" [21]. Recommendation: settings.numbering: 'latn' | 'native' | 'arab' | 'arabext' at form level, overridable per locale and per question; default latn for inputs, native for read-only display; always normalize typed Arabic-Indic/Extended digits to ASCII on save (anynum-style map, ~200 bytes) [55].

Calendars: Intl.DateTimeFormat(locale, {calendar:'islamic-umalqura' | 'islamic-civil' | 'persian' | 'ethiopic'}) works on evergreen web/WebView (V8/JSC/SpiderMonkey via ICU); Hermes support is undocumented — treat as [unverified] and keep @umalqura/core (6.1 KB gz) or an in-house Umm al-Qura table as the deterministic fallback for Hijri display/pickers [1][9]. Temporal is not Baseline yet [25]; if you want its calendar API, temporal-polyfill costs 19.8 KB gz [9]. Device calendar preference is available from expo-localization getCalendars()[0].calendar (values include the islamic* variants, persian, ethiopic) [43] or react-native-localize getCalendar() [44].

5. Bidi/RTL correctness checklist (renderer components)​

Web techniques come from the W3C inline-bidi guidance [20]; RN from RN docs/issues [16][17][18][19].

  • Form root: set dir from the active form locale (ar, ckb, fa, ps, ur, he, sd, ug → rtl); use logical CSS (margin-inline-start, inset-inline-*, text-align: start). RN: wrap the form in a View with the Yoga direction: 'rtl' | 'ltr' style so start/end, marginStart/End, paddingStart/End, borderStartWidth, insetInlineStart/End flip per subtree without I18nManager.forceRTL() + restart [17]. Verify Android/Fabric honors non-root direction on your RN version [unverified].
  • Labels/hints with mixed script (Arabic + Latin product codes, "WFP", numbers): render label text in an element with unicode-bidi: isolate (implicit with dir) and wrap injected values ({name}, respondent names, choice labels from datasets) in <bdi>/dir="auto". RN: default Text direction is first-strong on Android and writingDirection:'auto' on iOS; for values inserted into an Arabic sentence use ⁨…⁩ (FSI/PDI) around the value string because RN has no <bdi>.
  • Free-text answers: <textarea dir="auto">/<input dir="auto"> or CSS unicode-bidi: plaintext so an English answer inside an Arabic form aligns left and an Arabic answer aligns right. RN TextInput: do not rely on writingDirection (iOS-only, renders broken until focus in RN 0.81 — issue #54399, open) [16][19]; instead detect the first strong char of the current value and set textAlign ('right'/'left') explicitly.
  • Numeric inputs, phone numbers, national IDs, barcodes, coordinates, dates typed as digits: force an LTR island: dir="ltr" + text-align: end inside RTL forms on web; RN textAlign:'left' and, on Android, expect caret quirks with numbers in RTL (historic issue #33483) [18]. Never use unicode-bidi: bidi-override for this.
  • Neutral characters at edges (+962…, (3), %, -, /): put the whole token in an LTR isolate; if markup is impossible (attribute text, placeholder, title), append U+200E/U+200F or use FSI/PDI [20][21].
  • Progress indicators, sliders, range, ranking, steppers: progress bars and slider tracks must fill from the inline-start; use transform: scaleX(-1) only for icons, never for text. RN Slider/Animated components generally do not flip — mirror manually when direction === 'rtl'.
  • Icons/chevrons: flip directional glyphs (back/next, list bullets) in RTL; do not flip clocks, checkmarks, media controls.
  • Placeholders/hints in a different language than the field value: placeholder direction is independent — set dir per placeholder language or wrap with control chars [21].
  • Date pickers/calendars: grid order should follow dir; weekday headers, month navigation arrows flip; keep digits per §4 policy; Hijri labels are RTL Arabic even in an English UI (<bdi lang="ar">).
  • Choice lists (radio/checkbox/select): control on inline-start, label text unicode-bidi: isolate; long Latin option values inside Arabic lists need <bdi>.
  • Validation/error strings with numbers or values ("يجب ألا يتجاوز {max}"): interpolate values with isolates; MF2 explicitly reserves u:dir and permits U+061C/U+200E/U+200F/U+2066–2069 around RTL content [10].
  • Snapshot/visual tests: run each component under en, ar, en-XB (FormatJS's RTL pseudo-locale) and a long-text pseudo-locale (xx-LS) [8].

6. Locale negotiation and fallback​

  • Inputs: form settings.locales (ordered), settings.defaultLocale; host request (<RasdForm locale="ar-JO">); device (navigator.languages on web; expo-localization.getLocales() incl. textDirection, languageScriptCode, regionCode [43] or react-native-localize.getLocales()/findBestLanguageTag() [44]); persisted user choice.
  • Algorithm (BCP 47 lookup with script/region truncation, then best-fit by macro-language): ar-JO → ar → (ar-* any) → defaultLocale; ku-Arab ↔ ckb are not the same language — do not fall back between them; fa-AF/prs (Dari) → fa; ps-AF → ps; ur-PK → ur; zh-Hant/zh-Hans split by script. Reuse @formatjs/intl-localematcher@0.8.13 (7.4 KB gz, no deps) if you don't want to hand-roll [9].
  • Per-string fallback for form content: string → requested locale → its language-only parent → defaultLocale → any locale with a value → key. Different from XLSForm/Collect, which render blank for missing cells [27]; make the fallback visible in the builder (badge "showing default") and exportable as a completeness report.
  • The dir of a fallback string may differ from the form's dir (English fallback in an Arabic form): render fallback strings with dir="auto".

7. Runtime API sketch (React + RN, same surface)​

// @rasd/i18n (core, ~4–5 KB gz target)
type Localized<T = string> = T | { [bcp47: string]: T };
interface RasdLocaleConfig {
locale: string; // negotiated, e.g. "ar-JO"
fallbacks: string[]; // ["ar", "en"]
dir: 'ltr' | 'rtl';
numbering: 'latn' | 'native' | 'arab' | 'arabext' | 'beng';
calendar: 'gregory' | 'islamic-umalqura' | 'islamic-civil' | 'persian' | 'ethiopic';
hourCycle?: 'h12' | 'h23';
timeZone?: string;
}
const { locale, dir, t, tf, formatNumber, formatDate, formatList, collator, setLocale } = useLocale();
t('validation.required'); // library chrome, key-based, mini-message
t('validation.maxLength', { max: 250 }); // plural/select via compiled CLDR rules
tf(question.label); // resolve Localized<string> with fallback chain
tf(question.label, { count: 3 }); // form-authored mini-message with plural
formatNumber(1234.5, { style: 'decimal' }); // honors numbering setting via -u-nu-
formatDate(d, { dateStyle: 'medium' }); // honors calendar/numbering
formatList(['أ','ب','ج']); // in-house join tables per locale
collator.compare(a, b); // Intl.Collator(locale,{sensitivity:'base',ignorePunctuation:true})
  • RasdI18nProvider props: locale, messages (host dictionaries merged over built-ins, per key, per locale), loadLocale(locale) => Promise<Dict> for lazy catalogs, formatters (override number/date/list/plural functions), onMissingKey(key, locale) telemetry, pluralRules (inject compiled functions for extra locales).
  • Overrides: messages: { ar: { 'nav.next': 'التالي' }, 'ar-SD': { … } } — deep-merge, region beats language beats built-in; unknown keys warn once in dev.
  • Everything reads from a single RasdLocaleConfig so dir, digits and calendar switch atomically when the enumerator changes language mid-form (no restart on RN thanks to Yoga direction).

8. Library UI-string catalog format, tiers and workflow​

Source-of-truth format: flat JSON per locale, ICU-compatible mini-message values, key namespaces (nav.*, validation.*, a11y.*, builder.*, sync.*), plus a meta block (nativeName, englishName, dir, plurals list, numbering, calendar, completeness). Rationale: JSON/ICU is accepted natively by Weblate ("JSON", "Format.JS JSON", i18next v4), Crowdin, Lokalise, Transifex Structured JSON, and can be round-tripped to XLIFF 2.1 (@messageformat/xliff alpha, or a 100-line converter) and gettext PO (Plural-Forms: nplurals=6 for Arabic) when translators insist [49][52]. Keep builder strings in a separate namespace/catalog so the runtime bundle carries only renderer chrome (~120–200 keys).

Tiers. Tier-1 (bundled in core, RTL/long-text snapshot-tested each release): en, ar (source Arabic reviewed by native speaker; MSA), fr, es, uk, fa (+ prs Dari variant file), ps, ur, ckb, ku, so, am, ti, ha, bn, my, sw, tr, pt, ru. Tier-2 (community/MT-seeded, loaded on demand): rhg (Rohingya — Hanifi script Rohg, RTL, low literacy in any script; provide audio labels instead), sd, ug, he, hi, ne, si, ta, km, id, tl, ht, pl, de, it, zh-Hans, ko, ja, vi. Script/direction notes: Arabic-script RTL = ar, ckb, fa/prs, ps, ur, sd, ug; Rohingya Rohg RTL; Ethiopic (am, ti) LTR with syllabary and Ethiopic numerals not used by default; Bengali/Burmese need complex-shaping fonts and their native digits are CLDR default (beng, mymr) [45]. Only Kurdish Kurmanji, Somali, Hausa, Swahili are Latin-script one/other languages.

Workflow. (1) Developers add keys only in en.json with descriptions (@key ARB-style comments or a sibling en.meta.json); (2) CI: rasd-i18n extract verifies every t() key exists, no orphan keys, mini-message parses, plural categories complete per CLDR, placeholders identical across locales, max-length hints respected — the same rules Weblate exposes as checks [50]; (3) push source to Weblate (self-hosted GPL or Libre plan if the catalog repo is public; hosted from EUR 47/mo otherwise) or Crowdin (free for qualifying OSS) [48][51]; (4) MT pre-fill for Tier-2 through the same provider hook as the builder (Azure F0 gives 2 M chars/month free; DeepL covers ar/uk/fa/ur/ckb/kmr/bn/my/ps/ha/sw but not so/am/ti per its list) [46][47]; (5) tx pull-style scheduled PR imports translations, regenerates meta.completeness; (6) release gate: pseudo-locales en-XA (accents/length) and en-XB (RTL) rendered in Storybook/Detox snapshot suites [8]; (7) community: language-owner CODEOWNERS per file, exactly like SurveyJS's per-language PR model [33].

9. Per-form translation tooling (builder Translations tab)​

  • Model: every Localized field is a cell in a virtual grid: rows = (path, field) e.g. q.age.label, q.age.hint, q.age.constraintMessage, choices.yesno.yes.label, q.photo.label.image; columns = settings.locales. Fields include media per language (image, audio, video) to match XLSForm's image::Language (code) columns [27][28].
  • Completeness validation: per locale % complete; hard errors: missing plural category, placeholder mismatch vs. default locale, empty required (labels), stale flag when the source changed after translation (SurveyJS exposes clearTranslationsOnSourceTextChange; Rasd should mark stale rather than clear) [34]. Show fallback origin inline.
  • MT hook: onMachineTranslate({ from, to, strings: [{id, text, context}] }) => Promise<{id, text}[]> — provider-agnostic like SurveyJS Creator's onMachineTranslate event [34]; ship adapters @rasd/mt-azure, @rasd/mt-deepl, @rasd/mt-google [Google/DeepL pricing figures not verifiable from fetched pages — DeepL API Free 500k chars/mo and Pro $5.49/mo + $25 per M chars, Google ~$20 per M chars with 500k free/mo are from memory] [46][47]. Always mark MT output state: 'mt' (XLIFF state="translated" + a mt note), keep glossary/DNT terms (WFP, UNRWA, place names) via a per-form glossary, and require human review before publish for Tier-1 languages.
  • Export/import: XLIFF 2.1 (srcLang, trgLang, unit id=path, segment state, notes with question type/choices, srcDir/trgDir) for TMS/LSP; CSV/XLSX for field partners (columns path,field,default,ar,fr…), matching Kobo's "Update translations" table so partners feel at home [26]; JSON round-trip for Git.
  • XLSForm interop: export label::Arabic (ar), hint::…, guidance_hint::…, constraint_message::…, required_message::…, image/audio/video::…, settings.default_language = "Arabic (ar)"; language header = nativeOrEnglishName (bcp47); on import parse Name (code) (Kobo requires the IANA code) and warn when code missing (XLSForm makes it optional) [26][27][28]; remember there is no XLSForm fallback, so exported cells must be filled from Rasd's fallback chain or intentionally left blank.
  • RTL detection parity: Enketo/Kobo pick direction from the first visible translated text and set data-dir per language [26][32]; Rasd sets it from the locale tag but should warn when a locale's first label's first strong char contradicts the tag (e.g. ar file full of English MT leftovers).

10. Arabic-aware search and sort for choice lists/datasets​

  • Sort: new Intl.Collator(locale, { sensitivity: 'base', ignorePunctuation: true, numeric: true }) — available on web, WebView and Hermes (both platforms) [1]. Because أ/إ/آ canonically decompose to alef + combining marks and tashkeel are combining marks, primary-strength comparison already ignores hamza forms and harakat; taa marbuta (ة) vs haa (ه) and alef maksura (ى) vs yaa (ي) remain distinct and must be folded manually if desired [collation-strength claims are from Unicode/DUCET knowledge; test per engine].
  • Search normalizer (apply to both query and index): NFKC (maps presentation forms U+FB50–FDFF/FE70–FEFF to base letters), strip U+064B–U+0652 harakat + U+0670 + U+0640 tatweel, fold أإآٱ→ا, ى→ي, ة→ه (optional per dataset), ك/ک, ي/ی (Persian/Urdu variants), map Arabic-Indic and Extended digits to ASCII, collapse whitespace; then substring/prefix match, or a Damerau distance ≤1 for names. This is what arabic-normalize@1.0.1 (Aug 2026, MIT) does incl. Persian/Urdu variants and Quranic marks [55]; ecosystem is thin enough that an in-house 40-line implementation is safer for a library.
  • Large lists (10k+ admin units/beneficiary IDs): pre-normalize into the SQLite/IndexedDB index at import time (store label_norm), keep original label for display; for RN SQLite use LIKE on label_norm (or FTS5 with unicode61 remove_diacritics 2 tokenizer if compiled in) rather than JS filtering.
  • Latin transliteration search (typing "amman" to find "عمّان"): optional, ICU transliteration is not exposed in Intl; ship an optional @rasd/search-translit with a Buckwalter-ish reverse map only if users ask.

11. Fonts and shaping (delta to earlier note)​

pyftsubset (fonttools) keeps by default calt, ccmp, clig, curs, dnom, frac, kern, liga, locl, mark, mkmk, numr, rclt, rlig, rvrn "and all features required for script shaping" (init/medi/fina/isol for Arabic), so --unicodes="U+0600-06FF,U+0750-077F,U+08A0-08FF,U+FB50-FDFF,U+FE70-FEFF,U+200C-200F,U+2066-2069" --layout-features='*' --flavor=woff2 --no-hinting is a safe recipe; never drop mark/mkmk (harakat positioning) or rlig [53][54]. Use unicode-range per @font-face so Latin-only pages never download Arabic subsets. Include U+061C (ALM), U+200E/F, U+2066–2069 in the subset so bidi controls do not fall back to a different font.

12. Implications & recommendations for Rasd Forms​

  1. Core Intl policy: rely natively on Intl.NumberFormat, Intl.DateTimeFormat, Intl.Collator; ship compiled CLDR plural functions per locale and an in-house BCP 47 parser/matcher; never import @formatjs/intl-locale/getcanonicallocales in core (~133 KB gz together) [1][9]. Publish a "known engine gaps" doc mirroring the matrix in §1.
  2. Message format: implement the "Rasd Mini-Message" ICU-compatible subset ({var}, plural with CLDR categories + =n, select, #, ' escaping) for both chrome and form-authored strings; adapters for full MF1 (intl-messageformat) and MF2 (messageformat@4) as separate packages [7][12]. Reject anything else at build/validate time so TMS ICU checks pass [50].
  3. Digits & calendars are settings, not locale side-effects: settings.numbering (default latn inputs / native display), settings.calendar, per-locale/per-question overrides, ASCII normalization on save; document CLDR defaults (ar-JO = arab, ar-AE/ar-MA = latn, fa/ps = arabext, ur = latn) so authors are not surprised [45].
  4. Bidi checklist in §5 becomes a lint + Storybook matrix (en, ar, en-XB, xx-LS) run on every renderer component; add unit tests asserting isolates around interpolated values and dir="auto" on free-text inputs [8][20].
  5. RN direction without restarts: implement per-form direction with Yoga direction on the form root; treat writingDirection and I18nManager as last resorts; verify Android/Fabric behaviour on RN 0.87 and low-end Android 7–9 devices (older ICU) [1][17][19].
  6. Locale negotiation: lookup + parent truncation + macro-language best-fit as in §6; explicit non-fallbacks (ckb≠ku); per-string fallback with visible "showing default" and dir="auto" on fallback text.
  7. Catalog & workflow: flat JSON/ICU per locale with meta, Tier-1/Tier-2 lists from §8, Weblate (self-hosted GPL or Libre) or Crowdin OSS, CI checks for missing keys/placeholders/plural categories, MT pre-fill for Tier-2 only, per-language CODEOWNERS; be the first forms library that ships ckb/ku/so/am/ti/ha/bn/ps/prs chrome [35][48][51].
  8. Builder Translations tab: grid model with completeness %, stale flags (do not auto-clear), provider-agnostic onMachineTranslate with glossary/DNT and mt state, XLIFF 2.1 + CSV/XLSX + XLSForm round-trip with Name (code) headers and default_language [26][27][28][34][52].
  9. Search/sort: Intl.Collator(base, ignorePunctuation, numeric) + in-house normalizer stored as label_norm in the offline store; optional FTS5 on native.
  10. Fonts: subset with fonttools defaults + --layout-features='*', unicode-range splitting, include bidi controls in the Arabic subset [53][54].

Unverified / to test on devices​

  • Hermes support for calendar:'islamic-umalqura', -u-nu- in DateTimeFormat on iOS, Intl.Collator ignorePunctuation/numeric options — the Hermes doc lists gaps but no exhaustive option table [1].
  • Whether messageformat@4 MF2 :number selection needs Intl.PluralRules (README does not say) [12].
  • Yoga direction on non-root views under Android/Fabric in RN 0.87 [17].
  • DeepL and Google Cloud Translation price points (fetch failed; Azure F0 2 M chars/month confirmed) [46][47]; DeepL page claimed "230+ languages" while not listing Somali/Amharic/Tigrinya — inconsistent, re-check.
  • Actual digit conventions in WFP/UNRWA/UNHCR Arabic questionnaires (no primary style guide fetched).
  • Tigrinya plural rule (one: n=0..1) taken from CLDR memory.
  • Temporal's removal of islamic/islamic-rgsa calendar IDs (recall of a 2025 proposal change) — confirm before documenting.

Sources (accessed 2026-08-15)​

  1. Hermes — Intl APIs support doc: https://github.com/facebook/hermes/blob/main/doc/IntlAPIs.md
  2. Hermes issue #1462 "Intl.PluralRules support" (open since 2024-07-23): https://github.com/facebook/hermes/issues/1462
  3. Hermes issues search "Intl.PluralRules": https://github.com/facebook/hermes/issues?q=is%3Aissue+Intl.PluralRules
  4. FormatJS polyfills overview: https://formatjs.github.io/docs/polyfills
  5. @formatjs/intl-pluralrules docs (deps, polyfill-force on RN): https://formatjs.github.io/docs/polyfills/intl-pluralrules
  6. FormatJS runtime requirements (RN polyfill order): https://formatjs.github.io/docs/guides/runtime-requirements/
  7. intl-messageformat docs: https://formatjs.github.io/docs/intl-messageformat/
  8. FormatJS CLI (pseudo-locales en-XA/en-XB, TMS formats): https://formatjs.github.io/docs/tooling/cli/
  9. bundlephobia size API (intl-messageformat, messageformat, i18next, i18next-icu, @lingui/core, @formatjs/intl-* packages, @formatjs/intl-localematcher, temporal-polyfill, @js-temporal/polyfill, @umalqura/core): https://bundlephobia.com/api/size?package=
  10. Unicode MessageFormat 2.0, LDML Part 9 v48.2 (stable): https://www.unicode.org/reports/tr35/tr35-messageFormat.html
  11. messageformat monorepo README: https://github.com/messageformat/messageformat
  12. messageformat v4 README (LDML 48.2, March 2026): https://raw.githubusercontent.com/messageformat/messageformat/main/mf2/messageformat/README.md
  13. CLDR 48 language plural rules chart: https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html
  14. React Native blog index (0.87 on 2026-08-11, 0.84 Hermes V1 default): https://reactnative.dev/blog
  15. React Native 0.84 release post: https://reactnative.dev/blog/2026/02/11/react-native-0.84
  16. RN Text style props (writingDirection iOS-only, textAlign): https://reactnative.dev/docs/text-style-props
  17. RN Layout props (direction, start/end, insetInline*): https://reactnative.dev/docs/layout-props
  18. RN issues search "RTL TextInput": https://github.com/facebook/react-native/issues?q=is%3Aissue+RTL+TextInput
  19. RN issue #54399 "TextInput broken with writingDirection" (open): https://github.com/facebook/react-native/issues/54399
  20. W3C i18n — Inline markup and bidirectional text in HTML: https://www.w3.org/International/articles/inline-bidi-markup/
  21. MDN <input type="number">: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/number
  22. MDN Intl.PluralRules (Baseline Sep 2019): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules
  23. MDN Intl.Segmenter (Baseline Apr 2024): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter
  24. MDN Intl.DisplayNames (Chrome 81/FF 86/Safari 14.1): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames
  25. MDN Temporal (not Baseline): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal
  26. KoboToolbox — Adding another language: https://support.kobotoolbox.org/language_dashboard.html
  27. ODK Docs — Form language: https://docs.getodk.org/form-language/
  28. XLSForm docs — multiple language support: https://xlsform.org/en/
  29. ODK Web Forms repo (archived, moved to central-frontend): https://github.com/getodk/web-forms
  30. ODK Central Frontend CONTRIBUTING (vue-i18n, Transifex): https://raw.githubusercontent.com/getodk/central-frontend/master/apps/central/docs/CONTRIBUTING.md
  31. Enketo Express README (Transifex, languages): https://raw.githubusercontent.com/enketo/enketo/main/packages/enketo-express/README.md
  32. Enketo Core language.js (data-dir per language option): https://raw.githubusercontent.com/enketo/enketo/main/packages/enketo-core/src/js/language.js
  33. SurveyJS localization docs: https://surveyjs.io/form-library/documentation/survey-localization
  34. SurveyJS Creator translation tab / API reference: https://surveyjs.io/survey-creator/documentation/survey-localization-translate-surveys-to-different-languages and https://surveyjs.io/survey-creator/documentation/api-reference/survey-creator
  35. SurveyJS localization directory (73 files): https://github.com/surveyjs/survey-library/tree/master/packages/survey-core/src/localization
  36. RJSF form props (translateString, v6.8.0): https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/form-props
  37. JSON Forms i18n: https://jsonforms.io/docs/i18n
  38. Lingui React Native tutorial: https://lingui.dev/tutorials/react-native
  39. Paraglide JS: https://paraglidejs.com/
  40. i18next plurals: https://www.i18next.com/translation-function/plurals
  41. typesafe-i18n: https://github.com/ivanhofer/typesafe-i18n
  42. make-plural: https://github.com/eemeli/make-plural
  43. expo-localization (SDK 57): https://docs.expo.dev/versions/latest/sdk/localization/
  44. react-native-localize: https://github.com/zoontek/react-native-localize
  45. CLDR JSON numbers (ar-JO, ar-MA, ar-AE, ar-LB, ur, ckb, fa, bn, ps): https://raw.githubusercontent.com/unicode-org/cldr-json/main/cldr-json/cldr-numbers-full/main//numbers.json
  46. DeepL API supported languages: https://developers.deepl.com/docs/getting-started/supported-languages
  47. Azure AI Translator pricing (F0 2 M chars/month): https://azure.microsoft.com/en-us/pricing/details/cognitive-services/translator/
  48. Weblate hosting/pricing (GPL-3+, Libre plan): https://weblate.org/en/hosting/
  49. Weblate supported formats: https://docs.weblate.org/en/latest/formats.html
  50. Weblate quality checks (ICU MessageFormat, missing plurals): https://docs.weblate.org/en/latest/user/checks.html
  51. Crowdin open-source plan: https://crowdin.com/page/open-source-project-setup-request
  52. OASIS XLIFF 2.1 (13 Feb 2018): https://docs.oasis-open.org/xliff/xliff-core/v2.1/os/xliff-core-v2.1-os.html
  53. fonttools subset docs: https://fonttools.readthedocs.io/en/latest/subset/index.html
  54. fonttools subset source (default layout features + script shaping): https://raw.githubusercontent.com/fonttools/fonttools/main/Lib/fontTools/subset/__init__.py
  55. npm registry search "arabic normalize"/"tashkeel" (arabic-normalize, anynum): https://registry.npmjs.org/-/v1/search?text=arabic%20normalize&size=15
  56. CLEAR Global humanitarian language data: https://clearglobal.org/language-data-by-country/