Flutter SDK
The easyi18n Flutter package translates the strings your app passes to
context.tr(). The source text is the key, so there is no key file to
maintain and no code generation. The package reads the translations you
publish, keeps a copy on the device, and replaces them while the app runs
when you publish again.
This page describes easyi18n 0.2.0, published on
pub.dev on September 27, 2026. It needs
Dart 3.9 and Flutter 3.24 or later.
Install
The CLI sets the package up for you. From the root of your Flutter project:
easyi18n init --workspace acme --project mobile-app --mode b
flutter pub get
init adds the dependency, registers the assets/easyi18n/ folder and tries
to wrap runApp in Easyi18nScope. Read its report: when it cannot edit your
main.dart safely it prints the snippet for you to paste.
Configure a project covers the
command.
To install by hand:
flutter pub add easyi18n
Mount the scope
Wrap MaterialApp in Easyi18nScope and name your project.
import 'package:easyi18n/easyi18n.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
Easyi18nScope(
workspace: 'acme',
slug: 'mobile-app',
child: Builder(
builder: (context) => MaterialApp(
localizationsDelegates: GlobalMaterialLocalizations.delegates,
supportedLocales: Easyi18nScope.localesOf(context),
home: const HomePage(),
),
),
);
Easyi18nScope.localesOf returns the locales you declared plus every locale
your project publishes, so a language you publish later reaches the app
without a code change. The app follows the device language. Pass locale: to
MaterialApp only if your app has its own language switch.
| Argument | Default | Use |
|---|---|---|
workspace and slug | The workspace handle and project slug from your project URL. | |
projectId | The project ID, instead of workspace and slug. Give one form, not both. | |
supportedLocales | ['en'] | The locales loaded at startup. The first one is the base locale. |
refreshOnResume | true | Check for a new version when the app returns to the foreground. |
refreshInterval | none | Also check on this interval while the app is open. |
bakedAssetPathBuilder | assets/easyi18n/{locale}.json | Where the bundled translations of a locale are. |
captureToken | none | A development key that turns on auto-capture. |
baseUrl | https://api.easyi18n.com | The origin to read from, for a local backend. |
Translate with context.tr()
Text(context.tr('Welcome back'))
context.tr reads the locale of the nearest Localizations widget, and the
widget that calls it rebuilds when a new translation arrives.
Two identical strings with different meanings need a ctx to tell them
apart. Each ctx is a separate key in your project:
context.tr('Open', ctx: 'verb')
context.tr('Open', ctx: 'adjective')
'Welcome back'.tr() works without a BuildContext. It takes the locale as
an argument, 'Welcome back'.tr(locale: 'es'), and uses the base locale when
you leave it out. A widget that calls it does not rebuild when a translation
changes, so use context.tr inside build methods.
Pass the source as a string literal. easyi18n extract reads your code
without running it, so it cannot see a variable or a string built with Dart
interpolation.
Placeholders
Write a placeholder as {name} in the source and pass its value in args:
context.tr('Welcome {name}', args: {'name': user.name})
A source with no translation yet is shown as written, with the placeholder filled in.
Plurals
A plural is an ICU message in the source string:
context.tr(
'{count, plural, one{You have {count} message} other{You have {count} messages}}',
args: {'count': n},
)
The package formats the message with the plural rules of the widget's locale and the number you pass.
A message the package cannot format, such as one with an unbalanced brace, is shown as written instead of throwing.
Where a translation comes from
For each tr() call the package looks in four places, in this order:
- The version downloaded while the app is running.
- The last version saved on the device.
- The translations bundled in the app.
- The source string itself.
The first place that has a translation for the string wins, so the app never shows a blank or a key name.
The requested locale is matched exactly first, then by language (es-MX
falls back to es), then to the base locale.
Cache and updates
When the scope mounts, it loads the bundled translations and the last saved
version, then asks the server whether a newer version is published. It asks
again each time the app returns to the foreground, and on refreshInterval
if you set one.
The check is a conditional request: an unchanged project answers 304 with
no body. When a version changed, the package downloads only the locales whose
content changed, verifies each file against its hash, saves it and rebuilds
the widgets that use it. A failed check changes nothing: the app keeps the
translations it has.
On iOS, Android and desktop the saved version lives in an easyi18n folder
inside the application support directory and survives a restart. On the web
it is kept in memory only, so each page load starts from the bundled
translations.
An update changes the text of tr() calls that are already in the app. A
tr() call you add to the code needs a new build.
Bundle translations for the first launch
Without bundled translations, the first launch shows source strings until
the first download finishes. To ship the app translated, put one file per
locale in assets/easyi18n/ and list the folder in pubspec.yaml:
flutter:
assets:
- assets/easyi18n/
easyi18n init --mode b creates the folder and registers it. From CLI
0.13.0, easyi18n pull in a project whose pubspec.yaml registers the
folder downloads the published version of every locale into it. CLI 0.12.0,
the version on pub.dev on October 11, 2026, does not write these files.
Register and publish your strings
The CLI reads the tr() calls in your code, registers the new strings,
translates them and publishes a version:
easyi18n extract
easyi18n push --dry-run
easyi18n push --yes --max-credits 50 --publish --require-complete
extract works offline and reports what it found. push --dry-run shows the
estimated cost and spends nothing. The last command registers the strings,
waits for the AI translation and publishes; the running app picks the version
up on its next check.
Push strings from the Flutter runtime SDK
explains each option, and an AI agent can run the same
steps for you.
Commit easyi18n.lock. It records the strings already registered.
Auto-capture in debug builds
With a capture token, a debug build registers every tr() source your
project does not know yet as a draft key, so it shows up in the dashboard
without a CLI run.
Easyi18nScope(
workspace: 'acme',
slug: 'mobile-app',
captureToken: const String.fromEnvironment('EASYI18N_CAPTURE_TOKEN'),
child: const MyApp(),
)
flutter run --dart-define=EASYI18N_CAPTURE_TOKEN=eik_your_dev_token
Use an API key with the capture scope only. Capture never runs in a release
build, but a key compiled into a release can still be read from the binary,
so pass it only to debug runs. Captured keys are not translated and spend no
credits until you translate them.
Run against a local backend
Easyi18nScope(
workspace: 'acme',
slug: 'mobile-app',
baseUrl: Uri.parse('http://localhost:8080'),
child: const MyApp(),
)