gen_localizations.dart 9.81 KB
Newer Older
1 2 3 4
// 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.

5
// This program generates a Dart "localizations" Map definition that combines
6
// the contents of the arb files. The map can be used to lookup a localized
7
// string: `localizations[localeString][resourceId]`.
8
//
9
// The *.arb files are in packages/flutter_localizations/lib/src/l10n.
10 11 12 13 14
//
// The arb (JSON) format files must contain a single map indexed by locale.
// Each map value is itself a map with resource identifier keys and localized
// resource string values.
//
15 16 17 18
// The arb filenames are expected to have the form "material_(\w+)\.arb", where
// the group following "_" identifies the language code and the country code,
// e.g. "material_en.arb" or "material_en_GB.arb". In most cases both codes are
// just two characters.
19 20 21 22
//
// This app is typically run by hand when a module's .arb files have been
// updated.
//
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
// ## Usage
//
// Run this program from the root of the git repository.
//
// The following outputs the generated Dart code to the console as a dry run:
//
// ```
// dart dev/tools/gen_localizations.dart
// ```
//
// If the data looks good, use the `-w` option to overwrite the
// packages/flutter_localizations/lib/src/l10n/localizations.dart file:
//
// ```
// dart dev/tools/gen_localizations.dart --overwrite
// ```
39

40
import 'dart:convert' show json;
41 42
import 'dart:io';

43 44 45
import 'package:path/path.dart' as pathlib;

import 'localizations_utils.dart';
46 47
import 'localizations_validator.dart';

48 49 50 51 52 53 54 55 56 57
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
// found in the LICENSE file.

// This file has been automatically generated.  Please do not edit it manually.
// To regenerate the file, use:
// @(regenerate)
''';

58
/// Maps locales to resource key/value pairs.
59 60
final Map<String, Map<String, String>> localeToResources = <String, Map<String, String>>{};

61
/// Maps locales to resource attributes.
62
///
63 64 65
/// See also https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
final Map<String, Map<String, dynamic>> localeToResourceAttributes = <String, Map<String, dynamic>>{};

66 67 68 69
// Return s as a Dart-parseable raw string in single or double quotes. Expand double quotes:
// foo => r'foo'
// foo "bar" => r'foo "bar"'
// foo 'bar' => r'foo ' "'" r'bar' "'"
70
String generateString(String s) {
71 72
  if (!s.contains("'"))
    return "r'$s'";
73 74 75 76

  final StringBuffer output = new StringBuffer();
  bool started = false; // Have we started writing a raw string.
  for (int i = 0; i < s.length; i++) {
77
    if (s[i] == "'") {
78
      if (started)
79 80
        output.write("'");
      output.write(' "\'" ');
81 82
      started = false;
    } else if (!started) {
83
      output.write("r'${s[i]}");
84 85 86 87 88 89
      started = true;
    } else {
      output.write(s[i]);
    }
  }
  if (started)
90
    output.write("'");
91 92 93
  return output.toString();
}

94
String generateTranslationBundles() {
95 96
  final StringBuffer output = new StringBuffer();

97 98
  final Map<String, List<String>> languageToLocales = <String, List<String>>{};
  final Set<String> allResourceIdentifiers = new Set<String>();
99
  for (String locale in localeToResources.keys.toList()..sort()) {
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
    final List<String> codes = locale.split('_'); // [language, country]
    assert(codes.length == 1 || codes.length == 2);
    languageToLocales[codes[0]] ??= <String>[];
    languageToLocales[codes[0]].add(locale);
    allResourceIdentifiers.addAll(localeToResources[locale].keys);
  }

  // Generate the TranslationsBundle base class. It contains one getter
  // per resource identifier found in any of the .arb files.
  //
  // class TranslationsBundle {
  //   const TranslationsBundle(this.parent);
  //   final TranslationsBundle parent;
  //   String get scriptCategory => parent?.scriptCategory;
  //   ...
  // }
116
  output.writeln('''
117 118 119 120 121 122 123
// The TranslationBundle subclasses defined here encode all of the translations
// found in the flutter_localizations/lib/src/l10n/*.arb files.
//
// The [MaterialLocalizations] class uses the (generated)
// translationBundleForLocale() function to look up a const TranslationBundle
// instance for a locale.

124 125
// ignore_for_file: public_member_api_docs

126
import \'dart:ui\' show Locale;
127

128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
class TranslationBundle {
  const TranslationBundle(this.parent);
  final TranslationBundle parent;''');
  for (String key in allResourceIdentifiers)
    output.writeln('  String get $key => parent?.$key;');
  output.writeln('''
}''');

  // Generate one private TranslationBundle subclass per supported
  // language. Each of these classes overrides every resource identifier
  // getter. For example:
  //
  // class _Bundle_en extends TranslationBundle {
  //   const _Bundle_en() : super(null);
  //   @override String get scriptCategory => r'English-like';
  //   ...
  // }
145
  for (String language in languageToLocales.keys) {
146 147
    final Map<String, String> resources = localeToResources[language];
    output.writeln('''
148

149 150 151 152 153 154 155
// ignore: camel_case_types
class _Bundle_$language extends TranslationBundle {
  const _Bundle_$language() : super(null);''');
    for (String key in resources.keys) {
      final String value = generateString(resources[key]);
      output.writeln('''
  @override String get $key => $value;''');
156
    }
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
   output.writeln('''
}''');
  }

  // Generate one private TranslationBundle subclass for each locale
  // with a country code. The parent of these subclasses is a const
  // instance of a translation bundle for the same locale, but without
  // a country code. These subclasses only override getters that
  // return different value than the parent class, or a resource identifier
  // that's not defined in the parent class. For example:
  //
  // class _Bundle_en_CA extends TranslationBundle {
  //   const _Bundle_en_CA() : super(const _Bundle_en());
  //   @override String get licensesPageTitle => r'Licences';
  //   ...
  // }
173
  for (String language in languageToLocales.keys) {
174
    final Map<String, String> languageResources = localeToResources[language];
175
    for (String localeName in languageToLocales[language]) {
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
      if (localeName == language)
        continue;
      final Map<String, String> localeResources = localeToResources[localeName];
      output.writeln('''

// ignore: camel_case_types
class _Bundle_$localeName extends TranslationBundle {
  const _Bundle_$localeName() : super(const _Bundle_$language());''');
      for (String key in localeResources.keys) {
        if (languageResources[key] == localeResources[key])
          continue;
        final String value = generateString(localeResources[key]);
        output.writeln('''
  @override String get $key => $value;''');
      }
     output.writeln('''
}''');
    }
  }

  // Generate the translationBundleForLocale function. Given a Locale
  // it returns the corresponding const TranslationBundle.
  output.writeln('''

TranslationBundle translationBundleForLocale(Locale locale) {
201 202
  switch (locale.languageCode) {''');
  for (String language in languageToLocales.keys) {
203 204 205 206 207 208 209
    if (languageToLocales[language].length == 1) {
      output.writeln('''
    case \'$language\':
      return const _Bundle_${languageToLocales[language][0]}();''');
    } else {
      output.writeln('''
    case \'$language\': {
210 211
      switch (locale.toString()) {''');
      for (String localeName in languageToLocales[language]) {
212 213 214 215 216 217 218 219 220 221 222 223 224
        if (localeName == language)
          continue;
        output.writeln('''
        case \'$localeName\':
          return const _Bundle_$localeName();''');
      }
      output.writeln('''
      }
      return const _Bundle_$language();
    }''');
    }
  }
  output.writeln('''
225
  }
226 227
  return const TranslationBundle(null);
}''');
228 229 230 231 232 233

  return output.toString();
}

void processBundle(File file, String locale) {
  localeToResources[locale] ??= <String, String>{};
234
  localeToResourceAttributes[locale] ??= <String, dynamic>{};
235
  final Map<String, String> resources = localeToResources[locale];
236
  final Map<String, dynamic> attributes = localeToResourceAttributes[locale];
237
  final Map<String, dynamic> bundle = json.decode(file.readAsStringSync());
238 239 240
  for (String key in bundle.keys) {
    // The ARB file resource "attributes" for foo are called @foo.
    if (key.startsWith('@'))
241 242 243
      attributes[key.substring(1)] = bundle[key];
    else
      resources[key] = bundle[key];
244 245 246
  }
}

247 248 249
void main(List<String> rawArgs) {
  checkCwdIsRepoRoot('gen_localizations');
  final GeneratorOptions options = parseArgs(rawArgs);
250 251 252 253 254

  // filenames are assumed to end in "prefix_lc.arb" or "prefix_lc_cc.arb", where prefix
  // is the 2nd command line argument, lc is a language code and cc is the country
  // code. In most cases both codes are just two characters.

255 256
  final Directory directory = new Directory(pathlib.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'));
  final RegExp filenameRE = new RegExp(r'material_(\w+)\.arb$');
257

258 259 260 261
  exitWithError(
    validateEnglishLocalizations(new File(pathlib.join(directory.path, 'material_en.arb')))
  );

262 263 264 265 266 267 268
  for (FileSystemEntity entity in directory.listSync()) {
    final String path = entity.path;
    if (FileSystemEntity.isFileSync(path) && filenameRE.hasMatch(path)) {
      final String locale = filenameRE.firstMatch(path)[1];
      processBundle(new File(path), locale);
    }
  }
269 270 271 272

  exitWithError(
    validateLocalizations(localeToResources, localeToResourceAttributes)
  );
273

274
  const String regenerate = 'dart dev/tools/gen_localizations.dart --overwrite';
275 276
  final StringBuffer buffer = new StringBuffer();
  buffer.writeln(outputHeader.replaceFirst('@(regenerate)', regenerate));
277
  buffer.write(generateTranslationBundles());
278 279 280

  if (options.writeToFile) {
    final File localizationsFile = new File(pathlib.join(directory.path, 'localizations.dart'));
281
    localizationsFile.writeAsStringSync(buffer.toString());
282
  } else {
283
    stdout.write(buffer.toString());
284
  }
285
}