Skip to content
| Marketplace
Sign in
Visual Studio Code>Programming Languages>Modular Flutter L10nNew to Visual Studio Code? Get it now.
Modular Flutter L10n

Modular Flutter L10n

Utanium.org

|
14 installs
| (0) | Free
Modular localization for Flutter with full Flutter Intl compatibility. Organize translations by feature/module for better scalability.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

Modular Flutter L10n

Modular Flutter L10n Logo

Scale your Flutter localization with modular architecture – Organize translations by feature while maintaining full compatibility with Flutter's official Intl library.

Version Installs Ratings GitHub Repo


✨ Why Modular Localization?

Traditional Flutter localization stores all translations in a single namespace. In large apps, this creates:

❌ Naming collisions – Need verbose prefixes like authLoginButton, authSignupButton
❌ Team conflicts – Multiple developers editing the same massive ARB files
❌ Poor organization – Hard to find translations for specific features
❌ Tight coupling – Changes to one feature's strings require regenerating everything

✅ Modular L10n solves this by:

  • Organizing translations by feature/module (auth/, settings/, payments/)
  • Generating type-safe accessors (ML.of(context).auth.loginButton)
  • Supporting independent locale management per module
  • Coexisting peacefully with Flutter Intl for legacy projects

🚀 Quick Start

1. Install Extension

Via VS Code:

  1. Open Extensions (Ctrl+Shift+X / Cmd+Shift+X)
  2. Search "Modular Flutter L10n"
  3. Click Install

Prerequisites:

  • Flutter extension installed
  • Flutter project with pubspec.yaml

2. Initialize Project (One Command!)

  1. Open Command Palette (Ctrl+Shift+P / Cmd+Shift+P)
  2. Run: Modular L10n: Initialize
  3. Answer prompts:
    Default locale: en
    First module name: auth
    Module path: features/auth
    Generated class name: ML  ← KEEP THIS (avoids Flutter Intl conflicts)
    

What happens:

  • ✅ Creates lib/features/auth/l10n/auth_en.arb
  • ✅ Adds config to pubspec.yaml
  • ✅ Generates Dart files in lib/generated/modular_l10n/
  • ✅ Sets up everything needed for localization

📁 Project Structure

lib/
├── features/
│   ├── auth/
│   │   └── l10n/
│   │       ├── auth_en.arb      ← English auth translations
│   │       └── auth_ar.arb      ← Arabic auth translations
│   ├── home/
│   │   └── l10n/
│   │       ├── home_en.arb
│   │       └── home_ar.arb
│   └── settings/
│       └── l10n/
│           ├── settings_en.arb
│           └── settings_ar.arb
├── generated/
│   └── modular_l10n/            ← Auto-generated (DON'T EDIT!)
│       ├── ml.dart              ← Main entry point
│       ├── auth_l10n.dart       ← Auth module class
│       ├── home_l10n.dart
│       ├── settings_l10n.dart
│       ├── app_localization_delegate.dart
│       └── intl/                ← Message lookup tables
│           ├── modular_messages_all.dart
│           ├── modular_messages_en.dart
│           └── modular_messages_ar.dart
└── main.dart

📝 ARB File Format (Critical!)

Every ARB file MUST include two metadata properties:

{
  "@@locale": "en",
  "@@context": "auth",
  
  "loginButton": "Log In",
  "emailLabel": "Email Address",
  "passwordLabel": "Password",
  
  "@loginButton": {
    "description": "Label for login button"
  }
}
Property Required Purpose
@@locale ✅ Yes Locale code (en, ar, fr_FR, zh_Hans_CN, etc.)
@@context ✅ Yes Module name – identifies which module owns these translations
@key ❌ Optional Metadata (description, placeholders, formatting)

⚠️ Without @@context, the extension skips the file. This distinguishes modular ARB files from Flutter Intl's intl_*.arb files.

Supported Locale Formats

The extension validates locales against comprehensive standards:

Simple:     en, ar, fr, de, ja, zh
Regional:   en_US, ar_EG, fr_CA, zh_CN
Script:     zh_Hans, zh_Hant, sr_Latn, sr_Cyrl
Complex:    zh_Hans_CN, zh_Hant_TW, sr_Latn_RS

See full list in module_scanner.ts.


🔧 Flutter Setup

1. Add Dependencies

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.19.0

Run:

flutter pub get

2. Configure MaterialApp

import 'package:flutter_localizations/flutter_localizations.dart';
import 'generated/modular_l10n/l10n.dart';

MaterialApp(
  // Add delegates
  localizationsDelegates: const [
    ML.delegate,                              // ← Modular L10n
    GlobalMaterialLocalizations.delegate,     // ← Material widgets
    GlobalWidgetsLocalizations.delegate,      // ← Flutter widgets
    GlobalCupertinoLocalizations.delegate,    // ← Cupertino widgets
  ],
  
  // Supported locales (auto-detected from ARB files)
  supportedLocales: ML.supportedLocales,
  
  // Optional: Set initial locale
  locale: const Locale('en'),
  
  home: MyHomePage(),
)

That's it! No need for Directionality wrapper – the delegates handle RTL automatically.

3. Platform Configuration (For In-App Switching)

Only needed if you want to change language without restarting the app.

Android (android/app/src/main/AndroidManifest.xml)

<activity
  android:name=".MainActivity"
  android:configChanges="locale|layoutDirection"  ← Add this
  android:supportsRtl="true">                      ← Add this for RTL
  <!-- ... -->
</activity>

iOS (ios/Runner/Info.plist)

<key>CFBundleLocalizations</key>
<array>
  <string>en</string>
  <string>ar</string>
  <!-- Add all supported locales -->
</array>

Why? Without these, the OS restarts your app when locale changes. With them, the change is instant.


💻 Using Translations in Code

In Widgets (with BuildContext)

// Simple strings
Text(ML.of(context).auth.loginButton)

// With placeholders
Text(ML.of(context).auth.welcomeMessage('John'))

// ICU plurals
Text(ML.of(context).home.messageCount(5))
// Outputs: "5 messages" (or "1 message" for count=1)

// ICU gender/select
Text(ML.of(context).profile.greeting('male'))
// Outputs: "Hello, sir!" (or "Hello, ma'am!" for 'female')

In Non-Widget Code (Services/Blocs/Cubits)

// Access without context
final message = ML.current.auth.loginButton;
final greeting = ML.current.auth.welcomeMessage('Sarah');

// Check current locale
final locale = ML.current.auth.instance; // Returns localized instance

In-App Language Switching

💡 Note: This example uses Cubit (from flutter_bloc), but you can use any state management solution you prefer (Provider, Riverpod, GetX, etc.). The key is to store the locale in state and rebuild MaterialApp when it changes.

1. Create Locale Cubit

// lib/core/locale/locale_cubit.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class LocaleCubit extends Cubit<Locale> {
  static const _localeKey = 'app_locale';
  
  LocaleCubit() : super(const Locale('en')) {
    _loadSavedLocale();
  }

  /// Load saved locale from storage on app start
  Future<void> _loadSavedLocale() async {
    final prefs = await SharedPreferences.getInstance();
    final savedLocale = prefs.getString(_localeKey);
    
    if (savedLocale != null) {
      emit(_localeFromString(savedLocale));
    }
  }

  /// Change locale and persist to storage
  Future<void> changeLocale(Locale newLocale) async {
    emit(newLocale);
    
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_localeKey, newLocale.toString());
  }

  /// Parse locale from string (e.g., "en_US" -> Locale('en', 'US'))
  Locale _localeFromString(String localeStr) {
    final parts = localeStr.split('_');
    if (parts.length == 1) return Locale(parts[0]);
    if (parts.length == 2) return Locale(parts[0], parts[1]);
    return Locale(parts[0], parts[1]);
  }
}

2. Provide Cubit in App Root

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'core/locale/locale_cubit.dart';
import 'generated/modular_l10n/l10n.dart';

void main() {
  runApp(
    BlocProvider(
      create: (context) => LocaleCubit(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LocaleCubit, Locale>(
      builder: (context, locale) {
        return MaterialApp(
          locale: locale,
          localizationsDelegates: const [
            ML.delegate,
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
            GlobalCupertinoLocalizations.delegate,
          ],
          supportedLocales: ML.supportedLocales,
          home: const MyHomePage(),
        );
      },
    );
  }
}

3. Create Language Switcher Widget

// lib/widgets/language_switcher.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../core/locale/locale_cubit.dart';
import '../generated/modular_l10n/l10n.dart';

class LanguageSwitcher extends StatelessWidget {
  const LanguageSwitcher({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LocaleCubit, Locale>(
      builder: (context, currentLocale) {
        return DropdownButton<Locale>(
          value: currentLocale,
          items: ML.supportedLocales.map((locale) {
            return DropdownMenuItem(
              value: locale,
              child: Text(_getLocaleName(locale)),
            );
          }).toList(),
          onChanged: (newLocale) {
            if (newLocale != null) {
              context.read<LocaleCubit>().changeLocale(newLocale);
            }
          },
        );
      },
    );
  }

  String _getLocaleName(Locale locale) {
    switch (locale.languageCode) {
      case 'en': return 'English';
      case 'ar': return 'العربية';
      case 'fr': return 'Français';
      case 'de': return 'Deutsch';
      default: return locale.toString();
    }
  }
}

4. Use in Your App

// In any screen
import '../widgets/language_switcher.dart';

AppBar(
  title: Text('Settings'),
  actions: [
    Padding(
      padding: EdgeInsets.symmetric(horizontal: 16),
      child: LanguageSwitcher(),
    ),
  ],
)

Alternative: Using Provider

If you prefer Provider, replace the Cubit with a ChangeNotifier:

// lib/core/locale/locale_provider.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class LocaleProvider extends ChangeNotifier {
  static const _localeKey = 'app_locale';
  Locale _locale = const Locale('en');

  Locale get locale => _locale;

  LocaleProvider() {
    _loadSavedLocale();
  }

  Future<void> _loadSavedLocale() async {
    final prefs = await SharedPreferences.getInstance();
    final savedLocale = prefs.getString(_localeKey);
    if (savedLocale != null) {
      _locale = _localeFromString(savedLocale);
      notifyListeners();
    }
  }

  Future<void> changeLocale(Locale newLocale) async {
    _locale = newLocale;
    notifyListeners();
    
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_localeKey, newLocale.toString());
  }

  Locale _localeFromString(String localeStr) {
    final parts = localeStr.split('_');
    if (parts.length == 1) return Locale(parts[0]);
    if (parts.length == 2) return Locale(parts[0], parts[1]);
    return Locale(parts[0], parts[1]);
  }
}

// main.dart
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => LocaleProvider(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      locale: context.watch<LocaleProvider>().locale,
      // ... rest of config
    );
  }
}

// In language switcher
context.read<LocaleProvider>().changeLocale(newLocale);

Alternative: Using Riverpod

For Riverpod users:

// lib/core/locale/locale_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

final localeProvider = StateNotifierProvider<LocaleNotifier, Locale>((ref) {
  return LocaleNotifier();
});

class LocaleNotifier extends StateNotifier<Locale> {
  static const _localeKey = 'app_locale';

  LocaleNotifier() : super(const Locale('en')) {
    _loadSavedLocale();
  }

  Future<void> _loadSavedLocale() async {
    final prefs = await SharedPreferences.getInstance();
    final savedLocale = prefs.getString(_localeKey);
    if (savedLocale != null) {
      state = _localeFromString(savedLocale);
    }
  }

  Future<void> changeLocale(Locale newLocale) async {
    state = newLocale;
    
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_localeKey, newLocale.toString());
  }

  Locale _localeFromString(String localeStr) {
    final parts = localeStr.split('_');
    if (parts.length == 1) return Locale(parts[0]);
    if (parts.length == 2) return Locale(parts[0], parts[1]);
    return Locale(parts[0], parts[1]);
  }
}

// main.dart
void main() {
  runApp(
    ProviderScope(
      child: const MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final locale = ref.watch(localeProvider);
    
    return MaterialApp(
      locale: locale,
      // ... rest of config
    );
  }
}

// In language switcher
ref.read(localeProvider.notifier).changeLocale(newLocale);

🌍 Advanced ARB Features

1. Placeholders

{
  "@@locale": "en",
  "@@context": "auth",
  
  "welcomeMessage": "Welcome, {name}!",
  
  "@welcomeMessage": {
    "placeholders": {
      "name": {
        "type": "String"
      }
    }
  }
}

Usage:

ML.of(context).auth.welcomeMessage('Alice')
// Output: "Welcome, Alice!"

2. ICU Plural Messages

{
  "messageCount": "{count, plural, =0{No messages} =1{1 message} other{{count} messages}}",
  
  "@messageCount": {
    "placeholders": {
      "count": {
        "type": "int"
      }
    }
  }
}

Usage:

ML.of(context).home.messageCount(0)   // "No messages"
ML.of(context).home.messageCount(1)   // "1 message"
ML.of(context).home.messageCount(5)   // "5 messages"

3. ICU Select Messages

{
  "greeting": "{gender, select, male{Hello, sir!} female{Hello, ma'am!} other{Hello!}}",
  
  "@greeting": {
    "placeholders": {
      "gender": {
        "type": "String"
      }
    }
  }
}

Usage:

ML.of(context).profile.greeting('male')    // "Hello, sir!"
ML.of(context).profile.greeting('female')  // "Hello, ma'am!"
ML.of(context).profile.greeting('other')   // "Hello!"

4. Number Formatting

{
  "totalAmount": "Total: {amount}",
  
  "@totalAmount": {
    "placeholders": {
      "amount": {
        "type": "double",
        "format": "currency",
        "optionalParameters": {
          "symbol": "$",
          "decimalDigits": 2
        }
      }
    }
  }
}

Usage:

ML.of(context).payments.totalAmount(125.5)
// Output: "Total: $125.50"

5. Date/Time Formatting

{
  "orderDate": "Order placed on {date}",
  
  "@orderDate": {
    "placeholders": {
      "date": {
        "type": "DateTime",
        "format": "yMd"
      }
    }
  }
}

Usage:

ML.of(context).orders.orderDate(DateTime(2024, 1, 15))
// Output: "Order placed on 1/15/2024"

Available date formats: yMd, yMMMMd, jm, Hm, and more from Intl.

6. Compound ICU Messages

Multiple ICU expressions in one string:

{
  "orderSummary": "{gender, select, male{He} female{She} other{They}} ordered {count, plural, =0{nothing} one{1 item} other{{count} items}}",
  
  "@orderSummary": {
    "placeholders": {
      "gender": {"type": "String"},
      "count": {"type": "int"}
    }
  }
}

Usage:

ML.of(context).orders.orderSummary('female', 3)
// Output: "She ordered 3 items"

⚙️ Configuration

Zero-Config Default Behavior

The extension works out-of-the-box with these defaults:

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

Code Action: Extract to ARB

Select a string literal in your Dart code → lightbulb appears → choose "Modular L10n: Extract to ARB":

// Before
Text('Log In')
     ^^^^^^^^ (select this)

// After extraction
Text(ML.of(context).auth.loginButton)

// ARB file updated
{
  "loginButton": "Log In"
}

🤝 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

  1. Class Name

    • ✅ Modular L10n: ML (default)
    • ✅ Flutter Intl: S (default)
    • ❌ Never use same name for both!
  2. 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)
  3. Required Properties

    • ✅ Modular: Must have @@context property
    • ✅ Flutter Intl: No @@context property
    • This is how the extension distinguishes them
  4. Output Directories

    • ✅ Modular: lib/generated/modular_l10n/
    • ✅ Flutter Intl: lib/generated/
    • Keep separate to avoid file overwrites

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 Locale command
  • Use same locale codes across all modules (e.g., all use en_US or all use en)
  • 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:

  1. Keep Flutter Intl for global strings (low churn)
  2. Migrate high-churn features first (auth, settings)
  3. Use Migrate from Flutter Intl command to split by prefix
  4. 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

Ibrahim El Mourchidi

Ibrahim El Mourchidi

Flutter & Backend Engineer • Cairo, Egypt

GitHub Email LinkedIn


👥 Contributors


✨ Built with ❤️ for Flutter developers scaling international apps
Star us on GitHub if this helps you!

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
© 2026 Microsoft