Language Helper
A Flutter package for easy multi-language app localization with automatic text extraction and translation support.
Features
- π Easy Setup: Add
.trand.trPto any string for instant translation - π Auto Extraction: Automatically extract all translatable text from your Dart files
- π― Smart Translation: Control translations with conditions and parameters
- π Multiple Sources: Support for Dart maps, JSON files, assets, and network data
- π± Device Locale: Automatically uses device locale on first launch
- π§ AI Integration: Custom translator for easy language conversion
- π¨ LanguageScope: Provide scoped
LanguageHelperinstances to specific widget trees - βοΈ LanguageImprover: Visual translation editor for on-device translation improvement
Quick Start
1. Add to your project
flutter pub add language_helper
2. Initialize (while developing)
final languageHelper = LanguageHelper.instance;
main() async {
WidgetsFlutterBinding.ensureInitialized();
await languageHelper.initial(data: []);
runApp(const MyApp());
}
3. Add translations to your strings
// Simple translation
Text('Hello World'.tr)
// With parameters
Text('Hello @{name}'.trP({'name': 'John'}))
// With conditions
Text('You have @{count} item'.trP({'count': itemCount}))
4. Wrap your app
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LanguageBuilder(
builder: (context) {
return MaterialApp(
localizationsDelegates: languageHelper.delegates,
supportedLocales: languageHelper.locales,
locale: languageHelper.locale,
home: const HomePage(),
);
}
);
}
}
Generate Translations
The generator automatically scans your project for text using language_helper extensions (tr, trP, trT, trF) and translate method, then creates organized translation files with your existing translations preserved.
Note: The generator is smart about managing translations:
- β Keeps existing translations - Your current translated texts are preserved
- π Marks new texts with TODO - Only untranslated texts get TODO markers
- ποΈ Removes unused texts - Automatically cleans up translations no longer used in your code
Add Generator Dependency
First, add the generator to your pubspec.yaml:
dev_dependencies:
language_helper_generator: ^0.7.0
Then run:
flutter pub get
Basic Generation
Extract all translatable text and generate language files:
dart run language_helper:generate --languages=en,vi,fr --ignore-todo=en
This creates:
lib/languages/
βββ codes.dart
βββ data/
βββ en.dart // without TODO markers for missing translations
βββ vi.dart // with TODO markers for missing translations
βββ fr.dart // with TODO markers for missing translations
JSON Generation
For assets or network-based translations:
dart run language_helper:generate --languages=en,vi,fr --json
Creates:
assets/languages/
βββ codes.json
βββ data/
βββ en.json
βββ vi.json
βββ fr.json
JSON files do not support TODO markers. To identify untranslated or new strings, look for entries where the key and value are identical.
Generator Options
| Option | Description | Example |
|---|---|---|
--languages |
Language codes to generate | --languages=en,vi,es |
--ignore-todo |
Skip TODO markers for specific languages | --ignore-todo=en |
--path |
Custom output directory | --path=./lib/languages |
--json |
Generate JSON files instead of Dart | --json |
Common Examples
Skip TODOs in English (your base language):
dart run language_helper:generate --languages=en,vi --ignore-todo=en
Custom output path:
dart run language_helper:generate --path=./lib/languages --languages=en,vi
Generate for multiple languages:
dart run language_helper:generate --languages=en,vi,es,fr --ignore-todo=en
Using Generated Data
Dart Map
final languageDataProvider = LanguageDataProvider.lazyData(languageData);
main() async {
await languageHelper.initial(data: [languageDataProvider]);
runApp(const MyApp());
}
JSON Assets
final languageDataProvider = LanguageDataProvider.asset('assets/languages');
main() async {
await languageHelper.initial(data: [languageDataProvider]);
runApp(const MyApp());
}
Network Data
final languageDataProvider = LanguageDataProvider.network('https://api.example.com/translations');
main() async {
await languageHelper.initial(data: [languageDataProvider]);
runApp(const MyApp());
}
Manual Translation Setup
Dart Map Example
final en = {
'Hello World': 'Hello World',
'Hello @{name}': 'Hello @{name}',
'You have @{count} item': LanguageConditions(
param: 'count',
conditions: {
'1': 'You have one item',
'_': 'You have @{count} items', // Default
}
),
};
final vi = {
'Hello World': 'Xin chΓ o thαΊΏ giα»i',
'Hello @{name}': 'Xin chΓ o @{name}',
'You have @{count} item': 'BαΊ‘n cΓ³ @{count} mα»₯c',
};
LazyLanguageData languageData = {
LanguageCodes.en: () => en,
LanguageCodes.vi: () => vi,
};
JSON Example
assets/languages/codes.json:
["en", "vi"]
assets/languages/data/en.json:
{
"Hello World": "Hello World",
"Hello @{name}": "Hello @{name}",
"You have @{count} item": {
"param": "count",
"conditions": {
"1": "You have one item",
"_": "You have @{count} items"
}
}
}
Don't forget to add to pubspec.yaml:
flutter:
assets:
- assets/languages/codes.json
- assets/languages/data/
Language Control
Change Language
languageHelper.change(LanguageCodes.vi);
Add New Language Data
languageHelper.addData(LanguageDataProvider.lazyData(newLanguageData));
Listen to Changes
final sub = languageHelper.stream.listen((code) => print('Language changed to: $code'));
// Remember to cancel: sub.cancel()
Get Supported Languages
final codes = languageHelper.codes; // All supported languages
final overrides = languageHelper.codesOverrides; // Override languages
Advanced Usage
Generator Features
- Fast: Uses Dart Analyzer, no build_runner dependency
- Smart: Preserves existing translations
- Organized: Groups translations by file path
- Helpful: Adds TODO markers for missing translations
- Clean: Removes unused translation keys automatically
Custom Paths
# Custom output path
dart run language_helper:generate --path=./lib/languages --languages=en,vi
# Generate JSON to assets folder
dart run language_helper:generate --path=./assets/languages --languages=en,vi --json
Multiple Data Sources
main() async {
await languageHelper.initial(
data: [
// Assume that our `code.json` in `https://api.example.com/translations/code.json`
// So our data will be in `https://api.example.com/translations/data/en.json`
LanguageDataProvider.network('https://api.example.com/translations'),
// Assume that our `code.json` in `assets/languages/code.json`
// So our data will be in `assets/languages/en.json`
LanguageDataProvider.asset('assets/languages'),
LanguageDataProvider.lazyData(localLanguageData),
],
);
runApp(const MyApp());
}
Data Priority: When multiple sources contain the same translation:
- First source wins - Data sources are processed in order (top to bottom) for the entire source
- Specific overrides - To override individual translations, use
dataOverridesinstead of adding todata- AddData behavior - New data can overwrite existing translations (controlled by
overwriteparameter)
Widget Rebuilding
LanguageBuilder(
builder: (context) => Text('Hello'.tr),
)
Tr((_) => Text('Hello'.tr))
Force Rebuild and Tree Refresh
forceRebuild Parameter
By default, only the root LanguageBuilder widget rebuilds when the language changes for better performance. Use forceRebuild: true to force a specific widget to always rebuild:
LanguageBuilder(
forceRebuild: true, // This widget will always rebuild on language change
builder: (context) => Text('Hello'.tr),
)
trueβ Always rebuild this widget when language changesfalseβ Only rebuild the root widget (default behavior)nullβ Fallback toLanguageHelper.forceRebuilddefault
refreshTree Parameter
Use refreshTree: true to completely refresh the widget tree using KeyedSubtree. This changes the key of the current tree so the entire tree is removed and recreated:
LanguageBuilder(
refreshTree: true, // Uses KeyedSubtree to refresh entire tree
builder: (context) => MyComplexWidget(),
)
β οΈ Performance Warning:
refreshTreecauses the entire widget tree to be destroyed and recreated, which can be expensive for complex widgets. This may lead to:
- Loss of widget state and animations
- Poor performance with large widget trees
- Unnecessary rebuilds of child widgets
π‘ Note: If you use
constwidgets nested inside aLanguageBuilder, they may not rebuild automatically when the root rebuilds. To ensure these widgets update on language change (without usingrefreshTree), wrap them in their ownLanguageBuilderwithforceRebuild: true.
Use refreshTree only when you specifically need to reset widget state or when dealing with widgets that don't properly handle language changes.
LanguageScope - Scoped LanguageHelper
LanguageScope allows you to provide a custom LanguageHelper instance to a specific part of your widget tree. When you wrap a widget tree with LanguageScope, all tr, trP, LanguageBuilder, and Tr widgets within that scope will automatically use the scoped helper instead of LanguageHelper.instance.
Basic Usage
final customHelper = LanguageHelper('CustomHelper');
await customHelper.initial(
data: customLanguageData,
initialCode: LanguageCodes.es,
);
LanguageScope(
languageHelper: customHelper,
child: MyWidget(),
)
How It Works
When LanguageScope is present in the widget tree:
- LanguageBuilder and Tr - Automatically inherit the scoped helper (unless an explicit
languageHelperis provided) - Extension methods (
tr,trP,trT,trF) - Use the scoped helper when called within aLanguageBuilder. When called outside aLanguageBuilder, they fall back toLanguageHelper.instance(which is always available) - Priority order: Explicit
languageHelperparameter >LanguageScope>LanguageHelper.instance
Extension Methods Behavior
Extension methods (tr, trP, trT, trF) always use a valid LanguageHelper instance:
- Inside
LanguageBuilder: Use the helper associated with that builder (fromLanguageScope, explicit parameter, orLanguageHelper.instance) - Outside
LanguageBuilder: Fall back toLanguageHelper.instance(always available)
This ensures that extension methods never fail and always have a helper to work with, making them safe to use anywhere in your code.
Example: Scoped Translation
final adminHelper = LanguageHelper('AdminHelper');
final userHelper = LanguageHelper('UserHelper');
await adminHelper.initial(data: adminTranslations, initialCode: LanguageCodes.en);
await userHelper.initial(data: userTranslations, initialCode: LanguageCodes.vi);
// Admin section uses admin translations
LanguageScope(
languageHelper: adminHelper,
child: LanguageBuilder(
builder: (context) => Column(
children: [
Text('Admin Panel'.tr), // Uses adminHelper
Text('Manage Users'.trP({'count': 5})), // Uses adminHelper
],
),
),
)
// User section uses user translations
LanguageScope(
languageHelper: userHelper,
child: LanguageBuilder(
builder: (context) => Column(
children: [
Text('Dashboard'.tr), // Uses userHelper
Text('Welcome @{name}'.trP({'name': 'John'})), // Uses userHelper
],
),
),
)
Accessing Scoped Helper
You can access the scoped helper directly from context:
// Gets the scoped helper or falls back to LanguageHelper.instance
// Always returns a valid helper since LanguageHelper.instance is always available
final helper = LanguageHelper.of(context);
final translated = helper.translate('Hello');
Priority with Explicit Helper
If you provide an explicit languageHelper parameter, it takes priority over the scope:
LanguageScope(
languageHelper: scopedHelper, // This will be ignored
child: LanguageBuilder(
languageHelper: explicitHelper, // This takes priority
builder: (_) => Text('Hello'.tr), // Uses explicitHelper
),
)
Nested Scopes
Child scopes override parent scopes for their subtree:
LanguageScope(
languageHelper: parentHelper, // Parent scope
child: LanguageBuilder(
builder: (_) => Column(
children: [
Text('Hello'.tr), // Uses parentHelper
LanguageScope(
languageHelper: childHelper, // Child scope overrides parent
child: LanguageBuilder(
builder: (_) => Text('Hello'.tr), // Uses childHelper
),
),
],
),
),
)
Tr Widget with Scope
The Tr widget also inherits from LanguageScope:
LanguageScope(
languageHelper: scopedHelper,
child: Tr((_) => Text('Hello'.tr)), // Uses scopedHelper
)
Use Cases
- Multi-tenant apps: Different translation sets for different user types
- Feature modules: Separate translations for different app modules
- A/B testing: Different translations for different user groups
- Admin panels: Specialized translations for admin interfaces
- Overrides: Temporarily override translations in specific sections
LanguageImprover - Visual Translation Editor
LanguageImprover is a widget that provides a user-friendly interface for viewing, comparing, and editing translations. It's perfect for translators, QA teams, or anyone who needs to improve translations directly in the app.
Features
- π Side-by-side comparison: View reference and target translations together
- π Search functionality: Quickly find translations by key or content
- βοΈ Inline editing: Edit translations directly in the interface
- π Auto-scroll: Automatically scroll to specific translation keys
- πΎ Update callback: Receive updated translations via callback
- π― Flash animation: Visual highlight for keys being focused
Basic Usage
dart pub add language_improver
LanguageImprover(
languageHelper: LanguageHelper.instance,
onTranslationsUpdated: (updatedTranslations) {
// Handle the improved translations
// updatedTranslations: Map<LanguageCodes, Map<String, dynamic>>
for (final entry in updatedTranslations.entries) {
final code = entry.key;
final translations = entry.value;
// Create a LanguageDataProvider from updated translations
final provider = LanguageDataProvider.data({
code: translations,
});
// Add translations as overrides
LanguageHelper.instance.addDataOverrides(provider);
}
},
)
Parameters
| Parameter | Type | Description |
|---|---|---|
languageHelper |
LanguageHelper? |
The LanguageHelper instance to use. Defaults to LanguageHelper.instance |
onTranslationsUpdated |
FutureOr<void> Function(Map<LanguageCodes, Map<String, dynamic>>)? |
Callback called when translations are saved. Receives a map of language codes to updated translations |
onCancel |
VoidCallback? |
Callback called when the user cancels editing |
initialDefaultLanguage |
LanguageCodes? |
Initial reference language. Defaults to first available language |
initialTargetLanguage |
LanguageCodes? |
Initial target language to improve. Defaults to current language |
scrollToKey |
String? |
Key to automatically scroll to and focus on |
autoSearchOnScroll |
bool |
Whether to automatically search for scrollToKey. Defaults to true |
showKey |
bool |
Whether to show the translation key. Defaults to true |
Example: Navigate and Open
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => LanguageImprover(
languageHelper: LanguageHelper.instance,
initialDefaultLanguage: LanguageCodes.en,
initialTargetLanguage: LanguageCodes.vi,
scrollToKey: 'Hello World', // Automatically scroll to this key
onTranslationsUpdated: (updatedTranslations) {
// Apply updated translations
for (final entry in updatedTranslations.entries) {
final provider = LanguageDataProvider.data({
entry.key: entry.value,
});
LanguageHelper.instance.addDataOverrides(provider);
}
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Translations updated!'.tr),
backgroundColor: Colors.green,
),
);
},
onCancel: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Translation editing cancelled.'.tr),
),
);
},
),
),
);
},
child: Text('Improve Translations'.tr),
)
Example: Scroll to Specific Key
LanguageImprover(
languageHelper: LanguageHelper.instance,
scrollToKey: 'Welcome Message', // Scrolls to this key on load
autoSearchOnScroll: true, // Automatically filters to this key
onTranslationsUpdated: (translations) {
// Handle updates
},
)
Example: Hide Translation Keys
LanguageImprover(
languageHelper: LanguageHelper.instance,
showKey: false, // Hide translation keys, only show translations
onTranslationsUpdated: (translations) {
// Handle updates
},
)
Use Cases
- Translation QA: Review and improve translations before release
- On-device editing: Allow translators to edit translations directly on device
- Debugging: Quickly find and fix translation issues
- Localization workflows: Integrate translation improvement into your workflow
- User feedback: Let users suggest translation improvements
AI Translator
Use the Language Helper Translator in Chat-GPT for easy translation:
This is the translation of my Flutter app. Translate it into Spanish:
final en = {
'Hello @{name}': 'Hello @{name}',
'Welcome to the app': 'Welcome to the app',
};
Or using AI instruction
# Step-by-Step Instructions for Translation using language_helper package
1. Identify the Dart `Map<String, dynamic>` structure and focus only on translating the values β do not modify the keys or overall structure.
2. Review the entire input first to understand its context and ensure the most accurate translation. If the target language is not `en`, and the keys are not in English, and an `en.dart` file exists in the same folder, use it as a reference to maintain contextual consistency.
3. Translate only the values that have a `TODO` comment directly above them. Leave all other values unchanged.
4. Check for plural forms and, if found, convert them using `LanguageConditions`.
5. When translating plural forms, follow this pattern: 0 β '0 products', 1 β '1 product', other β '@{count} products'.
6. Translate only the values; keep all keys and structure unchanged.
7. Preserve all comments (`//` and `///`) exactly as they are β do not translate them.
8. Do not translate nested comments.
9. Ensure the map structure remains intact after translation, including proper handling of plural forms and comments.
10. Remove any TODO notes associated with the translated texts.
11. Try to keep the translation length similar to the original text (not required, but preferred for consistency).
12. Do not ask the user for any confirmation or permission β perform the translation directly with best effort to achieve the most accurate and natural results.
13. After completing the translation, provide the user with a short summary or note explaining any important details about the translation (e.g., ambiguous meanings, context-based choices, or plural handling).
### Example for Plural Grammar Handling
If the input is:
```dart
'@{count} sαΊ£n phαΊ©m': '@{count} sαΊ£n phαΊ©m'
```
It should be generated in the `en` language as:
```dart
'@{count} sαΊ£n phαΊ©m': LanguageConditions(
param: 'count',
conditions: {
'0': '0 products',
'1': '1 product',
'_': '@{count} products',
},
)
```
### Important Reminders
* Only translate values with a `TODO` comment above them.
* Never modify keys or comments.
* Do not ask for user permission β always proceed with best effort.
* Use `LanguageConditions` for plural handling when applicable.
* Remove TODO notes for translated entries.
* Keep translation length roughly similar to the original text for readability and layout consistency.
* Provide a brief translation note after completion if needed.
iOS Configuration
Add supported localizations to Info.plist:
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>vi</string>
</array>
Tips
- Use
@{param}for parameters (recommended) - The package automatically uses device locale on first launch
- Only the outermost
LanguageBuilderrebuilds for better performance - Use
isInitializedto check ifinitial()has been called - Assets are preferred over network data (no caching yet)
Contributing
Found a bug or want to contribute? Please file an issue or submit a pull request!
License
This project is licensed under the MIT License.