← Back to Blog

Fix Flutter gen-l10n NumberFormat Constructor Error

flutterl10ngen-l10narbintlnumberformat

Fix Flutter gen-l10n NumberFormat Constructor Error

You added a currency placeholder to one ARB message, ran flutter gen-l10n (or just hit build), and instead of generated Dart you got this:

Number format null for the number placeholder does not have a corresponding NumberFormat constructor.

The message that broke isn't the one you edited. It's a different message — one that has worked for months with a plain {count} int placeholder. Nothing about it changed. So why is it failing now?

This is one of the most confusing errors in Flutter i18n because the fix lives in a rule Flutter never states plainly. Once you know it, it takes 30 seconds to silence.

The non-obvious rule

Here it is, the whole thing in one sentence:

The moment any numeric placeholder in a message carries a format (or is typed num/double), gen-l10n routes every numeric placeholder in that message — including bare ints — through a NumberFormat constructor. A placeholder with no format resolves to null, and null is not a valid NumberFormat constructor. Hence the error.

When an int placeholder stands alone with no format, gen-l10n just interpolates it as $count — no NumberFormat involved, no problem. But add a formatted sibling (say a double amount with format: "currency"), and the whole message switches to the number-formatting code path. Now your untouched int needs an explicit format too, or gen-l10n has nothing to hand NumberFormat and emits the null you see above. This is tracked in flutter/flutter#115989, and it's working-as-designed enough that you should just give every numeric placeholder a format.

A message that reproduces it

This ARB compiles fine on its own:

{
  "itemCount": "You have {count} items",
  "@itemCount": {
    "placeholders": {
      "count": { "type": "int" }
    }
  }
}

Now you extend it to also show a total price:

{
  "cartSummary": "{count} items for {total}",
  "@cartSummary": {
    "placeholders": {
      "count": { "type": "int" },
      "total": {
        "type": "double",
        "format": "currency"
      }
    }
  }
}

Run flutter gen-l10n and it dies. total is fine — it has a format. count is the culprit: it's a numeric placeholder in a message that now uses NumberFormat, but it has no format, so the generator tries to build NumberFormat.null(...) and bails.

The fix: give the bare int a format

Add an explicit format to the int. For a plain integer you almost always want decimalPattern — it produces locale-aware digit grouping (1,234 in en-US, 1 234 in fr, 1.234 in de) and takes no extra parameters:

{
  "cartSummary": "{count} items for {total}",
  "@cartSummary": {
    "placeholders": {
      "count": {
        "type": "int",
        "format": "decimalPattern"
      },
      "total": {
        "type": "double",
        "format": "currency",
        "optionalParameters": {
          "symbol": "$",
          "decimalDigits": 2
        }
      }
    }
  }
}

That's it. gen-l10n now has a real constructor (NumberFormat.decimalPattern) for count, and the build passes. The generated Dart looks roughly like:

String cartSummary(int count, double total) {
  final countString = NumberFormat.decimalPattern(localeName).format(count);
  final totalString = NumberFormat.currency(
    locale: localeName,
    symbol: '\$',
    decimalDigits: 2,
  ).format(total);
  return '$countString items for $totalString';
}

Call it exactly as before — the signature keeps native Dart types:

Text(AppLocalizations.of(context)!.cartSummary(3, 19.99));
// en-US: "3 items for $19.99"

Which formats are actually valid

gen-l10n validates your format string against a fixed set that mirrors NumberFormat's factory constructors. Anything outside this set throws "does not have a corresponding NumberFormat constructor" — this time because the name, not null, is unknown. The valid names are:

  • compact
  • compactCurrency
  • compactSimpleCurrency
  • compactLong
  • currency
  • decimalPattern
  • decimalPatternDigits
  • decimalPercentPattern
  • percentPattern
  • scientificPattern
  • simpleCurrency

Two gotchas worth knowing:

  • Watch the spelling. decimalPatternDigits has historically been rejected or mishandled by some tool versions (#122785); for a plain int, decimalPattern is the safe, universally-supported choice.
  • optionalParameters only apply to the constructors that accept them. currency, compactCurrency, and simpleCurrency take named args like symbol, name, decimalDigits, and customPattern. Put anything else there and you'll get a different, equally cryptic codegen failure.

A fully-specified currency placeholder using all the useful knobs:

"amount": {
  "type": "double",
  "format": "currency",
  "optionalParameters": {
    "name": "USD",
    "symbol": "\$",
    "decimalDigits": 2,
    "customPattern": "\u00a4#0.00"
  }
}

The mental model: never mix a bare int in

Don't think of it as "one message broke." Think of it as: within a single message, numeric placeholders are all-or-nothing. If one of them needs formatting, they all declare a format. It's cheap insurance — adding "format": "decimalPattern" to every int/num/double placeholder costs nothing and eliminates a whole class of build-time surprises, including the ones that only appear when a future edit introduces a formatted sibling.

For the full breakdown of every placeholder type — DateTime, String, num, custom types, and the ICU select/plural interactions — see our complete guide to Flutter ARB placeholders.

Catch it before CI does

The real pain isn't the fix — it's that raw ARB JSON hides the problem until flutter gen-l10n runs, often in CI, on a message you didn't touch. A missing format on an int is invisible when you're staring at braces and commas.

That's exactly what the FlutterLocalisation ARB editor is built to surface. You edit each message's placeholders in a structured UI — type and format are fields, not hand-typed JSON keys — so a numeric placeholder without a resolvable format is obvious while you're editing it, not three commits later when the pipeline goes red. Combined with its ICU plural-syntax validation (which flags locales missing a plural category the language actually needs), it keeps the whole class of gen-l10n codegen errors out of your build.

You can manage every locale of your app_<locale>.arb files in one place, and the free tier is enough to clean up an existing project. Try FlutterLocalisation free and stop debugging NumberFormat constructors in CI logs.