From dca37ad17ff0354924f9b5bbbcbb2ce6ed819230 Mon Sep 17 00:00:00 2001 From: kzrnm Date: Fri, 15 Nov 2024 06:48:14 +0900 Subject: [PATCH] [gen_l10n] When localizing a message, prefer placeholder definitions defined by the current locale rather than the template locale (#153459) - Fixes #153457 - Fixes #116716 --- .../lib/src/localizations/gen_l10n.dart | 61 ++- .../src/localizations/gen_l10n_templates.dart | 4 +- .../lib/src/localizations/gen_l10n_types.dart | 35 +- .../generate_localizations_test.dart | 396 +++++++++++++++++- 4 files changed, 453 insertions(+), 43 deletions(-) diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart index 781602346f..0ef68cff42 100644 --- a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart +++ b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart @@ -123,23 +123,36 @@ 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, bool useNamedParameters) { - return message.placeholders.values.map((Placeholder placeholder) { +List generateMethodParameters(Message message, LocaleInfo? locale, bool useNamedParameters) { + + // Check the compatibility of template placeholders and locale placeholders. + final Map? localePlaceholders = message.localePlaceholders[locale]; + + return message.templatePlaceholders.entries.map((MapEntry e) { + final Placeholder placeholder = e.value; + final Placeholder? localePlaceholder = localePlaceholders?[e.key]; + if (localePlaceholder != null && placeholder.type != localePlaceholder.type) { + throw L10nException( + 'The placeholder, ${placeholder.name}, has its "type" resource attribute set to ' + 'the "${localePlaceholder.type}" type in locale "$locale", but it is "${placeholder.type}" ' + 'in the template placeholder. For compatibility with template placeholder, change ' + 'the "type" attribute to "${placeholder.type}".'); + } return '${useNamedParameters ? 'required ' : ''}${placeholder.type} ${placeholder.name}'; }).toList(); } // Similar to above, but is used for passing arguments into helper functions. List generateMethodArguments(Message message) { - return message.placeholders.values.map((Placeholder placeholder) => placeholder.name).toList(); + return message.templatePlaceholders.values.map((Placeholder placeholder) => placeholder.name).toList(); } -String generateDateFormattingLogic(Message message) { - if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting) { +String generateDateFormattingLogic(Message message, LocaleInfo locale) { + if (message.templatePlaceholders.isEmpty) { return '@(none)'; } - final Iterable formatStatements = message.placeholders.values + final Iterable formatStatements = message.getPlaceholders(locale) .where((Placeholder placeholder) => placeholder.requiresDateFormatting) .map((Placeholder placeholder) { final String? placeholderFormat = placeholder.format; @@ -177,12 +190,12 @@ String generateDateFormattingLogic(Message message) { return formatStatements.isEmpty ? '@(none)' : formatStatements.join(); } -String generateNumberFormattingLogic(Message message) { - if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting) { +String generateNumberFormattingLogic(Message message, LocaleInfo locale) { + if (message.templatePlaceholders.isEmpty) { return '@(none)'; } - final Iterable formatStatements = message.placeholders.values + final Iterable formatStatements = message.getPlaceholders(locale) .where((Placeholder placeholder) => placeholder.requiresNumFormatting) .map((Placeholder placeholder) { final String? placeholderFormat = placeholder.format; @@ -242,12 +255,12 @@ String generateBaseClassMethod(Message message, LocaleInfo? templateArbLocale, b /// In $templateArbLocale, this message translates to: /// **'${generateString(message.value)}'**'''; - if (message.placeholders.isNotEmpty) { + if (message.templatePlaceholders.isNotEmpty) { return (useNamedParameters ? baseClassMethodWithNamedParameterTemplate : baseClassMethodTemplate) .replaceAll('@(comment)', comment) .replaceAll('@(templateLocaleTranslationComment)', templateLocaleTranslationComment) .replaceAll('@(name)', message.resourceId) - .replaceAll('@(parameters)', generateMethodParameters(message, useNamedParameters).join(', ')); + .replaceAll('@(parameters)', generateMethodParameters(message, null, useNamedParameters).join(', ')); } return baseClassGetterTemplate .replaceAll('@(comment)', comment) @@ -610,10 +623,6 @@ class LocalizationsGenerator { /// ['es', 'en'] is passed in, the 'es' locale will take priority over 'en'. final List preferredSupportedLocales; - // Whether we need to import intl or not. This flag is updated after parsing - // all of the messages. - bool requiresIntlImport = false; - // Whether we want to use escaping for ICU messages. bool useEscaping = false; @@ -993,8 +1002,7 @@ class LocalizationsGenerator { .replaceAll('@(fileName)', fileName) .replaceAll('@(class)', '$className${locale.camelCase()}') .replaceAll('@(localeName)', locale.toString()) - .replaceAll('@(methods)', methods.join('\n\n')) - .replaceAll('@(requiresIntlImport)', requiresIntlImport ? "import 'package:intl/intl.dart' as intl;\n\n" : ''); + .replaceAll('@(methods)', methods.join('\n\n')); } String _generateSubclass( @@ -1143,7 +1151,6 @@ class LocalizationsGenerator { .replaceAll('@(messageClassImports)', sortedClassImports.join('\n')) .replaceAll('@(delegateClass)', delegateClass) .replaceAll('@(requiresFoundationImport)', useDeferredLoading ? '' : "import 'package:flutter/foundation.dart';") - .replaceAll('@(requiresIntlImport)', requiresIntlImport ? "import 'package:intl/intl.dart' as intl;" : '') .replaceAll('@(canBeNullable)', usesNullableGetter ? '?' : '') .replaceAll('@(needsNullCheck)', usesNullableGetter ? '' : '!') // Removes all trailing whitespace from the generated file. @@ -1154,15 +1161,10 @@ class LocalizationsGenerator { String _generateMethod(Message message, LocaleInfo locale) { try { - // Determine if we must import intl for date or number formatting. - if (message.placeholdersRequireFormatting) { - requiresIntlImport = true; - } - final String translationForMessage = message.messages[locale]!; final Node node = message.parsedMessages[locale]!; // If the placeholders list is empty, then return a getter method. - if (message.placeholders.isEmpty) { + if (message.templatePlaceholders.isEmpty) { // Use the parsed translation to handle escaping with the same behavior. return getterTemplate .replaceAll('@(name)', message.resourceId) @@ -1196,14 +1198,13 @@ class LocalizationsGenerator { case ST.placeholderExpr: assert(node.children[1].type == ST.identifier); final String identifier = node.children[1].value!; - final Placeholder placeholder = message.placeholders[identifier]!; + final Placeholder placeholder = message.localePlaceholders[locale]?[identifier] ?? message.templatePlaceholders[identifier]!; if (placeholder.requiresFormatting) { return '\$${node.children[1].value}String'; } return '\$${node.children[1].value}'; case ST.pluralExpr: - requiresIntlImport = true; final Map pluralLogicArgs = {}; // Recall that pluralExpr are of the form // pluralExpr := "{" ID "," "plural" "," pluralParts "}" @@ -1259,7 +1260,6 @@ The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", " return '\$$tempVarName'; case ST.selectExpr: - requiresIntlImport = true; // Recall that pluralExpr are of the form // pluralExpr := "{" ID "," "plural" "," pluralParts "}" assert(node.children[1].type == ST.identifier); @@ -1284,7 +1284,6 @@ The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", " ); return '\$$tempVarName'; case ST.argumentExpr: - requiresIntlImport = true; assert(node.children[1].type == ST.identifier); assert(node.children[3].type == ST.argType); assert(node.children[7].type == ST.identifier); @@ -1320,9 +1319,9 @@ The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", " final String tempVarLines = tempVariables.isEmpty ? '' : '${tempVariables.join('\n')}\n'; return (useNamedParameters ? methodWithNamedParameterTemplate : methodTemplate) .replaceAll('@(name)', message.resourceId) - .replaceAll('@(parameters)', generateMethodParameters(message, useNamedParameters).join(', ')) - .replaceAll('@(dateFormatting)', generateDateFormattingLogic(message)) - .replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message)) + .replaceAll('@(parameters)', generateMethodParameters(message, locale, useNamedParameters).join(', ')) + .replaceAll('@(dateFormatting)', generateDateFormattingLogic(message, locale)) + .replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message, locale)) .replaceAll('@(tempVars)', tempVarLines) .replaceAll('@(message)', messageString) .replaceAll('@(none)\n', ''); 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 db87cecf14..3f15740c46 100644 --- a/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart +++ b/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart @@ -171,7 +171,9 @@ const String dateVariableTemplate = ''' String @(varName) = intl.DateFormat.@(formatType)(localeName).format(@(argument));'''; const String classFileTemplate = ''' -@(header)@(requiresIntlImport)import '@(fileName)'; +@(header)// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import '@(fileName)'; // ignore_for_file: type=lint diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart index e6d78c1128..2bae97f0f8 100644 --- a/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart +++ b/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart @@ -347,7 +347,8 @@ class Message { ) : assert(resourceId.isNotEmpty), value = _value(templateBundle.resources, resourceId), description = _description(templateBundle.resources, resourceId, isResourceAttributeRequired), - placeholders = _placeholders(templateBundle.resources, resourceId, isResourceAttributeRequired), + templatePlaceholders = _placeholders(templateBundle.resources, resourceId, isResourceAttributeRequired), + localePlaceholders = >{}, messages = {}, parsedMessages = {} { // Filenames for error handling. @@ -357,9 +358,14 @@ class Message { filenames[bundle.locale] = bundle.file.basename; final String? translation = bundle.translationFor(resourceId); messages[bundle.locale] = translation; + + localePlaceholders[bundle.locale] = templateBundle.locale == bundle.locale + ? templatePlaceholders + : _placeholders(bundle.resources, resourceId, false); + List? validPlaceholders; if (useRelaxedSyntax) { - validPlaceholders = placeholders.entries.map((MapEntry e) => e.key).toList(); + validPlaceholders = templatePlaceholders.entries.map((MapEntry e) => e.key).toList(); } try { parsedMessages[bundle.locale] = translation == null ? null : Parser( @@ -378,7 +384,7 @@ class Message { } } // Infer the placeholders - _inferPlaceholders(filenames); + _inferPlaceholders(); } final String resourceId; @@ -386,13 +392,21 @@ class Message { final String? description; late final Map messages; final Map parsedMessages; - final Map placeholders; + final Map> localePlaceholders; + final Map templatePlaceholders; final bool useEscaping; final bool useRelaxedSyntax; final Logger? logger; bool hadErrors = false; - bool get placeholdersRequireFormatting => placeholders.values.any((Placeholder p) => p.requiresFormatting); + Iterable getPlaceholders(LocaleInfo locale) { + final Map? placeholders = localePlaceholders[locale]; + if (placeholders == null) { + return templatePlaceholders.values; + } + return templatePlaceholders.values + .map((Placeholder templatePlaceholder) => placeholders[templatePlaceholder.name] ?? templatePlaceholder); + } static String _value(Map bundle, String resourceId) { final Object? value = bundle[resourceId]; @@ -488,12 +502,15 @@ class Message { // Using parsed translations, attempt to infer types of placeholders used by plurals and selects. // For undeclared placeholders, create a new placeholder. - void _inferPlaceholders(Map filenames) { + void _inferPlaceholders() { // We keep the undeclared placeholders separate so that we can sort them alphabetically afterwards. final Map undeclaredPlaceholders = {}; // Helper for getting placeholder by name. - Placeholder? getPlaceholder(String name) => placeholders[name] ?? undeclaredPlaceholders[name]; for (final LocaleInfo locale in parsedMessages.keys) { + Placeholder? getPlaceholder(String name) => + localePlaceholders[locale]?[name] ?? + templatePlaceholders[name] ?? + undeclaredPlaceholders[name]; if (parsedMessages[locale] == null) { continue; } @@ -529,7 +546,7 @@ class Message { traversalStack.addAll(node.children); } } - placeholders.addEntries( + templatePlaceholders.addEntries( undeclaredPlaceholders.entries .toList() ..sort((MapEntry p1, MapEntry p2) => p1.key.compareTo(p2.key)) @@ -542,7 +559,7 @@ class Message { || !x && !y && !z; } - for (final Placeholder placeholder in placeholders.values) { + for (final Placeholder placeholder in templatePlaceholders.values) { if (!atMostOneOf(placeholder.isPlural, placeholder.isDateTime, placeholder.isSelect)) { throw L10nException('Placeholder is used as plural/select/datetime in certain languages.'); } else if (placeholder.isPlural) { 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 068bb6830f..55a33a294b 100644 --- a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart +++ b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart @@ -792,6 +792,8 @@ flutter: expect(fs.file('/lib/l10n/bar_en.dart').readAsStringSync(), ''' HEADER +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'bar.dart'; // ignore_for_file: type=lint @@ -894,6 +896,8 @@ flutter:\r ); expect(fs.file('/lib/l10n/app_localizations_en.dart').readAsStringSync(), ''' +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -927,6 +931,8 @@ class AppLocalizationsEn extends AppLocalizations { expect(fs.file('/lib/l10n/app_localizations_en.dart').readAsStringSync(), ''' HEADER +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -1705,6 +1711,270 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e )), ); }); + + testWithoutContext('handle date with multiple locale', () { + setupLocalizations({ + 'en': ''' +{ + "@@locale": "en", + "springBegins": "Spring begins on {springStartDate}", + "@springBegins": { + "description": "The first day of spring", + "placeholders": { + "springStartDate": { + "type": "DateTime", + "format": "MMMd" + } + } + } +}''', + 'ja': ''' +{ + "@@locale": "ja", + "springBegins": "春が始まるのは{springStartDate}", + "@springBegins": { + "placeholders": { + "springStartDate": { + "type": "DateTime", + "format": "MMMMd" + } + } + } +}''' + }); + + expect(getGeneratedFileContent(locale: 'en'), contains('intl.DateFormat.MMMd(localeName)')); + expect(getGeneratedFileContent(locale: 'ja'), contains('intl.DateFormat.MMMMd(localeName)')); + expect(getGeneratedFileContent(locale: 'en'), contains('String springBegins(DateTime springStartDate)')); + expect(getGeneratedFileContent(locale: 'ja'), contains('String springBegins(DateTime springStartDate)')); + }); + + testWithoutContext('handle date with multiple locale when only template has placeholders', () { + setupLocalizations({ + 'en': ''' +{ + "@@locale": "en", + "springBegins": "Spring begins on {springStartDate}", + "@springBegins": { + "description": "The first day of spring", + "placeholders": { + "springStartDate": { + "type": "DateTime", + "format": "MMMd" + } + } + } +}''', + 'ja': ''' +{ + "@@locale": "ja", + "springBegins": "春が始まるのは{springStartDate}" +}''' + }); + + expect(getGeneratedFileContent(locale: 'en'), contains('intl.DateFormat.MMMd(localeName)')); + expect(getGeneratedFileContent(locale: 'ja'), contains('intl.DateFormat.MMMd(localeName)')); + expect(getGeneratedFileContent(locale: 'en'), contains('String springBegins(DateTime springStartDate)')); + expect(getGeneratedFileContent(locale: 'ja'), contains('String springBegins(DateTime springStartDate)')); + }); + + testWithoutContext('handle date with multiple locale when there is unused placeholder', () { + setupLocalizations({ + 'en': ''' +{ + "@@locale": "en", + "springBegins": "Spring begins on {springStartDate}", + "@springBegins": { + "description": "The first day of spring", + "placeholders": { + "springStartDate": { + "type": "DateTime", + "format": "MMMd" + } + } + } +}''', + 'ja': ''' +{ + "@@locale": "ja", + "springBegins": "春が始まるのは{springStartDate}", + "@springBegins": { + "description": "The first day of spring", + "placeholders": { + "notUsed": { + "type": "DateTime", + "format": "MMMMd" + } + } + } +}''' + }); + + expect(getGeneratedFileContent(locale: 'en'), contains('intl.DateFormat.MMMd(localeName)')); + expect(getGeneratedFileContent(locale: 'ja'), contains('intl.DateFormat.MMMd(localeName)')); + expect(getGeneratedFileContent(locale: 'en'), contains('String springBegins(DateTime springStartDate)')); + expect(getGeneratedFileContent(locale: 'ja'), contains('String springBegins(DateTime springStartDate)')); + expect(getGeneratedFileContent(locale: 'ja'), isNot(contains('notUsed'))); + }); + + testWithoutContext('handle date with multiple locale when placeholders are incompatible', () { + expect( + () { + setupLocalizations({ + 'en': ''' + { + "@@locale": "en", + "springBegins": "Spring begins on {springStartDate}", + "@springBegins": { + "description": "The first day of spring", + "placeholders": { + "springStartDate": { + "type": "DateTime", + "format": "MMMd" + } + } + } + }''', + 'ja': ''' + { + "@@locale": "ja", + "springBegins": "春が始まるのは{springStartDate}", + "@springBegins": { + "description": "The first day of spring", + "placeholders": { + "springStartDate": { + "type": "String" + } + } + } + }''' + }); + }, + throwsA(isA().having( + (L10nException e) => e.message, + 'message', + contains('The placeholder, springStartDate, has its "type" resource attribute set to the "String" type in locale "ja", but it is "DateTime" in the template placeholder.'), + )), + ); + }); + + testWithoutContext('handle date with multiple locale when non-template placeholder does not specify type', () { + expect( + () { + setupLocalizations({ + 'en': ''' + { + "@@locale": "en", + "springBegins": "Spring begins on {springStartDate}", + "@springBegins": { + "description": "The first day of spring", + "placeholders": { + "springStartDate": { + "type": "DateTime", + "format": "MMMd" + } + } + } + }''', + 'ja': ''' + { + "@@locale": "ja", + "springBegins": "春が始まるのは{springStartDate}", + "@springBegins": { + "description": "The first day of spring", + "placeholders": { + "springStartDate": { + "format": "MMMMd" + } + } + } + }''' + }); + }, + throwsA(isA().having( + (L10nException e) => e.message, + 'message', + contains('The placeholder, springStartDate, has its "type" resource attribute set to the "null" type in locale "ja", but it is "DateTime" in the template placeholder.'), + )), + ); + }); + + testWithoutContext('handle ordinary formatted date and arbitrary formatted date', () { + setupLocalizations({ + 'en': ''' +{ + "@@locale": "en", + "springBegins": "Spring begins on {springStartDate}", + "@springBegins": { + "description": "The first day of spring", + "placeholders": { + "springStartDate": { + "type": "DateTime", + "format": "MMMd" + } + } + } +}''', + 'ja': ''' +{ + "@@locale": "ja", + "springBegins": "春が始まるのは{springStartDate}", + "@springBegins": { + "placeholders": { + "springStartDate": { + "type": "DateTime", + "format": "立春", + "isCustomDateFormat": "true" + } + } + } +}''' + }); + + expect(getGeneratedFileContent(locale: 'en'), contains('intl.DateFormat.MMMd(localeName)')); + expect(getGeneratedFileContent(locale: 'ja'), contains(r"DateFormat('立春', localeName)")); + expect(getGeneratedFileContent(locale: 'en'), contains('String springBegins(DateTime springStartDate)')); + expect(getGeneratedFileContent(locale: 'ja'), contains('String springBegins(DateTime springStartDate)')); + }); + + testWithoutContext('handle arbitrary formatted date with multiple locale', () { + setupLocalizations({ + 'en': ''' +{ + "@@locale": "en", + "springBegins": "Spring begins on {springStartDate}", + "@springBegins": { + "description": "The first day of spring", + "placeholders": { + "springStartDate": { + "type": "DateTime", + "format": "asdf o'clock", + "isCustomDateFormat": "true" + } + } + } +}''', + 'ja': ''' +{ + "@@locale": "ja", + "springBegins": "春が始まるのは{springStartDate}", + "@springBegins": { + "placeholders": { + "springStartDate": { + "type": "DateTime", + "format": "立春", + "isCustomDateFormat": "true" + } + } + } +}''' + }); + + expect(getGeneratedFileContent(locale: 'en'), contains(r"DateFormat('asdf o\'clock', localeName)")); + expect(getGeneratedFileContent(locale: 'ja'), contains(r"DateFormat('立春', localeName)")); + expect(getGeneratedFileContent(locale: 'en'), contains('String springBegins(DateTime springStartDate)')); + expect(getGeneratedFileContent(locale: 'ja'), contains('String springBegins(DateTime springStartDate)')); + }); }); group('NumberFormat tests', () { @@ -1766,7 +2036,7 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e 'en': singleMessageArbFileString, 'es': singleEsMessageArbFileString, }); - expect(getGeneratedFileContent(locale: 'es'), isNot(contains(intlImportDartCode))); + expect(getGeneratedFileContent(locale: 'es'), contains(intlImportDartCode)); }); testWithoutContext('warnings are generated when plural parts are repeated', () { @@ -2425,6 +2695,128 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e )), ); }); + + testWithoutContext('handle number with multiple locale', () { + setupLocalizations({ + 'en': ''' +{ +"@@locale": "en", +"money": "Sum {number}", +"@money": { + "placeholders": { + "number": { + "type": "int", + "format": "currency" + } + } +} +}''', + 'ja': ''' +{ +"@@locale": "ja", +"money": "合計 {number}", +"@money": { + "placeholders": { + "number": { + "type": "int", + "format": "decimalPatternDigits", + "optionalParameters": { + "decimalDigits": 3 + } + } + } +} +}''' + }); + + expect(getGeneratedFileContent(locale: 'en'), contains('String money(int number)')); + expect(getGeneratedFileContent(locale: 'ja'), contains('String money(int number)')); + expect(getGeneratedFileContent(locale: 'en'), contains('intl.NumberFormat.currency(')); + expect(getGeneratedFileContent(locale: 'ja'), contains('intl.NumberFormat.decimalPatternDigits(')); + expect(getGeneratedFileContent(locale: 'ja'), contains('decimalDigits: 3')); + }); + + testWithoutContext('handle number with multiple locale specifying a format only in template', () { + setupLocalizations({ + 'en': ''' +{ +"@@locale": "en", +"money": "Sum {number}", +"@money": { + "placeholders": { + "number": { + "type": "int", + "format": "decimalPatternDigits", + "optionalParameters": { + "decimalDigits": 3 + } + } + } +} +}''', + 'ja': ''' +{ +"@@locale": "ja", +"money": "合計 {number}", +"@money": { + "placeholders": { + "number": { + "type": "int" + } + } +} +}''' + }); + + expect(getGeneratedFileContent(locale: 'en'), contains('String money(int number)')); + expect(getGeneratedFileContent(locale: 'ja'), contains('String money(int number)')); + expect(getGeneratedFileContent(locale: 'en'), contains('intl.NumberFormat.decimalPatternDigits(')); + expect(getGeneratedFileContent(locale: 'en'), contains('decimalDigits: 3')); + expect(getGeneratedFileContent(locale: 'en'), contains(r"return 'Sum $numberString'")); + expect(getGeneratedFileContent(locale: 'ja'), isNot(contains('intl.NumberFormat'))); + expect(getGeneratedFileContent(locale: 'ja'), contains(r"return '合計 $number'")); + }); + + testWithoutContext('handle number with multiple locale specifying a format only in non-template', () { + setupLocalizations({ + 'en': ''' +{ +"@@locale": "en", +"money": "Sum {number}", +"@money": { + "placeholders": { + "number": { + "type": "int" + } + } +} +}''', + 'ja': ''' +{ +"@@locale": "ja", +"money": "合計 {number}", +"@money": { + "placeholders": { + "number": { + "type": "int", + "format": "decimalPatternDigits", + "optionalParameters": { + "decimalDigits": 3 + } + } + } +} +}''' + }); + + expect(getGeneratedFileContent(locale: 'en'), contains('String money(int number)')); + expect(getGeneratedFileContent(locale: 'ja'), contains('String money(int number)')); + expect(getGeneratedFileContent(locale: 'en'), isNot(contains('intl.NumberFormat'))); + expect(getGeneratedFileContent(locale: 'en'), contains(r"return 'Sum $number'")); + expect(getGeneratedFileContent(locale: 'ja'), contains('intl.NumberFormat.decimalPatternDigits(')); + expect(getGeneratedFileContent(locale: 'ja'), contains('decimalDigits: 3')); + expect(getGeneratedFileContent(locale: 'ja'), contains(r"return '合計 $numberString'")); + }); }); testWithoutContext('should generate a valid pubspec.yaml file when using synthetic package if it does not already exist', () { @@ -2477,7 +2869,7 @@ String orderNumber(int number) { return 'This is order #$number.'; } ''')); - expect(getGeneratedFileContent(locale: 'en'), isNot(contains(intlImportDartCode))); + expect(getGeneratedFileContent(locale: 'en'), contains(intlImportDartCode)); }); testWithoutContext('app localizations lookup is a public method', () {