• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:convert' show json;
6import 'dart:io';
7
8import 'localizations_utils.dart';
9
10// The first suffix in kPluralSuffixes must be "Other". "Other" is special
11// because it's the only one that is required.
12const List<String> kPluralSuffixes = <String>['Other', 'Zero', 'One', 'Two', 'Few', 'Many'];
13final RegExp kPluralRegexp = RegExp(r'(\w*)(' + kPluralSuffixes.skip(1).join(r'|') + r')$');
14
15class ValidationError implements Exception {
16  ValidationError(this. message);
17  final String message;
18  @override
19  String toString() => message;
20}
21
22/// Sanity checking of the @foo metadata in the English translations, *_en.arb.
23///
24/// - For each foo, resource, there must be a corresponding @foo.
25/// - For each @foo resource, there must be a corresponding foo, except
26///   for plurals, for which there must be a fooOther.
27/// - Each @foo resource must have a Map value with a String valued
28///   description entry.
29///
30/// Throws an exception upon failure.
31void validateEnglishLocalizations(File file) {
32  final StringBuffer errorMessages = StringBuffer();
33
34  if (!file.existsSync()) {
35    errorMessages.writeln('English localizations do not exist: $file');
36    throw ValidationError(errorMessages.toString());
37  }
38
39  final Map<String, dynamic> bundle = json.decode(file.readAsStringSync());
40
41  for (String resourceId in bundle.keys) {
42    if (resourceId.startsWith('@'))
43      continue;
44
45    if (bundle['@$resourceId'] != null)
46      continue;
47
48    bool checkPluralResource(String suffix) {
49      final int suffixIndex = resourceId.indexOf(suffix);
50      return suffixIndex != -1 && bundle['@${resourceId.substring(0, suffixIndex)}'] != null;
51    }
52    if (kPluralSuffixes.any(checkPluralResource))
53      continue;
54
55    errorMessages.writeln('A value was not specified for @$resourceId');
56  }
57
58  for (String atResourceId in bundle.keys) {
59    if (!atResourceId.startsWith('@'))
60      continue;
61
62    final dynamic atResourceValue = bundle[atResourceId];
63    final Map<String, dynamic> atResource =
64        atResourceValue is Map<String, dynamic> ? atResourceValue : null;
65    if (atResource == null) {
66      errorMessages.writeln('A map value was not specified for $atResourceId');
67      continue;
68    }
69
70    final String description = atResource['description'];
71    if (description == null)
72      errorMessages.writeln('No description specified for $atResourceId');
73
74    final String plural = atResource['plural'];
75    final String resourceId = atResourceId.substring(1);
76    if (plural != null) {
77      final String resourceIdOther = '${resourceId}Other';
78      if (!bundle.containsKey(resourceIdOther))
79        errorMessages.writeln('Default plural resource $resourceIdOther undefined');
80    } else {
81      if (!bundle.containsKey(resourceId))
82        errorMessages.writeln('No matching $resourceId defined for $atResourceId');
83    }
84  }
85
86  if (errorMessages.isNotEmpty)
87    throw ValidationError(errorMessages.toString());
88}
89
90/// Enforces the following invariants in our localizations:
91///
92/// - Resource keys are valid, i.e. they appear in the canonical list.
93/// - Resource keys are complete for language-level locales, e.g. "es", "he".
94///
95/// Uses "en" localizations as the canonical source of locale keys that other
96/// locales are compared against.
97///
98/// If validation fails, throws an exception.
99void validateLocalizations(
100  Map<LocaleInfo, Map<String, String>> localeToResources,
101  Map<LocaleInfo, Map<String, dynamic>> localeToAttributes,
102) {
103  final Map<String, String> canonicalLocalizations = localeToResources[LocaleInfo.fromString('en')];
104  final Set<String> canonicalKeys = Set<String>.from(canonicalLocalizations.keys);
105  final StringBuffer errorMessages = StringBuffer();
106  bool explainMissingKeys = false;
107  for (final LocaleInfo locale in localeToResources.keys) {
108    final Map<String, String> resources = localeToResources[locale];
109
110    // Whether `key` corresponds to one of the plural variations of a key with
111    // the same prefix and suffix "Other".
112    //
113    // Many languages require only a subset of these variations, so we do not
114    // require them so long as the "Other" variation exists.
115    bool isPluralVariation(String key) {
116      final Match pluralMatch = kPluralRegexp.firstMatch(key);
117      if (pluralMatch == null)
118        return false;
119      final String prefix = pluralMatch[1];
120      return resources.containsKey('${prefix}Other');
121    }
122
123    final Set<String> keys = Set<String>.from(
124      resources.keys.where((String key) => !isPluralVariation(key))
125    );
126
127    // Make sure keys are valid (i.e. they also exist in the canonical
128    // localizations)
129    final Set<String> invalidKeys = keys.difference(canonicalKeys);
130    if (invalidKeys.isNotEmpty)
131      errorMessages.writeln('Locale "$locale" contains invalid resource keys: ${invalidKeys.join(', ')}');
132    // For language-level locales only, check that they have a complete list of
133    // keys, or opted out of using certain ones.
134    if (locale.length == 1) {
135      final Map<String, dynamic> attributes = localeToAttributes[locale];
136      final List<String> missingKeys = <String>[];
137       for (final String missingKey in canonicalKeys.difference(keys)) {
138        final dynamic attribute = attributes[missingKey];
139        final bool intentionallyOmitted = attribute is Map && attribute.containsKey('notUsed');
140        if (!intentionallyOmitted && !isPluralVariation(missingKey))
141          missingKeys.add(missingKey);
142      }
143      if (missingKeys.isNotEmpty) {
144        explainMissingKeys = true;
145        errorMessages.writeln('Locale "$locale" is missing the following resource keys: ${missingKeys.join(', ')}');
146      }
147    }
148  }
149
150  if (errorMessages.isNotEmpty) {
151    if (explainMissingKeys) {
152        errorMessages
153          ..writeln()
154          ..writeln(
155            'If a resource key is intentionally omitted, add an attribute corresponding '
156            'to the key name with a "notUsed" property explaining why. Example:'
157          )
158          ..writeln()
159          ..writeln('"@anteMeridiemAbbreviation": {')
160          ..writeln('  "notUsed": "Sindhi time format does not use a.m. indicator"')
161          ..writeln('}');
162    }
163    throw ValidationError(errorMessages.toString());
164  }
165}
166