excel2l10n

pub package

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

  1. Create excel2l10n.yaml in your project root
  2. Fill in your spreadsheet data following the table format
  3. 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
  • key and description are 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 class L with static const key strings
  • locales.g.dartMyLocale extends Translations with 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: @icu entries are not supported by the localizations target. A warning is printed at generation time and the entry is skipped. Use the arb target 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