Move gen_l10n into flutter_tools (#65025)

This commit is contained in:
Shi-Hao Hong 2020-09-04 00:26:58 +08:00 committed by GitHub
parent eddf0a8abf
commit b80b432555
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 48784 additions and 486 deletions

View file

@ -4,199 +4,26 @@
import 'dart:io';
import 'package:args/args.dart' as argslib;
import 'package:file/local.dart' as local;
import 'package:path/path.dart' as path;
import '../gen_l10n.dart';
import '../gen_l10n_types.dart';
import '../localizations_utils.dart';
void main(List<String> arguments) {
final argslib.ArgParser parser = argslib.ArgParser();
parser.addFlag(
'help',
defaultsTo: false,
negatable: false,
help: 'Print this help message.',
);
parser.addOption(
'arb-dir',
defaultsTo: path.join('lib', 'l10n'),
help: 'The directory where the template and translated arb files are located.',
);
parser.addOption(
'output-dir',
help: 'The directory where the generated localization classes will be written '
'if the synthetic-package flag is set to false.'
'\n\n'
'If output-dir is specified and the synthetic-package flag is enabled, '
'this option will be ignored by the tool.'
'\n\n'
'The app must import the file specified in the \'output-localization-file\' '
'option from this directory. If unspecified, this defaults to the same '
'directory as the input directory specified in \'arb-dir\'.',
);
parser.addOption(
'template-arb-file',
defaultsTo: 'app_en.arb',
help: 'The template arb file that will be used as the basis for '
'generating the Dart localization and messages files.',
);
parser.addOption(
'output-localization-file',
defaultsTo: 'app_localizations.dart',
help: 'The filename for the output localization and localizations '
'delegate classes.',
);
parser.addOption(
'untranslated-messages-file',
help: 'The location of a file that describes the localization\n'
'messages have not been translated yet. Using this option will create\n'
'a JSON file at the target location, in the following format:\n\n'
'"locale": ["message_1", "message_2" ... "message_n"]\n\n'
'If this option is not specified, a summary of the messages that\n'
'have not been translated will be printed on the command line.'
);
parser.addOption(
'output-class',
defaultsTo: 'AppLocalizations',
help: 'The Dart class name to use for the output localization and '
'localizations delegate classes.',
);
parser.addOption(
'preferred-supported-locales',
help: 'The list of preferred supported locales for the application. '
'By default, the tool will generate the supported locales list in '
'alphabetical order. Use this flag if you would like to default to '
'a different locale. \n\n'
"For example, pass in ['en_US'] if you would like your app to "
'default to American English if a device supports it.',
);
parser.addOption(
'header',
help: 'The header to prepend to the generated Dart localizations '
'files. This option takes in a string. \n\n'
'For example, pass in "/// All localized files." if you would '
'like this string prepended to the generated Dart file. \n\n'
'Alternatively, see the `header-file` option to pass in a text '
'file for longer headers.'
);
parser.addOption(
'header-file',
help: 'The header to prepend to the generated Dart localizations '
'files. The value of this option is the name of the file that '
'contains the header text which will be inserted at the top '
'of each generated Dart file. \n\n'
'Alternatively, see the `header` option to pass in a string '
'for a simpler header. \n\n'
'This file should be placed in the directory specified in \'arb-dir\'.'
);
parser.addFlag(
'use-deferred-loading',
defaultsTo: false,
help: 'Whether to generate the Dart localization file with locales imported'
' as deferred, allowing for lazy loading of each locale in Flutter web.\n'
'\n'
'This can reduce a web apps initial startup time by decreasing the '
'size of the JavaScript bundle. When this flag is set to true, the '
'messages for a particular locale are only downloaded and loaded by the '
'Flutter app as they are needed. For projects with a lot of different '
'locales and many localization strings, it can be an performance '
'improvement to have deferred loading. For projects with a small number '
'of locales, the difference is negligible, and might slow down the start '
'up compared to bundling the localizations with the rest of the '
'application.\n\n'
'Note that this flag does not affect other platforms such as mobile or '
'desktop.',
);
parser.addOption(
'gen-inputs-and-outputs-list',
valueHelp: 'path-to-output-directory',
help: 'When specified, the tool generates a JSON file containing the '
'tool\'s inputs and outputs named gen_l10n_inputs_and_outputs.json.'
'\n\n'
'This can be useful for keeping track of which files of the Flutter '
'project were used when generating the latest set of localizations. '
'For example, the Flutter tool\'s build system uses this file to '
'keep track of when to call gen_l10n during hot reload.\n\n'
'The value of this option is the directory where the JSON file will be '
'generated.'
'\n\n'
'When null, the JSON file will not be generated.'
);
parser.addFlag(
'synthetic-package',
defaultsTo: true,
help: 'Determines whether or not the generated output files will be '
'generated as a synthetic package or at a specified directory in '
'the Flutter project.'
'\n\n'
'This flag is set to true by default.'
'\n\n'
'When synthetic-package is set to false, it will generate the '
'localizations files in the directory specified by arb-dir by default. '
'\n\n'
'If output-dir is specified, files will be generated there.',
);
parser.addOption(
'project-dir',
valueHelp: 'absolute/path/to/flutter/project',
help: 'When specified, the tool uses the path passed into this option '
'as the directory of the root Flutter project.'
'\n\n'
'When null, the relative path to the present working directory will be used.'
/// Runs `flutter generate_localizations with arguments passed in.
///
/// This script exists as a legacy entrypoint, since existing users of
/// gen_l10n tool used to call
/// `dart ${FLUTTER}/dev/tools/localizations/bin/gen_l10n.dart <options>` to
/// generate their Flutter project's localizations resources.
///
/// Now, the appropriate way to use this tool is to either define an `l10n.yaml`
/// file in the Flutter project repository, or call
/// `flutter generate_localizations <options>`, since the code has moved
/// into `flutter_tools`.
Future<void> main(List<String> rawArgs) async {
final ProcessResult result = await Process.run(
'flutter',
<String>[
'generate_localizations',
...rawArgs,
],
);
final argslib.ArgResults results = parser.parse(arguments);
if (results['help'] == true) {
print(parser.usage);
exit(0);
}
precacheLanguageAndRegionTags();
final String inputPathString = results['arb-dir'] as String;
final String outputPathString = results['output-dir'] as String;
final String outputFileString = results['output-localization-file'] as String;
final String templateArbFileName = results['template-arb-file'] as String;
final String untranslatedMessagesFile = results['untranslated-messages-file'] as String;
final String classNameString = results['output-class'] as String;
final String preferredSupportedLocaleString = results['preferred-supported-locales'] as String;
final String headerString = results['header'] as String;
final String headerFile = results['header-file'] as String;
final bool useDeferredLoading = results['use-deferred-loading'] as bool;
final String inputsAndOutputsListPath = results['gen-inputs-and-outputs-list'] as String;
final bool useSyntheticPackage = results['synthetic-package'] as bool;
final String projectPathString = results['project-dir'] as String;
const local.LocalFileSystem fs = local.LocalFileSystem();
final LocalizationsGenerator localizationsGenerator = LocalizationsGenerator(fs);
try {
localizationsGenerator
..initialize(
inputPathString: inputPathString,
outputPathString: outputPathString,
templateArbFileName: templateArbFileName,
outputFileString: outputFileString,
classNameString: classNameString,
preferredSupportedLocaleString: preferredSupportedLocaleString,
headerString: headerString,
headerFile: headerFile,
useDeferredLoading: useDeferredLoading,
inputsAndOutputsListPath: inputsAndOutputsListPath,
useSyntheticPackage: useSyntheticPackage,
projectPathString: projectPathString,
)
..loadResources()
..writeOutputFiles()
..outputUnimplementedMessages(untranslatedMessagesFile);
} on FileSystemException catch (e) {
exitWithError(e.message);
} on FormatException catch (e) {
exitWithError(e.message);
} on L10nException catch (e) {
exitWithError(e.message);
}
stdout.write(result.stdout);
stderr.write(result.stderr);
}

View file

@ -17,11 +17,19 @@ Future<void> main() async {
final HttpClientRequest request = await client.getUrl(Uri.parse(registry));
final HttpClientResponse response = await request.close();
final String body = (await response.cast<List<int>>().transform<String>(utf8.decoder).toList()).join('');
print('''// Copyright 2014 The Flutter Authors. All rights reserved.
final File subtagRegistry = File('../language_subtag_registry.dart');
final File subtagRegistryFlutterTools = File('../../../../packages/flutter_tools/lib/src/localizations/language_subtag_registry.dart');
final String content = '''// 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.
/// Cache of $registry.
const String languageSubtagRegistry = \'\'\'$body\'\'\';''');
const String languageSubtagRegistry = \'\'\'$body\'\'\';''';
subtagRegistry.writeAsStringSync(content);
subtagRegistryFlutterTools.writeAsStringSync(content);
client.close(force: true);
}

View file

@ -1,71 +0,0 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../localization/localizations_utils.dart';
import '../common.dart';
void main() {
group('generateString', () {
test('handles simple string', () {
expect(generateString('abc'), "'abc'");
});
test('handles string with quote', () {
expect(generateString("ab'c"), "'ab\\\'c'");
});
test('handles string with double quote', () {
expect(generateString('ab"c'), "'ab\\\"c'");
});
test('handles string with both single and double quote', () {
expect(generateString('''a'b"c'''), '\'a\\\'b\\"c\'');
});
test('handles string with a triple single quote and a double quote', () {
expect(generateString("""a"b'''c"""), '\'a\\"b\\\'\\\'\\\'c\'');
});
test('handles string with a triple double quote and a single quote', () {
expect(generateString('''a'b"""c'''), '\'a\\\'b\\"\\"\\"c\'');
});
test('handles string with both triple single and triple double quote', () {
expect(generateString('''a\'''b"""c'''), '\'a\\\'\\\'\\\'b\\"\\"\\"c\'');
});
test('escapes dollar when escapeDollar is true', () {
expect(generateString(r'ab$c'), "'ab\\\$c'");
});
test('handles backslash', () {
expect(generateString(r'ab\c'), r"'ab\\c'");
});
test('handles backslash followed by "n" character', () {
expect(generateString(r'ab\nc'), r"'ab\\nc'");
});
test('supports newline escaping', () {
expect(generateString('ab\nc'), "'ab\\nc'");
});
test('supports form feed escaping', () {
expect(generateString('ab\fc'), "'ab\\fc'");
});
test('supports tab escaping', () {
expect(generateString('ab\tc'), "'ab\\tc'");
});
test('supports carriage return escaping', () {
expect(generateString('ab\rc'), "'ab\\rc'");
});
test('supports backspace escaping', () {
expect(generateString('ab\bc'), "'ab\\bc'");
});
});
}

View file

@ -34,6 +34,7 @@ import 'src/commands/drive.dart';
import 'src/commands/emulators.dart';
import 'src/commands/format.dart';
import 'src/commands/generate.dart';
import 'src/commands/generate_localizations.dart';
import 'src/commands/ide_config.dart';
import 'src/commands/inject_plugins.dart';
import 'src/commands/install.dart';
@ -98,6 +99,9 @@ Future<void> main(List<String> args) async {
EmulatorsCommand(),
FormatCommand(),
GenerateCommand(),
GenerateLocalizationsCommand(
fileSystem: globals.fs,
),
InstallCommand(),
LogsCommand(),
MakeHostAppEditableCommand(),

View file

@ -3,15 +3,15 @@
// found in the LICENSE file.
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'package:yaml/yaml.dart';
import '../../artifacts.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../base/logger.dart';
import '../../convert.dart';
import '../../globals.dart' as globals;
import '../../localizations/gen_l10n.dart';
import '../../localizations/gen_l10n_types.dart';
import '../../localizations/localizations_utils.dart';
import '../../project.dart';
import '../build_system.dart';
import '../depfile.dart';
@ -19,25 +19,13 @@ import '../depfile.dart';
const String _kDependenciesFileName = 'gen_l10n_inputs_and_outputs.json';
/// Run the localizations generation script with the configuration [options].
Future<void> generateLocalizations({
@required LocalizationOptions options,
@required String flutterRoot,
@required FileSystem fileSystem,
@required ProcessManager processManager,
@required Logger logger,
void generateLocalizations({
@required Directory projectDir,
@required String dartBinaryPath,
@required Directory dependenciesDir,
}) async {
final String genL10nPath = fileSystem.path.join(
flutterRoot,
'dev',
'tools',
'localization',
'bin',
'gen_l10n.dart',
);
@required LocalizationOptions options,
@required LocalizationsGenerator localizationsGenerator,
@required Logger logger,
}) {
// If generating a synthetic package, generate a warning if
// flutter: generate is not set.
final FlutterProject flutterProject = FlutterProject.fromDirectory(projectDir);
@ -53,35 +41,35 @@ Future<void> generateLocalizations({
throw Exception();
}
final ProcessResult result = await processManager.run(<String>[
dartBinaryPath,
'--disable-dart-dev',
genL10nPath,
'--gen-inputs-and-outputs-list=${dependenciesDir.path}',
'--project-dir=${projectDir.path}',
if (options.arbDirectory != null)
'--arb-dir=${options.arbDirectory.toFilePath()}',
if (options.templateArbFile != null)
'--template-arb-file=${options.templateArbFile.toFilePath()}',
if (options.outputLocalizationsFile != null)
'--output-localization-file=${options.outputLocalizationsFile.toFilePath()}',
if (options.untranslatedMessagesFile != null)
'--untranslated-messages-file=${options.untranslatedMessagesFile.toFilePath()}',
if (options.outputClass != null)
'--output-class=${options.outputClass}',
if (options.headerFile != null)
'--header-file=${options.headerFile.toFilePath()}',
if (options.header != null)
'--header=${options.header}',
if (options.deferredLoading != null)
'--use-deferred-loading',
if (options.preferredSupportedLocales != null)
'--preferred-supported-locales=${options.preferredSupportedLocales}',
if (!options.useSyntheticPackage)
'--no-synthetic-package'
]);
if (result.exitCode != 0) {
logger.printError(result.stdout + result.stderr as String);
precacheLanguageAndRegionTags();
final String inputPathString = options?.arbDirectory?.toFilePath() ?? globals.fs.path.join('lib', 'l10n');
final String templateArbFileName = options?.templateArbFile?.toFilePath() ?? 'app_en.arb';
final String outputFileString = options?.outputLocalizationsFile?.toFilePath() ?? 'app_localizations.dart';
try {
localizationsGenerator
..initialize(
inputsAndOutputsListPath: dependenciesDir.path,
projectPathString: projectDir.path,
inputPathString: inputPathString,
templateArbFileName: templateArbFileName,
outputFileString: outputFileString,
classNameString: options.outputClass ?? 'AppLocalizations',
preferredSupportedLocaleString: options.preferredSupportedLocales,
headerString: options.header,
headerFile: options?.headerFile?.toFilePath(),
useDeferredLoading: options.deferredLoading ?? false,
useSyntheticPackage: options.useSyntheticPackage ?? true,
)
..loadResources()
..writeOutputFiles()
..outputUnimplementedMessages(
options?.untranslatedMessagesFile?.toFilePath(),
logger,
);
} on L10nException catch (e) {
logger.printError(e.message);
throw Exception();
}
}
@ -132,19 +120,17 @@ class GenerateLocalizationsTarget extends Target {
fileSystem: environment.fileSystem,
);
await generateLocalizations(
fileSystem: environment.fileSystem,
flutterRoot: environment.flutterRootDir.path,
generateLocalizations(
logger: environment.logger,
processManager: environment.processManager,
options: options,
projectDir: environment.projectDir,
dartBinaryPath: environment.artifacts
.getArtifactPath(Artifact.engineDartBinary),
dependenciesDir: environment.buildDir,
localizationsGenerator: LocalizationsGenerator(environment.fileSystem),
);
final Map<String, Object> dependencies = json
.decode(environment.buildDir.childFile(_kDependenciesFileName).readAsStringSync()) as Map<String, Object>;
final Map<String, Object> dependencies = json.decode(
environment.buildDir.childFile(_kDependenciesFileName).readAsStringSync()
) as Map<String, Object>;
final Depfile depfile = Depfile(
<File>[
configFile,

View file

@ -0,0 +1,207 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../base/common.dart';
import '../base/file_system.dart';
import '../globals.dart' as globals;
import '../localizations/gen_l10n.dart';
import '../localizations/gen_l10n_types.dart';
import '../localizations/localizations_utils.dart';
import '../runner/flutter_command.dart';
/// A command to generate localizations source files for a Flutter project.
///
/// It generates Dart localization source files from arb files.
///
/// For a more comprehensive tutorial on the tool, please see the
/// [internationalization user guide](flutter.dev/go/i18n-user-guide).
class GenerateLocalizationsCommand extends FlutterCommand {
GenerateLocalizationsCommand({
FileSystem fileSystem,
}) :
_fileSystem = fileSystem {
argParser.addOption(
'arb-dir',
defaultsTo: globals.fs.path.join('lib', 'l10n'),
help: 'The directory where the template and translated arb files are located.',
);
argParser.addOption(
'output-dir',
help: 'The directory where the generated localization classes will be written '
'if the synthetic-package flag is set to false.'
'\n\n'
'If output-dir is specified and the synthetic-package flag is enabled, '
'this option will be ignored by the tool.'
'\n\n'
'The app must import the file specified in the \'output-localization-file\' '
'option from this directory. If unspecified, this defaults to the same '
'directory as the input directory specified in \'arb-dir\'.',
);
argParser.addOption(
'template-arb-file',
defaultsTo: 'app_en.arb',
help: 'The template arb file that will be used as the basis for '
'generating the Dart localization and messages files.',
);
argParser.addOption(
'output-localization-file',
defaultsTo: 'app_localizations.dart',
help: 'The filename for the output localization and localizations '
'delegate classes.',
);
argParser.addOption(
'untranslated-messages-file',
help: 'The location of a file that describes the localization\n'
'messages have not been translated yet. Using this option will create\n'
'a JSON file at the target location, in the following format:\n\n'
'"locale": ["message_1", "message_2" ... "message_n"]\n\n'
'If this option is not specified, a summary of the messages that\n'
'have not been translated will be printed on the command line.'
);
argParser.addOption(
'output-class',
defaultsTo: 'AppLocalizations',
help: 'The Dart class name to use for the output localization and '
'localizations delegate classes.',
);
argParser.addOption(
'preferred-supported-locales',
help: 'The list of preferred supported locales for the application. '
'By default, the tool will generate the supported locales list in '
'alphabetical order. Use this flag if you would like to default to '
'a different locale. \n\n'
"For example, pass in ['en_US'] if you would like your app to "
'default to American English if a device supports it.',
);
argParser.addOption(
'header',
help: 'The header to prepend to the generated Dart localizations '
'files. This option takes in a string. \n\n'
'For example, pass in "/// All localized files." if you would '
'like this string prepended to the generated Dart file. \n\n'
'Alternatively, see the `header-file` option to pass in a text '
'file for longer headers.'
);
argParser.addOption(
'header-file',
help: 'The header to prepend to the generated Dart localizations '
'files. The value of this option is the name of the file that '
'contains the header text which will be inserted at the top '
'of each generated Dart file. \n\n'
'Alternatively, see the `header` option to pass in a string '
'for a simpler header. \n\n'
'This file should be placed in the directory specified in \'arb-dir\'.'
);
argParser.addFlag(
'use-deferred-loading',
defaultsTo: false,
help: 'Whether to generate the Dart localization file with locales imported'
' as deferred, allowing for lazy loading of each locale in Flutter web.\n'
'\n'
'This can reduce a web apps initial startup time by decreasing the '
'size of the JavaScript bundle. When this flag is set to true, the '
'messages for a particular locale are only downloaded and loaded by the '
'Flutter app as they are needed. For projects with a lot of different '
'locales and many localization strings, it can be an performance '
'improvement to have deferred loading. For projects with a small number '
'of locales, the difference is negligible, and might slow down the start '
'up compared to bundling the localizations with the rest of the '
'application.\n\n'
'Note that this flag does not affect other platforms such as mobile or '
'desktop.',
);
argParser.addOption(
'gen-inputs-and-outputs-list',
valueHelp: 'path-to-output-directory',
help: 'When specified, the tool generates a JSON file containing the '
'tool\'s inputs and outputs named gen_l10n_inputs_and_outputs.json.'
'\n\n'
'This can be useful for keeping track of which files of the Flutter '
'project were used when generating the latest set of localizations. '
'For example, the Flutter tool\'s build system uses this file to '
'keep track of when to call gen_l10n during hot reload.\n\n'
'The value of this option is the directory where the JSON file will be '
'generated.'
'\n\n'
'When null, the JSON file will not be generated.'
);
argParser.addFlag(
'synthetic-package',
defaultsTo: true,
help: 'Determines whether or not the generated output files will be '
'generated as a synthetic package or at a specified directory in '
'the Flutter project.'
'\n\n'
'This flag is set to true by default.'
'\n\n'
'When synthetic-package is set to false, it will generate the '
'localizations files in the directory specified by arb-dir by default. '
'\n\n'
'If output-dir is specified, files will be generated there.',
);
argParser.addOption(
'project-dir',
valueHelp: 'absolute/path/to/flutter/project',
help: 'When specified, the tool uses the path passed into this option '
'as the directory of the root Flutter project.'
'\n\n'
'When null, the relative path to the present working directory will be used.'
);
}
final FileSystem _fileSystem;
@override
String get description => 'Generate localizations for the Flutter project.';
@override
String get name => 'gen-l10n';
@override
Future<FlutterCommandResult> runCommand() async {
precacheLanguageAndRegionTags();
final String inputPathString = stringArg('arb-dir');
final String outputPathString = stringArg('output-dir');
final String outputFileString = stringArg('output-localization-file');
final String templateArbFileName = stringArg('template-arb-file');
final String untranslatedMessagesFile = stringArg('untranslated-messages-file');
final String classNameString = stringArg('output-class');
final String preferredSupportedLocaleString = stringArg('preferred-supported-locales');
final String headerString = stringArg('header');
final String headerFile = stringArg('header-file');
final bool useDeferredLoading = boolArg('use-deferred-loading');
final String inputsAndOutputsListPath = stringArg('gen-inputs-and-outputs-list');
final bool useSyntheticPackage = boolArg('synthetic-package');
final String projectPathString = stringArg('project-dir');
final LocalizationsGenerator localizationsGenerator = LocalizationsGenerator(_fileSystem);
try {
localizationsGenerator
..initialize(
inputPathString: inputPathString,
outputPathString: outputPathString,
templateArbFileName: templateArbFileName,
outputFileString: outputFileString,
classNameString: classNameString,
preferredSupportedLocaleString: preferredSupportedLocaleString,
headerString: headerString,
headerFile: headerFile,
useDeferredLoading: useDeferredLoading,
inputsAndOutputsListPath: inputsAndOutputsListPath,
useSyntheticPackage: useSyntheticPackage,
projectPathString: projectPathString,
)
..loadResources()
..writeOutputFiles()
..outputUnimplementedMessages(untranslatedMessagesFile, globals.logger);
} on L10nException catch (e) {
throwToolExit(e.message);
}
return FlutterCommandResult.success();
}
}

View file

@ -2,12 +2,12 @@
// 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:file/file.dart' as file;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import '../base/file_system.dart';
import '../base/logger.dart';
import '../convert.dart';
import '../globals.dart' as globals;
import 'gen_l10n_templates.dart';
import 'gen_l10n_types.dart';
@ -18,7 +18,7 @@ import 'localizations_utils.dart';
///
/// See [LocalizationsGenerator.initialize] for where and how it is used by the
/// localizations tool.
final String defaultSyntheticPackagePath = path.join('.dart_tool', 'flutter_gen', 'gen_l10n');
final String defaultSyntheticPackagePath = globals.fs.path.join('.dart_tool', 'flutter_gen', 'gen_l10n');
List<String> generateMethodParameters(Message message) {
assert(message.placeholders.isNotEmpty);
@ -30,8 +30,9 @@ List<String> generateMethodParameters(Message message) {
}
String generateDateFormattingLogic(Message message) {
if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting)
if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting) {
return '@(none)';
}
final Iterable<String> formatStatements = message.placeholders
.where((Placeholder placeholder) => placeholder.isDate)
@ -104,8 +105,9 @@ String generatePluralMethod(Message message, AppResourceBundle bundle) {
// To make it easier to parse the plurals message, temporarily replace each
// "{placeholder}" parameter with "#placeholder#".
String easyMessage = bundle.translationFor(message);
for (final Placeholder placeholder in message.placeholders)
for (final Placeholder placeholder in message.placeholders) {
easyMessage = easyMessage.replaceAll('{${placeholder.name}}', '#${placeholder.name}#');
}
final Placeholder countPlaceholder = message.getCountPlaceholder();
if (countPlaceholder == null) {
@ -246,8 +248,9 @@ String _generateLookupByScriptCode(
return locale.scriptCode != null && locale.countryCode == null;
});
if (localesWithScriptCodes.isEmpty)
if (localesWithScriptCodes.isEmpty) {
return null;
}
return nestedSwitchTemplate
.replaceAll('@(languageCode)', language)
@ -278,8 +281,9 @@ String _generateLookupByCountryCode(
return locale.countryCode != null && locale.scriptCode == null;
});
if (localesWithCountryCodes.isEmpty)
if (localesWithCountryCodes.isEmpty) {
return null;
}
return nestedSwitchTemplate
.replaceAll('@(languageCode)', language)
@ -309,8 +313,9 @@ String _generateLookupByLanguageCode(
return locale.countryCode == null && locale.scriptCode == null;
});
if (localesWithLanguageCode.isEmpty)
if (localesWithLanguageCode.isEmpty) {
return null;
}
return localesWithLanguageCode.map((LocaleInfo locale) {
return generateSwitchClauseTemplate(locale)
@ -396,7 +401,7 @@ class LocalizationsGenerator {
/// It takes in a [FileSystem] representation that the class will act upon.
LocalizationsGenerator(this._fs);
final file.FileSystem _fs;
final FileSystem _fs;
Iterable<Message> _allMessages;
AppResourceBundleCollection _allBundles;
LocaleInfo _templateArbLocale;
@ -582,26 +587,29 @@ class LocalizationsGenerator {
/// Sets the reference [Directory] for [inputDirectory].
@visibleForTesting
void setInputDirectory(String inputPathString) {
if (inputPathString == null)
if (inputPathString == null) {
throw L10nException('inputPathString argument cannot be null');
}
inputDirectory = _fs.directory(
projectDirectory != null
? _getAbsoluteProjectPath(inputPathString)
: inputPathString
);
if (!inputDirectory.existsSync())
throw FileSystemException(
if (!inputDirectory.existsSync()) {
throw L10nException(
"The 'arb-dir' directory, '$inputDirectory', does not exist.\n"
'Make sure that the correct path was provided.'
);
}
final FileStat fileStat = inputDirectory.statSync();
if (_isNotReadable(fileStat) || _isNotWritable(fileStat))
throw FileSystemException(
if (_isNotReadable(fileStat) || _isNotWritable(fileStat)) {
throw L10nException(
"The 'arb-dir' directory, '$inputDirectory', doesn't allow reading and writing.\n"
'Please ensure that the user has read and write permissions.'
);
}
}
/// Sets the reference [Directory] for [outputDirectory].
@ -617,11 +625,12 @@ class LocalizationsGenerator {
: defaultSyntheticPackagePath
);
} else {
if (outputPathString == null)
if (outputPathString == null) {
throw L10nException(
'outputPathString argument cannot be null if not using '
'synthetic package option.'
);
}
outputDirectory = _fs.directory(
projectDirectory != null
@ -634,41 +643,49 @@ class LocalizationsGenerator {
/// Sets the reference [File] for [templateArbFile].
@visibleForTesting
void setTemplateArbFile(String templateArbFileName) {
if (templateArbFileName == null)
if (templateArbFileName == null) {
throw L10nException('templateArbFileName argument cannot be null');
if (inputDirectory == null)
}
if (inputDirectory == null) {
throw L10nException('inputDirectory cannot be null when setting template arb file');
}
templateArbFile = _fs.file(path.join(inputDirectory.path, templateArbFileName));
templateArbFile = _fs.file(globals.fs.path.join(inputDirectory.path, templateArbFileName));
final String templateArbFileStatModeString = templateArbFile.statSync().modeString();
if (templateArbFileStatModeString[0] == '-' && templateArbFileStatModeString[3] == '-')
throw FileSystemException(
if (templateArbFileStatModeString[0] == '-' && templateArbFileStatModeString[3] == '-') {
throw L10nException(
"The 'template-arb-file', $templateArbFile, is not readable.\n"
'Please ensure that the user has read permissions.'
);
}
}
/// Sets the reference [File] for the localizations delegate [outputFile].
@visibleForTesting
void setBaseOutputFile(String outputFileString) {
if (outputFileString == null)
if (outputFileString == null) {
throw L10nException('outputFileString argument cannot be null');
baseOutputFile = _fs.file(path.join(outputDirectory.path, outputFileString));
}
baseOutputFile = _fs.file(globals.fs.path.join(outputDirectory.path, outputFileString));
}
static bool _isValidClassName(String className) {
// Public Dart class name cannot begin with an underscore
if (className[0] == '_')
if (className[0] == '_') {
return false;
}
// Dart class name cannot contain non-alphanumeric symbols
if (className.contains(RegExp(r'[^a-zA-Z_\d]')))
if (className.contains(RegExp(r'[^a-zA-Z_\d]'))) {
return false;
}
// Dart class name must start with upper case character
if (className[0].contains(RegExp(r'[a-z]')))
if (className[0].contains(RegExp(r'[a-z]'))) {
return false;
}
// Dart class name cannot start with a number
if (className[0].contains(RegExp(r'\d')))
if (className[0].contains(RegExp(r'\d'))) {
return false;
}
return true;
}
@ -676,12 +693,14 @@ class LocalizationsGenerator {
/// classes.
@visibleForTesting
set className(String classNameString) {
if (classNameString == null || classNameString.isEmpty)
if (classNameString == null || classNameString.isEmpty) {
throw L10nException('classNameString argument cannot be null or empty');
if (!_isValidClassName(classNameString))
}
if (!_isValidClassName(classNameString)) {
throw L10nException(
"The 'output-class', $classNameString, is not a valid public Dart class name.\n"
);
}
_className = classNameString;
}
@ -716,7 +735,7 @@ class LocalizationsGenerator {
header = headerString;
} else if (headerFile != null) {
try {
header = _fs.file(path.join(inputDirectory.path, headerFile)).readAsStringSync();
header = _fs.file(globals.fs.path.join(inputDirectory.path, headerFile)).readAsStringSync();
} on FileSystemException catch (error) {
throw L10nException (
'Failed to read header file: "$headerFile". \n'
@ -726,7 +745,7 @@ class LocalizationsGenerator {
}
}
String _getAbsoluteProjectPath(String relativePath) => _fs.path.join(projectDirectory.path, relativePath);
String _getAbsoluteProjectPath(String relativePath) => globals.fs.path.join(projectDirectory.path, relativePath);
void _setUseDeferredLoading(bool useDeferredLoading) {
if (useDeferredLoading == null) {
@ -736,11 +755,12 @@ class LocalizationsGenerator {
}
void _setInputsAndOutputsListFile(String inputsAndOutputsListPath) {
if (inputsAndOutputsListPath == null)
if (inputsAndOutputsListPath == null) {
return;
}
_inputsAndOutputsListFile = _fs.file(
path.join(inputsAndOutputsListPath, 'gen_l10n_inputs_and_outputs.json'),
globals.fs.path.join(inputsAndOutputsListPath, 'gen_l10n_inputs_and_outputs.json'),
);
_inputFileList = <String>[];
@ -749,17 +769,21 @@ class LocalizationsGenerator {
static bool _isValidGetterAndMethodName(String name) {
// Public Dart method name must not start with an underscore
if (name[0] == '_')
if (name[0] == '_') {
return false;
}
// Dart getter and method name cannot contain non-alphanumeric symbols
if (name.contains(RegExp(r'[^a-zA-Z_\d]')))
if (name.contains(RegExp(r'[^a-zA-Z_\d]'))) {
return false;
}
// Dart method name must start with lower case character
if (name[0].contains(RegExp(r'[A-Z]')))
if (name[0].contains(RegExp(r'[A-Z]'))) {
return false;
}
// Dart class name cannot start with a number
if (name[0].contains(RegExp(r'\d')))
if (name[0].contains(RegExp(r'\d'))) {
return false;
}
return true;
}
@ -769,7 +793,7 @@ class LocalizationsGenerator {
final AppResourceBundle templateBundle = AppResourceBundle(templateArbFile);
_templateArbLocale = templateBundle.locale;
_allMessages = templateBundle.resourceIds.map((String id) => Message(templateBundle.resources, id));
for (final String resourceId in templateBundle.resourceIds)
for (final String resourceId in templateBundle.resourceIds) {
if (!_isValidGetterAndMethodName(resourceId)) {
throw L10nException(
'Invalid ARB resource name "$resourceId" in $templateArbFile.\n'
@ -778,6 +802,7 @@ class LocalizationsGenerator {
'contain non-alphanumeric characters.'
);
}
}
_allBundles = AppResourceBundleCollection(inputDirectory);
if (_inputsAndOutputsListFile != null) {
@ -888,8 +913,8 @@ class LocalizationsGenerator {
.map((AppResourceBundle bundle) => bundle.locale).toList();
}
final String directory = path.basename(outputDirectory.path);
final String outputFileName = path.basename(baseOutputFile.path);
final String directory = globals.fs.path.basename(outputDirectory.path);
final String outputFileName = globals.fs.path.basename(baseOutputFile.path);
final Iterable<String> supportedLocalesCode = supportedLocales.map((LocaleInfo locale) {
final String languageCode = locale.languageCode;
@ -916,7 +941,7 @@ class LocalizationsGenerator {
for (final LocaleInfo locale in allLocales) {
if (isBaseClassLocale(locale, locale.languageCode)) {
final File languageMessageFile = _fs.file(
path.join(outputDirectory.path, '${fileName}_$locale.dart'),
globals.fs.path.join(outputDirectory.path, '${fileName}_$locale.dart'),
);
// Generate the template for the base class file. Further string
@ -993,11 +1018,12 @@ class LocalizationsGenerator {
// Ensure that the created directory has read/write permissions.
final FileStat fileStat = outputDirectory.statSync();
if (_isNotReadable(fileStat) || _isNotWritable(fileStat))
throw FileSystemException(
if (_isNotReadable(fileStat) || _isNotWritable(fileStat)) {
throw L10nException(
"The 'output-dir' directory, $outputDirectory, doesn't allow reading and writing.\n"
'Please ensure that the user has read and write permissions.'
);
}
// Generate the required files for localizations.
_languageFileMap.forEach((File file, String contents) {
@ -1025,12 +1051,18 @@ class LocalizationsGenerator {
}
}
void outputUnimplementedMessages(String untranslatedMessagesFile) {
void outputUnimplementedMessages(String untranslatedMessagesFile, Logger logger) {
if (logger == null) {
throw L10nException(
'Logger must be defined when generating untranslated messages file.'
);
}
if (untranslatedMessagesFile == null || untranslatedMessagesFile == '') {
_unimplementedMessages.forEach((LocaleInfo locale, List<String> messages) {
stdout.writeln('"$locale": ${messages.length} untranslated message(s).');
logger.printStatus('"$locale": ${messages.length} untranslated message(s).');
});
stdout.writeln(
logger.printStatus(
'To see a detailed report, use the --untranslated-messages-file \n'
'option in the tool to generate a JSON format file containing \n'
'all messages that need to be translated.'

View file

@ -2,11 +2,12 @@
// 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:intl/locale.dart';
import 'package:path/path.dart' as path;
import '../base/file_system.dart';
import '../convert.dart';
import '../globals.dart' as globals;
import 'localizations_utils.dart';
@ -218,8 +219,9 @@ class Placeholder {
String attributeName,
) {
final dynamic value = attributes[attributeName];
if (value == null)
if (value == null) {
return null;
}
if (value is! String || (value as String).isEmpty) {
throw L10nException(
'The "$attributeName" value of the "$name" placeholder in message $resourceId '
@ -235,8 +237,9 @@ class Placeholder {
Map<String, dynamic> attributes
) {
final dynamic value = attributes['optionalParameters'];
if (value == null)
if (value == null) {
return <OptionalParameter>[];
}
if (value is! Map<String, Object>) {
throw L10nException(
'The "optionalParameters" value of the "$name" placeholder in message '
@ -300,10 +303,12 @@ class Message {
static String _value(Map<String, dynamic> bundle, String resourceId) {
final dynamic value = bundle[resourceId];
if (value == null)
if (value == null) {
throw L10nException('A value for resource "$resourceId" was not found.');
if (value is! String)
}
if (value is! String) {
throw L10nException('The value of "$resourceId" is not a string.');
}
return bundle[resourceId] as String;
}
@ -326,8 +331,9 @@ class Message {
static String _description(Map<String, dynamic> bundle, String resourceId) {
final dynamic value = _attributes(bundle, resourceId)['description'];
if (value == null)
if (value == null) {
return null;
}
if (value is! String) {
throw L10nException(
'The description for "@$resourceId" is not a properly formatted String.'
@ -338,8 +344,9 @@ class Message {
static List<Placeholder> _placeholders(Map<String, dynamic> bundle, String resourceId) {
final dynamic value = _attributes(bundle, resourceId)['placeholders'];
if (value == null)
if (value == null) {
return <Placeholder>[];
}
if (value is! Map<String, dynamic>) {
throw L10nException(
'The "placeholders" attribute for message $resourceId, is not '
@ -379,7 +386,7 @@ class AppResourceBundle {
String localeString = resources['@@locale'] as String;
// Look for the first instance of an ISO 639-1 language code, matching exactly.
final String fileName = path.basenameWithoutExtension(file.path);
final String fileName = globals.fs.path.basenameWithoutExtension(file.path);
for (int index = 0; index < fileName.length; index += 1) {
// If an underscore was found, check if locale string follows.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,288 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:meta/meta.dart';
import '../base/file_system.dart';
import 'language_subtag_registry.dart';
typedef HeaderGenerator = String Function(String regenerateInstructions);
typedef ConstructorGenerator = String Function(LocaleInfo locale);
int sortFilesByPath (File a, File b) {
return a.path.compareTo(b.path);
}
/// Simple data class to hold parsed locale. Does not promise validity of any data.
@immutable
class LocaleInfo implements Comparable<LocaleInfo> {
const LocaleInfo({
this.languageCode,
this.scriptCode,
this.countryCode,
this.length,
this.originalString,
});
/// Simple parser. Expects the locale string to be in the form of 'language_script_COUNTRY'
/// where the language is 2 characters, script is 4 characters with the first uppercase,
/// and country is 2-3 characters and all uppercase.
///
/// 'language_COUNTRY' or 'language_script' are also valid. Missing fields will be null.
///
/// When `deriveScriptCode` is true, if [scriptCode] was unspecified, it will
/// be derived from the [languageCode] and [countryCode] if possible.
factory LocaleInfo.fromString(String locale, { bool deriveScriptCode = false }) {
final List<String> codes = locale.split('_'); // [language, script, country]
assert(codes.isNotEmpty && codes.length < 4);
final String languageCode = codes[0];
String scriptCode;
String countryCode;
int length = codes.length;
String originalString = locale;
if (codes.length == 2) {
scriptCode = codes[1].length >= 4 ? codes[1] : null;
countryCode = codes[1].length < 4 ? codes[1] : null;
} else if (codes.length == 3) {
scriptCode = codes[1].length > codes[2].length ? codes[1] : codes[2];
countryCode = codes[1].length < codes[2].length ? codes[1] : codes[2];
}
assert(codes[0] != null && codes[0].isNotEmpty);
assert(countryCode == null || countryCode.isNotEmpty);
assert(scriptCode == null || scriptCode.isNotEmpty);
/// Adds scriptCodes to locales where we are able to assume it to provide
/// finer granularity when resolving locales.
///
/// The basis of the assumptions here are based off of known usage of scripts
/// across various countries. For example, we know Taiwan uses traditional (Hant)
/// script, so it is safe to apply (Hant) to Taiwanese languages.
if (deriveScriptCode && scriptCode == null) {
switch (languageCode) {
case 'zh': {
if (countryCode == null) {
scriptCode = 'Hans';
}
switch (countryCode) {
case 'CN':
case 'SG':
scriptCode = 'Hans';
break;
case 'TW':
case 'HK':
case 'MO':
scriptCode = 'Hant';
break;
}
break;
}
case 'sr': {
if (countryCode == null) {
scriptCode = 'Cyrl';
}
break;
}
}
// Increment length if we were able to assume a scriptCode.
if (scriptCode != null) {
length += 1;
}
// Update the base string to reflect assumed scriptCodes.
originalString = languageCode;
if (scriptCode != null) {
originalString += '_' + scriptCode;
}
if (countryCode != null) {
originalString += '_' + countryCode;
}
}
return LocaleInfo(
languageCode: languageCode,
scriptCode: scriptCode,
countryCode: countryCode,
length: length,
originalString: originalString,
);
}
final String languageCode;
final String scriptCode;
final String countryCode;
final int length; // The number of fields. Ranges from 1-3.
final String originalString; // Original un-parsed locale string.
String camelCase() {
return originalString
.split('_')
.map<String>((String part) => part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase())
.join('');
}
@override
bool operator ==(Object other) {
return other is LocaleInfo
&& other.originalString == originalString;
}
@override
int get hashCode {
return originalString.hashCode;
}
@override
String toString() {
return originalString;
}
@override
int compareTo(LocaleInfo other) {
return originalString.compareTo(other.originalString);
}
}
// See also //master/tools/gen_locale.dart in the engine repo.
Map<String, List<String>> _parseSection(String section) {
final Map<String, List<String>> result = <String, List<String>>{};
List<String> lastHeading;
for (final String line in section.split('\n')) {
if (line == '') {
continue;
}
if (line.startsWith(' ')) {
lastHeading[lastHeading.length - 1] = '${lastHeading.last}${line.substring(1)}';
continue;
}
final int colon = line.indexOf(':');
if (colon <= 0) {
throw 'not sure how to deal with "$line"';
}
final String name = line.substring(0, colon);
final String value = line.substring(colon + 2);
lastHeading = result.putIfAbsent(name, () => <String>[]);
result[name].add(value);
}
return result;
}
final Map<String, String> _languages = <String, String>{};
final Map<String, String> _regions = <String, String>{};
final Map<String, String> _scripts = <String, String>{};
const String kProvincePrefix = ', Province of ';
const String kParentheticalPrefix = ' (';
/// Prepares the data for the [describeLocale] method below.
///
/// The data is obtained from the official IANA registry.
void precacheLanguageAndRegionTags() {
final List<Map<String, List<String>>> sections =
languageSubtagRegistry.split('%%').skip(1).map<Map<String, List<String>>>(_parseSection).toList();
for (final Map<String, List<String>> section in sections) {
assert(section.containsKey('Type'), section.toString());
final String type = section['Type'].single;
if (type == 'language' || type == 'region' || type == 'script') {
assert(section.containsKey('Subtag') && section.containsKey('Description'), section.toString());
final String subtag = section['Subtag'].single;
String description = section['Description'].join(' ');
if (description.startsWith('United ')) {
description = 'the $description';
}
if (description.contains(kParentheticalPrefix)) {
description = description.substring(0, description.indexOf(kParentheticalPrefix));
}
if (description.contains(kProvincePrefix)) {
description = description.substring(0, description.indexOf(kProvincePrefix));
}
if (description.endsWith(' Republic')) {
description = 'the $description';
}
switch (type) {
case 'language':
_languages[subtag] = description;
break;
case 'region':
_regions[subtag] = description;
break;
case 'script':
_scripts[subtag] = description;
break;
}
}
}
}
String describeLocale(String tag) {
final List<String> subtags = tag.split('_');
assert(subtags.isNotEmpty);
assert(_languages.containsKey(subtags[0]));
final String language = _languages[subtags[0]];
String output = language;
String region;
String script;
if (subtags.length == 2) {
region = _regions[subtags[1]];
script = _scripts[subtags[1]];
assert(region != null || script != null);
} else if (subtags.length >= 3) {
region = _regions[subtags[2]];
script = _scripts[subtags[1]];
assert(region != null && script != null);
}
if (region != null) {
output += ', as used in $region';
}
if (script != null) {
output += ', using the $script script';
}
return output;
}
/// Return the input string as a Dart-parseable string.
///
/// ```
/// foo => 'foo'
/// foo "bar" => 'foo "bar"'
/// foo 'bar' => "foo 'bar'"
/// foo 'bar' "baz" => '''foo 'bar' "baz"'''
/// ```
///
/// This function is used by tools that take in a JSON-formatted file to
/// generate Dart code. For this reason, characters with special meaning
/// in JSON files. For example, the backspace character (\b) have to be
/// properly escaped by this function so that the generated Dart code
/// correctly represents this character:
/// ```
/// foo\bar => 'foo\\bar'
/// foo\nbar => 'foo\\nbar'
/// foo\\nbar => 'foo\\\\nbar'
/// foo\\bar => 'foo\\\\bar'
/// foo\ bar => 'foo\\ bar'
/// foo$bar = 'foo\$bar'
/// ```
String generateString(String value) {
const String backslash = '__BACKSLASH__';
assert(
!value.contains(backslash),
'Input string cannot contain the sequence: '
'"__BACKSLASH__", as it is used as part of '
'backslash character processing.'
);
value = value
// Replace backslashes with a placeholder for now to properly parse
// other special characters.
.replaceAll('\\', backslash)
.replaceAll('\$', '\\\$')
.replaceAll("'", "\\'")
.replaceAll('"', '\\"')
.replaceAll('\n', '\\n')
.replaceAll('\f', '\\f')
.replaceAll('\t', '\\t')
.replaceAll('\r', '\\r')
.replaceAll('\b', '\\b')
// Reintroduce escaped backslashes into generated Dart string.
.replaceAll(backslash, '\\\\');
return "'$value'";
}

View file

@ -0,0 +1,115 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/commands/generate_localizations.dart';
import '../../src/common.dart';
import '../../src/context.dart';
void main() {
testUsingContext('default l10n settings', () async {
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
final File arbFile = fileSystem.file(fileSystem.path.join('lib', 'l10n', 'app_en.arb'))
..createSync(recursive: true);
arbFile.writeAsStringSync('''{
"helloWorld": "Hello, World!",
"@helloWorld": {
"description": "Sample description"
}
}''');
fileSystem.file('l10n.yaml').createSync();
final File pubspecFile = fileSystem.file('pubspec.yaml')..createSync();
final String content = pubspecFile.readAsStringSync().replaceFirst(
'\nflutter:\n',
'\nflutter:\n generate: true\n',
);
pubspecFile.writeAsStringSync(content);
final GenerateLocalizationsCommand command = GenerateLocalizationsCommand(
fileSystem: fileSystem,
);
await createTestCommandRunner(command).run(<String>['gen-l10n']);
final FlutterCommandResult result = await command.runCommand();
expect(result.exitStatus, ExitStatus.success);
final Directory outputDirectory = fileSystem.directory(fileSystem.path.join('.dart_tool', 'flutter_gen', 'gen_l10n'));
expect(outputDirectory.existsSync(), true);
expect(outputDirectory.childFile('app_localizations_en.dart').existsSync(), true);
expect(outputDirectory.childFile('app_localizations.dart').existsSync(), true);
});
testUsingContext('not using synthetic packages', () async {
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
final Directory l10nDirectory = fileSystem.directory(
fileSystem.path.join('lib', 'l10n'),
);
final File arbFile = l10nDirectory.childFile(
'app_en.arb',
)..createSync(recursive: true);
arbFile.writeAsStringSync('''{
"helloWorld": "Hello, World!",
"@helloWorld": {
"description": "Sample description"
}
}''');
fileSystem.file('l10n.yaml').createSync();
final File pubspecFile = fileSystem.file('pubspec.yaml')..createSync();
final String content = pubspecFile.readAsStringSync().replaceFirst(
'\nflutter:\n',
'\nflutter:\n generate: true\n',
);
pubspecFile.writeAsStringSync(content);
final GenerateLocalizationsCommand command = GenerateLocalizationsCommand(
fileSystem: fileSystem,
);
await createTestCommandRunner(command).run(<String>[
'gen-l10n',
'--no-synthetic-package',
]);
final FlutterCommandResult result = await command.runCommand();
expect(result.exitStatus, ExitStatus.success);
expect(l10nDirectory.existsSync(), true);
expect(l10nDirectory.childFile('app_localizations_en.dart').existsSync(), true);
expect(l10nDirectory.childFile('app_localizations.dart').existsSync(), true);
});
testUsingContext('throws error when arguments are invalid', () async {
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
final File arbFile = fileSystem.file(fileSystem.path.join('lib', 'l10n', 'app_en.arb'))
..createSync(recursive: true);
arbFile.writeAsStringSync('''{
"helloWorld": "Hello, World!",
"@helloWorld": {
"description": "Sample description"
}
}''');
fileSystem.file('l10n.yaml').createSync();
final File pubspecFile = fileSystem.file('pubspec.yaml')..createSync();
final String content = pubspecFile.readAsStringSync().replaceFirst(
'\nflutter:\n',
'\nflutter:\n generate: true\n',
);
pubspecFile.writeAsStringSync(content);
fileSystem.file('header.txt').writeAsStringSync('a header file');
final GenerateLocalizationsCommand command = GenerateLocalizationsCommand(
fileSystem: fileSystem,
);
expect(
() => createTestCommandRunner(command).run(<String>[
'gen-l10n',
'--header="some header',
'--header-file="header.txt"',
]),
throwsA(isA<ToolExit>()),
);
});
}

View file

@ -7,6 +7,8 @@ import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/localizations.dart';
import 'package:flutter_tools/src/localizations/gen_l10n.dart';
import 'package:mockito/mockito.dart';
import '../../../src/common.dart';
import '../../../src/context.dart';
@ -17,28 +19,6 @@ void main() {
testUsingContext('generateLocalizations forwards arguments correctly', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final Logger logger = BufferLogger.test();
final String projectDir = fileSystem.path.join('path', 'to', 'flutter_project');
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: <String>[
'dart',
'--disable-dart-dev',
'dev/tools/localization/bin/gen_l10n.dart',
'--gen-inputs-and-outputs-list=/',
'--project-dir=$projectDir',
'--arb-dir=arb',
'--template-arb-file=example.arb',
'--output-localization-file=bar',
'--untranslated-messages-file=untranslated',
'--output-class=Foo',
'--header-file=header',
'--header=HEADER',
'--use-deferred-loading',
'--preferred-supported-locales=en_US',
'--no-synthetic-package',
],
),
]);
final Directory flutterProjectDirectory = fileSystem
.directory(fileSystem.path.join('path', 'to', 'flutter_project'))
..createSync(recursive: true);
@ -60,24 +40,40 @@ void main() {
untranslatedMessagesFile: Uri.file('untranslated'),
useSyntheticPackage: false,
);
await generateLocalizations(
final LocalizationsGenerator mockLocalizationsGenerator = MockLocalizationsGenerator();
generateLocalizations(
localizationsGenerator: mockLocalizationsGenerator,
options: options,
logger: logger,
fileSystem: fileSystem,
processManager: processManager,
projectDir: flutterProjectDirectory,
dartBinaryPath: 'dart',
flutterRoot: '',
projectDir: fileSystem.currentDirectory,
dependenciesDir: fileSystem.currentDirectory,
);
expect(processManager.hasRemainingExpectations, false);
verify(
mockLocalizationsGenerator.initialize(
inputPathString: 'arb',
outputPathString: null,
templateArbFileName: 'example.arb',
outputFileString: 'bar',
classNameString: 'Foo',
preferredSupportedLocaleString: 'en_US',
headerString: 'HEADER',
headerFile: 'header',
useDeferredLoading: true,
inputsAndOutputsListPath: '/',
useSyntheticPackage: false,
projectPathString: '/',
),
).called(1);
verify(mockLocalizationsGenerator.loadResources()).called(1);
verify(mockLocalizationsGenerator.writeOutputFiles()).called(1);
verify(mockLocalizationsGenerator.outputUnimplementedMessages('untranslated', logger)).called(1);
});
testUsingContext('generateLocalizations throws exception on missing flutter: generate: true flag', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final BufferLogger logger = BufferLogger.test();
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[]);
final Directory arbDirectory = fileSystem.directory('arb')
..createSync();
arbDirectory.childFile('foo.arb').createSync();
@ -103,15 +99,13 @@ flutter:
useSyntheticPackage: true,
);
final LocalizationsGenerator mockLocalizationsGenerator = MockLocalizationsGenerator();
expect(
() => generateLocalizations(
localizationsGenerator: mockLocalizationsGenerator,
options: options,
logger: logger,
fileSystem: fileSystem,
processManager: processManager,
projectDir: fileSystem.currentDirectory,
dartBinaryPath: 'dart',
flutterRoot: '',
dependenciesDir: fileSystem.currentDirectory,
),
throwsA(isA<Exception>()),
@ -186,3 +180,5 @@ use-deferred-loading: string
);
});
}
class MockLocalizationsGenerator extends Mock implements LocalizationsGenerator {}

View file

@ -5,18 +5,21 @@
import 'dart:convert';
import 'dart:io';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:path/path.dart' as path;
import '../../localization/gen_l10n.dart';
import '../../localization/gen_l10n_types.dart';
import '../../localization/localizations_utils.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/localizations/gen_l10n.dart';
import 'package:flutter_tools/src/localizations/gen_l10n_types.dart';
import 'package:flutter_tools/src/localizations/localizations_utils.dart';
import '../common.dart';
import 'package:test_api/test_api.dart' hide TypeMatcher, isInstanceOf; // ignore: deprecated_member_use
// ignore: deprecated_member_use
export 'package:test_core/test_core.dart' hide TypeMatcher, isInstanceOf, test; // Defines a 'package:test' shim.
final String defaultL10nPathString = path.join('lib', 'l10n');
final String syntheticPackagePath = path.join('.dart_tool', 'flutter_gen', 'gen_l10n');
final String defaultL10nPathString = globals.fs.path.join('lib', 'l10n');
final String syntheticPackagePath = globals.fs.path.join('.dart_tool', 'flutter_gen', 'gen_l10n');
const String defaultTemplateArbFileName = 'app_en.arb';
const String defaultOutputFileString = 'output-localization-file.dart';
const String defaultClassNameString = 'AppLocalizations';
@ -72,7 +75,7 @@ void main() {
final LocalizationsGenerator generator = LocalizationsGenerator(fs);
try {
generator.setInputDirectory('lib');
} on FileSystemException catch (e) {
} on L10nException catch (e) {
expect(e.message, contains('Make sure that the correct path was provided'));
return;
}
@ -218,7 +221,7 @@ void main() {
// Run localizations generator in specified absolute path.
final LocalizationsGenerator generator = LocalizationsGenerator(fs);
final String flutterProjectPath = path.join('absolute', 'path', 'to', 'flutter_project');
final String flutterProjectPath = fs.path.join('absolute', 'path', 'to', 'flutter_project');
try {
generator.initialize(
projectPathString: flutterProjectPath,
@ -238,7 +241,7 @@ void main() {
// Output files should be generated in the provided absolute path.
expect(
fs.isFileSync(path.join(
fs.isFileSync(fs.path.join(
flutterProjectPath,
'.dart_tool',
'flutter_gen',
@ -248,7 +251,7 @@ void main() {
true,
);
expect(
fs.isFileSync(path.join(
fs.isFileSync(fs.path.join(
flutterProjectPath,
'.dart_tool',
'flutter_gen',
@ -481,13 +484,16 @@ void main() {
)
..loadResources()
..writeOutputFiles()
..outputUnimplementedMessages(path.join('lib', 'l10n', 'unimplemented_message_translations.json'));
..outputUnimplementedMessages(
fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json'),
BufferLogger.test(),
);
} on L10nException catch (e) {
fail('Generating output should not fail: \n${e.message}');
}
final File unimplementedOutputFile = fs.file(
path.join('lib', 'l10n', 'unimplemented_message_translations.json'),
fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json'),
);
final String unimplementedOutputString = unimplementedOutputFile.readAsStringSync();
try {
@ -554,7 +560,7 @@ void main() {
generator
..initialize(
inputPathString: defaultL10nPathString,
outputPathString: path.join('lib', 'l10n', 'output'),
outputPathString: fs.path.join('lib', 'l10n', 'output'),
templateArbFileName: defaultTemplateArbFileName,
outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString,
@ -586,7 +592,7 @@ void main() {
generator
..initialize(
inputPathString: defaultL10nPathString,
outputPathString: path.join('lib', 'l10n', 'output'),
outputPathString: fs.path.join('lib', 'l10n', 'output'),
templateArbFileName: defaultTemplateArbFileName,
outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString,
@ -627,7 +633,7 @@ void main() {
}
final File inputsAndOutputsList = fs.file(
path.join(syntheticPackagePath, 'gen_l10n_inputs_and_outputs.json'),
fs.path.join(syntheticPackagePath, 'gen_l10n_inputs_and_outputs.json'),
);
expect(inputsAndOutputsList.existsSync(), isTrue);
@ -918,9 +924,9 @@ void main() {
fail('Setting language and locales should not fail: \n${e.message}');
}
expect(generator.arbPathStrings.first, path.join('lib', 'l10n', 'app_en.arb'));
expect(generator.arbPathStrings.elementAt(1), path.join('lib', 'l10n', 'app_es.arb'));
expect(generator.arbPathStrings.elementAt(2), path.join('lib', 'l10n', 'app_zh.arb'));
expect(generator.arbPathStrings.first, fs.path.join('lib', 'l10n', 'app_en.arb'));
expect(generator.arbPathStrings.elementAt(1), fs.path.join('lib', 'l10n', 'app_es.arb'));
expect(generator.arbPathStrings.elementAt(2), fs.path.join('lib', 'l10n', 'app_zh.arb'));
});
test('correctly parses @@locale property in arb file', () {
@ -1130,11 +1136,11 @@ void main() {
fail('Generating output files should not fail: $e');
}
expect(fs.isFileSync(path.join(syntheticPackagePath, 'output-localization-file_en.dart')), true);
expect(fs.isFileSync(path.join(syntheticPackagePath, 'output-localization-file_en_US.dart')), false);
expect(fs.isFileSync(fs.path.join(syntheticPackagePath, 'output-localization-file_en.dart')), true);
expect(fs.isFileSync(fs.path.join(syntheticPackagePath, 'output-localization-file_en_US.dart')), false);
final String englishLocalizationsFile = fs.file(
path.join(syntheticPackagePath, 'output-localization-file_en.dart')
fs.path.join(syntheticPackagePath, 'output-localization-file_en.dart')
).readAsStringSync();
expect(englishLocalizationsFile, contains('class AppLocalizationsEnCa extends AppLocalizationsEn'));
expect(englishLocalizationsFile, contains('class AppLocalizationsEn extends AppLocalizations'));
@ -1164,7 +1170,7 @@ void main() {
}
final String localizationsFile = fs.file(
path.join(syntheticPackagePath, defaultOutputFileString),
fs.path.join(syntheticPackagePath, defaultOutputFileString),
).readAsStringSync();
expect(localizationsFile, contains(
'''
@ -1194,7 +1200,7 @@ import 'output-localization-file_zh.dart';
}
final String localizationsFile = fs.file(
path.join(syntheticPackagePath, defaultOutputFileString),
fs.path.join(syntheticPackagePath, defaultOutputFileString),
).readAsStringSync();
expect(localizationsFile, contains(
'''

View file

@ -6,7 +6,6 @@ import 'dart:async';
import 'package:flutter_tools/src/base/dds.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/widget_cache.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
@ -1180,64 +1179,41 @@ void main() {
}));
testUsingContext('ResidentRunner can run source generation', () => testbed.run(() async {
final FakeProcessManager processManager = globals.processManager as FakeProcessManager;
final Directory dependencies = globals.fs.directory(
globals.fs.path.join('build', '6ec2559087977927717927ede0a147f1'));
processManager.addCommand(FakeCommand(
command: <String>[
globals.artifacts.getArtifactPath(Artifact.engineDartBinary),
'--disable-dart-dev',
globals.fs.path.join(Cache.flutterRoot, 'dev', 'tools', 'localization', 'bin', 'gen_l10n.dart'),
'--gen-inputs-and-outputs-list=${dependencies.absolute.path}',
'--project-dir=${globals.fs.currentDirectory.path}',
],
onRun: () {
dependencies
.childFile('gen_l10n_inputs_and_outputs.json')
..createSync()
..writeAsStringSync('{"inputs":[],"outputs":[]}');
}
));
globals.fs.file(globals.fs.path.join('lib', 'l10n', 'foo.arb'))
.createSync(recursive: true);
final File arbFile = globals.fs.file(globals.fs.path.join('lib', 'l10n', 'app_en.arb'))
..createSync(recursive: true);
arbFile.writeAsStringSync('''{
"helloWorld": "Hello, World!",
"@helloWorld": {
"description": "Sample description"
}
}''');
globals.fs.file('l10n.yaml').createSync();
globals.fs.file('pubspec.yaml').writeAsStringSync('flutter:\n generate: true\n');
await residentRunner.runSourceGenerators();
expect(testLogger.errorText, isEmpty);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[]),
expect(testLogger.statusText, contains('use the --untranslated-messages-file'));
}));
testUsingContext('ResidentRunner can run source generation - generation fails', () => testbed.run(() async {
final FakeProcessManager processManager = globals.processManager as FakeProcessManager;
final Directory dependencies = globals.fs.directory(
globals.fs.path.join('build', '6ec2559087977927717927ede0a147f1'));
processManager.addCommand(FakeCommand(
command: <String>[
globals.artifacts.getArtifactPath(Artifact.engineDartBinary),
'--disable-dart-dev',
globals.fs.path.join(Cache.flutterRoot, 'dev', 'tools', 'localization', 'bin', 'gen_l10n.dart'),
'--gen-inputs-and-outputs-list=${dependencies.absolute.path}',
'--project-dir=${globals.fs.currentDirectory.path}',
],
exitCode: 1,
stderr: 'stderr'
));
globals.fs.file(globals.fs.path.join('lib', 'l10n', 'foo.arb'))
.createSync(recursive: true);
// Intentionally define arb file with wrong name. generate_localizations defaults
// to app_en.arb.
final File arbFile = globals.fs.file(globals.fs.path.join('lib', 'l10n', 'foo.arb'))
..createSync(recursive: true);
arbFile.writeAsStringSync('''{
"helloWorld": "Hello, World!",
"@helloWorld": {
"description": "Sample description"
}
}''');
globals.fs.file('l10n.yaml').createSync();
globals.fs.file('pubspec.yaml').writeAsStringSync('flutter:\n generate: true\n');
await residentRunner.runSourceGenerators();
expect(testLogger.errorText, allOf(
contains('stderr'), // Message from gen_l10n.dart
contains('Exception') // Message from build_system
));
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[]),
expect(testLogger.errorText, allOf(contains('Exception')));
expect(testLogger.statusText, isEmpty);
}));
testUsingContext('ResidentRunner printHelpDetails', () => testbed.run(() {