← Back to Blog

Fix the Flutter localization delegates warning for ckb

flutterlocalizationgen-l10nrtlmaterial

Fix the Flutter localization delegates warning for ckb

You added app_ckb.arb and ran flutter gen-l10n. Your own strings show up in Kurdish Sorani. Then the debug console prints this:

Warning: This application's locale, ckb, is not supported by all of its localization delegates.

In a release build, opening showDatePicker throws No MaterialLocalizations found, or the back button tooltip and dialog buttons are in the wrong language.

Your ARB files are fine. Flutter's built-in localizations only cover a fixed list of languages, and yours isn't on it. This post shows a small fallback delegate that fixes all three problems for Kurdish Sorani (ckb), Tigrinya (ti), Haitian Creole (ht) and any other locale Flutter doesn't ship.

Why the warning appears

flutter_localizations ships three delegates: GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate and GlobalWidgetsLocalizations.delegate. They hold the strings and formats for Flutter's own widgets: date pickers, the "Back" tooltip, text selection menus, OK/CANCEL labels and, importantly, the text direction.

Each of them supports a fixed set of language codes. The current list (exposed in code as kMaterialSupportedLanguages) includes ar, fa, am, fr and about 80 others. It does not include ckb, ku, ti or ht.

When your app's locale resolves to ckb, Flutter looks for a delegate of each type that returns true from isSupported(Locale('ckb')). Your generated AppLocalizations.delegate says yes. The three global delegates say no. Flutter then reports the unsupported types in that warning.

Why you only see it in debug

The check lives in _debugCheckLocalizations, which runs inside an assert. Asserts are stripped from profile and release builds, so the warning disappears there. The problem doesn't go away, though. Nothing provides MaterialLocalizations for ckb (the fallback DefaultMaterialLocalizations.delegate that MaterialApp adds only supports English), so any widget that calls MaterialLocalizations.of(context) fails with:

No MaterialLocalizations found.

The date picker, TextField selection toolbar and showAboutDialog are the usual crash sites. Text direction also falls back to left-to-right, which breaks a right-to-left script like Sorani.

The two real options

The official Flutter guide handles an unsupported language by subclassing GlobalMaterialLocalizations (its example is Norwegian Nynorsk). That means writing about 70 translated getters plus custom date patterns and symbols. If you have a translator and time, it's the most complete result.

Most teams need something smaller: the app's own strings in the new language, and system widgets that work, read in a related language and use the right text direction. A fallback delegate does that in about 20 lines.

A copy-paste fallback delegate

This generic delegate claims support for your extra language codes, then loads an existing locale's localizations from the real global delegate:

import 'package:flutter/widgets.dart';

/// Answers for locales Flutter does not ship by loading another
/// locale's localizations from [delegate].
class FallbackLocalizationsDelegate<T> extends LocalizationsDelegate<T> {
  const FallbackLocalizationsDelegate({
    required this.delegate,
    required this.fallbacks,
  });

  final LocalizationsDelegate<T> delegate;

  /// Unsupported language code -> supported locale to borrow from.
  final Map<String, Locale> fallbacks;

  @override
  bool isSupported(Locale locale) =>
      fallbacks.containsKey(locale.languageCode);

  @override
  Future<T> load(Locale locale) =>
      delegate.load(fallbacks[locale.languageCode]!);

  @override
  bool shouldReload(covariant LocalizationsDelegate<T> old) => false;
}

LocalizationsDelegate<T>.type returns T, so a FallbackLocalizationsDelegate<MaterialLocalizations> registers as a Material delegate, which is what Flutter checks.

Pick the borrowed locales

Direction comes from WidgetsLocalizations.textDirection, so the Widgets fallback for an RTL language must also be RTL. Material and Cupertino fallbacks only decide which language the system labels use.

import 'dart:ui';

// Widgets: controls text direction.
const widgetsFallbacks = {
  'ckb': Locale('ar'), // Sorani is RTL
  'ti': Locale('am'),  // Ge'ez script, LTR
  'ht': Locale('fr'),
};

// Material and Cupertino: controls system labels and date picker text.
const materialFallbacks = {
  'ckb': Locale('ar'), // or Locale('en') if you prefer English labels
  'ti': Locale('am'),
  'ht': Locale('fr'),
};

If your Sorani users would rather see English "OK" and "Cancel" than Arabic, map Material and Cupertino to en and keep Widgets on ar. The layout stays RTL either way.

Wire it into MaterialApp

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

MaterialApp(
  localizationsDelegates: const [
    FallbackLocalizationsDelegate<MaterialLocalizations>(
      delegate: GlobalMaterialLocalizations.delegate,
      fallbacks: materialFallbacks,
    ),
    FallbackLocalizationsDelegate<CupertinoLocalizations>(
      delegate: GlobalCupertinoLocalizations.delegate,
      fallbacks: materialFallbacks,
    ),
    FallbackLocalizationsDelegate<WidgetsLocalizations>(
      delegate: GlobalWidgetsLocalizations.delegate,
      fallbacks: widgetsFallbacks,
    ),
    ...AppLocalizations.localizationsDelegates,
  ],
  supportedLocales: AppLocalizations.supportedLocales,
  home: const HomePage(),
);

(Import flutter_localizations in the file too, and adjust the AppLocalizations import if you generate into your own package with synthetic-package: false, which newer Flutter versions use by default.)

For each type, Flutter uses the first delegate that supports the locale. The fallbacks come first and only claim ckb, ti and ht, so every other locale still goes to the real global delegates. AppLocalizations.delegate keeps serving your ARB strings in the actual new language, because gen-l10n generated a ckb class from app_ckb.arb.

Restart the app with the device or locale: set to Locale('ckb'). The warning is gone, showDatePicker opens, and the layout mirrors right to left.

Watch out for date and number placeholders

The fallback fixes Flutter's widgets, but your own ARB messages still format with localeName = 'ckb'. Plurals are safe: intl falls back to a default plural rule for locales it has no rule data for. Placeholders typed DateTime with a format, or num with a NumberFormat, are riskier. If intl has no date or number data for that locale, DateFormat and NumberFormat can throw an ArgumentError for an invalid locale.

Test every formatted placeholder in the new locale. If one fails, pass a pre-formatted String placeholder and format it in Dart with the same fallback locale you chose above.

Keep app_ckb.arb in sync with app_en.arb

The fallback hides missing system strings, and gen-l10n quietly falls back to the template language for missing ARB keys. That makes untranslated keys easy to miss. Let the tool tell you:

# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
untranslated-messages-file: build/untranslated.json

After flutter gen-l10n, build/untranslated.json lists every key per locale that has no translation, for example {"ckb": ["checkoutTitle", "itemsInCart"]}.

For CI, a short Dart script that fails the build on drift catches both missing and stale keys:

// tool/check_arb.dart
import 'dart:convert';
import 'dart:io';

Set<String> keys(String path) =>
    (jsonDecode(File(path).readAsStringSync()) as Map<String, dynamic>)
        .keys
        .where((k) => !k.startsWith('@'))
        .toSet();

void main(List<String> args) {
  final locale = args.isEmpty ? 'ckb' : args.first;
  final en = keys('lib/l10n/app_en.arb');
  final other = keys('lib/l10n/app_$locale.arb');

  final missing = en.difference(other);
  final stale = other.difference(en);

  if (missing.isNotEmpty) print('Missing in $locale: $missing');
  if (stale.isNotEmpty) print('Not in app_en.arb: $stale');
  if (missing.isNotEmpty || stale.isNotEmpty) exit(1);
}

Run it with dart run tool/check_arb.dart ckb.

If hand-editing raw ARB JSON for a right-to-left language gets painful (mixed-direction strings in a code editor are hard to read), the FlutterLocalisation ARB editor lets you edit app_ckb.arb next to app_en.arb in a UI and manage translations across all your locales. It also validates ICU plural syntax, so a plural message missing a category the language needs gets flagged before release.

Quick checklist

  • Add app_ckb.arb (or app_ti.arb, app_ht.arb) and run flutter gen-l10n.
  • Add the three FallbackLocalizationsDelegate entries before the global delegates.
  • Use an RTL locale for the Widgets fallback when the language is RTL.
  • Test showDatePicker, a TextField selection menu and any formatted placeholders in a release build, since the debug warning won't appear there.
  • On iOS, list the new language under CFBundleLocalizations in Info.plist so the system offers it.
  • Turn on untranslated-messages-file and add the key check to CI.

More guides on gen-l10n, plurals and RTL are on the FlutterLocalisation blog.

Try FlutterLocalisation free

Adding a language Flutter doesn't cover means one more ARB file to keep in step with the rest. Try FlutterLocalisation free to edit and manage your app_<locale>.arb files in one place, or see the pricing when your team grows.