blob: 690e060f70655949b51ebffaff70bbb27bb501cc [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/widgets.dart';
import 'package:intl/intl.dart' as intl;
import 'package:intl/date_symbols.dart' as intl;
import 'package:intl/date_symbol_data_local.dart' as intl_local_date_data;
import 'i18n/localizations.dart';
import 'time.dart';
import 'typography.dart';
/// Defines the localized resource values used by the Material widgets.
///
/// See also:
///
/// * [DefaultMaterialLocalizations], which implements this interface
/// and supports a variety of locales.
abstract class MaterialLocalizations {
/// The tooltip for the leading [AppBar] menu (aka 'hamburger') button.
String get openAppDrawerTooltip;
/// The [BackButton]'s tooltip.
String get backButtonTooltip;
/// The [CloseButton]'s tooltip.
String get closeButtonTooltip;
/// The tooltip for the [MonthPicker]'s "next month" button.
String get nextMonthTooltip;
/// The tooltip for the [MonthPicker]'s "previous month" button.
String get previousMonthTooltip;
/// The tooltip for the [PaginatedDataTable]'s "next page" button.
String get nextPageTooltip;
/// The tooltip for the [PaginatedDataTable]'s "previous page" button.
String get previousPageTooltip;
/// The default [PopupMenuButton] tooltip.
String get showMenuTooltip;
/// The default title for [AboutListTile].
String aboutListTileTitle(String applicationName);
/// Title for the [LicensePage] widget.
String get licensesPageTitle;
/// Title for the [PaginatedDataTable]'s row info footer.
String pageRowsInfoTitle(int firstRow, int lastRow, int rowCount, bool rowCountIsApproximate);
/// Title for the [PaginatedDataTable]'s "rows per page" footer.
String get rowsPerPageTitle;
/// Title for the [PaginatedDataTable]'s selected row count header.
String selectedRowCountTitle(int selectedRowCount);
/// Label for "cancel" buttons and menu items.
String get cancelButtonLabel;
/// Label for "close" buttons and menu items.
String get closeButtonLabel;
/// Label for "continue" buttons and menu items.
String get continueButtonLabel;
/// Label for "copy" edit buttons and menu items.
String get copyButtonLabel;
/// Label for "cut" edit buttons and menu items.
String get cutButtonLabel;
/// Label for OK buttons and menu items.
String get okButtonLabel;
/// Label for "paste" edit buttons and menu items.
String get pasteButtonLabel;
/// Label for "select all" edit buttons and menu items.
String get selectAllButtonLabel;
/// Label for the [AboutDialog] button that shows the [LicensePage].
String get viewLicensesButtonLabel;
/// The abbreviation for ante meridiem (before noon) shown in the time picker.
String get anteMeridiemAbbreviation;
/// The abbreviation for post meridiem (after noon) shown in the time picker.
String get postMeridiemAbbreviation;
/// The format used to lay out the time picker.
///
/// The documentation for [TimeOfDayFormat] enum values provides details on
/// each supported layout.
TimeOfDayFormat get timeOfDayFormat;
/// Provides geometric text preferences for the current locale.
///
/// This text theme is incomplete. For example, it lacks text color
/// information. This theme must be merged with another text theme that
/// provides the missing values. The text styles provided by this theme have
/// their [TextStyle.inherit] property set to true.
///
/// Typically a complete theme is obtained via [Theme.of], which can be
/// localized using the [Localizations] widget.
///
/// See also: https://material.io/guidelines/style/typography.html
TextTheme get localTextGeometry;
/// Formats [TimeOfDay.hour] in the given time of day according to the value
/// of [timeOfDayFormat].
String formatHour(TimeOfDay timeOfDay);
/// Formats [TimeOfDay.minute] in the given time of day according to the value
/// of [timeOfDayFormat].
String formatMinute(TimeOfDay timeOfDay);
/// Formats [timeOfDay] according to the value of [timeOfDayFormat].
String formatTimeOfDay(TimeOfDay timeOfDay);
/// Full unabbreviated year format, e.g. 2017 rather than 17.
String formatYear(DateTime date);
/// Formats the date using a medium-width format.
///
/// Abbreviates month and days of week. This appears in the header of the date
/// picker invoked using [showDatePicker].
///
/// Examples:
///
/// - US English: Wed, Sep 27
/// - Russian: ср, сент. 27
String formatMediumDate(DateTime date);
/// Formats the month and the year of the given [date].
///
/// The returned string does not contain the day of the month. This appears
/// in the date picker invoked using [showDatePicker].
String formatMonthYear(DateTime date);
/// List of week day names in narrow format, usually 1- or 2-letter
/// abbreviations of full names.
///
/// The list begins with the value corresponding to Sunday and ends with
/// Saturday. Use [firstDayOfWeekIndex] to find the first day of week in this
/// list.
///
/// Examples:
///
/// - US English: S, M, T, W, T, F, S
/// - Russian: вс, пн, вт, ср, чт, пт, сб - notice that the list begins with
/// вс (Sunday) even though the first day of week for Russian is Monday.
List<String> get narrowWeekDays;
/// Index of the first day of week, where 0 points to Sunday, and 6 points to
/// Saturday.
///
/// This getter is compatible with [narrowWeekDays]. For example:
///
/// ```dart
/// var localizations = MaterialLocalizations.of(context);
/// // The name of the first day of week for the current locale.
/// var firstDayOfWeek = localizations.narrowWeekDays[localizations.firstDayOfWeekIndex];
/// ```
int get firstDayOfWeekIndex;
/// The `MaterialLocalizations` from the closest [Localizations] instance
/// that encloses the given context.
///
/// This method is just a convenient shorthand for:
/// `Localizations.of<MaterialLocalizations>(context, MaterialLocalizations)`.
///
/// References to the localized resources defined by this class are typically
/// written in terms of this method. For example:
///
/// ```dart
/// tooltip: MaterialLocalizations.of(context).backButtonTooltip,
/// ```
static MaterialLocalizations of(BuildContext context) {
return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
}
}
/// Localized strings for the material widgets.
class DefaultMaterialLocalizations 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.
DefaultMaterialLocalizations(this.locale)
: assert(locale != null),
this._localeName = _computeLocaleName(locale) {
_loadDateIntlDataIfNotLoaded();
if (localizations.containsKey(locale.languageCode))
_nameToValue.addAll(localizations[locale.languageCode]);
if (localizations.containsKey(_localeName))
_nameToValue.addAll(localizations[_localeName]);
const String kMediumDatePattern = 'E, MMM\u00a0d';
if (intl.DateFormat.localeExists(_localeName)) {
_fullYearFormat = new intl.DateFormat.y(_localeName);
_mediumDateFormat = new intl.DateFormat(kMediumDatePattern, _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);
_yearMonthFormat = new intl.DateFormat('yMMMM', locale.languageCode);
} else {
_fullYearFormat = new intl.DateFormat.y();
_mediumDateFormat = new intl.DateFormat(kMediumDatePattern);
_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;
final Map<String, String> _nameToValue = <String, String>{};
/// Formats numbers using variable length format with no zero padding.
///
/// See also [_twoDigitZeroPaddedFormat].
intl.NumberFormat _decimalFormat;
/// Formats numbers as two-digits.
///
/// If the number is less than 10, zero-pads it.
intl.NumberFormat _twoDigitZeroPaddedFormat;
/// Full unabbreviated year format, e.g. 2017 rather than 17.
intl.DateFormat _fullYearFormat;
intl.DateFormat _mediumDateFormat;
intl.DateFormat _yearMonthFormat;
static String _computeLocaleName(Locale locale) {
final String localeName = locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
return intl.Intl.canonicalizedLocale(localeName);
}
// 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 _nameToPluralValue(int count, String key) {
String text;
if (count == 0)
text = _nameToValue['${key}Zero'];
else if (count == 1)
text = _nameToValue['${key}One'];
else if (count == 2)
text = _nameToValue['${key}Two'];
else if (count > 2)
text = _nameToValue['${key}Many'];
text ??= _nameToValue['${key}Other'];
assert(text != null);
return text;
}
@override
String formatHour(TimeOfDay timeOfDay) {
switch (hourFormat(of: timeOfDayFormat)) {
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 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;
/// Formats a [number] using local decimal number format.
///
/// Inserts locale-appropriate thousands separator, if necessary.
String formatDecimal(int number) {
return _decimalFormat.format(number);
}
@override
String formatTimeOfDay(TimeOfDay timeOfDay) {
// 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.
switch (timeOfDayFormat) {
case TimeOfDayFormat.h_colon_mm_space_a:
return '${formatHour(timeOfDay)}:${formatMinute(timeOfDay)} ${_formatDayPeriod(timeOfDay)}';
case TimeOfDayFormat.H_colon_mm:
case TimeOfDayFormat.HH_colon_mm:
return '${formatHour(timeOfDay)}:${formatMinute(timeOfDay)}';
case TimeOfDayFormat.HH_dot_mm:
return '${formatHour(timeOfDay)}.${formatMinute(timeOfDay)}';
case TimeOfDayFormat.a_space_h_colon_mm:
return '${_formatDayPeriod(timeOfDay)} ${formatHour(timeOfDay)}:${formatMinute(timeOfDay)}';
case TimeOfDayFormat.frenchCanadian:
return '${formatHour(timeOfDay)} h ${formatMinute(timeOfDay)}';
}
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 => _nameToValue['openAppDrawerTooltip'];
@override
String get backButtonTooltip => _nameToValue['backButtonTooltip'];
@override
String get closeButtonTooltip => _nameToValue['closeButtonTooltip'];
@override
String get nextMonthTooltip => _nameToValue['nextMonthTooltip'];
@override
String get previousMonthTooltip => _nameToValue['previousMonthTooltip'];
@override
String get nextPageTooltip => _nameToValue['nextPageTooltip'];
@override
String get previousPageTooltip => _nameToValue['previousPageTooltip'];
@override
String get showMenuTooltip => _nameToValue['showMenuTooltip'];
@override
String aboutListTileTitle(String applicationName) {
final String text = _nameToValue['aboutListTileTitle'];
return text.replaceFirst(r'$applicationName', applicationName);
}
@override
String get licensesPageTitle => _nameToValue['licensesPageTitle'];
@override
String pageRowsInfoTitle(int firstRow, int lastRow, int rowCount, bool rowCountIsApproximate) {
String text = rowCountIsApproximate ? _nameToValue['pageRowsInfoTitleApproximate'] : null;
text ??= _nameToValue['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 => _nameToValue['rowsPerPageTitle'];
@override
String selectedRowCountTitle(int selectedRowCount) {
return _nameToPluralValue(selectedRowCount, 'selectedRowCountTitle') // asserts on no match
.replaceFirst(r'$selectedRowCount', formatDecimal(selectedRowCount));
}
@override
String get cancelButtonLabel => _nameToValue['cancelButtonLabel'];
@override
String get closeButtonLabel => _nameToValue['closeButtonLabel'];
@override
String get continueButtonLabel => _nameToValue['continueButtonLabel'];
@override
String get copyButtonLabel => _nameToValue['copyButtonLabel'];
@override
String get cutButtonLabel => _nameToValue['cutButtonLabel'];
@override
String get okButtonLabel => _nameToValue['okButtonLabel'];
@override
String get pasteButtonLabel => _nameToValue['pasteButtonLabel'];
@override
String get selectAllButtonLabel => _nameToValue['selectAllButtonLabel'];
@override
String get viewLicensesButtonLabel => _nameToValue['viewLicensesButtonLabel'];
@override
String get anteMeridiemAbbreviation => _nameToValue['anteMeridiemAbbreviation'];
@override
String get postMeridiemAbbreviation => _nameToValue['postMeridiemAbbreviation'];
/// 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 get timeOfDayFormat {
final String icuShortTimePattern = _nameToValue['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;
}());
return _icuTimeOfDayToEnum[icuShortTimePattern];
}
/// Looks up text geometry defined in [MaterialTextGeometry].
@override
TextTheme get localTextGeometry => MaterialTextGeometry.forScriptCategory(_nameToValue["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 DefaultMaterialLocalizations(locale));
}
}
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,
};
/// 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) {
// The returned Future is intentionally dropped on the floor. The
// function only returns it to be compatible with the async counterparts.
// The Future has no value otherwise.
intl_local_date_data.initializeDateFormatting();
_dateIntlDataInitialized = true;
}
}