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 for the subset of locales supported by the flutter_localizations
|
||||||
/// package.
|
/// 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
|
/// ## Usage
|
||||||
///
|
///
|
||||||
@ -89,7 +90,7 @@ Future<Null> main(List<String> rawArgs) async {
|
|||||||
});
|
});
|
||||||
buffer.writeln('};');
|
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>.
|
// Map<String, String> not Map<String, dynamic>.
|
||||||
buffer.writeln('const Map<String, Map<String, String>> datePatterns = const <String, Map<String, String>> {');
|
buffer.writeln('const Map<String, Map<String, String>> datePatterns = const <String, Map<String, String>> {');
|
||||||
patternFiles.forEach((String locale, File data) {
|
patternFiles.forEach((String locale, File data) {
|
||||||
|
@ -30,17 +30,19 @@
|
|||||||
// dart dev/tools/gen_localizations.dart
|
// 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:
|
// packages/flutter_localizations/lib/src/l10n/localizations.dart file:
|
||||||
//
|
//
|
||||||
// ```
|
// ```
|
||||||
// dart dev/tools/gen_localizations.dart --overwrite
|
// dart dev/tools/gen_localizations.dart --overwrite
|
||||||
// ```
|
// ```
|
||||||
|
|
||||||
import 'dart:convert' show json;
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
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_utils.dart';
|
||||||
import 'localizations_validator.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
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// 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:
|
// To regenerate the file, use:
|
||||||
// @(regenerate)
|
// @(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.
|
/// Maps locales to resource key/value pairs.
|
||||||
final Map<String, Map<String, String>> localeToResources = <String, Map<String, String>>{};
|
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>>{};
|
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:
|
/// Return `s` as a Dart-parseable raw string in single or double quotes.
|
||||||
// foo => r'foo'
|
///
|
||||||
// foo "bar" => r'foo "bar"'
|
/// Double quotes are expanded:
|
||||||
// foo 'bar' => r'foo ' "'" r'bar' "'"
|
///
|
||||||
|
/// ```
|
||||||
|
/// foo => r'foo'
|
||||||
|
/// foo "bar" => r'foo "bar"'
|
||||||
|
/// foo 'bar' => r'foo ' "'" r'bar' "'"
|
||||||
|
/// ```
|
||||||
String generateString(String s) {
|
String generateString(String s) {
|
||||||
if (!s.contains("'"))
|
if (!s.contains("'"))
|
||||||
return "r'$s'";
|
return "r'$s'";
|
||||||
@ -91,8 +106,10 @@ String generateString(String s) {
|
|||||||
return output.toString();
|
return output.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This is the core of this script; it generates the code used for translations.
|
||||||
String generateTranslationBundles() {
|
String generateTranslationBundles() {
|
||||||
final StringBuffer output = new StringBuffer();
|
final StringBuffer output = new StringBuffer();
|
||||||
|
final StringBuffer supportedLocales = new StringBuffer();
|
||||||
|
|
||||||
final Map<String, List<String>> languageToLocales = <String, List<String>>{};
|
final Map<String, List<String>> languageToLocales = <String, List<String>>{};
|
||||||
final Set<String> allResourceIdentifiers = new Set<String>();
|
final Set<String> allResourceIdentifiers = new Set<String>();
|
||||||
@ -104,132 +121,262 @@ String generateTranslationBundles() {
|
|||||||
allResourceIdentifiers.addAll(localeToResources[locale].keys);
|
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('''
|
output.writeln('''
|
||||||
// The TranslationBundle subclasses defined here encode all of the translations
|
// The classes defined here encode all of the translations found in the
|
||||||
// found in the flutter_localizations/lib/src/l10n/*.arb files.
|
// `flutter_localizations/lib/src/l10n/*.arb` files.
|
||||||
//
|
//
|
||||||
// The [MaterialLocalizations] class uses the (generated)
|
// These classes are constructed by the [getTranslation] method at the bottom of
|
||||||
// translationBundleForLocale() function to look up a const TranslationBundle
|
// this file, and used by the [_MaterialLocalizationsDelegate.load] method defined
|
||||||
// instance for a locale.
|
// 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 {
|
final List<String> allKeys = allResourceIdentifiers.toList()..sort();
|
||||||
const TranslationBundle(this.parent);
|
final List<String> languageCodes = languageToLocales.keys.toList()..sort();
|
||||||
final TranslationBundle parent;''');
|
for (String languageName in languageCodes) {
|
||||||
for (String key in allResourceIdentifiers)
|
final String camelCaseLanguage = camelCase(languageName);
|
||||||
output.writeln(' String get $key => parent?.$key;');
|
final Map<String, String> languageResources = localeToResources[languageName];
|
||||||
output.writeln('''
|
final String languageClassName = 'MaterialLocalization$camelCaseLanguage';
|
||||||
}''');
|
final String constructor = generateConstructor(languageClassName, languageName);
|
||||||
|
output.writeln('');
|
||||||
// Generate one private TranslationBundle subclass per supported
|
output.writeln('/// The translations for ${describeLocale(languageName)} (`$languageName`).');
|
||||||
// language. Each of these classes overrides every resource identifier
|
output.writeln('class $languageClassName extends GlobalMaterialLocalizations {');
|
||||||
// getter. For example:
|
output.writeln(constructor);
|
||||||
//
|
for (String key in allKeys) {
|
||||||
// class _Bundle_en extends TranslationBundle {
|
final Map<String, dynamic> attributes = localeToResourceAttributes['en'][key];
|
||||||
// const _Bundle_en() : super(null);
|
output.writeln(generateGetter(key, languageResources[key], attributes));
|
||||||
// @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;''');
|
|
||||||
}
|
}
|
||||||
output.writeln('''
|
output.writeln('}');
|
||||||
}''');
|
int countryCodeCount = 0;
|
||||||
}
|
final List<String> localeCodes = languageToLocales[languageName]..sort();
|
||||||
|
for (String localeName in localeCodes) {
|
||||||
// Generate one private TranslationBundle subclass for each locale
|
if (localeName == languageName)
|
||||||
// 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)
|
|
||||||
continue;
|
continue;
|
||||||
|
countryCodeCount += 1;
|
||||||
|
final String camelCaseLocaleName = camelCase(localeName);
|
||||||
final Map<String, String> localeResources = localeToResources[localeName];
|
final Map<String, String> localeResources = localeToResources[localeName];
|
||||||
output.writeln('''
|
final String localeClassName = 'MaterialLocalization$camelCaseLocaleName';
|
||||||
|
final String constructor = generateConstructor(localeClassName, localeName);
|
||||||
// ignore: camel_case_types
|
output.writeln('');
|
||||||
class _Bundle_$localeName extends TranslationBundle {
|
output.writeln('/// The translations for ${describeLocale(localeName)} (`$localeName`).');
|
||||||
const _Bundle_$localeName() : super(const _Bundle_$language());''');
|
output.writeln('class $localeClassName extends $languageClassName {');
|
||||||
|
output.writeln(constructor);
|
||||||
for (String key in localeResources.keys) {
|
for (String key in localeResources.keys) {
|
||||||
if (languageResources[key] == localeResources[key])
|
if (languageResources[key] == localeResources[key])
|
||||||
continue;
|
continue;
|
||||||
final String value = generateString(localeResources[key]);
|
final Map<String, dynamic> attributes = localeToResourceAttributes['en'][key];
|
||||||
output.writeln('''
|
output.writeln(generateGetter(key, localeResources[key], attributes));
|
||||||
@override String get $key => $value;''');
|
|
||||||
}
|
}
|
||||||
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
|
// Generate the getTranslation function. Given a Locale it returns the
|
||||||
// it returns the corresponding const TranslationBundle.
|
// corresponding const GlobalMaterialLocalizations.
|
||||||
output.writeln('''
|
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) {''');
|
switch (locale.languageCode) {''');
|
||||||
|
const String arguments = 'fullYearFormat: fullYearFormat, mediumDateFormat: mediumDateFormat, longDateFormat: longDateFormat, yearMonthFormat: yearMonthFormat, decimalFormat: decimalFormat, twoDigitZeroPaddedFormat: twoDigitZeroPaddedFormat';
|
||||||
for (String language in languageToLocales.keys) {
|
for (String language in languageToLocales.keys) {
|
||||||
if (languageToLocales[language].length == 1) {
|
if (languageToLocales[language].length == 1) {
|
||||||
output.writeln('''
|
output.writeln('''
|
||||||
case \'$language\':
|
case '$language':
|
||||||
return const _Bundle_${languageToLocales[language][0]}();''');
|
return new MaterialLocalization${camelCase(languageToLocales[language][0])}($arguments);''');
|
||||||
} else {
|
} else {
|
||||||
output.writeln('''
|
output.writeln('''
|
||||||
case \'$language\': {
|
case '$language': {
|
||||||
switch (locale.toString()) {''');
|
switch (locale.countryCode) {''');
|
||||||
for (String localeName in languageToLocales[language]) {
|
for (String localeName in languageToLocales[language]) {
|
||||||
if (localeName == language)
|
if (localeName == language)
|
||||||
continue;
|
continue;
|
||||||
|
assert(localeName.contains('_'));
|
||||||
|
final String countryCode = localeName.substring(localeName.indexOf('_') + 1);
|
||||||
output.writeln('''
|
output.writeln('''
|
||||||
case \'$localeName\':
|
case '$countryCode':
|
||||||
return const _Bundle_$localeName();''');
|
return new MaterialLocalization${camelCase(localeName)}($arguments);''');
|
||||||
}
|
}
|
||||||
output.writeln('''
|
output.writeln('''
|
||||||
}
|
}
|
||||||
return const _Bundle_$language();
|
return new MaterialLocalization${camelCase(language)}($arguments);
|
||||||
}''');
|
}''');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
output.writeln('''
|
output.writeln('''
|
||||||
}
|
}
|
||||||
return const TranslationBundle(null);
|
assert(false, 'getTranslation() called for unsupported locale "\$locale"');
|
||||||
|
return null;
|
||||||
}''');
|
}''');
|
||||||
|
|
||||||
return output.toString();
|
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>{};
|
localeToResources[locale] ??= <String, String>{};
|
||||||
localeToResourceAttributes[locale] ??= <String, dynamic>{};
|
localeToResourceAttributes[locale] ??= <String, dynamic>{};
|
||||||
final Map<String, String> resources = localeToResources[locale];
|
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');
|
checkCwdIsRepoRoot('gen_localizations');
|
||||||
final GeneratorOptions options = parseArgs(rawArgs);
|
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
|
// 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.
|
// 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$');
|
final RegExp filenameRE = new RegExp(r'material_(\w+)\.arb$');
|
||||||
|
|
||||||
exitWithError(
|
try {
|
||||||
validateEnglishLocalizations(new File(pathlib.join(directory.path, 'material_en.arb')))
|
validateEnglishLocalizations(new File(path.join(directory.path, 'material_en.arb')));
|
||||||
);
|
} on ValidationError catch (exception) {
|
||||||
|
exitWithError('$exception');
|
||||||
|
}
|
||||||
|
|
||||||
|
await precacheLanguageAndRegionTags();
|
||||||
|
|
||||||
for (FileSystemEntity entity in directory.listSync()) {
|
for (FileSystemEntity entity in directory.listSync()) {
|
||||||
final String path = entity.path;
|
final String entityPath = entity.path;
|
||||||
if (FileSystemEntity.isFileSync(path) && filenameRE.hasMatch(path)) {
|
if (FileSystemEntity.isFileSync(entityPath) && filenameRE.hasMatch(entityPath)) {
|
||||||
final String locale = filenameRE.firstMatch(path)[1];
|
processBundle(new File(entityPath), locale: filenameRE.firstMatch(entityPath)[1]);
|
||||||
processBundle(new File(path), locale);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exitWithError(
|
try {
|
||||||
validateLocalizations(localeToResources, localeToResourceAttributes)
|
validateLocalizations(localeToResources, localeToResourceAttributes);
|
||||||
);
|
} on ValidationError catch (exception) {
|
||||||
|
exitWithError('$exception');
|
||||||
|
}
|
||||||
|
|
||||||
const String regenerate = 'dart dev/tools/gen_localizations.dart --overwrite';
|
|
||||||
final StringBuffer buffer = new StringBuffer();
|
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());
|
buffer.write(generateTranslationBundles());
|
||||||
|
|
||||||
if (options.writeToFile) {
|
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());
|
localizationsFile.writeAsStringSync(buffer.toString());
|
||||||
} else {
|
} else {
|
||||||
stdout.write(buffer.toString());
|
stdout.write(buffer.toString());
|
||||||
|
@ -2,15 +2,16 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:args/args.dart' as argslib;
|
import 'package:args/args.dart' as argslib;
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
void exitWithError(String errorMessage) {
|
void exitWithError(String errorMessage) {
|
||||||
if (errorMessage == null)
|
assert(errorMessage != null);
|
||||||
return;
|
stderr.writeln('fatal: $errorMessage');
|
||||||
stderr.writeln('Fatal Error: $errorMessage');
|
|
||||||
exit(1);
|
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) {
|
GeneratorOptions parseArgs(List<String> rawArgs) {
|
||||||
final argslib.ArgParser argParser = new argslib.ArgParser()
|
final argslib.ArgParser argParser = new argslib.ArgParser()
|
||||||
..addFlag(
|
..addFlag(
|
||||||
@ -45,3 +53,90 @@ class GeneratorOptions {
|
|||||||
|
|
||||||
final bool writeToFile;
|
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:convert' show json;
|
||||||
import 'dart:io';
|
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,
|
/// Sanity checking of the @foo metadata in the English translations,
|
||||||
/// material_en.arb.
|
/// material_en.arb.
|
||||||
///
|
///
|
||||||
@ -14,13 +26,13 @@ import 'dart:io';
|
|||||||
/// - Each @foo resource must have a Map value with a String valued
|
/// - Each @foo resource must have a Map value with a String valued
|
||||||
/// description entry.
|
/// description entry.
|
||||||
///
|
///
|
||||||
/// Returns an error message upon failure, null on success.
|
/// Throws an exception upon failure.
|
||||||
String validateEnglishLocalizations(File file) {
|
void validateEnglishLocalizations(File file) {
|
||||||
final StringBuffer errorMessages = new StringBuffer();
|
final StringBuffer errorMessages = new StringBuffer();
|
||||||
|
|
||||||
if (!file.existsSync()) {
|
if (!file.existsSync()) {
|
||||||
errorMessages.writeln('English localizations do not exist: $file');
|
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());
|
final Map<String, dynamic> bundle = json.decode(file.readAsStringSync());
|
||||||
@ -36,7 +48,7 @@ String validateEnglishLocalizations(File file) {
|
|||||||
final int suffixIndex = resourceId.indexOf(suffix);
|
final int suffixIndex = resourceId.indexOf(suffix);
|
||||||
return suffixIndex != -1 && bundle['@${resourceId.substring(0, suffixIndex)}'] != null;
|
return suffixIndex != -1 && bundle['@${resourceId.substring(0, suffixIndex)}'] != null;
|
||||||
}
|
}
|
||||||
if (<String>['Zero', 'One', 'Two', 'Few', 'Many', 'Other'].any(checkPluralResource))
|
if (kPluralSuffixes.any(checkPluralResource))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
errorMessages.writeln('A value was not specified for @$resourceId');
|
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:
|
/// 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
|
/// Uses "en" localizations as the canonical source of locale keys that other
|
||||||
/// locales are compared against.
|
/// locales are compared against.
|
||||||
///
|
///
|
||||||
/// If validation fails, return an error message, otherwise return null.
|
/// If validation fails, throws an exception.
|
||||||
String validateLocalizations(
|
void validateLocalizations(
|
||||||
Map<String, Map<String, String>> localeToResources,
|
Map<String, Map<String, String>> localeToResources,
|
||||||
Map<String, Map<String, dynamic>> localeToAttributes,
|
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
|
// Many languages require only a subset of these variations, so we do not
|
||||||
// require them so long as the "Other" variation exists.
|
// require them so long as the "Other" variation exists.
|
||||||
bool isPluralVariation(String key) {
|
bool isPluralVariation(String key) {
|
||||||
final RegExp pluralRegexp = new RegExp(r'(\w*)(Zero|One|Two|Few|Many)$');
|
final Match pluralMatch = kPluralRegexp.firstMatch(key);
|
||||||
final Match pluralMatch = pluralRegexp.firstMatch(key);
|
|
||||||
|
|
||||||
if (pluralMatch == null)
|
if (pluralMatch == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
final String prefix = pluralMatch[1];
|
final String prefix = pluralMatch[1];
|
||||||
return resources.containsKey('${prefix}Other');
|
return resources.containsKey('${prefix}Other');
|
||||||
}
|
}
|
||||||
@ -151,7 +161,6 @@ String validateLocalizations(
|
|||||||
..writeln(' "notUsed": "Sindhi time format does not use a.m. indicator"')
|
..writeln(' "notUsed": "Sindhi time format does not use a.m. indicator"')
|
||||||
..writeln('}');
|
..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)) {
|
if ((index - startForLengthCalculations > width) || (index == message.length)) {
|
||||||
// we are over the width line, so break
|
// we are over the width line, so break
|
||||||
if ((index - startForLengthCalculations <= width) || (lastWordEnd == null)) {
|
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;
|
lastWordEnd = index;
|
||||||
}
|
}
|
||||||
if (addPrefix) {
|
if (addPrefix) {
|
||||||
|
@ -5,5 +5,6 @@
|
|||||||
/// Localizations for the Flutter library
|
/// Localizations for the Flutter library
|
||||||
library flutter_localizations;
|
library flutter_localizations;
|
||||||
|
|
||||||
export 'src/material_localizations.dart' show GlobalMaterialLocalizations;
|
export 'src/l10n/localizations.dart';
|
||||||
export 'src/widgets_localizations.dart' show GlobalWidgetsLocalizations;
|
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
|
For each resource ID defined for English in material_en.arb, there is
|
||||||
an additional resource with an '@' prefix. These '@' resources are not
|
an additional resource with an '@' prefix. These '@' resources are not
|
||||||
used by the material library at run time, they just exist to inform
|
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
|
```dart
|
||||||
"cancelButtonLabel": "CANCEL",
|
"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
|
The value of `timeOfDayFormat` defines how a time picker displayed by
|
||||||
[showTimePicker()](https://docs.flutter.io/flutter/material/showTimePicker.html)
|
[showTimePicker()](https://docs.flutter.io/flutter/material/showTimePicker.html)
|
||||||
formats and lays out its time controls. The value of `timeOfDayFormat` must be
|
formats and lays out its time controls. The value of `timeOfDayFormat`
|
||||||
a string that matches one of the formats defined by
|
must be a string that matches one of the formats defined by
|
||||||
https://docs.flutter.io/flutter/material/TimeOfDayFormat-class.html.
|
<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
|
The value of `scriptCategory` is based on the
|
||||||
[Language categories reference](https://material.io/go/design-typography#typography-language-categories-reference)
|
[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'''],
|
'AMPMS': <dynamic>[r'''AM''', r'''PM'''],
|
||||||
'DATEFORMATS': <dynamic>[
|
'DATEFORMATS': <dynamic>[
|
||||||
r'''EEEE, MMMM d, y''',
|
r'''EEEE، d MMMM، y''',
|
||||||
r'''MMMM d, y''',
|
r'''d MMMM، y''',
|
||||||
r'''MMM d, y''',
|
r'''d MMM، y''',
|
||||||
r'''d/M/yy'''
|
r'''d/M/yy'''
|
||||||
],
|
],
|
||||||
'TIMEFORMATS': <dynamic>[
|
'TIMEFORMATS': <dynamic>[
|
||||||
@ -9994,7 +9994,7 @@ const Map<String, Map<String, String>> datePatterns =
|
|||||||
'MMMd': r'''d MMM''',
|
'MMMd': r'''d MMM''',
|
||||||
'MMMEd': r'''EEE، d MMM''',
|
'MMMEd': r'''EEE، d MMM''',
|
||||||
'MMMM': r'''LLLL''',
|
'MMMM': r'''LLLL''',
|
||||||
'MMMMd': r'''MMMM d''',
|
'MMMMd': r'''d MMMM''',
|
||||||
'MMMMEEEEd': r'''EEEE، d MMMM''',
|
'MMMMEEEEd': r'''EEEE، d MMMM''',
|
||||||
'QQQ': r'''QQQ''',
|
'QQQ': r'''QQQ''',
|
||||||
'QQQQ': r'''QQQQ''',
|
'QQQQ': r'''QQQQ''',
|
||||||
@ -10006,8 +10006,8 @@ const Map<String, Map<String, String>> datePatterns =
|
|||||||
'yMMMd': r'''d MMM، y''',
|
'yMMMd': r'''d MMM، y''',
|
||||||
'yMMMEd': r'''EEE، d MMM، y''',
|
'yMMMEd': r'''EEE، d MMM، y''',
|
||||||
'yMMMM': r'''MMMM y''',
|
'yMMMM': r'''MMMM y''',
|
||||||
'yMMMMd': r'''MMMM d, y''',
|
'yMMMMd': r'''d MMMM، y''',
|
||||||
'yMMMMEEEEd': r'''EEEE, MMMM d, y''',
|
'yMMMMEEEEd': r'''EEEE، d MMMM، y''',
|
||||||
'yQQQ': r'''QQQ y''',
|
'yQQQ': r'''QQQ y''',
|
||||||
'yQQQQ': r'''QQQQ y''',
|
'yQQQQ': r'''QQQQ y''',
|
||||||
'H': r'''HH''',
|
'H': r'''HH''',
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,8 @@
|
|||||||
|
|
||||||
"timeOfDayFormat": "h:mm a",
|
"timeOfDayFormat": "h:mm a",
|
||||||
"@timeOfDayFormat": {
|
"@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",
|
"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 'package:intl/date_symbol_data_custom.dart' as date_symbol_data_custom;
|
||||||
import 'l10n/date_localizations.dart' as date_localizations;
|
import 'l10n/date_localizations.dart' as date_localizations;
|
||||||
|
|
||||||
import 'l10n/localizations.dart' show TranslationBundle, translationBundleForLocale;
|
import 'l10n/localizations.dart';
|
||||||
import 'widgets_localizations.dart';
|
import 'widgets_localizations.dart';
|
||||||
|
|
||||||
// Watch out: the supported locales list in the doc comment below must be kept
|
// 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
|
// in sync with the list we test, see test/translations_test.dart, and of course
|
||||||
// the actual list of supported locales in _MaterialLocalizationsDelegate.
|
// 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],
|
/// To include the localizations provided by this class in a [MaterialApp],
|
||||||
/// add [GlobalMaterialLocalizations.delegates] to
|
/// add [GlobalMaterialLocalizations.delegates] to
|
||||||
@ -29,133 +40,87 @@ import 'widgets_localizations.dart';
|
|||||||
/// new MaterialApp(
|
/// new MaterialApp(
|
||||||
/// localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
/// localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
||||||
/// supportedLocales: [
|
/// supportedLocales: [
|
||||||
/// const Locale('en', 'US'), // English
|
/// const Locale('en', 'US'), // American English
|
||||||
/// const Locale('he', 'IL'), // Hebrew
|
/// const Locale('he', 'IL'), // Israeli Hebrew
|
||||||
/// // ...
|
/// // ...
|
||||||
/// ],
|
/// ],
|
||||||
/// // ...
|
/// // ...
|
||||||
/// )
|
/// )
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// This class supports locales with the following [Locale.languageCode]s:
|
/// ## Overriding translations
|
||||||
///
|
///
|
||||||
/// * ar - Arabic
|
/// To create a translation that's similar to an existing language's translation
|
||||||
/// * bg - Bulgarian
|
/// but has slightly different strings, subclass the relevant translation
|
||||||
/// * bs - Bosnian
|
/// directly and then create a [LocalizationsDelegate<MaterialLocalizations>]
|
||||||
/// * ca - Catalan
|
/// subclass to define how to load it.
|
||||||
/// * cs - Czech
|
///
|
||||||
/// * da - Danish
|
/// Avoid subclassing an unrelated language (for example, subclassing
|
||||||
/// * de - German
|
/// [MaterialLocalizationEn] and then passing a non-English `localeName` to the
|
||||||
/// * el - Greek
|
/// constructor). Doing so will cause confusion for locale-specific behaviors;
|
||||||
/// * en - English
|
/// in particular, translations that use the `localeName` for determining how to
|
||||||
/// * es - Spanish
|
/// pluralize will end up doing invalid things. Subclassing an existing
|
||||||
/// * et - Estonian
|
/// language's translations is only suitable for making small changes to the
|
||||||
/// * fa - Farsi
|
/// existing strings. For providing a new language entirely, implement
|
||||||
/// * fi - Finnish
|
/// [MaterialLocalizations] directly.
|
||||||
/// * 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
|
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * The Flutter Internationalization Tutorial,
|
/// * The Flutter Internationalization Tutorial,
|
||||||
/// <https://flutter.io/tutorials/internationalization/>.
|
/// <https://flutter.io/tutorials/internationalization/>.
|
||||||
/// * [DefaultMaterialLocalizations], which only provides US English translations.
|
/// * [DefaultMaterialLocalizations], which only provides US English translations.
|
||||||
class GlobalMaterialLocalizations implements MaterialLocalizations {
|
abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
|
||||||
/// Constructs an object that defines the material widgets' localized strings
|
/// Initializes an object that defines the material widgets' localized strings
|
||||||
/// for the given `locale`.
|
/// for the given `locale`.
|
||||||
///
|
///
|
||||||
/// [LocalizationsDelegate] implementations typically call the static [load]
|
/// The arguments are used for further runtime localization of data,
|
||||||
/// function, rather than constructing this class directly.
|
/// specifically for selecting plurals, date and time formatting, and number
|
||||||
GlobalMaterialLocalizations(this.locale)
|
/// formatting. They correspond to the following values:
|
||||||
: assert(locale != null),
|
///
|
||||||
_localeName = _computeLocaleName(locale) {
|
/// 1. The string that would be returned by [Intl.canonicalizedLocale] for
|
||||||
_loadDateIntlDataIfNotLoaded();
|
/// the locale.
|
||||||
|
/// 2. The [intl.DateFormat] for [formatYear].
|
||||||
_translationBundle = translationBundleForLocale(locale);
|
/// 3. The [intl.DateFormat] for [formatMediumDate].
|
||||||
assert(_translationBundle != null);
|
/// 4. The [intl.DateFormat] for [formatFullDate].
|
||||||
|
/// 5. The [intl.DateFormat] for [formatMonthYear].
|
||||||
if (intl.DateFormat.localeExists(_localeName)) {
|
/// 6. The [NumberFormat] for [formatDecimal] (also used by [formatHour] and
|
||||||
_fullYearFormat = new intl.DateFormat.y(_localeName);
|
/// [formatTimeOfDay] when [timeOfDayFormat] doesn't use [HourFormat.HH]).
|
||||||
_mediumDateFormat = new intl.DateFormat.MMMEd(_localeName);
|
/// 7. The [NumberFormat] for [formatHour] and the hour part of
|
||||||
_longDateFormat = new intl.DateFormat.yMMMMEEEEd(_localeName);
|
/// [formatTimeOfDay] when [timeOfDayFormat] uses [HourFormat.HH], and for
|
||||||
_yearMonthFormat = new intl.DateFormat.yMMMM(_localeName);
|
/// [formatMinute] and the minute part of [formatTimeOfDay].
|
||||||
} else if (intl.DateFormat.localeExists(locale.languageCode)) {
|
///
|
||||||
_fullYearFormat = new intl.DateFormat.y(locale.languageCode);
|
/// The [narrowWeekdays] and [firstDayOfWeekIndex] properties use the values
|
||||||
_mediumDateFormat = new intl.DateFormat.MMMEd(locale.languageCode);
|
/// from the [intl.DateFormat] used by [formatFullDate].
|
||||||
_longDateFormat = new intl.DateFormat.yMMMMEEEEd(locale.languageCode);
|
const GlobalMaterialLocalizations({
|
||||||
_yearMonthFormat = new intl.DateFormat.yMMMM(locale.languageCode);
|
@required String localeName,
|
||||||
} else {
|
@required intl.DateFormat fullYearFormat,
|
||||||
_fullYearFormat = new intl.DateFormat.y();
|
@required intl.DateFormat mediumDateFormat,
|
||||||
_mediumDateFormat = new intl.DateFormat.MMMEd();
|
@required intl.DateFormat longDateFormat,
|
||||||
_longDateFormat = new intl.DateFormat.yMMMMEEEEd();
|
@required intl.DateFormat yearMonthFormat,
|
||||||
_yearMonthFormat = new intl.DateFormat.yMMMM();
|
@required intl.NumberFormat decimalFormat,
|
||||||
}
|
@required intl.NumberFormat twoDigitZeroPaddedFormat,
|
||||||
|
}) : assert(localeName != null),
|
||||||
if (intl.NumberFormat.localeExists(_localeName)) {
|
this._localeName = localeName,
|
||||||
_decimalFormat = new intl.NumberFormat.decimalPattern(_localeName);
|
assert(fullYearFormat != null),
|
||||||
_twoDigitZeroPaddedFormat = new intl.NumberFormat('00', _localeName);
|
this._fullYearFormat = fullYearFormat,
|
||||||
} else if (intl.NumberFormat.localeExists(locale.languageCode)) {
|
assert(mediumDateFormat != null),
|
||||||
_decimalFormat = new intl.NumberFormat.decimalPattern(locale.languageCode);
|
this._mediumDateFormat = mediumDateFormat,
|
||||||
_twoDigitZeroPaddedFormat = new intl.NumberFormat('00', locale.languageCode);
|
assert(longDateFormat != null),
|
||||||
} else {
|
this._longDateFormat = longDateFormat,
|
||||||
_decimalFormat = new intl.NumberFormat.decimalPattern();
|
assert(yearMonthFormat != null),
|
||||||
_twoDigitZeroPaddedFormat = new intl.NumberFormat('00');
|
this._yearMonthFormat = yearMonthFormat,
|
||||||
}
|
assert(decimalFormat != null),
|
||||||
}
|
this._decimalFormat = decimalFormat,
|
||||||
|
assert(twoDigitZeroPaddedFormat != null),
|
||||||
/// The locale for which the values of this class's localized resources
|
this._twoDigitZeroPaddedFormat = twoDigitZeroPaddedFormat;
|
||||||
/// have been translated.
|
|
||||||
final Locale locale;
|
|
||||||
|
|
||||||
final String _localeName;
|
final String _localeName;
|
||||||
|
final intl.DateFormat _fullYearFormat;
|
||||||
TranslationBundle _translationBundle;
|
final intl.DateFormat _mediumDateFormat;
|
||||||
|
final intl.DateFormat _longDateFormat;
|
||||||
intl.NumberFormat _decimalFormat;
|
final intl.DateFormat _yearMonthFormat;
|
||||||
|
final intl.NumberFormat _decimalFormat;
|
||||||
intl.NumberFormat _twoDigitZeroPaddedFormat;
|
final 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());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String formatHour(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat = false }) {
|
String formatHour(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat = false }) {
|
||||||
@ -198,11 +163,11 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> get narrowWeekdays {
|
List<String> get narrowWeekdays {
|
||||||
return _fullYearFormat.dateSymbols.NARROWWEEKDAYS;
|
return _longDateFormat.dateSymbols.NARROWWEEKDAYS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get firstDayOfWeekIndex => (_fullYearFormat.dateSymbols.FIRSTDAYOFWEEK + 1) % 7;
|
int get firstDayOfWeekIndex => (_longDateFormat.dateSymbols.FIRSTDAYOFWEEK + 1) % 7;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String formatDecimal(int number) {
|
String formatDecimal(int number) {
|
||||||
@ -234,7 +199,6 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
|
|||||||
case TimeOfDayFormat.frenchCanadian:
|
case TimeOfDayFormat.frenchCanadian:
|
||||||
return '$hour h $minute';
|
return '$hour h $minute';
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,152 +212,162 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
/// The raw version of [aboutListTileTitle], with `$applicationName` verbatim
|
||||||
String get openAppDrawerTooltip => _translationBundle.openAppDrawerTooltip;
|
/// in the string.
|
||||||
|
@protected
|
||||||
@override
|
String get aboutListTileTitleRaw;
|
||||||
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;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String aboutListTileTitle(String applicationName) {
|
String aboutListTileTitle(String applicationName) {
|
||||||
final String text = _translationBundle.aboutListTileTitle;
|
final String text = aboutListTileTitleRaw;
|
||||||
return text.replaceFirst(r'$applicationName', applicationName);
|
return text.replaceFirst(r'$applicationName', applicationName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
/// The raw version of [pageRowsInfoTitle], with `$firstRow`, `$lastRow`' and
|
||||||
String get licensesPageTitle => _translationBundle.licensesPageTitle;
|
/// `$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
|
@override
|
||||||
String pageRowsInfoTitle(int firstRow, int lastRow, int rowCount, bool rowCountIsApproximate) {
|
String pageRowsInfoTitle(int firstRow, int lastRow, int rowCount, bool rowCountIsApproximate) {
|
||||||
String text = rowCountIsApproximate ? _translationBundle.pageRowsInfoTitleApproximate : null;
|
String text = rowCountIsApproximate ? pageRowsInfoTitleApproximateRaw : null;
|
||||||
text ??= _translationBundle.pageRowsInfoTitle;
|
text ??= pageRowsInfoTitleRaw;
|
||||||
assert(text != null, 'A $locale localization was not found for pageRowsInfoTitle or pageRowsInfoTitleApproximate');
|
assert(text != null, 'A $_localeName localization was not found for pageRowsInfoTitle or pageRowsInfoTitleApproximate');
|
||||||
// TODO(hansmuller): this could be more efficient.
|
|
||||||
return text
|
return text
|
||||||
.replaceFirst(r'$firstRow', formatDecimal(firstRow))
|
.replaceFirst(r'$firstRow', formatDecimal(firstRow))
|
||||||
.replaceFirst(r'$lastRow', formatDecimal(lastRow))
|
.replaceFirst(r'$lastRow', formatDecimal(lastRow))
|
||||||
.replaceFirst(r'$rowCount', formatDecimal(rowCount));
|
.replaceFirst(r'$rowCount', formatDecimal(rowCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
/// The raw version of [tabLabel], with `$tabIndex` and `$tabCount` verbatim
|
||||||
String get rowsPerPageTitle => _translationBundle.rowsPerPageTitle;
|
/// in the string.
|
||||||
|
@protected
|
||||||
|
String get tabLabelRaw;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String tabLabel({int tabIndex, int tabCount}) {
|
String tabLabel({int tabIndex, int tabCount}) {
|
||||||
assert(tabIndex >= 1);
|
assert(tabIndex >= 1);
|
||||||
assert(tabCount >= 1);
|
assert(tabCount >= 1);
|
||||||
final String template = _translationBundle.tabLabel;
|
final String template = tabLabelRaw;
|
||||||
return template
|
return template
|
||||||
.replaceFirst(r'$tabIndex', formatDecimal(tabIndex))
|
.replaceFirst(r'$tabIndex', formatDecimal(tabIndex))
|
||||||
.replaceFirst(r'$tabCount', formatDecimal(tabCount));
|
.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
|
@override
|
||||||
String selectedRowCountTitle(int selectedRowCount) {
|
String selectedRowCountTitle(int selectedRowCount) {
|
||||||
// TODO(hmuller): the rules for mapping from an integer value to
|
return intl.Intl.plural(
|
||||||
// "one" or "two" etc. are locale specific and an additional "few" category
|
selectedRowCount,
|
||||||
// is needed. See http://cldr.unicode.org/index/cldr-spec/plural-rules
|
zero: selectedRowCountTitleZero,
|
||||||
String text;
|
one: selectedRowCountTitleOne,
|
||||||
if (selectedRowCount == 0)
|
two: selectedRowCountTitleTwo,
|
||||||
text = _translationBundle.selectedRowCountTitleZero;
|
few: selectedRowCountTitleFew,
|
||||||
else if (selectedRowCount == 1)
|
many: selectedRowCountTitleMany,
|
||||||
text = _translationBundle.selectedRowCountTitleOne;
|
other: selectedRowCountTitleOther,
|
||||||
else if (selectedRowCount == 2)
|
locale: _localeName,
|
||||||
text = _translationBundle.selectedRowCountTitleTwo;
|
).replaceFirst(r'$selectedRowCount', formatDecimal(selectedRowCount));
|
||||||
else if (selectedRowCount > 2)
|
|
||||||
text = _translationBundle.selectedRowCountTitleMany;
|
|
||||||
text ??= _translationBundle.selectedRowCountTitleOther;
|
|
||||||
assert(text != null);
|
|
||||||
|
|
||||||
return text.replaceFirst(r'$selectedRowCount', formatDecimal(selectedRowCount));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
/// The format to use for [timeOfDayFormat].
|
||||||
String get cancelButtonLabel => _translationBundle.cancelButtonLabel;
|
@protected
|
||||||
|
TimeOfDayFormat get timeOfDayFormatRaw;
|
||||||
@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 [TimeOfDayFormat] corresponding to one of the following supported
|
/// The [TimeOfDayFormat] corresponding to one of the following supported
|
||||||
/// patterns:
|
/// patterns:
|
||||||
@ -409,44 +383,28 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
|
|||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * http://demo.icu-project.org/icu-bin/locexp?d_=en&_=en_US shows the
|
/// * <http://demo.icu-project.org/icu-bin/locexp?d_=en&_=en_US>, which shows
|
||||||
/// short time pattern used in locale en_US
|
/// the short time pattern used in the `en_US` locale.
|
||||||
@override
|
@override
|
||||||
TimeOfDayFormat timeOfDayFormat({ bool alwaysUse24HourFormat = false }) {
|
TimeOfDayFormat timeOfDayFormat({ bool alwaysUse24HourFormat = false }) {
|
||||||
final String icuShortTimePattern = _translationBundle.timeOfDayFormat;
|
assert(alwaysUse24HourFormat != null);
|
||||||
|
|
||||||
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];
|
|
||||||
|
|
||||||
if (alwaysUse24HourFormat)
|
if (alwaysUse24HourFormat)
|
||||||
return _get24HourVersionOf(icuFormat);
|
return _get24HourVersionOf(timeOfDayFormatRaw);
|
||||||
|
return timeOfDayFormatRaw;
|
||||||
return icuFormat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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].
|
/// Looks up text geometry defined in [MaterialTextGeometry].
|
||||||
@override
|
@override
|
||||||
TextTheme get localTextGeometry => MaterialTextGeometry.forScriptCategory(_translationBundle.scriptCategory);
|
TextTheme get localTextGeometry => MaterialTextGeometry.forScriptCategory(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));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A [LocalizationsDelegate] that uses [GlobalMaterialLocalizations.load]
|
/// A [LocalizationsDelegate] that uses [GlobalMaterialLocalizations.load]
|
||||||
/// to create an instance of this class.
|
/// 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
|
/// A value for [MaterialApp.localizationsDelegates] that's typically used by
|
||||||
/// internationalized apps.
|
/// internationalized apps.
|
||||||
///
|
///
|
||||||
|
/// ## Sample code
|
||||||
|
///
|
||||||
/// To include the localizations provided by this class and by
|
/// To include the localizations provided by this class and by
|
||||||
/// [GlobalWidgetsLocalizations] in a [MaterialApp],
|
/// [GlobalWidgetsLocalizations] in a [MaterialApp],
|
||||||
/// use [GlobalMaterialLocalizations.delegates] as the value of
|
/// 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
|
/// Finds the [TimeOfDayFormat] to use instead of the `original` when the
|
||||||
/// `original` uses 12-hour format and [MediaQueryData.alwaysUse24HourFormat]
|
/// `original` uses 12-hour format and [MediaQueryData.alwaysUse24HourFormat]
|
||||||
/// is true.
|
/// is true.
|
||||||
@ -509,86 +458,91 @@ TimeOfDayFormat _get24HourVersionOf(TimeOfDayFormat original) {
|
|||||||
return TimeOfDayFormat.HH_colon_mm;
|
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> {
|
class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
|
||||||
const _MaterialLocalizationsDelegate();
|
const _MaterialLocalizationsDelegate();
|
||||||
|
|
||||||
// Watch out: this list must match the one in the GlobalMaterialLocalizations
|
@override
|
||||||
// class doc and the list we test, see test/translations_test.dart.
|
bool isSupported(Locale locale) => kSupportedLanguages.contains(locale.languageCode);
|
||||||
static const List<String> _supportedLanguages = <String>[
|
|
||||||
'ar', // Arabic
|
/// Tracks if date i18n data has been loaded.
|
||||||
'bg', // Bulgarian
|
static bool _dateIntlDataInitialized = false;
|
||||||
'bs', // Bosnian
|
|
||||||
'ca', // Catalan
|
/// Loads i18n data for dates if it hasn't be loaded yet.
|
||||||
'cs', // Czech
|
///
|
||||||
'da', // Danish
|
/// Only the first invocation of this function has the effect of loading the
|
||||||
'de', // German
|
/// data. Subsequent invocations have no effect.
|
||||||
'el', // Greek
|
static void _loadDateIntlDataIfNotLoaded() {
|
||||||
'en', // English
|
if (!_dateIntlDataInitialized) {
|
||||||
'es', // Spanish
|
date_localizations.dateSymbols.forEach((String locale, dynamic data) {
|
||||||
'et', // Estonian
|
assert(date_localizations.datePatterns.containsKey(locale));
|
||||||
'fa', // Farsi (Persian)
|
final intl.DateSymbols symbols = new intl.DateSymbols.deserializeFromMap(data);
|
||||||
'fi', // Finnish
|
date_symbol_data_custom.initializeDateFormattingCustom(
|
||||||
'fil', // Fillipino
|
locale: locale,
|
||||||
'fr', // French
|
symbols: symbols,
|
||||||
'gsw', // Swiss German
|
patterns: date_localizations.datePatterns[locale],
|
||||||
'he', // Hebrew
|
);
|
||||||
'hi', // Hindi
|
});
|
||||||
'hr', // Croatian
|
_dateIntlDataInitialized = true;
|
||||||
'hu', // Hungarian
|
}
|
||||||
'id', // Indonesian
|
}
|
||||||
'it', // Italian
|
|
||||||
'ja', // Japanese
|
static final Map<Locale, Future<MaterialLocalizations>> _loadedTranslations = <Locale, Future<MaterialLocalizations>>{};
|
||||||
'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
|
@override
|
||||||
bool isSupported(Locale locale) => _supportedLanguages.contains(locale.languageCode);
|
Future<MaterialLocalizations> load(Locale locale) {
|
||||||
|
assert(isSupported(locale));
|
||||||
|
return _loadedTranslations.putIfAbsent(locale, () {
|
||||||
|
_loadDateIntlDataIfNotLoaded();
|
||||||
|
|
||||||
@override
|
final String localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||||
Future<MaterialLocalizations> load(Locale locale) => GlobalMaterialLocalizations.load(locale);
|
|
||||||
|
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
|
@override
|
||||||
bool shouldReload(_MaterialLocalizationsDelegate old) => false;
|
bool shouldReload(_MaterialLocalizationsDelegate old) => false;
|
||||||
|
@ -10,19 +10,20 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group(GlobalMaterialLocalizations, () {
|
group(GlobalMaterialLocalizations, () {
|
||||||
test('uses exact locale when exists', () {
|
test('uses exact locale when exists', () async {
|
||||||
final GlobalMaterialLocalizations localizations = new GlobalMaterialLocalizations(const Locale('pt', 'PT'));
|
final GlobalMaterialLocalizations localizations = await GlobalMaterialLocalizations.delegate.load(const Locale('pt', 'PT'));
|
||||||
expect(localizations.formatDecimal(10000), '10\u00A0000');
|
expect(localizations.formatDecimal(10000), '10\u00A0000');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('falls back to language code when exact locale is missing', () {
|
test('falls back to language code when exact locale is missing', () async {
|
||||||
final GlobalMaterialLocalizations localizations = new GlobalMaterialLocalizations(const Locale('pt', 'XX'));
|
final GlobalMaterialLocalizations localizations = await GlobalMaterialLocalizations.delegate.load(const Locale('pt', 'XX'));
|
||||||
expect(localizations.formatDecimal(10000), '10.000');
|
expect(localizations.formatDecimal(10000), '10.000');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('falls back to default format when neither language code nor exact locale are available', () {
|
test('fails when neither language code nor exact locale are available', () async {
|
||||||
final GlobalMaterialLocalizations localizations = new GlobalMaterialLocalizations(const Locale('xx', 'XX'));
|
await expectLater(() async {
|
||||||
expect(localizations.formatDecimal(10000), '10,000');
|
await GlobalMaterialLocalizations.delegate.load(const Locale('xx', 'XX'));
|
||||||
|
}, throwsAssertionError);
|
||||||
});
|
});
|
||||||
|
|
||||||
group('formatHour', () {
|
group('formatHour', () {
|
||||||
@ -65,8 +66,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('formatMinute', () {
|
group('formatMinute', () {
|
||||||
test('formats English', () {
|
test('formats English', () async {
|
||||||
final GlobalMaterialLocalizations localizations = new GlobalMaterialLocalizations(const Locale('en', 'US'));
|
final GlobalMaterialLocalizations localizations = await GlobalMaterialLocalizations.delegate.load(const Locale('en', 'US'));
|
||||||
expect(localizations.formatMinute(const TimeOfDay(hour: 1, minute: 32)), '32');
|
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/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:intl/intl.dart' as intl;
|
||||||
|
|
||||||
class FooMaterialLocalizations extends GlobalMaterialLocalizations {
|
class FooMaterialLocalizations extends MaterialLocalizationEn {
|
||||||
FooMaterialLocalizations(Locale locale, this.backButtonTooltip) : super(locale);
|
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
|
@override
|
||||||
final String backButtonTooltip;
|
final String backButtonTooltip;
|
||||||
@ -44,7 +56,7 @@ Widget buildFrame({
|
|||||||
LocaleResolutionCallback localeResolutionCallback,
|
LocaleResolutionCallback localeResolutionCallback,
|
||||||
Iterable<Locale> supportedLocales = const <Locale>[
|
Iterable<Locale> supportedLocales = const <Locale>[
|
||||||
Locale('en', 'US'),
|
Locale('en', 'US'),
|
||||||
Locale('es', 'es'),
|
Locale('es', 'ES'),
|
||||||
],
|
],
|
||||||
}) {
|
}) {
|
||||||
return new MaterialApp(
|
return new MaterialApp(
|
||||||
@ -81,12 +93,12 @@ void main() {
|
|||||||
expect(tester.widget<Text>(find.byKey(textKey)).data, 'Back');
|
expect(tester.widget<Text>(find.byKey(textKey)).data, 'Back');
|
||||||
|
|
||||||
// Unrecognized locale falls back to 'en'
|
// Unrecognized locale falls back to 'en'
|
||||||
await tester.binding.setLocale('foo', 'bar');
|
await tester.binding.setLocale('foo', 'BAR');
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(tester.widget<Text>(find.byKey(textKey)).data, 'Back');
|
expect(tester.widget<Text>(find.byKey(textKey)).data, 'Back');
|
||||||
|
|
||||||
// Spanish Bolivia locale, falls back to just 'es'
|
// Spanish Bolivia locale, falls back to just 'es'
|
||||||
await tester.binding.setLocale('es', 'bo');
|
await tester.binding.setLocale('es', 'BO');
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(tester.widget<Text>(find.byKey(textKey)).data, 'Atrás');
|
expect(tester.widget<Text>(find.byKey(textKey)).data, 'Atrás');
|
||||||
});
|
});
|
||||||
@ -169,7 +181,6 @@ void main() {
|
|||||||
Locale('de', ''),
|
Locale('de', ''),
|
||||||
],
|
],
|
||||||
buildContent: (BuildContext context) {
|
buildContent: (BuildContext context) {
|
||||||
// Should always be 'foo', no matter what the locale is
|
|
||||||
return new Text(
|
return new Text(
|
||||||
MaterialLocalizations.of(context).backButtonTooltip,
|
MaterialLocalizations.of(context).backButtonTooltip,
|
||||||
key: textKey,
|
key: textKey,
|
||||||
|
@ -7,63 +7,13 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// Watch out: this list must be kept in sync with the comment at the top of
|
for (String language in kSupportedLanguages) {
|
||||||
// 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) {
|
|
||||||
testWidgets('translations exist for $language', (WidgetTester tester) async {
|
testWidgets('translations exist for $language', (WidgetTester tester) async {
|
||||||
final Locale locale = new Locale(language, '');
|
final Locale locale = new Locale(language, '');
|
||||||
|
|
||||||
expect(GlobalMaterialLocalizations.delegate.isSupported(locale), isTrue);
|
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.openAppDrawerTooltip, isNotNull);
|
||||||
expect(localizations.backButtonTooltip, isNotNull);
|
expect(localizations.backButtonTooltip, isNotNull);
|
||||||
@ -119,22 +69,43 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testWidgets('spot check selectedRowCount translations', (WidgetTester tester) async {
|
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(0), 'No items selected');
|
||||||
expect(localizations.selectedRowCountTitle(1), '1 item selected');
|
expect(localizations.selectedRowCountTitle(1), '1 item selected');
|
||||||
expect(localizations.selectedRowCountTitle(2), '2 items 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');
|
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(0), 'No se han seleccionado elementos');
|
||||||
expect(localizations.selectedRowCountTitle(1), '1 elemento seleccionado');
|
expect(localizations.selectedRowCountTitle(1), '1 elemento seleccionado');
|
||||||
expect(localizations.selectedRowCountTitle(2), '2 elementos seleccionados');
|
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');
|
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(0), 'Nu există elemente selectate');
|
||||||
expect(localizations.selectedRowCountTitle(1), 'Un articol selectat');
|
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');
|
expect(localizations.selectedRowCountTitle(123456789), '123.456.789 de articole selectate');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user