← Back to Blog

Fix Flutter gen-l10n ICU Errors: Apostrophes & Braces

fluttergen-l10narbi18niculocalization

Fix Flutter gen-l10n ICU Errors: Apostrophes & Braces

You add one innocent string — "Don't have an account?" or "Use { } around the code" — run flutter gen-l10n, and your build falls apart. These are two of the most common, most confusing failures in Flutter localization, and they look completely different:

  • Apostrophes (don't, French l' / n', Dutch 'n) fail silently at generation — gen-l10n happily emits Dart that won't compile.
  • Literal curly braces fail loudly at parse time with ICU Syntax Error: Expected "identifier" but found "}".

Two symptoms, one root cause: gen-l10n runs your ARB values through an ICU message-format parser, where ' and { } are reserved syntax, not plain text. Here's exactly what breaks and the exact use-escaping: true fix — plus the trap that fix sets for your real placeholders.

Failure mode 1: apostrophes generate broken Dart

Say you have this in app_en.arb:

{
  "rallyLoginNoAccount": "Het jy nie 'n rekening nie?"
}

gen-l10n wraps every message in a single-quoted Dart string. With the apostrophe inside, you get output like this:

@override
String get rallyLoginNoAccount => 'Het jy nie 'n rekening nie?';

The ' in 'n terminates the Dart string early. The generator doesn't warn you — it writes syntactically invalid Dart, and the failure surfaces later as a cryptic analyzer/compile error in a generated file you never wrote (app_localizations.dart), often something like Expected an identifier or Expected ';' after this. Developers waste an hour staring at generated code before realizing the culprit is a don't three files away.

This is flutter/flutter issue #51787, a long-standing quirk of how the tool quotes strings. It bites every language full of contractions and elisions: English (don't, you're), French (l'application, n'est, aujourd'hui), Italian (un'app), Dutch ('n, 't).

Failure mode 2: literal curly braces throw at parse time

Now the loud one. You want to show literal braces — maybe in a code hint or a template example:

{
  "placeholderHint": "Wrap the variable in { } to interpolate"
}

Run gen-l10n and it stops immediately:

ICU Syntax Error: Expected "identifier" but found "}".
    Wrap the variable in { } to interpolate
                          ^

To the ICU parser, { opens a placeholder and it expects an identifier name next (like {userName}). A space or a closing } where a name should be is a syntax error. Flutter 3.3+ ships a stricter ICU parser (the ICU Message Syntax Parser, PR #112390), so strings that slipped through on older SDKs now fail hard.

The one fix: use-escaping: true

Both problems are solved by turning on ICU escaping and then quoting the literal characters. Add this to your l10n.yaml:

arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
use-escaping: true

With use-escaping: true, the parser follows two rules (straight from the official Flutter i18n docs):

The parser ignores any string of characters wrapped with a pair of single quotes. To use a normal single quote character, use a pair of consecutive single quotes.

In practice:

  • A literal single quote → double it: ''
  • Literal { or } → wrap each in single quotes: '{' and '}'

Fixing the apostrophe strings

{
  "rallyLoginNoAccount": "Het jy nie ''n rekening nie?",
  "loginNoAccount": "Don''t have an account?",
  "frGreeting": "Aujourd''hui, l''application est prête"
}

Each apostrophe becomes '', and gen-l10n now emits a correctly escaped Dart string that compiles.

Fixing the literal braces

{
  "placeholderHint": "Wrap the variable in '{' '}' to interpolate"
}

The braces are wrapped in single quotes, so the parser treats them as plain text instead of placeholder delimiters.

The canonical mixed example

The Flutter docs show both rules combined in one message:

{
  "helloWorld": "Hello! '{Isn''t}' this a wonderful day?"
}

That resolves to the Dart string:

"Hello! {Isn't} this a wonderful day?"

Note what happened: '{...}' wraps the braces to keep them literal, and inside it '' produces the apostrophe in Isn't.

The trap: enabling escaping can break your real placeholders

Here's what most tutorials skip. use-escaping is a global, all-or-nothing switch. The moment you flip it on, every single quote in every ARB string across every locale changes meaning — a lone ' now starts an escape sequence instead of printing a quote.

Worse, it changes how real placeholders behave when they sit next to quotes. Consider a working plural string:

{
  "itemCount": "{count, plural, one{1 item} other{{count} items}}"
}

That's fine. But suppose another string mixes a placeholder with an apostrophe you didn't escape:

{
  "welcome": "You're viewing {productName}"
}

Before escaping, You're rendered literally. After you set use-escaping: true, that single quote in You're opens an escape span — and depending on the rest of the string, {productName} can get swallowed into the "escaped" region and stop interpolating, or the message parses in a way you didn't intend. The fix must be applied everywhere at once:

{
  "welcome": "You''re viewing {productName}"
}

So enabling escaping is not a one-line change — it's a migration. Before you ship it:

  1. Turn on use-escaping: true.
  2. Grep every app_*.arb for a single quote: grep -rn "'" lib/l10n.
  3. Double every literal apostrophe ('''), and wrap every literal brace ({'{', }'}').
  4. Re-run flutter gen-l10n and confirm each locale compiles — translators often introduce apostrophes (French and Italian especially) that your English template never had.

That last point is the killer: your app_en.arb may be clean while app_fr.arb is full of l' and n'. The build breaks only when that locale's getters are generated, so a change that passes locally can fail in CI once translations land.

Audit apostrophes and braces before they reach your build

This is exactly the kind of cross-locale consistency problem that's painful in raw JSON and easy in a proper editor. FlutterLocalisation's ARB editor lets you edit app_<locale>.arb files in a UI across every locale at once, so a rogue apostrophe or unbalanced brace in your French or Arabic file doesn't hide until CI. And because ICU placeholders and plurals are the other half of this failure surface, its ICU plural-syntax validation flags locales that are missing a plural category the language actually needs — for example a dropped few/many for Arabic, Polish, or Russian — before gen-l10n ever chokes on them.

For more on the config file that controls all of this, see our guide to l10n.yaml configuration.

TL;DR

  • Apostrophes generate invalid Dart getters (silent); literal braces throw Expected identifier but found } (loud). Same cause: the ICU parser.
  • Set use-escaping: true in l10n.yaml, then escape literals: apostrophe → '', braces → '{' and '}'.
  • use-escaping is global — audit every locale's ARB file for stray quotes before you enable it, or you'll trade one broken build for another.

Stop debugging generated Dart and start editing ARB files with cross-locale validation built in. Try FlutterLocalisation free.