diff --git a/AUTHORS b/AUTHORS index f6926999a9..3be8fb32d3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -118,4 +118,5 @@ Kim Jiun LinXunFeng Sabin Neupane Mahdi Bagheri <1839491@gmail.com> +Mok Kah Wai Lucas Saudon diff --git a/packages/flutter_tools/lib/src/commands/generate_localizations.dart b/packages/flutter_tools/lib/src/commands/generate_localizations.dart index f7dd479fbe..bbe0260ca4 100644 --- a/packages/flutter_tools/lib/src/commands/generate_localizations.dart +++ b/packages/flutter_tools/lib/src/commands/generate_localizations.dart @@ -207,6 +207,10 @@ class GenerateLocalizationsCommand extends FlutterCommand { 'and "}" is treated as a string if it does not close any previous "{" ' 'that is treated as a special character.', ); + argParser.addFlag( + 'use-named-parameters', + help: 'Whether or not to use named parameters for the generated localization methods.', + ); } final FileSystem _fileSystem; diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart index 206dedbaf1..1cc931a789 100644 --- a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart +++ b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart @@ -73,6 +73,7 @@ Future generateLocalizations({ logger: logger, suppressWarnings: options.suppressWarnings, useRelaxedSyntax: options.relaxSyntax, + useNamedParameters: options.useNamedParameters, ) ..loadResources() ..writeOutputFiles(isFromYaml: true, useCRLF: useCRLF); @@ -122,9 +123,9 @@ String _syntheticL10nPackagePath(FileSystem fileSystem) => fileSystem.path.join( // For example, if placeholders are used for plurals and no type was specified, then the type will // automatically set to 'num'. Similarly, if such placeholders are used for selects, then the type // will be set to 'String'. For such placeholders that are used for both, we should throw an error. -List generateMethodParameters(Message message) { +List generateMethodParameters(Message message, bool useNamedParameters) { return message.placeholders.values.map((Placeholder placeholder) { - return '${placeholder.type} ${placeholder.name}'; + return '${useNamedParameters ? 'required ' : ''}${placeholder.type} ${placeholder.name}'; }).toList(); } @@ -231,7 +232,7 @@ Map pluralCases = { 'other': 'other', }; -String generateBaseClassMethod(Message message, LocaleInfo? templateArbLocale) { +String generateBaseClassMethod(Message message, LocaleInfo? templateArbLocale, bool useNamedParameters) { final String comment = message .description ?.split('\n') @@ -242,11 +243,11 @@ String generateBaseClassMethod(Message message, LocaleInfo? templateArbLocale) { /// **'${generateString(message.value)}'**'''; if (message.placeholders.isNotEmpty) { - return baseClassMethodTemplate + return (useNamedParameters ? baseClassMethodWithNamedParameterTemplate : baseClassMethodTemplate) .replaceAll('@(comment)', comment) .replaceAll('@(templateLocaleTranslationComment)', templateLocaleTranslationComment) .replaceAll('@(name)', message.resourceId) - .replaceAll('@(parameters)', generateMethodParameters(message).join(', ')); + .replaceAll('@(parameters)', generateMethodParameters(message, useNamedParameters).join(', ')); } return baseClassGetterTemplate .replaceAll('@(comment)', comment) @@ -492,6 +493,7 @@ class LocalizationsGenerator { required Logger logger, bool suppressWarnings = false, bool useRelaxedSyntax = false, + bool useNamedParameters = false, }) { final Directory? projectDirectory = projectDirFromPath(fileSystem, projectPathString); final Directory inputDirectory = inputDirectoryFromPath(fileSystem, inputPathString, projectDirectory); @@ -516,6 +518,7 @@ class LocalizationsGenerator { logger: logger, suppressWarnings: suppressWarnings, useRelaxedSyntax: useRelaxedSyntax, + useNamedParameters: useNamedParameters, ); } @@ -541,6 +544,7 @@ class LocalizationsGenerator { this.useEscaping = false, this.suppressWarnings = false, this.useRelaxedSyntax = false, + this.useNamedParameters = false, }); final FileSystem _fs; @@ -685,6 +689,14 @@ class LocalizationsGenerator { /// Whether or not to suppress warnings or not. final bool suppressWarnings; + /// Whether to generate the Dart localization methods with named parameters. + /// + /// If this sets to true, the generated Dart localization methods will be: + /// ``` + /// String helloWorld({required String name}); + /// ``` + final bool useNamedParameters; + static bool _isNotReadable(FileStat fileStat) { final String rawStatString = fileStat.modeString(); // Removes potential prepended permission bits, such as '(suid)' and '(guid)'. @@ -1124,7 +1136,7 @@ class LocalizationsGenerator { return fileTemplate .replaceAll('@(header)', header.isEmpty ? '' : '$header\n') .replaceAll('@(class)', className) - .replaceAll('@(methods)', _allMessages.map((Message message) => generateBaseClassMethod(message, _templateArbLocale)).join('\n')) + .replaceAll('@(methods)', _allMessages.map((Message message) => generateBaseClassMethod(message, _templateArbLocale, useNamedParameters)).join('\n')) .replaceAll('@(importFile)', '$directory/$outputFileName') .replaceAll('@(supportedLocales)', supportedLocalesCode.join(',\n ')) .replaceAll('@(supportedLanguageCodes)', supportedLanguageCodes.join(', ')) @@ -1306,9 +1318,9 @@ The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", " } final String messageString = generateVariables(node, isRoot: true); final String tempVarLines = tempVariables.isEmpty ? '' : '${tempVariables.join('\n')}\n'; - return methodTemplate + return (useNamedParameters ? methodWithNamedParameterTemplate : methodTemplate) .replaceAll('@(name)', message.resourceId) - .replaceAll('@(parameters)', generateMethodParameters(message).join(', ')) + .replaceAll('@(parameters)', generateMethodParameters(message, useNamedParameters).join(', ')) .replaceAll('@(dateFormatting)', generateDateFormattingLogic(message)) .replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message)) .replaceAll('@(tempVars)', tempVarLines) diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart index 15e9cd6b4a..068ab3611c 100644 --- a/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart +++ b/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart @@ -142,6 +142,14 @@ const String methodTemplate = ''' @(tempVars) return @(message); }'''; +const String methodWithNamedParameterTemplate = ''' + @override + String @(name)({@(parameters)}) { +@(dateFormatting) +@(numberFormatting) +@(tempVars) return @(message); + }'''; + const String pluralVariableTemplate = ''' String @(varName) = intl.Intl.pluralLogic( @(count), @@ -195,6 +203,13 @@ const String baseClassMethodTemplate = ''' String @(name)(@(parameters)); '''; +const String baseClassMethodWithNamedParameterTemplate = ''' +@(comment) + /// +@(templateLocaleTranslationComment) + String @(name)({@(parameters)}); +'''; + // DELEGATE CLASS TEMPLATES const String delegateClassTemplate = ''' diff --git a/packages/flutter_tools/lib/src/localizations/localizations_utils.dart b/packages/flutter_tools/lib/src/localizations/localizations_utils.dart index 84bfa86251..e6774d79f1 100644 --- a/packages/flutter_tools/lib/src/localizations/localizations_utils.dart +++ b/packages/flutter_tools/lib/src/localizations/localizations_utils.dart @@ -355,6 +355,7 @@ class LocalizationOptions { bool? useEscaping, bool? suppressWarnings, bool? relaxSyntax, + bool? useNamedParameters, }) : templateArbFile = templateArbFile ?? 'app_en.arb', outputLocalizationFile = outputLocalizationFile ?? 'app_localizations.dart', outputClass = outputClass ?? 'AppLocalizations', @@ -365,7 +366,8 @@ class LocalizationOptions { format = format ?? false, useEscaping = useEscaping ?? false, suppressWarnings = suppressWarnings ?? false, - relaxSyntax = relaxSyntax ?? false; + relaxSyntax = relaxSyntax ?? false, + useNamedParameters = useNamedParameters ?? false; /// The `--arb-dir` argument. /// @@ -467,6 +469,14 @@ class LocalizationOptions { /// This was added in for backward compatibility and is not recommended /// as it may mask errors. final bool relaxSyntax; + + /// The `use-named-parameters` argument. + /// + /// Whether or not to use named parameters for the generated localization + /// methods. + /// + /// Defaults to `false`. + final bool useNamedParameters; } /// Parse the localizations configuration options from [file]. @@ -511,6 +521,7 @@ LocalizationOptions parseLocalizationsOptionsFromYAML({ useEscaping: _tryReadBool(yamlNode, 'use-escaping', logger), suppressWarnings: _tryReadBool(yamlNode, 'suppress-warnings', logger), relaxSyntax: _tryReadBool(yamlNode, 'relax-syntax', logger), + useNamedParameters: _tryReadBool(yamlNode, 'use-named-parameters', logger), ); } @@ -537,6 +548,7 @@ LocalizationOptions parseLocalizationsOptionsFromCommand({ format: command.boolArg('format'), useEscaping: command.boolArg('use-escaping'), suppressWarnings: command.boolArg('suppress-warnings'), + useNamedParameters: command.boolArg('use-named-parameters'), ); } diff --git a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart index c69efb9b55..74e9964c6e 100644 --- a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart +++ b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart @@ -96,6 +96,7 @@ void main() { bool areResourceAttributeRequired = false, bool suppressWarnings = false, bool relaxSyntax = false, + bool useNamedParameters = false, void Function(Directory)? setup, } ) { @@ -128,6 +129,7 @@ void main() { areResourceAttributesRequired: areResourceAttributeRequired, suppressWarnings: suppressWarnings, useRelaxedSyntax: relaxSyntax, + useNamedParameters: useNamedParameters, ) ..loadResources() ..writeOutputFiles(isFromYaml: isFromYaml); @@ -2491,4 +2493,43 @@ NumberFormat.decimalPatternDigits( setupLocalizations({ 'en': dollarSignWithSelect }); expect(getGeneratedFileContent(locale: 'en'), contains(r'\$nice_bug\nHello Bug! Manifistation #1 $_temp0')); }); + + testWithoutContext('can generate method with named parameter', () { + const String arbFile = ''' +{ + "helloName": "Hello {name}!", + "@helloName": { + "description": "A more personal greeting", + "placeholders": { + "name": { + "type": "String", + "description": "The name of the person to greet" + } + } + }, + "helloNameAndAge": "Hello {name}! You are {age} years old.", + "@helloNameAndAge": { + "description": "A more personal greeting", + "placeholders": { + "name": { + "type": "String", + "description": "The name of the person to greet" + }, + "age": { + "type": "int", + "description": "The age of the person to greet" + } + } + } +} + '''; + setupLocalizations({ 'en': arbFile }, useNamedParameters: true); + final String localizationsFile = getGeneratedFileContent(locale: 'en'); + expect(localizationsFile, containsIgnoringWhitespace(r''' +String helloName({required String name}) { + ''')); + expect(localizationsFile, containsIgnoringWhitespace(r''' +String helloNameAndAge({required String name, required int age}) { + ''')); + }); } diff --git a/packages/flutter_tools/test/integration.shard/gen_l10n_test.dart b/packages/flutter_tools/test/integration.shard/gen_l10n_test.dart index 13be650bc8..268ec5e024 100644 --- a/packages/flutter_tools/test/integration.shard/gen_l10n_test.dart +++ b/packages/flutter_tools/test/integration.shard/gen_l10n_test.dart @@ -11,7 +11,12 @@ import 'test_data/gen_l10n_project.dart'; import 'test_driver.dart'; import 'test_utils.dart'; -final GenL10nProject project = GenL10nProject(); +final GenL10nProject project = GenL10nProject( + useNamedParameters: false, +); +final GenL10nProject projectWithNamedParameter = GenL10nProject( + useNamedParameters: true, +); // Verify that the code generated by gen_l10n executes correctly. // It can fail if gen_l10n produces a lib/l10n/app_localizations.dart that @@ -180,4 +185,11 @@ void main() { final StringBuffer stdout = await runApp(); expectOutput(stdout); }); + + testWithoutContext('generated l10n classes produce expected localized strings when named parameter is used', () async { + await projectWithNamedParameter.setUpIn(tempDir); + flutter = FlutterRunTestDriver(tempDir); + final StringBuffer stdout = await runApp(); + expectOutput(stdout); + }); } diff --git a/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart b/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart index 3444979b77..3f184bb263 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart @@ -8,6 +8,8 @@ import '../test_utils.dart'; import 'project.dart'; class GenL10nProject extends Project { + GenL10nProject({required this.useNamedParameters}); + @override Future setUpIn(Directory dir, { bool useDeferredLoading = false, @@ -26,10 +28,12 @@ class GenL10nProject extends Project { writeFile(fileSystem.path.join(dir.path, 'l10n.yaml'), l10nYaml( useDeferredLoading: useDeferredLoading, useSyntheticPackage: useSyntheticPackage, + useNamedParameters: useNamedParameters, )); return super.setUpIn(dir); } + @override final String pubspec = ''' name: test_l10n_project @@ -44,290 +48,13 @@ dependencies: intl: any # Pick up the pinned version from flutter_localizations '''; + String? _main; + @override - final String main = r''' -import 'package:flutter/material.dart'; + String get main => + _main ??= (useNamedParameters ? _getMainWithNamedParameters() : _getMain()); -import 'l10n/app_localizations.dart'; - -class LocaleBuilder extends StatelessWidget { - const LocaleBuilder({ - Key? key, - this.locale, - this.test, - required this.callback, - }) : super(key: key); - - final Locale? locale; - final String? test; - final void Function (BuildContext context) callback; - - @override build(BuildContext context) { - return Localizations.override( - locale: locale, - context: context, - child: ResultBuilder( - test: test, - callback: callback, - ), - ); - } -} - -class ResultBuilder extends StatelessWidget { - const ResultBuilder({ - Key? key, - this.test, - required this.callback, - }) : super(key: key); - - final String? test; - final void Function (BuildContext context) callback; - - @override build(BuildContext context) { - return Builder( - builder: (BuildContext context) { - try { - callback(context); - } on Exception catch (e) { - print('#l10n A(n) $e has occurred trying to generate "$test" results.'); - print('#l10n END'); - } - return Container(); - }, - ); - } -} - -class Home extends StatelessWidget { - @override - Widget build(BuildContext context) { - final List results = []; - return Row( - children: [ - LocaleBuilder( - test: 'supportedLocales', - callback: (BuildContext context) { - results.add('--- supportedLocales tests ---'); - int n = 0; - for (Locale locale in AppLocalizations.supportedLocales) { - String languageCode = locale.languageCode; - String? countryCode = locale.countryCode; - String? scriptCode = locale.scriptCode; - results.add('supportedLocales[$n]: languageCode: $languageCode, countryCode: $countryCode, scriptCode: $scriptCode'); - n += 1; - } - }, - ), - LocaleBuilder( - locale: Locale('en', 'CA'), - test: 'countryCode - en_CA', - callback: (BuildContext context) { - results.add('--- countryCode (en_CA) tests ---'); - results.add(AppLocalizations.of(context)!.helloWorld); - results.add(AppLocalizations.of(context)!.hello("CA fallback World")); - }, - ), - LocaleBuilder( - locale: Locale('en', 'GB'), - test: 'countryCode - en_GB', - callback: (BuildContext context) { - results.add('--- countryCode (en_GB) tests ---'); - results.add(AppLocalizations.of(context)!.helloWorld); - results.add(AppLocalizations.of(context)!.hello("GB fallback World")); - }, - ), - LocaleBuilder( - locale: Locale('zh'), - test: 'zh', - callback: (BuildContext context) { - results.add('--- zh ---'); - results.add(AppLocalizations.of(context)!.helloWorld); - results.add(AppLocalizations.of(context)!.helloWorlds(0)); - results.add(AppLocalizations.of(context)!.helloWorlds(1)); - results.add(AppLocalizations.of(context)!.helloWorlds(2)); - // Should use the fallback language, in this case, - // "Hello 世界" should be displayed. - results.add(AppLocalizations.of(context)!.hello("世界")); - // helloCost is tested in 'zh' because 'es' currency format contains a - // non-breaking space character (U+00A0), which if removed, - // makes it hard to decipher why the test is failing. - results.add(AppLocalizations.of(context)!.helloCost("价钱", 123)); - }, - ), - LocaleBuilder( - locale: Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'), - test: 'zh', - callback: (BuildContext context) { - results.add('--- scriptCode: zh_Hans ---'); - results.add(AppLocalizations.of(context)!.helloWorld); - }, - ), - LocaleBuilder( - locale: Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), - test: 'scriptCode - zh_Hant', - callback: (BuildContext context) { - results.add('--- scriptCode - zh_Hant ---'); - results.add(AppLocalizations.of(context)!.helloWorld); - }, - ), - LocaleBuilder( - locale: Locale.fromSubtags(languageCode: 'zh', countryCode: 'TW', scriptCode: 'Hant'), - test: 'scriptCode - zh_TW_Hant', - callback: (BuildContext context) { - results.add('--- scriptCode - zh_Hant_TW ---'); - results.add(AppLocalizations.of(context)!.helloWorld); - }, - ), - LocaleBuilder( - locale: Locale('en'), - test: 'General formatting', - callback: (BuildContext context) { - results.add('--- General formatting tests ---'); - final AppLocalizations localizations = AppLocalizations.of(context)!; - results.addAll([ - '${localizations.helloWorld}', - '${localizations.helloNewlineWorld}', - '${localizations.testDollarSign}', - '${localizations.hello("World")}', - '${localizations.greeting("Hello", "World")}', - '${localizations.helloWorldOn(DateTime(1960))}', - '${localizations.helloOn("world argument", DateTime(1960), DateTime(1960))}', - '${localizations.helloWorldDuring(DateTime(1960), DateTime(2020))}', - '${localizations.helloFor(123)}', - '${localizations.helloCost("price", 123)}', - '${localizations.helloCostWithOptionalParam("price", .5)}', - '${localizations.helloCostWithSpecialCharacter1("price", .5)}', - '${localizations.helloCostWithSpecialCharacter2("price", .5)}', - '${localizations.helloCostWithSpecialCharacter3("price", .5)}', - '${localizations.helloDecimalPattern(1200000)}', - '${localizations.helloPercentPattern(1200000)}', - '${localizations.helloScientificPattern(1200000)}', - '${localizations.helloWorlds(0)}', - '${localizations.helloWorlds(1)}', - '${localizations.helloWorlds(2)}', - '${localizations.helloAdjectiveWorlds(0, "new")}', - '${localizations.helloAdjectiveWorlds(1, "new")}', - '${localizations.helloAdjectiveWorlds(2, "new")}', - '${localizations.helloWorldsOn(0, DateTime(1960))}', - '${localizations.helloWorldsOn(1, DateTime(1960))}', - '${localizations.helloWorldsOn(2, DateTime(1960))}', - '${localizations.helloWorldPopulation(0, 100)}', - '${localizations.helloWorldPopulation(1, 101)}', - '${localizations.helloWorldPopulation(2, 102)}', - '${localizations.helloWorldsInterpolation(123, "Hello", "World")}', - '${localizations.dollarSign}', - '${localizations.dollarSignPlural(1)}', - '${localizations.singleQuote}', - '${localizations.singleQuotePlural(2)}', - '${localizations.doubleQuote}', - '${localizations.doubleQuotePlural(2)}', - "${localizations.vehicleSelect('truck')}", - "${localizations.singleQuoteSelect('sedan')}", - "${localizations.doubleQuoteSelect('cabriolet')}", - "${localizations.pluralInString(1)}", - "${localizations.selectInString('he')}", - "${localizations.selectWithPlaceholder('male', 'ice cream')}", - "${localizations.selectWithPlaceholder('female', 'chocolate')}", - "${localizations.selectInPlural('male', 1)}", - "${localizations.selectInPlural('male', 2)}", - "${localizations.selectInPlural('female', 1)}", - '${localizations.datetime1(DateTime(2023, 6, 26))}', - '${localizations.datetime2(DateTime(2023, 6, 26, 5, 23))}', - ]); - }, - ), - LocaleBuilder( - locale: Locale('es'), - test: '--- es ---', - callback: (BuildContext context) { - results.add('--- es ---'); - final AppLocalizations localizations = AppLocalizations.of(context)!; - results.addAll([ - '${localizations.helloWorld}', - '${localizations.helloNewlineWorld}', - '${localizations.testDollarSign}', - '${localizations.hello("Mundo")}', - '${localizations.greeting("Hola", "Mundo")}', - '${localizations.helloWorldOn(DateTime(1960))}', - '${localizations.helloOn("world argument", DateTime(1960), DateTime(1960))}', - '${localizations.helloWorldDuring(DateTime(1960), DateTime(2020))}', - '${localizations.helloFor(123)}', - // helloCost is tested in 'zh' because 'es' currency format contains a - // non-breaking space character (U+00A0), which if removed, - // makes it hard to decipher why the test is failing. - '${localizations.helloWorlds(0)}', - '${localizations.helloWorlds(1)}', - '${localizations.helloWorlds(2)}', - '${localizations.helloAdjectiveWorlds(0, "nuevo")}', - '${localizations.helloAdjectiveWorlds(1, "nuevo")}', - '${localizations.helloAdjectiveWorlds(2, "nuevo")}', - '${localizations.helloWorldsOn(0, DateTime(1960))}', - '${localizations.helloWorldsOn(1, DateTime(1960))}', - '${localizations.helloWorldsOn(2, DateTime(1960))}', - '${localizations.helloWorldPopulation(0, 100)}', - '${localizations.helloWorldPopulation(1, 101)}', - '${localizations.helloWorldPopulation(2, 102)}', - '${localizations.helloWorldsInterpolation(123, "Hola", "Mundo")}', - '${localizations.dollarSign}', - '${localizations.dollarSignPlural(1)}', - '${localizations.singleQuote}', - '${localizations.singleQuotePlural(2)}', - '${localizations.doubleQuote}', - '${localizations.doubleQuotePlural(2)}', - "${localizations.vehicleSelect('truck')}", - "${localizations.singleQuoteSelect('sedan')}", - "${localizations.doubleQuoteSelect('cabriolet')}", - "${localizations.pluralInString(1)}", - "${localizations.selectInString('he')}", - ]); - }, - ), - LocaleBuilder( - locale: Locale.fromSubtags(languageCode: 'es', countryCode: '419'), - test: 'countryCode - es_419', - callback: (BuildContext context) { - results.add('--- es_419 ---'); - final AppLocalizations localizations = AppLocalizations.of(context)!; - results.addAll([ - '${localizations.helloWorld}', - '${localizations.helloWorlds(0)}', - '${localizations.helloWorlds(1)}', - '${localizations.helloWorlds(2)}', - ]); - }, - ), - LocaleBuilder( - callback: (BuildContext context) { - try { - int n = 0; - for (final String result in results) { - // Newline character replacement is necessary because - // the stream breaks up stdout by new lines. - print('#l10n $n (${result.replaceAll('\n', '_NEWLINE_')})'); - n += 1; - } - } - finally { - print('#l10n END'); - } - }, - ), - ], - ); - } -} - -void main() { - runApp( - MaterialApp( - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - home: Home(), - ), - ); -} -'''; + final bool useNamedParameters; final String appEn = r''' { @@ -777,9 +504,577 @@ void main() { } '''; + String _getMain() => r''' +import 'package:flutter/material.dart'; + +import 'l10n/app_localizations.dart'; + +class LocaleBuilder extends StatelessWidget { + const LocaleBuilder({ + Key? key, + this.locale, + this.test, + required this.callback, + }) : super(key: key); + + final Locale? locale; + final String? test; + final void Function (BuildContext context) callback; + + @override build(BuildContext context) { + return Localizations.override( + locale: locale, + context: context, + child: ResultBuilder( + test: test, + callback: callback, + ), + ); + } +} + +class ResultBuilder extends StatelessWidget { + const ResultBuilder({ + Key? key, + this.test, + required this.callback, + }) : super(key: key); + + final String? test; + final void Function (BuildContext context) callback; + + @override build(BuildContext context) { + return Builder( + builder: (BuildContext context) { + try { + callback(context); + } on Exception catch (e) { + print('#l10n A(n) $e has occurred trying to generate "$test" results.'); + print('#l10n END'); + } + return Container(); + }, + ); + } +} + +class Home extends StatelessWidget { + @override + Widget build(BuildContext context) { + final List results = []; + return Row( + children: [ + LocaleBuilder( + test: 'supportedLocales', + callback: (BuildContext context) { + results.add('--- supportedLocales tests ---'); + int n = 0; + for (Locale locale in AppLocalizations.supportedLocales) { + String languageCode = locale.languageCode; + String? countryCode = locale.countryCode; + String? scriptCode = locale.scriptCode; + results.add('supportedLocales[$n]: languageCode: $languageCode, countryCode: $countryCode, scriptCode: $scriptCode'); + n += 1; + } + }, + ), + LocaleBuilder( + locale: Locale('en', 'CA'), + test: 'countryCode - en_CA', + callback: (BuildContext context) { + results.add('--- countryCode (en_CA) tests ---'); + results.add(AppLocalizations.of(context)!.helloWorld); + results.add(AppLocalizations.of(context)!.hello("CA fallback World")); + }, + ), + LocaleBuilder( + locale: Locale('en', 'GB'), + test: 'countryCode - en_GB', + callback: (BuildContext context) { + results.add('--- countryCode (en_GB) tests ---'); + results.add(AppLocalizations.of(context)!.helloWorld); + results.add(AppLocalizations.of(context)!.hello("GB fallback World")); + }, + ), + LocaleBuilder( + locale: Locale('zh'), + test: 'zh', + callback: (BuildContext context) { + results.add('--- zh ---'); + results.add(AppLocalizations.of(context)!.helloWorld); + results.add(AppLocalizations.of(context)!.helloWorlds(0)); + results.add(AppLocalizations.of(context)!.helloWorlds(1)); + results.add(AppLocalizations.of(context)!.helloWorlds(2)); + // Should use the fallback language, in this case, + // "Hello 世界" should be displayed. + results.add(AppLocalizations.of(context)!.hello("世界")); + // helloCost is tested in 'zh' because 'es' currency format contains a + // non-breaking space character (U+00A0), which if removed, + // makes it hard to decipher why the test is failing. + results.add(AppLocalizations.of(context)!.helloCost("价钱", 123)); + }, + ), + LocaleBuilder( + locale: Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'), + test: 'zh', + callback: (BuildContext context) { + results.add('--- scriptCode: zh_Hans ---'); + results.add(AppLocalizations.of(context)!.helloWorld); + }, + ), + LocaleBuilder( + locale: Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), + test: 'scriptCode - zh_Hant', + callback: (BuildContext context) { + results.add('--- scriptCode - zh_Hant ---'); + results.add(AppLocalizations.of(context)!.helloWorld); + }, + ), + LocaleBuilder( + locale: Locale.fromSubtags(languageCode: 'zh', countryCode: 'TW', scriptCode: 'Hant'), + test: 'scriptCode - zh_TW_Hant', + callback: (BuildContext context) { + results.add('--- scriptCode - zh_Hant_TW ---'); + results.add(AppLocalizations.of(context)!.helloWorld); + }, + ), + LocaleBuilder( + locale: Locale('en'), + test: 'General formatting', + callback: (BuildContext context) { + results.add('--- General formatting tests ---'); + final AppLocalizations localizations = AppLocalizations.of(context)!; + results.addAll([ + '${localizations.helloWorld}', + '${localizations.helloNewlineWorld}', + '${localizations.testDollarSign}', + '${localizations.hello("World")}', + '${localizations.greeting("Hello", "World")}', + '${localizations.helloWorldOn(DateTime(1960))}', + '${localizations.helloOn("world argument", DateTime(1960), DateTime(1960))}', + '${localizations.helloWorldDuring(DateTime(1960), DateTime(2020))}', + '${localizations.helloFor(123)}', + '${localizations.helloCost("price", 123)}', + '${localizations.helloCostWithOptionalParam("price", .5)}', + '${localizations.helloCostWithSpecialCharacter1("price", .5)}', + '${localizations.helloCostWithSpecialCharacter2("price", .5)}', + '${localizations.helloCostWithSpecialCharacter3("price", .5)}', + '${localizations.helloDecimalPattern(1200000)}', + '${localizations.helloPercentPattern(1200000)}', + '${localizations.helloScientificPattern(1200000)}', + '${localizations.helloWorlds(0)}', + '${localizations.helloWorlds(1)}', + '${localizations.helloWorlds(2)}', + '${localizations.helloAdjectiveWorlds(0, "new")}', + '${localizations.helloAdjectiveWorlds(1, "new")}', + '${localizations.helloAdjectiveWorlds(2, "new")}', + '${localizations.helloWorldsOn(0, DateTime(1960))}', + '${localizations.helloWorldsOn(1, DateTime(1960))}', + '${localizations.helloWorldsOn(2, DateTime(1960))}', + '${localizations.helloWorldPopulation(0, 100)}', + '${localizations.helloWorldPopulation(1, 101)}', + '${localizations.helloWorldPopulation(2, 102)}', + '${localizations.helloWorldsInterpolation(123, "Hello", "World")}', + '${localizations.dollarSign}', + '${localizations.dollarSignPlural(1)}', + '${localizations.singleQuote}', + '${localizations.singleQuotePlural(2)}', + '${localizations.doubleQuote}', + '${localizations.doubleQuotePlural(2)}', + "${localizations.vehicleSelect('truck')}", + "${localizations.singleQuoteSelect('sedan')}", + "${localizations.doubleQuoteSelect('cabriolet')}", + "${localizations.pluralInString(1)}", + "${localizations.selectInString('he')}", + "${localizations.selectWithPlaceholder('male', 'ice cream')}", + "${localizations.selectWithPlaceholder('female', 'chocolate')}", + "${localizations.selectInPlural('male', 1)}", + "${localizations.selectInPlural('male', 2)}", + "${localizations.selectInPlural('female', 1)}", + '${localizations.datetime1(DateTime(2023, 6, 26))}', + '${localizations.datetime2(DateTime(2023, 6, 26, 5, 23))}', + ]); + }, + ), + LocaleBuilder( + locale: Locale('es'), + test: '--- es ---', + callback: (BuildContext context) { + results.add('--- es ---'); + final AppLocalizations localizations = AppLocalizations.of(context)!; + results.addAll([ + '${localizations.helloWorld}', + '${localizations.helloNewlineWorld}', + '${localizations.testDollarSign}', + '${localizations.hello("Mundo")}', + '${localizations.greeting("Hola", "Mundo")}', + '${localizations.helloWorldOn(DateTime(1960))}', + '${localizations.helloOn("world argument", DateTime(1960), DateTime(1960))}', + '${localizations.helloWorldDuring(DateTime(1960), DateTime(2020))}', + '${localizations.helloFor(123)}', + // helloCost is tested in 'zh' because 'es' currency format contains a + // non-breaking space character (U+00A0), which if removed, + // makes it hard to decipher why the test is failing. + '${localizations.helloWorlds(0)}', + '${localizations.helloWorlds(1)}', + '${localizations.helloWorlds(2)}', + '${localizations.helloAdjectiveWorlds(0, "nuevo")}', + '${localizations.helloAdjectiveWorlds(1, "nuevo")}', + '${localizations.helloAdjectiveWorlds(2, "nuevo")}', + '${localizations.helloWorldsOn(0, DateTime(1960))}', + '${localizations.helloWorldsOn(1, DateTime(1960))}', + '${localizations.helloWorldsOn(2, DateTime(1960))}', + '${localizations.helloWorldPopulation(0, 100)}', + '${localizations.helloWorldPopulation(1, 101)}', + '${localizations.helloWorldPopulation(2, 102)}', + '${localizations.helloWorldsInterpolation(123, "Hola", "Mundo")}', + '${localizations.dollarSign}', + '${localizations.dollarSignPlural(1)}', + '${localizations.singleQuote}', + '${localizations.singleQuotePlural(2)}', + '${localizations.doubleQuote}', + '${localizations.doubleQuotePlural(2)}', + "${localizations.vehicleSelect('truck')}", + "${localizations.singleQuoteSelect('sedan')}", + "${localizations.doubleQuoteSelect('cabriolet')}", + "${localizations.pluralInString(1)}", + "${localizations.selectInString('he')}", + ]); + }, + ), + LocaleBuilder( + locale: Locale.fromSubtags(languageCode: 'es', countryCode: '419'), + test: 'countryCode - es_419', + callback: (BuildContext context) { + results.add('--- es_419 ---'); + final AppLocalizations localizations = AppLocalizations.of(context)!; + results.addAll([ + '${localizations.helloWorld}', + '${localizations.helloWorlds(0)}', + '${localizations.helloWorlds(1)}', + '${localizations.helloWorlds(2)}', + ]); + }, + ), + LocaleBuilder( + callback: (BuildContext context) { + try { + int n = 0; + for (final String result in results) { + // Newline character replacement is necessary because + // the stream breaks up stdout by new lines. + print('#l10n $n (${result.replaceAll('\n', '_NEWLINE_')})'); + n += 1; + } + } + finally { + print('#l10n END'); + } + }, + ), + ], + ); + } +} + +void main() { + runApp( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Home(), + ), + ); +} +'''; + + String _getMainWithNamedParameters() => r''' +import 'package:flutter/material.dart'; + +import 'l10n/app_localizations.dart'; + +class LocaleBuilder extends StatelessWidget { + const LocaleBuilder({ + Key? key, + this.locale, + this.test, + required this.callback, + }) : super(key: key); + + final Locale? locale; + final String? test; + final void Function (BuildContext context) callback; + + @override build(BuildContext context) { + return Localizations.override( + locale: locale, + context: context, + child: ResultBuilder( + test: test, + callback: callback, + ), + ); + } +} + +class ResultBuilder extends StatelessWidget { + const ResultBuilder({ + Key? key, + this.test, + required this.callback, + }) : super(key: key); + + final String? test; + final void Function (BuildContext context) callback; + + @override build(BuildContext context) { + return Builder( + builder: (BuildContext context) { + try { + callback(context); + } on Exception catch (e) { + print('#l10n A(n) $e has occurred trying to generate "$test" results.'); + print('#l10n END'); + } + return Container(); + }, + ); + } +} + +class Home extends StatelessWidget { + @override + Widget build(BuildContext context) { + final List results = []; + return Row( + children: [ + LocaleBuilder( + test: 'supportedLocales', + callback: (BuildContext context) { + results.add('--- supportedLocales tests ---'); + int n = 0; + for (Locale locale in AppLocalizations.supportedLocales) { + String languageCode = locale.languageCode; + String? countryCode = locale.countryCode; + String? scriptCode = locale.scriptCode; + results.add('supportedLocales[$n]: languageCode: $languageCode, countryCode: $countryCode, scriptCode: $scriptCode'); + n += 1; + } + }, + ), + LocaleBuilder( + locale: Locale('en', 'CA'), + test: 'countryCode - en_CA', + callback: (BuildContext context) { + results.add('--- countryCode (en_CA) tests ---'); + results.add(AppLocalizations.of(context)!.helloWorld); + results.add(AppLocalizations.of(context)!.hello(world: "CA fallback World")); + }, + ), + LocaleBuilder( + locale: Locale('en', 'GB'), + test: 'countryCode - en_GB', + callback: (BuildContext context) { + results.add('--- countryCode (en_GB) tests ---'); + results.add(AppLocalizations.of(context)!.helloWorld); + results.add(AppLocalizations.of(context)!.hello(world: "GB fallback World")); + }, + ), + LocaleBuilder( + locale: Locale('zh'), + test: 'zh', + callback: (BuildContext context) { + results.add('--- zh ---'); + results.add(AppLocalizations.of(context)!.helloWorld); + results.add(AppLocalizations.of(context)!.helloWorlds(count: 0)); + results.add(AppLocalizations.of(context)!.helloWorlds(count: 1)); + results.add(AppLocalizations.of(context)!.helloWorlds(count: 2)); + // Should use the fallback language, in this case, + // "Hello 世界" should be displayed. + results.add(AppLocalizations.of(context)!.hello(world: "世界")); + // helloCost is tested in 'zh' because 'es' currency format contains a + // non-breaking space character (U+00A0), which if removed, + // makes it hard to decipher why the test is failing. + results.add(AppLocalizations.of(context)!.helloCost(price: "价钱", value: 123)); + }, + ), + LocaleBuilder( + locale: Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'), + test: 'zh', + callback: (BuildContext context) { + results.add('--- scriptCode: zh_Hans ---'); + results.add(AppLocalizations.of(context)!.helloWorld); + }, + ), + LocaleBuilder( + locale: Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), + test: 'scriptCode - zh_Hant', + callback: (BuildContext context) { + results.add('--- scriptCode - zh_Hant ---'); + results.add(AppLocalizations.of(context)!.helloWorld); + }, + ), + LocaleBuilder( + locale: Locale.fromSubtags(languageCode: 'zh', countryCode: 'TW', scriptCode: 'Hant'), + test: 'scriptCode - zh_TW_Hant', + callback: (BuildContext context) { + results.add('--- scriptCode - zh_Hant_TW ---'); + results.add(AppLocalizations.of(context)!.helloWorld); + }, + ), + LocaleBuilder( + locale: Locale('en'), + test: 'General formatting', + callback: (BuildContext context) { + results.add('--- General formatting tests ---'); + final AppLocalizations localizations = AppLocalizations.of(context)!; + results.addAll([ + '${localizations.helloWorld}', + '${localizations.helloNewlineWorld}', + '${localizations.testDollarSign}', + '${localizations.hello(world: "World")}', + '${localizations.greeting(hello: "Hello", world: "World")}', + '${localizations.helloWorldOn(date: DateTime(1960))}', + '${localizations.helloOn(world: "world argument", date: DateTime(1960), time: DateTime(1960))}', + '${localizations.helloWorldDuring(startDate: DateTime(1960), endDate: DateTime(2020))}', + '${localizations.helloFor(value: 123)}', + '${localizations.helloCost(price: "price", value: 123)}', + '${localizations.helloCostWithOptionalParam(price: "price", value: .5)}', + '${localizations.helloCostWithSpecialCharacter1(price: "price", value: .5)}', + '${localizations.helloCostWithSpecialCharacter2(price: "price", value: .5)}', + '${localizations.helloCostWithSpecialCharacter3(price: "price", value: .5)}', + '${localizations.helloDecimalPattern(value: 1200000)}', + '${localizations.helloPercentPattern(value: 1200000)}', + '${localizations.helloScientificPattern(value: 1200000)}', + '${localizations.helloWorlds(count: 0)}', + '${localizations.helloWorlds(count: 1)}', + '${localizations.helloWorlds(count: 2)}', + '${localizations.helloAdjectiveWorlds(count: 0, adjective: "new")}', + '${localizations.helloAdjectiveWorlds(count: 1, adjective: "new")}', + '${localizations.helloAdjectiveWorlds(count: 2, adjective: "new")}', + '${localizations.helloWorldsOn(count: 0, date: DateTime(1960))}', + '${localizations.helloWorldsOn(count: 1, date: DateTime(1960))}', + '${localizations.helloWorldsOn(count: 2, date: DateTime(1960))}', + '${localizations.helloWorldPopulation(count: 0, population: 100)}', + '${localizations.helloWorldPopulation(count: 1, population: 101)}', + '${localizations.helloWorldPopulation(count: 2, population: 102)}', + '${localizations.helloWorldsInterpolation(count: 123, hello: "Hello", world: "World")}', + '${localizations.dollarSign}', + '${localizations.dollarSignPlural(count: 1)}', + '${localizations.singleQuote}', + '${localizations.singleQuotePlural(count: 2)}', + '${localizations.doubleQuote}', + '${localizations.doubleQuotePlural(count: 2)}', + "${localizations.vehicleSelect(vehicleType: 'truck')}", + "${localizations.singleQuoteSelect(vehicleType: 'sedan')}", + "${localizations.doubleQuoteSelect(vehicleType: 'cabriolet')}", + "${localizations.pluralInString(count: 1)}", + "${localizations.selectInString(gender: 'he')}", + "${localizations.selectWithPlaceholder(gender: 'male', preference: 'ice cream')}", + "${localizations.selectWithPlaceholder(gender: 'female', preference: 'chocolate')}", + "${localizations.selectInPlural(gender: 'male', count: 1)}", + "${localizations.selectInPlural(gender: 'male', count: 2)}", + "${localizations.selectInPlural(gender: 'female', count: 1)}", + '${localizations.datetime1(today: DateTime(2023, 6, 26))}', + '${localizations.datetime2(current: DateTime(2023, 6, 26, 5, 23))}', + ]); + }, + ), + LocaleBuilder( + locale: Locale('es'), + test: '--- es ---', + callback: (BuildContext context) { + results.add('--- es ---'); + final AppLocalizations localizations = AppLocalizations.of(context)!; + results.addAll([ + '${localizations.helloWorld}', + '${localizations.helloNewlineWorld}', + '${localizations.testDollarSign}', + '${localizations.hello(world: "Mundo")}', + '${localizations.greeting(hello: "Hola", world: "Mundo")}', + '${localizations.helloWorldOn(date: DateTime(1960))}', + '${localizations.helloOn(world: "world argument", date: DateTime(1960), time: DateTime(1960))}', + '${localizations.helloWorldDuring(startDate: DateTime(1960), endDate: DateTime(2020))}', + '${localizations.helloFor(value: 123)}', + // helloCost is tested in 'zh' because 'es' currency format contains a + // non-breaking space character (U+00A0), which if removed, + // makes it hard to decipher why the test is failing. + '${localizations.helloWorlds(count: 0)}', + '${localizations.helloWorlds(count: 1)}', + '${localizations.helloWorlds(count: 2)}', + '${localizations.helloAdjectiveWorlds(count: 0, adjective: "nuevo")}', + '${localizations.helloAdjectiveWorlds(count: 1, adjective: "nuevo")}', + '${localizations.helloAdjectiveWorlds(count: 2, adjective: "nuevo")}', + '${localizations.helloWorldsOn(count: 0, date: DateTime(1960))}', + '${localizations.helloWorldsOn(count: 1, date: DateTime(1960))}', + '${localizations.helloWorldsOn(count: 2, date: DateTime(1960))}', + '${localizations.helloWorldPopulation(count: 0, population: 100)}', + '${localizations.helloWorldPopulation(count: 1, population: 101)}', + '${localizations.helloWorldPopulation(count: 2, population: 102)}', + '${localizations.helloWorldsInterpolation(count: 123, hello: "Hola", world: "Mundo")}', + '${localizations.dollarSign}', + '${localizations.dollarSignPlural(count: 1)}', + '${localizations.singleQuote}', + '${localizations.singleQuotePlural(count: 2)}', + '${localizations.doubleQuote}', + '${localizations.doubleQuotePlural(count: 2)}', + "${localizations.vehicleSelect(vehicleType: 'truck')}", + "${localizations.singleQuoteSelect(vehicleType: 'sedan')}", + "${localizations.doubleQuoteSelect(vehicleType: 'cabriolet')}", + "${localizations.pluralInString(count: 1)}", + "${localizations.selectInString(gender: 'he')}", + ]); + }, + ), + LocaleBuilder( + locale: Locale.fromSubtags(languageCode: 'es', countryCode: '419'), + test: 'countryCode - es_419', + callback: (BuildContext context) { + results.add('--- es_419 ---'); + final AppLocalizations localizations = AppLocalizations.of(context)!; + results.addAll([ + '${localizations.helloWorld}', + '${localizations.helloWorlds(count: 0)}', + '${localizations.helloWorlds(count: 1)}', + '${localizations.helloWorlds(count: 2)}', + ]); + }, + ), + LocaleBuilder( + callback: (BuildContext context) { + try { + int n = 0; + for (final String result in results) { + // Newline character replacement is necessary because + // the stream breaks up stdout by new lines. + print('#l10n $n (${result.replaceAll('\n', '_NEWLINE_')})'); + n += 1; + } + } + finally { + print('#l10n END'); + } + }, + ), + ], + ); + } +} + +void main() { + runApp( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Home(), + ), + ); +}'''; + String l10nYaml({ required bool useDeferredLoading, required bool useSyntheticPackage, + required bool useNamedParameters, }) { String l10nYamlString = ''; @@ -791,6 +1086,10 @@ void main() { l10nYamlString += 'synthetic-package: false\n'; } + if (useNamedParameters) { + l10nYamlString += 'use-named-parameters: true\n'; + } + return l10nYamlString; } }