blob: 1a03fec6ac91629c5f85e6bfd3a0181381fd8fa0 [file] [log] [blame]
// 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 'package:yaml/yaml.dart';
import '../../base/file_system.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';
const String _kDependenciesFileName = 'gen_l10n_inputs_and_outputs.json';
/// Run the localizations generation script with the configuration [options].
void generateLocalizations({
@required Directory projectDir,
@required Directory dependenciesDir,
@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);
if (options.useSyntheticPackage && !flutterProject.manifest.generateSyntheticPackage) {
logger.printError(
'Attempted to generate localizations code without having '
'the flutter: generate flag turned on.'
'\n'
'Check pubspec.yaml and ensure that flutter: generate: true has '
'been added and rebuild the project. Otherwise, the localizations '
'source code will not be importable.'
);
throw Exception();
}
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',
preferredSupportedLocale: 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();
}
}
/// A build step that runs the generate localizations script from
/// dev/tool/localizations.
class GenerateLocalizationsTarget extends Target {
const GenerateLocalizationsTarget();
@override
List<Target> get dependencies => <Target>[];
@override
List<Source> get inputs => <Source>[
// This is added as a convenience for developing the tool.
const Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/localizations.dart'),
// TODO(jonahwilliams): once https://github.com/flutter/flutter/issues/56321 is
// complete, we should add the artifact as a dependency here. Since the tool runs
// this code from source, looking up each dependency will be cumbersome.
];
@override
String get name => 'gen_localizations';
@override
List<Source> get outputs => <Source>[];
@override
List<String> get depfiles => <String>['gen_localizations.d'];
@override
bool canSkip(Environment environment) {
final File configFile = environment.projectDir.childFile('l10n.yaml');
return !configFile.existsSync();
}
@override
Future<void> build(Environment environment) async {
final File configFile = environment.projectDir.childFile('l10n.yaml');
assert(configFile.existsSync());
final LocalizationOptions options = parseLocalizationsOptions(
file: configFile,
logger: globals.logger,
);
final DepfileService depfileService = DepfileService(
logger: environment.logger,
fileSystem: environment.fileSystem,
);
generateLocalizations(
logger: environment.logger,
options: options,
projectDir: environment.projectDir,
dependenciesDir: environment.buildDir,
localizationsGenerator: LocalizationsGenerator(environment.fileSystem),
);
final Map<String, Object> dependencies = json.decode(
environment.buildDir.childFile(_kDependenciesFileName).readAsStringSync()
) as Map<String, Object>;
final Depfile depfile = Depfile(
<File>[
configFile,
for (dynamic inputFile in dependencies['inputs'] as List<dynamic>)
environment.fileSystem.file(inputFile)
],
<File>[
for (dynamic outputFile in dependencies['outputs'] as List<dynamic>)
environment.fileSystem.file(outputFile)
]
);
depfileService.writeToFile(
depfile,
environment.buildDir.childFile('gen_localizations.d'),
);
}
}
/// Typed configuration from the localizations config file.
class LocalizationOptions {
const LocalizationOptions({
this.arbDirectory,
this.templateArbFile,
this.outputLocalizationsFile,
this.untranslatedMessagesFile,
this.header,
this.outputClass,
this.preferredSupportedLocales,
this.headerFile,
this.deferredLoading,
this.useSyntheticPackage = true,
}) : assert(useSyntheticPackage != null);
/// The `--arb-dir` argument.
///
/// The directory where all localization files should reside.
final Uri arbDirectory;
/// The `--template-arb-file` argument.
///
/// This URI is relative to [arbDirectory].
final Uri templateArbFile;
/// The `--output-localization-file` argument.
///
/// This URI is relative to [arbDirectory].
final Uri outputLocalizationsFile;
/// The `--untranslated-messages-file` argument.
///
/// This URI is relative to [arbDirectory].
final Uri untranslatedMessagesFile;
/// The `--header` argument.
///
/// The header to prepend to the generated Dart localizations.
final String header;
/// The `--output-class` argument.
final String outputClass;
/// The `--preferred-supported-locales` argument.
final List<String> preferredSupportedLocales;
/// The `--header-file` argument.
///
/// A file containing the header to prepend to the generated
/// Dart localizations.
final Uri headerFile;
/// The `--use-deferred-loading` argument.
///
/// Whether to generate the Dart localization file with locales imported
/// as deferred.
final bool deferredLoading;
/// The `--synthetic-package` argument.
///
/// Whether to generate the Dart localization files in a synthetic package
/// or in a custom directory.
final bool useSyntheticPackage;
}
/// Parse the localizations configuration options from [file].
///
/// Throws [Exception] if any of the contents are invalid. Returns a
/// [LocalizationOptions] with all fields as `null` if the config file exists
/// but is empty.
LocalizationOptions parseLocalizationsOptions({
@required File file,
@required Logger logger,
}) {
final String contents = file.readAsStringSync();
if (contents.trim().isEmpty) {
return const LocalizationOptions();
}
final YamlNode yamlNode = loadYamlNode(file.readAsStringSync());
if (yamlNode is! YamlMap) {
logger.printError('Expected ${file.path} to contain a map, instead was $yamlNode');
throw Exception();
}
final YamlMap yamlMap = yamlNode as YamlMap;
return LocalizationOptions(
arbDirectory: _tryReadUri(yamlMap, 'arb-dir', logger),
templateArbFile: _tryReadUri(yamlMap, 'template-arb-file', logger),
outputLocalizationsFile: _tryReadUri(yamlMap, 'output-localization-file', logger),
untranslatedMessagesFile: _tryReadUri(yamlMap, 'untranslated-messages-file', logger),
header: _tryReadString(yamlMap, 'header', logger),
outputClass: _tryReadString(yamlMap, 'output-class', logger),
preferredSupportedLocales: _tryReadStringList(yamlMap, 'preferred-supported-locales', logger),
headerFile: _tryReadUri(yamlMap, 'header-file', logger),
deferredLoading: _tryReadBool(yamlMap, 'use-deferred-loading', logger),
useSyntheticPackage: _tryReadBool(yamlMap, 'synthetic-package', logger) ?? true,
);
}
// Try to read a `bool` value or null from `yamlMap`, otherwise throw.
bool _tryReadBool(YamlMap yamlMap, String key, Logger logger) {
final Object value = yamlMap[key];
if (value == null) {
return null;
}
if (value is! bool) {
logger.printError('Expected "$key" to have a bool value, instead was "$value"');
throw Exception();
}
return value as bool;
}
// Try to read a `String` value or null from `yamlMap`, otherwise throw.
String _tryReadString(YamlMap yamlMap, String key, Logger logger) {
final Object value = yamlMap[key];
if (value == null) {
return null;
}
if (value is! String) {
logger.printError('Expected "$key" to have a String value, instead was "$value"');
throw Exception();
}
return value as String;
}
List<String> _tryReadStringList(YamlMap yamlMap, String key, Logger logger) {
final Object value = yamlMap[key];
if (value == null) {
return null;
}
if (value is String) {
return <String>[value];
}
if (value is Iterable) {
return value.map((dynamic e) => e.toString()).toList();
}
logger.printError('"$value" must be String or List.');
throw Exception();
}
// Try to read a valid `Uri` or null from `yamlMap`, otherwise throw.
Uri _tryReadUri(YamlMap yamlMap, String key, Logger logger) {
final String value = _tryReadString(yamlMap, key, logger);
if (value == null) {
return null;
}
final Uri uri = Uri.tryParse(value);
if (uri == null) {
logger.printError('"$value" must be a relative file URI');
}
return uri;
}