Update gen_l10n tool to require base locale; Stocks app refresh (#51602)
diff --git a/dev/benchmarks/test_apps/stocks/README.md b/dev/benchmarks/test_apps/stocks/README.md index d8d0785..88b3dde 100644 --- a/dev/benchmarks/test_apps/stocks/README.md +++ b/dev/benchmarks/test_apps/stocks/README.md
@@ -23,16 +23,10 @@ ## Internationalization This app has been internationalized (just enough to show how it's -done). It's an example of how one can do so with the -[Dart intl package](https://pub.dev/packages/intl). +done). It's an example of how one can do so with the gen_l10n tool. -The [Flutter Internationalization Tutorial](https://flutter.dev/tutorials/internationalization/) -covers Flutter app internationalization in general. - -See [regenerate.md](lib/i18n/regenerate.md) for an explanation -of how the Dart internationalization tools, like -`intl_translation:generate_from_arb`, were used to generate -localizations for this app. +See [regenerate.md](lib/i18n/regenerate.md) for an explanation for how +the tool is used to generate localizations for this app. ## Icon
diff --git a/dev/benchmarks/test_apps/stocks/lib/i18n/.dartignore b/dev/benchmarks/test_apps/stocks/lib/i18n/.dartignore deleted file mode 100644 index e69de29..0000000 --- a/dev/benchmarks/test_apps/stocks/lib/i18n/.dartignore +++ /dev/null
diff --git a/dev/benchmarks/test_apps/stocks/lib/i18n/messages_all.dart b/dev/benchmarks/test_apps/stocks/lib/i18n/messages_all.dart deleted file mode 100644 index 867afc7..0000000 --- a/dev/benchmarks/test_apps/stocks/lib/i18n/messages_all.dart +++ /dev/null
@@ -1,67 +0,0 @@ -// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart -// This is a library that looks up messages for specific locales by -// delegating to the appropriate library. - -// Ignore issues from commonly used lints in this file. -// ignore_for_file:implementation_imports, file_names, unnecessary_new -// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering -// ignore_for_file:argument_type_not_assignable, invalid_assignment -// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases -// ignore_for_file:comment_references - -import 'dart:async'; - -import 'package:intl/intl.dart'; -import 'package:intl/message_lookup_by_library.dart'; -import 'package:intl/src/intl_helpers.dart'; - -import 'messages_en_US.dart' as messages_en_us; -import 'messages_es_ES.dart' as messages_es_es; - -typedef Future<dynamic> LibraryLoader(); -Map<String, LibraryLoader> _deferredLibraries = { - 'en_US': () => new Future.value(null), - 'es_ES': () => new Future.value(null), -}; - -MessageLookupByLibrary _findExact(String localeName) { - switch (localeName) { - case 'en_US': - return messages_en_us.messages; - case 'es_ES': - return messages_es_es.messages; - default: - return null; - } -} - -/// User programs should call this before using [localeName] for messages. -Future<bool> initializeMessages(String localeName) async { - var availableLocale = Intl.verifiedLocale( - localeName, - (locale) => _deferredLibraries[locale] != null, - onFailure: (_) => null); - if (availableLocale == null) { - return new Future.value(false); - } - var lib = _deferredLibraries[availableLocale]; - await (lib == null ? new Future.value(false) : lib()); - initializeInternalMessageLookup(() => new CompositeMessageLookup()); - messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); - return new Future.value(true); -} - -bool _messagesExistFor(String locale) { - try { - return _findExact(locale) != null; - } catch (e) { - return false; - } -} - -MessageLookupByLibrary _findGeneratedMessagesFor(String locale) { - var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor, - onFailure: (_) => null); - if (actualLocale == null) return null; - return _findExact(actualLocale); -}
diff --git a/dev/benchmarks/test_apps/stocks/lib/i18n/messages_en_US.dart b/dev/benchmarks/test_apps/stocks/lib/i18n/messages_en_US.dart deleted file mode 100644 index fedb57c..0000000 --- a/dev/benchmarks/test_apps/stocks/lib/i18n/messages_en_US.dart +++ /dev/null
@@ -1,28 +0,0 @@ -// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart -// This is a library that provides messages for a en_US locale. All the -// messages from the main program should be duplicated here with the same -// function name. - -// Ignore issues from commonly used lints in this file. -// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new -// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering -// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases -// ignore_for_file:unused_import, file_names - -import 'package:intl/intl.dart'; -import 'package:intl/message_lookup_by_library.dart'; - -final messages = new MessageLookup(); - -typedef String MessageIfAbsent(String messageStr, List<dynamic> args); - -class MessageLookup extends MessageLookupByLibrary { - String get localeName => 'en_US'; - - final messages = _notInlinedMessages(_notInlinedMessages); - static _notInlinedMessages(_) => <String, Function> { - "market" : MessageLookupByLibrary.simpleMessage("MARKET"), - "portfolio" : MessageLookupByLibrary.simpleMessage("PORTFOLIO"), - "title" : MessageLookupByLibrary.simpleMessage("Stocks") - }; -}
diff --git a/dev/benchmarks/test_apps/stocks/lib/i18n/messages_es_ES.dart b/dev/benchmarks/test_apps/stocks/lib/i18n/messages_es_ES.dart deleted file mode 100644 index b2c0fd3..0000000 --- a/dev/benchmarks/test_apps/stocks/lib/i18n/messages_es_ES.dart +++ /dev/null
@@ -1,28 +0,0 @@ -// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart -// This is a library that provides messages for a es_ES locale. All the -// messages from the main program should be duplicated here with the same -// function name. - -// Ignore issues from commonly used lints in this file. -// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new -// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering -// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases -// ignore_for_file:unused_import, file_names - -import 'package:intl/intl.dart'; -import 'package:intl/message_lookup_by_library.dart'; - -final messages = new MessageLookup(); - -typedef String MessageIfAbsent(String messageStr, List<dynamic> args); - -class MessageLookup extends MessageLookupByLibrary { - String get localeName => 'es_ES'; - - final messages = _notInlinedMessages(_notInlinedMessages); - static _notInlinedMessages(_) => <String, Function> { - "market" : MessageLookupByLibrary.simpleMessage("MERCADO"), - "portfolio" : MessageLookupByLibrary.simpleMessage("CARTERA"), - "title" : MessageLookupByLibrary.simpleMessage("Acciones") - }; -}
diff --git a/dev/benchmarks/test_apps/stocks/lib/i18n/regenerate.md b/dev/benchmarks/test_apps/stocks/lib/i18n/regenerate.md index e75b945..c4bbb1f 100644 --- a/dev/benchmarks/test_apps/stocks/lib/i18n/regenerate.md +++ b/dev/benchmarks/test_apps/stocks/lib/i18n/regenerate.md
@@ -6,8 +6,9 @@ Rebuilding everything requires two steps. -1. Create or update the English and Spanish localizations, `stocks_en_US.arb` -and `stocks_es_ES.arb`. See the [ARB specification](https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification) +1. Create or update the English and Spanish localizations, +`stocks_en_US.arb`, `stocks_en.arb`, and `stocks_es.arb`. See the +[ARB specification](https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification) for more info. 2. With `examples/stocks` as the current directory, generate a @@ -20,7 +21,9 @@ --output-class=StockStrings ``` -The `StockStrings` class uses the generated `initializeMessages()`function -(`messages_all.dart`) to load the localized messages and `Intl.message()` -to look them up. The generated class's API documentation explains how to add -the new localizations delegate and supported locales to the Flutter application. +The `StockStrings` class creates a delegate that performs message lookups +based on the locale of the device. In this case, the stocks app supports +`en`, `en_US`, and `es`. Thus, the `StockStringsEn` and `StockStringsEs` +classes extends `StockStrings`. `StockStringsEnUs` extends +`StockStringsEn`. This allows `StockStringsEnUs` to fall back on messages +in `StockStringsEn`. \ No newline at end of file
diff --git a/dev/benchmarks/test_apps/stocks/lib/i18n/stock_strings.dart b/dev/benchmarks/test_apps/stocks/lib/i18n/stock_strings.dart index 8c12e5a..aee1bf6 100644 --- a/dev/benchmarks/test_apps/stocks/lib/i18n/stock_strings.dart +++ b/dev/benchmarks/test_apps/stocks/lib/i18n/stock_strings.dart
@@ -1,10 +1,16 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:intl/intl.dart'; +// ignore: unused_import +import 'package:intl/intl.dart' as intl; -import 'messages_all.dart'; +// ignore_for_file: unnecessary_brace_in_string_interps /// Callers can lookup localized strings with an instance of StockStrings returned /// by `StockStrings.of(context)`. @@ -58,16 +64,12 @@ /// you wish to add from the pop-up menu in the Value field. This list should /// be consistent with the languages listed in the StockStrings.supportedLocales /// property. -class StockStrings { - StockStrings(Locale locale) : _localeName = Intl.canonicalizedLocale(locale.toString()); +abstract class StockStrings { + StockStrings(String locale) : assert(locale != null), _localeName = intl.Intl.canonicalizedLocale(locale.toString()); + // ignore: unused_field final String _localeName; - static Future<StockStrings> load(Locale locale) { - return initializeMessages(locale.toString()) - .then<StockStrings>((_) => StockStrings(locale)); - } - static StockStrings of(BuildContext context) { return Localizations.of<StockStrings>(context, StockStrings); } @@ -80,6 +82,10 @@ /// Returns a list of localizations delegates containing this delegate along with /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[ delegate, GlobalMaterialLocalizations.delegate, @@ -89,44 +95,28 @@ /// A list of this localizations delegate's supported locales. static const List<Locale> supportedLocales = <Locale>[ - Locale('en', 'US'), - Locale('es', 'ES'), + Locale('en'), + Locale('en, US'), + Locale('es') ]; - String get market { - return Intl.message( - r'MARKET', - locale: _localeName, - name: 'market', - desc: r'Label for the Market tab' - ); - } + // Title for the Stocks application + String get title; - String get portfolio { - return Intl.message( - r'PORTFOLIO', - locale: _localeName, - name: 'portfolio', - desc: r'Label for the Portfolio tab' - ); - } + // Label for the Market tab + String get market; - String get title { - return Intl.message( - r'Stocks', - locale: _localeName, - name: 'title', - desc: r'Title for the Stocks application' - ); - } - + // Label for the Portfolio tab + String get portfolio; } class _StockStringsDelegate extends LocalizationsDelegate<StockStrings> { const _StockStringsDelegate(); @override - Future<StockStrings> load(Locale locale) => StockStrings.load(locale); + Future<StockStrings> load(Locale locale) { + return SynchronousFuture<StockStrings>(_lookupStockStrings(locale)); + } @override bool isSupported(Locale locale) => <String>['en', 'es'].contains(locale.languageCode); @@ -134,3 +124,59 @@ @override bool shouldReload(_StockStringsDelegate old) => false; } + +/// The translations for English (`en`). +class StockStringsEn extends StockStrings { + StockStringsEn([String locale = 'en']) : super(locale); + + @override + String get title => 'Stocks'; + + @override + String get market => 'MARKET'; + + @override + String get portfolio => 'PORTFOLIO'; +} + +/// The translations for English, as used in the United States (`en_US`). +class StockStringsEnUs extends StockStringsEn { + StockStringsEnUs([String locale = 'en_US']) : super(locale); + + @override + String get title => 'Stocks'; + + @override + String get market => 'MARKET'; + + @override + String get portfolio => 'PORTFOLIO'; +} + +/// The translations for Spanish Castilian (`es`). +class StockStringsEs extends StockStrings { + StockStringsEs([String locale = 'es']) : super(locale); + + @override + String get title => 'Acciones'; + + @override + String get market => 'MERCADO'; + + @override + String get portfolio => 'CARTERA'; +} + +StockStrings _lookupStockStrings(Locale locale) { + switch(locale.languageCode) { + case 'en': { + switch (locale.countryCode) { + case 'US': return StockStringsEnUs(); + } + return StockStringsEn(); + } + case 'es': return StockStringsEs(); + } + assert(false, 'StockStrings.delegate failed to load unsupported locale "$locale"'); + return null; +}
diff --git a/dev/benchmarks/test_apps/stocks/lib/i18n/stocks_en.arb b/dev/benchmarks/test_apps/stocks/lib/i18n/stocks_en.arb new file mode 100644 index 0000000..90c141e --- /dev/null +++ b/dev/benchmarks/test_apps/stocks/lib/i18n/stocks_en.arb
@@ -0,0 +1,5 @@ +{ + "title": "Stocks", + "market": "MARKET", + "portfolio": "PORTFOLIO" +}
diff --git a/dev/benchmarks/test_apps/stocks/lib/i18n/stocks_es_ES.arb b/dev/benchmarks/test_apps/stocks/lib/i18n/stocks_es.arb similarity index 100% rename from dev/benchmarks/test_apps/stocks/lib/i18n/stocks_es_ES.arb rename to dev/benchmarks/test_apps/stocks/lib/i18n/stocks_es.arb
diff --git a/dev/benchmarks/test_apps/stocks/test/locale_test.dart b/dev/benchmarks/test_apps/stocks/test/locale_test.dart index a17e925..9b2daf5 100644 --- a/dev/benchmarks/test_apps/stocks/test/locale_test.dart +++ b/dev/benchmarks/test_apps/stocks/test/locale_test.dart
@@ -11,18 +11,10 @@ testWidgets('Changing locale', (WidgetTester tester) async { stocks.main(); - await tester.idle(); // see https://github.com/flutter/flutter/issues/1865 await tester.pump(); // The initial test app's locale is "_", so we're seeing the fallback translation here. expect(find.text('MARKET'), findsOneWidget); - await tester.binding.setLocale('es', 'US'); - await tester.idle(); - - // The Localizations widget has been built with the new locale. The - // new locale's strings are loaded asynchronously, so we're still - // displaying the previous locale's strings. - await tester.pump(); - expect(find.text('MARKET'), findsOneWidget); + await tester.binding.setLocale('es', ''); // The localized strings have finished loading and dependent // widgets have been updated.
diff --git a/dev/tools/localization/gen_l10n_templates.dart b/dev/tools/localization/gen_l10n_templates.dart index 328be9a..ad34cad 100644 --- a/dev/tools/localization/gen_l10n_templates.dart +++ b/dev/tools/localization/gen_l10n_templates.dart
@@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +// ignore: unused_import import 'package:intl/intl.dart' as intl; // ignore_for_file: unnecessary_brace_in_string_interps @@ -67,6 +68,7 @@ abstract class @(class) { @(class)(String locale) : assert(locale != null), _localeName = intl.Intl.canonicalizedLocale(locale.toString()); + // ignore: unused_field final String _localeName; static @(class) of(BuildContext context) {
diff --git a/dev/tools/localization/gen_l10n_types.dart b/dev/tools/localization/gen_l10n_types.dart index b760dfc..b2d06a8 100644 --- a/dev/tools/localization/gen_l10n_types.dart +++ b/dev/tools/localization/gen_l10n_types.dart
@@ -423,6 +423,23 @@ languageToLocales[bundle.locale.languageCode].add(bundle.locale); } } + + languageToLocales.forEach((String language, List<LocaleInfo> listOfCorrespondingLocales) { + final List<String> localeStrings = listOfCorrespondingLocales.map((LocaleInfo locale) { + return locale.toString(); + }).toList(); + if (!localeStrings.contains(language)) { + throw L10nException( + 'Arb file for a fallback, $language, does not exist, even though \n' + 'the following locale(s) exist: $listOfCorrespondingLocales. \n' + 'When locales specify a script code or country code, a \n' + 'base locale (without the script code or country code) should \n' + 'exist as the fallback. Please create a {fileName}_$language.arb \n' + 'file.' + ); + } + }); + return AppResourceBundleCollection._(directory, localeToBundle, languageToLocales); }
diff --git a/dev/tools/test/localization/gen_l10n_test.dart b/dev/tools/test/localization/gen_l10n_test.dart index e090bc0..058c0dc 100644 --- a/dev/tools/test/localization/gen_l10n_test.dart +++ b/dev/tools/test/localization/gen_l10n_test.dart
@@ -15,7 +15,7 @@ import '../common.dart'; final String defaultArbPathString = path.join('lib', 'l10n'); -const String defaultTemplateArbFileName = 'app_en_US.arb'; +const String defaultTemplateArbFileName = 'app_en.arb'; const String defaultOutputFileString = 'output-localization-file'; const String defaultClassNameString = 'AppLocalizations'; const String singleMessageArbFileString = ''' @@ -251,7 +251,7 @@ fail('Setting language and locales should not fail: \n$e'); } - expect(generator.supportedLocales.contains(LocaleInfo.fromString('en_US')), true); + expect(generator.supportedLocales.contains(LocaleInfo.fromString('en')), true); expect(generator.supportedLocales.contains(LocaleInfo.fromString('es')), true); }); @@ -263,7 +263,7 @@ .writeAsStringSync(singleZhMessageArbFileString); l10nDirectory.childFile('app_es.arb') .writeAsStringSync(singleEsMessageArbFileString); - l10nDirectory.childFile('app_en_US.arb') + l10nDirectory.childFile('app_en.arb') .writeAsStringSync(singleMessageArbFileString); LocalizationsGenerator generator; @@ -280,7 +280,7 @@ fail('Setting language and locales should not fail: \n$e'); } - expect(generator.supportedLocales.first, LocaleInfo.fromString('en_US')); + expect(generator.supportedLocales.first, LocaleInfo.fromString('en')); expect(generator.supportedLocales.elementAt(1), LocaleInfo.fromString('es')); expect(generator.supportedLocales.elementAt(2), LocaleInfo.fromString('zh')); }); @@ -288,7 +288,7 @@ test('adds preferred locales to the top of supportedLocales and supportedLanguageCodes', () { final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') ..createSync(recursive: true); - l10nDirectory.childFile('app_en_US.arb') + l10nDirectory.childFile('app_en.arb') .writeAsStringSync(singleMessageArbFileString); l10nDirectory.childFile('app_es.arb') .writeAsStringSync(singleEsMessageArbFileString); @@ -313,7 +313,7 @@ expect(generator.supportedLocales.first, LocaleInfo.fromString('zh')); expect(generator.supportedLocales.elementAt(1), LocaleInfo.fromString('es')); - expect(generator.supportedLocales.elementAt(2), LocaleInfo.fromString('en_US')); + expect(generator.supportedLocales.elementAt(2), LocaleInfo.fromString('en')); }); test( @@ -322,14 +322,14 @@ () { final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') ..createSync(recursive: true); - l10nDirectory.childFile('app_en_US.arb') + l10nDirectory.childFile('app_en.arb') .writeAsStringSync(singleMessageArbFileString); l10nDirectory.childFile('app_es.arb') .writeAsStringSync(singleEsMessageArbFileString); l10nDirectory.childFile('app_zh.arb') .writeAsStringSync(singleZhMessageArbFileString); - const String preferredSupportedLocaleString = '[44, "en_US"]'; + const String preferredSupportedLocaleString = '[44, "en"]'; LocalizationsGenerator generator; try { generator = LocalizationsGenerator(fs); @@ -363,7 +363,7 @@ () { final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') ..createSync(recursive: true); - l10nDirectory.childFile('app_en_US.arb') + l10nDirectory.childFile('app_en.arb') .writeAsStringSync(singleMessageArbFileString); l10nDirectory.childFile('app_es.arb') .writeAsStringSync(singleEsMessageArbFileString); @@ -405,7 +405,7 @@ .writeAsStringSync(singleZhMessageArbFileString); l10nDirectory.childFile('app_es.arb') .writeAsStringSync(singleEsMessageArbFileString); - l10nDirectory.childFile('app_en_US.arb') + l10nDirectory.childFile('app_en.arb') .writeAsStringSync(singleMessageArbFileString); LocalizationsGenerator generator; @@ -423,11 +423,11 @@ } if (Platform.isWindows) { - expect(generator.arbPathStrings.first, r'lib\l10n\app_en_US.arb'); + expect(generator.arbPathStrings.first, r'lib\l10n\app_en.arb'); expect(generator.arbPathStrings.elementAt(1), r'lib\l10n\app_es.arb'); expect(generator.arbPathStrings.elementAt(2), r'lib\l10n\app_zh.arb'); } else { - expect(generator.arbPathStrings.first, 'lib/l10n/app_en_US.arb'); + expect(generator.arbPathStrings.first, 'lib/l10n/app_en.arb'); expect(generator.arbPathStrings.elementAt(1), 'lib/l10n/app_es.arb'); expect(generator.arbPathStrings.elementAt(2), 'lib/l10n/app_zh.arb'); } @@ -583,6 +583,32 @@ 'should fail' ); }); + + test('throws when the base locale does not exist', () { + final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true); + l10nDirectory.childFile('app_en_US.arb') + .writeAsStringSync(singleMessageArbFileString); + + try { + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + generator.initialize( + l10nDirectoryPath: defaultArbPathString, + templateArbFileName: 'app_en_US.arb', + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + ); + generator.loadResources(); + } on L10nException catch (e) { + expect(e.message, contains('Arb file for a fallback, en, does not exist')); + return; + } + + fail( + 'Since en_US.arb is specified, but en.arb is not, ' + 'the tool should throw an error.' + ); + }); }); group('generateCode', () {