| // 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. | |
| // This program updates the language locale arb files with any missing resource | |
| // entries that are included in the English arb files. This is useful when | |
| // adding new resources for localization. You can just add the appropriate | |
| // entries to the English arb file and then run this script. It will then check | |
| // all of the other language locale arb files and update them with the English | |
| // source for any missing resources. These will be picked up by the localization | |
| // team and then translated. | |
| // | |
| // ## Usage | |
| // | |
| // Run this program from the root of the git repository. | |
| // | |
| // ``` | |
| // dart dev/tools/localization/bin/gen_missing_localizations.dart | |
| // ``` | |
| import 'dart:convert'; | |
| import 'dart:io'; | |
| import 'package:path/path.dart' as path; | |
| import '../localizations_utils.dart'; | |
| import '../localizations_validator.dart'; | |
| Future<void> main(List<String> rawArgs) async { | |
| bool removeUndefined = false; | |
| if (rawArgs.contains('--remove-undefined')) { | |
| removeUndefined = true; | |
| } | |
| checkCwdIsRepoRoot('gen_missing_localizations'); | |
| final String localizationPath = path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'); | |
| updateMissingResources(localizationPath, 'material', removeUndefined: removeUndefined); | |
| updateMissingResources(localizationPath, 'cupertino', removeUndefined: removeUndefined); | |
| } | |
| Map<String, dynamic> loadBundle(File file) { | |
| if (!FileSystemEntity.isFileSync(file.path)) { | |
| exitWithError('Unable to find input file: ${file.path}'); | |
| } | |
| return json.decode(file.readAsStringSync()) as Map<String, dynamic>; | |
| } | |
| void writeBundle(File file, Map<String, dynamic> bundle) { | |
| final StringBuffer contents = StringBuffer(); | |
| contents.writeln('{'); | |
| for (final String key in bundle.keys) { | |
| contents.writeln(' "$key": ${json.encode(bundle[key])}${key == bundle.keys.last ? '' : ','}'); | |
| } | |
| contents.writeln('}'); | |
| file.writeAsStringSync(contents.toString()); | |
| } | |
| Set<String> resourceKeys(Map<String, dynamic> bundle) { | |
| return Set<String>.from( | |
| // Skip any attribute keys | |
| bundle.keys.where((String key) => !key.startsWith('@')) | |
| ); | |
| } | |
| bool intentionallyOmitted(String key, Map<String, dynamic> bundle) { | |
| final String attributeKey = '@$key'; | |
| final dynamic attribute = bundle[attributeKey]; | |
| return attribute is Map && attribute.containsKey('notUsed'); | |
| } | |
| /// Whether `key` corresponds to one of the plural variations of a key with | |
| /// the same prefix and suffix "Other". | |
| bool isPluralVariation(String key, Map<String, dynamic> bundle) { | |
| final Match? pluralMatch = kPluralRegexp.firstMatch(key); | |
| if (pluralMatch == null) { | |
| return false; | |
| } | |
| final String prefix = pluralMatch[1]!; | |
| return bundle.containsKey('${prefix}Other'); | |
| } | |
| void updateMissingResources(String localizationPath, String groupPrefix, {bool removeUndefined = false}) { | |
| final Directory localizationDir = Directory(localizationPath); | |
| final RegExp filenamePattern = RegExp('${groupPrefix}_(\\w+)\\.arb'); | |
| final Map<String, dynamic> englishBundle = loadBundle(File(path.join(localizationPath, '${groupPrefix}_en.arb'))); | |
| final Set<String> requiredKeys = resourceKeys(englishBundle); | |
| for (final FileSystemEntity entity in localizationDir.listSync().toList()..sort(sortFilesByPath)) { | |
| final String entityPath = entity.path; | |
| if (FileSystemEntity.isFileSync(entityPath) && filenamePattern.hasMatch(entityPath)) { | |
| final String localeString = filenamePattern.firstMatch(entityPath)![1]!; | |
| final LocaleInfo locale = LocaleInfo.fromString(localeString); | |
| // Only look at top-level language locales | |
| if (locale.length == 1) { | |
| final File arbFile = File(entityPath); | |
| final Map<String, dynamic> localeBundle = loadBundle(arbFile); | |
| final Set<String> localeResources = resourceKeys(localeBundle); | |
| // Whether or not the resources were modified and need to be updated. | |
| bool shouldWrite = false; | |
| // Remove any localizations that are not defined in the canonical | |
| // locale. This allows unused localizations to be removed if | |
| // --remove-undefined is passed. | |
| if (removeUndefined) { | |
| bool isIncluded(String key) { | |
| return !isPluralVariation(key, localeBundle) | |
| && !intentionallyOmitted(key, localeBundle); | |
| } | |
| // Find any resources in this locale that don't appear in the | |
| // canonical locale, and skipping any which should not be included | |
| // (plurals and intentionally omitted). | |
| final Set<String> extraResources = localeResources | |
| .difference(requiredKeys) | |
| .where(isIncluded) | |
| .toSet(); | |
| // Remove them. | |
| localeBundle.removeWhere((String key, dynamic value) { | |
| final bool found = extraResources.contains(key); | |
| if (found) { | |
| shouldWrite = true; | |
| } | |
| return found; | |
| }); | |
| if (shouldWrite) { | |
| print('Updating $entityPath by removing extra entries for $extraResources'); | |
| } | |
| } | |
| // Add in any resources that are in the canonical locale and not present | |
| // in this locale. | |
| final Set<String> missingResources = requiredKeys.difference(localeResources).where( | |
| (String key) => !isPluralVariation(key, localeBundle) && !intentionallyOmitted(key, localeBundle) | |
| ).toSet(); | |
| if (missingResources.isNotEmpty) { | |
| localeBundle.addEntries(missingResources.map((String k) => | |
| MapEntry<String, String>(k, englishBundle[k].toString()))); | |
| shouldWrite = true; | |
| print('Updating $entityPath with missing entries for $missingResources'); | |
| } | |
| if (shouldWrite) { | |
| writeBundle(arbFile, localeBundle); | |
| } | |
| } | |
| } | |
| } | |
| } |