Fix Flutter's "Locale data has not been initialized"
You added a second locale, someone ran the app with a German device locale, and this showed up in Crashlytics:
LocaleDataException: Locale data has not been initialized, call initializeDateFormatting(<locale>).
#0 UninitializedLocaleData._throwException (package:intl/src/intl_helpers.dart)
#1 UninitializedLocaleData.containsKey (package:intl/src/intl_helpers.dart)
#2 DateFormat.localeExists (package:intl/src/intl/date_format.dart)
#3 verifiedLocale (package:intl/src/intl_helpers.dart)
#4 new DateFormat (package:intl/src/intl/date_format.dart)
The confusing part is that AppLocalizations.of(context)!.lastSynced(date) renders a perfect German date, while DateFormat('yMMMd', 'de').format(date) three lines away throws. Same intl package, same locale, opposite outcomes.
This walks through the actual mechanism in intl 0.20.x and flutter_localizations, then the copy-paste fixes for main(), language switches, and background isolates.
The fix, if you just want to ship
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeDateFormatting();
runApp(const MyApp());
}
That covers every locale, permanently, for the main isolate. Read on if you also have background work, a language switcher, or a locale string that isn't taking effect.
The crash happens in the constructor, not in format()
Look at where DateFormat resolves its locale:
// package:intl/src/intl/date_format.dart
DateFormat([String? newPattern, String? locale])
: _locale = helpers.verifiedLocale(locale, localeExists, null)! {
addPattern(newPattern);
}
static bool localeExists(String? localeName) {
if (localeName == null) return false;
return dateTimeSymbols.containsKey(localeName);
}
Before any initialization, dateTimeSymbols is not a map — it's a sentinel:
// package:intl/src/intl_helpers.dart
class UninitializedLocaleData<F> implements MessageLookup {
bool _isFallback(String key) => canonicalizedLocale(key) == 'en_US';
bool containsKey(String key) {
if (!_isFallback(key)) {
_throwException();
}
return true;
}
F _throwException() {
throw LocaleDataException('Locale data has not been initialized, call $message.');
}
}
Two things fall out of this:
en_USis special-cased. That's why your app worked for months before you added a second language —DateFormat.yMMMd()with no locale canonicalizes toen_USand hits the built-in fallback data. The moment a real locale string arrives,containsKeythrows.- The exception fires while constructing
DateFormat, not while formatting. Sotry { ... .format(d) }around the format call won't catch it if the constructor lives in a field initializer or alate final.
Two completely separate paths load date symbols
Path 1 — flutter_localizations, from inside the widget tree
_MaterialLocalizationsDelegate.load() calls util.loadDateIntlDataIfNotLoaded():
// packages/flutter_localizations/lib/src/utils/date_localizations.dart
void loadDateIntlDataIfNotLoaded() {
if (!_dateIntlDataInitialized) {
date_localizations.dateSymbols.forEach((String locale, intl.DateSymbols symbols) {
assert(date_localizations.datePatterns.containsKey(locale));
date_symbol_data_custom.initializeDateFormattingCustom(
locale: locale,
symbols: symbols,
patterns: date_localizations.datePatterns[locale],
);
});
_dateIntlDataInitialized = true;
}
}
This registers the ~97 locales that ship inside flutter_localizations — and it only runs the first time a MaterialApp/CupertinoApp actually builds its Localizations widget with GlobalMaterialLocalizations.delegate (or the Cupertino one) in localizationsDelegates.
Path 2 — intl's own CLDR table
initializeDateFormatting from package:intl/date_symbol_data_local.dart installs the full CLDR v48 symbol and pattern maps that ship with intl 0.20.3. Nothing in Flutter calls this for you.
gen-l10n rides on Path 1 — that's the whole trick
Open your generated file. gen-l10n uses DateFormat exactly like you do:
// .dart_tool/flutter_gen/gen_l10n/app_localizations_de.dart
class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale);
@override
String lastSynced(DateTime date) {
final intl.DateFormat dateDateFormat = intl.DateFormat.yMMMd(localeName);
final String dateString = dateDateFormat.format(date);
return 'Zuletzt synchronisiert am $dateString';
}
}
It isn't privileged. It just always executes after Path 1 has run, because you can only reach it through AppLocalizations.of(context) — which means Localizations is already built, which means GlobalMaterialLocalizations.delegate.load() already fired. (The generated AppLocalizations.localizationsDelegates list includes the three Global delegates for you.)
Any DateFormat that runs off that path has nothing loaded:
- code in
main()beforerunApp - repositories, DTO parsers, notification schedulers constructed at startup
- background isolates —
Workmanager,flutter_background_service,Isolate.run,compute. Static state is per-isolate, so Path 1's_dateIntlDataInitializedflag and the symbol map do not cross over - apps that wire up only
AppLocalizations.delegateand drop the Global delegates — then even gen-l10n date placeholders throw
You do not need to re-initialize on language switch
A lot of advice says to call initializeDateFormatting('de') every time the user picks a language. With the local data source, that's a no-op. Here is the real implementation:
// package:intl/date_symbol_data_local.dart
/// This should be called for at least one [locale] before any date
/// formatting methods are called. It sets up the lookup for date
/// symbols. Both the [locale] and [ignored] parameter are ignored, as
/// the data for all locales is directly available.
Future<void> initializeDateFormatting([String? locale, String? ignored]) {
initializeDateSymbols(dateTimeSymbolMap);
initializeDatePatterns(dateTimePatternMap);
return new Future.value();
}
One call, no arguments, loads everything — and the returned future is already complete, so it's effectively synchronous. What you do want on a language switch is the default locale for un-parameterized DateFormat calls:
void applyLocale(Locale locale) {
// Locale('de', 'DE').toString() == 'de_DE'
Intl.defaultLocale = Intl.canonicalizedLocale(locale.toString());
// then rebuild / set MaterialApp.locale as usual
}
The per-locale argument only matters for the lazy sources: date_symbol_data_file.dart and date_symbol_data_http_request.dart, where each locale is fetched on demand.
Initialize inside the isolate entrypoint
import 'package:intl/intl.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:workmanager/workmanager.dart';
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((String task, Map<String, dynamic>? inputData) async {
await initializeDateFormatting();
Intl.defaultLocale = inputData?['locale'] as String? ?? 'en_US';
final String header = DateFormat.yMMMMd().format(DateTime.now());
// ...build the notification / digest
return true;
});
}
Same rule for one-shot isolates:
final List<String> labels = await Isolate.run(() async {
await initializeDateFormatting();
final DateFormat f = DateFormat.yMMMd('de_DE');
return items.map((Item i) => f.format(i.date)).toList();
});
Pass the locale string in as data. Intl.defaultLocale doesn't propagate across isolate boundaries either.
Which import to use
| Import | Use it for |
|---|---|
package:intl/date_symbol_data_local.dart |
The default. All locales, bundled, zero async work. |
package:intl/date_symbol_data_custom.dart |
Supplying your own DateSymbols — this is what flutter_localizations uses. |
package:intl/date_symbol_data_file.dart / ..._http_request.dart |
Loading per locale on demand; here the locale argument is real. |
package:intl/find_locale.dart |
findSystemLocale(), to set Intl.systemLocale from the platform. |
On that last one: import find_locale.dart, not intl_standalone.dart. find_locale.dart is a conditional export that picks intl_standalone.dart on dart:io and intl_browser.dart on the web, so the same code compiles for Flutter web:
import 'package:intl/find_locale.dart';
await findSystemLocale(); // sets Intl.systemLocale from Platform.localeName / navigator.language
Importing intl_standalone.dart directly pulls in dart:io and breaks your web build.
The trade-off with date_symbol_data_local is size: it's a single generated map literal covering every CLDR locale, so it can't be tree-shaken down to the two languages you actually ship. If binary size is critical and you only need Flutter's supported locales, the Global delegates already cover you inside the widget tree — the explicit call is what buys you correctness everywhere else.
de_DE vs de: the silent fallback that bites later
Once data is loaded, verifiedLocale tries a chain of fallbacks before giving up: the string as-is, canonicalized, language+region only, language only, then deprecated-code swaps (iw↔he, id↔in, nb↔no, fil↔tl), then a literal 'fallback' key.
So DateFormat('yMMMd', 'de_DE') quietly resolves to de. That's usually harmless, but note what flutter_localizations actually registers: it has de and de_CH but no de_DE; pt and pt_PT but no pt_BR; zh, zh_HK, zh_TW but no zh_CN. If you rely on Path 1 alone, a regional variant you carefully set up in ARB may be formatting with the base language. Loading the full CLDR table via initializeDateFormatting() gives you the regional entries that exist upstream.
And if nothing matches at all, you get a different error entirely — ArgumentError: Invalid locale "xx" — which is a strong hint you're passing a BCP-47 tag with a hyphen (de-DE) somewhere intl expected an underscore. Intl.canonicalizedLocale fixes that.
The practical rule: the locale string you hand to DateFormat should be the same tag as your ARB filename. app_de.arb produces AppLocalizationsDe with localeName == 'de'; app_pt_BR.arb produces 'pt_BR'. Keeping those tags consistent across files is exactly the kind of thing the FlutterLocalisation ARB editor is built to keep straight — along with ICU plural-category validation, which catches a dropped few/many in Polish or Arabic before it reaches a device.
table_calendar, date pickers, and other packages
Third-party widgets take a locale string and construct DateFormat with it internally:
TableCalendar<Event>(
locale: 'de_DE',
firstDay: DateTime.utc(2020, 1, 1),
lastDay: DateTime.utc(2030, 12, 31),
focusedDay: _focusedDay,
)
The widget builds fine in the debugger and throws on a device set to another language, because the header formatter is constructed with the string you passed. There is nothing to fix in the package — await initializeDateFormatting() in main() resolves it. This is the single most-reported cause of the table_calendar "locale data has not been initialized" issue.
Why it looks like a release-only crash
Nothing about symbol loading differs between debug and release. What differs is visibility: in debug, an exception thrown during build paints the red error widget and the app keeps running, so you scroll past it. In release, ErrorWidget is a bare grey box and the exception lands in your crash reporter as a hard failure. Add background work — which runs on a fresh isolate on a real device, in a real locale — and the release channel is simply where the bug gets exercised.
Checklist
await initializeDateFormatting();inmain(), afterWidgetsFlutterBinding.ensureInitialized()- The same call at the top of every
@pragma('vm:entry-point')isolate entrypoint Intl.defaultLocale = Intl.canonicalizedLocale(locale.toString());on language switch — no re-init neededGlobalMaterialLocalizations.delegatepresent inlocalizationsDelegates(useAppLocalizations.localizationsDelegates)- Locale strings use underscores and match your ARB filename tags
- Import
package:intl/find_locale.dart, neverintl_standalone.dartdirectly, if you build for web
Keep the locale tags honest
Most of these crashes trace back to one string being slightly wrong in one place. FlutterLocalisation gives you an ARB editor and translation management across every locale in your app, with ICU plural validation so a missing plural category doesn't ship. See pricing, browse more Flutter i18n guides, or try FlutterLocalisation free.