flutter/dev/tools/localization/gen_l10n.dart
2019-12-05 22:34:06 +01:00

486 lines
17 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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:path/path.dart' as path;
import 'localizations_utils.dart';
const String defaultFileTemplate = '''
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart';
import 'messages_all.dart';
/// Callers can lookup localized strings with an instance of @className returned
/// by `@className.of(context)`.
///
/// Applications need to include `@className.delegate()` in their app\'s
/// localizationDelegates list, and the locales they support in the app\'s
/// supportedLocales list. For example:
///
/// ```
/// import '@importFile';
///
/// return MaterialApp(
/// localizationsDelegates: @className.localizationsDelegates,
/// supportedLocales: @className.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: 0.16.0
/// intl_translation: 0.17.7
///
/// # rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the @className.supportedLocales
/// property.
class @className {
@className(Locale locale) : _localeName = Intl.canonicalizedLocale(locale.toString());
final String _localeName;
static Future<@className> load(Locale locale) {
return initializeMessages(locale.toString())
.then<@className>((_) => @className(locale));
}
static @className of(BuildContext context) {
return Localizations.of<@className>(context, @className);
}
static const LocalizationsDelegate<@className> delegate = _@classNameDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
@supportedLocales
@classMethods
}
class _@classNameDelegate extends LocalizationsDelegate<@className> {
const _@classNameDelegate();
@override
Future<@className> load(Locale locale) => @className.load(locale);
@override
bool isSupported(Locale locale) => <String>[@supportedLanguageCodes].contains(locale.languageCode);
@override
bool shouldReload(_@classNameDelegate old) => false;
}
''';
const String getterMethodTemplate = '''
String get @methodName {
return Intl.message(
@message,
locale: _localeName,
@intlMethodArgs
);
}
''';
const String simpleMethodTemplate = '''
String @methodName(@methodParameters) {
return Intl.message(
@message,
locale: _localeName,
@intlMethodArgs
);
}
''';
const String pluralMethodTemplate = '''
String @methodName(@methodParameters) {
return Intl.plural(
@intlMethodArgs
);
}
''';
int sortFilesByPath (FileSystemEntity a, FileSystemEntity b) {
return a.path.compareTo(b.path);
}
List<String> genMethodParameters(Map<String, dynamic> bundle, String key, String type) {
final Map<String, dynamic> attributesMap = bundle['@$key'] as Map<String, dynamic>;
if (attributesMap != null && attributesMap.containsKey('placeholders')) {
final Map<String, dynamic> placeholders = attributesMap['placeholders'] as Map<String, dynamic>;
return placeholders.keys.map((String parameter) => '$type $parameter').toList();
}
return <String>[];
}
List<String> genIntlMethodArgs(Map<String, dynamic> bundle, String key) {
final List<String> attributes = <String>['name: \'$key\''];
final Map<String, dynamic> attributesMap = bundle['@$key'] as Map<String, dynamic>;
if (attributesMap != null) {
if (attributesMap.containsKey('description')) {
final String description = attributesMap['description'] as String;
attributes.add('desc: ${generateString(description)}');
}
if (attributesMap.containsKey('placeholders')) {
final Map<String, dynamic> placeholders = attributesMap['placeholders'] as Map<String, dynamic>;
if (placeholders.isNotEmpty) {
final String args = placeholders.keys.join(', ');
attributes.add('args: <Object>[$args]');
}
}
}
return attributes;
}
String genSimpleMethod(Map<String, dynamic> bundle, String key) {
String genSimpleMethodMessage(Map<String, dynamic> bundle, String key) {
String message = bundle[key] as String;
final Map<String, dynamic> attributesMap = bundle['@$key'] as Map<String, dynamic>;
final Map<String, dynamic> placeholders = attributesMap['placeholders'] as Map<String, dynamic>;
for (String placeholder in placeholders.keys)
message = message.replaceAll('{$placeholder}', '\$$placeholder');
return generateString(message);
}
final Map<String, dynamic> attributesMap = bundle['@$key'] as Map<String, dynamic>;
if (attributesMap == null)
exitWithError(
'Resource attribute "@$key" was not found. Please ensure that each '
'resource id has a corresponding resource attribute.'
);
if (attributesMap.containsKey('placeholders')) {
return simpleMethodTemplate
.replaceAll('@methodName', key)
.replaceAll('@methodParameters', genMethodParameters(bundle, key, 'Object').join(', '))
.replaceAll('@message', '${genSimpleMethodMessage(bundle, key)}')
.replaceAll('@intlMethodArgs', genIntlMethodArgs(bundle, key).join(',\n '));
}
return getterMethodTemplate
.replaceAll('@methodName', key)
.replaceAll('@message', '${generateString(bundle[key] as String)}')
.replaceAll('@intlMethodArgs', genIntlMethodArgs(bundle, key).join(',\n '));
}
String genPluralMethod(Map<String, dynamic> bundle, String key) {
final Map<String, dynamic> attributesMap = bundle['@$key'] as Map<String, dynamic>;
assert(attributesMap != null && attributesMap.containsKey('placeholders'));
final Iterable<String> placeholders = attributesMap['placeholders'].keys as Iterable<String>;
// To make it easier to parse the plurals message, temporarily replace each
// "{placeholder}" parameter with "#placeholder#".
String message = bundle[key] as String;
for (String placeholder in placeholders)
message = message.replaceAll('{$placeholder}', '#$placeholder#');
final Map<String, String> pluralIds = <String, String>{
'=0': 'zero',
'=1': 'one',
'=2': 'two',
'few': 'few',
'many': 'many',
'other': 'other'
};
final List<String> methodArgs = <String>[
...placeholders,
'locale: _localeName',
...genIntlMethodArgs(bundle, key),
];
for(String pluralKey in pluralIds.keys) {
final RegExp expRE = RegExp('($pluralKey){([^}]+)}');
final RegExpMatch match = expRE.firstMatch(message);
if (match != null && match.groupCount == 2) {
String argValue = match.group(2);
for (String placeholder in placeholders)
argValue = argValue.replaceAll('#$placeholder#', '\$$placeholder');
methodArgs.add("${pluralIds[pluralKey]}: '$argValue'");
}
}
return pluralMethodTemplate
.replaceAll('@methodName', key)
.replaceAll('@methodParameters', genMethodParameters(bundle, key, 'int').join(', '))
.replaceAll('@intlMethodArgs', methodArgs.join(',\n '));
}
String genSupportedLocaleProperty(Set<LocaleInfo> supportedLocales) {
const String prefix = 'static const List<Locale> supportedLocales = <Locale>[\n Locale(''';
const String suffix = '),\n ];';
String resultingProperty = prefix;
for (LocaleInfo locale in supportedLocales) {
final String languageCode = locale.languageCode;
final String countryCode = locale.countryCode;
resultingProperty += '\'$languageCode\'';
if (countryCode != null)
resultingProperty += ', \'$countryCode\'';
resultingProperty += '),\n Locale(';
}
resultingProperty = resultingProperty.substring(0, resultingProperty.length - '),\n Locale('.length);
resultingProperty += suffix;
return resultingProperty;
}
bool _isValidClassName(String className) {
// Dart class name cannot contain non-alphanumeric symbols
if (className.contains(RegExp(r'[^a-zA-Z\d]')))
return false;
// Dart class name must start with upper case character
if (className[0].contains(RegExp(r'[a-z]')))
return false;
// Dart class name cannot start with a number
if (className[0].contains(RegExp(r'\d')))
return false;
return true;
}
bool _isValidGetterAndMethodName(String name) {
// Dart getter and method name cannot contain non-alphanumeric symbols
if (name.contains(RegExp(r'[^a-zA-Z\d]')))
return false;
// Dart class name must start with lower case character
if (name[0].contains(RegExp(r'[A-Z]')))
return false;
// Dart class name cannot start with a number
if (name[0].contains(RegExp(r'\d')))
return false;
return true;
}
bool _isDirectoryReadableAndWritable(String statString) {
if (statString[0] == '-' || statString[1] == '-')
return false;
return true;
}
String _importFilePath(String path, String fileName) {
final String replaceLib = path.replaceAll('lib/', '');
return '$replaceLib/$fileName';
}
Future<void> main(List<String> arguments) async {
final argslib.ArgParser parser = argslib.ArgParser();
parser.addFlag(
'help',
defaultsTo: false,
negatable: false,
help: 'Print this help message.',
);
parser.addOption(
'arb-dir',
defaultsTo: path.join('lib', 'l10n'),
help: 'The directory where all localization files should reside. For '
'example, the template and translated arb files should be located here. '
'Also, the generated output messages Dart files for each locale and the '
'generated localizations classes will be created here.',
);
parser.addOption(
'template-arb-file',
defaultsTo: 'app_en.arb',
help: 'The template arb file that will be used as the basis for '
'generating the Dart localization and messages files.',
);
parser.addOption(
'output-localization-file',
defaultsTo: 'app_localizations.dart',
help: 'The filename for the output localization and localizations '
'delegate classes.',
);
parser.addOption(
'output-class',
defaultsTo: 'AppLocalizations',
help: 'The Dart class name to use for the output localization and '
'localizations delegate classes.',
);
final argslib.ArgResults results = parser.parse(arguments);
if (results['help'] == true) {
print(parser.usage);
exit(0);
}
final String arbPathString = results['arb-dir'] as String;
final String outputFileString = results['output-localization-file'] as String;
final Directory l10nDirectory = Directory(arbPathString);
final File templateArbFile = File(path.join(l10nDirectory.path, results['template-arb-file'] as String));
final File outputFile = File(path.join(l10nDirectory.path, outputFileString));
final String stringsClassName = results['output-class'] as String;
if (!l10nDirectory.existsSync())
exitWithError(
"The 'arb-dir' directory, $l10nDirectory, does not exist.\n"
'Make sure that the correct path was provided.'
);
final String l10nDirectoryStatModeString = l10nDirectory.statSync().modeString();
if (!_isDirectoryReadableAndWritable(l10nDirectoryStatModeString))
exitWithError(
"The 'arb-dir' directory, $l10nDirectory, doesn't allow reading and writing.\n"
'Please ensure that the user has read and write permissions.'
);
final String templateArbFileStatModeString = templateArbFile.statSync().modeString();
if (templateArbFileStatModeString[0] == '-')
exitWithError(
"The 'template-arb-file', $templateArbFile, is not readable.\n"
'Please ensure that the user has read permissions.'
);
if (!_isValidClassName(stringsClassName))
exitWithError(
"The 'output-class', $stringsClassName, is not valid Dart class name.\n"
);
final List<String> arbFilenames = <String>[];
final Set<String> supportedLanguageCodes = <String>{};
final Set<LocaleInfo> supportedLocales = <LocaleInfo>{};
for (FileSystemEntity entity in l10nDirectory.listSync().toList()..sort(sortFilesByPath)) {
final String entityPath = entity.path;
if (FileSystemEntity.isFileSync(entityPath)) {
final RegExp arbFilenameRE = RegExp(r'(\w+)\.arb$');
if (arbFilenameRE.hasMatch(entityPath)) {
final File arbFile = File(entityPath);
final Map<String, dynamic> arbContents = json.decode(arbFile.readAsStringSync()) as Map<String, dynamic>;
String localeString = arbContents['@@locale'] as String;
if (localeString == null) {
final RegExp arbFilenameLocaleRE = RegExp(r'^[^_]*_(\w+)\.arb$');
final RegExpMatch arbFileMatch = arbFilenameLocaleRE.firstMatch(entityPath);
if (arbFileMatch == null) {
exitWithError(
"The following .arb file's locale could not be determined: \n"
'$entityPath \n'
"Make sure that the locale is specified in the '@@locale' "
'property or as part of the filename (ie. file_en.arb)'
);
}
localeString = arbFilenameLocaleRE.firstMatch(entityPath)[1];
}
arbFilenames.add(entityPath);
final LocaleInfo localeInfo = LocaleInfo.fromString(localeString);
if (supportedLocales.contains(localeInfo))
exitWithError(
'Multiple arb files with the same locale detected. \n'
'Ensure that there is exactly one arb file for each locale.'
);
supportedLocales.add(localeInfo);
supportedLanguageCodes.add('\'${localeInfo.languageCode}\'');
}
}
}
final List<String> classMethods = <String>[];
Map<String, dynamic> bundle;
try {
bundle = json.decode(templateArbFile.readAsStringSync()) as Map<String, dynamic>;
} on FileSystemException catch (e) {
exitWithError('Unable to read input arb file: $e');
} on FormatException catch (e) {
exitWithError('Unable to parse arb file: $e');
}
final RegExp pluralValueRE = RegExp(r'^\s*\{[\w\s,]*,\s*plural\s*,');
for (String key in bundle.keys.toList()..sort()) {
if (key.startsWith('@'))
continue;
if (!_isValidGetterAndMethodName(key))
exitWithError(
'Invalid key format: $key \n It has to be in camel case, cannot start '
'with a number, and cannot contain non-alphanumeric characters.'
);
if (pluralValueRE.hasMatch(bundle[key] as String))
classMethods.add(genPluralMethod(bundle, key));
else
classMethods.add(genSimpleMethod(bundle, key));
}
outputFile.writeAsStringSync(
defaultFileTemplate
.replaceAll('@className', stringsClassName)
.replaceAll('@classMethods', classMethods.join('\n'))
.replaceAll('@importFile', _importFilePath(arbPathString, outputFileString))
.replaceAll('@supportedLocales', genSupportedLocaleProperty(supportedLocales))
.replaceAll('@supportedLanguageCodes', supportedLanguageCodes.toList().join(', '))
);
final ProcessResult pubGetResult = await Process.run('flutter', <String>['pub', 'get']);
if (pubGetResult.exitCode != 0) {
stderr.write(pubGetResult.stderr);
exit(1);
}
final ProcessResult generateFromArbResult = await Process.run('flutter', <String>[
'pub',
'pub',
'run',
'intl_translation:generate_from_arb',
'--output-dir=${l10nDirectory.path}',
'--no-use-deferred-loading',
outputFile.path,
...arbFilenames,
]);
if (generateFromArbResult.exitCode != 0) {
stderr.write(generateFromArbResult.stderr);
exit(1);
}
}