1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// 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.
import 'dart:convert' show json;
import 'dart:io';
import 'localizations_utils.dart';
// The first suffix in kPluralSuffixes must be "Other". "Other" is special
// because it's the only one that is required.
const List<String> kPluralSuffixes = <String>['Other', 'Zero', 'One', 'Two', 'Few', 'Many'];
final RegExp kPluralRegexp = RegExp(r'(\w*)(' + kPluralSuffixes.skip(1).join(r'|') + r')$');
class ValidationError implements Exception {
ValidationError(this. message);
final String message;
@override
String toString() => message;
}
/// Sanity checking of the @foo metadata in the English translations, *_en.arb.
///
/// - For each foo, resource, there must be a corresponding @foo.
/// - 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.
///
/// Throws an exception upon failure.
void validateEnglishLocalizations(File file) {
final StringBuffer errorMessages = StringBuffer();
if (!file.existsSync()) {
errorMessages.writeln('English localizations do not exist: $file');
throw ValidationError(errorMessages.toString());
}
final Map<String, dynamic> bundle = json.decode(file.readAsStringSync()) as Map<String, dynamic>;
for (final 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 (kPluralSuffixes.any(checkPluralResource))
continue;
errorMessages.writeln('A value was not specified for @$resourceId');
}
for (final String atResourceId in bundle.keys) {
if (!atResourceId.startsWith('@'))
continue;
final dynamic atResourceValue = bundle[atResourceId];
final Map<String, dynamic> atResource =
atResourceValue is Map<String, dynamic> ? atResourceValue : null;
if (atResource == null) {
errorMessages.writeln('A map value was not specified for $atResourceId');
continue;
}
final String description = atResource['description'] as String;
if (description == null)
errorMessages.writeln('No description specified for $atResourceId');
final String plural = atResource['plural'] as String;
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');
}
}
if (errorMessages.isNotEmpty)
throw ValidationError(errorMessages.toString());
}
/// Enforces the following invariants in our localizations:
///
/// - Resource keys are valid, i.e. they appear in the canonical list.
/// - Resource keys are complete for language-level locales, e.g. "es", "he".
///
/// Uses "en" localizations as the canonical source of locale keys that other
/// locales are compared against.
///
/// If validation fails, throws an exception.
void validateLocalizations(
Map<LocaleInfo, Map<String, String>> localeToResources,
Map<LocaleInfo, Map<String, dynamic>> localeToAttributes,
) {
final Map<String, String> canonicalLocalizations = localeToResources[LocaleInfo.fromString('en')];
final Set<String> canonicalKeys = Set<String>.from(canonicalLocalizations.keys);
final StringBuffer errorMessages = StringBuffer();
bool explainMissingKeys = false;
for (final LocaleInfo 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 Match pluralMatch = kPluralRegexp.firstMatch(key);
if (pluralMatch == null)
return false;
final String prefix = pluralMatch[1];
return resources.containsKey('${prefix}Other');
}
final Set<String> keys = 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 == 1) {
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('}');
}
throw ValidationError(errorMessages.toString());
}
}