Unverified Commit 8be335f2 authored by Tae Hyung Kim's avatar Tae Hyung Kim Committed by GitHub

Handle dollar signs properly when generating localizations (#125514)

Currently, the code doesn't properly handle strings which contain dollar signs. The return expression for the generated localization function is computed by `generateReturnExpr` which concatenates several strings, which are either interpolated placeholders, interpolated function calls, or normal strings, but we didn't properly escape dollar signs before sending normal strings to `generateReturnExpr`.

Fixes #125461.
parent 9a28f56e
...@@ -1131,7 +1131,7 @@ class LocalizationsGenerator { ...@@ -1131,7 +1131,7 @@ class LocalizationsGenerator {
case ST.message: case ST.message:
final List<String> expressions = node.children.map<String>((Node node) { final List<String> expressions = node.children.map<String>((Node node) {
if (node.type == ST.string) { if (node.type == ST.string) {
return node.value!; return generateString(node.value!);
} }
return generateVariables(node); return generateVariables(node);
}).toList(); }).toList();
......
...@@ -291,10 +291,25 @@ String generateString(String value) { ...@@ -291,10 +291,25 @@ String generateString(String value) {
return value; return value;
} }
/// Given a list of strings, placeholders, or helper function calls, concatenate /// Given a list of normal strings or interpolated variables, concatenate them
/// them into one expression to be returned. /// into a single dart string to be returned. An example of a normal string
/// would be "'Hello world!'" and an example of a interpolated variable would be
/// "'$placeholder'".
/// ///
/// If `isSingleStringVar` is passed, then we want to convert "'$expr'" to "expr". /// Each of the strings in [expressions] should be a raw string, which, if it
/// were to be added to a dart file, would be a properly formatted dart string
/// with escapes and/or interpolation. The purpose of this function is to
/// concatenate these dart strings into a single dart string which can be
/// returned in the generated localization files.
///
/// The following rules describe the kinds of string expressions that can be
/// handled:
/// 1. If [expressions] is empty, return the empty string "''".
/// 2. If [expressions] has only one [String] which is an interpolated variable,
/// it is converted to the variable itself e.g. ["'$expr'"] -> "expr".
/// 3. If one string in [expressions] is an interpolation and the next begins
/// with an alphanumeric character, then the former interpolation should be
/// wrapped in braces e.g. ["'$expr1'", "'another'"] -> "'${expr1}another'".
String generateReturnExpr(List<String> expressions, { bool isSingleStringVar = false }) { String generateReturnExpr(List<String> expressions, { bool isSingleStringVar = false }) {
if (expressions.isEmpty) { if (expressions.isEmpty) {
return "''"; return "''";
...@@ -304,7 +319,7 @@ String generateReturnExpr(List<String> expressions, { bool isSingleStringVar = f ...@@ -304,7 +319,7 @@ String generateReturnExpr(List<String> expressions, { bool isSingleStringVar = f
} else { } else {
final String string = expressions.reversed.fold<String>('', (String string, String expression) { final String string = expressions.reversed.fold<String>('', (String string, String expression) {
if (expression[0] != r'$') { if (expression[0] != r'$') {
return generateString(expression) + string; return expression + string;
} }
final RegExp alphanumeric = RegExp(r'^([0-9a-zA-Z]|_)+$'); final RegExp alphanumeric = RegExp(r'^([0-9a-zA-Z]|_)+$');
if (alphanumeric.hasMatch(expression.substring(1)) && !(string.isNotEmpty && alphanumeric.hasMatch(string[0]))) { if (alphanumeric.hasMatch(expression.substring(1)) && !(string.isNotEmpty && alphanumeric.hasMatch(string[0]))) {
......
...@@ -3244,4 +3244,32 @@ NumberFormat.decimalPatternDigits( ...@@ -3244,4 +3244,32 @@ NumberFormat.decimalPatternDigits(
); );
''')); '''));
}); });
// Regression test for https://github.com/flutter/flutter/issues/125461.
testWithoutContext('dollar signs are escaped properly when there is a select clause', () {
const String dollarSignWithSelect = r'''
{
"dollarSignWithSelect": "$nice_bug\nHello Bug! Manifistation #1 {selectPlaceholder, select, case{message} other{messageOther}}"
}''';
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
..createSync(recursive: true);
l10nDirectory.childFile(defaultTemplateArbFileName)
.writeAsStringSync(dollarSignWithSelect);
LocalizationsGenerator(
fileSystem: fs,
inputPathString: defaultL10nPathString,
outputPathString: defaultL10nPathString,
templateArbFileName: defaultTemplateArbFileName,
outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString,
logger: logger,
suppressWarnings: true,
)
..loadResources()
..writeOutputFiles();
final String localizationsFile = fs.file(
fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.dart'),
).readAsStringSync();
expect(localizationsFile, contains(r'\$nice_bug\nHello Bug! Manifistation #1 $_temp0'));
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment