localizations_validator.dart 5.87 KB
Newer Older
1 2 3 4
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:convert' show json;
6 7
import 'dart:io';

8 9 10
/// Sanity checking of the @foo metadata in the English translations,
/// material_en.arb.
///
11
/// - For each foo, resource, there must be a corresponding @foo.
12 13 14 15 16 17 18 19 20 21 22 23 24 25
/// - For each @foo resource, there must be a corresponding foo, except
///   for plurals, for which there must be a fooOther.
/// - Each @foo resource must have a Map value with a String valued
///   description entry.
///
/// Returns an error message upon failure, null on success.
String validateEnglishLocalizations(File file) {
  final StringBuffer errorMessages = new StringBuffer();

  if (!file.existsSync()) {
    errorMessages.writeln('English localizations do not exist: $file');
    return errorMessages.toString();
  }

26
  final Map<String, dynamic> bundle = json.decode(file.readAsStringSync());
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

  for (String resourceId in bundle.keys) {
    if (resourceId.startsWith('@'))
      continue;

    if (bundle['@$resourceId'] != null)
      continue;

    bool checkPluralResource(String suffix) {
      final int suffixIndex = resourceId.indexOf(suffix);
      return suffixIndex != -1 && bundle['@${resourceId.substring(0, suffixIndex)}'] != null;
    }
    if (<String>['Zero', 'One', 'Two', 'Few', 'Many', 'Other'].any(checkPluralResource))
      continue;

    errorMessages.writeln('A value was not specified for @$resourceId');
  }

45 46 47 48 49
  for (String atResourceId in bundle.keys) {
    if (!atResourceId.startsWith('@'))
      continue;

    final dynamic atResourceValue = bundle[atResourceId];
50 51
    final Map<String, dynamic> atResource =
        atResourceValue is Map<String, dynamic> ? atResourceValue : null;
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
    if (atResource == null) {
      errorMessages.writeln('A map value was not specified for $atResourceId');
      continue;
    }

    final String description = atResource['description'];
    if (description == null)
      errorMessages.writeln('No description specified for $atResourceId');

    final String plural = atResource['plural'];
    final String resourceId = atResourceId.substring(1);
    if (plural != null) {
      final String resourceIdOther = '${resourceId}Other';
      if (!bundle.containsKey(resourceIdOther))
        errorMessages.writeln('Default plural resource $resourceIdOther undefined');
    } else {
      if (!bundle.containsKey(resourceId))
        errorMessages.writeln('No matching $resourceId defined for $atResourceId');
    }
  }

  return errorMessages.isEmpty ? null : errorMessages.toString();
}

76
/// Enforces the following invariants in our localizations:
77
///
78 79
/// - Resource keys are valid, i.e. they appear in the canonical list.
/// - Resource keys are complete for language-level locales, e.g. "es", "he".
80
///
81 82
/// Uses "en" localizations as the canonical source of locale keys that other
/// locales are compared against.
83 84 85
///
/// If validation fails, return an error message, otherwise return null.
String validateLocalizations(
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
  Map<String, Map<String, String>> localeToResources,
  Map<String, Map<String, dynamic>> localeToAttributes,
) {
  final Map<String, String> canonicalLocalizations = localeToResources['en'];
  final Set<String> canonicalKeys = new Set<String>.from(canonicalLocalizations.keys);
  final StringBuffer errorMessages = new StringBuffer();
  bool explainMissingKeys = false;
  for (final String locale in localeToResources.keys) {
    final Map<String, String> resources = localeToResources[locale];

    // Whether `key` corresponds to one of the plural variations of a key with
    // the same prefix and suffix "Other".
    //
    // Many languages require only a subset of these variations, so we do not
    // require them so long as the "Other" variation exists.
    bool isPluralVariation(String key) {
      final RegExp pluralRegexp = new RegExp(r'(\w*)(Zero|One|Two|Few|Many)$');
      final Match pluralMatch = pluralRegexp.firstMatch(key);
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 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
      if (pluralMatch == null)
        return false;

      final String prefix = pluralMatch[1];
      return resources.containsKey('${prefix}Other');
    }

    final Set<String> keys = new Set<String>.from(
      resources.keys.where((String key) => !isPluralVariation(key))
    );

    // Make sure keys are valid (i.e. they also exist in the canonical
    // localizations)
    final Set<String> invalidKeys = keys.difference(canonicalKeys);
    if (invalidKeys.isNotEmpty)
      errorMessages.writeln('Locale "$locale" contains invalid resource keys: ${invalidKeys.join(', ')}');

    // For language-level locales only, check that they have a complete list of
    // keys, or opted out of using certain ones.
    if (locale.length == 2) {
      final Map<String, dynamic> attributes = localeToAttributes[locale];
      final List<String> missingKeys = <String>[];

      for (final String missingKey in canonicalKeys.difference(keys)) {
        final dynamic attribute = attributes[missingKey];
        final bool intentionallyOmitted = attribute is Map && attribute.containsKey('notUsed');
        if (!intentionallyOmitted && !isPluralVariation(missingKey))
          missingKeys.add(missingKey);
      }
      if (missingKeys.isNotEmpty) {
        explainMissingKeys = true;
        errorMessages.writeln('Locale "$locale" is missing the following resource keys: ${missingKeys.join(', ')}');
      }
    }
  }

  if (errorMessages.isNotEmpty) {
    if (explainMissingKeys) {
        errorMessages
          ..writeln()
          ..writeln(
            'If a resource key is intentionally omitted, add an attribute corresponding '
            'to the key name with a "notUsed" property explaining why. Example:'
          )
          ..writeln()
          ..writeln('"@anteMeridiemAbbreviation": {')
          ..writeln('  "notUsed": "Sindhi time format does not use a.m. indicator"')
          ..writeln('}');
    }
154
    return errorMessages.toString();
155
  }
156
  return null;
157
}