diff --git a/packages/flutter/lib/src/material/i18n/localizations.dart b/packages/flutter/lib/src/material/i18n/localizations.dart index 454d9e2ff7..144f29a21b 100644 --- a/packages/flutter/lib/src/material/i18n/localizations.dart +++ b/packages/flutter/lib/src/material/i18n/localizations.dart @@ -17,7 +17,14 @@ const Map> localizations = const > localizations = const > localizations = const > localizations = const > localizations = const > localizations = const > localizations = const > localizations = const > localizations = const > localizations = const > localizations = const > localizations = const > localizations = const > localizations = const > localizations = const {}; @@ -99,50 +118,111 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { /// have been translated. final Locale locale; - @override - String get openAppDrawerTooltip => _nameToValue["openAppDrawerTooltip"]; + String get _localeName { + final String localeName = locale.countryCode.isEmpty ? locale.languageCode : locale.toString(); + return 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; + } + + String _formatInteger(int n) { + final String localeName = _localeName; + if (!NumberFormat.localeExists(localeName)) + return n.toString(); + return new NumberFormat.decimalPattern(localeName).format(n); + + } @override - String get backButtonTooltip => _nameToValue["backButtonTooltip"]; + String get openAppDrawerTooltip => _nameToValue['openAppDrawerTooltip']; @override - String get closeButtonTooltip => _nameToValue["closeButtonTooltip"]; + String get backButtonTooltip => _nameToValue['backButtonTooltip']; @override - String get nextMonthTooltip => _nameToValue["nextMonthTooltip"]; + String get closeButtonTooltip => _nameToValue['closeButtonTooltip']; @override - String get previousMonthTooltip => _nameToValue["previousMonthTooltip"]; + String get nextMonthTooltip => _nameToValue['nextMonthTooltip']; @override - String get licensesPageTitle => _nameToValue["licensesPageTitle"]; + String get previousMonthTooltip => _nameToValue['previousMonthTooltip']; @override - String get cancelButtonLabel => _nameToValue["cancelButtonLabel"]; + String get nextPageTooltip => _nameToValue['nextPageTooltip']; @override - String get closeButtonLabel => _nameToValue["closeButtonLabel"]; + String get previousPageTooltip => _nameToValue['previousPageTooltip']; @override - String get continueButtonLabel => _nameToValue["continueButtonLabel"]; + String get showMenuTooltip => _nameToValue['showMenuTooltip']; @override - String get copyButtonLabel => _nameToValue["copyButtonLabel"]; + String get licensesPageTitle => _nameToValue['licensesPageTitle']; @override - String get cutButtonLabel => _nameToValue["cutButtonLabel"]; + 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', _formatInteger(firstRow)) + .replaceFirst(r'$lastRow', _formatInteger(lastRow)) + .replaceFirst(r'$rowCount', _formatInteger(rowCount)); + } @override - String get okButtonLabel => _nameToValue["okButtonLabel"]; + String get rowsPerPageTitle => _nameToValue['rowsPerPageTitle']; @override - String get pasteButtonLabel => _nameToValue["pasteButtonLabel"]; + String selectedRowCountTitle(int selectedRowCount) { + return _nameToPluralValue(selectedRowCount, 'selectedRowCountTitle') // asserts on no match + .replaceFirst(r'$selectedRowCount', _formatInteger(selectedRowCount)); + } @override - String get selectAllButtonLabel => _nameToValue["selectAllButtonLabel"]; + String get cancelButtonLabel => _nameToValue['cancelButtonLabel']; @override - String get viewLicensesButtonLabel => _nameToValue["viewLicensesButtonLabel"]; + 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']; /// Creates an object that provides localized resource values for the /// for the widgets of the material library. diff --git a/packages/flutter/lib/src/material/paginated_data_table.dart b/packages/flutter/lib/src/material/paginated_data_table.dart index 7fc7620de3..64fedfbff1 100644 --- a/packages/flutter/lib/src/material/paginated_data_table.dart +++ b/packages/flutter/lib/src/material/paginated_data_table.dart @@ -16,6 +16,7 @@ import 'data_table_source.dart'; import 'dropdown.dart'; import 'icon_button.dart'; import 'icons.dart'; +import 'material_localizations.dart'; import 'progress_indicator.dart'; import 'theme.dart'; @@ -286,6 +287,7 @@ class PaginatedDataTableState extends State { Widget build(BuildContext context) { // TODO(ianh): This whole build function doesn't handle RTL yet. final ThemeData themeData = Theme.of(context); + final MaterialLocalizations localizations = MaterialLocalizations.of(context); // HEADER final List headerWidgets = []; double startPadding = 24.0; @@ -300,11 +302,10 @@ class PaginatedDataTableState extends State { // TODO(ianh): Better magic. See https://github.com/flutter/flutter/issues/4460 startPadding = 12.0; } - } else if (_selectedRowCount == 1) { - // TODO(ianh): Real l10n. - headerWidgets.add(const Expanded(child: const Text('1 item selected'))); } else { - headerWidgets.add(new Expanded(child: new Text('$_selectedRowCount items selected'))); + headerWidgets.add(new Expanded( + child: new Text(localizations.selectedRowCountTitle(_selectedRowCount)), + )); } if (widget.actions != null) { headerWidgets.addAll( @@ -332,7 +333,7 @@ class PaginatedDataTableState extends State { }) .toList(); footerWidgets.addAll([ - const Text('Rows per page:'), + new Text(localizations.rowsPerPageTitle), new DropdownButtonHideUnderline( child: new DropdownButton( items: availableRowsPerPage, @@ -347,20 +348,25 @@ class PaginatedDataTableState extends State { footerWidgets.addAll([ new Container(width: 32.0), new Text( - '${_firstRowIndex + 1}\u2013${_firstRowIndex + widget.rowsPerPage} ${ _rowCountApproximate ? "of about" : "of" } $_rowCount' + localizations.pageRowsInfoTitle( + _firstRowIndex + 1, + _firstRowIndex + widget.rowsPerPage, + _rowCount, + _rowCountApproximate + ) ), new Container(width: 32.0), new IconButton( icon: const Icon(Icons.chevron_left), padding: EdgeInsets.zero, - tooltip: 'Previous page', + tooltip: localizations.previousPageTooltip, onPressed: _firstRowIndex <= 0 ? null : _handlePrevious ), new Container(width: 24.0), new IconButton( icon: const Icon(Icons.chevron_right), padding: EdgeInsets.zero, - tooltip: 'Next page', + tooltip: localizations.nextPageTooltip, onPressed: (!_rowCountApproximate && (_firstRowIndex + widget.rowsPerPage >= _rowCount)) ? null : _handleNext ), new Container(width: 14.0), diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index 9c48e1a955..df4d4467a8 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -14,6 +14,7 @@ import 'icons.dart'; import 'ink_well.dart'; import 'list_tile.dart'; import 'material.dart'; +import 'material_localizations.dart'; import 'theme.dart'; // Examples can assume: @@ -674,7 +675,7 @@ class PopupMenuButton extends StatefulWidget { @required this.itemBuilder, this.initialValue, this.onSelected, - this.tooltip: 'Show menu', + this.tooltip, this.elevation: 8.0, this.padding: const EdgeInsets.all(8.0), this.child, @@ -765,7 +766,7 @@ class _PopupMenuButtonState extends State> { : new IconButton( icon: widget.icon ?? _getIcon(Theme.of(context).platform), padding: widget.padding, - tooltip: widget.tooltip, + tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, onPressed: showButtonMenu, ); } diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index f5b3885bc8..4f01802d43 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -299,6 +299,21 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv Locale _locale; Locale _resolveLocale(Locale newLocale, Iterable supportedLocales) { + // Android devices (Java really) report 3 deprecated language codes, see + // http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4140555 + // and https://developer.android.com/reference/java/util/Locale.html + switch(newLocale.languageCode) { + case 'iw': + newLocale = new Locale('he', newLocale.countryCode); // Hebrew + break; + case 'ji': + newLocale = new Locale('yi', newLocale.countryCode); // Yiddish + break; + case 'in': + newLocale = new Locale('id', newLocale.countryCode); // Indonesian + break; + } + if (widget.localeResolutionCallback != null) { final Locale locale = widget.localeResolutionCallback(newLocale, widget.supportedLocales); if (locale != null) diff --git a/packages/flutter/test/material/localizations_test.dart b/packages/flutter/test/material/localizations_test.dart index a5067f622a..73e2ecb339 100644 --- a/packages/flutter/test/material/localizations_test.dart +++ b/packages/flutter/test/material/localizations_test.dart @@ -8,14 +8,15 @@ import 'package:flutter_test/flutter_test.dart'; Widget buildFrame({ Locale locale, WidgetBuilder buildContent, + Iterable supportedLocales: const [ + const Locale('en', 'US'), + const Locale('es', 'es'), + ], }) { return new MaterialApp( color: const Color(0xFFFFFFFF), locale: locale, - supportedLocales: const [ - const Locale('en', 'US'), - const Locale('es', 'es'), - ], + supportedLocales: supportedLocales, onGenerateRoute: (RouteSettings settings) { return new MaterialPageRoute( builder: (BuildContext context) { @@ -77,12 +78,17 @@ void main() { 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); @@ -92,6 +98,77 @@ void main() { expect(localizations.pasteButtonLabel, isNotNull); expect(localizations.selectAllButtonLabel, isNotNull); expect(localizations.viewLicensesButtonLabel, isNotNull); + + 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('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'); + }); + }