[gen_l10n] When localizing a message, prefer placeholder definitions defined by the current locale rather than the template locale (#153459)

- Fixes #153457
- Fixes #116716
This commit is contained in:
kzrnm 2024-11-15 06:48:14 +09:00 committed by GitHub
parent f35cd558f8
commit dca37ad17f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 453 additions and 43 deletions

View File

@ -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 // 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 // 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. // will be set to 'String'. For such placeholders that are used for both, we should throw an error.
List<String> generateMethodParameters(Message message, bool useNamedParameters) { List<String> generateMethodParameters(Message message, LocaleInfo? locale, bool useNamedParameters) {
return message.placeholders.values.map((Placeholder placeholder) {
// Check the compatibility of template placeholders and locale placeholders.
final Map<String, Placeholder>? localePlaceholders = message.localePlaceholders[locale];
return message.templatePlaceholders.entries.map((MapEntry<String, Placeholder> 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}'; return '${useNamedParameters ? 'required ' : ''}${placeholder.type} ${placeholder.name}';
}).toList(); }).toList();
} }
// Similar to above, but is used for passing arguments into helper functions. // Similar to above, but is used for passing arguments into helper functions.
List<String> generateMethodArguments(Message message) { List<String> 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) { String generateDateFormattingLogic(Message message, LocaleInfo locale) {
if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting) { if (message.templatePlaceholders.isEmpty) {
return '@(none)'; return '@(none)';
} }
final Iterable<String> formatStatements = message.placeholders.values final Iterable<String> formatStatements = message.getPlaceholders(locale)
.where((Placeholder placeholder) => placeholder.requiresDateFormatting) .where((Placeholder placeholder) => placeholder.requiresDateFormatting)
.map((Placeholder placeholder) { .map((Placeholder placeholder) {
final String? placeholderFormat = placeholder.format; final String? placeholderFormat = placeholder.format;
@ -177,12 +190,12 @@ String generateDateFormattingLogic(Message message) {
return formatStatements.isEmpty ? '@(none)' : formatStatements.join(); return formatStatements.isEmpty ? '@(none)' : formatStatements.join();
} }
String generateNumberFormattingLogic(Message message) { String generateNumberFormattingLogic(Message message, LocaleInfo locale) {
if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting) { if (message.templatePlaceholders.isEmpty) {
return '@(none)'; return '@(none)';
} }
final Iterable<String> formatStatements = message.placeholders.values final Iterable<String> formatStatements = message.getPlaceholders(locale)
.where((Placeholder placeholder) => placeholder.requiresNumFormatting) .where((Placeholder placeholder) => placeholder.requiresNumFormatting)
.map((Placeholder placeholder) { .map((Placeholder placeholder) {
final String? placeholderFormat = placeholder.format; final String? placeholderFormat = placeholder.format;
@ -242,12 +255,12 @@ String generateBaseClassMethod(Message message, LocaleInfo? templateArbLocale, b
/// In $templateArbLocale, this message translates to: /// In $templateArbLocale, this message translates to:
/// **'${generateString(message.value)}'**'''; /// **'${generateString(message.value)}'**''';
if (message.placeholders.isNotEmpty) { if (message.templatePlaceholders.isNotEmpty) {
return (useNamedParameters ? baseClassMethodWithNamedParameterTemplate : baseClassMethodTemplate) return (useNamedParameters ? baseClassMethodWithNamedParameterTemplate : baseClassMethodTemplate)
.replaceAll('@(comment)', comment) .replaceAll('@(comment)', comment)
.replaceAll('@(templateLocaleTranslationComment)', templateLocaleTranslationComment) .replaceAll('@(templateLocaleTranslationComment)', templateLocaleTranslationComment)
.replaceAll('@(name)', message.resourceId) .replaceAll('@(name)', message.resourceId)
.replaceAll('@(parameters)', generateMethodParameters(message, useNamedParameters).join(', ')); .replaceAll('@(parameters)', generateMethodParameters(message, null, useNamedParameters).join(', '));
} }
return baseClassGetterTemplate return baseClassGetterTemplate
.replaceAll('@(comment)', comment) .replaceAll('@(comment)', comment)
@ -610,10 +623,6 @@ class LocalizationsGenerator {
/// ['es', 'en'] is passed in, the 'es' locale will take priority over 'en'. /// ['es', 'en'] is passed in, the 'es' locale will take priority over 'en'.
final List<LocaleInfo> preferredSupportedLocales; final List<LocaleInfo> 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. // Whether we want to use escaping for ICU messages.
bool useEscaping = false; bool useEscaping = false;
@ -993,8 +1002,7 @@ class LocalizationsGenerator {
.replaceAll('@(fileName)', fileName) .replaceAll('@(fileName)', fileName)
.replaceAll('@(class)', '$className${locale.camelCase()}') .replaceAll('@(class)', '$className${locale.camelCase()}')
.replaceAll('@(localeName)', locale.toString()) .replaceAll('@(localeName)', locale.toString())
.replaceAll('@(methods)', methods.join('\n\n')) .replaceAll('@(methods)', methods.join('\n\n'));
.replaceAll('@(requiresIntlImport)', requiresIntlImport ? "import 'package:intl/intl.dart' as intl;\n\n" : '');
} }
String _generateSubclass( String _generateSubclass(
@ -1143,7 +1151,6 @@ class LocalizationsGenerator {
.replaceAll('@(messageClassImports)', sortedClassImports.join('\n')) .replaceAll('@(messageClassImports)', sortedClassImports.join('\n'))
.replaceAll('@(delegateClass)', delegateClass) .replaceAll('@(delegateClass)', delegateClass)
.replaceAll('@(requiresFoundationImport)', useDeferredLoading ? '' : "import 'package:flutter/foundation.dart';") .replaceAll('@(requiresFoundationImport)', useDeferredLoading ? '' : "import 'package:flutter/foundation.dart';")
.replaceAll('@(requiresIntlImport)', requiresIntlImport ? "import 'package:intl/intl.dart' as intl;" : '')
.replaceAll('@(canBeNullable)', usesNullableGetter ? '?' : '') .replaceAll('@(canBeNullable)', usesNullableGetter ? '?' : '')
.replaceAll('@(needsNullCheck)', usesNullableGetter ? '' : '!') .replaceAll('@(needsNullCheck)', usesNullableGetter ? '' : '!')
// Removes all trailing whitespace from the generated file. // Removes all trailing whitespace from the generated file.
@ -1154,15 +1161,10 @@ class LocalizationsGenerator {
String _generateMethod(Message message, LocaleInfo locale) { String _generateMethod(Message message, LocaleInfo locale) {
try { try {
// Determine if we must import intl for date or number formatting.
if (message.placeholdersRequireFormatting) {
requiresIntlImport = true;
}
final String translationForMessage = message.messages[locale]!; final String translationForMessage = message.messages[locale]!;
final Node node = message.parsedMessages[locale]!; final Node node = message.parsedMessages[locale]!;
// If the placeholders list is empty, then return a getter method. // 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. // Use the parsed translation to handle escaping with the same behavior.
return getterTemplate return getterTemplate
.replaceAll('@(name)', message.resourceId) .replaceAll('@(name)', message.resourceId)
@ -1196,14 +1198,13 @@ class LocalizationsGenerator {
case ST.placeholderExpr: case ST.placeholderExpr:
assert(node.children[1].type == ST.identifier); assert(node.children[1].type == ST.identifier);
final String identifier = node.children[1].value!; 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) { if (placeholder.requiresFormatting) {
return '\$${node.children[1].value}String'; return '\$${node.children[1].value}String';
} }
return '\$${node.children[1].value}'; return '\$${node.children[1].value}';
case ST.pluralExpr: case ST.pluralExpr:
requiresIntlImport = true;
final Map<String, String> pluralLogicArgs = <String, String>{}; final Map<String, String> pluralLogicArgs = <String, String>{};
// Recall that pluralExpr are of the form // Recall that pluralExpr are of the form
// pluralExpr := "{" ID "," "plural" "," pluralParts "}" // pluralExpr := "{" ID "," "plural" "," pluralParts "}"
@ -1259,7 +1260,6 @@ The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", "
return '\$$tempVarName'; return '\$$tempVarName';
case ST.selectExpr: case ST.selectExpr:
requiresIntlImport = true;
// Recall that pluralExpr are of the form // Recall that pluralExpr are of the form
// pluralExpr := "{" ID "," "plural" "," pluralParts "}" // pluralExpr := "{" ID "," "plural" "," pluralParts "}"
assert(node.children[1].type == ST.identifier); 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'; return '\$$tempVarName';
case ST.argumentExpr: case ST.argumentExpr:
requiresIntlImport = true;
assert(node.children[1].type == ST.identifier); assert(node.children[1].type == ST.identifier);
assert(node.children[3].type == ST.argType); assert(node.children[3].type == ST.argType);
assert(node.children[7].type == ST.identifier); 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'; final String tempVarLines = tempVariables.isEmpty ? '' : '${tempVariables.join('\n')}\n';
return (useNamedParameters ? methodWithNamedParameterTemplate : methodTemplate) return (useNamedParameters ? methodWithNamedParameterTemplate : methodTemplate)
.replaceAll('@(name)', message.resourceId) .replaceAll('@(name)', message.resourceId)
.replaceAll('@(parameters)', generateMethodParameters(message, useNamedParameters).join(', ')) .replaceAll('@(parameters)', generateMethodParameters(message, locale, useNamedParameters).join(', '))
.replaceAll('@(dateFormatting)', generateDateFormattingLogic(message)) .replaceAll('@(dateFormatting)', generateDateFormattingLogic(message, locale))
.replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message)) .replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message, locale))
.replaceAll('@(tempVars)', tempVarLines) .replaceAll('@(tempVars)', tempVarLines)
.replaceAll('@(message)', messageString) .replaceAll('@(message)', messageString)
.replaceAll('@(none)\n', ''); .replaceAll('@(none)\n', '');

View File

@ -171,7 +171,9 @@ const String dateVariableTemplate = '''
String @(varName) = intl.DateFormat.@(formatType)(localeName).format(@(argument));'''; String @(varName) = intl.DateFormat.@(formatType)(localeName).format(@(argument));''';
const String classFileTemplate = ''' 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 // ignore_for_file: type=lint

View File

@ -347,7 +347,8 @@ class Message {
) : assert(resourceId.isNotEmpty), ) : assert(resourceId.isNotEmpty),
value = _value(templateBundle.resources, resourceId), value = _value(templateBundle.resources, resourceId),
description = _description(templateBundle.resources, resourceId, isResourceAttributeRequired), description = _description(templateBundle.resources, resourceId, isResourceAttributeRequired),
placeholders = _placeholders(templateBundle.resources, resourceId, isResourceAttributeRequired), templatePlaceholders = _placeholders(templateBundle.resources, resourceId, isResourceAttributeRequired),
localePlaceholders = <LocaleInfo, Map<String, Placeholder>>{},
messages = <LocaleInfo, String?>{}, messages = <LocaleInfo, String?>{},
parsedMessages = <LocaleInfo, Node?>{} { parsedMessages = <LocaleInfo, Node?>{} {
// Filenames for error handling. // Filenames for error handling.
@ -357,9 +358,14 @@ class Message {
filenames[bundle.locale] = bundle.file.basename; filenames[bundle.locale] = bundle.file.basename;
final String? translation = bundle.translationFor(resourceId); final String? translation = bundle.translationFor(resourceId);
messages[bundle.locale] = translation; messages[bundle.locale] = translation;
localePlaceholders[bundle.locale] = templateBundle.locale == bundle.locale
? templatePlaceholders
: _placeholders(bundle.resources, resourceId, false);
List<String>? validPlaceholders; List<String>? validPlaceholders;
if (useRelaxedSyntax) { if (useRelaxedSyntax) {
validPlaceholders = placeholders.entries.map((MapEntry<String, Placeholder> e) => e.key).toList(); validPlaceholders = templatePlaceholders.entries.map((MapEntry<String, Placeholder> e) => e.key).toList();
} }
try { try {
parsedMessages[bundle.locale] = translation == null ? null : Parser( parsedMessages[bundle.locale] = translation == null ? null : Parser(
@ -378,7 +384,7 @@ class Message {
} }
} }
// Infer the placeholders // Infer the placeholders
_inferPlaceholders(filenames); _inferPlaceholders();
} }
final String resourceId; final String resourceId;
@ -386,13 +392,21 @@ class Message {
final String? description; final String? description;
late final Map<LocaleInfo, String?> messages; late final Map<LocaleInfo, String?> messages;
final Map<LocaleInfo, Node?> parsedMessages; final Map<LocaleInfo, Node?> parsedMessages;
final Map<String, Placeholder> placeholders; final Map<LocaleInfo, Map<String, Placeholder>> localePlaceholders;
final Map<String, Placeholder> templatePlaceholders;
final bool useEscaping; final bool useEscaping;
final bool useRelaxedSyntax; final bool useRelaxedSyntax;
final Logger? logger; final Logger? logger;
bool hadErrors = false; bool hadErrors = false;
bool get placeholdersRequireFormatting => placeholders.values.any((Placeholder p) => p.requiresFormatting); Iterable<Placeholder> getPlaceholders(LocaleInfo locale) {
final Map<String, Placeholder>? placeholders = localePlaceholders[locale];
if (placeholders == null) {
return templatePlaceholders.values;
}
return templatePlaceholders.values
.map((Placeholder templatePlaceholder) => placeholders[templatePlaceholder.name] ?? templatePlaceholder);
}
static String _value(Map<String, Object?> bundle, String resourceId) { static String _value(Map<String, Object?> bundle, String resourceId) {
final Object? value = bundle[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. // Using parsed translations, attempt to infer types of placeholders used by plurals and selects.
// For undeclared placeholders, create a new placeholder. // For undeclared placeholders, create a new placeholder.
void _inferPlaceholders(Map<LocaleInfo, String> filenames) { void _inferPlaceholders() {
// We keep the undeclared placeholders separate so that we can sort them alphabetically afterwards. // We keep the undeclared placeholders separate so that we can sort them alphabetically afterwards.
final Map<String, Placeholder> undeclaredPlaceholders = <String, Placeholder>{}; final Map<String, Placeholder> undeclaredPlaceholders = <String, Placeholder>{};
// Helper for getting placeholder by name. // Helper for getting placeholder by name.
Placeholder? getPlaceholder(String name) => placeholders[name] ?? undeclaredPlaceholders[name];
for (final LocaleInfo locale in parsedMessages.keys) { for (final LocaleInfo locale in parsedMessages.keys) {
Placeholder? getPlaceholder(String name) =>
localePlaceholders[locale]?[name] ??
templatePlaceholders[name] ??
undeclaredPlaceholders[name];
if (parsedMessages[locale] == null) { if (parsedMessages[locale] == null) {
continue; continue;
} }
@ -529,7 +546,7 @@ class Message {
traversalStack.addAll(node.children); traversalStack.addAll(node.children);
} }
} }
placeholders.addEntries( templatePlaceholders.addEntries(
undeclaredPlaceholders.entries undeclaredPlaceholders.entries
.toList() .toList()
..sort((MapEntry<String, Placeholder> p1, MapEntry<String, Placeholder> p2) => p1.key.compareTo(p2.key)) ..sort((MapEntry<String, Placeholder> p1, MapEntry<String, Placeholder> p2) => p1.key.compareTo(p2.key))
@ -542,7 +559,7 @@ class Message {
|| !x && !y && !z; || !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)) { if (!atMostOneOf(placeholder.isPlural, placeholder.isDateTime, placeholder.isSelect)) {
throw L10nException('Placeholder is used as plural/select/datetime in certain languages.'); throw L10nException('Placeholder is used as plural/select/datetime in certain languages.');
} else if (placeholder.isPlural) { } else if (placeholder.isPlural) {

View File

@ -792,6 +792,8 @@ flutter:
expect(fs.file('/lib/l10n/bar_en.dart').readAsStringSync(), ''' expect(fs.file('/lib/l10n/bar_en.dart').readAsStringSync(), '''
HEADER HEADER
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'bar.dart'; import 'bar.dart';
// ignore_for_file: type=lint // ignore_for_file: type=lint
@ -894,6 +896,8 @@ flutter:\r
); );
expect(fs.file('/lib/l10n/app_localizations_en.dart').readAsStringSync(), ''' expect(fs.file('/lib/l10n/app_localizations_en.dart').readAsStringSync(), '''
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart'; import 'app_localizations.dart';
// ignore_for_file: type=lint // ignore_for_file: type=lint
@ -927,6 +931,8 @@ class AppLocalizationsEn extends AppLocalizations {
expect(fs.file('/lib/l10n/app_localizations_en.dart').readAsStringSync(), ''' expect(fs.file('/lib/l10n/app_localizations_en.dart').readAsStringSync(), '''
HEADER HEADER
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart'; import 'app_localizations.dart';
// ignore_for_file: type=lint // 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(<String, String>{
'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(<String, String>{
'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(<String, String>{
'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(<String, String>{
'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<L10nException>().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(<String, String>{
'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<L10nException>().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(<String, String>{
'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(<String, String>{
'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', () { group('NumberFormat tests', () {
@ -1766,7 +2036,7 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
'en': singleMessageArbFileString, 'en': singleMessageArbFileString,
'es': singleEsMessageArbFileString, 'es': singleEsMessageArbFileString,
}); });
expect(getGeneratedFileContent(locale: 'es'), isNot(contains(intlImportDartCode))); expect(getGeneratedFileContent(locale: 'es'), contains(intlImportDartCode));
}); });
testWithoutContext('warnings are generated when plural parts are repeated', () { 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(<String, String>{
'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(<String, String>{
'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(<String, String>{
'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', () { 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.'; 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', () { testWithoutContext('app localizations lookup is a public method', () {