Skip to content
better-i18n.com
On this page

Mobile apps need to work without network. better_i18n uses a network-first strategy with a 4-tier fallback chain so your app always has translations — even on first launch in airplane mode.

How It Works #

When BetterI18nProvider initializes (or when you call context.setI18nLocale()), the SDK follows this flow:

Code
BetterI18nProvider.initialize()
│
├─ 1. Memory cache (TtlCache — static class-level)
│     └─ Hit? → Return instantly, no network call
│
├─ 2. CDN fetch (http package, timeout + retry)
│     ├─ Success → Cache in memory + write-through to storage
│     └─ Failure ↓
│
├─ 3. Persistent storage (SharedPrefsStorage)
│     ├─ Cached? → Return + populate memory cache
│     └─ No cache ↓
│
└─ 4. staticData (bundled fallback, optional)
      └─ Always available — no network or storage needed

Most apps only need tiers 1–3. staticData (tier 4) is for edge cases — see Advanced: Static Data Fallback below.

Cache Layers #

1. Memory Cache (TtlCache) #

The fastest layer. Translations are cached in a static TtlCache at the I18nCore class level — shared across all BetterI18nController instances for the same project.

  • Survives navigation and widget rebuilds
  • Cleared when the app process is killed
  • Default TTL: 5 minutes (300,000 ms) — configurable via ttl prop

2. CDN Fetch #

On memory cache miss, the SDK fetches from https://cdn.better-i18n.com/{org}/{project}/{locale}/translations.json. On success, it:

  1. Populates the memory cache
  2. Write-through to persistent storage (fire-and-forget, non-blocking)

3. Persistent Storage (SharedPrefsStorage) #

When the CDN is unreachable, the SDK falls back to shared_preferences. This covers:

  • Airplane mode
  • Flaky network connections
  • CDN downtime

Storage keys:

Code
@better-i18n:manifest:{org}/{project}          → CDN manifest JSON
@better-i18n:messages:{org}/{project}:{locale} → Translation messages JSON

Advanced: Static Data Fallback #

Bundle translations directly in your app binary as a last-resort fallback:

lib/main.dart
BetterI18nProvider(
  projectId: 'acme/app',
  defaultLocale: 'en',
  staticData: { // [!code highlight]
    'en': { // [!code highlight]
      'common': {'welcome': 'Welcome', 'appTitle': 'My App'}, // [!code highlight]
    }, // [!code highlight]
    'tr': { // [!code highlight]
      'common': {'welcome': 'Hoş geldiniz', 'appTitle': 'Uygulamam'}, // [!code highlight]
    }, // [!code highlight]
  }, // [!code highlight]
  child: const MyApp(),
)

These translations increase your app binary size and may go stale between app releases — use only when necessary.

SharedPrefsStorage #

Enable offline support by passing a SharedPrefsStorage() instance:

lib/main.dart
import 'package:better_i18n/better_i18n.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(
    BetterI18nProvider(
      projectId: 'acme/app',
      defaultLocale: 'en',
      storage: SharedPrefsStorage(), // [!code highlight]
      child: const MyApp(),
    ),
  );
}

SharedPrefsStorage wraps the shared_preferences package and implements the TranslationStorage interface. Translations and manifest data are persisted to device storage automatically.

Custom Storage #

For a custom storage backend, implement the TranslationStorage abstract class:

Dart
import 'package:better_i18n/better_i18n.dart';

class MyCustomStorage implements TranslationStorage {
  @override
  Future<String?> get(String key) async {
    // Read from your storage
    return await myDb.read(key);
  }

  @override
  Future<void> set(String key, String value) async {
    // Write to your storage
    await myDb.write(key, value);
  }

  @override
  Future<void> remove(String key) async {
    // Delete from your storage
    await myDb.delete(key);
  }
}

Then pass it to BetterI18nProvider:

Dart
BetterI18nProvider(
  projectId: 'acme/app',
  defaultLocale: 'en',
  storage: MyCustomStorage(),
  child: const MyApp(),
)

Loading & Error States #

Handle loading and error states with loadingBuilder and errorBuilder:

lib/main.dart
BetterI18nProvider(
  projectId: 'acme/app',
  defaultLocale: 'en',
  storage: SharedPrefsStorage(),
  loadingBuilder: (_) => const MaterialApp( // [!code highlight]
    home: Scaffold(
      body: Center(child: CircularProgressIndicator()),
    ),
  ),
  errorBuilder: (ctx, err) => MaterialApp( // [!code highlight]
    home: Scaffold(
      body: Center(
        child: Text('Failed to load translations: $err'),
      ),
    ),
  ),
  child: const MyApp(),
)

If neither builder is provided, BetterI18nProvider renders SizedBox.shrink() (an invisible empty widget) during loading and on error — the app appears blank until translations load.

Without Persistent Storage #

If no storage is provided, translations are cached in memory only:

  • Translations are fetched from CDN on every app launch
  • No offline support between sessions
  • Suitable for development and testing

For production apps, always pass SharedPrefsStorage() or a custom TranslationStorage implementation.

Testing #

For unit and widget tests, use MemoryStorage to avoid real file I/O:

Dart
import 'package:better_i18n/better_i18n.dart';
import 'package:flutter_test/flutter_test.dart';

testWidgets('shows translated text', (tester) async {
  // Create a controller with mock data
  final controller = BetterI18nController(
    config: const I18nConfig(
      projectId: 'test/app',
      defaultLocale: 'en',
      staticData: {
        'en': {'common': {'welcome': 'Welcome'}},
      },
    ),
  );
  await controller.initialize();

  await tester.pumpWidget(
    BetterI18nScope( // [!code highlight]
      controller: controller, // [!code highlight]
      child: Builder(
        builder: (ctx) => Text(ctx.t('common.welcome')),
      ),
    ),
  );

  expect(find.text('Welcome'), findsOneWidget);
});

See the API Reference for BetterI18nScope and I18nCore.clearAllCaches().