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
- 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.getCanonicalLocalesand thetoLocale*methods;Intl.PluralRules,RelativeTimeFormat,ListFormat,DisplayNames,Locale,Segmenter,DateTimeFormat.formatToParts/formatRangeare absent, and issue #1462 (PluralRules, opened Jul 2024) is still open with no maintainer response [1][2][3][14][15]. - Polyfilling
Intl.PluralRulesthe 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 onIntl.PluralRulesat runtime. - Unicode MessageFormat 2.0 is now a stable spec (LDML Part 9, version 48.2), and the
messageformat@4.0.0npm package (8.2 KB gz, zero deps) implements LDML 48.2 (March 2026) [10][11][12]. ICU MF1 viaintl-messageformat@11.2.13is 9.9 KB gz and needsIntl.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. - 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 havemanyfor exact millions since CLDR 38 [13]. - Digits are a per-locale, per-region decision, not "Arabic = Arabic-Indic": CLDR default numbering system is
arabforar,ar-JO,ar-LB,ckb;latnforar-MA,ar-AE,ur;arabextforfa,ps;bengforbn[45]. Rasd needs an explicitnumberingSystemsetting per form/locale (defaultlatnfor data-entry fields, locale-native for display) with-u-nu-overrides. - Web bidi is solved with markup, not control characters:
dir="auto"/<bdi>for user-provided values,unicode-bidi: isolate(default fordir),unicode-bidi: plaintextfor free-text inputs,dir="ltr"islands for phone numbers/IDs/codes, U+2066–2069 isolates only inside attribute strings [20]. On RN,Textinherits Android first-strong/iOSwritingDirection:'auto',writingDirectionis iOS-only and buggy inTextInput(#54399, RN 0.81, open) and the Yogadirectionlayout style is the way to flip a subtree withoutI18nManager.forceRTL()restarts [16][17][19]. - 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]. - Kobo/ODK/XLSForm multi-language:
label::Language (code)for label/hint/guidance_hint/constraint_message/required_message/image/audio/video,default_languagein settings, no fallback — a missing cell renders blank, and Enketo/Kobo decide RTL from the first visible text of the translation, applied asdata-diron the language<option>[26][27][28][32]. - 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/trgDirbidi 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]. - 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].
| API | Web (evergreen) | Android WebView (old) | Hermes iOS | Hermes Android | Rasd decision |
|---|---|---|---|---|---|
Intl.NumberFormat (format, numberingSystem, -u-nu-) | yes | yes (Chrome 24+) | yes, but no notation:'compact'/'engineering', compactDisplay, signDisplay [1] | yes, incl. formatToParts [1] | rely natively; avoid compact/signDisplay in core |
Intl.DateTimeFormat | yes | yes | yes, no numberingSystem, formatMatcher [1] | yes, no dayPeriod, fractionalSecondDigits, formatMatcher [1] | rely natively; test calendar:'islamic-umalqura' per device [unverified on Hermes] |
DateTimeFormat.formatToParts/formatRange | yes | formatToParts yes; formatRange Chrome 76+ | no [1] | no [1] | do not use in core |
Intl.PluralRules | yes (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) | yes | yes | yes [1] | yes [1] | rely natively (option coverage on Hermes not documented — test) |
Intl.RelativeTimeFormat | yes | Chrome 71+ | no | no | optional feature module; @formatjs/intl-relativetimeformat 9.4 KB gz [9] |
Intl.ListFormat | yes | Chrome 72+ | no | no | tiny in-house list join per locale (", " / " و ") ; polyfill 9.0 KB gz if needed [9] |
Intl.Locale / getCanonicalLocales | yes | Chrome 74+ | Locale no; getCanonicalLocales yes [1] | same | own BCP 47 parser (~40 lines); never pull @formatjs/intl-locale (73.5 KB gz) [9] |
Intl.DisplayNames | yes (Baseline Apr 2021; Chrome 81/FF 86/Safari 14.1) [24] | Chrome 81+ | no | no | ship language names in catalog metadata (nativeName), no polyfill |
Intl.Segmenter | Baseline Apr 2024 [23] | Chrome 87+ | no | no | not needed |
Temporal | not Baseline (Aug 2026) [25] | no | no | no | do 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)
| Library | Version (Aug 2026) | min+gz | Format | Plurals need Intl.PluralRules? | RN/Hermes | Notes |
|---|---|---|---|---|---|---|
intl-messageformat (FormatJS) | 11.2.13 | 9.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.0 | 8.2 KB, 0 deps | Unicode MF2 (LDML 48.2, Mar 2026) [12] | uses Intl.PluralRules for :number selection [unverified on Hermes] | untested | polyfill 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/_other | yes, "no fallback mechanism" without it (v24+) [40] | needs polyfill on Hermes [40] | huge ecosystem, TMS support everywhere |
@lingui/core | 6.6.0 | 2.1 KB | ICU MF1 in PO/JSON, compiled at build | yes (Lingui RN docs mandate FormatJS polyfills) [38] | RN ≥0.76/Expo ≥52 supported | best DX for app authors, heavier toolchain for a library |
| Paraglide JS | v2 | tree-shaken per message | inlang 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-i18n | 5.x | ~1 KB | own template syntax | relies on Intl PluralRules [41] | not documented | tiny, 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]
| Language | Categories | Rule sketch |
|---|---|---|
Arabic ar | zero, one, two, few, many, other | zero 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 ne | one, other | one n=1 |
Persian/Dari fa, Amharic am, Bengali bn, Hindi hi | one, other | one i=0 or n=1 (0 is "one") |
Urdu ur, English en, Swahili sw, Somali so | one, other | one i=1 and v=0 |
Tigrinya ti | one, other | one n=0..1 [from CLDR memory — chart fetch did not return ti] |
French fr | one, many, other | one i=0,1; many e=0 & i%1e6=0 |
Spanish es | one, many, other | one n=1; many exact millions |
Portuguese pt | one, many, other | one i=0..1 |
Ukrainian uk, Russian ru, Polish pl (structure) | one, few, many, other | one v=0 & i%10=1 & i%100≠11; few i%10∈2..4 & i%100∉12..14; many rest integers |
Burmese my, Japanese, Chinese, Vietnamese | other | none |
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
dirfrom 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 aViewwith the Yogadirection: 'rtl' | 'ltr'style sostart/end,marginStart/End,paddingStart/End,borderStartWidth,insetInlineStart/Endflip per subtree withoutI18nManager.forceRTL()+ restart [17]. Verify Android/Fabric honors non-rootdirectionon 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 withdir) and wrap injected values ({name}, respondent names, choice labels from datasets) in<bdi>/dir="auto". RN: defaultTextdirection is first-strong on Android andwritingDirection:'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 CSSunicode-bidi: plaintextso an English answer inside an Arabic form aligns left and an Arabic answer aligns right. RNTextInput: do not rely onwritingDirection(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 settextAlign('right'/'left') explicitly. - Numeric inputs, phone numbers, national IDs, barcodes, coordinates, dates typed as digits: force an LTR island:
dir="ltr"+text-align: endinside RTL forms on web; RNtextAlign:'left'and, on Android, expect caret quirks with numbers in RTL (historic issue #33483) [18]. Never useunicode-bidi: bidi-overridefor 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. RNSlider/Animatedcomponents generally do not flip — mirror manually whendirection === '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
dirper 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 reservesu:dirand 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.languageson web;expo-localization.getLocales()incl.textDirection,languageScriptCode,regionCode[43] orreact-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↔ckbare not the same language — do not fall back between them;fa-AF/prs(Dari) →fa;ps-AF→ps;ur-PK→ur;zh-Hant/zh-Hanssplit 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
dirof a fallback string may differ from the form'sdir(English fallback in an Arabic form): render fallback strings withdir="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})
RasdI18nProviderprops: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
RasdLocaleConfigsodir, digits and calendar switch atomically when the enumerator changes language mid-form (no restart on RN thanks to Yogadirection).
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
Localizedfield 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'simage::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'sonMachineTranslateevent [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 outputstate: 'mt'(XLIFFstate="translated"+ amtnote), 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,noteswith question type/choices,srcDir/trgDir) for TMS/LSP; CSV/XLSX for field partners (columnspath,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 parseName (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-dirper 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.arfile 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 whatarabic-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 useLIKEonlabel_norm(or FTS5 withunicode61 remove_diacritics 2tokenizer 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-translitwith 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
- 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/getcanonicallocalesin core (~133 KB gz together) [1][9]. Publish a "known engine gaps" doc mirroring the matrix in §1. - Message format: implement the "Rasd Mini-Message" ICU-compatible subset (
{var},pluralwith 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]. - Digits & calendars are settings, not locale side-effects:
settings.numbering(defaultlatninputs /nativedisplay),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]. - 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 anddir="auto"on free-text inputs [8][20]. - RN direction without restarts: implement per-form direction with Yoga
directionon the form root; treatwritingDirectionandI18nManageras last resorts; verify Android/Fabric behaviour on RN 0.87 and low-end Android 7–9 devices (older ICU) [1][17][19]. - Locale negotiation: lookup + parent truncation + macro-language best-fit as in §6; explicit non-fallbacks (
ckb≠ku); per-string fallback with visible "showing default" anddir="auto"on fallback text. - 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]. - Builder Translations tab: grid model with completeness %, stale flags (do not auto-clear), provider-agnostic
onMachineTranslatewith glossary/DNT andmtstate, XLIFF 2.1 + CSV/XLSX + XLSForm round-trip withName (code)headers anddefault_language[26][27][28][34][52]. - Search/sort:
Intl.Collator(base, ignorePunctuation, numeric)+ in-house normalizer stored aslabel_normin the offline store; optional FTS5 on native. - Fonts: subset with fonttools defaults +
--layout-features='*',unicode-rangesplitting, include bidi controls in the Arabic subset [53][54].
Unverified / to test on devices
- Hermes support for
calendar:'islamic-umalqura',-u-nu-inDateTimeFormaton iOS,Intl.CollatorignorePunctuation/numericoptions — the Hermes doc lists gaps but no exhaustive option table [1]. - Whether
messageformat@4MF2:numberselection needsIntl.PluralRules(README does not say) [12]. - Yoga
directionon 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-rgsacalendar IDs (recall of a 2025 proposal change) — confirm before documenting.
Sources (accessed 2026-08-15)
- Hermes — Intl APIs support doc: https://github.com/facebook/hermes/blob/main/doc/IntlAPIs.md
- Hermes issue #1462 "Intl.PluralRules support" (open since 2024-07-23): https://github.com/facebook/hermes/issues/1462
- Hermes issues search "Intl.PluralRules": https://github.com/facebook/hermes/issues?q=is%3Aissue+Intl.PluralRules
- FormatJS polyfills overview: https://formatjs.github.io/docs/polyfills
- @formatjs/intl-pluralrules docs (deps, polyfill-force on RN): https://formatjs.github.io/docs/polyfills/intl-pluralrules
- FormatJS runtime requirements (RN polyfill order): https://formatjs.github.io/docs/guides/runtime-requirements/
- intl-messageformat docs: https://formatjs.github.io/docs/intl-messageformat/
- FormatJS CLI (pseudo-locales en-XA/en-XB, TMS formats): https://formatjs.github.io/docs/tooling/cli/
- 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=
- Unicode MessageFormat 2.0, LDML Part 9 v48.2 (stable): https://www.unicode.org/reports/tr35/tr35-messageFormat.html
- messageformat monorepo README: https://github.com/messageformat/messageformat
- messageformat v4 README (LDML 48.2, March 2026): https://raw.githubusercontent.com/messageformat/messageformat/main/mf2/messageformat/README.md
- CLDR 48 language plural rules chart: https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html
- React Native blog index (0.87 on 2026-08-11, 0.84 Hermes V1 default): https://reactnative.dev/blog
- React Native 0.84 release post: https://reactnative.dev/blog/2026/02/11/react-native-0.84
- RN Text style props (
writingDirectioniOS-only,textAlign): https://reactnative.dev/docs/text-style-props - RN Layout props (
direction,start/end,insetInline*): https://reactnative.dev/docs/layout-props - RN issues search "RTL TextInput": https://github.com/facebook/react-native/issues?q=is%3Aissue+RTL+TextInput
- RN issue #54399 "TextInput broken with writingDirection" (open): https://github.com/facebook/react-native/issues/54399
- W3C i18n — Inline markup and bidirectional text in HTML: https://www.w3.org/International/articles/inline-bidi-markup/
- MDN
<input type="number">: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/number - MDN Intl.PluralRules (Baseline Sep 2019): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules
- MDN Intl.Segmenter (Baseline Apr 2024): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter
- MDN Intl.DisplayNames (Chrome 81/FF 86/Safari 14.1): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames
- MDN Temporal (not Baseline): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal
- KoboToolbox — Adding another language: https://support.kobotoolbox.org/language_dashboard.html
- ODK Docs — Form language: https://docs.getodk.org/form-language/
- XLSForm docs — multiple language support: https://xlsform.org/en/
- ODK Web Forms repo (archived, moved to central-frontend): https://github.com/getodk/web-forms
- ODK Central Frontend CONTRIBUTING (vue-i18n, Transifex): https://raw.githubusercontent.com/getodk/central-frontend/master/apps/central/docs/CONTRIBUTING.md
- Enketo Express README (Transifex, languages): https://raw.githubusercontent.com/enketo/enketo/main/packages/enketo-express/README.md
- Enketo Core language.js (
data-dirper language option): https://raw.githubusercontent.com/enketo/enketo/main/packages/enketo-core/src/js/language.js - SurveyJS localization docs: https://surveyjs.io/form-library/documentation/survey-localization
- 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
- SurveyJS localization directory (73 files): https://github.com/surveyjs/survey-library/tree/master/packages/survey-core/src/localization
- RJSF form props (
translateString, v6.8.0): https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/form-props - JSON Forms i18n: https://jsonforms.io/docs/i18n
- Lingui React Native tutorial: https://lingui.dev/tutorials/react-native
- Paraglide JS: https://paraglidejs.com/
- i18next plurals: https://www.i18next.com/translation-function/plurals
- typesafe-i18n: https://github.com/ivanhofer/typesafe-i18n
- make-plural: https://github.com/eemeli/make-plural
- expo-localization (SDK 57): https://docs.expo.dev/versions/latest/sdk/localization/
- react-native-localize: https://github.com/zoontek/react-native-localize
- 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 - DeepL API supported languages: https://developers.deepl.com/docs/getting-started/supported-languages
- Azure AI Translator pricing (F0 2 M chars/month): https://azure.microsoft.com/en-us/pricing/details/cognitive-services/translator/
- Weblate hosting/pricing (GPL-3+, Libre plan): https://weblate.org/en/hosting/
- Weblate supported formats: https://docs.weblate.org/en/latest/formats.html
- Weblate quality checks (ICU MessageFormat, missing plurals): https://docs.weblate.org/en/latest/user/checks.html
- Crowdin open-source plan: https://crowdin.com/page/open-source-project-setup-request
- OASIS XLIFF 2.1 (13 Feb 2018): https://docs.oasis-open.org/xliff/xliff-core/v2.1/os/xliff-core-v2.1-os.html
- fonttools subset docs: https://fonttools.readthedocs.io/en/latest/subset/index.html
- fonttools subset source (default layout features + script shaping): https://raw.githubusercontent.com/fonttools/fonttools/main/Lib/fontTools/subset/__init__.py
- npm registry search "arabic normalize"/"tashkeel" (arabic-normalize, anynum): https://registry.npmjs.org/-/v1/search?text=arabic%20normalize&size=15
- CLEAR Global humanitarian language data: https://clearglobal.org/language-data-by-country/