| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | import 'package:meta/meta.dart' ; |
| 6 | import 'package:yaml/yaml.dart' ; |
| 7 | |
| 8 | import '../base/common.dart'; |
| 9 | import '../base/file_system.dart'; |
| 10 | import '../base/logger.dart'; |
| 11 | import '../globals.dart' as globals; |
| 12 | import '../runner/flutter_command.dart'; |
| 13 | import 'gen_l10n_types.dart'; |
| 14 | import 'language_subtag_registry.dart'; |
| 15 | |
| 16 | typedef HeaderGenerator = String Function(String regenerateInstructions); |
| 17 | typedef ConstructorGenerator = String Function(LocaleInfo locale); |
| 18 | |
| 19 | int sortFilesByPath(File a, File b) { |
| 20 | return a.path.compareTo(b.path); |
| 21 | } |
| 22 | |
| 23 | /// Simple data class to hold parsed locale. Does not promise validity of any data. |
| 24 | @immutable |
| 25 | class LocaleInfo implements Comparable<LocaleInfo> { |
| 26 | const LocaleInfo({ |
| 27 | required this.languageCode, |
| 28 | required this.scriptCode, |
| 29 | required this.countryCode, |
| 30 | required this.length, |
| 31 | required this.originalString, |
| 32 | }); |
| 33 | |
| 34 | /// Simple parser. Expects the locale string to be in the form of 'language_script_COUNTRY' |
| 35 | /// where the language is 2 characters, script is 4 characters with the first uppercase, |
| 36 | /// and country is 2-3 characters and all uppercase. |
| 37 | /// |
| 38 | /// 'language_COUNTRY' or 'language_script' are also valid. Missing fields will be null. |
| 39 | /// |
| 40 | /// When `deriveScriptCode` is true, if [scriptCode] was unspecified, it will |
| 41 | /// be derived from the [languageCode] and [countryCode] if possible. |
| 42 | factory LocaleInfo.fromString(String locale, {bool deriveScriptCode = false}) { |
| 43 | final List<String> codes = locale.split('_' ); // [language, script, country] |
| 44 | assert(codes.isNotEmpty && codes.length < 4); |
| 45 | final String languageCode = codes[0]; |
| 46 | String? scriptCode; |
| 47 | String? countryCode; |
| 48 | int length = codes.length; |
| 49 | var originalString = locale; |
| 50 | if (codes.length == 2) { |
| 51 | scriptCode = codes[1].length >= 4 ? codes[1] : null; |
| 52 | countryCode = codes[1].length < 4 ? codes[1] : null; |
| 53 | } else if (codes.length == 3) { |
| 54 | scriptCode = codes[1].length > codes[2].length ? codes[1] : codes[2]; |
| 55 | countryCode = codes[1].length < codes[2].length ? codes[1] : codes[2]; |
| 56 | } |
| 57 | assert(codes[0].isNotEmpty); |
| 58 | assert(countryCode == null || countryCode.isNotEmpty); |
| 59 | assert(scriptCode == null || scriptCode.isNotEmpty); |
| 60 | |
| 61 | /// Adds scriptCodes to locales where we are able to assume it to provide |
| 62 | /// finer granularity when resolving locales. |
| 63 | /// |
| 64 | /// The basis of the assumptions here are based off of known usage of scripts |
| 65 | /// across various countries. For example, we know Taiwan uses traditional (Hant) |
| 66 | /// script, so it is safe to apply (Hant) to Taiwanese languages. |
| 67 | if (deriveScriptCode && scriptCode == null) { |
| 68 | scriptCode = switch ((languageCode, countryCode)) { |
| 69 | ('zh' , 'CN' || 'SG' || null) => 'Hans' , |
| 70 | ('zh' , 'TW' || 'HK' || 'MO' ) => 'Hant' , |
| 71 | ('sr' , null) => 'Cyrl' , |
| 72 | _ => null, |
| 73 | }; |
| 74 | // Increment length if we were able to assume a scriptCode. |
| 75 | if (scriptCode != null) { |
| 76 | length += 1; |
| 77 | } |
| 78 | // Update the base string to reflect assumed scriptCodes. |
| 79 | originalString = languageCode; |
| 80 | if (scriptCode != null) { |
| 81 | originalString += '_ $scriptCode' ; |
| 82 | } |
| 83 | if (countryCode != null) { |
| 84 | originalString += '_ $countryCode' ; |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | return LocaleInfo( |
| 89 | languageCode: languageCode, |
| 90 | scriptCode: scriptCode, |
| 91 | countryCode: countryCode, |
| 92 | length: length, |
| 93 | originalString: originalString, |
| 94 | ); |
| 95 | } |
| 96 | |
| 97 | final String languageCode; |
| 98 | final String? scriptCode; |
| 99 | final String? countryCode; |
| 100 | final int length; // The number of fields. Ranges from 1-3. |
| 101 | final String originalString; // Original un-parsed locale string. |
| 102 | |
| 103 | String camelCase() { |
| 104 | return originalString |
| 105 | .split('_' ) |
| 106 | .map<String>( |
| 107 | (String part) => part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase(), |
| 108 | ) |
| 109 | .join(); |
| 110 | } |
| 111 | |
| 112 | @override |
| 113 | bool operator ==(Object other) { |
| 114 | return other is LocaleInfo && other.originalString == originalString; |
| 115 | } |
| 116 | |
| 117 | @override |
| 118 | int get hashCode => originalString.hashCode; |
| 119 | |
| 120 | @override |
| 121 | String toString() { |
| 122 | return originalString; |
| 123 | } |
| 124 | |
| 125 | @override |
| 126 | int compareTo(LocaleInfo other) { |
| 127 | return originalString.compareTo(other.originalString); |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | // See also //master/tools/gen_locale.dart in the engine repo. |
| 132 | Map<String, List<String>> _parseSection(String section) { |
| 133 | final result = <String, List<String>>{}; |
| 134 | late List<String> lastHeading; |
| 135 | for (final String line in section.split('\n' )) { |
| 136 | if (line == '' ) { |
| 137 | continue; |
| 138 | } |
| 139 | if (line.startsWith(' ' )) { |
| 140 | lastHeading[lastHeading.length - 1] = ' ${lastHeading.last}${line.substring(1)}' ; |
| 141 | continue; |
| 142 | } |
| 143 | final int colon = line.indexOf(':' ); |
| 144 | if (colon <= 0) { |
| 145 | throw Exception('not sure how to deal with " $line"' ); |
| 146 | } |
| 147 | final String name = line.substring(0, colon); |
| 148 | final String value = line.substring(colon + 2); |
| 149 | lastHeading = result.putIfAbsent(name, () => <String>[]); |
| 150 | result[name]!.add(value); |
| 151 | } |
| 152 | return result; |
| 153 | } |
| 154 | |
| 155 | final _languages = <String, String>{}; |
| 156 | final _regions = <String, String>{}; |
| 157 | final _scripts = <String, String>{}; |
| 158 | const kProvincePrefix = ', Province of ' ; |
| 159 | const kParentheticalPrefix = ' (' ; |
| 160 | |
| 161 | /// Prepares the data for the [describeLocale] method below. |
| 162 | /// |
| 163 | /// The data is obtained from the official IANA registry. |
| 164 | void precacheLanguageAndRegionTags() { |
| 165 | final List<Map<String, List<String>>> sections = languageSubtagRegistry |
| 166 | .split('%%' ) |
| 167 | .skip(1) |
| 168 | .map<Map<String, List<String>>>(_parseSection) |
| 169 | .toList(); |
| 170 | for (final section in sections) { |
| 171 | assert(section.containsKey('Type' ), section.toString()); |
| 172 | final String type = section['Type' ]!.single; |
| 173 | if (type == 'language' || type == 'region' || type == 'script' ) { |
| 174 | assert( |
| 175 | section.containsKey('Subtag' ) && section.containsKey('Description' ), |
| 176 | section.toString(), |
| 177 | ); |
| 178 | final String subtag = section['Subtag' ]!.single; |
| 179 | String description = section['Description' ]!.join(' ' ); |
| 180 | if (description.startsWith('United ' )) { |
| 181 | description = 'the $description' ; |
| 182 | } |
| 183 | if (description.contains(kParentheticalPrefix)) { |
| 184 | description = description.substring(0, description.indexOf(kParentheticalPrefix)); |
| 185 | } |
| 186 | if (description.contains(kProvincePrefix)) { |
| 187 | description = description.substring(0, description.indexOf(kProvincePrefix)); |
| 188 | } |
| 189 | if (description.endsWith(' Republic' )) { |
| 190 | description = 'the $description' ; |
| 191 | } |
| 192 | switch (type) { |
| 193 | case 'language' : |
| 194 | _languages[subtag] = description; |
| 195 | case 'region' : |
| 196 | _regions[subtag] = description; |
| 197 | case 'script' : |
| 198 | _scripts[subtag] = description; |
| 199 | } |
| 200 | } |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | String describeLocale(String tag) { |
| 205 | final List<String> subtags = tag.split('_' ); |
| 206 | assert(subtags.isNotEmpty); |
| 207 | final String languageCode = subtags[0]; |
| 208 | if (!_languages.containsKey(languageCode)) { |
| 209 | throw L10nException( |
| 210 | '" $languageCode" is not a supported language code.\n' |
| 211 | 'See https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry ' |
| 212 | 'for the supported list.' , |
| 213 | ); |
| 214 | } |
| 215 | final String language = _languages[languageCode]!; |
| 216 | var output = language; |
| 217 | String? region; |
| 218 | String? script; |
| 219 | if (subtags.length == 2) { |
| 220 | region = _regions[subtags[1]]; |
| 221 | script = _scripts[subtags[1]]; |
| 222 | assert(region != null || script != null); |
| 223 | } else if (subtags.length >= 3) { |
| 224 | region = _regions[subtags[2]]; |
| 225 | script = _scripts[subtags[1]]; |
| 226 | assert(region != null && script != null); |
| 227 | } |
| 228 | if (region != null) { |
| 229 | output += ', as used in $region' ; |
| 230 | } |
| 231 | if (script != null) { |
| 232 | output += ', using the $script script' ; |
| 233 | } |
| 234 | return output; |
| 235 | } |
| 236 | |
| 237 | /// Return the input string as a Dart-parsable string. |
| 238 | /// |
| 239 | /// ```none |
| 240 | /// foo => 'foo' |
| 241 | /// foo "bar" => 'foo "bar"' |
| 242 | /// foo 'bar' => "foo 'bar'" |
| 243 | /// foo 'bar' "baz" => '''foo 'bar' "baz"''' |
| 244 | /// ``` |
| 245 | /// |
| 246 | /// This function is used by tools that take in a JSON-formatted file to |
| 247 | /// generate Dart code. For this reason, characters with special meaning |
| 248 | /// in JSON files are escaped. For example, the backspace character (\b) |
| 249 | /// has to be properly escaped by this function so that the generated |
| 250 | /// Dart code correctly represents this character: |
| 251 | /// ```none |
| 252 | /// foo\bar => 'foo\\bar' |
| 253 | /// foo\nbar => 'foo\\nbar' |
| 254 | /// foo\\nbar => 'foo\\\\nbar' |
| 255 | /// foo\\bar => 'foo\\\\bar' |
| 256 | /// foo\ bar => 'foo\\ bar' |
| 257 | /// foo$bar = 'foo\$bar' |
| 258 | /// ``` |
| 259 | String generateString(String value) { |
| 260 | const backslash = '__BACKSLASH__' ; |
| 261 | assert( |
| 262 | !value.contains(backslash), |
| 263 | 'Input string cannot contain the sequence: ' |
| 264 | '"__BACKSLASH__", as it is used as part of ' |
| 265 | 'backslash character processing.' , |
| 266 | ); |
| 267 | |
| 268 | value = value |
| 269 | // Replace backslashes with a placeholder for now to properly parse |
| 270 | // other special characters. |
| 271 | .replaceAll(r'\' , backslash) |
| 272 | .replaceAll(r'$' , r'\$' ) |
| 273 | .replaceAll("'" , r"\'" ) |
| 274 | .replaceAll('"' , r'\"' ) |
| 275 | .replaceAll('\n' , r'\n' ) |
| 276 | .replaceAll('\f' , r'\f' ) |
| 277 | .replaceAll('\t' , r'\t' ) |
| 278 | .replaceAll('\r' , r'\r' ) |
| 279 | .replaceAll('\b' , r'\b' ) |
| 280 | // Reintroduce escaped backslashes into generated Dart string. |
| 281 | .replaceAll(backslash, r'\\' ); |
| 282 | |
| 283 | return value; |
| 284 | } |
| 285 | |
| 286 | /// Given a list of normal strings or interpolated variables, concatenate them |
| 287 | /// into a single dart string to be returned. An example of a normal string |
| 288 | /// would be "'Hello world!'" and an example of a interpolated variable would be |
| 289 | /// "'$placeholder'". |
| 290 | /// |
| 291 | /// Each of the strings in [expressions] should be a raw string, which, if it |
| 292 | /// were to be added to a dart file, would be a properly formatted dart string |
| 293 | /// with escapes and/or interpolation. The purpose of this function is to |
| 294 | /// concatenate these dart strings into a single dart string which can be |
| 295 | /// returned in the generated localization files. |
| 296 | /// |
| 297 | /// The following rules describe the kinds of string expressions that can be |
| 298 | /// handled: |
| 299 | /// 1. If [expressions] is empty, return the empty string "''". |
| 300 | /// 2. If [expressions] has only one [String] which is an interpolated variable, |
| 301 | /// it is converted to the variable itself e.g. ["'$expr'"] -> "expr". |
| 302 | /// 3. If one string in [expressions] is an interpolation and the next begins |
| 303 | /// with an alphanumeric character, then the former interpolation should be |
| 304 | /// wrapped in braces e.g. ["'$expr1'", "'another'"] -> "'${expr1}another'". |
| 305 | String generateReturnExpr(List<String> expressions, {bool isSingleStringVar = false}) { |
| 306 | if (expressions.isEmpty) { |
| 307 | return "''" ; |
| 308 | } else if (isSingleStringVar) { |
| 309 | // If our expression is "$varName" where varName is a String, this is equivalent to just varName. |
| 310 | return expressions[0].substring(1); |
| 311 | } else { |
| 312 | final String string = expressions.reversed.fold<String>('' , (String string, String expression) { |
| 313 | if (expression[0] != r'$' ) { |
| 314 | return expression + string; |
| 315 | } |
| 316 | final alphanumeric = RegExp(r'^([0-9a-zA-Z]|_)+$' ); |
| 317 | if (alphanumeric.hasMatch(expression.substring(1)) && |
| 318 | !(string.isNotEmpty && alphanumeric.hasMatch(string[0]))) { |
| 319 | return ' $expression$string' ; |
| 320 | } else {
|
| 321 | return '\${ ${expression.substring(1)}} $string' ;
|
| 322 | }
|
| 323 | });
|
| 324 | return "' $string'" ;
|
| 325 | }
|
| 326 | }
|
| 327 |
|
| 328 | /// Typed configuration from the localizations config file.
|
| 329 | class LocalizationOptions {
|
| 330 | LocalizationOptions({
|
| 331 | required this.arbDir,
|
| 332 | this.outputDir,
|
| 333 | String? templateArbFile,
|
| 334 | String? outputLocalizationFile,
|
| 335 | this.untranslatedMessagesFile,
|
| 336 | String? outputClass,
|
| 337 | this.preferredSupportedLocales,
|
| 338 | this.header,
|
| 339 | this.headerFile,
|
| 340 | bool? useDeferredLoading,
|
| 341 | this.genInputsAndOutputsList,
|
| 342 | this.projectDir,
|
| 343 | bool? requiredResourceAttributes,
|
| 344 | bool? nullableGetter,
|
| 345 | bool? format,
|
| 346 | bool? useEscaping,
|
| 347 | bool? suppressWarnings,
|
| 348 | bool? relaxSyntax,
|
| 349 | bool? useNamedParameters,
|
| 350 | }) : templateArbFile = templateArbFile ?? 'app_en.arb' ,
|
| 351 | outputLocalizationFile = outputLocalizationFile ?? 'app_localizations.dart' ,
|
| 352 | outputClass = outputClass ?? 'AppLocalizations' ,
|
| 353 | useDeferredLoading = useDeferredLoading ?? false,
|
| 354 | requiredResourceAttributes = requiredResourceAttributes ?? false,
|
| 355 | nullableGetter = nullableGetter ?? true,
|
| 356 | format = format ?? true,
|
| 357 | useEscaping = useEscaping ?? false,
|
| 358 | suppressWarnings = suppressWarnings ?? false,
|
| 359 | relaxSyntax = relaxSyntax ?? false,
|
| 360 | useNamedParameters = useNamedParameters ?? false;
|
| 361 |
|
| 362 | /// The `--arb-dir` argument.
|
| 363 | ///
|
| 364 | /// The directory where all input localization files should reside.
|
| 365 | final String arbDir;
|
| 366 |
|
| 367 | /// The `--output-dir` argument.
|
| 368 | ///
|
| 369 | /// The directory where all output localization files should be generated.
|
| 370 | final String? outputDir;
|
| 371 |
|
| 372 | /// The `--template-arb-file` argument.
|
| 373 | ///
|
| 374 | /// This path is relative to [arbDir].
|
| 375 | final String templateArbFile;
|
| 376 |
|
| 377 | /// The `--output-localization-file` argument.
|
| 378 | ///
|
| 379 | /// This path is relative to [arbDir].
|
| 380 | final String outputLocalizationFile;
|
| 381 |
|
| 382 | /// The `--untranslated-messages-file` argument.
|
| 383 | ///
|
| 384 | /// This path is relative to [arbDir].
|
| 385 | final String? untranslatedMessagesFile;
|
| 386 |
|
| 387 | /// The `--output-class` argument.
|
| 388 | final String outputClass;
|
| 389 |
|
| 390 | /// The `--preferred-supported-locales` argument.
|
| 391 | final List<String>? preferredSupportedLocales;
|
| 392 |
|
| 393 | /// The `--header` argument.
|
| 394 | ///
|
| 395 | /// The header to prepend to the generated Dart localizations.
|
| 396 | final String? header;
|
| 397 |
|
| 398 | /// The `--header-file` argument.
|
| 399 | ///
|
| 400 | /// A file containing the header to prepend to the generated
|
| 401 | /// Dart localizations.
|
| 402 | final String? headerFile;
|
| 403 |
|
| 404 | /// The `--use-deferred-loading` argument.
|
| 405 | ///
|
| 406 | /// Whether to generate the Dart localization file with locales imported
|
| 407 | /// as deferred.
|
| 408 | final bool useDeferredLoading;
|
| 409 |
|
| 410 | /// The `--gen-inputs-and-outputs-list` argument.
|
| 411 | ///
|
| 412 | /// This path is relative to [arbDir].
|
| 413 | final String? genInputsAndOutputsList;
|
| 414 |
|
| 415 | /// The `--project-dir` argument.
|
| 416 | ///
|
| 417 | /// This path is relative to [arbDir].
|
| 418 | final String? projectDir;
|
| 419 |
|
| 420 | /// The `required-resource-attributes` argument.
|
| 421 | ///
|
| 422 | /// Whether to require all resource ids to contain a corresponding
|
| 423 | /// resource attribute.
|
| 424 | final bool requiredResourceAttributes;
|
| 425 |
|
| 426 | /// The `nullable-getter` argument.
|
| 427 | ///
|
| 428 | /// Whether or not the localizations class getter is nullable.
|
| 429 | final bool nullableGetter;
|
| 430 |
|
| 431 | /// The `format` argument.
|
| 432 | ///
|
| 433 | /// Whether or not to format the generated files.
|
| 434 | final bool format;
|
| 435 |
|
| 436 | /// The `use-escaping` argument.
|
| 437 | ///
|
| 438 | /// Whether or not the ICU escaping syntax is used.
|
| 439 | final bool useEscaping;
|
| 440 |
|
| 441 | /// The `suppress-warnings` argument.
|
| 442 | ///
|
| 443 | /// Whether or not to suppress warnings.
|
| 444 | final bool suppressWarnings;
|
| 445 |
|
| 446 | /// The `relax-syntax` argument.
|
| 447 | ///
|
| 448 | /// Whether or not to relax the syntax. When specified, the syntax will be
|
| 449 | /// relaxed so that the special character "{" is treated as a string if it is
|
| 450 | /// not followed by a valid placeholder and "}" is treated as a string if it
|
| 451 | /// does not close any previous "{" that is treated as a special character.
|
| 452 | /// This was added in for backward compatibility and is not recommended
|
| 453 | /// as it may mask errors.
|
| 454 | final bool relaxSyntax;
|
| 455 |
|
| 456 | /// The `use-named-parameters` argument.
|
| 457 | ///
|
| 458 | /// Whether or not to use named parameters for the generated localization
|
| 459 | /// methods.
|
| 460 | ///
|
| 461 | /// Defaults to `false`.
|
| 462 | final bool useNamedParameters;
|
| 463 | }
|
| 464 |
|
| 465 | /// Parse the localizations configuration options from [file].
|
| 466 | ///
|
| 467 | /// Throws [Exception] if any of the contents are invalid. Returns a
|
| 468 | /// [LocalizationOptions] with all fields as `null` if the config file exists
|
| 469 | /// but is empty.
|
| 470 | LocalizationOptions parseLocalizationsOptionsFromYAML({
|
| 471 | required File file,
|
| 472 | required Logger logger,
|
| 473 | required FileSystem fileSystem,
|
| 474 | required String defaultArbDir,
|
| 475 | }) {
|
| 476 | final String contents = file.readAsStringSync();
|
| 477 | if (contents.trim().isEmpty) {
|
| 478 | return LocalizationOptions(arbDir: defaultArbDir);
|
| 479 | }
|
| 480 | final YamlNode yamlNode;
|
| 481 | try {
|
| 482 | yamlNode = loadYamlNode(file.readAsStringSync());
|
| 483 | } on YamlException catch (err) {
|
| 484 | throwToolExit(err.message);
|
| 485 | }
|
| 486 | if (yamlNode is! YamlMap) {
|
| 487 | logger.printError('Expected ${file.path} to contain a map, instead was $yamlNode' );
|
| 488 | throw Exception();
|
| 489 | }
|
| 490 | const kSyntheticPackage = 'synthetic-package' ;
|
| 491 | const kFlutterGenNotice = 'http://flutter.dev/to/flutter-gen-deprecation';
|
| 492 | final bool? syntheticPackage = _tryReadBool(yamlNode, kSyntheticPackage, logger);
|
| 493 | if (syntheticPackage != null) {
|
| 494 | if (syntheticPackage) {
|
| 495 | throwToolExit(
|
| 496 | ' ${file.path}: Cannot enable " $kSyntheticPackage", this feature has '
|
| 497 | 'been removed. See $kFlutterGenNotice.' ,
|
| 498 | );
|
| 499 | } else {
|
| 500 | logger.printWarning(
|
| 501 | ' ${file.path}: The argument " $kSyntheticPackage" no longer has any '
|
| 502 | 'effect and should be removed. See $kFlutterGenNotice' ,
|
| 503 | );
|
| 504 | }
|
| 505 | }
|
| 506 | return LocalizationOptions(
|
| 507 | arbDir: _tryReadFilePath(yamlNode, 'arb-dir' , logger, fileSystem) ?? defaultArbDir,
|
| 508 | outputDir: _tryReadFilePath(yamlNode, 'output-dir' , logger, fileSystem),
|
| 509 | templateArbFile: _tryReadFilePath(yamlNode, 'template-arb-file' , logger, fileSystem),
|
| 510 | outputLocalizationFile: _tryReadFilePath(
|
| 511 | yamlNode,
|
| 512 | 'output-localization-file' ,
|
| 513 | logger,
|
| 514 | fileSystem,
|
| 515 | ),
|
| 516 | untranslatedMessagesFile: _tryReadFilePath(
|
| 517 | yamlNode,
|
| 518 | 'untranslated-messages-file' ,
|
| 519 | logger,
|
| 520 | fileSystem,
|
| 521 | ),
|
| 522 | outputClass: _tryReadString(yamlNode, 'output-class' , logger),
|
| 523 | header: _tryReadString(yamlNode, 'header' , logger),
|
| 524 | headerFile: _tryReadFilePath(yamlNode, 'header-file' , logger, fileSystem),
|
| 525 | useDeferredLoading: _tryReadBool(yamlNode, 'use-deferred-loading' , logger),
|
| 526 | preferredSupportedLocales: _tryReadStringList(yamlNode, 'preferred-supported-locales' , logger),
|
| 527 | requiredResourceAttributes: _tryReadBool(yamlNode, 'required-resource-attributes' , logger),
|
| 528 | nullableGetter: _tryReadBool(yamlNode, 'nullable-getter' , logger),
|
| 529 | format: _tryReadBool(yamlNode, 'format' , logger),
|
| 530 | useEscaping: _tryReadBool(yamlNode, 'use-escaping' , logger),
|
| 531 | suppressWarnings: _tryReadBool(yamlNode, 'suppress-warnings' , logger),
|
| 532 | relaxSyntax: _tryReadBool(yamlNode, 'relax-syntax' , logger),
|
| 533 | useNamedParameters: _tryReadBool(yamlNode, 'use-named-parameters' , logger),
|
| 534 | );
|
| 535 | }
|
| 536 |
|
| 537 | /// Parse the localizations configuration from [FlutterCommand].
|
| 538 | LocalizationOptions parseLocalizationsOptionsFromCommand({
|
| 539 | required FlutterCommand command,
|
| 540 | required String defaultArbDir,
|
| 541 | }) {
|
| 542 | const kSyntheticPackage = 'synthetic-package' ;
|
| 543 | const kFlutterGenNotice = 'http://flutter.dev/to/flutter-gen-deprecation';
|
| 544 | if (command.argResults!.wasParsed(kSyntheticPackage)) {
|
| 545 | if (command.boolArg(kSyntheticPackage)) {
|
| 546 | throwToolExit(
|
| 547 | 'Cannot enable " $kSyntheticPackage", this feature has been removed. '
|
| 548 | 'See $kFlutterGenNotice.' ,
|
| 549 | );
|
| 550 | } else {
|
| 551 | globals.logger.printWarning(
|
| 552 | 'The argument " $kSyntheticPackage" no longer has any effect and should '
|
| 553 | 'be removed. See $kFlutterGenNotice' ,
|
| 554 | );
|
| 555 | }
|
| 556 | }
|
| 557 | return LocalizationOptions(
|
| 558 | arbDir: command.stringArg('arb-dir' ) ?? defaultArbDir,
|
| 559 | outputDir: command.stringArg('output-dir' ),
|
| 560 | outputLocalizationFile: command.stringArg('output-localization-file' ),
|
| 561 | templateArbFile: command.stringArg('template-arb-file' ),
|
| 562 | untranslatedMessagesFile: command.stringArg('untranslated-messages-file' ),
|
| 563 | outputClass: command.stringArg('output-class' ),
|
| 564 | header: command.stringArg('header' ),
|
| 565 | headerFile: command.stringArg('header-file' ),
|
| 566 | useDeferredLoading: command.boolArg('use-deferred-loading' ),
|
| 567 | genInputsAndOutputsList: command.stringArg('gen-inputs-and-outputs-list' ),
|
| 568 | projectDir: command.stringArg('project-dir' ),
|
| 569 | requiredResourceAttributes: command.boolArg('required-resource-attributes' ),
|
| 570 | nullableGetter: command.boolArg('nullable-getter' ),
|
| 571 | format: command.boolArg('format' ),
|
| 572 | useEscaping: command.boolArg('use-escaping' ),
|
| 573 | suppressWarnings: command.boolArg('suppress-warnings' ),
|
| 574 | useNamedParameters: command.boolArg('use-named-parameters' ),
|
| 575 | );
|
| 576 | }
|
| 577 |
|
| 578 | // Try to read a `bool` value or null from `yamlMap`, otherwise throw.
|
| 579 | bool? _tryReadBool(YamlMap yamlMap, String key, Logger logger) {
|
| 580 | final Object? value = yamlMap[key];
|
| 581 | if (value == null) {
|
| 582 | return null;
|
| 583 | }
|
| 584 | if (value is! bool) {
|
| 585 | logger.printError('Expected " $key" to have a bool value, instead was " $value"' );
|
| 586 | throw Exception();
|
| 587 | }
|
| 588 | return value;
|
| 589 | }
|
| 590 |
|
| 591 | // Try to read a `String` value or null from `yamlMap`, otherwise throw.
|
| 592 | String? _tryReadString(YamlMap yamlMap, String key, Logger logger) {
|
| 593 | final Object? value = yamlMap[key];
|
| 594 | if (value == null) {
|
| 595 | return null;
|
| 596 | }
|
| 597 | if (value is! String) {
|
| 598 | logger.printError('Expected " $key" to have a String value, instead was " $value"' );
|
| 599 | throw Exception();
|
| 600 | }
|
| 601 | return value;
|
| 602 | }
|
| 603 |
|
| 604 | List<String>? _tryReadStringList(YamlMap yamlMap, String key, Logger logger) {
|
| 605 | final Object? value = yamlMap[key];
|
| 606 | if (value == null) {
|
| 607 | return null;
|
| 608 | }
|
| 609 | if (value is String) {
|
| 610 | return <String>[value];
|
| 611 | }
|
| 612 | if (value is Iterable) {
|
| 613 | return value.map((dynamic e) => e.toString()).toList();
|
| 614 | }
|
| 615 | logger.printError('" $value" must be String or List.' );
|
| 616 | throw Exception();
|
| 617 | }
|
| 618 |
|
| 619 | // Try to read a valid file `Uri` or null from `yamlMap` to file path, otherwise throw.
|
| 620 | String? _tryReadFilePath(YamlMap yamlMap, String key, Logger logger, FileSystem fileSystem) {
|
| 621 | final String? value = _tryReadString(yamlMap, key, logger);
|
| 622 | if (value == null) {
|
| 623 | return null;
|
| 624 | }
|
| 625 | final Uri? uri = Uri.tryParse(value);
|
| 626 | if (uri == null) {
|
| 627 | logger.printError('" $value" must be a relative file URI' );
|
| 628 | }
|
| 629 | return uri != null ? fileSystem.path.normalize(uri.path) : null;
|
| 630 | }
|
| 631 |
|