[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:
parent
f35cd558f8
commit
dca37ad17f
@ -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', '');
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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', () {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user