rerune 0.12.0 copy "rerune: ^0.12.0" to clipboard
rerune: ^0.12.0 copied to clipboard

OTA localization updates for Flutter apps with build_runner code generation.

rerune #

OTA localization updates for Flutter apps.

ReRune layers server-provided translations on top of your generated AppLocalizations values, without changing how you call localization getters in widgets.

Remote Language Delivery #

ReRune's core OTA value is remote language delivery: add a new supported language in the ReRune dashboard and make it available to already-installed apps without shipping a new app-store build just for that language.

After the SDK fetches or loads the dashboard language bundle from cache, the locale appears in ReRune.supportedLocales. Apps can let the device locale resolve to it automatically, or build a language picker from ReRune.supportedLocales so remote languages appear next to compiled app locales.

The app still ships the generated localization API and at least one compiled fallback locale. ReRune updates values for existing keys and expands the runtime locale list from dashboard data.

Requirements #

  • Flutter >=3.22.0
  • Dart >=3.4.0

Install #

dependencies:
  rerune: ^0.12.0

dev_dependencies:
  # Optional: needed only if you use the build_runner path below.
  build_runner: ^2.4.13

1) Generate Flutter localizations #

flutter gen-l10n

2) Generate ReRune localization config #

Fast path (ReRune-only generation):

dart run rerune

Alternative (build_runner pipeline):

dart run build_runner build --delete-conflicting-outputs

Both commands generate identical *.rerune.g.dart artifacts. Parity is protected by dedicated drift-guard tests in CI.

With default Flutter l10n naming, ReRune generates:

  • lib/.../app_localizations.rerune.g.dart
  • reRuneAppLocalizationsConfig

No manual anchor class or annotation file is needed.

3) Wire ReRune.setup(...) in main() #

import 'package:flutter/widgets.dart';
import 'package:rerune/rerune.dart';

import 'l10n/gen/app_localizations.rerune.g.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await ReRune.setup(
    otaPublishId: 'your-ota-publish-id',
    localizations: reRuneAppLocalizationsConfig, // Generated by rerune/build_runner
    updatePolicy: const ReRuneUpdatePolicy(checkOnStart: true),
  );

  runApp(const MyApp());
}

4) Use ReRune delegates/locales in your app #

MaterialApp(
  localizationsDelegates: ReRune.localizationsDelegates,
  supportedLocales: ReRune.supportedLocales,
)

Use ReRune.supportedLocales anywhere you need the current runtime locale list, including custom language pickers. The list starts with compiled app locales and expands with fetched or cached dashboard languages.

Runtime APIs #

  • Setup and load cached bundles: await ReRune.setup(...)
  • Check manually: await ReRune.checkForUpdates()
  • Listen when fetched OTA message values are applied: ReRune.onFetchedTextsApplied.listen(...)
  • Rebuild subtree on any localization revision: ReRuneBuilder(builder: ...)
  • Listen to any localization revision: ReRune.localizationsRevisionListenable

ReRune does not force Flutter's root element to rebuild. Without ReRuneBuilder or an application-owned revision listener, existing widgets remain unchanged until the application rebuilds them for another reason. Generated localization getters resolve against the current in-memory OTA bundles on every invocation, so newly built widgets still receive the latest loaded values.

Diagnostic logging #

ReRune logging is disabled by default. Consumers can select a level during setup:

await ReRune.setup(
  otaPublishId: 'your-ota-publish-id',
  localizations: reRuneAppLocalizationsConfig,
  logLevel: ReRuneLogLevel.info,
);
Level Output
ReRuneLogLevel.off No diagnostics. This is the default.
ReRuneLogLevel.error High-level failures without causes, stack traces, headers, or response bodies.
ReRuneLogLevel.info Failures plus request URLs, operations, and response status codes.
ReRuneLogLevel.verbose All available diagnostics, including raw headers, response bodies, causes, and stack traces.

verbose is an explicit sensitive-data opt-in. It can expose the OTA publish ID and translated content in application logs. The lower levels never emit request headers or response bodies.

Dashboard-Only Locale Additions #

This is the remote-language delivery path for apps that want language expansion to be controlled from the dashboard. await ReRune.setup(...) loads cached manifest/ARB bundles before the first app frame, so a dashboard-only locale fetched during a previous run is available in ReRune.supportedLocales before MaterialApp resolves the device locale. If checkOnStart is enabled, the network refresh starts in the background after cached bundles are loaded; it does not block setup.

After a successful manifest/update fetch, ReRune.supportedLocales contains the app's compiled locales plus dashboard locales whose ARB bundle has been fetched or loaded from cache. The generated delegate uses the app's first compiled locale as the fallback base when Flutter's generated delegate cannot load a dashboard-only locale.

When a newly fetched manifest no longer lists a previously published locale, ReRune removes that locale's in-memory OTA bundle and persistent cache. A dashboard-only locale then disappears from ReRune.supportedLocales; a compiled locale remains supported by the app but falls back through the remaining OTA chain and bundled translations.

Missing-Key OTA Fallback #

The backend manifest declares the project's source locale through the required main_language field. The value must identify one of the manifest's locales. When a key is missing from the requested OTA locale, ReRune resolves it in this order:

requested OTA locale variants
-> OTA main_language variants
-> bundled Flutter localization

For example, an es_MX request with main_language: "en" resolves through es_MX, es, and then OTA en before using the bundled Flutter value. Lookup is per key, so an available es_MX bundle that lacks one key does not prevent that key from falling back through es and en.

Placeholder, plural, and select messages use the locale of the OTA bundle that supplied the value. An English OTA fallback therefore uses English plural rules even when the active app locale is Spanish, Polish, or Arabic.

Manifests without a valid main_language are rejected. ReRune does not infer a main language from locale ordering, compiled locales, or the active app locale.

Language Pickers #

Build language pickers from ReRune.supportedLocales rather than from the compiled AppLocalizations.supportedLocales list. That gives consumers one source of truth for:

  • locales bundled into the app at build time
  • dashboard-only locales that were fetched in this run
  • dashboard-only locales loaded from cache during startup

If a picker must update immediately after a same-run background fetch, rebuild it from ReRuneBuilder or listen to ReRune.localizationsRevisionListenable. Without that rebuild, the newly fetched language is available on the next app launch after await ReRune.setup(...) loads it from cache.

ReRune does not own the selected app locale. Keep that state in your app, for example with a small LocaleNotifier, Riverpod, Bloc, or your existing app settings model. Use ReRune.supportedLocales for picker options and pass your selected locale to MaterialApp.locale. Apps without a language picker can omit MaterialApp.locale entirely and keep Flutter's normal system-locale resolution.

class LocaleNotifier extends ValueNotifier<Locale?> {
  LocaleNotifier(super.value);
}

final LocaleNotifier localeNotifier = LocaleNotifier(null);

ReRuneBuilder(
  builder: (_) => ValueListenableBuilder<Locale?>(
    valueListenable: localeNotifier,
    builder: (_, locale, __) {
      return MaterialApp(
        locale: locale, // Optional: only for app-owned picker overrides.
        localizationsDelegates: ReRune.localizationsDelegates,
        supportedLocales: ReRune.supportedLocales,
      );
    },
  ),
)

Set the notifier from your picker. Setting it to null returns to Flutter's normal device-locale resolution.

DropdownButton<Locale?>(
  value: localeNotifier.value,
  hint: const Text('System default'),
  items: [
    const DropdownMenuItem<Locale?>(
      value: null,
      child: Text('System default'),
    ),
    ...ReRune.supportedLocales.map((locale) {
      return DropdownMenuItem<Locale?>(
        value: locale,
        child: Text(locale.toLanguageTag()),
      );
    }),
  ],
  onChanged: (locale) {
    localeNotifier.value = locale;
  },
)

Any non-null selected locale should come from ReRune.supportedLocales. A custom LocaleNotifier does not collide with ReRune because ReRune does not expose its own selected-locale setter. ReRune does not fetch unavailable languages from picker selection; dashboard-only languages appear in the list only after their ARB bundle is fetched or loaded from cache.

Startup Modes #

Use awaited setup when you want deterministic second-run locale support without waiting on the network:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await ReRune.setup(
    otaPublishId: 'your-ota-publish-id',
    localizations: reRuneAppLocalizationsConfig,
  );

  runApp(const MyApp());
}

Use the builder when you also want the app to follow the device language immediately after a same-run background fetch:

ReRuneBuilder(
  builder: (_) => MaterialApp(
    localizationsDelegates: ReRune.localizationsDelegates,
    supportedLocales: ReRune.supportedLocales,
  ),
)

Limits:

  • Without ReRuneBuilder or another rebuild trigger, existing widgets do not refresh solely because OTA content changed. Newly built widgets still resolve current OTA values for their loaded locale.
  • A dashboard-only locale fetched during the current run requires MaterialApp to rebuild before Flutter can reconsider supportedLocales and locale selection. Otherwise it is picked up on the next launch after await ReRune.setup(...) loads it from cache.
  • ReRune does not store the selected app locale. Apps with language pickers should keep that state themselves and pass it to MaterialApp.locale; null still means Flutter resolves the system language normally.
  • New localization keys or changed placeholder signatures still require running flutter gen-l10n and dart run rerune, then shipping an app update.
  • Platform/app-store language metadata and OS-level language listings are not changed by OTA localization.

Advanced And Custom Integrations #

Custom Flutter l10n class/file names #

If your app does not use app_localizations.dart / AppLocalizations, add a build.yaml in the app root:

targets:
  $default:
    builders:
      rerune|re_rune_localizations_overlay:
        generate_for:
          - lib/**/my_localizations.dart
        options:
          localizations_file_name: my_localizations.dart
          localizations_class_name: MyLocalizations

Then run:

flutter gen-l10n
dart run rerune

# or

dart run build_runner build --delete-conflicting-outputs

This generates my_localizations.rerune.g.dart and reRuneMyLocalizationsConfig.

Use it in startup:

import 'package:rerune/rerune.dart';

import 'l10n/gen/my_localizations.rerune.g.dart';

void main() {
  ReRune.setup(
    otaPublishId: 'your-ota-publish-id',
    localizations: reRuneMyLocalizationsConfig,
  );

  runApp(const MyApp());
}

Disable automatic startup fetch #

ReRune.setup(
  otaPublishId: 'your-ota-publish-id',
  localizations: reRuneAppLocalizationsConfig, // Generated by rerune/build_runner
  updatePolicy: const ReRuneUpdatePolicy(checkOnStart: false),
);

Schedule periodic refresh in hours or days #

ReRune.setup(
  otaPublishId: 'your-ota-publish-id',
  localizations: reRuneAppLocalizationsConfig,
  updatePolicy: const ReRuneUpdatePolicy(
    checkOnStart: true,
    periodicIntervalInHours: 2,
    periodicIntervalInDays: 3,
  ),
);

ReRuneUpdatePolicy only accepts whole-hour or whole-day periodic refresh intervals. If both fields are set, they are combined into a single refresh cadence. On web, if the combined interval exceeds the supported timer limit, ReRune clamps it to the maximum supported delay (24 days and 20 hours) instead of throwing.

Trigger updates from UI actions #

final result = await ReRune.checkForUpdates();
if (result.hasErrors) {
  // show error state
}

for (final locale in result.updatedLocales) {
  // Fetched OTA message values changed.
}
for (final locale in result.removedLocales) {
  // The locale is no longer published in the manifest.
}

result.hasUpdates is true when either fetched values changed or locales were unpublished. Unpublishing does not emit ReRune.onFetchedTextsApplied; it increments ReRune.localizationsRevisionListenable when effective in-memory translations or locale availability changed.

Incremental locale updates #

Locale downloads are version-gated by the manifest. The first successful download for a locale omits updated_at, receives the complete ARB, and stores the request-start time in UTC. Every manifest locale also declares a required minimum_delta_base_version, which ReRune persists independently with that locale's cache.

When the manifest value differs from the cached minimum_delta_base_version, ReRune omits updated_at, fetches the complete ARB, and replaces the cached locale. Otherwise, when the locale version differs from the successfully applied cached version, ReRune sends the stored cursor as an RFC 3339 updated_at query parameter. A new global manifest does not trigger a language request when that language's version and minimum delta base version are unchanged and its complete cached ARB remains valid. A missing cache entry produces no error. An existing cache envelope that cannot be read or decoded reports a storage error, while a valid envelope whose embedded ARB cannot be parsed reports a parse error. Both failure cases perform a full request without updated_at so valid backend content can repair the cache.

The backend must return every locale key whose update time is greater than or equal to the supplied cursor. ReRune merges that response into the complete cached ARB; incoming values override existing values, while absent keys remain unchanged. Incremental deletion is not supported. A full response replaces the complete locale, so keys absent from that response are removed.

The cursor, applied locale version, and minimum delta base version advance only after the response is parsed and cached successfully. An empty or identical delta, or an identical full replacement, advances that metadata without emitting a text-update event or triggering a localization rebuild. Cursors are derived from the device's UTC clock because locale responses do not provide a server timestamp.

Locale request failures are reported as network errors, invalid ARB or merge failures as parse errors, and failed locale cache commits as storage errors. A failed locale does not stop synchronization of the remaining published languages.

Provide your own cache store #

If you need custom storage behavior, implement ReRuneCacheStore and pass it to ReRune.setup(cacheStore: ...). Custom stores must persist all ReRuneCachedArb fields: the complete merged data, applied version, and per-locale minimumDeltaBaseVersion and updatedAt cursor. They must also implement deleteArb(...) so unpublished languages can be removed. A custom write must not expose a candidate as committed when it throws; preserve the previous complete payload until the replacement succeeds.

Custom stores must return null from readManifest() or readArb(...) only when the requested entry does not exist. If persisted state exists but cannot be read or decoded, the read must throw so ReRune can report a storage error and repair locale content through a complete request.

Troubleshooting #

  • Target of URI hasn't been generated: run flutter gen-l10n, then dart run rerune (or dart run build_runner build --delete-conflicting-outputs).
  • Undefined name reRune...Config: generated .rerune.g.dart file is missing, stale, or imported from a wrong path.
  • Changed l10n keys/signatures: rerun both generators.

Manual platform verification #

Use the Flutter SDK manual test protocol to validate a release against the real ReRune platform on physical Android and iOS devices, Chrome, and Safari.

License #

This package is proprietary software.

  • Copyright (c) 2026 BasalBit GmbH. All rights reserved.
  • Commercial license terms: https://rerune.io/terms
  • Issue tracker: https://rerune.io/issue-tracker
0
likes
0
points
1.03k
downloads

Publisher

unverified uploader

Weekly Downloads

OTA localization updates for Flutter apps with build_runner code generation.

Homepage
View/report issues

License

unknown (license)

Dependencies

analyzer, build, flutter, glob, http, intl, path_provider, shared_preferences, source_gen, yaml

More

Packages that depend on rerune