generate_localizations.dart 14.2 KB
Newer Older
1 2 3 4
// 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.

5 6 7
import 'package:process/process.dart';

import '../artifacts.dart';
8 9
import '../base/common.dart';
import '../base/file_system.dart';
10
import '../base/io.dart';
11
import '../base/logger.dart';
12
import '../globals.dart' as globals;
13 14 15 16 17 18 19 20 21 22 23 24 25
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({
26 27
    required FileSystem fileSystem,
    required Logger logger,
28 29
    required Artifacts artifacts,
    required ProcessManager processManager,
30
  }) :
31
    _fileSystem = fileSystem,
32 33 34
    _logger = logger,
    _artifacts = artifacts,
    _processManager = processManager {
35 36 37 38 39 40 41 42
    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 '
43 44 45 46 47 48 49 50
            '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".',
51 52 53 54 55
    );
    argParser.addOption(
      'template-arb-file',
      defaultsTo: 'app_en.arb',
      help: 'The template arb file that will be used as the basis for '
56
            'generating the Dart localization and messages files.',
57 58 59 60 61
    );
    argParser.addOption(
      'output-localization-file',
      defaultsTo: 'app_localizations.dart',
      help: 'The filename for the output localization and localizations '
62
            'delegate classes.',
63 64 65
    );
    argParser.addOption(
      'untranslated-messages-file',
66 67 68 69 70 71 72 73
      help: 'The location of a file that describes the localization '
            'messages have not been translated yet. Using this option will create '
            '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 '
            'have not been translated will be printed on the command line.',
74 75 76 77 78
    );
    argParser.addOption(
      'output-class',
      defaultsTo: 'AppLocalizations',
      help: 'The Dart class name to use for the output localization and '
79
            'localizations delegate classes.',
80
    );
81
    argParser.addMultiOption(
82
      'preferred-supported-locales',
83
      valueHelp: 'locale',
84
      help: 'The list of preferred supported locales for the application. '
85 86 87 88 89 90
            '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. '
            'For example, pass in "en_US" if you would like your app to '
            'default to American English on devices that support it. '
            'Pass this option multiple times to define multiple items.',
91 92 93 94
    );
    argParser.addOption(
      'header',
      help: 'The header to prepend to the generated Dart localizations '
95 96 97 98 99 100 101
            '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.'
102 103 104 105
    );
    argParser.addOption(
      'header-file',
      help: 'The header to prepend to the generated Dart localizations '
106 107 108 109 110 111 112 113
            '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".'
114 115 116
    );
    argParser.addFlag(
      'use-deferred-loading',
117 118 119 120 121 122 123 124 125 126 127 128 129 130
      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'
            'This flag does not affect other platforms such as mobile or desktop.',
131 132 133 134 135
    );
    argParser.addOption(
      'gen-inputs-and-outputs-list',
      valueHelp: 'path-to-output-directory',
      help: 'When specified, the tool generates a JSON file containing the '
136
            "tool's inputs and outputs named gen_l10n_inputs_and_outputs.json.\n"
137 138 139
            '\n'
            'This can be useful for keeping track of which files of the Flutter '
            'project were used when generating the latest set of localizations. '
140
            "For example, the Flutter tool's build system uses this file to "
141 142 143 144 145 146
            '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.'
147 148 149 150 151
    );
    argParser.addFlag(
      'synthetic-package',
      defaultsTo: true,
      help: 'Determines whether or not the generated output files will be '
152 153 154 155 156 157 158 159 160
            '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.',
161 162 163 164 165
    );
    argParser.addOption(
      'project-dir',
      valueHelp: 'absolute/path/to/flutter/project',
      help: 'When specified, the tool uses the path passed into this option '
166 167 168
            'as the directory of the root Flutter project.\n'
            '\n'
            'When null, the relative path to the present working directory will be used.'
169
    );
170 171
    argParser.addFlag(
      'required-resource-attributes',
172 173 174 175 176 177 178
      help: 'Requires all resource ids to contain a corresponding resource attribute.\n'
            '\n'
            'By default, simple messages will not require metadata, but it is highly '
            'recommended as this provides context for the meaning of a message to '
            'readers.\n'
            '\n'
            'Resource attributes are still required for plural messages.'
179
    );
180 181 182 183 184 185
    argParser.addFlag(
      'nullable-getter',
      help: 'Whether or not the localizations class getter is nullable.\n'
            '\n'
            'By default, this value is set to true so that '
            'Localizations.of(context) returns a nullable value '
186
            'for backwards compatibility. If this value is set to false, then '
187 188 189 190
            'a null check is performed on the returned value of '
            'Localizations.of(context), removing the need for null checking in '
            'user code.'
    );
191 192 193 194
    argParser.addFlag(
      'format',
      help: 'When specified, the "dart format" command is run after generating the localization files.'
    );
195 196 197 198 199 200 201 202 203
    argParser.addFlag(
      'use-escaping',
      help: 'Whether or not to use escaping for messages.\n'
            '\n'
            'By default, this value is set to false for backwards compatibility. '
            'Turning this flag on will cause the parser to treat any special characters '
            'contained within pairs of single quotes as normal strings and treat all '
            'consecutive pairs of single quotes as a single quote character.',
    );
204 205 206 207
    argParser.addFlag(
      'suppress-warnings',
      help: 'When specified, all warnings will be suppressed.\n'
    );
208 209 210
  }

  final FileSystem _fileSystem;
211
  final Logger _logger;
212 213
  final Artifacts _artifacts;
  final ProcessManager _processManager;
214 215

  @override
216
  String get description => 'Generate localizations for the current project.';
217 218 219 220

  @override
  String get name => 'gen-l10n';

221 222 223
  @override
  String get category => FlutterCommandCategory.project;

224 225
  @override
  Future<FlutterCommandResult> runCommand() async {
226 227
    final List<String> outputFileList;
    File? untranslatedMessagesFile;
228 229 230

    bool format = boolArg('format') ?? false;

231 232 233 234 235 236 237 238 239 240 241
    if (_fileSystem.file('l10n.yaml').existsSync()) {
      final LocalizationOptions options = parseLocalizationsOptions(
        file: _fileSystem.file('l10n.yaml'),
        logger: _logger,
      );
      _logger.printStatus(
        'Because l10n.yaml exists, the options defined there will be used '
        'instead.\n'
        'To use the command line arguments, delete the l10n.yaml file in the '
        'Flutter project.\n\n'
      );
242
      final LocalizationsGenerator generator = generateLocalizations(
243 244 245
        logger: _logger,
        options: options,
        projectDir: _fileSystem.currentDirectory,
246
        fileSystem: _fileSystem,
247 248 249
      );
      outputFileList = generator.outputFileList;
      untranslatedMessagesFile = generator.untranslatedMessagesFile;
250 251 252 253 254 255
      format = format || options.format;
    } else {
      final String inputPathString = stringArgDeprecated('arb-dir')!; // Has default value, cannot be null.
      final String? outputPathString = stringArgDeprecated('output-dir');
      final String outputFileString = stringArgDeprecated('output-localization-file')!; // Has default value, cannot be null.
      final String templateArbFileName = stringArgDeprecated('template-arb-file')!; // Has default value, cannot be null.
256
      final String? untranslatedMessagesFilePath = stringArgDeprecated('untranslated-messages-file');
257 258 259 260 261 262 263 264 265 266
      final String classNameString = stringArgDeprecated('output-class')!; // Has default value, cannot be null.
      final List<String> preferredSupportedLocales = stringsArg('preferred-supported-locales');
      final String? headerString = stringArgDeprecated('header');
      final String? headerFile = stringArgDeprecated('header-file');
      final bool useDeferredLoading = boolArgDeprecated('use-deferred-loading');
      final String? inputsAndOutputsListPath = stringArgDeprecated('gen-inputs-and-outputs-list');
      final bool useSyntheticPackage = boolArgDeprecated('synthetic-package');
      final String? projectPathString = stringArgDeprecated('project-dir');
      final bool areResourceAttributesRequired = boolArgDeprecated('required-resource-attributes');
      final bool usesNullableGetter = boolArgDeprecated('nullable-getter');
267
      final bool useEscaping = boolArgDeprecated('use-escaping');
268
      final bool suppressWarnings = boolArgDeprecated('suppress-warnings');
269

270
      precacheLanguageAndRegionTags();
271

272
      try {
273
        final LocalizationsGenerator generator = LocalizationsGenerator(
274 275 276 277 278 279 280 281 282 283 284 285 286 287
          fileSystem: _fileSystem,
          inputPathString: inputPathString,
          outputPathString: outputPathString,
          templateArbFileName: templateArbFileName,
          outputFileString: outputFileString,
          classNameString: classNameString,
          preferredSupportedLocales: preferredSupportedLocales,
          headerString: headerString,
          headerFile: headerFile,
          useDeferredLoading: useDeferredLoading,
          inputsAndOutputsListPath: inputsAndOutputsListPath,
          useSyntheticPackage: useSyntheticPackage,
          projectPathString: projectPathString,
          areResourceAttributesRequired: areResourceAttributesRequired,
288
          untranslatedMessagesFile: untranslatedMessagesFilePath,
289
          usesNullableGetter: usesNullableGetter,
290
          useEscaping: useEscaping,
291
          logger: _logger,
292
          suppressWarnings: suppressWarnings,
293 294
        )
          ..loadResources()
295 296 297
          ..writeOutputFiles();
        outputFileList = generator.outputFileList;
        untranslatedMessagesFile = generator.untranslatedMessagesFile;
298 299 300 301
      } on L10nException catch (e) {
        throwToolExit(e.message);
      }
    }
302

303 304 305 306 307
    // All other post processing.
    if (format) {
      if (outputFileList.isEmpty) {
        return FlutterCommandResult.success();
      }
308 309 310 311 312 313 314 315
      final List<String> formatFileList = outputFileList.toList();
      if (untranslatedMessagesFile != null) {
        // Don't format the messages file using `dart format`.
        formatFileList.remove(untranslatedMessagesFile.absolute.path);
      }
      if (formatFileList.isEmpty) {
        return FlutterCommandResult.success();
      }
316
      final String dartBinary = _artifacts.getArtifactPath(Artifact.engineDartBinary);
317
      final List<String> command = <String>[dartBinary, 'format', ...formatFileList];
318 319 320 321
      final ProcessResult result = await _processManager.run(command);
      if (result.exitCode != 0) {
        throwToolExit('Formatting failed: $result', exitCode: result.exitCode);
      }
322 323 324 325 326
    }

    return FlutterCommandResult.success();
  }
}