locale_chain 0.1.0
locale_chain: ^0.1.0 copied to clipboard
Smart locale fallback chains for Flutter. Your pt-BR users deserve pt-PT, not English.
LocaleChain for Flutter #
Smart locale fallback chains for Flutter — because pt-BR users deserve pt-PT, not English.
The Problem #
Flutter's i18n ecosystem has zero support for configurable locale fallback chains. Every major package — easy_localization, gen-l10n, slang, flutter_i18n — only supports a single fallback locale.
When pt-BR translations are missing a key, the system jumps straight to English. Your users see English when a perfectly good Portuguese translation exists in pt-PT.
The Solution #
One line change. Zero migration. Your existing context.tr('key') calls just work.
LocaleChain intercepts at the asset-loading layer and deep-merges translations from a configurable fallback chain. When a key is missing in pt-BR, it checks pt-PT, then pt, then en — before giving up.
Installation #
dependencies:
locale_chain: ^0.1.0
Quick Start #
import 'package:locale_chain/locale_chain.dart';
import 'package:locale_chain/easy_localization.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
LocaleChain.configure(); // 1. Configure chains
runApp(
EasyLocalization(
supportedLocales: [Locale('en'), Locale('pt', 'BR'), Locale('pt', 'PT')],
path: 'assets/translations',
assetLoader: LocaleChainAssetLoader( // 2. Swap asset loader
baseLoader: RootBundleAssetLoader(),
),
fallbackLocale: Locale('en'),
child: MyApp(),
),
);
}
// In widgets — unchanged
Text(context.tr('greeting'))
That's it. A pt-BR user will now see pt-PT translations when pt-BR is missing a key.
Custom Configuration #
Default (zero config) #
LocaleChain.configure();
Uses all built-in fallback chains. Covers Portuguese, Spanish, French, German, Italian, Dutch, Norwegian, and Malay regional variants.
With overrides (merge with defaults) #
LocaleChain.configure(
fallbacks: {'pt-BR': ['pt-PT', 'pt']},
);
Your overrides replace matching keys in the default map. All other defaults remain.
Full custom (replace defaults) #
LocaleChain.configure(
fallbacks: {
'pt-BR': ['pt-PT', 'pt'],
'es-MX': ['es-419', 'es'],
},
mergeDefaults: false,
);
Only the chains you specify will be active. No defaults.
Standalone Usage #
Don't use easy_localization? Use mergedMessages() directly with any Map-based i18n system:
LocaleChain.configure();
final messages = await LocaleChain.mergedMessages(
locale: Locale('pt', 'BR'),
loader: (locale) async {
final tag = localeToTag(locale);
final json = await rootBundle.loadString('assets/i18n/$tag.json');
return jsonDecode(json);
},
);
Default Fallback Map #
Portuguese #
| Locale | Fallback Chain |
|---|---|
| pt-BR | pt-PT -> pt -> (default) |
| pt-PT | pt -> (default) |
Spanish #
| Locale | Fallback Chain |
|---|---|
| es-419 | es -> (default) |
| es-MX | es-419 -> es -> (default) |
| es-AR | es-419 -> es -> (default) |
| es-CO | es-419 -> es -> (default) |
| es-CL | es-419 -> es -> (default) |
| es-PE | es-419 -> es -> (default) |
| es-VE | es-419 -> es -> (default) |
| es-EC | es-419 -> es -> (default) |
| es-GT | es-419 -> es -> (default) |
| es-CU | es-419 -> es -> (default) |
| es-BO | es-419 -> es -> (default) |
| es-DO | es-419 -> es -> (default) |
| es-HN | es-419 -> es -> (default) |
| es-PY | es-419 -> es -> (default) |
| es-SV | es-419 -> es -> (default) |
| es-NI | es-419 -> es -> (default) |
| es-CR | es-419 -> es -> (default) |
| es-PA | es-419 -> es -> (default) |
| es-UY | es-419 -> es -> (default) |
| es-PR | es-419 -> es -> (default) |
French #
| Locale | Fallback Chain |
|---|---|
| fr-CA | fr -> (default) |
| fr-BE | fr -> (default) |
| fr-CH | fr -> (default) |
| fr-LU | fr -> (default) |
| fr-MC | fr -> (default) |
| fr-SN | fr -> (default) |
| fr-CI | fr -> (default) |
| fr-ML | fr -> (default) |
| fr-CM | fr -> (default) |
| fr-MG | fr -> (default) |
| fr-CD | fr -> (default) |
German #
| Locale | Fallback Chain |
|---|---|
| de-AT | de -> (default) |
| de-CH | de -> (default) |
| de-LU | de -> (default) |
| de-LI | de -> (default) |
Italian #
| Locale | Fallback Chain |
|---|---|
| it-CH | it -> (default) |
Dutch #
| Locale | Fallback Chain |
|---|---|
| nl-BE | nl -> (default) |
Norwegian #
| Locale | Fallback Chain |
|---|---|
| nb | no -> (default) |
| nn | nb -> no -> (default) |
Malay #
| Locale | Fallback Chain |
|---|---|
| ms-MY | ms -> (default) |
| ms-SG | ms -> (default) |
| ms-BN | ms -> (default) |
How It Works #
configure()stores the fallback chain configuration in memory.LocaleChainAssetLoaderwraps your existingAssetLoader(e.g.,RootBundleAssetLoader).- When easy_localization requests translations for a locale, the adapter loads translations for every locale in the chain.
- Translations are deep-merged from least specific (default) to most specific (current locale). Missing keys are filled from the next locale in the chain.
- easy_localization receives a complete Map with no missing keys — chain fallback is invisible to the rest of your app.
FAQ #
Is this safe for production?
Yes. The adapter wraps easy_localization's AssetLoader interface — a stable, public API. No monkey-patching or reflection.
Performance impact? Negligible. Merged translations are cached per locale. The chain loading only happens once per locale (on first access or locale change).
Does it work with nested translation keys? Yes. The deep merge is recursive — nested Maps are merged at every level.
Can I use a non-English default locale?
Yes. Pass defaultLocale to configure():
LocaleChain.configure(defaultLocale: 'es');
Can I deactivate it?
Yes. Call LocaleChain.reset() to clear configuration. The LocaleChainAssetLoader will pass through to the base loader without chain merging.
Minimum Flutter version? Flutter 3.10+ (Dart 3.0+).
Contributing #
- Open issues for bugs or feature requests.
- PRs welcome, especially for adding new locale fallback chains.
- Run
flutter testfor unit tests. - Run
flutter analyzefor static analysis.
License #
MIT License - see LICENSE file.
Built by i18nagent.ai