diff --git a/dev/tools/localization/bin/encode_kn_arb_files.dart b/dev/tools/localization/bin/encode_kn_arb_files.dart index 6477a5de99..f974215c54 100644 --- a/dev/tools/localization/bin/encode_kn_arb_files.dart +++ b/dev/tools/localization/bin/encode_kn_arb_files.dart @@ -2,26 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// This program replaces the material_kn.arb and cupertino_kn.arb -// files in flutter_localizations/packages/lib/src/l10n with versions -// where the contents of the localized strings have been replaced by JSON -// escapes. This is done because some of those strings contain characters -// that can crash Emacs on Linux. There is more information +// The utility function `encodeKnArbFiles` replaces the material_kn.arb +// and cupertino_kn.arb files in flutter_localizations/packages/lib/src/l10n +// with versions where the contents of the localized strings have been +// replaced by JSON escapes. This is done because some of those strings +// contain characters that can crash Emacs on Linux. There is more information // here: https://github.com/flutter/flutter/issues/36704 and in the README // in flutter_localizations/packages/lib/src/l10n. // -// This app needs to be run by hand when material_kn.arb or cupertino_kn.arb -// have been updated. -// -// ## Usage -// -// Run this program from the root of the git repository. -// -// ``` -// dart dev/tools/localization/bin/encode_kn_arb_files.dart -// ``` +// This utility is run by `gen_localizations.dart` if --overwrite is passed +// in as an option. -import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -29,13 +20,13 @@ import 'package:path/path.dart' as path; import '../localizations_utils.dart'; -Map loadBundle(File file) { +Map _loadBundle(File file) { if (!FileSystemEntity.isFileSync(file.path)) exitWithError('Unable to find input file: ${file.path}'); return json.decode(file.readAsStringSync()) as Map; } -void encodeBundleTranslations(Map bundle) { +void _encodeBundleTranslations(Map bundle) { for (final String key in bundle.keys) { // The ARB file resource "attributes" for foo are called @foo. Don't need // to encode them. @@ -51,7 +42,7 @@ void encodeBundleTranslations(Map bundle) { } } -void checkEncodedTranslations(Map encodedBundle, Map bundle) { +void _checkEncodedTranslations(Map encodedBundle, Map bundle) { bool errorFound = false; const JsonDecoder decoder = JsonDecoder(); for (final String key in bundle.keys) { @@ -64,7 +55,7 @@ void checkEncodedTranslations(Map encodedBundle, Map bundle) { +void _rewriteBundle(File file, Map bundle) { final StringBuffer contents = StringBuffer(); contents.writeln('{'); for (final String key in bundle.keys) { @@ -74,22 +65,19 @@ void rewriteBundle(File file, Map bundle) { file.writeAsStringSync(contents.toString()); } -Future main(List rawArgs) async { - checkCwdIsRepoRoot('encode_kn_arb_files'); +void encodeKnArbFiles(Directory directory) { + final File materialArbFile = File(path.join(directory.path, 'material_kn.arb')); + final File cupertinoArbFile = File(path.join(directory.path, 'cupertino_kn.arb')); - final String l10nPath = path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'); - final File materialArbFile = File(path.join(l10nPath, 'material_kn.arb')); - final File cupertinoArbFile = File(path.join(l10nPath, 'cupertino_kn.arb')); + final Map materialBundle = _loadBundle(materialArbFile); + final Map cupertinoBundle = _loadBundle(cupertinoArbFile); - final Map materialBundle = loadBundle(materialArbFile); - final Map cupertinoBundle = loadBundle(cupertinoArbFile); + _encodeBundleTranslations(materialBundle); + _encodeBundleTranslations(cupertinoBundle); - encodeBundleTranslations(materialBundle); - encodeBundleTranslations(cupertinoBundle); + _checkEncodedTranslations(materialBundle, _loadBundle(materialArbFile)); + _checkEncodedTranslations(cupertinoBundle, _loadBundle(cupertinoArbFile)); - checkEncodedTranslations(materialBundle, loadBundle(materialArbFile)); - checkEncodedTranslations(cupertinoBundle, loadBundle(cupertinoArbFile)); - - rewriteBundle(materialArbFile, materialBundle); - rewriteBundle(cupertinoArbFile, cupertinoBundle); + _rewriteBundle(materialArbFile, materialBundle); + _rewriteBundle(cupertinoArbFile, cupertinoBundle); } diff --git a/dev/tools/localization/bin/gen_localizations.dart b/dev/tools/localization/bin/gen_localizations.dart index 7820b6c0ea..4bf9fc40a4 100644 --- a/dev/tools/localization/bin/gen_localizations.dart +++ b/dev/tools/localization/bin/gen_localizations.dart @@ -50,6 +50,8 @@ import '../gen_material_localizations.dart'; import '../localizations_utils.dart'; import '../localizations_validator.dart'; +import 'encode_kn_arb_files.dart'; + /// This is the core of this script; it generates the code used for translations. String generateArbBasedLocalizationSubclasses({ @required Map> localeToResources, @@ -526,6 +528,16 @@ void main(List rawArgs) { exitWithError('$exception'); } + // Only rewrite material_kn.arb and cupertino_en.arb if overwriting the + // Material and Cupertino localizations files. + if (options.writeToFile) { + // Encodes the material_kn.arb file and the cupertino_en.arb files before + // generating localizations. This prevents a subset of Emacs users from + // crashing when opening up the Flutter source code. + // See https://github.com/flutter/flutter/issues/36704 for more context. + encodeKnArbFiles(directory); + } + precacheLanguageAndRegionTags(); // Maps of locales to resource key/value pairs for Material ARBs. diff --git a/packages/flutter_localizations/test/cupertino/translations_test.dart b/packages/flutter_localizations/test/cupertino/translations_test.dart index be7a37cd78..a9d7d690b6 100644 --- a/packages/flutter_localizations/test/cupertino/translations_test.dart +++ b/packages/flutter_localizations/test/cupertino/translations_test.dart @@ -2,12 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; import 'dart:io'; +import 'package:path/path.dart' as path; + import 'package:flutter/cupertino.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../test_utils.dart'; + +final String rootDirectoryPath = Directory.current.parent.path; + void main() { for (final String language in kCupertinoSupportedLanguages) { testWidgets('translations exist for $language', (WidgetTester tester) async { @@ -129,8 +136,13 @@ void main() { // Regression test for https://github.com/flutter/flutter/issues/53036. testWidgets('`nb` uses `no` as its synonym when `nb` arb file is not present', (WidgetTester tester) async { - final File nbCupertinoArbFile = File('lib/src/l10n/cupertino_nb.arb'); - final File noCupertinoArbFile = File('lib/src/l10n/cupertino_no.arb'); + final File nbCupertinoArbFile = File( + path.join(rootDirectoryPath, 'lib', 'src', 'l10n', 'cupertino_nb.arb'), + ); + final File noCupertinoArbFile = File( + path.join(rootDirectoryPath, 'lib', 'src', 'l10n', 'cupertino_no.arb'), + ); + if (noCupertinoArbFile.existsSync() && !nbCupertinoArbFile.existsSync()) { Locale locale = const Locale.fromSubtags(languageCode: 'no', scriptCode: null, countryCode: null); @@ -151,4 +163,31 @@ void main() { expect(localizations.cutButtonLabel, cutButtonLabelNo); } }); + + // Regression test for https://github.com/flutter/flutter/issues/36704. + testWidgets('kn arb file should be properly Unicode escaped', (WidgetTester tester) async { + final File file = File( + path.join(rootDirectoryPath, 'lib', 'src', 'l10n', 'cupertino_kn.arb'), + ); + + final Map bundle = json.decode(file.readAsStringSync()) as Map; + + // Encodes the arb resource values if they have not already been + // encoded. + encodeBundleTranslations(bundle); + + // Generates the encoded arb output file in as a string. + final String encodedArbFile = generateArbString(bundle); + + // After encoding the bundles, the generated string should match + // the existing material_kn.arb. + if (Platform.isWindows) { + // On Windows, the character '\n' can output the two-character sequence + // '\r\n' (and when reading the file back, '\r\n' is translated back + // into a single '\n' character). + expect(file.readAsStringSync().replaceAll('\r\n', '\n'), encodedArbFile); + } else { + expect(file.readAsStringSync(), encodedArbFile); + } + }); } diff --git a/packages/flutter_localizations/test/material/translations_test.dart b/packages/flutter_localizations/test/material/translations_test.dart index b6cc3a2cdc..4db88cd0ad 100644 --- a/packages/flutter_localizations/test/material/translations_test.dart +++ b/packages/flutter_localizations/test/material/translations_test.dart @@ -2,12 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; import 'dart:io'; +import 'package:path/path.dart' as path; + import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../test_utils.dart'; + +final String rootDirectoryPath = Directory.current.parent.path; + void main() { for (final String language in kMaterialSupportedLanguages) { testWidgets('translations exist for $language', (WidgetTester tester) async { @@ -467,8 +474,12 @@ void main() { // Regression test for https://github.com/flutter/flutter/issues/53036. testWidgets('`nb` uses `no` as its synonym when `nb` arb file is not present', (WidgetTester tester) async { - final File nbMaterialArbFile = File('lib/src/l10n/material_nb.arb'); - final File noMaterialArbFile = File('lib/src/l10n/material_no.arb'); + final File nbMaterialArbFile = File( + path.join(rootDirectoryPath, 'lib', 'src', 'l10n', 'material_nb.arb'), + ); + final File noMaterialArbFile = File( + path.join(rootDirectoryPath, 'lib', 'src', 'l10n', 'material_no.arb'), + ); // No need to run test if `nb` arb file exists or if `no` arb file does not exist. if (noMaterialArbFile.existsSync() && !nbMaterialArbFile.existsSync()) { @@ -492,4 +503,33 @@ void main() { expect(localizations.okButtonLabel, okButtonLabelNo); } }); + + // Regression test for https://github.com/flutter/flutter/issues/36704. + testWidgets('kn arb file should be properly Unicode escaped', (WidgetTester tester) async { + final File file = File( + path.join(rootDirectoryPath, 'lib', 'src', 'l10n', 'material_kn.arb'), + ); + + final Map bundle = json.decode( + file.readAsStringSync(), + ) as Map; + + // Encodes the arb resource values if they have not already been + // encoded. + encodeBundleTranslations(bundle); + + // Generates the encoded arb output file in as a string. + final String encodedArbFile = generateArbString(bundle); + + // After encoding the bundles, the generated string should match + // the existing material_kn.arb. + if (Platform.isWindows) { + // On Windows, the character '\n' can output the two-character sequence + // '\r\n' (and when reading the file back, '\r\n' is translated back + // into a single '\n' character). + expect(file.readAsStringSync().replaceAll('\r\n', '\n'), encodedArbFile); + } else { + expect(file.readAsStringSync(), encodedArbFile); + } + }); } diff --git a/packages/flutter_localizations/test/test_utils.dart b/packages/flutter_localizations/test/test_utils.dart new file mode 100644 index 0000000000..cec115cf7a --- /dev/null +++ b/packages/flutter_localizations/test/test_utils.dart @@ -0,0 +1,30 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Encodes ARB file resource values with Unicode escapes. +void encodeBundleTranslations(Map bundle) { + for (final String key in bundle.keys) { + // The ARB file resource "attributes" for foo are called @foo. Don't need + // to encode them. + if (key.startsWith('@')) + continue; + final String translation = bundle[key] as String; + // Rewrite the string as a series of unicode characters in JSON format. + // Like "\u0012\u0123\u1234". + bundle[key] = translation.runes.map((int code) { + final String codeString = '00${code.toRadixString(16)}'; + return '\\u${codeString.substring(codeString.length - 4)}'; + }).join(); + } +} + +String generateArbString(Map bundle) { + final StringBuffer contents = StringBuffer(); + contents.writeln('{'); + for (final String key in bundle.keys) { + contents.writeln(' "$key": "${bundle[key]}"${key == bundle.keys.last ? '' : ','}'); + } + contents.writeln('}'); + return contents.toString(); +}