Commit f2d09601 authored by Yegor's avatar Yegor Committed by GitHub

Internationalize time numerals in the time picker and TimeOfDay (#12166)

* internationalize time numerals

* tests

* use foundation.dart instead of meta.dart

* address comments
parent 441b5c20
......@@ -101,7 +101,7 @@ class _DateTimePicker extends StatelessWidget {
new Expanded(
flex: 3,
child: new _InputDropdown(
valueText: selectedTime.toString(),
valueText: selectedTime.format(context),
valueStyle: valueStyle,
onPressed: () { _selectTime(context); },
),
......
......@@ -182,7 +182,7 @@ class DialogDemoState extends State<DialogDemo> {
if (value != null && value != _selectedTime) {
_selectedTime = value;
_scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text('You selected: $value')
content: new Text('You selected: ${value.format(context)}')
));
}
});
......
......@@ -82,7 +82,7 @@ class DateTimeItem extends StatelessWidget {
},
child: new Row(
children: <Widget>[
new Text('$time'),
new Text('${time.format(context)}'),
const Icon(Icons.arrow_drop_down, color: Colors.black54),
]
)
......
......@@ -82,6 +82,7 @@ export 'src/material/text_form_field.dart';
export 'src/material/text_selection.dart';
export 'src/material/theme.dart';
export 'src/material/theme_data.dart';
export 'src/material/time.dart';
export 'src/material/time_picker.dart';
export 'src/material/toggleable.dart';
export 'src/material/tooltip.dart';
......
......@@ -9,6 +9,7 @@ import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart' as intl;
import 'i18n/localizations.dart';
import 'time.dart';
import 'typography.dart';
/// Defines the localized resource values used by the Material widgets.
......@@ -109,6 +110,17 @@ abstract class MaterialLocalizations {
/// 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);
/// The `MaterialLocalizations` from the closest [Localizations] instance
/// that encloses the given context.
///
......@@ -133,22 +145,45 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
///
/// [LocalizationsDelegate] implementations typically call the static [load]
/// function, rather than constructing this class directly.
DefaultMaterialLocalizations(this.locale) {
assert(locale != null);
DefaultMaterialLocalizations(this.locale)
: assert(locale != null),
this._localeName = _computeLocaleName(locale) {
if (localizations.containsKey(locale.languageCode))
_nameToValue.addAll(localizations[locale.languageCode]);
if (localizations.containsKey(_localeName))
_nameToValue.addAll(localizations[_localeName]);
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>{};
String get _localeName {
/// 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;
static String _computeLocaleName(Locale locale) {
final String localeName = locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
return intl.Intl.canonicalizedLocale(localeName);
}
......@@ -171,12 +206,67 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
return text;
}
String _formatInteger(int n) {
final String localeName = _localeName;
if (!intl.NumberFormat.localeExists(localeName))
return n.toString();
return new intl.NumberFormat.decimalPattern(localeName).format(n);
@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);
}
/// 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
......@@ -219,9 +309,9 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
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', _formatInteger(firstRow))
.replaceFirst(r'$lastRow', _formatInteger(lastRow))
.replaceFirst(r'$rowCount', _formatInteger(rowCount));
.replaceFirst(r'$firstRow', formatDecimal(firstRow))
.replaceFirst(r'$lastRow', formatDecimal(lastRow))
.replaceFirst(r'$rowCount', formatDecimal(rowCount));
}
@override
......@@ -230,7 +320,7 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
@override
String selectedRowCountTitle(int selectedRowCount) {
return _nameToPluralValue(selectedRowCount, 'selectedRowCountTitle') // asserts on no match
.replaceFirst(r'$selectedRowCount', _formatInteger(selectedRowCount));
.replaceFirst(r'$selectedRowCount', formatDecimal(selectedRowCount));
}
@override
......@@ -325,55 +415,3 @@ const Map<String, TimeOfDayFormat> _icuTimeOfDayToEnum = const <String, TimeOfDa
'a h:mm': TimeOfDayFormat.a_space_h_colon_mm,
'ah:mm': TimeOfDayFormat.a_space_h_colon_mm,
};
/// Determines how the time picker invoked using [showTimePicker] formats and
/// lays out the time controls.
///
/// The time picker provides layout configurations optimized for each of the
/// enum values.
enum TimeOfDayFormat {
/// Corresponds to the ICU 'HH:mm' pattern.
///
/// This format uses 24-hour two-digit zero-padded hours. Controls are always
/// laid out horizontally. Hours are separated from minutes by one colon
/// character.
HH_colon_mm,
/// Corresponds to the ICU 'HH.mm' pattern.
///
/// This format uses 24-hour two-digit zero-padded hours. Controls are always
/// laid out horizontally. Hours are separated from minutes by one dot
/// character.
HH_dot_mm,
/// Corresponds to the ICU "HH 'h' mm" pattern used in Canadian French.
///
/// This format uses 24-hour two-digit zero-padded hours. Controls are always
/// laid out horizontally. Hours are separated from minutes by letter 'h'.
frenchCanadian,
/// Corresponds to the ICU 'H:mm' pattern.
///
/// This format uses 24-hour non-padded variable-length hours. Controls are
/// always laid out horizontally. Hours are separated from minutes by one
/// colon character.
H_colon_mm,
/// Corresponds to the ICU 'h:mm a' pattern.
///
/// This format uses 12-hour non-padded variable-length hours with a day
/// period. Controls are laid out horizontally in portrait mode. In landscape
/// mode, the day period appears vertically after (consistent with the ambient
/// [TextDirection]) hour-minute indicator. Hours are separated from minutes
/// by one colon character.
h_colon_mm_space_a,
/// Corresponds to the ICU 'a h:mm' pattern.
///
/// This format uses 12-hour non-padded variable-length hours with a day
/// period. Controls are laid out horizontally in portrait mode. In landscape
/// mode, the day period appears vertically before (consistent with the
/// ambient [TextDirection]) hour-minute indicator. Hours are separated from
/// minutes by one colon character.
a_space_h_colon_mm,
}
// 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:ui' show hashValues;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'material_localizations.dart';
/// Whether the [TimeOfDay] is before or after noon.
enum DayPeriod {
/// Ante meridiem (before noon).
am,
/// Post meridiem (after noon).
pm,
}
/// A value representing a time during the day, independent of the date that
/// day might fall on or the time zone.
///
/// The time is represented by [hour] and [minute] pair.
///
/// See also:
///
/// * [showTimePicker], which returns this type.
/// * [MaterialLocalizations], which provides methods for formatting values of
/// this type according to the chosen [Locale].
/// * [DateTime], which represents date and time, and is subject to eras and
/// time zones.
@immutable
class TimeOfDay {
/// The number of hours in one day, i.e. 24.
static const int hoursPerDay = 24;
/// The number of hours in one day period (see also [DayPeriod]), i.e. 12.
static const int hoursPerPeriod = 12;
/// The number of minutes in one hour, i.e. 60.
static const int minutesPerHour = 60;
/// Creates a time of day.
///
/// The [hour] argument must be between 0 and 23, inclusive. The [minute]
/// argument must be between 0 and 59, inclusive.
const TimeOfDay({ @required this.hour, @required this.minute });
/// Creates a time of day based on the given time.
///
/// The [hour] is set to the time's hour and the [minute] is set to the time's
/// minute in the timezone of the given [DateTime].
TimeOfDay.fromDateTime(DateTime time) : hour = time.hour, minute = time.minute;
/// Creates a time of day based on the current time.
///
/// The [hour] is set to the current hour and the [minute] is set to the
/// current minute in the local time zone.
factory TimeOfDay.now() { return new TimeOfDay.fromDateTime(new DateTime.now()); }
/// Returns a new TimeOfDay with the hour and/or minute replaced.
TimeOfDay replacing({ int hour, int minute }) {
assert(hour == null || (hour >= 0 && hour < hoursPerDay));
assert(minute == null || (minute >= 0 && minute < minutesPerHour));
return new TimeOfDay(hour: hour ?? this.hour, minute: minute ?? this.minute);
}
/// The selected hour, in 24 hour time from 0..23.
final int hour;
/// The selected minute.
final int minute;
/// Whether this time of day is before or after noon.
DayPeriod get period => hour < hoursPerPeriod ? DayPeriod.am : DayPeriod.pm;
/// Which hour of the current period (e.g., am or pm) this time is.
int get hourOfPeriod => hour - periodOffset;
/// The hour at which the current period starts.
int get periodOffset => period == DayPeriod.am ? 0 : hoursPerPeriod;
/// Returns the localized string representation of this time of day.
///
/// This is a shortcut for [MaterialLocalizations.formatTimeOfDay].
String format(BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
return localizations.formatTimeOfDay(this);
}
@override
bool operator ==(dynamic other) {
if (other is! TimeOfDay)
return false;
final TimeOfDay typedOther = other;
return typedOther.hour == hour
&& typedOther.minute == minute;
}
@override
int get hashCode => hashValues(hour, minute);
@override
String toString() {
String _addLeadingZeroIfNeeded(int value) {
if (value < 10)
return '0$value';
return value.toString();
}
final String hourLabel = _addLeadingZeroIfNeeded(hour);
final String minuteLabel = _addLeadingZeroIfNeeded(minute);
return '$TimeOfDay($hourLabel:$minuteLabel)';
}
}
/// Determines how the time picker invoked using [showTimePicker] formats and
/// lays out the time controls.
///
/// The time picker provides layout configurations optimized for each of the
/// enum values.
enum TimeOfDayFormat {
/// Corresponds to the ICU 'HH:mm' pattern.
///
/// This format uses 24-hour two-digit zero-padded hours. Controls are always
/// laid out horizontally. Hours are separated from minutes by one colon
/// character.
HH_colon_mm,
/// Corresponds to the ICU 'HH.mm' pattern.
///
/// This format uses 24-hour two-digit zero-padded hours. Controls are always
/// laid out horizontally. Hours are separated from minutes by one dot
/// character.
HH_dot_mm,
/// Corresponds to the ICU "HH 'h' mm" pattern used in Canadian French.
///
/// This format uses 24-hour two-digit zero-padded hours. Controls are always
/// laid out horizontally. Hours are separated from minutes by letter 'h'.
frenchCanadian,
/// Corresponds to the ICU 'H:mm' pattern.
///
/// This format uses 24-hour non-padded variable-length hours. Controls are
/// always laid out horizontally. Hours are separated from minutes by one
/// colon character.
H_colon_mm,
/// Corresponds to the ICU 'h:mm a' pattern.
///
/// This format uses 12-hour non-padded variable-length hours with a day
/// period. Controls are laid out horizontally in portrait mode. In landscape
/// mode, the day period appears vertically after (consistent with the ambient
/// [TextDirection]) hour-minute indicator. Hours are separated from minutes
/// by one colon character.
h_colon_mm_space_a,
/// Corresponds to the ICU 'a h:mm' pattern.
///
/// This format uses 12-hour non-padded variable-length hours with a day
/// period. Controls are laid out horizontally in portrait mode. In landscape
/// mode, the day period appears vertically before (consistent with the
/// ambient [TextDirection]) hour-minute indicator. Hours are separated from
/// minutes by one colon character.
a_space_h_colon_mm,
}
/// Describes how hours are formatted.
enum HourFormat {
/// Zero-padded two-digit 24-hour format ranging from "00" to "23".
HH,
/// Non-padded variable-length 24-hour format ranging from "0" to "23".
H,
/// Non-padded variable-length hour in day period format ranging from "1" to
/// "12".
h,
}
/// The [HourFormat] used for the given [TimeOfDayFormat].
HourFormat hourFormat({ @required TimeOfDayFormat of }) {
switch (of) {
case TimeOfDayFormat.h_colon_mm_space_a:
case TimeOfDayFormat.a_space_h_colon_mm:
return HourFormat.h;
case TimeOfDayFormat.H_colon_mm:
return HourFormat.H;
case TimeOfDayFormat.HH_dot_mm:
case TimeOfDayFormat.HH_colon_mm:
case TimeOfDayFormat.frenchCanadian:
return HourFormat.HH;
}
return null;
}
......@@ -17,103 +17,13 @@ import 'feedback.dart';
import 'flat_button.dart';
import 'material_localizations.dart';
import 'theme.dart';
import 'time.dart';
import 'typography.dart';
const Duration _kDialAnimateDuration = const Duration(milliseconds: 200);
const double _kTwoPi = 2 * math.PI;
const int _kHoursPerDay = 24;
const int _kHoursPerPeriod = 12;
const int _kMinutesPerHour = 60;
const Duration _kVibrateCommitDelay = const Duration(milliseconds: 100);
/// Whether the [TimeOfDay] is before or after noon.
enum DayPeriod {
/// Ante meridiem (before noon).
am,
/// Post meridiem (after noon).
pm,
}
/// A value representing a time during the day.
@immutable
class TimeOfDay {
/// Creates a time of day.
///
/// The [hour] argument must be between 0 and 23, inclusive. The [minute]
/// argument must be between 0 and 59, inclusive.
const TimeOfDay({ @required this.hour, @required this.minute });
/// Creates a time of day based on the given time.
///
/// The [hour] is set to the time's hour and the [minute] is set to the time's
/// minute in the timezone of the given [DateTime].
TimeOfDay.fromDateTime(DateTime time) : hour = time.hour, minute = time.minute;
/// Creates a time of day based on the current time.
///
/// The [hour] is set to the current hour and the [minute] is set to the
/// current minute in the local time zone.
factory TimeOfDay.now() { return new TimeOfDay.fromDateTime(new DateTime.now()); }
/// Returns a new TimeOfDay with the hour and/or minute replaced.
TimeOfDay replacing({ int hour, int minute }) {
assert(hour == null || (hour >= 0 && hour < _kHoursPerDay));
assert(minute == null || (minute >= 0 && minute < _kMinutesPerHour));
return new TimeOfDay(hour: hour ?? this.hour, minute: minute ?? this.minute);
}
/// The selected hour, in 24 hour time from 0..23.
final int hour;
/// The selected minute.
final int minute;
/// Whether this time of day is before or after noon.
DayPeriod get period => hour < _kHoursPerPeriod ? DayPeriod.am : DayPeriod.pm;
/// Which hour of the current period (e.g., am or pm) this time is.
int get hourOfPeriod => hour - periodOffset;
String _addLeadingZeroIfNeeded(int value) {
if (value < 10)
return '0$value';
return value.toString();
}
/// A string representing the hour, in 24 hour time (e.g., '04' or '18').
String get hourLabel => _addLeadingZeroIfNeeded(hour);
/// A string representing the minute (e.g., '07').
String get minuteLabel => _addLeadingZeroIfNeeded(minute);
/// A string representing the hour of the current period (e.g., '4' or '6').
String get hourOfPeriodLabel {
final int hourOfPeriod = this.hourOfPeriod;
if (hourOfPeriod == 0)
return '12';
return hourOfPeriod.toString();
}
/// The hour at which the current period starts.
int get periodOffset => period == DayPeriod.am ? 0 : _kHoursPerPeriod;
@override
bool operator ==(dynamic other) {
if (other is! TimeOfDay)
return false;
final TimeOfDay typedOther = other;
return typedOther.hour == hour
&& typedOther.minute == minute;
}
@override
int get hashCode => hashValues(hour, minute);
@override
String toString() => '$hourLabel:$minuteLabel';
}
enum _TimePickerMode { hour, minute }
const double _kTimePickerHeaderPortraitHeight = 96.0;
......@@ -181,19 +91,6 @@ class _TimePickerFragmentContext {
final ValueChanged<_TimePickerMode> onModeChange;
}
/// Describes how hours are formatted.
enum _TimePickerHourFormat {
/// Zero-padded two-digit 24-hour format ranging from "00" to "23".
HH,
/// Non-padded variable-length 24-hour format ranging from "0" to "23".
H,
/// Non-padded variable-length hour in day period format ranging from "1" to
/// "12".
h,
}
/// Contains the [widget] and layout properties of an atom of time information,
/// such as am/pm indicator, hour, minute and string literals appearing in the
/// formatted time string.
......@@ -287,7 +184,7 @@ class _DayPeriodControl extends StatelessWidget {
final _TimePickerFragmentContext fragmentContext;
void _handleChangeDayPeriod() {
final int newHour = (fragmentContext.selectedTime.hour + _kHoursPerPeriod) % _kHoursPerDay;
final int newHour = (fragmentContext.selectedTime.hour + TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerDay;
fragmentContext.onTimeChange(fragmentContext.selectedTime.replacing(hour: newHour));
}
......@@ -331,32 +228,20 @@ class _HourControl extends StatelessWidget {
});
final _TimePickerFragmentContext fragmentContext;
final _TimePickerHourFormat hourFormat;
final HourFormat hourFormat;
@override
Widget build(BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final TextStyle hourStyle = fragmentContext.mode == _TimePickerMode.hour
? fragmentContext.activeStyle
: fragmentContext.inactiveStyle;
return new GestureDetector(
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.hour), context),
child: new Text(_formatHour(), style: hourStyle),
child: new Text(localizations.formatHour(fragmentContext.selectedTime), style: hourStyle),
);
}
String _formatHour() {
assert(hourFormat != null);
switch (hourFormat) {
case _TimePickerHourFormat.HH:
return fragmentContext.selectedTime.hourLabel;
case _TimePickerHourFormat.H:
return fragmentContext.selectedTime.hour.toString();
case _TimePickerHourFormat.h:
return fragmentContext.selectedTime.hourOfPeriodLabel;
}
return null;
}
}
/// A passive fragment showing a string value.
......@@ -387,40 +272,25 @@ class _MinuteControl extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final TextStyle minuteStyle = fragmentContext.mode == _TimePickerMode.minute
? fragmentContext.activeStyle
: fragmentContext.inactiveStyle;
return new GestureDetector(
onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.minute), context),
child: new Text(fragmentContext.selectedTime.minuteLabel, style: minuteStyle),
child: new Text(localizations.formatMinute(fragmentContext.selectedTime), style: minuteStyle),
);
}
}
_TimePickerHourFormat _getHourFormat(TimeOfDayFormat format) {
switch (format) {
case TimeOfDayFormat.h_colon_mm_space_a:
case TimeOfDayFormat.a_space_h_colon_mm:
return _TimePickerHourFormat.h;
case TimeOfDayFormat.H_colon_mm:
return _TimePickerHourFormat.H;
case TimeOfDayFormat.HH_dot_mm:
case TimeOfDayFormat.HH_colon_mm:
case TimeOfDayFormat.frenchCanadian:
return _TimePickerHourFormat.HH;
}
return null;
}
/// Provides time picker header layout configuration for the given
/// [timeOfDayFormat] passing [context] to each widget in the configuration.
///
/// [timeOfDayFormat] and [context] must not be `null`.
_TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _TimePickerFragmentContext context) {
// Creates an hour fragment.
_TimePickerHeaderFragment hour(_TimePickerHourFormat hourFormat) {
_TimePickerHeaderFragment hour(HourFormat hourFormat) {
return new _TimePickerHeaderFragment(
layoutId: _TimePickerHeaderId.hour,
widget: new _HourControl(fragmentContext: context, hourFormat: hourFormat),
......@@ -494,7 +364,7 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim
0,
piece(
pivotIndex: 1,
fragment1: hour(_TimePickerHourFormat.h),
fragment1: hour(HourFormat.h),
fragment2: string(_TimePickerHeaderId.colon, ':'),
fragment3: minute(),
),
......@@ -506,14 +376,14 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim
case TimeOfDayFormat.H_colon_mm:
return format(0, piece(
pivotIndex: 1,
fragment1: hour(_TimePickerHourFormat.H),
fragment1: hour(HourFormat.H),
fragment2: string(_TimePickerHeaderId.colon, ':'),
fragment3: minute(),
));
case TimeOfDayFormat.HH_dot_mm:
return format(0, piece(
pivotIndex: 1,
fragment1: hour(_TimePickerHourFormat.HH),
fragment1: hour(HourFormat.HH),
fragment2: string(_TimePickerHeaderId.dot, '.'),
fragment3: minute(),
));
......@@ -526,7 +396,7 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim
),
piece(
pivotIndex: 1,
fragment1: hour(_TimePickerHourFormat.h),
fragment1: hour(HourFormat.h),
fragment2: string(_TimePickerHeaderId.colon, ':'),
fragment3: minute(),
),
......@@ -534,14 +404,14 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim
case TimeOfDayFormat.frenchCanadian:
return format(0, piece(
pivotIndex: 1,
fragment1: hour(_TimePickerHourFormat.HH),
fragment1: hour(HourFormat.HH),
fragment2: string(_TimePickerHeaderId.hString, 'h'),
fragment3: minute(),
));
case TimeOfDayFormat.HH_colon_mm:
return format(0, piece(
pivotIndex: 1,
fragment1: hour(_TimePickerHourFormat.HH),
fragment1: hour(HourFormat.HH),
fragment2: string(_TimePickerHeaderId.colon, ':'),
fragment3: minute(),
));
......@@ -801,26 +671,65 @@ enum _DialRing {
inner,
}
List<TextPainter> _initHours(TextTheme textTheme, _DialRing ring, bool is24h) {
const List<String> amHours = const <String>[
'12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'
];
const List<String> pmHours = const <String>[
'00', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23'
];
switch (ring) {
case _DialRing.outer:
return _initPainters(textTheme, is24h ? pmHours : amHours);
case _DialRing.inner:
return is24h ? _initPainters(textTheme, amHours) : null;
}
return null;
const List<TimeOfDay> _amHours = const <TimeOfDay>[
const TimeOfDay(hour: 0, minute: 0),
const TimeOfDay(hour: 1, minute: 0),
const TimeOfDay(hour: 2, minute: 0),
const TimeOfDay(hour: 3, minute: 0),
const TimeOfDay(hour: 4, minute: 0),
const TimeOfDay(hour: 5, minute: 0),
const TimeOfDay(hour: 6, minute: 0),
const TimeOfDay(hour: 7, minute: 0),
const TimeOfDay(hour: 8, minute: 0),
const TimeOfDay(hour: 9, minute: 0),
const TimeOfDay(hour: 10, minute: 0),
const TimeOfDay(hour: 11, minute: 0),
];
const List<TimeOfDay> _pmHours = const <TimeOfDay>[
const TimeOfDay(hour: 12, minute: 0),
const TimeOfDay(hour: 13, minute: 0),
const TimeOfDay(hour: 14, minute: 0),
const TimeOfDay(hour: 15, minute: 0),
const TimeOfDay(hour: 16, minute: 0),
const TimeOfDay(hour: 17, minute: 0),
const TimeOfDay(hour: 18, minute: 0),
const TimeOfDay(hour: 19, minute: 0),
const TimeOfDay(hour: 20, minute: 0),
const TimeOfDay(hour: 21, minute: 0),
const TimeOfDay(hour: 22, minute: 0),
const TimeOfDay(hour: 23, minute: 0),
];
List<TextPainter> _init24HourInnerRing(TextTheme textTheme, MaterialLocalizations localizations) {
return _initPainters(textTheme, _amHours.map(localizations.formatHour).toList());
}
List<TextPainter> _init24HourOuterRing(TextTheme textTheme, MaterialLocalizations localizations) {
return _initPainters(textTheme, _pmHours.map(localizations.formatHour).toList());
}
List<TextPainter> _initMinutes(TextTheme textTheme) {
return _initPainters(textTheme, <String>[
'00', '05', '10', '15', '20', '25', '30', '35', '40', '45', '50', '55'
]);
List<TextPainter> _init12HourOuterRing(TextTheme textTheme, MaterialLocalizations localizations) {
return _initPainters(textTheme, _amHours.map(localizations.formatHour).toList());
}
const List<TimeOfDay> _minuteMarkerValues = const <TimeOfDay>[
const TimeOfDay(hour: 0, minute: 0),
const TimeOfDay(hour: 0, minute: 5),
const TimeOfDay(hour: 0, minute: 10),
const TimeOfDay(hour: 0, minute: 15),
const TimeOfDay(hour: 0, minute: 20),
const TimeOfDay(hour: 0, minute: 25),
const TimeOfDay(hour: 0, minute: 30),
const TimeOfDay(hour: 0, minute: 35),
const TimeOfDay(hour: 0, minute: 40),
const TimeOfDay(hour: 0, minute: 45),
const TimeOfDay(hour: 0, minute: 50),
const TimeOfDay(hour: 0, minute: 55),
];
List<TextPainter> _initMinutes(TextTheme textTheme, MaterialLocalizations localizations) {
return _initPainters(textTheme, _minuteMarkerValues.map(localizations.formatMinute).toList());
}
class _DialPainter extends CustomPainter {
......@@ -992,21 +901,21 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
double _getThetaForTime(TimeOfDay time) {
final double fraction = (widget.mode == _TimePickerMode.hour) ?
(time.hour / _kHoursPerPeriod) % _kHoursPerPeriod :
(time.minute / _kMinutesPerHour) % _kMinutesPerHour;
(time.hour / TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerPeriod :
(time.minute / TimeOfDay.minutesPerHour) % TimeOfDay.minutesPerHour;
return (math.PI / 2.0 - fraction * _kTwoPi) % _kTwoPi;
}
TimeOfDay _getTimeForTheta(double theta) {
final double fraction = (0.25 - (theta % _kTwoPi) / _kTwoPi) % 1.0;
if (widget.mode == _TimePickerMode.hour) {
int newHour = (fraction * _kHoursPerPeriod).round() % _kHoursPerPeriod;
int newHour = (fraction * TimeOfDay.hoursPerPeriod).round() % TimeOfDay.hoursPerPeriod;
if (widget.is24h) {
if (_activeRing == _DialRing.outer) {
if (newHour != 0)
newHour = (newHour + _kHoursPerPeriod) % _kHoursPerDay;
newHour = (newHour + TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerDay;
} else if (newHour == 0) {
newHour = _kHoursPerPeriod;
newHour = TimeOfDay.hoursPerPeriod;
}
} else {
newHour = newHour + widget.selectedTime.periodOffset;
......@@ -1014,7 +923,7 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
return widget.selectedTime.replacing(hour: newHour);
} else {
return widget.selectedTime.replacing(
minute: (fraction * _kMinutesPerHour).round() % _kMinutesPerHour
minute: (fraction * TimeOfDay.minutesPerHour).round() % TimeOfDay.minutesPerHour
);
}
}
......@@ -1076,6 +985,7 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
Color backgroundColor;
switch (themeData.brightness) {
......@@ -1094,15 +1004,20 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
List<TextPainter> secondaryInnerLabels;
switch (widget.mode) {
case _TimePickerMode.hour:
primaryOuterLabels = _initHours(theme.textTheme, _DialRing.outer, widget.is24h);
secondaryOuterLabels = _initHours(theme.accentTextTheme, _DialRing.outer, widget.is24h);
primaryInnerLabels = _initHours(theme.textTheme, _DialRing.inner, widget.is24h);
secondaryInnerLabels = _initHours(theme.accentTextTheme, _DialRing.inner, widget.is24h);
if (widget.is24h) {
primaryOuterLabels = _init24HourOuterRing(theme.textTheme, localizations);
secondaryOuterLabels = _init24HourOuterRing(theme.accentTextTheme, localizations);
primaryInnerLabels = _init24HourInnerRing(theme.textTheme, localizations);
secondaryInnerLabels = _init24HourInnerRing(theme.accentTextTheme, localizations);
} else {
primaryOuterLabels = _init12HourOuterRing(theme.textTheme, localizations);
secondaryOuterLabels = _init12HourOuterRing(theme.accentTextTheme, localizations);
}
break;
case _TimePickerMode.minute:
primaryOuterLabels = _initMinutes(theme.textTheme);
primaryOuterLabels = _initMinutes(theme.textTheme, localizations);
primaryInnerLabels = null;
secondaryOuterLabels = _initMinutes(theme.accentTextTheme);
secondaryOuterLabels = _initMinutes(theme.accentTextTheme, localizations);
secondaryInnerLabels = null;
break;
}
......@@ -1191,7 +1106,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
@override
Widget build(BuildContext context) {
final TimeOfDayFormat timeOfDayFormat = MaterialLocalizations.of(context).timeOfDayFormat;
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final TimeOfDayFormat timeOfDayFormat = localizations.timeOfDayFormat;
final Widget picker = new Padding(
padding: const EdgeInsets.all(16.0),
......@@ -1199,14 +1115,13 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
aspectRatio: 1.0,
child: new _Dial(
mode: _mode,
is24h: _getHourFormat(timeOfDayFormat) != _TimePickerHourFormat.h,
is24h: hourFormat(of: timeOfDayFormat) != HourFormat.h,
selectedTime: _selectedTime,
onChanged: _handleTimeChanged,
)
)
);
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final Widget actions = new ButtonTheme.bar(
child: new ButtonBar(
children: <Widget>[
......@@ -1309,6 +1224,6 @@ Future<TimeOfDay> showTimePicker({
assert(initialTime != null);
return await showDialog(
context: context,
child: new _TimePickerDialog(initialTime: initialTime)
child: new _TimePickerDialog(initialTime: initialTime),
);
}
......@@ -330,7 +330,7 @@ class Localizations extends StatefulWidget {
/// Overrides the inherited [Locale] or [LocalizationsDelegate]s for `child`.
///
/// This factory constructor is used for the (usually rare) situtation where part
/// This factory constructor is used for the (usually rare) situation where part
/// of an app should be localized for a different locale than the one defined
/// for the device, or if its localizations should come from a different list
/// of [LocalizationsDelegate]s than the list defined by
......
......@@ -32,6 +32,119 @@ void main() {
final LocalizationTrackerState innerTracker = tester.state(find.byKey(const ValueKey<String>('inner')));
expect(innerTracker.captionFontSize, 13.0);
});
group(DefaultMaterialLocalizations, () {
test('uses exact locale when exists', () {
final DefaultMaterialLocalizations localizations = new DefaultMaterialLocalizations(const Locale('pt', 'PT'));
expect(localizations.formatDecimal(10000), '10\u00A0000');
});
test('falls back to language code when exact locale is missing', () {
final DefaultMaterialLocalizations localizations = new DefaultMaterialLocalizations(const Locale('pt', 'XX'));
expect(localizations.formatDecimal(10000), '10.000');
});
test('falls back to default format when neither language code nor exact locale are available', () {
final DefaultMaterialLocalizations localizations = new DefaultMaterialLocalizations(const Locale('xx', 'XX'));
expect(localizations.formatDecimal(10000), '10,000');
});
group('formatHour', () {
test('formats h', () {
DefaultMaterialLocalizations localizations;
localizations = new DefaultMaterialLocalizations(const Locale('en', 'US'));
expect(localizations.formatHour(const TimeOfDay(hour: 10, minute: 0)), '10');
expect(localizations.formatHour(const TimeOfDay(hour: 20, minute: 0)), '8');
localizations = new DefaultMaterialLocalizations(const Locale('ar', ''));
expect(localizations.formatHour(const TimeOfDay(hour: 10, minute: 0)), '١٠');
expect(localizations.formatHour(const TimeOfDay(hour: 20, minute: 0)), '٨');
});
test('formats HH', () {
DefaultMaterialLocalizations localizations;
localizations = new DefaultMaterialLocalizations(const Locale('de', ''));
expect(localizations.formatHour(const TimeOfDay(hour: 9, minute: 0)), '09');
expect(localizations.formatHour(const TimeOfDay(hour: 20, minute: 0)), '20');
localizations = new DefaultMaterialLocalizations(const Locale('en', 'GB'));
expect(localizations.formatHour(const TimeOfDay(hour: 9, minute: 0)), '09');
expect(localizations.formatHour(const TimeOfDay(hour: 20, minute: 0)), '20');
});
test('formats H', () {
DefaultMaterialLocalizations localizations;
localizations = new DefaultMaterialLocalizations(const Locale('es', ''));
expect(localizations.formatHour(const TimeOfDay(hour: 9, minute: 0)), '9');
expect(localizations.formatHour(const TimeOfDay(hour: 20, minute: 0)), '20');
localizations = new DefaultMaterialLocalizations(const Locale('fa', ''));
expect(localizations.formatHour(const TimeOfDay(hour: 9, minute: 0)), '۹');
expect(localizations.formatHour(const TimeOfDay(hour: 20, minute: 0)), '۲۰');
});
});
group('formatMinute', () {
test('formats English', () {
final DefaultMaterialLocalizations localizations = new DefaultMaterialLocalizations(const Locale('en', 'US'));
expect(localizations.formatMinute(const TimeOfDay(hour: 1, minute: 32)), '32');
});
test('formats Arabic', () {
final DefaultMaterialLocalizations localizations = new DefaultMaterialLocalizations(const Locale('ar', ''));
expect(localizations.formatMinute(const TimeOfDay(hour: 1, minute: 32)), '٣٢');
});
});
group('formatTimeOfDay', () {
test('formats ${TimeOfDayFormat.h_colon_mm_space_a}', () {
DefaultMaterialLocalizations localizations;
localizations = new DefaultMaterialLocalizations(const Locale('ar', ''));
expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '٩:٣٢ ص');
localizations = new DefaultMaterialLocalizations(const Locale('en', ''));
expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '9:32 AM');
});
test('formats ${TimeOfDayFormat.HH_colon_mm}', () {
DefaultMaterialLocalizations localizations;
localizations = new DefaultMaterialLocalizations(const Locale('de', ''));
expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '09:32');
localizations = new DefaultMaterialLocalizations(const Locale('en', 'ZA'));
expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '09:32');
});
test('formats ${TimeOfDayFormat.H_colon_mm}', () {
DefaultMaterialLocalizations localizations;
localizations = new DefaultMaterialLocalizations(const Locale('es', ''));
expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '9:32');
localizations = new DefaultMaterialLocalizations(const Locale('ja', ''));
expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '9:32');
});
test('formats ${TimeOfDayFormat.frenchCanadian}', () {
DefaultMaterialLocalizations localizations;
localizations = new DefaultMaterialLocalizations(const Locale('fr', 'CA'));
expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '09 h 32');
});
test('formats ${TimeOfDayFormat.a_space_h_colon_mm}', () {
DefaultMaterialLocalizations localizations;
localizations = new DefaultMaterialLocalizations(const Locale('zh', ''));
expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '上午 9:32');
});
});
});
}
class LocalizationTracker extends StatefulWidget {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment