Unverified Commit 75960f35 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

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