Cleanup in localizations code (#20018)
The following changes are made by this PR: * Translation bundles now implement MaterialLocalizations directly, and are public so that they can be directly extended. * The list of supported languages is now a generated constant. * The icuShortTimePattern/TimeOfDayFormat values are now pre-parsed. * Various other changes for consistency with the style guide and the rest of the codebase, e.g. the class names don't use `_`, the `path` library is imported as such, more dartdocs, fewer `// ignore`s, validation using exceptions. This reduces our technical debt benchmark.
This commit is contained in:
parent
a96fb44911
commit
75960f35d4
@ -6,7 +6,8 @@
|
||||
/// package for the subset of locales supported by the flutter_localizations
|
||||
/// package.
|
||||
///
|
||||
/// The extracted data is written into packages/flutter_localizations/lib/src/l10n/date_localizations.dart.
|
||||
/// The extracted data is written into:
|
||||
/// packages/flutter_localizations/lib/src/l10n/date_localizations.dart
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
@ -89,7 +90,7 @@ Future<Null> main(List<String> rawArgs) async {
|
||||
});
|
||||
buffer.writeln('};');
|
||||
|
||||
// Note: code that uses datePatterns expects it to contain values of type
|
||||
// Code that uses datePatterns expects it to contain values of type
|
||||
// Map<String, String> not Map<String, dynamic>.
|
||||
buffer.writeln('const Map<String, Map<String, String>> datePatterns = const <String, Map<String, String>> {');
|
||||
patternFiles.forEach((String locale, File data) {
|
||||
|
@ -30,17 +30,19 @@
|
||||
// dart dev/tools/gen_localizations.dart
|
||||
// ```
|
||||
//
|
||||
// If the data looks good, use the `-w` option to overwrite the
|
||||
// If the data looks good, use the `-w` or `--overwrite` option to overwrite the
|
||||
// packages/flutter_localizations/lib/src/l10n/localizations.dart file:
|
||||
//
|
||||
// ```
|
||||
// dart dev/tools/gen_localizations.dart --overwrite
|
||||
// ```
|
||||
|
||||
import 'dart:convert' show json;
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as pathlib;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'localizations_utils.dart';
|
||||
import 'localizations_validator.dart';
|
||||
@ -50,23 +52,36 @@ const String outputHeader = '''
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// This file has been automatically generated. Please do not edit it manually.
|
||||
// This file has been automatically generated. Please do not edit it manually.
|
||||
// To regenerate the file, use:
|
||||
// @(regenerate)
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import '../material_localizations.dart';
|
||||
''';
|
||||
|
||||
/// Maps locales to resource key/value pairs.
|
||||
final Map<String, Map<String, String>> localeToResources = <String, Map<String, String>>{};
|
||||
|
||||
/// Maps locales to resource attributes.
|
||||
/// Maps locales to resource key/attributes pairs.
|
||||
///
|
||||
/// See also https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
|
||||
/// See also: <https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes>
|
||||
final Map<String, Map<String, dynamic>> localeToResourceAttributes = <String, Map<String, dynamic>>{};
|
||||
|
||||
// Return s as a Dart-parseable raw string in single or double quotes. Expand double quotes:
|
||||
// foo => r'foo'
|
||||
// foo "bar" => r'foo "bar"'
|
||||
// foo 'bar' => r'foo ' "'" r'bar' "'"
|
||||
/// Return `s` as a Dart-parseable raw string in single or double quotes.
|
||||
///
|
||||
/// Double quotes are expanded:
|
||||
///
|
||||
/// ```
|
||||
/// foo => r'foo'
|
||||
/// foo "bar" => r'foo "bar"'
|
||||
/// foo 'bar' => r'foo ' "'" r'bar' "'"
|
||||
/// ```
|
||||
String generateString(String s) {
|
||||
if (!s.contains("'"))
|
||||
return "r'$s'";
|
||||
@ -91,8 +106,10 @@ String generateString(String s) {
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
/// This is the core of this script; it generates the code used for translations.
|
||||
String generateTranslationBundles() {
|
||||
final StringBuffer output = new StringBuffer();
|
||||
final StringBuffer supportedLocales = new StringBuffer();
|
||||
|
||||
final Map<String, List<String>> languageToLocales = <String, List<String>>{};
|
||||
final Set<String> allResourceIdentifiers = new Set<String>();
|
||||
@ -104,132 +121,262 @@ String generateTranslationBundles() {
|
||||
allResourceIdentifiers.addAll(localeToResources[locale].keys);
|
||||
}
|
||||
|
||||
// Generate the TranslationsBundle base class. It contains one getter
|
||||
// per resource identifier found in any of the .arb files.
|
||||
//
|
||||
// class TranslationsBundle {
|
||||
// const TranslationsBundle(this.parent);
|
||||
// final TranslationsBundle parent;
|
||||
// String get scriptCategory => parent?.scriptCategory;
|
||||
// ...
|
||||
// }
|
||||
output.writeln('''
|
||||
// The TranslationBundle subclasses defined here encode all of the translations
|
||||
// found in the flutter_localizations/lib/src/l10n/*.arb files.
|
||||
// The classes defined here encode all of the translations found in the
|
||||
// `flutter_localizations/lib/src/l10n/*.arb` files.
|
||||
//
|
||||
// The [MaterialLocalizations] class uses the (generated)
|
||||
// translationBundleForLocale() function to look up a const TranslationBundle
|
||||
// instance for a locale.
|
||||
// These classes are constructed by the [getTranslation] method at the bottom of
|
||||
// this file, and used by the [_MaterialLocalizationsDelegate.load] method defined
|
||||
// in `flutter_localizations/lib/src/material_localizations.dart`.''');
|
||||
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// We generate one class per supported language (e.g.
|
||||
// `MaterialLocalizationEn`). These implement everything that is needed by
|
||||
// GlobalMaterialLocalizations.
|
||||
|
||||
import \'dart:ui\' show Locale;
|
||||
// We also generate one subclass for each locale with a country code (e.g.
|
||||
// `MaterialLocalizationEnGb`). Their superclasses are the aforementioned
|
||||
// language classes for the same locale but without a country code (e.g.
|
||||
// `MaterialLocalizationEn`). These classes only override getters that return
|
||||
// a different value than their superclass.
|
||||
|
||||
class TranslationBundle {
|
||||
const TranslationBundle(this.parent);
|
||||
final TranslationBundle parent;''');
|
||||
for (String key in allResourceIdentifiers)
|
||||
output.writeln(' String get $key => parent?.$key;');
|
||||
output.writeln('''
|
||||
}''');
|
||||
|
||||
// Generate one private TranslationBundle subclass per supported
|
||||
// language. Each of these classes overrides every resource identifier
|
||||
// getter. For example:
|
||||
//
|
||||
// class _Bundle_en extends TranslationBundle {
|
||||
// const _Bundle_en() : super(null);
|
||||
// @override String get scriptCategory => r'English-like';
|
||||
// ...
|
||||
// }
|
||||
for (String language in languageToLocales.keys) {
|
||||
final Map<String, String> resources = localeToResources[language];
|
||||
output.writeln('''
|
||||
|
||||
// ignore: camel_case_types
|
||||
class _Bundle_$language extends TranslationBundle {
|
||||
const _Bundle_$language() : super(null);''');
|
||||
for (String key in resources.keys) {
|
||||
final String value = generateString(resources[key]);
|
||||
output.writeln('''
|
||||
@override String get $key => $value;''');
|
||||
final List<String> allKeys = allResourceIdentifiers.toList()..sort();
|
||||
final List<String> languageCodes = languageToLocales.keys.toList()..sort();
|
||||
for (String languageName in languageCodes) {
|
||||
final String camelCaseLanguage = camelCase(languageName);
|
||||
final Map<String, String> languageResources = localeToResources[languageName];
|
||||
final String languageClassName = 'MaterialLocalization$camelCaseLanguage';
|
||||
final String constructor = generateConstructor(languageClassName, languageName);
|
||||
output.writeln('');
|
||||
output.writeln('/// The translations for ${describeLocale(languageName)} (`$languageName`).');
|
||||
output.writeln('class $languageClassName extends GlobalMaterialLocalizations {');
|
||||
output.writeln(constructor);
|
||||
for (String key in allKeys) {
|
||||
final Map<String, dynamic> attributes = localeToResourceAttributes['en'][key];
|
||||
output.writeln(generateGetter(key, languageResources[key], attributes));
|
||||
}
|
||||
output.writeln('''
|
||||
}''');
|
||||
}
|
||||
|
||||
// Generate one private TranslationBundle subclass for each locale
|
||||
// with a country code. The parent of these subclasses is a const
|
||||
// instance of a translation bundle for the same locale, but without
|
||||
// a country code. These subclasses only override getters that
|
||||
// return different value than the parent class, or a resource identifier
|
||||
// that's not defined in the parent class. For example:
|
||||
//
|
||||
// class _Bundle_en_CA extends TranslationBundle {
|
||||
// const _Bundle_en_CA() : super(const _Bundle_en());
|
||||
// @override String get licensesPageTitle => r'Licences';
|
||||
// ...
|
||||
// }
|
||||
for (String language in languageToLocales.keys) {
|
||||
final Map<String, String> languageResources = localeToResources[language];
|
||||
for (String localeName in languageToLocales[language]) {
|
||||
if (localeName == language)
|
||||
output.writeln('}');
|
||||
int countryCodeCount = 0;
|
||||
final List<String> localeCodes = languageToLocales[languageName]..sort();
|
||||
for (String localeName in localeCodes) {
|
||||
if (localeName == languageName)
|
||||
continue;
|
||||
countryCodeCount += 1;
|
||||
final String camelCaseLocaleName = camelCase(localeName);
|
||||
final Map<String, String> localeResources = localeToResources[localeName];
|
||||
output.writeln('''
|
||||
|
||||
// ignore: camel_case_types
|
||||
class _Bundle_$localeName extends TranslationBundle {
|
||||
const _Bundle_$localeName() : super(const _Bundle_$language());''');
|
||||
final String localeClassName = 'MaterialLocalization$camelCaseLocaleName';
|
||||
final String constructor = generateConstructor(localeClassName, localeName);
|
||||
output.writeln('');
|
||||
output.writeln('/// The translations for ${describeLocale(localeName)} (`$localeName`).');
|
||||
output.writeln('class $localeClassName extends $languageClassName {');
|
||||
output.writeln(constructor);
|
||||
for (String key in localeResources.keys) {
|
||||
if (languageResources[key] == localeResources[key])
|
||||
continue;
|
||||
final String value = generateString(localeResources[key]);
|
||||
output.writeln('''
|
||||
@override String get $key => $value;''');
|
||||
final Map<String, dynamic> attributes = localeToResourceAttributes['en'][key];
|
||||
output.writeln(generateGetter(key, localeResources[key], attributes));
|
||||
}
|
||||
output.writeln('''
|
||||
}''');
|
||||
output.writeln('}');
|
||||
}
|
||||
if (countryCodeCount == 0) {
|
||||
supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)}');
|
||||
} else if (countryCodeCount == 1) {
|
||||
supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)} (plus one variant)');
|
||||
} else {
|
||||
supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)} (plus $countryCodeCount variants)');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the translationBundleForLocale function. Given a Locale
|
||||
// it returns the corresponding const TranslationBundle.
|
||||
// Generate the getTranslation function. Given a Locale it returns the
|
||||
// corresponding const GlobalMaterialLocalizations.
|
||||
output.writeln('''
|
||||
|
||||
TranslationBundle translationBundleForLocale(Locale locale) {
|
||||
/// The set of supported languages, as language code strings.
|
||||
///
|
||||
/// The [GlobalMaterialLocalizations.delegate] can generate localizations for
|
||||
/// any [Locale] with a language code from this set, regardless of the region.
|
||||
/// Some regions have specific support (e.g. `de` covers all forms of German,
|
||||
/// but there is support for `de-CH` specifically to override some of the
|
||||
/// translations for Switzerland).
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [getTranslation], whose documentation describes these values.
|
||||
final Set<String> kSupportedLanguages = new HashSet<String>.from(const <String>[
|
||||
${languageCodes.map((String value) => " '$value', // ${describeLocale(value)}").toList().join('\n')}
|
||||
]);
|
||||
|
||||
/// Creates a [GlobalMaterialLocalizations] instance for the given `locale`.
|
||||
///
|
||||
/// All of the function's arguments except `locale` will be passed to the [new
|
||||
/// GlobalMaterialLocalizations] constructor. (The `localeName` argument of that
|
||||
/// constructor is specified by the actual subclass constructor by this
|
||||
/// function.)
|
||||
///
|
||||
/// The following locales are supported by this package:
|
||||
///
|
||||
/// {@template flutter.localizations.languages}
|
||||
$supportedLocales/// {@endtemplate}
|
||||
///
|
||||
/// Generally speaking, this method is only intended to be used by
|
||||
/// [GlobalMaterialLocalizations.delegate].
|
||||
GlobalMaterialLocalizations getTranslation(
|
||||
Locale locale,
|
||||
intl.DateFormat fullYearFormat,
|
||||
intl.DateFormat mediumDateFormat,
|
||||
intl.DateFormat longDateFormat,
|
||||
intl.DateFormat yearMonthFormat,
|
||||
intl.NumberFormat decimalFormat,
|
||||
intl.NumberFormat twoDigitZeroPaddedFormat,
|
||||
) {
|
||||
switch (locale.languageCode) {''');
|
||||
const String arguments = 'fullYearFormat: fullYearFormat, mediumDateFormat: mediumDateFormat, longDateFormat: longDateFormat, yearMonthFormat: yearMonthFormat, decimalFormat: decimalFormat, twoDigitZeroPaddedFormat: twoDigitZeroPaddedFormat';
|
||||
for (String language in languageToLocales.keys) {
|
||||
if (languageToLocales[language].length == 1) {
|
||||
output.writeln('''
|
||||
case \'$language\':
|
||||
return const _Bundle_${languageToLocales[language][0]}();''');
|
||||
case '$language':
|
||||
return new MaterialLocalization${camelCase(languageToLocales[language][0])}($arguments);''');
|
||||
} else {
|
||||
output.writeln('''
|
||||
case \'$language\': {
|
||||
switch (locale.toString()) {''');
|
||||
case '$language': {
|
||||
switch (locale.countryCode) {''');
|
||||
for (String localeName in languageToLocales[language]) {
|
||||
if (localeName == language)
|
||||
continue;
|
||||
assert(localeName.contains('_'));
|
||||
final String countryCode = localeName.substring(localeName.indexOf('_') + 1);
|
||||
output.writeln('''
|
||||
case \'$localeName\':
|
||||
return const _Bundle_$localeName();''');
|
||||
case '$countryCode':
|
||||
return new MaterialLocalization${camelCase(localeName)}($arguments);''');
|
||||
}
|
||||
output.writeln('''
|
||||
}
|
||||
return const _Bundle_$language();
|
||||
return new MaterialLocalization${camelCase(language)}($arguments);
|
||||
}''');
|
||||
}
|
||||
}
|
||||
output.writeln('''
|
||||
}
|
||||
return const TranslationBundle(null);
|
||||
assert(false, 'getTranslation() called for unsupported locale "\$locale"');
|
||||
return null;
|
||||
}''');
|
||||
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
void processBundle(File file, String locale) {
|
||||
/// Returns the appropriate type for getters with the given attributes.
|
||||
///
|
||||
/// Typically "String", but some (e.g. "timeOfDayFormat") return enums.
|
||||
///
|
||||
/// Used by [generateGetter] below.
|
||||
String generateType(Map<String, dynamic> attributes) {
|
||||
if (attributes != null) {
|
||||
switch (attributes['x-flutter-type']) {
|
||||
case 'icuShortTimePattern':
|
||||
return 'TimeOfDayFormat';
|
||||
}
|
||||
}
|
||||
return 'String';
|
||||
}
|
||||
|
||||
/// Returns the appropriate name for getters with the given attributes.
|
||||
///
|
||||
/// Typically this is the key unmodified, but some have parameters, and
|
||||
/// the GlobalMaterialLocalizations class does the substitution, and for
|
||||
/// those we have to therefore provide an alternate name.
|
||||
///
|
||||
/// Used by [generateGetter] below.
|
||||
String generateKey(String key, Map<String, dynamic> attributes) {
|
||||
if (attributes != null) {
|
||||
if (attributes.containsKey('parameters'))
|
||||
return '${key}Raw';
|
||||
switch (attributes['x-flutter-type']) {
|
||||
case 'icuShortTimePattern':
|
||||
return '${key}Raw';
|
||||
}
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
const Map<String, String> _icuTimeOfDayToEnum = <String, String>{
|
||||
'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',
|
||||
};
|
||||
|
||||
/// Returns the literal that describes the value returned by getters
|
||||
/// with the given attributes.
|
||||
///
|
||||
/// This handles cases like the value being a literal `null`, an enum, and so
|
||||
/// on. The default is to treat the value as a string and escape it and quote
|
||||
/// it.
|
||||
///
|
||||
/// Used by [generateGetter] below.
|
||||
String generateValue(String value, Map<String, dynamic> attributes) {
|
||||
if (value == null)
|
||||
return null;
|
||||
if (attributes != null) {
|
||||
switch (attributes['x-flutter-type']) {
|
||||
case 'icuShortTimePattern':
|
||||
if (!_icuTimeOfDayToEnum.containsKey(value)) {
|
||||
throw new Exception(
|
||||
'"$value" 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 _icuTimeOfDayToEnum[value];
|
||||
}
|
||||
}
|
||||
return generateString(value);
|
||||
}
|
||||
|
||||
/// Combines [generateType], [generateKey], and [generateValue] to return
|
||||
/// the source of getters for the GlobalMaterialLocalizations subclass.
|
||||
String generateGetter(String key, String value, Map<String, dynamic> attributes) {
|
||||
final String type = generateType(attributes);
|
||||
key = generateKey(key, attributes);
|
||||
value = generateValue(value, attributes);
|
||||
return '''
|
||||
|
||||
@override
|
||||
$type get $key => $value;''';
|
||||
}
|
||||
|
||||
/// Returns the source of the constructor for a GlobalMaterialLocalizations
|
||||
/// subclass.
|
||||
String generateConstructor(String className, String localeName) {
|
||||
return '''
|
||||
/// Create an instance of the translation bundle for ${describeLocale(localeName)}.
|
||||
///
|
||||
/// For details on the meaning of the arguments, see [GlobalMaterialLocalizations].
|
||||
const $className({
|
||||
String localeName = '$localeName',
|
||||
@required intl.DateFormat fullYearFormat,
|
||||
@required intl.DateFormat mediumDateFormat,
|
||||
@required intl.DateFormat longDateFormat,
|
||||
@required intl.DateFormat yearMonthFormat,
|
||||
@required intl.NumberFormat decimalFormat,
|
||||
@required intl.NumberFormat twoDigitZeroPaddedFormat,
|
||||
}) : super(
|
||||
localeName: localeName,
|
||||
fullYearFormat: fullYearFormat,
|
||||
mediumDateFormat: mediumDateFormat,
|
||||
longDateFormat: longDateFormat,
|
||||
yearMonthFormat: yearMonthFormat,
|
||||
decimalFormat: decimalFormat,
|
||||
twoDigitZeroPaddedFormat: twoDigitZeroPaddedFormat,
|
||||
);''';
|
||||
}
|
||||
|
||||
/// Parse the data for a locale from a file, and store it in the [attributes]
|
||||
/// and [resources] keys.
|
||||
void processBundle(File file, { @required String locale }) {
|
||||
assert(locale != null);
|
||||
localeToResources[locale] ??= <String, String>{};
|
||||
localeToResourceAttributes[locale] ??= <String, dynamic>{};
|
||||
final Map<String, String> resources = localeToResources[locale];
|
||||
@ -244,7 +391,7 @@ void processBundle(File file, String locale) {
|
||||
}
|
||||
}
|
||||
|
||||
void main(List<String> rawArgs) {
|
||||
Future<void> main(List<String> rawArgs) async {
|
||||
checkCwdIsRepoRoot('gen_localizations');
|
||||
final GeneratorOptions options = parseArgs(rawArgs);
|
||||
|
||||
@ -252,32 +399,36 @@ void main(List<String> rawArgs) {
|
||||
// is the 2nd command line argument, lc is a language code and cc is the country
|
||||
// code. In most cases both codes are just two characters.
|
||||
|
||||
final Directory directory = new Directory(pathlib.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'));
|
||||
final Directory directory = new Directory(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'));
|
||||
final RegExp filenameRE = new RegExp(r'material_(\w+)\.arb$');
|
||||
|
||||
exitWithError(
|
||||
validateEnglishLocalizations(new File(pathlib.join(directory.path, 'material_en.arb')))
|
||||
);
|
||||
try {
|
||||
validateEnglishLocalizations(new File(path.join(directory.path, 'material_en.arb')));
|
||||
} on ValidationError catch (exception) {
|
||||
exitWithError('$exception');
|
||||
}
|
||||
|
||||
await precacheLanguageAndRegionTags();
|
||||
|
||||
for (FileSystemEntity entity in directory.listSync()) {
|
||||
final String path = entity.path;
|
||||
if (FileSystemEntity.isFileSync(path) && filenameRE.hasMatch(path)) {
|
||||
final String locale = filenameRE.firstMatch(path)[1];
|
||||
processBundle(new File(path), locale);
|
||||
final String entityPath = entity.path;
|
||||
if (FileSystemEntity.isFileSync(entityPath) && filenameRE.hasMatch(entityPath)) {
|
||||
processBundle(new File(entityPath), locale: filenameRE.firstMatch(entityPath)[1]);
|
||||
}
|
||||
}
|
||||
|
||||
exitWithError(
|
||||
validateLocalizations(localeToResources, localeToResourceAttributes)
|
||||
);
|
||||
try {
|
||||
validateLocalizations(localeToResources, localeToResourceAttributes);
|
||||
} on ValidationError catch (exception) {
|
||||
exitWithError('$exception');
|
||||
}
|
||||
|
||||
const String regenerate = 'dart dev/tools/gen_localizations.dart --overwrite';
|
||||
final StringBuffer buffer = new StringBuffer();
|
||||
buffer.writeln(outputHeader.replaceFirst('@(regenerate)', regenerate));
|
||||
buffer.writeln(outputHeader.replaceFirst('@(regenerate)', 'dart dev/tools/gen_localizations.dart --overwrite'));
|
||||
buffer.write(generateTranslationBundles());
|
||||
|
||||
if (options.writeToFile) {
|
||||
final File localizationsFile = new File(pathlib.join(directory.path, 'localizations.dart'));
|
||||
final File localizationsFile = new File(path.join(directory.path, 'localizations.dart'));
|
||||
localizationsFile.writeAsStringSync(buffer.toString());
|
||||
} else {
|
||||
stdout.write(buffer.toString());
|
||||
|
@ -2,15 +2,16 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart' as argslib;
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
void exitWithError(String errorMessage) {
|
||||
if (errorMessage == null)
|
||||
return;
|
||||
stderr.writeln('Fatal Error: $errorMessage');
|
||||
assert(errorMessage != null);
|
||||
stderr.writeln('fatal: $errorMessage');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@ -25,6 +26,13 @@ void checkCwdIsRepoRoot(String commandName) {
|
||||
}
|
||||
}
|
||||
|
||||
String camelCase(String locale) {
|
||||
return locale
|
||||
.split('_')
|
||||
.map((String part) => part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase())
|
||||
.join('');
|
||||
}
|
||||
|
||||
GeneratorOptions parseArgs(List<String> rawArgs) {
|
||||
final argslib.ArgParser argParser = new argslib.ArgParser()
|
||||
..addFlag(
|
||||
@ -45,3 +53,90 @@ class GeneratorOptions {
|
||||
|
||||
final bool writeToFile;
|
||||
}
|
||||
|
||||
const String registry = 'https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry';
|
||||
|
||||
// See also //master/tools/gen_locale.dart in the engine repo.
|
||||
Map<String, List<String>> _parseSection(String section) {
|
||||
final Map<String, List<String>> result = <String, List<String>>{};
|
||||
List<String> lastHeading;
|
||||
for (String line in section.split('\n')) {
|
||||
if (line == '')
|
||||
continue;
|
||||
if (line.startsWith(' ')) {
|
||||
lastHeading[lastHeading.length - 1] = '${lastHeading.last}${line.substring(1)}';
|
||||
continue;
|
||||
}
|
||||
final int colon = line.indexOf(':');
|
||||
if (colon <= 0)
|
||||
throw 'not sure how to deal with "$line"';
|
||||
final String name = line.substring(0, colon);
|
||||
final String value = line.substring(colon + 2);
|
||||
lastHeading = result.putIfAbsent(name, () => <String>[]);
|
||||
result[name].add(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
final Map<String, String> _languages = <String, String>{};
|
||||
final Map<String, String> _regions = <String, String>{};
|
||||
final Map<String, String> _scripts = <String, String>{};
|
||||
const String kProvincePrefix = ', Province of ';
|
||||
const String kParentheticalPrefix = ' (';
|
||||
|
||||
/// Prepares the data for the [describeLocale] method below.
|
||||
///
|
||||
/// The data is obtained from the official IANA registry.
|
||||
Future<void> precacheLanguageAndRegionTags() async {
|
||||
final HttpClient client = new HttpClient();
|
||||
final HttpClientRequest request = await client.getUrl(Uri.parse(registry));
|
||||
final HttpClientResponse response = await request.close();
|
||||
final String body = (await response.transform(utf8.decoder).toList()).join('');
|
||||
client.close(force: true);
|
||||
final List<Map<String, List<String>>> sections = body.split('%%').skip(1).map<Map<String, List<String>>>(_parseSection).toList();
|
||||
for (Map<String, List<String>> section in sections) {
|
||||
assert(section.containsKey('Type'), section.toString());
|
||||
final String type = section['Type'].single;
|
||||
if (type == 'language' || type == 'region' || type == 'script') {
|
||||
assert(section.containsKey('Subtag') && section.containsKey('Description'), section.toString());
|
||||
final String subtag = section['Subtag'].single;
|
||||
String description = section['Description'].join(' ');
|
||||
if (description.startsWith('United '))
|
||||
description = 'the $description';
|
||||
if (description.contains(kParentheticalPrefix))
|
||||
description = description.substring(0, description.indexOf(kParentheticalPrefix));
|
||||
if (description.contains(kProvincePrefix))
|
||||
description = description.substring(0, description.indexOf(kProvincePrefix));
|
||||
if (description.endsWith(' Republic'))
|
||||
description = 'the $description';
|
||||
switch (type) {
|
||||
case 'language':
|
||||
_languages[subtag] = description;
|
||||
break;
|
||||
case 'region':
|
||||
_regions[subtag] = description;
|
||||
break;
|
||||
case 'script':
|
||||
_scripts[subtag] = description;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String describeLocale(String tag) {
|
||||
final List<String> subtags = tag.split('_');
|
||||
assert(subtags.isNotEmpty);
|
||||
assert(_languages.containsKey(subtags[0]));
|
||||
final String language = _languages[subtags[0]];
|
||||
if (subtags.length >= 2) {
|
||||
final String region = _regions[subtags[1]];
|
||||
final String script = _scripts[subtags[1]];
|
||||
assert(region != null || script != null);
|
||||
if (region != null)
|
||||
return '$language, as used in $region';
|
||||
if (script != null)
|
||||
return '$language, using the $script script';
|
||||
}
|
||||
return '$language';
|
||||
}
|
@ -5,6 +5,18 @@
|
||||
import 'dart:convert' show json;
|
||||
import 'dart:io';
|
||||
|
||||
// The first suffix in kPluralSuffixes must be "Other". "Other" is special
|
||||
// because it's the only one that is required.
|
||||
const List<String> kPluralSuffixes = <String>['Other', 'Zero', 'One', 'Two', 'Few', 'Many'];
|
||||
final RegExp kPluralRegexp = new RegExp(r'(\w*)(' + kPluralSuffixes.skip(1).join(r'|') + r')$');
|
||||
|
||||
class ValidationError implements Exception {
|
||||
ValidationError(this. message);
|
||||
final String message;
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
/// Sanity checking of the @foo metadata in the English translations,
|
||||
/// material_en.arb.
|
||||
///
|
||||
@ -14,13 +26,13 @@ import 'dart:io';
|
||||
/// - Each @foo resource must have a Map value with a String valued
|
||||
/// description entry.
|
||||
///
|
||||
/// Returns an error message upon failure, null on success.
|
||||
String validateEnglishLocalizations(File file) {
|
||||
/// Throws an exception upon failure.
|
||||
void validateEnglishLocalizations(File file) {
|
||||
final StringBuffer errorMessages = new StringBuffer();
|
||||
|
||||
if (!file.existsSync()) {
|
||||
errorMessages.writeln('English localizations do not exist: $file');
|
||||
return errorMessages.toString();
|
||||
throw new ValidationError(errorMessages.toString());
|
||||
}
|
||||
|
||||
final Map<String, dynamic> bundle = json.decode(file.readAsStringSync());
|
||||
@ -36,7 +48,7 @@ String validateEnglishLocalizations(File file) {
|
||||
final int suffixIndex = resourceId.indexOf(suffix);
|
||||
return suffixIndex != -1 && bundle['@${resourceId.substring(0, suffixIndex)}'] != null;
|
||||
}
|
||||
if (<String>['Zero', 'One', 'Two', 'Few', 'Many', 'Other'].any(checkPluralResource))
|
||||
if (kPluralSuffixes.any(checkPluralResource))
|
||||
continue;
|
||||
|
||||
errorMessages.writeln('A value was not specified for @$resourceId');
|
||||
@ -70,7 +82,8 @@ String validateEnglishLocalizations(File file) {
|
||||
}
|
||||
}
|
||||
|
||||
return errorMessages.isEmpty ? null : errorMessages.toString();
|
||||
if (errorMessages.isNotEmpty)
|
||||
throw new ValidationError(errorMessages.toString());
|
||||
}
|
||||
|
||||
/// Enforces the following invariants in our localizations:
|
||||
@ -81,8 +94,8 @@ String validateEnglishLocalizations(File file) {
|
||||
/// Uses "en" localizations as the canonical source of locale keys that other
|
||||
/// locales are compared against.
|
||||
///
|
||||
/// If validation fails, return an error message, otherwise return null.
|
||||
String validateLocalizations(
|
||||
/// If validation fails, throws an exception.
|
||||
void validateLocalizations(
|
||||
Map<String, Map<String, String>> localeToResources,
|
||||
Map<String, Map<String, dynamic>> localeToAttributes,
|
||||
) {
|
||||
@ -99,12 +112,9 @@ String validateLocalizations(
|
||||
// Many languages require only a subset of these variations, so we do not
|
||||
// require them so long as the "Other" variation exists.
|
||||
bool isPluralVariation(String key) {
|
||||
final RegExp pluralRegexp = new RegExp(r'(\w*)(Zero|One|Two|Few|Many)$');
|
||||
final Match pluralMatch = pluralRegexp.firstMatch(key);
|
||||
|
||||
final Match pluralMatch = kPluralRegexp.firstMatch(key);
|
||||
if (pluralMatch == null)
|
||||
return false;
|
||||
|
||||
final String prefix = pluralMatch[1];
|
||||
return resources.containsKey('${prefix}Other');
|
||||
}
|
||||
@ -151,7 +161,6 @@ String validateLocalizations(
|
||||
..writeln(' "notUsed": "Sindhi time format does not use a.m. indicator"')
|
||||
..writeln('}');
|
||||
}
|
||||
return errorMessages.toString();
|
||||
throw new ValidationError(errorMessages.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -134,7 +134,8 @@ Iterable<String> debugWordWrap(String message, int width, { String wrapIndent =
|
||||
if ((index - startForLengthCalculations > width) || (index == message.length)) {
|
||||
// we are over the width line, so break
|
||||
if ((index - startForLengthCalculations <= width) || (lastWordEnd == null)) {
|
||||
// we should use this point, before either it doesn't actually go over the end (last line), or it does, but there was no earlier break point
|
||||
// we should use this point, because either it doesn't actually go over the
|
||||
// end (last line), or it does, but there was no earlier break point
|
||||
lastWordEnd = index;
|
||||
}
|
||||
if (addPrefix) {
|
||||
|
@ -5,5 +5,6 @@
|
||||
/// Localizations for the Flutter library
|
||||
library flutter_localizations;
|
||||
|
||||
export 'src/material_localizations.dart' show GlobalMaterialLocalizations;
|
||||
export 'src/widgets_localizations.dart' show GlobalWidgetsLocalizations;
|
||||
export 'src/l10n/localizations.dart';
|
||||
export 'src/material_localizations.dart';
|
||||
export 'src/widgets_localizations.dart';
|
||||
|
@ -68,7 +68,8 @@ contain translations for the same set of resource IDs as
|
||||
For each resource ID defined for English in material_en.arb, there is
|
||||
an additional resource with an '@' prefix. These '@' resources are not
|
||||
used by the material library at run time, they just exist to inform
|
||||
translators about how the value will be used.
|
||||
translators about how the value will be used, and to inform the code
|
||||
generator about what code to write.
|
||||
|
||||
```dart
|
||||
"cancelButtonLabel": "CANCEL",
|
||||
@ -130,9 +131,11 @@ help define an app's text theme and time picker layout respectively.
|
||||
|
||||
The value of `timeOfDayFormat` defines how a time picker displayed by
|
||||
[showTimePicker()](https://docs.flutter.io/flutter/material/showTimePicker.html)
|
||||
formats and lays out its time controls. The value of `timeOfDayFormat` must be
|
||||
a string that matches one of the formats defined by
|
||||
https://docs.flutter.io/flutter/material/TimeOfDayFormat-class.html.
|
||||
formats and lays out its time controls. The value of `timeOfDayFormat`
|
||||
must be a string that matches one of the formats defined by
|
||||
<https://docs.flutter.io/flutter/material/TimeOfDayFormat-class.html>.
|
||||
It is converted to an enum value because the `material_en.arb` file
|
||||
has this value labeled as `"x-flutter-type": "icuShortTimePattern"`.
|
||||
|
||||
The value of `scriptCategory` is based on the
|
||||
[Language categories reference](https://material.io/go/design-typography#typography-language-categories-reference)
|
||||
|
@ -7668,9 +7668,9 @@ const Map<String, dynamic> dateSymbols = <String, dynamic>{
|
||||
],
|
||||
'AMPMS': <dynamic>[r'''AM''', r'''PM'''],
|
||||
'DATEFORMATS': <dynamic>[
|
||||
r'''EEEE, MMMM d, y''',
|
||||
r'''MMMM d, y''',
|
||||
r'''MMM d, y''',
|
||||
r'''EEEE، d MMMM، y''',
|
||||
r'''d MMMM، y''',
|
||||
r'''d MMM، y''',
|
||||
r'''d/M/yy'''
|
||||
],
|
||||
'TIMEFORMATS': <dynamic>[
|
||||
@ -9994,7 +9994,7 @@ const Map<String, Map<String, String>> datePatterns =
|
||||
'MMMd': r'''d MMM''',
|
||||
'MMMEd': r'''EEE، d MMM''',
|
||||
'MMMM': r'''LLLL''',
|
||||
'MMMMd': r'''MMMM d''',
|
||||
'MMMMd': r'''d MMMM''',
|
||||
'MMMMEEEEd': r'''EEEE، d MMMM''',
|
||||
'QQQ': r'''QQQ''',
|
||||
'QQQQ': r'''QQQQ''',
|
||||
@ -10006,8 +10006,8 @@ const Map<String, Map<String, String>> datePatterns =
|
||||
'yMMMd': r'''d MMM، y''',
|
||||
'yMMMEd': r'''EEE، d MMM، y''',
|
||||
'yMMMM': r'''MMMM y''',
|
||||
'yMMMMd': r'''MMMM d, y''',
|
||||
'yMMMMEEEEd': r'''EEEE, MMMM d, y''',
|
||||
'yMMMMd': r'''d MMMM، y''',
|
||||
'yMMMMEEEEd': r'''EEEE، d MMMM، y''',
|
||||
'yQQQ': r'''QQQ y''',
|
||||
'yQQQQ': r'''QQQQ y''',
|
||||
'H': r'''HH''',
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,8 @@
|
||||
|
||||
"timeOfDayFormat": "h:mm a",
|
||||
"@timeOfDayFormat": {
|
||||
"description": "The ICU 'Short Time' pattern, such as 'HH:mm', 'h:mm a', 'H:mm'. See: http://demo.icu-project.org/icu-bin/locexp?d_=en&_=en_US"
|
||||
"description": "The ICU 'Short Time' pattern, such as 'HH:mm', 'h:mm a', 'H:mm'. See: http://demo.icu-project.org/icu-bin/locexp?d_=en&_=en_US",
|
||||
"x-flutter-type": "icuShortTimePattern"
|
||||
},
|
||||
|
||||
"openAppDrawerTooltip": "Open navigation menu",
|
||||
|
@ -11,14 +11,25 @@ import 'package:intl/date_symbols.dart' as intl;
|
||||
import 'package:intl/date_symbol_data_custom.dart' as date_symbol_data_custom;
|
||||
import 'l10n/date_localizations.dart' as date_localizations;
|
||||
|
||||
import 'l10n/localizations.dart' show TranslationBundle, translationBundleForLocale;
|
||||
import 'l10n/localizations.dart';
|
||||
import 'widgets_localizations.dart';
|
||||
|
||||
// Watch out: the supported locales list in the doc comment below must be kept
|
||||
// in sync with the list we test, see test/translations_test.dart, and of course
|
||||
// the actual list of supported locales in _MaterialLocalizationsDelegate.
|
||||
|
||||
/// Localized strings for the material widgets.
|
||||
/// Implementation of localized strings for the material widgets using the
|
||||
/// `intl` package for date and time formatting.
|
||||
///
|
||||
/// ## Supported languages
|
||||
///
|
||||
/// This class supports locales with the following [Locale.languageCode]s:
|
||||
///
|
||||
/// {@macro flutter.localizations.languages}
|
||||
///
|
||||
/// This list is available programatically via [kSupportedLanguages].
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// To include the localizations provided by this class in a [MaterialApp],
|
||||
/// add [GlobalMaterialLocalizations.delegates] to
|
||||
@ -29,133 +40,87 @@ import 'widgets_localizations.dart';
|
||||
/// new MaterialApp(
|
||||
/// localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
||||
/// supportedLocales: [
|
||||
/// const Locale('en', 'US'), // English
|
||||
/// const Locale('he', 'IL'), // Hebrew
|
||||
/// const Locale('en', 'US'), // American English
|
||||
/// const Locale('he', 'IL'), // Israeli Hebrew
|
||||
/// // ...
|
||||
/// ],
|
||||
/// // ...
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// This class supports locales with the following [Locale.languageCode]s:
|
||||
/// ## Overriding translations
|
||||
///
|
||||
/// * ar - Arabic
|
||||
/// * bg - Bulgarian
|
||||
/// * bs - Bosnian
|
||||
/// * ca - Catalan
|
||||
/// * cs - Czech
|
||||
/// * da - Danish
|
||||
/// * de - German
|
||||
/// * el - Greek
|
||||
/// * en - English
|
||||
/// * es - Spanish
|
||||
/// * et - Estonian
|
||||
/// * fa - Farsi
|
||||
/// * fi - Finnish
|
||||
/// * fil - Fillipino
|
||||
/// * fr - French
|
||||
/// * gsw - Swiss German
|
||||
/// * hi - Hindi
|
||||
/// * he - Hebrew
|
||||
/// * hr - Croatian
|
||||
/// * hu - Hungarian
|
||||
/// * id - Indonesian
|
||||
/// * it - Italian
|
||||
/// * ja - Japanese
|
||||
/// * ko - Korean
|
||||
/// * lv - Latvian
|
||||
/// * lt - Lithuanian
|
||||
/// * ms - Malay
|
||||
/// * nl - Dutch
|
||||
/// * nb - Norwegian
|
||||
/// * pl - Polish
|
||||
/// * ps - Pashto
|
||||
/// * pt - Portuguese
|
||||
/// * ro - Romanian
|
||||
/// * ru - Russian
|
||||
/// * sk - Slovak
|
||||
/// * sl - Slovenian
|
||||
/// * sr - Serbian
|
||||
/// * sv - Swedish
|
||||
/// * tl - Tagalog
|
||||
/// * th - Thai
|
||||
/// * tr - Turkish
|
||||
/// * uk - Ukranian
|
||||
/// * ur - Urdu
|
||||
/// * vi - Vietnamese
|
||||
/// * zh - Simplified Chinese
|
||||
/// To create a translation that's similar to an existing language's translation
|
||||
/// but has slightly different strings, subclass the relevant translation
|
||||
/// directly and then create a [LocalizationsDelegate<MaterialLocalizations>]
|
||||
/// subclass to define how to load it.
|
||||
///
|
||||
/// Avoid subclassing an unrelated language (for example, subclassing
|
||||
/// [MaterialLocalizationEn] and then passing a non-English `localeName` to the
|
||||
/// constructor). Doing so will cause confusion for locale-specific behaviors;
|
||||
/// in particular, translations that use the `localeName` for determining how to
|
||||
/// pluralize will end up doing invalid things. Subclassing an existing
|
||||
/// language's translations is only suitable for making small changes to the
|
||||
/// existing strings. For providing a new language entirely, implement
|
||||
/// [MaterialLocalizations] directly.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * The Flutter Internationalization Tutorial,
|
||||
/// <https://flutter.io/tutorials/internationalization/>.
|
||||
/// * [DefaultMaterialLocalizations], which only provides US English translations.
|
||||
class GlobalMaterialLocalizations implements MaterialLocalizations {
|
||||
/// Constructs an object that defines the material widgets' localized strings
|
||||
abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
|
||||
/// Initializes 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),
|
||||
_localeName = _computeLocaleName(locale) {
|
||||
_loadDateIntlDataIfNotLoaded();
|
||||
|
||||
_translationBundle = translationBundleForLocale(locale);
|
||||
assert(_translationBundle != null);
|
||||
|
||||
if (intl.DateFormat.localeExists(_localeName)) {
|
||||
_fullYearFormat = new intl.DateFormat.y(_localeName);
|
||||
_mediumDateFormat = new intl.DateFormat.MMMEd(_localeName);
|
||||
_longDateFormat = new intl.DateFormat.yMMMMEEEEd(_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.MMMEd(locale.languageCode);
|
||||
_longDateFormat = new intl.DateFormat.yMMMMEEEEd(locale.languageCode);
|
||||
_yearMonthFormat = new intl.DateFormat.yMMMM(locale.languageCode);
|
||||
} else {
|
||||
_fullYearFormat = new intl.DateFormat.y();
|
||||
_mediumDateFormat = new intl.DateFormat.MMMEd();
|
||||
_longDateFormat = new intl.DateFormat.yMMMMEEEEd();
|
||||
_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;
|
||||
/// The arguments are used for further runtime localization of data,
|
||||
/// specifically for selecting plurals, date and time formatting, and number
|
||||
/// formatting. They correspond to the following values:
|
||||
///
|
||||
/// 1. The string that would be returned by [Intl.canonicalizedLocale] for
|
||||
/// the locale.
|
||||
/// 2. The [intl.DateFormat] for [formatYear].
|
||||
/// 3. The [intl.DateFormat] for [formatMediumDate].
|
||||
/// 4. The [intl.DateFormat] for [formatFullDate].
|
||||
/// 5. The [intl.DateFormat] for [formatMonthYear].
|
||||
/// 6. The [NumberFormat] for [formatDecimal] (also used by [formatHour] and
|
||||
/// [formatTimeOfDay] when [timeOfDayFormat] doesn't use [HourFormat.HH]).
|
||||
/// 7. The [NumberFormat] for [formatHour] and the hour part of
|
||||
/// [formatTimeOfDay] when [timeOfDayFormat] uses [HourFormat.HH], and for
|
||||
/// [formatMinute] and the minute part of [formatTimeOfDay].
|
||||
///
|
||||
/// The [narrowWeekdays] and [firstDayOfWeekIndex] properties use the values
|
||||
/// from the [intl.DateFormat] used by [formatFullDate].
|
||||
const GlobalMaterialLocalizations({
|
||||
@required String localeName,
|
||||
@required intl.DateFormat fullYearFormat,
|
||||
@required intl.DateFormat mediumDateFormat,
|
||||
@required intl.DateFormat longDateFormat,
|
||||
@required intl.DateFormat yearMonthFormat,
|
||||
@required intl.NumberFormat decimalFormat,
|
||||
@required intl.NumberFormat twoDigitZeroPaddedFormat,
|
||||
}) : assert(localeName != null),
|
||||
this._localeName = localeName,
|
||||
assert(fullYearFormat != null),
|
||||
this._fullYearFormat = fullYearFormat,
|
||||
assert(mediumDateFormat != null),
|
||||
this._mediumDateFormat = mediumDateFormat,
|
||||
assert(longDateFormat != null),
|
||||
this._longDateFormat = longDateFormat,
|
||||
assert(yearMonthFormat != null),
|
||||
this._yearMonthFormat = yearMonthFormat,
|
||||
assert(decimalFormat != null),
|
||||
this._decimalFormat = decimalFormat,
|
||||
assert(twoDigitZeroPaddedFormat != null),
|
||||
this._twoDigitZeroPaddedFormat = twoDigitZeroPaddedFormat;
|
||||
|
||||
final String _localeName;
|
||||
|
||||
TranslationBundle _translationBundle;
|
||||
|
||||
intl.NumberFormat _decimalFormat;
|
||||
|
||||
intl.NumberFormat _twoDigitZeroPaddedFormat;
|
||||
|
||||
intl.DateFormat _fullYearFormat;
|
||||
|
||||
intl.DateFormat _mediumDateFormat;
|
||||
|
||||
intl.DateFormat _longDateFormat;
|
||||
|
||||
intl.DateFormat _yearMonthFormat;
|
||||
|
||||
static String _computeLocaleName(Locale locale) {
|
||||
return intl.Intl.canonicalizedLocale(locale.toString());
|
||||
}
|
||||
final intl.DateFormat _fullYearFormat;
|
||||
final intl.DateFormat _mediumDateFormat;
|
||||
final intl.DateFormat _longDateFormat;
|
||||
final intl.DateFormat _yearMonthFormat;
|
||||
final intl.NumberFormat _decimalFormat;
|
||||
final intl.NumberFormat _twoDigitZeroPaddedFormat;
|
||||
|
||||
@override
|
||||
String formatHour(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat = false }) {
|
||||
@ -198,11 +163,11 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
|
||||
|
||||
@override
|
||||
List<String> get narrowWeekdays {
|
||||
return _fullYearFormat.dateSymbols.NARROWWEEKDAYS;
|
||||
return _longDateFormat.dateSymbols.NARROWWEEKDAYS;
|
||||
}
|
||||
|
||||
@override
|
||||
int get firstDayOfWeekIndex => (_fullYearFormat.dateSymbols.FIRSTDAYOFWEEK + 1) % 7;
|
||||
int get firstDayOfWeekIndex => (_longDateFormat.dateSymbols.FIRSTDAYOFWEEK + 1) % 7;
|
||||
|
||||
@override
|
||||
String formatDecimal(int number) {
|
||||
@ -234,7 +199,6 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
|
||||
case TimeOfDayFormat.frenchCanadian:
|
||||
return '$hour h $minute';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -248,152 +212,162 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String get openAppDrawerTooltip => _translationBundle.openAppDrawerTooltip;
|
||||
|
||||
@override
|
||||
String get backButtonTooltip => _translationBundle.backButtonTooltip;
|
||||
|
||||
@override
|
||||
String get closeButtonTooltip => _translationBundle.closeButtonTooltip;
|
||||
|
||||
@override
|
||||
String get deleteButtonTooltip => _translationBundle.deleteButtonTooltip;
|
||||
|
||||
@override
|
||||
String get nextMonthTooltip => _translationBundle.nextMonthTooltip;
|
||||
|
||||
@override
|
||||
String get previousMonthTooltip => _translationBundle.previousMonthTooltip;
|
||||
|
||||
@override
|
||||
String get nextPageTooltip => _translationBundle.nextPageTooltip;
|
||||
|
||||
@override
|
||||
String get previousPageTooltip => _translationBundle.previousPageTooltip;
|
||||
|
||||
@override
|
||||
String get showMenuTooltip => _translationBundle.showMenuTooltip;
|
||||
|
||||
@override
|
||||
String get drawerLabel => _translationBundle.alertDialogLabel;
|
||||
|
||||
@override
|
||||
String get popupMenuLabel => _translationBundle.popupMenuLabel;
|
||||
|
||||
@override
|
||||
String get dialogLabel => _translationBundle.dialogLabel;
|
||||
|
||||
@override
|
||||
String get alertDialogLabel => _translationBundle.alertDialogLabel;
|
||||
|
||||
@override
|
||||
String get searchFieldLabel => _translationBundle.searchFieldLabel;
|
||||
/// The raw version of [aboutListTileTitle], with `$applicationName` verbatim
|
||||
/// in the string.
|
||||
@protected
|
||||
String get aboutListTileTitleRaw;
|
||||
|
||||
@override
|
||||
String aboutListTileTitle(String applicationName) {
|
||||
final String text = _translationBundle.aboutListTileTitle;
|
||||
final String text = aboutListTileTitleRaw;
|
||||
return text.replaceFirst(r'$applicationName', applicationName);
|
||||
}
|
||||
|
||||
@override
|
||||
String get licensesPageTitle => _translationBundle.licensesPageTitle;
|
||||
/// The raw version of [pageRowsInfoTitle], with `$firstRow`, `$lastRow`' and
|
||||
/// `$rowCount` verbatim in the string, for the case where the value is
|
||||
/// approximate.
|
||||
@protected
|
||||
String get pageRowsInfoTitleApproximateRaw;
|
||||
|
||||
/// The raw version of [pageRowsInfoTitle], with `$firstRow`, `$lastRow`' and
|
||||
/// `$rowCount` verbatim in the string, for the case where the value is
|
||||
/// precise.
|
||||
@protected
|
||||
String get pageRowsInfoTitleRaw;
|
||||
|
||||
@override
|
||||
String pageRowsInfoTitle(int firstRow, int lastRow, int rowCount, bool rowCountIsApproximate) {
|
||||
String text = rowCountIsApproximate ? _translationBundle.pageRowsInfoTitleApproximate : null;
|
||||
text ??= _translationBundle.pageRowsInfoTitle;
|
||||
assert(text != null, 'A $locale localization was not found for pageRowsInfoTitle or pageRowsInfoTitleApproximate');
|
||||
// TODO(hansmuller): this could be more efficient.
|
||||
String text = rowCountIsApproximate ? pageRowsInfoTitleApproximateRaw : null;
|
||||
text ??= pageRowsInfoTitleRaw;
|
||||
assert(text != null, 'A $_localeName localization was not found for pageRowsInfoTitle or pageRowsInfoTitleApproximate');
|
||||
return text
|
||||
.replaceFirst(r'$firstRow', formatDecimal(firstRow))
|
||||
.replaceFirst(r'$lastRow', formatDecimal(lastRow))
|
||||
.replaceFirst(r'$rowCount', formatDecimal(rowCount));
|
||||
}
|
||||
|
||||
@override
|
||||
String get rowsPerPageTitle => _translationBundle.rowsPerPageTitle;
|
||||
/// The raw version of [tabLabel], with `$tabIndex` and `$tabCount` verbatim
|
||||
/// in the string.
|
||||
@protected
|
||||
String get tabLabelRaw;
|
||||
|
||||
@override
|
||||
String tabLabel({int tabIndex, int tabCount}) {
|
||||
assert(tabIndex >= 1);
|
||||
assert(tabCount >= 1);
|
||||
final String template = _translationBundle.tabLabel;
|
||||
final String template = tabLabelRaw;
|
||||
return template
|
||||
.replaceFirst(r'$tabIndex', formatDecimal(tabIndex))
|
||||
.replaceFirst(r'$tabCount', formatDecimal(tabCount));
|
||||
}
|
||||
|
||||
/// The "zero" form of [selectedRowCountTitle].
|
||||
///
|
||||
/// This form is optional.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Intl.plural], to which this form is passed.
|
||||
/// * [selectedRowCountTitleOne], the "one" form
|
||||
/// * [selectedRowCountTitleTwo], the "two" form
|
||||
/// * [selectedRowCountTitleFew], the "few" form
|
||||
/// * [selectedRowCountTitleMany], the "many" form
|
||||
/// * [selectedRowCountTitleOther], the "other" form
|
||||
@protected
|
||||
String get selectedRowCountTitleZero => null;
|
||||
|
||||
/// The "one" form of [selectedRowCountTitle].
|
||||
///
|
||||
/// This form is optional.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Intl.plural], to which this form is passed.
|
||||
/// * [selectedRowCountTitleZero], the "zero" form
|
||||
/// * [selectedRowCountTitleTwo], the "two" form
|
||||
/// * [selectedRowCountTitleFew], the "few" form
|
||||
/// * [selectedRowCountTitleMany], the "many" form
|
||||
/// * [selectedRowCountTitleOther], the "other" form
|
||||
@protected
|
||||
String get selectedRowCountTitleOne => null;
|
||||
|
||||
/// The "two" form of [selectedRowCountTitle].
|
||||
///
|
||||
/// This form is optional.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Intl.plural], to which this form is passed.
|
||||
/// * [selectedRowCountTitleZero], the "zero" form
|
||||
/// * [selectedRowCountTitleOne], the "one" form
|
||||
/// * [selectedRowCountTitleFew], the "few" form
|
||||
/// * [selectedRowCountTitleMany], the "many" form
|
||||
/// * [selectedRowCountTitleOther], the "other" form
|
||||
@protected
|
||||
String get selectedRowCountTitleTwo => null;
|
||||
|
||||
/// The "few" form of [selectedRowCountTitle].
|
||||
///
|
||||
/// This form is optional.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Intl.plural], to which this form is passed.
|
||||
/// * [selectedRowCountTitleZero], the "zero" form
|
||||
/// * [selectedRowCountTitleOne], the "one" form
|
||||
/// * [selectedRowCountTitleTwo], the "two" form
|
||||
/// * [selectedRowCountTitleMany], the "many" form
|
||||
/// * [selectedRowCountTitleOther], the "other" form
|
||||
@protected
|
||||
String get selectedRowCountTitleFew => null;
|
||||
|
||||
/// The "many" form of [selectedRowCountTitle].
|
||||
///
|
||||
/// This form is optional.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Intl.plural], to which this form is passed.
|
||||
/// * [selectedRowCountTitleZero], the "zero" form
|
||||
/// * [selectedRowCountTitleOne], the "one" form
|
||||
/// * [selectedRowCountTitleTwo], the "two" form
|
||||
/// * [selectedRowCountTitleFew], the "few" form
|
||||
/// * [selectedRowCountTitleOther], the "other" form
|
||||
@protected
|
||||
String get selectedRowCountTitleMany => null;
|
||||
|
||||
/// The "other" form of [selectedRowCountTitle].
|
||||
///
|
||||
/// This form is required.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Intl.plural], to which this form is passed.
|
||||
/// * [selectedRowCountTitleZero], the "zero" form
|
||||
/// * [selectedRowCountTitleOne], the "one" form
|
||||
/// * [selectedRowCountTitleTwo], the "two" form
|
||||
/// * [selectedRowCountTitleFew], the "few" form
|
||||
/// * [selectedRowCountTitleMany], the "many" form
|
||||
@protected
|
||||
String get selectedRowCountTitleOther;
|
||||
|
||||
@override
|
||||
String selectedRowCountTitle(int selectedRowCount) {
|
||||
// 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 text;
|
||||
if (selectedRowCount == 0)
|
||||
text = _translationBundle.selectedRowCountTitleZero;
|
||||
else if (selectedRowCount == 1)
|
||||
text = _translationBundle.selectedRowCountTitleOne;
|
||||
else if (selectedRowCount == 2)
|
||||
text = _translationBundle.selectedRowCountTitleTwo;
|
||||
else if (selectedRowCount > 2)
|
||||
text = _translationBundle.selectedRowCountTitleMany;
|
||||
text ??= _translationBundle.selectedRowCountTitleOther;
|
||||
assert(text != null);
|
||||
|
||||
return text.replaceFirst(r'$selectedRowCount', formatDecimal(selectedRowCount));
|
||||
return intl.Intl.plural(
|
||||
selectedRowCount,
|
||||
zero: selectedRowCountTitleZero,
|
||||
one: selectedRowCountTitleOne,
|
||||
two: selectedRowCountTitleTwo,
|
||||
few: selectedRowCountTitleFew,
|
||||
many: selectedRowCountTitleMany,
|
||||
other: selectedRowCountTitleOther,
|
||||
locale: _localeName,
|
||||
).replaceFirst(r'$selectedRowCount', formatDecimal(selectedRowCount));
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelButtonLabel => _translationBundle.cancelButtonLabel;
|
||||
|
||||
@override
|
||||
String get closeButtonLabel => _translationBundle.closeButtonLabel;
|
||||
|
||||
@override
|
||||
String get continueButtonLabel => _translationBundle.continueButtonLabel;
|
||||
|
||||
@override
|
||||
String get copyButtonLabel => _translationBundle.copyButtonLabel;
|
||||
|
||||
@override
|
||||
String get cutButtonLabel => _translationBundle.cutButtonLabel;
|
||||
|
||||
@override
|
||||
String get okButtonLabel => _translationBundle.okButtonLabel;
|
||||
|
||||
@override
|
||||
String get pasteButtonLabel => _translationBundle.pasteButtonLabel;
|
||||
|
||||
@override
|
||||
String get selectAllButtonLabel => _translationBundle.selectAllButtonLabel;
|
||||
|
||||
@override
|
||||
String get viewLicensesButtonLabel => _translationBundle.viewLicensesButtonLabel;
|
||||
|
||||
@override
|
||||
String get anteMeridiemAbbreviation => _translationBundle.anteMeridiemAbbreviation;
|
||||
|
||||
@override
|
||||
String get postMeridiemAbbreviation => _translationBundle.postMeridiemAbbreviation;
|
||||
|
||||
@override
|
||||
String get timePickerHourModeAnnouncement => _translationBundle.timePickerHourModeAnnouncement;
|
||||
|
||||
@override
|
||||
String get timePickerMinuteModeAnnouncement => _translationBundle.timePickerMinuteModeAnnouncement;
|
||||
|
||||
@override
|
||||
String get modalBarrierDismissLabel => _translationBundle.modalBarrierDismissLabel;
|
||||
|
||||
@override
|
||||
String get signedInLabel => _translationBundle.signedInLabel;
|
||||
|
||||
@override
|
||||
String get hideAccountsLabel => _translationBundle.hideAccountsLabel;
|
||||
|
||||
@override
|
||||
String get showAccountsLabel => _translationBundle.showAccountsLabel;
|
||||
/// The format to use for [timeOfDayFormat].
|
||||
@protected
|
||||
TimeOfDayFormat get timeOfDayFormatRaw;
|
||||
|
||||
/// The [TimeOfDayFormat] corresponding to one of the following supported
|
||||
/// patterns:
|
||||
@ -409,44 +383,28 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * http://demo.icu-project.org/icu-bin/locexp?d_=en&_=en_US shows the
|
||||
/// short time pattern used in locale en_US
|
||||
/// * <http://demo.icu-project.org/icu-bin/locexp?d_=en&_=en_US>, which shows
|
||||
/// the short time pattern used in the `en_US` locale.
|
||||
@override
|
||||
TimeOfDayFormat timeOfDayFormat({ bool alwaysUse24HourFormat = false }) {
|
||||
final String icuShortTimePattern = _translationBundle.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;
|
||||
}());
|
||||
|
||||
final TimeOfDayFormat icuFormat = _icuTimeOfDayToEnum[icuShortTimePattern];
|
||||
|
||||
assert(alwaysUse24HourFormat != null);
|
||||
if (alwaysUse24HourFormat)
|
||||
return _get24HourVersionOf(icuFormat);
|
||||
|
||||
return icuFormat;
|
||||
return _get24HourVersionOf(timeOfDayFormatRaw);
|
||||
return timeOfDayFormatRaw;
|
||||
}
|
||||
|
||||
/// The script category used by [localTextGeometry]. Must be one of the strings
|
||||
/// declared in [MaterialTextGeometry].
|
||||
///
|
||||
/// TODO(ianh): make this return a TextTheme from MaterialTextGeometry.
|
||||
/// TODO(ianh): drop the constructor on MaterialTextGeometry.
|
||||
/// TODO(ianh): drop the strings on MaterialTextGeometry.
|
||||
@protected
|
||||
String get scriptCategory;
|
||||
|
||||
/// Looks up text geometry defined in [MaterialTextGeometry].
|
||||
@override
|
||||
TextTheme get localTextGeometry => MaterialTextGeometry.forScriptCategory(_translationBundle.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<MaterialLocalizations> load(Locale locale) {
|
||||
return new SynchronousFuture<MaterialLocalizations>(new GlobalMaterialLocalizations(locale));
|
||||
}
|
||||
TextTheme get localTextGeometry => MaterialTextGeometry.forScriptCategory(scriptCategory);
|
||||
|
||||
/// A [LocalizationsDelegate] that uses [GlobalMaterialLocalizations.load]
|
||||
/// to create an instance of this class.
|
||||
@ -459,6 +417,8 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
|
||||
/// A value for [MaterialApp.localizationsDelegates] that's typically used by
|
||||
/// internationalized apps.
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// To include the localizations provided by this class and by
|
||||
/// [GlobalWidgetsLocalizations] in a [MaterialApp],
|
||||
/// use [GlobalMaterialLocalizations.delegates] as the value of
|
||||
@ -481,17 +441,6 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
|
||||
];
|
||||
}
|
||||
|
||||
const Map<String, TimeOfDayFormat> _icuTimeOfDayToEnum = <String, TimeOfDayFormat>{
|
||||
'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,
|
||||
};
|
||||
|
||||
/// Finds the [TimeOfDayFormat] to use instead of the `original` when the
|
||||
/// `original` uses 12-hour format and [MediaQueryData.alwaysUse24HourFormat]
|
||||
/// is true.
|
||||
@ -509,86 +458,91 @@ TimeOfDayFormat _get24HourVersionOf(TimeOfDayFormat original) {
|
||||
return TimeOfDayFormat.HH_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) {
|
||||
date_localizations.dateSymbols.forEach((String locale, dynamic data) {
|
||||
assert(date_localizations.datePatterns.containsKey(locale));
|
||||
final intl.DateSymbols symbols = new intl.DateSymbols.deserializeFromMap(data);
|
||||
date_symbol_data_custom.initializeDateFormattingCustom(
|
||||
locale: locale,
|
||||
symbols: symbols,
|
||||
patterns: date_localizations.datePatterns[locale],
|
||||
);
|
||||
});
|
||||
_dateIntlDataInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
|
||||
const _MaterialLocalizationsDelegate();
|
||||
|
||||
// Watch out: this list must match the one in the GlobalMaterialLocalizations
|
||||
// class doc and the list we test, see test/translations_test.dart.
|
||||
static const List<String> _supportedLanguages = <String>[
|
||||
'ar', // Arabic
|
||||
'bg', // Bulgarian
|
||||
'bs', // Bosnian
|
||||
'ca', // Catalan
|
||||
'cs', // Czech
|
||||
'da', // Danish
|
||||
'de', // German
|
||||
'el', // Greek
|
||||
'en', // English
|
||||
'es', // Spanish
|
||||
'et', // Estonian
|
||||
'fa', // Farsi (Persian)
|
||||
'fi', // Finnish
|
||||
'fil', // Fillipino
|
||||
'fr', // French
|
||||
'gsw', // Swiss German
|
||||
'he', // Hebrew
|
||||
'hi', // Hindi
|
||||
'hr', // Croatian
|
||||
'hu', // Hungarian
|
||||
'id', // Indonesian
|
||||
'it', // Italian
|
||||
'ja', // Japanese
|
||||
'ko', // Korean
|
||||
'lv', // Latvian
|
||||
'lt', // Lithuanian
|
||||
'ms', // Malay
|
||||
'nl', // Dutch
|
||||
'nb', // Norwegian
|
||||
'pl', // Polish
|
||||
'ps', // Pashto
|
||||
'pt', // Portugese
|
||||
'ro', // Romanian
|
||||
'ru', // Russian
|
||||
'sr', // Serbian
|
||||
'sk', // Slovak
|
||||
'sl', // Slovenian
|
||||
'th', // Thai
|
||||
'sv', // Swedish
|
||||
'tl', // Tagalog
|
||||
'tr', // Turkish
|
||||
'uk', // Ukranian
|
||||
'ur', // Urdu
|
||||
'vi', // Vietnamese
|
||||
'zh', // Chinese (simplified)
|
||||
];
|
||||
@override
|
||||
bool isSupported(Locale locale) => kSupportedLanguages.contains(locale.languageCode);
|
||||
|
||||
/// Tracks if date i18n data has been loaded.
|
||||
static 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.
|
||||
static void _loadDateIntlDataIfNotLoaded() {
|
||||
if (!_dateIntlDataInitialized) {
|
||||
date_localizations.dateSymbols.forEach((String locale, dynamic data) {
|
||||
assert(date_localizations.datePatterns.containsKey(locale));
|
||||
final intl.DateSymbols symbols = new intl.DateSymbols.deserializeFromMap(data);
|
||||
date_symbol_data_custom.initializeDateFormattingCustom(
|
||||
locale: locale,
|
||||
symbols: symbols,
|
||||
patterns: date_localizations.datePatterns[locale],
|
||||
);
|
||||
});
|
||||
_dateIntlDataInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
static final Map<Locale, Future<MaterialLocalizations>> _loadedTranslations = <Locale, Future<MaterialLocalizations>>{};
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) => _supportedLanguages.contains(locale.languageCode);
|
||||
Future<MaterialLocalizations> load(Locale locale) {
|
||||
assert(isSupported(locale));
|
||||
return _loadedTranslations.putIfAbsent(locale, () {
|
||||
_loadDateIntlDataIfNotLoaded();
|
||||
|
||||
@override
|
||||
Future<MaterialLocalizations> load(Locale locale) => GlobalMaterialLocalizations.load(locale);
|
||||
final String localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
intl.DateFormat fullYearFormat;
|
||||
intl.DateFormat mediumDateFormat;
|
||||
intl.DateFormat longDateFormat;
|
||||
intl.DateFormat yearMonthFormat;
|
||||
if (intl.DateFormat.localeExists(localeName)) {
|
||||
fullYearFormat = new intl.DateFormat.y(localeName);
|
||||
mediumDateFormat = new intl.DateFormat.MMMEd(localeName);
|
||||
longDateFormat = new intl.DateFormat.yMMMMEEEEd(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.MMMEd(locale.languageCode);
|
||||
longDateFormat = new intl.DateFormat.yMMMMEEEEd(locale.languageCode);
|
||||
yearMonthFormat = new intl.DateFormat.yMMMM(locale.languageCode);
|
||||
} else {
|
||||
fullYearFormat = new intl.DateFormat.y();
|
||||
mediumDateFormat = new intl.DateFormat.MMMEd();
|
||||
longDateFormat = new intl.DateFormat.yMMMMEEEEd();
|
||||
yearMonthFormat = new intl.DateFormat.yMMMM();
|
||||
}
|
||||
|
||||
intl.NumberFormat decimalFormat;
|
||||
intl.NumberFormat twoDigitZeroPaddedFormat;
|
||||
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');
|
||||
}
|
||||
|
||||
assert(locale.toString() == localeName, 'comparing "$locale" to "$localeName"');
|
||||
|
||||
return new SynchronousFuture<MaterialLocalizations>(getTranslation(
|
||||
locale,
|
||||
fullYearFormat,
|
||||
mediumDateFormat,
|
||||
longDateFormat,
|
||||
yearMonthFormat,
|
||||
decimalFormat,
|
||||
twoDigitZeroPaddedFormat,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReload(_MaterialLocalizationsDelegate old) => false;
|
||||
|
@ -10,19 +10,20 @@ 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'));
|
||||
test('uses exact locale when exists', () async {
|
||||
final GlobalMaterialLocalizations localizations = await GlobalMaterialLocalizations.delegate.load(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'));
|
||||
test('falls back to language code when exact locale is missing', () async {
|
||||
final GlobalMaterialLocalizations localizations = await GlobalMaterialLocalizations.delegate.load(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');
|
||||
test('fails when neither language code nor exact locale are available', () async {
|
||||
await expectLater(() async {
|
||||
await GlobalMaterialLocalizations.delegate.load(const Locale('xx', 'XX'));
|
||||
}, throwsAssertionError);
|
||||
});
|
||||
|
||||
group('formatHour', () {
|
||||
@ -65,8 +66,8 @@ void main() {
|
||||
});
|
||||
|
||||
group('formatMinute', () {
|
||||
test('formats English', () {
|
||||
final GlobalMaterialLocalizations localizations = new GlobalMaterialLocalizations(const Locale('en', 'US'));
|
||||
test('formats English', () async {
|
||||
final GlobalMaterialLocalizations localizations = await GlobalMaterialLocalizations.delegate.load(const Locale('en', 'US'));
|
||||
expect(localizations.formatMinute(const TimeOfDay(hour: 1, minute: 32)), '32');
|
||||
});
|
||||
});
|
||||
|
@ -6,9 +6,21 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
class FooMaterialLocalizations extends GlobalMaterialLocalizations {
|
||||
FooMaterialLocalizations(Locale locale, this.backButtonTooltip) : super(locale);
|
||||
class FooMaterialLocalizations extends MaterialLocalizationEn {
|
||||
FooMaterialLocalizations(
|
||||
Locale localeName,
|
||||
this.backButtonTooltip,
|
||||
) : super(
|
||||
localeName: localeName.toString(),
|
||||
fullYearFormat: new intl.DateFormat.y(),
|
||||
mediumDateFormat: new intl.DateFormat('E, MMM\u00a0d'),
|
||||
longDateFormat: new intl.DateFormat.yMMMMEEEEd(),
|
||||
yearMonthFormat: new intl.DateFormat.yMMMM(),
|
||||
decimalFormat: new intl.NumberFormat.decimalPattern(),
|
||||
twoDigitZeroPaddedFormat: new intl.NumberFormat('00'),
|
||||
);
|
||||
|
||||
@override
|
||||
final String backButtonTooltip;
|
||||
@ -44,7 +56,7 @@ Widget buildFrame({
|
||||
LocaleResolutionCallback localeResolutionCallback,
|
||||
Iterable<Locale> supportedLocales = const <Locale>[
|
||||
Locale('en', 'US'),
|
||||
Locale('es', 'es'),
|
||||
Locale('es', 'ES'),
|
||||
],
|
||||
}) {
|
||||
return new MaterialApp(
|
||||
@ -81,12 +93,12 @@ void main() {
|
||||
expect(tester.widget<Text>(find.byKey(textKey)).data, 'Back');
|
||||
|
||||
// Unrecognized locale falls back to 'en'
|
||||
await tester.binding.setLocale('foo', 'bar');
|
||||
await tester.binding.setLocale('foo', 'BAR');
|
||||
await tester.pump();
|
||||
expect(tester.widget<Text>(find.byKey(textKey)).data, 'Back');
|
||||
|
||||
// Spanish Bolivia locale, falls back to just 'es'
|
||||
await tester.binding.setLocale('es', 'bo');
|
||||
await tester.binding.setLocale('es', 'BO');
|
||||
await tester.pump();
|
||||
expect(tester.widget<Text>(find.byKey(textKey)).data, 'Atrás');
|
||||
});
|
||||
@ -169,7 +181,6 @@ void main() {
|
||||
Locale('de', ''),
|
||||
],
|
||||
buildContent: (BuildContext context) {
|
||||
// Should always be 'foo', no matter what the locale is
|
||||
return new Text(
|
||||
MaterialLocalizations.of(context).backButtonTooltip,
|
||||
key: textKey,
|
||||
|
@ -7,63 +7,13 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
// Watch out: this list must be kept in sync with the comment at the top of
|
||||
// GlobalMaterialLocalizations.
|
||||
final List<String> languages = <String>[
|
||||
'ar', // Arabic
|
||||
'bg', // Bulgarian
|
||||
'bs', // Bosnian
|
||||
'ca', // Catalan
|
||||
'cs', // Czech
|
||||
'da', // Danish
|
||||
'de', // German
|
||||
'el', // Greek
|
||||
'en', // English
|
||||
'es', // Spanish
|
||||
'et', // Estonian
|
||||
'fa', // Farsi (Persian)
|
||||
'fi', // Finnish
|
||||
'fil', // Fillipino
|
||||
'fr', // French
|
||||
'gsw', // Swiss German
|
||||
'he', // Hebrew
|
||||
'hi', // Hindi
|
||||
'hr', // Croatian
|
||||
'hu', // Hungarian
|
||||
'id', // Indonesian
|
||||
'it', // Italian
|
||||
'ja', // Japanese
|
||||
'ko', // Korean
|
||||
'lv', // Latvian
|
||||
'lt', // Lithuanian
|
||||
'ms', // Malay
|
||||
'nl', // Dutch
|
||||
'nb', // Norwegian
|
||||
'pl', // Polish
|
||||
'ps', // Pashto
|
||||
'pt', // Portugese
|
||||
'ro', // Romanian
|
||||
'ru', // Russian
|
||||
'sr', // Serbian
|
||||
'sk', // Slovak
|
||||
'sl', // Slovenian
|
||||
'sv', // Swedish
|
||||
'th', // Thai
|
||||
'tl', // Tagalog
|
||||
'tr', // Turkish
|
||||
'uk', // Ukranian
|
||||
'ur', // Urdu
|
||||
'vi', // Vietnamese
|
||||
'zh', // Chinese (simplified)
|
||||
];
|
||||
|
||||
for (String language in languages) {
|
||||
for (String language in kSupportedLanguages) {
|
||||
testWidgets('translations exist for $language', (WidgetTester tester) async {
|
||||
final Locale locale = new Locale(language, '');
|
||||
|
||||
expect(GlobalMaterialLocalizations.delegate.isSupported(locale), isTrue);
|
||||
|
||||
final MaterialLocalizations localizations = new GlobalMaterialLocalizations(locale);
|
||||
final MaterialLocalizations localizations = await GlobalMaterialLocalizations.delegate.load(locale);
|
||||
|
||||
expect(localizations.openAppDrawerTooltip, isNotNull);
|
||||
expect(localizations.backButtonTooltip, isNotNull);
|
||||
@ -119,22 +69,43 @@ void main() {
|
||||
}
|
||||
|
||||
testWidgets('spot check selectedRowCount translations', (WidgetTester tester) async {
|
||||
MaterialLocalizations localizations = new GlobalMaterialLocalizations(const Locale('en', ''));
|
||||
MaterialLocalizations localizations = await GlobalMaterialLocalizations.delegate.load(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(3), '3 items selected');
|
||||
expect(localizations.selectedRowCountTitle(5), '5 items selected');
|
||||
expect(localizations.selectedRowCountTitle(10), '10 items selected');
|
||||
expect(localizations.selectedRowCountTitle(15), '15 items selected');
|
||||
expect(localizations.selectedRowCountTitle(29), '29 items selected');
|
||||
expect(localizations.selectedRowCountTitle(10000), '10,000 items selected');
|
||||
expect(localizations.selectedRowCountTitle(10019), '10,019 items selected');
|
||||
expect(localizations.selectedRowCountTitle(123456789), '123,456,789 items selected');
|
||||
|
||||
localizations = new GlobalMaterialLocalizations(const Locale('es', ''));
|
||||
localizations = await GlobalMaterialLocalizations.delegate.load(const Locale('es', ''));
|
||||
expect(localizations.selectedRowCountTitle(0), 'No se han seleccionado elementos');
|
||||
expect(localizations.selectedRowCountTitle(1), '1 elemento seleccionado');
|
||||
expect(localizations.selectedRowCountTitle(2), '2 elementos seleccionados');
|
||||
expect(localizations.selectedRowCountTitle(3), '3 elementos seleccionados');
|
||||
expect(localizations.selectedRowCountTitle(5), '5 elementos seleccionados');
|
||||
expect(localizations.selectedRowCountTitle(10), '10 elementos seleccionados');
|
||||
expect(localizations.selectedRowCountTitle(15), '15 elementos seleccionados');
|
||||
expect(localizations.selectedRowCountTitle(29), '29 elementos seleccionados');
|
||||
expect(localizations.selectedRowCountTitle(10000), '10.000 elementos seleccionados');
|
||||
expect(localizations.selectedRowCountTitle(10019), '10.019 elementos seleccionados');
|
||||
expect(localizations.selectedRowCountTitle(123456789), '123.456.789 elementos seleccionados');
|
||||
|
||||
localizations = new GlobalMaterialLocalizations(const Locale('ro', ''));
|
||||
localizations = await GlobalMaterialLocalizations.delegate.load(const Locale('ro', ''));
|
||||
expect(localizations.selectedRowCountTitle(0), 'Nu există elemente selectate');
|
||||
expect(localizations.selectedRowCountTitle(1), 'Un articol selectat');
|
||||
expect(localizations.selectedRowCountTitle(2), '2 de articole selectate');
|
||||
expect(localizations.selectedRowCountTitle(2), '2 articole selectate');
|
||||
expect(localizations.selectedRowCountTitle(3), '3 articole selectate');
|
||||
expect(localizations.selectedRowCountTitle(5), '5 articole selectate');
|
||||
expect(localizations.selectedRowCountTitle(10), '10 articole selectate');
|
||||
expect(localizations.selectedRowCountTitle(15), '15 articole selectate');
|
||||
expect(localizations.selectedRowCountTitle(29), '29 de articole selectate');
|
||||
expect(localizations.selectedRowCountTitle(10000), '10.000 de articole selectate');
|
||||
expect(localizations.selectedRowCountTitle(10019), '10.019 articole selectate');
|
||||
expect(localizations.selectedRowCountTitle(123456789), '123.456.789 de articole selectate');
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user