| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| /// This provides utilities for generating localized versions of |
| /// messages. It does not stand alone, but expects to be given |
| /// TranslatedMessage objects and generate code for a particular locale |
| /// based on them. |
| /// |
| /// An example of usage can be found |
| /// in test/message_extract/generate_from_json.dart |
| library generate_localized; |
| |
| import 'package:intl/intl.dart'; |
| import 'src/intl_message.dart'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| import 'package:path/path.dart' as path; |
| |
| class MessageGeneration { |
| /// If the import path following package: is something else, modify the |
| /// [intlImportPath] variable to change the import directives in the generated |
| /// code. |
| var intlImportPath = 'intl'; |
| |
| /// If the path to the generated files is something other than the current |
| /// directory, update the [generatedImportPath] variable to change the import |
| /// directives in the generated code. |
| var generatedImportPath = ''; |
| |
| /// Given a base file, return the file prefixed with the path to import it. |
| /// By default, that is in the current directory, but if [generatedImportPath] |
| /// has been set, then use that as a prefix. |
| String importForGeneratedFile(String file) => |
| generatedImportPath.isEmpty ? file : "$generatedImportPath/$file"; |
| |
| /// A list of all the locales for which we have translations. Code that does |
| /// the reading of translations should add to this. |
| List<String> allLocales = []; |
| |
| /// If we have more than one set of messages to generate in a particular |
| /// directory we may want to prefix some to distinguish them. |
| String generatedFilePrefix = ''; |
| |
| /// Should we use deferred loading for the generated libraries. |
| bool useDeferredLoading = true; |
| |
| /// The mode to generate in - either 'release' or 'debug'. |
| /// |
| /// In release mode, a missing translation is an error. In debug mode, it |
| /// falls back to the original string. |
| String codegenMode; |
| |
| /// What is the path to the package for which we are generating. |
| /// |
| /// The exact format of this string depends on the generation mechanism, |
| /// so it's left undefined. |
| String package; |
| |
| get releaseMode => codegenMode == 'release'; |
| |
| bool get jsonMode => false; |
| |
| /// Holds the generated translations. |
| StringBuffer output = new StringBuffer(); |
| |
| void clearOutput() { |
| output = new StringBuffer(); |
| } |
| |
| /// Generate a file <[generated_file_prefix]>_messages_<[locale]>.dart |
| /// for the [translations] in [locale] and put it in [targetDir]. |
| void generateIndividualMessageFile(String basicLocale, |
| Iterable<TranslatedMessage> translations, String targetDir) { |
| clearOutput(); |
| var locale = new MainMessage() |
| .escapeAndValidateString(Intl.canonicalizedLocale(basicLocale)); |
| output.write(prologue(locale)); |
| // Exclude messages with no translation and translations with no matching |
| // original message (e.g. if we're using some messages from a larger |
| // catalog) |
| var usableTranslations = translations |
| .where((each) => each.originalMessages != null && each.message != null) |
| .toList(); |
| for (var each in usableTranslations) { |
| for (var original in each.originalMessages) { |
| original.addTranslation(locale, each.message); |
| } |
| } |
| usableTranslations.sort((a, b) => |
| a.originalMessages.first.name.compareTo(b.originalMessages.first.name)); |
| |
| writeTranslations(usableTranslations, locale); |
| |
| // To preserve compatibility, we don't use the canonical version of the |
| // locale in the file name. |
| var filename = path.join( |
| targetDir, "${generatedFilePrefix}messages_$basicLocale.dart"); |
| new File(filename).writeAsStringSync(output.toString()); |
| } |
| |
| /// Write out the translated forms. |
| void writeTranslations( |
| Iterable<TranslatedMessage> usableTranslations, String locale) { |
| for (var translation in usableTranslations) { |
| // Some messages we generate as methods in this class. Simpler ones |
| // we inline in the map from names to messages. |
| var messagesThatNeedMethods = translation.originalMessages |
| .where((each) => _hasArguments(each)) |
| .toSet() |
| .toList(); |
| for (var original in messagesThatNeedMethods) { |
| output |
| ..write(" ") |
| ..write( |
| original.toCodeForLocale(locale, _methodNameFor(original.name))) |
| ..write("\n\n"); |
| } |
| } |
| output.write(messagesDeclaration); |
| |
| // Now write the map of names to either the direct translation or to a |
| // method. |
| var entries = (usableTranslations |
| .expand((translation) => translation.originalMessages) |
| .toSet() |
| .toList() |
| ..sort((a, b) => a.name.compareTo(b.name))) |
| .map((original) => |
| ' "${original.escapeAndValidateString(original.name)}" ' |
| ': ${_mapReference(original, locale)}'); |
| output..write(entries.join(",\n"))..write("\n };\n}\n"); |
| } |
| |
| /// Any additional imports the individual message files need. |
| String get extraImports => ''; |
| |
| String get messagesDeclaration => |
| // Includes some gyrations to prevent parts of the deferred libraries from |
| // being inlined into the main one, defeating the space savings. Issue |
| // 24356 |
| """ |
| final messages = _notInlinedMessages(_notInlinedMessages); |
| static _notInlinedMessages(_) => <String, Function> { |
| """; |
| |
| /// [generateIndividualMessageFile] for the beginning of the file, |
| /// parameterized by [locale]. |
| String prologue(String locale) => |
| """ |
| // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart |
| // This is a library that provides messages for a $locale locale. All the |
| // messages from the main program should be duplicated here with the same |
| // function name. |
| |
| import 'package:$intlImportPath/intl.dart'; |
| import 'package:$intlImportPath/message_lookup_by_library.dart'; |
| $extraImports |
| // ignore: unnecessary_new |
| final messages = new MessageLookup(); |
| |
| // ignore: unused_element |
| final _keepAnalysisHappy = Intl.defaultLocale; |
| |
| // ignore: non_constant_identifier_names |
| typedef MessageIfAbsent(String message_str, List args); |
| |
| class MessageLookup extends MessageLookupByLibrary { |
| get localeName => '$locale'; |
| |
| """ + |
| (releaseMode ? overrideLookup : ""); |
| |
| String overrideLookup = """ |
| String lookupMessage( |
| String message_str, String locale, String name, List args, String meaning, |
| {MessageIfAbsent ifAbsent}) { |
| String failedLookup(String message_str, List args) { |
| // If there's no message_str, then we are an internal lookup, e.g. an |
| // embedded plural, and shouldn't fail. |
| if (message_str == null) return null; |
| // ignore: unnecessary_new |
| throw new UnsupportedError( |
| "No translation found for message '\$name',\\n" |
| " original text '\$message_str'"); |
| } |
| return super.lookupMessage(message_str, locale, name, args, meaning, |
| ifAbsent: ifAbsent ?? failedLookup); |
| } |
| |
| """; |
| |
| /// This section generates the messages_all.dart file based on the list of |
| /// [allLocales]. |
| String generateMainImportFile() { |
| clearOutput(); |
| output.write(mainPrologue); |
| for (var locale in allLocales) { |
| var baseFile = '${generatedFilePrefix}messages_$locale.dart'; |
| var file = importForGeneratedFile(baseFile); |
| output.write("import '$file' "); |
| if (useDeferredLoading) output.write("deferred "); |
| output.write("as ${libraryName(locale)};\n"); |
| } |
| output.write("\n"); |
| output.write("typedef Future<dynamic> LibraryLoader();\n"); |
| output.write("Map<String, LibraryLoader> _deferredLibraries = {\n"); |
| for (var rawLocale in allLocales) { |
| var locale = Intl.canonicalizedLocale(rawLocale); |
| var loadOperation = (useDeferredLoading) |
| ? " '$locale': () => ${libraryName(locale)}.loadLibrary(),\n" |
| : "// ignore: unnecessary_new\n" |
| + " '$locale': () => new Future.value(null),\n"; |
| output.write(loadOperation); |
| } |
| output.write("};\n"); |
| output.write("\nMessageLookupByLibrary _findExact(localeName) {\n" |
| " switch (localeName) {\n"); |
| for (var rawLocale in allLocales) { |
| var locale = Intl.canonicalizedLocale(rawLocale); |
| output.write( |
| " case '$locale':\n return ${libraryName(locale)}.messages;\n"); |
| } |
| output.write(closing); |
| return output.toString(); |
| } |
| |
| /// Constant string used in [generateMainImportFile] for the beginning of the |
| /// file. |
| get mainPrologue => """ |
| // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart |
| // This is a library that looks up messages for specific locales by |
| // delegating to the appropriate library. |
| |
| import 'dart:async'; |
| |
| import 'package:$intlImportPath/intl.dart'; |
| import 'package:$intlImportPath/message_lookup_by_library.dart'; |
| // ignore: implementation_imports |
| import 'package:$intlImportPath/src/intl_helpers.dart'; |
| |
| """; |
| |
| /// Constant string used in [generateMainImportFile] as the end of the file. |
| get closing => """ |
| default:\n return null; |
| } |
| } |
| |
| /// User programs should call this before using [localeName] for messages. |
| Future<bool> initializeMessages(String localeName) async { |
| var availableLocale = Intl.verifiedLocale( |
| localeName, |
| (locale) => _deferredLibraries[locale] != null, |
| onFailure: (_) => null); |
| if (availableLocale == null) { |
| // ignore: unnecessary_new |
| return new Future.value(false); |
| } |
| var lib = _deferredLibraries[availableLocale]; |
| // ignore: unnecessary_new |
| await (lib == null ? new Future.value(false) : lib()); |
| // ignore: unnecessary_new |
| initializeInternalMessageLookup(() => new CompositeMessageLookup()); |
| messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); |
| // ignore: unnecessary_new |
| return new Future.value(true); |
| } |
| |
| bool _messagesExistFor(String locale) { |
| try { |
| return _findExact(locale) != null; |
| } catch (e) { |
| return false; |
| } |
| } |
| |
| MessageLookupByLibrary _findGeneratedMessagesFor(locale) { |
| var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor, |
| onFailure: (_) => null); |
| if (actualLocale == null) return null; |
| return _findExact(actualLocale); |
| } |
| """; |
| } |
| |
| class JsonMessageGeneration extends MessageGeneration { |
| /// We import the main file so as to get the shared code to evaluate |
| /// the JSON data. |
| String get extraImports => ''' |
| import 'dart:convert'; |
| import '${generatedFilePrefix}messages_all.dart' show evaluateJsonTemplate; |
| '''; |
| |
| String prologue(locale) => |
| super.prologue(locale) + |
| ''' |
| String evaluateMessage(translation, List args) { |
| return evaluateJsonTemplate(translation, args); |
| } |
| '''; |
| |
| void writeTranslations( |
| Iterable<TranslatedMessage> usableTranslations, String locale) { |
| output.write(r""" |
| var _messages; |
| // ignore: unnecessary_new |
| get messages => _messages ??= new JsonDecoder().convert(messageText); |
| """); |
| |
| output.write(" static final messageText = "); |
| var entries = usableTranslations |
| .expand((translation) => translation.originalMessages); |
| var map = {}; |
| for (var original in entries) { |
| map[original.name] = original.toJsonForLocale(locale); |
| } |
| output.write( |
| "r'''\n" + new JsonEncoder.withIndent(' ').convert(map) + "''';\n}"); |
| } |
| |
| get closing => |
| super.closing + |
| ''' |
| /// Turn the JSON template into a string. |
| /// |
| /// We expect one of the following forms for the template. |
| /// * null -> null |
| /// * String s -> s |
| /// * int n -> '\${args[n]}' |
| /// * List list, one of |
| /// * \['Intl.plural', int howMany, (templates for zero, one, ...)\] |
| /// * \['Intl.gender', String gender, (templates for female, male, other)\] |
| /// * \['Intl.select', String choice, { 'case' : template, ...} \] |
| /// * \['text alternating with ', 0 , ' indexes in the argument list'\] |
| String evaluateJsonTemplate(Object input, List args) { |
| if (input == null) return null; |
| if (input is String) return input; |
| if (input is int) { |
| return "\${args[input]}"; |
| } |
| |
| List template = input; |
| var messageName = template.first; |
| if (messageName == "Intl.plural") { |
| var howMany = args[template[1]]; |
| return evaluateJsonTemplate( |
| Intl.pluralLogic( |
| howMany, |
| zero: template[2], |
| one: template[3], |
| two: template[4], |
| few: template[5], |
| many: template[6], |
| other: template[7]), |
| args); |
| } |
| if (messageName == "Intl.gender") { |
| var gender = args[template[1]]; |
| return evaluateJsonTemplate( |
| Intl.genderLogic( |
| gender, |
| female: template[2], |
| male: template[3], |
| other: template[4]), |
| args); |
| } |
| if (messageName == "Intl.select") { |
| var select = args[template[1]]; |
| var choices = template[2]; |
| return evaluateJsonTemplate(Intl.selectLogic(select, choices), args); |
| } |
| |
| // If we get this far, then we are a basic interpolation, just strings and |
| // ints. |
| // ignore: unnecessary_new |
| var output = new StringBuffer(); |
| for (var entry in template) { |
| if (entry is int) { |
| output.write("\${args[entry]}"); |
| } else { |
| output.write("\$entry"); |
| } |
| } |
| return output.toString(); |
| } |
| |
| '''; |
| } |
| |
| /// This represents a message and its translation. We assume that the |
| /// translation has some identifier that allows us to figure out the original |
| /// message it corresponds to, and that it may want to transform the translated |
| /// text in some way, e.g. to turn whatever format the translation uses for |
| /// variables into a Dart string interpolation. Specific translation mechanisms |
| /// are expected to subclass this. |
| abstract class TranslatedMessage { |
| /// The identifier for this message. In the simplest case, this is the name |
| /// parameter from the Intl.message call, |
| /// but it can be any identifier that this program and the output of the |
| /// translation can agree on as identifying a message. |
| final String id; |
| |
| /// Our translated version of all the [originalMessages]. |
| final Message translated; |
| |
| /// The original messages that we are a translation of. There can |
| /// be more than one original message for the same translation. |
| List<MainMessage> _originalMessages; |
| |
| List<MainMessage> get originalMessages => _originalMessages; |
| set originalMessages(List<MainMessage> x) { |
| _originalMessages = x; |
| } |
| |
| /// For backward compatibility, we still have the originalMessage API. |
| MainMessage get originalMessage => originalMessages.first; |
| set originalMessage(MainMessage m) { |
| originalMessages = [m]; |
| } |
| |
| TranslatedMessage(this.id, this.translated); |
| |
| Message get message => translated; |
| |
| toString() => id.toString(); |
| |
| operator ==(x) => x is TranslatedMessage && x.id == id; |
| |
| get hashCode => id.hashCode; |
| } |
| |
| /// We can't use a hyphen in a Dart library name, so convert the locale |
| /// separator to an underscore. |
| String libraryName(String x) => 'messages_' + x.replaceAll('-', '_'); |
| |
| bool _hasArguments(MainMessage message) => message.arguments.length != 0; |
| |
| /// Simple messages are printed directly in the map of message names to |
| /// functions as a call that returns a lambda. e.g. |
| /// |
| /// "foo" : simpleMessage("This is foo"), |
| /// |
| /// This is helpful for the compiler. |
| /// */ |
| String _mapReference(MainMessage original, String locale) { |
| if (!_hasArguments(original)) { |
| // No parameters, can be printed simply. |
| return 'MessageLookupByLibrary.simpleMessage("' |
| '${original.translations[locale]}")'; |
| } else { |
| return _methodNameFor(original.name); |
| } |
| } |
| |
| /// Generated method counter for use in [_methodNameFor]. |
| int _methodNameCounter = 0; |
| |
| /// A map from Intl message names to the generated method names |
| /// for their translated versions. |
| Map<String, String> _internalMethodNames = {}; |
| |
| /// Generate a Dart method name of the form "m<number>". |
| String _methodNameFor(String name) { |
| return _internalMethodNames.putIfAbsent( |
| name, () => "m${_methodNameCounter++}"); |
| } |