Until this package reaches 1.0.0, every version update could contain breaking changes. I'll try to deprecate things a few versions ahead
Signal translator
This is a translation package for the Signals framework. 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.
License
Licensed under a MIT License.
(Non) features
- Supports singular and pluralization (via inline ICU format; nested JSON keys are deprecated)
- ICU-style
pluralandselectblocks 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
- All the components that use translations must use some kind of Watch functionality as described in the Signals package.
- Text widgets cannot be constants anymore, since their Signals. Although this completely logical, it might have some impact on performance.
Steps
- In root, create a folder called
assets/translations/ - 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"
}
}
- Add the following to your
pubspec.yamlfile:
dependencies:
signals_translator: ^0.1.0
[...]
flutter:
assets:
- assets/translations/
Then run flutter pub get to install the package.
- Add a widget that needs translating
import 'package:signals/signals_flutter.dart';
import 'package:signals_translator/signals_translator.dart';
Watch(
(context) => Text(
tl('example translation'),
)
);
- To set the language, you can use the
loadLocalemethod of theSignalTranslatorclass. This method takes aStringparameter that represents the language code (e.g.en,fr, etc.).
SignalTranslator().loadLocale('en');
- 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']));
Pluralization (tlp) — deprecated
Deprecated. Use
tlvwith an ICU plural block instead (see ICU message format below).
// Before
Text(tlp('I have {0} apples', 5));
// After — flat ICU string + tlv
// "apples": "{0, plural, =0 {I have no apples} one {I have # apple} other {I have # apples}}"
Text(tlv('apples', '5'));
Pluralization with multiple counts (tlpm) — deprecated
Deprecated. Use
tlvmwith ICU plural blocks instead (see ICU message format below).
// Before
Text(tlpm('I have {0} strawberries and {1} bananas', [2, 3]));
// After — flat ICU string + tlvm
// "fruit": "{0, plural, =0 {no strawberries} one {# strawberry} other {# strawberries}} and {1, plural, =0 {no bananas} one {# banana} other {# bananas}}"
Text(tlvm('fruit', ['2', '3']));
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.
This solves problems the nested-JSON plural approach cannot, such as 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 totlv/tlvm. Whitespace around the index and type is allowed.- Inside a plural or selectordinal form,
#is replaced with the count (minus theoffset, 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
otherform 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)
The nested-JSON approach cannot express verb changes because the whole sentence structure differs. ICU handles it naturally.
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
ICU vs nested-JSON pluralization
The nested-JSON approach (
tlp/tlpm) is deprecated. Prefer ICU inline for all new translation keys. Both formats still work and can coexist in the same file during migration.tlv/tlvmalso read keys that are still in the nested-JSON format (selectingzero/one/otherthe same waytlpdoes), so you can switch a call site totlvbefore every locale file has been converted.
Nested JSON (tlp/tlpm) ⚠️ deprecated |
ICU inline (tl/tlv/tlvm) |
|
|---|---|---|
| Verb agreement | No | Yes |
Exact match (=0, =1 …) |
No | Yes |
select (gender, category) |
No | Yes |
| Multiple counts in one string | Underscore-joined keys | Natural |
| Format lives in | JSON structure | Translation string |
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. 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);
Development
Publish extension using dart pub publish --dry-run