excel2l10n
A Dart tool that reads localization data from a spreadsheet (e.g. Feishu / Lark) and generates localization files for Flutter projects. Supports simple text, typed placeholders, plurals, gender/select, ordinals, and raw ICU strings — all expressed directly in a spreadsheet table.
Installation
Add to your project's dev dependencies:
dart pub add dev:excel2l10n
Quick Start
- Create
excel2l10n.yamlin your project root - Fill in your spreadsheet data following the table format
- Run:
dart run excel2l10n
Configuration (excel2l10n.yaml)
platform:
name: feishu # Currently only feishu is supported
app_id: cli_xxxxxxxxxxxxxxxx # From https://open.feishu.cn/
app_secret: xxxxxxxxxxxxxxxxxxxxxxxx # From https://open.feishu.cn/
spreadsheet_token: xxxxxxxxxxx # Token in your spreadsheet URL
target: arb # arb | getx | localizations
output_dir: output # Output directory
If the target requires extra options, use the object form:
target:
name: localizations
className: L
outputFileName: app_localizations
genExtension: false
Table Format
The spreadsheet must have the following column layout:
| key | description | en | zh | … |
|---|---|---|---|---|
| … | … | … | … | … |
keyanddescriptionare required as the first two columns.- All subsequent columns are language codes (
en,zh,ja, etc.). - Do not leave empty columns in the middle.
- Multiple sheets are supported; entries from all sheets are merged (later sheets override earlier ones on duplicate keys).
Layer 1 — Simple Text
| key | description | en | zh |
|---|---|---|---|
greeting |
Greeting on home page | Hello! | 你好! |
Layer 2 — Typed Placeholders
Use {name} for a String placeholder, or {name:type} to specify the type.
Supported types: string (default), int, double, num, datetime
| key | description | en | zh |
|---|---|---|---|
welcome |
Hello, {name} | 你好,{name} | |
fileSize |
{size:double} MB | {size:double} MB | |
countdown |
{days:int} days left | 剩余 {days:int} 天 |
Layer 3 — Plural
Use @plural(pivotVar) in the description of the parent row, then add sub-rows with key[form] keys.
Supported forms: zero, one, two, few, many, other
| key | description | en | zh |
|---|---|---|---|
itemCount |
@plural(count) |
||
itemCount[one] |
{count} item | {count} 件 | |
itemCount[other] |
{count} items | {count} 件 |
Layer 3 — Select / Gender
Use @select(pivotVar) in the description. Case names can be anything (e.g. male, female, other).
| key | description | en | zh |
|---|---|---|---|
userTitle |
@select(gender) |
||
userTitle[male] |
Mr. {name} | {name} 先生 | |
userTitle[female] |
Ms. {name} | {name} 女士 | |
userTitle[other] |
Mx. {name} | {name} |
Layer 3 — Ordinal
Use @ordinal(pivotVar). Forms follow the same set as plural.
| key | description | en | zh |
|---|---|---|---|
rankLabel |
@ordinal(position) |
||
rankLabel[one] |
{position}st | 第 {position} 名 | |
rankLabel[two] |
{position}nd | 第 {position} 名 | |
rankLabel[few] |
{position}rd | 第 {position} 名 | |
rankLabel[other] |
{position}th | 第 {position} 名 |
Layer 4 — Raw ICU Pass-through
For complex nested ICU expressions (e.g. plural inside select), mark the description as @icu and write the ICU string directly in each language cell.
| key | description | en | zh |
|---|---|---|---|
complexMsg |
@icu |
{gender, select, male{{count, plural, one{He has 1} other{He has {count}}}} other{...}} |
Adding a Description Alongside an Annotation
Any text after the annotation (separated by a newline or space) is treated as the entry's human-readable description and will be used as a doc comment in generated code.
| key | description | en | zh |
|---|---|---|---|
userTitle |
@select(gender)性别称谓 |
||
itemCount |
@plural(count)商品数量 |
Targets
arb
Generates .arb files compatible with Flutter's gen_l10n tool.
target:
name: arb
genL10nYaml: true # Also generate l10n.yaml (default: false)
Output (one file per language, e.g. app_en.arb):
{
"@@locale": "en",
"greeting": "Hello!",
"@greeting": { "description": "Greeting on home page" },
"welcome": "Hello, {name}",
"@welcome": {
"placeholders": { "name": { "type": "String" } }
},
"itemCount": "{count, plural, one{{count} item} other{{count} items}}",
"@itemCount": {
"placeholders": { "count": { "type": "num" } }
}
}
| Option | Type | Default | Description |
|---|---|---|---|
genL10nYaml |
bool | false |
Generate l10n.yaml in the project root |
getx
Generates two Dart files for use with GetX:
locales_helper.g.dart— abstract classLwithstatic constkey stringslocales.g.dart—MyLocale extends Translationswith the full translation map
Plural / Select / Ordinal entries are stored as raw ICU strings (pair with an ICU-aware library at runtime).
target: getx
localizations
Directly generates .dart files — no need for .arb files or gen_l10n. Suitable when you want full control over the generated API.
target:
name: localizations
className: L # Abstract class name (default: L)
outputFileName: app_localizations # Base file name (default: app_localizations)
genExtension: false # Generate extension skeleton files (default: false)
Generated files:
| File | Description |
|---|---|
{outputFileName}.dart |
Abstract class + LMixin with all method signatures |
{outputFileName}_{lang}.dart |
Concrete implementation for each language |
extension_{outputFileName}.dart |
Extension mixin skeleton (when genExtension: true) |
extension_{outputFileName}_{lang}.dart |
Language-specific extension mixin (when genExtension: true) |
Rich text (TextSpan) support:
For every entry that has parameters, a Span variant is generated alongside the String method, allowing use with Text.rich / RichText:
| Entry type | String method | TextSpan method |
|---|---|---|
PlaceholderItem |
String welcome(String name) |
TextSpan welcomeSpan(InlineSpan name) |
PluralItem |
String itemCount(num count) |
TextSpan itemCountSpan(num count) |
SelectItem |
String userTitle(String gender, String name) |
TextSpan userTitleSpan(String gender, InlineSpan name) |
OrdinalItem |
String rankLabel(num position) |
TextSpan rankLabelSpan(num position) |
In the Span variant, the pivot parameter (num/String) stays its original type for runtime selection logic, while additional placeholder parameters become InlineSpan so callers can inject styled widgets.
Note:
@icuentries are not supported by thelocalizationstarget. A warning is printed at generation time and the entry is skipped. Use thearbtarget for raw ICU strings.
Usage example:
// String version
Text(L.of(context).welcome('Alice'))
// TextSpan version — style the name differently
Text.rich(
L.of(context).welcomeSpan(
TextSpan(text: 'Alice', style: TextStyle(fontWeight: FontWeight.bold)),
),
)
// Plural
Text(L.of(context).itemCount(3))
// Plural with styled count
Text.rich(
L.of(context).itemCountSpan(
3,
// The num pivot renders as plain text inside the span by default
),
)
genExtension option:
When true, skeleton mixin files are generated that you can fill in with custom overrides. These files are never overwritten on subsequent runs, so your customizations are preserved.
Platform: Feishu / Lark
platform:
name: feishu
app_id: cli_xxxxxxxxxxxxxxxx
app_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
spreadsheet_token: xxxxxxxxxxxxxxxxxxxxxxxxxxx
| Field | Description |
|---|---|
app_id |
App ID from Feishu Open Platform |
app_secret |
App Secret from Feishu Open Platform |
spreadsheet_token |
Token in the spreadsheet URL: https://xxx.feishu.cn/sheets/{spreadsheet_token} |
All sheets in the spreadsheet are fetched and merged automatically.
Full Configuration Example
platform:
name: feishu
app_id: cli_xxxxxxxxxxxxxxxx
app_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
spreadsheet_token: xxxxxxxxxxxxxxxxxxxxxxxxxxx
target:
name: localizations
className: L
outputFileName: app_localizations
genExtension: true
output_dir: lib/l10n
Run with a custom config file path:
dart run excel2l10n --config path/to/config.yaml
# or
dart run excel2l10n -c path/to/config.yaml