Skip to content

Commit b4bce91

Browse files
AlexV525thkim1011
andauthored
Fix message type inconsistency between locales (flutter#120129) (flutter#120678)
(cherry picked from commit becb6bd) Co-authored-by: Tae Hyung Kim <thkim1011@users.noreply.github.com>
1 parent a396cc3 commit b4bce91

4 files changed

Lines changed: 159 additions & 28 deletions

File tree

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,7 @@ class LocalizationsGenerator {
874874
_allMessages = _templateBundle.resourceIds.map((String id) => Message(
875875
_templateBundle, _allBundles, id, areResourceAttributesRequired, useEscaping: useEscaping, logger: logger,
876876
)).toList();
877+
hadErrors = _allMessages.any((Message message) => message.hadErrors);
877878
if (inputsAndOutputsListFile != null) {
878879
_inputFileList.addAll(_allBundles.bundles.map((AppResourceBundle bundle) {
879880
return bundle.file.absolute.path;
@@ -912,16 +913,19 @@ class LocalizationsGenerator {
912913
final LocaleInfo locale,
913914
) {
914915
final Iterable<String> methods = _allMessages.map((Message message) {
916+
LocaleInfo localeWithFallback = locale;
915917
if (message.messages[locale] == null) {
916918
_addUnimplementedMessage(locale, message.resourceId);
917-
return _generateMethod(
918-
message,
919-
_templateArbLocale,
920-
);
919+
localeWithFallback = _templateArbLocale;
920+
}
921+
if (message.parsedMessages[localeWithFallback] == null) {
922+
// The message exists, but parsedMessages[locale] is null due to a syntax error.
923+
// This means that we have already set hadErrors = true while constructing the Message.
924+
return '';
921925
}
922926
return _generateMethod(
923927
message,
924-
locale,
928+
localeWithFallback,
925929
);
926930
});
927931

@@ -950,7 +954,7 @@ class LocalizationsGenerator {
950954
});
951955

952956
final Iterable<String> methods = _allMessages
953-
.where((Message message) => message.messages[locale] != null)
957+
.where((Message message) => message.parsedMessages[locale] != null)
954958
.map((Message message) => _generateMethod(message, locale));
955959

956960
return subclassTemplate
@@ -1100,8 +1104,8 @@ class LocalizationsGenerator {
11001104

11011105
final String translationForMessage = message.messages[locale]!;
11021106
final Node node = message.parsedMessages[locale]!;
1103-
// If parse tree is only a string, then return a getter method.
1104-
if (node.children.every((Node child) => child.type == ST.string)) {
1107+
// If the placeholders list is empty, then return a getter method.
1108+
if (message.placeholders.isEmpty) {
11051109
// Use the parsed translation to handle escaping with the same behavior.
11061110
return getterTemplate
11071111
.replaceAll('@(name)', message.resourceId)

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -353,13 +353,20 @@ class Message {
353353
filenames[bundle.locale] = bundle.file.basename;
354354
final String? translation = bundle.translationFor(resourceId);
355355
messages[bundle.locale] = translation;
356-
parsedMessages[bundle.locale] = translation == null ? null : Parser(
357-
resourceId,
358-
bundle.file.basename,
359-
translation,
360-
useEscaping: useEscaping,
361-
logger: logger
362-
).parse();
356+
try {
357+
parsedMessages[bundle.locale] = translation == null ? null : Parser(
358+
resourceId,
359+
bundle.file.basename,
360+
translation,
361+
useEscaping: useEscaping,
362+
logger: logger
363+
).parse();
364+
} on L10nParserException catch (error) {
365+
logger?.printError(error.toString());
366+
// Treat it as an untranslated message in case we can't parse.
367+
parsedMessages[bundle.locale] = null;
368+
hadErrors = true;
369+
}
363370
}
364371
// Infer the placeholders
365372
_inferPlaceholders(filenames);
@@ -373,6 +380,7 @@ class Message {
373380
final Map<String, Placeholder> placeholders;
374381
final bool useEscaping;
375382
final Logger? logger;
383+
bool hadErrors = false;
376384

377385
bool get placeholdersRequireFormatting => placeholders.values.any((Placeholder p) => p.requiresFormatting);
378386

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

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ class Parser {
342342
parsingStack.addAll(grammarRule.reversed);
343343

344344
// For tree construction, add nodes to the parent until the parent has all
345-
// all the children it is expecting.
345+
// the children it is expecting.
346346
parent.children.add(node);
347347
if (parent.isFull) {
348348
treeTraversalStack.removeLast();
@@ -587,17 +587,8 @@ class Parser {
587587
}
588588

589589
Node parse() {
590-
try {
591-
final Node syntaxTree = compress(parseIntoTree());
592-
checkExtraRules(syntaxTree);
593-
return syntaxTree;
594-
} on L10nParserException catch (error) {
595-
// For debugging purposes.
596-
if (logger == null) {
597-
rethrow;
598-
}
599-
logger?.printError(error.toString());
600-
return Node(ST.empty, 0, value: '');
601-
}
590+
final Node syntaxTree = compress(parseIntoTree());
591+
checkExtraRules(syntaxTree);
592+
return syntaxTree;
602593
}
603594
}

packages/flutter_tools/test/general.shard/generate_localizations_test.dart

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,6 +1663,47 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
16631663
).readAsStringSync();
16641664
expect(localizationsFile, contains('String helloWorld(Object name) {'));
16651665
});
1666+
1667+
testWithoutContext('placeholder parameter list should be consistent between languages', () {
1668+
const String messageEn = '''
1669+
{
1670+
"helloWorld": "Hello {name}",
1671+
"@helloWorld": {
1672+
"placeholders": {
1673+
"name": {}
1674+
}
1675+
}
1676+
}''';
1677+
const String messageEs = '''
1678+
{
1679+
"helloWorld": "Hola"
1680+
}
1681+
''';
1682+
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
1683+
..createSync(recursive: true);
1684+
l10nDirectory.childFile(defaultTemplateArbFileName)
1685+
.writeAsStringSync(messageEn);
1686+
l10nDirectory.childFile('app_es.arb')
1687+
.writeAsStringSync(messageEs);
1688+
LocalizationsGenerator(
1689+
fileSystem: fs,
1690+
inputPathString: defaultL10nPathString,
1691+
templateArbFileName: defaultTemplateArbFileName,
1692+
outputFileString: defaultOutputFileString,
1693+
classNameString: defaultClassNameString,
1694+
logger: logger,
1695+
)
1696+
..loadResources()
1697+
..writeOutputFiles();
1698+
final String localizationsFileEn = fs.file(
1699+
fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.dart'),
1700+
).readAsStringSync();
1701+
final String localizationsFileEs = fs.file(
1702+
fs.path.join(syntheticL10nPackagePath, 'output-localization-file_es.dart'),
1703+
).readAsStringSync();
1704+
expect(localizationsFileEn, contains('String helloWorld(Object name) {'));
1705+
expect(localizationsFileEs, contains('String helloWorld(Object name) {'));
1706+
});
16661707
});
16671708

16681709
group('DateTime tests', () {
@@ -2189,6 +2230,93 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
21892230
});
21902231
});
21912232

2233+
// All error handling for messages should collect errors on a per-error
2234+
// basis and log them out individually. Then, it will throw an L10nException.
2235+
group('error handling tests', () {
2236+
testWithoutContext('syntax/code-gen errors properly logs errors per message', () {
2237+
// TODO(thkim1011): Fix error handling so that long indents don't get truncated.
2238+
// See https://github.com/flutter/flutter/issues/120490.
2239+
const String messagesWithSyntaxErrors = '''
2240+
{
2241+
"hello": "Hello { name",
2242+
"plural": "This is an incorrectly formatted plural: { count, plural, zero{No frog} one{One frog} other{{count} frogs}",
2243+
"explanationWithLexingError": "The 'string above is incorrect as it forgets to close the brace",
2244+
"pluralWithInvalidCase": "{ count, plural, woohoo{huh?} other{lol} }"
2245+
}''';
2246+
try {
2247+
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
2248+
..createSync(recursive: true);
2249+
l10nDirectory.childFile(defaultTemplateArbFileName)
2250+
.writeAsStringSync(messagesWithSyntaxErrors);
2251+
LocalizationsGenerator(
2252+
fileSystem: fs,
2253+
inputPathString: defaultL10nPathString,
2254+
outputPathString: defaultL10nPathString,
2255+
templateArbFileName: defaultTemplateArbFileName,
2256+
outputFileString: defaultOutputFileString,
2257+
classNameString: defaultClassNameString,
2258+
useEscaping: true,
2259+
logger: logger,
2260+
)
2261+
..loadResources()
2262+
..writeOutputFiles();
2263+
} on L10nException {
2264+
expect(logger.errorText, contains('''
2265+
[app_en.arb:hello] ICU Syntax Error: Expected "}" but found no tokens.
2266+
Hello { name
2267+
^
2268+
[app_en.arb:plural] ICU Syntax Error: Expected "}" but found no tokens.
2269+
This is an incorrectly formatted plural: { count, plural, zero{No frog} one{One frog} other{{count} frogs}
2270+
^
2271+
[app_en.arb:explanationWithLexingError] ICU Lexing Error: Unmatched single quotes.
2272+
The 'string above is incorrect as it forgets to close the brace
2273+
^
2274+
[app_en.arb:pluralWithInvalidCase] ICU Syntax Error: Plural expressions case must be one of "zero", "one", "two", "few", "many", or "other".
2275+
{ count, plural, woohoo{huh?} other{lol} }
2276+
^'''));
2277+
}
2278+
});
2279+
2280+
testWithoutContext('errors thrown in multiple languages are all shown', () {
2281+
const String messageEn = '''
2282+
{
2283+
"hello": "Hello { name"
2284+
}''';
2285+
const String messageEs = '''
2286+
{
2287+
"hello": "Hola { name"
2288+
}''';
2289+
try {
2290+
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
2291+
..createSync(recursive: true);
2292+
l10nDirectory.childFile(defaultTemplateArbFileName)
2293+
.writeAsStringSync(messageEn);
2294+
l10nDirectory.childFile('app_es.arb')
2295+
.writeAsStringSync(messageEs);
2296+
LocalizationsGenerator(
2297+
fileSystem: fs,
2298+
inputPathString: defaultL10nPathString,
2299+
outputPathString: defaultL10nPathString,
2300+
templateArbFileName: defaultTemplateArbFileName,
2301+
outputFileString: defaultOutputFileString,
2302+
classNameString: defaultClassNameString,
2303+
useEscaping: true,
2304+
logger: logger,
2305+
)
2306+
..loadResources()
2307+
..writeOutputFiles();
2308+
} on L10nException {
2309+
expect(logger.errorText, contains('''
2310+
[app_en.arb:hello] ICU Syntax Error: Expected "}" but found no tokens.
2311+
Hello { name
2312+
^
2313+
[app_es.arb:hello] ICU Syntax Error: Expected "}" but found no tokens.
2314+
Hola { name
2315+
^'''));
2316+
}
2317+
});
2318+
});
2319+
21922320
testWithoutContext('intl package import should be omitted in subclass files when no plurals are included', () {
21932321
fs.currentDirectory.childDirectory('lib').childDirectory('l10n')..createSync(recursive: true)
21942322
..childFile(defaultTemplateArbFileName).writeAsStringSync(singleMessageArbFileString)

0 commit comments

Comments
 (0)