gen_missing_localizations.dart 5.81 KB
Newer Older
1 2 3 4 5 6 7 8
// 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
9 10
// 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
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
// 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 {
30 31 32 33
  bool removeUndefined = false;
  if (rawArgs.contains('--remove-undefined')) {
    removeUndefined = true;
  }
34 35 36
  checkCwdIsRepoRoot('gen_missing_localizations');

  final String localizationPath = path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n');
37 38
  updateMissingResources(localizationPath, 'material', removeUndefined: removeUndefined);
  updateMissingResources(localizationPath, 'cupertino', removeUndefined: removeUndefined);
39 40 41
}

Map<String, dynamic> loadBundle(File file) {
42
  if (!FileSystemEntity.isFileSync(file.path)) {
43
    exitWithError('Unable to find input file: ${file.path}');
44
  }
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
  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) {
74
  final Match? pluralMatch = kPluralRegexp.firstMatch(key);
75
  if (pluralMatch == null) {
76
    return false;
77
  }
78
  final String prefix = pluralMatch[1]!;
79 80 81
  return bundle.containsKey('${prefix}Other');
}

82
void updateMissingResources(String localizationPath, String groupPrefix, {bool removeUndefined = false}) {
83
  final Directory localizationDir = Directory(localizationPath);
84
  final RegExp filenamePattern = RegExp('${groupPrefix}_(\\w+)\\.arb');
85

86 87
  final Map<String, dynamic> englishBundle = loadBundle(File(path.join(localizationPath, '${groupPrefix}_en.arb')));
  final Set<String> requiredKeys = resourceKeys(englishBundle);
88 89 90 91

  for (final FileSystemEntity entity in localizationDir.listSync().toList()..sort(sortFilesByPath)) {
    final String entityPath = entity.path;
    if (FileSystemEntity.isFileSync(entityPath) && filenamePattern.hasMatch(entityPath)) {
92
      final String localeString = filenamePattern.firstMatch(entityPath)![1]!;
93 94 95 96 97 98 99
      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);
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
        // 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.
135 136 137 138
        final Set<String> missingResources = requiredKeys.difference(localeResources).where(
          (String key) => !isPluralVariation(key, localeBundle) && !intentionallyOmitted(key, localeBundle)
        ).toSet();
        if (missingResources.isNotEmpty) {
139 140
          localeBundle.addEntries(missingResources.map((String k) =>
            MapEntry<String, String>(k, englishBundle[k].toString())));
141 142 143 144
          shouldWrite = true;
          print('Updating $entityPath with missing entries for $missingResources');
        }
        if (shouldWrite) {
145 146 147 148 149 150
          writeBundle(arbFile, localeBundle);
        }
      }
    }
  }
}