l10n_extractor
A command line tool that walks every Dart file in your Flutter project, pulls out the
hardcoded strings, turns them into translation keys, and writes one translation file per
locale you pick — with the translations left as "" so you (or a translator) can fill
them in.
$ dart run l10n_extractor extract
? Which locales do you need? (comma separated) [en, ar]: en, ar, fr
? Output format (json / arb) [json]:
? Where should the locale files go? [assets/translations]:
? Which locale holds the original text? (empty = write "" everywhere) [en]:
About to write 3 locale file(s) into assets/translations:
• assets/translations/en.json (source, keeps original text)
• assets/translations/ar.json
• assets/translations/fr.json
? Continue? (Y/n)
Scan
Files scanned 3
Strings found 21
Unique keys 20
Skipped literals 11
Locales
en assets/translations/en.json +20 new, 0 kept, created
ar assets/translations/ar.json +20 new, 0 kept, created
fr assets/translations/fr.json +20 new, 0 kept, created
✓ Done in 96ms.
assets/translations/en.json
{
"email_address": "Email address",
"hello_you_have_3_new_messages": "Hello {userName}, you have 3 new messages",
"sign_in": "Sign in"
}
assets/translations/ar.json
{
"email_address": "",
"hello_you_have_3_new_messages": "",
"sign_in": ""
}
Install
Add it as a dev dependency in the project you want to scan:
dev_dependencies:
l10n_extractor: ^0.1.0
dart pub get
dart run l10n_extractor extract
Or install it once and use it in any project:
dart pub global activate l10n_extractor
l10n_extractor extract
Usage
# Interactive: asks for locales, format, output folder and the source locale.
dart run l10n_extractor extract
# Fully scripted (no prompts).
dart run l10n_extractor extract -l en,ar,fr --source en -y
# ARB files for flutter_localizations / intl codegen.
dart run l10n_extractor extract -l en,ar -f arb -o lib/l10n
# See what would change without writing anything. The report is still written,
# so you can review every key before the first real run.
dart run l10n_extractor extract --dry-run --report l10n_report.json
# Save the answers to a config file, then just run `extract` from then on.
dart run l10n_extractor init
# Piping the answers in from a script.
printf 'en, ar, fr\n\n\nen\ny\n' | dart run l10n_extractor extract --interactive
Commands
| Command | What it does |
|---|---|
extract [path] |
Scans the project and writes/merges the locale files. This is the default. |
init |
Creates l10n_extractor.yaml with your answers. |
help |
Shows the usage screen. |
Options
| Option | Description |
|---|---|
-l, --locale |
Locale codes, e.g. -l en -l ar or -l en,ar. |
-s, --source |
Locale that keeps the original text. Every other locale gets "". Omit it to write "" everywhere. |
-f, --format |
json (default) or arb. |
-o, --output |
Output folder. Defaults to assets/translations (json) or lib/l10n (arb). |
--path |
Project root to scan. Can also be passed positionally. |
--include |
Folders/files to scan. Defaults to lib. |
--exclude |
Extra path fragments to skip. |
--key-style |
snake (default) or camel. |
--group-by-file |
Nest keys under the file they came from: login_page.sign_in. |
--no-interpolations |
Skip 'Hello $name' strings instead of converting them. |
--keys-class |
Path of a generated Dart class of key constants. |
--prune |
Drop keys that no longer exist anywhere in the code. |
-n, --dry-run |
Report what would change, write nothing. |
--report |
Write a JSON report with every key and the file:line it came from. Works with --dry-run. |
-y, --yes |
Never prompt; fail instead of asking. |
--interactive |
Ask the questions even when stdin is not a terminal, so answers can be piped in. |
Config file
l10n_extractor init writes l10n_extractor.yaml next to your pubspec.yaml. CLI flags
always override it.
include:
- lib
exclude:
- .g.dart
- generated/
locales:
- en
- ar
source_locale: en
format: json
output_dir: assets/translations
key_style: snake
group_by_file: false
include_interpolations: true
keys_class: lib/generated/locale_keys.g.dart
min_length: 2
max_key_words: 6
ignore_map_keys: true
ignore_patterns:
- '^[A-Z0-9_]+$'
ignore_functions:
- myLogger
ignore_named_arguments:
- analyticsName
How keys are generated
The key is derived from the text itself, so it stays the same on every run:
| String in code | Key |
|---|---|
'Sign in' |
sign_in |
'Sign in to your account' |
sign_in_to_your_account |
'Hello $userName, you have 3 messages' |
hello_you_have_3_messages |
'حساب جديد' |
text_dff2b9 (hash fallback for non-Latin text) |
- Keys are capped at
max_key_wordswords. - Two different strings that slugify to the same key get
_2,_3, ... appended. - If a key already exists in the source locale file with the same text, that key is reused — so renaming a key by hand survives the next run.
- Interpolations become named placeholders:
'Hi $name'→"Hi {name}". In ARB output the matching@key.placeholdersmetadata is written too.
What is skipped
The scanner parses real Dart AST (via package:analyzer), not regexes, so it can tell
what a literal is actually used for. It skips:
import/export/partURIs and annotations- asset paths, URLs, emails, hex colors, mime types, date patterns
- identifier-like strings:
user_name,app.title,/home - map keys (
{'user_id': ...}) and index keys (json['name']) — values are still extracted - equality comparisons:
if (status == 'pending') - strings that are only a placeholder (
'$value') or a URL template ('$baseUrl/cars/$id') - named arguments like
key:,fontFamily:,restorationId: - arguments of calls that never take copy:
debugPrint,RegExp,ValueKey,pushNamed,Image.asset, ... case 'value':labels andassert(..., 'message')messages- strings that are already localized:
'key'.tr() - generated files:
*.g.dart,*.freezed.dart,generated/,build/ - the output folder itself
Everything on that list is configurable through l10n_extractor.yaml.
Re-running is safe
- Existing translations are never overwritten — only missing keys are appended.
- An empty value in the source locale is backfilled with the original text.
- Nothing is deleted unless you pass
--prune. - Output is sorted, so diffs stay small.
Generated keys class
With --keys-class lib/generated/locale_keys.g.dart you also get:
abstract final class LocaleKeys {
const LocaleKeys._();
static const String emailAddress = 'email_address';
/// Placeholders: {userName}
static const String helloYouHave3NewMessages = 'hello_you_have_3_new_messages';
static const String signIn = 'sign_in';
}
So call sites read Text(LocaleKeys.signIn.tr()) instead of a raw string.
Wiring the files into your app
The generated JSON is the layout easy_localization expects:
# pubspec.yaml
flutter:
assets:
- assets/translations/
await EasyLocalization.ensureInitialized();
runApp(EasyLocalization(
supportedLocales: const [Locale('en'), Locale('ar')],
path: 'assets/translations',
fallbackLocale: const Locale('en'),
child: const MyApp(),
));
With --format arb the files feed flutter gen-l10n / package:intl instead.
Using it as a library
Every piece is exported, so you can embed the scan in your own tooling:
import 'package:l10n_extractor/l10n_extractor.dart';
final result = Extractor(
ExtractorConfig(
projectRoot: Directory.current.path,
include: const ['lib'],
exclude: ExtractorConfig.defaultExclude,
locales: const ['en', 'ar'],
sourceLocale: 'en',
outputDir: 'assets/translations',
dryRun: true,
),
).run();
for (final entry in result.keyed) {
print('${entry.key} -> "${entry.value}" (${entry.occurrences.length} places)');
}
Example
example/ is a small app with the typical mess — asset paths, route names, map
literals, interpolation, Arabic text — plus the config and the locale files the tool
produced from it.
dart run l10n_extractor extract example -y
Limitations
- The tool writes translation files; it does not rewrite your widgets. Use
--report(or the generated keys class) to migrate the call sites. - Plurals and genders are not detected — an interpolated count becomes a plain
{count}placeholder. - A string built at runtime (
'Total: ' + price) is extracted asTotal:only.
Libraries
- l10n_extractor
- Extract hardcoded strings from a Dart/Flutter project into translation files.