diff --git a/dev/automated_tests/pubspec.yaml b/dev/automated_tests/pubspec.yaml index 7da920780f..7538d76088 100644 --- a/dev/automated_tests/pubspec.yaml +++ b/dev/automated_tests/pubspec.yaml @@ -7,7 +7,7 @@ dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/dev/benchmarks/complex_layout/pubspec.yaml b/dev/benchmarks/complex_layout/pubspec.yaml index 37c10003a2..dca5efa9fb 100644 --- a/dev/benchmarks/complex_layout/pubspec.yaml +++ b/dev/benchmarks/complex_layout/pubspec.yaml @@ -19,7 +19,7 @@ dev_dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/dev/benchmarks/microbenchmarks/pubspec.yaml b/dev/benchmarks/microbenchmarks/pubspec.yaml index cfedc59697..df3d8a23ae 100644 --- a/dev/benchmarks/microbenchmarks/pubspec.yaml +++ b/dev/benchmarks/microbenchmarks/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/dev/bots/pubspec.yaml b/dev/bots/pubspec.yaml index acdc1009d6..35263c7fa4 100644 --- a/dev/bots/pubspec.yaml +++ b/dev/bots/pubspec.yaml @@ -6,7 +6,7 @@ dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/dev/bots/test.dart b/dev/bots/test.dart index c12ab271cc..c3fffa71b2 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -57,13 +57,13 @@ Future _verifyInternationalizations() async { dart, [ path.join('dev', 'tools', 'gen_localizations.dart'), - path.join('packages', 'flutter', 'lib', 'src', 'material', 'i18n'), + path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'), 'material' ], workingDirectory: flutterRoot, ); - final String localizationsFile = path.join('packages', 'flutter', 'lib', 'src', 'material', 'i18n', 'localizations.dart'); + final String localizationsFile = path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n', 'localizations.dart'); final EvalResult sourceContents = await _evalCommand( 'cat', @@ -156,6 +156,7 @@ Future _runTests() async { // Run tests. await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter')); + await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_localizations')); await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_driver')); await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_test')); await _pubRunTest(path.join(flutterRoot, 'packages', 'flutter_tools')); diff --git a/dev/devicelab/pubspec.yaml b/dev/devicelab/pubspec.yaml index f7618e7efd..62f14a48f2 100644 --- a/dev/devicelab/pubspec.yaml +++ b/dev/devicelab/pubspec.yaml @@ -24,7 +24,7 @@ dev_dependencies: archive: 1.0.31 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY boolean_selector: 1.0.2 # TRANSITIVE DEPENDENCY browser: 0.10.0+2 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY diff --git a/dev/integration_tests/channels/pubspec.yaml b/dev/integration_tests/channels/pubspec.yaml index 08af6303ae..38da5a13b1 100644 --- a/dev/integration_tests/channels/pubspec.yaml +++ b/dev/integration_tests/channels/pubspec.yaml @@ -9,7 +9,7 @@ dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/dev/integration_tests/flavors/pubspec.yaml b/dev/integration_tests/flavors/pubspec.yaml index 1d29942eeb..1e9d71efaf 100644 --- a/dev/integration_tests/flavors/pubspec.yaml +++ b/dev/integration_tests/flavors/pubspec.yaml @@ -9,7 +9,7 @@ dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/dev/integration_tests/platform_interaction/pubspec.yaml b/dev/integration_tests/platform_interaction/pubspec.yaml index 20d2a559bf..6986c44b60 100644 --- a/dev/integration_tests/platform_interaction/pubspec.yaml +++ b/dev/integration_tests/platform_interaction/pubspec.yaml @@ -9,7 +9,7 @@ dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/dev/integration_tests/ui/pubspec.yaml b/dev/integration_tests/ui/pubspec.yaml index 7b072fb1dd..ddc639898a 100644 --- a/dev/integration_tests/ui/pubspec.yaml +++ b/dev/integration_tests/ui/pubspec.yaml @@ -14,7 +14,7 @@ dev_dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY boolean_selector: 1.0.2 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY diff --git a/dev/manual_tests/pubspec.yaml b/dev/manual_tests/pubspec.yaml index 025859867e..11e8a3c069 100644 --- a/dev/manual_tests/pubspec.yaml +++ b/dev/manual_tests/pubspec.yaml @@ -10,7 +10,7 @@ dev_dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/dev/tools/pubspec.yaml b/dev/tools/pubspec.yaml index 2f8c3d8d17..d99e9c554d 100644 --- a/dev/tools/pubspec.yaml +++ b/dev/tools/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: path: 1.4.2 async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/examples/catalog/pubspec.yaml b/examples/catalog/pubspec.yaml index 38befc84d7..08f696a51d 100644 --- a/examples/catalog/pubspec.yaml +++ b/examples/catalog/pubspec.yaml @@ -13,7 +13,7 @@ dev_dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/examples/flutter_gallery/pubspec.yaml b/examples/flutter_gallery/pubspec.yaml index e6b6bba0ad..07291c7009 100644 --- a/examples/flutter_gallery/pubspec.yaml +++ b/examples/flutter_gallery/pubspec.yaml @@ -21,7 +21,7 @@ dev_dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY convert: 2.0.1 # TRANSITIVE DEPENDENCY diff --git a/examples/flutter_view/pubspec.yaml b/examples/flutter_view/pubspec.yaml index dbf42b4cb2..09acc0a5fa 100644 --- a/examples/flutter_view/pubspec.yaml +++ b/examples/flutter_view/pubspec.yaml @@ -7,7 +7,7 @@ dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/examples/hello_world/pubspec.yaml b/examples/hello_world/pubspec.yaml index 18731f52ac..26354a1474 100644 --- a/examples/hello_world/pubspec.yaml +++ b/examples/hello_world/pubspec.yaml @@ -10,7 +10,7 @@ dev_dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/examples/layers/pubspec.yaml b/examples/layers/pubspec.yaml index c3838cbe12..5eba9e0e51 100644 --- a/examples/layers/pubspec.yaml +++ b/examples/layers/pubspec.yaml @@ -9,7 +9,7 @@ dev_dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/examples/platform_channel/pubspec.yaml b/examples/platform_channel/pubspec.yaml index d4e817c660..8ee0ce4225 100644 --- a/examples/platform_channel/pubspec.yaml +++ b/examples/platform_channel/pubspec.yaml @@ -12,7 +12,7 @@ dev_dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/examples/platform_channel_swift/pubspec.yaml b/examples/platform_channel_swift/pubspec.yaml index 07e4e6e210..db6b961813 100644 --- a/examples/platform_channel_swift/pubspec.yaml +++ b/examples/platform_channel_swift/pubspec.yaml @@ -12,7 +12,7 @@ dev_dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/examples/platform_view/pubspec.yaml b/examples/platform_view/pubspec.yaml index f8a36f76e9..a21fcf2379 100644 --- a/examples/platform_view/pubspec.yaml +++ b/examples/platform_view/pubspec.yaml @@ -6,7 +6,7 @@ dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/examples/stocks/lib/main.dart b/examples/stocks/lib/main.dart index 8364d466d1..a89769ab76 100644 --- a/examples/stocks/lib/main.dart +++ b/examples/stocks/lib/main.dart @@ -13,6 +13,7 @@ import 'package:flutter/rendering.dart' show debugPaintLayerBordersEnabled, debugPaintPointersEnabled, debugRepaintRainbowEnabled; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'stock_data.dart'; import 'stock_home.dart'; @@ -118,8 +119,10 @@ class StocksAppState extends State { return new MaterialApp( title: 'Stocks', theme: theme, - localizationsDelegates: <_StocksLocalizationsDelegate>[ + localizationsDelegates: >[ new _StocksLocalizationsDelegate(), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, ], supportedLocales: const [ const Locale('en', 'US'), diff --git a/examples/stocks/pubspec.yaml b/examples/stocks/pubspec.yaml index 75979509b9..ca7614786c 100644 --- a/examples/stocks/pubspec.yaml +++ b/examples/stocks/pubspec.yaml @@ -2,6 +2,8 @@ name: stocks dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter intl: 0.15.1 intl_translation: 0.15.0 http: 0.11.3+14 @@ -15,7 +17,7 @@ dev_dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY collection: 1.14.3 # TRANSITIVE DEPENDENCY diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index 78c0a36c81..fbf61a33a7 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -2,8 +2,6 @@ // 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/rendering.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -26,17 +24,6 @@ const TextStyle _errorTextStyle = const TextStyle( decorationStyle: TextDecorationStyle.double ); -class _MaterialLocalizationsDelegate extends LocalizationsDelegate { - const _MaterialLocalizationsDelegate(); - - @override - Future load(Locale locale) => DefaultMaterialLocalizations.load(locale); - - @override - bool shouldReload(_MaterialLocalizationsDelegate old) => false; -} - - /// An application that uses material design. /// /// A convenience widget that wraps a number of widgets that are commonly @@ -463,7 +450,7 @@ class _MaterialAppState extends State { Iterable> get _localizationsDelegates sync* { if (widget.localizationsDelegates != null) yield* widget.localizationsDelegates; - yield const _MaterialLocalizationsDelegate(); + yield DefaultMaterialLocalizations.delegate; } RectTween _createRectTween(Rect begin, Rect end) { diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart index 91c5a44545..60616e00c5 100644 --- a/packages/flutter/lib/src/material/date_picker.dart +++ b/packages/flutter/lib/src/material/date_picker.dart @@ -295,8 +295,8 @@ class DayPicker extends StatelessWidget { List _getDayHeaders(TextStyle headerStyle, MaterialLocalizations localizations) { final List result = []; for (int i = localizations.firstDayOfWeekIndex; true; i = (i + 1) % 7) { - final String weekDay = localizations.narrowWeekDays[i]; - result.add(new Center(child: new Text(weekDay, style: headerStyle))); + final String weekday = localizations.narrowWeekdays[i]; + result.add(new Center(child: new Text(weekday, style: headerStyle))); if (i == (localizations.firstDayOfWeekIndex - 1) % 7) break; } @@ -350,19 +350,19 @@ class DayPicker extends StatelessWidget { /// - [DateTime.weekday] provides a 1-based index into days of week, with 1 /// falling on Monday. /// - [MaterialLocalizations.firstDayOfWeekIndex] provides a 0-based index - /// into the [MaterialLocalizations.narrowWeekDays] list. - /// - [MaterialLocalizations.narrowWeekDays] list provides localized names of + /// into the [MaterialLocalizations.narrowWeekdays] list. + /// - [MaterialLocalizations.narrowWeekdays] list provides localized names of /// days of week, always starting with Sunday and ending with Saturday. int _computeFirstDayOffset(int year, int month, MaterialLocalizations localizations) { // 0-based day of week, with 0 representing Monday. - final int weekDayFromMonday = new DateTime(year, month).weekday - 1; + final int weekdayFromMonday = new DateTime(year, month).weekday - 1; // 0-based day of week, with 0 representing Sunday. final int firstDayOfWeekFromSunday = localizations.firstDayOfWeekIndex; // firstDayOfWeekFromSunday recomputed to be Monday-based final int firstDayOfWeekFromMonday = (firstDayOfWeekFromSunday - 1) % 7; // Number of days between the first day of week appearing on the calendar, // and the day corresponding to the 1-st of the month. - return (weekDayFromMonday - firstDayOfWeekFromMonday) % 7; + return (weekdayFromMonday - firstDayOfWeekFromMonday) % 7; } @override diff --git a/packages/flutter/lib/src/material/material_localizations.dart b/packages/flutter/lib/src/material/material_localizations.dart index 690e060f70..c872dbb99e 100644 --- a/packages/flutter/lib/src/material/material_localizations.dart +++ b/packages/flutter/lib/src/material/material_localizations.dart @@ -6,11 +6,7 @@ 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'; @@ -18,8 +14,10 @@ import 'typography.dart'; /// /// See also: /// -/// * [DefaultMaterialLocalizations], which implements this interface -/// and supports a variety of locales. +/// * [DefaultMaterialLocalizations], the default, English-only, implementation +/// of this interface. +/// * [GlobalMaterialLocalizations], which provides material localizations for +/// many languages. abstract class MaterialLocalizations { /// The tooltip for the leading [AppBar] menu (aka 'hamburger') button. String get openAppDrawerTooltip; @@ -155,17 +153,17 @@ abstract class MaterialLocalizations { /// - 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 get narrowWeekDays; + List 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: + /// 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]; + /// var firstDayOfWeek = localizations.narrowWeekdays[localizations.firstDayOfWeekIndex]; /// ``` int get firstDayOfWeekIndex; @@ -186,173 +184,118 @@ abstract class MaterialLocalizations { } } -/// Localized strings for the material widgets. +class _MaterialLocalizationsDelegate extends LocalizationsDelegate { + const _MaterialLocalizationsDelegate(); + + @override + Future load(Locale locale) => DefaultMaterialLocalizations.load(locale); + + @override + bool shouldReload(_MaterialLocalizationsDelegate old) => false; +} + +/// US English strings for the material widgets. +/// +/// See also: +/// +/// * [GlobalMaterialLocalizations], which provides material localizations for +/// many languages. +/// * [MaterialApp.delegates], which automatically includes +/// [DefaultMaterialLocalizations.delegate] by default. class DefaultMaterialLocalizations implements MaterialLocalizations { /// Constructs an object that defines the material widgets' localized strings - /// for the given `locale`. + /// for US English (only). /// /// [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(); + const DefaultMaterialLocalizations(); - 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'); - } + // Ordered to match DateTime.MONDAY=1, DateTime.SUNDAY=6 + static const List_shortWeekdays = const [ + 'Mon', + 'Tue', + 'Wed', + 'Thu', + 'Fri', + 'Sat', + 'Sun', + ]; - 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'); - } - } + static const List _narrowWeekdays = const [ + 'S', + 'M', + 'T', + 'W', + 'T', + 'F', + 'S', + ]; - /// The locale for which the values of this class's localized resources - /// have been translated. - final Locale locale; + static const List _shortMonths = const [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; - final String _localeName; - - final Map _nameToValue = {}; - - /// 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; - } + static const List _months = const [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; @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; + assert(hourFormat(of: timeOfDayFormat) == HourFormat.h); + return formatDecimal(timeOfDay.hour); } @override String formatMinute(TimeOfDay timeOfDay) { - return _twoDigitZeroPaddedFormat.format(timeOfDay.minute); + final int minute = timeOfDay.minute; + return minute < 10 ? '0$minute' : minute.toString(); } @override - String formatYear(DateTime date) { - return _fullYearFormat.format(date); - } + String formatYear(DateTime date) => date.year.toString(); @override String formatMediumDate(DateTime date) { - return _mediumDateFormat.format(date); + final String day = _shortWeekdays[date.weekday - DateTime.MONDAY]; + final String month = _shortMonths[date.month - DateTime.JANUARY]; + return '$day, $month ${date.day}'; } @override String formatMonthYear(DateTime date) { - return _yearMonthFormat.format(date); + final String year = formatYear(date); + final String month = _months[date.month - DateTime.JANUARY]; + return '$month $year'; } @override - List get narrowWeekDays { - return _fullYearFormat.dateSymbols.NARROWWEEKDAYS; - } + List get narrowWeekdays => _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; - } + int get firstDayOfWeekIndex => 0; // narrowWeekdays[0] is 'S' for Sunday String _formatDayPeriod(TimeOfDay timeOfDay) { switch (timeOfDay.period) { @@ -364,166 +307,142 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { return null; } - @override - String get openAppDrawerTooltip => _nameToValue['openAppDrawerTooltip']; + /// Formats an integer, inserting thousands separators as needed. + String formatDecimal(int number) { + if (number > -1000 && number < 1000) + return number.toString(); - @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); + final String digits = number.abs().toString(); + final StringBuffer result = new StringBuffer(number < 0 ? '-' : ''); + final int maxDigitIndex = digits.length - 1; + for (int i = 0; i <= maxDigitIndex; i += 1) { + result.write(digits[i]); + if (i < maxDigitIndex && (maxDigitIndex - i) % 3 == 0) + result.write(','); + } + return result.toString(); } @override - String get licensesPageTitle => _nameToValue['licensesPageTitle']; + String formatTimeOfDay(TimeOfDay timeOfDay) { + assert(timeOfDayFormat == TimeOfDayFormat.h_colon_mm_space_a); + // 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. + return '${formatHour(timeOfDay)}:${formatMinute(timeOfDay)} ${_formatDayPeriod(timeOfDay)}'; + } + + @override + String get openAppDrawerTooltip => 'Open navigation menu'; + + @override + String get backButtonTooltip => 'Back'; + + @override + String get closeButtonTooltip => 'Close'; + + @override + String get nextMonthTooltip => 'Next month'; + + @override + String get previousMonthTooltip => 'Previous month'; + + @override + String get nextPageTooltip => 'Next page'; + + @override + String get previousPageTooltip => 'Previous page'; + + @override + String get showMenuTooltip => 'Show menu'; + + @override + String aboutListTileTitle(String applicationName) => 'About $applicationName'; + + @override + String get licensesPageTitle => 'Licenses'; @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)); + return rowCountIsApproximate + ? '$firstRow–$lastRow of about $rowCount' + : '$firstRow–$lastRow of $rowCount'; } @override - String get rowsPerPageTitle => _nameToValue['rowsPerPageTitle']; + String get rowsPerPageTitle => 'Rows per page'; @override String selectedRowCountTitle(int selectedRowCount) { - return _nameToPluralValue(selectedRowCount, 'selectedRowCountTitle') // asserts on no match - .replaceFirst(r'$selectedRowCount', formatDecimal(selectedRowCount)); + switch (selectedRowCount) { + case 0: + return 'No items selected'; + case 1: + return '1 item selected'; + default: + return '$selectedRowCount items selected'; + } } @override - String get cancelButtonLabel => _nameToValue['cancelButtonLabel']; + String get cancelButtonLabel => 'CANCEL'; @override - String get closeButtonLabel => _nameToValue['closeButtonLabel']; + String get closeButtonLabel => 'CLOSE'; @override - String get continueButtonLabel => _nameToValue['continueButtonLabel']; + String get continueButtonLabel => 'CONTINUE'; @override - String get copyButtonLabel => _nameToValue['copyButtonLabel']; + String get copyButtonLabel => 'COPY'; @override - String get cutButtonLabel => _nameToValue['cutButtonLabel']; + String get cutButtonLabel => 'CUT'; @override - String get okButtonLabel => _nameToValue['okButtonLabel']; + String get okButtonLabel => 'OK'; @override - String get pasteButtonLabel => _nameToValue['pasteButtonLabel']; + String get pasteButtonLabel => 'PASTE'; @override - String get selectAllButtonLabel => _nameToValue['selectAllButtonLabel']; + String get selectAllButtonLabel => 'SELECT ALL'; @override - String get viewLicensesButtonLabel => _nameToValue['viewLicensesButtonLabel']; + String get viewLicensesButtonLabel => 'VIEW LICENSES'; @override - String get anteMeridiemAbbreviation => _nameToValue['anteMeridiemAbbreviation']; + String get anteMeridiemAbbreviation => 'AM'; @override - String get postMeridiemAbbreviation => _nameToValue['postMeridiemAbbreviation']; + String get postMeridiemAbbreviation => 'PM'; - /// 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]; - } + TimeOfDayFormat get timeOfDayFormat => TimeOfDayFormat.h_colon_mm_space_a; /// Looks up text geometry defined in [MaterialTextGeometry]. @override - TextTheme get localTextGeometry => MaterialTextGeometry.forScriptCategory(_nameToValue["scriptCategory"]); + TextTheme get localTextGeometry => MaterialTextGeometry.englishLike; - /// Creates an object that provides localized resource values for the - /// for the widgets of the material library. + /// Creates an object that provides US English resource values for the material + /// library widgets. + /// + /// The [locale] parameter is ignored. /// /// This method is typically used to create a [LocalizationsDelegate]. /// The [MaterialApp] does so by default. static Future load(Locale locale) { - return new SynchronousFuture(new DefaultMaterialLocalizations(locale)); - } -} - -const Map _icuTimeOfDayToEnum = const { - '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; + return new SynchronousFuture(const DefaultMaterialLocalizations()); } + + /// A [LocalizationsDelegate] that uses [DefaultMaterialLocalizations.load] + /// to create an instance of this class. + /// + /// [MaterialApp] automatically adds this value to [MaterialApp.localizationsDelegates]. + static const LocalizationsDelegate delegate = const _MaterialLocalizationsDelegate(); } diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index 997c2ece82..f779cb5fe0 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -44,17 +44,6 @@ typedef Locale LocaleResolutionCallback(Locale locale, Iterable supporte /// This function must not return null. typedef String GenerateAppTitle(BuildContext context); -// Delegate that fetches the default (English) strings. -class _WidgetsLocalizationsDelegate extends LocalizationsDelegate { - const _WidgetsLocalizationsDelegate(); - - @override - Future load(Locale locale) => DefaultWidgetsLocalizations.load(locale); - - @override - bool shouldReload(_WidgetsLocalizationsDelegate old) => false; -} - /// A convenience class that wraps a number of widgets that are commonly /// required for an application. /// @@ -423,11 +412,11 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv // by the localizationsDelegates parameter, if any. Only the first delegate // of a particular LocalizationsDelegate.type is loaded so the // localizationsDelegate parameter can be used to override - // _WidgetsLocalizationsDelegate. + // WidgetsLocalizations.delegate. Iterable> get _localizationsDelegates sync* { if (widget.localizationsDelegates != null) yield* widget.localizationsDelegates; - yield const _WidgetsLocalizationsDelegate(); + yield DefaultWidgetsLocalizations.delegate; } @override diff --git a/packages/flutter/lib/src/widgets/localizations.dart b/packages/flutter/lib/src/widgets/localizations.dart index bfabeb26dd..ace2e3be70 100644 --- a/packages/flutter/lib/src/widgets/localizations.dart +++ b/packages/flutter/lib/src/widgets/localizations.dart @@ -161,44 +161,50 @@ abstract class WidgetsLocalizations { } } -/// Localized values for widgets. -class DefaultWidgetsLocalizations implements WidgetsLocalizations { - /// Construct an object that defines the localized values for the widgets - /// library for the given `locale`. - /// - /// [LocalizationsDelegate] implementations typically call the static [load] - /// function, rather than constructing this class directly. - DefaultWidgetsLocalizations(this.locale) { - final String language = locale.languageCode.toLowerCase(); - _textDirection = _rtlLanguages.contains(language) ? TextDirection.rtl : TextDirection.ltr; - } - - // See http://en.wikipedia.org/wiki/Right-to-left - static const List _rtlLanguages = const [ - 'ar', // Arabic - 'fa', // Farsi - 'he', // Hebrew - 'ps', // Pashto - 'sd', // Sindhi - 'ur', // Urdu - ]; - - /// The locale for which the values of this class's localized resources - /// have been translated. - final Locale locale; +class _WidgetsLocalizationsDelegate extends LocalizationsDelegate { + const _WidgetsLocalizationsDelegate(); @override - TextDirection get textDirection => _textDirection; - TextDirection _textDirection; + Future load(Locale locale) => DefaultWidgetsLocalizations.load(locale); - /// Creates an object that provides localized resource values for the - /// lowest levels of the Flutter framework. + @override + bool shouldReload(_WidgetsLocalizationsDelegate old) => false; +} + +/// US English localizations for the widgets library. +/// +/// See also: +/// +/// * [GlobalWidgetsLocalizations], which provides widgets localizations for +/// many languages. +/// * [WidgetsApp.delegates], which automatically includes +/// [DefaultWidgetsLocalizations.delegate] by default. +class DefaultWidgetsLocalizations implements WidgetsLocalizations { + /// Construct an object that defines the localized values for the widgets + /// library for US English (only). + /// + /// [LocalizationsDelegate] implementations typically call the static [load] + const DefaultWidgetsLocalizations(); + + @override + TextDirection get textDirection => TextDirection.ltr; + + /// Creates an object that provides US English resource values for the + /// lowest levels of the widgets library. + /// + /// The [locale] parameter is ignored. /// /// This method is typically used to create a [LocalizationsDelegate]. /// The [WidgetsApp] does so by default. static Future load(Locale locale) { - return new SynchronousFuture(new DefaultWidgetsLocalizations(locale)); + return new SynchronousFuture(const DefaultWidgetsLocalizations()); } + + /// A [LocalizationsDelegate] that uses [DefaultWidgetsLocalizations.load] + /// to create an instance of this class. + /// + /// [WidgetsApp] automatically adds this value to [WidgetApp.localizationsDelegates]. + static const LocalizationsDelegate delegate = const _WidgetsLocalizationsDelegate(); } class _LocalizationsScope extends InheritedWidget { diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index 4fef67545b..b94a470936 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -8,7 +8,6 @@ dependencies: # To update these, use "flutter update-packages --force-upgrade". collection: 1.14.3 http: 0.11.3+14 - intl: 0.15.1 # TODO(ianh): remove this, see https://github.com/flutter/flutter/issues/12050 meta: 1.1.1 typed_data: 1.1.4 vector_math: 2.0.5 @@ -23,7 +22,7 @@ dev_dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY boolean_selector: 1.0.2 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY @@ -35,6 +34,7 @@ dev_dependencies: html: 0.13.2 # TRANSITIVE DEPENDENCY http_multi_server: 2.0.4 # TRANSITIVE DEPENDENCY http_parser: 3.1.1 # TRANSITIVE DEPENDENCY + intl: 0.15.1 # TRANSITIVE DEPENDENCY intl_translation: 0.15.0 # TRANSITIVE DEPENDENCY isolate: 1.1.0 # TRANSITIVE DEPENDENCY js: 0.6.1 # TRANSITIVE DEPENDENCY diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart index df9cb470b6..69a7016b9b 100644 --- a/packages/flutter/test/material/date_picker_test.dart +++ b/packages/flutter/test/material/date_picker_test.dart @@ -4,8 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; import 'feedback_tester.dart'; @@ -419,250 +417,4 @@ void main() { expect(await date, isNull); }); }); - - group(DayPicker, () { - final Map> testLocales = >{ - // Tests the default. - const Locale('en', 'US'): { - 'textDirection': TextDirection.ltr, - 'expectedDaysOfWeek': ['S', 'M', 'T', 'W', 'T', 'F', 'S'], - 'expectedDaysOfMonth': new List.generate(30, (int i) => '${i + 1}'), - 'expectedMonthYearHeader': 'September 2017', - }, - // Tests a different first day of week. - const Locale('ru', 'RU'): { - 'textDirection': TextDirection.ltr, - 'expectedDaysOfWeek': ['пн', 'вт', 'ср', 'чт', 'пт', 'сб', 'вс'], - 'expectedDaysOfMonth': new List.generate(30, (int i) => '${i + 1}'), - 'expectedMonthYearHeader': 'сентябрь 2017 г.', - }, - // Tests RTL. - // TODO: change to Arabic numerals when these are fixed: - // TODO: https://github.com/dart-lang/intl/issues/143 - // TODO: https://github.com/flutter/flutter/issues/12289 - const Locale('ar', 'AR'): { - 'textDirection': TextDirection.rtl, - 'expectedDaysOfWeek': ['ح', 'ن', 'ث', 'ر', 'خ', 'ج', 'س'], - 'expectedDaysOfMonth': new List.generate(30, (int i) => '${i + 1}'), - 'expectedMonthYearHeader': 'سبتمبر 2017', - }, - }; - - for (Locale locale in testLocales.keys) { - testWidgets('shows dates for $locale', (WidgetTester tester) async { - final List expectedDaysOfWeek = testLocales[locale]['expectedDaysOfWeek']; - final List expectedDaysOfMonth = testLocales[locale]['expectedDaysOfMonth']; - final String expectedMonthYearHeader = testLocales[locale]['expectedMonthYearHeader']; - final TextDirection textDirection = testLocales[locale]['textDirection']; - final DateTime baseDate = new DateTime(2017, 9, 27); - - await _pumpBoilerplate(tester, new DayPicker( - selectedDate: baseDate, - currentDate: baseDate, - onChanged: (DateTime newValue) {}, - firstDate: baseDate.subtract(const Duration(days: 90)), - lastDate: baseDate.add(const Duration(days: 90)), - displayedMonth: baseDate, - ), locale: locale, textDirection: textDirection); - - expect(find.text(expectedMonthYearHeader), findsOneWidget); - - expectedDaysOfWeek.forEach((String dayOfWeek) { - expect(find.text(dayOfWeek), findsWidgets); - }); - - Offset previousCellOffset; - expectedDaysOfMonth.forEach((String dayOfMonth) { - final Finder dayCell = find.descendant(of: find.byType(GridView), matching: find.text(dayOfMonth)); - expect(dayCell, findsOneWidget); - - // Check that cells are correctly positioned relative to each other, - // taking text direction into account. - final Offset offset = tester.getCenter(dayCell); - if (previousCellOffset != null) { - if (textDirection == TextDirection.ltr) { - expect(offset.dx > previousCellOffset.dx && offset.dy == previousCellOffset.dy || offset.dy > previousCellOffset.dy, true); - } else { - expect(offset.dx < previousCellOffset.dx && offset.dy == previousCellOffset.dy || offset.dy > previousCellOffset.dy, true); - } - } - previousCellOffset = offset; - }); - }); - } - }); - - testWidgets('locale parameter overrides ambient locale', (WidgetTester tester) async { - await tester.pumpWidget(new MaterialApp( - locale: const Locale('en', 'US'), - supportedLocales: const [ - const Locale('en', 'US'), - const Locale('fr', 'CA'), - ], - home: new Material( - child: new Builder( - builder: (BuildContext context) { - return new FlatButton( - onPressed: () async { - await showDatePicker( - context: context, - initialDate: initialDate, - firstDate: firstDate, - lastDate: lastDate, - locale: const Locale('fr', 'CA'), - ); - }, - child: const Text('X'), - ); - }, - ), - ), - )); - - await tester.tap(find.text('X')); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - final Element dayPicker = tester.element(find.byType(DayPicker)); - expect( - Localizations.localeOf(dayPicker), - const Locale('fr', 'CA'), - ); - - expect( - Directionality.of(dayPicker), - TextDirection.ltr, - ); - - await tester.tap(find.text('ANNULER')); - }); - - testWidgets('textDirection parameter overrides ambient textDirection', (WidgetTester tester) async { - await tester.pumpWidget(new MaterialApp( - locale: const Locale('en', 'US'), - supportedLocales: const [ - const Locale('en', 'US'), - ], - home: new Material( - child: new Builder( - builder: (BuildContext context) { - return new FlatButton( - onPressed: () async { - await showDatePicker( - context: context, - initialDate: initialDate, - firstDate: firstDate, - lastDate: lastDate, - textDirection: TextDirection.rtl, - ); - }, - child: const Text('X'), - ); - }, - ), - ), - )); - - await tester.tap(find.text('X')); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - final Element dayPicker = tester.element(find.byType(DayPicker)); - expect( - Directionality.of(dayPicker), - TextDirection.rtl, - ); - - await tester.tap(find.text('CANCEL')); - }); - - testWidgets('textDirection parameter takes precendence over locale parameter', (WidgetTester tester) async { - await tester.pumpWidget(new MaterialApp( - locale: const Locale('en', 'US'), - supportedLocales: const [ - const Locale('en', 'US'), - const Locale('fr', 'CA'), - ], - home: new Material( - child: new Builder( - builder: (BuildContext context) { - return new FlatButton( - onPressed: () async { - await showDatePicker( - context: context, - initialDate: initialDate, - firstDate: firstDate, - lastDate: lastDate, - locale: const Locale('fr', 'CA'), - textDirection: TextDirection.rtl, - ); - }, - child: const Text('X'), - ); - }, - ), - ), - )); - - await tester.tap(find.text('X')); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - final Element dayPicker = tester.element(find.byType(DayPicker)); - expect( - Localizations.localeOf(dayPicker), - const Locale('fr', 'CA'), - ); - - expect( - Directionality.of(dayPicker), - TextDirection.rtl, - ); - - await tester.tap(find.text('ANNULER')); - }); -} - -Future _pumpBoilerplate( - WidgetTester tester, - Widget child, { - Locale locale = const Locale('en', 'US'), - TextDirection textDirection: TextDirection.ltr -}) async { - await tester.pumpWidget(new Directionality( - textDirection: TextDirection.ltr, - child: new Localizations( - locale: locale, - delegates: >[ - new _MaterialLocalizationsDelegate( - new DefaultMaterialLocalizations(locale), - ), - const DefaultWidgetsLocalizationsDelegate(), - ], - child: child, - ), - )); -} - -class _MaterialLocalizationsDelegate extends LocalizationsDelegate { - const _MaterialLocalizationsDelegate(this.localizations); - - final MaterialLocalizations localizations; - - @override - Future load(Locale locale) { - return new SynchronousFuture(localizations); - } - - @override - bool shouldReload(_MaterialLocalizationsDelegate old) => false; -} - -class DefaultWidgetsLocalizationsDelegate extends LocalizationsDelegate { - const DefaultWidgetsLocalizationsDelegate(); - - @override - Future load(Locale locale) { - return new SynchronousFuture(new DefaultWidgetsLocalizations(locale)); - } - - @override - bool shouldReload(DefaultWidgetsLocalizationsDelegate old) => false; } diff --git a/packages/flutter/test/material/localizations_test.dart b/packages/flutter/test/material/localizations_test.dart index 66366d1d90..a9c7f33452 100644 --- a/packages/flutter/test/material/localizations_test.dart +++ b/packages/flutter/test/material/localizations_test.dart @@ -3,323 +3,51 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; -class FooMaterialLocalizations extends DefaultMaterialLocalizations { - FooMaterialLocalizations(Locale locale) : super(locale); - - @override - String get backButtonTooltip => 'foo'; -} - -class FooMaterialLocalizationsDelegate extends LocalizationsDelegate { - const FooMaterialLocalizationsDelegate(); - - @override - Future load(Locale locale) { - return new SynchronousFuture(new FooMaterialLocalizations(locale)); - } - - @override - bool shouldReload(FooMaterialLocalizationsDelegate old) => false; -} - -/// A localizations delegate that does not contain any useful data, and is only -/// used to trigger didChangeDependencies upon locale change. -class _DummyLocalizationsDelegate extends LocalizationsDelegate { - @override - Future load(Locale locale) async => new DummyLocalizations(); - - @override - bool shouldReload(_DummyLocalizationsDelegate old) => true; -} - -class DummyLocalizations {} - -Widget buildFrame({ - Locale locale, - Iterable> delegates, - WidgetBuilder buildContent, - LocaleResolutionCallback localeResolutionCallback, - Iterable supportedLocales: const [ - const Locale('en', 'US'), - const Locale('es', 'es'), - ], -}) { - return new MaterialApp( - color: const Color(0xFFFFFFFF), - locale: locale, - localizationsDelegates: delegates, - localeResolutionCallback: localeResolutionCallback, - supportedLocales: supportedLocales, - onGenerateRoute: (RouteSettings settings) { - return new MaterialPageRoute( - builder: (BuildContext context) { - return buildContent(context); - } - ); - }, - ); -} - void main() { - testWidgets('sanity check', (WidgetTester tester) async { - final Key textKey = new UniqueKey(); + testWidgets('English translations exist for all MaterialLocalizations properties', (WidgetTester tester) async { + final MaterialLocalizations localizations = const DefaultMaterialLocalizations(); - await tester.pumpWidget( - buildFrame( - buildContent: (BuildContext context) { - return new Text( - MaterialLocalizations.of(context).backButtonTooltip, - key: textKey, - ); - } - ) - ); + expect(localizations.openAppDrawerTooltip, isNotNull); + expect(localizations.backButtonTooltip, isNotNull); + expect(localizations.closeButtonTooltip, isNotNull); + expect(localizations.nextMonthTooltip, isNotNull); + expect(localizations.previousMonthTooltip, isNotNull); + expect(localizations.nextPageTooltip, isNotNull); + expect(localizations.previousPageTooltip, isNotNull); + expect(localizations.showMenuTooltip, isNotNull); + expect(localizations.licensesPageTitle, isNotNull); + expect(localizations.rowsPerPageTitle, isNotNull); + expect(localizations.cancelButtonLabel, isNotNull); + expect(localizations.closeButtonLabel, isNotNull); + expect(localizations.continueButtonLabel, isNotNull); + expect(localizations.copyButtonLabel, isNotNull); + expect(localizations.cutButtonLabel, isNotNull); + expect(localizations.okButtonLabel, isNotNull); + expect(localizations.pasteButtonLabel, isNotNull); + expect(localizations.selectAllButtonLabel, isNotNull); + expect(localizations.viewLicensesButtonLabel, isNotNull); - expect(tester.widget(find.byKey(textKey)).data, 'Back'); + expect(localizations.aboutListTileTitle('FOO'), isNotNull); + expect(localizations.aboutListTileTitle('FOO'), contains('FOO')); - // Unrecognized locale falls back to 'en' - await tester.binding.setLocale('foo', 'bar'); - await tester.pump(); - expect(tester.widget(find.byKey(textKey)).data, 'Back'); + expect(localizations.selectedRowCountTitle(0), isNotNull); + expect(localizations.selectedRowCountTitle(1), isNotNull); + expect(localizations.selectedRowCountTitle(2), isNotNull); + expect(localizations.selectedRowCountTitle(100), isNotNull); + expect(localizations.selectedRowCountTitle(0).contains(r'$selectedRowCount'), isFalse); + expect(localizations.selectedRowCountTitle(1).contains(r'$selectedRowCount'), isFalse); + expect(localizations.selectedRowCountTitle(2).contains(r'$selectedRowCount'), isFalse); + expect(localizations.selectedRowCountTitle(100).contains(r'$selectedRowCount'), isFalse); - // Spanish Bolivia locale, falls back to just 'es' - await tester.binding.setLocale('es', 'bo'); - await tester.pump(); - expect(tester.widget(find.byKey(textKey)).data, 'Espalda'); - - }); - - testWidgets('translations exist for all materia/i18n languages', (WidgetTester tester) async { - final List languages = [ - 'ar', // Arabic - 'de', // German - 'en', // English - 'es', // Spanish - 'fa', // Farsi (Persian) - 'fr', // French - 'he', // Hebrew - 'it', // Italian - 'ja', // Japanese - 'ps', // Pashto - 'pt', // Portugese - 'ru', // Russian - 'sd', // Sindhi - 'ur', // Urdu - 'zh', // Chinese (simplified) - ]; - - for (String language in languages) { - final Locale locale = new Locale(language, ''); - final MaterialLocalizations localizations = new DefaultMaterialLocalizations(locale); - - expect(localizations.openAppDrawerTooltip, isNotNull); - expect(localizations.backButtonTooltip, isNotNull); - expect(localizations.closeButtonTooltip, isNotNull); - expect(localizations.nextMonthTooltip, isNotNull); - expect(localizations.previousMonthTooltip, isNotNull); - expect(localizations.nextPageTooltip, isNotNull); - expect(localizations.previousPageTooltip, isNotNull); - expect(localizations.showMenuTooltip, isNotNull); - expect(localizations.licensesPageTitle, isNotNull); - expect(localizations.rowsPerPageTitle, isNotNull); - expect(localizations.cancelButtonLabel, isNotNull); - expect(localizations.closeButtonLabel, isNotNull); - expect(localizations.continueButtonLabel, isNotNull); - expect(localizations.copyButtonLabel, isNotNull); - expect(localizations.cutButtonLabel, isNotNull); - expect(localizations.okButtonLabel, isNotNull); - expect(localizations.pasteButtonLabel, isNotNull); - expect(localizations.selectAllButtonLabel, isNotNull); - expect(localizations.viewLicensesButtonLabel, isNotNull); - - expect(localizations.aboutListTileTitle('FOO'), isNotNull); - expect(localizations.aboutListTileTitle('FOO'), contains('FOO')); - - expect(localizations.selectedRowCountTitle(0), isNotNull); - expect(localizations.selectedRowCountTitle(1), isNotNull); - expect(localizations.selectedRowCountTitle(2), isNotNull); - expect(localizations.selectedRowCountTitle(100), isNotNull); - expect(localizations.selectedRowCountTitle(0).contains(r'$selectedRowCount'), isFalse); - expect(localizations.selectedRowCountTitle(1).contains(r'$selectedRowCount'), isFalse); - expect(localizations.selectedRowCountTitle(2).contains(r'$selectedRowCount'), isFalse); - expect(localizations.selectedRowCountTitle(100).contains(r'$selectedRowCount'), isFalse); - - expect(localizations.pageRowsInfoTitle(1, 10, 100, true), isNotNull); - expect(localizations.pageRowsInfoTitle(1, 10, 100, false), isNotNull); - expect(localizations.pageRowsInfoTitle(1, 10, 100, true).contains(r'$firstRow'), isFalse); - expect(localizations.pageRowsInfoTitle(1, 10, 100, true).contains(r'$lastRow'), isFalse); - expect(localizations.pageRowsInfoTitle(1, 10, 100, true).contains(r'$rowCount'), isFalse); - expect(localizations.pageRowsInfoTitle(1, 10, 100, false).contains(r'$firstRow'), isFalse); - expect(localizations.pageRowsInfoTitle(1, 10, 100, false).contains(r'$lastRow'), isFalse); - expect(localizations.pageRowsInfoTitle(1, 10, 100, false).contains(r'$rowCount'), isFalse); - } - }); - - testWidgets('spot check selectedRowCount translations', (WidgetTester tester) async { - MaterialLocalizations localizations = new DefaultMaterialLocalizations(const Locale('en', '')); - expect(localizations.selectedRowCountTitle(0), 'No items selected'); - expect(localizations.selectedRowCountTitle(1), '1 item selected'); - expect(localizations.selectedRowCountTitle(2), '2 items selected'); - expect(localizations.selectedRowCountTitle(123456789), '123,456,789 items selected'); - - localizations = new DefaultMaterialLocalizations(const Locale('es', '')); - expect(localizations.selectedRowCountTitle(0), 'No se han seleccionado elementos'); - expect(localizations.selectedRowCountTitle(1), '1 artículo seleccionado'); - expect(localizations.selectedRowCountTitle(2), '2 artículos seleccionados'); - expect(localizations.selectedRowCountTitle(123456789), '123.456.789 artículos seleccionados'); - }); - - testWidgets('Localizations.override widget tracks parent\'s locale', (WidgetTester tester) async { - Widget buildLocaleFrame(Locale locale) { - return buildFrame( - locale: locale, - buildContent: (BuildContext context) { - return new Localizations.override( - context: context, - child: new Builder( - builder: (BuildContext context) { - // No MaterialLocalizations are defined for the first Localizations - // ancestor, so we should get the values from the default one, i.e. - // the one created by WidgetsApp via the LocalizationsDelegate - // provided by MaterialApp. - return new Text(MaterialLocalizations.of(context).backButtonTooltip); - }, - ), - ); - } - ); - } - - await tester.pumpWidget(buildLocaleFrame(const Locale('en', 'US'))); - expect(find.text('Back'), findsOneWidget); - - await tester.pumpWidget(buildLocaleFrame(const Locale('de', 'DE'))); - expect(find.text('Zurück'), findsOneWidget); - - await tester.pumpWidget(buildLocaleFrame(const Locale('zh', 'CN'))); - expect(find.text('返回'), findsOneWidget); - }); - - testWidgets('Localizations.override widget with hardwired locale', (WidgetTester tester) async { - Widget buildLocaleFrame(Locale locale) { - return buildFrame( - locale: locale, - buildContent: (BuildContext context) { - return new Localizations.override( - context: context, - locale: const Locale('en', 'US'), - child: new Builder( - builder: (BuildContext context) { - // No MaterialLocalizations are defined for the Localizations.override - // ancestor, so we should get all values from the default one, i.e. - // the one created by WidgetsApp via the LocalizationsDelegate - // provided by MaterialApp. - return new Text(MaterialLocalizations.of(context).backButtonTooltip); - }, - ), - ); - } - ); - } - - await tester.pumpWidget(buildLocaleFrame(const Locale('en', 'US'))); - expect(find.text('Back'), findsOneWidget); - - await tester.pumpWidget(buildLocaleFrame(const Locale('de', 'DE'))); - expect(find.text('Back'), findsOneWidget); - - await tester.pumpWidget(buildLocaleFrame(const Locale('zh', 'CN'))); - expect(find.text('Back'), findsOneWidget); - }); - - testWidgets('MaterialApp overrides MaterialLocalizations', (WidgetTester tester) async { - final Key textKey = new UniqueKey(); - - await tester.pumpWidget( - buildFrame( - // Accept whatever locale we're given - localeResolutionCallback: (Locale locale, Iterable supportedLocales) => locale, - delegates: [ - const FooMaterialLocalizationsDelegate(), - ], - buildContent: (BuildContext context) { - // Should always be 'foo', no matter what the locale is - return new Text( - MaterialLocalizations.of(context).backButtonTooltip, - key: textKey, - ); - } - ) - ); - - expect(tester.widget(find.byKey(textKey)).data, 'foo'); - - await tester.binding.setLocale('zh', 'CN'); - await tester.pump(); - expect(find.text('foo'), findsOneWidget); - - await tester.binding.setLocale('de', 'DE'); - await tester.pump(); - expect(find.text('foo'), findsOneWidget); - - }); - - testWidgets('deprecated Android/Java locales are modernized', (WidgetTester tester) async { - final Key textKey = new UniqueKey(); - - await tester.pumpWidget( - buildFrame( - supportedLocales: [ - const Locale('en', 'US'), - const Locale('he', 'IL'), - const Locale('yi', 'IL'), - const Locale('id', 'JV'), - ], - buildContent: (BuildContext context) { - return new Text( - '${Localizations.localeOf(context)}', - key: textKey, - ); - }, - ) - ); - - expect(tester.widget(find.byKey(textKey)).data, 'en_US'); - - // Hebrew was iw (ISO-639) is he (ISO-639-1) - await tester.binding.setLocale('iw', 'IL'); - await tester.pump(); - expect(tester.widget(find.byKey(textKey)).data, 'he_IL'); - - // Yiddish was ji (ISO-639) is yi (ISO-639-1) - await tester.binding.setLocale('ji', 'IL'); - await tester.pump(); - expect(tester.widget(find.byKey(textKey)).data, 'yi_IL'); - - // Indonesian was in (ISO-639) is id (ISO-639-1) - await tester.binding.setLocale('in', 'JV'); - await tester.pump(); - expect(tester.widget(find.byKey(textKey)).data, 'id_JV'); - }); - - testWidgets('Localizations is compatible with ChangeNotifier.dispose() called during didChangeDependencies', (WidgetTester tester) async { - // PageView calls ScrollPosition.dispose() during didChangeDependencies. - await tester.pumpWidget(new MaterialApp( - supportedLocales: const [ - const Locale('en', 'US'), - const Locale('es', 'ES'), - ], - localizationsDelegates: <_DummyLocalizationsDelegate>[ - new _DummyLocalizationsDelegate(), - ], - home: new PageView(), - )); - - await tester.binding.setLocale('es', 'US'); - await tester.pump(); - await tester.pumpWidget(new Container()); + expect(localizations.pageRowsInfoTitle(1, 10, 100, true), isNotNull); + expect(localizations.pageRowsInfoTitle(1, 10, 100, false), isNotNull); + expect(localizations.pageRowsInfoTitle(1, 10, 100, true).contains(r'$firstRow'), isFalse); + expect(localizations.pageRowsInfoTitle(1, 10, 100, true).contains(r'$lastRow'), isFalse); + expect(localizations.pageRowsInfoTitle(1, 10, 100, true).contains(r'$rowCount'), isFalse); + expect(localizations.pageRowsInfoTitle(1, 10, 100, false).contains(r'$firstRow'), isFalse); + expect(localizations.pageRowsInfoTitle(1, 10, 100, false).contains(r'$lastRow'), isFalse); + expect(localizations.pageRowsInfoTitle(1, 10, 100, false).contains(r'$rowCount'), isFalse); }); } diff --git a/packages/flutter/test/material/material_localizations_test.dart b/packages/flutter/test/material/material_localizations_test.dart deleted file mode 100644 index b4eba2d1d8..0000000000 --- a/packages/flutter/test/material/material_localizations_test.dart +++ /dev/null @@ -1,193 +0,0 @@ -// 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 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/widgets.dart'; - -void main() { - testWidgets('$MaterialLocalizations localizes text inside the tree', (WidgetTester tester) async { - await tester.pumpWidget(new MaterialApp( - home: new ListView( - children: [ - new LocalizationTracker(key: const ValueKey('outer')), - new Localizations( - locale: const Locale('zh', 'CN'), - delegates: >[ - new _MaterialLocalizationsDelegate( - new DefaultMaterialLocalizations(const Locale('zh', 'CN')), - ), - const DefaultWidgetsLocalizationsDelegate(), - ], - child: new LocalizationTracker(key: const ValueKey('inner')), - ), - ], - ), - )); - - final LocalizationTrackerState outerTracker = tester.state(find.byKey(const ValueKey('outer'))); - expect(outerTracker.captionFontSize, 12.0); - final LocalizationTrackerState innerTracker = tester.state(find.byKey(const ValueKey('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 { - LocalizationTracker({Key key}) : super(key: key); - - @override - State createState() => new LocalizationTrackerState(); -} - -class LocalizationTrackerState extends State { - double captionFontSize; - - @override - Widget build(BuildContext context) { - captionFontSize = Theme.of(context).textTheme.caption.fontSize; - return new Container(); - } -} - -// Same as _MaterialLocalizationsDelegate in widgets/app.dart -class _MaterialLocalizationsDelegate extends LocalizationsDelegate { - const _MaterialLocalizationsDelegate(this.localizations); - - final MaterialLocalizations localizations; - - @override - Future load(Locale locale) { - return new SynchronousFuture(localizations); - } - - @override - bool shouldReload(_MaterialLocalizationsDelegate old) => false; -} - -// Same as _WidgetsLocalizationsDelegate in widgets/app.dart -class DefaultWidgetsLocalizationsDelegate extends LocalizationsDelegate { - const DefaultWidgetsLocalizationsDelegate(); - - @override - Future load(Locale locale) { - return new SynchronousFuture(new DefaultWidgetsLocalizations(locale)); - } - - @override - bool shouldReload(DefaultWidgetsLocalizationsDelegate old) => false; -} diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart index 09a0491311..863dcef660 100644 --- a/packages/flutter/test/material/time_picker_test.dart +++ b/packages/flutter/test/material/time_picker_test.dart @@ -205,78 +205,4 @@ void main() { expect(feedback.hapticCount, 3); }); }); - - group('localization', () { - testWidgets('can localize the header in all known formats', (WidgetTester tester) async { - // TODO(yjbanov): also test `HH.mm` (in_ID), `a h:mm` (ko_KR) and `HH:mm น.` (th_TH) when we have .arb files for them - final Map> locales = >{ - const Locale('en', 'US'): const ['hour h', 'string :', 'minute', 'period'], //'h:mm a' - const Locale('en', 'GB'): const ['hour HH', 'string :', 'minute'], //'HH:mm' - const Locale('es', 'ES'): const ['hour H', 'string :', 'minute'], //'H:mm' - const Locale('fr', 'CA'): const ['hour HH', 'string h', 'minute'], //'HH \'h\' mm' - const Locale('zh', 'ZH'): const ['period', 'hour h', 'string :', 'minute'], //'ah:mm' - }; - - for (Locale locale in locales.keys) { - final Offset center = await startPicker(tester, (TimeOfDay time) { }, locale: locale); - final List actual = []; - tester.element(find.byType(CustomMultiChildLayout)).visitChildren((Element child) { - final LayoutId layout = child.widget; - final String fragmentType = '${layout.child.runtimeType}'; - final dynamic widget = layout.child; - if (fragmentType == '_MinuteControl') { - actual.add('minute'); - } else if (fragmentType == '_DayPeriodControl') { - actual.add('period'); - } else if (fragmentType == '_HourControl') { - actual.add('hour ${widget.hourFormat.toString().split('.').last}'); - } else if (fragmentType == '_StringFragment') { - actual.add('string ${widget.value}'); - } else { - fail('Unsupported fragment type: $fragmentType'); - } - }); - expect(actual, locales[locale]); - await tester.tapAt(new Offset(center.dx, center.dy - 50.0)); - await finishPicker(tester); - } - }); - - testWidgets('uses single-ring 12-hour dial for h hour format', (WidgetTester tester) async { - // Tap along the segment stretching from the center to the edge at - // 12:00 AM position. Because there's only one ring, no matter where you - // tap the time will be the same. See the 24-hour dial test that behaves - // differently. - for (int i = 1; i < 10; i++) { - TimeOfDay result; - final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; }); - final Size size = tester.getSize(find.byKey(const Key('time-picker-dial'))); - final double dy = (size.height / 2.0 / 10) * i; - await tester.tapAt(new Offset(center.dx, center.dy - dy)); - await finishPicker(tester); - expect(result, equals(const TimeOfDay(hour: 0, minute: 0))); - } - }); - - testWidgets('uses two-ring 24-hour dial for H and HH hour formats', (WidgetTester tester) async { - const List locales = const [ - const Locale('en', 'GB'), // HH - const Locale('es', 'ES'), // H - ]; - for (Locale locale in locales) { - // Tap along the segment stretching from the center to the edge at - // 12:00 AM position. There are two rings. At ~70% mark, the ring - // switches between inner ring and outer ring. - for (int i = 1; i < 10; i++) { - TimeOfDay result; - final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; }, locale: locale); - final Size size = tester.getSize(find.byKey(const Key('time-picker-dial'))); - final double dy = (size.height / 2.0 / 10) * i; - await tester.tapAt(new Offset(center.dx, center.dy - dy)); - await finishPicker(tester); - expect(result, equals(new TimeOfDay(hour: i < 7 ? 12 : 0, minute: 0))); - } - } - }); - }); } diff --git a/packages/flutter_driver/pubspec.yaml b/packages/flutter_driver/pubspec.yaml index 227a6440ec..ffa29d6a29 100644 --- a/packages/flutter_driver/pubspec.yaml +++ b/packages/flutter_driver/pubspec.yaml @@ -27,7 +27,7 @@ dev_dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY boolean_selector: 1.0.2 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY diff --git a/packages/flutter_localizations/lib/flutter_localizations.dart b/packages/flutter_localizations/lib/flutter_localizations.dart new file mode 100644 index 0000000000..934f31c97e --- /dev/null +++ b/packages/flutter_localizations/lib/flutter_localizations.dart @@ -0,0 +1,9 @@ +// 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. + +/// Localizations for the Flutter library +library flutter_localizations; + +export 'src/material_localizations.dart' show GlobalMaterialLocalizations; +export 'src/widgets_localizations.dart' show GlobalWidgetsLocalizations; diff --git a/packages/flutter/lib/src/material/i18n/README.md b/packages/flutter_localizations/lib/src/l10n/README.md similarity index 94% rename from packages/flutter/lib/src/material/i18n/README.md rename to packages/flutter_localizations/lib/src/l10n/README.md index e1d030ec54..ff2ef14a8e 100644 --- a/packages/flutter/lib/src/material/i18n/README.md +++ b/packages/flutter_localizations/lib/src/l10n/README.md @@ -1,12 +1,12 @@ -# Material Library Internationalization +# Material Library Localizations The `.arb` files in this directory contain localized values (primarily strings) used by the material library. The `localizations.dart` file -combines all of the localizations into a single Dart Map that is -linked with the rest of the material library. +combines all of the localizations into a single Map that is +linked with the rest of flutter_localizations package. If you're looking for information about internationalizing Flutter -apps in general, see th +apps in general, see the [Internationalizing Flutter Apps](https://flutter.io/tutorials/internationalization/) tutorial. @@ -54,7 +54,7 @@ translation of "CANCEL" which is defined for the `cancelButtonLabel` resource ID. Each of the language-specific .arb files contains an entry for -`cancelButtonLabel`. They're all represented by the Dart `Map` in the +`cancelButtonLabel`. They're all represented by the `Map` in the generated `localizations.dart` file. The Map is used by the MaterialLocalizations class. @@ -123,18 +123,18 @@ the "Other" suffix. For example the English translations ``` -### Generated file localizations.dart: all of the localizations as a Dart Map +### Generated file localizations.dart: all of the localizations as a Map If you look at the comment at the top of `localizations.dart` you'll see that it was manually generated using a `dev/tools` app called `gen_localizations` roughly like this: ```dart -dev/tools/gen_localizations.dart packages/flutter/lib/src/material/i18n material +dart dev/tools/gen_localizations.dart packages/flutter_localizations/lib/src/l10n material ``` The gen_localizations app just combines the contents of all of the -.arb files into a single Dart `Map` that has entries for each .arb +.arb files into a single `Map` that has entries for each .arb file's locale. The `MaterialLocalizations` class implementation uses this Map to implement the methods that lookup localized resource values. diff --git a/packages/flutter/lib/src/material/i18n/localizations.dart b/packages/flutter_localizations/lib/src/l10n/localizations.dart similarity index 99% rename from packages/flutter/lib/src/material/i18n/localizations.dart rename to packages/flutter_localizations/lib/src/l10n/localizations.dart index 09ccef5fc7..987e53f5e0 100644 --- a/packages/flutter/lib/src/material/i18n/localizations.dart +++ b/packages/flutter_localizations/lib/src/l10n/localizations.dart @@ -4,7 +4,7 @@ // This file has been automatically generated. Please do not edit it manually. // To regenerate the file, use: -// dart dev/tools/gen_localizations.dart packages/flutter/lib/src/material/i18n material +// dart dev/tools/gen_localizations.dart packages/flutter_localizations/lib/src/l10n material /// Maps from [Locale.languageCode] to a map that contains the localized strings /// for that locale. @@ -452,4 +452,3 @@ const Map> localizations = const . +/// * [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), + 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 _nameToValue = {}; + + intl.NumberFormat _decimalFormat; + + intl.NumberFormat _twoDigitZeroPaddedFormat; + + 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 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 load(Locale locale) { + return new SynchronousFuture(new GlobalMaterialLocalizations(locale)); + } + + /// A [LocalizationsDelegate] that uses [GlobalMaterialLocalizations.load] + /// to create an instance of this class. + /// + /// Most internationlized 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 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> delegates = const >[ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; +} + +const Map _icuTimeOfDayToEnum = const { + '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; + } +} + +class _MaterialLocalizationsDelegate extends LocalizationsDelegate { + const _MaterialLocalizationsDelegate(); + + @override + Future load(Locale locale) => GlobalMaterialLocalizations.load(locale); + + @override + bool shouldReload(_MaterialLocalizationsDelegate old) => false; +} diff --git a/packages/flutter_localizations/lib/src/widgets_localizations.dart b/packages/flutter_localizations/lib/src/widgets_localizations.dart new file mode 100644 index 0000000000..9670af2a56 --- /dev/null +++ b/packages/flutter_localizations/lib/src/widgets_localizations.dart @@ -0,0 +1,75 @@ +// 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'; + +/// Localized values for widgets. +/// +/// Currently this class just maps [locale] to [textDirection]. All locales +/// are [TextDirection.ltr] except for locales with the following +/// [Locale.languageCode] values, which are [TextDirection.rtl]: +/// +/// * ar - Arabic +/// * fa - Farsi +/// * he - Hebrew +/// * ps - Pashto +/// * sd - Sindhi +/// * ur - Urdu +class GlobalWidgetsLocalizations implements WidgetsLocalizations { + /// Construct an object that defines the localized values for the widgets + /// library for the given `locale`. + /// + /// [LocalizationsDelegate] implementations typically call the static [load] + /// function, rather than constructing this class directly. + GlobalWidgetsLocalizations(this.locale) { + final String language = locale.languageCode.toLowerCase(); + _textDirection = _rtlLanguages.contains(language) ? TextDirection.rtl : TextDirection.ltr; + } + + // See http://en.wikipedia.org/wiki/Right-to-left + static const List _rtlLanguages = const [ + 'ar', // Arabic + 'fa', // Farsi + 'he', // Hebrew + 'ps', // Pashto + 'sd', // Sindhi + 'ur', // Urdu + ]; + + /// The locale for which the values of this class's localized resources + /// have been translated. + final Locale locale; + + @override + TextDirection get textDirection => _textDirection; + TextDirection _textDirection; + + /// Creates an object that provides localized resource values for the + /// lowest levels of the Flutter framework. + /// + /// This method is typically used to create a [LocalizationsDelegate]. + /// The [WidgetsApp] does so by default. + static Future load(Locale locale) { + return new SynchronousFuture(new GlobalWidgetsLocalizations(locale)); + } + + /// A [LocalizationsDelegate] that uses [GlobalWidgetsLocalizations.load] + /// to create an instance of this class. + /// + /// [WidgetsApp] automatically adds this value to [WidgetApp.localizationsDelegates]. + static const LocalizationsDelegate delegate = const _WidgetsLocalizationsDelegate(); +} + +class _WidgetsLocalizationsDelegate extends LocalizationsDelegate { + const _WidgetsLocalizationsDelegate(); + + @override + Future load(Locale locale) => GlobalWidgetsLocalizations.load(locale); + + @override + bool shouldReload(_WidgetsLocalizationsDelegate old) => false; +} diff --git a/packages/flutter_localizations/pubspec.yaml b/packages/flutter_localizations/pubspec.yaml new file mode 100644 index 0000000000..70fd20c778 --- /dev/null +++ b/packages/flutter_localizations/pubspec.yaml @@ -0,0 +1,63 @@ +name: flutter_localizations +version: 0.0.1-dev +dependencies: + # To update these, use "flutter update-packages --force-upgrade". + + flutter: + sdk: flutter + intl: 0.15.1 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: 2.2.0 + + args: 0.13.7 # TRANSITIVE DEPENDENCY + async: 1.13.3 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY + boolean_selector: 1.0.2 # TRANSITIVE DEPENDENCY + charcode: 1.1.1 # TRANSITIVE DEPENDENCY + cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY + collection: 1.14.3 # TRANSITIVE DEPENDENCY + convert: 2.0.1 # TRANSITIVE DEPENDENCY + crypto: 2.0.2+1 # TRANSITIVE DEPENDENCY + csslib: 0.14.1 # TRANSITIVE DEPENDENCY + dart_style: 1.0.8 # TRANSITIVE DEPENDENCY + glob: 1.1.5 # TRANSITIVE DEPENDENCY + html: 0.13.2 # TRANSITIVE DEPENDENCY + http: 0.11.3+14 # TRANSITIVE DEPENDENCY + http_multi_server: 2.0.4 # TRANSITIVE DEPENDENCY + http_parser: 3.1.1 # TRANSITIVE DEPENDENCY + intl_translation: 0.15.0 # TRANSITIVE DEPENDENCY + isolate: 1.1.0 # TRANSITIVE DEPENDENCY + js: 0.6.1 # TRANSITIVE DEPENDENCY + logging: 0.11.3+1 # TRANSITIVE DEPENDENCY + matcher: 0.12.1+4 # TRANSITIVE DEPENDENCY + meta: 1.1.1 # TRANSITIVE DEPENDENCY + mime: 0.9.3 # TRANSITIVE DEPENDENCY + node_preamble: 1.4.0 # TRANSITIVE DEPENDENCY + package_config: 1.0.3 # TRANSITIVE DEPENDENCY + package_resolver: 1.0.2 # TRANSITIVE DEPENDENCY + path: 1.4.2 # TRANSITIVE DEPENDENCY + petitparser: 1.6.1 # TRANSITIVE DEPENDENCY + plugin: 0.2.0+2 # TRANSITIVE DEPENDENCY + pool: 1.3.3 # TRANSITIVE DEPENDENCY + pub_semver: 1.3.2 # TRANSITIVE DEPENDENCY + shelf: 0.7.0 # TRANSITIVE DEPENDENCY + shelf_packages_handler: 1.0.3 # TRANSITIVE DEPENDENCY + shelf_static: 0.2.5 # TRANSITIVE DEPENDENCY + shelf_web_socket: 0.2.2 # TRANSITIVE DEPENDENCY + source_map_stack_trace: 1.1.4 # TRANSITIVE DEPENDENCY + source_maps: 0.10.4 # TRANSITIVE DEPENDENCY + source_span: 1.4.0 # TRANSITIVE DEPENDENCY + stack_trace: 1.8.2 # TRANSITIVE DEPENDENCY + stream_channel: 1.6.2 # TRANSITIVE DEPENDENCY + string_scanner: 1.0.2 # TRANSITIVE DEPENDENCY + term_glyph: 1.0.0 # TRANSITIVE DEPENDENCY + test: 0.12.24+8 # TRANSITIVE DEPENDENCY + typed_data: 1.1.4 # TRANSITIVE DEPENDENCY + utf: 0.9.0+3 # TRANSITIVE DEPENDENCY + vector_math: 2.0.5 # TRANSITIVE DEPENDENCY + watcher: 0.9.7+4 # TRANSITIVE DEPENDENCY + web_socket_channel: 1.0.6 # TRANSITIVE DEPENDENCY + yaml: 2.1.13 # TRANSITIVE DEPENDENCY diff --git a/packages/flutter_localizations/test/basics_test.dart b/packages/flutter_localizations/test/basics_test.dart new file mode 100644 index 0000000000..f6f7883c02 --- /dev/null +++ b/packages/flutter_localizations/test/basics_test.dart @@ -0,0 +1,79 @@ +// Copyright 2016 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:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Nested Localizations', (WidgetTester tester) async { + await tester.pumpWidget(new MaterialApp( // Creates the outer Localizations widget. + home: new ListView( + children: [ + new LocalizationTracker(key: const ValueKey('outer')), + new Localizations( + locale: const Locale('zh', 'CN'), + delegates: GlobalMaterialLocalizations.delegates, + child: new LocalizationTracker(key: const ValueKey('inner')), + ), + ], + ), + )); + + final LocalizationTrackerState outerTracker = tester.state(find.byKey(const ValueKey('outer'))); + expect(outerTracker.captionFontSize, 12.0); + final LocalizationTrackerState innerTracker = tester.state(find.byKey(const ValueKey('inner'))); + expect(innerTracker.captionFontSize, 13.0); + }); + + testWidgets('Localizations is compatible with ChangeNotifier.dispose() called during didChangeDependencies', (WidgetTester tester) async { + // PageView calls ScrollPosition.dispose() during didChangeDependencies. + await tester.pumpWidget( + new MaterialApp( + supportedLocales: const [ + const Locale('en', 'US'), + const Locale('es', 'ES'), + ], + localizationsDelegates: >[ + new _DummyLocalizationsDelegate(), + GlobalMaterialLocalizations.delegate, + ], + home: new PageView(), + ) + ); + + await tester.binding.setLocale('es', 'US'); + await tester.pump(); + await tester.pumpWidget(new Container()); + }); +} + +/// A localizations delegate that does not contain any useful data, and is only +/// used to trigger didChangeDependencies upon locale change. +class _DummyLocalizationsDelegate extends LocalizationsDelegate { + @override + Future load(Locale locale) async => new DummyLocalizations(); + + @override + bool shouldReload(_DummyLocalizationsDelegate old) => true; +} + +class DummyLocalizations {} + +class LocalizationTracker extends StatefulWidget { + LocalizationTracker({Key key}) : super(key: key); + + @override + State createState() => new LocalizationTrackerState(); +} + +class LocalizationTrackerState extends State { + double captionFontSize; + + @override + Widget build(BuildContext context) { + captionFontSize = Theme.of(context).textTheme.caption.fontSize; + return new Container(); + } +} diff --git a/packages/flutter_localizations/test/date_picker_test.dart b/packages/flutter_localizations/test/date_picker_test.dart new file mode 100644 index 0000000000..123ffaf928 --- /dev/null +++ b/packages/flutter_localizations/test/date_picker_test.dart @@ -0,0 +1,236 @@ +// Copyright 2016 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:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + DateTime firstDate; + DateTime lastDate; + DateTime initialDate; + + setUp(() { + firstDate = new DateTime(2001, DateTime.JANUARY, 1); + lastDate = new DateTime(2031, DateTime.DECEMBER, 31); + initialDate = new DateTime(2016, DateTime.JANUARY, 15); + }); + + group(DayPicker, () { + final Map> testLocales = >{ + // Tests the default. + const Locale('en', 'US'): { + 'textDirection': TextDirection.ltr, + 'expectedDaysOfWeek': ['S', 'M', 'T', 'W', 'T', 'F', 'S'], + 'expectedDaysOfMonth': new List.generate(30, (int i) => '${i + 1}'), + 'expectedMonthYearHeader': 'September 2017', + }, + // Tests a different first day of week. + const Locale('ru', 'RU'): { + 'textDirection': TextDirection.ltr, + 'expectedDaysOfWeek': ['пн', 'вт', 'ср', 'чт', 'пт', 'сб', 'вс'], + 'expectedDaysOfMonth': new List.generate(30, (int i) => '${i + 1}'), + 'expectedMonthYearHeader': 'сентябрь 2017 г.', + }, + // Tests RTL. + // TODO: change to Arabic numerals when these are fixed: + // TODO: https://github.com/dart-lang/intl/issues/143 + // TODO: https://github.com/flutter/flutter/issues/12289 + const Locale('ar', 'AR'): { + 'textDirection': TextDirection.rtl, + 'expectedDaysOfWeek': ['ح', 'ن', 'ث', 'ر', 'خ', 'ج', 'س'], + 'expectedDaysOfMonth': new List.generate(30, (int i) => '${i + 1}'), + 'expectedMonthYearHeader': 'سبتمبر 2017', + }, + }; + + for (Locale locale in testLocales.keys) { + testWidgets('shows dates for $locale', (WidgetTester tester) async { + final List expectedDaysOfWeek = testLocales[locale]['expectedDaysOfWeek']; + final List expectedDaysOfMonth = testLocales[locale]['expectedDaysOfMonth']; + final String expectedMonthYearHeader = testLocales[locale]['expectedMonthYearHeader']; + final TextDirection textDirection = testLocales[locale]['textDirection']; + final DateTime baseDate = new DateTime(2017, 9, 27); + + await _pumpBoilerplate(tester, new DayPicker( + selectedDate: baseDate, + currentDate: baseDate, + onChanged: (DateTime newValue) {}, + firstDate: baseDate.subtract(const Duration(days: 90)), + lastDate: baseDate.add(const Duration(days: 90)), + displayedMonth: baseDate, + ), locale: locale, textDirection: textDirection); + + expect(find.text(expectedMonthYearHeader), findsOneWidget); + + expectedDaysOfWeek.forEach((String dayOfWeek) { + expect(find.text(dayOfWeek), findsWidgets); + }); + + Offset previousCellOffset; + expectedDaysOfMonth.forEach((String dayOfMonth) { + final Finder dayCell = find.descendant(of: find.byType(GridView), matching: find.text(dayOfMonth)); + expect(dayCell, findsOneWidget); + + // Check that cells are correctly positioned relative to each other, + // taking text direction into account. + final Offset offset = tester.getCenter(dayCell); + if (previousCellOffset != null) { + if (textDirection == TextDirection.ltr) { + expect(offset.dx > previousCellOffset.dx && offset.dy == previousCellOffset.dy || offset.dy > previousCellOffset.dy, true); + } else { + expect(offset.dx < previousCellOffset.dx && offset.dy == previousCellOffset.dy || offset.dy > previousCellOffset.dy, true); + } + } + previousCellOffset = offset; + }); + }); + } + }); + + testWidgets('locale parameter overrides ambient locale', (WidgetTester tester) async { + await tester.pumpWidget(new MaterialApp( + locale: const Locale('en', 'US'), + supportedLocales: const [ + const Locale('en', 'US'), + const Locale('fr', 'CA'), + ], + localizationsDelegates: GlobalMaterialLocalizations.delegates, + home: new Material( + child: new Builder( + builder: (BuildContext context) { + return new FlatButton( + onPressed: () async { + await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: firstDate, + lastDate: lastDate, + locale: const Locale('fr', 'CA'), + ); + }, + child: const Text('X'), + ); + }, + ), + ), + )); + + await tester.tap(find.text('X')); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + final Element dayPicker = tester.element(find.byType(DayPicker)); + expect( + Localizations.localeOf(dayPicker), + const Locale('fr', 'CA'), + ); + + expect( + Directionality.of(dayPicker), + TextDirection.ltr, + ); + + await tester.tap(find.text('ANNULER')); + }); + + testWidgets('textDirection parameter overrides ambient textDirection', (WidgetTester tester) async { + await tester.pumpWidget(new MaterialApp( + locale: const Locale('en', 'US'), + supportedLocales: const [ + const Locale('en', 'US'), + ], + home: new Material( + child: new Builder( + builder: (BuildContext context) { + return new FlatButton( + onPressed: () async { + await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: firstDate, + lastDate: lastDate, + textDirection: TextDirection.rtl, + ); + }, + child: const Text('X'), + ); + }, + ), + ), + )); + + await tester.tap(find.text('X')); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + final Element dayPicker = tester.element(find.byType(DayPicker)); + expect( + Directionality.of(dayPicker), + TextDirection.rtl, + ); + + await tester.tap(find.text('CANCEL')); + }); + + testWidgets('textDirection parameter takes precendence over locale parameter', (WidgetTester tester) async { + await tester.pumpWidget(new MaterialApp( + locale: const Locale('en', 'US'), + supportedLocales: const [ + const Locale('en', 'US'), + const Locale('fr', 'CA'), + ], + localizationsDelegates: GlobalMaterialLocalizations.delegates, + home: new Material( + child: new Builder( + builder: (BuildContext context) { + return new FlatButton( + onPressed: () async { + await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: firstDate, + lastDate: lastDate, + locale: const Locale('fr', 'CA'), + textDirection: TextDirection.rtl, + ); + }, + child: const Text('X'), + ); + }, + ), + ), + )); + + await tester.tap(find.text('X')); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + final Element dayPicker = tester.element(find.byType(DayPicker)); + expect( + Localizations.localeOf(dayPicker), + const Locale('fr', 'CA'), + ); + + expect( + Directionality.of(dayPicker), + TextDirection.rtl, + ); + + await tester.tap(find.text('ANNULER')); + }); +} + +Future _pumpBoilerplate( + WidgetTester tester, + Widget child, { + Locale locale = const Locale('en', 'US'), + TextDirection textDirection: TextDirection.ltr +}) async { + await tester.pumpWidget(new Directionality( + textDirection: TextDirection.ltr, + child: new Localizations( + locale: locale, + delegates: GlobalMaterialLocalizations.delegates, + child: child, + ), + )); +} diff --git a/packages/flutter_localizations/test/date_time_test.dart b/packages/flutter_localizations/test/date_time_test.dart new file mode 100644 index 0000000000..a1caf27c5b --- /dev/null +++ b/packages/flutter_localizations/test/date_time_test.dart @@ -0,0 +1,122 @@ +// 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 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group(GlobalMaterialLocalizations, () { + test('uses exact locale when exists', () { + final GlobalMaterialLocalizations localizations = new GlobalMaterialLocalizations(const Locale('pt', 'PT')); + expect(localizations.formatDecimal(10000), '10\u00A0000'); + }); + + test('falls back to language code when exact locale is missing', () { + final GlobalMaterialLocalizations localizations = new GlobalMaterialLocalizations(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 GlobalMaterialLocalizations localizations = new GlobalMaterialLocalizations(const Locale('xx', 'XX')); + expect(localizations.formatDecimal(10000), '10,000'); + }); + + group('formatHour', () { + test('formats h', () { + GlobalMaterialLocalizations localizations; + + localizations = new GlobalMaterialLocalizations(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 GlobalMaterialLocalizations(const Locale('ar', '')); + expect(localizations.formatHour(const TimeOfDay(hour: 10, minute: 0)), '١٠'); + expect(localizations.formatHour(const TimeOfDay(hour: 20, minute: 0)), '٨'); + }); + + test('formats HH', () { + GlobalMaterialLocalizations localizations; + + localizations = new GlobalMaterialLocalizations(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 GlobalMaterialLocalizations(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', () { + GlobalMaterialLocalizations localizations; + + localizations = new GlobalMaterialLocalizations(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 GlobalMaterialLocalizations(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 GlobalMaterialLocalizations localizations = new GlobalMaterialLocalizations(const Locale('en', 'US')); + expect(localizations.formatMinute(const TimeOfDay(hour: 1, minute: 32)), '32'); + }); + + test('formats Arabic', () { + final GlobalMaterialLocalizations localizations = new GlobalMaterialLocalizations(const Locale('ar', '')); + expect(localizations.formatMinute(const TimeOfDay(hour: 1, minute: 32)), '٣٢'); + }); + }); + + group('formatTimeOfDay', () { + test('formats ${TimeOfDayFormat.h_colon_mm_space_a}', () { + GlobalMaterialLocalizations localizations; + + localizations = new GlobalMaterialLocalizations(const Locale('ar', '')); + expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '٩:٣٢ ص'); + + localizations = new GlobalMaterialLocalizations(const Locale('en', '')); + expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '9:32 AM'); + }); + + test('formats ${TimeOfDayFormat.HH_colon_mm}', () { + GlobalMaterialLocalizations localizations; + + localizations = new GlobalMaterialLocalizations(const Locale('de', '')); + expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '09:32'); + + localizations = new GlobalMaterialLocalizations(const Locale('en', 'ZA')); + expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '09:32'); + }); + + test('formats ${TimeOfDayFormat.H_colon_mm}', () { + GlobalMaterialLocalizations localizations; + + localizations = new GlobalMaterialLocalizations(const Locale('es', '')); + expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '9:32'); + + localizations = new GlobalMaterialLocalizations(const Locale('ja', '')); + expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '9:32'); + }); + + test('formats ${TimeOfDayFormat.frenchCanadian}', () { + GlobalMaterialLocalizations localizations; + + localizations = new GlobalMaterialLocalizations(const Locale('fr', 'CA')); + expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '09 h 32'); + }); + + test('formats ${TimeOfDayFormat.a_space_h_colon_mm}', () { + GlobalMaterialLocalizations localizations; + + localizations = new GlobalMaterialLocalizations(const Locale('zh', '')); + expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '上午 9:32'); + }); + }); + }); +} diff --git a/packages/flutter_localizations/test/override_test.dart b/packages/flutter_localizations/test/override_test.dart new file mode 100644 index 0000000000..16cc58d69a --- /dev/null +++ b/packages/flutter_localizations/test/override_test.dart @@ -0,0 +1,214 @@ +// Copyright 2016 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:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FooMaterialLocalizations extends GlobalMaterialLocalizations { + FooMaterialLocalizations(Locale locale) : super(locale); + + @override + String get backButtonTooltip => 'foo'; +} + +class FooMaterialLocalizationsDelegate extends LocalizationsDelegate { + const FooMaterialLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return new SynchronousFuture(new FooMaterialLocalizations(locale)); + } + + @override + bool shouldReload(FooMaterialLocalizationsDelegate old) => false; +} + +Widget buildFrame({ + Locale locale, + Iterable> delegates: GlobalMaterialLocalizations.delegates, + WidgetBuilder buildContent, + LocaleResolutionCallback localeResolutionCallback, + Iterable supportedLocales: const [ + const Locale('en', 'US'), + const Locale('es', 'es'), + ], +}) { + return new MaterialApp( + color: const Color(0xFFFFFFFF), + locale: locale, + supportedLocales: supportedLocales, + localizationsDelegates: delegates, + localeResolutionCallback: localeResolutionCallback, + onGenerateRoute: (RouteSettings settings) { + return new MaterialPageRoute( + builder: (BuildContext context) { + return buildContent(context); + } + ); + }, + ); +} + +void main() { + testWidgets('Locale fallbacks', (WidgetTester tester) async { + final Key textKey = new UniqueKey(); + + await tester.pumpWidget( + buildFrame( + buildContent: (BuildContext context) { + return new Text( + MaterialLocalizations.of(context).backButtonTooltip, + key: textKey, + ); + } + ) + ); + + expect(tester.widget(find.byKey(textKey)).data, 'Back'); + + // Unrecognized locale falls back to 'en' + await tester.binding.setLocale('foo', 'bar'); + await tester.pump(); + expect(tester.widget(find.byKey(textKey)).data, 'Back'); + + // Spanish Bolivia locale, falls back to just 'es' + await tester.binding.setLocale('es', 'bo'); + await tester.pump(); + expect(tester.widget(find.byKey(textKey)).data, 'Espalda'); + }); + + testWidgets('Localizations.override widget tracks parent\'s locale', (WidgetTester tester) async { + Widget buildLocaleFrame(Locale locale) { + return buildFrame( + locale: locale, + buildContent: (BuildContext context) { + return new Localizations.override( + context: context, + child: new Builder( + builder: (BuildContext context) { + // No MaterialLocalizations are defined for the first Localizations + // ancestor, so we should get the values from the default one, i.e. + // the one created by WidgetsApp via the LocalizationsDelegate + // provided by MaterialApp. + return new Text(MaterialLocalizations.of(context).backButtonTooltip); + }, + ), + ); + } + ); + } + + await tester.pumpWidget(buildLocaleFrame(const Locale('en', 'US'))); + expect(find.text('Back'), findsOneWidget); + + await tester.pumpWidget(buildLocaleFrame(const Locale('de', 'DE'))); + expect(find.text('Zurück'), findsOneWidget); + + await tester.pumpWidget(buildLocaleFrame(const Locale('zh', 'CN'))); + expect(find.text('返回'), findsOneWidget); + }); + + testWidgets('Localizations.override widget with hardwired locale', (WidgetTester tester) async { + Widget buildLocaleFrame(Locale locale) { + return buildFrame( + locale: locale, + buildContent: (BuildContext context) { + return new Localizations.override( + context: context, + locale: const Locale('en', 'US'), + child: new Builder( + builder: (BuildContext context) { + // No MaterialLocalizations are defined for the Localizations.override + // ancestor, so we should get all values from the default one, i.e. + // the one created by WidgetsApp via the LocalizationsDelegate + // provided by MaterialApp. + return new Text(MaterialLocalizations.of(context).backButtonTooltip); + }, + ), + ); + } + ); + } + + await tester.pumpWidget(buildLocaleFrame(const Locale('en', 'US'))); + expect(find.text('Back'), findsOneWidget); + + await tester.pumpWidget(buildLocaleFrame(const Locale('de', 'DE'))); + expect(find.text('Back'), findsOneWidget); + + await tester.pumpWidget(buildLocaleFrame(const Locale('zh', 'CN'))); + expect(find.text('Back'), findsOneWidget); + }); + + testWidgets('MaterialApp overrides MaterialLocalizations', (WidgetTester tester) async { + final Key textKey = new UniqueKey(); + + await tester.pumpWidget( + buildFrame( + // Accept whatever locale we're given + localeResolutionCallback: (Locale locale, Iterable supportedLocales) => locale, + delegates: [ + const FooMaterialLocalizationsDelegate(), + ], + buildContent: (BuildContext context) { + // Should always be 'foo', no matter what the locale is + return new Text( + MaterialLocalizations.of(context).backButtonTooltip, + key: textKey, + ); + } + ) + ); + + expect(tester.widget(find.byKey(textKey)).data, 'foo'); + + await tester.binding.setLocale('zh', 'CN'); + await tester.pump(); + expect(find.text('foo'), findsOneWidget); + + await tester.binding.setLocale('de', 'DE'); + await tester.pump(); + expect(find.text('foo'), findsOneWidget); + }); + + testWidgets('deprecated Android/Java locales are modernized', (WidgetTester tester) async { + final Key textKey = new UniqueKey(); + + await tester.pumpWidget( + buildFrame( + supportedLocales: [ + const Locale('en', 'US'), + const Locale('he', 'IL'), + const Locale('yi', 'IL'), + const Locale('id', 'JV'), + ], + buildContent: (BuildContext context) { + return new Text( + '${Localizations.localeOf(context)}', + key: textKey, + ); + }, + ) + ); + + expect(tester.widget(find.byKey(textKey)).data, 'en_US'); + + // Hebrew was iw (ISO-639) is he (ISO-639-1) + await tester.binding.setLocale('iw', 'IL'); + await tester.pump(); + expect(tester.widget(find.byKey(textKey)).data, 'he_IL'); + + // Yiddish was ji (ISO-639) is yi (ISO-639-1) + await tester.binding.setLocale('ji', 'IL'); + await tester.pump(); + expect(tester.widget(find.byKey(textKey)).data, 'yi_IL'); + + // Indonesian was in (ISO-639) is id (ISO-639-1) + await tester.binding.setLocale('in', 'JV'); + await tester.pump(); + expect(tester.widget(find.byKey(textKey)).data, 'id_JV'); + }); +} diff --git a/packages/flutter_localizations/test/time_picker_test.dart b/packages/flutter_localizations/test/time_picker_test.dart new file mode 100644 index 0000000000..8e770d2f5d --- /dev/null +++ b/packages/flutter_localizations/test/time_picker_test.dart @@ -0,0 +1,129 @@ +// Copyright 2016 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:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class _TimePickerLauncher extends StatelessWidget { + const _TimePickerLauncher({ Key key, this.onChanged, this.locale }) : super(key: key); + + final ValueChanged onChanged; + final Locale locale; + + @override + Widget build(BuildContext context) { + return new MaterialApp( + locale: locale, + localizationsDelegates: GlobalMaterialLocalizations.delegates, + home: new Material( + child: new Center( + child: new Builder( + builder: (BuildContext context) { + return new RaisedButton( + child: const Text('X'), + onPressed: () async { + onChanged(await showTimePicker( + context: context, + initialTime: const TimeOfDay(hour: 7, minute: 0) + )); + } + ); + } + ) + ) + ) + ); + } +} + +Future startPicker(WidgetTester tester, ValueChanged onChanged, + { Locale locale: const Locale('en', 'US') }) async { + await tester.pumpWidget(new _TimePickerLauncher(onChanged: onChanged, locale: locale,)); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(const Duration(seconds: 1)); + return tester.getCenter(find.byKey(const Key('time-picker-dial'))); +} + +Future finishPicker(WidgetTester tester) async { + final Element timePickerElement = tester.element(find.byElementPredicate((Element element) => element.widget.runtimeType.toString() == '_TimePickerDialog')); + final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(timePickerElement); + await tester.tap(find.text(materialLocalizations.okButtonLabel)); + await tester.pumpAndSettle(const Duration(seconds: 1)); +} + +void main() { + testWidgets('can localize the header in all known formats', (WidgetTester tester) async { + // TODO(yjbanov): also test `HH.mm` (in_ID), `a h:mm` (ko_KR) and `HH:mm น.` (th_TH) when we have .arb files for them + final Map> locales = >{ + const Locale('en', 'US'): const ['hour h', 'string :', 'minute', 'period'], //'h:mm a' + const Locale('en', 'GB'): const ['hour HH', 'string :', 'minute'], //'HH:mm' + const Locale('es', 'ES'): const ['hour H', 'string :', 'minute'], //'H:mm' + const Locale('fr', 'CA'): const ['hour HH', 'string h', 'minute'], //'HH \'h\' mm' + const Locale('zh', 'ZH'): const ['period', 'hour h', 'string :', 'minute'], //'ah:mm' + }; + + for (Locale locale in locales.keys) { + final Offset center = await startPicker(tester, (TimeOfDay time) { }, locale: locale); + final List actual = []; + tester.element(find.byType(CustomMultiChildLayout)).visitChildren((Element child) { + final LayoutId layout = child.widget; + final String fragmentType = '${layout.child.runtimeType}'; + final dynamic widget = layout.child; + if (fragmentType == '_MinuteControl') { + actual.add('minute'); + } else if (fragmentType == '_DayPeriodControl') { + actual.add('period'); + } else if (fragmentType == '_HourControl') { + actual.add('hour ${widget.hourFormat.toString().split('.').last}'); + } else if (fragmentType == '_StringFragment') { + actual.add('string ${widget.value}'); + } else { + fail('Unsupported fragment type: $fragmentType'); + } + }); + expect(actual, locales[locale]); + await tester.tapAt(new Offset(center.dx, center.dy - 50.0)); + await finishPicker(tester); + } + }); + + testWidgets('uses single-ring 12-hour dial for h hour format', (WidgetTester tester) async { + // Tap along the segment stretching from the center to the edge at + // 12:00 AM position. Because there's only one ring, no matter where you + // tap the time will be the same. See the 24-hour dial test that behaves + // differently. + for (int i = 1; i < 10; i++) { + TimeOfDay result; + final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; }); + final Size size = tester.getSize(find.byKey(const Key('time-picker-dial'))); + final double dy = (size.height / 2.0 / 10) * i; + await tester.tapAt(new Offset(center.dx, center.dy - dy)); + await finishPicker(tester); + expect(result, equals(const TimeOfDay(hour: 0, minute: 0))); + } + }); + + testWidgets('uses two-ring 24-hour dial for H and HH hour formats', (WidgetTester tester) async { + const List locales = const [ + const Locale('en', 'GB'), // HH + const Locale('es', 'ES'), // H + ]; + for (Locale locale in locales) { + // Tap along the segment stretching from the center to the edge at + // 12:00 AM position. There are two rings. At ~70% mark, the ring + // switches between inner ring and outer ring. + for (int i = 1; i < 10; i++) { + TimeOfDay result; + final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; }, locale: locale); + final Size size = tester.getSize(find.byKey(const Key('time-picker-dial'))); + final double dy = (size.height / 2.0 / 10) * i; + await tester.tapAt(new Offset(center.dx, center.dy - dy)); + await finishPicker(tester); + expect(result, equals(new TimeOfDay(hour: i < 7 ? 12 : 0, minute: 0))); + } + } + }); +} diff --git a/packages/flutter_localizations/test/translations_test.dart b/packages/flutter_localizations/test/translations_test.dart new file mode 100644 index 0000000000..81c2c4c2cf --- /dev/null +++ b/packages/flutter_localizations/test/translations_test.dart @@ -0,0 +1,89 @@ +// 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 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + final List languages = [ + 'ar', // Arabic + 'de', // German + 'en', // English + 'es', // Spanish + 'fa', // Farsi (Persian) + 'fr', // French + 'he', // Hebrew + 'it', // Italian + 'ja', // Japanese + 'ps', // Pashto + 'pt', // Portugese + 'ru', // Russian + 'sd', // Sindhi + 'ur', // Urdu + 'zh', // Chinese (simplified) + ]; + + for (String language in languages) { + testWidgets('translations exist for $language', (WidgetTester tester) async { + final Locale locale = new Locale(language, ''); + final MaterialLocalizations localizations = new GlobalMaterialLocalizations(locale); + + expect(localizations.openAppDrawerTooltip, isNotNull); + expect(localizations.backButtonTooltip, isNotNull); + expect(localizations.closeButtonTooltip, isNotNull); + expect(localizations.nextMonthTooltip, isNotNull); + expect(localizations.previousMonthTooltip, isNotNull); + expect(localizations.nextPageTooltip, isNotNull); + expect(localizations.previousPageTooltip, isNotNull); + expect(localizations.showMenuTooltip, isNotNull); + expect(localizations.licensesPageTitle, isNotNull); + expect(localizations.rowsPerPageTitle, isNotNull); + expect(localizations.cancelButtonLabel, isNotNull); + expect(localizations.closeButtonLabel, isNotNull); + expect(localizations.continueButtonLabel, isNotNull); + expect(localizations.copyButtonLabel, isNotNull); + expect(localizations.cutButtonLabel, isNotNull); + expect(localizations.okButtonLabel, isNotNull); + expect(localizations.pasteButtonLabel, isNotNull); + expect(localizations.selectAllButtonLabel, isNotNull); + expect(localizations.viewLicensesButtonLabel, isNotNull); + + expect(localizations.aboutListTileTitle('FOO'), isNotNull); + expect(localizations.aboutListTileTitle('FOO'), contains('FOO')); + + expect(localizations.selectedRowCountTitle(0), isNotNull); + expect(localizations.selectedRowCountTitle(1), isNotNull); + expect(localizations.selectedRowCountTitle(2), isNotNull); + expect(localizations.selectedRowCountTitle(100), isNotNull); + expect(localizations.selectedRowCountTitle(0).contains(r'$selectedRowCount'), isFalse); + expect(localizations.selectedRowCountTitle(1).contains(r'$selectedRowCount'), isFalse); + expect(localizations.selectedRowCountTitle(2).contains(r'$selectedRowCount'), isFalse); + expect(localizations.selectedRowCountTitle(100).contains(r'$selectedRowCount'), isFalse); + + expect(localizations.pageRowsInfoTitle(1, 10, 100, true), isNotNull); + expect(localizations.pageRowsInfoTitle(1, 10, 100, false), isNotNull); + expect(localizations.pageRowsInfoTitle(1, 10, 100, true).contains(r'$firstRow'), isFalse); + expect(localizations.pageRowsInfoTitle(1, 10, 100, true).contains(r'$lastRow'), isFalse); + expect(localizations.pageRowsInfoTitle(1, 10, 100, true).contains(r'$rowCount'), isFalse); + expect(localizations.pageRowsInfoTitle(1, 10, 100, false).contains(r'$firstRow'), isFalse); + expect(localizations.pageRowsInfoTitle(1, 10, 100, false).contains(r'$lastRow'), isFalse); + expect(localizations.pageRowsInfoTitle(1, 10, 100, false).contains(r'$rowCount'), isFalse); + }); + } + + testWidgets('spot check selectedRowCount translations', (WidgetTester tester) async { + MaterialLocalizations localizations = new GlobalMaterialLocalizations(const Locale('en', '')); + expect(localizations.selectedRowCountTitle(0), 'No items selected'); + expect(localizations.selectedRowCountTitle(1), '1 item selected'); + expect(localizations.selectedRowCountTitle(2), '2 items selected'); + expect(localizations.selectedRowCountTitle(123456789), '123,456,789 items selected'); + + localizations = new GlobalMaterialLocalizations(const Locale('es', '')); + expect(localizations.selectedRowCountTitle(0), 'No se han seleccionado elementos'); + expect(localizations.selectedRowCountTitle(1), '1 artículo seleccionado'); + expect(localizations.selectedRowCountTitle(2), '2 artículos seleccionados'); + expect(localizations.selectedRowCountTitle(123456789), '123.456.789 artículos seleccionados'); + }); +} diff --git a/packages/flutter/test/widgets/localizations_test.dart b/packages/flutter_localizations/test/widgets_test.dart similarity index 97% rename from packages/flutter/test/widgets/localizations_test.dart rename to packages/flutter_localizations/test/widgets_test.dart index 489947ab78..e5ca6280f0 100644 --- a/packages/flutter/test/widgets/localizations_test.dart +++ b/packages/flutter_localizations/test/widgets_test.dart @@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; class TestLocalizations { TestLocalizations(this.locale, this.prefix); @@ -103,20 +104,7 @@ class AsyncMoreLocalizationsDelegate extends LocalizationsDelegate false; } -// Same as _WidgetsLocalizationsDelegate in widgets/app.dart -class DefaultWidgetsLocalizationsDelegate extends LocalizationsDelegate { - const DefaultWidgetsLocalizationsDelegate(); - - @override - Future load(Locale locale) => DefaultWidgetsLocalizations.load(locale); - - @override - bool shouldReload(DefaultWidgetsLocalizationsDelegate old) => false; -} - class OnlyRTLDefaultWidgetsLocalizations extends DefaultWidgetsLocalizations { - OnlyRTLDefaultWidgetsLocalizations(Locale locale) : super(locale); - @override TextDirection get textDirection => TextDirection.rtl; } @@ -126,7 +114,7 @@ class OnlyRTLDefaultWidgetsLocalizationsDelegate extends LocalizationsDelegate load(Locale locale) { - return new SynchronousFuture(new OnlyRTLDefaultWidgetsLocalizations(locale)); + return new SynchronousFuture(new OnlyRTLDefaultWidgetsLocalizations()); } @override @@ -224,7 +212,7 @@ void main() { testWidgets('Synchronously loaded localizations in a WidgetsApp', (WidgetTester tester) async { final List> delegates = >[ new SyncTestLocalizationsDelegate(), - const DefaultWidgetsLocalizationsDelegate(), + DefaultWidgetsLocalizations.delegate, ]; Future pumpTest(Locale locale) async { @@ -349,7 +337,7 @@ void main() { locale: const Locale('en', 'GB'), delegates: >[ new SyncTestLocalizationsDelegate(), - const DefaultWidgetsLocalizationsDelegate(), + DefaultWidgetsLocalizations.delegate, ], // Create a new context within the en_GB Localization child: new Builder( @@ -476,6 +464,9 @@ void main() { await tester.pumpWidget( buildFrame( + delegates: const >[ + GlobalWidgetsLocalizations.delegate, + ], supportedLocales: const [ const Locale('en', 'GB'), const Locale('ar', 'EG'), @@ -557,6 +548,9 @@ void main() { buildFrame( // Accept whatever locale we're given localeResolutionCallback: (Locale locale, Iterable supportedLocales) => locale, + delegates: const >[ + GlobalWidgetsLocalizations.delegate, + ], buildContent: (BuildContext context) { return new Localizations.override( context: context, diff --git a/packages/flutter_test/pubspec.yaml b/packages/flutter_test/pubspec.yaml index 1c7bbb9032..45e6f896e2 100644 --- a/packages/flutter_test/pubspec.yaml +++ b/packages/flutter_test/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: args: 0.13.7 # TRANSITIVE DEPENDENCY async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY boolean_selector: 1.0.2 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY cli_util: 0.1.2+1 # TRANSITIVE DEPENDENCY diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 3f14f1b828..f6c7329d99 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -48,7 +48,7 @@ dev_dependencies: mockito: 2.2.0 async: 1.13.3 # TRANSITIVE DEPENDENCY - barback: 0.15.2+12 # TRANSITIVE DEPENDENCY + barback: 0.15.2+13 # TRANSITIVE DEPENDENCY boolean_selector: 1.0.2 # TRANSITIVE DEPENDENCY charcode: 1.1.1 # TRANSITIVE DEPENDENCY convert: 2.0.1 # TRANSITIVE DEPENDENCY