← Back to Blog

ARB vs JSON for Flutter Localization: Which Format Should You Use?

flutterarbjsoncomparisonlocalizationi18n

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.

Related Resources