문서 목차
이 페이지에서목차

slang-i18n Reference

Detailed API and syntax reference for slang-based internationalization.

Detailed API and syntax reference for slang-based internationalization.

JSON Syntax Reference#

Translation files use the .i18n.json format with snake_case keys and {param} (braces) string interpolation. See slang.yaml below.

Basic Strings#

{
  "common": {
    "app_name": "Unibook",
    "ok": "확인",
    "cancel": "취소",
    "save": "저장",
    "loading": "로딩 중..."
  }
}

Nested Structure#

{
  "auth": {
    "login": {
      "title": "로그인",
      "button": "로그인하기",
      "error": "로그인에 실패했습니다"
    },
    "signup": {
      "title": "회원가입",
      "button": "가입하기"
    }
  }
}

Parameters (Placeholders)#

{
  "user": {
    "greeting": "{name}님, 안녕하세요!",
    "points": "{count} 포인트 보유",
    "join_date": "{date}에 가입"
  }
}

Dart Usage:

context.i10n.user.greeting(name: 'John')
context.i10n.user.points(count: '100')

Pluralization#

{
  "items": {
    "count(param=n)": {
      "zero": "항목 없음",
      "one": "항목 1개",
      "other": "항목 {n}개"
    }
  },
  "cart": {
    "item_count(param=count)": {
      "zero": "장바구니가 비어있습니다",
      "one": "장바구니에 상품 1개",
      "other": "장바구니에 상품 {count}개"
    }
  }
}

Dart Usage:

context.i10n.items.count(n: 0)     // "항목 없음"
context.i10n.items.count(n: 1)     // "항목 1개"
context.i10n.items.count(n: 5)     // "항목 5개"

Context-Based#

{
  "pet": {
    "type(context=PetType)": {
      "dog": "강아지",
      "cat": "고양이",
      "bird": "새",
      "other": "반려동물"
    }
  },
  "gender": {
    "pronoun(context=Gender)": {
      "male": "그",
      "female": "그녀",
      "other": "그들"
    }
  }
}

Dart Usage:

context.i10n.pet.type(context: PetType.dog)  // "강아지"

Rich Text#

{
  "terms": {
    "agreement": "이용약관에 동의합니다. <link>자세히 보기</link>"
  }
}

Dart Usage:

context.i10n.terms.agreement(
  link: (text) => TextSpan(
    text: text,
    style: TextStyle(color: Colors.blue),
    recognizer: TapGestureRecognizer()..onTap = () => openTerms(),
  ),
)

File Structure Reference#

Directory Layout#

shared/
└── i10n/                              # App translations
    └── lib/
        └── src/
            ├── json/
            │   ├── ko.i18n.json       # Base (Korean)
            │   ├── en.i18n.json       # English
            │   ├── ja.i18n.json       # Japanese
            │   ├── zh-Hans.i18n.json  # Simplified Chinese
            │   ├── de.i18n.json       # German
            │   ├── fr.i18n.json       # French
            │   ├── es.i18n.json       # Spanish
            │   ├── it.i18n.json       # Italian
            │   ├── pt.i18n.json       # Portuguese
            │   ├── ru.i18n.json       # Russian
            │   └── ar.i18n.json       # Arabic
            └── translations/                  # Generated by dart run slang
                ├── translations.g.dart        # AppLocale, LocaleSettings, TranslationProvider
                ├── translations_ko.g.dart     # Base locale
                ├── translations_en.g.dart
                └── ...                        # One file per locale

slang.yaml Configuration#

base_locale: ko
fallback_strategy: base_locale
input_directory: lib/src/json
input_file_pattern: .i18n.json
output_directory: lib/src/translations
output_file_name: translations.g.dart
key_case: snake
key_map_case: snake
param_case: snake
string_interpolation: braces
flat_map: false
timestamp: true
statistics: true
translate_var: translations

slang 4.0.0 removed the output_format key. slang 4 always writes translations.g.dart plus one translations_<locale>.g.dart per locale (zh-Hans becomes translations_zh_Hans.g.dart). If the key is still in slang.yaml, slang 4 ignores it and prints this line on every run, so do not add it: The "output_format" key is no longer supported since slang v4.

Namespace Files#

With namespaces: true, each file is named <namespace>_<locale>.i18n.json and the namespace becomes the first level of the translation tree. slang 4 (4.0.0 through 4.19.2) reads the namespace part of the file name with the pattern [a-zA-Z0-9]+, so write it in camelCase:

lib/src/json/
├── orderHistory_ko.i18n.json    # ✅ namespace orderHistory
├── orderHistory_en.i18n.json
├── order-history_ko.i18n.json   # ❌ skipped
└── order_history_ko.i18n.json   # ❌ skipped

slang skips a file whose name does not match. It prints no warning and still reports success, so the first sign is a compile error where code uses the missing keys. The Dart name of the namespace follows key_case: with key_case: snake, orderHistory_ko.i18n.json is read as order_history.


Code Usage Reference#

Basic Access#

// context.i10n extension is provided by package:core (not package:i10n)
import 'package:core/core.dart';

// BuildContext extension
Text(context.i10n.common.app_name)

// Direct access (without context) — translate_var is `translations`
final appName = AppLocale.ko.translations.common.app_name;

App Configuration#

MaterialApp(
  locale: TranslationProvider.of(context).flutterLocale,
  supportedLocales: AppLocaleUtils.supportedLocales,
  localizationsDelegates: GlobalMaterialLocalizations.delegates,
)

Locale Switching#

// Change locale
LocaleSettings.setLocale(AppLocale.en);

// Check current locale
final currentLocale = LocaleSettings.currentLocale;

// Use system locale
LocaleSettings.useDeviceLocale();

Jaspr Sites: Translate on the Server#

Checked with Jaspr 0.23.4 (static mode), slang 4.19.2 and Dart 3.13.0. The same code also passes dart analyze and build_runner build on Jaspr 0.23.5.

Jaspr compiles a @client island to JavaScript together with everything the island imports. If an island imports the generated translations, the client bundle carries the strings the island uses in every locale. Translate in server components instead, and pass the finished strings to the island as constructor parameters. Jaspr writes those parameters into the page, so each page carries only its own locale.

The translations are plain Dart here, and every locale is loaded up front, with no deferred imports:

# slang.yaml
base_locale: ko
input_directory: lib/i18n/json
input_file_pattern: .i18n.json
output_directory: lib/i18n
output_file_name: translations.g.dart
namespaces: true
flutter_integration: false # a Jaspr project has no Flutter
lazy: false # buildSync() on the Dart VM needs every locale loaded

With the default lazy: true, the other locales are deferred imports, and AppLocale.en.buildSync() on the Dart VM throws Deferred library l_en was not loaded.

lib/i18n/json/newsletter_en.i18n.json (and one file per locale):

{ "title": "Get the newsletter", "button": "Subscribe", "done": "Thanks, you are on the list" }
// lib/i18n.dart: server only. No @client file imports it.
import 'i18n/translations.g.dart';

export 'i18n/translations.g.dart' show AppLocale, Translations;

final _cache = <AppLocale, Translations>{};

/// One translation tree per locale. The server prerenders every locale in one
/// process, so it never switches the global LocaleSettings.
Translations translationsFor(AppLocale locale) =>
    _cache[locale] ??= locale.buildSync();
// lib/components/newsletter_section.dart: a server component.
import 'package:jaspr/dom.dart';
import 'package:jaspr/jaspr.dart';

import '../i18n.dart';
import 'subscribe_button.dart';

class NewsletterSection extends StatelessComponent {
  const NewsletterSection({required this.locale, super.key});

  final AppLocale locale;

  @override
  Component build(BuildContext context) {
    final t = translationsFor(locale).newsletter;
    return section([
      h2([Component.text(t.title)]),
      SubscribeButton(label: t.button, done: t.done),
    ]);
  }
}
// lib/components/subscribe_button.dart: the island. It gets finished strings.
import 'package:jaspr/dom.dart';
import 'package:jaspr/jaspr.dart';

@client
class SubscribeButton extends StatefulComponent {
  const SubscribeButton({required this.label, required this.done, super.key});

  final String label;
  final String done;

  @override
  State<SubscribeButton> createState() => _SubscribeButtonState();
}

class _SubscribeButtonState extends State<SubscribeButton> {
  bool _sent = false;

  @override
  Component build(BuildContext context) {
    if (_sent) return p([Component.text(component.done)]);
    return button(
      onClick: () => setState(() => _sent = true),
      [Component.text(component.label)],
    );
  }
}
  • Build one tree per locale on the server. Call buildSync() for each locale and keep the result. Do not call LocaleSettings.setLocale in server code: it sets one current locale for the whole process, and the server renders every locale.
  • Do not name the helper tr. package:jaspr/dom.dart exports the <tr> element as tr, so a file that imports both and calls tr(...) fails with ambiguous_import.
  • Pass only types Jaspr can encode. Island parameters must be initializing formals (this.label) of type String, int, double, num, bool, List or Map<String, …> of those, or a class with @encoder and @decoder methods. A (map) node is a Map<String, String> and can be passed as it is. An enum such as AppLocale is not supported: dart analyze accepts it, but the build fails with @client components only support parameters of primitive serializable types …. If the island needs the locale, pass locale.languageTag.

Jaspr writes the parameters as JSON in an HTML comment before each island instance. This is the English section of the prerendered page:

<!--@subscribe_button data={"label":"Subscribe","done":"Thanks, you are on the list"}-->
<button>Subscribe</button>
<!--/@subscribe_button-->

The payload is repeated for every instance, so pass only the strings the island shows. A string that appears only after hydration, like done here, exists only in that comment. Tools that read the page text but skip comments, such as a font subsetter or a copy linter, do not see it.


Bundled Font Coverage After Translation Changes#

Translation-key parity and successful Slang generation do not establish glyph coverage. One new character in an existing language can be absent from both the primary font and its bundled fallback. A Korean-only golden cannot catch that.

When a project subsets its fonts:

  1. After adding translations or changing shared fixture data, run its existing coverage gate against all locale catalogs and fixture characters, including strings shown only after interaction. Compare their codepoints with the actual bundled primary and fallback fonts, not just a declared family or manifest.
  2. If a codepoint is missing, find its source string and confirm the font's cmap and shaping result. A missing .notdef in the bundle is real evidence; a browser may conceal it with OS/network fallback, so do not claim every device shows a missing-glyph box without checking that device.
  3. Use the repository's official subset generator with its pinned source commit, blob verification and toolchain. If the installed tool version differs, select or install the reviewed version; do not relax the pin or hand-edit output hashes/coverage lists. Keep source licenses and reserved-font-name obligations.
  4. Commit the generated fonts and their manifest together. Verify coverage and source/license consistency, then inspect whether only expected codepoints were added. For existing glyphs, compare outlines and advances; preserve complex script shaping checks (for example Arabic), not just character counts.
  5. Load the actual bundled fonts in affected widget tests. Exercise the changed locale at normal and enlarged text scale, preserve the real action/state, and run affected visual comparisons. Report coverage, shaping, functional tests, golden comparison and browser checks separately; none substitutes for all others.

For repositories using cob_project_tools, the existing commands are tool/project check-locale-fallback-font and tool/project build-locale-fallback. Other projects should use their own equivalent tooling rather than inventing these commands or copying a fixed HarfBuzz version from another app.

A concrete failure: adding Chinese 刚 (U+521A) to an exchange comparison message left translation parity green but failed bundled coverage. Official regeneration added that glyph to Regular/Bold while preserving all 4,857 prior glyph outlines and metrics. The remedy was regeneration, not changing the translation to avoid its character or suppressing the gate. See the BottariFX correction and verification.


Command Reference#

Code Generation#

# Generate translation code (melos → dart run slang)
melos run generate:locale

# Force regeneration directly
cd shared/i10n && dart run slang

GPT Auto-Translation#

# Translate base locale → all locales
melos run translate:slang

# Variants
melos run translate:en           # English only
melos run translate:incremental  # only missing keys
melos run translate:full         # full re-translation
melos run translate:all          # all locales
melos run translate:fix          # fix/repair pass

# Direct execution
cd shared/i10n && dart run slang_gpt

One --target per Run (slang_gpt 0.11.x)

slang_gpt 0.11.0 through 0.11.4 keeps a single --target= value, so each --target= flag replaces the one before it. Before it sends any request, the run prints the locales it will translate on a Target: line. Check that line.

# ❌ Only the last flag applies: this translates the gdp-10 preset and skips en
dart run slang_gpt --target=en --target=gdp-10 --api-key=$OPENAI_API_KEY

# ✅ One command per target
dart run slang_gpt --target=en --api-key=$OPENAI_API_KEY
dart run slang_gpt --target=gdp-10 --api-key=$OPENAI_API_KEY

One process sends its requests one at a time. To translate many locales faster, start one process per locale (--target=de, --target=fr, …). Each process writes only the translation files of its own locale. The request logs are the exception: with --debug, or when a response is not valid JSON, slang_gpt writes _gpt_01.txt, _gpt_02.txt, … into the output directory (the input directory by default). Parallel processes overwrite each other's logs. Keep the logs out of version control, for example with a .gitignore entry for _gpt_*.txt.

Analysis#

# Check for missing translations
dart run slang analyze

# Output statistics
dart run slang analyze --stats

Troubleshooting#

Translations Not Applied#

SymptomCauseSolution
Key not found error Code not generated Run melos run generate:locale
English not displayedEnglish file missingCheck en.i18n.json
Pluralization not workingSyntax errorVerify param=n format

Code Generation Errors#

SymptomCauseSolution
JSON parsing errorInvalid JSONValidate trailing commas / quotes
Duplicate key errorSame key existsRename the key
Parameter errorWrong delimitersUse {name} (braces) format

Generated Code That Does Not Compile#

dart run slang validates the JSON, not the Dart it writes. In slang 4.0.0 and 4.19.2 the cases below print Translations generated successfully and exit 0. The error appears only when the package is analyzed or built. Run dart analyze (or flutter analyze in a Flutter package) on the translations package after every dart run slang, and do the same in CI:

dart run slang
dart analyze lib

A plain nested object inside an interface= container. slang gives every child of the container the same mixin. A child may hold strings, (map) string maps and nested (map, interface=…) containers, but not a plain nested object:

// ❌ slang writes `UnsupportedType get cta;` into the PlanCard mixin
{
  "plans(map, interface=PlanCard)": {
    "basic": { "title": "기본", "cta": { "label": "선택", "hint": "기본 요금제 선택" } },
    "pro": { "title": "프로", "cta": { "label": "선택", "hint": "프로 요금제 선택" } }
  }
}

The analyzer then reports Undefined class 'UnsupportedType'. This happens with the base locale file alone, whatever the fallback_strategy. Flatten the child or make it a string map. Both fixes compile with either fallback setting:

// ✅ Flat keys: the mixin gets `String get cta_label;` and `String get cta_hint;`
"basic": { "title": "기본", "cta_label": "선택", "cta_hint": "기본 요금제 선택" }

// ✅ String map: the mixin gets `Map<String, String> get cta;`
"basic": { "title": "기본", "cta(map)": { "label": "선택", "hint": "기본 요금제 선택" } }

Marking the child cta(singleInterface=PlanCta) depends on the fallback setting (slang 4.0.0 and 4.19.2):

  • With the default fallback_strategy: none, the mixin gets PlanCta get cta; and the package compiles.
  • With fallback_strategy: base_locale, the value in "slang.yaml Configuration" above, the other locale files fail with invalid_override. Use flat keys or a string map instead.

Modifiers that differ between locale files. A modifier such as (map, interface=PlanCard) is part of the key. If another locale file writes the key without it ("plans"), its generated class no longer matches the base class. The analyzer reports invalid_override plus extends_non_class (with fallback_strategy: base_locale) or implements_non_class (with the default none). Copy every key with its modifiers into every locale file.

GPT Translation Errors#

SymptomCauseSolution
API error Missing key Pass the key with --api-key=. slang_gpt 0.11.x does not read it from the environment
Low translation qualityLack of contextProvide hints via comments

Best Practices#

Key Naming#

// ✅ Good example: nested structure + snake_case
{
  "user": {
    "profile": {
      "edit_button": "프로필 수정",
      "save_button": "저장"
    }
  }
}

// ❌ Bad example
{
  "editProfileBtn": "프로필 수정",  // camelCase, not nested
  "user_profile_save": "저장"        // flat instead of nested
}

Pluralization Handling#

// ✅ Handle all cases
{
  "notifications": {
    "count(param=n)": {
      "zero": "알림 없음",
      "one": "알림 1개",
      "other": "알림 {n}개"
    }
  }
}

// ❌ Incomplete handling
{
  "notifications": {
    "count": "알림 {n}개"
  }
}

Parameter Notes#

JSON files do not support comments, so document parameters in the PR / key design notes. Parameter names must be identical across all locale files.

{
  "user": {
    "greeting": "{name}님, 안녕하세요!",
    "points": "{count} 포인트"
  }
}

검색어를 입력하면 문서를 찾습니다.

↑ ↓ 이동 · Enter 열기Esc 닫기