// 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. import 'dart:io'; /// 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, print an error message to STDERR and quit with exit /// code 1. void validateLocalizations( Map> localeToResources, Map> localeToAttributes, ) { final Map canonicalLocalizations = localeToResources['en']; final Set canonicalKeys = new Set.from(canonicalLocalizations.keys); final StringBuffer errorMessages = new StringBuffer(); bool explainMissingKeys = false; for (final String locale in localeToResources.keys) { final Map 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); if (pluralMatch == null) return false; final String prefix = pluralMatch[1]; return resources.containsKey('${prefix}Other'); } final Set keys = new Set.from( resources.keys.where((String key) => !isPluralVariation(key)) ); // Make sure keys are valid (i.e. they also exist in the canonical // localizations) final Set 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 attributes = localeToAttributes[locale]; final List missingKeys = []; 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('}'); } stderr.writeln('ERROR:'); stderr.writeln(errorMessages); exit(1); } }