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,