misc .arb fixes; localizations validator (#12197) * misc .arb fixes; localizations validator * regenerate localizations * address comments * do not treat plural variations as invalid keys
diff --git a/dev/bots/test.dart b/dev/bots/test.dart index a746a4d..c12ab27 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart
@@ -52,8 +52,42 @@ print('${bold}DONE: test.dart does nothing in the docs shard.$reset'); } +Future<Null> _verifyInternationalizations() async { + final EvalResult genResult = await _evalCommand( + dart, + <String>[ + path.join('dev', 'tools', 'gen_localizations.dart'), + path.join('packages', 'flutter', 'lib', 'src', 'material', 'i18n'), + 'material' + ], + workingDirectory: flutterRoot, + ); + + final String localizationsFile = path.join('packages', 'flutter', 'lib', 'src', 'material', 'i18n', 'localizations.dart'); + + final EvalResult sourceContents = await _evalCommand( + 'cat', + <String>[localizationsFile], + workingDirectory: flutterRoot, + ); + + if (genResult.stdout.trim() != sourceContents.stdout.trim()) { + stderr + ..writeln('<<<<<<< $localizationsFile') + ..writeln(sourceContents.stdout.trim()) + ..writeln('=======') + ..writeln(genResult.stdout.trim()) + ..writeln('>>>>>>> gen_localizations') + ..writeln('The contents of $localizationsFile are different from that produced by gen_localizations.') + ..writeln() + ..writeln('Did you forget to run gen_localizations.dart after updating a .arb file?'); + exit(1); + } +} + Future<Null> _analyzeRepo() async { await _verifyNoBadImports(flutterRoot); + await _verifyInternationalizations(); // Analyze all the Dart code in the repo. await _runFlutterAnalyze(flutterRoot, @@ -176,6 +210,57 @@ return _runCommand(pub, args, workingDirectory: workingDirectory); } +class EvalResult { + EvalResult({ + this.stdout, + this.stderr, + }); + + final String stdout; + final String stderr; +} + +Future<EvalResult> _evalCommand(String executable, List<String> arguments, { + String workingDirectory, + Map<String, String> environment, + bool skip: false, +}) async { + final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}'; + final String relativeWorkingDir = path.relative(workingDirectory); + if (skip) { + _printProgress('SKIPPING', relativeWorkingDir, commandDescription); + return null; + } + _printProgress('RUNNING', relativeWorkingDir, commandDescription); + + final Process process = await Process.start(executable, arguments, + workingDirectory: workingDirectory, + environment: environment, + ); + + final Future<List<List<int>>> savedStdout = process.stdout.toList(); + final Future<List<List<int>>> savedStderr = process.stderr.toList(); + final int exitCode = await process.exitCode; + final EvalResult result = new EvalResult( + stdout: UTF8.decode((await savedStdout).expand((List<int> ints) => ints).toList()), + stderr: UTF8.decode((await savedStderr).expand((List<int> ints) => ints).toList()), + ); + + if (exitCode != 0) { + stderr.write(result.stderr); + print( + '$red━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$reset\n' + '${bold}ERROR:$red Last command exited with $exitCode.$reset\n' + '${bold}Command:$red $commandDescription$reset\n' + '${bold}Relative working directory:$red $relativeWorkingDir$reset\n' + '$red━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$reset' + ); + exit(1); + } + + return result; +} + Future<Null> _runCommand(String executable, List<String> arguments, { String workingDirectory, Map<String, String> environment,
diff --git a/dev/tools/gen_localizations.dart b/dev/tools/gen_localizations.dart index 493044b..b3e9e61 100644 --- a/dev/tools/gen_localizations.dart +++ b/dev/tools/gen_localizations.dart
@@ -26,6 +26,8 @@ import 'dart:convert' show JSON; import 'dart:io'; +import 'localizations_validator.dart'; + const String outputHeader = ''' // Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be @@ -36,8 +38,14 @@ // @(regenerate) '''; +/// Maps locales to resource key/value pairs. final Map<String, Map<String, String>> localeToResources = <String, Map<String, String>>{}; +/// Maps locales to resource attributes. +/// +/// See also https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes +final Map<String, Map<String, dynamic>> localeToResourceAttributes = <String, Map<String, dynamic>>{}; + // Return s as a Dart-parseable raw string in double quotes. Expand double quotes: // foo => r"foo" // foo "bar" => r"foo " '"' r"bar" '"' @@ -92,13 +100,16 @@ void processBundle(File file, String locale) { localeToResources[locale] ??= <String, String>{}; + localeToResourceAttributes[locale] ??= <String, dynamic>{}; final Map<String, String> resources = localeToResources[locale]; + final Map<String, dynamic> attributes = localeToResourceAttributes[locale]; final Map<String, dynamic> bundle = JSON.decode(file.readAsStringSync()); for (String key in bundle.keys) { // The ARB file resource "attributes" for foo are called @foo. if (key.startsWith('@')) - continue; - resources[key] = bundle[key]; + attributes[key.substring(1)] = bundle[key]; + else + resources[key] = bundle[key]; } } @@ -121,6 +132,7 @@ processBundle(new File(path), locale); } } + validateLocalizations(localeToResources, localeToResourceAttributes); final String regenerate = 'dart dev/tools/gen_localizations.dart ${directory.path} ${args[1]}'; print(outputHeader.replaceFirst('@(regenerate)', regenerate));
diff --git a/dev/tools/localizations_validator.dart b/dev/tools/localizations_validator.dart new file mode 100644 index 0000000..90aec93 --- /dev/null +++ b/dev/tools/localizations_validator.dart
@@ -0,0 +1,91 @@ +// 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<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); + + 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('}'); + } + + stderr.writeln('ERROR:'); + stderr.writeln(errorMessages); + exit(1); + } +}
diff --git a/packages/flutter/lib/src/material/i18n/localizations.dart b/packages/flutter/lib/src/material/i18n/localizations.dart index 496641b..ce972ac 100644 --- a/packages/flutter/lib/src/material/i18n/localizations.dart +++ b/packages/flutter/lib/src/material/i18n/localizations.dart
@@ -4,7 +4,7 @@ // This file has been automatically generated. Please do not edit it manually. // To regenerate the file, use: -// dart dev/tools/gen_localizations.dart lib/src/material/i18n material +// dart dev/tools/gen_localizations.dart packages/flutter/lib/src/material/i18n material /// Maps from [Locale.languageCode] to a map that contains the localized strings /// for that locale. @@ -136,6 +136,8 @@ }, "es_US": const <String, String>{ "timeOfDayFormat": r"h:mm a", + "anteMeridiemAbbreviation": r"AM", + "postMeridiemAbbreviation": r"PM", }, "fa": const <String, String>{ "timeOfDayFormat": r"H:mm", @@ -339,7 +341,9 @@ "rowsPerPageTitle": r"Строки на страницу:", "aboutListTileTitle": r"O $applicationName", "licensesPageTitle": r"Лицензии", - "selectedRowCountTitleOther": r"Выбранно $selectedRowCount строк", + "selectedRowCountTitleZero": r"Строки не выбраны", + "selectedRowCountTitleOne": r"Выбрана 1 строка", + "selectedRowCountTitleOther": r"Выбрано $selectedRowCount строк", "cancelButtonLabel": r"ОТМЕНИТЬ", "closeButtonLabel": r"ЗАКРЫТЬ", "continueButtonLabel": r"ПРОДОЛЖИТЬ",
diff --git a/packages/flutter/lib/src/material/i18n/material_de.arb b/packages/flutter/lib/src/material/i18n/material_de.arb index e85f798..6d05c7e 100644 --- a/packages/flutter/lib/src/material/i18n/material_de.arb +++ b/packages/flutter/lib/src/material/i18n/material_de.arb
@@ -1,5 +1,7 @@ { "timeOfDayFormat": "HH:mm", + "@anteMeridiemAbbreviation": { "notUsed": "German time format does not use a.m. indicator" }, + "@postMeridiemAbbreviation": { "notUsed": "German time format does not use p.m. indicator" }, "openAppDrawerTooltip": "Navigationsmenü öffnen", "backButtonTooltip": "Zurück", "closeButtonTooltip": "Schließen",
diff --git a/packages/flutter/lib/src/material/i18n/material_en.arb b/packages/flutter/lib/src/material/i18n/material_en.arb index fd29893..6d7e42f 100644 --- a/packages/flutter/lib/src/material/i18n/material_en.arb +++ b/packages/flutter/lib/src/material/i18n/material_en.arb
@@ -67,7 +67,7 @@ "pageRowsInfoTitle": "$firstRow–$lastRow of $rowCount", "pageRowsInfoTitleApproximate": "$firstRow–$lastRow of about $rowCount", - "@pageRowInfoTitle": { + "@pageRowsInfoTitle": { "description": "Title for the [PaginatedDataTable]'s row info footer", "type": "text" },
diff --git a/packages/flutter/lib/src/material/i18n/material_es.arb b/packages/flutter/lib/src/material/i18n/material_es.arb index fe7e2f7..a6371ed 100644 --- a/packages/flutter/lib/src/material/i18n/material_es.arb +++ b/packages/flutter/lib/src/material/i18n/material_es.arb
@@ -1,5 +1,7 @@ { "timeOfDayFormat": "H:mm", + "@anteMeridiemAbbreviation": { "notUsed": "Standard Spanish time format does not use a.m. indicator" }, + "@postMeridiemAbbreviation": { "notUsed": "Standard Spanish time format does not use p.m. indicator" }, "openAppDrawerTooltip": "Abrir el menú de navegación", "backButtonTooltip": "Espalda", "closeButtonTooltip": "Cerrar",
diff --git a/packages/flutter/lib/src/material/i18n/material_es_US.arb b/packages/flutter/lib/src/material/i18n/material_es_US.arb index 1de168c..95c984c 100644 --- a/packages/flutter/lib/src/material/i18n/material_es_US.arb +++ b/packages/flutter/lib/src/material/i18n/material_es_US.arb
@@ -1,3 +1,5 @@ { - "timeOfDayFormat": "h:mm a" + "timeOfDayFormat": "h:mm a", + "anteMeridiemAbbreviation": "AM", + "postMeridiemAbbreviation": "PM" }
diff --git a/packages/flutter/lib/src/material/i18n/material_fa.arb b/packages/flutter/lib/src/material/i18n/material_fa.arb index 2c1303a..799e727 100644 --- a/packages/flutter/lib/src/material/i18n/material_fa.arb +++ b/packages/flutter/lib/src/material/i18n/material_fa.arb
@@ -1,5 +1,7 @@ { "timeOfDayFormat": "H:mm", + "@anteMeridiemAbbreviation": { "notUsed": "Farsi time format does not use a.m. indicator" }, + "@postMeridiemAbbreviation": { "notUsed": "Farsi time format does not use p.m. indicator" }, "openAppDrawerTooltip": "منوی ناوبری را باز کنید", "backButtonTooltip": "بازگشت", "closeButtonTooltip": "بستن",
diff --git a/packages/flutter/lib/src/material/i18n/material_fr.arb b/packages/flutter/lib/src/material/i18n/material_fr.arb index 70083c9..80c1766 100644 --- a/packages/flutter/lib/src/material/i18n/material_fr.arb +++ b/packages/flutter/lib/src/material/i18n/material_fr.arb
@@ -1,5 +1,7 @@ { "timeOfDayFormat": "HH:mm", + "@anteMeridiemAbbreviation": { "notUsed": "French time format does not use a.m. indicator" }, + "@postMeridiemAbbreviation": { "notUsed": "French time format does not use p.m. indicator" }, "openAppDrawerTooltip": "Ouvrir le menu de navigation", "backButtonTooltip": "Retour", "closeButtonTooltip": "Fermer",
diff --git a/packages/flutter/lib/src/material/i18n/material_he.arb b/packages/flutter/lib/src/material/i18n/material_he.arb index 17ee50a..da02eb7 100644 --- a/packages/flutter/lib/src/material/i18n/material_he.arb +++ b/packages/flutter/lib/src/material/i18n/material_he.arb
@@ -1,5 +1,7 @@ { "timeOfDayFormat": "H:mm", + "@anteMeridiemAbbreviation": { "notUsed": "Hebrew time format does not use a.m. indicator" }, + "@postMeridiemAbbreviation": { "notUsed": "Hebrew time format does not use p.m. indicator" }, "openAppDrawerTooltip": "פתח תפריט ניווט", "backButtonTooltip": "אחורה", "closeButtonTooltip": "סגור",
diff --git a/packages/flutter/lib/src/material/i18n/material_it.arb b/packages/flutter/lib/src/material/i18n/material_it.arb index c23c004..9ff289d 100644 --- a/packages/flutter/lib/src/material/i18n/material_it.arb +++ b/packages/flutter/lib/src/material/i18n/material_it.arb
@@ -1,5 +1,7 @@ { "timeOfDayFormat": "HH:mm", + "@anteMeridiemAbbreviation": { "notUsed": "Italian time format does not use a.m. indicator" }, + "@postMeridiemAbbreviation": { "notUsed": "Italian time format does not use p.m. indicator" }, "openAppDrawerTooltip": "Apri il menu di navigazione", "backButtonTooltip": "Indietro", "closeButtonTooltip": "Chiudi",
diff --git a/packages/flutter/lib/src/material/i18n/material_ja.arb b/packages/flutter/lib/src/material/i18n/material_ja.arb index 9f4d5f3..574f058 100644 --- a/packages/flutter/lib/src/material/i18n/material_ja.arb +++ b/packages/flutter/lib/src/material/i18n/material_ja.arb
@@ -1,5 +1,7 @@ { "timeOfDayFormat": "H:mm", + "@anteMeridiemAbbreviation": { "notUsed": "Japanese time format does not use a.m. indicator" }, + "@postMeridiemAbbreviation": { "notUsed": "Japanese time format does not use p.m. indicator" }, "openAppDrawerTooltip": "ナビゲーションメニューを開く", "backButtonTooltip": "戻る", "closeButtonTooltip": "閉じる",
diff --git a/packages/flutter/lib/src/material/i18n/material_ps.arb b/packages/flutter/lib/src/material/i18n/material_ps.arb index 010bde3..f1e3d23 100644 --- a/packages/flutter/lib/src/material/i18n/material_ps.arb +++ b/packages/flutter/lib/src/material/i18n/material_ps.arb
@@ -1,5 +1,7 @@ { "timeOfDayFormat": "HH:mm", + "@anteMeridiemAbbreviation": { "notUsed": "Pashto time format does not use a.m. indicator" }, + "@postMeridiemAbbreviation": { "notUsed": "Pashto time format does not use p.m. indicator" }, "openAppDrawerTooltip": "د پرانیستی نیینګ مینو", "backButtonTooltip": "شاته", "closeButtonTooltip": "بنده",
diff --git a/packages/flutter/lib/src/material/i18n/material_pt.arb b/packages/flutter/lib/src/material/i18n/material_pt.arb index 15deeb4..6f17d41 100644 --- a/packages/flutter/lib/src/material/i18n/material_pt.arb +++ b/packages/flutter/lib/src/material/i18n/material_pt.arb
@@ -1,5 +1,7 @@ { "timeOfDayFormat": "HH:mm", + "@anteMeridiemAbbreviation": { "notUsed": "Portuguese time format does not use a.m. indicator" }, + "@postMeridiemAbbreviation": { "notUsed": "Portuguese time format does not use p.m. indicator" }, "openAppDrawerTooltip": "Abrir menu de navegação", "backButtonTooltip": "Costas", "closeButtonTooltip": "Fechar",
diff --git a/packages/flutter/lib/src/material/i18n/material_ru.arb b/packages/flutter/lib/src/material/i18n/material_ru.arb index 2ac9fd6..985941e 100644 --- a/packages/flutter/lib/src/material/i18n/material_ru.arb +++ b/packages/flutter/lib/src/material/i18n/material_ru.arb
@@ -1,5 +1,7 @@ { "timeOfDayFormat": "H:mm", + "@anteMeridiemAbbreviation": { "notUsed": "Russian time format does not use a.m. indicator" }, + "@postMeridiemAbbreviation": { "notUsed": "Russian time format does not use p.m. indicator" }, "openAppDrawerTooltip": "Открыть меню навигации", "backButtonTooltip": "Назад", "closeButtonTooltip": "Закрыть", @@ -13,7 +15,9 @@ "rowsPerPageTitle": "Строки на страницу:", "aboutListTileTitle": "O $applicationName", "licensesPageTitle": "Лицензии", - "selectedRowCountTitleOther": "Выбранно $selectedRowCount строк", + "selectedRowCountTitleZero": "Строки не выбраны", + "selectedRowCountTitleOne": "Выбрана 1 строка", + "selectedRowCountTitleOther": "Выбрано $selectedRowCount строк", "cancelButtonLabel": "ОТМЕНИТЬ", "closeButtonLabel": "ЗАКРЫТЬ", "continueButtonLabel": "ПРОДОЛЖИТЬ",
diff --git a/packages/flutter/lib/src/material/i18n/material_sd.arb b/packages/flutter/lib/src/material/i18n/material_sd.arb index 903f972..09f560c 100644 --- a/packages/flutter/lib/src/material/i18n/material_sd.arb +++ b/packages/flutter/lib/src/material/i18n/material_sd.arb
@@ -1,5 +1,7 @@ { "timeOfDayFormat": "HH:mm", + "@anteMeridiemAbbreviation": { "notUsed": "Sindhi time format does not use a.m. indicator" }, + "@postMeridiemAbbreviation": { "notUsed": "Sindhi time format does not use p.m. indicator" }, "openAppDrawerTooltip": "اوپن جي مينڊيٽ مينيو", "backButtonTooltip": "پوئتي", "closeButtonTooltip": "بند ڪريو",