Flutter Localization: ARB Files, ICU Plurals and gen-l10n in Depth
Flutter localization is flutter_localizations + intl, .arb files, and flutter gen-l10n driven by l10n.yaml. The happy path is short. Almost every production failure is an ARB consistency problem, an ICU parse problem, or a generator config problem. This page assumes you already added the packages and still get wrong Dart, silent missing strings, or a generator that will not run.
Minimal correct setup
In pubspec.yaml add flutter_localizations with sdk: flutter, add intl, and under the flutter: section set generate: true. That flag is required for generated l10n source to be importable. Generated files now live in the project; the old synthetic package:flutter_gen layout is no longer the default story. If you run the generator without the flag, you get:
Attempted to generate localizations code without having the flutter: generate flag turned on. Check pubspec.yaml and ensure that flutter: generate: true has been added and rebuild the project. Otherwise, the localizations source code will not be importable.
Put l10n.yaml in the project root. The options that actually matter for a first working tree:
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false
arb-dir is the input directory. template-arb-file is the template the tool uses to check and validate every other .arb. output-localization-file is the generated Dart file name. output-class is the class your widgets call. nullable-getter: false generates a non-nullable getter.
Run flutter gen-l10n, or let the build invoke it because generate: true is set. Full option list: flutter gen-l10n --help. Other documented names: output-dir, synthetic-package, use-deferred-loading, preferred-supported-locales, untranslated-messages-file, required-resource-attributes, gen-inputs-and-outputs-list. If you set synthetic-package: false and omit output-dir, files are generated into arb-dir. Pointing imports at a path you never configured is a common “file not found” after that change.
gen-inputs-and-outputs-list writes gen_l10n_inputs_and_outputs.json so the build system knows when to rerun during hot reload. Prefer l10n.yaml over a long CLI line: explicit false values on the CLI have been ignored, so YAML is the reliable source of truth.
ARB anatomy
An .arb file is JSON. Message entries are key–value strings. Sibling metadata uses @ plus the same key. File-level metadata uses @@ keys and is not extracted as messages.
{
"@@locale": "en_US",
"@@last_modified": "2021-02-15T09:58:36.312610",
"title_bar": "My Cool Home",
"@title_bar": {
"description": "Title in the app bar",
"placeholders": {
"userName": {
"type": "String",
"example": "Alice"
}
}
}
}
Common globals: @locale, @last_modified, @author, @context. The @key object holds description and placeholders. Each placeholder may declare type and example. ICU tokens such as {name}, {count, plural, …}, and {g, select, …} are inline codes. Translators must leave that syntax intact. The template ARB is the contract: every other locale is validated against it.
ICU plurals
CLDR categories are zero, one, two, few, many, other. A language uses a subset. English typically uses one and other only. Pattern:
{count, plural,
one {# item}
other {# items}
}
That is {variable, plural, pluralForms}. Branches are category names or exact matches such as =0 and =1. Exact matches are tested before categories. # is replaced by the formatted numeric value.
{count, plural,
=0 {No files}
one {# file}
other {# files}
}
other is mandatory. It is the fallback when no =N branch matches and no category applies for that locale. Omit it and ICU reports a parse error (a message without other fails to parse). gen-l10n parses those ICU strings, so a missing other is a generator failure, not a runtime surprise.
ICU select
Select branches on a string (gender, status, enum-like values):
{gender, select,
male {He is here.}
female {She is here.}
other {They are here.}
}
You can nest plural inside select or the reverse. Nesting is legal ICU. Mixing a plain {name} placeholder with a plural in the same string is where Flutter’s generator has historically mis-parsed the message. Keep the two forms in separate messages when you can; if you cannot, treat the next section as required reading.
Failure modes
Placeholders present in one locale but missing in another
If the template (or one locale) interpolates {userName} and another locale’s string omits that placeholder, generation is wrong. The tool treats the template as the schema and still has to emit one Dart method. Inconsistent placeholder sets across files produce console errors and generated methods that do not match what every locale actually uses. Fix: every locale’s string for a key must use the same placeholder names. Add or remove a placeholder in the template and in every other .arb in the same change.
Placeholder type and DateTime format not respected
Declaring a placeholder with a formatted DateTime in the ARB does not guarantee the generated method takes a DateTime and formats it. Types and formats in placeholders have been dropped or ignored in generated code. After flutter gen-l10n, open the generated file and check the method signature and the intl format call. If the type is missing, the getter will take a String or Object and you will format in Dart yourself. Put type on every placeholder in the template, keep those types identical in every locale’s metadata, and re-inspect generated output whenever you change a type.
Plurals not producing expected output
A message that parses can still pick the wrong branch at runtime. Causes that actually show up: using a category the locale does not have (English has no few); omitting other; writing # in a branch that should be a fixed phrase; putting the number in the branch as a nested {count} instead of #. Categories are locale rules, not English grammar copied into every file. For a locale that only distinguishes one/other, extra categories never run. Prefer =0 when zero must be a special sentence, and always keep other.
Combining simple and plural placeholders
This pattern has broken ARB parsing:
{
"@@locale": "en",
"eventConflict": "Event \"{title}\" is conflicting with this event. You must wait at least {count, plural, one{1 day} other{{count} days}} between two events.",
"@eventConflict": {
"description": "Message to display when there is a conflict between events.",
"placeholders": {
"title": { "type": "String" },
"count": {}
}
}
}
A normal placeholder ({title}) plus a plural on count in one string, plus an empty count metadata object, produced generated English that did not match the intended placeholders. Nested {count} inside the other branch is also the wrong ICU; that branch should use #. Split into two messages, or keep one plural-only string and concatenate in Dart. If you must keep one message, declare both placeholders with real types, do not nest the variable inside the plural branch, and read the generated method before you ship.
Missing @placeholders metadata
Placeholder metadata is how the generator decides types and parameter shape. An empty {} for a placeholder, a type in the template but not elsewhere, or a type that does not match usage, yields syntax/generation errors or the wrong API (optional vs named parameters misread). Every placeholder that appears in the ICU string needs a named entry under placeholders with at least type. Keep that block aligned with the template. If generation “succeeds” but the method signature is wrong, the metadata is the first thing to diff.
New .arb files not detected
A newly added locale-suffixed .arb that has no values may not produce a generated class. Looking up strings for that locale then fails with a null pointer instead of falling back to the template. An empty file is not “this locale is not ready”; it is a broken locale. Either fill the file from the template or do not add it until it has real keys. After adding a file, run flutter gen-l10n explicitly; hot reload has missed new ARBs. Confirm a generated class exists for that locale before you put it in supportedLocales.
Missing translations not failing loudly
Missing keys in non-template ARBs are not handled as a hard, obvious fallback. You can generate, run, and only later notice English leaking in, or a weak tool message. The template is complete; secondary files are often not. Do not assume a missing key equals template lookup. Diff keys against the template, and use the untranslated-messages file below as a checklist rather than as optional noise.
Suppressing untranslated warnings with untranslated-messages-file
There is no boolean “quiet” flag. Route the list to a file:
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
nullable-getter: false
untranslated-messages-file: l10n_errors.txt
flutter gen-l10n writes missing keys to that file and stops printing them on the console. The value can be a path such as l10n_errors.txt or untranslated_keys. Treat the file as the completeness report for a partial locale rollout, not as a way to ignore holes forever.
Escaping literal curly braces and apostrophes
Literal { and } in a message are parsed as placeholders. You cannot drop a brace into copy and expect it to survive. That is why JSON examples that need a visible brace in UI copy fail at parse time or generate nonsense methods. Apostrophes are ICU-sensitive as well; a raw ' can terminate a quoted segment. Prefer wording that does not need braces. If you must show a brace, keep it out of ICU placeholder position and verify the generated string. Do not assume a generator escape switch; treat braces in source copy as a parse hazard.
A generic Found syntax errors. after an upgrade is the same class of problem: bad ICU, inconsistent placeholders, or a config the current generator rejects. Fix the ARB, then rerun. Do not chase the Dart.
Keeping N .arb files in sync
The template is the schema. Every new key, placeholder name, placeholder type, plural category set, and select branch has to land in every locale file or you hit the failures above. As the app grows, that is a merge problem, not a Dart problem: key drift, placeholder drift, and untranslated keys. Generate, read untranslated-messages-file, and refuse to add a locale file that is empty. When you need every App Store language filled from the template without hand-editing dozens of JSON files, a one-shot translator such as Localize Your App is the mechanical step; the generator still requires those files to match the template’s placeholders and ICU.
Once the pipeline is right, the remaining work is keeping every app_xx.arb in step as the app grows. The .arb translation walkthrough covers doing that in one pass, the file formats guide compares ARB with the other mobile formats, and the complete app localization guide covers choosing languages and budget.
Frequently asked questions
Why does flutter gen-l10n refuse to generate even though l10n.yaml is present?
flutter: generate: true is missing under the flutter: section of pubspec.yaml. The generator will not emit importable source without it.
Where do generated files go if I set synthetic-package: false?
Into arb-dir unless you also set output-dir. Imports must match that path.
How do I stop untranslated-message console warnings?
Set untranslated-messages-file in l10n.yaml. There is no off switch; warnings are redirected to that file.
Why must every plural include other?
ICU requires other as fallback. Without it the message fails to parse, and gen-l10n cannot generate the method.
Why did a new app_fr.arb not get a generated class?
A locale-suffixed file with no values may not be generated. Lookup for that locale can null-dereference. Fill the file or remove it, then run flutter gen-l10n (hot reload may not see it).
Which l10n.yaml keys are actually documented?
arb-dir, template-arb-file, output-localization-file, output-class, output-dir, synthetic-package, nullable-getter, use-deferred-loading, preferred-supported-locales, untranslated-messages-file, required-resource-attributes, gen-inputs-and-outputs-list. Use flutter gen-l10n --help for the rest of the current binary.
Everything in Flutter localization
ARB files, gen-l10n, ICU plurals and the placeholder metadata the official docs gloss over.
Ship your app in 50 languages by tonight
Upload your localization file, review side by side, download ready-to-import files for every language. One-time credits from $9 — no subscription.
No subscription. Credits never expire.