Flutter gen-l10n select: Fix ICU Syntax Error for Gender
You add a gendered string, a translator sends back the French file, and flutter gen-l10n stops with this:
[app_fr.arb:welcomeBack] ICU Syntax Error: Select expressions must have an "other" case.
{gender, select, female{Bon retour, {name}. Vous êtes connectée.} male{Bon retour, {name}. Vous êtes connecté.}}
^
Found syntax errors.
The format is always the same: [file:key], the rule that failed, the message, and a caret under the place the parser gave up. Every error and code sample below was run against Flutter 3.47.4 (Dart 3.13). The error strings come straight from message_parser.dart in flutter_tools.
Three mistakes cause almost every broken select. A fourth one produces no error at all and just renders the wrong branch.
Mistake 1: no other branch
English gets away with female, male and other. A French or Spanish translator often sees only two grammatical genders and deletes the third branch. The ARB is still valid JSON, so nothing complains until codegen.
gen-l10n checks every select in every locale file, not only the template. One locale missing other fails the whole build. That is the flutter arb select other required rule, and there is no flag to turn it off.
The fix is to put other back with wording that avoids agreement entirely:
"welcomeBack": "{gender, select, female{Bon retour, {name}. Vous êtes connectée.} male{Bon retour, {name}. Vous êtes connecté.} other{Bon retour, {name}. Connexion réussie.}}"
other is also the runtime fallback. The generated code calls intl.Intl.selectLogic with a map of your cases, and any value that is not a key in that map lands on other.
Mistake 2: the placeholder is not a String
This one prints a single line with no file name and no caret:
Placeholders used in selects must be of type 'String'
It appears when the flutter icu select placeholder is declared as anything else. The usual causes are copying a plural entry and leaving "type": "int", or writing "type": "Gender" in the hope that gen-l10n will accept your enum. It will not. A select placeholder must be String, or left undeclared so the tool infers String for you.
"@welcomeBack": {
"placeholders": {
"gender": {"type": "String"},
"name": {"type": "String", "example": "Sam"}
}
}
Because the message names no file, search your template ARB for the placeholder used after , select,. If the types disagree between locale files instead, that is a different failure, covered in Fix Flutter gen-l10n Placeholder Type Mismatch.
Mistake 3: a select nested badly inside a plural
A nested expression is a message inside a branch, so it needs its own pair of braces. Drop one pair and you get a confusing error:
[app_en.arb:invitedGuests] ICU Syntax Error: Expected "}" but found "invited".
{count, plural, =1{gender, select, female{She invited 1 guest} other{They invited 1 guest}} other{{count} guests invited}}
^
Found syntax errors.
=1{gender, select, ...} should be =1{{gender, select, ...}}. Count the braces: one pair for the branch, one pair for the expression inside it.
The second nesting failure is Mistake 1 in disguise. Each inner select needs its own other, in every plural branch. Arabic can have six plural branches, so a plural on the outside means up to six copies of the select and six chances to drop an other.
Put the select on the outside and write whole sentences in each branch:
"invitedGuests": "{gender, select, female{{count, plural, =1{She invited 1 guest} other{She invited {count} guests}}} male{{count, plural, =1{He invited 1 guest} other{He invited {count} guests}}} other{{count, plural, =1{They invited 1 guest} other{They invited {count} guests}}}}",
"@invitedGuests": {
"placeholders": {
"gender": {"type": "String"},
"count": {"type": "int"}
}
}
Selecting only the pronoun ({gender, select, female{She} other{They}} invited 1 guest) also compiles, but it breaks in translation. In French the past participle can agree with the subject, and in Arabic the verb itself changes, so the translator needs the whole sentence inside the branch.
For the plural categories each locale needs, see Fix Flutter gen-l10n Arabic Plurals. French strings are also full of apostrophes, which matter once use-escaping is on: Fix Flutter gen-l10n ICU Errors: Apostrophes & Braces.
The silent one: the wrong branch renders
No flutter gen-l10n ICU syntax error here, just a French user who always sees the neutral text. Two causes:
- Translated keys. A translator turns
female{...}intofemme{...}. Case names are arbitrary identifiers, so gen-l10n accepts it. Your code still passes'female', nothing matches, andotherwins. - Case. Matching is case sensitive.
'Female'is not'female', as the Flutter docs point out.
A typed enum fixes the call site. A test fixes the translated keys.
Copy-paste ARB files
lib/l10n/app_en.arb:
{
"@@locale": "en",
"welcomeBack": "{gender, select, female{Welcome back, {name}} male{Welcome back, {name}} other{Welcome back, {name}}}",
"@welcomeBack": {
"description": "Home screen greeting. Wording changes with the user's gender in fr, es and ar.",
"placeholders": {
"gender": {"type": "String"},
"name": {"type": "String", "example": "Sam"}
}
}
}
The English branches are identical on purpose. Keeping all three in the template shows translators which keys exist.
lib/l10n/app_fr.arb:
{
"@@locale": "fr",
"welcomeBack": "{gender, select, female{Bon retour, {name}. Vous êtes connectée.} male{Bon retour, {name}. Vous êtes connecté.} other{Bon retour, {name}. Connexion réussie.}}"
}
lib/l10n/app_ar.arb:
{
"@@locale": "ar",
"welcomeBack": "{gender, select, female{مرحبًا بعودتكِ يا {name}} male{مرحبًا بعودتكَ يا {name}} other{مرحبًا بعودتكم يا {name}}}"
}
Spanish follows the French pattern: Bienvenida for female, Bienvenido for male, and a neutral sentence such as Te damos la bienvenida for other.
A typed enum for the select key
The generated method is String welcomeBack(String gender, String name). Any string compiles, including typos. Wrap it once:
import 'l10n/app_localizations.dart';
/// Value names must match the select keys in the ARB files.
enum Gender { female, male, other }
extension GenderedMessages on AppLocalizations {
String welcomeBackFor(Gender gender, String name) =>
welcomeBack(gender.name, name);
}
Call sites become l10n.welcomeBackFor(user.gender, user.name). The raw string exists in one place, and Gender.other covers users who did not say. Adjust the import to wherever your project writes the generated file.
A widget test that asserts each branch
This test catches translated keys, because the expected French text can only come from the female branch.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/gender.dart';
import 'package:my_app/l10n/app_localizations.dart';
Future<AppLocalizations> pumpL10n(WidgetTester tester, Locale locale) async {
late AppLocalizations l10n;
await tester.pumpWidget(
MaterialApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Builder(
builder: (context) {
l10n = AppLocalizations.of(context)!;
return Text(l10n.welcomeBackFor(Gender.female, 'Sam'));
},
),
),
);
await tester.pumpAndSettle();
return l10n;
}
void main() {
testWidgets('French renders the right agreement per branch', (tester) async {
final l10n = await pumpL10n(tester, const Locale('fr'));
expect(find.text('Bon retour, Sam. Vous êtes connectée.'), findsOneWidget);
expect(l10n.welcomeBackFor(Gender.male, 'Sam'), endsWith('connecté.'));
expect(l10n.welcomeBackFor(Gender.other, 'Sam'), endsWith('réussie.'));
});
testWidgets('Arabic has a distinct branch per gender', (tester) async {
final l10n = await pumpL10n(tester, const Locale('ar'));
final rendered = {
for (final gender in Gender.values) l10n.welcomeBackFor(gender, 'Sam'),
};
expect(rendered, hasLength(Gender.values.length));
});
testWidgets('an unknown key falls back to other', (tester) async {
final l10n = await pumpL10n(tester, const Locale('fr'));
expect(
l10n.welcomeBack('Female', 'Sam'),
l10n.welcomeBackFor(Gender.other, 'Sam'),
);
});
}
Rename female to femme in app_fr.arb and the first test fails with Found 0 widgets with text "Bon retour, Sam. Vous êtes connectée.". The Arabic test uses a set so you do not have to paste Arabic into Dart: if two branches collapse into the same string, the set shrinks and the test fails.
Keep select strings readable while you edit them
Most of these bugs start as an editing slip in a long line of raw JSON. The FlutterLocalisation ARB editor lets you edit app_<locale>.arb translations in a UI, with every locale managed in one place, and its ICU plural-syntax validation flags locales that are missing a plural category the language needs. Plans are on the pricing page, and there is a free tier.
Try FlutterLocalisation free and stop hand-editing nested braces.