Unverified Commit 5a6c140d authored by xster's avatar xster Committed by GitHub

Cupertino localization step 7: modularize material specific things out of...

Cupertino localization step 7: modularize material specific things out of gen_localizations.dart (#29822)
parent 421f16a6
...@@ -39,85 +39,38 @@ ...@@ -39,85 +39,38 @@
// ``` // ```
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'gen_material_localizations.dart';
import 'localizations_utils.dart'; import 'localizations_utils.dart';
import 'localizations_validator.dart'; import 'localizations_validator.dart';
const String outputHeader = ''' /// This is the core of this script; it generates the code used for translations.
// Copyright 2017 The Chromium Authors. All rights reserved. String generateArbBasedLocalizationSubclasses({
// Use of this source code is governed by a BSD-style license that can be @required Map<LocaleInfo, Map<String, String>> localeToResources,
// found in the LICENSE file. @required Map<LocaleInfo, Map<String, dynamic>> localeToResourceAttributes,
@required String generatedClassPrefix,
// This file has been automatically generated. Please do not edit it manually. @required String baseClass,
// To regenerate the file, use: @required HeaderGenerator generateHeader,
// @(regenerate) @required ConstructorGenerator generateConstructor,
@required String factoryDeclaration,
import 'dart:collection'; @required String factoryArguments,
}) {
import 'package:flutter/foundation.dart'; assert(localeToResources != null);
import 'package:flutter/material.dart'; assert(localeToResourceAttributes != null);
import 'package:intl/intl.dart' as intl; assert(generatedClassPrefix.isNotEmpty);
assert(baseClass.isNotEmpty);
import '../material_localizations.dart'; assert(generateHeader != null);
'''; assert(generateConstructor != null);
assert(factoryDeclaration.isNotEmpty);
/// Maps locales to resource key/value pairs. assert(factoryArguments.isNotEmpty);
final Map<LocaleInfo, Map<String, String>> localeToResources = <LocaleInfo, Map<String, String>>{};
/// Maps locales to resource key/attributes pairs.
///
/// See also: <https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes>
final Map<LocaleInfo, Map<String, dynamic>> localeToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{};
/// Set that holds the locales that were assumed from the existing locales.
///
/// For example, when the data lacks data for zh_Hant, we will use the data of
/// the first Hant Chinese locale as a default by repeating the data. If an
/// explicit match is later found, we can reference this set to see if we should
/// overwrite the existing assumed data.
final Set<LocaleInfo> assumedLocales = <LocaleInfo>{};
/// 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'";
final StringBuffer output = StringBuffer(); final StringBuffer output = StringBuffer();
bool started = false; // Have we started writing a raw string. output.writeln(generateHeader('dart dev/tools/localization/gen_localizations.dart --overwrite'));
for (int i = 0; i < s.length; i++) {
if (s[i] == "'") {
if (started)
output.write("'");
output.write(' "\'" ');
started = false;
} else if (!started) {
output.write("r'${s[i]}");
started = true;
} else {
output.write(s[i]);
}
}
if (started)
output.write("'");
return output.toString();
}
/// This is the core of this script; it generates the code used for translations.
String generateTranslationBundles() {
final StringBuffer output = StringBuffer();
final StringBuffer supportedLocales = StringBuffer(); final StringBuffer supportedLocales = StringBuffer();
final Map<String, List<LocaleInfo>> languageToLocales = <String, List<LocaleInfo>>{}; final Map<String, List<LocaleInfo>> languageToLocales = <String, List<LocaleInfo>>{};
...@@ -140,17 +93,9 @@ String generateTranslationBundles() { ...@@ -140,17 +93,9 @@ String generateTranslationBundles() {
allResourceIdentifiers.addAll(localeToResources[locale].keys); allResourceIdentifiers.addAll(localeToResources[locale].keys);
} }
output.writeln('''
// The classes defined here encode all of the translations found in the
// `flutter_localizations/lib/src/l10n/*.arb` files.
//
// These classes are constructed by the [getMaterialTranslation] 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. // We generate one class per supported language (e.g.
// `MaterialLocalizationEn`). These implement everything that is needed by // `MaterialLocalizationEn`). These implement everything that is needed by the
// GlobalMaterialLocalizations. // superclass (e.g. GlobalMaterialLocalizations).
// We also generate one subclass for each locale with a script code (e.g. // We also generate one subclass for each locale with a script code (e.g.
// `MaterialLocalizationZhHant`). Their superclasses are the aforementioned // `MaterialLocalizationZhHant`). Their superclasses are the aforementioned
...@@ -177,7 +122,9 @@ String generateTranslationBundles() { ...@@ -177,7 +122,9 @@ String generateTranslationBundles() {
final LocaleInfo canonicalLocale = LocaleInfo.fromString('en'); final LocaleInfo canonicalLocale = LocaleInfo.fromString('en');
for (String languageName in languageCodes) { for (String languageName in languageCodes) {
final LocaleInfo languageLocale = LocaleInfo.fromString(languageName); final LocaleInfo languageLocale = LocaleInfo.fromString(languageName);
writeClassHeader(output, languageLocale, 'GlobalMaterialLocalizations'); output.writeln(generateClassDeclaration(languageLocale, generatedClassPrefix, baseClass));
output.writeln(generateConstructor(languageLocale));
final Map<String, String> languageResources = localeToResources[languageLocale]; final Map<String, String> languageResources = localeToResources[languageLocale];
for (String key in allKeys) { for (String key in allKeys) {
final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key]; final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key];
...@@ -192,7 +139,12 @@ String generateTranslationBundles() { ...@@ -192,7 +139,12 @@ String generateTranslationBundles() {
// script default values before language default values. // script default values before language default values.
for (String scriptCode in languageToScriptCodes[languageName]) { for (String scriptCode in languageToScriptCodes[languageName]) {
final LocaleInfo scriptBaseLocale = LocaleInfo.fromString(languageName + '_' + scriptCode); final LocaleInfo scriptBaseLocale = LocaleInfo.fromString(languageName + '_' + scriptCode);
writeClassHeader(output, scriptBaseLocale, 'MaterialLocalization${camelCase(languageLocale)}'); output.writeln(generateClassDeclaration(
scriptBaseLocale,
generatedClassPrefix,
'$generatedClassPrefix${camelCase(languageLocale)}',
));
output.writeln(generateConstructor(scriptBaseLocale));
final Map<String, String> scriptResources = localeToResources[scriptBaseLocale]; final Map<String, String> scriptResources = localeToResources[scriptBaseLocale];
for (String key in scriptResources.keys) { for (String key in scriptResources.keys) {
if (languageResources[key] == scriptResources[key]) if (languageResources[key] == scriptResources[key])
...@@ -211,7 +163,12 @@ String generateTranslationBundles() { ...@@ -211,7 +163,12 @@ String generateTranslationBundles() {
if (locale.scriptCode != scriptCode) if (locale.scriptCode != scriptCode)
continue; continue;
countryCodeCount += 1; countryCodeCount += 1;
writeClassHeader(output, locale, 'MaterialLocalization${camelCase(scriptBaseLocale)}'); output.writeln(generateClassDeclaration(
locale,
generatedClassPrefix,
'$generatedClassPrefix${camelCase(scriptBaseLocale)}',
));
output.writeln(generateConstructor(locale));
final Map<String, String> localeResources = localeToResources[locale]; final Map<String, String> localeResources = localeToResources[locale];
for (String key in localeResources.keys) { for (String key in localeResources.keys) {
// When script fallback contains the key, we compare to it instead of language fallback. // When script fallback contains the key, we compare to it instead of language fallback.
...@@ -232,7 +189,12 @@ String generateTranslationBundles() { ...@@ -232,7 +189,12 @@ String generateTranslationBundles() {
continue; continue;
countryCodeCount += 1; countryCodeCount += 1;
final Map<String, String> localeResources = localeToResources[locale]; final Map<String, String> localeResources = localeToResources[locale];
writeClassHeader(output, locale, 'MaterialLocalization${camelCase(languageLocale)}'); output.writeln(generateClassDeclaration(
locale,
generatedClassPrefix,
'$generatedClassPrefix${camelCase(languageLocale)}',
));
output.writeln(generateConstructor(locale));
for (String key in localeResources.keys) { for (String key in localeResources.keys) {
if (languageResources[key] == localeResources[key]) if (languageResources[key] == localeResources[key])
continue; continue;
...@@ -256,13 +218,13 @@ String generateTranslationBundles() { ...@@ -256,13 +218,13 @@ String generateTranslationBundles() {
} }
} }
// Generate the getMaterialTranslation function. Given a Locale it returns the // Generate the factory function. Given a Locale it returns the corresponding
// corresponding const GlobalMaterialLocalizations. // base class implementation.
output.writeln(''' output.writeln('''
/// The set of supported languages, as language code strings. /// The set of supported languages, as language code strings.
/// ///
/// The [GlobalMaterialLocalizations.delegate] can generate localizations for /// The [$baseClass.delegate] can generate localizations for
/// any [Locale] with a language code from this set, regardless of the region. /// 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, /// 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 /// but there is support for `de-CH` specifically to override some of the
...@@ -275,10 +237,10 @@ final Set<String> kSupportedLanguages = HashSet<String>.from(const <String>[ ...@@ -275,10 +237,10 @@ final Set<String> kSupportedLanguages = HashSet<String>.from(const <String>[
${languageCodes.map<String>((String value) => " '$value', // ${describeLocale(value)}").join('\n')} ${languageCodes.map<String>((String value) => " '$value', // ${describeLocale(value)}").join('\n')}
]); ]);
/// Creates a [GlobalMaterialLocalizations] instance for the given `locale`. /// Creates a [$baseClass] instance for the given `locale`.
/// ///
/// All of the function's arguments except `locale` will be passed to the [new /// All of the function's arguments except `locale` will be passed to the [
/// GlobalMaterialLocalizations] constructor. (The `localeName` argument of that /// $baseClass] constructor. (The `localeName` argument of that
/// constructor is specified by the actual subclass constructor by this /// constructor is specified by the actual subclass constructor by this
/// function.) /// function.)
/// ///
...@@ -288,24 +250,15 @@ ${languageCodes.map<String>((String value) => " '$value', // ${describeLocale(v ...@@ -288,24 +250,15 @@ ${languageCodes.map<String>((String value) => " '$value', // ${describeLocale(v
$supportedLocales/// {@endtemplate} $supportedLocales/// {@endtemplate}
/// ///
/// Generally speaking, this method is only intended to be used by /// Generally speaking, this method is only intended to be used by
/// [GlobalMaterialLocalizations.delegate]. /// [$baseClass.delegate].
GlobalMaterialLocalizations getMaterialTranslation( $factoryDeclaration
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) {
// Only one instance of the language. // Only one instance of the language.
if (languageToLocales[language].length == 1) { if (languageToLocales[language].length == 1) {
output.writeln(''' output.writeln('''
case '$language': case '$language':
return MaterialLocalization${camelCase(languageToLocales[language][0])}($arguments);'''); return $generatedClassPrefix${camelCase(languageToLocales[language][0])}($factoryArguments);''');
} else if (!languageToScriptCodes.containsKey(language)) { // Does not distinguish between scripts. Switch on countryCode directly. } else if (!languageToScriptCodes.containsKey(language)) { // Does not distinguish between scripts. Switch on countryCode directly.
output.writeln(''' output.writeln('''
case '$language': { case '$language': {
...@@ -317,11 +270,11 @@ GlobalMaterialLocalizations getMaterialTranslation( ...@@ -317,11 +270,11 @@ GlobalMaterialLocalizations getMaterialTranslation(
final String countryCode = locale.countryCode; final String countryCode = locale.countryCode;
output.writeln(''' output.writeln('''
case '$countryCode': case '$countryCode':
return MaterialLocalization${camelCase(locale)}($arguments);'''); return $generatedClassPrefix${camelCase(locale)}($factoryArguments);''');
} }
output.writeln(''' output.writeln('''
} }
return MaterialLocalization${camelCase(LocaleInfo.fromString(language))}($arguments); return $generatedClassPrefix${camelCase(LocaleInfo.fromString(language))}($factoryArguments);
}'''); }''');
} else { // Language has scriptCode, add additional switch logic. } else { // Language has scriptCode, add additional switch logic.
bool hasCountryCode = false; bool hasCountryCode = false;
...@@ -347,7 +300,7 @@ GlobalMaterialLocalizations getMaterialTranslation( ...@@ -347,7 +300,7 @@ GlobalMaterialLocalizations getMaterialTranslation(
final String countryCode = locale.countryCode; final String countryCode = locale.countryCode;
output.writeln(''' output.writeln('''
case '$countryCode': case '$countryCode':
return MaterialLocalization${camelCase(locale)}($arguments);'''); return $generatedClassPrefix${camelCase(locale)}($factoryArguments);''');
} }
} }
// Return a fallback locale that matches scriptCode, but not countryCode. // Return a fallback locale that matches scriptCode, but not countryCode.
...@@ -359,7 +312,7 @@ GlobalMaterialLocalizations getMaterialTranslation( ...@@ -359,7 +312,7 @@ GlobalMaterialLocalizations getMaterialTranslation(
}'''); }''');
} }
output.writeln(''' output.writeln('''
return MaterialLocalization${camelCase(scriptLocale)}($arguments); return $generatedClassPrefix${camelCase(scriptLocale)}($factoryArguments);
}'''); }''');
} else { } else {
// Not Explicitly defined, fallback to first locale with the same language and // Not Explicitly defined, fallback to first locale with the same language and
...@@ -372,7 +325,7 @@ GlobalMaterialLocalizations getMaterialTranslation( ...@@ -372,7 +325,7 @@ GlobalMaterialLocalizations getMaterialTranslation(
}'''); }''');
} }
output.writeln(''' output.writeln('''
return MaterialLocalization${camelCase(scriptLocale)}($arguments); return $generatedClassPrefix${camelCase(scriptLocale)}($factoryArguments);
}'''); }''');
break; break;
} }
...@@ -392,13 +345,13 @@ GlobalMaterialLocalizations getMaterialTranslation( ...@@ -392,13 +345,13 @@ GlobalMaterialLocalizations getMaterialTranslation(
final String countryCode = locale.countryCode; final String countryCode = locale.countryCode;
output.writeln(''' output.writeln('''
case '$countryCode': case '$countryCode':
return MaterialLocalization${camelCase(locale)}($arguments);'''); return $generatedClassPrefix${camelCase(locale)}($factoryArguments);''');
} }
output.writeln(''' output.writeln('''
}'''); }''');
} }
output.writeln(''' output.writeln('''
return MaterialLocalization${camelCase(LocaleInfo.fromString(language))}($arguments); return $generatedClassPrefix${camelCase(LocaleInfo.fromString(language))}($factoryArguments);
}'''); }''');
} }
} }
...@@ -411,17 +364,6 @@ GlobalMaterialLocalizations getMaterialTranslation( ...@@ -411,17 +364,6 @@ GlobalMaterialLocalizations getMaterialTranslation(
return output.toString(); return output.toString();
} }
/// Writes the header of each class which corresponds to a locale.
void writeClassHeader(StringBuffer output, LocaleInfo locale, String superClass) {
final String camelCaseName = camelCase(locale);
final String className = 'MaterialLocalization$camelCaseName';
final String constructor = generateConstructor(className, locale);
output.writeln('');
output.writeln('/// The translations for ${describeLocale(locale.originalString)} (`${locale.originalString}`).');
output.writeln('class $className extends $superClass {');
output.writeln(constructor);
}
/// Returns the appropriate type for getters with the given attributes. /// Returns the appropriate type for getters with the given attributes.
/// ///
/// Typically "String", but some (e.g. "timeOfDayFormat") return enums. /// Typically "String", but some (e.g. "timeOfDayFormat") return enums.
...@@ -486,6 +428,7 @@ const Map<String, String> _scriptCategoryToEnum = <String, String>{ ...@@ -486,6 +428,7 @@ const Map<String, String> _scriptCategoryToEnum = <String, String>{
String generateValue(String value, Map<String, dynamic> attributes) { String generateValue(String value, Map<String, dynamic> attributes) {
if (value == null) if (value == null)
return null; return null;
// cupertino_en.arb doesn't use x-flutter-type.
if (attributes != null) { if (attributes != null) {
switch (attributes['x-flutter-type']) { switch (attributes['x-flutter-type']) {
case 'icuShortTimePattern': case 'icuShortTimePattern':
...@@ -523,77 +466,6 @@ String generateGetter(String key, String value, Map<String, dynamic> attributes) ...@@ -523,77 +466,6 @@ String generateGetter(String key, String value, Map<String, dynamic> attributes)
$type get $key => $value;'''; $type get $key => $value;''';
} }
/// Returns the source of the constructor for a GlobalMaterialLocalizations
/// subclass.
String generateConstructor(String className, LocaleInfo locale) {
final String localeName = locale.originalString;
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 localeString }) {
assert(localeString != null);
// Helper method to fill the maps with the correct data from file.
void populateResources(LocaleInfo locale) {
final Map<String, String> resources = localeToResources[locale];
final Map<String, dynamic> attributes = localeToResourceAttributes[locale];
final Map<String, dynamic> bundle = json.decode(file.readAsStringSync());
for (String key in bundle.keys) {
// The ARB file resource "attributes" for foo are called @foo.
if (key.startsWith('@'))
attributes[key.substring(1)] = bundle[key];
else
resources[key] = bundle[key];
}
}
// Only pre-assume scriptCode if there is a country or script code to assume off of.
// When we assume scriptCode based on languageCode-only, we want this initial pass
// to use the un-assumed version as a base class.
LocaleInfo locale = LocaleInfo.fromString(localeString, deriveScriptCode: localeString.split('_').length > 1);
// Allow overwrite if the existing data is assumed.
if (assumedLocales.contains(locale)) {
localeToResources[locale] = <String, String>{};
localeToResourceAttributes[locale] = <String, dynamic>{};
assumedLocales.remove(locale);
} else {
localeToResources[locale] ??= <String, String>{};
localeToResourceAttributes[locale] ??= <String, dynamic>{};
}
populateResources(locale);
// Add an assumed locale to default to when there is no info on scriptOnly locales.
locale = LocaleInfo.fromString(localeString, deriveScriptCode: true);
if (locale.scriptCode != null) {
final LocaleInfo scriptLocale = LocaleInfo.fromString(locale.languageCode + '_' + locale.scriptCode);
if (!localeToResources.containsKey(scriptLocale)) {
assumedLocales.add(scriptLocale);
localeToResources[scriptLocale] ??= <String, String>{};
localeToResourceAttributes[scriptLocale] ??= <String, dynamic>{};
populateResources(scriptLocale);
}
}
}
Future<void> main(List<String> rawArgs) async { Future<void> main(List<String> rawArgs) async {
checkCwdIsRepoRoot('gen_localizations'); checkCwdIsRepoRoot('gen_localizations');
final GeneratorOptions options = parseArgs(rawArgs); final GeneratorOptions options = parseArgs(rawArgs);
...@@ -604,36 +476,63 @@ Future<void> main(List<String> rawArgs) async { ...@@ -604,36 +476,63 @@ Future<void> main(List<String> rawArgs) async {
final Directory directory = Directory(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n')); final Directory directory = Directory(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'));
final RegExp materialFilenameRE = RegExp(r'material_(\w+)\.arb$'); final RegExp materialFilenameRE = RegExp(r'material_(\w+)\.arb$');
final RegExp cupertinoFilenameRE = RegExp(r'cupertino_(\w+)\.arb$');
try { try {
validateEnglishLocalizations(File(path.join(directory.path, 'material_en.arb'))); validateEnglishLocalizations(File(path.join(directory.path, 'material_en.arb')));
validateEnglishLocalizations(File(path.join(directory.path, 'cupertino_en.arb')));
} on ValidationError catch (exception) { } on ValidationError catch (exception) {
exitWithError('$exception'); exitWithError('$exception');
} }
await precacheLanguageAndRegionTags(); await precacheLanguageAndRegionTags();
for (FileSystemEntity entity in directory.listSync()) { // Maps of locales to resource key/value pairs for Material ARBs.
final String entityPath = entity.path; final Map<LocaleInfo, Map<String, String>> materialLocaleToResources = <LocaleInfo, Map<String, String>>{};
if (FileSystemEntity.isFileSync(entityPath) && materialFilenameRE.hasMatch(entityPath)) { // Maps of locales to resource key/attributes pairs for Material ARBs..
processBundle(File(entityPath), localeString: materialFilenameRE.firstMatch(entityPath)[1]); // https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
} final Map<LocaleInfo, Map<String, dynamic>> materialLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{};
} // Maps of locales to resource key/value pairs for Cupertino ARBs.
final Map<LocaleInfo, Map<String, String>> cupertinoLocaleToResources = <LocaleInfo, Map<String, String>>{};
// Maps of locales to resource key/attributes pairs for Cupertino ARBs..
// https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
final Map<LocaleInfo, Map<String, dynamic>> cupertinoLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{};
loadMatchingArbsIntoBundleMaps(
directory: directory,
filenamePattern: materialFilenameRE,
localeToResources: materialLocaleToResources,
localeToResourceAttributes: materialLocaleToResourceAttributes,
);
loadMatchingArbsIntoBundleMaps(
directory: directory,
filenamePattern: cupertinoFilenameRE,
localeToResources: cupertinoLocaleToResources,
localeToResourceAttributes: cupertinoLocaleToResourceAttributes,
);
try { try {
validateLocalizations(localeToResources, localeToResourceAttributes); validateLocalizations(materialLocaleToResources, materialLocaleToResourceAttributes);
validateLocalizations(cupertinoLocaleToResources, cupertinoLocaleToResourceAttributes);
} on ValidationError catch (exception) { } on ValidationError catch (exception) {
exitWithError('$exception'); exitWithError('$exception');
} }
final StringBuffer buffer = StringBuffer(); final String materialLocalizations = generateArbBasedLocalizationSubclasses(
buffer.writeln(outputHeader.replaceFirst('@(regenerate)', 'dart dev/tools/localization/gen_localizations.dart --overwrite')); localeToResources: materialLocaleToResources,
buffer.write(generateTranslationBundles()); localeToResourceAttributes: materialLocaleToResourceAttributes,
generatedClassPrefix: 'MaterialLocalization',
baseClass: 'GlobalMaterialLocalizations',
generateHeader: generateMaterialHeader,
generateConstructor: generateMaterialConstructor,
factoryDeclaration: materialFactoryDeclaration,
factoryArguments: materialFactoryArguments,
);
if (options.writeToFile) { if (options.writeToFile) {
final File localizationsFile = File(path.join(directory.path, 'generated_material_localizations.dart')); final File localizationsFile = File(path.join(directory.path, 'generated_material_localizations.dart'));
localizationsFile.writeAsStringSync(buffer.toString(), flush: true); localizationsFile.writeAsStringSync(materialLocalizations, flush: true);
} else { } else {
stdout.write(buffer.toString()); stdout.write(materialLocalizations);
} }
} }
import 'localizations_utils.dart';
HeaderGenerator generateMaterialHeader = (String regenerateInstructions) {
return '''
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This file has been automatically generated. Please do not edit it manually.
// To regenerate the file, use:
// $regenerateInstructions
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart' as intl;
import '../material_localizations.dart';
// The classes defined here encode all of the translations found in the
// `flutter_localizations/lib/src/l10n/*.arb` files.
//
// These classes are constructed by the [getMaterialTranslation] method at the
// bottom of this file, and used by the [_MaterialLocalizationsDelegate.load]
// method defined in `flutter_localizations/lib/src/material_localizations.dart`.''';
};
/// Returns the source of the constructor for a GlobalMaterialLocalizations
/// subclass.
ConstructorGenerator generateMaterialConstructor = (LocaleInfo locale) {
final String localeName = locale.originalString;
return '''
/// Create an instance of the translation bundle for ${describeLocale(localeName)}.
///
/// For details on the meaning of the arguments, see [GlobalMaterialLocalizations].
const MaterialLocalization${camelCase(locale)}({
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,
);''';
};
const String materialFactoryDeclaration = '''
GlobalMaterialLocalizations getMaterialTranslation(
Locale locale,
intl.DateFormat fullYearFormat,
intl.DateFormat mediumDateFormat,
intl.DateFormat longDateFormat,
intl.DateFormat yearMonthFormat,
intl.NumberFormat decimalFormat,
intl.NumberFormat twoDigitZeroPaddedFormat,
) {''';
const String materialFactoryArguments =
'fullYearFormat: fullYearFormat, mediumDateFormat: mediumDateFormat, longDateFormat: longDateFormat, yearMonthFormat: yearMonthFormat, decimalFormat: decimalFormat, twoDigitZeroPaddedFormat: twoDigitZeroPaddedFormat';
...@@ -9,6 +9,9 @@ import 'dart:io'; ...@@ -9,6 +9,9 @@ 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';
typedef HeaderGenerator = String Function(String regenerateInstructions);
typedef ConstructorGenerator = String Function(LocaleInfo locale);
/// Simple data class to hold parsed locale. Does not promise validity of any data. /// Simple data class to hold parsed locale. Does not promise validity of any data.
class LocaleInfo implements Comparable<LocaleInfo> { class LocaleInfo implements Comparable<LocaleInfo> {
LocaleInfo({ LocaleInfo({
...@@ -129,6 +132,75 @@ class LocaleInfo implements Comparable<LocaleInfo> { ...@@ -129,6 +132,75 @@ class LocaleInfo implements Comparable<LocaleInfo> {
} }
} }
/// Parse the data for a locale from a file, and store it in the [attributes]
/// and [resources] keys.
void loadMatchingArbsIntoBundleMaps({
@required Directory directory,
@required RegExp filenamePattern,
@required Map<LocaleInfo, Map<String, String>> localeToResources,
@required Map<LocaleInfo, Map<String, dynamic>> localeToResourceAttributes,
}) {
assert(directory != null);
assert(filenamePattern != null);
assert(localeToResources != null);
assert(localeToResourceAttributes != null);
/// Set that holds the locales that were assumed from the existing locales.
///
/// For example, when the data lacks data for zh_Hant, we will use the data of
/// the first Hant Chinese locale as a default by repeating the data. If an
/// explicit match is later found, we can reference this set to see if we should
/// overwrite the existing assumed data.
final Set<LocaleInfo> assumedLocales = <LocaleInfo>{};
for (FileSystemEntity entity in directory.listSync()) {
final String entityPath = entity.path;
if (FileSystemEntity.isFileSync(entityPath) && filenamePattern.hasMatch(entityPath)) {
final String localeString = filenamePattern.firstMatch(entityPath)[1];
final File arbFile = File(entityPath);
// Helper method to fill the maps with the correct data from file.
void populateResources(LocaleInfo locale, File file) {
final Map<String, String> resources = localeToResources[locale];
final Map<String, dynamic> attributes = localeToResourceAttributes[locale];
final Map<String, dynamic> bundle = json.decode(file.readAsStringSync());
for (String key in bundle.keys) {
// The ARB file resource "attributes" for foo are called @foo.
if (key.startsWith('@'))
attributes[key.substring(1)] = bundle[key];
else
resources[key] = bundle[key];
}
}
// Only pre-assume scriptCode if there is a country or script code to assume off of.
// When we assume scriptCode based on languageCode-only, we want this initial pass
// to use the un-assumed version as a base class.
LocaleInfo locale = LocaleInfo.fromString(localeString, deriveScriptCode: localeString.split('_').length > 1);
// Allow overwrite if the existing data is assumed.
if (assumedLocales.contains(locale)) {
localeToResources[locale] = <String, String>{};
localeToResourceAttributes[locale] = <String, dynamic>{};
assumedLocales.remove(locale);
} else {
localeToResources[locale] ??= <String, String>{};
localeToResourceAttributes[locale] ??= <String, dynamic>{};
}
populateResources(locale, arbFile);
// Add an assumed locale to default to when there is no info on scriptOnly locales.
locale = LocaleInfo.fromString(localeString, deriveScriptCode: true);
if (locale.scriptCode != null) {
final LocaleInfo scriptLocale = LocaleInfo.fromString(locale.languageCode + '_' + locale.scriptCode);
if (!localeToResources.containsKey(scriptLocale)) {
assumedLocales.add(scriptLocale);
localeToResources[scriptLocale] ??= <String, String>{};
localeToResourceAttributes[scriptLocale] ??= <String, dynamic>{};
populateResources(scriptLocale, arbFile);
}
}
}
}
}
void exitWithError(String errorMessage) { void exitWithError(String errorMessage) {
assert(errorMessage != null); assert(errorMessage != null);
stderr.writeln('fatal: $errorMessage'); stderr.writeln('fatal: $errorMessage');
...@@ -267,3 +339,49 @@ String describeLocale(String tag) { ...@@ -267,3 +339,49 @@ String describeLocale(String tag) {
output += ', using the $script script'; output += ', using the $script script';
return output; return output;
} }
/// Writes the header of each class which corresponds to a locale.
String generateClassDeclaration(
LocaleInfo locale,
String classNamePrefix,
String superClass,
) {
final String camelCaseName = camelCase(locale);
return '''
/// The translations for ${describeLocale(locale.originalString)} (`${locale.originalString}`).
class $classNamePrefix$camelCaseName extends $superClass {''';
}
/// 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'";
final StringBuffer output = StringBuffer();
bool started = false; // Have we started writing a raw string.
for (int i = 0; i < s.length; i++) {
if (s[i] == "'") {
if (started)
output.write("'");
output.write(' "\'" ');
started = false;
} else if (!started) {
output.write("r'${s[i]}");
started = true;
} else {
output.write(s[i]);
}
}
if (started)
output.write("'");
return output.toString();
}
...@@ -13202,7 +13202,7 @@ final Set<String> kSupportedLanguages = HashSet<String>.from(const <String>[ ...@@ -13202,7 +13202,7 @@ final Set<String> kSupportedLanguages = HashSet<String>.from(const <String>[
/// Creates a [GlobalMaterialLocalizations] instance for the given `locale`. /// Creates a [GlobalMaterialLocalizations] instance for the given `locale`.
/// ///
/// All of the function's arguments except `locale` will be passed to the [new /// All of the function's arguments except `locale` will be passed to the [
/// GlobalMaterialLocalizations] constructor. (The `localeName` argument of that /// GlobalMaterialLocalizations] constructor. (The `localeName` argument of that
/// constructor is specified by the actual subclass constructor by this /// constructor is specified by the actual subclass constructor by this
/// function.) /// function.)
......
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