mirror of
https://github.com/flutter/flutter
synced 2024-09-17 23:31:55 +00:00
Move gen_l10n into flutter_tools (#65025)
This commit is contained in:
parent
eddf0a8abf
commit
b80b432555
|
@ -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 app’s 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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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'");
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 app’s 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();
|
||||
}
|
||||
}
|
|
@ -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.'
|
|
@ -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
|
@ -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'";
|
||||
}
|
|
@ -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>()),
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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 {}
|
||||
|
|
|
@ -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(
|
||||
'''
|
|
@ -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(() {
|
||||
|
|
Loading…
Reference in a new issue