← Back to Blog

Fix Flutter gen-l10n Placeholder Type Mismatch

fluttergen-l10narbi18nlocalization

Fix Flutter gen-l10n Placeholder Type Mismatch

You add one placeholder to app_en.arb, run flutter gen-l10n, and the build explodes with something like:

The argument type 'Object' can't be assigned to the parameter type 'num'.

…or gen-l10n itself throws before Dart even compiles. The frustrating part: your English template looks perfect, and the other locale you barely touched is the one blamed. This is the classic flutter gen-l10n placeholder type mismatch, and it comes down to one thing gen-l10n does quietly: it infers placeholder types per-locale, and those inferences don't always agree.

Here's exactly why it happens and the copy-paste fix — plus the int-vs-double formatting trap from flutter/flutter#115989 that bites even when your types do match.

What gen-l10n actually does when type is missing

An ARB placeholder can declare a type: String, Object, int, double, num, or DateTime. When you omit type, gen-l10n does not pick a single default — it infers one from how the message uses the placeholder, and it does this independently for every app_<locale>.arb file:

  • Referenced inside a plural ({count, plural, ...}) → inferred as num.
  • Referenced inside a select → inferred as String.
  • Referenced as a plain interpolation {name} → falls back to Object.
  • Not referenced at all in that locale's string → effectively nothing to infer.

Because the inference reads the string body, two locales can translate the same key differently and land on different types. That mismatch is what breaks CI.

The reproduction

Template app_en.arb — English uses a plural, so count infers as num:

{
  "itemsLeft": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
  "@itemsLeft": {
    "placeholders": {
      "count": {}
    }
  }
}

Now app_ja.arb. Japanese has no plural distinction, so a translator writes a single flat string and drops the ICU plural block:

{
  "itemsLeft": "{count}個のアイテム"
}

In app_en.arb, count is used in a plural → num. In app_ja.arb, count is a plain interpolation → Object. gen-l10n now generates one shared method signature but conflicting bodies, and the analyzer rejects passing an Object where a num is expected. Same key, same placeholder name, two silently-inferred types. (See flutter/flutter#119557 — "wrong generation when the placeholder is missing in other languages" — and the fix work in PR #163690 to infer types on both templates and localizations.)

The arb placeholder type null variant is the same story with an empty {} metadata block: nothing declared, so inference decides, and the two locales disagree.

The fix: declare the type explicitly in the template

Stop letting gen-l10n guess. Pin the type in the template ARB, and every locale inherits it — no more per-locale inference to disagree with:

{
  "itemsLeft": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
  "@itemsLeft": {
    "placeholders": {
      "count": {
        "type": "int"
      }
    }
  }
}

That's the whole fix for the mismatch: an explicit @key.placeholders.type. Use int for counts (plurals accept int or num), num if the value can be fractional, String for select, and DateTime for dates. Because only the template placeholder metadata drives the generated method signature, your app_ja.arb no longer needs its own metadata — the flat Japanese string is now perfectly valid against an int count.

Rule of thumb to avoid the flutter gen-l10n type inference error entirely: every placeholder that carries a value gets an explicit type in the template ARB. Empty {} blocks are a time bomb.

The int/double formatting trap (#115989)

Even with matching types, numbers have a second trap. A number placeholder with no format is rendered with plain Dart .toString() — which is not locale-aware:

{
  "price": "Total: {amount}",
  "@price": {
    "placeholders": { "amount": { "type": "double" } }
  }
}

Pass 1234.5 and every locale prints 1234.5 — no grouping separator, always a dot, and 2.0 renders as the ugly "2.0". To get locale-correct output you must add a format (one of compact, compactCurrency, compactSimpleCurrency, compactLong, currency, decimalPattern, decimalPercentPattern, percentPattern, scientificPattern, or simpleCurrency):

{
  "price": "Total: {amount}",
  "@price": {
    "placeholders": {
      "amount": {
        "type": "double",
        "format": "currency",
        "optionalParameters": { "decimalDigits": 2 }
      }
    }
  }
}

Now the flutter localization placeholder int double error from #115989: the reported behavior is that an int placeholder that's fine on its own starts requiring a format once another number placeholder in the same message uses num/double with a format. gen-l10n's requiresNumFormatting check flips based on the neighboring placeholders, so a message that built yesterday breaks when you add a formatted currency next to a bare count. The robust workaround is to be explicit about every numeric placeholder in the string — give each its own type and format:

{
  "summary": "{count} orders, {total} spent",
  "@summary": {
    "placeholders": {
      "count": { "type": "int", "format": "decimalPattern" },
      "total": { "type": "double", "format": "currency", "optionalParameters": { "symbol": "$" } }
    }
  }
}

Don't mix a formatted num with a bare int and hope. Declaring count as int with decimalPattern also fixes the flutter arb placeholder type num confusion, since you're no longer relying on inference to promote it to num.

Why this keeps reaching CI — and how to stop it

The root cause isn't your Dart. It's that type consistency lives across N separate JSON files that humans and translators edit by hand. Nobody sees the whole picture: your translator flattens a plural in one locale, inference diverges, and the mismatch only surfaces on the build server.

This is exactly what FlutterLocalisation's ARB editor removes. You edit placeholders in a UI that treats a placeholder's type as a single property of the key, applied consistently across every app_<locale>.arb — so count can't be int in English and an inferred Object in Japanese. It also runs ICU plural-syntax validation, flagging locales that dropped a plural category the language actually needs (a missing few/many for Arabic, Polish, or Russian) — the precise editing pattern that triggers divergent inference in the first place. For the full field reference, see our ARB placeholder guide and the broader localization-error playbook.

Fixing a type mismatch takes 30 seconds once you know the rule. Not shipping it to CI in the first place is the real win.

TL;DR

  • gen-l10n infers placeholder types per-locale from the string body; a plural infers num, a plain {x} infers Object — and two locales can disagree.
  • Fix the mismatch by declaring @key.placeholders.type in the template ARB; locales inherit it.
  • Number placeholders need a format for locale-aware output; per #115989, an int can suddenly require one when a formatted num/double sits beside it — so type-and-format every numeric placeholder.

Try FlutterLocalisation free to keep placeholder types consistent across every locale automatically — start here.