blob: a2edf2dc00d33aab83445a8d0073e8af9f54ab52 [file] [log] [blame]
Hans Muller541afae2017-08-31 07:45:30 -07001// 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
Yegor41bd66f2017-10-31 20:23:58 -07005// This program generates a Dart "localizations" Map definition that combines
Hans Muller541afae2017-08-31 07:45:30 -07006// the contents of the arb files. The map can be used to lookup a localized
Yegor41bd66f2017-10-31 20:23:58 -07007// string: `localizations[localeString][resourceId]`.
Hans Muller541afae2017-08-31 07:45:30 -07008//
Yegor41bd66f2017-10-31 20:23:58 -07009// The *.arb files are in packages/flutter_localizations/lib/src/l10n.
Hans Muller541afae2017-08-31 07:45:30 -070010//
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//
Yegor41bd66f2017-10-31 20:23:58 -070015// 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 Muller541afae2017-08-31 07:45:30 -070019//
20// This app is typically run by hand when a module's .arb files have been
21// updated.
22//
Yegor41bd66f2017-10-31 20:23:58 -070023// ## 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 Muller541afae2017-08-31 07:45:30 -070039
40import 'dart:convert' show JSON;
41import 'dart:io';
42
Yegor41bd66f2017-10-31 20:23:58 -070043import 'package:path/path.dart' as pathlib;
44
45import 'localizations_utils.dart';
Yegorf4f20c22017-09-22 12:26:47 -070046import 'localizations_validator.dart';
47
Hans Muller541afae2017-08-31 07:45:30 -070048const 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
Yegorf4f20c22017-09-22 12:26:47 -070058/// Maps locales to resource key/value pairs.
Hans Muller541afae2017-08-31 07:45:30 -070059final Map<String, Map<String, String>> localeToResources = <String, Map<String, String>>{};
60
Yegorf4f20c22017-09-22 12:26:47 -070061/// Maps locales to resource attributes.
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +020062///
Yegorf4f20c22017-09-22 12:26:47 -070063/// See also https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
64final Map<String, Map<String, dynamic>> localeToResourceAttributes = <String, Map<String, dynamic>>{};
65
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +020066// 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 Muller541afae2017-08-31 07:45:30 -070070String generateString(String s) {
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +020071 if (!s.contains("'"))
72 return "r'$s'";
Hans Muller541afae2017-08-31 07:45:30 -070073
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 Ardhuin1fce14a2017-10-22 18:11:36 +020077 if (s[i] == "'") {
Hans Muller541afae2017-08-31 07:45:30 -070078 if (started)
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +020079 output.write("'");
80 output.write(' "\'" ');
Hans Muller541afae2017-08-31 07:45:30 -070081 started = false;
82 } else if (!started) {
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +020083 output.write("r'${s[i]}");
Hans Muller541afae2017-08-31 07:45:30 -070084 started = true;
85 } else {
86 output.write(s[i]);
87 }
88 }
89 if (started)
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +020090 output.write("'");
Hans Muller541afae2017-08-31 07:45:30 -070091 return output.toString();
92}
93
Hans Muller31418572017-12-19 14:02:22 -080094String generateTranslationBundles() {
Hans Muller541afae2017-08-31 07:45:30 -070095 final StringBuffer output = new StringBuffer();
96
Hans Muller31418572017-12-19 14:02:22 -080097 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 Muller541afae2017-08-31 07:45:30 -0700105 }
106
Hans Muller31418572017-12-19 14:02:22 -0800107 // 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
124import \'dart:ui\' show Locale;
125
126class 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
148class _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
180class _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
198TranslationBundle 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 Muller541afae2017-08-31 07:45:30 -0700227 return output.toString();
228}
229
230void processBundle(File file, String locale) {
231 localeToResources[locale] ??= <String, String>{};
Yegorf4f20c22017-09-22 12:26:47 -0700232 localeToResourceAttributes[locale] ??= <String, dynamic>{};
Hans Muller541afae2017-08-31 07:45:30 -0700233 final Map<String, String> resources = localeToResources[locale];
Yegorf4f20c22017-09-22 12:26:47 -0700234 final Map<String, dynamic> attributes = localeToResourceAttributes[locale];
Hans Muller541afae2017-08-31 07:45:30 -0700235 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('@'))
Yegorf4f20c22017-09-22 12:26:47 -0700239 attributes[key.substring(1)] = bundle[key];
240 else
241 resources[key] = bundle[key];
Hans Muller541afae2017-08-31 07:45:30 -0700242 }
243}
244
Yegor41bd66f2017-10-31 20:23:58 -0700245void main(List<String> rawArgs) {
246 checkCwdIsRepoRoot('gen_localizations');
247 final GeneratorOptions options = parseArgs(rawArgs);
Hans Muller541afae2017-08-31 07:45:30 -0700248
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
Yegor41bd66f2017-10-31 20:23:58 -0700253 final Directory directory = new Directory(pathlib.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'));
254 final RegExp filenameRE = new RegExp(r'material_(\w+)\.arb$');
Hans Muller541afae2017-08-31 07:45:30 -0700255
Hans Muller5c1320e2017-11-01 16:14:10 -0700256 exitWithError(
257 validateEnglishLocalizations(new File(pathlib.join(directory.path, 'material_en.arb')))
258 );
259
Hans Muller541afae2017-08-31 07:45:30 -0700260 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 Muller5c1320e2017-11-01 16:14:10 -0700267
268 exitWithError(
269 validateLocalizations(localeToResources, localeToResourceAttributes)
270 );
Hans Muller541afae2017-08-31 07:45:30 -0700271
Yegor41bd66f2017-10-31 20:23:58 -0700272 final String regenerate = 'dart dev/tools/gen_localizations.dart --overwrite';
273 final StringBuffer buffer = new StringBuffer();
274 buffer.writeln(outputHeader.replaceFirst('@(regenerate)', regenerate));
Hans Muller31418572017-12-19 14:02:22 -0800275 buffer.write(generateTranslationBundles());
Yegor41bd66f2017-10-31 20:23:58 -0700276
277 if (options.writeToFile) {
278 final File localizationsFile = new File(pathlib.join(directory.path, 'localizations.dart'));
Hans Muller31418572017-12-19 14:02:22 -0800279 localizationsFile.writeAsStringSync(buffer.toString());
Yegor41bd66f2017-10-31 20:23:58 -0700280 } else {
Hans Muller31418572017-12-19 14:02:22 -0800281 stdout.write(buffer.toString());
Yegor41bd66f2017-10-31 20:23:58 -0700282 }
Hans Muller541afae2017-08-31 07:45:30 -0700283}