Runtime, sharded, msgid-based internationalization for Flutter - no code generation required.
A tiny, production-ready i18n layer for Flutter that solves the pain points of traditional approaches:
- β
No codegen - Pure runtime lookups with
context.t('Sign in')or'Sign in'.tx - β
Sharded by feature -
assets/i18n/<locale>/<feature>.jsonprevents merge conflicts - β Msgid ergonomics - Use readable English directly in code; auto-fallback if missing
- β
BLoC-ready - Tiny
LanguageCubitdrivesLocale; UI pulls strings via context - β Dynamic switching - Change language at runtime without restart
- β
CLDR plurals - Proper
one/few/many/otherforms for 15+ languages - β
Automated migration - Migrate existing apps automatically with
shard_i18n_migrator - β AI-powered CLI - Auto-translate missing keys with OpenAI/DeepL
Large teams fight over one giant ARB/JSON file and slow codegen cycles. shard_i18n removes those bottlenecks:
| Problem | shard_i18n Solution |
|---|---|
| Merge conflicts in monolithic translation files | Sharded translations by feature (core.json, auth.json, etc.) |
| Slow code generation cycles | No codegen - direct runtime lookups |
| Cryptic generated method names | Natural msgid usage: context.t('Sign in') |
| Complex setup for dynamic language switching | Built-in locale switching with AnimatedBuilder |
| Manual plural form management | CLDR-based plural resolver for 15+ languages |
| Time-consuming manual migration | Automated migrator extracts and transforms strings automatically |
| Tedious translation workflows | AI-powered CLI to fill missing translations |
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
shard_i18n: ^0.2.2
flutter_bloc: ^8.1.4 # for state management (optional but recommended)
shared_preferences: ^2.2.3 # for persisting language choice (optional)
flutter:
assets:
- assets/i18n/Create sharded JSON files per locale:
assets/i18n/
en/
core.json
auth.json
de/
core.json
auth.json
tr/
core.json
ru/
core.json
Example: assets/i18n/en/auth.json
{
"Sign in": "Sign in",
"Hello, {name}!": "Hello, {name}!",
"items_count": {
"one": "{count} item",
"other": "{count} items"
}
}German: assets/i18n/de/auth.json
{
"Sign in": "Anmelden",
"Hello, {name}!": "Hallo, {name}!",
"items_count": {
"one": "{count} Artikel",
"other": "{count} Artikel"
}
}import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:shard_i18n/shard_i18n.dart';
import 'language_cubit.dart'; // see below
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final initialLocale = await LanguageCubit.loadInitial();
await ShardI18n.instance.bootstrap(initialLocale);
runApp(
BlocProvider(
create: (_) => LanguageCubit(initialLocale),
child: const MyApp(),
),
);
}class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
final locale = context.watch<LanguageCubit>().state;
return AnimatedBuilder(
animation: ShardI18n.instance,
builder: (_, __) {
return MaterialApp(
locale: locale,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: ShardI18n.instance.supportedLocales,
home: const HomePage(),
);
},
);
}
}class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.t('Hello, {name}!', params: {'name': 'World'})),
),
body: Center(
child: Text(context.tn('items_count', count: 5)),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<LanguageCubit>().setLocale(Locale('de')),
child: const Icon(Icons.language),
),
);
}
}// Simple translation with interpolation
context.t('Hello, {name}!', params: {'name': 'Alice'})
// Plural forms (automatically selects one/few/many/other based on locale)
context.tn('items_count', count: 5)For even more concise code, use string extensions (no context required):
// Simple translation (getter)
Text('Hello World'.tx)
// Translation with parameters
Text('Hello, {name}!'.t({'name': 'Alice'}))
// Plural forms
Text('items_count'.tn(count: 5))| Method | Description | Example |
|---|---|---|
.tx |
Simple translation (getter) | 'Sign in'.tx |
.t() |
Translation with optional params | 'Hello, {name}!'.t({'name': 'World'}) |
.tn() |
Pluralization with count | 'items_count'.tn(count: 5) |
Note: String extensions use
ShardI18n.instancedirectly, so they work anywhere - in widgets, controllers, or utility classes.
// Bootstrap before runApp
await ShardI18n.instance.bootstrap(Locale('en'));
// Change locale at runtime
await ShardI18n.instance.setLocale(Locale('de'));
// Get current locale
ShardI18n.instance.locale
// Get discovered locales from assets
ShardI18n.instance.supportedLocales
// Register custom plural rules
ShardI18n.instance.registerPluralRule('fr', (n) => n <= 1 ? 'one' : 'other');
// Clear cache (useful for testing)
ShardI18n.instance.clearCache();import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shard_i18n/shard_i18n.dart';
class LanguageCubit extends Cubit<Locale> {
LanguageCubit(super.initialLocale);
static const _k = 'app_locale';
static Future<Locale> loadInitial() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString(_k);
if (saved != null && saved.isNotEmpty) {
final p = saved.split('-');
return p.length == 2 ? Locale(p[0], p[1]) : Locale(p[0]);
}
return WidgetsBinding.instance.platformDispatcher.locale;
}
Future<void> setLocale(Locale locale) async {
if (state == locale) return;
await ShardI18n.instance.setLocale(locale);
final prefs = await SharedPreferences.getInstance();
final tag = locale.countryCode?.isNotEmpty == true
? '${locale.languageCode}-${locale.countryCode}'
: locale.languageCode;
await prefs.setString(_k, tag);
emit(locale);
}
}By default, use natural English msgids for readability:
Text(context.t('Sign in'))Switch to stable IDs when English copy is volatile:
Text(context.t('auth.sign_in'))The lookup works for both! English translation file becomes:
{
"auth.sign_in": "Sign in"
}Named placeholders using {name} syntax:
{
"Hello, {name}!": "Hallo, {name}!"
}context.t('Hello, {name}!', params: {'name': 'Uli'})Define plural forms matching your locale's rules:
{
"items_count": {
"one": "{count} item",
"other": "{count} items"
}
}Russian (complex one/few/many/other):
{
"items_count": {
"one": "{count} ΠΏΡΠ΅Π΄ΠΌΠ΅Ρ",
"few": "{count} ΠΏΡΠ΅Π΄ΠΌΠ΅ΡΠ°",
"many": "{count} ΠΏΡΠ΅Π΄ΠΌΠ΅ΡΠΎΠ²",
"other": "{count} ΠΏΡΠ΅Π΄ΠΌΠ΅ΡΠ°"
}
}Turkish (no plural distinction):
{
"items_count": {
"other": "{count} ΓΆΔe"
}
}Supported plural rules: en, de, nl, sv, no, da, fi, it, es, pt, tr, ro, bg, el, hu, ru, uk, sr, hr, bs, pl, cs, sk, fr, lt, lv.
1. Locale + country (e.g., de-DE)
β (if missing)
2. Locale language (e.g., de)
β (if missing)
3. English (en)
β (if missing)
4. Msgid/stable ID itself (developer-friendly)
await context.read<LanguageCubit>().setLocale(Locale('de'));ShardI18n hot-loads the new locale's shards and notifies AnimatedBuilder to rebuild the UI. No app restart required!
shard_i18n includes two powerful CLI tools to streamline your i18n workflow:
Global installation (recommended):
# Install globally
dart pub global activate shard_i18n
# Use commands directly
shard_i18n_cli verify
shard_i18n_migrator analyze lib/Local execution (without global install):
# Run from your project directory
dart run shard_i18n_cli verify
dart run shard_i18n_migrator analyze lib/Manage and automate translation workflows.
Check for missing keys and placeholder consistency:
shard_i18n_cli verify
# or: dart run shard_i18n_cli verifyOutput:
π Verifying translations in: assets/i18n
π Found locales: en, de, tr, ru
π Reference locale: en (15 keys)
de:
β
All keys present (15 keys)
tr:
β οΈ Missing 2 key(s):
- Welcome to shard_i18n
- Features
ru:
β
All keys present (15 keys)
Auto-translate missing keys using AI:
# Using OpenAI
shard_i18n_cli fill \
--from=en \
--to=de,tr,fr \
--provider=openai \
--key=$OPENAI_API_KEY
# Using DeepL
shard_i18n_cli fill \
--from=en \
--to=de \
--provider=deepl \
--key=$DEEPL_API_KEY
# Dry run (preview without writing)
shard_i18n_cli fill \
--from=en \
--to=de \
--provider=openai \
--key=$OPENAI_API_KEY \
--dry-runThe CLI preserves {placeholders} and writes translated entries to the appropriate locale files.
Automatically migrate existing Flutter apps to use shard_i18n. The migrator analyzes your codebase, extracts translatable strings, and transforms your code to use the shard_i18n API.
Preview what strings will be extracted without making changes:
shard_i18n_migrator analyze lib/
# or: dart run shard_i18n_migrator analyze lib/Output shows:
- Total translatable strings found
- Breakdown by category (extractable, technical, ambiguous)
- Confidence scores
- Strings with interpolation and plurals
Options:
--verbose- Show detailed analysis per file--config=path/to/config.yaml- Use custom configuration
Transform your code to use shard_i18n:
# Dry run (preview changes without writing)
shard_i18n_migrator migrate lib/ --dry-run
# Interactive mode (asks for confirmation on ambiguous strings)
shard_i18n_migrator migrate lib/
# Automatic mode (extracts everything above confidence threshold)
shard_i18n_migrator migrate lib/ --autoWhat it does:
- Analyzes all Dart files in the specified directory
- Extracts translatable strings (UI text, error messages, etc.)
- Transforms code to use
context.t()andcontext.tn()for plurals - Generates JSON translation files in
assets/i18n/en/ - Adds necessary imports (
package:shard_i18n/shard_i18n.dart) - Preserves interpolation parameters and plural forms
Migration options:
--dry-run- Preview without modifying files--auto- Skip interactive prompts for ambiguous strings--verbose- Show detailed migration progress--config=path- Use custom migration config--threshold=0.8- Set confidence threshold (0.0-1.0)
Create a shard_i18n_config.yaml for fine-tuned migration:
# Minimum confidence score to auto-extract (0.0 - 1.0)
autoExtractThreshold: 0.7
# Directories to exclude from analysis
excludePaths:
- lib/generated/
- test/
- .dart_tool/
# Patterns to skip (regex)
skipPatterns:
- '^[A-Z_]+$' # ALL_CAPS constants
- '^\d+$' # Pure numbers
# Feature-based sharding
sharding:
enabled: true
defaultFeature: core
# Map directories to features
featureMapping:
lib/auth/: auth
lib/settings/: settings
lib/profile/: profileRecommended workflow for existing apps:
-
Analyze first:
shard_i18n_migrator analyze lib/ --verbose
-
Test on a single feature:
shard_i18n_migrator migrate lib/auth/ --dry-run shard_i18n_migrator migrate lib/auth/
-
Run tests:
flutter test -
Migrate remaining code:
shard_i18n_migrator migrate lib/ --auto
-
Verify translations:
shard_i18n_cli verify
Interactive mode is recommended for the first migration - it prompts for confirmation on strings with low confidence scores, helping you avoid extracting technical strings or constants.
assets/i18n/
en/ # Source locale
core.json # App-wide strings
auth.json # Authentication feature
settings.json # Settings feature
de/ # German translations
core.json
auth.json
settings.json
tr/ # Turkish translations
core.json
auth.json
ru/ # Russian translations
core.json
Why sharded?
- Fewer merge conflicts: Feature teams work on separate files
- Faster loading: Only current locale loaded (not all languages)
- Easier maintenance: Clear ownership per feature
import 'package:flutter_test/flutter_test.dart';
import 'package:shard_i18n/shard_i18n.dart';
void main() {
test('interpolation works', () {
final result = ShardI18n.instance.translate(
'Hello, {name}!',
params: {'name': 'World'},
);
expect(result, equals('Hello, World!'));
});
}testWidgets('displays translated text', (tester) async {
await ShardI18n.instance.bootstrap(Locale('de'));
await tester.pumpWidget(MyApp());
expect(find.text('Anmelden'), findsOneWidget); // German "Sign in"
});Use the shard_i18n_migrator tool for automated migration from any existing i18n solution:
# 1. Analyze your codebase
shard_i18n_migrator analyze lib/ --verbose
# 2. Run migration (interactive mode)
shard_i18n_migrator migrate lib/
# 3. Review changes and test
flutter test
# 4. Verify translations
shard_i18n_cli verifyThe migrator automatically:
- β Extracts translatable strings from your code
- β
Transforms to
context.t()andcontext.tn()calls - β Generates JSON translation files
- β Preserves interpolation and plural forms
- β Adds necessary imports
See the Migration Tool section above for detailed usage.
If you prefer manual migration or have a unique setup:
- Export your current locale files to
assets/i18n/<locale>/core.json - Replace generated method calls with
context.t('msgid') - Keep
GlobalMaterialLocalizationsetc. if you use them - Run
shard_i18n_cli verifyto check consistency
Mixed mode is fine: Keep legacy screens on old i18n while moving new features to shard_i18n.
- Startup: Only current locale shards loaded (lazy, async)
- Locale switch: ~50-100ms for typical app (depends on shard count)
- Lookups: O(1) HashMap lookups in memory
- Interpolation: Simple regex replace
- Best practice: Keep shards <5-10k lines each
- Rich ICU message format support (
select,gender) - Dev overlay for live-editing translations in debug mode
- VS Code extension (quick-add keys, jump to definition)
- Build-time reporting for CI (missing keys diff)
- JSON schema validation for translation files
Contributions welcome! Please:
- Open an issue first to discuss major changes
- Add tests for new features
- Run
flutter testbefore submitting PR - Follow existing code style
This package uses automated publishing via GitHub Actions. See PUBLISHING.md for details on releasing new versions.
Quick release process:
- Update version in
pubspec.yamlandCHANGELOG.md - Run
./scripts/pre_publish_check.shto verify readiness - Create and push a version tag:
git tag -a v0.2.0 -m "Release 0.2.0" && git push origin v0.2.0 - GitHub Actions will automatically publish to pub.dev
MIT License - see LICENSE file for details.
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: support@moinsen.dev
Made with β€οΈ by the moinsen team