skip to content

In a Flutter .arb file for gen-l10n, how do placeholders turn a message into a method, and what do type, format and ICU select control?

level: middleimportance: nice to knowfreq 26%

answer

  1. curly braces make a method
  2. @key placeholders metadata
  3. untyped means Object
  4. format names an intl constructor
  5. select is case-sensitive, needs other

basics

~20 s

A {name} in an ARB message makes gen-l10n generate a method whose parameters are the placeholders. The @key metadata sets each placeholder's Dart type and an intl number or date format; an ICU select picks text by a String value.

solid answer

~40 s

gen-l10n turns `"deliveryOn": "Delivery on {date}"` into `String deliveryOn(DateTime date)`, with positional parameters unless `use-named-parameters` is on. Each placeholder can be declared under `@deliveryOn.placeholders` with a `type`, an `example`, a `format` and `optionalParameters`. Undeclared placeholders are inferred: plain ones become `Object` and print with `toString()`, plural ones `num`, select ones `String`. For `int`, `double` and `num`, `format` names a `NumberFormat` constructor such as `simpleCurrency` or `decimalPattern`; for `DateTime` a `format` is required and names a `DateFormat` constructor such as `yMMMd`, or a raw pattern with `isCustomDateFormat`. An ICU `{slot, select, morning{...} evening{...} other{...}}` becomes an `Intl.selectLogic` call on a `String`; matching is case-sensitive and `other` is mandatory.

code

json · 19 lines
json
{
  "orderTotal": "Total: {total}",
  "@orderTotal": {
    "placeholders": {
      "total": {
        "type": "double",
        "format": "simpleCurrency",
        "optionalParameters": {"decimalDigits": 2}
      }
    }
  },
  "deliveryOn": "Delivery on {date}",
  "@deliveryOn": {
    "placeholders": {
      "date": {"type": "DateTime", "format": "yMMMd"}
    }
  },
  "deliveryWindow": "{slot, select, morning{Delivered 8-12} evening{Delivered 17-21} other{Delivery time to be confirmed}}"
}

go deeper

for a junior

Know that a message with {name} becomes a method you call with that value, and that plain messages stay getters.

for a middle

Explain the placeholders metadata, the inferred types, how format picks an intl NumberFormat or DateFormat constructor, and the rules of ICU select.

for a senior

Type every numeric and date placeholder, keep select keys stable across translations, and switch to named parameters before messages grow several values.

for a principal

Set conventions for translator metadata and message granularity so catalogs stay translatable as the product adds formatted values.

## From message to method In an **ARB** file a message is a JSON string. When the string contains **placeholders** in curly braces, gen-l10n generates a **method** instead of a getter: - `"cartTitle": "Your cart"` becomes `String get cartTitle`. - `"deliveryOn": "Delivery on {date}"` becomes `String deliveryOn(DateTime date)` (given the metadata below). Parameters are **positional** by default, in the order the placeholders are declared in the metadata (undeclared ones follow alphabetically); with `use-named-parameters: true` in `l10n.yaml` they become `required` named parameters, which reads better once a message has two or three values. A placeholder name must be a valid Dart identifier. ## Declaring placeholders in metadata The `@messageId` entry next to a message holds its metadata: - `description`: context for translators. The whole `@` entry is optional unless `required-resource-attributes` is on, and even then only the entry's presence is checked. - `placeholders`: a map from placeholder name to its attributes: - `type`: the Dart type of the parameter. - `example`: a sample value for translators. - `format`: a number or date format, below. - `optionalParameters`: named arguments passed to that format's constructor, such as `decimalDigits` or `name`. - `isCustomDateFormat`: `"true"` when `format` is a raw date pattern. The template file's placeholder types win: a translation that declares a different `type` for the same placeholder is rejected. ## What gets inferred when you declare nothing | Placeholder used as | Inferred type | Allowed declared types | |---|---|---| | plain `{name}` | `Object` (printed via `toString()`) | any | | plural selector | `num` | `num`, `int` | | select selector | `String` | `String` | | date argument | `DateTime` | `DateTime` | A plain untyped placeholder therefore accepts anything and formats nothing. Declare the type whenever the value is a number or a date. ## Number and date formats For `int`, `double` and `num` placeholders, `format` names one of `intl`'s `NumberFormat` constructors: `compact`, `compactCurrency`, `compactSimpleCurrency`, `compactLong`, `currency`, `decimalPattern`, `decimalPatternDigits`, `decimalPercentPattern`, `percentPattern`, `scientificPattern` or `simpleCurrency`. The generated method creates that formatter with the message's locale, so `{total}` in the German file prints with a decimal comma. Without a `format`, a number is simply interpolated. For `DateTime` placeholders a `format` is **required**; leaving it out fails generation. It names a `DateFormat` constructor such as `yMd` or `yMMMd` (several can be joined with `+`), or a raw pattern such as `EEE, d MMM` together with `"isCustomDateFormat": "true"`. An unknown name without that flag is an error. Calling `NumberFormat` or `DateFormat` directly in Dart code is a different topic; here the ARB file chooses the formatter and the generated code calls it. ## ICU select `select` chooses a branch by a `String` value: `"deliveryWindow": "{slot, select, morning{Delivered 8-12} evening{Delivered 17-21} other{Delivery time to be confirmed}}"` 1. gen-l10n generates `String deliveryWindow(String slot)` and an `Intl.selectLogic(slot, {...})` call. 2. Matching is **case-sensitive**: `deliveryWindow('Morning')` returns the `other` text. 3. `other` is **mandatory**; the parser rejects a select without it. 4. Each translation must keep the same case keys, because the keys are values your code passes in; only the text inside the branches is translated. Select is the right tool for grammatical variation driven by a value (a pronoun, a delivery type); it is not a lookup table for arbitrary data. ## A grocery checkout, put together For the checkout screen the template might define `orderTotal` with a `double` placeholder formatted as `simpleCurrency`, `deliveryOn` with a `DateTime` placeholder formatted as `yMMMd`, and `deliveryWindow` as a select over the slot the backend returns. In German the total prints with a comma as decimal separator and the date in German month names; in Polish the same call produces Polish formatting, because the generated formatter is built with the Polish class's locale name. The Dart call sites never change: they pass a `double`, a `DateTime` and a `String`, and the catalog decides how each is shown. ## Escaping braces Curly braces are syntax. To show a literal brace, enable `use-escaping: true` in `l10n.yaml` and wrap the text in single quotes; two consecutive single quotes produce one quote character. `relax-syntax: true` is the alternative that treats unmatched braces as text.

  • What happens if a DateTime placeholder in the template has no format?
    Generation fails: gen-l10n reports that the `format` attribute must be set to choose a `DateFormat`. Give it a constructor name such as `yMMMd`, or a raw pattern with `isCustomDateFormat` set to true.
  • A translator changes a placeholder's type from int to String in app_de.arb; what does gen-l10n do?
    It rejects the file: the template's placeholder type defines the method signature, and a locale that declares a different type for the same placeholder triggers an error asking for the template's type.
  • When should you turn on use-named-parameters?
    When messages take more than one value, so calls read `l10n.deliverySummary(count: 3, date: d)` rather than relying on positional order. It changes every generated placeholder method, so switch it on early or migrate all call sites in one change.

saying these in an interview costs you the question

  • An undeclared placeholder is typed String and locale-formatted automatically.
  • ICU select ignores case, so 'Morning' matches morning.
  • A select only needs the cases you expect; other is optional.
  • Translators may rename select case keys to their own language.
  • A DateTime placeholder without a format prints the ISO string.