Repository navigation
ADR 0006 Interface Text Is Tokens
Source of truth: this wiki page.
- Status: Accepted
- Date: 2026-09-21
Fluenough is a language-agnostic drilling app whose earliest target languages are Bengali, Hindi, Gujarati, Telugu and Urdu. A learner of Hindi is a Hindi speaker. The overwhelming likelihood is that they want the app in a language they already read, and for four of those five the interface language and the reading direction are both wrong by default.
The UI does not exist yet. Every screen in this repository is unwritten: #7 through #15 are the ones that will write them. That is the entire reason this decision is being taken now rather than later — retrofitting localisation is not a refactor of the strings, it is a re-layout of every screen that was built assuming English word lengths and left-to-right flow, and it is reliably one of the most expensive things a project can defer.
The specific trap here is #33, right-to-left layout, which is already on the
roadmap for Urdu. An interface whose strings are extracted but whose widgets
hardcode EdgeInsets.only(left: …) is no more localisable than one with the
strings inline.
No user-visible string is written as a literal in a widget. Every label, title, button, hint, error, empty state, and every piece of prose in the interface is referenced by a token and resolved at runtime.
The mechanism is Flutter's own: ARB files under lib/l10n/, an l10n.yaml,
and the AppLocalizations class generated by gen_l10n. This adds
flutter_localizations (an SDK package) and intl.
English is the base locale and ships built in, as app_en.arb. It is a
translation like any other, not a fallback hardcoded into the widgets.
| Is a token | Is not |
|---|---|
| Labels, titles, buttons, hints, tooltips | Deck content — target, native, reading, notes
|
| Error and empty-state text shown to a user | Log messages, exception text, assertions |
| Semantic labels for screen readers | Card ids, deck ids, mode names, BCP-47 tags |
| Anything a user reads that is not deck content | Debug-only output |
Deck content is already data and stays data. The distinction is the one the architecture already draws: a deck is content the app displays, the interface is the app.
Extraction alone does not make an interface localisable. Also required, from the first screen:
-
Directional insets, never sided ones.
EdgeInsetsDirectional,AlignmentDirectional,TextAlign.start. Neverleft/right. - No string concatenation to build a sentence. Use placeholders, so the translator controls word order.
-
Plurals through ICU
plural, not anif (n == 1). - No layout that assumes English string length. German and Telugu are longer; a fixed-width button will clip.
The cost is two dependencies and a step in the loop: adding a string means
adding it to app_en.arb rather than typing it into the widget. That is the
whole of the ongoing cost, and it is paid per string, in seconds.
What it buys is that the app can be translated by someone who does not write
Dart, which for this project is the relevant population — a deck contributor
for Telugu is exactly the person who can translate the interface into Telugu,
and under this decision they never have to open a .dart file to do it.
It also makes #33 tractable. RTL support becomes a matter of Directionality
and a pass over the widgets, rather than a rewrite.
It does not, on its own, ship a second language. No translation beyond English is committed here. The point is that adding one later is a file, not a project.
Hardcode English now, extract later. Rejected. This is the default path and the reason so many apps are English-only: the extraction is easy and the re-layout that follows it is not, so it never happens. The cost of deciding this now is close to zero because no screen exists; in six months it is a sprint.
A hand-rolled Map<String, String> per locale. No dependency, which fits
the project's instinct. Rejected because it means reimplementing plural rules,
locale resolution and fallback, none of which is interesting work, and because
no translation tool reads it. ARB is a published format with an ecosystem, and
gen_l10n ships in the SDK we already require.
intl alone, without gen_l10n. Rejected: it gives up compile-time
checking of token names, which is the property that makes a missing string a
build failure rather than a blank label on a device.
See also: Contributing · ADR-Index · Home
1. Learner guides
- Learner Guide
- Getting Started
- Today and Daily Lessons
- Drills and Skills
- Courses and Paths
- Proficiency Levels
- Scripts and Readings
- Readings ISO 15919
- Speaking and Listening
- Progress and Stats
- Profiles and Settings
- Backup and Restore
- Reporting a Problem
2. Deck building guides
- Deck Building Guide
- Deck Format
- Romanising Indic Languages
- Deck Licences and Quoted Books
- Deck Tools
3. Review guides
4. Translation guide
5. Building on iOS
6. Roadmap
7. Sources and attributions
8. Contribution and agentic coding