// 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); } } } } }