Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 1 | // Copyright 2017 The Chromium 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 | |
Yegor | 41bd66f | 2017-10-31 20:23:58 -0700 | [diff] [blame] | 5 | // This program generates a Dart "localizations" Map definition that combines |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 6 | // the contents of the arb files. The map can be used to lookup a localized |
Yegor | 41bd66f | 2017-10-31 20:23:58 -0700 | [diff] [blame] | 7 | // string: `localizations[localeString][resourceId]`. |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 8 | // |
Yegor | 41bd66f | 2017-10-31 20:23:58 -0700 | [diff] [blame] | 9 | // The *.arb files are in packages/flutter_localizations/lib/src/l10n. |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 10 | // |
| 11 | // The arb (JSON) format files must contain a single map indexed by locale. |
| 12 | // Each map value is itself a map with resource identifier keys and localized |
| 13 | // resource string values. |
| 14 | // |
Yegor | 41bd66f | 2017-10-31 20:23:58 -0700 | [diff] [blame] | 15 | // The arb filenames are expected to have the form "material_(\w+)\.arb", where |
| 16 | // the group following "_" identifies the language code and the country code, |
| 17 | // e.g. "material_en.arb" or "material_en_GB.arb". In most cases both codes are |
| 18 | // just two characters. |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 19 | // |
| 20 | // This app is typically run by hand when a module's .arb files have been |
| 21 | // updated. |
| 22 | // |
Yegor | 41bd66f | 2017-10-31 20:23:58 -0700 | [diff] [blame] | 23 | // ## Usage |
| 24 | // |
| 25 | // Run this program from the root of the git repository. |
| 26 | // |
| 27 | // The following outputs the generated Dart code to the console as a dry run: |
| 28 | // |
| 29 | // ``` |
| 30 | // dart dev/tools/gen_localizations.dart |
| 31 | // ``` |
| 32 | // |
| 33 | // If the data looks good, use the `-w` option to overwrite the |
| 34 | // packages/flutter_localizations/lib/src/l10n/localizations.dart file: |
| 35 | // |
| 36 | // ``` |
| 37 | // dart dev/tools/gen_localizations.dart --overwrite |
| 38 | // ``` |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 39 | |
| 40 | import 'dart:convert' show JSON; |
| 41 | import 'dart:io'; |
| 42 | |
Yegor | 41bd66f | 2017-10-31 20:23:58 -0700 | [diff] [blame] | 43 | import 'package:path/path.dart' as pathlib; |
| 44 | |
| 45 | import 'localizations_utils.dart'; |
Yegor | f4f20c2 | 2017-09-22 12:26:47 -0700 | [diff] [blame] | 46 | import 'localizations_validator.dart'; |
| 47 | |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 48 | const String outputHeader = ''' |
| 49 | // Copyright 2017 The Chromium Authors. All rights reserved. |
| 50 | // Use of this source code is governed by a BSD-style license that can be |
| 51 | // found in the LICENSE file. |
| 52 | |
| 53 | // This file has been automatically generated. Please do not edit it manually. |
| 54 | // To regenerate the file, use: |
| 55 | // @(regenerate) |
| 56 | '''; |
| 57 | |
Yegor | f4f20c2 | 2017-09-22 12:26:47 -0700 | [diff] [blame] | 58 | /// Maps locales to resource key/value pairs. |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 59 | final Map<String, Map<String, String>> localeToResources = <String, Map<String, String>>{}; |
| 60 | |
Yegor | f4f20c2 | 2017-09-22 12:26:47 -0700 | [diff] [blame] | 61 | /// Maps locales to resource attributes. |
Alexandre Ardhuin | 1fce14a | 2017-10-22 18:11:36 +0200 | [diff] [blame] | 62 | /// |
Yegor | f4f20c2 | 2017-09-22 12:26:47 -0700 | [diff] [blame] | 63 | /// See also https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes |
| 64 | final Map<String, Map<String, dynamic>> localeToResourceAttributes = <String, Map<String, dynamic>>{}; |
| 65 | |
Alexandre Ardhuin | 1fce14a | 2017-10-22 18:11:36 +0200 | [diff] [blame] | 66 | // Return s as a Dart-parseable raw string in single or double quotes. Expand double quotes: |
| 67 | // foo => r'foo' |
| 68 | // foo "bar" => r'foo "bar"' |
| 69 | // foo 'bar' => r'foo ' "'" r'bar' "'" |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 70 | String generateString(String s) { |
Alexandre Ardhuin | 1fce14a | 2017-10-22 18:11:36 +0200 | [diff] [blame] | 71 | if (!s.contains("'")) |
| 72 | return "r'$s'"; |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 73 | |
| 74 | final StringBuffer output = new StringBuffer(); |
| 75 | bool started = false; // Have we started writing a raw string. |
| 76 | for (int i = 0; i < s.length; i++) { |
Alexandre Ardhuin | 1fce14a | 2017-10-22 18:11:36 +0200 | [diff] [blame] | 77 | if (s[i] == "'") { |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 78 | if (started) |
Alexandre Ardhuin | 1fce14a | 2017-10-22 18:11:36 +0200 | [diff] [blame] | 79 | output.write("'"); |
| 80 | output.write(' "\'" '); |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 81 | started = false; |
| 82 | } else if (!started) { |
Alexandre Ardhuin | 1fce14a | 2017-10-22 18:11:36 +0200 | [diff] [blame] | 83 | output.write("r'${s[i]}"); |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 84 | started = true; |
| 85 | } else { |
| 86 | output.write(s[i]); |
| 87 | } |
| 88 | } |
| 89 | if (started) |
Alexandre Ardhuin | 1fce14a | 2017-10-22 18:11:36 +0200 | [diff] [blame] | 90 | output.write("'"); |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 91 | return output.toString(); |
| 92 | } |
| 93 | |
Hans Muller | 3141857 | 2017-12-19 14:02:22 -0800 | [diff] [blame^] | 94 | String generateTranslationBundles() { |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 95 | final StringBuffer output = new StringBuffer(); |
| 96 | |
Hans Muller | 3141857 | 2017-12-19 14:02:22 -0800 | [diff] [blame^] | 97 | final Map<String, List<String>> languageToLocales = <String, List<String>>{}; |
| 98 | final Set<String> allResourceIdentifiers = new Set<String>(); |
| 99 | for(String locale in localeToResources.keys.toList()..sort()) { |
| 100 | final List<String> codes = locale.split('_'); // [language, country] |
| 101 | assert(codes.length == 1 || codes.length == 2); |
| 102 | languageToLocales[codes[0]] ??= <String>[]; |
| 103 | languageToLocales[codes[0]].add(locale); |
| 104 | allResourceIdentifiers.addAll(localeToResources[locale].keys); |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 105 | } |
| 106 | |
Hans Muller | 3141857 | 2017-12-19 14:02:22 -0800 | [diff] [blame^] | 107 | // Generate the TranslationsBundle base class. It contains one getter |
| 108 | // per resource identifier found in any of the .arb files. |
| 109 | // |
| 110 | // class TranslationsBundle { |
| 111 | // const TranslationsBundle(this.parent); |
| 112 | // final TranslationsBundle parent; |
| 113 | // String get scriptCategory => parent?.scriptCategory; |
| 114 | // ... |
| 115 | // } |
| 116 | output.writeln(''' |
| 117 | // The TranslationBundle subclasses defined here encode all of the translations |
| 118 | // found in the flutter_localizations/lib/src/l10n/*.arb files. |
| 119 | // |
| 120 | // The [MaterialLocalizations] class uses the (generated) |
| 121 | // translationBundleForLocale() function to look up a const TranslationBundle |
| 122 | // instance for a locale. |
| 123 | |
| 124 | import \'dart:ui\' show Locale; |
| 125 | |
| 126 | class TranslationBundle { |
| 127 | const TranslationBundle(this.parent); |
| 128 | final TranslationBundle parent;'''); |
| 129 | for (String key in allResourceIdentifiers) |
| 130 | output.writeln(' String get $key => parent?.$key;'); |
| 131 | output.writeln(''' |
| 132 | }'''); |
| 133 | |
| 134 | // Generate one private TranslationBundle subclass per supported |
| 135 | // language. Each of these classes overrides every resource identifier |
| 136 | // getter. For example: |
| 137 | // |
| 138 | // class _Bundle_en extends TranslationBundle { |
| 139 | // const _Bundle_en() : super(null); |
| 140 | // @override String get scriptCategory => r'English-like'; |
| 141 | // ... |
| 142 | // } |
| 143 | for(String language in languageToLocales.keys) { |
| 144 | final Map<String, String> resources = localeToResources[language]; |
| 145 | output.writeln(''' |
| 146 | |
| 147 | // ignore: camel_case_types |
| 148 | class _Bundle_$language extends TranslationBundle { |
| 149 | const _Bundle_$language() : super(null);'''); |
| 150 | for (String key in resources.keys) { |
| 151 | final String value = generateString(resources[key]); |
| 152 | output.writeln(''' |
| 153 | @override String get $key => $value;'''); |
| 154 | } |
| 155 | output.writeln(''' |
| 156 | }'''); |
| 157 | } |
| 158 | |
| 159 | // Generate one private TranslationBundle subclass for each locale |
| 160 | // with a country code. The parent of these subclasses is a const |
| 161 | // instance of a translation bundle for the same locale, but without |
| 162 | // a country code. These subclasses only override getters that |
| 163 | // return different value than the parent class, or a resource identifier |
| 164 | // that's not defined in the parent class. For example: |
| 165 | // |
| 166 | // class _Bundle_en_CA extends TranslationBundle { |
| 167 | // const _Bundle_en_CA() : super(const _Bundle_en()); |
| 168 | // @override String get licensesPageTitle => r'Licences'; |
| 169 | // ... |
| 170 | // } |
| 171 | for(String language in languageToLocales.keys) { |
| 172 | final Map<String, String> languageResources = localeToResources[language]; |
| 173 | for(String localeName in languageToLocales[language]) { |
| 174 | if (localeName == language) |
| 175 | continue; |
| 176 | final Map<String, String> localeResources = localeToResources[localeName]; |
| 177 | output.writeln(''' |
| 178 | |
| 179 | // ignore: camel_case_types |
| 180 | class _Bundle_$localeName extends TranslationBundle { |
| 181 | const _Bundle_$localeName() : super(const _Bundle_$language());'''); |
| 182 | for (String key in localeResources.keys) { |
| 183 | if (languageResources[key] == localeResources[key]) |
| 184 | continue; |
| 185 | final String value = generateString(localeResources[key]); |
| 186 | output.writeln(''' |
| 187 | @override String get $key => $value;'''); |
| 188 | } |
| 189 | output.writeln(''' |
| 190 | }'''); |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | // Generate the translationBundleForLocale function. Given a Locale |
| 195 | // it returns the corresponding const TranslationBundle. |
| 196 | output.writeln(''' |
| 197 | |
| 198 | TranslationBundle translationBundleForLocale(Locale locale) { |
| 199 | switch(locale.languageCode) {'''); |
| 200 | for(String language in languageToLocales.keys) { |
| 201 | if (languageToLocales[language].length == 1) { |
| 202 | output.writeln(''' |
| 203 | case \'$language\': |
| 204 | return const _Bundle_${languageToLocales[language][0]}();'''); |
| 205 | } else { |
| 206 | output.writeln(''' |
| 207 | case \'$language\': { |
| 208 | switch(locale.toString()) {'''); |
| 209 | for(String localeName in languageToLocales[language]) { |
| 210 | if (localeName == language) |
| 211 | continue; |
| 212 | output.writeln(''' |
| 213 | case \'$localeName\': |
| 214 | return const _Bundle_$localeName();'''); |
| 215 | } |
| 216 | output.writeln(''' |
| 217 | } |
| 218 | return const _Bundle_$language(); |
| 219 | }'''); |
| 220 | } |
| 221 | } |
| 222 | output.writeln(''' |
| 223 | } |
| 224 | return const TranslationBundle(null); |
| 225 | }'''); |
| 226 | |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 227 | return output.toString(); |
| 228 | } |
| 229 | |
| 230 | void processBundle(File file, String locale) { |
| 231 | localeToResources[locale] ??= <String, String>{}; |
Yegor | f4f20c2 | 2017-09-22 12:26:47 -0700 | [diff] [blame] | 232 | localeToResourceAttributes[locale] ??= <String, dynamic>{}; |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 233 | final Map<String, String> resources = localeToResources[locale]; |
Yegor | f4f20c2 | 2017-09-22 12:26:47 -0700 | [diff] [blame] | 234 | final Map<String, dynamic> attributes = localeToResourceAttributes[locale]; |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 235 | final Map<String, dynamic> bundle = JSON.decode(file.readAsStringSync()); |
| 236 | for (String key in bundle.keys) { |
| 237 | // The ARB file resource "attributes" for foo are called @foo. |
| 238 | if (key.startsWith('@')) |
Yegor | f4f20c2 | 2017-09-22 12:26:47 -0700 | [diff] [blame] | 239 | attributes[key.substring(1)] = bundle[key]; |
| 240 | else |
| 241 | resources[key] = bundle[key]; |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 242 | } |
| 243 | } |
| 244 | |
Yegor | 41bd66f | 2017-10-31 20:23:58 -0700 | [diff] [blame] | 245 | void main(List<String> rawArgs) { |
| 246 | checkCwdIsRepoRoot('gen_localizations'); |
| 247 | final GeneratorOptions options = parseArgs(rawArgs); |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 248 | |
| 249 | // filenames are assumed to end in "prefix_lc.arb" or "prefix_lc_cc.arb", where prefix |
| 250 | // is the 2nd command line argument, lc is a language code and cc is the country |
| 251 | // code. In most cases both codes are just two characters. |
| 252 | |
Yegor | 41bd66f | 2017-10-31 20:23:58 -0700 | [diff] [blame] | 253 | final Directory directory = new Directory(pathlib.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n')); |
| 254 | final RegExp filenameRE = new RegExp(r'material_(\w+)\.arb$'); |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 255 | |
Hans Muller | 5c1320e | 2017-11-01 16:14:10 -0700 | [diff] [blame] | 256 | exitWithError( |
| 257 | validateEnglishLocalizations(new File(pathlib.join(directory.path, 'material_en.arb'))) |
| 258 | ); |
| 259 | |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 260 | for (FileSystemEntity entity in directory.listSync()) { |
| 261 | final String path = entity.path; |
| 262 | if (FileSystemEntity.isFileSync(path) && filenameRE.hasMatch(path)) { |
| 263 | final String locale = filenameRE.firstMatch(path)[1]; |
| 264 | processBundle(new File(path), locale); |
| 265 | } |
| 266 | } |
Hans Muller | 5c1320e | 2017-11-01 16:14:10 -0700 | [diff] [blame] | 267 | |
| 268 | exitWithError( |
| 269 | validateLocalizations(localeToResources, localeToResourceAttributes) |
| 270 | ); |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 271 | |
Yegor | 41bd66f | 2017-10-31 20:23:58 -0700 | [diff] [blame] | 272 | final String regenerate = 'dart dev/tools/gen_localizations.dart --overwrite'; |
| 273 | final StringBuffer buffer = new StringBuffer(); |
| 274 | buffer.writeln(outputHeader.replaceFirst('@(regenerate)', regenerate)); |
Hans Muller | 3141857 | 2017-12-19 14:02:22 -0800 | [diff] [blame^] | 275 | buffer.write(generateTranslationBundles()); |
Yegor | 41bd66f | 2017-10-31 20:23:58 -0700 | [diff] [blame] | 276 | |
| 277 | if (options.writeToFile) { |
| 278 | final File localizationsFile = new File(pathlib.join(directory.path, 'localizations.dart')); |
Hans Muller | 3141857 | 2017-12-19 14:02:22 -0800 | [diff] [blame^] | 279 | localizationsFile.writeAsStringSync(buffer.toString()); |
Yegor | 41bd66f | 2017-10-31 20:23:58 -0700 | [diff] [blame] | 280 | } else { |
Hans Muller | 3141857 | 2017-12-19 14:02:22 -0800 | [diff] [blame^] | 281 | stdout.write(buffer.toString()); |
Yegor | 41bd66f | 2017-10-31 20:23:58 -0700 | [diff] [blame] | 282 | } |
Hans Muller | 541afae | 2017-08-31 07:45:30 -0700 | [diff] [blame] | 283 | } |