flutter_bhasha
A production-grade Flutter package for bilingual and multilingual localization with dynamic language switching, JSON-based translations, RTL/LTR support, interpolation, fallback language, persistent storage, and a clean developer API.
Features
| Feature | Details |
|---|---|
| Dynamic language switching | Swap locale at runtime — no app restart |
| JSON translations | Simple en.json, te.json, … files |
| Nested + namespaced keys | auth.login, errors.network |
| Parameter interpolation | "welcome": "Hello, {name}!" |
| Fallback language | Auto-falls back when a key is missing |
| Persistent storage | Remembers the user's choice via shared_preferences |
| Device locale detection | Matches device language on first launch |
| RTL / LTR auto-detection | Arabic, Hebrew etc. get TextDirection.rtl |
| Missing key logger | Console warnings during development |
| Provider integration | ChangeNotifier + Provider for reactive rebuilds |
| Extension methods | context.tr("key") and "key".tr() |
| Ready-made widgets | LanguageSelectorDropdown, LanguageSelectorListTile, RtlWrapper |
Installation
dependencies:
flutter_bhasha: ^1.0.0
provider: ^6.1.2
flutter_localizations:
sdk: flutter
Add your translation assets to pubspec.yaml:
flutter:
assets:
- assets/translations/en.json
- assets/translations/te.json
# add more languages here
Quick Start
1. Create translation files
assets/translations/en.json
{
"hello": "Hello",
"welcome": "Welcome, {name}!",
"auth": {
"login": "Login"
}
}
assets/translations/te.json
{
"hello": "హలో",
"welcome": "స్వాగతం, {name}!",
"auth": {
"login": "లాగిన్"
}
}
2. Initialize
import 'package:flutter_bhasha/flutter_bhasha.dart';
const enLocale = BhashaLocale(
languageCode: 'en',
displayName: 'English',
nativeName: 'English',
flagEmoji: '🇺🇸',
);
const teLocale = BhashaLocale(
languageCode: 'te',
displayName: 'Telugu',
nativeName: 'తెలుగు',
flagEmoji: '🇮🇳',
);
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final controller = await BhashaController.initialize(
config: BhashaConfig(
supportedLocales: [enLocale, teLocale],
fallbackLocale: enLocale,
assetPath: 'assets/translations',
persistLocale: true,
useDeviceLocale: true,
),
);
runApp(
BhashaProvider(
controller: controller,
child: const MyApp(),
),
);
}
3. Wire up MaterialApp
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = context.watch<BhashaController>();
return MaterialApp(
locale: controller.currentLocale.toLocale(),
supportedLocales: controller.config.flutterLocales,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: const HomeScreen(),
);
}
}
4. Translate
// BuildContext extension
Text(context.tr('hello'))
Text(context.tr('welcome', params: {'name': 'Ranganath Pavan'}))
// String extension (no context needed)
Text('hello'.tr())
Text('welcome'.tr(params: {'name': 'Ranganath Pavan'}))
5. Switch language
// Programmatically
await context.bhasha.setLocale(teLocale);
await context.bhasha.setLocaleByCode('te');
// Built-in dropdown widget
LanguageSelectorDropdown()
API Reference
BhashaConfig
| Property | Type | Default | Description |
|---|---|---|---|
supportedLocales |
List<BhashaLocale> |
required | All locales the app supports |
fallbackLocale |
BhashaLocale |
required | Used when no match is found |
assetPath |
String |
packages/flutter_bhasha/assets/translations |
Path to JSON files in asset bundle |
persistLocale |
bool |
true |
Save selected locale to shared_preferences |
useDeviceLocale |
bool |
true |
Auto-detect device locale on first run |
missingKeyBehavior |
MissingKeyBehavior |
returnKey |
What to return for missing keys |
logMissingKeys |
bool |
true |
Print console warning for missing keys |
BhashaLocale
const BhashaLocale(
languageCode: 'ar',
countryCode: 'SA', // optional
displayName: 'Arabic',
nativeName: 'العربية', // shown in the language selector
textDirection: TextDirection.rtl,
flagEmoji: '🇸🇦', // optional
)
BhashaController
| Method / Getter | Description |
|---|---|
BhashaController.initialize(config:) |
Creates and returns the singleton |
setLocale(BhashaLocale) |
Switch to locale (persists if configured) |
setLocaleByCode(String) |
Switch by language code string |
resetLocale() |
Revert to fallback and clear persistence |
translate(key, params:) |
Translate with optional interpolation |
currentLocale |
Active BhashaLocale |
isRTL |
true when current locale is RTL |
isLoading |
true while loading translation files |
supportedLocales |
All configured locales |
Context extensions
context.tr('key') // translate
context.tr('key', params: {'x': 'val'}) // with interpolation
context.bhasha // controller (no rebuild)
context.bhashaWatch // controller (rebuilds on change)
context.setLocale(locale) // switch locale
Widgets
| Widget | Description |
|---|---|
BhashaProvider |
Root provider — wraps your MaterialApp |
BhashaBuilder |
Consumer-style builder |
LanguageSelectorDropdown |
Dropdown for AppBar / toolbar |
LanguageSelectorListTile |
Settings-screen list tile |
RtlWrapper |
Auto Directionality driven by active locale |
Adding a New Language
- Create
assets/translations/fr.json. - Register it in
pubspec.yamlunderflutter: assets:. - Add a
BhashaLocaletoBhashaConfig.supportedLocales:
const frLocale = BhashaLocale(
languageCode: 'fr',
displayName: 'French',
nativeName: 'Français',
flagEmoji: '🇫🇷',
);
No code generation required.
Nested & Namespaced Keys
{
"auth": {
"login": "Login",
"errors": {
"invalid_email": "Invalid email address"
}
}
}
context.tr('auth.login')
context.tr('auth.errors.invalid_email')
RTL Support
const arLocale = BhashaLocale(
languageCode: 'ar',
displayName: 'Arabic',
textDirection: TextDirection.rtl,
);
Use the built-in wrapper:
RtlWrapper(child: MyWidget())
Or manually:
Directionality(
textDirection: context.bhashaWatch.textDirection,
child: MyWidget(),
)
Performance Tips
- Pre-warm cache — call
TranslationLoader.preload()at startup for all languages to avoid lag on first switch. - Lazy-load — only the active locale loads at startup; subsequent locales are loaded and cached on first access.
- Keep JSON lean — split large translation sets into namespaced files and load on demand.
- Use
context.bhasha(notcontext.bhashaWatch) in widgets that don't need to rebuild on locale change.
Enterprise Scalability
- Remote translations — replace
TranslationLoaderwith a custom implementation that fetches JSON from a CDN or API. - OTA updates — inject a custom
AssetBundlethat reads from disk/network to push translation fixes without a new release. - Custom storage — implement
LocaleStorageto store the locale in a database, Keychain, or user profile service. - Namespace splitting — load feature-level JSON files on demand by creating multiple
TranslationLoaderinstances with differentassetPathvalues.
Publishing to pub.dev
# Validate
flutter pub publish --dry-run
# Publish
flutter pub publish
Checklist:
Realhomepageandrepositoryinpubspec.yamlCHANGELOG.mdentry for the current versiondart pub publish --dry-runcleanflutter testpassesflutter analyzezero issues
Score boosters:
- Add
topics: [localization, i18n, l10n, flutter, multilingual]topubspec.yaml - Keep description 60–180 characters
- Provide a complete
example/app - Achieve 80%+ test coverage
Testing
flutter test
Use _NoOpStorage and a fake AssetBundle to keep tests hermetic:
final controller = await BhashaController.initialize(
config: config,
storage: _NoOpStorage(),
loader: TranslationLoader(bundle: FakeAssetBundle()),
);
Call BhashaController.reset() in setUp to clear the singleton between tests.
License
MIT