Update gen_l10n tool to require base locale; Stocks app refresh (#51602)

This commit is contained in:
Shi-Hao Hong 2020-02-28 22:21:01 -08:00 committed by GitHub
parent 6a337a76dd
commit c98500dea3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 158 additions and 196 deletions

View file

@ -23,16 +23,10 @@ The `flutter run --release` command both builds and installs the Flutter app.
## 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

View file

@ -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);
}

View file

@ -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")
};
}

View file

@ -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")
};
}

View file

@ -6,8 +6,9 @@ stocks app uses the [Dart `intl` package](https://github.com/dart-lang/intl).
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 @@ dart ${FLUTTER_PATH}/dev/tools/localization/bin/gen_l10n.dart --arb-dir=lib/i18n
--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`.

View file

@ -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 @@ import 'messages_all.dart';
/// 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 @@ class StockStrings {
/// 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 @@ class StockStrings {
/// 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'
);
}
String get title {
return Intl.message(
r'Stocks',
locale: _localeName,
name: 'title',
desc: r'Title for the Stocks application'
);
}
// Label for the Market tab
String get market;
// 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 @@ class _StockStringsDelegate extends LocalizationsDelegate<StockStrings> {
@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;
}

View file

@ -0,0 +1,5 @@
{
"title": "Stocks",
"market": "MARKET",
"portfolio": "PORTFOLIO"
}

View file

@ -11,18 +11,10 @@ void main() {
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.

View file

@ -8,6 +8,7 @@ import 'dart:async';
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 @@ import 'package:intl/intl.dart' as intl;
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) {

View file

@ -423,6 +423,23 @@ class AppResourceBundleCollection {
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);
}

View file

@ -15,7 +15,7 @@ import '../../localization/localizations_utils.dart';
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 @@ void main() {
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 @@ void main() {
.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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
() {
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 @@ void main() {
() {
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 @@ void main() {
.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 @@ void main() {
}
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 @@ void main() {
'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', () {