Skip to main content

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.

Version

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.

ArgumentDefaultUse
workspace and slugThe workspace handle and project slug from your project URL.
projectIdThe 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.
refreshOnResumetrueCheck for a new version when the app returns to the foreground.
refreshIntervalnoneAlso check on this interval while the app is open.
bakedAssetPathBuilderassets/easyi18n/{locale}.jsonWhere the bundled translations of a locale are.
captureTokennoneA development key that turns on auto-capture.
baseUrlhttps://api.easyi18n.comThe 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:

  1. The version downloaded while the app is running.
  2. The last version saved on the device.
  3. The translations bundled in the app.
  4. 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(),
)