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 0000000000..557a9344d9 --- /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 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> delegates = >[ + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; +} + +class _GlobalCupertinoLocalizationsDelegate extends LocalizationsDelegate { + const _GlobalCupertinoLocalizationsDelegate(); + + @override + bool isSupported(Locale locale) => false; // TODO(xster): implement. + + static final Map> _loadedTranslations = >{}; + + @override + Future 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(_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 1dbeaca37b..a0b72d2eb1 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/foundation.dart'; 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 @@ class _MaterialLocalizationsDelegate extends LocalizationsDelegate 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 initializedLocales = {}; - date_localizations.dateSymbols.forEach((String locale, dynamic data) { - // Strip scriptCode from the locale, as we do not distinguish between scripts - // for dates. - final List 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> _loadedTranslations = >{}; @override Future 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 @@ class _MaterialLocalizationsDelegate extends LocalizationsDelegate(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 0000000000..704322c102 --- /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 initializedLocales = {}; + date_localizations.dateSymbols.forEach((String locale, dynamic data) { + // Strip scriptCode from the locale, as we do not distinguish between scripts + // for dates. + final List 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