Unverified Commit 4451ffca authored by Per Classon's avatar Per Classon Committed by GitHub

Add option for deferred loading to gen_l10n (#53824)

parent 0d459f23
...@@ -81,6 +81,24 @@ void main(List<String> arguments) { ...@@ -81,6 +81,24 @@ void main(List<String> arguments) {
'Alternatively, see the `header` option to pass in a string ' 'Alternatively, see the `header` option to pass in a string '
'for a simpler header.' 'for a simpler header.'
); );
parser.addFlag(
'use-deferred-loading',
defaultsTo: false,
help: 'Whether to generate the Dart localization file with locales imported'
' as deferred, allowing for lazy loading of each locale in Flutter web.\n'
'\n'
'This can reduce a web app’s initial startup time by decreasing the '
'size of the JavaScript bundle. When this flag is set to true, the '
'messages for a particular locale are only downloaded and loaded by the '
'Flutter app as they are needed. For projects with a lot of different '
'locales and many localization strings, it can be an performance '
'improvement to have deferred loading. For projects with a small number '
'of locales, the difference is negligible, and might slow down the start '
'up compared to bundling the localizations with the rest of the '
'application.\n\n'
'Note that this flag does not affect other platforms such as mobile or '
'desktop.',
);
final argslib.ArgResults results = parser.parse(arguments); final argslib.ArgResults results = parser.parse(arguments);
if (results['help'] == true) { if (results['help'] == true) {
...@@ -98,6 +116,7 @@ void main(List<String> arguments) { ...@@ -98,6 +116,7 @@ void main(List<String> arguments) {
final String preferredSupportedLocaleString = results['preferred-supported-locales'] as String; final String preferredSupportedLocaleString = results['preferred-supported-locales'] as String;
final String headerString = results['header'] as String; final String headerString = results['header'] as String;
final String headerFile = results['header-file'] as String; final String headerFile = results['header-file'] as String;
final bool useDeferredLoading = results['use-deferred-loading'] as bool;
const local.LocalFileSystem fs = local.LocalFileSystem(); const local.LocalFileSystem fs = local.LocalFileSystem();
final LocalizationsGenerator localizationsGenerator = LocalizationsGenerator(fs); final LocalizationsGenerator localizationsGenerator = LocalizationsGenerator(fs);
...@@ -112,6 +131,7 @@ void main(List<String> arguments) { ...@@ -112,6 +131,7 @@ void main(List<String> arguments) {
preferredSupportedLocaleString: preferredSupportedLocaleString, preferredSupportedLocaleString: preferredSupportedLocaleString,
headerString: headerString, headerString: headerString,
headerFile: headerFile, headerFile: headerFile,
useDeferredLoading: useDeferredLoading,
) )
..loadResources() ..loadResources()
..writeOutputFile() ..writeOutputFile()
......
This diff is collapsed.
...@@ -6,6 +6,7 @@ const String fileTemplate = ''' ...@@ -6,6 +6,7 @@ const String fileTemplate = '''
@(header) @(header)
import 'dart:async'; import 'dart:async';
// ignore: unused_import
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
...@@ -104,26 +105,7 @@ abstract class @(class) { ...@@ -104,26 +105,7 @@ abstract class @(class) {
@(methods)} @(methods)}
class _@(class)Delegate extends LocalizationsDelegate<@(class)> { @(delegateClass)
const _@(class)Delegate();
@override
Future<@(class)> load(Locale locale) {
return SynchronousFuture<@(class)>(@(lookupName)(locale));
}
@override
bool isSupported(Locale locale) => <String>[@(supportedLanguageCodes)].contains(locale.languageCode);
@override
bool shouldReload(_@(class)Delegate old) => false;
}
@(class) @(lookupName)(Locale locale) {
@(lookupBody)
assert(false, '@(class).delegate failed to load unsupported locale "\$locale"');
return null;
}
'''; ''';
const String numberFormatTemplate = ''' const String numberFormatTemplate = '''
...@@ -205,14 +187,67 @@ const String baseClassMethodTemplate = ''' ...@@ -205,14 +187,67 @@ const String baseClassMethodTemplate = '''
String @(name)(@(parameters)); String @(name)(@(parameters));
'''; ''';
// DELEGATE CLASS TEMPLATES
const String delegateClassTemplate = '''
class _@(class)Delegate extends LocalizationsDelegate<@(class)> {
const _@(class)Delegate();
@override
Future<@(class)> load(Locale locale) {
@(loadBody)
}
@override
bool isSupported(Locale locale) => <String>[@(supportedLanguageCodes)].contains(locale.languageCode);
@override
bool shouldReload(_@(class)Delegate old) => false;
}
@(lookupFunction)''';
const String loadBodyTemplate = '''return SynchronousFuture<@(class)>(@(lookupName)(locale));''';
const String loadBodyDeferredLoadingTemplate = '''return @(lookupName)(locale);''';
// DELEGATE LOOKUP TEMPLATES // DELEGATE LOOKUP TEMPLATES
const String lookupFunctionTemplate = '''
@(class) @(lookupName)(Locale locale) {
@(lookupBody)
assert(false, '@(class).delegate failed to load unsupported locale "\$locale"');
return null;
}''';
const String lookupFunctionDeferredLoadingTemplate = '''
/// Lazy load the library for web, on other platforms we return the
/// localizations synchronously.
Future<@(class)> _loadLibraryForWeb(
Future<dynamic> Function() loadLibrary,
@(class) Function() localizationClosure,
) {
if (kIsWeb) {
return loadLibrary().then((dynamic _) => localizationClosure());
} else {
return SynchronousFuture<@(class)>(localizationClosure());
}
}
Future<@(class)> @(lookupName)(Locale locale) {
@(lookupBody)
assert(false, '@(class).delegate failed to load unsupported locale "\$locale"');
return null;
}''';
const String lookupBodyTemplate = '''@(lookupAllCodesSpecified) const String lookupBodyTemplate = '''@(lookupAllCodesSpecified)
@(lookupScriptCodeSpecified) @(lookupScriptCodeSpecified)
@(lookupCountryCodeSpecified) @(lookupCountryCodeSpecified)
@(lookupLanguageCodeSpecified)'''; @(lookupLanguageCodeSpecified)''';
const String switchClauseTemplate = '''case '@(case)': return @(class)();'''; const String switchClauseTemplate = '''case '@(case)': return @(localeClass)();''';
const String switchClauseDeferredLoadingTemplate = '''case '@(case)': return _loadLibraryForWeb(@(library).loadLibrary, () => @(library).@(localeClass)());''';
const String nestedSwitchTemplate = '''case '@(languageCode)': { const String nestedSwitchTemplate = '''case '@(languageCode)': {
switch (locale.@(code)) { switch (locale.@(code)) {
......
...@@ -380,6 +380,28 @@ void main() { ...@@ -380,6 +380,28 @@ void main() {
fail('Setting headerFile that does not exist should fail'); fail('Setting headerFile that does not exist should fail');
}); });
test('setting useDefferedLoading to null should fail', () {
_standardFlutterDirectoryL10nSetup(fs);
LocalizationsGenerator generator;
try {
generator = LocalizationsGenerator(fs);
generator.initialize(
l10nDirectoryPath: defaultArbPathString,
templateArbFileName: defaultTemplateArbFileName,
outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString,
headerString: '/// Sample header',
useDeferredLoading: null,
);
} on L10nException catch (e) {
expect(e.message, contains('useDeferredLoading argument cannot be null.'));
return;
}
fail('Setting useDefferedLoading to null should fail');
});
group('loadResources', () { group('loadResources', () {
test('correctly initializes supportedLocales and supportedLanguageCodes properties', () { test('correctly initializes supportedLocales and supportedLanguageCodes properties', () {
_standardFlutterDirectoryL10nSetup(fs); _standardFlutterDirectoryL10nSetup(fs);
...@@ -792,6 +814,67 @@ void main() { ...@@ -792,6 +814,67 @@ void main() {
expect(englishLocalizationsFile, contains('class AppLocalizationsEn extends AppLocalizations')); expect(englishLocalizationsFile, contains('class AppLocalizationsEn extends AppLocalizations'));
}); });
test('language imports are sorted when preferredSupportedLocaleString is given', () {
fs.currentDirectory.childDirectory('lib').childDirectory('l10n')..createSync(recursive: true)
..childFile(defaultTemplateArbFileName).writeAsStringSync(singleMessageArbFileString)
..childFile('app_zh.arb').writeAsStringSync(singleZhMessageArbFileString)
..childFile('app_es.arb').writeAsStringSync(singleEsMessageArbFileString);
const String preferredSupportedLocaleString = '["zh"]';
final LocalizationsGenerator generator = LocalizationsGenerator(fs);
try {
generator.initialize(
l10nDirectoryPath: defaultArbPathString,
templateArbFileName: defaultTemplateArbFileName,
outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString,
preferredSupportedLocaleString: preferredSupportedLocaleString,
);
generator.loadResources();
generator.writeOutputFile();
} on Exception catch (e) {
fail('Generating output files should not fail: $e');
}
final String localizationsFile = fs.file(
path.join('lib', 'l10n', defaultOutputFileString),
).readAsStringSync();
expect(localizationsFile, contains(
'''
import '${defaultOutputFileString}_en.dart';
import '${defaultOutputFileString}_es.dart';
import '${defaultOutputFileString}_zh.dart';
'''));
});
test('imports are deferred when useDeferredImports are set', () {
fs.currentDirectory.childDirectory('lib').childDirectory('l10n')..createSync(recursive: true)
..childFile(defaultTemplateArbFileName).writeAsStringSync(singleMessageArbFileString);
final LocalizationsGenerator generator = LocalizationsGenerator(fs);
try {
generator.initialize(
l10nDirectoryPath: defaultArbPathString,
templateArbFileName: defaultTemplateArbFileName,
outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString,
useDeferredLoading: true,
);
generator.loadResources();
generator.writeOutputFile();
} on Exception catch (e) {
fail('Generating output files should not fail: $e');
}
final String localizationsFile = fs.file(
path.join('lib', 'l10n', defaultOutputFileString),
).readAsStringSync();
expect(localizationsFile, contains(
'''
import '${defaultOutputFileString}_en.dart' deferred as ${defaultOutputFileString}_en;
'''));
});
group('DateTime tests', () { group('DateTime tests', () {
test('throws an exception when improperly formatted date is passed in', () { test('throws an exception when improperly formatted date is passed in', () {
const String singleDateMessageArbFileString = ''' const String singleDateMessageArbFileString = '''
......
...@@ -48,7 +48,7 @@ void main() { ...@@ -48,7 +48,7 @@ void main() {
} }
} }
test('generated l10n classes produce expected localized strings', () async { void setUpAndRunGenL10n({List<String> args}) {
// Get the intl packages before running gen_l10n. // Get the intl packages before running gen_l10n.
final String flutterBin = globals.platform.isWindows ? 'flutter.bat' : 'flutter'; final String flutterBin = globals.platform.isWindows ? 'flutter.bat' : 'flutter';
final String flutterPath = globals.fs.path.join(getFlutterRoot(), 'bin', flutterBin); final String flutterPath = globals.fs.path.join(getFlutterRoot(), 'bin', flutterBin);
...@@ -58,8 +58,10 @@ void main() { ...@@ -58,8 +58,10 @@ void main() {
final String genL10nPath = globals.fs.path.join(getFlutterRoot(), 'dev', 'tools', 'localization', 'bin', 'gen_l10n.dart'); final String genL10nPath = globals.fs.path.join(getFlutterRoot(), 'dev', 'tools', 'localization', 'bin', 'gen_l10n.dart');
final String dartBin = globals.platform.isWindows ? 'dart.exe' : 'dart'; final String dartBin = globals.platform.isWindows ? 'dart.exe' : 'dart';
final String dartPath = globals.fs.path.join(getFlutterRoot(), 'bin', 'cache', 'dart-sdk', 'bin', dartBin); final String dartPath = globals.fs.path.join(getFlutterRoot(), 'bin', 'cache', 'dart-sdk', 'bin', dartBin);
runCommand(<String>[dartPath, genL10nPath]); runCommand(<String>[dartPath, genL10nPath, args?.join(' ')]);
}
Future<StringBuffer> runApp() async {
// Run the app defined in GenL10nProject.main and wait for it to // Run the app defined in GenL10nProject.main and wait for it to
// send '#l10n END' to its stdout. // send '#l10n END' to its stdout.
final Completer<void> l10nEnd = Completer<void>(); final Completer<void> l10nEnd = Completer<void>();
...@@ -75,6 +77,10 @@ void main() { ...@@ -75,6 +77,10 @@ void main() {
await _flutter.run(); await _flutter.run();
await l10nEnd.future; await l10nEnd.future;
await subscription.cancel(); await subscription.cancel();
return stdout;
}
void expectOutput(StringBuffer stdout) {
expect(stdout.toString(), expect(stdout.toString(),
'#l10n 0 (--- supportedLocales tests ---)\n' '#l10n 0 (--- supportedLocales tests ---)\n'
'#l10n 1 (supportedLocales[0]: languageCode: en, countryCode: null, scriptCode: null)\n' '#l10n 1 (supportedLocales[0]: languageCode: en, countryCode: null, scriptCode: null)\n'
...@@ -133,5 +139,17 @@ void main() { ...@@ -133,5 +139,17 @@ void main() {
'#l10n 54 (Flutter is "amazing", times 2!)\n' '#l10n 54 (Flutter is "amazing", times 2!)\n'
'#l10n END\n' '#l10n END\n'
); );
}
test('generated l10n classes produce expected localized strings', () async {
setUpAndRunGenL10n();
final StringBuffer stdout = await runApp();
expectOutput(stdout);
});
test('generated l10n classes produce expected localized strings with deferred loading', () async {
setUpAndRunGenL10n(args: <String>['--use-deferred-loading']);
final StringBuffer stdout = await runApp();
expectOutput(stdout);
}); });
} }
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