Skip to content

Commit cef4c2a

Browse files
authored
ICU Message Syntax Parser (#112390)
* init * code generation * improve syntax error, add tests * add tests and fix bugs * code generation fix * fix all tests :) * fix bug * init * fix all code gen issues * FIXED ALL TESTS :D * add license * remove trailing spaces * remove print * tests fix * specify type annotation * fix test * lint * fix todos * fix subclass issues * final fix; flutter gallery runs * escaping for later pr * fix comment * address PR comments * more * more descriptive errors * last fixes
1 parent e0e7027 commit cef4c2a

File tree

9 files changed

+1590
-453
lines changed

9 files changed

+1590
-453
lines changed

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

Lines changed: 240 additions & 302 deletions
Large diffs are not rendered by default.

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

Lines changed: 24 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -135,70 +135,37 @@ const String getterTemplate = '''
135135
String get @(name) => @(message);''';
136136

137137
const String methodTemplate = '''
138-
@override
139-
String @(name)(@(parameters)) {
140-
return @(message);
141-
}''';
142-
143-
const String formatMethodTemplate = '''
144138
@override
145139
String @(name)(@(parameters)) {
146140
@(dateFormatting)
147141
@(numberFormatting)
142+
@(helperMethods)
148143
return @(message);
149144
}''';
150145

151-
const String pluralMethodTemplate = '''
152-
@override
153-
String @(name)(@(parameters)) {
154-
@(dateFormatting)
155-
@(numberFormatting)
156-
return intl.Intl.pluralLogic(
157-
@(count),
158-
locale: localeName,
159-
@(pluralLogicArgs),
160-
);
161-
}''';
162-
163-
const String pluralMethodTemplateInString = '''
164-
@override
165-
String @(name)(@(parameters)) {
166-
@(dateFormatting)
167-
@(numberFormatting)
168-
final String @(variable) = intl.Intl.pluralLogic(
169-
@(count),
170-
locale: localeName,
171-
@(pluralLogicArgs),
172-
);
173-
174-
return @(string);
175-
}''';
176-
177-
const String selectMethodTemplate = '''
178-
@override
179-
String @(name)(@(parameters)) {
180-
return intl.Intl.select(
181-
@(choice),
182-
{
183-
@(cases)
184-
},
185-
desc: '@(description)'
186-
);
187-
}''';
188-
189-
const String selectMethodTemplateInString = '''
190-
@override
191-
String @(name)(@(parameters)) {
192-
final String @(variable) = intl.Intl.select(
193-
@(choice),
194-
{
195-
@(cases)
196-
},
197-
desc: '@(description)'
198-
);
199-
200-
return @(string);
201-
}''';
146+
const String messageHelperTemplate = '''
147+
String @(name)(@(parameters)) {
148+
return @(message);
149+
}''';
150+
151+
const String pluralHelperTemplate = '''
152+
String @(name)(@(parameters)) {
153+
return intl.Intl.pluralLogic(
154+
@(count),
155+
locale: localeName,
156+
@(pluralLogicArgs)
157+
);
158+
}''';
159+
160+
const String selectHelperTemplate = '''
161+
String @(name)(@(parameters)) {
162+
return intl.Intl.selectLogic(
163+
@(choice),
164+
{
165+
@(selectCases)
166+
},
167+
);
168+
}''';
202169

203170
const String classFileTemplate = '''
204171
@(header)@(requiresIntlImport)import '@(fileName)';

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

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,25 @@ class L10nException implements Exception {
129129
String toString() => message;
130130
}
131131

132+
class L10nParserException extends L10nException {
133+
L10nParserException(
134+
this.error,
135+
this.fileName,
136+
this.messageId,
137+
this.messageString,
138+
this.charNumber
139+
): super('''
140+
$error
141+
[$fileName:$messageId] $messageString
142+
${List<String>.filled(4 + fileName.length + messageId.length + charNumber, ' ').join()}^''');
143+
144+
final String error;
145+
final String fileName;
146+
final String messageId;
147+
final String messageString;
148+
final int charNumber;
149+
}
150+
132151
// One optional named parameter to be used by a NumberFormat.
133152
//
134153
// Some of the NumberFormat factory constructors have optional named parameters.
@@ -202,16 +221,16 @@ class Placeholder {
202221
final String resourceId;
203222
final String name;
204223
final String? example;
205-
final String? type;
224+
String? type;
206225
final String? format;
207226
final List<OptionalParameter> optionalParameters;
208227
final bool? isCustomDateFormat;
209228

210-
bool get requiresFormatting => <String>['DateTime', 'double', 'num'].contains(type) || (type == 'int' && format != null);
211-
bool get isNumber => <String>['double', 'int', 'num'].contains(type);
229+
bool get requiresFormatting => requiresDateFormatting || requiresNumFormatting;
230+
bool get requiresDateFormatting => type == 'DateTime';
231+
bool get requiresNumFormatting => <String>['int', 'num', 'double'].contains(type) && format != null;
212232
bool get hasValidNumberFormat => _validNumberFormats.contains(format);
213233
bool get hasNumberFormatWithParameters => _numberFormatsWithNamedParameters.contains(format);
214-
bool get isDate => 'DateTime' == type;
215234
bool get hasValidDateFormat => _validDateFormats.contains(format);
216235

217236
static String? _stringAttribute(
@@ -290,6 +309,8 @@ class Placeholder {
290309
// The value of this Message is "Hello World". The Message's value is the
291310
// localized string to be shown for the template ARB file's locale.
292311
// The docs for the Placeholder explain how placeholder entries are defined.
312+
// TODO(thkim1011): We need to refactor this Message class to own all the messages in each language.
313+
// See https://github.com/flutter/flutter/issues/112709.
293314
class Message {
294315
Message(Map<String, Object?> bundle, this.resourceId, bool isResourceAttributeRequired)
295316
: assert(bundle != null),
@@ -298,7 +319,12 @@ class Message {
298319
description = _description(bundle, resourceId, isResourceAttributeRequired),
299320
placeholders = _placeholders(bundle, resourceId, isResourceAttributeRequired),
300321
_pluralMatch = _pluralRE.firstMatch(_value(bundle, resourceId)),
301-
_selectMatch = _selectRE.firstMatch(_value(bundle, resourceId));
322+
_selectMatch = _selectRE.firstMatch(_value(bundle, resourceId)) {
323+
if (isPlural) {
324+
final Placeholder placeholder = getCountPlaceholder();
325+
placeholder.type = 'num';
326+
}
327+
}
302328

303329
static final RegExp _pluralRE = RegExp(r'\s*\{([\w\s,]*),\s*plural\s*,');
304330
static final RegExp _selectRE = RegExp(r'\s*\{([\w\s,]*),\s*select\s*,');
@@ -769,3 +795,50 @@ final Set<String> _iso639Languages = <String>{
769795
'zh',
770796
'zu',
771797
};
798+
799+
// Used in LocalizationsGenerator._generateMethod.generateHelperMethod.
800+
class HelperMethod {
801+
HelperMethod(this.dependentPlaceholders, {this.helper, this.placeholder, this.string }):
802+
assert((() {
803+
// At least one of helper, placeholder, string must be nonnull.
804+
final bool a = helper == null;
805+
final bool b = placeholder == null;
806+
final bool c = string == null;
807+
return (!a && b && c) || (a && !b && c) || (a && b && !c);
808+
})());
809+
810+
Set<Placeholder> dependentPlaceholders;
811+
String? helper;
812+
Placeholder? placeholder;
813+
String? string;
814+
815+
String get helperOrPlaceholder {
816+
if (helper != null) {
817+
return '$helper($methodArguments)';
818+
} else if (string != null) {
819+
return '$string';
820+
} else {
821+
if (placeholder!.requiresFormatting) {
822+
return '${placeholder!.name}String';
823+
} else {
824+
return placeholder!.name;
825+
}
826+
}
827+
}
828+
829+
String get methodParameters {
830+
assert(helper != null);
831+
return dependentPlaceholders.map((Placeholder placeholder) =>
832+
(placeholder.requiresFormatting)
833+
? 'String ${placeholder.name}String'
834+
: '${placeholder.type} ${placeholder.name}').join(', ');
835+
}
836+
837+
String get methodArguments {
838+
assert(helper != null);
839+
return dependentPlaceholders.map((Placeholder placeholder) =>
840+
(placeholder.requiresFormatting)
841+
? '${placeholder.name}String'
842+
: placeholder.name).join(', ');
843+
}
844+
}

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,34 @@ String generateString(String value) {
292292
// Reintroduce escaped backslashes into generated Dart string.
293293
.replaceAll(backslash, r'\\');
294294

295-
return "'$value'";
295+
return value;
296+
}
297+
298+
/// Given a list of strings, placeholders, or helper function calls, concatenate
299+
/// them into one expression to be returned.
300+
String generateReturnExpr(List<HelperMethod> helpers) {
301+
if (helpers.isEmpty) {
302+
return "''";
303+
} else if (
304+
helpers.length == 1
305+
&& helpers[0].string == null
306+
&& (helpers[0].placeholder?.type == 'String' || helpers[0].helper != null)
307+
) {
308+
return helpers[0].helperOrPlaceholder;
309+
} else {
310+
final String string = helpers.reversed.fold<String>('', (String string, HelperMethod helper) {
311+
if (helper.string != null) {
312+
return generateString(helper.string!) + string;
313+
}
314+
final RegExp alphanumeric = RegExp(r'^([0-9a-zA-Z]|_)+$');
315+
if (alphanumeric.hasMatch(helper.helperOrPlaceholder) && !(string.isNotEmpty && alphanumeric.hasMatch(string[0]))) {
316+
return '\$${helper.helperOrPlaceholder}$string';
317+
} else {
318+
return '\${${helper.helperOrPlaceholder}}$string';
319+
}
320+
});
321+
return "'$string'";
322+
}
296323
}
297324

298325
/// Typed configuration from the localizations config file.

0 commit comments

Comments
 (0)