blob: 2072fe89b8f38b2e8844405650651d7cef364a66 [file] [log] [blame]
// 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 acutal 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
/// * he - Hebrew
/// * id - Indonesian
/// * it - Italian
/// * ja - Japanese
/// * ko - Korean
/// * ms - Malay
/// * nl - Dutch
/// * no - 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 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 = const _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 = const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
}
const Map<String, TimeOfDayFormat> _icuTimeOfDayToEnum = const <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 = const <String>[
'ar', // Arabic
'de', // German
'en', // English
'es', // Spanish
'fa', // Farsi (Persian)
'fr', // French
'he', // Hebrew
'id', // Indonesian
'it', // Italian
'ja', // Japanese
'ko', // Korean
'ms', // Malay
'nl', // Dutch
'no', // 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;
}