alien_signals_translator 1.0.0 copy "alien_signals_translator: ^1.0.0" to clipboard
alien_signals_translator: ^1.0.0 copied to clipboard

JSON-based i18n with ICU message format for Flutter, powered by alien_signals. Same API as signals_translator.

Alien signals translator #

This is a translation package for alien_signals. It's not a pluralization package.

For my own project, I needed a lightweight translation solution that could be used in dart. I previously used the easy_localization package, but I didn't like how it made my app re-render. This is not because of the package itself, but because I'm a Signals enthusiast. This package currently fits all my needs, but it might not fit yours. Please read the (non) features section to see if this package fits your needs. And feel free to open an issue or a PR if you want to add something.

Pick your framework #

The same translator ships for three reactive frameworks, with an identical API:

Framework Package Rebuild widget
signals signals_translator SignalBuilder (signals_flutter)
alien_signals alien_signals_translator SignalBuilder (oref)
solidart solidart_translator SignalBuilder (flutter_solidart)

License #

Licensed under a MIT License.

(Non) features #

  • Supports singular and pluralization (via inline ICU format)
  • ICU-style plural and select blocks embedded directly in translation strings
  • Display app in system language by default
  • Support setting a different language
  • Supports setting system language as default
  • Only supports JSON translation files
  • Does not support dates

Getting started #

important #

  1. All the components that use translations must read them inside oref's SignalBuilder (or watch(context, ...)). oref only tracks signals it creates when you read them in a bare build(), and this package's signals come from alien_signals, so always use SignalBuilder.
  2. Text widgets cannot be constants anymore, since their Signals. Although this completely logical, it might have some impact on performance.

Steps #

  1. In root, create a folder called assets/translations/
  2. In the assets/translations/ folder, create a JSON file for each language you want to support. The name of the file should be the language code (e.g. en.json, fr.json, etc.). The content of the file should be a JSON object with key-value pairs for each translation:
{
  "language": "English",
  "translations": {
    "example translation": "example translation",
    "example translation 2": "example translation 2"
  }
}
  1. Add the following to your pubspec.yaml file:
dependencies:
  alien_signals_translator: ^1.0.0
  oref: ^2.10.0

  [...]

flutter:
  assets:
    - assets/translations/

oref requires Dart 3.13 / Flutter 3.47.

Then run flutter pub get to install the package.

  1. Add a widget that needs translating
import 'package:alien_signals_translator/alien_signals_translator.dart';
import 'package:oref/oref.dart' show SignalBuilder;

SignalBuilder(
  builder: (context) => Text(tl('example translation')),
);
  1. To set the language, you can use the loadLocale method of the SignalTranslator class. This method takes a String parameter that represents the language code (e.g. en, fr, etc.).
SignalTranslator().loadLocale('en');
  1. To view the currently set language:
SignalTranslator().currentLocale;

This is done automatically when the app starts, but if you want to build in a language selector, you can use this method to highlight the currently selected language.

If you need a concrete locale string for APIs like intl's DateFormat — which don't understand the 'sys' sentinel — use resolvedLocale instead. It returns currentLocale for explicit choices and the device locale when 'sys' is selected:

SignalTranslator().resolvedLocale;

Examples #

Basic translation (tl) #

{
  "English": "English"
}
Text(tl('English'));

Translation with a variable (tlv) #

"{0} has won the game!": "{0} has won the game!"
Text(tlv('{0} has won the game!', 'David'));

Translation with multiple variables (tlvm) #

"He came in {0}, while his partner came in at the {1} place": "He came in {0}, while his partner came in at the {1} place",
Text(tlvm('He came in {0}, while his partner came in at the {1} place', ['first', 'second']));

ICU message format #

ICU-style {N, plural, ...}, {N, selectordinal, ...} and {N, select, ...} blocks can be embedded directly inside any translation string value. They are resolved automatically by tl, tlv, and tlvm — no separate function needed. ICU is a recognized international standard. It also handles verb agreement ("There is 1 winner" vs "There are 5 winners") where the entire sentence structure changes, not just a noun.

Syntax #

{variableIndex, plural,        [offset:n] form1 {text} form2 {text} ...}
{variableIndex, selectordinal, form1 {text} form2 {text} ...}
{variableIndex, select,        value1 {text} value2 {text} other {text}}
  • variableIndex — zero-based index matching the variable passed to tlv/tlvm. Whitespace around the index and type is allowed.
  • Inside a plural or selectordinal form, # is replaced with the count (minus the offset, if any). In nested blocks, # refers to the innermost plural. Write '#' for a literal #.
  • {N} placeholders are substituted everywhere they appear, inside and outside ICU blocks. Substituted values are inserted as-is and never re-read as placeholders.
  • Multiple ICU blocks can appear in the same string, and blocks can be nested.
  • A count that isn't a number selects the other form and is shown as-is for #.
  • A malformed block (for example an unclosed {) is rendered verbatim, and a block whose variable is missing renders as empty. Both print a debug-mode warning naming the key.

Plural forms #

Key When used
=N Exact match on the count — takes priority over categories. With offset, exact matches use the count before the offset is subtracted.
zero one two few many The CLDR plural category of the count in the active locale (via package:intl). English only has one and other, so English needs =0 for a zero message; Polish uses few/many, Arabic uses all six.
other All other counts, and the fallback when no exact/category match is found.

selectordinal uses English ordinal categories (one → 1st, two → 2nd, few → 3rd, other → 4th); for other languages it always selects other.

Example: verb agreement (is / are) #

Each form holds the whole sentence, so the verb can change along with the noun.

en.json

"winner_announcement": "There {0, plural, one {is # winner} other {are # winners}}!"

nl.json

"winner_announcement": "Er {0, plural, one {is # winnaar} other {zijn # winnaars}}!"

Dart

Text(tlv('winner_announcement', '1'));  // There is 1 winner!
Text(tlv('winner_announcement', '5'));  // There are 5 winners!

Example: exact match with =0 #

Use =0 (or any =N) for an exact-count override that takes priority over the named category.

en.json

"inbox_count": "You have {0, plural, =0 {no messages} one {# message} other {# messages}} in your inbox."

Dart

Text(tlv('inbox_count', '0'));   // You have no messages in your inbox.
Text(tlv('inbox_count', '1'));   // You have 1 message in your inbox.
Text(tlv('inbox_count', '12'));  // You have 12 messages in your inbox.

Example: multiple ICU blocks in one string #

Each {N, plural, ...} block is resolved independently. Use tlvm to supply all values.

en.json

"cart_summary": "Your cart has {0, plural, =0 {no items} one {# item} other {# items}} and {1, plural, =0 {no coupons} one {# coupon} other {# coupons}} applied."

Dart

Text(tlvm('cart_summary', ['0', '0']));  // Your cart has no items and no coupons applied.
Text(tlvm('cart_summary', ['1', '0']));  // Your cart has 1 item and no coupons applied.
Text(tlvm('cart_summary', ['3', '1']));  // Your cart has 3 items and 1 coupon applied.
Text(tlvm('cart_summary', ['2', '4']));  // Your cart has 2 items and 4 coupons applied.

Example: select (gender / category) #

select chooses a form based on a string value rather than a number. Falls back to other for unknown values.

en.json

"reaction": "{0, select, female {She} male {He} other {They}} liked your post."

nl.json

"reaction": "{0, select, female {Ze} male {Hij} other {Ze}} vond je bericht leuk."

Dart

Text(tlv('reaction', 'female'));    // She liked your post.
Text(tlv('reaction', 'male'));      // He liked your post.
Text(tlv('reaction', 'nonbinary')); // They liked your post.  (falls back to "other")

Example: ICU plural combined with a regular {N} variable #

ICU blocks and normal {N} placeholders can coexist. The ICU block is resolved first, then remaining {N} tokens are substituted.

en.json

"search_results": "Found {0, plural, =0 {no results} one {# result} other {# results}} for \"{1}\"."

Dart

Text(tlvm('search_results', ['0', 'dart']));     // Found no results for "dart".
Text(tlvm('search_results', ['1', 'flutter']));  // Found 1 result for "flutter".
Text(tlvm('search_results', ['42', 'signals'])); // Found 42 results for "signals".

Example: {N} variable inside an ICU form body #

{N} placeholders inside form bodies are substituted normally after ICU resolution.

en.json

"order_line": "{0, plural, one {# item costing {1}} other {# items costing {1} each}}"

Dart

Text(tlvm('order_line', ['1', '\$9.99'])); // 1 item costing $9.99
Text(tlvm('order_line', ['3', '\$4.50'])); // 3 items costing $4.50 each

Regional locales #

You can ship region-specific translation files alongside the generic ones. Files use the canonical name <lang>_<REGION>.json:

assets/translations/
  en.json
  en_GB.json
  en_US.json
  nl.json

Then call loadLocale with the regional code:

SignalTranslator().loadLocale('en_GB');

If en_GB.json is missing, the package falls back to en.json, then to your configured fallbackLocale (and then to its bare language if regional). This means you can introduce regional files incrementally. Then you can use this otherwise in your application, for example when formatting dates per region, while still using the generic en translations for text.

Input is case- and separator-tolerant: 'en_GB', 'en-gb', and 'EN_gB' all resolve to the same canonical form, which is what gets loaded (en_GB.json) and persisted. Asset files must use the canonical name; a hyphen-named en-gb.json is not found. Non-canonical input logs a one-line debugPrint warning so typos are visible in debug builds.

Wiring from MaterialApp's Locale is a one-liner:

final code = l.countryCode == null
    ? l.languageCode
    : '${l.languageCode}_${l.countryCode}';
SignalTranslator().loadLocale(code);

Translations in another location #

By default the <locale>.json files are loaded from assets/translations/. To use translation files bundled by another package (for example one shared across several apps in a monorepo), point translationsPath at Flutter's package asset prefix right after first accessing the translator:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SignalTranslator().translationsPath =
      'packages/my_translations/assets/translations';
  runApp(const MyApp());
}

The package declares the files under flutter: assets: in its own pubspec.yaml; the app only needs to depend on it.

Development #

Publish extension using dart pub publish --dry-run

0
likes
160
points
83
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

JSON-based i18n with ICU message format for Flutter, powered by alien_signals. Same API as signals_translator.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

alien_signals, flutter, signals_translator_core

More

Packages that depend on alien_signals_translator