Unverified Commit 351457cd authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by GitHub

[gen_l10n] Separate out AppLocalizations classes and subclasses by language code (#52335)

parent 3fd6808b
......@@ -10,6 +10,9 @@ import 'package:flutter_localizations/flutter_localizations.dart';
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'stock_strings_en.dart';
import 'stock_strings_es.dart';
// ignore_for_file: unnecessary_brace_in_string_interps
/// Callers can lookup localized strings with an instance of StockStrings returned
......@@ -65,10 +68,10 @@ import 'package:intl/intl.dart' as intl;
/// be consistent with the languages listed in the StockStrings.supportedLocales
/// property.
abstract class StockStrings {
StockStrings(String locale) : assert(locale != null), _localeName = intl.Intl.canonicalizedLocale(locale.toString());
StockStrings(String locale) : assert(locale != null), localeName = intl.Intl.canonicalizedLocale(locale.toString());
// ignore: unused_field
final String _localeName;
final String localeName;
static StockStrings of(BuildContext context) {
return Localizations.of<StockStrings>(context, StockStrings);
......@@ -125,48 +128,6 @@ class _StockStringsDelegate extends LocalizationsDelegate<StockStrings> {
bool shouldReload(_StockStringsDelegate old) => false;
}
/// The translations for English (`en`).
class StockStringsEn extends StockStrings {
StockStringsEn([String locale = 'en']) : super(locale);
@override
String get title => 'Stocks';
@override
String get market => 'MARKET';
@override
String get portfolio => 'PORTFOLIO';
}
/// The translations for English, as used in the United States (`en_US`).
class StockStringsEnUs extends StockStringsEn {
StockStringsEnUs([String locale = 'en_US']) : super(locale);
@override
String get title => 'Stocks';
@override
String get market => 'MARKET';
@override
String get portfolio => 'PORTFOLIO';
}
/// The translations for Spanish Castilian (`es`).
class StockStringsEs extends StockStrings {
StockStringsEs([String locale = 'es']) : super(locale);
@override
String get title => 'Acciones';
@override
String get market => 'MERCADO';
@override
String get portfolio => 'CARTERA';
}
StockStrings _lookupStockStrings(Locale locale) {
switch(locale.languageCode) {
case 'en': {
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'stock_strings.dart';
// ignore_for_file: unnecessary_brace_in_string_interps
/// The translations for English (`en`).
class StockStringsEn extends StockStrings {
StockStringsEn([String locale = 'en']) : super(locale);
@override
String get title => 'Stocks';
@override
String get market => 'MARKET';
@override
String get portfolio => 'PORTFOLIO';
}
/// The translations for English, as used in the United States (`en_US`).
class StockStringsEnUs extends StockStringsEn {
StockStringsEnUs(): super('en_US');
@override
String get title => 'Stocks';
@override
String get market => 'MARKET';
@override
String get portfolio => 'PORTFOLIO';
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'stock_strings.dart';
// ignore_for_file: unnecessary_brace_in_string_interps
/// The translations for Spanish Castilian (`es`).
class StockStringsEs extends StockStrings {
StockStringsEs([String locale = 'es']) : super(locale);
@override
String get title => 'Acciones';
@override
String get market => 'MERCADO';
@override
String get portfolio => 'CARTERA';
}
......@@ -164,11 +164,7 @@ String generateMethod(Message message, AppResourceBundle bundle) {
}
}
// Escape single and double quotes.
messageValue = messageValue.replaceAll("'", '\\\'');
messageValue = messageValue.replaceAll('"', '\\\"');
return "'$messageValue'";
return generateString(messageValue, escapeDollar: false);
}
if (message.isPlural) {
......@@ -197,21 +193,43 @@ String generateMethod(Message message, AppResourceBundle bundle) {
.replaceAll('@(message)', generateMessage());
}
String generateClass(String className, AppResourceBundle bundle, Iterable<Message> messages) {
String generateBaseClassFile(
String className,
String fileName,
String header,
AppResourceBundle bundle,
Iterable<Message> messages,
) {
final LocaleInfo locale = bundle.locale;
final Iterable<String> methods = messages
.where((Message message) => bundle.translationFor(message) != null)
.map((Message message) => generateMethod(message, bundle));
String baseClassName = className;
if (locale.countryCode != null) {
baseClassName = '$className${LocaleInfo.fromString(locale.languageCode).camelCase()}';
}
return classFileTemplate
.replaceAll('@(header)', header)
.replaceAll('@(language)', describeLocale(locale.toString()))
.replaceAll('@(baseClass)', className)
.replaceAll('@(fileName)', fileName)
.replaceAll('@(class)', '$className${locale.camelCase()}')
.replaceAll('@(localeName)', locale.toString())
.replaceAll('@(methods)', methods.join('\n\n'));
}
String generateSubclass({
String className,
AppResourceBundle bundle,
Iterable<Message> messages,
}) {
final LocaleInfo locale = bundle.locale;
final String baseClassName = '$className${LocaleInfo.fromString(locale.languageCode).camelCase()}';
final Iterable<String> methods = messages
.where((Message message) => bundle.translationFor(message) != null)
.map((Message message) => generateMethod(message, bundle));
return classTemplate
return subclassTemplate
.replaceAll('@(language)', describeLocale(locale.toString()))
.replaceAll('@(baseClass)', baseClassName)
.replaceAll('@(baseLanguageClassName)', baseClassName)
.replaceAll('@(class)', '$className${locale.camelCase()}')
.replaceAll('@(localeName)', locale.toString())
.replaceAll('@(methods)', methods.join('\n\n'));
......@@ -525,8 +543,25 @@ class LocalizationsGenerator {
supportedLocales.addAll(allLocales);
}
// Generate the AppLocalizations class, its LocalizationsDelegate subclass.
// Generate the AppLocalizations class, its LocalizationsDelegate subclass,
// and all AppLocalizations subclasses for every locale.
String generateCode() {
bool isBaseClassLocale(LocaleInfo locale, String language) {
return locale.languageCode == language
&& locale.countryCode == null
&& locale.scriptCode == null;
}
List<LocaleInfo> getLocalesForLanguage(String language) {
return _allBundles.bundles
// Return locales for the language specified, except for the base locale itself
.where((AppResourceBundle bundle) {
final LocaleInfo locale = bundle.locale;
return !isBaseClassLocale(locale, language) && locale.languageCode == language;
})
.map((AppResourceBundle bundle) => bundle.locale).toList();
}
final String directory = path.basename(l10nDirectory.path);
final String outputFileName = path.basename(outputFile.path);
......@@ -540,15 +575,49 @@ class LocalizationsGenerator {
_allBundles.locales.map<String>((LocaleInfo locale) => '\'${locale.languageCode}\'')
);
final StringBuffer allMessagesClasses = StringBuffer();
final List<LocaleInfo> allLocales = _allBundles.locales.toList()..sort();
final String fileName = outputFileName.split('.')[0];
for (final LocaleInfo locale in allLocales) {
allMessagesClasses.writeln();
allMessagesClasses.writeln(
generateClass(className, _allBundles.bundleFor(locale), _allMessages)
);
if (isBaseClassLocale(locale, locale.languageCode)) {
final File localeMessageFile = _fs.file(
path.join(l10nDirectory.path, '${fileName}_$locale.dart'),
);
// Generate the template for the base class file. Further string
// interpolation will be done to determine if there are
// subclasses that extend the base class.
final String languageBaseClassFile = generateBaseClassFile(
className,
outputFileName,
header,
_allBundles.bundleFor(locale),
_allMessages,
);
// Every locale for the language except the base class.
final List<LocaleInfo> localesForLanguage = getLocalesForLanguage(locale.languageCode);
// Generate every subclass that is needed for the particular language
final Iterable<String> subclasses = localesForLanguage.map<String>((LocaleInfo locale) {
return generateSubclass(
className: className,
bundle: _allBundles.bundleFor(locale),
messages: _allMessages,
);
});
localeMessageFile.writeAsStringSync(
languageBaseClassFile.replaceAll('@(subclasses)', subclasses.join()),
);
}
}
final Iterable<String> localeImports = supportedLocales
.where((LocaleInfo locale) => isBaseClassLocale(locale, locale.languageCode))
.map((LocaleInfo locale) {
return "import '${fileName}_${locale.toString()}.dart';";
});
final String lookupBody = generateLookupBody(_allBundles, className);
return fileTemplate
......@@ -558,7 +627,7 @@ class LocalizationsGenerator {
.replaceAll('@(importFile)', '$directory/$outputFileName')
.replaceAll('@(supportedLocales)', supportedLocalesCode.join(',\n '))
.replaceAll('@(supportedLanguageCodes)', supportedLanguageCodes.join(', '))
.replaceAll('@(allMessagesClasses)', allMessagesClasses.toString().trim())
.replaceAll('@(messageClassImports)', localeImports.join('\n'))
.replaceAll('@(lookupName)', '_lookup$className')
.replaceAll('@(lookupBody)', lookupBody);
}
......
......@@ -12,6 +12,8 @@ import 'package:flutter_localizations/flutter_localizations.dart';
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
@(messageClassImports)
// ignore_for_file: unnecessary_brace_in_string_interps
/// Callers can lookup localized strings with an instance of @(class) returned
......@@ -67,10 +69,10 @@ import 'package:intl/intl.dart' as intl;
/// be consistent with the languages listed in the @(class).supportedLocales
/// property.
abstract class @(class) {
@(class)(String locale) : assert(locale != null), _localeName = intl.Intl.canonicalizedLocale(locale.toString());
@(class)(String locale) : assert(locale != null), localeName = intl.Intl.canonicalizedLocale(locale.toString());
// ignore: unused_field
final String _localeName;
final String localeName;
static @(class) of(BuildContext context) {
return Localizations.of<@(class)>(context, @(class));
......@@ -117,8 +119,6 @@ class _@(class)Delegate extends LocalizationsDelegate<@(class)> {
bool shouldReload(_@(class)Delegate old) => false;
}
@(allMessagesClasses)
@(class) @(lookupName)(Locale locale) {
switch(locale.languageCode) {
@(lookupBody)
......@@ -130,14 +130,14 @@ class _@(class)Delegate extends LocalizationsDelegate<@(class)> {
const String numberFormatTemplate = '''
final intl.NumberFormat @(placeholder)NumberFormat = intl.NumberFormat.@(format)(
locale: _localeName,
locale: localeName,
@(parameters)
);
final String @(placeholder)String = @(placeholder)NumberFormat.format(@(placeholder));
''';
const String dateFormatTemplate = '''
final intl.DateFormat @(placeholder)DateFormat = intl.DateFormat.@(format)(_localeName);
final intl.DateFormat @(placeholder)DateFormat = intl.DateFormat.@(format)(localeName);
final String @(placeholder)String = @(placeholder)DateFormat.format(@(placeholder));
''';
......@@ -166,18 +166,36 @@ const String pluralMethodTemplate = '''
@(numberFormatting)
return intl.Intl.pluralLogic(
@(count),
locale: _localeName,
locale: localeName,
@(pluralLogicArgs),
);
}''';
const String classTemplate = '''
const String classFileTemplate = '''
@(header)
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import '@(fileName)';
// ignore_for_file: unnecessary_brace_in_string_interps
/// The translations for @(language) (`@(localeName)`).
class @(class) extends @(baseClass) {
@(class)([String locale = '@(localeName)']) : super(locale);
@(methods)
}''';
}
@(subclasses)''';
const String subclassTemplate = '''
/// The translations for @(language) (`@(localeName)`).
class @(class) extends @(baseLanguageClassName) {
@(class)(): super('@(localeName)');
@(methods)
}
''';
const String baseClassGetterTemplate = '''
// @(comment)
......
......@@ -104,11 +104,11 @@ const Set<String> _validNumberFormats = <String>{
//
// Example of code that uses named parameters:
// final NumberFormat format = NumberFormat.compact(
// locale: _localeName,
// locale: localeName,
// );
//
// Example of code that uses positional parameters:
// final NumberFormat format = NumberFormat.scientificPattern(_localeName);
// final NumberFormat format = NumberFormat.scientificPattern(localeName);
const Set<String> _numberFormatsWithNamedParameters = <String>{
'compact',
'compactCurrency',
......
......@@ -371,37 +371,62 @@ String generateClassDeclaration(
class $classNamePrefix$camelCaseName extends $superClass {''';
}
/// Return `s` as a Dart-parseable string.
///
/// The result tries to avoid character escaping:
/// Return the input string as a Dart-parseable string.
///
/// ```
/// foo => 'foo'
/// foo "bar" => 'foo "bar"'
/// foo 'bar' => "foo 'bar'"
/// foo 'bar' "baz" => '''foo 'bar' "baz"'''
/// foo\bar => r'foo\bar'
/// foo\bar => 'foo\\bar'
/// foo\nbar => 'foo\\\\nbar'
/// ```
///
/// When [shouldEscapeDollar] is set to true, the
/// result avoids character escaping, with the
/// exception of the dollar sign:
///
/// ```
/// foo$bar = 'foo\$bar'
/// ```
///
/// When [shouldEscapeDollar] is set to false, the
/// result tries to avoid character escaping:
///
/// ```
/// foo$bar => 'foo\\\$bar'
/// ```
///
/// [shouldEscapeDollar] is true by default.
///
/// Strings with newlines are not supported.
String generateString(String value) {
assert(!value.contains('\n'));
final String rawPrefix = value.contains(r'$') || value.contains(r'\') ? 'r' : '';
if (!value.contains("'"))
return "$rawPrefix'$value'";
if (!value.contains('"'))
return '$rawPrefix"$value"';
if (!value.contains("'''"))
return "$rawPrefix'''$value'''";
if (!value.contains('"""'))
return '$rawPrefix"""$value"""';
return value.split("'''")
.map(generateString)
// If value contains more than 6 consecutive single quotes some empty strings may be generated.
// The following map removes them.
.map((String part) => part == "''" ? '' : part)
.join(" \"'''\" ");
String generateString(String value, { bool escapeDollar = true }) {
assert(escapeDollar != null);
assert(
!value.contains('\n'),
'Since it is assumed that the input string comes '
'from a json/arb file source, messages cannot '
'contain newlines.'
);
const String backslash = '__BACKSLASH__';
assert(
!value.contains(backslash),
'Input string cannot contain the sequence: '
'"__BACKSLASH__", as it is used as part of '
'backslash character processing.'
);
value = value.replaceAll('\\', backslash);
if (escapeDollar)
value = value.replaceAll('\$', '\\\$');
value = value
.replaceAll("'", "\\'")
.replaceAll('"', '\\"')
.replaceAll(backslash, '\\\\');
return "'$value'";
}
/// Only used to generate localization strings for the Kannada locale ('kn') because
......
......@@ -710,6 +710,39 @@ void main() {
});
group('generateCode', () {
test('should generate a file per language', () {
const String singleEnCaMessageArbFileString = '''
{
"title": "Canadian Title"
}''';
fs.currentDirectory.childDirectory('lib').childDirectory('l10n')..createSync(recursive: true)
..childFile(defaultTemplateArbFileName).writeAsStringSync(singleMessageArbFileString)
..childFile('app_en_CA.arb').writeAsStringSync(singleEnCaMessageArbFileString);
final LocalizationsGenerator generator = LocalizationsGenerator(fs);
try {
generator.initialize(
l10nDirectoryPath: defaultArbPathString,
templateArbFileName: defaultTemplateArbFileName,
outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString,
);
generator.loadResources();
generator.writeOutputFile();
} on Exception catch (e) {
fail('Generating output files should not fail: $e');
}
expect(fs.isFileSync(path.join('lib', 'l10n', 'output-localization-file_en.dart')), true);
expect(fs.isFileSync(path.join('lib', 'l10n', 'output-localization-file_en_US.dart')), false);
final String englishLocalizationsFile = fs.file(
path.join('lib', 'l10n', 'output-localization-file_en.dart')
).readAsStringSync();
expect(englishLocalizationsFile, contains('class AppLocalizationsEnCa extends AppLocalizationsEn'));
expect(englishLocalizationsFile, contains('class AppLocalizationsEn extends AppLocalizations'));
});
group('DateTime tests', () {
test('throws an exception when improperly formatted date is passed in', () {
const String singleDateMessageArbFileString = '''
......@@ -1112,37 +1145,4 @@ void main() {
});
});
});
group('generateString', () {
test('handles simple string', () {
expect(generateString('abc'), "'abc'");
});
test('handles string with quote', () {
expect(generateString("ab'c"), '''"ab'c"''');
});
test('handles string with double quote', () {
expect(generateString('ab"c'), """'ab"c'""");
});
test('handles string with both single and double quote', () {
expect(generateString('''a'b"c'''), """'''a'b"c'''""");
});
test('handles string with a triple single quote and a double quote', () {
expect(generateString("""a"b'''c"""), '''"""a"b\'''c"""''');
});
test('handles string with a triple double quote and a single quote', () {
expect(generateString('''a'b"""c'''), """'''a'b\"""c'''""");
});
test('handles string with both triple single and triple double quote', () {
expect(generateString('''a\'''\'''\''b"""c'''), """'a' "'''" "'''" '''''b\"""c'''""");
});
test('handles dollar', () {
expect(generateString(r'ab$c'), r"r'ab$c'");
});
test('handles back slash', () {
expect(generateString(r'ab\c'), r"r'ab\c'");
});
test("doesn't support multiline strings", () {
expect(() => generateString('ab\nc'), throwsA(isA<AssertionError>()));
});
});
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../localization/localizations_utils.dart';
import '../common.dart';
void main() {
group('generateString', () {
test('handles simple string', () {
expect(generateString('abc'), "'abc'");
});
test('handles string with quote', () {
expect(generateString("ab'c"), "'ab\\\'c'");
});
test('handles string with double quote', () {
expect(generateString('ab"c'), "'ab\\\"c'");
});
test('handles string with both single and double quote', () {
expect(generateString('''a'b"c'''), '\'a\\\'b\\"c\'');
});
test('handles string with a triple single quote and a double quote', () {
expect(generateString("""a"b'''c"""), '\'a\\"b\\\'\\\'\\\'c\'');
});
test('handles string with a triple double quote and a single quote', () {
expect(generateString('''a'b"""c'''), '\'a\\\'b\\"\\"\\"c\'');
});
test('handles string with both triple single and triple double quote', () {
expect(generateString('''a\'''b"""c'''), '\'a\\\'\\\'\\\'b\\"\\"\\"c\'');
});
test('escapes dollar when escapeDollar is true', () {
expect(generateString(r'ab$c', escapeDollar: true), "'ab\\\$c'");
});
test('does not escape dollar when escapeDollar is false', () {
expect(generateString(r'ab$c', escapeDollar: false), "'ab\$c'");
});
test('handles backslash', () {
expect(generateString(r'ab\c'), "'ab\\\\c'");
});
test('handles backslash followed by "n" character', () {
expect(generateString(r'ab\nc'), "'ab\\\\nc'");
});
test('does not support multiline strings', () {
expect(() => generateString('ab\nc'), throwsA(isA<AssertionError>()));
});
});
}
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