Skip to main content

flutter_localisation_cli 1.14.3 copy "flutter_localisation_cli: ^1.14.3" to clipboard
flutter_localisation_cli: ^1.14.3 copied to clipboard

CLI (fl) and MCP server (fl_mcp) for FlutterLocalisation: add, edit and AI-translate ARB keys from the terminal, then pull ARBs or iOS String Catalogs.

flutter_localisation_cli #

Terminal CLI (fl) and MCP server (fl_mcp) for FlutterLocalisation — add, edit, delete and AI-translate your app's localization keys from the shell or from Claude, then pull the ARBs. No dashboard round-trip.

Pure Dart — no Flutter SDK required. This is the tooling companion to the flutter_localisation runtime package (which your app depends on to load the ARBs). Strings live in the FlutterLocalisation backend; this package changes them and the backend regenerates + pushes the ARBs to your git repo.

Install #

dart pub global activate flutter_localisation_cli

This installs the fl and fl_mcp executables in ~/.pub-cache/bin (put that on your PATH).

Auth #

Create a scoped API token in the dashboard → API Keys (flk_live_…), then either:

fl login --token flk_live_xxx      # stored in ~/.config/flutterlocalisation (chmod 600)
# or: export FL_API_TOKEN=flk_live_xxx     # or pass --token on each call

The token is workspace-scoped, so you pick projects by name — no numeric ids.

First run: fl init #

cd my_app
fl init

One wizard for a fresh checkout. It asks for the token if none is stored, lists the projects the token sees (and the flavors, when there are several), prefills the folders from the repository (l10n.yaml arb-dir for ARBs, an existing Localizable.xcstrings or the iOS target folder for catalogs), writes flutterlocalisation.json, pulls the strings, regenerates the Dart for a Flutter app, then offers fl guard and fl native. Every question has a default; fl init -y takes them all, and --project, --flavor, --arb-dir, --xcstrings-dir answer the ones that have no default. --no-pull only writes the config. The project itself is still created in the dashboard: the token cannot do that.

CLI #

fl init                                  # first run: token, project, folders, first pull
fl projects                              # list your projects (name, flavors, locales)
fl status --project "Chat Bot"           # completion % per locale
fl add greeting --value "Hello" -t       # add key + AI-translate every other locale
fl edit greeting --locale fr --value "Bonjour"
fl edit done_body --locale fr --value "{arg1} en {arg2} s"   # catalog key by its stem
fl import strings.arb -t                 # bulk-create MANY keys from one ARB, then translate
fl translate greeting --missing          # fill only empty locales
fl languages                             # list locales + how many keys each has filled
fl languages add id -t                   # add a locale and AI-fill EVERY existing key
fl languages remove id                   # drop a locale and its translations
fl delete greeting                       # whole key (or --locale fr for one locale)
fl repo                                  # print the ARB git repo URL (from the dashboard)
fl clone                                 # first-time: clone that repo into arb_dir
fl pull                                  # sync arb_dir with the service (git or API)
fl sync app_en.arb                       # import, pull and regenerate the Dart, in one
fl guard                                 # stop AI agents editing ARBs + generated Dart
fl native                                # tell iOS + Android about the locales, translate
                                         # the iOS permission alerts
fl glossary add Dictee --keep            # never translate the app's name
fl glossary add Snippets --as de=Schnipsel --as fr=Extraits
fl style set de --formal                 # Sie, never du (de-AT, de-CH follow)
fl glossary apply                        # re-translate strings that use glossary terms
fl lock support_url                      # this key keeps the base text in every locale

Glossary and voice #

The translator sees one batch of strings at a time. Without guidance it translates your product's name like any other word, says "du" in one batch and "Sie" in the next, and calls the same feature two different things. Three settings fix that for every later translation:

  • Glossary terms. --keep keeps a term exactly as written in every locale (brand and product names); a translation that changes it is refused and the string stays as it was. --as <locale>=<text> pins the translation of a term; a translation that does not use it is saved but marked for review. A term's --note tells the translator what it means. fl glossary suggest proposes terms from your strings (project name, recurring names, acronyms), fl glossary import glossary.csv loads a whole list (term,keep,note,de,fr,... or the JSON fl glossary export writes).
  • Language style. fl style set <locale> --formal|--informal [--instructions "..."]. A regional variant (de-AT, pt-BR) follows its language unless it has its own style.
  • Locked keys. fl lock <key> copies the base text into every locale and never sends the key for translation: URLs, code samples, names. It is the same flag as Xcode's "shouldTranslate": false, and both directions round-trip (@key.x-translate: false in ARBs).

These apply to what is translated from then on. fl glossary apply re-translates the strings that contain glossary terms (--term narrows it, --all covers every string after a style change; preview with --dry-run).

Two more things happen without a setting: a string that quotes another string of your app ("Tap “Tap to speak”") gets that string's existing translation, and a regional variant (de-CH) is written from its language's translation (de) instead of from the source on its own.

fl import <file.arb> bulk-creates every key in an ARB in one request (instead of many fl add calls) — the right tool for large migrations. --overwrite replaces existing values; -t/--translate batch-fills the other locales afterwards; --language <code> sets the locale the file represents (default: the base language). Preview with --dry-run.

The result says what the server actually wrote: keys written, keys skipped because they already hold a different value (pass --overwrite to replace them), and keys already identical. Importing one locale's translations replaces rows that still hold a copy of the base text (what adding a locale seeds them with) without --overwrite; rows marked checked are kept. Placeholder types always come from the base language, so a translated ARB without @key metadata cannot turn an int into a String.

fl status and fl retranslate treat a variant of the base language (en-GB, en-US for an en base) that reads the same as the base as finished, and never flag a value with no words in it ({count}). fl retranslate --include-variants sends the variant rows anyway.

Bulk rules — read before scripting this CLI #

Work must cost O(1) requests, not O(N). Two commands cover every bulk case:

Goal Use Cost
Add / change many keys fl import file.arb -t 1 request
Populate new locales fl languages add <c1> <c2> <c3> -t 1 per locale, one command (backend back-fills every key)
Repair every locale fl retranslate (no --locale) batched
Accept the loanword rows fl retranslate --accept-unchanged 1 request
Add one key fl add 1 request

Never loop fl add, fl edit, fl translate, fl languages add or fl retranslate --locale. Each is already the bulk form of itself. Looping one is refused after the fourth single-target call in 15 minutes, and the refusal prints the command you should have run. If a request times out, do not lower the batch size — that multiplies request count; the client halves and retries by itself.

Never loop fl add, fl edit or fl translate over a list of keys. Each fl add -t is ~6 backend requests (resolve, create, batch-translate, verify). A 115-key ARB done that way is ~700 requests — enough to exhaust a whole day's quota for the account. The same job via fl import is one.

This is enforced, not merely advised:

  • the backend rate-limits single-key creation to 10/min and answers with a 429 that names the bulk endpoint to use instead;
  • the MCP server refuses add_string after a handful of applied calls in a session.

A 429 from either is not a signal to sleep and retry — backing off runs the same wrong algorithm slower. Switch to the bulk call.

Likewise, if fl import -t reports that nothing was translated, that is now a hard error naming the locales that were left unseeded (usually: re-run the import against the base language so every locale gets rows). Fix the bulk call; do not fall back to per-key writes. That silent "nothing to translate" success was the original trap.

fl pull / fl sync — getting the ARBs into the repo #

The ARB directory is generated output the backend owns. Nothing in it is a local contribution, so fl pull does not merge or rebase: it fetches and hard-resets onto the upstream branch. A dirty file or a stale local commit cannot block it, and whatever it discards is parked under refs/fl/pre-sync/<stamp>, printed so it can be read back with git show. You never need to run git in that directory yourself — that is the point.

When the git mirror cannot deliver (no repo linked, repository deleted, the wrong git credential active on the machine, no git at all), pull writes the same files from the API instead and says which path it took. The two writers are byte-identical, so failing over does not produce a diff.

fl pull                # sync arb_dir: git mirror if usable, API otherwise
fl pull --no-git       # skip the mirror, write from the API
fl sync app_en.arb     # import that file, pull, then regenerate the Dart
fl export path/to/dir  # just the write, to any directory

Keys that exist only on disk stop a sync. Both paths rewrite the template from the service, so a key staged by hand for an import that never ran would vanish — it has happened, and it took an app's build with it. pull and export name those keys and refuse; fl pull --adopt imports them first, --discard-local takes the service copy on purpose.

This is the dashboard's Export, from the terminal. The ARBs are the backend's data, not hand-written: @@locale is written in the same underscored form as the file name (gen_l10n compares the two and aborts the build when they disagree), and @key metadata goes in the template ARB only, which is where gen_l10n reads it from.

Prefer pull when the repo is linked — it is the path the backend pushes to.

Native iOS (String Catalogs) #

A project can ship a Flutter app, a native iOS app, or both. Set its platforms in the dashboard (project settings). With ios on, the backend renders every locale into one Xcode String Catalog, Localizable.xcstrings (plus InfoPlist.xcstrings when the project holds Info.plist keys such as NSCameraUsageDescription), and the git mirror pushes it like the ARBs. Xcode 15 or later compiles the catalog for any deployment target. Product page: https://flutterlocalisation.com/ios (free .xcstrings viewer and ARB converter there too).

fl import Gush/Resources/Localizable.xcstrings   # seed a project from the catalog Xcode made
fl pull                                          # write Localizable.xcstrings into xcstrings_dir
fl export Gush/Resources                         # same, straight from the API, to a directory

fl pull writes the catalog bytes exactly as the backend serves them, in Xcode's own formatting, so a build does not rewrite the file and the mirror never sees a diff. The local-only guard applies to the catalog too: a key added in Xcode and never imported stops a pull; --adopt imports it first. An iOS-only project has no ARBs and fl sync skips the Dart codegen for it.

Placeholders map by type: {name} becomes %@, an int becomes %lld, a double %lf; with more than one argument they are numbered (%1$@, %2$lld). ICU plurals become plural variations. Not supported in a catalog: ICU select, substitutions (several plural arguments in one string) and device variations; the export names the offending keys and refuses.

fl native — the half of localisation that is not the ARBs #

Translating an app is three jobs, and the ARBs are only the first.

The UI comes from the ARBs. The declarations do not: CFBundleLocalizations in Info.plist and res/xml/locales_config.xml on Android. Neither translates anything — they are what puts every language in the App Store product page's Languages row and the per-app language picker in Settings. With the iOS key absent the bundle reads as English-only, so an app translated into 31 languages advertises one in every storefront. The permission alerts do not either: NSCameraUsageDescription and its siblings are drawn by iOS itself, from the bundle, before the Dart isolate exists. No runtime lookup can reach them. The only way to translate them is one <code>.lproj/InfoPlist.strings per language, registered with the Xcode target — an unregistered file is never copied into the bundle, which looks exactly like never having translated it.

All three rot silently. Nothing fails; the app just stops being translated where nobody looks. So fl native generates them from the locales the project already ships (read from the generated Dart, so it needs no ARB checkout, no token and no network):

fl native                # write everything
fl native --check        # exit 1 if anything is stale — for CI
fl native --dry-run      # list what would change
fl native --init-copy    # scaffold the permission-copy file

The permission sentences live in ios/Runner/InfoPlist.copy.json, keyed by locale:

{
  "en": { "NSCameraUsageDescription": "Trace over what your camera sees." },
  "fr": { "NSCameraUsageDescription": "Décalquez ce que voit votre appareil photo." }
}

They are not dashboard keys. Nothing in that file ever reaches Dart, so putting it through the ARB pipeline would buy nothing and cost a translation round-trip; translate it once and commit it. A locale in the app with no entry here is a hard failure rather than a silent fallback to English — a fluent- looking alert in the wrong language is the bug this command exists to prevent. --init-copy writes a starter from whatever your Info.plist says today.

fl native also writes locales_config.xml if the project has none and points <application android:localeConfig> at it, since a file nothing references is the same as no file.

First-time setup on a new machine or project #

The backend pushes generated ARBs to a git repo you link in the dashboard (Project → Git). fl pull only updates a repo that is already on disk, so bootstrap with clone:

fl repo --project "V-Shape"      # → https://github.com/you/shape_arbs  (github)
fl clone --project "V-Shape"     # clones it into arb_dir (default: ./arbs)
echo '/arbs/' >> .gitignore      # the backend owns that repo; don't vendor it

Then point l10n.yaml at the clone and run codegen:

arb-dir: arbs/Default
output-dir: lib/localization/generated
output-localization-file: app_localizations.dart
nullable-getter: false

fl repo reads the linked provider off the project detail endpoint (github, gitlab, bitbucket or bitbucket_server) and warns when the git token needs reconnecting. --json gives {project, repository: {provider, url, needs_reauth}}; it exits non-zero when no provider is linked. fl clone [<dir>] refuses to clobber a non-empty directory — use fl pull to update an existing clone. Preview either with --dry-run. If the clone never happens, or the machine's git credential cannot see the repo, fl pull still works: it writes the ARBs from the API.

fl guard writes permissions.deny rules into .claude/settings.json so Claude Code refuses to Edit/Write the backend-managed files — the ARB directory (arb_dir + l10n.yaml arb-dir/output-dir) and lib/generated_translation_methods.dart. Those may only change via fl / flutter_localisation + fl pull; a direct AI edit is silently overwritten on the next sync (or lost, for the gitignored ARB repo). Run it once per project (--dry-run to preview).

A project-local flutterlocalisation.json is optional — set it to avoid --project each time:

{ "project": "Chat Bot", "flavor": "Default", "arb_dir": "widget_chat_arbs" }

xcstrings_dir (optional) is where fl pull writes the iOS String Catalogs, relative to the config file like arb_dir; an iOS-only app points it at its Xcode resources folder. Without it the catalogs go beside the ARBs.

Global flags: --project <name|id>, --flavor, --config, --dry-run, --json.

MCP server (Claude) #

fl_mcp lets Claude manage your translations. Mutating tools are preview-by-default — they only write when Claude passes apply: true.

Claude Code:

claude mcp add flutter-localisation --env FL_API_TOKEN=flk_live_xxx -- fl_mcp

Claude Desktop (use the absolute path — Desktop's PATH often omits ~/.pub-cache/bin):

{
  "mcpServers": {
    "flutter-localisation": {
      "command": "/Users/you/.pub-cache/bin/fl_mcp",
      "env": { "FL_API_TOKEN": "flk_live_xxx" }
    }
  }
}

Then ask: "list my projects", then "in Chat Bot, add checkout_button = 'Buy now' and translate it to all locales." Tools: list_projects, list_status, list_languages, add_language, project_repo, add_string, edit_string, delete_string, translate_key, retranslate, import_arb, pull_arbs, glossary, language_style, lock_keys (each takes an optional project/flavor), and sync_native_locales — the MCP face of fl native, which needs no token and takes an optional project_root. project_repo returns the linked ARB git repo so the agent can clone it before codegen; pull_arbs writes the ARBs into arb_dir (and the String Catalogs into xcstrings_dir) the same way fl pull does. import_arb takes an .xcstrings too.

How strings reach your app #

fl add / Claude ─▶ backend creates/translates ─▶ backend pushes ARBs to your git repo
                                                            │
                                     fl pull / fl sync  ◀───┘ ─▶ codegen ─▶ typed strings

License #

MIT.

0
likes
160
points
1.14k
downloads

Documentation

Documentation
API reference

Publisher

verified publishertajaouart.com

Weekly Downloads

CLI (fl) and MCP server (fl_mcp) for FlutterLocalisation: add, edit and AI-translate ARB keys from the terminal, then pull ARBs or iOS String Catalogs.

Homepage
Repository (GitHub)
View/report issues

Topics

#localization #i18n #cli #mcp

License

MIT (license)

Dependencies

args, http, path

More

Packages that depend on flutter_localisation_cli