flutter/dev/tools/localization/bin/gen_missing_localizations.dart
Michael Goderbauer 5491c8c146
Auto-format Framework (#160545)
This auto-formats all *.dart files in the repository outside of the
`engine` subdirectory and enforces that these files stay formatted with
a presubmit check.

**Reviewers:** Please carefully review all the commits except for the
one titled "formatted". The "formatted" commit was auto-generated by
running `dev/tools/format.sh -a -f`. The other commits were hand-crafted
to prepare the repo for the formatting change. I recommend reviewing the
commits one-by-one via the "Commits" tab and avoiding Github's "Files
changed" tab as it will likely slow down your browser because of the
size of this PR.

---------

Co-authored-by: Kate Lovett <katelovett@google.com>
Co-authored-by: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com>
2024-12-19 20:06:21 +00:00

171 lines
6.0 KiB
Dart

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