ARB vs JSON for Flutter Localization: Which Format Should You Use?
Choosing between ARB and JSON for Flutter translations? This comparison covers syntax, tooling, features, and use cases to help you pick the right format.
Quick Comparison
| Feature | ARB | JSON |
|---|---|---|
| Official Flutter Support | ✅ Native | ⚠️ Via packages |
| Metadata Support | ✅ Built-in | ❌ No |
| Placeholders | ✅ Type-safe | ⚠️ String only |
| Pluralization | ✅ ICU format | ⚠️ Package-dependent |
| Code Generation | ✅ flutter gen-l10n | ⚠️ Package-dependent |
| IDE Support | ✅ Excellent | ✅ Excellent |
| Human Readability | ⚠️ Verbose | ✅ Simple |
| Learning Curve | Medium | Easy |
TL;DR: Use ARB for Flutter apps. Use JSON only if you need cross-platform compatibility with non-Flutter systems.
Understanding ARB Format
ARB (Application Resource Bundle) is JSON with conventions for localization metadata:
{
"@@locale": "en",
"@@last_modified": "2025-12-25T10:00:00Z",
"greeting": "Hello, {name}!",
"@greeting": {
"description": "Greeting with user's name",
"placeholders": {
"name": {
"type": "String",
"example": "John"
}
}
},
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"@itemCount": {
"description": "Item counter with pluralization",
"placeholders": {
"count": {
"type": "int"
}
}
},
"lastUpdated": "Last updated: {date}",
"@lastUpdated": {
"placeholders": {
"date": {
"type": "DateTime",
"format": "yMMMd"
}
}
}
}
ARB Advantages
1. Type-Safe Code Generation
// Generated code with type safety
abstract class AppLocalizations {
String greeting(String name); // String required
String itemCount(int count); // int required
String lastUpdated(DateTime date); // DateTime required
}
// Usage - compile-time type checking
Text(AppLocalizations.of(context)!.greeting('John'));
Text(AppLocalizations.of(context)!.itemCount(5));
Text(AppLocalizations.of(context)!.lastUpdated(DateTime.now()));
2. Built-in Metadata
{
"saveButton": "Save",
"@saveButton": {
"description": "Button to save user data",
"context": "Profile editing screen",
"placeholders": {}
}
}
Translators see context, reducing errors.
3. Native Flutter Integration
# l10n.yaml - Native configuration
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
# Native generation
flutter gen-l10n
4. ICU Message Format
{
"notifications": "{count, plural, =0{No notifications} =1{1 notification} other{{count} notifications}}",
"gender": "{gender, select, male{He liked} female{She liked} other{They liked}} your post",
"ordinal": "You finished {place, selectordinal, =1{1st} =2{2nd} =3{3rd} other{{place}th}}"
}
Understanding Plain JSON Format
Plain JSON is simpler but requires third-party packages:
{
"greeting": "Hello, {name}!",
"itemCount": "{count} items",
"lastUpdated": "Last updated: {date}",
"buttons": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete"
}
}
JSON Advantages
1. Simpler Syntax
// JSON - Clean and minimal
{
"hello": "Hello",
"goodbye": "Goodbye"
}
// ARB - More verbose
{
"hello": "Hello",
"@hello": {
"description": "Greeting"
},
"goodbye": "Goodbye",
"@goodbye": {
"description": "Farewell"
}
}
2. Nested Structure Support
{
"home": {
"title": "Home",
"welcome": "Welcome back!"
},
"settings": {
"title": "Settings",
"language": "Language",
"theme": "Theme"
}
}
ARB is flat by design:
{
"homeTitle": "Home",
"homeWelcome": "Welcome back!",
"settingsTitle": "Settings",
"settingsLanguage": "Language",
"settingsTheme": "Theme"
}
3. Cross-Platform Compatibility
JSON works with web frameworks, mobile, and backend:
// Same file works in:
// - Flutter (with easy_localization)
// - React (react-i18next)
// - Vue (vue-i18n)
// - Node.js (i18next)
4. Familiar to Non-Flutter Developers
Web developers and translators often know JSON but not ARB.
Using JSON in Flutter
Option 1: easy_localization
# pubspec.yaml
dependencies:
easy_localization: ^3.0.0
// main.dart
import 'package:easy_localization/easy_localization.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
runApp(
EasyLocalization(
supportedLocales: [Locale('en'), Locale('es')],
path: 'assets/translations',
fallbackLocale: Locale('en'),
child: MyApp(),
),
);
}
// Usage
Text('greeting'.tr(args: ['John']));
Text('itemCount'.plural(5));
Option 2: slang
# pubspec.yaml
dependencies:
slang: ^3.0.0
dev_dependencies:
slang_build_runner: ^3.0.0
build_runner: ^2.0.0
// strings_en.json
{
"greeting": "Hello, {name}!",
"items(context=count)": {
"zero": "No items",
"one": "1 item",
"other": "{count} items"
}
}
// Generated type-safe code
Text(t.greeting(name: 'John'));
Text(t.items(count: 5));
Feature Comparison
Placeholders
ARB - Type-safe with metadata:
{
"welcome": "Welcome, {firstName} {lastName}!",
"@welcome": {
"placeholders": {
"firstName": {"type": "String"},
"lastName": {"type": "String"}
}
}
}
// Generated: String welcome(String firstName, String lastName)
l10n.welcome('John', 'Doe'); // Type-checked
JSON - String interpolation:
{
"welcome": "Welcome, {firstName} {lastName}!"
}
// Runtime string replacement
'welcome'.tr(namedArgs: {'firstName': 'John', 'lastName': 'Doe'});
Pluralization
ARB - ICU format (official):
{
"messages": "{count, plural, =0{No messages} =1{1 message} other{{count} messages}}"
}
JSON with easy_localization:
{
"messages": {
"zero": "No messages",
"one": "1 message",
"other": "{} messages"
}
}
'messages'.plural(5); // "5 messages"
Number/Date Formatting
ARB - Built-in formatting:
{
"price": "Price: {amount}",
"@price": {
"placeholders": {
"amount": {
"type": "double",
"format": "currency",
"optionalParameters": {
"symbol": "$"
}
}
}
}
}
JSON - Manual formatting:
// You handle formatting yourself
final formatted = NumberFormat.currency(symbol: '\$').format(amount);
'price'.tr(args: [formatted]);
Project Structure Comparison
ARB Structure
lib/
├── l10n/
│ ├── app_en.arb
│ ├── app_es.arb
│ └── app_fr.arb
├── main.dart
l10n.yaml
JSON Structure
assets/
├── translations/
│ ├── en.json
│ ├── es.json
│ └── fr.json
lib/
├── main.dart
Migration Guide
JSON to ARB
# convert_json_to_arb.py
import json
import sys
def convert(json_file, output_file, locale):
with open(json_file, 'r') as f:
data = json.load(f)
arb = {"@@locale": locale}
def flatten(obj, prefix=''):
for key, value in obj.items():
full_key = f"{prefix}{key}" if prefix else key
if isinstance(value, dict):
flatten(value, f"{full_key}_")
else:
arb[full_key] = value
arb[f"@{full_key}"] = {"description": f"Translation for {full_key}"}
flatten(data)
with open(output_file, 'w') as f:
json.dump(arb, f, indent=2, ensure_ascii=False)
# Usage: python convert_json_to_arb.py en.json app_en.arb en
convert(sys.argv[1], sys.argv[2], sys.argv[3])
ARB to JSON
# convert_arb_to_json.py
import json
import sys
def convert(arb_file, output_file):
with open(arb_file, 'r') as f:
data = json.load(f)
# Remove metadata keys
result = {}
for key, value in data.items():
if not key.startswith('@'):
result[key] = value
with open(output_file, 'w') as f:
json.dump(result, f, indent=2, ensure_ascii=False)
convert(sys.argv[1], sys.argv[2])
When to Use Each Format
Use ARB When:
✅ Building a Flutter-only app ✅ You want type-safe generated code ✅ You need ICU message format (plurals, gender) ✅ You want official Flutter tooling support ✅ You need placeholder metadata for translators ✅ You're using professional translation services (they support ARB)
Use JSON When:
✅ Sharing translations with web/backend teams ✅ Migrating from another framework ✅ You prefer nested organization ✅ Your team is more familiar with JSON ✅ Using translation management that only exports JSON
Performance Comparison
Both formats compile to Dart code, so runtime performance is identical:
// ARB (flutter gen-l10n)
// Compiles to:
class AppLocalizationsEn extends AppLocalizations {
@override
String get hello => 'Hello';
}
// JSON (slang)
// Compiles to:
class StringsEn extends Strings {
@override
String get hello => 'Hello';
}
Build-time is also similar for typical app sizes (<10,000 strings).
Tooling Comparison
| Tool | ARB | JSON |
|---|---|---|
| VS Code Extension | ARB Editor | Built-in |
| Android Studio | Flutter Intl | Built-in |
| Lokalise | ✅ | ✅ |
| Localizely | ✅ | ✅ |
| POEditor | ✅ | ✅ |
| Crowdin | ✅ | ✅ |
| FlutterLocalisation | ✅ | ❌ |
Recommendation
For new Flutter projects: Use ARB
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
For cross-platform teams: Use JSON with slang (type-safe)
# slang.yaml
base_locale: en
fallback_strategy: base_locale
input_directory: assets/translations
output_directory: lib/gen
For simple apps: Either works, but ARB has better long-term support
Conclusion
ARB is the clear choice for Flutter-only projects:
- Official support with
flutter gen-l10n - Type-safe generated code
- Rich metadata for translators
- ICU format for complex messages
JSON makes sense when:
- Sharing translations across platforms
- Team preference for simpler syntax
- Using tools that only support JSON
Both formats ultimately compile to efficient Dart code, so the decision comes down to developer experience and tooling preferences.