Cupertino localization step 6: add a GlobalCupertinoLocalizations base class with date time formatting (#29767)

diff --git a/packages/flutter_localizations/lib/src/cupertino_localizations.dart b/packages/flutter_localizations/lib/src/cupertino_localizations.dart
new file mode 100644
index 0000000..557a934
--- /dev/null
+++ b/packages/flutter_localizations/lib/src/cupertino_localizations.dart
@@ -0,0 +1,439 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/foundation.dart';
+import 'package:intl/intl.dart' as intl;
+import 'package:intl/date_symbols.dart' as intl;
+
+import 'utils/date_localizations.dart' as util;
+import 'widgets_localizations.dart';
+
+/// Implementation of localized strings for Cupertino widgets using the `intl`
+/// package for date and time formatting.
+///
+/// Further localization of strings beyond date time formatting are provided
+/// by language specific subclasses of [GlobalCupertinoLocalizations]
+///
+/// See also:
+///
+///  * [DefaultCupertinoLocalizations], which provides US English localizations
+///    for Cupertino widgets.
+abstract class GlobalCupertinoLocalizations implements CupertinoLocalizations {
+  /// Initializes an object that defines the Cupertino widgets' localized
+  /// strings for the given `localeName`.
+  ///
+  /// The remaining '*Format' arguments uses the intl package to provide
+  /// [DateFormat] configurations for the `localeName`.
+  const GlobalCupertinoLocalizations({
+    @required String localeName,
+    @required intl.DateFormat fullYearFormat,
+    @required intl.DateFormat dayFormat,
+    @required intl.DateFormat mediumDateFormat,
+    @required intl.DateFormat singleDigitHourFormat,
+    @required intl.DateFormat singleDigitMinuteFormat,
+    @required intl.DateFormat doubleDigitMinuteFormat,
+    @required intl.DateFormat singleDigitSecondFormat,
+    @required intl.NumberFormat decimalFormat,
+  }) : assert(localeName != null),
+       _localeName = localeName,
+       assert(fullYearFormat != null),
+       _fullYearFormat = fullYearFormat,
+       assert(dayFormat != null),
+       _dayFormat = dayFormat,
+       assert(mediumDateFormat != null),
+       _mediumDateFormat = mediumDateFormat,
+       assert(singleDigitHourFormat != null),
+       _singleDigitHourFormat = singleDigitHourFormat,
+       assert(singleDigitMinuteFormat != null),
+       _singleDigitMinuteFormat = singleDigitMinuteFormat,
+       assert(doubleDigitMinuteFormat != null),
+       _doubleDigitMinuteFormat = doubleDigitMinuteFormat,
+       assert(singleDigitSecondFormat != null),
+       _singleDigitSecondFormat = singleDigitSecondFormat,
+       assert(decimalFormat != null),
+       _decimalFormat =decimalFormat;
+
+  final String _localeName;
+  final intl.DateFormat _fullYearFormat;
+  final intl.DateFormat _dayFormat;
+  final intl.DateFormat _mediumDateFormat;
+  final intl.DateFormat _singleDigitHourFormat;
+  final intl.DateFormat _singleDigitMinuteFormat;
+  final intl.DateFormat _doubleDigitMinuteFormat;
+  final intl.DateFormat _singleDigitSecondFormat;
+  final intl.NumberFormat _decimalFormat;
+
+  @override
+  String datePickerYear(int yearIndex) {
+    return _fullYearFormat.format(DateTime.utc(yearIndex));
+  }
+
+  @override
+  String datePickerMonth(int monthIndex) {
+    // It doesn't actually have anything to do with _fullYearFormat. It's just
+    // taking advantage of the fact that _fullYearFormat loaded the needed
+    // locale's symbols.
+    return _fullYearFormat.dateSymbols.MONTHS[monthIndex - 1];
+  }
+
+  @override
+  String datePickerDayOfMonth(int dayIndex) {
+    // Year and month doesn't matter since we just want to day formatted.
+    return _dayFormat.format(DateTime.utc(0, 0, dayIndex));
+  }
+
+  @override
+  String datePickerMediumDate(DateTime date) {
+    return _mediumDateFormat.format(date);
+  }
+
+  @override
+  String datePickerHour(int hour) {
+    return _singleDigitHourFormat.format(DateTime.utc(0, 0, 0, hour));
+  }
+
+  @override
+  String datePickerMinute(int minute) {
+    return _doubleDigitMinuteFormat.format(DateTime.utc(0, 0, 0, 0, minute));
+  }
+
+  /// Subclasses should provide the optional zero pluralization of [datePickerHourSemanticsLabel] based on the ARB file.
+  @protected String get datePickerHourSemanticsLabelZero => null;
+  /// Subclasses should provide the optional one pluralization of [datePickerHourSemanticsLabel] based on the ARB file.
+  @protected String get datePickerHourSemanticsLabelOne => null;
+  /// Subclasses should provide the optional two pluralization of [datePickerHourSemanticsLabel] based on the ARB file.
+  @protected String get datePickerHourSemanticsLabelTwo => null;
+  /// Subclasses should provide the optional few pluralization of [datePickerHourSemanticsLabel] based on the ARB file.
+  @protected String get datePickerHourSemanticsLabelFew => null;
+  /// Subclasses should provide the optional many pluralization of [datePickerHourSemanticsLabel] based on the ARB file.
+  @protected String get datePickerHourSemanticsLabelMany => null;
+  /// Subclasses should provide the required other pluralization of [datePickerHourSemanticsLabel] based on the ARB file.
+  @protected String get datePickerHourSemanticsLabelOther;
+
+  @override
+  String datePickerHourSemanticsLabel(int hour) {
+    return intl.Intl.pluralLogic(
+      hour,
+      zero: datePickerHourSemanticsLabelZero,
+      one: datePickerHourSemanticsLabelOne,
+      two: datePickerHourSemanticsLabelTwo,
+      few: datePickerHourSemanticsLabelFew,
+      many: datePickerHourSemanticsLabelMany,
+      other: datePickerHourSemanticsLabelOther,
+      locale: _localeName,
+    ).replaceFirst(r'$hour', _decimalFormat.format(hour));
+  }
+
+  /// Subclasses should provide the optional zero pluralization of [datePickerMinuteSemanticsLabel] based on the ARB file.
+  @protected String get datePickerMinuteSemanticsLabelZero => null;
+  /// Subclasses should provide the optional one pluralization of [datePickerMinuteSemanticsLabel] based on the ARB file.
+  @protected String get datePickerMinuteSemanticsLabelOne => null;
+  /// Subclasses should provide the optional two pluralization of [datePickerMinuteSemanticsLabel] based on the ARB file.
+  @protected String get datePickerMinuteSemanticsLabelTwo => null;
+  /// Subclasses should provide the optional few pluralization of [datePickerMinuteSemanticsLabel] based on the ARB file.
+  @protected String get datePickerMinuteSemanticsLabelFew => null;
+  /// Subclasses should provide the optional many pluralization of [datePickerMinuteSemanticsLabel] based on the ARB file.
+  @protected String get datePickerMinuteSemanticsLabelMany => null;
+  /// Subclasses should provide the required other pluralization of [datePickerMinuteSemanticsLabel] based on the ARB file.
+  @protected String get datePickerMinuteSemanticsLabelOther;
+
+  @override
+  String datePickerMinuteSemanticsLabel(int minute) {
+    return intl.Intl.pluralLogic(
+      minute,
+      zero: datePickerMinuteSemanticsLabelZero,
+      one: datePickerMinuteSemanticsLabelOne,
+      two: datePickerMinuteSemanticsLabelTwo,
+      few: datePickerMinuteSemanticsLabelFew,
+      many: datePickerMinuteSemanticsLabelMany,
+      other: datePickerMinuteSemanticsLabelOther,
+      locale: _localeName,
+    ).replaceFirst(r'$minute', _decimalFormat.format(minute));
+  }
+
+  /// A string describing the [DatePickerDateOrder] enum value.
+  ///
+  /// Subclasses should provide this string value based on the ARB file for
+  /// the locale.
+  ///
+  /// See also:
+  ///
+  ///  * [datePickerDateOrder], which provides the [DatePickerDateOrder]
+  ///    enum value for [CupertinoLocalizations] based on this string value
+  @protected
+  String get datePickerDateOrderString;
+
+  @override
+  DatePickerDateOrder get datePickerDateOrder {
+    switch (datePickerDateOrderString) {
+      case 'dmy':
+        return DatePickerDateOrder.dmy;
+      case 'mdy':
+        return DatePickerDateOrder.mdy;
+      case 'ymd':
+        return DatePickerDateOrder.ymd;
+      case 'ydm':
+        return DatePickerDateOrder.ydm;
+      default:
+        assert(
+          false,
+          'Failed to load DatePickerDateOrder $datePickerDateOrderString for '
+          'locale $_localeName.\nNon conforming string for $_localeName\'s '
+          '.arb file',
+        );
+        return null;
+    }
+  }
+
+  /// A string describing the [DatePickerDateTimeOrder] enum value.
+  ///
+  /// Subclasses should provide this string value based on the ARB file for
+  /// the locale.
+  ///
+  /// See also:
+  ///
+  ///  * [datePickerDateTimeOrder], which provides the [DatePickerDateTimeOrder]
+  ///    enum value for [CupertinoLocalizations] based on this string value.
+  @protected
+  String get datePickerDateTimeOrderString;
+
+  @override
+  DatePickerDateTimeOrder get datePickerDateTimeOrder {
+    switch (datePickerDateTimeOrderString) {
+      case 'date_time_dayPeriod':
+        return DatePickerDateTimeOrder.date_time_dayPeriod;
+      case 'date_dayPeriod_time':
+        return DatePickerDateTimeOrder.date_dayPeriod_time;
+      case 'time_dayPeriod_date':
+        return DatePickerDateTimeOrder.time_dayPeriod_date;
+      case 'dayPeriod_time_date':
+        return DatePickerDateTimeOrder.dayPeriod_time_date;
+      default:
+        assert(
+          false,
+          'Failed to load DatePickerDateTimeOrder $datePickerDateTimeOrderString '
+          'for locale $_localeName.\nNon conforming string for $_localeName\'s '
+          '.arb file',
+        );
+        return null;
+    }
+  }
+
+  @override
+  String timerPickerHour(int hour) {
+    return _singleDigitHourFormat.format(DateTime.utc(0, 0, 0, hour));
+  }
+
+  @override
+  String timerPickerMinute(int minute) {
+    return _singleDigitMinuteFormat.format(DateTime.utc(0, 0, 0, 0, minute));
+  }
+
+  @override
+  String timerPickerSecond(int second) {
+    return _singleDigitSecondFormat.format(DateTime.utc(0, 0, 0, 0, 0, second));
+  }
+
+  /// Subclasses should provide the optional zero pluralization of [timerPickerHourLabel] based on the ARB file.
+  @protected String get timerPickerHourLabelZero => null;
+  /// Subclasses should provide the optional one pluralization of [timerPickerHourLabel] based on the ARB file.
+  @protected String get timerPickerHourLabelOne => null;
+  /// Subclasses should provide the optional two pluralization of [timerPickerHourLabel] based on the ARB file.
+  @protected String get timerPickerHourLabelTwo => null;
+  /// Subclasses should provide the optional few pluralization of [timerPickerHourLabel] based on the ARB file.
+  @protected String get timerPickerHourLabelFew => null;
+  /// Subclasses should provide the optional many pluralization of [timerPickerHourLabel] based on the ARB file.
+  @protected String get timerPickerHourLabelMany => null;
+  /// Subclasses should provide the required other pluralization of [timerPickerHourLabel] based on the ARB file.
+  @protected String get timerPickerHourLabelOther;
+
+  @override
+  String timerPickerHourLabel(int hour) {
+    return intl.Intl.pluralLogic(
+      hour,
+      zero: timerPickerHourLabelZero,
+      one: timerPickerHourLabelOne,
+      two: timerPickerHourLabelTwo,
+      few: timerPickerHourLabelFew,
+      many: timerPickerHourLabelMany,
+      other: timerPickerHourLabelOther,
+      locale: _localeName,
+    ).replaceFirst(r'$hour', _decimalFormat.format(hour));
+  }
+
+  /// Subclasses should provide the optional zero pluralization of [timerPickerMinuteLabel] based on the ARB file.
+  @protected String get timerPickerMinuteLabelZero => null;
+  /// Subclasses should provide the optional one pluralization of [timerPickerMinuteLabel] based on the ARB file.
+  @protected String get timerPickerMinuteLabelOne => null;
+  /// Subclasses should provide the optional two pluralization of [timerPickerMinuteLabel] based on the ARB file.
+  @protected String get timerPickerMinuteLabelTwo => null;
+  /// Subclasses should provide the optional few pluralization of [timerPickerMinuteLabel] based on the ARB file.
+  @protected String get timerPickerMinuteLabelFew => null;
+  /// Subclasses should provide the optional many pluralization of [timerPickerMinuteLabel] based on the ARB file.
+  @protected String get timerPickerMinuteLabelMany => null;
+  /// Subclasses should provide the required other pluralization of [timerPickerMinuteLabel] based on the ARB file.
+  @protected String get timerPickerMinuteLabelOther;
+
+  @override
+  String timerPickerMinuteLabel(int minute) {
+    return intl.Intl.pluralLogic(
+      minute,
+      zero: timerPickerMinuteLabelZero,
+      one: timerPickerMinuteLabelOne,
+      two: timerPickerMinuteLabelTwo,
+      few: timerPickerMinuteLabelFew,
+      many: timerPickerMinuteLabelMany,
+      other: timerPickerMinuteLabelOther,
+      locale: _localeName,
+    ).replaceFirst(r'$minute', _decimalFormat.format(minute));
+  }
+
+  /// Subclasses should provide the optional zero pluralization of [timerPickerSecondLabel] based on the ARB file.
+  @protected String get timerPickerSecondLabelZero => null;
+  /// Subclasses should provide the optional one pluralization of [timerPickerSecondLabel] based on the ARB file.
+  @protected String get timerPickerSecondLabelOne => null;
+  /// Subclasses should provide the optional two pluralization of [timerPickerSecondLabel] based on the ARB file.
+  @protected String get timerPickerSecondLabelTwo => null;
+  /// Subclasses should provide the optional few pluralization of [timerPickerSecondLabel] based on the ARB file.
+  @protected String get timerPickerSecondLabelFew => null;
+  /// Subclasses should provide the optional many pluralization of [timerPickerSecondLabel] based on the ARB file.
+  @protected String get timerPickerSecondLabelMany => null;
+  /// Subclasses should provide the required other pluralization of [timerPickerSecondLabel] based on the ARB file.
+  @protected String get timerPickerSecondLabelOther;
+
+  @override
+  String timerPickerSecondLabel(int second) {
+    return intl.Intl.pluralLogic(
+      second,
+      zero: timerPickerSecondLabelZero,
+      one: timerPickerSecondLabelOne,
+      two: timerPickerSecondLabelTwo,
+      few: timerPickerSecondLabelFew,
+      many: timerPickerSecondLabelMany,
+      other: timerPickerSecondLabelOther,
+      locale: _localeName,
+    ).replaceFirst(r'$second', _decimalFormat.format(second));
+  }
+
+  /// A [LocalizationsDelegate] that uses [GlobalCupertinoLocalizations.load]
+  /// to create an instance of this class.
+  ///
+  /// Most internationalized apps will use [GlobalCupertinoLocalizations.delegates]
+  /// as the value of [CupertinoApp.localizationsDelegates] to include
+  /// the localizations for both the cupertino and widget libraries.
+  static const LocalizationsDelegate<CupertinoLocalizations> delegate = _GlobalCupertinoLocalizationsDelegate();
+
+  /// A value for [CupertinoApp.localizationsDelegates] that's typically used by
+  /// internationalized apps.
+  ///
+  /// ## Sample code
+  ///
+  /// To include the localizations provided by this class and by
+  /// [GlobalWidgetsLocalizations] in a [CupertinoApp],
+  /// use [GlobalCupertinoLocalizations.delegates] as the value of
+  /// [CupertinoApp.localizationsDelegates], and specify the locales your
+  /// app supports with [CupertinoApp.supportedLocales]:
+  ///
+  /// ```dart
+  /// new CupertinoApp(
+  ///   localizationsDelegates: GlobalCupertinoLocalizations.delegates,
+  ///   supportedLocales: [
+  ///     const Locale('en', 'US'), // English
+  ///     const Locale('he', 'IL'), // Hebrew
+  ///   ],
+  ///   // ...
+  /// )
+  /// ```
+  static const List<LocalizationsDelegate<dynamic>> delegates = <LocalizationsDelegate<dynamic>>[
+    GlobalCupertinoLocalizations.delegate,
+    GlobalWidgetsLocalizations.delegate,
+  ];
+}
+
+class _GlobalCupertinoLocalizationsDelegate extends LocalizationsDelegate<CupertinoLocalizations> {
+  const _GlobalCupertinoLocalizationsDelegate();
+
+  @override
+  bool isSupported(Locale locale) => false; // TODO(xster): implement.
+
+  static final Map<Locale, Future<CupertinoLocalizations>> _loadedTranslations = <Locale, Future<CupertinoLocalizations>>{};
+
+  @override
+  Future<CupertinoLocalizations> load(Locale locale) {
+    assert(isSupported(locale)); // TODO(xster): implement.
+    return _loadedTranslations.putIfAbsent(locale, () {
+      util.loadDateIntlDataIfNotLoaded();
+
+      final String localeName = intl.Intl.canonicalizedLocale(locale.toString());
+      assert(
+        locale.toString() == localeName,
+        'Flutter does not support the non-standard locale form $locale (which '
+        'might be $localeName',
+      );
+
+      intl.DateFormat fullYearFormat;
+      intl.DateFormat dayFormat;
+      intl.DateFormat mediumDateFormat;
+      // We don't want any additional decoration here. The am/pm is handled in
+      // the date picker. We just want an hour number localized.
+      intl.DateFormat singleDigitHourFormat;
+      intl.DateFormat singleDigitMinuteFormat;
+      intl.DateFormat doubleDigitMinuteFormat;
+      intl.DateFormat singleDigitSecondFormat;
+      intl.NumberFormat decimalFormat;
+
+      void loadFormats(String locale) {
+        fullYearFormat = intl.DateFormat.y(locale);
+        dayFormat = intl.DateFormat.d(locale);
+        mediumDateFormat = intl.DateFormat.MMMEd(locale);
+        // TODO(xster): fix when https://github.com/dart-lang/intl/issues/207 is resolved.
+        singleDigitHourFormat = intl.DateFormat('HH', locale);
+        singleDigitMinuteFormat = intl.DateFormat.m(locale);
+        doubleDigitMinuteFormat = intl.DateFormat('mm', locale);
+        singleDigitSecondFormat = intl.DateFormat.s(locale);
+        decimalFormat = intl.NumberFormat(locale);
+      }
+
+      if (intl.DateFormat.localeExists(localeName)) {
+        loadFormats(localeName);
+      } else if (intl.DateFormat.localeExists(locale.languageCode)) {
+        loadFormats(locale.languageCode);
+      } else {
+        loadFormats(null);
+      }
+
+      return SynchronousFuture<CupertinoLocalizations>(_getCupertinoTranslation(
+        localeName,
+        fullYearFormat,
+        dayFormat,
+        mediumDateFormat,
+        singleDigitHourFormat,
+        singleDigitMinuteFormat,
+        doubleDigitMinuteFormat,
+        singleDigitSecondFormat,
+        decimalFormat,
+      ));
+    });
+  }
+
+  @override
+  bool shouldReload(_GlobalCupertinoLocalizationsDelegate old) => false;
+}
+
+CupertinoLocalizations _getCupertinoTranslation(
+  String localeName,
+  intl.DateFormat fullYearFormat,
+  intl.DateFormat dayFormat,
+  intl.DateFormat mediumDateFormat,
+  intl.DateFormat singleDigitHourFormat,
+  intl.DateFormat singleDigitMinuteFormat,
+  intl.DateFormat doubleDigitMinuteFormat,
+  intl.DateFormat singleDigitSecondFormat,
+  intl.NumberFormat decimalFormat,
+) {
+  return null; // TODO(xster): implement in generated subclass.
+}
diff --git a/packages/flutter_localizations/lib/src/material_localizations.dart b/packages/flutter_localizations/lib/src/material_localizations.dart
index 1dbeaca..a0b72d2 100644
--- a/packages/flutter_localizations/lib/src/material_localizations.dart
+++ b/packages/flutter_localizations/lib/src/material_localizations.dart
@@ -8,10 +8,9 @@
 import 'package:flutter/material.dart';
 import 'package:intl/intl.dart' as intl;
 import 'package:intl/date_symbols.dart' as intl;
-import 'package:intl/date_symbol_data_custom.dart' as date_symbol_data_custom;
-import 'l10n/generated_date_localizations.dart' as date_localizations;
 
 import 'l10n/generated_material_localizations.dart';
+import 'utils/date_localizations.dart' as util;
 import 'widgets_localizations.dart';
 
 /// Implementation of localized strings for the material widgets using the
@@ -559,57 +558,20 @@
   @override
   bool isSupported(Locale locale) => kSupportedLanguages.contains(locale.languageCode);
 
-  /// Tracks if date i18n data has been loaded.
-  static bool _dateIntlDataInitialized = false;
-
-  /// Loads i18n data for dates if it hasn't be loaded yet.
-  ///
-  /// Only the first invocation of this function has the effect of loading the
-  /// data. Subsequent invocations have no effect.
-  static void _loadDateIntlDataIfNotLoaded() {
-    if (!_dateIntlDataInitialized) {
-      // TODO(garyq): Add support for scriptCodes. Do not strip scriptCode from string.
-
-      // Keep track of initialzed locales, or will fail on attempted double init.
-      // This can only happen if a locale with a stripped scriptCode has already
-      // been initialzed. This should be removed when scriptCode stripping is removed.
-      final Set<String> initializedLocales = <String>{};
-      date_localizations.dateSymbols.forEach((String locale, dynamic data) {
-        // Strip scriptCode from the locale, as we do not distinguish between scripts
-        // for dates.
-        final List<String> codes = locale.split('_');
-        String countryCode;
-        if (codes.length == 2) {
-          countryCode = codes[1].length < 4 ? codes[1] : null;
-        } else if (codes.length == 3) {
-          countryCode = codes[1].length < codes[2].length ? codes[1] : codes[2];
-        }
-        locale = codes[0] + (countryCode != null ? '_' + countryCode : '');
-        if (initializedLocales.contains(locale))
-          return;
-        initializedLocales.add(locale);
-        // Perform initialization.
-        assert(date_localizations.datePatterns.containsKey(locale));
-        final intl.DateSymbols symbols = intl.DateSymbols.deserializeFromMap(data);
-        date_symbol_data_custom.initializeDateFormattingCustom(
-          locale: locale,
-          symbols: symbols,
-          patterns: date_localizations.datePatterns[locale],
-        );
-      });
-      _dateIntlDataInitialized = true;
-    }
-  }
-
   static final Map<Locale, Future<MaterialLocalizations>> _loadedTranslations = <Locale, Future<MaterialLocalizations>>{};
 
   @override
   Future<MaterialLocalizations> load(Locale locale) {
     assert(isSupported(locale));
     return _loadedTranslations.putIfAbsent(locale, () {
-      _loadDateIntlDataIfNotLoaded();
+      util.loadDateIntlDataIfNotLoaded();
 
       final String localeName = intl.Intl.canonicalizedLocale(locale.toString());
+      assert(
+        locale.toString() == localeName,
+        'Flutter does not support the non-standard locale form $locale (which '
+        'might be $localeName',
+      );
 
       intl.DateFormat fullYearFormat;
       intl.DateFormat mediumDateFormat;
@@ -645,8 +607,6 @@
         twoDigitZeroPaddedFormat = intl.NumberFormat('00');
       }
 
-      assert(locale.toString() == localeName, 'comparing "$locale" to "$localeName"');
-
       return SynchronousFuture<MaterialLocalizations>(getMaterialTranslation(
         locale,
         fullYearFormat,
diff --git a/packages/flutter_localizations/lib/src/utils/date_localizations.dart b/packages/flutter_localizations/lib/src/utils/date_localizations.dart
new file mode 100644
index 0000000..704322c
--- /dev/null
+++ b/packages/flutter_localizations/lib/src/utils/date_localizations.dart
@@ -0,0 +1,49 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:intl/date_symbols.dart' as intl;
+import 'package:intl/date_symbol_data_custom.dart' as date_symbol_data_custom;
+import '../l10n/generated_date_localizations.dart' as date_localizations;
+
+/// Tracks if date i18n data has been loaded.
+bool _dateIntlDataInitialized = false;
+
+/// Loads i18n data for dates if it hasn't be loaded yet.
+///
+/// Only the first invocation of this function has the effect of loading the
+/// data. Subsequent invocations have no effect.
+void loadDateIntlDataIfNotLoaded() {
+  if (!_dateIntlDataInitialized) {
+    // TODO(garyq): Add support for scriptCodes. Do not strip scriptCode from string.
+
+    // Keep track of initialzed locales, or will fail on attempted double init.
+    // This can only happen if a locale with a stripped scriptCode has already
+    // been initialzed. This should be removed when scriptCode stripping is removed.
+    final Set<String> initializedLocales = <String>{};
+    date_localizations.dateSymbols.forEach((String locale, dynamic data) {
+      // Strip scriptCode from the locale, as we do not distinguish between scripts
+      // for dates.
+      final List<String> codes = locale.split('_');
+      String countryCode;
+      if (codes.length == 2) {
+        countryCode = codes[1].length < 4 ? codes[1] : null;
+      } else if (codes.length == 3) {
+        countryCode = codes[1].length < codes[2].length ? codes[1] : codes[2];
+      }
+      locale = codes[0] + (countryCode != null ? '_' + countryCode : '');
+      if (initializedLocales.contains(locale))
+        return;
+      initializedLocales.add(locale);
+      // Perform initialization.
+      assert(date_localizations.datePatterns.containsKey(locale));
+      final intl.DateSymbols symbols = intl.DateSymbols.deserializeFromMap(data);
+      date_symbol_data_custom.initializeDateFormattingCustom(
+        locale: locale,
+        symbols: symbols,
+        patterns: date_localizations.datePatterns[locale],
+      );
+    });
+    _dateIntlDataInitialized = true;
+  }
+}
\ No newline at end of file