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.jsonformat withsnake_casekeys and{param}(braces) string interpolation. Seeslang.yamlbelow.
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_formatkey. slang 4 always writestranslations.g.dartplus onetranslations_<locale>.g.dartper locale (zh-Hansbecomestranslations_zh_Hans.g.dart). If the key is still inslang.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 callLocaleSettings.setLocalein 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.dartexports the<tr>element astr, so a file that imports both and callstr(...)fails withambiguous_import. -
Pass only types Jaspr can encode. Island parameters must be initializing formals
(
this.label) of typeString,int,double,num,bool,ListorMap<String, …>of those, or a class with@encoderand@decodermethods. A(map)node is aMap<String, String>and can be passed as it is. An enum such asAppLocaleis not supported:dart analyzeaccepts it, but the build fails with@client components only support parameters of primitive serializable types …. If the island needs the locale, passlocale.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:
- 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.
-
If a codepoint is missing, find its source string and confirm the font's cmap
and shaping result. A missing
.notdefin 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. - 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.
- 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.
- 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#
| Symptom | Cause | Solution |
|---|---|---|
| Key not found error | Code not generated | Run melos run generate:locale |
| English not displayed | English file missing | Check en.i18n.json |
| Pluralization not working | Syntax error | Verify param=n format |
Code Generation Errors#
| Symptom | Cause | Solution |
|---|---|---|
| JSON parsing error | Invalid JSON | Validate trailing commas / quotes |
| Duplicate key error | Same key exists | Rename the key |
| Parameter error | Wrong delimiters | Use {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 getsPlanCta get cta;and the package compiles. -
With
fallback_strategy: base_locale, the value in "slang.yaml Configuration" above, the other locale files fail withinvalid_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#
| Symptom | Cause | Solution |
|---|---|---|
| API error | Missing key | Pass the key with --api-key=. slang_gpt 0.11.x does not read it from the environment |
| Low translation quality | Lack of context | Provide 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} 포인트"
}
}