Skip to content

Commit becb6bd

Browse files
authored
Fix message type inconsistency between locales (#120129)
* init * fix error handling * fix issue * lint? * error handling tests * lint
1 parent ddebe83 commit becb6bd

File tree

4 files changed

+158
-27
lines changed

4 files changed

+158
-27
lines changed

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,7 @@ class LocalizationsGenerator {
877877
_allMessages = _templateBundle.resourceIds.map((String id) => Message(
878878
_templateBundle, _allBundles, id, areResourceAttributesRequired, useEscaping: useEscaping, logger: logger,
879879
)).toList();
880+
hadErrors = _allMessages.any((Message message) => message.hadErrors);
880881
if (inputsAndOutputsListFile != null) {
881882
_inputFileList.addAll(_allBundles.bundles.map((AppResourceBundle bundle) {
882883
return bundle.file.absolute.path;
@@ -915,16 +916,19 @@ class LocalizationsGenerator {
915916
final LocaleInfo locale,
916917
) {
917918
final Iterable<String> methods = _allMessages.map((Message message) {
919+
LocaleInfo localeWithFallback = locale;
918920
if (message.messages[locale] == null) {
919921
_addUnimplementedMessage(locale, message.resourceId);
920-
return _generateMethod(
921-
message,
922-
_templateArbLocale,
923-
);
922+
localeWithFallback = _templateArbLocale;
923+
}
924+
if (message.parsedMessages[localeWithFallback] == null) {
925+
// The message exists, but parsedMessages[locale] is null due to a syntax error.
926+
// This means that we have already set hadErrors = true while constructing the Message.
927+
return '';
924928
}
925929
return _generateMethod(
926930
message,
927-
locale,
931+
localeWithFallback,
928932
);
929933
});
930934

@@ -953,7 +957,7 @@ class LocalizationsGenerator {
953957
});
954958

955959
final Iterable<String> methods = _allMessages
956-
.where((Message message) => message.messages[locale] != null)
960+
.where((Message message) => message.parsedMessages[locale] != null)
957961
.map((Message message) => _generateMethod(message, locale));
958962

959963
return subclassTemplate
@@ -1103,8 +1107,8 @@ class LocalizationsGenerator {
11031107

11041108
final String translationForMessage = message.messages[locale]!;
11051109
final Node node = message.parsedMessages[locale]!;
1106-
// If parse tree is only a string, then return a getter method.
1107-
if (node.children.every((Node child) => child.type == ST.string)) {
1110+
// If the placeholders list is empty, then return a getter method.
1111+
if (message.placeholders.isEmpty) {
11081112
// Use the parsed translation to handle escaping with the same behavior.
11091113
return getterTemplate
11101114
.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
@@ -349,13 +349,20 @@ class Message {
349349
filenames[bundle.locale] = bundle.file.basename;
350350
final String? translation = bundle.translationFor(resourceId);
351351
messages[bundle.locale] = translation;
352-
parsedMessages[bundle.locale] = translation == null ? null : Parser(
353-
resourceId,
354-
bundle.file.basename,
355-
translation,
356-
useEscaping: useEscaping,
357-
logger: logger
358-
).parse();
352+
try {
353+
parsedMessages[bundle.locale] = translation == null ? null : Parser(
354+
resourceId,
355+
bundle.file.basename,
356+
translation,
357+
useEscaping: useEscaping,
358+
logger: logger
359+
).parse();
360+
} on L10nParserException catch (error) {
361+
logger?.printError(error.toString());
362+
// Treat it as an untranslated message in case we can't parse.
363+
parsedMessages[bundle.locale] = null;
364+
hadErrors = true;
365+
}
359366
}
360367
// Infer the placeholders
361368
_inferPlaceholders(filenames);
@@ -369,6 +376,7 @@ class Message {
369376
final Map<String, Placeholder> placeholders;
370377
final bool useEscaping;
371378
final Logger? logger;
379+
bool hadErrors = false;
372380

373381
bool get placeholdersRequireFormatting => placeholders.values.any((Placeholder p) => p.requiresFormatting);
374382

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

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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
@@ -1733,6 +1733,47 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
17331733
).readAsStringSync();
17341734
expect(localizationsFile, contains('String helloWorld(Object name) {'));
17351735
});
1736+
1737+
testWithoutContext('placeholder parameter list should be consistent between languages', () {
1738+
const String messageEn = '''
1739+
{
1740+
"helloWorld": "Hello {name}",
1741+
"@helloWorld": {
1742+
"placeholders": {
1743+
"name": {}
1744+
}
1745+
}
1746+
}''';
1747+
const String messageEs = '''
1748+
{
1749+
"helloWorld": "Hola"
1750+
}
1751+
''';
1752+
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
1753+
..createSync(recursive: true);
1754+
l10nDirectory.childFile(defaultTemplateArbFileName)
1755+
.writeAsStringSync(messageEn);
1756+
l10nDirectory.childFile('app_es.arb')
1757+
.writeAsStringSync(messageEs);
1758+
LocalizationsGenerator(
1759+
fileSystem: fs,
1760+
inputPathString: defaultL10nPathString,
1761+
templateArbFileName: defaultTemplateArbFileName,
1762+
outputFileString: defaultOutputFileString,
1763+
classNameString: defaultClassNameString,
1764+
logger: logger,
1765+
)
1766+
..loadResources()
1767+
..writeOutputFiles();
1768+
final String localizationsFileEn = fs.file(
1769+
fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.dart'),
1770+
).readAsStringSync();
1771+
final String localizationsFileEs = fs.file(
1772+
fs.path.join(syntheticL10nPackagePath, 'output-localization-file_es.dart'),
1773+
).readAsStringSync();
1774+
expect(localizationsFileEn, contains('String helloWorld(Object name) {'));
1775+
expect(localizationsFileEs, contains('String helloWorld(Object name) {'));
1776+
});
17361777
});
17371778

17381779
group('DateTime tests', () {
@@ -2258,6 +2299,93 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
22582299
});
22592300
});
22602301

2302+
// All error handling for messages should collect errors on a per-error
2303+
// basis and log them out individually. Then, it will throw an L10nException.
2304+
group('error handling tests', () {
2305+
testWithoutContext('syntax/code-gen errors properly logs errors per message', () {
2306+
// TODO(thkim1011): Fix error handling so that long indents don't get truncated.
2307+
// See https://github.com/flutter/flutter/issues/120490.
2308+
const String messagesWithSyntaxErrors = '''
2309+
{
2310+
"hello": "Hello { name",
2311+
"plural": "This is an incorrectly formatted plural: { count, plural, zero{No frog} one{One frog} other{{count} frogs}",
2312+
"explanationWithLexingError": "The 'string above is incorrect as it forgets to close the brace",
2313+
"pluralWithInvalidCase": "{ count, plural, woohoo{huh?} other{lol} }"
2314+
}''';
2315+
try {
2316+
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
2317+
..createSync(recursive: true);
2318+
l10nDirectory.childFile(defaultTemplateArbFileName)
2319+
.writeAsStringSync(messagesWithSyntaxErrors);
2320+
LocalizationsGenerator(
2321+
fileSystem: fs,
2322+
inputPathString: defaultL10nPathString,
2323+
outputPathString: defaultL10nPathString,
2324+
templateArbFileName: defaultTemplateArbFileName,
2325+
outputFileString: defaultOutputFileString,
2326+
classNameString: defaultClassNameString,
2327+
useEscaping: true,
2328+
logger: logger,
2329+
)
2330+
..loadResources()
2331+
..writeOutputFiles();
2332+
} on L10nException {
2333+
expect(logger.errorText, contains('''
2334+
[app_en.arb:hello] ICU Syntax Error: Expected "}" but found no tokens.
2335+
Hello { name
2336+
^
2337+
[app_en.arb:plural] ICU Syntax Error: Expected "}" but found no tokens.
2338+
This is an incorrectly formatted plural: { count, plural, zero{No frog} one{One frog} other{{count} frogs}
2339+
^
2340+
[app_en.arb:explanationWithLexingError] ICU Lexing Error: Unmatched single quotes.
2341+
The 'string above is incorrect as it forgets to close the brace
2342+
^
2343+
[app_en.arb:pluralWithInvalidCase] ICU Syntax Error: Plural expressions case must be one of "zero", "one", "two", "few", "many", or "other".
2344+
{ count, plural, woohoo{huh?} other{lol} }
2345+
^'''));
2346+
}
2347+
});
2348+
2349+
testWithoutContext('errors thrown in multiple languages are all shown', () {
2350+
const String messageEn = '''
2351+
{
2352+
"hello": "Hello { name"
2353+
}''';
2354+
const String messageEs = '''
2355+
{
2356+
"hello": "Hola { name"
2357+
}''';
2358+
try {
2359+
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
2360+
..createSync(recursive: true);
2361+
l10nDirectory.childFile(defaultTemplateArbFileName)
2362+
.writeAsStringSync(messageEn);
2363+
l10nDirectory.childFile('app_es.arb')
2364+
.writeAsStringSync(messageEs);
2365+
LocalizationsGenerator(
2366+
fileSystem: fs,
2367+
inputPathString: defaultL10nPathString,
2368+
outputPathString: defaultL10nPathString,
2369+
templateArbFileName: defaultTemplateArbFileName,
2370+
outputFileString: defaultOutputFileString,
2371+
classNameString: defaultClassNameString,
2372+
useEscaping: true,
2373+
logger: logger,
2374+
)
2375+
..loadResources()
2376+
..writeOutputFiles();
2377+
} on L10nException {
2378+
expect(logger.errorText, contains('''
2379+
[app_en.arb:hello] ICU Syntax Error: Expected "}" but found no tokens.
2380+
Hello { name
2381+
^
2382+
[app_es.arb:hello] ICU Syntax Error: Expected "}" but found no tokens.
2383+
Hola { name
2384+
^'''));
2385+
}
2386+
});
2387+
});
2388+
22612389
testWithoutContext('intl package import should be omitted in subclass files when no plurals are included', () {
22622390
fs.currentDirectory.childDirectory('lib').childDirectory('l10n')..createSync(recursive: true)
22632391
..childFile(defaultTemplateArbFileName).writeAsStringSync(singleMessageArbFileString)

0 commit comments

Comments
 (0)