[gen-l10n] Remove need for ignoring two lints in generated code (#78778)
* Remove need for unused import for placeholder braces
* Remove need for unused intl import for when plurals aren't used in the generated code
diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart
index a65dc2e..3d26363 100644
--- a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart
+++ b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart
@@ -200,7 +200,7 @@
'=2': 'two',
'few': 'few',
'many': 'many',
- 'other': 'other'
+ 'other': 'other',
};
final List<String> pluralLogicArgs = <String>[];
@@ -211,9 +211,19 @@
String argValue = generateString(match.group(2));
for (final Placeholder placeholder in message.placeholders) {
if (placeholder != countPlaceholder && placeholder.requiresFormatting) {
- argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}String}');
+ argValue = argValue.replaceAll(
+ '#${placeholder.name}#',
+ _needsCurlyBracketStringInterpolation(argValue, placeholder.name)
+ ? '\${${placeholder.name}String}'
+ : '\$${placeholder.name}String'
+ );
} else {
- argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}}');
+ argValue = argValue.replaceAll(
+ '#${placeholder.name}#',
+ _needsCurlyBracketStringInterpolation(argValue, placeholder.name)
+ ? '\${${placeholder.name}}'
+ : '\$${placeholder.name}'
+ );
}
}
pluralLogicArgs.add(' ${pluralIds[pluralKey]}: $argValue');
@@ -238,14 +248,61 @@
.replaceAll('@(none)\n', '');
}
+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, AppResourceBundle bundle) {
String generateMessage() {
String messageValue = generateString(bundle.translationFor(message));
for (final Placeholder placeholder in message.placeholders) {
if (placeholder.requiresFormatting) {
- messageValue = messageValue.replaceAll('{${placeholder.name}}', '\${${placeholder.name}String}');
+ messageValue = messageValue.replaceAll(
+ '{${placeholder.name}}',
+ _needsCurlyBracketStringInterpolation(messageValue, placeholder.name)
+ ? '\${${placeholder.name}String}'
+ : '\$${placeholder.name}String'
+ );
} else {
- messageValue = messageValue.replaceAll('{${placeholder.name}}', '\${${placeholder.name}}');
+ messageValue = messageValue.replaceAll(
+ '{${placeholder.name}}',
+ _needsCurlyBracketStringInterpolation(messageValue, placeholder.name)
+ ? '\${${placeholder.name}}'
+ : '\$${placeholder.name}'
+ );
}
}
@@ -964,7 +1021,8 @@
.replaceAll('@(fileName)', fileName)
.replaceAll('@(class)', '$className${locale.camelCase()}')
.replaceAll('@(localeName)', locale.toString())
- .replaceAll('@(methods)', methods.join('\n\n'));
+ .replaceAll('@(methods)', methods.join('\n\n'))
+ .replaceAll('@(requiresIntlImport)', _containsPluralMessage() ? "import 'package:intl/intl.dart' as intl;" : '');
}
String _generateSubclass(
@@ -1103,9 +1161,12 @@
.replaceAll('@(supportedLocales)', supportedLocalesCode.join(',\n '))
.replaceAll('@(supportedLanguageCodes)', supportedLanguageCodes.join(', '))
.replaceAll('@(messageClassImports)', sortedClassImports.join('\n'))
- .replaceAll('@(delegateClass)', delegateClass);
+ .replaceAll('@(delegateClass)', delegateClass)
+ .replaceAll('@(requiresIntlImport)', _containsPluralMessage() ? "import 'package:intl/intl.dart' as intl;" : '');
}
+ bool _containsPluralMessage() => _allMessages.any((Message message) => message.isPlural);
+
void writeOutputFiles(Logger logger, { bool isFromYaml = false }) {
// First, generate the string contents of all necessary files.
_generateCode();
diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart
index 5169ef9..01f2c75 100644
--- a/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart
+++ b/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart
@@ -160,11 +160,9 @@
const String classFileTemplate = '''
@(header)
-// ignore: unused_import
-import 'package:intl/intl.dart' as intl;
-import '@(fileName)';
-// ignore_for_file: unnecessary_brace_in_string_interps
+@(requiresIntlImport)
+import '@(fileName)';
/// The translations for @(language) (`@(localeName)`).
class @(class) extends @(baseClass) {
diff --git a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart
index f0e9453..15515c5 100644
--- a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart
+++ b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart
@@ -56,6 +56,9 @@
{
"title": "标题"
}''';
+const String intlImportDartCode = '''
+import 'package:intl/intl.dart' as intl;
+''';
void _standardFlutterDirectoryL10nSetup(FileSystem fs) {
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
@@ -1728,6 +1731,281 @@
});
});
+ test('intl package import should be omitted in subclass files when no plurals are included', () {
+ fs.currentDirectory.childDirectory('lib').childDirectory('l10n')..createSync(recursive: true)
+ ..childFile(defaultTemplateArbFileName).writeAsStringSync(singleMessageArbFileString)
+ ..childFile('app_es.arb').writeAsStringSync(singleEsMessageArbFileString);
+
+ final LocalizationsGenerator generator = LocalizationsGenerator(fs);
+ try {
+ generator.initialize(
+ inputPathString: defaultL10nPathString,
+ outputPathString: defaultL10nPathString,
+ templateArbFileName: defaultTemplateArbFileName,
+ outputFileString: defaultOutputFileString,
+ classNameString: defaultClassNameString,
+ );
+ generator.loadResources();
+ generator.writeOutputFiles(BufferLogger.test());
+ } on Exception catch (e) {
+ fail('Generating output files should not fail: $e');
+ }
+
+ final String localizationsFile = fs.file(
+ fs.path.join(syntheticL10nPackagePath, 'output-localization-file_es.dart'),
+ ).readAsStringSync();
+ expect(localizationsFile, isNot(contains(intlImportDartCode)));
+ });
+
+ test('intl package import should be kept in subclass files when plurals are included', () {
+ const String pluralMessageArb = '''
+{
+ "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": "A plural message",
+ "placeholders": {
+ "count": {}
+ }
+ }
+}
+''';
+
+ const String pluralMessageEsArb = '''
+{
+ "helloWorlds": "{count,plural, =0{ES - Hello} =1{ES - Hello World} =2{ES - Hello two worlds} few{ES - Hello {count} worlds} many{ES - Hello all {count} worlds} other{ES - Hello other {count} worlds}}"
+}
+''';
+
+ fs.currentDirectory.childDirectory('lib').childDirectory('l10n')..createSync(recursive: true)
+ ..childFile(defaultTemplateArbFileName).writeAsStringSync(pluralMessageArb)
+ ..childFile('app_es.arb').writeAsStringSync(pluralMessageEsArb);
+
+ final LocalizationsGenerator generator = LocalizationsGenerator(fs);
+ try {
+ generator.initialize(
+ inputPathString: defaultL10nPathString,
+ outputPathString: defaultL10nPathString,
+ templateArbFileName: defaultTemplateArbFileName,
+ outputFileString: defaultOutputFileString,
+ classNameString: defaultClassNameString,
+ );
+ generator.loadResources();
+ generator.writeOutputFiles(BufferLogger.test());
+ } on Exception catch (e) {
+ fail('Generating output files should not fail: $e');
+ }
+
+ final String localizationsFile = fs.file(
+ fs.path.join(syntheticL10nPackagePath, 'output-localization-file_es.dart'),
+ ).readAsStringSync();
+ expect(localizationsFile, contains(intlImportDartCode));
+ });
+
+ test('check for string interpolation rules', () {
+ const String enArbCheckList = '''
+{
+ "one": "The number of {one} elapsed is: 44",
+ "@one": {
+ "description": "test one",
+ "placeholders": {
+ "one": {
+ "type": "String"
+ }
+ }
+ },
+ "two": "哈{two}哈",
+ "@two": {
+ "description": "test two",
+ "placeholders": {
+ "two": {
+ "type": "String"
+ }
+ }
+ },
+ "three": "m{three}m",
+ "@three": {
+ "description": "test three",
+ "placeholders": {
+ "three": {
+ "type": "String"
+ }
+ }
+ },
+ "four": "I have to work _{four}_ sometimes.",
+ "@four": {
+ "description": "test four",
+ "placeholders": {
+ "four": {
+ "type": "String"
+ }
+ }
+ },
+ "five": "{five} elapsed.",
+ "@five": {
+ "description": "test five",
+ "placeholders": {
+ "five": {
+ "type": "String"
+ }
+ }
+ },
+ "six": "{six}m",
+ "@six": {
+ "description": "test six",
+ "placeholders": {
+ "six": {
+ "type": "String"
+ }
+ }
+ },
+ "seven": "hours elapsed: {seven}",
+ "@seven": {
+ "description": "test seven",
+ "placeholders": {
+ "seven": {
+ "type": "String"
+ }
+ }
+ },
+ "eight": " {eight}",
+ "@eight": {
+ "description": "test eight",
+ "placeholders": {
+ "eight": {
+ "type": "String"
+ }
+ }
+ },
+ "nine": "m{nine}",
+ "@nine": {
+ "description": "test nine",
+ "placeholders": {
+ "nine": {
+ "type": "String"
+ }
+ }
+ }
+}
+''';
+
+ // It's fine that the arb is identical -- Just checking
+ // generated code for use of '${variable}' vs '$variable'
+ const String esArbCheckList = '''
+{
+ "one": "The number of {one} elapsed is: 44",
+ "two": "哈{two}哈",
+ "three": "m{three}m",
+ "four": "I have to work _{four}_ sometimes.",
+ "five": "{five} elapsed.",
+ "six": "{six}m",
+ "seven": "hours elapsed: {seven}",
+ "eight": " {eight}",
+ "nine": "m{nine}"
+}
+''';
+
+ fs.currentDirectory.childDirectory('lib').childDirectory('l10n')..createSync(recursive: true)
+ ..childFile(defaultTemplateArbFileName).writeAsStringSync(enArbCheckList)
+ ..childFile('app_es.arb').writeAsStringSync(esArbCheckList);
+
+ final LocalizationsGenerator generator = LocalizationsGenerator(fs);
+ try {
+ generator.initialize(
+ inputPathString: defaultL10nPathString,
+ outputPathString: defaultL10nPathString,
+ templateArbFileName: defaultTemplateArbFileName,
+ outputFileString: defaultOutputFileString,
+ classNameString: defaultClassNameString,
+ );
+ generator.loadResources();
+ generator.writeOutputFiles(BufferLogger.test());
+ } on Exception catch (e) {
+ if (e is L10nException) {
+ print(e.message);
+ }
+ fail('Generating output files should not fail: $e');
+ }
+
+ final String localizationsFile = fs.file(
+ fs.path.join(syntheticL10nPackagePath, 'output-localization-file_es.dart'),
+ ).readAsStringSync();
+
+ expect(localizationsFile, contains(r'$one'));
+ expect(localizationsFile, contains(r'$two'));
+ expect(localizationsFile, contains(r'${three}'));
+ expect(localizationsFile, contains(r'${four}'));
+ expect(localizationsFile, contains(r'$five'));
+ expect(localizationsFile, contains(r'${six}m'));
+ expect(localizationsFile, contains(r'$seven'));
+ expect(localizationsFile, contains(r'$eight'));
+ expect(localizationsFile, contains(r'${nine}'));
+ });
+
+ test('check for string interpolation rules - plurals', () {
+ 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": {
+ "description": "First set of plural messages to test.",
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "second": "{count,plural, =0{test {count}} other{ {count}}",
+ "@second": {
+ "description": "Second set of plural messages to test.",
+ "placeholders": {
+ "count": {}
+ }
+ }
+}
+''';
+
+ // It's fine that the arb is identical -- Just checking
+ // generated code for use of '${variable}' vs '$variable'
+ const String esArbCheckList = '''
+{
+ "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}}"
+}
+''';
+
+ fs.currentDirectory.childDirectory('lib').childDirectory('l10n')..createSync(recursive: true)
+ ..childFile(defaultTemplateArbFileName).writeAsStringSync(enArbCheckList)
+ ..childFile('app_es.arb').writeAsStringSync(esArbCheckList);
+
+ final LocalizationsGenerator generator = LocalizationsGenerator(fs);
+ try {
+ generator.initialize(
+ inputPathString: defaultL10nPathString,
+ outputPathString: defaultL10nPathString,
+ templateArbFileName: defaultTemplateArbFileName,
+ outputFileString: defaultOutputFileString,
+ classNameString: defaultClassNameString,
+ );
+ generator.loadResources();
+ generator.writeOutputFiles(BufferLogger.test());
+ } on Exception catch (e) {
+ if (e is L10nException) {
+ print(e.message);
+ }
+ fail('Generating output files should not fail: $e');
+ }
+
+ final String localizationsFile = fs.file(
+ fs.path.join(syntheticL10nPackagePath, 'output-localization-file_es.dart'),
+ ).readAsStringSync();
+
+ expect(localizationsFile, contains(r'test $count test'));
+ expect(localizationsFile, contains(r'哈$count哈'));
+ expect(localizationsFile, contains(r'm${count}m'));
+ expect(localizationsFile, contains(r'_${count}_'));
+ expect(localizationsFile, contains(r'$count test'));
+ expect(localizationsFile, contains(r'${count}m'));
+ expect(localizationsFile, contains(r'test $count'));
+ expect(localizationsFile, contains(r' $count'));
+ });
+
test(
'should throw with descriptive error message when failing to parse the '
'arb file',