skip to content

In a Flutter app using package:intl, why can DateFormat.yMMMd() print English dates on an Arabic phone, and how do you fix it?

level: middleimportance: should knowfreq 34%

answer

  1. the locale argument is optional
  2. Intl.defaultLocale is never set by Flutter
  3. systemLocale falls back to en_US
  4. global delegates load date symbols
  5. skeletons adapt, patterns do not

basics

~10 s

DateFormat without a locale uses Intl.defaultLocale, which Flutter never sets, so it falls back to en_US. Pass the resolved locale, such as Localizations.localeOf(context).toString(), or set Intl.defaultLocale whenever the app locale changes.

solid answer

~40 s

`DateFormat.yMMMd([String? locale])` and the other constructors fall back to `Intl.defaultLocale`, then `Intl.systemLocale`, which is `en_US` unless someone sets it; Flutter's locale resolution does not touch either. Fix it by passing `Localizations.localeOf(context).toString()`, or by assigning `Intl.defaultLocale` when the app's locale resolves and changes. Locale data matters too: pure Dart needs `initializeDateFormatting` from `package:intl/date_symbol_data_local.dart`, otherwise a non-`en_US` locale throws a `LocaleDataException`; in Flutter, the `GlobalMaterialLocalizations` and `GlobalCupertinoLocalizations` delegates initialize date data for all their locales on first load. Prefer skeletons like `yMMMd`, which reorder per locale, over fixed patterns like `dd/MM/yyyy`. In Flutter's Arabic date data, `ar` dates use Arabic-Indic digits by default, unlike `NumberFormat('ar')`.

code

dart · 11 lines
dart
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart' as intl;

extension DateText on BuildContext {
  String shortDate(DateTime value) {
    final locale = Localizations.localeOf(this).toString();
    return intl.DateFormat.yMMMd(locale).format(value);
  }
}

// Usage inside build: Text(context.shortDate(rate.updatedAt))

go deeper

for a junior

Remember to pass a locale to DateFormat, or you get en_US formatting, and prefer skeleton constructors like yMMMd.

for a middle

Explain intl's fallback chain from Intl.defaultLocale to systemLocale, how Flutter's global delegates load date data, and why pure-Dart tests need initializeDateFormatting.

for a senior

Choose between explicit locales and a global default, keep tests deterministic, and settle the native-digit behaviour for Arabic dates versus numbers.

for a principal

Standardise one formatting helper for the codebase so locale choice, digit policy and caching are decided once, not per screen.

## Where DateFormat gets its locale Every `DateFormat` constructor takes an optional `locale`. When it is omitted, `package:intl` asks for the **current locale**: 1. a locale set by `Intl.withLocale` for the current zone, if any; 2. otherwise **`Intl.defaultLocale`**; 3. otherwise **`Intl.systemLocale`**, which is `en_US` unless code sets it (in pure Dart, `findSystemLocale` from `intl_standalone.dart` or `intl_browser.dart` does that). Flutter resolves a locale for its widgets and passes it to `Localizations`, but it **does not assign `Intl.defaultLocale`**. The generated gen-l10n code and the Material localizations pass their locale explicitly, so they are unaffected; your own `DateFormat.yMMMd()` call is not. ## Two ways to fix it - **Pass the locale at every call site**: `DateFormat.yMMMd(Localizations.localeOf(context).toString())`. Explicit and test-friendly; it also follows `Localizations.override` subtrees. - **Set the global default** when the locale is known, for example in a widget near the root that reads `Localizations.localeOf(context)` in `didChangeDependencies` and assigns `Intl.defaultLocale`. Less typing at call sites, but it is global mutable state: every test must reset it, and a subtree that overrides the locale is ignored. Many teams wrap the explicit approach in a small helper or extension on `BuildContext` so call sites stay short. ## Locale data must be loaded Date formatting needs **symbols and patterns per locale** (month names, day names, skeleton patterns). intl ships them but does not load them by default: | Environment | What loads the date data | What happens without it | |---|---|---| | Pure Dart (a CLI, a server, a unit test) | `await initializeDateFormatting('ar')` from `package:intl/date_symbol_data_local.dart` | `LocaleDataException`: locale data has not been initialized, call `initializeDateFormatting(<locale>)` | | Flutter app with `flutter_localizations` | The first load of `GlobalMaterialLocalizations` or `GlobalCupertinoLocalizations` initializes every locale they support | Formatting before `runApp`, or in a test that never pumps those delegates, can hit the same exception | `en_US` always works because intl keeps it as built-in fallback data. An unknown locale string such as `'xx'` fails differently, with an `ArgumentError` about an invalid locale. ## Skeletons versus patterns - **Skeleton constructors** such as `DateFormat.yMMMd()`, `yMd()` or `MMMEd()` name the fields you want; intl looks up the **locale's own ordering and punctuation**. The same skeleton gives month-first dates in `en_US` and day-first dates in `he`. - **Explicit patterns** such as `DateFormat('dd/MM/yyyy')` print exactly that order in every locale. Use them for machine-facing text, not for people. - `add_jm()` and similar methods append time skeletons to a date skeleton. ## Digits in Arabic dates `DateFormat` uses a locale's **native digits** by default where the locale's date symbols define them. Flutter's generated date data for `ar` specifies Arabic-Indic digits, so `DateFormat.yMMMd('ar')` prints `٣` where `NumberFormat.decimalPattern('ar')` prints `3`. If the product wants one digit system: 1. set `useNativeDigits = false` on a `DateFormat` instance, or 2. call `DateFormat.useNativeDigitsByDefaultFor('ar', false)` once at startup, or 3. choose a regional locale whose number data also uses native digits, such as `ar_EG`, if Arabic-Indic digits are the goal everywhere. ## Keeping tests deterministic - In pure-Dart tests, call `initializeDateFormatting` in `setUpAll` for every locale you assert on. - If code relies on `Intl.defaultLocale`, set it in `setUp` and reset it to `null` in `tearDown`, because it is process-wide and leaks between tests. - In widget tests, pump `MaterialApp` with the real `localizationsDelegates` so the global delegates load the date data, then format with `Localizations.localeOf(context)` exactly as the app does. - Compare against strings produced by the same `DateFormat`, or strip bidi marks first; Hebrew and Arabic patterns can contain invisible direction marks. ## Out of scope but related Converting between UTC and local time is a `DateTime` concern, done before formatting; `DateFormat` formats whatever instant you hand it.

  • Why does a unit test that formats a Hebrew date throw a LocaleDataException while the app works?
    In the app, the global Material or Cupertino delegates load intl's date data when they first load. A plain unit test never pumps those delegates, so it must call `initializeDateFormatting('he')` from `date_symbol_data_local.dart` before formatting.
  • What are the downsides of setting Intl.defaultLocale instead of passing the locale?
    It is global mutable state: tests must reset it, it must be updated when the user changes language, and it ignores `Localizations.override` subtrees that render in another locale. Passing the locale is explicit and follows the widget tree.
  • Why prefer DateFormat.yMMMd() over DateFormat('MMM d, y') for users?
    The skeleton asks intl for the locale's own arrangement of year, month and day, so Hebrew and Arabic users get their conventional order and punctuation. The explicit pattern forces the English order everywhere.

saying these in an interview costs you the question

  • Flutter sets Intl.defaultLocale to the resolved app locale.
  • DateFormat('dd/MM/yyyy') adapts its order to the user's locale.
  • initializeDateFormatting is never needed anywhere in Flutter code.
  • Arabic dates and numbers from intl always use the same digits.
  • An unsupported locale silently falls back to English.