language_extract
Stop translating by hand. Extract, generate, replace — done.
language_extract is a Dart CLI tool that scans a Flutter project's source code for hardcoded UI strings, generates localization files for your chosen i18n package, and optionally rewrites every string literal with the correct localized call — including dynamic strings with $variables.
Features
- AST-based scanning — uses the Dart analyzer, not regex. Understands context: only extracts strings inside UI widgets (
Text,AppBar,SnackBar,AlertDialog, …), not route names, asset paths, or code identifiers. - Three i18n packages supported — GetX, easy_localization, intl (ARB).
- Handles interpolation —
'Hello $name'becomes'hello_name'.trParams({'name': name.toString()})(GetX) or'hello_name'.tr(namedArgs: {'name': name.toString()})(easy_localization). - Idempotent — safe to re-run. Source locale files are always regenerated; non-source locale files merge new keys while preserving existing human translations.
- Auto
constremoval — when a string inside aconstwidget is replaced, theconstkeyword is removed automatically. - Auto import insertion — the required package import is added to every modified file.
- One-time setup hints — prints setup instructions on first run; silently skips them once your project is already wired up.
Installation
Installation
Install once, run from any directory:
dart pub global activate language_extract
Then run from anywhere in your terminal:
language_extract getx /path/to/my_flutter_app --locales en_US,ar_AR
Quick start
Important:
<project_path>must be the Flutter project root — the folder that containspubspec.yamlandlib/. Do not point it at thelib/folder itself, or the tool will fail looking forlib/lib/.
# Correct — project root
language_extract getx ~/projects/myapp --locales en_US,ar_AR
# Wrong — do not point at lib/
language_extract getx ~/projects/myapp/lib --locales en_US,ar_AR
# 1. Scan and generate locale files (dry run — source untouched)
language_extract getx /path/to/app --locales en_US,ar_AR
# 2. Also rewrite source files (adds .tr calls, removes const, adds imports)
language_extract getx /path/to/app --locales en_US,ar_AR --replace
# 3. Use easy_localization instead
language_extract easy_localization /path/to/app --locales en,ar --replace
# 4. Use the intl package
language_extract intl /path/to/app --locales en_US,ar_AR --replace
Usage
language_extract <package> <project_path> [options]
Packages:
getx GetX — generates lib/translations/*.dart
easy_localization easy_localization — generates assets/translations/*.json
intl intl / flutter gen-l10n — generates lib/l10n/app_*.arb
Options:
-l, --locales Comma-separated list of locales [required]
e.g. --locales en_US,ar_AR,fr_FR
-s, --source-locale Which locale to use as the source (values from code).
Must be one of the --locales values.
Defaults to the first locale in the list.
-r, --replace Rewrite source files with localized calls.
-h, --help Show this help message.
Examples
# GetX — English source, add Arabic
language_extract getx ~/projects/myapp --locales en_US,ar_AR --source-locale en_US
# easy_localization — three locales, rewrite source
language_extract easy_localization ~/projects/myapp \
--locales en,ar,fr \
--source-locale en \
--replace
# intl — default source locale (first in list)
language_extract intl ~/projects/myapp --locales en_US,ar_AR --replace
What gets generated
GetX
lib/
translations/
en_US.dart ← source locale (values from code, always regenerated)
ar_AR.dart ← other locales (existing translations preserved,
new keys appended as empty strings)
app_translations.dart ← Translations subclass, register in GetMaterialApp
en_US.dart
const Map<String, String> enUS = {
'home_screen_welcome': 'Welcome',
'home_screen_hello_name': 'Hello @name',
};
ar_AR.dart (fill in your translations)
const Map<String, String> arAR = {
'home_screen_welcome': '', // ← translate here
'home_screen_hello_name': '',
};
app_translations.dart
import 'package:get/get.dart';
import 'en_US.dart';
import 'ar_AR.dart';
class AppTranslations extends Translations {
@override
Map<String, Map<String, String>> get keys => {
'en_US': enUS,
'ar_AR': arAR,
};
}
One-time main.dart setup (printed on first run):
import 'translations/app_translations.dart';
GetMaterialApp(
translations: AppTranslations(),
locale: const Locale('en', 'US'),
fallbackLocale: const Locale('en', 'US'),
// ... rest of your config
)
easy_localization
assets/
translations/
en.json ← source locale
ar.json ← other locales (merged)
en.json
{
"home_screen_welcome": "Welcome",
"home_screen_hello_name": "Hello {name}"
}
One-time setup (printed on first run):
-
pubspec.yaml:flutter: assets: - assets/translations/ -
main.dart:void main() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); runApp( EasyLocalization( supportedLocales: [Locale('en'), Locale('ar')], path: 'assets/translations', fallbackLocale: Locale('en'), child: MyApp(), ), ); } -
In
MaterialApp:localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales,
intl (flutter gen-l10n)
lib/
l10n/
app_en.arb ← source locale (values + metadata for gen-l10n)
app_ar.arb ← other locales (merged)
app_en.arb
{
"@@locale": "en",
"homeScreenWelcome": "Welcome",
"@homeScreenWelcome": { "description": "home_screen_welcome" },
"homeScreenHelloName": "Hello {name}",
"@homeScreenHelloName": {
"description": "home_screen_hello_name",
"placeholders": { "name": { "type": "String" } }
}
}
One-time setup (printed on first run):
-
Create
l10n.yamlin your project root:arb-dir: lib/l10n template-arb-file: app_en.arb output-localization-file: app_localizations.dart -
Add to
pubspec.yaml:flutter: generate: true -
Run
flutter gen-l10nto generateAppLocalizations.
The --replace flag
Without --replace, only locale files are written — your source code stays untouched. With --replace, every detected string literal is rewritten in place.
Before
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final user = 'Alice';
return Scaffold(
appBar: AppBar(title: const Text('My Dashboard')),
body: Column(
children: [
Text('Welcome back, $user'),
const Text('Settings'),
],
),
);
}
}
After (GetX)
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final user = 'Alice';
return Scaffold(
appBar: AppBar(title: Text('home_screen_my_dashboard'.tr)),
body: Column(
children: [
Text('home_screen_welcome_back_user'.trParams({'user': (user).toString()})),
Text('home_screen_settings'.tr),
],
),
);
}
}
Notice:
constremoved fromTextwidgets (.tris a runtime call, soconstis invalid)import 'package:get/get.dart';added automatically- Dynamic
$usermapped totrParamswith.toString()conversion (safe for int, double, etc.)
After (easy_localization)
Text('home_screen_my_dashboard'.tr())
Text('home_screen_welcome_back_user'.tr(namedArgs: {'user': (user).toString()}))
After (intl)
Text(AppLocalizations.of(context)!.homeScreenMyDashboard)
Text(AppLocalizations.of(context)!.homeScreenWelcomeBackUser((user).toString()))
Key format
Keys are derived from the file name and string value:
| File | String | Key |
|---|---|---|
home_screen.dart |
"Welcome" |
home_screen_welcome |
home_screen.dart |
"Hello $name" |
home_screen_hello_name |
signup_screen.dart |
"Create your account today" |
signup_screen_create_your_account |
Rules:
- Prefix — file name without
.dart, converted tosnake_case - Suffix — up to 4 words from the string value, lowercased, joined by
_ - Deduplication — same string in the same file → same key (no duplicates in output)
- Collision resolution — different strings that map to the same key get
_2,_3suffixes
What gets skipped
The scanner ignores strings that are clearly not UI text:
| Pattern | Example |
|---|---|
| Route strings | /home, /auth/login |
| URLs | https://api.example.com |
| Asset paths | assets/images/logo.png |
| Pure numbers | '42', '3.14' |
| Reverse-domain IDs | com.example.app |
camelCase identifiers |
myVariableName |
snake_case identifiers |
my_variable_name |
| Hex colors | #FF5733 |
| Single characters | '/', ':' |
| Strings not in UI context | Log messages, map keys, constants |
Idempotency & merge behavior
| Locale type | Behavior on re-run |
|---|---|
| Source locale | Always regenerated from current source code |
| Other locales | Existing translations preserved; new keys appended as empty strings |
Re-run the tool at any time as your codebase grows. Translators can fill in non-source locale files between runs without losing their work.
Interpolation handling
| Input | GetX output |
|---|---|
'Hello $name' |
'hello_name'.trParams({'name': (name).toString()}) |
'Order #${order.id}' |
'order_id'.trParams({'orderId': (order.id).toString()}) |
'Hi ${user.name ?? "Guest"}' |
'hi_user_name_guest'.trParams({'userNameGuest': (user.name ?? "Guest").toString()}) |
Locale file values use package-specific placeholder syntax:
| Package | Placeholder syntax |
|---|---|
| GetX | Hello @name |
| easy_localization | Hello {name} |
| intl | Hello {name} |
All dynamic values are wrapped in (expr).toString() — this ensures safety when the expression is an int, double, or uses null-coalescing (??) where operator precedence would otherwise cause issues.
Supported UI widgets
The scanner detects strings in these built-in Flutter widgets:
Text · AppBar · SnackBar · AlertDialog · CupertinoAlertDialog · TextButton · ElevatedButton · OutlinedButton · FloatingActionButton · ListTile · DropdownMenuItem · InputDecoration · PopupMenuEntry · Tab · Tooltip · Card · Chip · Badge · NavigationDestination · BottomNavigationBarItem · DrawerHeader · ExpansionTile · DataColumn · DataCell · SelectableText
Custom widgets are also supported — any string passed to a recognized named parameter is extracted:
title: · label: · text: · message: · hintText: · labelText: · helperText: · errorText: · prefixText: · suffixText: · counterText: · tooltip: · semanticLabel: · buttonText: · confirmText: · cancelText: · placeholderText:
Requirements
- Dart SDK
>=3.0.0 - A Flutter project with the standard structure (
pubspec.yaml+lib/at the root) - Pass the project root as
<project_path>— the tool automatically scanslib/and writes output relative to that root
Contributing
Contributions are welcome! Please open an issue first to discuss what you'd like to change.
Run from source
git clone https://github.com/usamaahsan/flutter_language_extract.git
cd flutter_language_extract
dart pub get
dart run bin/language_extract.dart getx /path/to/my_flutter_app --locales en_US,ar_AR
Run tests
dart test
Analyze
dart analyze
License
MIT