Fix gen-l10n Dropping Placeholders in Plural/Select
You define two placeholders in your ARB file. You run flutter gen-l10n. The generated AppLocalizations method only takes one of them — the other has vanished from the signature, and worse, the text it was supposed to interpolate is gone from the output string too. You didn't get an error. The build passed. The bug ships.
This is the flutter gen-l10n missing placeholder problem, tracked as flutter/flutter #120036 and #110329. It bites specifically when you place an ordinary placeholder — a {name}, a {title} — next to an ICU plural or select block in the same message. Let's reproduce it exactly, then fix it with an ARB structure you can copy-paste.
The malformed pattern that triggers it
Here's the shape that breaks. A greeting with a select for the time of day, plus a plain {username} sitting after it:
{
"greetUser": "Good {partOfDay, select, morning{morning} afternoon{afternoon} other{day}}, {username}!",
"@greetUser": {
"placeholders": {
"partOfDay": { "type": "String" },
"username": { "type": "String" }
}
}
}
On the affected SDKs (the reports cover Flutter 3.0–3.3.x), gen-l10n parses the select block, then stops paying attention. The generated method looks like this:
// GENERATED — note: no `username` parameter, and the tail text is gone
String greetUser(String partOfDay) {
String _temp0 = intl.Intl.selectLogic(
partOfDay,
{
'morning': 'morning',
'afternoon': 'afternoon',
'other': 'day',
},
);
return 'Good $_temp0';
}
The {username} placeholder is dropped from the parameter list and the , {username}! tail is chopped off the returned string. This is the flutter arb select drops placeholder failure in the flesh.
The plural variant of #110329 is identical in spirit:
{
"eventConflict": "Event \"{title}\" conflicts. Wait at least {count, plural, one{1 day} other{{count} days}} between events.",
"@eventConflict": {
"placeholders": {
"title": { "type": "String" },
"count": {}
}
}
}
Here the generator truncates the message around the plural and loses {title} entirely — the output collapses to roughly 'Event "$_temp0 between events.'. Same flutter plural placeholder not passed disease, same silent failure.
Why it happens
gen-l10n treats a message as either a plain interpolation string or an ICU construct, and its older parser handled text that flanked a top-level plural/select poorly. The sibling placeholder lives outside the ICU braces, so when the parser hands control to the plural/select branch handler, the surrounding tokens fall through the cracks. You get a valid-but-wrong Dart method with a smaller signature.
Because the method compiles, nothing warns you. The first sign is often a runtime call site that no longer accepts your argument — an AppLocalizations missing argument analyzer error where you write context.l10n.greetUser(partOfDay, username) and Dart complains the method only takes one positional argument.
The fix: nest the outer placeholder inside every branch
The reliable, portable structure is to stop putting the extra placeholder outside the ICU block. Instead, move all surrounding text and placeholders inside every branch of the plural or select. This is how you flutter l10n combine placeholder plural without tripping the parser — every code path becomes a self-contained string that the generator can see start to finish.
Rewrite the greeting so {username} lives in each select branch:
{
"greetUser": "{partOfDay, select, morning{Good morning, {username}!} afternoon{Good afternoon, {username}!} other{Hello, {username}!}}",
"@greetUser": {
"placeholders": {
"partOfDay": { "type": "String" },
"username": { "type": "String" }
}
}
}
Now the entire message is one select. There is no dangling text outside the braces for the parser to lose, and {username} is referenced inside the branches, so it survives into the signature. gen-l10n produces what you expect:
String greetUser(String partOfDay, String username) {
return intl.Intl.selectLogic(
partOfDay,
{
'morning': 'Good morning, $username!',
'afternoon': 'Good afternoon, $username!',
'other': 'Hello, $username!',
},
);
}
Same move for the plural. Pull {title} (and the surrounding sentence) into each branch:
{
"eventConflict": "{count, plural, one{Event \"{title}\" conflicts. Wait at least 1 day between events.} other{Event \"{title}\" conflicts. Wait at least {count} days between events.}}",
"@eventConflict": {
"description": "Shown when two events overlap.",
"placeholders": {
"title": { "type": "String" },
"count": { "type": "int" }
}
}
}
Call it cleanly:
Text(AppLocalizations.of(context)!.eventConflict('Standup', conflicts));
Yes, it's more verbose — you repeat the framing sentence in every branch. That repetition is the price of a message the generator parses correctly on every SDK, and it's also better ICU: languages with few/many categories often need the whole sentence to change, not just the noun, so per-branch text is what a translator actually wants.
Declare types explicitly
Two details that quietly cause their own failures:
- The
countin apluralis always generated asint, regardless of what you write. Declare"type": "int"so your call site and analyzer agree. - Every placeholder you reference — including ones only used inside a branch — must appear in the
placeholdersmap. A placeholder used in the ICU body but not declared is another way text silently disappears.
How to spot the malformed pattern before it ships
Because the failure is silent, catch it at authoring time:
- Read the generated method signature. Open
.dart_tool/flutter_gen/gen_l10n/app_localizations.dart(or youroutput-dir) after generation and confirm the method takes every placeholder you declared. A missing parameter is the tell. - Grep for the anti-pattern: any message where a
{placeholder}sits outside a{..., plural,or{..., select,block in the same string. That flanking text is the risk. - Diff the call sites. If upgrading Flutter suddenly makes an AppLocalizations missing argument error appear or disappear, a plural/select message likely changed its signature underneath you.
- Validate your ICU before generating. Malformed plural categories and undeclared placeholders are easy to miss by eye across dozens of locales.
That last point is where a dedicated editor pays off. The FlutterLocalisation ARB editor lets you edit app_<locale>.arb files in a UI instead of hand-balancing braces in raw JSON, and its ICU plural-syntax validation flags locales missing a plural category the language actually needs — a dropped few/many for Arabic, Polish, or Russian — before you ever run gen-l10n. Combined with reviewing the nested structure above, you close the gap between "it compiled" and "it's correct."
Takeaway
When an ordinary placeholder disappears from a generated method, it's almost always because it sat beside a plural or select instead of inside it. Move the whole sentence — surrounding text and sibling placeholders — into every branch, declare count as int, and check the generated signature. Your ICU messages will interpolate correctly, on every Flutter version.
Managing plurals, selects, and dozens of locales by hand is exactly where silent ARB bugs hide. Try FlutterLocalisation free to edit and validate your ARB files with ICU-aware checks — and browse the Flutter i18n blog for more copy-paste fixes.