[gen_l10n] Improvements to gen_l10n
(#116202)
* init * fix tests * fix lint * extra changes * oops missed some merge conflicts * fix lexer add tests * consistent warnings and errors * throw error at the end * improve efficiency, improve code generation * fix * nit * fix test * remove helper method class * two d's * oops * empty commit as google testing won't pass :(
This commit is contained in:
parent
43d5cbb15e
commit
7802c7acd8
@ -201,6 +201,10 @@ class GenerateLocalizationsCommand extends FlutterCommand {
|
|||||||
'contained within pairs of single quotes as normal strings and treat all '
|
'contained within pairs of single quotes as normal strings and treat all '
|
||||||
'consecutive pairs of single quotes as a single quote character.',
|
'consecutive pairs of single quotes as a single quote character.',
|
||||||
);
|
);
|
||||||
|
argParser.addFlag(
|
||||||
|
'suppress-warnings',
|
||||||
|
help: 'When specified, all warnings will be suppressed.\n'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final FileSystem _fileSystem;
|
final FileSystem _fileSystem;
|
||||||
@ -258,6 +262,7 @@ class GenerateLocalizationsCommand extends FlutterCommand {
|
|||||||
final bool areResourceAttributesRequired = boolArgDeprecated('required-resource-attributes');
|
final bool areResourceAttributesRequired = boolArgDeprecated('required-resource-attributes');
|
||||||
final bool usesNullableGetter = boolArgDeprecated('nullable-getter');
|
final bool usesNullableGetter = boolArgDeprecated('nullable-getter');
|
||||||
final bool useEscaping = boolArgDeprecated('use-escaping');
|
final bool useEscaping = boolArgDeprecated('use-escaping');
|
||||||
|
final bool suppressWarnings = boolArgDeprecated('suppress-warnings');
|
||||||
|
|
||||||
precacheLanguageAndRegionTags();
|
precacheLanguageAndRegionTags();
|
||||||
|
|
||||||
@ -281,6 +286,7 @@ class GenerateLocalizationsCommand extends FlutterCommand {
|
|||||||
usesNullableGetter: usesNullableGetter,
|
usesNullableGetter: usesNullableGetter,
|
||||||
useEscaping: useEscaping,
|
useEscaping: useEscaping,
|
||||||
logger: _logger,
|
logger: _logger,
|
||||||
|
suppressWarnings: suppressWarnings,
|
||||||
)
|
)
|
||||||
..loadResources()
|
..loadResources()
|
||||||
..writeOutputFiles())
|
..writeOutputFiles())
|
||||||
|
@ -67,6 +67,7 @@ LocalizationsGenerator generateLocalizations({
|
|||||||
usesNullableGetter: options.usesNullableGetter,
|
usesNullableGetter: options.usesNullableGetter,
|
||||||
useEscaping: options.useEscaping,
|
useEscaping: options.useEscaping,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
suppressWarnings: options.suppressWarnings,
|
||||||
)
|
)
|
||||||
..loadResources()
|
..loadResources()
|
||||||
..writeOutputFiles(isFromYaml: true);
|
..writeOutputFiles(isFromYaml: true);
|
||||||
@ -90,8 +91,6 @@ String _syntheticL10nPackagePath(FileSystem fileSystem) => fileSystem.path.join(
|
|||||||
// For example, if placeholders are used for plurals and no type was specified, then the type will
|
// 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
|
// 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.
|
// 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) {
|
||||||
return message.placeholders.values.map((Placeholder placeholder) {
|
return message.placeholders.values.map((Placeholder placeholder) {
|
||||||
return '${placeholder.type} ${placeholder.name}';
|
return '${placeholder.type} ${placeholder.name}';
|
||||||
@ -456,6 +455,7 @@ class LocalizationsGenerator {
|
|||||||
bool usesNullableGetter = true,
|
bool usesNullableGetter = true,
|
||||||
bool useEscaping = false,
|
bool useEscaping = false,
|
||||||
required Logger logger,
|
required Logger logger,
|
||||||
|
bool suppressWarnings = false,
|
||||||
}) {
|
}) {
|
||||||
final Directory? projectDirectory = projectDirFromPath(fileSystem, projectPathString);
|
final Directory? projectDirectory = projectDirFromPath(fileSystem, projectPathString);
|
||||||
final Directory inputDirectory = inputDirectoryFromPath(fileSystem, inputPathString, projectDirectory);
|
final Directory inputDirectory = inputDirectoryFromPath(fileSystem, inputPathString, projectDirectory);
|
||||||
@ -478,6 +478,7 @@ class LocalizationsGenerator {
|
|||||||
areResourceAttributesRequired: areResourceAttributesRequired,
|
areResourceAttributesRequired: areResourceAttributesRequired,
|
||||||
useEscaping: useEscaping,
|
useEscaping: useEscaping,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
suppressWarnings: suppressWarnings,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -501,10 +502,11 @@ class LocalizationsGenerator {
|
|||||||
this.usesNullableGetter = true,
|
this.usesNullableGetter = true,
|
||||||
required this.logger,
|
required this.logger,
|
||||||
this.useEscaping = false,
|
this.useEscaping = false,
|
||||||
|
this.suppressWarnings = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final FileSystem _fs;
|
final FileSystem _fs;
|
||||||
Iterable<Message> _allMessages = <Message>[];
|
List<Message> _allMessages = <Message>[];
|
||||||
late final AppResourceBundleCollection _allBundles = AppResourceBundleCollection(inputDirectory);
|
late final AppResourceBundleCollection _allBundles = AppResourceBundleCollection(inputDirectory);
|
||||||
late final AppResourceBundle _templateBundle = AppResourceBundle(templateArbFile);
|
late final AppResourceBundle _templateBundle = AppResourceBundle(templateArbFile);
|
||||||
late final Map<LocaleInfo, String> _inputFileNames = Map<LocaleInfo, String>.fromEntries(
|
late final Map<LocaleInfo, String> _inputFileNames = Map<LocaleInfo, String>.fromEntries(
|
||||||
@ -637,6 +639,9 @@ 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;
|
||||||
|
|
||||||
|
/// Whether or not to suppress warnings or not.
|
||||||
|
final bool suppressWarnings;
|
||||||
|
|
||||||
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)'.
|
||||||
@ -851,9 +856,6 @@ class LocalizationsGenerator {
|
|||||||
// Load _allMessages from templateArbFile and _allBundles from all of the ARB
|
// Load _allMessages from templateArbFile and _allBundles from all of the ARB
|
||||||
// files in inputDirectory. Also initialized: supportedLocales.
|
// files in inputDirectory. Also initialized: supportedLocales.
|
||||||
void loadResources() {
|
void loadResources() {
|
||||||
_allMessages = _templateBundle.resourceIds.map((String id) => Message(
|
|
||||||
_templateBundle, _allBundles, id, areResourceAttributesRequired, useEscaping: useEscaping,
|
|
||||||
));
|
|
||||||
for (final String resourceId in _templateBundle.resourceIds) {
|
for (final String resourceId in _templateBundle.resourceIds) {
|
||||||
if (!_isValidGetterAndMethodName(resourceId)) {
|
if (!_isValidGetterAndMethodName(resourceId)) {
|
||||||
throw L10nException(
|
throw L10nException(
|
||||||
@ -864,7 +866,10 @@ class LocalizationsGenerator {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The call to .toList() is absolutely necessary. Otherwise, it is an iterator and will call Message's constructor again.
|
||||||
|
_allMessages = _templateBundle.resourceIds.map((String id) => Message(
|
||||||
|
_templateBundle, _allBundles, id, areResourceAttributesRequired, useEscaping: useEscaping, logger: logger,
|
||||||
|
)).toList();
|
||||||
if (inputsAndOutputsListFile != null) {
|
if (inputsAndOutputsListFile != null) {
|
||||||
_inputFileList.addAll(_allBundles.bundles.map((AppResourceBundle bundle) {
|
_inputFileList.addAll(_allBundles.bundles.map((AppResourceBundle bundle) {
|
||||||
return bundle.file.absolute.path;
|
return bundle.file.absolute.path;
|
||||||
@ -1083,6 +1088,7 @@ class LocalizationsGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _generateMethod(Message message, LocaleInfo locale) {
|
String _generateMethod(Message message, LocaleInfo locale) {
|
||||||
|
try {
|
||||||
// Determine if we must import intl for date or number formatting.
|
// Determine if we must import intl for date or number formatting.
|
||||||
if (message.placeholdersRequireFormatting) {
|
if (message.placeholdersRequireFormatting) {
|
||||||
requiresIntlImport = true;
|
requiresIntlImport = true;
|
||||||
@ -1098,74 +1104,37 @@ class LocalizationsGenerator {
|
|||||||
.replaceAll('@(message)', "'${generateString(node.children.map((Node child) => child.value!).join())}'");
|
.replaceAll('@(message)', "'${generateString(node.children.map((Node child) => child.value!).join())}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<String> helperMethods = <String>[];
|
final List<String> tempVariables = <String>[];
|
||||||
|
// Get a unique temporary variable name.
|
||||||
// Get a unique helper method name.
|
int variableCount = 0;
|
||||||
int methodNameCount = 0;
|
String getTempVariableName() {
|
||||||
String getHelperMethodName() {
|
return '_temp${variableCount++}';
|
||||||
return '_${message.resourceId}${methodNameCount++}';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do a DFS post order traversal, generating dependent
|
// Do a DFS post order traversal through placeholderExpr, pluralExpr, and selectExpr nodes.
|
||||||
// placeholder, plural, select helper methods, and combine these into
|
// When traversing through a placeholderExpr node, return "$placeholderName".
|
||||||
// one message. Returns the method/placeholder to use in parent string.
|
// When traversing through a pluralExpr node, return "$tempVarN" and add variable declaration in "tempVariables".
|
||||||
HelperMethod generateHelperMethods(Node node, { bool isRoot = false }) {
|
// When traversing through a selectExpr node, return "$tempVarN" and add variable declaration in "tempVariables".
|
||||||
final Set<Placeholder> dependentPlaceholders = <Placeholder>{};
|
// When traversing through a message node, return concatenation of all of "generateVariables(child)" for each child.
|
||||||
|
String generateVariables(Node node, { bool isRoot = false }) {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case ST.message:
|
case ST.message:
|
||||||
final List<HelperMethod> helpers = node.children.map<HelperMethod>((Node node) {
|
final List<String> expressions = node.children.map<String>((Node node) {
|
||||||
if (node.type == ST.string) {
|
if (node.type == ST.string) {
|
||||||
return HelperMethod(<Placeholder>{}, string: node.value);
|
return node.value!;
|
||||||
}
|
}
|
||||||
final HelperMethod helper = generateHelperMethods(node);
|
return generateVariables(node);
|
||||||
dependentPlaceholders.addAll(helper.dependentPlaceholders);
|
|
||||||
return helper;
|
|
||||||
}).toList();
|
}).toList();
|
||||||
final String messageString = generateReturnExpr(helpers);
|
return generateReturnExpr(expressions);
|
||||||
|
|
||||||
// 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:
|
case ST.placeholderExpr:
|
||||||
assert(node.children[1].type == ST.identifier);
|
assert(node.children[1].type == ST.identifier);
|
||||||
final Node identifier = node.children[1];
|
final String identifier = node.children[1].value!;
|
||||||
// Check that placeholders exist.
|
final Placeholder placeholder = message.placeholders[identifier]!;
|
||||||
final Placeholder? placeholder = message.placeholders[identifier.value];
|
if (placeholder.requiresFormatting) {
|
||||||
if (placeholder == null) {
|
return '\$${node.children[1].value}String';
|
||||||
throw L10nParserException(
|
|
||||||
'Make sure that the specified placeholder is defined in your arb file.',
|
|
||||||
_inputFileNames[locale]!,
|
|
||||||
message.resourceId,
|
|
||||||
translationForMessage,
|
|
||||||
identifier.positionInMessage,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
dependentPlaceholders.add(placeholder);
|
return '\$${node.children[1].value}';
|
||||||
return HelperMethod(dependentPlaceholders, placeholder: placeholder);
|
|
||||||
|
|
||||||
case ST.pluralExpr:
|
case ST.pluralExpr:
|
||||||
requiresIntlImport = true;
|
requiresIntlImport = true;
|
||||||
@ -1178,28 +1147,6 @@ class LocalizationsGenerator {
|
|||||||
final Node identifier = node.children[1];
|
final Node identifier = node.children[1];
|
||||||
final Node pluralParts = node.children[5];
|
final Node pluralParts = node.children[5];
|
||||||
|
|
||||||
// Check that placeholders exist and is of type int or num.
|
|
||||||
final Placeholder? placeholder = message.placeholders[identifier.value];
|
|
||||||
if (placeholder == null) {
|
|
||||||
throw L10nParserException(
|
|
||||||
'Make sure that the specified placeholder is defined in your arb file.',
|
|
||||||
_inputFileNames[locale]!,
|
|
||||||
message.resourceId,
|
|
||||||
translationForMessage,
|
|
||||||
identifier.positionInMessage,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (placeholder.type != 'num' && placeholder.type != 'int') {
|
|
||||||
throw L10nParserException(
|
|
||||||
'The specified placeholder must be of type int or num.',
|
|
||||||
_inputFileNames[locale]!,
|
|
||||||
message.resourceId,
|
|
||||||
translationForMessage,
|
|
||||||
identifier.positionInMessage,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
dependentPlaceholders.add(placeholder);
|
|
||||||
|
|
||||||
for (final Node pluralPart in pluralParts.children.reversed) {
|
for (final Node pluralPart in pluralParts.children.reversed) {
|
||||||
String pluralCase;
|
String pluralCase;
|
||||||
Node pluralMessage;
|
Node pluralMessage;
|
||||||
@ -1215,26 +1162,22 @@ class LocalizationsGenerator {
|
|||||||
pluralMessage = pluralPart.children[2];
|
pluralMessage = pluralPart.children[2];
|
||||||
}
|
}
|
||||||
if (!pluralLogicArgs.containsKey(pluralCases[pluralCase])) {
|
if (!pluralLogicArgs.containsKey(pluralCases[pluralCase])) {
|
||||||
final HelperMethod pluralPartHelper = generateHelperMethods(pluralMessage);
|
final String pluralPartExpression = generateVariables(pluralMessage);
|
||||||
pluralLogicArgs[pluralCases[pluralCase]!] = ' ${pluralCases[pluralCase]}: ${pluralPartHelper.helperOrPlaceholder},';
|
pluralLogicArgs[pluralCases[pluralCase]!] = ' ${pluralCases[pluralCase]}: $pluralPartExpression,';
|
||||||
dependentPlaceholders.addAll(pluralPartHelper.dependentPlaceholders);
|
} else if (!suppressWarnings) {
|
||||||
} else {
|
|
||||||
logger.printWarning('''
|
logger.printWarning('''
|
||||||
The plural part specified below is overrided by a later plural part.
|
[${_inputFileNames[locale]}:${message.resourceId}] ICU Syntax Warning: The plural part specified below is overridden by a later plural part.
|
||||||
$translationForMessage
|
$translationForMessage
|
||||||
${Parser.indentForError(pluralPart.positionInMessage)}
|
${Parser.indentForError(pluralPart.positionInMessage)}''');
|
||||||
''');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final String helperMethodName = getHelperMethodName();
|
final String tempVarName = getTempVariableName();
|
||||||
final HelperMethod pluralHelper = HelperMethod(dependentPlaceholders, helper: helperMethodName);
|
tempVariables.add(pluralVariableTemplate
|
||||||
helperMethods.add(pluralHelperTemplate
|
.replaceAll('@(varName)', tempVarName)
|
||||||
.replaceAll('@(name)', helperMethodName)
|
|
||||||
.replaceAll('@(parameters)', pluralHelper.methodParameters)
|
|
||||||
.replaceAll('@(count)', identifier.value!)
|
.replaceAll('@(count)', identifier.value!)
|
||||||
.replaceAll('@(pluralLogicArgs)', pluralLogicArgs.values.join('\n'))
|
.replaceAll('@(pluralLogicArgs)', pluralLogicArgs.values.join('\n'))
|
||||||
);
|
);
|
||||||
return pluralHelper;
|
return '\$$tempVarName';
|
||||||
|
|
||||||
case ST.selectExpr:
|
case ST.selectExpr:
|
||||||
requiresIntlImport = true;
|
requiresIntlImport = true;
|
||||||
@ -1244,53 +1187,53 @@ ${Parser.indentForError(pluralPart.positionInMessage)}
|
|||||||
assert(node.children[5].type == ST.selectParts);
|
assert(node.children[5].type == ST.selectParts);
|
||||||
|
|
||||||
final Node identifier = node.children[1];
|
final Node identifier = node.children[1];
|
||||||
// Check that placeholders exist.
|
|
||||||
final Placeholder? placeholder = message.placeholders[identifier.value];
|
|
||||||
if (placeholder == null) {
|
|
||||||
throw L10nParserException(
|
|
||||||
'Make sure that the specified placeholder is defined in your arb file.',
|
|
||||||
_inputFileNames[locale]!,
|
|
||||||
message.resourceId,
|
|
||||||
translationForMessage,
|
|
||||||
identifier.positionInMessage,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
dependentPlaceholders.add(placeholder);
|
|
||||||
final List<String> selectLogicArgs = <String>[];
|
final List<String> selectLogicArgs = <String>[];
|
||||||
final Node selectParts = node.children[5];
|
final Node selectParts = node.children[5];
|
||||||
|
|
||||||
for (final Node selectPart in selectParts.children) {
|
for (final Node selectPart in selectParts.children) {
|
||||||
assert(selectPart.children[0].type == ST.identifier || selectPart.children[0].type == ST.other);
|
assert(selectPart.children[0].type == ST.identifier || selectPart.children[0].type == ST.other);
|
||||||
assert(selectPart.children[2].type == ST.message);
|
assert(selectPart.children[2].type == ST.message);
|
||||||
final String selectCase = selectPart.children[0].value!;
|
final String selectCase = selectPart.children[0].value!;
|
||||||
final Node selectMessage = selectPart.children[2];
|
final Node selectMessage = selectPart.children[2];
|
||||||
final HelperMethod selectPartHelper = generateHelperMethods(selectMessage);
|
final String selectPartExpression = generateVariables(selectMessage);
|
||||||
selectLogicArgs.add(" '$selectCase': ${selectPartHelper.helperOrPlaceholder},");
|
selectLogicArgs.add(" '$selectCase': $selectPartExpression,");
|
||||||
dependentPlaceholders.addAll(selectPartHelper.dependentPlaceholders);
|
|
||||||
}
|
}
|
||||||
final String helperMethodName = getHelperMethodName();
|
final String tempVarName = getTempVariableName();
|
||||||
final HelperMethod selectHelper = HelperMethod(dependentPlaceholders, helper: helperMethodName);
|
tempVariables.add(selectVariableTemplate
|
||||||
|
.replaceAll('@(varName)', tempVarName)
|
||||||
helperMethods.add(selectHelperTemplate
|
|
||||||
.replaceAll('@(name)', helperMethodName)
|
|
||||||
.replaceAll('@(parameters)', selectHelper.methodParameters)
|
|
||||||
.replaceAll('@(choice)', identifier.value!)
|
.replaceAll('@(choice)', identifier.value!)
|
||||||
.replaceAll('@(selectCases)', selectLogicArgs.join('\n'))
|
.replaceAll('@(selectCases)', selectLogicArgs.join('\n'))
|
||||||
);
|
);
|
||||||
return HelperMethod(dependentPlaceholders, helper: helperMethodName);
|
return '\$$tempVarName';
|
||||||
// ignore: no_default_cases
|
// ignore: no_default_cases
|
||||||
default:
|
default:
|
||||||
throw Exception('Cannot call "generateHelperMethod" on node type ${node.type}');
|
throw Exception('Cannot call "generateHelperMethod" on node type ${node.type}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
generateHelperMethods(node, isRoot: true);
|
final String messageString = generateVariables(node, isRoot: true);
|
||||||
return helperMethods.last.replaceAll('@(helperMethods)', helperMethods.sublist(0, helperMethods.length - 1).join('\n\n'));
|
final String tempVarLines = tempVariables.isEmpty ? '' : '${tempVariables.join('\n')}\n';
|
||||||
|
return methodTemplate
|
||||||
|
.replaceAll('@(name)', message.resourceId)
|
||||||
|
.replaceAll('@(parameters)', generateMethodParameters(message).join(', '))
|
||||||
|
.replaceAll('@(dateFormatting)', generateDateFormattingLogic(message))
|
||||||
|
.replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message))
|
||||||
|
.replaceAll('@(tempVars)', tempVarLines)
|
||||||
|
.replaceAll('@(message)', messageString)
|
||||||
|
.replaceAll('@(none)\n', '');
|
||||||
|
} on L10nParserException catch (error) {
|
||||||
|
logger.printError(error.toString());
|
||||||
|
return '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
||||||
final String generatedLocalizationsFile = _generateCode();
|
final String generatedLocalizationsFile = _generateCode();
|
||||||
|
|
||||||
|
// If there were any syntax errors, don't write to files.
|
||||||
|
if (logger.hadErrorOutput) {
|
||||||
|
throw L10nException('Found syntax errors.');
|
||||||
|
}
|
||||||
|
|
||||||
// A pubspec.yaml file is required when using a synthetic package. If it does not
|
// A pubspec.yaml file is required when using a synthetic package. If it does not
|
||||||
// exist, create a blank one.
|
// exist, create a blank one.
|
||||||
if (useSyntheticPackage) {
|
if (useSyntheticPackage) {
|
||||||
|
@ -139,33 +139,23 @@ const String methodTemplate = '''
|
|||||||
String @(name)(@(parameters)) {
|
String @(name)(@(parameters)) {
|
||||||
@(dateFormatting)
|
@(dateFormatting)
|
||||||
@(numberFormatting)
|
@(numberFormatting)
|
||||||
@(helperMethods)
|
@(tempVars) return @(message);
|
||||||
return @(message);
|
|
||||||
}''';
|
}''';
|
||||||
|
|
||||||
const String messageHelperTemplate = '''
|
const String pluralVariableTemplate = '''
|
||||||
String @(name)(@(parameters)) {
|
String @(varName) = intl.Intl.pluralLogic(
|
||||||
return @(message);
|
@(count),
|
||||||
}''';
|
locale: localeName,
|
||||||
|
|
||||||
const String pluralHelperTemplate = '''
|
|
||||||
String @(name)(@(parameters)) {
|
|
||||||
return intl.Intl.pluralLogic(
|
|
||||||
@(count),
|
|
||||||
locale: localeName,
|
|
||||||
@(pluralLogicArgs)
|
@(pluralLogicArgs)
|
||||||
);
|
);''';
|
||||||
}''';
|
|
||||||
|
|
||||||
const String selectHelperTemplate = '''
|
const String selectVariableTemplate = '''
|
||||||
String @(name)(@(parameters)) {
|
String @(varName) = intl.Intl.selectLogic(
|
||||||
return intl.Intl.selectLogic(
|
@(choice),
|
||||||
@(choice),
|
{
|
||||||
{
|
|
||||||
@(selectCases)
|
@(selectCases)
|
||||||
},
|
},
|
||||||
);
|
);''';
|
||||||
}''';
|
|
||||||
|
|
||||||
const String classFileTemplate = '''
|
const String classFileTemplate = '''
|
||||||
@(header)@(requiresIntlImport)import '@(fileName)';
|
@(header)@(requiresIntlImport)import '@(fileName)';
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import 'package:intl/locale.dart';
|
import 'package:intl/locale.dart';
|
||||||
|
|
||||||
import '../base/file_system.dart';
|
import '../base/file_system.dart';
|
||||||
|
import '../base/logger.dart';
|
||||||
import '../convert.dart';
|
import '../convert.dart';
|
||||||
import 'localizations_utils.dart';
|
import 'localizations_utils.dart';
|
||||||
import 'message_parser.dart';
|
import 'message_parser.dart';
|
||||||
@ -138,17 +139,31 @@ class L10nParserException extends L10nException {
|
|||||||
this.messageString,
|
this.messageString,
|
||||||
this.charNumber
|
this.charNumber
|
||||||
): super('''
|
): super('''
|
||||||
$error
|
[$fileName:$messageId] $error
|
||||||
[$fileName:$messageId] $messageString
|
$messageString
|
||||||
${List<String>.filled(4 + fileName.length + messageId.length + charNumber, ' ').join()}^''');
|
${List<String>.filled(charNumber, ' ').join()}^''');
|
||||||
|
|
||||||
final String error;
|
final String error;
|
||||||
final String fileName;
|
final String fileName;
|
||||||
final String messageId;
|
final String messageId;
|
||||||
final String messageString;
|
final String messageString;
|
||||||
|
// Position of character within the "messageString" where the error is.
|
||||||
final int charNumber;
|
final int charNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class L10nMissingPlaceholderException extends L10nParserException {
|
||||||
|
L10nMissingPlaceholderException(
|
||||||
|
super.error,
|
||||||
|
super.fileName,
|
||||||
|
super.messageId,
|
||||||
|
super.messageString,
|
||||||
|
super.charNumber,
|
||||||
|
this.placeholderName,
|
||||||
|
);
|
||||||
|
|
||||||
|
final String placeholderName;
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@ -319,7 +334,10 @@ class Message {
|
|||||||
AppResourceBundleCollection allBundles,
|
AppResourceBundleCollection allBundles,
|
||||||
this.resourceId,
|
this.resourceId,
|
||||||
bool isResourceAttributeRequired,
|
bool isResourceAttributeRequired,
|
||||||
{ this.useEscaping = false }
|
{
|
||||||
|
this.useEscaping = false,
|
||||||
|
this.logger,
|
||||||
|
}
|
||||||
) : assert(templateBundle != null),
|
) : assert(templateBundle != null),
|
||||||
assert(allBundles != null),
|
assert(allBundles != null),
|
||||||
assert(resourceId != null && resourceId.isNotEmpty),
|
assert(resourceId != null && resourceId.isNotEmpty),
|
||||||
@ -335,64 +353,16 @@ class Message {
|
|||||||
filenames[bundle.locale] = bundle.file.basename;
|
filenames[bundle.locale] = bundle.file.basename;
|
||||||
final String? translation = bundle.translationFor(resourceId);
|
final String? translation = bundle.translationFor(resourceId);
|
||||||
messages[bundle.locale] = translation;
|
messages[bundle.locale] = translation;
|
||||||
parsedMessages[bundle.locale] = translation == null ? null : Parser(resourceId, bundle.file.basename, translation, useEscaping: useEscaping).parse();
|
parsedMessages[bundle.locale] = translation == null ? null : Parser(
|
||||||
}
|
resourceId,
|
||||||
// Using parsed translations, attempt to infer types of placeholders used by plurals and selects.
|
bundle.file.basename,
|
||||||
for (final LocaleInfo locale in parsedMessages.keys) {
|
translation,
|
||||||
if (parsedMessages[locale] == null) {
|
useEscaping: useEscaping,
|
||||||
continue;
|
logger: logger
|
||||||
}
|
).parse();
|
||||||
final List<Node> traversalStack = <Node>[parsedMessages[locale]!];
|
|
||||||
while (traversalStack.isNotEmpty) {
|
|
||||||
final Node node = traversalStack.removeLast();
|
|
||||||
if (node.type == ST.pluralExpr) {
|
|
||||||
final Placeholder? placeholder = placeholders[node.children[1].value!];
|
|
||||||
if (placeholder == null) {
|
|
||||||
throw L10nParserException(
|
|
||||||
'Make sure that the specified plural placeholder is defined in your arb file.',
|
|
||||||
filenames[locale]!,
|
|
||||||
resourceId,
|
|
||||||
messages[locale]!,
|
|
||||||
node.children[1].positionInMessage
|
|
||||||
);
|
|
||||||
}
|
|
||||||
placeholders[node.children[1].value!]!.isPlural = true;
|
|
||||||
}
|
|
||||||
if (node.type == ST.selectExpr) {
|
|
||||||
final Placeholder? placeholder = placeholders[node.children[1].value!];
|
|
||||||
if (placeholder == null) {
|
|
||||||
throw L10nParserException(
|
|
||||||
'Make sure that the specified select placeholder is defined in your arb file.',
|
|
||||||
filenames[locale]!,
|
|
||||||
resourceId,
|
|
||||||
messages[locale]!,
|
|
||||||
node.children[1].positionInMessage
|
|
||||||
);
|
|
||||||
}
|
|
||||||
placeholders[node.children[1].value!]!.isSelect = true;
|
|
||||||
}
|
|
||||||
traversalStack.addAll(node.children);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (final Placeholder placeholder in placeholders.values) {
|
|
||||||
if (placeholder.isPlural && placeholder.isSelect) {
|
|
||||||
throw L10nException('Placeholder is used as both a plural and select in certain languages.');
|
|
||||||
} else if (placeholder.isPlural) {
|
|
||||||
if (placeholder.type == null) {
|
|
||||||
placeholder.type = 'num';
|
|
||||||
}
|
|
||||||
else if (!<String>['num', 'int'].contains(placeholder.type)) {
|
|
||||||
throw L10nException("Placeholders used in plurals must be of type 'num' or 'int'");
|
|
||||||
}
|
|
||||||
} else if (placeholder.isSelect) {
|
|
||||||
if (placeholder.type == null) {
|
|
||||||
placeholder.type = 'String';
|
|
||||||
} else if (placeholder.type != 'String') {
|
|
||||||
throw L10nException("Placeholders used in selects must be of type 'String'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
placeholder.type ??= 'Object';
|
|
||||||
}
|
}
|
||||||
|
// Infer the placeholders
|
||||||
|
_inferPlaceholders(filenames);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String resourceId;
|
final String resourceId;
|
||||||
@ -402,6 +372,7 @@ class Message {
|
|||||||
final Map<LocaleInfo, Node?> parsedMessages;
|
final Map<LocaleInfo, Node?> parsedMessages;
|
||||||
final Map<String, Placeholder> placeholders;
|
final Map<String, Placeholder> placeholders;
|
||||||
final bool useEscaping;
|
final bool useEscaping;
|
||||||
|
final Logger? logger;
|
||||||
|
|
||||||
bool get placeholdersRequireFormatting => placeholders.values.any((Placeholder p) => p.requiresFormatting);
|
bool get placeholdersRequireFormatting => placeholders.values.any((Placeholder p) => p.requiresFormatting);
|
||||||
|
|
||||||
@ -496,6 +467,63 @@ class Message {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Using parsed translations, attempt to infer types of placeholders used by plurals and selects.
|
||||||
|
// For undeclared placeholders, create a new placeholder.
|
||||||
|
void _inferPlaceholders(Map<LocaleInfo, String> filenames) {
|
||||||
|
// We keep the undeclared placeholders separate so that we can sort them alphabetically afterwards.
|
||||||
|
final Map<String, Placeholder> undeclaredPlaceholders = <String, Placeholder>{};
|
||||||
|
// Helper for getting placeholder by name.
|
||||||
|
Placeholder? getPlaceholder(String name) => placeholders[name] ?? undeclaredPlaceholders[name];
|
||||||
|
for (final LocaleInfo locale in parsedMessages.keys) {
|
||||||
|
if (parsedMessages[locale] == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final List<Node> traversalStack = <Node>[parsedMessages[locale]!];
|
||||||
|
while (traversalStack.isNotEmpty) {
|
||||||
|
final Node node = traversalStack.removeLast();
|
||||||
|
if (<ST>[ST.placeholderExpr, ST.pluralExpr, ST.selectExpr].contains(node.type)) {
|
||||||
|
final String identifier = node.children[1].value!;
|
||||||
|
Placeholder? placeholder = getPlaceholder(identifier);
|
||||||
|
if (placeholder == null) {
|
||||||
|
placeholder = Placeholder(resourceId, identifier, <String, Object?>{});
|
||||||
|
undeclaredPlaceholders[identifier] = placeholder;
|
||||||
|
}
|
||||||
|
if (node.type == ST.pluralExpr) {
|
||||||
|
placeholder.isPlural = true;
|
||||||
|
} else if (node.type == ST.selectExpr) {
|
||||||
|
placeholder.isSelect = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
traversalStack.addAll(node.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
placeholders.addEntries(
|
||||||
|
undeclaredPlaceholders.entries
|
||||||
|
.toList()
|
||||||
|
..sort((MapEntry<String, Placeholder> p1, MapEntry<String, Placeholder> p2) => p1.key.compareTo(p2.key))
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final Placeholder placeholder in placeholders.values) {
|
||||||
|
if (placeholder.isPlural && placeholder.isSelect) {
|
||||||
|
throw L10nException('Placeholder is used as both a plural and select in certain languages.');
|
||||||
|
} else if (placeholder.isPlural) {
|
||||||
|
if (placeholder.type == null) {
|
||||||
|
placeholder.type = 'num';
|
||||||
|
}
|
||||||
|
else if (!<String>['num', 'int'].contains(placeholder.type)) {
|
||||||
|
throw L10nException("Placeholders used in plurals must be of type 'num' or 'int'");
|
||||||
|
}
|
||||||
|
} else if (placeholder.isSelect) {
|
||||||
|
if (placeholder.type == null) {
|
||||||
|
placeholder.type = 'String';
|
||||||
|
} else if (placeholder.type != 'String') {
|
||||||
|
throw L10nException("Placeholders used in selects must be of type 'String'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
placeholder.type ??= 'Object';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Represents the contents of one ARB file.
|
// Represents the contents of one ARB file.
|
||||||
@ -834,50 +862,3 @@ 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(', ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -297,25 +297,23 @@ String generateString(String value) {
|
|||||||
|
|
||||||
/// Given a list of strings, placeholders, or helper function calls, concatenate
|
/// Given a list of strings, placeholders, or helper function calls, concatenate
|
||||||
/// them into one expression to be returned.
|
/// them into one expression to be returned.
|
||||||
String generateReturnExpr(List<HelperMethod> helpers) {
|
/// If isSingleStringVar is passed, then we want to convert "'$expr'" to simply "expr".
|
||||||
if (helpers.isEmpty) {
|
String generateReturnExpr(List<String> expressions, { bool isSingleStringVar = false }) {
|
||||||
|
if (expressions.isEmpty) {
|
||||||
return "''";
|
return "''";
|
||||||
} else if (
|
} else if (isSingleStringVar) {
|
||||||
helpers.length == 1
|
// If our expression is "$varName" where varName is a String, this is equivalent to just varName.
|
||||||
&& helpers[0].string == null
|
return expressions[0].substring(1);
|
||||||
&& (helpers[0].placeholder?.type == 'String' || helpers[0].helper != null)
|
|
||||||
) {
|
|
||||||
return helpers[0].helperOrPlaceholder;
|
|
||||||
} else {
|
} else {
|
||||||
final String string = helpers.reversed.fold<String>('', (String string, HelperMethod helper) {
|
final String string = expressions.reversed.fold<String>('', (String string, String expression) {
|
||||||
if (helper.string != null) {
|
if (expression[0] != r'$') {
|
||||||
return generateString(helper.string!) + string;
|
return generateString(expression) + string;
|
||||||
}
|
}
|
||||||
final RegExp alphanumeric = RegExp(r'^([0-9a-zA-Z]|_)+$');
|
final RegExp alphanumeric = RegExp(r'^([0-9a-zA-Z]|_)+$');
|
||||||
if (alphanumeric.hasMatch(helper.helperOrPlaceholder) && !(string.isNotEmpty && alphanumeric.hasMatch(string[0]))) {
|
if (alphanumeric.hasMatch(expression.substring(1)) && !(string.isNotEmpty && alphanumeric.hasMatch(string[0]))) {
|
||||||
return '\$${helper.helperOrPlaceholder}$string';
|
return '$expression$string';
|
||||||
} else {
|
} else {
|
||||||
return '\${${helper.helperOrPlaceholder}}$string';
|
return '\${${expression.substring(1)}}$string';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return "'$string'";
|
return "'$string'";
|
||||||
@ -340,6 +338,7 @@ class LocalizationOptions {
|
|||||||
this.usesNullableGetter = true,
|
this.usesNullableGetter = true,
|
||||||
this.format = false,
|
this.format = false,
|
||||||
this.useEscaping = false,
|
this.useEscaping = false,
|
||||||
|
this.suppressWarnings = false,
|
||||||
}) : assert(useSyntheticPackage != null);
|
}) : assert(useSyntheticPackage != null);
|
||||||
|
|
||||||
/// The `--arb-dir` argument.
|
/// The `--arb-dir` argument.
|
||||||
@ -416,6 +415,11 @@ class LocalizationOptions {
|
|||||||
///
|
///
|
||||||
/// Whether or not the ICU escaping syntax is used.
|
/// Whether or not the ICU escaping syntax is used.
|
||||||
final bool useEscaping;
|
final bool useEscaping;
|
||||||
|
|
||||||
|
/// The `suppress-warnings` argument.
|
||||||
|
///
|
||||||
|
/// Whether or not to suppress warnings.
|
||||||
|
final bool suppressWarnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the localizations configuration options from [file].
|
/// Parse the localizations configuration options from [file].
|
||||||
@ -450,8 +454,9 @@ LocalizationOptions parseLocalizationsOptions({
|
|||||||
useSyntheticPackage: _tryReadBool(yamlNode, 'synthetic-package', logger) ?? true,
|
useSyntheticPackage: _tryReadBool(yamlNode, 'synthetic-package', logger) ?? true,
|
||||||
areResourceAttributesRequired: _tryReadBool(yamlNode, 'required-resource-attributes', logger) ?? false,
|
areResourceAttributesRequired: _tryReadBool(yamlNode, 'required-resource-attributes', logger) ?? false,
|
||||||
usesNullableGetter: _tryReadBool(yamlNode, 'nullable-getter', logger) ?? true,
|
usesNullableGetter: _tryReadBool(yamlNode, 'nullable-getter', logger) ?? true,
|
||||||
format: _tryReadBool(yamlNode, 'format', logger) ?? true,
|
format: _tryReadBool(yamlNode, 'format', logger) ?? false,
|
||||||
useEscaping: _tryReadBool(yamlNode, 'use-escaping', logger) ?? false,
|
useEscaping: _tryReadBool(yamlNode, 'use-escaping', logger) ?? false,
|
||||||
|
suppressWarnings: _tryReadBool(yamlNode, 'suppress-warnings', logger) ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
// See https://flutter.dev/go/icu-message-parser.
|
// See https://flutter.dev/go/icu-message-parser.
|
||||||
|
|
||||||
// Symbol Types
|
// Symbol Types
|
||||||
|
import '../base/logger.dart';
|
||||||
import 'gen_l10n_types.dart';
|
import 'gen_l10n_types.dart';
|
||||||
|
|
||||||
enum ST {
|
enum ST {
|
||||||
@ -181,13 +182,17 @@ class Parser {
|
|||||||
this.messageId,
|
this.messageId,
|
||||||
this.filename,
|
this.filename,
|
||||||
this.messageString,
|
this.messageString,
|
||||||
{ this.useEscaping = false }
|
{
|
||||||
|
this.useEscaping = false,
|
||||||
|
this.logger
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
final String messageId;
|
final String messageId;
|
||||||
final String messageString;
|
final String messageString;
|
||||||
final String filename;
|
final String filename;
|
||||||
final bool useEscaping;
|
final bool useEscaping;
|
||||||
|
final Logger? logger;
|
||||||
|
|
||||||
static String indentForError(int position) {
|
static String indentForError(int position) {
|
||||||
return '${List<String>.filled(position, ' ').join()}^';
|
return '${List<String>.filled(position, ' ').join()}^';
|
||||||
@ -297,6 +302,11 @@ class Parser {
|
|||||||
// Do not add whitespace as a token.
|
// Do not add whitespace as a token.
|
||||||
startIndex = match.end;
|
startIndex = match.end;
|
||||||
continue;
|
continue;
|
||||||
|
} else if (<ST>[ST.plural, ST.select].contains(matchedType) && tokens.last.type == ST.openBrace) {
|
||||||
|
// Treat "plural" or "select" as identifier if it comes right after an open brace.
|
||||||
|
tokens.add(Node(ST.identifier, startIndex, value: match.group(0)));
|
||||||
|
startIndex = match.end;
|
||||||
|
continue;
|
||||||
} else {
|
} else {
|
||||||
tokens.add(Node(matchedType!, startIndex, value: match.group(0)));
|
tokens.add(Node(matchedType!, startIndex, value: match.group(0)));
|
||||||
startIndex = match.end;
|
startIndex = match.end;
|
||||||
@ -566,8 +576,13 @@ class Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Node parse() {
|
Node parse() {
|
||||||
final Node syntaxTree = compress(parseIntoTree());
|
try {
|
||||||
checkExtraRules(syntaxTree);
|
final Node syntaxTree = compress(parseIntoTree());
|
||||||
return syntaxTree;
|
checkExtraRules(syntaxTree);
|
||||||
|
return syntaxTree;
|
||||||
|
} on L10nParserException catch (error) {
|
||||||
|
logger?.printError(error.toString());
|
||||||
|
return Node(ST.empty, 0, value: '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1284,6 +1284,40 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('writeOutputFiles', () {
|
group('writeOutputFiles', () {
|
||||||
|
testWithoutContext('multiple messages with syntax error all log their errors', () {
|
||||||
|
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
|
||||||
|
..createSync(recursive: true);
|
||||||
|
l10nDirectory.childFile(defaultTemplateArbFileName)
|
||||||
|
.writeAsStringSync(r'''
|
||||||
|
{
|
||||||
|
"msg1": "{",
|
||||||
|
"msg2": "{ {"
|
||||||
|
}''');
|
||||||
|
l10nDirectory.childFile(esArbFileName)
|
||||||
|
.writeAsStringSync(singleEsMessageArbFileString);
|
||||||
|
try {
|
||||||
|
LocalizationsGenerator(
|
||||||
|
fileSystem: fs,
|
||||||
|
inputPathString: defaultL10nPathString,
|
||||||
|
outputPathString: defaultL10nPathString,
|
||||||
|
templateArbFileName: defaultTemplateArbFileName,
|
||||||
|
outputFileString: defaultOutputFileString,
|
||||||
|
classNameString: defaultClassNameString,
|
||||||
|
logger: logger,
|
||||||
|
)
|
||||||
|
..loadResources()
|
||||||
|
..writeOutputFiles();
|
||||||
|
} on L10nException catch (error) {
|
||||||
|
expect(error.message, equals('Found syntax errors.'));
|
||||||
|
expect(logger.errorText, contains('''
|
||||||
|
[app_en.arb:msg1] ICU Syntax Error: Expected "identifier" but found no tokens.
|
||||||
|
{
|
||||||
|
^
|
||||||
|
[app_en.arb:msg2] ICU Syntax Error: Expected "identifier" but found "{".
|
||||||
|
{ {
|
||||||
|
^'''));
|
||||||
|
}
|
||||||
|
});
|
||||||
testWithoutContext('message without placeholders - should generate code comment with description and template message translation', () {
|
testWithoutContext('message without placeholders - should generate code comment with description and template message translation', () {
|
||||||
_standardFlutterDirectoryL10nSetup(fs);
|
_standardFlutterDirectoryL10nSetup(fs);
|
||||||
LocalizationsGenerator(
|
LocalizationsGenerator(
|
||||||
@ -1581,47 +1615,31 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('placeholder tests', () {
|
group('placeholder tests', () {
|
||||||
testWithoutContext('should throw attempting to generate a select message without placeholders', () {
|
testWithoutContext('should automatically infer placeholders that are not explicitly defined', () {
|
||||||
const String selectMessageWithoutPlaceholdersAttribute = '''
|
const String messageWithoutDefinedPlaceholder = '''
|
||||||
{
|
{
|
||||||
"helloWorld": "Hello {name}",
|
"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')
|
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
|
||||||
..createSync(recursive: true);
|
..createSync(recursive: true);
|
||||||
l10nDirectory.childFile(defaultTemplateArbFileName)
|
l10nDirectory.childFile(defaultTemplateArbFileName)
|
||||||
.writeAsStringSync(selectMessageWithoutPlaceholdersAttribute);
|
.writeAsStringSync(messageWithoutDefinedPlaceholder);
|
||||||
|
LocalizationsGenerator(
|
||||||
expect(
|
fileSystem: fs,
|
||||||
() {
|
inputPathString: defaultL10nPathString,
|
||||||
LocalizationsGenerator(
|
outputPathString: defaultL10nPathString,
|
||||||
fileSystem: fs,
|
templateArbFileName: defaultTemplateArbFileName,
|
||||||
inputPathString: defaultL10nPathString,
|
outputFileString: defaultOutputFileString,
|
||||||
outputPathString: defaultL10nPathString,
|
classNameString: defaultClassNameString,
|
||||||
templateArbFileName: defaultTemplateArbFileName,
|
logger: logger,
|
||||||
outputFileString: defaultOutputFileString,
|
)
|
||||||
classNameString: defaultClassNameString,
|
..loadResources()
|
||||||
logger: logger,
|
..writeOutputFiles();
|
||||||
)
|
final String localizationsFile = fs.file(
|
||||||
..loadResources()
|
fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.dart'),
|
||||||
..writeOutputFiles();
|
).readAsStringSync();
|
||||||
},
|
expect(localizationsFile, contains('String helloWorld(Object name) {'));
|
||||||
throwsA(isA<L10nException>().having(
|
|
||||||
(L10nException e) => e.message,
|
|
||||||
'message',
|
|
||||||
contains('''
|
|
||||||
Make sure that the specified placeholder is defined in your arb file.
|
|
||||||
[app_en.arb:helloWorld] Hello {name}
|
|
||||||
^'''),
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1909,7 +1927,37 @@ Make sure that the specified placeholder is defined in your arb file.
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('plural messages', () {
|
group('plural messages', () {
|
||||||
testWithoutContext('should throw attempting to generate a plural message without placeholders', () {
|
testWithoutContext('warnings are generated when plural parts are repeated', () {
|
||||||
|
const String pluralMessageWithOverriddenParts = '''
|
||||||
|
{
|
||||||
|
"helloWorlds": "{count,plural, =0{Hello}zero{hello} other{hi}}",
|
||||||
|
"@helloWorlds": {
|
||||||
|
"description": "Properly formatted but has redundant zero cases."
|
||||||
|
}
|
||||||
|
}''';
|
||||||
|
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
|
||||||
|
..createSync(recursive: true);
|
||||||
|
l10nDirectory.childFile(defaultTemplateArbFileName)
|
||||||
|
.writeAsStringSync(pluralMessageWithOverriddenParts);
|
||||||
|
LocalizationsGenerator(
|
||||||
|
fileSystem: fs,
|
||||||
|
inputPathString: defaultL10nPathString,
|
||||||
|
outputPathString: defaultL10nPathString,
|
||||||
|
templateArbFileName: defaultTemplateArbFileName,
|
||||||
|
outputFileString: defaultOutputFileString,
|
||||||
|
classNameString: defaultClassNameString,
|
||||||
|
logger: logger,
|
||||||
|
)
|
||||||
|
..loadResources()
|
||||||
|
..writeOutputFiles();
|
||||||
|
expect(logger.hadWarningOutput, isTrue);
|
||||||
|
expect(logger.warningText, contains('''
|
||||||
|
[app_en.arb:helloWorlds] ICU Syntax Warning: The plural part specified below is overridden by a later plural part.
|
||||||
|
{count,plural, =0{Hello}zero{hello} other{hi}}
|
||||||
|
^'''));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('should automatically infer plural placeholders that are not explicitly defined', () {
|
||||||
const String pluralMessageWithoutPlaceholdersAttribute = '''
|
const String pluralMessageWithoutPlaceholdersAttribute = '''
|
||||||
{
|
{
|
||||||
"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": "{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}}",
|
||||||
@ -1922,106 +1970,21 @@ Make sure that the specified placeholder is defined in your arb file.
|
|||||||
..createSync(recursive: true);
|
..createSync(recursive: true);
|
||||||
l10nDirectory.childFile(defaultTemplateArbFileName)
|
l10nDirectory.childFile(defaultTemplateArbFileName)
|
||||||
.writeAsStringSync(pluralMessageWithoutPlaceholdersAttribute);
|
.writeAsStringSync(pluralMessageWithoutPlaceholdersAttribute);
|
||||||
|
LocalizationsGenerator(
|
||||||
expect(
|
fileSystem: fs,
|
||||||
() {
|
inputPathString: defaultL10nPathString,
|
||||||
LocalizationsGenerator(
|
outputPathString: defaultL10nPathString,
|
||||||
fileSystem: fs,
|
templateArbFileName: defaultTemplateArbFileName,
|
||||||
inputPathString: defaultL10nPathString,
|
outputFileString: defaultOutputFileString,
|
||||||
outputPathString: defaultL10nPathString,
|
classNameString: defaultClassNameString,
|
||||||
templateArbFileName: defaultTemplateArbFileName,
|
logger: logger,
|
||||||
outputFileString: defaultOutputFileString,
|
)
|
||||||
classNameString: defaultClassNameString,
|
..loadResources()
|
||||||
logger: logger,
|
..writeOutputFiles();
|
||||||
)
|
final String localizationsFile = fs.file(
|
||||||
..loadResources()
|
fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.dart'),
|
||||||
..writeOutputFiles();
|
).readAsStringSync();
|
||||||
},
|
expect(localizationsFile, contains('String helloWorlds(num count) {'));
|
||||||
throwsA(isA<L10nException>().having(
|
|
||||||
(L10nException e) => e.message,
|
|
||||||
'message',
|
|
||||||
contains('''
|
|
||||||
Make sure that the specified plural placeholder is defined in your arb file.
|
|
||||||
[app_en.arb: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}}
|
|
||||||
^'''),
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWithoutContext('should throw attempting to generate a plural message with an empty placeholders map', () {
|
|
||||||
const String pluralMessageWithEmptyPlaceholdersMap = '''
|
|
||||||
{
|
|
||||||
"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": {
|
|
||||||
"description": "Improperly formatted since it has no placeholder attribute.",
|
|
||||||
"placeholders": {}
|
|
||||||
}
|
|
||||||
}''';
|
|
||||||
|
|
||||||
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
|
|
||||||
..createSync(recursive: true);
|
|
||||||
l10nDirectory.childFile(defaultTemplateArbFileName)
|
|
||||||
.writeAsStringSync(pluralMessageWithEmptyPlaceholdersMap);
|
|
||||||
|
|
||||||
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 plural placeholder is defined in your arb file.
|
|
||||||
[app_en.arb: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}}
|
|
||||||
^'''),
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWithoutContext('should throw attempting to generate a plural message with no resource attributes', () {
|
|
||||||
const String pluralMessageWithoutResourceAttributes = '''
|
|
||||||
{
|
|
||||||
"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}}"
|
|
||||||
}''';
|
|
||||||
|
|
||||||
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
|
|
||||||
..createSync(recursive: true);
|
|
||||||
l10nDirectory.childFile(defaultTemplateArbFileName)
|
|
||||||
.writeAsStringSync(pluralMessageWithoutResourceAttributes);
|
|
||||||
|
|
||||||
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 plural placeholder is defined in your arb file.
|
|
||||||
[app_en.arb: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}}
|
|
||||||
^'''),
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWithoutContext('should throw attempting to generate a plural message with incorrect format for placeholders', () {
|
testWithoutContext('should throw attempting to generate a plural message with incorrect format for placeholders', () {
|
||||||
@ -2065,7 +2028,7 @@ Make sure that the specified plural placeholder is defined in your arb file.
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('select messages', () {
|
group('select messages', () {
|
||||||
testWithoutContext('should throw attempting to generate a select message without placeholders', () {
|
testWithoutContext('should auotmatically infer select placeholders that are not explicitly defined', () {
|
||||||
const String selectMessageWithoutPlaceholdersAttribute = '''
|
const String selectMessageWithoutPlaceholdersAttribute = '''
|
||||||
{
|
{
|
||||||
"genderSelect": "{gender, select, female {She} male {He} other {they} }",
|
"genderSelect": "{gender, select, female {She} male {He} other {they} }",
|
||||||
@ -2078,106 +2041,21 @@ Make sure that the specified plural placeholder is defined in your arb file.
|
|||||||
..createSync(recursive: true);
|
..createSync(recursive: true);
|
||||||
l10nDirectory.childFile(defaultTemplateArbFileName)
|
l10nDirectory.childFile(defaultTemplateArbFileName)
|
||||||
.writeAsStringSync(selectMessageWithoutPlaceholdersAttribute);
|
.writeAsStringSync(selectMessageWithoutPlaceholdersAttribute);
|
||||||
|
LocalizationsGenerator(
|
||||||
expect(
|
fileSystem: fs,
|
||||||
() {
|
inputPathString: defaultL10nPathString,
|
||||||
LocalizationsGenerator(
|
outputPathString: defaultL10nPathString,
|
||||||
fileSystem: fs,
|
templateArbFileName: defaultTemplateArbFileName,
|
||||||
inputPathString: defaultL10nPathString,
|
outputFileString: defaultOutputFileString,
|
||||||
outputPathString: defaultL10nPathString,
|
classNameString: defaultClassNameString,
|
||||||
templateArbFileName: defaultTemplateArbFileName,
|
logger: logger,
|
||||||
outputFileString: defaultOutputFileString,
|
)
|
||||||
classNameString: defaultClassNameString,
|
..loadResources()
|
||||||
logger: logger,
|
..writeOutputFiles();
|
||||||
)
|
final String localizationsFile = fs.file(
|
||||||
..loadResources()
|
fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.dart'),
|
||||||
..writeOutputFiles();
|
).readAsStringSync();
|
||||||
},
|
expect(localizationsFile, contains('String genderSelect(String gender) {'));
|
||||||
throwsA(isA<L10nException>().having(
|
|
||||||
(L10nException e) => e.message,
|
|
||||||
'message',
|
|
||||||
contains('''
|
|
||||||
Make sure that the specified select placeholder is defined in your arb file.
|
|
||||||
[app_en.arb:genderSelect] {gender, select, female {She} male {He} other {they} }
|
|
||||||
^'''),
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWithoutContext('should throw attempting to generate a select message with an empty placeholders map', () {
|
|
||||||
const String selectMessageWithEmptyPlaceholdersMap = '''
|
|
||||||
{
|
|
||||||
"genderSelect": "{gender, select, female {She} male {He} other {they} }",
|
|
||||||
"@genderSelect": {
|
|
||||||
"description": "Improperly formatted since it has no placeholder attribute.",
|
|
||||||
"placeholders": {}
|
|
||||||
}
|
|
||||||
}''';
|
|
||||||
|
|
||||||
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
|
|
||||||
..createSync(recursive: true);
|
|
||||||
l10nDirectory.childFile(defaultTemplateArbFileName)
|
|
||||||
.writeAsStringSync(selectMessageWithEmptyPlaceholdersMap);
|
|
||||||
|
|
||||||
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 select placeholder is defined in your arb file.
|
|
||||||
[app_en.arb:genderSelect] {gender, select, female {She} male {He} other {they} }
|
|
||||||
^'''),
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWithoutContext('should throw attempting to generate a select message with no resource attributes', () {
|
|
||||||
const String selectMessageWithoutResourceAttributes = '''
|
|
||||||
{
|
|
||||||
"genderSelect": "{gender, select, female {She} male {He} other {they} }"
|
|
||||||
}''';
|
|
||||||
|
|
||||||
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
|
|
||||||
..createSync(recursive: true);
|
|
||||||
l10nDirectory.childFile(defaultTemplateArbFileName)
|
|
||||||
.writeAsStringSync(selectMessageWithoutResourceAttributes);
|
|
||||||
|
|
||||||
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 select placeholder is defined in your arb file.
|
|
||||||
[app_en.arb:genderSelect] {gender, select, female {She} male {He} other {they} }
|
|
||||||
^'''),
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWithoutContext('should throw attempting to generate a select message with incorrect format for placeholders', () {
|
testWithoutContext('should throw attempting to generate a select message with incorrect format for placeholders', () {
|
||||||
@ -2234,30 +2112,25 @@ Make sure that the specified select placeholder is defined in your arb file.
|
|||||||
..createSync(recursive: true);
|
..createSync(recursive: true);
|
||||||
l10nDirectory.childFile(defaultTemplateArbFileName)
|
l10nDirectory.childFile(defaultTemplateArbFileName)
|
||||||
.writeAsStringSync(selectMessageWithoutPlaceholdersAttribute);
|
.writeAsStringSync(selectMessageWithoutPlaceholdersAttribute);
|
||||||
|
try {
|
||||||
expect(
|
LocalizationsGenerator(
|
||||||
() {
|
fileSystem: fs,
|
||||||
LocalizationsGenerator(
|
inputPathString: defaultL10nPathString,
|
||||||
fileSystem: fs,
|
outputPathString: defaultL10nPathString,
|
||||||
inputPathString: defaultL10nPathString,
|
templateArbFileName: defaultTemplateArbFileName,
|
||||||
outputPathString: defaultL10nPathString,
|
outputFileString: defaultOutputFileString,
|
||||||
templateArbFileName: defaultTemplateArbFileName,
|
classNameString: defaultClassNameString,
|
||||||
outputFileString: defaultOutputFileString,
|
logger: logger,
|
||||||
classNameString: defaultClassNameString,
|
)
|
||||||
logger: logger,
|
..loadResources()
|
||||||
)
|
..writeOutputFiles();
|
||||||
..loadResources()
|
} on L10nException {
|
||||||
..writeOutputFiles();
|
expect(logger.errorText, contains('''
|
||||||
},
|
[app_en.arb:genderSelect] ICU Syntax Error: Select expressions must have an "other" case.
|
||||||
throwsA(isA<L10nException>().having(
|
{gender, select,}
|
||||||
(L10nException e) => e.message,
|
^''')
|
||||||
'message',
|
);
|
||||||
contains('''
|
}
|
||||||
Select expressions must have an "other" case.
|
|
||||||
[app_en.arb:genderSelect] {gender, select,}
|
|
||||||
^'''),
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2984,37 +2857,66 @@ AppLocalizations lookupAppLocalizations(Locale locale) {
|
|||||||
'''));
|
'''));
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(thkim1011): Uncomment when implementing escaping.
|
testWithoutContext('escaping with single quotes', () {
|
||||||
// See https://github.com/flutter/flutter/issues/113455.
|
const String arbFile = '''
|
||||||
// testWithoutContext('escaping with single quotes', () {
|
{
|
||||||
// const String arbFile = '''
|
"singleQuote": "Flutter''s amazing!",
|
||||||
// {
|
"@singleQuote": {
|
||||||
// "singleQuote": "Flutter''s amazing!",
|
"description": "A message with a single quote."
|
||||||
// "@singleQuote": {
|
}
|
||||||
// "description": "A message with a single quote."
|
}''';
|
||||||
// }
|
|
||||||
// }''';
|
|
||||||
|
|
||||||
// final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
|
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
|
||||||
// ..createSync(recursive: true);
|
..createSync(recursive: true);
|
||||||
// l10nDirectory.childFile(defaultTemplateArbFileName)
|
l10nDirectory.childFile(defaultTemplateArbFileName)
|
||||||
// .writeAsStringSync(arbFile);
|
.writeAsStringSync(arbFile);
|
||||||
|
|
||||||
// LocalizationsGenerator(
|
LocalizationsGenerator(
|
||||||
// fileSystem: fs,
|
fileSystem: fs,
|
||||||
// inputPathString: defaultL10nPathString,
|
inputPathString: defaultL10nPathString,
|
||||||
// outputPathString: defaultL10nPathString,
|
outputPathString: defaultL10nPathString,
|
||||||
// templateArbFileName: defaultTemplateArbFileName,
|
templateArbFileName: defaultTemplateArbFileName,
|
||||||
// outputFileString: defaultOutputFileString,
|
outputFileString: defaultOutputFileString,
|
||||||
// classNameString: defaultClassNameString,
|
classNameString: defaultClassNameString,
|
||||||
// logger: logger,
|
logger: logger,
|
||||||
// )
|
useEscaping: true,
|
||||||
// ..loadResources()
|
)
|
||||||
// ..writeOutputFiles();
|
..loadResources()
|
||||||
|
..writeOutputFiles();
|
||||||
|
|
||||||
// final String localizationsFile = fs.file(
|
final String localizationsFile = fs.file(
|
||||||
// fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.dart'),
|
fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.dart'),
|
||||||
// ).readAsStringSync();
|
).readAsStringSync();
|
||||||
// expect(localizationsFile, contains(r"Flutter\'s amazing"));
|
expect(localizationsFile, contains(r"Flutter\'s amazing"));
|
||||||
// });
|
});
|
||||||
|
|
||||||
|
testWithoutContext('suppress warnings flag actually suppresses warnings', () {
|
||||||
|
const String pluralMessageWithOverriddenParts = '''
|
||||||
|
{
|
||||||
|
"helloWorlds": "{count,plural, =0{Hello}zero{hello} other{hi}}",
|
||||||
|
"@helloWorlds": {
|
||||||
|
"description": "Properly formatted but has redundant zero cases.",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}''';
|
||||||
|
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
|
||||||
|
..createSync(recursive: true);
|
||||||
|
l10nDirectory.childFile(defaultTemplateArbFileName)
|
||||||
|
.writeAsStringSync(pluralMessageWithOverriddenParts);
|
||||||
|
LocalizationsGenerator(
|
||||||
|
fileSystem: fs,
|
||||||
|
inputPathString: defaultL10nPathString,
|
||||||
|
outputPathString: defaultL10nPathString,
|
||||||
|
templateArbFileName: defaultTemplateArbFileName,
|
||||||
|
outputFileString: defaultOutputFileString,
|
||||||
|
classNameString: defaultClassNameString,
|
||||||
|
logger: logger,
|
||||||
|
suppressWarnings: true,
|
||||||
|
)
|
||||||
|
..loadResources()
|
||||||
|
..writeOutputFiles();
|
||||||
|
expect(logger.hadWarningOutput, isFalse);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -218,6 +218,14 @@ void main() {
|
|||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWithoutContext('lexer identifier names can be "select" or "plural"', () {
|
||||||
|
final List<Node> tokens = Parser('keywords', 'app_en.arb', '{ select } { plural, select, singular{test} other{hmm} }').lexIntoTokens();
|
||||||
|
expect(tokens[1].value, equals('select'));
|
||||||
|
expect(tokens[1].type, equals(ST.identifier));
|
||||||
|
expect(tokens[5].value, equals('plural'));
|
||||||
|
expect(tokens[5].type, equals(ST.identifier));
|
||||||
|
});
|
||||||
|
|
||||||
testWithoutContext('lexer: lexically correct but syntactically incorrect', () {
|
testWithoutContext('lexer: lexically correct but syntactically incorrect', () {
|
||||||
final List<Node> tokens = Parser(
|
final List<Node> tokens = Parser(
|
||||||
'syntax',
|
'syntax',
|
||||||
@ -242,9 +250,9 @@ void main() {
|
|||||||
testWithoutContext('lexer unmatched single quote', () {
|
testWithoutContext('lexer unmatched single quote', () {
|
||||||
const String message = "here''s an unmatched single quote: '";
|
const String message = "here''s an unmatched single quote: '";
|
||||||
const String expectedError = '''
|
const String expectedError = '''
|
||||||
ICU Lexing Error: Unmatched single quotes.
|
[app_en.arb:escaping] ICU Lexing Error: Unmatched single quotes.
|
||||||
[app_en.arb:escaping] here''s an unmatched single quote: '
|
here''s an unmatched single quote: '
|
||||||
^''';
|
^''';
|
||||||
expect(
|
expect(
|
||||||
() => Parser('escaping', 'app_en.arb', message, useEscaping: true).lexIntoTokens(),
|
() => Parser('escaping', 'app_en.arb', message, useEscaping: true).lexIntoTokens(),
|
||||||
throwsA(isA<L10nException>().having(
|
throwsA(isA<L10nException>().having(
|
||||||
@ -257,9 +265,9 @@ ICU Lexing Error: Unmatched single quotes.
|
|||||||
testWithoutContext('lexer unexpected character', () {
|
testWithoutContext('lexer unexpected character', () {
|
||||||
const String message = '{ * }';
|
const String message = '{ * }';
|
||||||
const String expectedError = '''
|
const String expectedError = '''
|
||||||
ICU Lexing Error: Unexpected character.
|
[app_en.arb:lex] ICU Lexing Error: Unexpected character.
|
||||||
[app_en.arb:lex] { * }
|
{ * }
|
||||||
^''';
|
^''';
|
||||||
expect(
|
expect(
|
||||||
() => Parser('lex', 'app_en.arb', message).lexIntoTokens(),
|
() => Parser('lex', 'app_en.arb', message).lexIntoTokens(),
|
||||||
throwsA(isA<L10nException>().having(
|
throwsA(isA<L10nException>().having(
|
||||||
@ -460,11 +468,11 @@ ICU Lexing Error: Unexpected character.
|
|||||||
testWithoutContext('parser unexpected token', () {
|
testWithoutContext('parser unexpected token', () {
|
||||||
// unexpected token
|
// unexpected token
|
||||||
const String expectedError1 = '''
|
const String expectedError1 = '''
|
||||||
ICU Syntax Error: Expected "}" but found "=".
|
[app_en.arb:unexpectedToken] ICU Syntax Error: Expected "}" but found "=".
|
||||||
[app_en.arb:unexpectedToken] { placeholder =
|
{ placeholder =
|
||||||
^''';
|
^''';
|
||||||
expect(
|
expect(
|
||||||
() => Parser('unexpectedToken', 'app_en.arb', '{ placeholder =').parse(),
|
() => Parser('unexpectedToken', 'app_en.arb', '{ placeholder =').parseIntoTree(),
|
||||||
throwsA(isA<L10nException>().having(
|
throwsA(isA<L10nException>().having(
|
||||||
(L10nException e) => e.message,
|
(L10nException e) => e.message,
|
||||||
'message',
|
'message',
|
||||||
@ -472,11 +480,11 @@ ICU Syntax Error: Expected "}" but found "=".
|
|||||||
)));
|
)));
|
||||||
|
|
||||||
const String expectedError2 = '''
|
const String expectedError2 = '''
|
||||||
ICU Syntax Error: Expected "number" but found "}".
|
[app_en.arb:unexpectedToken] ICU Syntax Error: Expected "number" but found "}".
|
||||||
[app_en.arb:unexpectedToken] { count, plural, = }
|
{ count, plural, = }
|
||||||
^''';
|
^''';
|
||||||
expect(
|
expect(
|
||||||
() => Parser('unexpectedToken', 'app_en.arb', '{ count, plural, = }').parse(),
|
() => Parser('unexpectedToken', 'app_en.arb', '{ count, plural, = }').parseIntoTree(),
|
||||||
throwsA(isA<L10nException>().having(
|
throwsA(isA<L10nException>().having(
|
||||||
(L10nException e) => e.message,
|
(L10nException e) => e.message,
|
||||||
'message',
|
'message',
|
||||||
@ -484,11 +492,11 @@ ICU Syntax Error: Expected "number" but found "}".
|
|||||||
)));
|
)));
|
||||||
|
|
||||||
const String expectedError3 = '''
|
const String expectedError3 = '''
|
||||||
ICU Syntax Error: Expected "identifier" but found ",".
|
[app_en.arb:unexpectedToken] ICU Syntax Error: Expected "identifier" but found ",".
|
||||||
[app_en.arb:unexpectedToken] { , plural , = }
|
{ , plural , = }
|
||||||
^''';
|
^''';
|
||||||
expect(
|
expect(
|
||||||
() => Parser('unexpectedToken', 'app_en.arb', '{ , plural , = }').parse(),
|
() => Parser('unexpectedToken', 'app_en.arb', '{ , plural , = }').parseIntoTree(),
|
||||||
throwsA(isA<L10nException>().having(
|
throwsA(isA<L10nException>().having(
|
||||||
(L10nException e) => e.message,
|
(L10nException e) => e.message,
|
||||||
'message',
|
'message',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user