mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

* Update project.pbxproj files to say Flutter rather than Chromium Also, the templates now have an empty organization so that we don't cause people to give their apps a Flutter copyright. * Update the copyright notice checker to require a standard notice on all files * Update copyrights on Dart files. (This was a mechanical commit.) * Fix weird license headers on Dart files that deviate from our conventions; relicense Shrine. Some were already marked "The Flutter Authors", not clear why. Their dates have been normalized. Some were missing the blank line after the license. Some were randomly different in trivial ways for no apparent reason (e.g. missing the trailing period). * Clean up the copyrights in non-Dart files. (Manual edits.) Also, make sure templates don't have copyrights. * Fix some more ORGANIZATIONNAMEs
419 lines
15 KiB
Dart
419 lines
15 KiB
Dart
// Copyright 2014 The Flutter 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:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:args/args.dart' as argslib;
|
|
import 'package:meta/meta.dart';
|
|
|
|
typedef HeaderGenerator = String Function(String regenerateInstructions);
|
|
typedef ConstructorGenerator = String Function(LocaleInfo locale);
|
|
|
|
int sortFilesByPath (FileSystemEntity a, FileSystemEntity b) {
|
|
return a.path.compareTo(b.path);
|
|
}
|
|
|
|
/// Simple data class to hold parsed locale. Does not promise validity of any data.
|
|
class LocaleInfo implements Comparable<LocaleInfo> {
|
|
LocaleInfo({
|
|
this.languageCode,
|
|
this.scriptCode,
|
|
this.countryCode,
|
|
this.length,
|
|
this.originalString,
|
|
});
|
|
|
|
/// Simple parser. Expects the locale string to be in the form of 'language_script_COUNTRY'
|
|
/// where the language is 2 characters, script is 4 characters with the first uppercase,
|
|
/// and country is 2-3 characters and all uppercase.
|
|
///
|
|
/// 'language_COUNTRY' or 'language_script' are also valid. Missing fields will be null.
|
|
///
|
|
/// When `deriveScriptCode` is true, if [scriptCode] was unspecified, it will
|
|
/// be derived from the [languageCode] and [countryCode] if possible.
|
|
factory LocaleInfo.fromString(String locale, { bool deriveScriptCode = false }) {
|
|
final List<String> codes = locale.split('_'); // [language, script, country]
|
|
assert(codes.isNotEmpty && codes.length < 4);
|
|
final String languageCode = codes[0];
|
|
String scriptCode;
|
|
String countryCode;
|
|
int length = codes.length;
|
|
String originalString = locale;
|
|
if (codes.length == 2) {
|
|
scriptCode = codes[1].length >= 4 ? codes[1] : null;
|
|
countryCode = codes[1].length < 4 ? codes[1] : null;
|
|
} else if (codes.length == 3) {
|
|
scriptCode = codes[1].length > codes[2].length ? codes[1] : codes[2];
|
|
countryCode = codes[1].length < codes[2].length ? codes[1] : codes[2];
|
|
}
|
|
assert(codes[0] != null && codes[0].isNotEmpty);
|
|
assert(countryCode == null || countryCode.isNotEmpty);
|
|
assert(scriptCode == null || scriptCode.isNotEmpty);
|
|
|
|
/// Adds scriptCodes to locales where we are able to assume it to provide
|
|
/// finer granularity when resolving locales.
|
|
///
|
|
/// The basis of the assumptions here are based off of known usage of scripts
|
|
/// across various countries. For example, we know Taiwan uses traditional (Hant)
|
|
/// script, so it is safe to apply (Hant) to Taiwanese languages.
|
|
if (deriveScriptCode && scriptCode == null) {
|
|
switch (languageCode) {
|
|
case 'zh': {
|
|
if (countryCode == null) {
|
|
scriptCode = 'Hans';
|
|
}
|
|
switch (countryCode) {
|
|
case 'CN':
|
|
case 'SG':
|
|
scriptCode = 'Hans';
|
|
break;
|
|
case 'TW':
|
|
case 'HK':
|
|
case 'MO':
|
|
scriptCode = 'Hant';
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
case 'sr': {
|
|
if (countryCode == null) {
|
|
scriptCode = 'Cyrl';
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
// Increment length if we were able to assume a scriptCode.
|
|
if (scriptCode != null) {
|
|
length += 1;
|
|
}
|
|
// Update the base string to reflect assumed scriptCodes.
|
|
originalString = languageCode;
|
|
if (scriptCode != null)
|
|
originalString += '_' + scriptCode;
|
|
if (countryCode != null)
|
|
originalString += '_' + countryCode;
|
|
}
|
|
|
|
return LocaleInfo(
|
|
languageCode: languageCode,
|
|
scriptCode: scriptCode,
|
|
countryCode: countryCode,
|
|
length: length,
|
|
originalString: originalString,
|
|
);
|
|
}
|
|
|
|
final String languageCode;
|
|
final String scriptCode;
|
|
final String countryCode;
|
|
final int length; // The number of fields. Ranges from 1-3.
|
|
final String originalString; // Original un-parsed locale string.
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (!(other is LocaleInfo))
|
|
return false;
|
|
final LocaleInfo otherLocale = other;
|
|
return originalString == otherLocale.originalString;
|
|
}
|
|
|
|
@override
|
|
int get hashCode {
|
|
return originalString.hashCode;
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return originalString;
|
|
}
|
|
|
|
@override
|
|
int compareTo(LocaleInfo other) {
|
|
return originalString.compareTo(other.originalString);
|
|
}
|
|
}
|
|
|
|
/// Parse the data for a locale from a file, and store it in the [attributes]
|
|
/// and [resources] keys.
|
|
void loadMatchingArbsIntoBundleMaps({
|
|
@required Directory directory,
|
|
@required RegExp filenamePattern,
|
|
@required Map<LocaleInfo, Map<String, String>> localeToResources,
|
|
@required Map<LocaleInfo, Map<String, dynamic>> localeToResourceAttributes,
|
|
}) {
|
|
assert(directory != null);
|
|
assert(filenamePattern != null);
|
|
assert(localeToResources != null);
|
|
assert(localeToResourceAttributes != null);
|
|
|
|
/// Set that holds the locales that were assumed from the existing locales.
|
|
///
|
|
/// For example, when the data lacks data for zh_Hant, we will use the data of
|
|
/// the first Hant Chinese locale as a default by repeating the data. If an
|
|
/// explicit match is later found, we can reference this set to see if we should
|
|
/// overwrite the existing assumed data.
|
|
final Set<LocaleInfo> assumedLocales = <LocaleInfo>{};
|
|
|
|
for (FileSystemEntity entity in directory.listSync().toList()..sort(sortFilesByPath)) {
|
|
final String entityPath = entity.path;
|
|
if (FileSystemEntity.isFileSync(entityPath) && filenamePattern.hasMatch(entityPath)) {
|
|
final String localeString = filenamePattern.firstMatch(entityPath)[1];
|
|
final File arbFile = File(entityPath);
|
|
|
|
// Helper method to fill the maps with the correct data from file.
|
|
void populateResources(LocaleInfo locale, File file) {
|
|
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('@'))
|
|
attributes[key.substring(1)] = bundle[key];
|
|
else
|
|
resources[key] = bundle[key];
|
|
}
|
|
}
|
|
// Only pre-assume scriptCode if there is a country or script code to assume off of.
|
|
// When we assume scriptCode based on languageCode-only, we want this initial pass
|
|
// to use the un-assumed version as a base class.
|
|
LocaleInfo locale = LocaleInfo.fromString(localeString, deriveScriptCode: localeString.split('_').length > 1);
|
|
// Allow overwrite if the existing data is assumed.
|
|
if (assumedLocales.contains(locale)) {
|
|
localeToResources[locale] = <String, String>{};
|
|
localeToResourceAttributes[locale] = <String, dynamic>{};
|
|
assumedLocales.remove(locale);
|
|
} else {
|
|
localeToResources[locale] ??= <String, String>{};
|
|
localeToResourceAttributes[locale] ??= <String, dynamic>{};
|
|
}
|
|
populateResources(locale, arbFile);
|
|
// Add an assumed locale to default to when there is no info on scriptOnly locales.
|
|
locale = LocaleInfo.fromString(localeString, deriveScriptCode: true);
|
|
if (locale.scriptCode != null) {
|
|
final LocaleInfo scriptLocale = LocaleInfo.fromString(locale.languageCode + '_' + locale.scriptCode);
|
|
if (!localeToResources.containsKey(scriptLocale)) {
|
|
assumedLocales.add(scriptLocale);
|
|
localeToResources[scriptLocale] ??= <String, String>{};
|
|
localeToResourceAttributes[scriptLocale] ??= <String, dynamic>{};
|
|
populateResources(scriptLocale, arbFile);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void exitWithError(String errorMessage) {
|
|
assert(errorMessage != null);
|
|
stderr.writeln('fatal: $errorMessage');
|
|
exit(1);
|
|
}
|
|
|
|
void checkCwdIsRepoRoot(String commandName) {
|
|
final bool isRepoRoot = Directory('.git').existsSync();
|
|
|
|
if (!isRepoRoot) {
|
|
exitWithError(
|
|
'$commandName must be run from the root of the Flutter repository. The '
|
|
'current working directory is: ${Directory.current.path}'
|
|
);
|
|
}
|
|
}
|
|
|
|
String camelCase(LocaleInfo locale) {
|
|
return locale.originalString
|
|
.split('_')
|
|
.map<String>((String part) => part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase())
|
|
.join('');
|
|
}
|
|
|
|
GeneratorOptions parseArgs(List<String> rawArgs) {
|
|
final argslib.ArgParser argParser = argslib.ArgParser()
|
|
..addFlag(
|
|
'overwrite',
|
|
abbr: 'w',
|
|
defaultsTo: false,
|
|
)
|
|
..addFlag(
|
|
'material',
|
|
help: 'Whether to print the generated classes for the Material package only. Ignored when --overwrite is passed.',
|
|
defaultsTo: false,
|
|
)
|
|
..addFlag(
|
|
'cupertino',
|
|
help: 'Whether to print the generated classes for the Cupertino package only. Ignored when --overwrite is passed.',
|
|
defaultsTo: false,
|
|
);
|
|
final argslib.ArgResults args = argParser.parse(rawArgs);
|
|
final bool writeToFile = args['overwrite'];
|
|
final bool materialOnly = args['material'];
|
|
final bool cupertinoOnly = args['cupertino'];
|
|
|
|
return GeneratorOptions(writeToFile: writeToFile, materialOnly: materialOnly, cupertinoOnly: cupertinoOnly);
|
|
}
|
|
|
|
class GeneratorOptions {
|
|
GeneratorOptions({
|
|
@required this.writeToFile,
|
|
@required this.materialOnly,
|
|
@required this.cupertinoOnly,
|
|
});
|
|
|
|
final bool writeToFile;
|
|
final bool materialOnly;
|
|
final bool cupertinoOnly;
|
|
}
|
|
|
|
const String registry = 'https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry';
|
|
|
|
// See also //master/tools/gen_locale.dart in the engine repo.
|
|
Map<String, List<String>> _parseSection(String section) {
|
|
final Map<String, List<String>> result = <String, List<String>>{};
|
|
List<String> lastHeading;
|
|
for (String line in section.split('\n')) {
|
|
if (line == '')
|
|
continue;
|
|
if (line.startsWith(' ')) {
|
|
lastHeading[lastHeading.length - 1] = '${lastHeading.last}${line.substring(1)}';
|
|
continue;
|
|
}
|
|
final int colon = line.indexOf(':');
|
|
if (colon <= 0)
|
|
throw 'not sure how to deal with "$line"';
|
|
final String name = line.substring(0, colon);
|
|
final String value = line.substring(colon + 2);
|
|
lastHeading = result.putIfAbsent(name, () => <String>[]);
|
|
result[name].add(value);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
final Map<String, String> _languages = <String, String>{};
|
|
final Map<String, String> _regions = <String, String>{};
|
|
final Map<String, String> _scripts = <String, String>{};
|
|
const String kProvincePrefix = ', Province of ';
|
|
const String kParentheticalPrefix = ' (';
|
|
|
|
/// Prepares the data for the [describeLocale] method below.
|
|
///
|
|
/// The data is obtained from the official IANA registry.
|
|
Future<void> precacheLanguageAndRegionTags() async {
|
|
final HttpClient client = HttpClient();
|
|
final HttpClientRequest request = await client.getUrl(Uri.parse(registry));
|
|
final HttpClientResponse response = await request.close();
|
|
final String body = (await response.cast<List<int>>().transform<String>(utf8.decoder).toList()).join('');
|
|
client.close(force: true);
|
|
final List<Map<String, List<String>>> sections = body.split('%%').skip(1).map<Map<String, List<String>>>(_parseSection).toList();
|
|
for (Map<String, List<String>> section in sections) {
|
|
assert(section.containsKey('Type'), section.toString());
|
|
final String type = section['Type'].single;
|
|
if (type == 'language' || type == 'region' || type == 'script') {
|
|
assert(section.containsKey('Subtag') && section.containsKey('Description'), section.toString());
|
|
final String subtag = section['Subtag'].single;
|
|
String description = section['Description'].join(' ');
|
|
if (description.startsWith('United '))
|
|
description = 'the $description';
|
|
if (description.contains(kParentheticalPrefix))
|
|
description = description.substring(0, description.indexOf(kParentheticalPrefix));
|
|
if (description.contains(kProvincePrefix))
|
|
description = description.substring(0, description.indexOf(kProvincePrefix));
|
|
if (description.endsWith(' Republic'))
|
|
description = 'the $description';
|
|
switch (type) {
|
|
case 'language':
|
|
_languages[subtag] = description;
|
|
break;
|
|
case 'region':
|
|
_regions[subtag] = description;
|
|
break;
|
|
case 'script':
|
|
_scripts[subtag] = description;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
String describeLocale(String tag) {
|
|
final List<String> subtags = tag.split('_');
|
|
assert(subtags.isNotEmpty);
|
|
assert(_languages.containsKey(subtags[0]));
|
|
final String language = _languages[subtags[0]];
|
|
String output = '$language';
|
|
String region;
|
|
String script;
|
|
if (subtags.length == 2) {
|
|
region = _regions[subtags[1]];
|
|
script = _scripts[subtags[1]];
|
|
assert(region != null || script != null);
|
|
} else if (subtags.length >= 3) {
|
|
region = _regions[subtags[2]];
|
|
script = _scripts[subtags[1]];
|
|
assert(region != null && script != null);
|
|
}
|
|
if (region != null)
|
|
output += ', as used in $region';
|
|
if (script != null)
|
|
output += ', using the $script script';
|
|
return output;
|
|
}
|
|
|
|
/// Writes the header of each class which corresponds to a locale.
|
|
String generateClassDeclaration(
|
|
LocaleInfo locale,
|
|
String classNamePrefix,
|
|
String superClass,
|
|
) {
|
|
final String camelCaseName = camelCase(locale);
|
|
return '''
|
|
|
|
/// The translations for ${describeLocale(locale.originalString)} (`${locale.originalString}`).
|
|
class $classNamePrefix$camelCaseName extends $superClass {''';
|
|
}
|
|
|
|
/// Return `s` as a Dart-parseable raw string in single or double quotes.
|
|
///
|
|
/// Double quotes are expanded:
|
|
///
|
|
/// ```
|
|
/// foo => r'foo'
|
|
/// foo "bar" => r'foo "bar"'
|
|
/// foo 'bar' => r'foo ' "'" r'bar' "'"
|
|
/// ```
|
|
String generateString(String s) {
|
|
if (!s.contains("'"))
|
|
return "r'$s'";
|
|
|
|
final StringBuffer output = StringBuffer();
|
|
bool started = false; // Have we started writing a raw string.
|
|
for (int i = 0; i < s.length; i++) {
|
|
if (s[i] == "'") {
|
|
if (started)
|
|
output.write("'");
|
|
output.write(' "\'" ');
|
|
started = false;
|
|
} else if (!started) {
|
|
output.write("r'${s[i]}");
|
|
started = true;
|
|
} else {
|
|
output.write(s[i]);
|
|
}
|
|
}
|
|
if (started)
|
|
output.write("'");
|
|
return output.toString();
|
|
}
|
|
|
|
/// Only used to generate localization strings for the Kannada locale ('kn') because
|
|
/// some of the localized strings contain characters that can crash Emacs on Linux.
|
|
/// See packages/flutter_localizations/lib/src/l10n/README for more information.
|
|
String generateEncodedString(String s) {
|
|
if (s.runes.every((int code) => code <= 0xFF))
|
|
return generateString(s);
|
|
|
|
final String unicodeEscapes = s.runes.map((int code) => '\\u{${code.toRadixString(16)}}').join();
|
|
return "'$unicodeEscapes'";
|
|
}
|