1// Copyright 2014 The Flutter 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()) as Map<String, dynamic>;
40
41 for (final String resourceId in bundle.keys) {
42 if (resourceId.startsWith('@')) {
43 continue;
44 }
45
46 if (bundle['@$resourceId'] != null) {
47 continue;
48 }
49
50 bool checkPluralResource(String suffix) {
51 final int suffixIndex = resourceId.indexOf(suffix);
52 return suffixIndex != -1 && bundle['@${resourceId.substring(0, suffixIndex)}'] != null;
53 }
54
55 if (kPluralSuffixes.any(checkPluralResource)) {
56 continue;
57 }
58
59 errorMessages.writeln('A value was not specified for @$resourceId');
60 }
61
62 for (final String atResourceId in bundle.keys) {
63 if (!atResourceId.startsWith('@')) {
64 continue;
65 }
66
67 final dynamic atResourceValue = bundle[atResourceId];
68 final Map<String, dynamic>? atResource =
69 atResourceValue is Map<String, dynamic> ? atResourceValue : null;
70 if (atResource == null) {
71 errorMessages.writeln('A map value was not specified for $atResourceId');
72 continue;
73 }
74
75 final bool optional = atResource.containsKey('optional');
76 final String? description = atResource['description'] as String?;
77 if (description == null && !optional) {
78 errorMessages.writeln('No description specified for $atResourceId');
79 }
80
81 final String? plural = atResource['plural'] as String?;
82 final String resourceId = atResourceId.substring(1);
83 if (plural != null) {
84 final String resourceIdOther = '${resourceId}Other';
85 if (!bundle.containsKey(resourceIdOther)) {
86 errorMessages.writeln('Default plural resource $resourceIdOther undefined');
87 }
88 } else {
89 if (!optional && !bundle.containsKey(resourceId)) {
90 errorMessages.writeln('No matching $resourceId defined for $atResourceId');
91 }
92 }
93 }
94
95 if (errorMessages.isNotEmpty) {
96 throw ValidationError(errorMessages.toString());
97 }
98}
99
100/// This removes undefined localizations (localizations that aren't present in
101/// the canonical locale anymore) by:
102///
103/// 1. Looking up the canonical (English, in this case) localizations.
104/// 2. For each locale, getting the resources.
105/// 3. Determining the set of keys that aren't plural variations (we're only
106/// interested in the base terms being translated and not their variants)
107/// 4. Determining the set of invalid keys; that is those that are (non-plural)
108/// keys in the resources for this locale, but which _aren't_ keys in the
109/// canonical list.
110/// 5. Removes the invalid mappings from this resource's locale.
111void removeUndefinedLocalizations(Map<LocaleInfo, Map<String, String>> localeToResources) {
112 final Map<String, String> canonicalLocalizations =
113 localeToResources[LocaleInfo.fromString('en')]!;
114 final Set<String> canonicalKeys = Set<String>.from(canonicalLocalizations.keys);
115
116 localeToResources.forEach((LocaleInfo locale, Map<String, String> resources) {
117 bool isPluralVariation(String key) {
118 final Match? pluralMatch = kPluralRegexp.firstMatch(key);
119 if (pluralMatch == null) {
120 return false;
121 }
122 final String? prefix = pluralMatch[1];
123 return resources.containsKey('${prefix}Other');
124 }
125
126 final Set<String> keys = Set<String>.from(
127 resources.keys.where((String key) => !isPluralVariation(key)),
128 );
129
130 final Set<String> invalidKeys = keys.difference(canonicalKeys);
131 resources.removeWhere((String key, String value) => invalidKeys.contains(key));
132 });
133}
134
135/// Enforces the following invariants in our localizations:
136///
137/// - Resource keys are valid, i.e. they appear in the canonical list.
138/// - Resource keys are complete for language-level locales, e.g. "es", "he".
139///
140/// Uses "en" localizations as the canonical source of locale keys that other
141/// locales are compared against.
142///
143/// If validation fails, throws an exception.
144void validateLocalizations(
145 Map<LocaleInfo, Map<String, String>> localeToResources,
146 Map<LocaleInfo, Map<String, dynamic>> localeToAttributes, {
147 bool removeUndefined = false,
148}) {
149 final Map<String, String> canonicalLocalizations =
150 localeToResources[LocaleInfo.fromString('en')]!;
151 final Set<String> canonicalKeys = Set<String>.from(canonicalLocalizations.keys);
152 final StringBuffer errorMessages = StringBuffer();
153 bool explainMissingKeys = false;
154 for (final LocaleInfo locale in localeToResources.keys) {
155 final Map<String, String> resources = localeToResources[locale]!;
156
157 // Whether `key` corresponds to one of the plural variations of a key with
158 // the same prefix and suffix "Other".
159 //
160 // Many languages require only a subset of these variations, so we do not
161 // require them so long as the "Other" variation exists.
162 bool isPluralVariation(String key) {
163 final Match? pluralMatch = kPluralRegexp.firstMatch(key);
164 if (pluralMatch == null) {
165 return false;
166 }
167 final String? prefix = pluralMatch[1];
168 return resources.containsKey('${prefix}Other');
169 }
170
171 final Set<String> keys = Set<String>.from(
172 resources.keys.where((String key) => !isPluralVariation(key)),
173 );
174
175 // Make sure keys are valid (i.e. they also exist in the canonical
176 // localizations)
177 final Set<String> invalidKeys = keys.difference(canonicalKeys);
178 if (invalidKeys.isNotEmpty && !removeUndefined) {
179 errorMessages.writeln(
180 'Locale "$locale" contains invalid resource keys: ${invalidKeys.join(', ')}',
181 );
182 }
183
184 // For language-level locales only, check that they have a complete list of
185 // keys, or opted out of using certain ones.
186 if (locale.length == 1) {
187 final Map<String, dynamic>? attributes = localeToAttributes[locale];
188 final List<String?> missingKeys = <String?>[];
189 for (final String missingKey in canonicalKeys.difference(keys)) {
190 final dynamic attribute = attributes?[missingKey];
191 final bool intentionallyOmitted = attribute is Map && attribute.containsKey('notUsed');
192 if (!intentionallyOmitted && !isPluralVariation(missingKey)) {
193 missingKeys.add(missingKey);
194 }
195 }
196 if (missingKeys.isNotEmpty) {
197 explainMissingKeys = true;
198 errorMessages.writeln(
199 'Locale "$locale" is missing the following resource keys: ${missingKeys.join(', ')}',
200 );
201 }
202 }
203 }
204
205 if (errorMessages.isNotEmpty) {
206 if (explainMissingKeys) {
207 errorMessages
208 ..writeln()
209 ..writeln(
210 'If a resource key is intentionally omitted, add an attribute corresponding '
211 'to the key name with a "notUsed" property explaining why. Example:',
212 )
213 ..writeln()
214 ..writeln('"@anteMeridiemAbbreviation": {')
215 ..writeln(' "notUsed": "Sindhi time format does not use a.m. indicator"')
216 ..writeln('}');
217 }
218 throw ValidationError(errorMessages.toString());
219 }
220}
221

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com