| // 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:convert'; |
| import 'dart:io'; |
| |
| import 'package:file/file.dart' as file; |
| import 'package:meta/meta.dart'; |
| 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, you’ll need to edit this |
| /// file. |
| /// |
| /// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. |
| /// Then, in the Project Navigator, open the Info.plist file under the Runner |
| /// project’s 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. |
| /// |
| /// Additional delegates can be added by appending to this list in |
| /// MaterialApp. This list does not have to be used at all if a custom list |
| /// of delegates is preferred or required. |
| 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) {@dateFormatting@numberFormatting |
| return Intl.message( |
| @message, |
| locale: _localeName, |
| @intlMethodArgs |
| ); |
| } |
| '''; |
| |
| const String pluralMethodTemplate = ''' |
| String @methodName(@methodParameters) {@dateFormatting@numberFormatting |
| return Intl.plural( |
| @intlMethodArgs |
| ); |
| } |
| '''; |
| |
| // The set of date formats that can be automatically localized. |
| // |
| // The localizations generation tool makes use of the intl library's |
| // DateFormat class to properly format dates based on the locale, the |
| // desired format, as well as the passed in [DateTime]. For example, using |
| // DateFormat.yMMMMd("en_US").format(DateTime.utc(1996, 7, 10)) results |
| // in the string "July 10, 1996". |
| // |
| // Since the tool generates code that uses DateFormat's constructor, it is |
| // necessary to verify that the constructor exists, or the |
| // tool will generate code that may cause a compile-time error. |
| // |
| // See also: |
| // |
| // * <https://pub.dev/packages/intl> |
| // * <https://pub.dev/documentation/intl/latest/intl/DateFormat-class.html> |
| // * <https://api.dartlang.org/stable/2.7.0/dart-core/DateTime-class.html> |
| const Set<String> allowableDateFormats = <String>{ |
| 'd', |
| 'E', |
| 'EEEE', |
| 'LLL', |
| 'LLLL', |
| 'M', |
| 'Md', |
| 'MEd', |
| 'MMM', |
| 'MMMd', |
| 'MMMEd', |
| 'MMMM', |
| 'MMMMd', |
| 'MMMMEEEEd', |
| 'QQQ', |
| 'QQQQ', |
| 'y', |
| 'yM', |
| 'yMd', |
| 'yMEd', |
| 'yMMM', |
| 'yMMMd', |
| 'yMMMEd', |
| 'yMMMM', |
| 'yMMMMd', |
| 'yMMMMEEEEd', |
| 'yQQQ', |
| 'yQQQQ', |
| 'H', |
| 'Hm', |
| 'Hms', |
| 'j', |
| 'jm', |
| 'jms', |
| 'jmv', |
| 'jmz', |
| 'jv', |
| 'jz', |
| 'm', |
| 'ms', |
| 's', |
| }; |
| |
| // The set of number formats that can be automatically localized. |
| // |
| // The localizations generation tool makes use of the intl library's |
| // NumberFormat class to properly format numbers based on the locale, the |
| // desired format, as well as the passed in number. For example, using |
| // DateFormat.compactLong("en_US").format(1200000) results |
| // in the string "1.2 million". |
| // |
| // Since the tool generates code that uses NumberFormat's constructor, it is |
| // necessary to verify that the constructor exists, or the |
| // tool will generate code that may cause a compile-time error. |
| // |
| // See also: |
| // |
| // * <https://pub.dev/packages/intl> |
| // * <https://pub.dev/documentation/intl/latest/intl/NumberFormat-class.html> |
| const Set<String> allowableNumberFormats = <String>{ |
| 'compact', |
| 'compactCurrency', |
| 'compactSimpleCurrency', |
| 'compactLong', |
| 'currency', |
| 'decimalPattern', |
| 'decimalPercentPattern', |
| 'percentPattern', |
| 'scientificPattern', |
| 'simpleCurrency', |
| }; |
| |
| // The names of the NumberFormat factory constructors which have named |
| // parameters rather than positional parameters. |
| // |
| // This helps the tool correctly generate number formmatting code correctly. |
| // |
| // Example of code that uses named parameters: |
| // final NumberFormat format = NumberFormat.compact( |
| // locale: _localeName, |
| // ); |
| // |
| // Example of code that uses positional parameters: |
| // final NumberFormat format = NumberFormat.scientificPattern(_localeName); |
| const Set<String> numberFormatsWithNamedParameters = <String>{ |
| 'compact', |
| 'compactCurrency', |
| 'compactSimpleCurrency', |
| 'compactLong', |
| 'currency', |
| 'decimalPercentPattern', |
| 'simpleCurrency', |
| }; |
| |
| bool _isDateParameter(Map<String, dynamic> placeholderValue) => placeholderValue['type'] == 'DateTime'; |
| bool _isNumberParameter(Map<String, dynamic> placeholderValue) => placeholderValue['type'] == 'Number'; |
| bool _containsFormatKey(Map<String, dynamic> placeholderValue, String placeholder) { |
| if (placeholderValue.containsKey('format')) |
| return true; |
| throw L10nException( |
| 'The placeholder, $placeholder, has its "type" resource attribute set to ' |
| 'the "${placeholderValue['type']}" type. To properly resolve for the right ' |
| '${placeholderValue['type']} format, the "format" attribute needs to be set ' |
| 'to determine which DateFormat to use. \n' |
| 'Check the intl library\'s DateFormat class constructors for allowed ' |
| 'date formats.' |
| ); |
| } |
| |
| bool _isValidDateParameter(Map<String, dynamic> placeholderValue, String placeholder) { |
| if (allowableDateFormats.contains(placeholderValue['format'])) |
| return true; |
| throw L10nException( |
| 'Date format ${placeholderValue['format']} for $placeholder \n' |
| 'placeholder does not have a corresponding DateFormat \n' |
| 'constructor. Check the intl library\'s DateFormat class \n' |
| 'constructors for allowed date formats.' |
| ); |
| } |
| |
| bool _isValidNumberParameter(Map<String, dynamic> placeholderValue, String placeholder) { |
| if (allowableNumberFormats.contains(placeholderValue['format'])) |
| return true; |
| throw L10nException( |
| 'Number format ${placeholderValue['format']} for the $placeholder \n' |
| 'placeholder does not have a corresponding NumberFormat \n' |
| 'constructor. Check the intl library\'s NumberFormat class \n' |
| 'constructors for allowed number formats.' |
| ); |
| } |
| |
| List<String> genMethodParameters(Map<String, dynamic> bundle, String resourceId, String type) { |
| final Map<String, dynamic> attributesMap = bundle['@$resourceId'] 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> genPluralMethodParameters(Iterable<String> placeholderKeys, String countPlaceholder, String resourceId) { |
| if (placeholderKeys.isEmpty) |
| throw L10nException( |
| 'Placeholders map for the $resourceId message is empty.\n' |
| 'Check to see if the plural message is in the proper ICU syntax format ' |
| 'and ensure that placeholders are properly specified.' |
| ); |
| |
| return placeholderKeys.map((String parameter) { |
| if (parameter == countPlaceholder) { |
| return 'int $parameter'; |
| } |
| return 'Object $parameter'; |
| }).toList(); |
| } |
| |
| String generateDateFormattingLogic(Map<String, dynamic> arbBundle, String resourceId) { |
| final StringBuffer result = StringBuffer(); |
| final Map<String, dynamic> attributesMap = arbBundle['@$resourceId'] as Map<String, dynamic>; |
| if (attributesMap != null && attributesMap.containsKey('placeholders')) { |
| final Map<String, dynamic> placeholders = attributesMap['placeholders'] as Map<String, dynamic>; |
| for (final String placeholder in placeholders.keys) { |
| final dynamic value = placeholders[placeholder]; |
| if (value is Map<String, dynamic> && _isValidDateFormat(value, placeholder)) { |
| result.write(''' |
| |
| final DateFormat ${placeholder}DateFormat = DateFormat.${value['format']}(_localeName); |
| final String ${placeholder}String = ${placeholder}DateFormat.format($placeholder); |
| '''); |
| } |
| } |
| } |
| |
| return result.toString(); |
| } |
| |
| String generateNumberFormattingLogic(Map<String, dynamic> arbBundle, String resourceId) { |
| final Map<String, dynamic> attributesMap = arbBundle['@$resourceId'] as Map<String, dynamic>; |
| if (attributesMap != null && attributesMap.containsKey('placeholders')) { |
| final StringBuffer result = StringBuffer(); |
| final Map<String, dynamic> placeholders = attributesMap['placeholders'] as Map<String, dynamic>; |
| final StringBuffer optionalParametersString = StringBuffer(); |
| for (final String placeholder in placeholders.keys) { |
| final dynamic value = placeholders[placeholder]; |
| if (value is Map<String, dynamic> && _isValidNumberFormat(value, placeholder)) { |
| if (numberFormatsWithNamedParameters.contains(value['format'])) { |
| if (value.containsKey('optionalParameters')) { |
| final Map<String, dynamic> optionalParameters = value['optionalParameters'] as Map<String, dynamic>; |
| for (final String parameter in optionalParameters.keys) |
| optionalParametersString.write('\n $parameter: ${optionalParameters[parameter]},'); |
| } |
| |
| result.write(''' |
| |
| final NumberFormat ${placeholder}NumberFormat = NumberFormat.${value['format']}( |
| locale: _localeName,@optionalParameters |
| ); |
| final String ${placeholder}String = ${placeholder}NumberFormat.format($placeholder); |
| '''); |
| } else { |
| result.write(''' |
| |
| final NumberFormat ${placeholder}NumberFormat = NumberFormat.${value['format']}(_localeName); |
| final String ${placeholder}String = ${placeholder}NumberFormat.format($placeholder); |
| '''); |
| } |
| } |
| } |
| |
| return result |
| .toString() |
| .replaceAll('@optionalParameters', optionalParametersString.toString()); |
| } |
| |
| return ''; |
| } |
| |
| bool _isValidDateFormat(Map<String, dynamic> value, String placeholder) { |
| return _isDateParameter(value) |
| && _containsFormatKey(value, placeholder) |
| && _isValidDateParameter(value, placeholder); |
| } |
| |
| bool _isValidNumberFormat(Map<String, dynamic> value, String placeholder) { |
| return _isNumberParameter(value) |
| && _containsFormatKey(value, placeholder) |
| && _isValidNumberParameter(value, placeholder); |
| } |
| |
| bool _isValidPlaceholder(Map<String, dynamic> value, String placeholder) { |
| return _isValidDateFormat(value, placeholder) || _isValidNumberFormat(value, placeholder); |
| } |
| |
| List<String> genIntlMethodArgs(Map<String, dynamic> arbBundle, String resourceId) { |
| final List<String> attributes = <String>['name: \'$resourceId\'']; |
| final Map<String, dynamic> attributesMap = arbBundle['@$resourceId'] 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 List<String> argumentList = <String>[]; |
| for (final String placeholder in placeholders.keys) { |
| final dynamic value = placeholders[placeholder]; |
| if (value is Map<String, dynamic> && _isValidPlaceholder(value, placeholder)) { |
| argumentList.add('${placeholder}String'); |
| } else { |
| argumentList.add(placeholder); |
| } |
| } |
| final String args = argumentList.join(', '); |
| attributes.add('args: <Object>[$args]'); |
| } |
| } |
| } |
| return attributes; |
| } |
| |
| String genSimpleMethod(Map<String, dynamic> arbBundle, String resourceId) { |
| String genSimpleMethodMessage(Map<String, dynamic> arbBundle, String resourceId) { |
| String message = arbBundle[resourceId] as String; |
| final Map<String, dynamic> attributesMap = arbBundle['@$resourceId'] as Map<String, dynamic>; |
| final Map<String, dynamic> placeholders = attributesMap['placeholders'] as Map<String, dynamic>; |
| for (final String placeholder in placeholders.keys) { |
| final dynamic value = placeholders[placeholder]; |
| if (value is Map<String, dynamic> && (_isDateParameter(value) || _isNumberParameter(value))) { |
| message = message.replaceAll('{$placeholder}', '\$${placeholder}String'); |
| } else { |
| message = message.replaceAll('{$placeholder}', '\$$placeholder'); |
| } |
| } |
| return generateString(message); |
| } |
| |
| final Map<String, dynamic> attributesMap = arbBundle['@$resourceId'] as Map<String, dynamic>; |
| if (attributesMap == null) |
| throw L10nException( |
| 'Resource attribute "@$resourceId" was not found. Please ensure that each ' |
| 'resource id has a corresponding resource attribute.' |
| ); |
| |
| if (attributesMap.containsKey('placeholders')) { |
| return simpleMethodTemplate |
| .replaceAll('@methodName', resourceId) |
| .replaceAll('@methodParameters', genMethodParameters(arbBundle, resourceId, 'Object').join(', ')) |
| .replaceAll('@dateFormatting', generateDateFormattingLogic(arbBundle, resourceId)) |
| .replaceAll('@numberFormatting', generateNumberFormattingLogic(arbBundle, resourceId)) |
| .replaceAll('@message', '${genSimpleMethodMessage(arbBundle, resourceId)}') |
| .replaceAll('@intlMethodArgs', genIntlMethodArgs(arbBundle, resourceId).join(',\n ')); |
| } |
| |
| return getterMethodTemplate |
| .replaceAll('@methodName', resourceId) |
| .replaceAll('@message', '${generateString(arbBundle[resourceId] as String)}') |
| .replaceAll('@intlMethodArgs', genIntlMethodArgs(arbBundle, resourceId).join(',\n ')); |
| } |
| |
| String genPluralMethod(Map<String, dynamic> arbBundle, String resourceId) { |
| final Map<String, dynamic> attributesMap = arbBundle['@$resourceId'] as Map<String, dynamic>; |
| if (attributesMap == null) |
| throw L10nException('Resource attribute for $resourceId does not exist.'); |
| if (!attributesMap.containsKey('placeholders')) |
| throw L10nException( |
| 'Unable to find placeholders for the plural message: $resourceId.\n' |
| 'Check to see if the plural message is in the proper ICU syntax format ' |
| 'and ensure that placeholders are properly specified.' |
| ); |
| if (attributesMap['placeholders'] is! Map<String, dynamic>) |
| throw L10nException( |
| 'The "placeholders" resource attribute for the message, $resourceId, ' |
| 'is not properly formatted. Ensure that it is a map with keys that are ' |
| 'strings.' |
| ); |
| |
| final Map<String, dynamic> placeholdersMap = attributesMap['placeholders'] as Map<String, dynamic>; |
| final Iterable<String> placeholders = placeholdersMap.keys; |
| |
| // Used to determine which placeholder is the plural count placeholder |
| final String resourceValue = arbBundle[resourceId] as String; |
| final String countPlaceholder = resourceValue.split(',')[0].substring(1); |
| |
| // To make it easier to parse the plurals message, temporarily replace each |
| // "{placeholder}" parameter with "#placeholder#". |
| String message = arbBundle[resourceId] as String; |
| for (final 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>[ |
| countPlaceholder, |
| 'locale: _localeName', |
| ...genIntlMethodArgs(arbBundle, resourceId), |
| ]; |
| |
| for (final 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 (final String placeholder in placeholders) { |
| final dynamic value = placeholdersMap[placeholder]; |
| if (value is Map<String, dynamic> && (_isDateParameter(value) || _isNumberParameter(value))) { |
| argValue = argValue.replaceAll('#$placeholder#', '\$${placeholder}String'); |
| } else { |
| argValue = argValue.replaceAll('#$placeholder#', '\$$placeholder'); |
| } |
| } |
| methodArgs.add("${pluralIds[pluralKey]}: '$argValue'"); |
| } |
| } |
| |
| return pluralMethodTemplate |
| .replaceAll('@methodName', resourceId) |
| .replaceAll('@methodParameters', genPluralMethodParameters(placeholders, countPlaceholder, resourceId).join(', ')) |
| .replaceAll('@dateFormatting', generateDateFormattingLogic(arbBundle, resourceId)) |
| .replaceAll('@numberFormatting', generateNumberFormattingLogic(arbBundle, resourceId)) |
| .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 (final 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 _isNotReadable(FileStat fileStat) { |
| final String rawStatString = fileStat.modeString(); |
| // Removes potential prepended permission bits, such as '(suid)' and '(guid)'. |
| final String statString = rawStatString.substring(rawStatString.length - 9); |
| return !(statString[0] == 'r' || statString[3] == 'r' || statString[6] == 'r'); |
| } |
| bool _isNotWritable(FileStat fileStat) { |
| final String rawStatString = fileStat.modeString(); |
| // Removes potential prepended permission bits, such as '(suid)' and '(guid)'. |
| final String statString = rawStatString.substring(rawStatString.length - 9); |
| return !(statString[1] == 'w' || statString[4] == 'w' || statString[7] == 'w'); |
| } |
| |
| 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; |
| } |
| |
| /// The localizations generation class used to generate the localizations |
| /// classes, as well as all pertinent Dart files required to internationalize a |
| /// Flutter application. |
| class LocalizationsGenerator { |
| /// Creates an instance of the localizations generator class. |
| /// |
| /// It takes in a [FileSystem] representation that the class will act upon. |
| LocalizationsGenerator(this._fs); |
| |
| static RegExp arbFilenameLocaleRE = RegExp(r'^[^_]*_(\w+)\.arb$'); |
| static RegExp arbFilenameRE = RegExp(r'(\w+)\.arb$'); |
| static RegExp pluralValueRE = RegExp(r'^\s*\{[\w\s,]*,\s*plural\s*,'); |
| |
| final file.FileSystem _fs; |
| |
| /// The reference to the project's l10n directory. |
| /// |
| /// It is assumed that all input files (e.g. [templateArbFile], arb files |
| /// for translated messages) and output files (e.g. The localizations |
| /// [outputFile], `messages_<locale>.dart` and `messages_all.dart`) |
| /// will reside here. |
| /// |
| /// This directory is specified with the [initialize] method. |
| Directory l10nDirectory; |
| |
| /// The input arb file which defines all of the messages that will be |
| /// exported by the generated class that's written to [outputFile]. |
| /// |
| /// This file is specified with the [initialize] method. |
| File templateArbFile; |
| |
| /// The file to write the generated localizations and localizations delegate |
| /// classes to. |
| /// |
| /// This file is specified with the [initialize] method. |
| File outputFile; |
| |
| /// The class name to be used for the localizations class in [outputFile]. |
| /// |
| /// For example, if 'AppLocalizations' is passed in, a class named |
| /// AppLocalizations will be used for localized message lookups. |
| /// |
| /// The class name is specified with the [initialize] method. |
| String get className => _className; |
| String _className; |
| |
| /// The list of preferred supported locales. |
| /// |
| /// By default, the list of supported locales in the localizations class |
| /// will be sorted in alphabetical order. However, this option |
| /// allows for a set of preferred locales to appear at the top of the |
| /// list. |
| /// |
| /// The order of locales in this list will also be the order of locale |
| /// priority. For example, if a device supports 'en' and 'es' and |
| /// ['es', 'en'] is passed in, the 'es' locale will take priority over 'en'. |
| /// |
| /// The list of preferred locales is specified with the [initialize] method. |
| List<LocaleInfo> get preferredSupportedLocales => _preferredSupportedLocales; |
| List<LocaleInfo> _preferredSupportedLocales; |
| |
| /// The list of all arb path strings in [l10nDirectory]. |
| final List<String> arbPathStrings = <String>[]; |
| |
| /// The supported language codes as found in the arb files located in |
| /// [l10nDirectory]. |
| final Set<String> supportedLanguageCodes = <String>{}; |
| |
| /// The supported locales as found in the arb files located in |
| /// [l10nDirectory]. |
| final Set<LocaleInfo> supportedLocales = <LocaleInfo>{}; |
| |
| /// The class methods that will be generated in the localizations class |
| /// based on messages found in the template arb file. |
| final List<String> classMethods = <String>[]; |
| |
| /// Initializes [l10nDirectory], [templateArbFile], [outputFile] and [className]. |
| /// |
| /// Throws an [L10nException] when a provided configuration is not allowed |
| /// by [LocalizationsGenerator]. |
| /// |
| /// Throws a [FileSystemException] when a file operation necessary for setting |
| /// up the [LocalizationsGenerator] cannot be completed. |
| void initialize({ |
| String l10nDirectoryPath, |
| String templateArbFileName, |
| String outputFileString, |
| String classNameString, |
| String preferredSupportedLocaleString, |
| }) { |
| setL10nDirectory(l10nDirectoryPath); |
| setTemplateArbFile(templateArbFileName); |
| setOutputFile(outputFileString); |
| setPreferredSupportedLocales(preferredSupportedLocaleString); |
| className = classNameString; |
| } |
| |
| /// Sets the reference [Directory] for [l10nDirectory]. |
| @visibleForTesting |
| void setL10nDirectory(String arbPathString) { |
| if (arbPathString == null) |
| throw L10nException('arbPathString argument cannot be null'); |
| l10nDirectory = _fs.directory(arbPathString); |
| if (!l10nDirectory.existsSync()) |
| throw FileSystemException( |
| "The 'arb-dir' directory, $l10nDirectory, does not exist.\n" |
| 'Make sure that the correct path was provided.' |
| ); |
| |
| final FileStat fileStat = l10nDirectory.statSync(); |
| if (_isNotReadable(fileStat) || _isNotWritable(fileStat)) |
| throw FileSystemException( |
| "The 'arb-dir' directory, $l10nDirectory, doesn't allow reading and writing.\n" |
| 'Please ensure that the user has read and write permissions.' |
| ); |
| } |
| |
| /// Sets the reference [File] for [templateArbFile]. |
| @visibleForTesting |
| void setTemplateArbFile(String templateArbFileName) { |
| if (templateArbFileName == null) |
| throw L10nException('templateArbFileName argument cannot be null'); |
| if (l10nDirectory == null) |
| throw L10nException('l10nDirectory cannot be null when setting template arb file'); |
| |
| templateArbFile = _fs.file(path.join(l10nDirectory.path, templateArbFileName)); |
| final String templateArbFileStatModeString = templateArbFile.statSync().modeString(); |
| if (templateArbFileStatModeString[0] == '-' && templateArbFileStatModeString[3] == '-') |
| throw FileSystemException( |
| "The 'template-arb-file', $templateArbFile, is not readable.\n" |
| 'Please ensure that the user has read permissions.' |
| ); |
| } |
| |
| /// Sets the reference [File] for the localizations delegate [outputFile]. |
| @visibleForTesting |
| void setOutputFile(String outputFileString) { |
| if (outputFileString == null) |
| throw L10nException('outputFileString argument cannot be null'); |
| outputFile = _fs.file(path.join(l10nDirectory.path, outputFileString)); |
| } |
| |
| /// Sets the [className] for the localizations and localizations delegate |
| /// classes. |
| @visibleForTesting |
| set className(String classNameString) { |
| if (classNameString == null) |
| throw L10nException('classNameString argument cannot be null'); |
| if (!_isValidClassName(classNameString)) |
| throw L10nException( |
| "The 'output-class', $classNameString, is not a valid Dart class name.\n" |
| ); |
| _className = classNameString; |
| } |
| |
| /// Sets [preferredSupportedLocales] so that this particular list of locales |
| /// will take priority over the other locales. |
| @visibleForTesting |
| void setPreferredSupportedLocales(String inputLocales) { |
| if (inputLocales != null) { |
| final List<dynamic> preferredLocalesStringList = json.decode(inputLocales) as List<dynamic>; |
| _preferredSupportedLocales = preferredLocalesStringList.map((dynamic localeString) { |
| if (localeString.runtimeType != String) { |
| throw L10nException('Incorrect runtime type for $localeString'); |
| } |
| return LocaleInfo.fromString(localeString.toString()); |
| }).toList(); |
| } |
| } |
| |
| /// Scans [l10nDirectory] for arb files and parses them for language and locale |
| /// information. |
| void parseArbFiles() { |
| final List<File> fileSystemEntityList = l10nDirectory |
| .listSync() |
| .whereType<File>() |
| .toList(); |
| final List<LocaleInfo> localeInfoList = <LocaleInfo>[]; |
| |
| for (final File file in fileSystemEntityList) { |
| final String filePath = file.path; |
| if (arbFilenameRE.hasMatch(filePath)) { |
| final Map<String, dynamic> arbContents = json.decode(file.readAsStringSync()) as Map<String, dynamic>; |
| String localeString = arbContents['@@locale'] as String; |
| if (localeString == null) { |
| final RegExpMatch arbFileMatch = arbFilenameLocaleRE.firstMatch(filePath); |
| if (arbFileMatch == null) { |
| throw L10nException( |
| "The following .arb file's locale could not be determined: \n" |
| '$filePath \n' |
| "Make sure that the locale is specified in the '@@locale' " |
| 'property or as part of the filename (e.g. file_en.arb)' |
| ); |
| } |
| |
| localeString = arbFilenameLocaleRE.firstMatch(filePath)[1]; |
| } |
| |
| arbPathStrings.add(filePath); |
| final LocaleInfo localeInfo = LocaleInfo.fromString(localeString); |
| if (localeInfoList.contains(localeInfo)) |
| throw L10nException( |
| 'Multiple arb files with the same locale detected. \n' |
| 'Ensure that there is exactly one arb file for each locale.' |
| ); |
| localeInfoList.add(localeInfo); |
| } |
| } |
| |
| arbPathStrings.sort(); |
| localeInfoList.sort(); |
| supportedLanguageCodes.addAll(localeInfoList.map((LocaleInfo localeInfo) { |
| return '\'${localeInfo.languageCode}\''; |
| })); |
| |
| if (preferredSupportedLocales != null) { |
| for (final LocaleInfo preferredLocale in preferredSupportedLocales) { |
| if (!localeInfoList.contains(preferredLocale)) { |
| throw L10nException( |
| 'The preferred supported locale, \'$preferredLocale\', cannot be ' |
| 'added. Please make sure that there is a corresponding arb file ' |
| 'with translations for the locale, or remove the locale from the ' |
| 'preferred supported locale list if there is no intent to support ' |
| 'it.' |
| ); |
| } |
| |
| localeInfoList.removeWhere((LocaleInfo localeInfo) => localeInfo == preferredLocale); |
| } |
| localeInfoList.insertAll(0, preferredSupportedLocales); |
| } |
| supportedLocales.addAll(localeInfoList); |
| } |
| |
| /// Generates the methods for the localizations class. |
| /// |
| /// The method parses [templateArbFile] and uses its resource ids as the |
| /// Dart method and getter names. It then uses each resource id's |
| /// corresponding resource value to figure out how to define these getters. |
| /// |
| /// For example, a message with plurals will be handled differently from |
| /// a simple, singular message. |
| /// |
| /// Throws an [L10nException] when a provided configuration is not allowed |
| /// by [LocalizationsGenerator]. |
| /// |
| /// Throws a [FileSystemException] when a file operation necessary for setting |
| /// up the [LocalizationsGenerator] cannot be completed. |
| /// |
| /// Throws a [FormatException] when parsing the arb file is unsuccessful. |
| void generateClassMethods() { |
| Map<String, dynamic> bundle; |
| try { |
| bundle = json.decode(templateArbFile.readAsStringSync()) as Map<String, dynamic>; |
| } on FileSystemException catch (e) { |
| throw FileSystemException('Unable to read input arb file: $e'); |
| } on FormatException catch (e) { |
| throw FormatException('Unable to parse arb file: $e'); |
| } |
| |
| final List<String> sortedArbKeys = bundle.keys.toList()..sort(); |
| for (final String key in sortedArbKeys) { |
| if (key.startsWith('@')) |
| continue; |
| if (!_isValidGetterAndMethodName(key)) |
| throw L10nException( |
| '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)); |
| } |
| } |
| |
| /// Generates a file that contains the localizations class and the |
| /// LocalizationsDelegate class. |
| void generateOutputFile() { |
| final String directory = path.basename(l10nDirectory.path); |
| final String outputFileName = path.basename(outputFile.path); |
| outputFile.writeAsStringSync( |
| defaultFileTemplate |
| .replaceAll('@className', className) |
| .replaceAll('@classMethods', classMethods.join('\n')) |
| .replaceAll('@importFile', '$directory/$outputFileName') |
| .replaceAll('@supportedLocales', genSupportedLocaleProperty(supportedLocales)) |
| .replaceAll('@supportedLanguageCodes', supportedLanguageCodes.toList().join(', ')) |
| ); |
| } |
| } |
| |
| class L10nException implements Exception { |
| L10nException(this.message); |
| |
| final String message; |
| } |