Motrgem - Flutter L10n Text Extractor

pub package License: MIT

A powerful command-line tool that automatically extracts hardcoded text strings from Flutter widgets and converts them to l10n (localization) format.

Installation

Run this command in your Flutter project:

flutter pub add dev:motrgem

This automatically adds motrgem to your dev_dependencies and runs flutter pub get.

Install globally to use the motrgem command anywhere:

dart pub global activate motrgem

Verify installation:

motrgem --help

Option 3: Manual Installation

Add to your pubspec.yaml:

dev_dependencies:
  motrgem: ^1.1.1

Then run:

flutter pub get

Command Usage:

  • Global install: motrgem start, motrgem --replace
  • Dev dependency: dart run motrgem start, dart run motrgem --replace

πŸ€– AI Prompt

Using an AI coding assistant (Claude Code, Cursor, Copilot Chat, etc.) in your Flutter project? Paste this prompt in and it will drive motrgem for you correctly, including the manual review steps the CLI can't do on its own:

This Flutter project should use the `motrgem` package to extract hardcoded
text into Flutter's l10n system. Please:

1. Add it as a dev dependency: `flutter pub add dev:motrgem`
2. If the project has no `l10n.yaml` / `lib/l10n` yet, run:
   `dart run motrgem start`
3. Run `dart run motrgem --dry-run` and show me what would be extracted
   before changing anything.
4. If I approve, run `dart run motrgem --replace` to extract the strings,
   update `lib/l10n/app_en.arb`, and rewrite the call sites.
5. After that finishes, run `flutter analyze` and fix anything it flags,
   in particular:
   - any remaining `const` on a widget that now calls
     `AppLocalizations.of(context)!.xxx` (remove the `const`)
   - any file missing the
     `import 'package:<package_name>/l10n/app_localizations.dart';` import
   - any ARB entry with `{value1}`/`{value2}`-style placeholders that needs
     its generated method call reviewed for correct argument order
6. Run `flutter gen-l10n` (or `flutter pub get`) to regenerate
   `AppLocalizations`, and confirm the app still builds.
7. Check `lib/l10n/possible_hardcoded_texts.txt` (motrgem writes it whenever
   it finds text it couldn't safely auto-fix: custom-widget/exception
   constructor args, `validator:` closures, const string lists). For each
   entry, either localize it by hand or add it to `motrgem.yaml`'s
   `extra_widgets`/`extra_text_params` so future runs handle it
   automatically, then re-run `dart run motrgem --replace`.
8. Ask me which locales to add, then run
   `dart run motrgem --add-locale <code>` for each one and tell me to
   review the machine translations in `lib/l10n/app_<code>.arb` before
   shipping.

Do not hand-write translations yourself β€” only use motrgem's ARB output.

Features

This project includes a powerful L10n Text Extractor library that automatically:

  • πŸ” Analyzes your Flutter code using the Dart analyzer to find hardcoded text strings in widgets
  • 🏷️ Generates unique IDs for each text string in camelCase format
  • πŸ“ Updates ARB files with extracted texts and metadata
  • πŸ”„ Replaces hardcoded strings with AppLocalizations calls
  • πŸ“¦ Automatically adds imports for localization files
  • 🌍 Supports multiple locales with easy locale file generation

Supported Widgets

The library extracts text from these common Flutter widgets:

  • Text
  • AppBar
  • TextButton, ElevatedButton, OutlinedButton
  • FloatingActionButton
  • Tooltip
  • SnackBar, AlertDialog
  • ListTile, Chip
  • InputDecoration (with parameters like hintText, labelText, etc.)

Beyond SDK Widgets: Possible Hardcoded Text & Custom Sinks

Auto-extraction only ever touches the SDK widgets/parameters above β€” it never rewrites your own reusable widgets, since it can't safely assume every call site has the same BuildContext availability an SDK widget does. Real apps route a lot of copy through those custom widgets anyway (e.g. MyStatCard(label: 'Bookings') that internally does Text(label)), so instead of staying silent about it, every --dry-run/--replace run also scans and reports (without auto-fixing) three common patterns:

  • String arguments passed to a locally-declared class constructor motrgem doesn't recognize (covers custom widgets and custom exception/error classes, e.g. throw AuthError(message: 'Your session has expired.'))
  • String literals inside a validator: closure (form-field validators)
  • Elements of a const/plain List<String> variable (e.g. const kDaysOfWeek = ['Monday', ...])

If anything is found, you'll see a console summary and a full list written to lib/l10n/possible_hardcoded_texts.txt β€” review it and either localize those spots by hand, or configure motrgem to auto-handle them (next section).

Configuring extra sinks (motrgem.yaml)

To make motrgem treat your own design-system components exactly like Text/AppBar (full extraction and auto-replacement, not just a report), add an optional motrgem.yaml at your project root:

extra_widgets:
  MyStatCard:
    - label
    - subtitle
extra_text_params:
  - placeholder
  • extra_widgets maps a class name to the parameter names on it that hold display text.
  • extra_text_params adds parameter names recognized on any widget (built-in or custom).

Widgets configured this way are also excluded from the possible-hardcoded-text report, since they're now fully handled instead of merely flagged.

Usage

Initialize Project (First Time Setup)

Initialize a Flutter project with l10n support:

dart run motrgem start

This command will:

  • Add necessary dependencies to pubspec.yaml (flutter_localizations, intl, analyzer, path, args)
  • Create l10n.yaml configuration file
  • Create lib/l10n directory
  • Create initial app_en.arb file
  • Enable generate: true in pubspec.yaml

Extract texts (Dry Run)

See what would be extracted without making any changes:

dart run motrgem --dry-run

Extract and replace

Extract texts, update ARB file, and replace hardcoded strings in your code:

dart run motrgem --replace
flutter clean
flutter pub get

Const Keyword remove

You should delete all const widgets which use AppLocalizations:

Before:

const Text(AppLocalizations.of(context)!.helloWorld)

Should be After:

Text(AppLocalizations.of(context)!.helloWorld)

Add a new locale

Create a new locale file (e.g., Spanish, French, Arabic):

dart run motrgem --add-locale es
dart run motrgem --add-locale fr
dart run motrgem --add-locale ar
flutter clean
flutter pub get

Note: If using as a dev dependency, prefix all commands with dart run, e.g., dart run motrgem start

Changing Language at Runtime

After setting up localization, you can allow users to change the app language dynamically:

1. Make your app stateful with locale management:

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:your_app/l10n/app_localizations.dart';

void main() => runApp(const MyApp());

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

  @override
  State<MyApp> createState() => _MyAppState();

  // Static method to change locale from anywhere in the app
  static void setLocale(BuildContext context, Locale newLocale) {
    _MyAppState? state = context.findAncestorStateOfType<_MyAppState>();
    state?.setLocale(newLocale);
  }
}

class _MyAppState extends State<MyApp> {
  Locale? _locale;

  void setLocale(Locale locale) {
    setState(() {
      _locale = locale;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      locale: _locale,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: const HomePage(),
    );
  }
}

2. Change language from anywhere in your app:

// Switch to Spanish
MyApp.setLocale(context, const Locale('es'));

// Switch to Arabic
MyApp.setLocale(context, const Locale('ar'));

// Switch to English
MyApp.setLocale(context, const Locale('en'));

3. Example: Language selector dropdown:

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

  @override
  Widget build(BuildContext context) {
    return DropdownButton<String>(
      value: Localizations.localeOf(context).languageCode,
      items: const [
        DropdownMenuItem(value: 'en', child: Text('English')),
        DropdownMenuItem(value: 'es', child: Text('EspaΓ±ol')),
        DropdownMenuItem(value: 'ar', child: Text('Ψ§Ω„ΨΉΨ±Ψ¨ΩŠΨ©')),
        DropdownMenuItem(value: 'fr', child: Text('FranΓ§ais')),
      ],
      onChanged: (String? languageCode) {
        if (languageCode != null) {
          MyApp.setLocale(context, Locale(languageCode));
        }
      },
    );
  }
}

Getting Started

Quick Start (New Project)

  1. Create or navigate to your Flutter project
  2. Initialize l10n support:
dart run motrgem start
  1. Install dependencies:
flutter pub get
  1. Extract and replace texts:
dart run motrgem --replace

Prerequisites

  • Flutter SDK installed

Setup Localization

  1. The project already includes l10n.yaml configuration:
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
  1. In your pubspec.yaml, ensure you have:
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.20.2

flutter:
  generate: true
  1. After running the extractor with --replace, add localization delegates to your MaterialApp:
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

MaterialApp(
  localizationsDelegates: AppLocalizations.localizationsDelegates,
  supportedLocales: AppLocalizations.supportedLocales,
  // ... other properties
)

How It Works

1. Text Extraction

The library uses the Dart analyzer package to parse your Flutter code's Abstract Syntax Tree (AST). It identifies:

  • Direct string literals in Text widgets
  • Named parameters containing text (like title:, tooltip:, label:)
  • Strings in various widget constructors

2. ID Generation

Text strings are converted to camelCase IDs:

  • "Hello World" β†’ helloWorld
  • "You have pushed the button" β†’ youHavePushedTheButton
  • "Sign In" β†’ signIn

The generator:

  • Removes special characters
  • Handles duplicates by appending numbers
  • Ensures valid Dart identifiers

3. ARB File Management

Extracted texts are added to lib/l10n/app_en.arb:

{
  "@@locale": "en",
  "youHavePushedTheButton": "You have pushed the button this many times:",
  "@youHavePushedTheButton": {
    "description": "Text from Text in main.dart"
  }
}

4. Code Replacement

Original code:

Text('You have pushed the button this many times:')

Becomes:

Text(AppLocalizations.of(context)!.youHavePushedTheButton)

Project Structure

lib/
β”œβ”€β”€ main.dart                          # Main app file
β”œβ”€β”€ l10n/
β”‚   └── app_en.arb                     # English translations
└── src/
    └── utils/
        β”œβ”€β”€ Text_extractor.dart        # Core analyzer and extractor
        β”œβ”€β”€ arb_manager.dart          # ARB file operations
        └── l10n_manager.dart         # Workflow orchestration

bin/
└── motrgem.dart                       # CLI tool

l10n.yaml                             # L10n configuration

Library Components

TextExtractor

Analyzes Dart files using the analyzer package to find hardcoded strings.

final extractor = TextExtractor();
final texts = await extractor.extractTextFromProject(projectPath);

ArbManager

Manages ARB file operations (reading, writing, adding locales).

final arbManager = ArbManager(projectPath: projectPath);
await arbManager.addTextsToArb(texts);

L10nManager

Orchestrates the complete workflow.

final manager = L10nManager(projectPath);
final result = await manager.processProject(replaceInCode: true);

Example Output

Initialize Command

πŸš€ Initializing Flutter L10n in project...

πŸ“‹ Setup Results:
  βœ… Updated pubspec.yaml with dependencies
  βœ… Created l10n.yaml configuration
  βœ… Created lib/l10n directory
  βœ… Created initial ARB file (app_en.arb)

βœ… Project initialized successfully!

πŸ“ Next steps:
  1. Run: flutter pub get
  2. Run: dart run motrgem --dry-run
  3. Run: dart run motrgem --replace

Extract Command

πŸš€ Flutter L10n Text Extractor
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
πŸ” Extracting texts from project: .
πŸ“ Found 2 hardcoded text(s)

πŸ“‹ Extracted texts:
  lib/main.dart:107:24 - [Text] "You have pushed the button..." -> youHavePushedTheButton
  lib/main.dart:117:18 - [FloatingActionButton.tooltip] "Increment" -> increment

πŸ“„ Updating ARB file...
ARB file updated: lib/l10n/app_en.arb
Added 2 text entries

πŸ”„ Replacing texts in code...
  βœ… Replaced in main.dart: "You have pushed the button..."
  βœ… Replaced in main.dart: "Increment"
  πŸ“¦ Added import to main.dart

✨ Summary:
  - Texts extracted: 2
  - Texts replaced: 2

πŸ“Š ARB Statistics:
  - Total entries: 2
  - With metadata: 2

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
βœ… Process completed successfully!

Advanced Usage

Filtering Technical Strings

The library automatically skips:

  • URLs (http://, https://, www.)
  • File paths
  • Numbers-only strings
  • ALL_CAPS constants
  • Format strings (%s, %d)
  • Template strings with ${}

Handling Duplicates

When the same base ID would be generated multiple times, the library automatically appends numbers:

  • First occurrence: buttonText
  • Second occurrence: buttonText2
  • Third occurrence: buttonText3

Development

Running Tests

dart test

Adding New Widget Support

Edit lib/src/utils/Text_extractor.dart and add to the textWidgets set:

static const textWidgets = {
  'Text',
  'YourCustomWidget',
  // ... more widgets
};

Adding New Text Parameters

Add to the textParams set in _isTextParameter():

const textParams = {
  'title',
  'yourCustomParam',
  // ... more parameters
};

Contributing

Feel free to submit issues and enhancement requests!

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

  • Built with the Dart analyzer package
  • Uses Flutter's official intl package for localization
  • Follows Flutter localization best practices

Libraries