Solidart translator
This is a translation package for solidart. 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
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 read them inside flutter_solidart's
SignalBuilder. - 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:
solidart_translator: ^1.0.0
flutter_solidart: ^2.7.4
[...]
flutter:
assets:
- assets/translations/
Then run flutter pub get to install the package.
- Add a widget that needs translating
import 'package:flutter_solidart/flutter_solidart.dart';
import 'package:solidart_translator/solidart_translator.dart';
SignalBuilder(
builder: (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']));
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 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)
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