Modular Flutter L10n
✨ Why Modular Localization?Traditional Flutter localization stores all translations in a single namespace. In large apps, this creates: ❌ Naming collisions – Need verbose prefixes like ✅ Modular L10n solves this by:
🚀 Quick Start1. Install ExtensionVia VS Code:
Prerequisites:
2. Initialize Project (One Command!)
What happens:
📁 Project Structure
📝 ARB File Format (Critical!)Every ARB file MUST include two metadata properties:
Supported Locale FormatsThe extension validates locales against comprehensive standards:
See full list in module_scanner.ts. 🔧 Flutter Setup1. Add Dependencies
Run:
2. Configure MaterialApp
That's it! No need for 3. Platform Configuration (For In-App Switching)Only needed if you want to change language without restarting the app. Android (
|
| Setting | Default | Description |
|---|---|---|
className |
ML |
Generated class name (keep as ML to avoid Flutter Intl conflicts) |
outputPath |
lib/generated/modular_l10n |
Where generated Dart files go |
defaultLocale |
en |
Fallback locale if a translation is missing |
arbFilePattern |
**/l10n/*.arb |
Where to find ARB files (excludes intl_*.arb) |
watchMode |
true |
Auto-regenerate on ARB file changes |
generateCombinedArb |
true |
Create combined ARB files in output directory |
useDeferredLoading |
false |
Enable lazy-loading for web optimization |
When to Configure
| Scenario | Method |
|---|---|
| Team project (recommended) | Edit pubspec.yaml → version-controlled, consistent |
| Personal preferences | VS Code Settings (settings.json) |
| Never | Most apps don't need custom configuration |
Option 1: pubspec.yaml (Recommended)
# pubspec.yaml
modular_l10n:
enabled: true
class_name: ML
default_locale: en
output_dir: lib/generated/modular_l10n
arb_dir_pattern: "**/l10n/*.arb"
generate_combined_arb: true
use_deferred_loading: false
watch_mode: true
Option 2: VS Code Settings
// .vscode/settings.json
{
"modularL10n.className": "ML",
"modularL10n.outputPath": "lib/generated/modular_l10n",
"modularL10n.defaultLocale": "en",
"modularL10n.arbFilePattern": "**/l10n/*.arb",
"modularL10n.generateCombinedArb": true,
"modularL10n.useDeferredLoading": false,
"modularL10n.watchMode": true
}
Priority: pubspec.yaml > VS Code settings > defaults
🔄 Extension Commands
Access via Command Palette (Ctrl+Shift+P / Cmd+Shift+P):
| Command | Description | When to Use |
|---|---|---|
| Initialize | One-click setup for new projects | First time setup |
| Generate Translations | Regenerate Dart files from ARB | After editing ARB files (auto-runs in watch mode) |
| Add Key | Add new translation key to existing module | Interactive key creation |
| Create Module | Create new feature module with ARB files | Starting a new feature |
| Add Locale | Add new locale to all existing modules | Supporting new language |
| Remove Locale | Remove locale from all modules | Dropping language support |
| Add L10n Folder (right-click) | Add l10n folder to directory | Organizing existing features |
| Migrate from Flutter Intl | Convert Flutter Intl ARB files to modular | Migrating existing projects |
| Extract to ARB (code action) | Extract string literal to ARB file | While coding in Dart files |
| Check Missing Translations | Show warnings for missing/empty translations | After adding keys to default locale |
| Scan Hardcoded Strings | Find user-facing strings that should be localized | Auditing existing code |
| Sort ARB Keys | Sort keys alphabetically in ARB files | Keeping ARB files tidy |
| Find Unused Keys | Find translation keys not referenced in Dart code | Cleaning up unused translations |
| Rename Translation Key | Rename a key across all ARB files and Dart code | Refactoring key names |
| Export Translations (CSV/XLIFF) | Export translations for external translators | Sending to translation team |
| Import Translations (CSV/XLIFF) | Import translated files back into ARB | Receiving translations |
| Generate Pseudo-Locale | Create accented/expanded strings for UI testing | Testing layout with different text lengths |
Code Action: Extract to ARB
Place your cursor inside any string literal in Dart code → the lightbulb appears → choose "Modular L10n: Extract to ARB". No need to select the full string — the extension auto-detects the string boundaries.
// Before — just place your cursor anywhere inside the string
Text('Log In')
^ cursor here is enough!
// After extraction
Text(ML.of(context).auth.loginButton)
// ARB file updated
{
"loginButton": "Log In"
}
Supported string types:
- Single-quoted:
'hello' - Double-quoted:
"hello" - Triple-quoted:
'''multi\nline'''and"""multi\nline""" - Raw strings:
r'no escapes'andr"no escapes" - Escaped characters:
'it\'s working'→ properly unescaped in ARB - Dart interpolation:
'Hello $name'→ auto-converted to"Hello {name}"with placeholder metadata
You can also still select the full string manually — both workflows are supported.
🔍 Editor Features
Inline Translation Hover
Hover over any translation key usage in Dart to see all locale values in a tooltip:
Text(ML.of(context).auth.loginButton)
// ^ hover here to see:
// | Locale | Translation |
// |--------|-------------|
// | **en** | Log In |
// | ar | تسجيل الدخول |
// | fr | Connexion |
Go to ARB Definition
Ctrl+Click (or Cmd+Click on macOS) on any translation key to jump directly to the corresponding entry in the default locale's ARB file.
Text(ML.of(context).auth.loginButton)
// ^ Ctrl+Click → opens auth_en.arb at "loginButton"
Missing Translation Diagnostics
Warnings appear automatically in the Problems panel when:
- A key exists in the default locale but is missing in other locales
- A key exists but has an empty value in a locale
Diagnostics run automatically when you save any ARB file.
You can also trigger them manually: Modular L10n: Check Missing Translations
🛠️ Maintenance Tools
Scan Hardcoded Strings
Find hardcoded user-facing strings that should be localized:
Modular L10n: Scan Hardcoded Strings
Scans lib/ for strings in UI contexts like Text(), label:, title:, hintText:, etc. Automatically filters out non-user-facing strings (imports, routes, asset paths, keys, URLs).
Results appear in both the Output panel and Problems panel as hints.
Find Unused Keys
Find translation keys in ARB files that are never referenced in Dart code:
Modular L10n: Find Unused Keys
Reports unused keys per module and optionally bulk-removes them from all ARB files.
Sort ARB Keys
Sort keys alphabetically in ARB files for cleaner diffs and easier navigation:
Modular L10n: Sort ARB Keys
@@meta keys stay at the top (@@locale,@@context)- Each key's
@keymetadata stays immediately after its key - Sort a single module or all modules at once
Rename Translation Key
Rename a key across all locale ARB files and all Dart code references in one action:
Modular L10n: Rename Translation Key
- Select the module
- Pick the key to rename
- Enter the new name
- All ARB files and Dart files are updated, then code is regenerated
🌐 Export & Import for Translators
Export to CSV
Modular L10n: Export Translations (CSV/XLIFF)
Creates a CSV file with columns: Module, Key, Description, then one column per locale. Opens in Excel/Google Sheets for translators.
Export to XLIFF
Same command, choose XLIFF 1.2 format — the industry standard for translation tools (memoQ, SDL Trados, Crowdin, etc.).
Import Translations
Modular L10n: Import Translations (CSV/XLIFF)
Import a translated CSV or XLIFF file back. The extension matches keys to the correct ARB files and updates them.
🧪 Pseudo-Localization
Test your UI layout with pseudo-translated strings:
Modular L10n: Generate Pseudo-Locale
Generates a special locale (default: en_XA) that transforms your default translations:
| Original | Pseudo-localized |
|---|---|
Log In |
[Ĺöğ Ïñ ~~~~~~] |
Welcome, {name}! |
[Ŵëĺçöɱë, {name}! ~~~~~~~~~~~] |
This helps catch:
- Truncation — expanded text (~30-50% longer) reveals overflow
- Hardcoded strings — anything not in brackets
[...]was missed - Concatenation bugs — brackets show if strings are incorrectly split
- Character encoding — accented characters reveal rendering issues
Placeholders ({name}) and ICU syntax are preserved.
🤝 Coexistence with Flutter Intl
✅ Both extensions can work together! This is intentional.
Recommended Hybrid Setup
| Scope | Extension | Location |
|---|---|---|
| Global strings (app name, shared actions) | Flutter Intl | lib/l10n/intl_*.arb |
| Feature strings (auth flows, settings) | Modular L10n | lib/features/**/l10n/*.arb |
Critical Rules to Avoid Conflicts
Class Name
- ✅ Modular L10n:
ML(default) - ✅ Flutter Intl:
S(default) - ❌ Never use same name for both!
- ✅ Modular L10n:
ARB File Naming
- ✅ Modular:
{module}_{locale}.arb(e.g.,auth_en.arb) - ✅ Flutter Intl:
intl_{locale}.arb(e.g.,intl_en.arb) - ❌ Never name modular files
intl_*.arb(auto-skipped)
- ✅ Modular:
Required Properties
- ✅ Modular: Must have
@@contextproperty - ✅ Flutter Intl: No
@@contextproperty - This is how the extension distinguishes them
- ✅ Modular: Must have
Output Directories
- ✅ Modular:
lib/generated/modular_l10n/ - ✅ Flutter Intl:
lib/generated/ - Keep separate to avoid file overwrites
- ✅ Modular:
Using Both in Code
// Modular translations (feature-specific)
Text(ML.of(context).auth.loginButton)
// Flutter Intl translations (global)
Text(S.of(context).appName)
// Both work with same delegates
MaterialApp(
localizationsDelegates: [
ML.delegate, // ← Modular
S.delegate, // ← Flutter Intl
GlobalMaterialLocalizations.delegate,
// ...
],
)
🚨 Troubleshooting
Build Errors
| Error | Cause | Solution |
|---|---|---|
The argument type 'ML' can't be assigned |
Missing delegate in MaterialApp | Add ML.delegate to localizationsDelegates |
No instance of ML present |
Delegate not registered | Ensure ML.delegate is in localizationsDelegates list |
Undefined class 'ML' |
Generated files not imported | Import package:your_app/generated/modular_l10n/l10n.dart |
The getter 'auth' isn't defined |
Module not generated | Run Modular L10n: Generate Translations |
ARB Files Not Detected
| Issue | Cause | Solution |
|---|---|---|
| Files ignored during scan | Missing @@context or @@locale |
Add both properties to ARB file |
| Wrong file pattern | Custom directory structure | Update arbFilePattern in config |
| Conflicting with Flutter Intl | File named intl_*.arb |
Rename to {module}_{locale}.arb |
In-App Language Switching
| Problem | Cause | Fix |
|---|---|---|
| App restarts on Android | Missing configChanges |
Add android:configChanges="locale\|layoutDirection" to AndroidManifest |
| Locale ignored on iOS | Locale not declared | Add all locales to CFBundleLocalizations in Info.plist |
| RTL not working | Missing RTL support | Add android:supportsRtl="true" (delegates handle direction automatically) |
| UI doesn't update | State not rebuilt | Call setState() or use state management after locale change |
Validation Errors
Check Output panel (View → Output → Select "Modular L10n"):
❌ lib/features/auth/l10n/auth_en.arb: Missing required property "@@context"
❌ lib/features/home/l10n/home_ar.arb: Invalid locale "ara" (should be "ar")
💡 Best Practices
1. Module Granularity
Good (feature-level):
lib/features/
├── auth/l10n/ ← Login, signup, password reset
├── profile/l10n/ ← User profile, settings
├── payments/l10n/ ← Checkout, payment methods
Too fine-grained (avoid):
lib/features/
├── login/l10n/ ← Too specific
├── signup/l10n/ ← Group under 'auth' instead
├── forgot_password/l10n/
2. Key Naming
Good (simple, module provides namespace):
{
"@@context": "auth",
"loginButton": "Log In",
"emailLabel": "Email"
}
Access: ML.of(context).auth.loginButton
Avoid (redundant prefix):
{
"@@context": "auth",
"authLoginButton": "Log In", ← 'auth' prefix redundant
"authEmailLabel": "Email"
}
3. Locale Organization
- Add new locales to all modules simultaneously using
Add Localecommand - Use same locale codes across all modules (e.g., all use
en_USor all useen) - Keep default locale (
en) as most complete; other locales can have empty strings initially
4. Version Control
Commit generated files:
# DON'T ignore these
# lib/generated/modular_l10n/
Why? CI/CD builds need them. The extension doesn't run in CI.
Do ignore:
# Generated ARB files (optional)
lib/generated/modular_l10n/arb/
5. Migration Strategy
When migrating existing Flutter Intl projects:
- Keep Flutter Intl for global strings (low churn)
- Migrate high-churn features first (auth, settings)
- Use
Migrate from Flutter Intlcommand to split by prefix - Gradually move remaining translations module by module
❓ Support & Feedback
- Bug report → GitHub Issues
- Feature request → GitHub Issues (enhancement)
- Questions → GitHub Discussions
📜 License
MIT License – See LICENSE
🙏 Acknowledgments
Built with:
- Intl – Flutter's internationalization library
- glob – File pattern matching
- chokidar – File watching
- yaml – YAML parsing
Inspired by Flutter Intl's developer experience while solving modular architecture needs.
🤝 About the Author
👥 Contributors
✨ Built with ❤️ for Flutter developers scaling international apps
Star us on GitHub if this helps you!