ADR-0046: Internationalization — slang over official gen-l10n
Status
Accepted (pending FieldForce Phase 1 implementation)Tags
fieldforce, digital-worker, mobile, flutter, i18n, localization, slang, codegenDecision
The Flutter apps use slang for internationalization, with infrastructure incore_i18n, rather than the official flutter_localizations + gen-l10n (ARB) approach.
- Translations are plain JSON/YAML files; slang generates type-safe, dot-notation accessors (
t.workOrders.title) that require noBuildContext. - Locales for MVP: EN, ZH (Simplified), MS.
- Translations are callable from controllers (no
BuildContext), not only from widgets.core_modelsitself never importscore_i18n(DAG, ADR-0039):AppFailurecarries diagnostic data only, and the presentation layer maps failure types to slang strings (ADR-0045). - Active locale is wired into the Riverpod composition root and persisted; switching language is a single reactive state update.
Why
The architecture is layered (ADR-0039) and user-facing messages are produced outside the widget tree (controllers, and the failure-type → message mapping in presentation — ADR-0045). The officialgen-l10n requires a BuildContext at every call site (AppLocalizations.of(context)!...), which is awkward or impossible to use cleanly from controllers and domain code. slang’s context-free, typed dot-notation access fits the layered architecture directly — this is the deciding factor.
Secondary factors: slang has far less boilerplate than ARB, and its cleaner call sites produce tidier generated code with less surface for the LLM to mangle (consistent with the “least debugging” priority, ADR-0037). gen-l10n’s main strength — full ICU plural/gender machinery — barely applies to the target locales (Chinese has no plural inflection, Malay minimal), so its biggest advantage is largely moot here.
Rejected alternatives:
- Official
flutter_localizations+gen-l10n(ARB). Zero third-party dependency, bulletproof, full ICU — but verbose ARB, and the mandatoryBuildContextmakes translation from controllers/domain code awkward. Rejected primarily on theBuildContextconstraint vs. the layered architecture; its ICU edge is moot for EN/ZH/MS. - easy_localization or other community packages. Less type-safe than slang’s generated accessors. Rejected.
- Hardcoded strings / ad-hoc maps. No type safety, no tooling. Rejected.
Consequences
- One well-maintained third-party dependency (slang) and its codegen step (consistent with the codegen discipline, CLAUDE.md §15; generated files committed).
- i18n infrastructure is centralized in
core_i18n; the mechanism must not be mixed with gen-l10n mid-project. - Locale switching is reactive via Riverpod; the active locale is persisted.
- ICU-style pluralization, if ever needed for a future locale, is handled via slang’s plural support rather than gen-l10n.
Rules for agents
- Use slang for ALL user-facing strings; infrastructure in
core_i18n. Do NOT mix with gen-l10n/ARB. - Access translations via typed dot-notation (
t.…); noBuildContextrequired — usable from controllers.core_modelsnever importscore_i18n; failure text is mapped in presentation (ADR-0045). - Keep locale files as the single source of translations; never hardcode user-facing strings.
- Wire active locale into the Riverpod composition root; persist it.
- Initial locales: EN, ZH (Simplified), MS.