Flutter · gen-l10n

The ARB files your app compiles, reviewed on a branch before they ship.

Flutter's own localization tooling reads ARB, and so does Mergua — including the @key descriptions and placeholders that make an ARB file more than a list of strings. lib/l10n stays where it is, gen-l10n keeps generating the same AppLocalizations class, and every language gains a branch, a review step and a pipeline that fills it in.

Flutter + gen-l10n

/for/flutter

Supported

locale files

lib/l10n

file format

arb

01

The files you already have

Mergua reads what is in your repo. Nothing is renamed, nothing moves, and no package is added to your project — the sync script is fetched with curl when the pipeline runs.

lib/l10n/
├── en.arb      ← source locale, the template
├── de.arb
└── fr.arb
lib/l10n/en.arb
{
  "@@locale": "en",
  "cartTitle": "Your cart",
  "@cartTitle": {
    "description": "Heading of the cart page"
  },
  "cartItems": "{count, plural, =0{Your cart is empty} =1{1 item} other{{count} items}}",
  "@cartItems": {
    "description": "Item count under the heading",
    "placeholders": {
      "count": { "type": "int" }
    }
  }
}
02

How gen-l10n reads them

This is gen-l10n's own setup, not Mergua's — if you already have it, skip the panel. It is here so the page is a working path from empty project to translated screen rather than a page about a product.

lib/cart/cart_summary.dart
import 'package:flutter/material.dart';

import '../l10n/app_localizations.dart';

class CartSummary extends StatelessWidget {
  const CartSummary({super.key, required this.count});

  final int count;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Column(
      children: [
        Text(l10n.cartTitle),
        Text(l10n.cartItems(count)),
      ],
    );
  }
}

l10n.yaml

arb-dir: lib/l10n
template-arb-file: en.arb
output-localization-file: app_localizations.dart
03

Back into the build

One job, in a stage before the build runs flutter gen-l10n. Both flags are needed: --format arb pulls every language back as its own .arb file beside the template, and --arb pushes the template itself, so a description written in @key reaches the translator. The files are committed to the branch the job is running on, and gen-l10n compiles them on the next build.

The job runs on the branch it is already on, so a feature branch syncs against the matching Mergua branch and main against main. That is the part no other translation tool does.

And you do not have to run it on a timer: a project can POST to a URL of yours when translations change or a branch is merged, so the job above starts on the change. The delivery is signed, it is filtered to the branch you name, and an import of four hundred keys arrives as one call.

# --format arb: the pull writes lib/l10n/de.arb beside en.arb.
# --arb: the push reads en.arb, @key descriptions included.
curl -sf https://api.mergua.com/v1/mergua.sh | sh -s -- sync --format arb --path lib/l10n --arb
04

Placeholders

gen-l10n writes a variable as {count}. Mergua treats it as part of the string: if a translation comes back without it, the validator says so on the way out, and the AI translator is told to carry it through.

the source string
"cartItems": "{count, plural, =0{Your cart is empty} =1{1 item} other{{count} items}}"
what fills it in
l10n.cartItems(3)
05

Between the push and the pull

The job above is both ends of one loop, and what happens in between is people. A translation gets written, somebody else approves it, and sometimes a whole language leaves for an agency and comes back — none of which touches a file in your repo until the next pull.

Reviewed before it is compiled in

gen-l10n turns every ARB file into Dart at build time, so a wrong string ships inside the binary and stays there until the next store release — which makes the review the last place anyone reads it. A reviewer approves or rejects one key at a time, with the description from its @key on the key itself, a review request mails everyone who could answer it, and a branch does not merge while anything on it is unreviewed.

How a key gets from draft to reviewed

The agency gets XLIFF, lib/l10n keeps ARB

A translation agency working in a CAT tool gets a language as one XLIFF 2.0 document and sends it back into the same keys. The document never reaches lib/l10n: gen-l10n compiles ARB, and the sync script does not carry XLIFF at all, so what the next build reads is still the ARB file Mergua wrote.

What travels in the document and what does not

Worth knowing on Flutter

Keys are Dart method names

gen-l10n turns every key into a getter or a method, so cartTitle works and cart.title stops the build with "Invalid ARB resource name". Mergua accepts either, so a project that started on nested JSON with dotted keys has to rename them before Flutter can read them. Write camelCase from the first key.

The @key object makes the round trip

An @key object is metadata, and Mergua keeps it as metadata: description becomes the key's description in the editor, x-maxLength its character limit, and placeholders with their types are kept as they are. Every pulled language carries the same @key objects back, and gen-l10n reads what it knows and ignores the rest.

What an ARB file carries

Plurals are checked by their placeholders

{count, plural, …} is ICU, and the placeholder check reads its structure. A translation that drops {count} from every case is flagged on the way out; one that adds few and many for Polish is not, because the categories a language needs are its own. The AI translator is told to keep the placeholders too.

How the placeholder check reads ICU

One file per language

gen-l10n compiles one ARB file per language into one class, so keep the Mergua project to one file per language as well. Splitting a language means a folder of files per language, and gen-l10n reads one flat folder of ARB files.

A different stack?

Mergua writes plain locale files, so anything that loads one works. These stacks have a page of their own as well.

Run it on one branch.

Upload the lib/l10n/en.arb you already have and see the whole loop on the free tier.

Create your account