| // Copyright 2017 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/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/date_localizations.dart' as date_localizations; |
| |
| import 'l10n/localizations.dart' show TranslationBundle, translationBundleForLocale; |
| import 'widgets_localizations.dart'; |
| |
| // Watch out: the supported locales list in the doc comment below must be kept |
| // in sync with the list we test, see test/translations_test.dart, and of course |
| // the actual list of supported locales in _MaterialLocalizationsDelegate. |
| |
| /// Localized strings for the material widgets. |
| /// |
| /// To include the localizations provided by this class in a [MaterialApp], |
| /// add [GlobalMaterialLocalizations.delegates] to |
| /// [MaterialApp.localizationsDelegates], and specify the locales your |
| /// app supports with [MaterialApp.supportedLocales]: |
| /// |
| /// ```dart |
| /// new MaterialApp( |
| /// localizationsDelegates: GlobalMaterialLocalizations.delegates, |
| /// supportedLocales: [ |
| /// const Locale('en', 'US'), // English |
| /// const Locale('he', 'IL'), // Hebrew |
| /// // ... |
| /// ], |
| /// // ... |
| /// ) |
| /// ``` |
| /// |
| /// This class supports locales with the following [Locale.languageCode]s: |
| /// |
| /// * ar - Arabic |
| /// * de - German |
| /// * en - English |
| /// * es - Spanish |
| /// * fa - Farsi |
| /// * fr - French |
| /// * gsw - Swiss German |
| /// * he - Hebrew |
| /// * id - Indonesian |
| /// * it - Italian |
| /// * ja - Japanese |
| /// * ko - Korean |
| /// * ms - Malay |
| /// * nl - Dutch |
| /// * nb - Norwegian |
| /// * pl - Polish |
| /// * ps - Pashto |
| /// * pt - Portuguese |
| /// * ro - Romanian |
| /// * ru - Russian |
| /// * th - Thai |
| /// * tr - Turkish |
| /// * ur - Urdu |
| /// * vi - Vietnamese |
| /// * zh - Simplified Chinese |
| /// |
| /// See also: |
| /// |
| /// * The Flutter Internationalization Tutorial, |
| /// <https://flutter.io/tutorials/internationalization/>. |
| /// * [DefaultMaterialLocalizations], which only provides US English translations. |
| class GlobalMaterialLocalizations implements MaterialLocalizations { |
| /// Constructs an object that defines the material widgets' localized strings |
| /// for the given `locale`. |
| /// |
| /// [LocalizationsDelegate] implementations typically call the static [load] |
| /// function, rather than constructing this class directly. |
| GlobalMaterialLocalizations(this.locale) |
| : assert(locale != null), |
| _localeName = _computeLocaleName(locale) { |
| _loadDateIntlDataIfNotLoaded(); |
| |
| _translationBundle = translationBundleForLocale(locale); |
| assert(_translationBundle != null); |
| |
| const String kMediumDatePattern = 'E, MMM\u00a0d'; |
| if (intl.DateFormat.localeExists(_localeName)) { |
| _fullYearFormat = new intl.DateFormat.y(_localeName); |
| _mediumDateFormat = new intl.DateFormat(kMediumDatePattern, _localeName); |
| _longDateFormat = new intl.DateFormat.yMMMMEEEEd(_localeName); |
| _yearMonthFormat = new intl.DateFormat('yMMMM', _localeName); |
| } else if (intl.DateFormat.localeExists(locale.languageCode)) { |
| _fullYearFormat = new intl.DateFormat.y(locale.languageCode); |
| _mediumDateFormat = new intl.DateFormat(kMediumDatePattern, locale.languageCode); |
| |
| _longDateFormat = new intl.DateFormat.yMMMMEEEEd(locale.languageCode); |
| _yearMonthFormat = new intl.DateFormat('yMMMM', locale.languageCode); |
| } else { |
| _fullYearFormat = new intl.DateFormat.y(); |
| _mediumDateFormat = new intl.DateFormat(kMediumDatePattern); |
| _longDateFormat = new intl.DateFormat.yMMMMEEEEd(); |
| _yearMonthFormat = new intl.DateFormat('yMMMM'); |
| } |
| |
| if (intl.NumberFormat.localeExists(_localeName)) { |
| _decimalFormat = new intl.NumberFormat.decimalPattern(_localeName); |
| _twoDigitZeroPaddedFormat = new intl.NumberFormat('00', _localeName); |
| } else if (intl.NumberFormat.localeExists(locale.languageCode)) { |
| _decimalFormat = new intl.NumberFormat.decimalPattern(locale.languageCode); |
| _twoDigitZeroPaddedFormat = new intl.NumberFormat('00', locale.languageCode); |
| } else { |
| _decimalFormat = new intl.NumberFormat.decimalPattern(); |
| _twoDigitZeroPaddedFormat = new intl.NumberFormat('00'); |
| } |
| } |
| |
| /// The locale for which the values of this class's localized resources |
| /// have been translated. |
| final Locale locale; |
| |
| final String _localeName; |
| |
| TranslationBundle _translationBundle; |
| |
| intl.NumberFormat _decimalFormat; |
| |
| intl.NumberFormat _twoDigitZeroPaddedFormat; |
| |
| intl.DateFormat _fullYearFormat; |
| |
| intl.DateFormat _mediumDateFormat; |
| |
| intl.DateFormat _longDateFormat; |
| |
| intl.DateFormat _yearMonthFormat; |
| |
| static String _computeLocaleName(Locale locale) { |
| return intl.Intl.canonicalizedLocale(locale.toString()); |
| } |
| |
| @override |
| String formatHour(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat = false }) { |
| switch (hourFormat(of: timeOfDayFormat(alwaysUse24HourFormat: alwaysUse24HourFormat))) { |
| case HourFormat.HH: |
| return _twoDigitZeroPaddedFormat.format(timeOfDay.hour); |
| case HourFormat.H: |
| return formatDecimal(timeOfDay.hour); |
| case HourFormat.h: |
| final int hour = timeOfDay.hourOfPeriod; |
| return formatDecimal(hour == 0 ? 12 : hour); |
| } |
| return null; |
| } |
| |
| @override |
| String formatMinute(TimeOfDay timeOfDay) { |
| return _twoDigitZeroPaddedFormat.format(timeOfDay.minute); |
| } |
| |
| @override |
| String formatYear(DateTime date) { |
| return _fullYearFormat.format(date); |
| } |
| |
| @override |
| String formatMediumDate(DateTime date) { |
| return _mediumDateFormat.format(date); |
| } |
| |
| @override |
| String formatFullDate(DateTime date) { |
| return _longDateFormat.format(date); |
| } |
| |
| @override |
| String formatMonthYear(DateTime date) { |
| return _yearMonthFormat.format(date); |
| } |
| |
| @override |
| List<String> get narrowWeekdays { |
| return _fullYearFormat.dateSymbols.NARROWWEEKDAYS; |
| } |
| |
| @override |
| int get firstDayOfWeekIndex => (_fullYearFormat.dateSymbols.FIRSTDAYOFWEEK + 1) % 7; |
| |
| @override |
| String formatDecimal(int number) { |
| return _decimalFormat.format(number); |
| } |
| |
| @override |
| String formatTimeOfDay(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat = false }) { |
| // Not using intl.DateFormat for two reasons: |
| // |
| // - DateFormat supports more formats than our material time picker does, |
| // and we want to be consistent across time picker format and the string |
| // formatting of the time of day. |
| // - DateFormat operates on DateTime, which is sensitive to time eras and |
| // time zones, while here we want to format hour and minute within one day |
| // no matter what date the day falls on. |
| final String hour = formatHour(timeOfDay, alwaysUse24HourFormat: alwaysUse24HourFormat); |
| final String minute = formatMinute(timeOfDay); |
| switch (timeOfDayFormat(alwaysUse24HourFormat: alwaysUse24HourFormat)) { |
| case TimeOfDayFormat.h_colon_mm_space_a: |
| return '$hour:$minute ${_formatDayPeriod(timeOfDay)}'; |
| case TimeOfDayFormat.H_colon_mm: |
| case TimeOfDayFormat.HH_colon_mm: |
| return '$hour:$minute'; |
| case TimeOfDayFormat.HH_dot_mm: |
| return '$hour.$minute'; |
| case TimeOfDayFormat.a_space_h_colon_mm: |
| return '${_formatDayPeriod(timeOfDay)} $hour:$minute'; |
| case TimeOfDayFormat.frenchCanadian: |
| return '$hour h $minute'; |
| } |
| |
| return null; |
| } |
| |
| String _formatDayPeriod(TimeOfDay timeOfDay) { |
| switch (timeOfDay.period) { |
| case DayPeriod.am: |
| return anteMeridiemAbbreviation; |
| case DayPeriod.pm: |
| return postMeridiemAbbreviation; |
| } |
| return null; |
| } |
| |
| @override |
| String get openAppDrawerTooltip => _translationBundle.openAppDrawerTooltip; |
| |
| @override |
| String get backButtonTooltip => _translationBundle.backButtonTooltip; |
| |
| @override |
| String get closeButtonTooltip => _translationBundle.closeButtonTooltip; |
| |
| @override |
| String get deleteButtonTooltip => _translationBundle.deleteButtonTooltip; |
| |
| @override |
| String get nextMonthTooltip => _translationBundle.nextMonthTooltip; |
| |
| @override |
| String get previousMonthTooltip => _translationBundle.previousMonthTooltip; |
| |
| @override |
| String get nextPageTooltip => _translationBundle.nextPageTooltip; |
| |
| @override |
| String get previousPageTooltip => _translationBundle.previousPageTooltip; |
| |
| @override |
| String get showMenuTooltip => _translationBundle.showMenuTooltip; |
| |
| @override |
| String get drawerLabel => _translationBundle.alertDialogLabel; |
| |
| @override |
| String get popupMenuLabel => _translationBundle.popupMenuLabel; |
| |
| @override |
| String get dialogLabel => _translationBundle.dialogLabel; |
| |
| @override |
| String get alertDialogLabel => _translationBundle.alertDialogLabel; |
| |
| @override |
| String get searchFieldLabel => _translationBundle.searchFieldLabel; |
| |
| @override |
| String aboutListTileTitle(String applicationName) { |
| final String text = _translationBundle.aboutListTileTitle; |
| return text.replaceFirst(r'$applicationName', applicationName); |
| } |
| |
| @override |
| String get licensesPageTitle => _translationBundle.licensesPageTitle; |
| |
| @override |
| String pageRowsInfoTitle(int firstRow, int lastRow, int rowCount, bool rowCountIsApproximate) { |
| String text = rowCountIsApproximate ? _translationBundle.pageRowsInfoTitleApproximate : null; |
| text ??= _translationBundle.pageRowsInfoTitle; |
| assert(text != null, 'A $locale localization was not found for pageRowsInfoTitle or pageRowsInfoTitleApproximate'); |
| // TODO(hansmuller): this could be more efficient. |
| return text |
| .replaceFirst(r'$firstRow', formatDecimal(firstRow)) |
| .replaceFirst(r'$lastRow', formatDecimal(lastRow)) |
| .replaceFirst(r'$rowCount', formatDecimal(rowCount)); |
| } |
| |
| @override |
| String get rowsPerPageTitle => _translationBundle.rowsPerPageTitle; |
| |
| @override |
| String tabLabel({int tabIndex, int tabCount}) { |
| assert(tabIndex >= 1); |
| assert(tabCount >= 1); |
| final String template = _translationBundle.tabLabel; |
| return template |
| .replaceFirst(r'$tabIndex', formatDecimal(tabIndex)) |
| .replaceFirst(r'$tabCount', formatDecimal(tabCount)); |
| } |
| |
| @override |
| String selectedRowCountTitle(int selectedRowCount) { |
| // TODO(hmuller): the rules for mapping from an integer value to |
| // "one" or "two" etc. are locale specific and an additional "few" category |
| // is needed. See http://cldr.unicode.org/index/cldr-spec/plural-rules |
| String text; |
| if (selectedRowCount == 0) |
| text = _translationBundle.selectedRowCountTitleZero; |
| else if (selectedRowCount == 1) |
| text = _translationBundle.selectedRowCountTitleOne; |
| else if (selectedRowCount == 2) |
| text = _translationBundle.selectedRowCountTitleTwo; |
| else if (selectedRowCount > 2) |
| text = _translationBundle.selectedRowCountTitleMany; |
| text ??= _translationBundle.selectedRowCountTitleOther; |
| assert(text != null); |
| |
| return text.replaceFirst(r'$selectedRowCount', formatDecimal(selectedRowCount)); |
| } |
| |
| @override |
| String get cancelButtonLabel => _translationBundle.cancelButtonLabel; |
| |
| @override |
| String get closeButtonLabel => _translationBundle.closeButtonLabel; |
| |
| @override |
| String get continueButtonLabel => _translationBundle.continueButtonLabel; |
| |
| @override |
| String get copyButtonLabel => _translationBundle.copyButtonLabel; |
| |
| @override |
| String get cutButtonLabel => _translationBundle.cutButtonLabel; |
| |
| @override |
| String get okButtonLabel => _translationBundle.okButtonLabel; |
| |
| @override |
| String get pasteButtonLabel => _translationBundle.pasteButtonLabel; |
| |
| @override |
| String get selectAllButtonLabel => _translationBundle.selectAllButtonLabel; |
| |
| @override |
| String get viewLicensesButtonLabel => _translationBundle.viewLicensesButtonLabel; |
| |
| @override |
| String get anteMeridiemAbbreviation => _translationBundle.anteMeridiemAbbreviation; |
| |
| @override |
| String get postMeridiemAbbreviation => _translationBundle.postMeridiemAbbreviation; |
| |
| @override |
| String get timePickerHourModeAnnouncement => _translationBundle.timePickerHourModeAnnouncement; |
| |
| @override |
| String get timePickerMinuteModeAnnouncement => _translationBundle.timePickerMinuteModeAnnouncement; |
| |
| @override |
| String get modalBarrierDismissLabel => _translationBundle.modalBarrierDismissLabel; |
| |
| @override |
| String get signedInLabel => _translationBundle.signedInLabel; |
| |
| @override |
| String get hideAccountsLabel => _translationBundle.hideAccountsLabel; |
| |
| @override |
| String get showAccountsLabel => _translationBundle.showAccountsLabel; |
| |
| /// The [TimeOfDayFormat] corresponding to one of the following supported |
| /// patterns: |
| /// |
| /// * `HH:mm` |
| /// * `HH.mm` |
| /// * `HH 'h' mm` |
| /// * `HH:mm น.` |
| /// * `H:mm` |
| /// * `h:mm a` |
| /// * `a h:mm` |
| /// * `ah:mm` |
| /// |
| /// See also: |
| /// |
| /// * http://demo.icu-project.org/icu-bin/locexp?d_=en&_=en_US shows the |
| /// short time pattern used in locale en_US |
| @override |
| TimeOfDayFormat timeOfDayFormat({ bool alwaysUse24HourFormat = false }) { |
| final String icuShortTimePattern = _translationBundle.timeOfDayFormat; |
| |
| assert(() { |
| if (!_icuTimeOfDayToEnum.containsKey(icuShortTimePattern)) { |
| throw new FlutterError( |
| '"$icuShortTimePattern" is not one of the ICU short time patterns ' |
| 'supported by the material library. Here is the list of supported ' |
| 'patterns:\n ' + |
| _icuTimeOfDayToEnum.keys.join('\n ') |
| ); |
| } |
| return true; |
| }()); |
| |
| final TimeOfDayFormat icuFormat = _icuTimeOfDayToEnum[icuShortTimePattern]; |
| |
| if (alwaysUse24HourFormat) |
| return _get24HourVersionOf(icuFormat); |
| |
| return icuFormat; |
| } |
| |
| /// Looks up text geometry defined in [MaterialTextGeometry]. |
| @override |
| TextTheme get localTextGeometry => MaterialTextGeometry.forScriptCategory(_translationBundle.scriptCategory); |
| |
| /// Creates an object that provides localized resource values for the |
| /// for the widgets of the material library. |
| /// |
| /// This method is typically used to create a [LocalizationsDelegate]. |
| /// The [MaterialApp] does so by default. |
| static Future<MaterialLocalizations> load(Locale locale) { |
| return new SynchronousFuture<MaterialLocalizations>(new GlobalMaterialLocalizations(locale)); |
| } |
| |
| /// A [LocalizationsDelegate] that uses [GlobalMaterialLocalizations.load] |
| /// to create an instance of this class. |
| /// |
| /// Most internationalized apps will use [GlobalMaterialLocalizations.delegates] |
| /// as the value of [MaterialApp.localizationsDelegates] to include |
| /// the localizations for both the material and widget libraries. |
| static const LocalizationsDelegate<MaterialLocalizations> delegate = _MaterialLocalizationsDelegate(); |
| |
| /// A value for [MaterialApp.localizationsDelegates] that's typically used by |
| /// internationalized apps. |
| /// |
| /// To include the localizations provided by this class and by |
| /// [GlobalWidgetsLocalizations] in a [MaterialApp], |
| /// use [GlobalMaterialLocalizations.delegates] as the value of |
| /// [MaterialApp.localizationsDelegates], and specify the locales your |
| /// app supports with [MaterialApp.supportedLocales]: |
| /// |
| /// ```dart |
| /// new MaterialApp( |
| /// localizationsDelegates: GlobalMaterialLocalizations.delegates, |
| /// supportedLocales: [ |
| /// const Locale('en', 'US'), // English |
| /// const Locale('he', 'IL'), // Hebrew |
| /// ], |
| /// // ... |
| /// ) |
| /// ``` |
| static const List<LocalizationsDelegate<dynamic>> delegates = <LocalizationsDelegate<dynamic>>[ |
| GlobalMaterialLocalizations.delegate, |
| GlobalWidgetsLocalizations.delegate, |
| ]; |
| } |
| |
| const Map<String, TimeOfDayFormat> _icuTimeOfDayToEnum = <String, TimeOfDayFormat>{ |
| 'HH:mm': TimeOfDayFormat.HH_colon_mm, |
| 'HH.mm': TimeOfDayFormat.HH_dot_mm, |
| "HH 'h' mm": TimeOfDayFormat.frenchCanadian, |
| 'HH:mm น.': TimeOfDayFormat.HH_colon_mm, |
| 'H:mm': TimeOfDayFormat.H_colon_mm, |
| 'h:mm a': TimeOfDayFormat.h_colon_mm_space_a, |
| 'a h:mm': TimeOfDayFormat.a_space_h_colon_mm, |
| 'ah:mm': TimeOfDayFormat.a_space_h_colon_mm, |
| }; |
| |
| /// Finds the [TimeOfDayFormat] to use instead of the `original` when the |
| /// `original` uses 12-hour format and [MediaQueryData.alwaysUse24HourFormat] |
| /// is true. |
| TimeOfDayFormat _get24HourVersionOf(TimeOfDayFormat original) { |
| switch (original) { |
| case TimeOfDayFormat.HH_colon_mm: |
| case TimeOfDayFormat.HH_dot_mm: |
| case TimeOfDayFormat.frenchCanadian: |
| case TimeOfDayFormat.H_colon_mm: |
| return original; |
| case TimeOfDayFormat.h_colon_mm_space_a: |
| case TimeOfDayFormat.a_space_h_colon_mm: |
| return TimeOfDayFormat.HH_colon_mm; |
| } |
| return TimeOfDayFormat.HH_colon_mm; |
| } |
| |
| /// 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) { |
| date_localizations.dateSymbols.forEach((String locale, dynamic data) { |
| assert(date_localizations.datePatterns.containsKey(locale)); |
| final intl.DateSymbols symbols = new intl.DateSymbols.deserializeFromMap(data); |
| date_symbol_data_custom.initializeDateFormattingCustom( |
| locale: locale, |
| symbols: symbols, |
| patterns: date_localizations.datePatterns[locale], |
| ); |
| }); |
| _dateIntlDataInitialized = true; |
| } |
| } |
| |
| class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> { |
| const _MaterialLocalizationsDelegate(); |
| |
| // Watch out: this list must match the one in the GlobalMaterialLocalizations |
| // class doc and the list we test, see test/translations_test.dart. |
| static const List<String> _supportedLanguages = <String>[ |
| 'ar', // Arabic |
| 'de', // German |
| 'en', // English |
| 'es', // Spanish |
| 'fa', // Farsi (Persian) |
| 'fr', // French |
| 'gsw', // Swiss German |
| 'he', // Hebrew |
| 'id', // Indonesian |
| 'it', // Italian |
| 'ja', // Japanese |
| 'ko', // Korean |
| 'ms', // Malay |
| 'nl', // Dutch |
| 'nb', // Norwegian |
| 'pl', // Polish |
| 'ps', // Pashto |
| 'pt', // Portugese |
| 'ro', // Romanian |
| 'ru', // Russian |
| 'th', // Thai |
| 'tr', // Turkish |
| 'ur', // Urdu |
| 'vi', // Vietnamese |
| 'zh', // Chinese (simplified) |
| ]; |
| |
| @override |
| bool isSupported(Locale locale) => _supportedLanguages.contains(locale.languageCode); |
| |
| @override |
| Future<MaterialLocalizations> load(Locale locale) => GlobalMaterialLocalizations.load(locale); |
| |
| @override |
| bool shouldReload(_MaterialLocalizationsDelegate old) => false; |
| } |