Skip to content

Commit dca37ad

Browse files
authored
[gen_l10n] When localizing a message, prefer placeholder definitions defined by the current locale rather than the template locale (#153459)
- Fixes #153457 - Fixes #116716
1 parent f35cd55 commit dca37ad

File tree

4 files changed

+453
-43
lines changed

4 files changed

+453
-43
lines changed

packages/flutter_tools/lib/src/localizations/gen_l10n.dart

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -123,23 +123,36 @@ String _syntheticL10nPackagePath(FileSystem fileSystem) => fileSystem.path.join(
123123
// For example, if placeholders are used for plurals and no type was specified, then the type will
124124
// automatically set to 'num'. Similarly, if such placeholders are used for selects, then the type
125125
// will be set to 'String'. For such placeholders that are used for both, we should throw an error.
126-
List<String> generateMethodParameters(Message message, bool useNamedParameters) {
127-
return message.placeholders.values.map((Placeholder placeholder) {
126+
List<String> generateMethodParameters(Message message, LocaleInfo? locale, bool useNamedParameters) {
127+
128+
// Check the compatibility of template placeholders and locale placeholders.
129+
final Map<String, Placeholder>? localePlaceholders = message.localePlaceholders[locale];
130+
131+
return message.templatePlaceholders.entries.map((MapEntry<String, Placeholder> e) {
132+
final Placeholder placeholder = e.value;
133+
final Placeholder? localePlaceholder = localePlaceholders?[e.key];
134+
if (localePlaceholder != null && placeholder.type != localePlaceholder.type) {
135+
throw L10nException(
136+
'The placeholder, ${placeholder.name}, has its "type" resource attribute set to '
137+
'the "${localePlaceholder.type}" type in locale "$locale", but it is "${placeholder.type}" '
138+
'in the template placeholder. For compatibility with template placeholder, change '
139+
'the "type" attribute to "${placeholder.type}".');
140+
}
128141
return '${useNamedParameters ? 'required ' : ''}${placeholder.type} ${placeholder.name}';
129142
}).toList();
130143
}
131144

132145
// Similar to above, but is used for passing arguments into helper functions.
133146
List<String> generateMethodArguments(Message message) {
134-
return message.placeholders.values.map((Placeholder placeholder) => placeholder.name).toList();
147+
return message.templatePlaceholders.values.map((Placeholder placeholder) => placeholder.name).toList();
135148
}
136149

137-
String generateDateFormattingLogic(Message message) {
138-
if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting) {
150+
String generateDateFormattingLogic(Message message, LocaleInfo locale) {
151+
if (message.templatePlaceholders.isEmpty) {
139152
return '@(none)';
140153
}
141154

142-
final Iterable<String> formatStatements = message.placeholders.values
155+
final Iterable<String> formatStatements = message.getPlaceholders(locale)
143156
.where((Placeholder placeholder) => placeholder.requiresDateFormatting)
144157
.map((Placeholder placeholder) {
145158
final String? placeholderFormat = placeholder.format;
@@ -177,12 +190,12 @@ String generateDateFormattingLogic(Message message) {
177190
return formatStatements.isEmpty ? '@(none)' : formatStatements.join();
178191
}
179192

180-
String generateNumberFormattingLogic(Message message) {
181-
if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting) {
193+
String generateNumberFormattingLogic(Message message, LocaleInfo locale) {
194+
if (message.templatePlaceholders.isEmpty) {
182195
return '@(none)';
183196
}
184197

185-
final Iterable<String> formatStatements = message.placeholders.values
198+
final Iterable<String> formatStatements = message.getPlaceholders(locale)
186199
.where((Placeholder placeholder) => placeholder.requiresNumFormatting)
187200
.map((Placeholder placeholder) {
188201
final String? placeholderFormat = placeholder.format;
@@ -242,12 +255,12 @@ String generateBaseClassMethod(Message message, LocaleInfo? templateArbLocale, b
242255
/// In $templateArbLocale, this message translates to:
243256
/// **'${generateString(message.value)}'**''';
244257

245-
if (message.placeholders.isNotEmpty) {
258+
if (message.templatePlaceholders.isNotEmpty) {
246259
return (useNamedParameters ? baseClassMethodWithNamedParameterTemplate : baseClassMethodTemplate)
247260
.replaceAll('@(comment)', comment)
248261
.replaceAll('@(templateLocaleTranslationComment)', templateLocaleTranslationComment)
249262
.replaceAll('@(name)', message.resourceId)
250-
.replaceAll('@(parameters)', generateMethodParameters(message, useNamedParameters).join(', '));
263+
.replaceAll('@(parameters)', generateMethodParameters(message, null, useNamedParameters).join(', '));
251264
}
252265
return baseClassGetterTemplate
253266
.replaceAll('@(comment)', comment)
@@ -610,10 +623,6 @@ class LocalizationsGenerator {
610623
/// ['es', 'en'] is passed in, the 'es' locale will take priority over 'en'.
611624
final List<LocaleInfo> preferredSupportedLocales;
612625

613-
// Whether we need to import intl or not. This flag is updated after parsing
614-
// all of the messages.
615-
bool requiresIntlImport = false;
616-
617626
// Whether we want to use escaping for ICU messages.
618627
bool useEscaping = false;
619628

@@ -993,8 +1002,7 @@ class LocalizationsGenerator {
9931002
.replaceAll('@(fileName)', fileName)
9941003
.replaceAll('@(class)', '$className${locale.camelCase()}')
9951004
.replaceAll('@(localeName)', locale.toString())
996-
.replaceAll('@(methods)', methods.join('\n\n'))
997-
.replaceAll('@(requiresIntlImport)', requiresIntlImport ? "import 'package:intl/intl.dart' as intl;\n\n" : '');
1005+
.replaceAll('@(methods)', methods.join('\n\n'));
9981006
}
9991007

10001008
String _generateSubclass(
@@ -1143,7 +1151,6 @@ class LocalizationsGenerator {
11431151
.replaceAll('@(messageClassImports)', sortedClassImports.join('\n'))
11441152
.replaceAll('@(delegateClass)', delegateClass)
11451153
.replaceAll('@(requiresFoundationImport)', useDeferredLoading ? '' : "import 'package:flutter/foundation.dart';")
1146-
.replaceAll('@(requiresIntlImport)', requiresIntlImport ? "import 'package:intl/intl.dart' as intl;" : '')
11471154
.replaceAll('@(canBeNullable)', usesNullableGetter ? '?' : '')
11481155
.replaceAll('@(needsNullCheck)', usesNullableGetter ? '' : '!')
11491156
// Removes all trailing whitespace from the generated file.
@@ -1154,15 +1161,10 @@ class LocalizationsGenerator {
11541161

11551162
String _generateMethod(Message message, LocaleInfo locale) {
11561163
try {
1157-
// Determine if we must import intl for date or number formatting.
1158-
if (message.placeholdersRequireFormatting) {
1159-
requiresIntlImport = true;
1160-
}
1161-
11621164
final String translationForMessage = message.messages[locale]!;
11631165
final Node node = message.parsedMessages[locale]!;
11641166
// If the placeholders list is empty, then return a getter method.
1165-
if (message.placeholders.isEmpty) {
1167+
if (message.templatePlaceholders.isEmpty) {
11661168
// Use the parsed translation to handle escaping with the same behavior.
11671169
return getterTemplate
11681170
.replaceAll('@(name)', message.resourceId)
@@ -1196,14 +1198,13 @@ class LocalizationsGenerator {
11961198
case ST.placeholderExpr:
11971199
assert(node.children[1].type == ST.identifier);
11981200
final String identifier = node.children[1].value!;
1199-
final Placeholder placeholder = message.placeholders[identifier]!;
1201+
final Placeholder placeholder = message.localePlaceholders[locale]?[identifier] ?? message.templatePlaceholders[identifier]!;
12001202
if (placeholder.requiresFormatting) {
12011203
return '\$${node.children[1].value}String';
12021204
}
12031205
return '\$${node.children[1].value}';
12041206

12051207
case ST.pluralExpr:
1206-
requiresIntlImport = true;
12071208
final Map<String, String> pluralLogicArgs = <String, String>{};
12081209
// Recall that pluralExpr are of the form
12091210
// pluralExpr := "{" ID "," "plural" "," pluralParts "}"
@@ -1259,7 +1260,6 @@ The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", "
12591260
return '\$$tempVarName';
12601261

12611262
case ST.selectExpr:
1262-
requiresIntlImport = true;
12631263
// Recall that pluralExpr are of the form
12641264
// pluralExpr := "{" ID "," "plural" "," pluralParts "}"
12651265
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", "
12841284
);
12851285
return '\$$tempVarName';
12861286
case ST.argumentExpr:
1287-
requiresIntlImport = true;
12881287
assert(node.children[1].type == ST.identifier);
12891288
assert(node.children[3].type == ST.argType);
12901289
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", "
13201319
final String tempVarLines = tempVariables.isEmpty ? '' : '${tempVariables.join('\n')}\n';
13211320
return (useNamedParameters ? methodWithNamedParameterTemplate : methodTemplate)
13221321
.replaceAll('@(name)', message.resourceId)
1323-
.replaceAll('@(parameters)', generateMethodParameters(message, useNamedParameters).join(', '))
1324-
.replaceAll('@(dateFormatting)', generateDateFormattingLogic(message))
1325-
.replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message))
1322+
.replaceAll('@(parameters)', generateMethodParameters(message, locale, useNamedParameters).join(', '))
1323+
.replaceAll('@(dateFormatting)', generateDateFormattingLogic(message, locale))
1324+
.replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message, locale))
13261325
.replaceAll('@(tempVars)', tempVarLines)
13271326
.replaceAll('@(message)', messageString)
13281327
.replaceAll('@(none)\n', '');

packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,9 @@ const String dateVariableTemplate = '''
171171
String @(varName) = intl.DateFormat.@(formatType)(localeName).format(@(argument));''';
172172

173173
const String classFileTemplate = '''
174-
@(header)@(requiresIntlImport)import '@(fileName)';
174+
@(header)// ignore: unused_import
175+
import 'package:intl/intl.dart' as intl;
176+
import '@(fileName)';
175177
176178
// ignore_for_file: type=lint
177179

packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,8 @@ class Message {
347347
) : assert(resourceId.isNotEmpty),
348348
value = _value(templateBundle.resources, resourceId),
349349
description = _description(templateBundle.resources, resourceId, isResourceAttributeRequired),
350-
placeholders = _placeholders(templateBundle.resources, resourceId, isResourceAttributeRequired),
350+
templatePlaceholders = _placeholders(templateBundle.resources, resourceId, isResourceAttributeRequired),
351+
localePlaceholders = <LocaleInfo, Map<String, Placeholder>>{},
351352
messages = <LocaleInfo, String?>{},
352353
parsedMessages = <LocaleInfo, Node?>{} {
353354
// Filenames for error handling.
@@ -357,9 +358,14 @@ class Message {
357358
filenames[bundle.locale] = bundle.file.basename;
358359
final String? translation = bundle.translationFor(resourceId);
359360
messages[bundle.locale] = translation;
361+
362+
localePlaceholders[bundle.locale] = templateBundle.locale == bundle.locale
363+
? templatePlaceholders
364+
: _placeholders(bundle.resources, resourceId, false);
365+
360366
List<String>? validPlaceholders;
361367
if (useRelaxedSyntax) {
362-
validPlaceholders = placeholders.entries.map((MapEntry<String, Placeholder> e) => e.key).toList();
368+
validPlaceholders = templatePlaceholders.entries.map((MapEntry<String, Placeholder> e) => e.key).toList();
363369
}
364370
try {
365371
parsedMessages[bundle.locale] = translation == null ? null : Parser(
@@ -378,21 +384,29 @@ class Message {
378384
}
379385
}
380386
// Infer the placeholders
381-
_inferPlaceholders(filenames);
387+
_inferPlaceholders();
382388
}
383389

384390
final String resourceId;
385391
final String value;
386392
final String? description;
387393
late final Map<LocaleInfo, String?> messages;
388394
final Map<LocaleInfo, Node?> parsedMessages;
389-
final Map<String, Placeholder> placeholders;
395+
final Map<LocaleInfo, Map<String, Placeholder>> localePlaceholders;
396+
final Map<String, Placeholder> templatePlaceholders;
390397
final bool useEscaping;
391398
final bool useRelaxedSyntax;
392399
final Logger? logger;
393400
bool hadErrors = false;
394401

395-
bool get placeholdersRequireFormatting => placeholders.values.any((Placeholder p) => p.requiresFormatting);
402+
Iterable<Placeholder> getPlaceholders(LocaleInfo locale) {
403+
final Map<String, Placeholder>? placeholders = localePlaceholders[locale];
404+
if (placeholders == null) {
405+
return templatePlaceholders.values;
406+
}
407+
return templatePlaceholders.values
408+
.map((Placeholder templatePlaceholder) => placeholders[templatePlaceholder.name] ?? templatePlaceholder);
409+
}
396410

397411
static String _value(Map<String, Object?> bundle, String resourceId) {
398412
final Object? value = bundle[resourceId];
@@ -488,12 +502,15 @@ class Message {
488502

489503
// Using parsed translations, attempt to infer types of placeholders used by plurals and selects.
490504
// For undeclared placeholders, create a new placeholder.
491-
void _inferPlaceholders(Map<LocaleInfo, String> filenames) {
505+
void _inferPlaceholders() {
492506
// We keep the undeclared placeholders separate so that we can sort them alphabetically afterwards.
493507
final Map<String, Placeholder> undeclaredPlaceholders = <String, Placeholder>{};
494508
// Helper for getting placeholder by name.
495-
Placeholder? getPlaceholder(String name) => placeholders[name] ?? undeclaredPlaceholders[name];
496509
for (final LocaleInfo locale in parsedMessages.keys) {
510+
Placeholder? getPlaceholder(String name) =>
511+
localePlaceholders[locale]?[name] ??
512+
templatePlaceholders[name] ??
513+
undeclaredPlaceholders[name];
497514
if (parsedMessages[locale] == null) {
498515
continue;
499516
}
@@ -529,7 +546,7 @@ class Message {
529546
traversalStack.addAll(node.children);
530547
}
531548
}
532-
placeholders.addEntries(
549+
templatePlaceholders.addEntries(
533550
undeclaredPlaceholders.entries
534551
.toList()
535552
..sort((MapEntry<String, Placeholder> p1, MapEntry<String, Placeholder> p2) => p1.key.compareTo(p2.key))
@@ -542,7 +559,7 @@ class Message {
542559
|| !x && !y && !z;
543560
}
544561

545-
for (final Placeholder placeholder in placeholders.values) {
562+
for (final Placeholder placeholder in templatePlaceholders.values) {
546563
if (!atMostOneOf(placeholder.isPlural, placeholder.isDateTime, placeholder.isSelect)) {
547564
throw L10nException('Placeholder is used as plural/select/datetime in certain languages.');
548565
} else if (placeholder.isPlural) {

0 commit comments

Comments
 (0)