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
This commit is contained in:
parent
e0e7027b62
commit
cef4c2aac8
@ -13,6 +13,7 @@ import '../flutter_manifest.dart';
|
|||||||
import 'gen_l10n_templates.dart';
|
import 'gen_l10n_templates.dart';
|
||||||
import 'gen_l10n_types.dart';
|
import 'gen_l10n_types.dart';
|
||||||
import 'localizations_utils.dart';
|
import 'localizations_utils.dart';
|
||||||
|
import 'message_parser.dart';
|
||||||
|
|
||||||
/// Run the localizations generation script with the configuration [options].
|
/// Run the localizations generation script with the configuration [options].
|
||||||
LocalizationsGenerator generateLocalizations({
|
LocalizationsGenerator generateLocalizations({
|
||||||
@ -84,22 +85,30 @@ String _defaultSyntheticPackagePath(FileSystem fileSystem) => fileSystem.path.jo
|
|||||||
/// localizations tool.
|
/// localizations tool.
|
||||||
String _syntheticL10nPackagePath(FileSystem fileSystem) => fileSystem.path.join(_defaultSyntheticPackagePath(fileSystem), 'gen_l10n');
|
String _syntheticL10nPackagePath(FileSystem fileSystem) => fileSystem.path.join(_defaultSyntheticPackagePath(fileSystem), 'gen_l10n');
|
||||||
|
|
||||||
|
// Generate method parameters and also infer the correct types from the usage of the placeholders
|
||||||
|
// 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
|
||||||
|
// will be set to 'String'. For such placeholders that are used for both, we should throw an error.
|
||||||
|
// TODO(thkim1011): Let's store the output of this function in the Message class, so that we don't
|
||||||
|
// recompute this. See https://github.com/flutter/flutter/issues/112709
|
||||||
List<String> generateMethodParameters(Message message) {
|
List<String> generateMethodParameters(Message message) {
|
||||||
assert(message.placeholders.isNotEmpty);
|
|
||||||
final Placeholder? countPlaceholder = message.isPlural ? message.getCountPlaceholder() : null;
|
|
||||||
return message.placeholders.map((Placeholder placeholder) {
|
return message.placeholders.map((Placeholder placeholder) {
|
||||||
final String? type = placeholder == countPlaceholder ? 'num' : placeholder.type;
|
return '${placeholder.type} ${placeholder.name}';
|
||||||
return '$type ${placeholder.name}';
|
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Similar to above, but is used for passing arguments into helper functions.
|
||||||
|
List<String> generateMethodArguments(Message message) {
|
||||||
|
return message.placeholders.map((Placeholder placeholder) => placeholder.name).toList();
|
||||||
|
}
|
||||||
|
|
||||||
String generateDateFormattingLogic(Message message) {
|
String generateDateFormattingLogic(Message message) {
|
||||||
if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting) {
|
if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting) {
|
||||||
return '@(none)';
|
return '@(none)';
|
||||||
}
|
}
|
||||||
|
|
||||||
final Iterable<String> formatStatements = message.placeholders
|
final Iterable<String> formatStatements = message.placeholders
|
||||||
.where((Placeholder placeholder) => placeholder.isDate)
|
.where((Placeholder placeholder) => placeholder.requiresDateFormatting)
|
||||||
.map((Placeholder placeholder) {
|
.map((Placeholder placeholder) {
|
||||||
final String? placeholderFormat = placeholder.format;
|
final String? placeholderFormat = placeholder.format;
|
||||||
if (placeholderFormat == null) {
|
if (placeholderFormat == null) {
|
||||||
@ -130,7 +139,7 @@ String generateDateFormattingLogic(Message message) {
|
|||||||
}
|
}
|
||||||
return dateFormatCustomTemplate
|
return dateFormatCustomTemplate
|
||||||
.replaceAll('@(placeholder)', placeholder.name)
|
.replaceAll('@(placeholder)', placeholder.name)
|
||||||
.replaceAll('@(format)', generateString(placeholderFormat));
|
.replaceAll('@(format)', "'${generateString(placeholderFormat)}'");
|
||||||
});
|
});
|
||||||
|
|
||||||
return formatStatements.isEmpty ? '@(none)' : formatStatements.join();
|
return formatStatements.isEmpty ? '@(none)' : formatStatements.join();
|
||||||
@ -142,7 +151,7 @@ String generateNumberFormattingLogic(Message message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Iterable<String> formatStatements = message.placeholders
|
final Iterable<String> formatStatements = message.placeholders
|
||||||
.where((Placeholder placeholder) => placeholder.isNumber)
|
.where((Placeholder placeholder) => placeholder.requiresNumFormatting)
|
||||||
.map((Placeholder placeholder) {
|
.map((Placeholder placeholder) {
|
||||||
final String? placeholderFormat = placeholder.format;
|
final String? placeholderFormat = placeholder.format;
|
||||||
if (!placeholder.hasValidNumberFormat || placeholderFormat == null) {
|
if (!placeholder.hasValidNumberFormat || placeholderFormat == null) {
|
||||||
@ -158,7 +167,7 @@ String generateNumberFormattingLogic(Message message) {
|
|||||||
if (parameter.value is num) {
|
if (parameter.value is num) {
|
||||||
return '${parameter.name}: ${parameter.value}';
|
return '${parameter.name}: ${parameter.value}';
|
||||||
} else {
|
} else {
|
||||||
return '${parameter.name}: ${generateString(parameter.value.toString())}';
|
return "${parameter.name}: '${generateString(parameter.value.toString())}'";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -178,279 +187,24 @@ String generateNumberFormattingLogic(Message message) {
|
|||||||
return formatStatements.isEmpty ? '@(none)' : formatStatements.join();
|
return formatStatements.isEmpty ? '@(none)' : formatStatements.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// To make it easier to parse plurals or select messages, temporarily replace
|
/// List of possible cases for plurals defined the ICU messageFormat syntax.
|
||||||
/// each "{placeholder}" parameter with "#placeholder#" for example.
|
Map<String, String> pluralCases = <String, String>{
|
||||||
String _replacePlaceholdersBraces(
|
'0': 'zero',
|
||||||
String translationForMessage,
|
'1': 'one',
|
||||||
Iterable<Placeholder> placeholders,
|
'2': 'two',
|
||||||
String replacementBraces,
|
'zero': 'zero',
|
||||||
) {
|
'one': 'one',
|
||||||
assert(replacementBraces.length == 2);
|
'two': 'two',
|
||||||
String easyMessage = translationForMessage;
|
'few': 'few',
|
||||||
for (final Placeholder placeholder in placeholders) {
|
'many': 'many',
|
||||||
easyMessage = easyMessage.replaceAll(
|
'other': 'other',
|
||||||
'{${placeholder.name}}',
|
};
|
||||||
'${replacementBraces[0]}${placeholder.name}${replacementBraces[1]}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return easyMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replaces message with the interpolated variable name of the given placeholders
|
|
||||||
/// with the ability to change braces to something other than {...}.
|
|
||||||
///
|
|
||||||
/// Examples:
|
|
||||||
///
|
|
||||||
/// * Replacing `{userName}`.
|
|
||||||
/// ```dart
|
|
||||||
/// final message = 'Hello my name is {userName}';
|
|
||||||
/// final transformed = _replacePlaceholdersWithVariables(message, placeholders);
|
|
||||||
/// // transformed == 'Hello my name is $userName'
|
|
||||||
/// ```
|
|
||||||
/// * Replacing `#choice#`.
|
|
||||||
/// ```dart
|
|
||||||
/// final message = 'I would like to have some #choice#';
|
|
||||||
/// final transformed = _replacePlaceholdersWithVariables(message, placeholders, '##');
|
|
||||||
/// transformed == 'I would like to have some $choice'
|
|
||||||
/// ```
|
|
||||||
String _replacePlaceholdersWithVariables(String message, Iterable<Placeholder> placeholders, [String braces = '{}']) {
|
|
||||||
assert(braces.length == 2);
|
|
||||||
String messageWithValues = message;
|
|
||||||
for (final Placeholder placeholder in placeholders) {
|
|
||||||
String variable = placeholder.name;
|
|
||||||
if (placeholder.requiresFormatting) {
|
|
||||||
variable += 'String';
|
|
||||||
}
|
|
||||||
messageWithValues = messageWithValues.replaceAll(
|
|
||||||
'${braces[0]}${placeholder.name}${braces[1]}',
|
|
||||||
_needsCurlyBracketStringInterpolation(messageWithValues, placeholder.name)
|
|
||||||
? '\${$variable}'
|
|
||||||
: '\$$variable'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return messageWithValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
String _generatePluralMethod(Message message, String translationForMessage) {
|
|
||||||
if (message.placeholders.isEmpty) {
|
|
||||||
throw L10nException(
|
|
||||||
'Unable to find placeholders for the plural message: ${message.resourceId}.\n'
|
|
||||||
'Check to see if the plural message is in the proper ICU syntax format '
|
|
||||||
'and ensure that placeholders are properly specified.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final String easyMessage = _replacePlaceholdersBraces(translationForMessage, message.placeholders, '##');
|
|
||||||
|
|
||||||
final Placeholder countPlaceholder = message.getCountPlaceholder();
|
|
||||||
const Map<String, String> pluralIds = <String, String>{
|
|
||||||
'=0': 'zero',
|
|
||||||
'=1': 'one',
|
|
||||||
'=2': 'two',
|
|
||||||
'few': 'few',
|
|
||||||
'many': 'many',
|
|
||||||
'other': 'other',
|
|
||||||
};
|
|
||||||
|
|
||||||
final List<String> pluralLogicArgs = <String>[];
|
|
||||||
for (final String pluralKey in pluralIds.keys) {
|
|
||||||
final RegExp expRE = RegExp('($pluralKey)\\s*{([^}]+)}');
|
|
||||||
final RegExpMatch? match = expRE.firstMatch(easyMessage);
|
|
||||||
if (match != null && match.groupCount == 2) {
|
|
||||||
final String argValue = _replacePlaceholdersWithVariables(generateString(match.group(2)!), message.placeholders, '##');
|
|
||||||
pluralLogicArgs.add(' ${pluralIds[pluralKey]}: $argValue');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<String> parameters = message.placeholders.map((Placeholder placeholder) {
|
|
||||||
final String? placeholderType = placeholder == countPlaceholder ? 'num' : placeholder.type;
|
|
||||||
return '$placeholderType ${placeholder.name}';
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
final String comment = message.description ?? 'No description provided in @${message.resourceId}';
|
|
||||||
|
|
||||||
if (translationForMessage.startsWith('{') && translationForMessage.endsWith('}')) {
|
|
||||||
return pluralMethodTemplate
|
|
||||||
.replaceAll('@(comment)', comment)
|
|
||||||
.replaceAll('@(name)', message.resourceId)
|
|
||||||
.replaceAll('@(dateFormatting)', generateDateFormattingLogic(message))
|
|
||||||
.replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message))
|
|
||||||
.replaceAll('@(parameters)', parameters.join(', '))
|
|
||||||
.replaceAll('@(count)', countPlaceholder.name)
|
|
||||||
.replaceAll('@(pluralLogicArgs)', pluralLogicArgs.join(',\n'))
|
|
||||||
.replaceAll('@(none)\n', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
const String variable = 'pluralString';
|
|
||||||
final String string = _replaceWithVariable(translationForMessage, variable);
|
|
||||||
return pluralMethodTemplateInString
|
|
||||||
.replaceAll('@(comment)', comment)
|
|
||||||
.replaceAll('@(name)', message.resourceId)
|
|
||||||
.replaceAll('@(dateFormatting)', generateDateFormattingLogic(message))
|
|
||||||
.replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message))
|
|
||||||
.replaceAll('@(parameters)', parameters.join(', '))
|
|
||||||
.replaceAll('@(variable)', variable)
|
|
||||||
.replaceAll('@(count)', countPlaceholder.name)
|
|
||||||
.replaceAll('@(pluralLogicArgs)', pluralLogicArgs.join(',\n'))
|
|
||||||
.replaceAll('@(none)\n', '')
|
|
||||||
.replaceAll('@(string)', string);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _replaceWithVariable(String translation, String variable) {
|
|
||||||
String prefix = generateString(translation.substring(0, translation.indexOf('{')));
|
|
||||||
prefix = prefix.substring(0, prefix.length - 1);
|
|
||||||
String suffix = generateString(translation.substring(translation.lastIndexOf('}') + 1));
|
|
||||||
suffix = suffix.substring(1);
|
|
||||||
|
|
||||||
// escape variable when the suffix can be combined with the variable
|
|
||||||
if (suffix.isNotEmpty && !suffix.startsWith(' ')) {
|
|
||||||
variable = '{$variable}';
|
|
||||||
}
|
|
||||||
return prefix + r'$' + variable + suffix;
|
|
||||||
}
|
|
||||||
|
|
||||||
String _generateSelectMethod(Message message, String translationForMessage) {
|
|
||||||
if (message.placeholders.isEmpty) {
|
|
||||||
throw L10nException(
|
|
||||||
'Unable to find placeholders for the select message: ${message.resourceId}.\n'
|
|
||||||
'Check to see if the select message is in the proper ICU syntax format '
|
|
||||||
'and ensure that placeholders are properly specified.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final String easyMessage = _replacePlaceholdersBraces(translationForMessage, message.placeholders, '##');
|
|
||||||
|
|
||||||
final List<String> cases = <String>[];
|
|
||||||
|
|
||||||
final RegExpMatch? selectMatch = LocalizationsGenerator._selectRE.firstMatch(easyMessage);
|
|
||||||
String? choice;
|
|
||||||
if (selectMatch != null && selectMatch.groupCount == 2) {
|
|
||||||
choice = selectMatch.group(1);
|
|
||||||
final String pattern = selectMatch.group(2)!;
|
|
||||||
final RegExp patternRE = RegExp(r'\s*([\w\d]+)\s*\{(.*?)\}');
|
|
||||||
for (final RegExpMatch patternMatch in patternRE.allMatches(pattern)) {
|
|
||||||
if (patternMatch.groupCount == 2) {
|
|
||||||
String value = patternMatch.group(2)!
|
|
||||||
.replaceAll("'", r"\'")
|
|
||||||
.replaceAll('"', r'\"');
|
|
||||||
value = _replacePlaceholdersWithVariables(value, message.placeholders, '##');
|
|
||||||
cases.add(
|
|
||||||
" '${patternMatch.group(1)}': '$value'",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw L10nException(
|
|
||||||
'Incorrect select message format for: ${message.resourceId}.\n'
|
|
||||||
'Check to see if the select message is in the proper ICU syntax format.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<String> parameters = message.placeholders.map((Placeholder placeholder) {
|
|
||||||
final String placeholderType = placeholder.type ?? 'object';
|
|
||||||
return '$placeholderType ${placeholder.name}';
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
final String description = message.description ?? 'No description provided in @${message.resourceId}';
|
|
||||||
|
|
||||||
if (translationForMessage.startsWith('{') && translationForMessage.endsWith('}')) {
|
|
||||||
return selectMethodTemplate
|
|
||||||
.replaceAll('@(name)', message.resourceId)
|
|
||||||
.replaceAll('@(parameters)', parameters.join(', '))
|
|
||||||
.replaceAll('@(choice)', choice!)
|
|
||||||
.replaceAll('@(cases)', cases.join(',\n').trim())
|
|
||||||
.replaceAll('@(description)', description);
|
|
||||||
}
|
|
||||||
|
|
||||||
const String variable = 'selectString';
|
|
||||||
final String string = _replaceWithVariable(translationForMessage, variable);
|
|
||||||
return selectMethodTemplateInString
|
|
||||||
.replaceAll('@(name)', message.resourceId)
|
|
||||||
.replaceAll('@(parameters)', parameters.join(', '))
|
|
||||||
.replaceAll('@(variable)', variable)
|
|
||||||
.replaceAll('@(choice)', choice!)
|
|
||||||
.replaceAll('@(cases)', cases.join(',\n').trim())
|
|
||||||
.replaceAll('@(description)', description)
|
|
||||||
.replaceAll('@(string)', string);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _needsCurlyBracketStringInterpolation(String messageString, String placeholder) {
|
|
||||||
final int placeholderIndex = messageString.indexOf(placeholder);
|
|
||||||
// This means that this message does not contain placeholders/parameters,
|
|
||||||
// since one was not found in the message.
|
|
||||||
if (placeholderIndex == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final bool isPlaceholderEndOfSubstring = placeholderIndex + placeholder.length + 2 == messageString.length;
|
|
||||||
|
|
||||||
if (placeholderIndex > 2 && !isPlaceholderEndOfSubstring) {
|
|
||||||
// Normal case
|
|
||||||
// Examples:
|
|
||||||
// "'The number of {hours} elapsed is: 44'" // no curly brackets.
|
|
||||||
// "'哈{hours}哈'" // no curly brackets.
|
|
||||||
// "'m#hours#m'" // curly brackets.
|
|
||||||
// "'I have to work _#hours#_' sometimes." // curly brackets.
|
|
||||||
final RegExp commonCaseRE = RegExp('[^a-zA-Z_][#{]$placeholder[#}][^a-zA-Z_]');
|
|
||||||
return !commonCaseRE.hasMatch(messageString);
|
|
||||||
} else if (placeholderIndex == 2) {
|
|
||||||
// Example:
|
|
||||||
// "'{hours} elapsed.'" // no curly brackets
|
|
||||||
// '#placeholder# ' // no curly brackets
|
|
||||||
// '#placeholder#m' // curly brackets
|
|
||||||
final RegExp startOfString = RegExp('[#{]$placeholder[#}][^a-zA-Z_]');
|
|
||||||
return !startOfString.hasMatch(messageString);
|
|
||||||
} else {
|
|
||||||
// Example:
|
|
||||||
// "'hours elapsed: {hours}'"
|
|
||||||
// "'Time elapsed: {hours}'" // no curly brackets
|
|
||||||
// ' #placeholder#' // no curly brackets
|
|
||||||
// 'm#placeholder#' // curly brackets
|
|
||||||
final RegExp endOfString = RegExp('[^a-zA-Z_][#{]$placeholder[#}]');
|
|
||||||
return !endOfString.hasMatch(messageString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _generateMethod(Message message, String translationForMessage) {
|
|
||||||
String generateMessage() {
|
|
||||||
return _replacePlaceholdersWithVariables(generateString(translationForMessage), message.placeholders);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.isPlural) {
|
|
||||||
return _generatePluralMethod(message, translationForMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.isSelect) {
|
|
||||||
return _generateSelectMethod(message, translationForMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.placeholdersRequireFormatting) {
|
|
||||||
return formatMethodTemplate
|
|
||||||
.replaceAll('@(name)', message.resourceId)
|
|
||||||
.replaceAll('@(parameters)', generateMethodParameters(message).join(', '))
|
|
||||||
.replaceAll('@(dateFormatting)', generateDateFormattingLogic(message))
|
|
||||||
.replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message))
|
|
||||||
.replaceAll('@(message)', generateMessage())
|
|
||||||
.replaceAll('@(none)\n', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.placeholders.isNotEmpty) {
|
|
||||||
return methodTemplate
|
|
||||||
.replaceAll('@(name)', message.resourceId)
|
|
||||||
.replaceAll('@(parameters)', generateMethodParameters(message).join(', '))
|
|
||||||
.replaceAll('@(message)', generateMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return getterTemplate
|
|
||||||
.replaceAll('@(name)', message.resourceId)
|
|
||||||
.replaceAll('@(message)', generateMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
String generateBaseClassMethod(Message message, LocaleInfo? templateArbLocale) {
|
String generateBaseClassMethod(Message message, LocaleInfo? templateArbLocale) {
|
||||||
final String comment = message.description ?? 'No description provided for @${message.resourceId}.';
|
final String comment = message.description ?? 'No description provided for @${message.resourceId}.';
|
||||||
final String templateLocaleTranslationComment = '''
|
final String templateLocaleTranslationComment = '''
|
||||||
/// In $templateArbLocale, this message translates to:
|
/// In $templateArbLocale, this message translates to:
|
||||||
/// **${generateString(message.value)}**''';
|
/// **'${generateString(message.value)}'**''';
|
||||||
|
|
||||||
if (message.placeholders.isNotEmpty) {
|
if (message.placeholders.isNotEmpty) {
|
||||||
return baseClassMethodTemplate
|
return baseClassMethodTemplate
|
||||||
@ -806,6 +560,10 @@ 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;
|
||||||
|
|
||||||
/// The list of all arb path strings in [inputDirectory].
|
/// The list of all arb path strings in [inputDirectory].
|
||||||
List<String> get arbPathStrings {
|
List<String> get arbPathStrings {
|
||||||
return _allBundles.bundles.map((AppResourceBundle bundle) => bundle.file.path).toList();
|
return _allBundles.bundles.map((AppResourceBundle bundle) => bundle.file.path).toList();
|
||||||
@ -870,8 +628,6 @@ class LocalizationsGenerator {
|
|||||||
/// Logger to be used during the execution of the script.
|
/// Logger to be used during the execution of the script.
|
||||||
Logger logger;
|
Logger logger;
|
||||||
|
|
||||||
static final RegExp _selectRE = RegExp(r'\{([\w\s,]*),\s*select\s*,\s*([\w\d]+\s*\{.*\})+\s*\}');
|
|
||||||
|
|
||||||
static bool _isNotReadable(FileStat fileStat) {
|
static bool _isNotReadable(FileStat fileStat) {
|
||||||
final String rawStatString = fileStat.modeString();
|
final String rawStatString = fileStat.modeString();
|
||||||
// Removes potential prepended permission bits, such as '(suid)' and '(guid)'.
|
// Removes potential prepended permission bits, such as '(suid)' and '(guid)'.
|
||||||
@ -1087,7 +843,7 @@ class LocalizationsGenerator {
|
|||||||
// files in inputDirectory. Also initialized: supportedLocales.
|
// files in inputDirectory. Also initialized: supportedLocales.
|
||||||
void loadResources() {
|
void loadResources() {
|
||||||
_allMessages = _templateBundle.resourceIds.map((String id) => Message(
|
_allMessages = _templateBundle.resourceIds.map((String id) => Message(
|
||||||
_templateBundle.resources, id, areResourceAttributesRequired,
|
_templateBundle.resources, id, areResourceAttributesRequired,
|
||||||
));
|
));
|
||||||
for (final String resourceId in _templateBundle.resourceIds) {
|
for (final String resourceId in _templateBundle.resourceIds) {
|
||||||
if (!_isValidGetterAndMethodName(resourceId)) {
|
if (!_isValidGetterAndMethodName(resourceId)) {
|
||||||
@ -1148,25 +904,11 @@ class LocalizationsGenerator {
|
|||||||
|
|
||||||
return _generateMethod(
|
return _generateMethod(
|
||||||
message,
|
message,
|
||||||
|
bundle.file.basename,
|
||||||
bundle.translationFor(message) ?? templateBundle.translationFor(message)!,
|
bundle.translationFor(message) ?? templateBundle.translationFor(message)!,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (final Message message in messages) {
|
|
||||||
if (message.isPlural) {
|
|
||||||
if (message.placeholders.isEmpty) {
|
|
||||||
throw L10nException(
|
|
||||||
'Unable to find placeholders for the plural message: ${message.resourceId}.\n'
|
|
||||||
'Check to see if the plural message is in the proper ICU syntax format '
|
|
||||||
'and ensure that placeholders are properly specified.');
|
|
||||||
}
|
|
||||||
final Placeholder countPlaceholder = message.getCountPlaceholder();
|
|
||||||
if (countPlaceholder.type != null && countPlaceholder.type != 'num') {
|
|
||||||
logger.printWarning("Placeholders for plurals are automatically converted to type 'num' for the message: ${message.resourceId}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return classFileTemplate
|
return classFileTemplate
|
||||||
.replaceAll('@(header)', header.isEmpty ? '' : '$header\n\n')
|
.replaceAll('@(header)', header.isEmpty ? '' : '$header\n\n')
|
||||||
.replaceAll('@(language)', describeLocale(locale.toString()))
|
.replaceAll('@(language)', describeLocale(locale.toString()))
|
||||||
@ -1175,7 +917,7 @@ class LocalizationsGenerator {
|
|||||||
.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" : '');
|
.replaceAll('@(requiresIntlImport)', requiresIntlImport ? "import 'package:intl/intl.dart' as intl;\n\n" : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
String _generateSubclass(
|
String _generateSubclass(
|
||||||
@ -1194,7 +936,7 @@ class LocalizationsGenerator {
|
|||||||
|
|
||||||
final Iterable<String> methods = messages
|
final Iterable<String> methods = messages
|
||||||
.where((Message message) => bundle.translationFor(message) != null)
|
.where((Message message) => bundle.translationFor(message) != null)
|
||||||
.map((Message message) => _generateMethod(message, bundle.translationFor(message)!));
|
.map((Message message) => _generateMethod(message, bundle.file.basename, bundle.translationFor(message)!));
|
||||||
|
|
||||||
return subclassTemplate
|
return subclassTemplate
|
||||||
.replaceAll('@(language)', describeLocale(locale.toString()))
|
.replaceAll('@(language)', describeLocale(locale.toString()))
|
||||||
@ -1328,7 +1070,7 @@ 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('@(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.
|
||||||
@ -1337,11 +1079,207 @@ class LocalizationsGenerator {
|
|||||||
.replaceAll('\n\n\n', '\n\n');
|
.replaceAll('\n\n\n', '\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _requiresIntlImport() => _allMessages.any((Message message) {
|
String _generateMethod(Message message, String filename, String translationForMessage) {
|
||||||
return message.isPlural
|
// Determine if we must import intl for date or number formatting.
|
||||||
|| message.isSelect
|
if (message.placeholdersRequireFormatting) {
|
||||||
|| message.placeholdersRequireFormatting;
|
requiresIntlImport = true;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
final Node node = Parser(message.resourceId, filename, translationForMessage).parse();
|
||||||
|
// If parse tree is only a string, then return a getter method.
|
||||||
|
if (node.children.every((Node child) => child.type == ST.string)) {
|
||||||
|
// Use the parsed translation to handle escaping with the same behavior.
|
||||||
|
return getterTemplate
|
||||||
|
.replaceAll('@(name)', message.resourceId)
|
||||||
|
.replaceAll('@(message)', "'${generateString(node.children.map((Node child) => child.value!).join())}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<String> helperMethods = <String>[];
|
||||||
|
|
||||||
|
// Get a unique helper method name.
|
||||||
|
int methodNameCount = 0;
|
||||||
|
String getHelperMethodName() {
|
||||||
|
return '_${message.resourceId}${methodNameCount++}';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do a DFS post order traversal, generating dependent
|
||||||
|
// placeholder, plural, select helper methods, and combine these into
|
||||||
|
// one message. Returns the method/placeholder to use in parent string.
|
||||||
|
HelperMethod generateHelperMethods(Node node, { bool isRoot = false }) {
|
||||||
|
final Set<Placeholder> dependentPlaceholders = <Placeholder>{};
|
||||||
|
switch (node.type) {
|
||||||
|
case ST.message:
|
||||||
|
final List<HelperMethod> helpers = node.children.map<HelperMethod>((Node node) {
|
||||||
|
if (node.type == ST.string) {
|
||||||
|
return HelperMethod(<Placeholder>{}, string: node.value);
|
||||||
|
}
|
||||||
|
final HelperMethod helper = generateHelperMethods(node);
|
||||||
|
dependentPlaceholders.addAll(helper.dependentPlaceholders);
|
||||||
|
return helper;
|
||||||
|
}).toList();
|
||||||
|
final String messageString = generateReturnExpr(helpers);
|
||||||
|
|
||||||
|
// If the message is just a normal string, then only return the string.
|
||||||
|
if (dependentPlaceholders.isEmpty) {
|
||||||
|
return HelperMethod(dependentPlaceholders, string: messageString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For messages, if we are generating the actual overridden method, then we should also deal with
|
||||||
|
// date and number formatting here.
|
||||||
|
final String helperMethodName = getHelperMethodName();
|
||||||
|
final HelperMethod messageHelper = HelperMethod(dependentPlaceholders, helper: helperMethodName);
|
||||||
|
if (isRoot) {
|
||||||
|
helperMethods.add(methodTemplate
|
||||||
|
.replaceAll('@(name)', message.resourceId)
|
||||||
|
.replaceAll('@(parameters)', generateMethodParameters(message).join(', '))
|
||||||
|
.replaceAll('@(dateFormatting)', generateDateFormattingLogic(message))
|
||||||
|
.replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message))
|
||||||
|
.replaceAll('@(message)', messageString)
|
||||||
|
.replaceAll('@(none)\n', '')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
helperMethods.add(messageHelperTemplate
|
||||||
|
.replaceAll('@(name)', helperMethodName)
|
||||||
|
.replaceAll('@(parameters)', messageHelper.methodParameters)
|
||||||
|
.replaceAll('@(message)', messageString)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return messageHelper;
|
||||||
|
|
||||||
|
case ST.placeholderExpr:
|
||||||
|
assert(node.children[1].type == ST.identifier);
|
||||||
|
final Node identifier = node.children[1];
|
||||||
|
// Check that placeholders exist.
|
||||||
|
// TODO(thkim1011): Make message.placeholders a map so that we don't need to do linear time search.
|
||||||
|
// See https://github.com/flutter/flutter/issues/112709
|
||||||
|
final Placeholder placeholder = message.placeholders.firstWhere(
|
||||||
|
(Placeholder placeholder) => placeholder.name == identifier.value,
|
||||||
|
orElse: () {
|
||||||
|
throw L10nException('''
|
||||||
|
Make sure that the specified placeholder is defined in your arb file.
|
||||||
|
$translationForMessage
|
||||||
|
${Parser.indentForError(identifier.positionInMessage)}''');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
dependentPlaceholders.add(placeholder);
|
||||||
|
return HelperMethod(dependentPlaceholders, placeholder: placeholder);
|
||||||
|
|
||||||
|
case ST.pluralExpr:
|
||||||
|
requiresIntlImport = true;
|
||||||
|
final Map<String, String> pluralLogicArgs = <String, String>{};
|
||||||
|
// Recall that pluralExpr are of the form
|
||||||
|
// pluralExpr := "{" ID "," "plural" "," pluralParts "}"
|
||||||
|
assert(node.children[1].type == ST.identifier);
|
||||||
|
assert(node.children[5].type == ST.pluralParts);
|
||||||
|
|
||||||
|
final Node identifier = node.children[1];
|
||||||
|
final Node pluralParts = node.children[5];
|
||||||
|
|
||||||
|
// Check that identifier exists and is of type int or num.
|
||||||
|
final Placeholder placeholder = message.placeholders.firstWhere(
|
||||||
|
(Placeholder placeholder) => placeholder.name == identifier.value,
|
||||||
|
orElse: () {
|
||||||
|
throw L10nException('''
|
||||||
|
Make sure that the specified plural placeholder is defined in your arb file.
|
||||||
|
$translationForMessage
|
||||||
|
${List<String>.filled(identifier.positionInMessage, ' ').join()}^''');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
dependentPlaceholders.add(placeholder);
|
||||||
|
// TODO(thkim1011): Uncomment the following lines after Message refactor.
|
||||||
|
// See https://github.com/flutter/flutter/issues/112709.
|
||||||
|
// if (placeholder.type != 'num' && placeholder.type != 'int') {
|
||||||
|
// throw L10nException('''
|
||||||
|
// The specified placeholder must be of type int or num.
|
||||||
|
// $translationForMessage
|
||||||
|
// ${List<String>.filled(identifier.positionInMessage, ' ').join()}^''');
|
||||||
|
// }
|
||||||
|
|
||||||
|
for (final Node pluralPart in pluralParts.children.reversed) {
|
||||||
|
String pluralCase;
|
||||||
|
Node pluralMessage;
|
||||||
|
if (pluralPart.children[0].value == '=') {
|
||||||
|
assert(pluralPart.children[1].type == ST.number);
|
||||||
|
assert(pluralPart.children[3].type == ST.message);
|
||||||
|
pluralCase = pluralPart.children[1].value!;
|
||||||
|
pluralMessage = pluralPart.children[3];
|
||||||
|
} else {
|
||||||
|
assert(pluralPart.children[0].type == ST.identifier || pluralPart.children[0].type == ST.other);
|
||||||
|
assert(pluralPart.children[2].type == ST.message);
|
||||||
|
pluralCase = pluralPart.children[0].value!;
|
||||||
|
pluralMessage = pluralPart.children[2];
|
||||||
|
}
|
||||||
|
if (!pluralLogicArgs.containsKey(pluralCases[pluralCase])) {
|
||||||
|
final HelperMethod pluralPartHelper = generateHelperMethods(pluralMessage);
|
||||||
|
pluralLogicArgs[pluralCases[pluralCase]!] = ' ${pluralCases[pluralCase]}: ${pluralPartHelper.helperOrPlaceholder},';
|
||||||
|
dependentPlaceholders.addAll(pluralPartHelper.dependentPlaceholders);
|
||||||
|
} else {
|
||||||
|
logger.printWarning('''
|
||||||
|
The plural part specified below is overrided by a later plural part.
|
||||||
|
$translationForMessage
|
||||||
|
${Parser.indentForError(pluralPart.positionInMessage)}
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final String helperMethodName = getHelperMethodName();
|
||||||
|
final HelperMethod pluralHelper = HelperMethod(dependentPlaceholders, helper: helperMethodName);
|
||||||
|
helperMethods.add(pluralHelperTemplate
|
||||||
|
.replaceAll('@(name)', helperMethodName)
|
||||||
|
.replaceAll('@(parameters)', pluralHelper.methodParameters)
|
||||||
|
.replaceAll('@(count)', identifier.value!)
|
||||||
|
.replaceAll('@(pluralLogicArgs)', pluralLogicArgs.values.join('\n'))
|
||||||
|
);
|
||||||
|
return pluralHelper;
|
||||||
|
|
||||||
|
case ST.selectExpr:
|
||||||
|
requiresIntlImport = true;
|
||||||
|
// Recall that pluralExpr are of the form
|
||||||
|
// pluralExpr := "{" ID "," "plural" "," pluralParts "}"
|
||||||
|
assert(node.children[1].type == ST.identifier);
|
||||||
|
assert(node.children[5].type == ST.selectParts);
|
||||||
|
|
||||||
|
final Node identifier = node.children[1];
|
||||||
|
// Check that identifier exists
|
||||||
|
final Placeholder placeholder = message.placeholders.firstWhere(
|
||||||
|
(Placeholder placeholder) => placeholder.name == identifier.value,
|
||||||
|
orElse: () {
|
||||||
|
throw L10nException('''
|
||||||
|
Make sure that the specified select placeholder is defined in your arb file.
|
||||||
|
$translationForMessage
|
||||||
|
${Parser.indentForError(identifier.positionInMessage)}''');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
dependentPlaceholders.add(placeholder);
|
||||||
|
final List<String> selectLogicArgs = <String>[];
|
||||||
|
final Node selectParts = node.children[5];
|
||||||
|
|
||||||
|
for (final Node selectPart in selectParts.children) {
|
||||||
|
assert(selectPart.children[0].type == ST.identifier || selectPart.children[0].type == ST.other);
|
||||||
|
assert(selectPart.children[2].type == ST.message);
|
||||||
|
final String selectCase = selectPart.children[0].value!;
|
||||||
|
final Node selectMessage = selectPart.children[2];
|
||||||
|
final HelperMethod selectPartHelper = generateHelperMethods(selectMessage);
|
||||||
|
selectLogicArgs.add(" '$selectCase': ${selectPartHelper.helperOrPlaceholder},");
|
||||||
|
dependentPlaceholders.addAll(selectPartHelper.dependentPlaceholders);
|
||||||
|
}
|
||||||
|
final String helperMethodName = getHelperMethodName();
|
||||||
|
final HelperMethod selectHelper = HelperMethod(dependentPlaceholders, helper: helperMethodName);
|
||||||
|
|
||||||
|
helperMethods.add(selectHelperTemplate
|
||||||
|
.replaceAll('@(name)', helperMethodName)
|
||||||
|
.replaceAll('@(parameters)', selectHelper.methodParameters)
|
||||||
|
.replaceAll('@(choice)', identifier.value!)
|
||||||
|
.replaceAll('@(selectCases)', selectLogicArgs.join('\n'))
|
||||||
|
);
|
||||||
|
return HelperMethod(dependentPlaceholders, helper: helperMethodName);
|
||||||
|
// ignore: no_default_cases
|
||||||
|
default:
|
||||||
|
throw Exception('Cannot call "generateHelperMethod" on node type ${node.type}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generateHelperMethods(node, isRoot: true);
|
||||||
|
return helperMethods.last.replaceAll('@(helperMethods)', helperMethods.sublist(0, helperMethods.length - 1).join('\n\n'));
|
||||||
|
}
|
||||||
|
|
||||||
List<String> writeOutputFiles({ bool isFromYaml = false }) {
|
List<String> writeOutputFiles({ bool isFromYaml = false }) {
|
||||||
// First, generate the string contents of all necessary files.
|
// First, generate the string contents of all necessary files.
|
||||||
|
@ -137,68 +137,35 @@ const String getterTemplate = '''
|
|||||||
const String methodTemplate = '''
|
const String methodTemplate = '''
|
||||||
@override
|
@override
|
||||||
String @(name)(@(parameters)) {
|
String @(name)(@(parameters)) {
|
||||||
|
@(dateFormatting)
|
||||||
|
@(numberFormatting)
|
||||||
|
@(helperMethods)
|
||||||
return @(message);
|
return @(message);
|
||||||
}''';
|
}''';
|
||||||
|
|
||||||
const String formatMethodTemplate = '''
|
const String messageHelperTemplate = '''
|
||||||
@override
|
String @(name)(@(parameters)) {
|
||||||
String @(name)(@(parameters)) {
|
return @(message);
|
||||||
@(dateFormatting)
|
}''';
|
||||||
@(numberFormatting)
|
|
||||||
return @(message);
|
|
||||||
}''';
|
|
||||||
|
|
||||||
const String pluralMethodTemplate = '''
|
const String pluralHelperTemplate = '''
|
||||||
@override
|
String @(name)(@(parameters)) {
|
||||||
String @(name)(@(parameters)) {
|
return intl.Intl.pluralLogic(
|
||||||
@(dateFormatting)
|
@(count),
|
||||||
@(numberFormatting)
|
locale: localeName,
|
||||||
return intl.Intl.pluralLogic(
|
@(pluralLogicArgs)
|
||||||
@(count),
|
);
|
||||||
locale: localeName,
|
}''';
|
||||||
@(pluralLogicArgs),
|
|
||||||
);
|
|
||||||
}''';
|
|
||||||
|
|
||||||
const String pluralMethodTemplateInString = '''
|
const String selectHelperTemplate = '''
|
||||||
@override
|
String @(name)(@(parameters)) {
|
||||||
String @(name)(@(parameters)) {
|
return intl.Intl.selectLogic(
|
||||||
@(dateFormatting)
|
@(choice),
|
||||||
@(numberFormatting)
|
{
|
||||||
final String @(variable) = intl.Intl.pluralLogic(
|
@(selectCases)
|
||||||
@(count),
|
},
|
||||||
locale: localeName,
|
);
|
||||||
@(pluralLogicArgs),
|
}''';
|
||||||
);
|
|
||||||
|
|
||||||
return @(string);
|
|
||||||
}''';
|
|
||||||
|
|
||||||
const String selectMethodTemplate = '''
|
|
||||||
@override
|
|
||||||
String @(name)(@(parameters)) {
|
|
||||||
return intl.Intl.select(
|
|
||||||
@(choice),
|
|
||||||
{
|
|
||||||
@(cases)
|
|
||||||
},
|
|
||||||
desc: '@(description)'
|
|
||||||
);
|
|
||||||
}''';
|
|
||||||
|
|
||||||
const String selectMethodTemplateInString = '''
|
|
||||||
@override
|
|
||||||
String @(name)(@(parameters)) {
|
|
||||||
final String @(variable) = intl.Intl.select(
|
|
||||||
@(choice),
|
|
||||||
{
|
|
||||||
@(cases)
|
|
||||||
},
|
|
||||||
desc: '@(description)'
|
|
||||||
);
|
|
||||||
|
|
||||||
return @(string);
|
|
||||||
}''';
|
|
||||||
|
|
||||||
const String classFileTemplate = '''
|
const String classFileTemplate = '''
|
||||||
@(header)@(requiresIntlImport)import '@(fileName)';
|
@(header)@(requiresIntlImport)import '@(fileName)';
|
||||||
|
@ -129,6 +129,25 @@ class L10nException implements Exception {
|
|||||||
String toString() => message;
|
String toString() => message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class L10nParserException extends L10nException {
|
||||||
|
L10nParserException(
|
||||||
|
this.error,
|
||||||
|
this.fileName,
|
||||||
|
this.messageId,
|
||||||
|
this.messageString,
|
||||||
|
this.charNumber
|
||||||
|
): super('''
|
||||||
|
$error
|
||||||
|
[$fileName:$messageId] $messageString
|
||||||
|
${List<String>.filled(4 + fileName.length + messageId.length + charNumber, ' ').join()}^''');
|
||||||
|
|
||||||
|
final String error;
|
||||||
|
final String fileName;
|
||||||
|
final String messageId;
|
||||||
|
final String messageString;
|
||||||
|
final int charNumber;
|
||||||
|
}
|
||||||
|
|
||||||
// One optional named parameter to be used by a NumberFormat.
|
// One optional named parameter to be used by a NumberFormat.
|
||||||
//
|
//
|
||||||
// Some of the NumberFormat factory constructors have optional named parameters.
|
// Some of the NumberFormat factory constructors have optional named parameters.
|
||||||
@ -202,16 +221,16 @@ class Placeholder {
|
|||||||
final String resourceId;
|
final String resourceId;
|
||||||
final String name;
|
final String name;
|
||||||
final String? example;
|
final String? example;
|
||||||
final String? type;
|
String? type;
|
||||||
final String? format;
|
final String? format;
|
||||||
final List<OptionalParameter> optionalParameters;
|
final List<OptionalParameter> optionalParameters;
|
||||||
final bool? isCustomDateFormat;
|
final bool? isCustomDateFormat;
|
||||||
|
|
||||||
bool get requiresFormatting => <String>['DateTime', 'double', 'num'].contains(type) || (type == 'int' && format != null);
|
bool get requiresFormatting => requiresDateFormatting || requiresNumFormatting;
|
||||||
bool get isNumber => <String>['double', 'int', 'num'].contains(type);
|
bool get requiresDateFormatting => type == 'DateTime';
|
||||||
|
bool get requiresNumFormatting => <String>['int', 'num', 'double'].contains(type) && format != null;
|
||||||
bool get hasValidNumberFormat => _validNumberFormats.contains(format);
|
bool get hasValidNumberFormat => _validNumberFormats.contains(format);
|
||||||
bool get hasNumberFormatWithParameters => _numberFormatsWithNamedParameters.contains(format);
|
bool get hasNumberFormatWithParameters => _numberFormatsWithNamedParameters.contains(format);
|
||||||
bool get isDate => 'DateTime' == type;
|
|
||||||
bool get hasValidDateFormat => _validDateFormats.contains(format);
|
bool get hasValidDateFormat => _validDateFormats.contains(format);
|
||||||
|
|
||||||
static String? _stringAttribute(
|
static String? _stringAttribute(
|
||||||
@ -290,6 +309,8 @@ class Placeholder {
|
|||||||
// The value of this Message is "Hello World". The Message's value is the
|
// The value of this Message is "Hello World". The Message's value is the
|
||||||
// localized string to be shown for the template ARB file's locale.
|
// localized string to be shown for the template ARB file's locale.
|
||||||
// The docs for the Placeholder explain how placeholder entries are defined.
|
// The docs for the Placeholder explain how placeholder entries are defined.
|
||||||
|
// TODO(thkim1011): We need to refactor this Message class to own all the messages in each language.
|
||||||
|
// See https://github.com/flutter/flutter/issues/112709.
|
||||||
class Message {
|
class Message {
|
||||||
Message(Map<String, Object?> bundle, this.resourceId, bool isResourceAttributeRequired)
|
Message(Map<String, Object?> bundle, this.resourceId, bool isResourceAttributeRequired)
|
||||||
: assert(bundle != null),
|
: assert(bundle != null),
|
||||||
@ -298,7 +319,12 @@ class Message {
|
|||||||
description = _description(bundle, resourceId, isResourceAttributeRequired),
|
description = _description(bundle, resourceId, isResourceAttributeRequired),
|
||||||
placeholders = _placeholders(bundle, resourceId, isResourceAttributeRequired),
|
placeholders = _placeholders(bundle, resourceId, isResourceAttributeRequired),
|
||||||
_pluralMatch = _pluralRE.firstMatch(_value(bundle, resourceId)),
|
_pluralMatch = _pluralRE.firstMatch(_value(bundle, resourceId)),
|
||||||
_selectMatch = _selectRE.firstMatch(_value(bundle, resourceId));
|
_selectMatch = _selectRE.firstMatch(_value(bundle, resourceId)) {
|
||||||
|
if (isPlural) {
|
||||||
|
final Placeholder placeholder = getCountPlaceholder();
|
||||||
|
placeholder.type = 'num';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static final RegExp _pluralRE = RegExp(r'\s*\{([\w\s,]*),\s*plural\s*,');
|
static final RegExp _pluralRE = RegExp(r'\s*\{([\w\s,]*),\s*plural\s*,');
|
||||||
static final RegExp _selectRE = RegExp(r'\s*\{([\w\s,]*),\s*select\s*,');
|
static final RegExp _selectRE = RegExp(r'\s*\{([\w\s,]*),\s*select\s*,');
|
||||||
@ -769,3 +795,50 @@ final Set<String> _iso639Languages = <String>{
|
|||||||
'zh',
|
'zh',
|
||||||
'zu',
|
'zu',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Used in LocalizationsGenerator._generateMethod.generateHelperMethod.
|
||||||
|
class HelperMethod {
|
||||||
|
HelperMethod(this.dependentPlaceholders, {this.helper, this.placeholder, this.string }):
|
||||||
|
assert((() {
|
||||||
|
// At least one of helper, placeholder, string must be nonnull.
|
||||||
|
final bool a = helper == null;
|
||||||
|
final bool b = placeholder == null;
|
||||||
|
final bool c = string == null;
|
||||||
|
return (!a && b && c) || (a && !b && c) || (a && b && !c);
|
||||||
|
})());
|
||||||
|
|
||||||
|
Set<Placeholder> dependentPlaceholders;
|
||||||
|
String? helper;
|
||||||
|
Placeholder? placeholder;
|
||||||
|
String? string;
|
||||||
|
|
||||||
|
String get helperOrPlaceholder {
|
||||||
|
if (helper != null) {
|
||||||
|
return '$helper($methodArguments)';
|
||||||
|
} else if (string != null) {
|
||||||
|
return '$string';
|
||||||
|
} else {
|
||||||
|
if (placeholder!.requiresFormatting) {
|
||||||
|
return '${placeholder!.name}String';
|
||||||
|
} else {
|
||||||
|
return placeholder!.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get methodParameters {
|
||||||
|
assert(helper != null);
|
||||||
|
return dependentPlaceholders.map((Placeholder placeholder) =>
|
||||||
|
(placeholder.requiresFormatting)
|
||||||
|
? 'String ${placeholder.name}String'
|
||||||
|
: '${placeholder.type} ${placeholder.name}').join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
String get methodArguments {
|
||||||
|
assert(helper != null);
|
||||||
|
return dependentPlaceholders.map((Placeholder placeholder) =>
|
||||||
|
(placeholder.requiresFormatting)
|
||||||
|
? '${placeholder.name}String'
|
||||||
|
: placeholder.name).join(', ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -292,7 +292,34 @@ String generateString(String value) {
|
|||||||
// Reintroduce escaped backslashes into generated Dart string.
|
// Reintroduce escaped backslashes into generated Dart string.
|
||||||
.replaceAll(backslash, r'\\');
|
.replaceAll(backslash, r'\\');
|
||||||
|
|
||||||
return "'$value'";
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a list of strings, placeholders, or helper function calls, concatenate
|
||||||
|
/// them into one expression to be returned.
|
||||||
|
String generateReturnExpr(List<HelperMethod> helpers) {
|
||||||
|
if (helpers.isEmpty) {
|
||||||
|
return "''";
|
||||||
|
} else if (
|
||||||
|
helpers.length == 1
|
||||||
|
&& helpers[0].string == null
|
||||||
|
&& (helpers[0].placeholder?.type == 'String' || helpers[0].helper != null)
|
||||||
|
) {
|
||||||
|
return helpers[0].helperOrPlaceholder;
|
||||||
|
} else {
|
||||||
|
final String string = helpers.reversed.fold<String>('', (String string, HelperMethod helper) {
|
||||||
|
if (helper.string != null) {
|
||||||
|
return generateString(helper.string!) + string;
|
||||||
|
}
|
||||||
|
final RegExp alphanumeric = RegExp(r'^([0-9a-zA-Z]|_)+$');
|
||||||
|
if (alphanumeric.hasMatch(helper.helperOrPlaceholder) && !(string.isNotEmpty && alphanumeric.hasMatch(string[0]))) {
|
||||||
|
return '\$${helper.helperOrPlaceholder}$string';
|
||||||
|
} else {
|
||||||
|
return '\${${helper.helperOrPlaceholder}}$string';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return "'$string'";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Typed configuration from the localizations config file.
|
/// Typed configuration from the localizations config file.
|
||||||
|
543
packages/flutter_tools/lib/src/localizations/message_parser.dart
Normal file
543
packages/flutter_tools/lib/src/localizations/message_parser.dart
Normal file
@ -0,0 +1,543 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
// The whole design for the lexing and parsing step can be found in this design doc.
|
||||||
|
// See https://flutter.dev/go/icu-message-parser.
|
||||||
|
|
||||||
|
// Symbol Types
|
||||||
|
import 'gen_l10n_types.dart';
|
||||||
|
|
||||||
|
enum ST {
|
||||||
|
// Terminal Types
|
||||||
|
openBrace,
|
||||||
|
closeBrace,
|
||||||
|
comma,
|
||||||
|
equalSign,
|
||||||
|
other,
|
||||||
|
plural,
|
||||||
|
select,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
identifier,
|
||||||
|
empty,
|
||||||
|
// Nonterminal Types
|
||||||
|
message,
|
||||||
|
|
||||||
|
placeholderExpr,
|
||||||
|
|
||||||
|
pluralExpr,
|
||||||
|
pluralParts,
|
||||||
|
pluralPart,
|
||||||
|
|
||||||
|
selectExpr,
|
||||||
|
selectParts,
|
||||||
|
selectPart,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The grammar of the syntax.
|
||||||
|
Map<ST, List<List<ST>>> grammar = <ST, List<List<ST>>>{
|
||||||
|
ST.message: <List<ST>>[
|
||||||
|
<ST>[ST.string, ST.message],
|
||||||
|
<ST>[ST.placeholderExpr, ST.message],
|
||||||
|
<ST>[ST.pluralExpr, ST.message],
|
||||||
|
<ST>[ST.selectExpr, ST.message],
|
||||||
|
<ST>[ST.empty],
|
||||||
|
],
|
||||||
|
ST.placeholderExpr: <List<ST>>[
|
||||||
|
<ST>[ST.openBrace, ST.identifier, ST.closeBrace],
|
||||||
|
],
|
||||||
|
ST.pluralExpr: <List<ST>>[
|
||||||
|
<ST>[ST.openBrace, ST.identifier, ST.comma, ST.plural, ST.comma, ST.pluralParts, ST.closeBrace],
|
||||||
|
],
|
||||||
|
ST.pluralParts: <List<ST>>[
|
||||||
|
<ST>[ST.pluralPart, ST.pluralParts],
|
||||||
|
<ST>[ST.empty],
|
||||||
|
],
|
||||||
|
ST.pluralPart: <List<ST>>[
|
||||||
|
<ST>[ST.identifier, ST.openBrace, ST.message, ST.closeBrace],
|
||||||
|
<ST>[ST.equalSign, ST.number, ST.openBrace, ST.message, ST.closeBrace],
|
||||||
|
<ST>[ST.other, ST.openBrace, ST.message, ST.closeBrace],
|
||||||
|
],
|
||||||
|
ST.selectExpr: <List<ST>>[
|
||||||
|
<ST>[ST.openBrace, ST.identifier, ST.comma, ST.select, ST.comma, ST.selectParts, ST.closeBrace],
|
||||||
|
<ST>[ST.other, ST.openBrace, ST.message, ST.closeBrace],
|
||||||
|
],
|
||||||
|
ST.selectParts: <List<ST>>[
|
||||||
|
<ST>[ST.selectPart, ST.selectParts],
|
||||||
|
<ST>[ST.empty],
|
||||||
|
],
|
||||||
|
ST.selectPart: <List<ST>>[
|
||||||
|
<ST>[ST.identifier, ST.openBrace, ST.message, ST.closeBrace],
|
||||||
|
<ST>[ST.other, ST.openBrace, ST.message, ST.closeBrace],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
class Node {
|
||||||
|
Node(this.type, this.positionInMessage, { this.expectedSymbolCount = 0, this.value, List<Node>? children }): children = children ?? <Node>[];
|
||||||
|
|
||||||
|
// Token constructors.
|
||||||
|
Node.openBrace(this.positionInMessage): type = ST.openBrace, value = '{';
|
||||||
|
Node.closeBrace(this.positionInMessage): type = ST.closeBrace, value = '}';
|
||||||
|
Node.brace(this.positionInMessage, String this.value) {
|
||||||
|
if (value == '{') {
|
||||||
|
type = ST.openBrace;
|
||||||
|
} else if (value == '}') {
|
||||||
|
type = ST.closeBrace;
|
||||||
|
} else {
|
||||||
|
// We should never arrive here.
|
||||||
|
throw L10nException('Provided value $value is not a brace.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Node.equalSign(this.positionInMessage): type = ST.equalSign, value = '=';
|
||||||
|
Node.comma(this.positionInMessage): type = ST.comma, value = ',';
|
||||||
|
Node.string(this.positionInMessage, String this.value): type = ST.string;
|
||||||
|
Node.number(this.positionInMessage, String this.value): type = ST.number;
|
||||||
|
Node.identifier(this.positionInMessage, String this.value): type = ST.identifier;
|
||||||
|
Node.pluralKeyword(this.positionInMessage): type = ST.plural, value = 'plural';
|
||||||
|
Node.selectKeyword(this.positionInMessage): type = ST.select, value = 'select';
|
||||||
|
Node.otherKeyword(this.positionInMessage): type = ST.other, value = 'other';
|
||||||
|
Node.empty(this.positionInMessage): type = ST.empty, value = '';
|
||||||
|
|
||||||
|
String? value;
|
||||||
|
late ST type;
|
||||||
|
List<Node> children = <Node>[];
|
||||||
|
int positionInMessage;
|
||||||
|
int expectedSymbolCount = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return _toStringHelper(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _toStringHelper(int indentLevel) {
|
||||||
|
final String indent = List<String>.filled(indentLevel, ' ').join();
|
||||||
|
if (children.isEmpty) {
|
||||||
|
return '''
|
||||||
|
${indent}Node($type, $positionInMessage${value == null ? '' : ", value: '$value'"})''';
|
||||||
|
}
|
||||||
|
final String childrenString = children.map((Node child) => child._toStringHelper(indentLevel + 1)).join(',\n');
|
||||||
|
return '''
|
||||||
|
${indent}Node($type, $positionInMessage${value == null ? '' : ", value: '$value'"}, children: <Node>[
|
||||||
|
$childrenString,
|
||||||
|
$indent])''';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only used for testing. We don't compare expectedSymbolCount because
|
||||||
|
// it is an auxiliary member used during the parse function but doesn't
|
||||||
|
// have meaning after calling compress.
|
||||||
|
@override
|
||||||
|
// ignore: avoid_equals_and_hash_code_on_mutable_classes, hash_and_equals
|
||||||
|
bool operator==(covariant Node other) {
|
||||||
|
if(value != other.value
|
||||||
|
|| type != other.type
|
||||||
|
|| positionInMessage != other.positionInMessage
|
||||||
|
|| children.length != other.children.length
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < children.length; i++) {
|
||||||
|
if (children[i] != other.children[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isFull {
|
||||||
|
return children.length >= expectedSymbolCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RegExp unescapedString = RegExp(r'[^{}]+');
|
||||||
|
RegExp brace = RegExp(r'{|}');
|
||||||
|
|
||||||
|
RegExp whitespace = RegExp(r'\s+');
|
||||||
|
RegExp pluralKeyword = RegExp(r'plural');
|
||||||
|
RegExp selectKeyword = RegExp(r'select');
|
||||||
|
RegExp otherKeyword = RegExp(r'other');
|
||||||
|
RegExp numeric = RegExp(r'[0-9]+');
|
||||||
|
RegExp alphanumeric = RegExp(r'[a-zA-Z0-9]+');
|
||||||
|
RegExp comma = RegExp(r',');
|
||||||
|
RegExp equalSign = RegExp(r'=');
|
||||||
|
|
||||||
|
// List of token matchers ordered by precedence
|
||||||
|
Map<ST, RegExp> matchers = <ST, RegExp>{
|
||||||
|
ST.empty: whitespace,
|
||||||
|
ST.plural: pluralKeyword,
|
||||||
|
ST.select: selectKeyword,
|
||||||
|
ST.other: otherKeyword,
|
||||||
|
ST.number: numeric,
|
||||||
|
ST.comma: comma,
|
||||||
|
ST.equalSign: equalSign,
|
||||||
|
ST.identifier: alphanumeric,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Parser {
|
||||||
|
Parser(this.messageId, this.filename, this.messageString);
|
||||||
|
|
||||||
|
final String messageId;
|
||||||
|
final String messageString;
|
||||||
|
final String filename;
|
||||||
|
|
||||||
|
static String indentForError(int position) {
|
||||||
|
return '${List<String>.filled(position, ' ').join()}^';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lexes the message into a list of typed tokens. General idea is that
|
||||||
|
// every instance of "{" and "}" toggles the isString boolean and every
|
||||||
|
// instance of "'" toggles the isEscaped boolean (and treats a double
|
||||||
|
// single quote "''" as a single quote "'"). When !isString and !isEscaped
|
||||||
|
// delimit tokens by whitespace and special characters.
|
||||||
|
List<Node> lexIntoTokens() {
|
||||||
|
final List<Node> tokens = <Node>[];
|
||||||
|
bool isString = true;
|
||||||
|
// Index specifying where to match from
|
||||||
|
int startIndex = 0;
|
||||||
|
|
||||||
|
// At every iteration, we should be able to match a new token until we
|
||||||
|
// reach the end of the string. If for some reason we don't match a
|
||||||
|
// token in any iteration of the loop, throw an error.
|
||||||
|
while (startIndex < messageString.length) {
|
||||||
|
Match? match;
|
||||||
|
if (isString) {
|
||||||
|
// TODO(thkim1011): Uncomment this when we add escaping as an option.
|
||||||
|
// See https://github.com/flutter/flutter/issues/113455.
|
||||||
|
// match = escapedString.matchAsPrefix(message, startIndex);
|
||||||
|
// if (match != null) {
|
||||||
|
// final String string = match.group(0)!;
|
||||||
|
// tokens.add(Node.string(startIndex, string == "''" ? "'" : string.substring(1, string.length - 1)));
|
||||||
|
// startIndex = match.end;
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
match = unescapedString.matchAsPrefix(messageString, startIndex);
|
||||||
|
if (match != null) {
|
||||||
|
tokens.add(Node.string(startIndex, match.group(0)!));
|
||||||
|
startIndex = match.end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match = brace.matchAsPrefix(messageString, startIndex);
|
||||||
|
if (match != null) {
|
||||||
|
tokens.add(Node.brace(startIndex, match.group(0)!));
|
||||||
|
isString = false;
|
||||||
|
startIndex = match.end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Theoretically, we only reach this point because of unmatched single quotes because
|
||||||
|
// 1. If it begins with single quotes, then we match the longest string contained in single quotes.
|
||||||
|
// 2. If it begins with braces, then we match those braces.
|
||||||
|
// 3. Else the first character is neither single quote or brace so it is matched by RegExp "unescapedString"
|
||||||
|
throw L10nParserException(
|
||||||
|
'ICU Lexing Error: Unmatched single quotes.',
|
||||||
|
filename,
|
||||||
|
messageId,
|
||||||
|
messageString,
|
||||||
|
startIndex,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
RegExp matcher;
|
||||||
|
ST? matchedType;
|
||||||
|
|
||||||
|
// Try to match tokens until we succeed
|
||||||
|
for (matchedType in matchers.keys) {
|
||||||
|
matcher = matchers[matchedType]!;
|
||||||
|
match = matcher.matchAsPrefix(messageString, startIndex);
|
||||||
|
if (match != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match == null) {
|
||||||
|
match = brace.matchAsPrefix(messageString, startIndex);
|
||||||
|
if (match != null) {
|
||||||
|
tokens.add(Node.brace(startIndex, match.group(0)!));
|
||||||
|
isString = true;
|
||||||
|
startIndex = match.end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// This should only happen when there are special characters we are unable to match.
|
||||||
|
throw L10nParserException(
|
||||||
|
'ICU Lexing Error: Unexpected character.',
|
||||||
|
filename,
|
||||||
|
messageId,
|
||||||
|
messageString,
|
||||||
|
startIndex
|
||||||
|
);
|
||||||
|
} else if (matchedType == ST.empty) {
|
||||||
|
// Do not add whitespace as a token.
|
||||||
|
startIndex = match.end;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
tokens.add(Node(matchedType!, startIndex, value: match.group(0)));
|
||||||
|
startIndex = match.end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node parseIntoTree() {
|
||||||
|
final List<Node> tokens = lexIntoTokens();
|
||||||
|
final List<ST> parsingStack = <ST>[ST.message];
|
||||||
|
final Node syntaxTree = Node(ST.empty, 0, expectedSymbolCount: 1);
|
||||||
|
final List<Node> treeTraversalStack = <Node>[syntaxTree];
|
||||||
|
|
||||||
|
// Helper function for parsing and constructing tree.
|
||||||
|
void parseAndConstructNode(ST nonterminal, int ruleIndex) {
|
||||||
|
final Node parent = treeTraversalStack.last;
|
||||||
|
final List<ST> grammarRule = grammar[nonterminal]![ruleIndex];
|
||||||
|
|
||||||
|
// When we run out of tokens, just use -1 to represent the last index.
|
||||||
|
final int positionInMessage = tokens.isNotEmpty ? tokens.first.positionInMessage : -1;
|
||||||
|
final Node node = Node(nonterminal, positionInMessage, expectedSymbolCount: grammarRule.length);
|
||||||
|
parsingStack.addAll(grammarRule.reversed);
|
||||||
|
|
||||||
|
// For tree construction, add nodes to the parent until the parent has all
|
||||||
|
// all the children it is expecting.
|
||||||
|
parent.children.add(node);
|
||||||
|
if (parent.isFull) {
|
||||||
|
treeTraversalStack.removeLast();
|
||||||
|
}
|
||||||
|
treeTraversalStack.add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (parsingStack.isNotEmpty) {
|
||||||
|
final ST symbol = parsingStack.removeLast();
|
||||||
|
|
||||||
|
// Figure out which production rule to use.
|
||||||
|
switch(symbol) {
|
||||||
|
case ST.message:
|
||||||
|
if (tokens.isEmpty) {
|
||||||
|
parseAndConstructNode(ST.message, 4);
|
||||||
|
} else if (tokens[0].type == ST.closeBrace) {
|
||||||
|
parseAndConstructNode(ST.message, 4);
|
||||||
|
} else if (tokens[0].type == ST.string) {
|
||||||
|
parseAndConstructNode(ST.message, 0);
|
||||||
|
} else if (tokens[0].type == ST.openBrace) {
|
||||||
|
if (3 < tokens.length && tokens[3].type == ST.plural) {
|
||||||
|
parseAndConstructNode(ST.message, 2);
|
||||||
|
} else if (3 < tokens.length && tokens[3].type == ST.select) {
|
||||||
|
parseAndConstructNode(ST.message, 3);
|
||||||
|
} else {
|
||||||
|
parseAndConstructNode(ST.message, 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Theoretically, we can never get here.
|
||||||
|
throw L10nException('ICU Syntax Error.');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ST.placeholderExpr:
|
||||||
|
parseAndConstructNode(ST.placeholderExpr, 0);
|
||||||
|
break;
|
||||||
|
case ST.pluralExpr:
|
||||||
|
parseAndConstructNode(ST.pluralExpr, 0);
|
||||||
|
break;
|
||||||
|
case ST.pluralParts:
|
||||||
|
if (tokens.isNotEmpty && (
|
||||||
|
tokens[0].type == ST.identifier ||
|
||||||
|
tokens[0].type == ST.other ||
|
||||||
|
tokens[0].type == ST.equalSign
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
parseAndConstructNode(ST.pluralParts, 0);
|
||||||
|
} else {
|
||||||
|
parseAndConstructNode(ST.pluralParts, 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ST.pluralPart:
|
||||||
|
if (tokens.isNotEmpty && tokens[0].type == ST.identifier) {
|
||||||
|
parseAndConstructNode(ST.pluralPart, 0);
|
||||||
|
} else if (tokens.isNotEmpty && tokens[0].type == ST.equalSign) {
|
||||||
|
parseAndConstructNode(ST.pluralPart, 1);
|
||||||
|
} else if (tokens.isNotEmpty && tokens[0].type == ST.other) {
|
||||||
|
parseAndConstructNode(ST.pluralPart, 2);
|
||||||
|
} else {
|
||||||
|
throw L10nParserException(
|
||||||
|
'ICU Syntax Error: Plural parts must be of the form "identifier { message }" or "= number { message }"',
|
||||||
|
filename,
|
||||||
|
messageId,
|
||||||
|
messageString,
|
||||||
|
tokens[0].positionInMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ST.selectExpr:
|
||||||
|
parseAndConstructNode(ST.selectExpr, 0);
|
||||||
|
break;
|
||||||
|
case ST.selectParts:
|
||||||
|
if (tokens.isNotEmpty && (
|
||||||
|
tokens[0].type == ST.identifier ||
|
||||||
|
tokens[0].type == ST.other
|
||||||
|
)) {
|
||||||
|
parseAndConstructNode(ST.selectParts, 0);
|
||||||
|
} else {
|
||||||
|
parseAndConstructNode(ST.selectParts, 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ST.selectPart:
|
||||||
|
if (tokens.isNotEmpty && tokens[0].type == ST.identifier) {
|
||||||
|
parseAndConstructNode(ST.selectPart, 0);
|
||||||
|
} else if (tokens.isNotEmpty && tokens[0].type == ST.other) {
|
||||||
|
parseAndConstructNode(ST.selectPart, 1);
|
||||||
|
} else {
|
||||||
|
throw L10nParserException(
|
||||||
|
'ICU Syntax Error: Select parts must be of the form "identifier { message }"',
|
||||||
|
filename,
|
||||||
|
messageId,
|
||||||
|
messageString,
|
||||||
|
tokens[0].positionInMessage
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// At this point, we are only handling terminal symbols.
|
||||||
|
// ignore: no_default_cases
|
||||||
|
default:
|
||||||
|
final Node parent = treeTraversalStack.last;
|
||||||
|
// If we match a terminal symbol, then remove it from tokens and
|
||||||
|
// add it to the tree.
|
||||||
|
if (symbol == ST.empty) {
|
||||||
|
parent.children.add(Node.empty(-1));
|
||||||
|
} else if (tokens.isEmpty) {
|
||||||
|
throw L10nParserException(
|
||||||
|
'ICU Syntax Error: Expected "${terminalTypeToString[symbol]}" but found no tokens.',
|
||||||
|
filename,
|
||||||
|
messageId,
|
||||||
|
messageString,
|
||||||
|
messageString.length + 1,
|
||||||
|
);
|
||||||
|
} else if (symbol == tokens[0].type) {
|
||||||
|
final Node token = tokens.removeAt(0);
|
||||||
|
parent.children.add(token);
|
||||||
|
} else {
|
||||||
|
throw L10nParserException(
|
||||||
|
'ICU Syntax Error: Expected "${terminalTypeToString[symbol]}" but found "${tokens[0].value}".',
|
||||||
|
filename,
|
||||||
|
messageId,
|
||||||
|
messageString,
|
||||||
|
tokens[0].positionInMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.isFull) {
|
||||||
|
treeTraversalStack.removeLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return syntaxTree.children[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<ST, String> terminalTypeToString = <ST, String>{
|
||||||
|
ST.openBrace: '{',
|
||||||
|
ST.closeBrace: '}',
|
||||||
|
ST.comma: ',',
|
||||||
|
ST.empty: '',
|
||||||
|
ST.identifier: 'identifier',
|
||||||
|
ST.number: 'number',
|
||||||
|
ST.plural: 'plural',
|
||||||
|
ST.select: 'select',
|
||||||
|
ST.equalSign: '=',
|
||||||
|
ST.other: 'other',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compress the syntax tree. Note that after
|
||||||
|
// parse(lex(message)), the individual parts (ST.string, ST.placeholderExpr,
|
||||||
|
// ST.pluralExpr, and ST.selectExpr) are structured as a linked list See diagram
|
||||||
|
// below. This
|
||||||
|
// function compresses these parts into a single children array (and does this
|
||||||
|
// for ST.pluralParts and ST.selectParts as well). Then it checks extra syntax
|
||||||
|
// rules. Essentially, it converts
|
||||||
|
//
|
||||||
|
// Message
|
||||||
|
// / \
|
||||||
|
// PluralExpr Message
|
||||||
|
// / \
|
||||||
|
// String Message
|
||||||
|
// / \
|
||||||
|
// SelectExpr ...
|
||||||
|
//
|
||||||
|
// to
|
||||||
|
//
|
||||||
|
// Message
|
||||||
|
// / | \
|
||||||
|
// PluralExpr String SelectExpr ...
|
||||||
|
//
|
||||||
|
// Keep in mind that this modifies the tree in place and the values of
|
||||||
|
// expectedSymbolCount and isFull is no longer useful after this operation.
|
||||||
|
Node compress(Node syntaxTree) {
|
||||||
|
Node node = syntaxTree;
|
||||||
|
final List<Node> children = <Node>[];
|
||||||
|
switch (syntaxTree.type) {
|
||||||
|
case ST.message:
|
||||||
|
case ST.pluralParts:
|
||||||
|
case ST.selectParts:
|
||||||
|
while (node.children.length == 2) {
|
||||||
|
children.add(node.children[0]);
|
||||||
|
compress(node.children[0]);
|
||||||
|
node = node.children[1];
|
||||||
|
}
|
||||||
|
syntaxTree.children = children;
|
||||||
|
break;
|
||||||
|
// ignore: no_default_cases
|
||||||
|
default:
|
||||||
|
node.children.forEach(compress);
|
||||||
|
}
|
||||||
|
return syntaxTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes in a compressed syntax tree and checks extra rules on
|
||||||
|
// plural parts and select parts.
|
||||||
|
void checkExtraRules(Node syntaxTree) {
|
||||||
|
final List<Node> children = syntaxTree.children;
|
||||||
|
switch(syntaxTree.type) {
|
||||||
|
case ST.pluralParts:
|
||||||
|
// Must have an "other" case.
|
||||||
|
if (children.every((Node node) => node.children[0].type != ST.other)) {
|
||||||
|
throw L10nParserException(
|
||||||
|
'ICU Syntax Error: Plural expressions must have an "other" case.',
|
||||||
|
filename,
|
||||||
|
messageId,
|
||||||
|
messageString,
|
||||||
|
syntaxTree.positionInMessage
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Identifier must be one of "zero", "one", "two", "few", "many".
|
||||||
|
for (final Node node in children) {
|
||||||
|
final Node pluralPartFirstToken = node.children[0];
|
||||||
|
const List<String> validIdentifiers = <String>['zero', 'one', 'two', 'few', 'many'];
|
||||||
|
if (pluralPartFirstToken.type == ST.identifier && !validIdentifiers.contains(pluralPartFirstToken.value)) {
|
||||||
|
throw L10nParserException(
|
||||||
|
'ICU Syntax Error: Plural expressions case must be one of "zero", "one", "two", "few", "many", or "other".',
|
||||||
|
filename,
|
||||||
|
messageId,
|
||||||
|
messageString,
|
||||||
|
node.positionInMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ST.selectParts:
|
||||||
|
if (children.every((Node node) => node.children[0].type != ST.other)) {
|
||||||
|
throw L10nParserException(
|
||||||
|
'ICU Syntax Error: Select expressions must have an "other" case.',
|
||||||
|
filename,
|
||||||
|
messageId,
|
||||||
|
messageString,
|
||||||
|
syntaxTree.positionInMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// ignore: no_default_cases
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
children.forEach(checkExtraRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
Node parse() {
|
||||||
|
final Node syntaxTree = compress(parseIntoTree());
|
||||||
|
checkExtraRules(syntaxTree);
|
||||||
|
return syntaxTree;
|
||||||
|
}
|
||||||
|
}
|
@ -1580,6 +1580,51 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
|
|||||||
expect(localizationsFile, contains('output-localization-file_en.loadLibrary()'));
|
expect(localizationsFile, contains('output-localization-file_en.loadLibrary()'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('placeholder tests', () {
|
||||||
|
testWithoutContext('should throw attempting to generate a select message without placeholders', () {
|
||||||
|
const String selectMessageWithoutPlaceholdersAttribute = '''
|
||||||
|
{
|
||||||
|
"helloWorld": "Hello {name}",
|
||||||
|
"@helloWorld": {
|
||||||
|
"description": "Improperly formatted since it has no placeholder attribute.",
|
||||||
|
"placeholders": {
|
||||||
|
"hello": {},
|
||||||
|
"world": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}''';
|
||||||
|
|
||||||
|
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
|
||||||
|
..createSync(recursive: true);
|
||||||
|
l10nDirectory.childFile(defaultTemplateArbFileName)
|
||||||
|
.writeAsStringSync(selectMessageWithoutPlaceholdersAttribute);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() {
|
||||||
|
LocalizationsGenerator(
|
||||||
|
fileSystem: fs,
|
||||||
|
inputPathString: defaultL10nPathString,
|
||||||
|
outputPathString: defaultL10nPathString,
|
||||||
|
templateArbFileName: defaultTemplateArbFileName,
|
||||||
|
outputFileString: defaultOutputFileString,
|
||||||
|
classNameString: defaultClassNameString,
|
||||||
|
logger: logger,
|
||||||
|
)
|
||||||
|
..loadResources()
|
||||||
|
..writeOutputFiles();
|
||||||
|
},
|
||||||
|
throwsA(isA<L10nException>().having(
|
||||||
|
(L10nException e) => e.message,
|
||||||
|
'message',
|
||||||
|
contains('''
|
||||||
|
Make sure that the specified placeholder is defined in your arb file.
|
||||||
|
Hello {name}
|
||||||
|
^'''),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('DateTime tests', () {
|
group('DateTime tests', () {
|
||||||
testWithoutContext('imports package:intl', () {
|
testWithoutContext('imports package:intl', () {
|
||||||
const String singleDateMessageArbFileString = '''
|
const String singleDateMessageArbFileString = '''
|
||||||
@ -1895,7 +1940,13 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
|
|||||||
throwsA(isA<L10nException>().having(
|
throwsA(isA<L10nException>().having(
|
||||||
(L10nException e) => e.message,
|
(L10nException e) => e.message,
|
||||||
'message',
|
'message',
|
||||||
contains('Check to see if the plural message is in the proper ICU syntax format'),
|
// TODO(thkim1011): Uncomment after work on refactoring the Message class.
|
||||||
|
// See https://github.com/flutter/flutter/issues/112709.
|
||||||
|
// contains('''
|
||||||
|
// Make sure that the specified plural placeholder is defined in your arb file.
|
||||||
|
// {count,plural, =0{Hello}=1{Hello World}=2{Hello two worlds}few{Hello {count} worlds}many{Hello all {count} worlds}other{Hello other {count} worlds}}
|
||||||
|
// ^'''),
|
||||||
|
contains('Cannot find the count placeholder in plural message "helloWorlds".'),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -1932,7 +1983,13 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
|
|||||||
throwsA(isA<L10nException>().having(
|
throwsA(isA<L10nException>().having(
|
||||||
(L10nException e) => e.message,
|
(L10nException e) => e.message,
|
||||||
'message',
|
'message',
|
||||||
contains('Check to see if the plural message is in the proper ICU syntax format'),
|
// TODO(thkim1011): Uncomment after work on refactoring the Message class.
|
||||||
|
// See https://github.com/flutter/flutter/issues/112709.
|
||||||
|
// contains('''
|
||||||
|
// Make sure that the specified plural placeholder is defined in your arb file.
|
||||||
|
// {count,plural, =0{Hello}=1{Hello World}=2{Hello two worlds}few{Hello {count} worlds}many{Hello all {count} worlds}other{Hello other {count} worlds}}
|
||||||
|
// ^'''),
|
||||||
|
contains('Cannot find the count placeholder in plural message "helloWorlds".'),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -1965,7 +2022,13 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
|
|||||||
throwsA(isA<L10nException>().having(
|
throwsA(isA<L10nException>().having(
|
||||||
(L10nException e) => e.message,
|
(L10nException e) => e.message,
|
||||||
'message',
|
'message',
|
||||||
contains('Resource attribute "@helloWorlds" was not found'),
|
// TODO(thkim1011): Uncomment after work on refactoring the Message class.
|
||||||
|
// See https://github.com/flutter/flutter/issues/112709.
|
||||||
|
// contains('''
|
||||||
|
// Make sure that the specified plural placeholder is defined in your arb file.
|
||||||
|
// {count,plural, =0{Hello}=1{Hello World}=2{Hello two worlds}few{Hello {count} worlds}many{Hello all {count} worlds}other{Hello other {count} worlds}}
|
||||||
|
// ^'''),
|
||||||
|
contains('Resource attribute "@helloWorlds" was not found. Please ensure that plural resources have a corresponding @resource.'),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -2008,36 +2071,6 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
|
|||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWithoutContext('should warn attempting to generate a plural message whose placeholder is not num or null', () {
|
|
||||||
const String pluralMessageWithIncorrectPlaceholderType = '''
|
|
||||||
{
|
|
||||||
"helloWorlds": "{count,plural, =0{Hello}=1{Hello World}=2{Hello two worlds}few{Hello {count} worlds}many{Hello all {count} worlds}other{Hello other {count} worlds}}",
|
|
||||||
"@helloWorlds": {
|
|
||||||
"placeholders": {
|
|
||||||
"count": {
|
|
||||||
"type": "int"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}''';
|
|
||||||
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
|
|
||||||
..createSync(recursive: true);
|
|
||||||
l10nDirectory.childFile(defaultTemplateArbFileName)
|
|
||||||
.writeAsStringSync(pluralMessageWithIncorrectPlaceholderType);
|
|
||||||
LocalizationsGenerator(
|
|
||||||
fileSystem: fs,
|
|
||||||
inputPathString: defaultL10nPathString,
|
|
||||||
outputPathString: defaultL10nPathString,
|
|
||||||
templateArbFileName: defaultTemplateArbFileName,
|
|
||||||
outputFileString: defaultOutputFileString,
|
|
||||||
classNameString: defaultClassNameString,
|
|
||||||
logger: logger,
|
|
||||||
)
|
|
||||||
..loadResources()
|
|
||||||
..writeOutputFiles();
|
|
||||||
expect(logger.warningText, contains("Placeholders for plurals are automatically converted to type 'num'"));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group('select messages', () {
|
group('select messages', () {
|
||||||
@ -2072,7 +2105,10 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
|
|||||||
throwsA(isA<L10nException>().having(
|
throwsA(isA<L10nException>().having(
|
||||||
(L10nException e) => e.message,
|
(L10nException e) => e.message,
|
||||||
'message',
|
'message',
|
||||||
contains('Check to see if the select message is in the proper ICU syntax format'),
|
contains('''
|
||||||
|
Make sure that the specified select placeholder is defined in your arb file.
|
||||||
|
{gender, select, female {She} male {He} other {they} }
|
||||||
|
^'''),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -2109,7 +2145,10 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
|
|||||||
throwsA(isA<L10nException>().having(
|
throwsA(isA<L10nException>().having(
|
||||||
(L10nException e) => e.message,
|
(L10nException e) => e.message,
|
||||||
'message',
|
'message',
|
||||||
contains('Check to see if the select message is in the proper ICU syntax format'),
|
contains('''
|
||||||
|
Make sure that the specified select placeholder is defined in your arb file.
|
||||||
|
{gender, select, female {She} male {He} other {they} }
|
||||||
|
^'''),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -2142,7 +2181,13 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
|
|||||||
throwsA(isA<L10nException>().having(
|
throwsA(isA<L10nException>().having(
|
||||||
(L10nException e) => e.message,
|
(L10nException e) => e.message,
|
||||||
'message',
|
'message',
|
||||||
contains('Resource attribute "@genderSelect" was not found'),
|
// TODO(thkim1011): Uncomment after work on refactoring the Message class.
|
||||||
|
// See https://github.com/flutter/flutter/issues/112709.
|
||||||
|
// contains('''
|
||||||
|
// Make sure that the specified select placeholder is defined in your arb file.
|
||||||
|
// {gender, select, female {She} male {He} other {they} }
|
||||||
|
// ^'''),
|
||||||
|
contains('Resource attribute "@genderSelect" was not found. Please ensure that select resources have a corresponding @resource.'),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -2219,10 +2264,10 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
|
|||||||
throwsA(isA<L10nException>().having(
|
throwsA(isA<L10nException>().having(
|
||||||
(L10nException e) => e.message,
|
(L10nException e) => e.message,
|
||||||
'message',
|
'message',
|
||||||
allOf(
|
contains('''
|
||||||
contains('Incorrect select message format for'),
|
Select expressions must have an "other" case.
|
||||||
contains('Check to see if the select message is in the proper ICU syntax format.'),
|
[app_en.arb:genderSelect] {gender, select,}
|
||||||
),
|
^'''),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -2543,27 +2588,27 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
|
|||||||
expect(localizationsFile, contains(r'${six}m'));
|
expect(localizationsFile, contains(r'${six}m'));
|
||||||
expect(localizationsFile, contains(r'$seven'));
|
expect(localizationsFile, contains(r'$seven'));
|
||||||
expect(localizationsFile, contains(r'$eight'));
|
expect(localizationsFile, contains(r'$eight'));
|
||||||
expect(localizationsFile, contains(r'${nine}'));
|
expect(localizationsFile, contains(r'$nine'));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWithoutContext('check for string interpolation rules - plurals', () {
|
testWithoutContext('check for string interpolation rules - plurals', () {
|
||||||
const String enArbCheckList = '''
|
const String enArbCheckList = '''
|
||||||
{
|
{
|
||||||
"first": "{count,plural, =0{test {count} test} =1{哈{count}哈} =2{m{count}m} few{_{count}_} many{{count} test} other{{count}m}",
|
"first": "{count,plural, =0{test {count} test} =1{哈{count}哈} =2{m{count}m} few{_{count}_} many{{count} test} other{{count}m}}",
|
||||||
"@first": {
|
"@first": {
|
||||||
"description": "First set of plural messages to test.",
|
"description": "First set of plural messages to test.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {}
|
"count": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"second": "{count,plural, =0{test {count}} other{ {count}}",
|
"second": "{count,plural, =0{test {count}} other{ {count}}}",
|
||||||
"@second": {
|
"@second": {
|
||||||
"description": "Second set of plural messages to test.",
|
"description": "Second set of plural messages to test.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {}
|
"count": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"third": "{total,plural, =0{test {total}} other{ {total}}",
|
"third": "{total,plural, =0{test {total}} other{ {total}}}",
|
||||||
"@third": {
|
"@third": {
|
||||||
"description": "Third set of plural messages to test, for number.",
|
"description": "Third set of plural messages to test, for number.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@ -2580,8 +2625,8 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
|
|||||||
// generated code for use of '${variable}' vs '$variable'
|
// generated code for use of '${variable}' vs '$variable'
|
||||||
const String esArbCheckList = '''
|
const String esArbCheckList = '''
|
||||||
{
|
{
|
||||||
"first": "{count,plural, =0{test {count} test} =1{哈{count}哈} =2{m{count}m} few{_{count}_} many{{count} test} other{{count}m}",
|
"first": "{count,plural, =0{test {count} test} =1{哈{count}哈} =2{m{count}m} few{_{count}_} many{{count} test} other{{count}m}}",
|
||||||
"second": "{count,plural, =0{test {count}} other{ {count}}"
|
"second": "{count,plural, =0{test {count}} other{ {count}}}"
|
||||||
}
|
}
|
||||||
''';
|
''';
|
||||||
|
|
||||||
@ -2614,8 +2659,8 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
|
|||||||
expect(localizationsFile, contains(r'test $count'));
|
expect(localizationsFile, contains(r'test $count'));
|
||||||
expect(localizationsFile, contains(r' $count'));
|
expect(localizationsFile, contains(r' $count'));
|
||||||
expect(localizationsFile, contains(r'String totalString = totalNumberFormat'));
|
expect(localizationsFile, contains(r'String totalString = totalNumberFormat'));
|
||||||
expect(localizationsFile, contains(r'test $totalString'));
|
expect(localizationsFile, contains(r'totalString'));
|
||||||
expect(localizationsFile, contains(r' $totalString'));
|
expect(localizationsFile, contains(r'totalString'));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWithoutContext(
|
testWithoutContext(
|
||||||
@ -2994,4 +3039,38 @@ AppLocalizations lookupAppLocalizations(Locale locale) {
|
|||||||
expect(localizationsFile, containsIgnoringWhitespace(r'String tryToPollute(num count) {'));
|
expect(localizationsFile, containsIgnoringWhitespace(r'String tryToPollute(num count) {'));
|
||||||
expect(localizationsFile, containsIgnoringWhitespace(r'String withoutType(num count) {'));
|
expect(localizationsFile, containsIgnoringWhitespace(r'String withoutType(num count) {'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO(thkim1011): Uncomment when implementing escaping.
|
||||||
|
// See https://github.com/flutter/flutter/issues/113455.
|
||||||
|
// testWithoutContext('escaping with single quotes', () {
|
||||||
|
// const String arbFile = '''
|
||||||
|
// {
|
||||||
|
// "singleQuote": "Flutter''s amazing!",
|
||||||
|
// "@singleQuote": {
|
||||||
|
// "description": "A message with a single quote."
|
||||||
|
// }
|
||||||
|
// }''';
|
||||||
|
|
||||||
|
// final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
|
||||||
|
// ..createSync(recursive: true);
|
||||||
|
// l10nDirectory.childFile(defaultTemplateArbFileName)
|
||||||
|
// .writeAsStringSync(arbFile);
|
||||||
|
|
||||||
|
// LocalizationsGenerator(
|
||||||
|
// fileSystem: fs,
|
||||||
|
// inputPathString: defaultL10nPathString,
|
||||||
|
// outputPathString: defaultL10nPathString,
|
||||||
|
// templateArbFileName: defaultTemplateArbFileName,
|
||||||
|
// outputFileString: defaultOutputFileString,
|
||||||
|
// classNameString: defaultClassNameString,
|
||||||
|
// logger: logger,
|
||||||
|
// )
|
||||||
|
// ..loadResources()
|
||||||
|
// ..writeOutputFiles();
|
||||||
|
|
||||||
|
// final String localizationsFile = fs.file(
|
||||||
|
// fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.dart'),
|
||||||
|
// ).readAsStringSync();
|
||||||
|
// expect(localizationsFile, contains(r"Flutter\'s amazing"));
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,491 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter_tools/src/localizations/gen_l10n_types.dart';
|
||||||
|
import 'package:flutter_tools/src/localizations/message_parser.dart';
|
||||||
|
import '../src/common.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Going to test that operator== is overloaded properly since the rest
|
||||||
|
// of the test depends on it.
|
||||||
|
testWithoutContext('node equality', () {
|
||||||
|
final Node actual = Node(
|
||||||
|
ST.placeholderExpr,
|
||||||
|
0,
|
||||||
|
expectedSymbolCount: 3,
|
||||||
|
children: <Node>[
|
||||||
|
Node.openBrace(0),
|
||||||
|
Node.string(1, 'var'),
|
||||||
|
Node.closeBrace(4),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final Node expected = Node(
|
||||||
|
ST.placeholderExpr,
|
||||||
|
0,
|
||||||
|
expectedSymbolCount: 3,
|
||||||
|
children: <Node>[
|
||||||
|
Node.openBrace(0),
|
||||||
|
Node.string(1, 'var'),
|
||||||
|
Node.closeBrace(4),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(actual, equals(expected));
|
||||||
|
|
||||||
|
final Node wrongType = Node(
|
||||||
|
ST.pluralExpr,
|
||||||
|
0,
|
||||||
|
expectedSymbolCount: 3,
|
||||||
|
children: <Node>[
|
||||||
|
Node.openBrace(0),
|
||||||
|
Node.string(1, 'var'),
|
||||||
|
Node.closeBrace(4),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(actual, isNot(equals(wrongType)));
|
||||||
|
|
||||||
|
final Node wrongPosition = Node(
|
||||||
|
ST.placeholderExpr,
|
||||||
|
1,
|
||||||
|
expectedSymbolCount: 3,
|
||||||
|
children: <Node>[
|
||||||
|
Node.openBrace(0),
|
||||||
|
Node.string(1, 'var'),
|
||||||
|
Node.closeBrace(4),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(actual, isNot(equals(wrongPosition)));
|
||||||
|
|
||||||
|
final Node wrongChildrenCount = Node(
|
||||||
|
ST.placeholderExpr,
|
||||||
|
0,
|
||||||
|
expectedSymbolCount: 3,
|
||||||
|
children: <Node>[
|
||||||
|
Node.string(1, 'var'),
|
||||||
|
Node.closeBrace(4),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(actual, isNot(equals(wrongChildrenCount)));
|
||||||
|
|
||||||
|
final Node wrongChild = Node(
|
||||||
|
ST.placeholderExpr,
|
||||||
|
0,
|
||||||
|
expectedSymbolCount: 3,
|
||||||
|
children: <Node>[
|
||||||
|
Node.closeBrace(0),
|
||||||
|
Node.string(1, 'var'),
|
||||||
|
Node.closeBrace(4),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(actual, isNot(equals(wrongChild)));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('lexer basic', () {
|
||||||
|
final List<Node> tokens1 = Parser(
|
||||||
|
'helloWorld',
|
||||||
|
'app_en.arb',
|
||||||
|
'Hello {name}'
|
||||||
|
).lexIntoTokens();
|
||||||
|
expect(tokens1, equals(<Node>[
|
||||||
|
Node.string(0, 'Hello '),
|
||||||
|
Node.openBrace(6),
|
||||||
|
Node.identifier(7, 'name'),
|
||||||
|
Node.closeBrace(11),
|
||||||
|
]));
|
||||||
|
|
||||||
|
final List<Node> tokens2 = Parser(
|
||||||
|
'plural',
|
||||||
|
'app_en.arb',
|
||||||
|
'There are {count} {count, plural, =1{cat} other{cats}}'
|
||||||
|
).lexIntoTokens();
|
||||||
|
expect(tokens2, equals(<Node>[
|
||||||
|
Node.string(0, 'There are '),
|
||||||
|
Node.openBrace(10),
|
||||||
|
Node.identifier(11, 'count'),
|
||||||
|
Node.closeBrace(16),
|
||||||
|
Node.string(17, ' '),
|
||||||
|
Node.openBrace(18),
|
||||||
|
Node.identifier(19, 'count'),
|
||||||
|
Node.comma(24),
|
||||||
|
Node.pluralKeyword(26),
|
||||||
|
Node.comma(32),
|
||||||
|
Node.equalSign(34),
|
||||||
|
Node.number(35, '1'),
|
||||||
|
Node.openBrace(36),
|
||||||
|
Node.string(37, 'cat'),
|
||||||
|
Node.closeBrace(40),
|
||||||
|
Node.otherKeyword(42),
|
||||||
|
Node.openBrace(47),
|
||||||
|
Node.string(48, 'cats'),
|
||||||
|
Node.closeBrace(52),
|
||||||
|
Node.closeBrace(53),
|
||||||
|
]));
|
||||||
|
|
||||||
|
final List<Node> tokens3 = Parser(
|
||||||
|
'gender',
|
||||||
|
'app_en.arb',
|
||||||
|
'{gender, select, male{he} female{she} other{they}}'
|
||||||
|
).lexIntoTokens();
|
||||||
|
expect(tokens3, equals(<Node>[
|
||||||
|
Node.openBrace(0),
|
||||||
|
Node.identifier(1, 'gender'),
|
||||||
|
Node.comma(7),
|
||||||
|
Node.selectKeyword(9),
|
||||||
|
Node.comma(15),
|
||||||
|
Node.identifier(17, 'male'),
|
||||||
|
Node.openBrace(21),
|
||||||
|
Node.string(22, 'he'),
|
||||||
|
Node.closeBrace(24),
|
||||||
|
Node.identifier(26, 'female'),
|
||||||
|
Node.openBrace(32),
|
||||||
|
Node.string(33, 'she'),
|
||||||
|
Node.closeBrace(36),
|
||||||
|
Node.otherKeyword(38),
|
||||||
|
Node.openBrace(43),
|
||||||
|
Node.string(44, 'they'),
|
||||||
|
Node.closeBrace(48),
|
||||||
|
Node.closeBrace(49),
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('lexer recursive', () {
|
||||||
|
final List<Node> tokens = Parser(
|
||||||
|
'plural',
|
||||||
|
'app_en.arb',
|
||||||
|
'{count, plural, =1{{gender, select, male{he} female{she}}} other{they}}'
|
||||||
|
).lexIntoTokens();
|
||||||
|
expect(tokens, equals(<Node>[
|
||||||
|
Node.openBrace(0),
|
||||||
|
Node.identifier(1, 'count'),
|
||||||
|
Node.comma(6),
|
||||||
|
Node.pluralKeyword(8),
|
||||||
|
Node.comma(14),
|
||||||
|
Node.equalSign(16),
|
||||||
|
Node.number(17, '1'),
|
||||||
|
Node.openBrace(18),
|
||||||
|
Node.openBrace(19),
|
||||||
|
Node.identifier(20, 'gender'),
|
||||||
|
Node.comma(26),
|
||||||
|
Node.selectKeyword(28),
|
||||||
|
Node.comma(34),
|
||||||
|
Node.identifier(36, 'male'),
|
||||||
|
Node.openBrace(40),
|
||||||
|
Node.string(41, 'he'),
|
||||||
|
Node.closeBrace(43),
|
||||||
|
Node.identifier(45, 'female'),
|
||||||
|
Node.openBrace(51),
|
||||||
|
Node.string(52, 'she'),
|
||||||
|
Node.closeBrace(55),
|
||||||
|
Node.closeBrace(56),
|
||||||
|
Node.closeBrace(57),
|
||||||
|
Node.otherKeyword(59),
|
||||||
|
Node.openBrace(64),
|
||||||
|
Node.string(65, 'they'),
|
||||||
|
Node.closeBrace(69),
|
||||||
|
Node.closeBrace(70),
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(thkim1011): Uncomment when implementing escaping.
|
||||||
|
// See https://github.com/flutter/flutter/issues/113455.
|
||||||
|
// testWithoutContext('lexer escaping', () {
|
||||||
|
// final List<Node> tokens1 = Parser("''").lexIntoTokens();
|
||||||
|
// expect(tokens1, equals(<Node>[Node.string(0, "'")]));
|
||||||
|
|
||||||
|
// final List<Node> tokens2 = Parser("'hello world { name }'").lexIntoTokens();
|
||||||
|
// expect(tokens2, equals(<Node>[Node.string(0, 'hello world { name }')]));
|
||||||
|
|
||||||
|
// final List<Node> tokens3 = Parser("'{ escaped string }' { not escaped }").lexIntoTokens();
|
||||||
|
// expect(tokens3, equals(<Node>[
|
||||||
|
// Node.string(0, '{ escaped string }'),
|
||||||
|
// Node.string(20, ' '),
|
||||||
|
// Node.openBrace(21),
|
||||||
|
// Node.identifier(23, 'not'),
|
||||||
|
// Node.identifier(27, 'escaped'),
|
||||||
|
// Node.closeBrace(35),
|
||||||
|
// ]));
|
||||||
|
|
||||||
|
// final List<Node> tokens4 = Parser("Flutter''s amazing!").lexIntoTokens();
|
||||||
|
// expect(tokens4, equals(<Node>[
|
||||||
|
// Node.string(0, 'Flutter'),
|
||||||
|
// Node.string(7, "'"),
|
||||||
|
// Node.string(9, 's amazing!'),
|
||||||
|
// ]));
|
||||||
|
// });
|
||||||
|
|
||||||
|
testWithoutContext('lexer: lexically correct but syntactically incorrect', () {
|
||||||
|
final List<Node> tokens = Parser(
|
||||||
|
'syntax',
|
||||||
|
'app_en.arb',
|
||||||
|
'string { identifier { string { identifier } } }'
|
||||||
|
).lexIntoTokens();
|
||||||
|
expect(tokens, equals(<Node>[
|
||||||
|
Node.string(0, 'string '),
|
||||||
|
Node.openBrace(7),
|
||||||
|
Node.identifier(9, 'identifier'),
|
||||||
|
Node.openBrace(20),
|
||||||
|
Node.string(21, ' string '),
|
||||||
|
Node.openBrace(29),
|
||||||
|
Node.identifier(31, 'identifier'),
|
||||||
|
Node.closeBrace(42),
|
||||||
|
Node.string(43, ' '),
|
||||||
|
Node.closeBrace(44),
|
||||||
|
Node.closeBrace(46),
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(thkim1011): Uncomment when implementing escaping.
|
||||||
|
// See https://github.com/flutter/flutter/issues/113455.
|
||||||
|
// testWithoutContext('lexer unmatched single quote', () {
|
||||||
|
// const String message = "here''s an unmatched single quote: '";
|
||||||
|
// const String expectedError = '''
|
||||||
|
// ICU Lexing Error: Unmatched single quotes.
|
||||||
|
// here''s an unmatched single quote: '
|
||||||
|
// ^''';
|
||||||
|
// expect(
|
||||||
|
// () => Parser(message).lexIntoTokens(),
|
||||||
|
// throwsA(isA<L10nException>().having(
|
||||||
|
// (L10nException e) => e.message,
|
||||||
|
// 'message',
|
||||||
|
// contains(expectedError),
|
||||||
|
// )));
|
||||||
|
// });
|
||||||
|
|
||||||
|
testWithoutContext('lexer unexpected character', () {
|
||||||
|
const String message = '{ * }';
|
||||||
|
const String expectedError = '''
|
||||||
|
ICU Lexing Error: Unexpected character.
|
||||||
|
[app_en.arb:lex] { * }
|
||||||
|
^''';
|
||||||
|
expect(
|
||||||
|
() => Parser('lex', 'app_en.arb', message).lexIntoTokens(),
|
||||||
|
throwsA(isA<L10nException>().having(
|
||||||
|
(L10nException e) => e.message,
|
||||||
|
'message',
|
||||||
|
contains(expectedError),
|
||||||
|
)));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
testWithoutContext('parser basic', () {
|
||||||
|
expect(Parser('helloWorld', 'app_en.arb', 'Hello {name}').parse(), equals(
|
||||||
|
Node(ST.message, 0, children: <Node>[
|
||||||
|
Node(ST.string, 0, value: 'Hello '),
|
||||||
|
Node(ST.placeholderExpr, 6, children: <Node>[
|
||||||
|
Node(ST.openBrace, 6, value: '{'),
|
||||||
|
Node(ST.identifier, 7, value: 'name'),
|
||||||
|
Node(ST.closeBrace, 11, value: '}')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(Parser(
|
||||||
|
'plural',
|
||||||
|
'app_en.arb',
|
||||||
|
'There are {count} {count, plural, =1{cat} other{cats}}'
|
||||||
|
).parse(), equals(
|
||||||
|
Node(ST.message, 0, children: <Node>[
|
||||||
|
Node(ST.string, 0, value: 'There are '),
|
||||||
|
Node(ST.placeholderExpr, 10, children: <Node>[
|
||||||
|
Node(ST.openBrace, 10, value: '{'),
|
||||||
|
Node(ST.identifier, 11, value: 'count'),
|
||||||
|
Node(ST.closeBrace, 16, value: '}')
|
||||||
|
]),
|
||||||
|
Node(ST.string, 17, value: ' '),
|
||||||
|
Node(ST.pluralExpr, 18, children: <Node>[
|
||||||
|
Node(ST.openBrace, 18, value: '{'),
|
||||||
|
Node(ST.identifier, 19, value: 'count'),
|
||||||
|
Node(ST.comma, 24, value: ','),
|
||||||
|
Node(ST.plural, 26, value: 'plural'),
|
||||||
|
Node(ST.comma, 32, value: ','),
|
||||||
|
Node(ST.pluralParts, 34, children: <Node>[
|
||||||
|
Node(ST.pluralPart, 34, children: <Node>[
|
||||||
|
Node(ST.equalSign, 34, value: '='),
|
||||||
|
Node(ST.number, 35, value: '1'),
|
||||||
|
Node(ST.openBrace, 36, value: '{'),
|
||||||
|
Node(ST.message, 37, children: <Node>[
|
||||||
|
Node(ST.string, 37, value: 'cat')
|
||||||
|
]),
|
||||||
|
Node(ST.closeBrace, 40, value: '}')
|
||||||
|
]),
|
||||||
|
Node(ST.pluralPart, 42, children: <Node>[
|
||||||
|
Node(ST.other, 42, value: 'other'),
|
||||||
|
Node(ST.openBrace, 47, value: '{'),
|
||||||
|
Node(ST.message, 48, children: <Node>[
|
||||||
|
Node(ST.string, 48, value: 'cats')
|
||||||
|
]),
|
||||||
|
Node(ST.closeBrace, 52, value: '}')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
Node(ST.closeBrace, 53, value: '}')
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(Parser(
|
||||||
|
'gender',
|
||||||
|
'app_en.arb',
|
||||||
|
'{gender, select, male{he} female{she} other{they}}'
|
||||||
|
).parse(), equals(
|
||||||
|
Node(ST.message, 0, children: <Node>[
|
||||||
|
Node(ST.selectExpr, 0, children: <Node>[
|
||||||
|
Node(ST.openBrace, 0, value: '{'),
|
||||||
|
Node(ST.identifier, 1, value: 'gender'),
|
||||||
|
Node(ST.comma, 7, value: ','),
|
||||||
|
Node(ST.select, 9, value: 'select'),
|
||||||
|
Node(ST.comma, 15, value: ','),
|
||||||
|
Node(ST.selectParts, 17, children: <Node>[
|
||||||
|
Node(ST.selectPart, 17, children: <Node>[
|
||||||
|
Node(ST.identifier, 17, value: 'male'),
|
||||||
|
Node(ST.openBrace, 21, value: '{'),
|
||||||
|
Node(ST.message, 22, children: <Node>[
|
||||||
|
Node(ST.string, 22, value: 'he'),
|
||||||
|
]),
|
||||||
|
Node(ST.closeBrace, 24, value: '}'),
|
||||||
|
]),
|
||||||
|
Node(ST.selectPart, 26, children: <Node>[
|
||||||
|
Node(ST.identifier, 26, value: 'female'),
|
||||||
|
Node(ST.openBrace, 32, value: '{'),
|
||||||
|
Node(ST.message, 33, children: <Node>[
|
||||||
|
Node(ST.string, 33, value: 'she'),
|
||||||
|
]),
|
||||||
|
Node(ST.closeBrace, 36, value: '}'),
|
||||||
|
]),
|
||||||
|
Node(ST.selectPart, 38, children: <Node>[
|
||||||
|
Node(ST.other, 38, value: 'other'),
|
||||||
|
Node(ST.openBrace, 43, value: '{'),
|
||||||
|
Node(ST.message, 44, children: <Node>[
|
||||||
|
Node(ST.string, 44, value: 'they'),
|
||||||
|
]),
|
||||||
|
Node(ST.closeBrace, 48, value: '}'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
Node(ST.closeBrace, 49, value: '}'),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(thkim1011): Uncomment when implementing escaping.
|
||||||
|
// See https://github.com/flutter/flutter/issues/113455.
|
||||||
|
// testWithoutContext('parser basic 2', () {
|
||||||
|
// expect(Parser("Flutter''s amazing!").parse(), equals(
|
||||||
|
// Node(ST.message, 0, children: <Node>[
|
||||||
|
// Node(ST.string, 0, value: 'Flutter'),
|
||||||
|
// Node(ST.string, 7, value: "'"),
|
||||||
|
// Node(ST.string, 9, value: 's amazing!'),
|
||||||
|
// ])
|
||||||
|
// ));
|
||||||
|
// });
|
||||||
|
|
||||||
|
testWithoutContext('parser recursive', () {
|
||||||
|
expect(Parser(
|
||||||
|
'pluralGender',
|
||||||
|
'app_en.arb',
|
||||||
|
'{count, plural, =1{{gender, select, male{he} female{she} other{they}}} other{they}}'
|
||||||
|
).parse(), equals(
|
||||||
|
Node(ST.message, 0, children: <Node>[
|
||||||
|
Node(ST.pluralExpr, 0, children: <Node>[
|
||||||
|
Node(ST.openBrace, 0, value: '{'),
|
||||||
|
Node(ST.identifier, 1, value: 'count'),
|
||||||
|
Node(ST.comma, 6, value: ','),
|
||||||
|
Node(ST.plural, 8, value: 'plural'),
|
||||||
|
Node(ST.comma, 14, value: ','),
|
||||||
|
Node(ST.pluralParts, 16, children: <Node>[
|
||||||
|
Node(ST.pluralPart, 16, children: <Node>[
|
||||||
|
Node(ST.equalSign, 16, value: '='),
|
||||||
|
Node(ST.number, 17, value: '1'),
|
||||||
|
Node(ST.openBrace, 18, value: '{'),
|
||||||
|
Node(ST.message, 19, children: <Node>[
|
||||||
|
Node(ST.selectExpr, 19, children: <Node>[
|
||||||
|
Node(ST.openBrace, 19, value: '{'),
|
||||||
|
Node(ST.identifier, 20, value: 'gender'),
|
||||||
|
Node(ST.comma, 26, value: ','),
|
||||||
|
Node(ST.select, 28, value: 'select'),
|
||||||
|
Node(ST.comma, 34, value: ','),
|
||||||
|
Node(ST.selectParts, 36, children: <Node>[
|
||||||
|
Node(ST.selectPart, 36, children: <Node>[
|
||||||
|
Node(ST.identifier, 36, value: 'male'),
|
||||||
|
Node(ST.openBrace, 40, value: '{'),
|
||||||
|
Node(ST.message, 41, children: <Node>[
|
||||||
|
Node(ST.string, 41, value: 'he'),
|
||||||
|
]),
|
||||||
|
Node(ST.closeBrace, 43, value: '}'),
|
||||||
|
]),
|
||||||
|
Node(ST.selectPart, 45, children: <Node>[
|
||||||
|
Node(ST.identifier, 45, value: 'female'),
|
||||||
|
Node(ST.openBrace, 51, value: '{'),
|
||||||
|
Node(ST.message, 52, children: <Node>[
|
||||||
|
Node(ST.string, 52, value: 'she'),
|
||||||
|
]),
|
||||||
|
Node(ST.closeBrace, 55, value: '}'),
|
||||||
|
]),
|
||||||
|
Node(ST.selectPart, 57, children: <Node>[
|
||||||
|
Node(ST.other, 57, value: 'other'),
|
||||||
|
Node(ST.openBrace, 62, value: '{'),
|
||||||
|
Node(ST.message, 63, children: <Node>[
|
||||||
|
Node(ST.string, 63, value: 'they'),
|
||||||
|
]),
|
||||||
|
Node(ST.closeBrace, 67, value: '}'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
Node(ST.closeBrace, 68, value: '}'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
Node(ST.closeBrace, 69, value: '}'),
|
||||||
|
]),
|
||||||
|
Node(ST.pluralPart, 71, children: <Node>[
|
||||||
|
Node(ST.other, 71, value: 'other'),
|
||||||
|
Node(ST.openBrace, 76, value: '{'),
|
||||||
|
Node(ST.message, 77, children: <Node>[
|
||||||
|
Node(ST.string, 77, value: 'they'),
|
||||||
|
]),
|
||||||
|
Node(ST.closeBrace, 81, value: '}'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
Node(ST.closeBrace, 82, value: '}'),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('parser unexpected token', () {
|
||||||
|
// unexpected token
|
||||||
|
const String expectedError1 = '''
|
||||||
|
ICU Syntax Error: Expected "}" but found "=".
|
||||||
|
[app_en.arb:unexpectedToken] { placeholder =
|
||||||
|
^''';
|
||||||
|
expect(
|
||||||
|
() => Parser('unexpectedToken', 'app_en.arb', '{ placeholder =').parse(),
|
||||||
|
throwsA(isA<L10nException>().having(
|
||||||
|
(L10nException e) => e.message,
|
||||||
|
'message',
|
||||||
|
contains(expectedError1),
|
||||||
|
)));
|
||||||
|
|
||||||
|
const String expectedError2 = '''
|
||||||
|
ICU Syntax Error: Expected "number" but found "}".
|
||||||
|
[app_en.arb:unexpectedToken] { count, plural, = }
|
||||||
|
^''';
|
||||||
|
expect(
|
||||||
|
() => Parser('unexpectedToken', 'app_en.arb', '{ count, plural, = }').parse(),
|
||||||
|
throwsA(isA<L10nException>().having(
|
||||||
|
(L10nException e) => e.message,
|
||||||
|
'message',
|
||||||
|
contains(expectedError2),
|
||||||
|
)));
|
||||||
|
|
||||||
|
const String expectedError3 = '''
|
||||||
|
ICU Syntax Error: Expected "identifier" but found ",".
|
||||||
|
[app_en.arb:unexpectedToken] { , plural , = }
|
||||||
|
^''';
|
||||||
|
expect(
|
||||||
|
() => Parser('unexpectedToken', 'app_en.arb', '{ , plural , = }').parse(),
|
||||||
|
throwsA(isA<L10nException>().having(
|
||||||
|
(L10nException e) => e.message,
|
||||||
|
'message',
|
||||||
|
contains(expectedError3),
|
||||||
|
)));
|
||||||
|
});
|
||||||
|
}
|
@ -123,45 +123,48 @@ void main() {
|
|||||||
'#l10n 70 (Indeed, they like Flutter!)\n'
|
'#l10n 70 (Indeed, they like Flutter!)\n'
|
||||||
'#l10n 71 (Indeed, he likes ice cream!)\n'
|
'#l10n 71 (Indeed, he likes ice cream!)\n'
|
||||||
'#l10n 72 (Indeed, she likes chocolate!)\n'
|
'#l10n 72 (Indeed, she likes chocolate!)\n'
|
||||||
'#l10n 73 (--- es ---)\n'
|
'#l10n 73 (he)\n'
|
||||||
'#l10n 74 (ES - Hello world)\n'
|
'#l10n 74 (they)\n'
|
||||||
'#l10n 75 (ES - Hello _NEWLINE_ World)\n'
|
'#l10n 75 (she)\n'
|
||||||
'#l10n 76 (ES - Hola \$ Mundo)\n'
|
'#l10n 76 (--- es ---)\n'
|
||||||
'#l10n 77 (ES - Hello Mundo)\n'
|
'#l10n 77 (ES - Hello world)\n'
|
||||||
'#l10n 78 (ES - Hola Mundo)\n'
|
'#l10n 78 (ES - Hello _NEWLINE_ World)\n'
|
||||||
'#l10n 79 (ES - Hello World on viernes, 1 de enero de 1960)\n'
|
'#l10n 79 (ES - Hola \$ Mundo)\n'
|
||||||
'#l10n 80 (ES - Hello world argument on 1/1/1960 at 0:00)\n'
|
'#l10n 80 (ES - Hello Mundo)\n'
|
||||||
'#l10n 81 (ES - Hello World from 1960 to 2020)\n'
|
'#l10n 81 (ES - Hola Mundo)\n'
|
||||||
'#l10n 82 (ES - Hello for 123)\n'
|
'#l10n 82 (ES - Hello World on viernes, 1 de enero de 1960)\n'
|
||||||
'#l10n 83 (ES - Hello)\n'
|
'#l10n 83 (ES - Hello world argument on 1/1/1960 at 0:00)\n'
|
||||||
'#l10n 84 (ES - Hello World)\n'
|
'#l10n 84 (ES - Hello World from 1960 to 2020)\n'
|
||||||
'#l10n 85 (ES - Hello two worlds)\n'
|
'#l10n 85 (ES - Hello for 123)\n'
|
||||||
'#l10n 86 (ES - Hello)\n'
|
'#l10n 86 (ES - Hello)\n'
|
||||||
'#l10n 87 (ES - Hello nuevo World)\n'
|
'#l10n 87 (ES - Hello World)\n'
|
||||||
'#l10n 88 (ES - Hello two nuevo worlds)\n'
|
'#l10n 88 (ES - Hello two worlds)\n'
|
||||||
'#l10n 89 (ES - Hello on viernes, 1 de enero de 1960)\n'
|
'#l10n 89 (ES - Hello)\n'
|
||||||
'#l10n 90 (ES - Hello World, on viernes, 1 de enero de 1960)\n'
|
'#l10n 90 (ES - Hello nuevo World)\n'
|
||||||
'#l10n 91 (ES - Hello two worlds, on viernes, 1 de enero de 1960)\n'
|
'#l10n 91 (ES - Hello two nuevo worlds)\n'
|
||||||
'#l10n 92 (ES - Hello other 0 worlds, with a total of 100 citizens)\n'
|
'#l10n 92 (ES - Hello on viernes, 1 de enero de 1960)\n'
|
||||||
'#l10n 93 (ES - Hello World of 101 citizens)\n'
|
'#l10n 93 (ES - Hello World, on viernes, 1 de enero de 1960)\n'
|
||||||
'#l10n 94 (ES - Hello two worlds with 102 total citizens)\n'
|
'#l10n 94 (ES - Hello two worlds, on viernes, 1 de enero de 1960)\n'
|
||||||
'#l10n 95 (ES - [Hola] -Mundo- #123#)\n'
|
'#l10n 95 (ES - Hello other 0 worlds, with a total of 100 citizens)\n'
|
||||||
'#l10n 96 (ES - \$!)\n'
|
'#l10n 96 (ES - Hello World of 101 citizens)\n'
|
||||||
'#l10n 97 (ES - One \$)\n'
|
'#l10n 97 (ES - Hello two worlds with 102 total citizens)\n'
|
||||||
"#l10n 98 (ES - Flutter's amazing!)\n"
|
'#l10n 98 (ES - [Hola] -Mundo- #123#)\n'
|
||||||
"#l10n 99 (ES - Flutter's amazing, times 2!)\n"
|
'#l10n 99 (ES - \$!)\n'
|
||||||
'#l10n 100 (ES - Flutter is "amazing"!)\n'
|
'#l10n 100 (ES - One \$)\n'
|
||||||
'#l10n 101 (ES - Flutter is "amazing", times 2!)\n'
|
"#l10n 101 (ES - Flutter's amazing!)\n"
|
||||||
'#l10n 102 (ES - 16 wheel truck)\n'
|
"#l10n 102 (ES - Flutter's amazing, times 2!)\n"
|
||||||
"#l10n 103 (ES - Sedan's elegance)\n"
|
'#l10n 103 (ES - Flutter is "amazing"!)\n'
|
||||||
'#l10n 104 (ES - Cabriolet has "acceleration")\n'
|
'#l10n 104 (ES - Flutter is "amazing", times 2!)\n'
|
||||||
'#l10n 105 (ES - Oh, she found ES - 1 itemES - !)\n'
|
'#l10n 105 (ES - 16 wheel truck)\n'
|
||||||
'#l10n 106 (ES - Indeed, ES - they like ES - Flutter!)\n'
|
"#l10n 106 (ES - Sedan's elegance)\n"
|
||||||
'#l10n 107 (--- es_419 ---)\n'
|
'#l10n 107 (ES - Cabriolet has "acceleration")\n'
|
||||||
'#l10n 108 (ES 419 - Hello World)\n'
|
'#l10n 108 (ES - Oh, she found ES - 1 itemES - !)\n'
|
||||||
'#l10n 109 (ES 419 - Hello)\n'
|
'#l10n 109 (ES - Indeed, ES - they like ES - Flutter!)\n'
|
||||||
'#l10n 110 (ES 419 - Hello World)\n'
|
'#l10n 110 (--- es_419 ---)\n'
|
||||||
'#l10n 111 (ES 419 - Hello two worlds)\n'
|
'#l10n 111 (ES 419 - Hello World)\n'
|
||||||
|
'#l10n 112 (ES 419 - Hello)\n'
|
||||||
|
'#l10n 113 (ES 419 - Hello World)\n'
|
||||||
|
'#l10n 114 (ES 419 - Hello two worlds)\n'
|
||||||
'#l10n END\n'
|
'#l10n END\n'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -229,6 +229,9 @@ class Home extends StatelessWidget {
|
|||||||
"${localizations.selectInString('he')}",
|
"${localizations.selectInString('he')}",
|
||||||
"${localizations.selectWithPlaceholder('male', 'ice cream')}",
|
"${localizations.selectWithPlaceholder('male', 'ice cream')}",
|
||||||
"${localizations.selectWithPlaceholder('female', 'chocolate')}",
|
"${localizations.selectWithPlaceholder('female', 'chocolate')}",
|
||||||
|
"${localizations.selectInPlural('male', 1)}",
|
||||||
|
"${localizations.selectInPlural('male', 2)}",
|
||||||
|
"${localizations.selectInPlural('female', 1)}",
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -627,7 +630,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"singleQuoteSelect": "{vehicleType, select, sedan{Sedan's elegance} cabriolet{Cabriolet' acceleration} truck{truck's heavy duty} other{Other's mirrors!}}",
|
"singleQuoteSelect": "{vehicleType, select, sedan{Sedan's elegance} cabriolet{Cabriolet's acceleration} truck{truck's heavy duty} other{Other's mirrors!}}",
|
||||||
"@singleQuoteSelect": {
|
"@singleQuoteSelect": {
|
||||||
"description": "A select message with a single quote.",
|
"description": "A select message with a single quote.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@ -666,6 +669,19 @@ void main() {
|
|||||||
"gender": {},
|
"gender": {},
|
||||||
"preference": {}
|
"preference": {}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"selectInPlural": "{count, plural, =1{{gender, select, male{he} female{she} other{they}}} other{they}}",
|
||||||
|
"@selectInPlural": {
|
||||||
|
"description": "Pronoun dependent on the count and gender.",
|
||||||
|
"placeholders": {
|
||||||
|
"gender": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"type": "num"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
''';
|
''';
|
||||||
@ -702,7 +718,7 @@ void main() {
|
|||||||
"helloFor": "ES - Hello for {value}",
|
"helloFor": "ES - Hello for {value}",
|
||||||
"helloAdjectiveWorlds": "{count,plural, =0{ES - Hello} =1{ES - Hello {adjective} World} =2{ES - Hello two {adjective} worlds} other{ES - Hello other {count} {adjective} worlds}}",
|
"helloAdjectiveWorlds": "{count,plural, =0{ES - Hello} =1{ES - Hello {adjective} World} =2{ES - Hello two {adjective} worlds} other{ES - Hello other {count} {adjective} worlds}}",
|
||||||
"helloWorldsOn": "{count,plural, =0{ES - Hello on {date}} =1{ES - Hello World, on {date}} =2{ES - Hello two worlds, on {date}} other{ES - Hello other {count} worlds, on {date}}}",
|
"helloWorldsOn": "{count,plural, =0{ES - Hello on {date}} =1{ES - Hello World, on {date}} =2{ES - Hello two worlds, on {date}} other{ES - Hello other {count} worlds, on {date}}}",
|
||||||
"helloWorldPopulation": "{ES - count,plural, =1{ES - Hello World of {population} citizens} =2{ES - Hello two worlds with {population} total citizens} many{ES - Hello all {count} worlds, with a total of {population} citizens} other{ES - Hello other {count} worlds, with a total of {population} citizens}}",
|
"helloWorldPopulation": "{count,plural, =1{ES - Hello World of {population} citizens} =2{ES - Hello two worlds with {population} total citizens} many{ES - Hello all {count} worlds, with a total of {population} citizens} other{ES - Hello other {count} worlds, with a total of {population} citizens}}",
|
||||||
"helloWorldInterpolation": "ES - [{hello}] #{world}#",
|
"helloWorldInterpolation": "ES - [{hello}] #{world}#",
|
||||||
"helloWorldsInterpolation": "{count,plural, other {ES - [{hello}] -{world}- #{count}#}}",
|
"helloWorldsInterpolation": "{count,plural, other {ES - [{hello}] -{world}- #{count}#}}",
|
||||||
"dollarSign": "ES - $!",
|
"dollarSign": "ES - $!",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user