Reference
Export Formats
The langctl CLI reads and writes 5 file formats: flat JSON, nested JSON, Android strings.xml, iOS Localizable.strings and Flutter ARB. This reference covers each format, its default path, and how keys and placeholders are converted.
Supported Formats
| Format id | Aliases | Platform | Default path | Placeholders |
|---|---|---|---|---|
json (default) | flat-json, i18n-json, i18next | Web (i18next, vue-i18n, ngx-translate), React Native, Node.js | locales/{lang}.json | {{param}} |
nested-json | json-nested | Web frameworks that expect nested objects | locales/{lang}.json | {{param}} |
android | android-xml, xml | Android (Kotlin, Java) | res/{android}/strings.xml | %1$s |
ios | ios-strings, strings | iOS / macOS (Swift, Objective-C) | {lang}.lproj/Localizable.strings | %1$@ |
arb | flutter, flutter-arb | Flutter | lib/l10n/app_{lang_}.arb | {param} |
Pick the format once with langctl init (it’s stored in langctl.json), or pass it to a command with -f/--format. To list the formats from the terminal:
langctl formats Tip
langctl stores placeholders as {{name}} and converts them for each format. Write {{username}} once, and it becomes %1$s for Android, %1$@ for iOS, and {username} for Flutter.
React Native apps use the json format — there is no separate React Native format.
Path Templates
The output path (output in langctl.json, or -o) is a template with one file per language:
| Variable | Example (pt-BR) | Typical use |
|---|---|---|
{lang} | pt-BR | JSON files, iOS .lproj folders |
{lang_} | pt_BR | Flutter ARB files |
{android} | values-pt-rBR (values for the project’s default language) | Android resource folders |
Paths are relative to langctl.json. Writing several languages to one fixed file name (no variable) is an error rather than silently overwriting it.
Flat JSON (json)
The default format, and the one most web libraries load directly.
Format Specification
- File extension:
.json - Encoding: UTF-8
- Structure: Flat object, keys as written in langctl (
"home.title") - Parameters:
{{param}}, unchanged - Plurals: i18next-style
_one/_othersuffixes are ordinary keys (notifications.message_one)
Example Output
{
"auth.login": "Log In",
"auth.signup": "Sign Up",
"home.subtitle": "Get started with your projects",
"home.title": "Dashboard",
"home.welcome": "Welcome, {{username}}!",
"notifications.message_one": "You have {{count}} new message",
"notifications.message_other": "You have {{count}} new messages"
} Usage
import { useTranslation } from 'react-i18next';
function WelcomeMessage() {
const { t } = useTranslation();
return (
<div>
<h1>{t('home.welcome', { username: 'John' })}</h1>
<p>{t('home.subtitle')}</p>
</div>
);
} <div>
<h1>{{ 'home.welcome' | translate: {username: 'John'} }}</h1>
<p>{{ 'home.subtitle' | translate }}</p>
</div> Pull
langctl pull -f json -o "src/locales/{lang}.json" Nested JSON (nested-json)
Hierarchical JSON: each dot in a key becomes a level of nesting. Use it with libraries that resolve dotted keys as paths, such as vue-i18n or svelte-i18n.
- File extension:
.json - Encoding: UTF-8
- Structure: Nested objects split on
. - Parameters:
{{param}}, unchanged
{
"auth": {
"login": "Log In",
"signup": "Sign Up"
},
"home": {
"subtitle": "Get started with your projects",
"title": "Dashboard",
"welcome": "Welcome, {{username}}!"
}
} <template>
<div>
<h1>{{ $t('home.welcome', { username: 'John' }) }}</h1>
<p>{{ $t('home.subtitle') }}</p>
</div>
</template> langctl pull -f nested-json -o "src/locales/{lang}.json" Warning
A key can’t be both a string and a parent in nested JSON (for example home and home.title). The pull fails and lists the conflicting keys instead of silently dropping one; rename a key or use the flat json format.
Android XML (android)
Native Android string resources for Kotlin and Java applications.
- File extension:
.xml - Encoding: UTF-8
- Structure:
<string>elements in a<resources>root; key descriptions are written as XML comments - Key names: converted to valid resource names (
home.welcome→home_welcome) - Parameters: positional
%1$s,%2$s, …
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="auth_login">Log In</string>
<string name="home_title">Dashboard</string>
<!-- Welcome message on home screen -->
<string name="home_welcome">Welcome, %1$s!</string>
<string name="profile_info">%1$s has %2$s followers</string>
<string name="upload_progress">%1$s%% done</string>
</resources> Key Names
Any character that isn’t a letter, digit or underscore becomes _, and names starting with a digit get a leading _:
home.welcome→home_welcomeauth.login.title→auth_login_title
If two keys map to the same name (for example home.title and home_title), the pull fails and names both keys.
// Simple string
val title = getString(R.string.home_title)
// With parameter
val welcome = getString(R.string.home_welcome, username)
// With multiple parameters
val info = getString(R.string.profile_info, name, followerCount.toString()) // Simple string
String title = getString(R.string.home_title);
// With parameter
String welcome = getString(R.string.home_welcome, username); With the default res/{android}/strings.xml path, each language goes to its own resource folder:
app/src/main/res/
├── values/ # Default language (e.g. English)
│ └── strings.xml
├── values-es/ # Spanish
│ └── strings.xml
├── values-pt-rBR/ # Portuguese (Brazil)
│ └── strings.xml
└── values-de/ # German
└── strings.xml langctl pull -f android -o "app/src/main/res/{android}/strings.xml" Escaping
langctl escapes text the way Android expects:
| Character | Written as | Example |
|---|---|---|
' (apostrophe) | \' | It\'s working |
" (quote) | \" | He said \"Hi\" |
< | < | x < 5 |
> | > | x > 3 |
& | & | Tom & Jerry |
| newline | \n | Multi-line text |
% (in strings with placeholders) | %% | %1$s%% done |
Note
langctl handles escaping when it writes and unescapes when it reads (push/import). You don’t need to escape characters in the dashboard or CLI.
iOS Strings (ios)
Native iOS and macOS localization format for Swift and Objective-C.
- File extension:
.strings - Encoding: UTF-8
- Structure:
"key" = "value";pairs; key descriptions are written as/* comments */ - Key names: unchanged (
"home.welcome") - Parameters: positional
%1$@,%2$@, …
"auth.login" = "Log In";
"home.title" = "Dashboard";
/* Welcome message on home screen */
"home.welcome" = "Welcome, %1$@!";
"profile.info" = "%1$@ has %2$@ followers"; // Simple string
let title = NSLocalizedString("home.title", comment: "")
// With parameter
let welcome = String(format: NSLocalizedString("home.welcome", comment: ""), username)
// With multiple parameters
let info = String(format: NSLocalizedString("profile.info", comment: ""), name, String(followerCount)) // Simple string
NSString *title = NSLocalizedString(@"home.title", @"");
// With parameter
NSString *welcome = [NSString stringWithFormat:
NSLocalizedString(@"home.welcome", @""), username]; MyApp/
├── en.lproj/
│ └── Localizable.strings
├── es.lproj/
│ └── Localizable.strings
├── pt-BR.lproj/
│ └── Localizable.strings
└── de.lproj/
└── Localizable.strings langctl pull -f ios -o "MyApp/{lang}.lproj/Localizable.strings" Flutter ARB (arb)
Application Resource Bundle format for Flutter and Dart applications.
- File extension:
.arb - Encoding: UTF-8
- Structure: JSON with
@@localeand@keymetadata (description and placeholders) - Key names: converted to Dart identifiers (
home.welcome→homeWelcome) - Parameters:
{param}, declared inplaceholders - No timestamps: there’s no
@@last_modified, so pulls only change files when translations change
{
"@@locale": "en",
"authLogin": "Log In",
"homeTitle": "Dashboard",
"homeWelcome": "Welcome, {username}!",
"@homeWelcome": {
"description": "Welcome message on home screen",
"placeholders": {
"username": {
"type": "String"
}
}
}
} langctl converts keys to camelCase:
home.welcome→homeWelcomeauth.login.title→authLoginTitle
As with Android, two keys that map to the same id make the pull fail with both names.
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
children: [
Text(l10n.homeWelcome('John')),
Text(l10n.homeTitle),
],
);
}
} Add to pubspec.yaml:
flutter:
generate: true
dependencies:
flutter_localizations:
sdk: flutter
intl: any Create l10n.yaml:
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
nullable-getter: false langctl pull -f arb # → lib/l10n/app_en.arb, lib/l10n/app_pt_BR.arb, … Parameter Conversion
langctl stores placeholders as {{name}}:
"Welcome, {{username}}! You have {{count}} items." | Format | Output |
|---|---|
json / nested-json | Welcome, {{username}}! You have {{count}} items. |
android | Welcome, %1$s! You have %2$s items. |
ios | Welcome, %1$@! You have %2$@ items. |
arb | Welcome, {username}! You have {count} items. |
Tip
Positions follow the order in which each placeholder first appears. A placeholder used twice gets the same position every time ({{name}} … {{name}} → %1$s … %1$s), and a literal % is escaped as %% where needed.
Reading Files (push and import)
langctl push and langctl import read every format pull writes, and convert placeholders back to {{name}}:
.json— flat or nested JSON (nested objects are flattened to dotted keys).xml— Androidstrings.xml.strings— iOSLocalizable.strings.arb— Flutter ARB (simple{name}placeholders; ICU blocks such as plurals are kept as-is)
The format is detected from the file extension; pass -f to override it.
Filtering by Module
Pull or export only specific modules:
langctl export my-project -l en -f json --module auth -o ./locales/auth-en.json
langctl pull --module home -o "src/locales/home/{lang}.json" Choosing a Format
| Format | Best For | Pros | Cons |
|---|---|---|---|
json | Web apps, React Native | Most popular, works with i18next out of the box | No structure |
nested-json | vue-i18n, svelte-i18n | Organized structure | A key can’t also be a parent |
android | Native Android | Native integration | Android-only |
ios | Native iOS | Native integration | iOS-only |
arb | Flutter apps | Full Flutter gen-l10n support | Flutter-only |
- React / Next.js / Angular / React Native -> json
- Vue / Svelte -> nested-json
- Android Native -> android
- iOS Native -> ios
- Flutter -> arb Best Practices
Preview changes before writing files:
langctl pull --dry-run # show what would change
langctl pull # write the files
git diff Commit pulled files to version control — output is sorted and has no timestamps, so diffs only show real translation changes:
git add langctl.json src/locales/ app/src/main/res/values*/strings.xml Check parameter syntax matches your format:
t('message', { username: 'John' })
getString(R.string.message, username)
String(format: NSLocalizedString("message", comment: ""), username) Ensure UTF-8 encoding:
file -I locales/en.json
iconv -f ISO-8859-1 -t UTF-8 input.json > output.json Plurals Not Working
langctl stores one string per key and language and doesn’t generate plural resources. Different platforms handle plurals differently:
- i18next: Use
_one,_othersuffixes (separate keys in langctl) - Android: Use the
<plurals>element (maintained outside langctl) - iOS: Use a
.stringsdictfile (maintained outside langctl) - Flutter: Use ICU message format in ARB - langctl keeps
{count, plural, …}blocks as they are
Next Steps
- Supported Languages - Language codes and coverage
- Export Guide - Dashboard export features
- CLI Export & Import - CLI pull, push, export and import commands
- Platform Integrations - Integration guides