├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yaml └── workflows │ ├── build.yaml │ └── publish.yaml ├── .gitignore ├── .status ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── PATENTS ├── README.md ├── analysis_options.yaml ├── bin ├── extract_to_arb.dart ├── generate_from_arb.dart ├── make_examples_const.dart └── rewrite_intl_messages.dart ├── example ├── Makefile ├── README.md └── lib │ ├── example_messages.dart │ ├── generated │ ├── messages_all.dart │ ├── messages_all_locales.dart │ ├── messages_de.dart │ ├── messages_de_CH.dart │ ├── messages_en.dart │ └── messages_es.dart │ └── messages │ ├── material_de.arb │ ├── material_de_CH.arb │ ├── material_en.arb │ └── material_es.arb ├── lib ├── extract_messages.dart ├── generate_localized.dart ├── src │ ├── arb_generation.dart │ ├── directory_utils.dart │ ├── message_parser.dart │ ├── message_rewriter.dart │ └── messages │ │ ├── complex_message.dart │ │ ├── composite_message.dart │ │ ├── literal_string_message.dart │ │ ├── main_message.dart │ │ ├── message.dart │ │ ├── message_extraction_exception.dart │ │ ├── pair_message.dart │ │ ├── submessages │ │ ├── gender.dart │ │ ├── plural.dart │ │ ├── select.dart │ │ └── submessage.dart │ │ └── variable_substitution_message.dart └── visitors │ ├── interpolation_visitor.dart │ ├── message_finding_visitor.dart │ └── plural_gender_visitor.dart ├── pubspec.yaml └── test ├── data_directory.dart ├── generate_localized ├── README.txt ├── app_translation_getfromthelocale.arb ├── code_map_messages_all.dart ├── code_map_messages_all_locales.dart ├── code_map_messages_fr.dart ├── code_map_test.dart └── regenerate.sh ├── intl_message_test.dart ├── message_extraction ├── arb_list.txt ├── dart_list.txt ├── debug.sh ├── embedded_plural_text_after.dart ├── embedded_plural_text_after_test.dart ├── embedded_plural_text_before.dart ├── embedded_plural_text_before_test.dart ├── examples_parsing_test.dart ├── failed_extraction_test.dart ├── find_messages_test.dart ├── foo_messages_all.dart ├── make_hardcoded_translation.dart ├── message_extraction_flutter_test.dart ├── message_extraction_json_test.dart ├── message_extraction_no_deferred_test.dart ├── message_extraction_test.dart ├── mock_flutter │ ├── foo_messages_de_DE.dart │ ├── foo_messages_fr.dart │ └── services.dart ├── part_of_sample_with_messages.dart ├── print_to_list.dart ├── really_fail_extraction_test.dart ├── run_and_verify.dart ├── sample_with_messages.dart └── verify_messages.dart ├── message_parser_test.dart └── two_components ├── README.txt ├── app_messages_all.dart ├── app_messages_all_locales.dart ├── app_messages_fr.dart ├── app_translation_getfromthelocale.arb ├── component.dart ├── component_messages_all.dart ├── component_messages_all_locales.dart ├── component_messages_fr_xyz123.dart ├── component_translation_fr.arb ├── initialize_child_test.dart ├── main_app_test.dart └── regenerate.sh /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug in intl_translation 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | **Which script did you execute?** 13 | [ ] `extract_to_arb` 14 | [ ] `generate_from_arb` 15 | [ ] other 16 | 17 | **Input/output** 18 | Any `.arb` file contents or Dart code which could help in reproducing the bug? 19 | 20 | **System info** 21 | 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | version: 2 3 | enable-beta-ecosystems: true 4 | 5 | updates: 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Dart 2 | 3 | on: 4 | schedule: 5 | # “At 00:00 (UTC) on Sunday.” 6 | - cron: '0 0 * * 0' 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | # TODO(devoncarew): Re-enable; there are currently 9 failing tests on 19 | # Windows. 20 | # os: [ubuntu-latest, macos-latest, windows-latest] 21 | os: [ubuntu-latest, macos-latest] 22 | sdk: [2.18.0, stable, dev] 23 | 24 | name: build on ${{ matrix.os }} for ${{ matrix.sdk }} 25 | 26 | steps: 27 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab 28 | - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f 29 | with: 30 | sdk: ${{ matrix.sdk }} 31 | 32 | - name: dart pub get 33 | run: dart pub get 34 | 35 | - name: dart format 36 | run: dart format --output=none --set-exit-if-changed bin/ lib/ test/ 37 | 38 | - name: dart analyze 39 | run: dart analyze --fatal-infos 40 | 41 | - name: dart test 42 | run: dart test 43 | 44 | # Ensure the example code is up-to-date with the generator. 45 | - name: validate example/ 46 | run: | 47 | (cd example; make) 48 | git diff --exit-code example 49 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # A CI configuration to auto-publish pub packages. 2 | 3 | name: Publish 4 | 5 | on: 6 | pull_request: 7 | branches: [ master ] 8 | push: 9 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+*' ] 10 | 11 | jobs: 12 | publish: 13 | uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't commit the following directories created by pub. 2 | .dart_tool/ 3 | .packages 4 | .pub 5 | build/ 6 | 7 | # Or the files created by dart2js. 8 | *.dart.js 9 | *.dart.precompiled.js 10 | *.js_ 11 | *.js.deps 12 | *.js.map 13 | *.sw? 14 | .idea/ 15 | .pub/ 16 | 17 | # Include when developing application packages. 18 | pubspec.lock 19 | -------------------------------------------------------------------------------- /.status: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | # for details. All rights reserved. Use of this source code is governed by a 3 | # BSD-style license that can be found in the LICENSE file. 4 | 5 | # Don't run any test-like files that show up in packages directories. 6 | */packages/*/*: Skip 7 | */*/packages/*/*: Skip 8 | */*/*/packages/*/*: Skip 9 | 10 | [ $compiler == dart2js ] 11 | test/*: Skip # raw tests only meant to be run in dartium. Other browsers run 12 | # the output of pub-build 13 | 14 | [ $runtime == vm ] 15 | build/*: Skip 16 | 17 | [ $browser ] 18 | test/message_extraction/examples_parsing_test: Skip # Users dart:io 19 | test/message_extraction/failed_extraction_test: Skip # Users dart:io 20 | test/message_extraction/message_extraction_test: Skip # Uses dart:io. 21 | test/message_extraction/message_extraction_no_deferred_test: Skip # Uses dart:io. 22 | test/message_extraction/really_fail_extraction_test: Skip # Users dart:io 23 | test/message_extraction/embedded_plural_text_after_test: Skip # Uses dart:io. 24 | test/message_extraction/embedded_plural_text_before_test: Skip # Uses dart:io. 25 | build/test/message_extraction/examples_parsing_test: Skip # Users dart:io 26 | build/test/message_extraction/failed_extraction_test: Skip # Users dart:io 27 | build/test/message_extraction/message_extraction_test: Skip # Uses dart:io. 28 | build/test/message_extraction/message_extraction_no_deferred_test: Skip # Uses dart:io. 29 | build/test/message_extraction/really_fail_extraction_test: Skip # Users dart:io 30 | build/test/message_extraction/embedded_plural_text_after_test: Skip # Uses dart:io. 31 | build/test/message_extraction/embedded_plural_text_before_test: Skip # Uses dart:io. 32 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Names should be added to this file with this pattern: 2 | # 3 | # For individuals: 4 | # Name 5 | # 6 | # For organizations: 7 | # Organization 8 | # 9 | Google Inc. <*@google.com> 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.18.1 2 | * Update analyzer dependency to `5.2.0`. 3 | * Address analyzer deprecations. 4 | * Require Dart 2.18. 5 | * Fix issue #182, enabling escaping of curly brackets with a single quote as per the ICU message format specs. 6 | * Require `package:intl` `^0.18.0`. 7 | 8 | ## 0.18.0 9 | * Add support for Flutter locale split. 10 | * Allow null safe code when parsing. 11 | * Update analyzer dependency. 12 | * Upgrade to `package:lints/recommended.yaml`. 13 | * Initial null safety conversion. 14 | * Remove petit_parser dependency. 15 | * Address analyzer deprecations, see [#168](https://github.com/dart-lang/intl_translation/issues/168). 16 | * Migrate to null safety. 17 | 18 | ## 0.17.10+1 19 | * Generate code that passes analysis with `implicit-casts: false`. 20 | * Allow use of `MessageExtraction` and `MessageGeneration` without `File`. 21 | * Move arb generation from bin to lib so it's available to external packages. 22 | * Update analyzer dependency. 23 | 24 | ## 0.17.10 25 | * Update petitparser dependency. 26 | 27 | ## 0.17.9 28 | * Fix pub complaint trying to precompile a library file in bin by moving that file to lib/src. 29 | 30 | ## 0.17.8 31 | * Add --sources-list-files and --translations-list-file to ARB handling 32 | utilities to read the input names from files. This is useful for large 33 | numbers of inputs. 34 | 35 | ## 0.17.7 36 | * Fixed the pubspec to allow intl version 0.16.* 37 | 38 | ## 0.17.6 39 | * Strip indentation from generated JSON output to improve codesize. 40 | * Make generated code not trigger most lints, either by fixing issues 41 | or by using lots of ignore_for_file directives. 42 | * Added --with-source-text option to include the source text in the extracted 43 | ARB metadata. 44 | 45 | ## 0.17.5 46 | * Allow multiple ARB files with the same locale and combine 47 | their translations. 48 | * Update analyzer constraints and stop using deprecated elements2 API. 49 | 50 | ## 0.17.4 51 | * Adds --suppress-meta-data on ARB extraction. 52 | * Allow Dart enums in an Intl.select call. The map of cases 53 | can either take enums directly, or the short string name 54 | of the enum. 55 | * Handles triple quotes in a translation properly when 56 | generating messages as JSON. 57 | 58 | ## 0.17.3 59 | * Make require_description also fail for empty strings. 60 | * Update analyzer dependency. 61 | 62 | ## 0.17.2 63 | * Changes to support new mixin syntax. 64 | 65 | ## 0.17.1 66 | * Added --suppress-last-modified flag to suppress output of the 67 | @@last_modified entry in output file. 68 | * Add a "package" field in MessageGeneration that can be useful for emitting 69 | additional information about e.g. which locales are available and which 70 | package we're generating for. Also makes libraryName public. 71 | * Silence unnecessary_new lint warnings in generated code. 72 | * Add --require_description command line option to message extraction. 73 | 74 | ## 0.17.0 75 | * Fully move to Dart 2.0 76 | * Delete the transformer and related code. 77 | * Minor update to analyzer API. 78 | * Update pubspec version requirements 79 | 80 | ## 0.16.8 81 | * Allow message extraction to find messages from prefixed uses of Intl. 82 | * Move analyzer dependency up to 0.33.0 83 | 84 | ## 0.16.7 85 | * Allow message extraction to find messages in class field declarations 86 | and top-level declarations. 87 | * Fix incorrect name and parameters propagation during extraction phase. 88 | * Still more uppercase constant removal. 89 | 90 | ## 0.16.6 91 | * More uppercase constant removal. 92 | 93 | ## 0.16.5 94 | * Replace uses of JSON constant for Dart 2 compatibility. 95 | 96 | ## 0.16.4 97 | * Update Intl compatibility requirements. This requires at least 0.15.3 of 98 | Intl, because the tests contain messages with the new "skip" parameter. 99 | 100 | ## 0.16.3 101 | * Fix https://github.com/flutter/flutter/issues/15458 - specify concrete type 102 | for generated map. 103 | 104 | ## 0.16.2 105 | * Handle fallback better when we provide translations for locale "xx" but 106 | initialize "xx_YY", initializing "xx". Previously we would do nothing. 107 | * Skip extracting messages that pass the 'skip' argument to Intl calls. 108 | * Move analyzer dependency up to 0.32.0 109 | 110 | ## 0.16.1 111 | * Add @@last_modified to extracted ARB files. 112 | * Handle @@locale in translated ARB files properly, and adds a --locale 113 | parameter to specify the locale. 114 | * Adds a --output-file parameter to extract_to_arb 115 | * Indent the output file for ARB for better readability. 116 | * A couple of tweaks to satisfy Flutter's default linter rules when run on the 117 | generated code. 118 | 119 | ## 0.16.0 120 | * BREAKING CHANGE: Require that the examples to message/plural/gender/select 121 | calls be const. DDC does not optimize non-const maps well, so it's a 122 | significant performance issue if these are non-const. 123 | * Added a utility to convert examples in calls to be const. See 124 | bin/make_examples_const.dart 125 | * Add a codegen_mode flag, which can be either release or debug. In release 126 | mode a missing translation throws an exception, in debug mode it returns the 127 | original text, which was the previous behavior. 128 | * Add support for generating translated messages as JSON rather than 129 | methods. This can significantly improve dart2js compile times for 130 | applications with many translations. The JSON is a literal string in the 131 | deferred library, so usage doesn't change at all. 132 | 133 | ## 0.15.0 134 | * Change non-transformer message rewriting to preserve the original message as 135 | much as possible. Adds --useStringSubstitution command-line arg. 136 | * Change non-transformer message rewriting to allow multiple input files to be 137 | specified on the command line. Adds --replace flag to ignore --output option 138 | and just replace files. 139 | * Make non-transformer message rewriting also run dartfmt on the output. 140 | * Make message extraction more robust: error message instead of stack trace 141 | when an Intl call is made outside a method, when a prefixed expression is 142 | used in an interpolation, and when a non-required example Map is not a 143 | literal. 144 | * Make message extraction more robust: if parsing triggers an exception then 145 | report it as an error instead of exiting. 146 | * Move barback to being a normal rather than a dev dependency. 147 | * Add a check for invalid select keywords. 148 | * Added a post-message construction validate, moved 149 | IntlMessageExtractionException into intl_message.dart 150 | * Make use of analyzer's new AstFactory class (requires analyzer version 151 | 0.29.1). 152 | * Fix error in transformer, pass the path instead of the asset id. 153 | * Prefer an explicit =0/=1/=2 to a ZERO/ONE/TWO if both are present. We don't 154 | distinguish the two as Intl.message arguments, we just have the "one" 155 | parameter, which we confusingly write out as =1. Tools interpret these 156 | differently, and in particular, a ONE clause is used for the zero case if 157 | there's no explicit zero. Translation tools may implement this by filling in 158 | both ZERO and ONE values with the OTHER clause when there's no ZERO 159 | provided, resulting in a translation with both =1 and ONE clauses which are 160 | different. We should prefer the explicit =1 in that case. In future we may 161 | distinguish the different forms, but that would probably break existing 162 | translations. 163 | * Switch to using package:test 164 | * Give a more specific type in the generated code to keep lints happy. 165 | 166 | ## 0.14.0 167 | * Split message extraction and code generation out into a separate 168 | package. Versioned to match the corresponding Intl version. 169 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /PATENTS: -------------------------------------------------------------------------------- 1 | Additional IP Rights Grant (Patents) 2 | 3 | "This implementation" means the copyrightable works distributed by 4 | Google as part of the Dart Project. 5 | 6 | Google hereby grants to you a perpetual, worldwide, non-exclusive, 7 | no-charge, royalty-free, irrevocable (except as stated in this 8 | section) patent license to make, have made, use, offer to sell, sell, 9 | import, transfer, and otherwise run, modify and propagate the contents 10 | of this implementation of Dart, where such license applies only to 11 | those patent claims, both currently owned by Google and acquired in 12 | the future, licensable by Google that are necessarily infringed by 13 | this implementation of Dart. This grant does not include claims that 14 | would be infringed only as a consequence of further modification of 15 | this implementation. If you or your agent or exclusive licensee 16 | institute or order or agree to the institution of patent litigation 17 | against any entity (including a cross-claim or counterclaim in a 18 | lawsuit) alleging that this implementation of Dart or any code 19 | incorporated within this implementation of Dart constitutes direct or 20 | contributory patent infringement, or inducement of patent 21 | infringement, then any patent rights granted to you under this License 22 | for this implementation of Dart shall terminate as of the date such 23 | litigation is filed. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## This repository has been moved to https://github.com/dart-lang/i18n/tree/main/pkgs/intl_translation. Please file any PRs and issues there. 2 | --- 3 | 4 | [![Dart](https://github.com/dart-lang/intl_translation/actions/workflows/build.yaml/badge.svg)](https://github.com/dart-lang/intl_translation/actions/workflows/build.yaml) 5 | [![Pub](https://img.shields.io/pub/v/intl_translation.svg)](https://pub.dev/packages/intl_translation) 6 | 7 | Provides message extraction and code generation from translated messages for the 8 | [intl][intl] package. It's a separate package so as to not require a dependency 9 | on analyzer for all users. 10 | 11 | ## Extracting And Using Translated Messages 12 | 13 | When your program contains messages that need translation, these must be 14 | extracted from the program source, sent to human translators, and the results 15 | need to be incorporated. 16 | 17 | To extract messages, run the `extract_to_arb.dart` program. 18 | 19 | ``` 20 | dart run intl_translation:extract_to_arb --output-dir=target/directory \ 21 | my_program.dart more_of_my_program.dart 22 | ``` 23 | 24 | This supports wildcards. For example, to extract messages from a series of files in path `lib/**/*.dart`, you can run 25 | ```dart 26 | dart run intl_translation:extract_to_arb --output-dir=target/directory 27 | lib/**/*.dart 28 | ``` 29 | 30 | This will produce a file `intl_messages.arb` with the messages from all of these 31 | programs. This is an [ARB][arb] format file which can be used for input to 32 | translation tools like [Localizely][localizely] or [Lyrebird][lyrebird]. The resulting translations can 33 | be used to generate a set of libraries using the `generate_from_arb.dart` 34 | program. 35 | 36 | This expects to receive a series of files, one per locale. 37 | 38 | ``` 39 | dart run intl_translation:generate_from_arb --generated-file-prefix= \ 40 | 41 | ``` 42 | 43 | This will generate Dart libraries, one per locale, which contain the translated 44 | versions. Your Dart libraries can import the primary file, named 45 | `messages_all.dart`, and then call the initialization for a specific 46 | locale. Once that's done, any [Intl.message][intl.message] calls made in the 47 | context of that locale will automatically print the translated version instead 48 | of the original. 49 | 50 | ```dart 51 | import "my_prefix_messages_all.dart"; 52 | ... 53 | initializeMessages("dk").then(printSomeMessages); 54 | ``` 55 | 56 | Once the `Future` returned from the initialization call completes, the message 57 | data is available. 58 | 59 | [intl]: https://pub.dev/packages/intl 60 | [intl.message]: https://pub.dev/documentation/intl/latest/intl/Intl/message.html 61 | [arb]: 62 | https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification 63 | [localizely]: https://localizely.com/ 64 | [lyrebird]: https://lyrebird.dev/ 65 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | analyzer: 4 | errors: 5 | # Challenging with the file name patterns that this package generates. 6 | file_names: ignore 7 | # This can cause issues with recognizing localizable strings. 8 | unnecessary_string_interpolations: ignore 9 | 10 | linter: 11 | rules: 12 | # Enable some additional lints. 13 | - always_declare_return_types 14 | - directives_ordering 15 | - omit_local_variable_types 16 | - prefer_relative_imports 17 | - prefer_single_quotes 18 | - sort_pub_dependencies 19 | - type_annotate_public_apis 20 | 21 | # This is currently challenging for this package. 22 | # - avoid_dynamic_calls 23 | -------------------------------------------------------------------------------- /bin/extract_to_arb.dart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dart 2 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 3 | // for details. All rights reserved. Use of this source code is governed by a 4 | // BSD-style license that can be found in the LICENSE file. 5 | 6 | /// This script uses the extract_messages.dart library to find the Intl.message 7 | /// calls in the target dart files and produces ARB format output. See 8 | /// https://code.google.com/p/arb/wiki/ApplicationResourceBundleSpecification 9 | library extract_to_arb; 10 | 11 | import 'dart:convert'; 12 | import 'dart:io'; 13 | 14 | import 'package:args/args.dart'; 15 | import 'package:intl_translation/extract_messages.dart'; 16 | import 'package:intl_translation/src/arb_generation.dart'; 17 | import 'package:intl_translation/src/directory_utils.dart'; 18 | import 'package:path/path.dart' as path; 19 | 20 | void main(List args) { 21 | var targetDir = '.'; 22 | var outputFilename = 'intl_messages.arb'; 23 | String? sourcesListFile; 24 | var transformer = false; 25 | var parser = ArgParser(); 26 | var extract = MessageExtraction(); 27 | String? locale; 28 | 29 | // Whether to include source_text in messages 30 | var includeSourceText = false; 31 | 32 | // If this is true, no translation meta data is written 33 | var suppressMetaData = false; 34 | 35 | // If this is true, the @@last_modified entry is not output. 36 | var suppressLastModified = false; 37 | 38 | parser.addFlag('help', 39 | abbr: 'h', negatable: false, help: 'Print this usage information.'); 40 | 41 | // If this is true, then treat all warnings as errors. 42 | parser.addFlag('suppress-last-modified', 43 | callback: (x) => suppressLastModified = x, 44 | help: 'Suppress @@last_modified entry.'); 45 | parser.addFlag('suppress-warnings', 46 | defaultsTo: false, 47 | callback: (x) => extract.suppressWarnings = x, 48 | help: 'Suppress printing of warnings.'); 49 | parser.addFlag('suppress-meta-data', 50 | callback: (x) => suppressMetaData = x, 51 | help: 'Suppress writing meta information'); 52 | parser.addFlag('warnings-are-errors', 53 | callback: (x) => extract.warningsAreErrors = x, 54 | help: 'Treat all warnings as errors, stop processing '); 55 | parser.addFlag('embedded-plurals', 56 | defaultsTo: true, 57 | callback: (x) => extract.allowEmbeddedPluralsAndGenders = x, 58 | help: 'Allow plurals and genders to be embedded as part of a larger ' 59 | 'string, otherwise they must be at the top level.'); 60 | //TODO(mosuem): All references to the transformer can be removed, but this 61 | // should happen in a separate PR to help with testing. 62 | parser.addFlag('transformer', 63 | callback: (x) => transformer = x, 64 | help: 'Assume that the transformer is in use, so name and args ' 65 | "don't need to be specified for messages."); 66 | parser.addOption('locale', 67 | defaultsTo: null, 68 | callback: (value) => locale = value, 69 | help: 'Specify the locale set inside the arb file.'); 70 | parser.addFlag( 71 | 'with-source-text', 72 | callback: (x) => includeSourceText = x, 73 | help: 'Include source_text in meta information.', 74 | ); 75 | parser.addOption( 76 | 'output-dir', 77 | callback: (value) { 78 | if (value != null) targetDir = value; 79 | }, 80 | help: 'Specify the output directory.', 81 | ); 82 | parser.addOption( 83 | 'output-file', 84 | callback: (value) { 85 | if (value != null) outputFilename = value; 86 | }, 87 | help: 'Specify the output file.', 88 | ); 89 | parser.addOption( 90 | 'sources-list-file', 91 | callback: (value) => sourcesListFile = value, 92 | help: 'A file that lists the Dart files to read, one per line.' 93 | 'The paths in the file can be absolute or relative to the ' 94 | 'location of this file.', 95 | ); 96 | parser.addFlag( 97 | 'require_descriptions', 98 | defaultsTo: false, 99 | help: "Fail for messages that don't have a description.", 100 | callback: (val) => extract.descriptionRequired = val, 101 | ); 102 | 103 | var argResults = parser.parse(args); 104 | var showHelp = (argResults['help'] as bool?) ?? false; 105 | if (args.isEmpty || showHelp) { 106 | print('Accepts Dart source files and produce $outputFilename as output.'); 107 | print(''); 108 | print('Usage: extract_to_arb [options] '); 109 | print(''); 110 | print(parser.usage); 111 | exit(0); 112 | } 113 | 114 | var allMessages = {}; 115 | if (locale != null) { 116 | allMessages['@@locale'] = locale!; 117 | } 118 | if (!suppressLastModified) { 119 | allMessages['@@last_modified'] = DateTime.now().toIso8601String(); 120 | } 121 | 122 | var dartFiles = [ 123 | ...args.where((x) => x.endsWith('.dart')), 124 | ...linesFromFile(sourcesListFile) 125 | ]; 126 | dartFiles 127 | .map((dartFile) => extract.parseFile(File(dartFile), transformer)) 128 | .expand((parsedFile) => parsedFile.entries) 129 | .map((nameToMessage) => toARB( 130 | message: nameToMessage.value, 131 | includeSourceText: includeSourceText, 132 | suppressMetadata: suppressMetaData, 133 | )) 134 | .forEach((message) => allMessages.addAll(message)); 135 | var file = File(path.join(targetDir, outputFilename)); 136 | file.writeAsStringSync(JsonEncoder.withIndent(' ').convert(allMessages)); 137 | if (extract.hasWarnings && extract.warningsAreErrors) { 138 | exit(1); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /bin/generate_from_arb.dart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dart 2 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 3 | // for details. All rights reserved. Use of this source code is governed by a 4 | // BSD-style license that can be found in the LICENSE file. 5 | 6 | /// A main program that takes as input a source Dart file and a number 7 | /// of ARB files representing translations of messages from the corresponding 8 | /// Dart file. See extract_to_arb.dart and make_hardcoded_translation.dart. 9 | /// 10 | /// If the ARB file has an @@locale or _locale value, that will be used as 11 | /// the locale. If not, we will try to figure out the locale from the end of 12 | /// the file name, e.g. foo_en_GB.arb will be assumed to be in en_GB locale. 13 | /// 14 | /// This produces a series of files named 15 | /// "messages_.dart" containing messages for a particular locale 16 | /// and a main import file named "messages_all.dart" which has imports all of 17 | /// them and provides an initializeMessages function. 18 | 19 | library generate_from_arb; 20 | 21 | import 'dart:convert'; 22 | import 'dart:io'; 23 | 24 | import 'package:args/args.dart'; 25 | import 'package:intl_translation/extract_messages.dart'; 26 | import 'package:intl_translation/generate_localized.dart'; 27 | import 'package:intl_translation/src/directory_utils.dart'; 28 | import 'package:intl_translation/src/message_parser.dart'; 29 | import 'package:intl_translation/src/messages/literal_string_message.dart'; 30 | import 'package:intl_translation/src/messages/main_message.dart'; 31 | import 'package:path/path.dart' as path; 32 | 33 | const jsonDecoder = JsonCodec(); 34 | 35 | void main(List args) { 36 | var targetDir = '.'; 37 | var parser = ArgParser(); 38 | var extraction = MessageExtraction(); 39 | var generation = MessageGeneration(); 40 | String? sourcesListFile; 41 | String? translationsListFile; 42 | var transformer = false; 43 | var useJsonFlag = false; 44 | var useCodeMapFlag = false; 45 | var useFlutterLocaleSplit = false; 46 | parser.addFlag('help', 47 | abbr: 'h', negatable: false, help: 'Print this usage information.'); 48 | parser.addFlag('json', callback: (useJson) { 49 | useJsonFlag = useJson; 50 | if (useJson) { 51 | generation = JsonMessageGeneration(); 52 | } 53 | }, help: 'Generate translations as a JSON string rather than as functions.'); 54 | parser.addFlag('code-map', callback: (useCodeMap) { 55 | useCodeMapFlag = useCodeMap; 56 | if (useCodeMap) { 57 | generation = CodeMapMessageGeneration(); 58 | } 59 | }, help: 'Generate translations as a JSON string rather than as functions.'); 60 | parser.addFlag('flutter', 61 | defaultsTo: false, 62 | callback: (val) => useFlutterLocaleSplit = val, 63 | help: 'Generate localization file that uses Flutter locale split.'); 64 | parser.addOption('flutter-import-path', 65 | callback: (val) => generation.flutterImportPath = val, 66 | hide: true, 67 | help: 'Customize the flutter import path, used for testing. Defaults to ' 68 | 'package:flutter.'); 69 | parser.addFlag('suppress-warnings', 70 | defaultsTo: false, 71 | callback: (x) => extraction.suppressWarnings = x, 72 | help: 'Suppress printing of warnings.'); 73 | parser.addOption( 74 | 'output-dir', 75 | callback: (x) { 76 | if (x != null) targetDir = x; 77 | }, 78 | help: 'Specify the output directory.', 79 | ); 80 | parser.addOption('generated-file-prefix', 81 | defaultsTo: '', 82 | callback: (x) => generation.generatedFilePrefix = x, 83 | help: 'Specify a prefix to be used for the generated file names.'); 84 | parser.addFlag('use-deferred-loading', 85 | defaultsTo: true, 86 | callback: (x) => generation.useDeferredLoading = x, 87 | help: 'Generate message code that must be loaded with deferred loading. ' 88 | 'Otherwise, all messages are eagerly loaded.'); 89 | parser.addFlag('null-safety', 90 | defaultsTo: true, 91 | callback: (val) => generation.nullSafety = val, 92 | help: 'Generate null safe code vs legacy (pre-null safe) code.'); 93 | parser.addOption('codegen_mode', 94 | allowed: ['release', 'debug'], 95 | defaultsTo: 'debug', 96 | callback: (x) => generation.codegenMode = x, 97 | help: 'What mode to run the code generator in. Either release or debug.'); 98 | parser.addOption('sources-list-file', 99 | callback: (value) => sourcesListFile = value, 100 | help: 'A file that lists the Dart files to read, one per line. ' 101 | 'The paths in the file can be absolute or relative to the ' 102 | 'location of this file.'); 103 | parser.addOption('translations-list-file', 104 | callback: (value) => translationsListFile = value, 105 | help: 'A file that lists the translation files to process, one per line. ' 106 | 'The paths in the file can be absolute or relative to the ' 107 | 'location of this file.'); 108 | parser.addFlag('transformer', 109 | callback: (x) => transformer = x, 110 | help: 'Assume that the transformer is in use, so name and args ' 111 | "don't need to be specified for messages."); 112 | 113 | var argResults = parser.parse(args); 114 | var dartFiles = [ 115 | ...args.where((x) => x.endsWith('dart')), 116 | ...linesFromFile(sourcesListFile) 117 | ]; 118 | var jsonFiles = [ 119 | ...args.where((x) => x.endsWith('.arb')), 120 | ...linesFromFile(translationsListFile) 121 | ]; 122 | var showHelp = (argResults['help'] as bool?) ?? false; 123 | if (dartFiles.isEmpty || jsonFiles.isEmpty || showHelp) { 124 | print('Usage: generate_from_arb [options]' 125 | ' file1.dart file2.dart ...' 126 | ' translation1_.arb translation2.arb ...'); 127 | print(''); 128 | print(parser.usage); 129 | exit(0); 130 | } 131 | 132 | if (useCodeMapFlag && useJsonFlag) { 133 | throw 'Only one of code-map and json can be specified'; 134 | } 135 | 136 | if (useCodeMapFlag && useFlutterLocaleSplit) { 137 | throw 'code-map cannot be used in combination with flutter locale split'; 138 | } 139 | 140 | // TODO(alanknight): There is a possible regression here. If a project is 141 | // using the transformer and expecting it to provide names for messages with 142 | // parameters, we may report those names as missing. We now have two distinct 143 | // mechanisms for providing names: the transformer and just using the message 144 | // text if there are no parameters. Previously this was always acting as if 145 | // the transformer was in use, but that breaks the case of using the message 146 | // text. The intent is to deprecate the transformer, but if this is an issue 147 | // for real projects we could provide a command-line flag to indicate which 148 | // sort of automated name we're using. 149 | //TODO(mosuem):Why is the suppress-warnings flag ignored? 150 | extraction.suppressWarnings = true; 151 | var allMessages = 152 | dartFiles.map((each) => extraction.parseFile(File(each), transformer)); 153 | 154 | /// Keeps track of all the messages we have processed so far, keyed by message 155 | /// name. 156 | var messages = >{}; 157 | for (var eachMap in allMessages) { 158 | eachMap.forEach((k, v) => messages.putIfAbsent(k, () => []).add(v)); 159 | } 160 | var messagesByLocale = >>{}; 161 | 162 | // In order to group these by locale, to support multiple input files, 163 | // we're reading all the data eagerly, which could be a memory 164 | // issue for very large projects. 165 | for (var arg in jsonFiles) { 166 | loadData(arg, messagesByLocale, generation); 167 | } 168 | 169 | messagesByLocale.forEach((locale, data) { 170 | generateLocaleFile(locale, data, targetDir, generation, messages); 171 | }); 172 | 173 | var mainImportFile = File(path.join( 174 | targetDir, '${generation.generatedFilePrefix}messages_all.dart')); 175 | mainImportFile.writeAsStringSync( 176 | generation.generateMainImportFile(flutter: useFlutterLocaleSplit)); 177 | 178 | var localesImportFile = File(path.join( 179 | targetDir, '${generation.generatedFilePrefix}messages_all_locales.dart')); 180 | localesImportFile.writeAsStringSync(generation.generateLocalesImportFile()); 181 | 182 | if (useFlutterLocaleSplit) { 183 | var flutterImportFile = File(path.join( 184 | targetDir, '${generation.generatedFilePrefix}messages_flutter.dart')); 185 | flutterImportFile.writeAsStringSync(generation.generateFlutterImportFile()); 186 | } 187 | } 188 | 189 | void loadData( 190 | String filename, 191 | Map>> messagesByLocale, 192 | MessageGeneration generation, 193 | ) { 194 | var file = File(filename); 195 | var arbFileContents = file.readAsStringSync(); 196 | Map parsedArb = jsonDecoder.decode(arbFileContents); 197 | String? locale = parsedArb['@@locale'] ?? parsedArb['_locale']; 198 | if (locale == null) { 199 | // Get the locale from the end of the file name. This assumes that the file 200 | // name doesn't contain any underscores except to begin the language tag 201 | // and to separate language from country. Otherwise we can't tell if 202 | // my_file_fr.arb is locale "fr" or "file_fr". 203 | var name = path.basenameWithoutExtension(file.path); 204 | locale = name.split('_').skip(1).join('_'); 205 | print('No @@locale or _locale field found in $name, ' 206 | "assuming '$locale' based on the file name."); 207 | } 208 | // Remove all metadata from the map 209 | parsedArb.remove('_locale'); 210 | parsedArb.removeWhere((key, _) => key.startsWith('@')); 211 | 212 | var messages = Map.castFrom(parsedArb); 213 | messagesByLocale.putIfAbsent(locale, () => []).add(messages); 214 | generation.allLocales.add(locale); 215 | } 216 | 217 | /// Create the file of generated code for a particular locale. 218 | /// 219 | /// We read the ARB 220 | /// data and create [BasicTranslatedMessage] instances from everything, 221 | /// excluding only the special _locale attribute that we use to indicate the 222 | /// locale. If that attribute is missing, we try to get the locale from the 223 | /// last section of the file name. Each ARB file produces a Map of message 224 | /// translations, and there can be multiple such maps in [localeData]. 225 | void generateLocaleFile( 226 | String locale, 227 | List> localeData, 228 | String targetDir, 229 | MessageGeneration generation, 230 | Map> messages) { 231 | var translations = localeData 232 | .expand((jsonTranslations) { 233 | return jsonTranslations.entries.map((e) { 234 | var id = e.key; 235 | var messageData = e.value; 236 | return recreateIntlObjects(id, messageData, messages); 237 | }); 238 | }) 239 | .whereType() 240 | .toList(); 241 | generation.generateIndividualMessageFile(locale, translations, targetDir); 242 | } 243 | 244 | /// Regenerate the original IntlMessage objects from the given [data]. For 245 | /// things that are messages, we expect [id] not to start with "@" and 246 | /// [data] to be a String. For metadata we expect [id] to start with "@" 247 | /// and [data] to be a Map or null. For metadata we return null. 248 | TranslatedMessage? recreateIntlObjects( 249 | String id, 250 | String data, 251 | Map> messages, 252 | ) { 253 | var messageParser = MessageParser(data); 254 | var parsed = messageParser.pluralGenderSelectParse(); 255 | if (parsed is LiteralString && parsed.string.isEmpty) { 256 | parsed = messageParser.nonIcuMessageParse(); 257 | } 258 | return TranslatedMessage(id, parsed, messages[id] ?? []); 259 | } 260 | -------------------------------------------------------------------------------- /bin/make_examples_const.dart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dart 2 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 3 | // for details. All rights reserved. Use of this source code is governed by a 4 | // BSD-style license that can be found in the LICENSE file. 5 | 6 | /// Converts the examples parameter for Intl messages to be const. 7 | import 'dart:io'; 8 | 9 | import 'package:args/args.dart'; 10 | import 'package:dart_style/dart_style.dart'; 11 | import 'package:intl_translation/src/message_rewriter.dart'; 12 | import 'package:intl_translation/src/messages/main_message.dart'; 13 | 14 | void main(List args) { 15 | var parser = ArgParser(); 16 | var rest = parser.parse(args).rest; 17 | if (rest.isEmpty) { 18 | print('Accepts Dart file paths and rewrites the examples to be const ' 19 | 'in Intl.message calls.'); 20 | print('Usage: make_examples_const [options] [file.dart]...'); 21 | print(parser.usage); 22 | exit(0); 23 | } 24 | 25 | var formatter = DartFormatter(); 26 | for (var inputFile in rest) { 27 | var outputFile = inputFile; 28 | var file = File(inputFile); 29 | var content = file.readAsStringSync(); 30 | var newSource = rewriteMessages(content, '$file'); 31 | if (content == newSource) { 32 | print('No changes to $outputFile'); 33 | } else { 34 | print('Writing new source to $outputFile'); 35 | var out = File(outputFile); 36 | out.writeAsStringSync(formatter.format(newSource)); 37 | } 38 | } 39 | } 40 | 41 | /// Rewrite all Intl.message/plural/etc. calls in [source] which have 42 | /// examples, making them be const. 43 | /// 44 | /// Return the modified source code. If there are errors parsing, list 45 | /// [sourceName] in the error message. 46 | String rewriteMessages(String source, String sourceName) { 47 | var messages = findMessages(source, sourceName); 48 | messages 49 | .sort((a, b) => a.sourcePosition?.compareTo(b.sourcePosition ?? 0) ?? 0); 50 | int? start = 0; 51 | var newSource = StringBuffer(); 52 | for (var message in messages) { 53 | if (message.examples.isNotEmpty) { 54 | newSource.write(source.substring(start!, message.sourcePosition)); 55 | rewrite(newSource, source, message); 56 | start = message.endPosition; 57 | } 58 | } 59 | newSource.write(source.substring(start!)); 60 | return newSource.toString(); 61 | } 62 | 63 | void rewrite(StringBuffer newSource, String source, MainMessage message) { 64 | var sourcePosition = message.sourcePosition; 65 | if (sourcePosition != null) { 66 | var originalSource = source.substring(sourcePosition, message.endPosition); 67 | var examples = nonConstExamples.firstMatch(originalSource); 68 | if (examples == null) { 69 | newSource.write(originalSource); 70 | } else { 71 | var modifiedSource = originalSource.replaceFirst( 72 | examples.group(1)!, '${examples.group(1)}const'); 73 | newSource.write(modifiedSource); 74 | } 75 | } 76 | } 77 | 78 | final RegExp nonConstExamples = RegExp('([\\n,]\\s+examples: ){'); 79 | -------------------------------------------------------------------------------- /bin/rewrite_intl_messages.dart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dart 2 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 3 | // for details. All rights reserved. Use of this source code is governed by a 4 | // BSD-style license that can be found in the LICENSE file. 5 | 6 | /// A main program that imitates the action of the transformer, adding 7 | /// name and args parameters to Intl.message calls automatically. 8 | /// 9 | /// This is mainly intended to test the transformer logic outside of barback. 10 | /// It takes as input a single source Dart file and rewrites any 11 | /// Intl.message or related calls to automatically include the name and args 12 | /// parameters and writes the result to stdout. 13 | import 'dart:io'; 14 | 15 | import 'package:args/args.dart'; 16 | import 'package:dart_style/dart_style.dart'; 17 | import 'package:intl_translation/src/message_rewriter.dart'; 18 | 19 | String? outputFileOption = 'transformed_output.dart'; 20 | 21 | bool useStringSubstitution = true; 22 | bool replace = false; 23 | 24 | void main(List args) { 25 | var parser = ArgParser(); 26 | parser.addOption('output', 27 | defaultsTo: 'transformed_output.dart', 28 | callback: (x) => outputFileOption = x, 29 | help: 'Specify the output file.'); 30 | parser.addFlag('replace', 31 | defaultsTo: false, 32 | callback: (x) => replace = x, 33 | help: 'Overwrite the input file; ignore --output option.'); 34 | parser.addFlag('useStringSubstitution', 35 | defaultsTo: true, 36 | callback: (x) => useStringSubstitution = x, 37 | help: 'If true, in rewriting, try to leave the text of the message' 38 | ' as close to the original as possible. This is slightly less reliable,' 39 | ' because it relies on string matching, but better for updating' 40 | ' source code to move away from the transformer. If false,' 41 | ' behave like the transformer, regenerating the message code' 42 | ' from our internal representation. This is more reliable, but' 43 | ' produces less readable code.'); 44 | print(args); 45 | var rest = parser.parse(args).rest; 46 | if (rest.isEmpty) { 47 | print('Accepts Dart file paths and adds "name" and "args" parameters ' 48 | ' to Intl.message calls.'); 49 | print('Primarily useful for exercising the transformer logic or ' 50 | 'for rewriting programs to not require the transformer.'); 51 | print('Usage: rewrite_intl_messages [options] [file.dart]...'); 52 | print(parser.usage); 53 | exit(0); 54 | } 55 | 56 | var formatter = DartFormatter(); 57 | for (var inputFile in rest) { 58 | var outputFile = replace ? inputFile : outputFileOption; 59 | var file = File(inputFile); 60 | var content = file.readAsStringSync(); 61 | var newSource = rewriteMessages(content, '$file', 62 | useStringSubstitution: useStringSubstitution); 63 | if (content == newSource) { 64 | print('No changes to $outputFile'); 65 | } else { 66 | print('Writing new source to $outputFile'); 67 | var out = File(outputFile!); 68 | out.writeAsStringSync(formatter.format(newSource)); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /example/Makefile: -------------------------------------------------------------------------------- 1 | OUTPUT_DIR=lib/generated 2 | 3 | MESSAGE_ARBS=\ 4 | lib/messages/material_de_CH.arb \ 5 | lib/messages/material_de.arb \ 6 | lib/messages/material_en.arb \ 7 | lib/messages/material_es.arb 8 | 9 | # --json \ 10 | # --codegen_mode=debug \ 11 | 12 | generate: $(MESSAGE_ARBS) 13 | dart ../bin/generate_from_arb.dart \ 14 | --output-dir $(OUTPUT_DIR) \ 15 | lib/example_messages.dart \ 16 | $(MESSAGE_ARBS) 17 | 18 | clean: 19 | rm $(OUTPUT_DIR)/*.dart 20 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ## What's this? 2 | 3 | This is an example to demonstrate the output from `bin/generate_from_arb.dart`. 4 | 5 | You can see the example generated code in `lib/generated`. 6 | 7 | Note that the Dart code using the Intl messages - `lib/example_messages.dart` - 8 | is atypical Dart code. It exists just to have references to the `Intl.message` 9 | messages from the lib/messages ARB files, so the generator will output the 10 | cooresponding messages in `lib/generated`. 11 | 12 | ## Re-generating the example code 13 | 14 | - `cd example` 15 | - `make` 16 | -------------------------------------------------------------------------------- /example/lib/example_messages.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | // An example to demonstrate the output from `bin/generate_from_arb.dart`. 6 | 7 | import 'package:intl/intl.dart'; 8 | 9 | import 'generated/messages_all.dart'; 10 | 11 | void main(List args) async { 12 | var locale = args.isNotEmpty ? args[0] : Intl.defaultLocale; 13 | 14 | await initializeMessages(locale); 15 | 16 | print("Displaying messages for the '$locale' locale:"); 17 | print(''); 18 | 19 | Intl.withLocale(locale, () { 20 | printMessages(); 21 | }); 22 | } 23 | 24 | void printMessages() { 25 | show(Intl.message('scriptCategory')); 26 | show(Intl.message('timeOfDayFormat')); 27 | show(Intl.message('openAppDrawerTooltip')); 28 | show(Intl.message('backButtonTooltip')); 29 | show(Intl.message('closeButtonTooltip')); 30 | show(Intl.message('deleteButtonTooltip')); 31 | show(Intl.message('moreButtonTooltip')); 32 | show(Intl.message('nextMonthTooltip')); 33 | show(Intl.message('previousMonthTooltip')); 34 | show(Intl.message('nextPageTooltip')); 35 | show(Intl.message('previousPageTooltip')); 36 | show(Intl.message('firstPageTooltip')); 37 | show(Intl.message('lastPageTooltip')); 38 | show(Intl.message('showMenuTooltip')); 39 | show(Intl.message('aboutListTileTitle')); 40 | show(Intl.message('licensesPageTitle')); 41 | show(Intl.message('licensesPackageDetailTextZero')); 42 | show(Intl.message('licensesPackageDetailTextOne')); 43 | show(Intl.message('licensesPackageDetailTextOther')); 44 | show(Intl.message('pageRowsInfoTitle')); 45 | show(Intl.message('pageRowsInfoTitleApproximate')); 46 | show(Intl.message('rowsPerPageTitle')); 47 | show(Intl.message('tabLabel')); 48 | show(Intl.message('selectedRowCountTitleZero')); 49 | show(Intl.message('selectedRowCountTitleOne')); 50 | show(Intl.message('selectedRowCountTitleOther')); 51 | show(Intl.message('cancelButtonLabel')); 52 | show(Intl.message('closeButtonLabel')); 53 | show(Intl.message('continueButtonLabel')); 54 | show(Intl.message('copyButtonLabel')); 55 | show(Intl.message('cutButtonLabel')); 56 | show(Intl.message('okButtonLabel')); 57 | show(Intl.message('pasteButtonLabel')); 58 | show(Intl.message('selectAllButtonLabel')); 59 | show(Intl.message('viewLicensesButtonLabel')); 60 | show(Intl.message('anteMeridiemAbbreviation')); 61 | show(Intl.message('postMeridiemAbbreviation')); 62 | show(Intl.message('timePickerHourModeAnnouncement')); 63 | show(Intl.message('timePickerMinuteModeAnnouncement')); 64 | show(Intl.message('modalBarrierDismissLabel')); 65 | show(Intl.message('dateSeparator')); 66 | show(Intl.message('dateHelpText')); 67 | show(Intl.message('selectYearSemanticsLabel')); 68 | show(Intl.message('unspecifiedDate')); 69 | show(Intl.message('unspecifiedDateRange')); 70 | show(Intl.message('dateInputLabel')); 71 | show(Intl.message('dateRangeStartLabel')); 72 | show(Intl.message('dateRangeEndLabel')); 73 | show(Intl.message('dateRangeStartDateSemanticLabel')); 74 | show(Intl.message('dateRangeEndDateSemanticLabel')); 75 | show(Intl.message('invalidDateFormatLabel')); 76 | show(Intl.message('invalidDateRangeLabel')); 77 | show(Intl.message('dateOutOfRangeLabel')); 78 | show(Intl.message('saveButtonLabel')); 79 | show(Intl.message('datePickerHelpText')); 80 | show(Intl.message('dateRangePickerHelpText')); 81 | show(Intl.message('calendarModeButtonLabel')); 82 | show(Intl.message('inputDateModeButtonLabel')); 83 | show(Intl.message('timePickerDialHelpText')); 84 | show(Intl.message('timePickerInputHelpText')); 85 | show(Intl.message('timePickerHourLabel')); 86 | show(Intl.message('timePickerMinuteLabel')); 87 | show(Intl.message('invalidTimeLabel')); 88 | show(Intl.message('dialModeButtonLabel')); 89 | show(Intl.message('inputTimeModeButtonLabel')); 90 | show(Intl.message('signedInLabel')); 91 | show(Intl.message('hideAccountsLabel')); 92 | show(Intl.message('showAccountsLabel')); 93 | show(Intl.message('drawerLabel')); 94 | show(Intl.message('menuBarMenuLabel')); 95 | show(Intl.message('popupMenuLabel')); 96 | show(Intl.message('dialogLabel')); 97 | show(Intl.message('alertDialogLabel')); 98 | show(Intl.message('searchFieldLabel')); 99 | show(Intl.message('reorderItemToStart')); 100 | show(Intl.message('reorderItemToEnd')); 101 | show(Intl.message('reorderItemUp')); 102 | show(Intl.message('reorderItemDown')); 103 | show(Intl.message('reorderItemLeft')); 104 | show(Intl.message('reorderItemRight')); 105 | show(Intl.message('expandedIconTapHint')); 106 | show(Intl.message('collapsedIconTapHint')); 107 | show(Intl.message('remainingTextFieldCharacterCountZero')); 108 | show(Intl.message('remainingTextFieldCharacterCountOne')); 109 | show(Intl.message('remainingTextFieldCharacterCountOther')); 110 | show(Intl.message('refreshIndicatorSemanticLabel')); 111 | show(Intl.message('keyboardKeyAlt')); 112 | show(Intl.message('keyboardKeyAltGraph')); 113 | show(Intl.message('keyboardKeyBackspace')); 114 | show(Intl.message('keyboardKeyCapsLock')); 115 | show(Intl.message('keyboardKeyChannelDown')); 116 | show(Intl.message('keyboardKeyChannelUp')); 117 | show(Intl.message('keyboardKeyControl')); 118 | show(Intl.message('keyboardKeyDelete')); 119 | show(Intl.message('keyboardKeyEject')); 120 | show(Intl.message('keyboardKeyEnd')); 121 | show(Intl.message('keyboardKeyEscape')); 122 | show(Intl.message('keyboardKeyFn')); 123 | show(Intl.message('keyboardKeyHome')); 124 | show(Intl.message('keyboardKeyInsert')); 125 | show(Intl.message('keyboardKeyMeta')); 126 | show(Intl.message('keyboardKeyMetaMacOs')); 127 | show(Intl.message('keyboardKeyMetaWindows')); 128 | show(Intl.message('keyboardKeyNumLock')); 129 | show(Intl.message('keyboardKeyNumpad1')); 130 | show(Intl.message('keyboardKeyNumpad2')); 131 | show(Intl.message('keyboardKeyNumpad3')); 132 | show(Intl.message('keyboardKeyNumpad4')); 133 | show(Intl.message('keyboardKeyNumpad5')); 134 | show(Intl.message('keyboardKeyNumpad6')); 135 | show(Intl.message('keyboardKeyNumpad7')); 136 | show(Intl.message('keyboardKeyNumpad8')); 137 | show(Intl.message('keyboardKeyNumpad9')); 138 | show(Intl.message('keyboardKeyNumpad0')); 139 | show(Intl.message('keyboardKeyNumpadAdd')); 140 | show(Intl.message('keyboardKeyNumpadComma')); 141 | show(Intl.message('keyboardKeyNumpadDecimal')); 142 | show(Intl.message('keyboardKeyNumpadDivide')); 143 | show(Intl.message('keyboardKeyNumpadEnter')); 144 | show(Intl.message('keyboardKeyNumpadEqual')); 145 | show(Intl.message('keyboardKeyNumpadMultiply')); 146 | show(Intl.message('keyboardKeyNumpadParenLeft')); 147 | show(Intl.message('keyboardKeyNumpadParenRight')); 148 | show(Intl.message('keyboardKeyNumpadSubtract')); 149 | show(Intl.message('keyboardKeyPageDown')); 150 | show(Intl.message('keyboardKeyPageUp')); 151 | show(Intl.message('keyboardKeyPower')); 152 | show(Intl.message('keyboardKeyPowerOff')); 153 | show(Intl.message('keyboardKeyPrintScreen')); 154 | show(Intl.message('keyboardKeyScrollLock')); 155 | show(Intl.message('keyboardKeySelect')); 156 | show(Intl.message('keyboardKeySpace')); 157 | } 158 | 159 | void show(String message) { 160 | print(" - '$message'"); 161 | } 162 | -------------------------------------------------------------------------------- /example/lib/generated/messages_all.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | export 'messages_all_locales.dart' 6 | show initializeMessages; 7 | 8 | -------------------------------------------------------------------------------- /example/lib/generated/messages_all_locales.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | // Ignore issues from commonly used lints in this file. 6 | // ignore_for_file:implementation_imports, file_names 7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering 8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment 9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases 10 | // ignore_for_file:comment_references 11 | 12 | import 'package:intl/intl.dart'; 13 | import 'package:intl/message_lookup_by_library.dart'; 14 | import 'package:intl/src/intl_helpers.dart'; 15 | 16 | import 'messages_de_CH.dart' deferred as messages_de_ch; 17 | import 'messages_de.dart' deferred as messages_de; 18 | import 'messages_en.dart' deferred as messages_en; 19 | import 'messages_es.dart' deferred as messages_es; 20 | 21 | typedef Future LibraryLoader(); 22 | Map _deferredLibraries = { 23 | 'de_CH': messages_de_ch.loadLibrary, 24 | 'de': messages_de.loadLibrary, 25 | 'en': messages_en.loadLibrary, 26 | 'es': messages_es.loadLibrary, 27 | }; 28 | 29 | MessageLookupByLibrary? _findExact(String localeName) { 30 | switch (localeName) { 31 | case 'de_CH': 32 | return messages_de_ch.messages; 33 | case 'de': 34 | return messages_de.messages; 35 | case 'en': 36 | return messages_en.messages; 37 | case 'es': 38 | return messages_es.messages; 39 | default: 40 | return null; 41 | } 42 | } 43 | 44 | /// User programs should call this before using [localeName] for messages. 45 | Future initializeMessages(String? localeName) async { 46 | var availableLocale = Intl.verifiedLocale( 47 | localeName, 48 | (locale) => _deferredLibraries[locale] != null, 49 | onFailure: (_) => null); 50 | if (availableLocale == null) { 51 | return Future.value(false); 52 | } 53 | var lib = _deferredLibraries[availableLocale]; 54 | await (lib == null ? Future.value(false) : lib()); 55 | initializeInternalMessageLookup(() => CompositeMessageLookup()); 56 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); 57 | return Future.value(true); 58 | } 59 | 60 | bool _messagesExistFor(String locale) { 61 | try { 62 | return _findExact(locale) != null; 63 | } catch (e) { 64 | return false; 65 | } 66 | } 67 | 68 | MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { 69 | var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor, 70 | onFailure: (_) => null); 71 | if (actualLocale == null) return null; 72 | return _findExact(actualLocale); 73 | } 74 | -------------------------------------------------------------------------------- /example/lib/generated/messages_de_CH.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that provides messages for a de_CH locale. All the 3 | // messages from the main program should be duplicated here with the same 4 | // function name. 5 | 6 | // Ignore issues from commonly used lints in this file. 7 | // ignore_for_file:unnecessary_brace_in_string_interps 8 | // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering 9 | // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases 10 | // ignore_for_file:unused_import, file_names 11 | 12 | import 'package:intl/intl.dart'; 13 | import 'package:intl/message_lookup_by_library.dart'; 14 | 15 | final messages = MessageLookup(); 16 | 17 | typedef String? MessageIfAbsent( 18 | String? messageStr, List? args); 19 | 20 | class MessageLookup extends MessageLookupByLibrary { 21 | @override 22 | String get localeName => 'de_CH'; 23 | 24 | @override 25 | final Map messages = _notInlinedMessages(_notInlinedMessages); 26 | 27 | static Map _notInlinedMessages(_) => { 28 | 'aboutListTileTitle': MessageLookupByLibrary.simpleMessage('Über \$applicationName'), 29 | 'alertDialogLabel': MessageLookupByLibrary.simpleMessage('Benachrichtigung'), 30 | 'anteMeridiemAbbreviation': MessageLookupByLibrary.simpleMessage('AM'), 31 | 'backButtonTooltip': MessageLookupByLibrary.simpleMessage('Zurück'), 32 | 'calendarModeButtonLabel': MessageLookupByLibrary.simpleMessage('Zum Kalender wechseln'), 33 | 'cancelButtonLabel': MessageLookupByLibrary.simpleMessage('ABBRECHEN'), 34 | 'closeButtonLabel': MessageLookupByLibrary.simpleMessage('SCHLIEẞEN'), 35 | 'closeButtonTooltip': MessageLookupByLibrary.simpleMessage('Schliessen'), 36 | 'collapsedIconTapHint': MessageLookupByLibrary.simpleMessage('Maximieren'), 37 | 'continueButtonLabel': MessageLookupByLibrary.simpleMessage('WEITER'), 38 | 'copyButtonLabel': MessageLookupByLibrary.simpleMessage('Kopieren'), 39 | 'cutButtonLabel': MessageLookupByLibrary.simpleMessage('Ausschneiden'), 40 | 'dateHelpText': MessageLookupByLibrary.simpleMessage('tt.mm.jjjj'), 41 | 'dateInputLabel': MessageLookupByLibrary.simpleMessage('Datum eingeben'), 42 | 'dateOutOfRangeLabel': MessageLookupByLibrary.simpleMessage('Ausserhalb des Zeitraums.'), 43 | 'datePickerHelpText': MessageLookupByLibrary.simpleMessage('DATUM AUSWÄHLEN'), 44 | 'dateRangeEndDateSemanticLabel': MessageLookupByLibrary.simpleMessage('Enddatum \$fullDate'), 45 | 'dateRangeEndLabel': MessageLookupByLibrary.simpleMessage('Enddatum'), 46 | 'dateRangePickerHelpText': MessageLookupByLibrary.simpleMessage('ZEITRAUM AUSWÄHLEN'), 47 | 'dateRangeStartDateSemanticLabel': MessageLookupByLibrary.simpleMessage('Startdatum \$fullDate'), 48 | 'dateRangeStartLabel': MessageLookupByLibrary.simpleMessage('Startdatum'), 49 | 'dateSeparator': MessageLookupByLibrary.simpleMessage('.'), 50 | 'deleteButtonTooltip': MessageLookupByLibrary.simpleMessage('Löschen'), 51 | 'dialModeButtonLabel': MessageLookupByLibrary.simpleMessage('Zur Uhrzeitauswahl wechseln'), 52 | 'dialogLabel': MessageLookupByLibrary.simpleMessage('Dialogfeld'), 53 | 'drawerLabel': MessageLookupByLibrary.simpleMessage('Navigationsmenü'), 54 | 'expandedIconTapHint': MessageLookupByLibrary.simpleMessage('Minimieren'), 55 | 'firstPageTooltip': MessageLookupByLibrary.simpleMessage('First page'), 56 | 'hideAccountsLabel': MessageLookupByLibrary.simpleMessage('Konten ausblenden'), 57 | 'inputDateModeButtonLabel': MessageLookupByLibrary.simpleMessage('Zur Texteingabe wechseln'), 58 | 'inputTimeModeButtonLabel': MessageLookupByLibrary.simpleMessage('Zum Texteingabemodus wechseln'), 59 | 'invalidDateFormatLabel': MessageLookupByLibrary.simpleMessage('Ungültiges Format.'), 60 | 'invalidDateRangeLabel': MessageLookupByLibrary.simpleMessage('Ungültiger Zeitraum.'), 61 | 'invalidTimeLabel': MessageLookupByLibrary.simpleMessage('Gib eine gültige Uhrzeit ein'), 62 | 'lastPageTooltip': MessageLookupByLibrary.simpleMessage('Last page'), 63 | 'licensesPackageDetailTextOne': MessageLookupByLibrary.simpleMessage('1 Lizenz'), 64 | 'licensesPackageDetailTextOther': MessageLookupByLibrary.simpleMessage('\$licenseCount Lizenzen'), 65 | 'licensesPageTitle': MessageLookupByLibrary.simpleMessage('Lizenzen'), 66 | 'modalBarrierDismissLabel': MessageLookupByLibrary.simpleMessage('Schliessen'), 67 | 'moreButtonTooltip': MessageLookupByLibrary.simpleMessage('Mehr'), 68 | 'nextMonthTooltip': MessageLookupByLibrary.simpleMessage('Nächster Monat'), 69 | 'nextPageTooltip': MessageLookupByLibrary.simpleMessage('Nächste Seite'), 70 | 'okButtonLabel': MessageLookupByLibrary.simpleMessage('OK'), 71 | 'openAppDrawerTooltip': MessageLookupByLibrary.simpleMessage('Navigationsmenü öffnen'), 72 | 'pageRowsInfoTitle': MessageLookupByLibrary.simpleMessage('\$firstRow–\$lastRow von \$rowCount'), 73 | 'pageRowsInfoTitleApproximate': MessageLookupByLibrary.simpleMessage('\$firstRow–\$lastRow von etwa \$rowCount'), 74 | 'pasteButtonLabel': MessageLookupByLibrary.simpleMessage('Einsetzen'), 75 | 'popupMenuLabel': MessageLookupByLibrary.simpleMessage('Pop-up-Menü'), 76 | 'postMeridiemAbbreviation': MessageLookupByLibrary.simpleMessage('PM'), 77 | 'previousMonthTooltip': MessageLookupByLibrary.simpleMessage('Vorheriger Monat'), 78 | 'previousPageTooltip': MessageLookupByLibrary.simpleMessage('Vorherige Seite'), 79 | 'refreshIndicatorSemanticLabel': MessageLookupByLibrary.simpleMessage('Aktualisieren'), 80 | 'remainingTextFieldCharacterCountOne': MessageLookupByLibrary.simpleMessage('Noch 1 Zeichen'), 81 | 'remainingTextFieldCharacterCountOther': MessageLookupByLibrary.simpleMessage('Noch \$remainingCount Zeichen'), 82 | 'remainingTextFieldCharacterCountZero': MessageLookupByLibrary.simpleMessage('TBD'), 83 | 'reorderItemDown': MessageLookupByLibrary.simpleMessage('Nach unten verschieben'), 84 | 'reorderItemLeft': MessageLookupByLibrary.simpleMessage('Nach links verschieben'), 85 | 'reorderItemRight': MessageLookupByLibrary.simpleMessage('Nach rechts verschieben'), 86 | 'reorderItemToEnd': MessageLookupByLibrary.simpleMessage('An das Ende verschieben'), 87 | 'reorderItemToStart': MessageLookupByLibrary.simpleMessage('An den Anfang verschieben'), 88 | 'reorderItemUp': MessageLookupByLibrary.simpleMessage('Nach oben verschieben'), 89 | 'rowsPerPageTitle': MessageLookupByLibrary.simpleMessage('Zeilen pro Seite:'), 90 | 'saveButtonLabel': MessageLookupByLibrary.simpleMessage('SPEICHERN'), 91 | 'scriptCategory': MessageLookupByLibrary.simpleMessage('English-like'), 92 | 'searchFieldLabel': MessageLookupByLibrary.simpleMessage('Suchen'), 93 | 'selectAllButtonLabel': MessageLookupByLibrary.simpleMessage('Alle auswählen'), 94 | 'selectYearSemanticsLabel': MessageLookupByLibrary.simpleMessage('Jahr auswählen'), 95 | 'selectedRowCountTitleOne': MessageLookupByLibrary.simpleMessage('1 Element ausgewählt'), 96 | 'selectedRowCountTitleOther': MessageLookupByLibrary.simpleMessage('\$selectedRowCount Elemente ausgewählt'), 97 | 'showAccountsLabel': MessageLookupByLibrary.simpleMessage('Konten anzeigen'), 98 | 'showMenuTooltip': MessageLookupByLibrary.simpleMessage('Menü anzeigen'), 99 | 'signedInLabel': MessageLookupByLibrary.simpleMessage('Angemeldet'), 100 | 'tabLabel': MessageLookupByLibrary.simpleMessage('Tab \$tabIndex von \$tabCount'), 101 | 'timeOfDayFormat': MessageLookupByLibrary.simpleMessage('HH:mm'), 102 | 'timePickerDialHelpText': MessageLookupByLibrary.simpleMessage('UHRZEIT AUSWÄHLEN'), 103 | 'timePickerHourLabel': MessageLookupByLibrary.simpleMessage('Stunde'), 104 | 'timePickerHourModeAnnouncement': MessageLookupByLibrary.simpleMessage('Stunden auswählen'), 105 | 'timePickerInputHelpText': MessageLookupByLibrary.simpleMessage('ZEIT EINGEBEN'), 106 | 'timePickerMinuteLabel': MessageLookupByLibrary.simpleMessage('Minute'), 107 | 'timePickerMinuteModeAnnouncement': MessageLookupByLibrary.simpleMessage('Minuten auswählen'), 108 | 'unspecifiedDate': MessageLookupByLibrary.simpleMessage('Datum'), 109 | 'unspecifiedDateRange': MessageLookupByLibrary.simpleMessage('Zeitraum'), 110 | 'viewLicensesButtonLabel': MessageLookupByLibrary.simpleMessage('LIZENZEN ANZEIGEN') 111 | }; 112 | } 113 | -------------------------------------------------------------------------------- /example/lib/messages/material_de.arb: -------------------------------------------------------------------------------- 1 | { 2 | "scriptCategory": "English-like", 3 | "timeOfDayFormat": "HH:mm", 4 | "openAppDrawerTooltip": "Navigationsmenü öffnen", 5 | "backButtonTooltip": "Zurück", 6 | "closeButtonTooltip": "Schließen", 7 | "deleteButtonTooltip": "Löschen", 8 | "nextMonthTooltip": "Nächster Monat", 9 | "previousMonthTooltip": "Vorheriger Monat", 10 | "nextPageTooltip": "Nächste Seite", 11 | "previousPageTooltip": "Vorherige Seite", 12 | "firstPageTooltip": "Erste Seite", 13 | "lastPageTooltip": "Letzte Seite", 14 | "showMenuTooltip": "Menü anzeigen", 15 | "aboutListTileTitle": "Über $applicationName", 16 | "licensesPageTitle": "Lizenzen", 17 | "pageRowsInfoTitle": "$firstRow–$lastRow von $rowCount", 18 | "pageRowsInfoTitleApproximate": "$firstRow–$lastRow von etwa $rowCount", 19 | "rowsPerPageTitle": "Zeilen pro Seite:", 20 | "tabLabel": "Tab $tabIndex von $tabCount", 21 | "selectedRowCountTitleZero": "Keine Objekte ausgewählt", 22 | "selectedRowCountTitleOne": "1 Element ausgewählt", 23 | "selectedRowCountTitleOther": "$selectedRowCount Elemente ausgewählt", 24 | "cancelButtonLabel": "ABBRECHEN", 25 | "closeButtonLabel": "SCHLIEẞEN", 26 | "continueButtonLabel": "WEITER", 27 | "copyButtonLabel": "Kopieren", 28 | "cutButtonLabel": "Ausschneiden", 29 | "okButtonLabel": "OK", 30 | "pasteButtonLabel": "Einsetzen", 31 | "selectAllButtonLabel": "Alle auswählen", 32 | "viewLicensesButtonLabel": "LIZENZEN ANZEIGEN", 33 | "anteMeridiemAbbreviation": "AM", 34 | "postMeridiemAbbreviation": "PM", 35 | "timePickerHourModeAnnouncement": "Stunden auswählen", 36 | "timePickerMinuteModeAnnouncement": "Minuten auswählen", 37 | "signedInLabel": "Angemeldet", 38 | "hideAccountsLabel": "Konten ausblenden", 39 | "showAccountsLabel": "Konten anzeigen", 40 | "modalBarrierDismissLabel": "Schließen", 41 | "drawerLabel": "Navigationsmenü", 42 | "popupMenuLabel": "Pop-up-Menü", 43 | "dialogLabel": "Dialogfeld", 44 | "alertDialogLabel": "Benachrichtigung", 45 | "searchFieldLabel": "Suchen", 46 | "reorderItemToStart": "An den Anfang verschieben", 47 | "reorderItemToEnd": "An das Ende verschieben", 48 | "reorderItemUp": "Nach oben verschieben", 49 | "reorderItemDown": "Nach unten verschieben", 50 | "reorderItemLeft": "Nach links verschieben", 51 | "reorderItemRight": "Nach rechts verschieben", 52 | "expandedIconTapHint": "Minimieren", 53 | "collapsedIconTapHint": "Maximieren", 54 | "remainingTextFieldCharacterCountZero": "TBD", 55 | "remainingTextFieldCharacterCountOne": "Noch 1 Zeichen", 56 | "remainingTextFieldCharacterCountOther": "Noch $remainingCount Zeichen", 57 | "refreshIndicatorSemanticLabel": "Aktualisieren", 58 | "moreButtonTooltip": "Mehr", 59 | "dateSeparator": ".", 60 | "dateHelpText": "tt.mm.jjjj", 61 | "selectYearSemanticsLabel": "Jahr auswählen", 62 | "unspecifiedDate": "Datum", 63 | "unspecifiedDateRange": "Zeitraum", 64 | "dateInputLabel": "Datum eingeben", 65 | "dateRangeStartLabel": "Startdatum", 66 | "dateRangeEndLabel": "Enddatum", 67 | "dateRangeStartDateSemanticLabel": "Startdatum $fullDate", 68 | "dateRangeEndDateSemanticLabel": "Enddatum $fullDate", 69 | "invalidDateFormatLabel": "Ungültiges Format.", 70 | "invalidDateRangeLabel": "Ungültiger Zeitraum.", 71 | "dateOutOfRangeLabel": "Außerhalb des Zeitraums.", 72 | "saveButtonLabel": "SPEICHERN", 73 | "datePickerHelpText": "DATUM AUSWÄHLEN", 74 | "dateRangePickerHelpText": "ZEITRAUM AUSWÄHLEN", 75 | "calendarModeButtonLabel": "Zum Kalender wechseln", 76 | "inputDateModeButtonLabel": "Zur Texteingabe wechseln", 77 | "timePickerDialHelpText": "UHRZEIT AUSWÄHLEN", 78 | "timePickerInputHelpText": "ZEIT EINGEBEN", 79 | "timePickerHourLabel": "Stunde", 80 | "timePickerMinuteLabel": "Minute", 81 | "invalidTimeLabel": "Geben Sie eine gültige Uhrzeit ein", 82 | "dialModeButtonLabel": "Zur Uhrzeitauswahl wechseln", 83 | "inputTimeModeButtonLabel": "Zum Texteingabemodus wechseln", 84 | "licensesPackageDetailTextZero": "No licenses", 85 | "licensesPackageDetailTextOne": "1 Lizenz", 86 | "licensesPackageDetailTextOther": "$licenseCount Lizenzen", 87 | "keyboardKeyAlt": "Alt", 88 | "keyboardKeyAltGraph": "AltGr", 89 | "keyboardKeyBackspace": "Rücktaste", 90 | "keyboardKeyCapsLock": "Feststelltaste", 91 | "keyboardKeyChannelDown": "Vorheriger Kanal", 92 | "keyboardKeyChannelUp": "Nächster Kanal", 93 | "keyboardKeyControl": "Strg", 94 | "keyboardKeyDelete": "Entf", 95 | "keyboardKeyEject": "Auswerfen", 96 | "keyboardKeyEnd": "Ende", 97 | "keyboardKeyEscape": "Esc", 98 | "keyboardKeyFn": "Fn", 99 | "keyboardKeyHome": "Pos1", 100 | "keyboardKeyInsert": "Einfg", 101 | "keyboardKeyMeta": "Meta", 102 | "keyboardKeyNumLock": "Num", 103 | "keyboardKeyNumpad1": "Num 1", 104 | "keyboardKeyNumpad2": "Num 2", 105 | "keyboardKeyNumpad3": "Num 3", 106 | "keyboardKeyNumpad4": "Num 4", 107 | "keyboardKeyNumpad5": "Num 5", 108 | "keyboardKeyNumpad6": "Num 6", 109 | "keyboardKeyNumpad7": "Num 7", 110 | "keyboardKeyNumpad8": "Num 8", 111 | "keyboardKeyNumpad9": "Num 9", 112 | "keyboardKeyNumpad0": "Num 0", 113 | "keyboardKeyNumpadAdd": "Num +", 114 | "keyboardKeyNumpadComma": "Num ,", 115 | "keyboardKeyNumpadDecimal": "Num .", 116 | "keyboardKeyNumpadDivide": "Num /", 117 | "keyboardKeyNumpadEnter": "Num Eingabetaste", 118 | "keyboardKeyNumpadEqual": "Num =", 119 | "keyboardKeyNumpadMultiply": "Num *", 120 | "keyboardKeyNumpadParenLeft": "Num (", 121 | "keyboardKeyNumpadParenRight": "Num )", 122 | "keyboardKeyNumpadSubtract": "Num -", 123 | "keyboardKeyPageDown": "Bild ab", 124 | "keyboardKeyPageUp": "Bild auf", 125 | "keyboardKeyPower": "Ein/Aus", 126 | "keyboardKeyPowerOff": "Aus", 127 | "keyboardKeyPrintScreen": "Druck", 128 | "keyboardKeyScrollLock": "Rollen", 129 | "keyboardKeySelect": "Auswählen", 130 | "keyboardKeySpace": "Leertaste", 131 | "keyboardKeyMetaMacOs": "Befehl", 132 | "keyboardKeyMetaWindows": "Win", 133 | "menuBarMenuLabel": "Menü in der Menüleiste" 134 | } 135 | -------------------------------------------------------------------------------- /example/lib/messages/material_de_CH.arb: -------------------------------------------------------------------------------- 1 | { 2 | "dialModeButtonLabel": "Zur Uhrzeitauswahl wechseln", 3 | "licensesPackageDetailTextOne": "1 Lizenz", 4 | "timePickerDialHelpText": "UHRZEIT AUSWÄHLEN", 5 | "timePickerInputHelpText": "ZEIT EINGEBEN", 6 | "timePickerHourLabel": "Stunde", 7 | "timePickerMinuteLabel": "Minute", 8 | "invalidTimeLabel": "Gib eine gültige Uhrzeit ein", 9 | "licensesPackageDetailTextOther": "$licenseCount Lizenzen", 10 | "inputTimeModeButtonLabel": "Zum Texteingabemodus wechseln", 11 | "dateSeparator": ".", 12 | "dateInputLabel": "Datum eingeben", 13 | "calendarModeButtonLabel": "Zum Kalender wechseln", 14 | "dateRangePickerHelpText": "ZEITRAUM AUSWÄHLEN", 15 | "datePickerHelpText": "DATUM AUSWÄHLEN", 16 | "saveButtonLabel": "SPEICHERN", 17 | "dateOutOfRangeLabel": "Ausserhalb des Zeitraums.", 18 | "invalidDateRangeLabel": "Ungültiger Zeitraum.", 19 | "invalidDateFormatLabel": "Ungültiges Format.", 20 | "dateRangeEndDateSemanticLabel": "Enddatum $fullDate", 21 | "dateRangeStartDateSemanticLabel": "Startdatum $fullDate", 22 | "dateRangeEndLabel": "Enddatum", 23 | "dateRangeStartLabel": "Startdatum", 24 | "inputDateModeButtonLabel": "Zur Texteingabe wechseln", 25 | "unspecifiedDateRange": "Zeitraum", 26 | "unspecifiedDate": "Datum", 27 | "selectYearSemanticsLabel": "Jahr auswählen", 28 | "dateHelpText": "tt.mm.jjjj", 29 | "moreButtonTooltip": "Mehr", 30 | "tabLabel": "Tab $tabIndex von $tabCount", 31 | "showAccountsLabel": "Konten anzeigen", 32 | "hideAccountsLabel": "Konten ausblenden", 33 | "signedInLabel": "Angemeldet", 34 | "timePickerMinuteModeAnnouncement": "Minuten auswählen", 35 | "timePickerHourModeAnnouncement": "Stunden auswählen", 36 | "scriptCategory": "English-like", 37 | "timeOfDayFormat": "HH:mm", 38 | "openAppDrawerTooltip": "Navigationsmenü öffnen", 39 | "backButtonTooltip": "Zurück", 40 | "closeButtonTooltip": "Schliessen", 41 | "deleteButtonTooltip": "Löschen", 42 | "nextMonthTooltip": "Nächster Monat", 43 | "previousMonthTooltip": "Vorheriger Monat", 44 | "nextPageTooltip": "Nächste Seite", 45 | "previousPageTooltip": "Vorherige Seite", 46 | "firstPageTooltip": "First page", 47 | "lastPageTooltip": "Last page", 48 | "showMenuTooltip": "Menü anzeigen", 49 | "aboutListTileTitle": "Über $applicationName", 50 | "licensesPageTitle": "Lizenzen", 51 | "pageRowsInfoTitle": "$firstRow–$lastRow von $rowCount", 52 | "pageRowsInfoTitleApproximate": "$firstRow–$lastRow von etwa $rowCount", 53 | "rowsPerPageTitle": "Zeilen pro Seite:", 54 | "selectedRowCountTitleOne": "1 Element ausgewählt", 55 | "selectedRowCountTitleOther": "$selectedRowCount Elemente ausgewählt", 56 | "cancelButtonLabel": "ABBRECHEN", 57 | "closeButtonLabel": "SCHLIEẞEN", 58 | "continueButtonLabel": "WEITER", 59 | "copyButtonLabel": "Kopieren", 60 | "cutButtonLabel": "Ausschneiden", 61 | "okButtonLabel": "OK", 62 | "pasteButtonLabel": "Einsetzen", 63 | "selectAllButtonLabel": "Alle auswählen", 64 | "viewLicensesButtonLabel": "LIZENZEN ANZEIGEN", 65 | "anteMeridiemAbbreviation": "AM", 66 | "postMeridiemAbbreviation": "PM", 67 | "modalBarrierDismissLabel": "Schliessen", 68 | "drawerLabel": "Navigationsmenü", 69 | "popupMenuLabel": "Pop-up-Menü", 70 | "dialogLabel": "Dialogfeld", 71 | "alertDialogLabel": "Benachrichtigung", 72 | "searchFieldLabel": "Suchen", 73 | "reorderItemToStart": "An den Anfang verschieben", 74 | "reorderItemToEnd": "An das Ende verschieben", 75 | "reorderItemUp": "Nach oben verschieben", 76 | "reorderItemDown": "Nach unten verschieben", 77 | "reorderItemLeft": "Nach links verschieben", 78 | "reorderItemRight": "Nach rechts verschieben", 79 | "expandedIconTapHint": "Minimieren", 80 | "collapsedIconTapHint": "Maximieren", 81 | "remainingTextFieldCharacterCountZero": "TBD", 82 | "remainingTextFieldCharacterCountOne": "Noch 1 Zeichen", 83 | "remainingTextFieldCharacterCountOther": "Noch $remainingCount Zeichen", 84 | "refreshIndicatorSemanticLabel": "Aktualisieren" 85 | } 86 | -------------------------------------------------------------------------------- /example/lib/messages/material_es.arb: -------------------------------------------------------------------------------- 1 | { 2 | "scriptCategory": "English-like", 3 | "timeOfDayFormat": "H:mm", 4 | "openAppDrawerTooltip": "Abrir el menú de navegación", 5 | "backButtonTooltip": "Atrás", 6 | "closeButtonTooltip": "Cerrar", 7 | "deleteButtonTooltip": "Eliminar", 8 | "nextMonthTooltip": "Mes siguiente", 9 | "previousMonthTooltip": "Mes anterior", 10 | "nextPageTooltip": "Página siguiente", 11 | "previousPageTooltip": "Página anterior", 12 | "firstPageTooltip": "Primera página", 13 | "lastPageTooltip": "Última página", 14 | "showMenuTooltip": "Mostrar menú", 15 | "aboutListTileTitle": "Sobre $applicationName", 16 | "licensesPageTitle": "Licencias", 17 | "pageRowsInfoTitle": "$firstRow‑$lastRow de $rowCount", 18 | "pageRowsInfoTitleApproximate": "$firstRow‑$lastRow de aproximadamente $rowCount", 19 | "rowsPerPageTitle": "Filas por página:", 20 | "tabLabel": "Pestaña $tabIndex de $tabCount", 21 | "selectedRowCountTitleZero": "No se han seleccionado elementos", 22 | "selectedRowCountTitleOne": "1 elemento seleccionado", 23 | "selectedRowCountTitleOther": "$selectedRowCount elementos seleccionados", 24 | "cancelButtonLabel": "CANCELAR", 25 | "closeButtonLabel": "CERRAR", 26 | "continueButtonLabel": "CONTINUAR", 27 | "copyButtonLabel": "Copiar", 28 | "cutButtonLabel": "Cortar", 29 | "okButtonLabel": "ACEPTAR", 30 | "pasteButtonLabel": "Pegar", 31 | "selectAllButtonLabel": "Seleccionar todo", 32 | "viewLicensesButtonLabel": "VER LICENCIAS", 33 | "anteMeridiemAbbreviation": "a. m.", 34 | "postMeridiemAbbreviation": "p. m.", 35 | "timePickerHourModeAnnouncement": "Seleccionar horas", 36 | "timePickerMinuteModeAnnouncement": "Seleccionar minutos", 37 | "signedInLabel": "Sesión iniciada", 38 | "hideAccountsLabel": "Ocultar cuentas", 39 | "showAccountsLabel": "Mostrar cuentas", 40 | "modalBarrierDismissLabel": "Cerrar", 41 | "drawerLabel": "Menú de navegación", 42 | "popupMenuLabel": "Menú emergente", 43 | "dialogLabel": "Cuadro de diálogo", 44 | "alertDialogLabel": "Alerta", 45 | "searchFieldLabel": "Buscar", 46 | "reorderItemToStart": "Mover al principio", 47 | "reorderItemToEnd": "Mover al final", 48 | "reorderItemUp": "Mover hacia arriba", 49 | "reorderItemDown": "Mover hacia abajo", 50 | "reorderItemLeft": "Mover hacia la izquierda", 51 | "reorderItemRight": "Mover hacia la derecha", 52 | "expandedIconTapHint": "Ocultar", 53 | "collapsedIconTapHint": "Mostrar", 54 | "remainingTextFieldCharacterCountZero": "TBD", 55 | "remainingTextFieldCharacterCountOne": "Queda 1 carácter.", 56 | "remainingTextFieldCharacterCountOther": "Quedan $remainingCount caracteres", 57 | "refreshIndicatorSemanticLabel": "Actualizar", 58 | "moreButtonTooltip": "Más", 59 | "dateSeparator": "/", 60 | "dateHelpText": "mm/dd/aaaa", 61 | "selectYearSemanticsLabel": "Seleccionar año", 62 | "unspecifiedDate": "Fecha", 63 | "unspecifiedDateRange": "Periodo", 64 | "dateInputLabel": "Introduce una fecha", 65 | "dateRangeStartLabel": "Fecha de inicio", 66 | "dateRangeEndLabel": "Fecha de finalización", 67 | "dateRangeStartDateSemanticLabel": "Fecha de inicio $fullDate", 68 | "dateRangeEndDateSemanticLabel": "Fecha de finalización $fullDate", 69 | "invalidDateFormatLabel": "Formato no válido.", 70 | "invalidDateRangeLabel": "Periodo no válido.", 71 | "dateOutOfRangeLabel": "Fuera del periodo válido.", 72 | "saveButtonLabel": "GUARDAR", 73 | "datePickerHelpText": "SELECCIONAR FECHA", 74 | "dateRangePickerHelpText": "SELECCIONAR PERIODO", 75 | "calendarModeButtonLabel": "Cambiar a calendario", 76 | "inputDateModeButtonLabel": "Cambiar a cuadro de texto", 77 | "timePickerDialHelpText": "SELECCIONAR HORA", 78 | "timePickerInputHelpText": "INTRODUCIR HORA", 79 | "timePickerHourLabel": "Hora", 80 | "timePickerMinuteLabel": "Minuto", 81 | "invalidTimeLabel": "Indica una hora válida", 82 | "dialModeButtonLabel": "Cambiar al modo de selección de hora", 83 | "inputTimeModeButtonLabel": "Cambiar al modo de introducción de texto", 84 | "licensesPackageDetailTextZero": "No licenses", 85 | "licensesPackageDetailTextOne": "1 licencia", 86 | "licensesPackageDetailTextOther": "$licenseCount licencias", 87 | "keyboardKeyAlt": "Alt", 88 | "keyboardKeyAltGraph": "Alt Gr", 89 | "keyboardKeyBackspace": "Retroceso", 90 | "keyboardKeyCapsLock": "Bloq Mayús", 91 | "keyboardKeyChannelDown": "Canal siguiente", 92 | "keyboardKeyChannelUp": "Canal anterior", 93 | "keyboardKeyControl": "Ctrl", 94 | "keyboardKeyDelete": "Supr", 95 | "keyboardKeyEject": "Expulsar", 96 | "keyboardKeyEnd": "Fin", 97 | "keyboardKeyEscape": "Esc", 98 | "keyboardKeyFn": "Fn", 99 | "keyboardKeyHome": "Inicio", 100 | "keyboardKeyInsert": "Insert", 101 | "keyboardKeyMeta": "Meta", 102 | "keyboardKeyNumLock": "Bloq Num", 103 | "keyboardKeyNumpad1": "Num 1", 104 | "keyboardKeyNumpad2": "Num 2", 105 | "keyboardKeyNumpad3": "Num 3", 106 | "keyboardKeyNumpad4": "Num 4", 107 | "keyboardKeyNumpad5": "Num 5", 108 | "keyboardKeyNumpad6": "Num 6", 109 | "keyboardKeyNumpad7": "Num 7", 110 | "keyboardKeyNumpad8": "Num 8", 111 | "keyboardKeyNumpad9": "Num 9", 112 | "keyboardKeyNumpad0": "Num 0", 113 | "keyboardKeyNumpadAdd": "Num +", 114 | "keyboardKeyNumpadComma": "Num ,", 115 | "keyboardKeyNumpadDecimal": "Num .", 116 | "keyboardKeyNumpadDivide": "Num /", 117 | "keyboardKeyNumpadEnter": "Num Intro", 118 | "keyboardKeyNumpadEqual": "Num =", 119 | "keyboardKeyNumpadMultiply": "Num *", 120 | "keyboardKeyNumpadParenLeft": "Num (", 121 | "keyboardKeyNumpadParenRight": "Num )", 122 | "keyboardKeyNumpadSubtract": "Num -", 123 | "keyboardKeyPageDown": "Av Pág", 124 | "keyboardKeyPageUp": "Re Pág", 125 | "keyboardKeyPower": "Encendido", 126 | "keyboardKeyPowerOff": "Apagado", 127 | "keyboardKeyPrintScreen": "Impr Pant", 128 | "keyboardKeyScrollLock": "Bloq Despl", 129 | "keyboardKeySelect": "Selección", 130 | "keyboardKeySpace": "Espacio", 131 | "keyboardKeyMetaMacOs": "Comando", 132 | "keyboardKeyMetaWindows": "Win", 133 | "menuBarMenuLabel": "Menú de la barra de menú" 134 | } 135 | -------------------------------------------------------------------------------- /lib/extract_messages.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// This is for use in extracting messages from a Dart program 6 | /// using the Intl.message() mechanism and writing them to a file for 7 | /// translation. This provides only the stub of a mechanism, because it 8 | /// doesn't define how the file should be written. It provides an 9 | /// [IntlMessage] class that holds the extracted data and [parseString] 10 | /// and [parseFile] methods which 11 | /// can extract messages that conform to the expected pattern: 12 | /// (parameters) => Intl.message("Message $parameters", desc: ...); 13 | /// It uses the analyzer package to do the parsing, so may 14 | /// break if there are changes to the API that it provides. 15 | /// An example can be found in test/message_extraction/extract_to_json.dart 16 | /// 17 | /// Note that this does not understand how to follow part directives, so it 18 | /// has to explicitly be given all the files that it needs. A typical use case 19 | /// is to run it on all .dart files in a directory. 20 | library extract_messages; 21 | 22 | import 'dart:io'; 23 | 24 | import 'package:analyzer/dart/analysis/features.dart'; 25 | import 'package:analyzer/dart/analysis/utilities.dart'; 26 | import 'package:analyzer/dart/ast/ast.dart'; 27 | 28 | import 'src/messages/main_message.dart'; 29 | import 'visitors/message_finding_visitor.dart'; 30 | 31 | /// A function that takes a message and does something useful with it. 32 | typedef OnMessage = void Function(String message); 33 | 34 | final _featureSet = FeatureSet.latestLanguageVersion(); 35 | 36 | /// A particular message extraction run. 37 | /// 38 | /// This encapsulates all the state required for message extraction so that 39 | /// it can be run inside a persistent process. 40 | class MessageExtraction { 41 | MessageExtraction({ 42 | this.onMessage = print, 43 | this.suppressWarnings = false, 44 | this.allowEmbeddedPluralsAndGenders = true, 45 | this.examplesRequired = false, 46 | this.descriptionRequired = false, 47 | this.warningsAreErrors = false, 48 | }) : warnings = []; 49 | 50 | /// If this is true, then treat all warnings as errors. 51 | bool warningsAreErrors; 52 | 53 | /// What to do when a message is encountered, defaults to [print]. 54 | OnMessage onMessage; 55 | 56 | /// If this is true, print warnings for skipped messages. Otherwise, warnings 57 | /// are suppressed. 58 | bool suppressWarnings; 59 | 60 | /// This accumulates a list of all warnings/errors we have found. These are 61 | /// saved as strings right now, so all that can really be done is print and 62 | /// count them. 63 | final List warnings; 64 | 65 | /// Were there any warnings or errors in extracting messages. 66 | bool get hasWarnings => warnings.isNotEmpty; 67 | 68 | /// Are plural and gender expressions required to be at the top level 69 | /// of an expression, or are they allowed to be embedded in string literals. 70 | /// 71 | /// For example, the following expression 72 | /// 'There are ${Intl.plural(...)} items'. 73 | /// is legal if [allowEmbeddedPluralsAndGenders] is true, but illegal 74 | /// if [allowEmbeddedPluralsAndGenders] is false. 75 | bool allowEmbeddedPluralsAndGenders; 76 | 77 | /// Are examples required on all messages. 78 | bool examplesRequired; 79 | 80 | bool descriptionRequired; 81 | 82 | /// How messages with the same name are resolved. 83 | /// 84 | /// This function is allowed to mutate its arguments. 85 | MainMessage Function(MainMessage, MainMessage)? mergeMessages; 86 | 87 | /// Parse the source of the Dart program file [file] and return a Map from 88 | /// message names to [IntlMessage] instances. 89 | /// 90 | /// If [transformer] is true, assume the transformer will supply any "name" 91 | /// and "args" parameters required in Intl.message calls. 92 | Map parseFile(File file, [bool transformer = false]) { 93 | var contents = file.readAsStringSync(); 94 | return parseContent(contents, file.path, transformer); 95 | } 96 | 97 | /// Parse the source of the Dart program from a file with content 98 | /// [fileContent] and path [path] and return a Map from message 99 | /// names to [IntlMessage] instances. 100 | /// 101 | /// If [transformer] is true, assume the transformer will supply any "name" 102 | /// and "args" parameters required in Intl.message calls. 103 | Map parseContent( 104 | String fileContent, 105 | String filepath, 106 | bool transformer, 107 | ) { 108 | var contents = fileContent; 109 | origin = filepath; 110 | // Optimization to avoid parsing files we're sure don't contain any messages. 111 | if (contents.contains('Intl.')) { 112 | root = _parseCompilationUnit(contents, origin!); 113 | } else { 114 | return {}; 115 | } 116 | var visitor = MessageFindingVisitor( 117 | this, 118 | generateNameAndArgs: transformer, 119 | ); 120 | root.accept(visitor); 121 | return visitor.messages; 122 | } 123 | 124 | CompilationUnit _parseCompilationUnit(String contents, String origin) { 125 | var result = parseString( 126 | content: contents, 127 | featureSet: _featureSet, 128 | throwIfDiagnostics: false, 129 | ); 130 | 131 | if (result.errors.isNotEmpty) { 132 | print('Error in parsing $origin, no messages extracted.'); 133 | throw ArgumentError('Parsing errors in $origin'); 134 | } 135 | 136 | return result.unit; 137 | } 138 | 139 | /// The root of the compilation unit, and the first node we visit. We hold 140 | /// on to this for error reporting, as it can give us line numbers of other 141 | /// nodes. 142 | late CompilationUnit root; 143 | 144 | /// An arbitrary string describing where the source code came from. Most 145 | /// obviously, this could be a file path. We use this when reporting 146 | /// invalid messages. 147 | String? origin; 148 | 149 | String reportErrorLocation(AstNode node) { 150 | var result = StringBuffer(); 151 | if (origin != null) result.write(' from $origin'); 152 | var line = root.lineInfo.getLocation(node.offset); 153 | result.write(' line: ${line.lineNumber}, column: ${line.columnNumber}'); 154 | return result.toString(); 155 | } 156 | } 157 | 158 | /// If a message is a string literal without interpolation, compute 159 | /// a name based on that and the meaning, if present. 160 | // NOTE: THIS LOGIC IS DUPLICATED IN intl AND THE TWO MUST MATCH. 161 | String? computeMessageName(String name, String? text, String? meaning) { 162 | if (name != '') return name; 163 | return meaning == null ? text : '${text}_$meaning'; 164 | } 165 | -------------------------------------------------------------------------------- /lib/src/arb_generation.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'messages/main_message.dart'; 6 | import 'messages/message.dart'; 7 | import 'messages/submessages/submessage.dart'; 8 | 9 | /// This is a placeholder for transforming a parameter substitution from 10 | /// the translation file format into a Dart interpolation. In our case we 11 | /// store it to the file in Dart interpolation syntax, so the transformation 12 | /// is trivial. 13 | String leaveTheInterpolationsInDartForm(MainMessage msg, dynamic chunk) { 14 | if (chunk is String) { 15 | return chunk; 16 | } else if (chunk is int) { 17 | return '\$${msg.arguments[chunk]}'; 18 | } else if (chunk is Message) { 19 | return chunk.toCode(); 20 | } else { 21 | throw FormatException('Illegal interpolation: $chunk'); 22 | } 23 | } 24 | 25 | /// Convert the [MainMessage] to a trivial JSON format. 26 | Map toARB({ 27 | required MainMessage message, 28 | bool suppressMetadata = false, 29 | bool includeSourceText = false, 30 | }) { 31 | var out = {}; 32 | if (message.messagePieces.isEmpty) return out; 33 | 34 | // Return a version of the message string with ICU parameters 35 | // "{variable}" rather than Dart interpolations "$variable". 36 | out[message.name] = message 37 | .expanded((msg, chunk) => turnInterpolationIntoICUForm(msg, chunk)); 38 | 39 | if (!suppressMetadata) { 40 | var arbMetadataForMessage = arbMetadata(message); 41 | out['@${message.name}'] = arbMetadataForMessage; 42 | if (includeSourceText) { 43 | arbMetadataForMessage['source_text'] = out[message.name]; 44 | } 45 | } 46 | return out; 47 | } 48 | 49 | Map arbMetadata(MainMessage message) { 50 | var out = {}; 51 | var desc = message.description; 52 | if (desc != null) { 53 | out['description'] = desc; 54 | } 55 | out['type'] = 'text'; 56 | var placeholders = {}; 57 | for (var arg in message.arguments) { 58 | addArgumentFor(message, arg, placeholders); 59 | } 60 | out['placeholders'] = placeholders; 61 | return out; 62 | } 63 | 64 | void addArgumentFor( 65 | MainMessage message, 66 | String arg, 67 | Map result, 68 | ) { 69 | var extraInfo = {}; 70 | if (message.examples[arg] != null) { 71 | extraInfo['example'] = message.examples[arg]; 72 | } 73 | result[arg] = extraInfo; 74 | } 75 | 76 | String turnInterpolationIntoICUForm( 77 | Message message, 78 | dynamic chunk, { 79 | bool shouldEscapeICU = false, 80 | }) { 81 | if (chunk is String) { 82 | return shouldEscapeICU ? escape(chunk) : chunk; 83 | } else if (chunk is int && chunk >= 0 && chunk < message.arguments.length) { 84 | return '{${message.arguments[chunk]}}'; 85 | } else if (chunk is SubMessage) { 86 | return chunk.expanded((message, chunk) => turnInterpolationIntoICUForm( 87 | message, 88 | chunk, 89 | shouldEscapeICU: true, 90 | )); 91 | } else if (chunk is Message) { 92 | return chunk.expanded((message, chunk) => turnInterpolationIntoICUForm( 93 | message, 94 | chunk, 95 | shouldEscapeICU: shouldEscapeICU, 96 | )); 97 | } 98 | throw FormatException('Illegal interpolation: $chunk'); 99 | } 100 | 101 | String escape(String s) { 102 | return s.replaceAll("'", "''").replaceAll('{', "'{").replaceAll('}', "'}"); 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/directory_utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:io'; 6 | 7 | import 'package:path/path.dart' as path; 8 | 9 | /// Takes a file with a list of file paths, one per line, and returns the names 10 | /// as paths in terms of the directory containing [fileName]. 11 | Iterable linesFromFile(String? fileName) { 12 | if (fileName == null) { 13 | return []; 14 | } 15 | var file = File(fileName); 16 | return file 17 | .readAsLinesSync() 18 | .map((line) => line.trim()) 19 | .where((line) => line.isNotEmpty) 20 | .map((name) => _relativeToBase(fileName, name)); 21 | } 22 | 23 | /// If [filename] is relative, make it relative to the dirname of base. 24 | /// 25 | /// This is useful if we're running tests in a separate directory. 26 | String _relativeToBase(String base, String filename) { 27 | if (path.isRelative(filename)) { 28 | return path.join(path.dirname(base), filename); 29 | } else { 30 | return filename; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/message_rewriter.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Code to rewrite Intl.message calls adding the name and args parameters 6 | /// automatically, primarily used by the transformer. 7 | import 'package:analyzer/dart/analysis/utilities.dart'; 8 | 9 | import '../extract_messages.dart'; 10 | import '../visitors/message_finding_visitor.dart'; 11 | import 'messages/main_message.dart'; 12 | 13 | /// Rewrite all Intl.message/plural/etc. calls in [source], adding "name" 14 | /// and "args" parameters if they are not provided. 15 | /// 16 | /// Return the modified source code. If there are errors parsing, list 17 | /// [sourceName] in the error message. 18 | String rewriteMessages(String source, String sourceName, 19 | {bool useStringSubstitution = false}) { 20 | var messages = findMessages(source, sourceName); 21 | messages 22 | .sort((a, b) => a.sourcePosition?.compareTo(b.sourcePosition ?? 0) ?? 0); 23 | 24 | int? start = 0; 25 | var newSource = StringBuffer(); 26 | for (var message in messages) { 27 | if (message.arguments.isNotEmpty) { 28 | newSource.write(source.substring(start!, message.sourcePosition)); 29 | if (useStringSubstitution) { 30 | rewriteWithStringSubstitution(newSource, source, message); 31 | } else { 32 | rewriteRegenerating(newSource, message); 33 | } 34 | start = message.endPosition; 35 | } 36 | } 37 | newSource.write(source.substring(start!)); 38 | return newSource.toString(); 39 | } 40 | 41 | /// Rewrite the message by regenerating from our internal representation. 42 | /// 43 | /// This may produce uglier source, but is more reliable. 44 | void rewriteRegenerating(StringBuffer newSource, MainMessage message) { 45 | // TODO(alanknight): We could generate more efficient code than the 46 | // original here, dispatching more directly to the MessageLookup. 47 | newSource.write(message.toOriginalCode()); 48 | } 49 | 50 | void rewriteWithStringSubstitution( 51 | StringBuffer newSource, String source, MainMessage message) { 52 | var sourcePosition = message.sourcePosition; 53 | if (sourcePosition != null) { 54 | var originalSource = source.substring(sourcePosition, message.endPosition); 55 | var closingParen = originalSource.lastIndexOf(')'); 56 | // This is very ugly, checking to see if name/args is already there by 57 | // examining the source string. But at least the failure mode should 58 | // be very direct if we end up omitting name or args. 59 | var hasName = originalSource.contains(nameCheck); 60 | var hasArgs = originalSource.contains(argsCheck); 61 | var withName = hasName ? '' : ",\nname: '${message.name}'"; 62 | var withArgs = hasArgs ? '' : ',\nargs: ${message.arguments}'; 63 | var nameAndArgs = '$withName$withArgs)'; 64 | newSource.write(originalSource.substring(0, closingParen)); 65 | newSource.write(nameAndArgs); 66 | // We normally don't have anything after the closing paren, but 67 | // be safe. 68 | newSource.write(originalSource.substring(closingParen + 1)); 69 | } 70 | } 71 | 72 | final RegExp nameCheck = RegExp('[\\n,]\\s+name:'); 73 | final RegExp argsCheck = RegExp('[\\n,]\\s+args:'); 74 | 75 | /// Find all the messages in the [source] text. 76 | /// 77 | /// Report errors as coming from [sourceName] 78 | List findMessages(String source, String sourceName, 79 | [MessageExtraction? extraction]) { 80 | extraction = extraction ?? MessageExtraction(); 81 | try { 82 | var result = parseString(content: source); 83 | if (result.errors.isNotEmpty) { 84 | var errorsStr = result.errors.map((e) => e.message).join('\n'); 85 | throw ArgumentError('Parsing errors in $sourceName: $errorsStr'); 86 | } 87 | extraction.root = result.unit; 88 | } on ArgumentError catch (e) { 89 | extraction 90 | .onMessage('Error in parsing $sourceName, no messages extracted.'); 91 | extraction.onMessage(' $e'); 92 | return []; 93 | } 94 | extraction.origin = sourceName; 95 | var visitor = MessageFindingVisitor( 96 | extraction, 97 | generateNameAndArgs: true, 98 | ); 99 | extraction.root.accept(visitor); 100 | return visitor.messages.values.toList(); 101 | } 102 | -------------------------------------------------------------------------------- /lib/src/messages/complex_message.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'message.dart'; 6 | 7 | /// Abstract class for messages with internal structure, representing the 8 | /// main Intl.message call, plurals, and genders. 9 | abstract class ComplexMessage extends Message { 10 | ComplexMessage(parent) : super(parent); 11 | 12 | /// When we create these from strings or from AST nodes, we want to look up 13 | /// and set their attributes by string names, so we override the indexing 14 | /// operators so that they behave like maps with respect to those attribute 15 | /// names. 16 | dynamic operator [](String attributeName); 17 | 18 | /// When we create these from strings or from AST nodes, we want to look up 19 | /// and set their attributes by string names, so we override the indexing 20 | /// operators so that they behave like maps with respect to those attribute 21 | /// names. 22 | void operator []=(String attributeName, dynamic rawValue); 23 | 24 | List get attributeNames; 25 | 26 | /// Return the name of the message type, as it will be generated into an 27 | /// ICU-type format. e.g. choice, select 28 | String get icuMessageName; 29 | 30 | /// Return the message name we would use for this when doing Dart code 31 | /// generation, e.g. "Intl.plural". 32 | String get dartMessageName; 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/messages/composite_message.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'message.dart'; 6 | 7 | /// This represents a message chunk that is a list of multiple sub-pieces, 8 | /// each of which is in turn a [Message]. 9 | class CompositeMessage extends Message { 10 | List pieces; 11 | 12 | CompositeMessage.withParent(parent) 13 | : pieces = const [], 14 | super(parent); 15 | CompositeMessage(this.pieces, [super.parent]) { 16 | for (var x in pieces) { 17 | x.parent = this; 18 | } 19 | } 20 | @override 21 | String toCode() => pieces.map((each) => each.toCode()).join(''); 22 | @override 23 | List toJson() => pieces.map((each) => each.toJson()).toList(); 24 | @override 25 | String toString() => 'CompositeMessage($pieces)'; 26 | @override 27 | String expanded( 28 | [String Function(dynamic, dynamic) transform = nullTransform]) => 29 | pieces.map((chunk) => transform(this, chunk)).join(''); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/messages/literal_string_message.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'message.dart'; 6 | 7 | /// Represents a simple constant string with no dynamic elements. 8 | class LiteralString extends Message { 9 | String string; 10 | LiteralString(this.string, [Message? parent]) : super(parent); 11 | @override 12 | String toCode() => Message.escapeString(string); 13 | @override 14 | String toJson() => string; 15 | @override 16 | String toString() => 'Literal($string)'; 17 | @override 18 | String expanded( 19 | [String Function(dynamic, dynamic) transform = nullTransform]) => 20 | transform(this, string); 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/messages/main_message.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:analyzer/dart/ast/ast.dart'; 6 | import 'complex_message.dart'; 7 | import 'message.dart'; 8 | import 'message_extraction_exception.dart'; 9 | 10 | class MainMessage extends ComplexMessage { 11 | MainMessage({ 12 | this.sourcePosition, 13 | this.endPosition, 14 | required this.arguments, 15 | }) : examples = {}, 16 | super(null); 17 | 18 | /// All the pieces of the message. When we go to print, these will 19 | /// all be expanded appropriately. The exact form depends on what we're 20 | /// printing it for See [expanded], [toCode]. 21 | List messagePieces = []; 22 | 23 | /// The position in the source at which this message starts. 24 | int? sourcePosition; 25 | 26 | /// The position in the source at which this message ends. 27 | int? endPosition; 28 | 29 | /// Optional documentation of the member that wraps the message definition. 30 | List documentation = []; 31 | 32 | /// Verify that this looks like a correct Intl.message invocation. 33 | static void checkValidity( 34 | MethodInvocation node, 35 | List arguments, 36 | String? outerName, 37 | List outerArgs, { 38 | bool nameAndArgsGenerated = false, 39 | bool examplesRequired = false, 40 | }) { 41 | if (arguments.first is! StringLiteral) { 42 | throw MessageExtractionException( 43 | 'Intl.message messages must be string literals'); 44 | } 45 | 46 | Message.checkValidity( 47 | node, 48 | arguments, 49 | outerName, 50 | outerArgs, 51 | nameAndArgsGenerated: nameAndArgsGenerated, 52 | examplesRequired: examplesRequired, 53 | ); 54 | } 55 | 56 | void addPieces(List messages) { 57 | for (var each in messages) { 58 | messagePieces.add(Message.from(each, this)); 59 | } 60 | } 61 | 62 | void validateDescription() { 63 | if (description == null || description == '') { 64 | throw MessageExtractionException('Missing description for message $this'); 65 | } 66 | } 67 | 68 | /// The description provided in the Intl.message call. 69 | String? description; 70 | 71 | /// The examples from the Intl.message call 72 | @override 73 | Map examples; 74 | 75 | /// A field to disambiguate two messages that might have exactly the 76 | /// same text. The two messages will also need different names, but 77 | /// this can be used by machine translation tools to distinguish them. 78 | String? meaning; 79 | 80 | /// The name, which may come from the function name, from the arguments 81 | /// to Intl.message, or we may just re-use the message. 82 | String? _name; 83 | 84 | /// A placeholder for any other identifier that the translation format 85 | /// may want to use. 86 | String? id; 87 | 88 | /// The arguments list from the Intl.message call. 89 | @override 90 | List arguments; 91 | 92 | /// The locale argument from the Intl.message call 93 | String? locale; 94 | 95 | /// Whether extraction skip outputting this message. 96 | /// 97 | /// For example, this could be used to define messages whose purpose is known, 98 | /// but whose text isn't final yet and shouldn't be sent for translation. 99 | bool skip = false; 100 | 101 | /// When generating code, we store translations for each locale 102 | /// associated with the original message. 103 | Map translations = {}; 104 | Map jsonTranslations = {}; 105 | 106 | /// If the message was not given a name, we use the entire message string as 107 | /// the name. 108 | @override 109 | String get name => _name ?? ''; 110 | set name(String? newName) { 111 | _name = newName; 112 | } 113 | 114 | /// Does this message have an assigned name. 115 | bool get hasNoName => _name == null; 116 | 117 | /// Return the full message, with any interpolation expressions transformed 118 | /// by [f] and all the results concatenated. The chunk argument to [f] may be 119 | /// either a String, an int or an object representing a more complex 120 | /// message entity. 121 | /// See [messagePieces]. 122 | @override 123 | String expanded( 124 | [String Function(Message, dynamic) transform = nullTransform]) => 125 | messagePieces.map((chunk) => transform(this, chunk)).join(''); 126 | 127 | /// Record the translation for this message in the given locale, after 128 | /// suitably escaping it. 129 | void addTranslation(String locale, Message translated) { 130 | translated.parent = this; 131 | translations[locale] = translated.toCode(); 132 | jsonTranslations[locale] = translated.toJson(); 133 | } 134 | 135 | @override 136 | String toCode() => 137 | throw UnsupportedError('MainMessage.toCode requires a locale'); 138 | 139 | @override 140 | String toJson() => 141 | throw UnsupportedError('MainMessage.toJson requires a locale'); 142 | 143 | /// Generate code for this message, expecting it to be part of a map 144 | /// keyed by name with values the function that calls Intl.message. 145 | String toCodeForLocale(String locale, String name) { 146 | var out = StringBuffer() 147 | ..write('static $name(') 148 | ..write(arguments.join(', ')) 149 | ..write(') => "') 150 | ..write(translations[locale]) 151 | ..write('";'); 152 | return out.toString(); 153 | } 154 | 155 | /// Return a JSON string representation of this message. 156 | dynamic toJsonForLocale(String locale) { 157 | return jsonTranslations[locale]; 158 | } 159 | 160 | String turnInterpolationBackIntoStringForm(Message message, dynamic chunk) { 161 | if (chunk is String) { 162 | return Message.escapeString(chunk); 163 | } else if (chunk is int) { 164 | return r'${message.arguments[chunk]}'; 165 | } else if (chunk is Message) { 166 | return chunk.toCode(); 167 | } else { 168 | throw ArgumentError.value(chunk, 'Unexpected value in Intl.message'); 169 | } 170 | } 171 | 172 | /// Create a string that will recreate this message, optionally 173 | /// including the compile-time only information desc and examples. 174 | String toOriginalCode( 175 | {bool includeDesc = true, bool includeExamples = true}) { 176 | var out = StringBuffer()..write("Intl.message('"); 177 | out.write(expanded(turnInterpolationBackIntoStringForm)); 178 | out.write("', "); 179 | out.write("name: '$name', "); 180 | out.write(locale == null ? '' : "locale: '$locale', "); 181 | if (includeDesc) { 182 | out.write(description == null 183 | ? '' 184 | : "desc: '${Message.escapeString(description!)}', "); 185 | } 186 | if (includeExamples) { 187 | // json is already mostly-escaped, but we need to handle interpolations. 188 | var json = jsonEncoder.encode(examples).replaceAll(r'$', r'\$'); 189 | out.write(examples.isEmpty ? '' : 'examples: const $json, '); 190 | } 191 | out.write(meaning == null 192 | ? '' 193 | : "meaning: '${Message.escapeString(meaning!)}', "); 194 | out.write("args: [${arguments.join(', ')}]"); 195 | out.write(')'); 196 | return out.toString(); 197 | } 198 | 199 | /// The AST node will have the attribute names as strings, so we translate 200 | /// between those and the fields of the class. 201 | @override 202 | void operator []=(String attributeName, dynamic value) { 203 | switch (attributeName) { 204 | case 'desc': 205 | description = value; 206 | return; 207 | case 'examples': 208 | examples = value as Map; 209 | return; 210 | case 'name': 211 | name = value; 212 | return; 213 | // We use the actual args from the parser rather than what's given in the 214 | // arguments to Intl.message. 215 | case 'args': 216 | return; 217 | case 'meaning': 218 | meaning = value; 219 | return; 220 | case 'locale': 221 | locale = value; 222 | return; 223 | case 'skip': 224 | skip = value as bool; 225 | return; 226 | default: 227 | return; 228 | } 229 | } 230 | 231 | /// The AST node will have the attribute names as strings, so we translate 232 | /// between those and the fields of the class. 233 | @override 234 | dynamic operator [](String attributeName) { 235 | switch (attributeName) { 236 | case 'desc': 237 | return description; 238 | case 'examples': 239 | return examples; 240 | case 'name': 241 | return name; 242 | // We use the actual args from the parser rather than what's given in the 243 | // arguments to Intl.message. 244 | case 'args': 245 | return []; 246 | case 'meaning': 247 | return meaning; 248 | case 'skip': 249 | return skip; 250 | default: 251 | return null; 252 | } 253 | } 254 | 255 | // This is the top-level construct, so there's no meaningful ICU name. 256 | @override 257 | String get icuMessageName => ''; 258 | 259 | @override 260 | String get dartMessageName => 'message'; 261 | 262 | /// The parameters that the Intl.message call may provide. 263 | @override 264 | List get attributeNames => 265 | const ['name', 'desc', 'examples', 'args', 'meaning', 'skip']; 266 | 267 | @override 268 | String toString() => 269 | 'Intl.message(${expanded()}, $name, $description, $examples, $arguments)'; 270 | } 271 | -------------------------------------------------------------------------------- /lib/src/messages/message_extraction_exception.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Exception thrown when we cannot process a message properly. 6 | class MessageExtractionException implements Exception { 7 | /// A message describing the error. 8 | final String message; 9 | 10 | /// Creates a new exception with an optional error [message]. 11 | const MessageExtractionException([this.message = '']); 12 | 13 | @override 14 | String toString() => 'MessageExtractionException: $message'; 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/messages/pair_message.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'message.dart'; 6 | 7 | class PairMessage extends Message { 8 | final T first; 9 | final S second; 10 | 11 | PairMessage(this.first, this.second, [Message? parent]) : super(parent); 12 | 13 | @override 14 | String expanded( 15 | [String Function(dynamic, dynamic) transform = nullTransform]) => 16 | [first, second].map((chunk) => transform(this, chunk)).join(''); 17 | 18 | @override 19 | String toCode() => [first, second].map((each) => each.toCode()).join(''); 20 | 21 | @override 22 | Object toJson() => [first, second].map((each) => each.toJson()).toList(); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/messages/submessages/gender.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import '../message.dart'; 6 | import 'submessage.dart'; 7 | 8 | /// Represents a message send of [Intl.gender] inside a message that is to 9 | /// be internationalized. This corresponds to an ICU message syntax "select" 10 | /// with "male", "female", and "other" as the possible options. 11 | class Gender extends SubMessage { 12 | Gender() : super.from('', [], null); 13 | 14 | /// Create a new Gender providing [mainArgument] and the list of possible 15 | /// clauses. Each clause is expected to be a list whose first element is a 16 | /// variable name and whose second element is either a [String] or 17 | /// a list of strings and [Message] or [VariableSubstitution]. 18 | Gender.from(String mainArgument, List clauses, [Message? parent]) 19 | : super.from(mainArgument, clauses, parent); 20 | 21 | Message? female; 22 | Message? male; 23 | Message? other; 24 | 25 | @override 26 | String get icuMessageName => 'select'; 27 | @override 28 | String get dartMessageName => 'Intl.gender'; 29 | 30 | @override 31 | List get attributeNames => ['female', 'male', 'other']; 32 | @override 33 | List get codeAttributeNames => attributeNames; 34 | 35 | /// The node will have the attribute names as strings, so we translate 36 | /// between those and the fields of the class. 37 | @override 38 | void operator []=(String attributeName, dynamic rawValue) { 39 | var value = Message.from(rawValue, this); 40 | switch (attributeName) { 41 | case 'female': 42 | female = value; 43 | return; 44 | case 'male': 45 | male = value; 46 | return; 47 | case 'other': 48 | other = value; 49 | return; 50 | default: 51 | return; 52 | } 53 | } 54 | 55 | @override 56 | Message? operator [](String attributeName) { 57 | switch (attributeName) { 58 | case 'female': 59 | return female; 60 | case 'male': 61 | return male; 62 | case 'other': 63 | return other; 64 | default: 65 | return other; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/messages/submessages/plural.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import '../message.dart'; 6 | import 'submessage.dart'; 7 | 8 | class Plural extends SubMessage { 9 | Plural() : super.from('', [], null); 10 | Plural.from(String mainArgument, List clauses, [Message? parent]) 11 | : super.from(mainArgument, clauses, parent); 12 | 13 | Message? zero; 14 | Message? one; 15 | Message? two; 16 | Message? few; 17 | Message? many; 18 | Message? other; 19 | 20 | @override 21 | String get icuMessageName => 'plural'; 22 | @override 23 | String get dartMessageName => 'Intl.plural'; 24 | 25 | @override 26 | List get attributeNames => ['=0', '=1', '=2', 'few', 'many', 'other']; 27 | @override 28 | List get codeAttributeNames => 29 | ['zero', 'one', 'two', 'few', 'many', 'other']; 30 | 31 | /// The node will have the attribute names as strings, so we translate 32 | /// between those and the fields of the class. 33 | @override 34 | void operator []=(String attributeName, dynamic rawValue) { 35 | var value = Message.from(rawValue, this); 36 | switch (attributeName) { 37 | case 'zero': 38 | // We prefer an explicit "=0" clause to a "ZERO" 39 | // if both are present. 40 | zero ??= value; 41 | return; 42 | case '=0': 43 | zero = value; 44 | return; 45 | case 'one': 46 | // We prefer an explicit "=1" clause to a "ONE" 47 | // if both are present. 48 | one ??= value; 49 | return; 50 | case '=1': 51 | one = value; 52 | return; 53 | case 'two': 54 | // We prefer an explicit "=2" clause to a "TWO" 55 | // if both are present. 56 | two ??= value; 57 | return; 58 | case '=2': 59 | two = value; 60 | return; 61 | case 'few': 62 | few = value; 63 | return; 64 | case 'many': 65 | many = value; 66 | return; 67 | case 'other': 68 | other = value; 69 | return; 70 | default: 71 | return; 72 | } 73 | } 74 | 75 | @override 76 | Message? operator [](String attributeName) { 77 | switch (attributeName) { 78 | case 'zero': 79 | return zero; 80 | case '=0': 81 | return zero; 82 | case 'one': 83 | return one; 84 | case '=1': 85 | return one; 86 | case 'two': 87 | return two; 88 | case '=2': 89 | return two; 90 | case 'few': 91 | return few; 92 | case 'many': 93 | return many; 94 | case 'other': 95 | return other; 96 | default: 97 | return other; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/src/messages/submessages/select.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:analyzer/dart/ast/ast.dart'; 6 | import '../message.dart'; 7 | import '../message_extraction_exception.dart'; 8 | import 'submessage.dart'; 9 | 10 | /// Represents a message send of [Intl.select] inside a message that is to 11 | /// be internationalized. This corresponds to an ICU message syntax "select" 12 | /// with arbitrary options. 13 | class Select extends SubMessage { 14 | Select() : super.from('', [], null); 15 | 16 | /// Create a new [Select] providing [mainArgument] and the list of possible 17 | /// clauses. Each clause is expected to be a list whose first element is a 18 | /// variable name and whose second element is either a String or 19 | /// a list of strings and [Message]s or [VariableSubstitution]s. 20 | Select.from(String mainArgument, List clauses, [Message? parent]) 21 | : super.from(mainArgument, clauses, parent); 22 | 23 | Map cases = {}; 24 | 25 | @override 26 | String get icuMessageName => 'select'; 27 | @override 28 | String get dartMessageName => 'Intl.select'; 29 | 30 | @override 31 | List get attributeNames => cases.keys.toList(); 32 | @override 33 | List get codeAttributeNames => attributeNames; 34 | 35 | // Check for valid select keys. 36 | // See http://site.icu-project.org/design/formatting/select 37 | static const selectPattern = '[a-zA-Z][a-zA-Z0-9_-]*'; 38 | static final validSelectKey = RegExp(selectPattern); 39 | 40 | @override 41 | void operator []=(String attributeName, dynamic rawValue) { 42 | var value = Message.from(rawValue, this); 43 | if (validSelectKey.stringMatch(attributeName) == attributeName) { 44 | cases[attributeName] = value; 45 | } else { 46 | throw MessageExtractionException( 47 | "Invalid select keyword: '$attributeName', must " 48 | "match '$selectPattern'"); 49 | } 50 | } 51 | 52 | @override 53 | Message? operator [](String attributeName) { 54 | var exact = cases[attributeName]; 55 | return exact ?? cases['other']; 56 | } 57 | 58 | /// Return the arguments that we care about for the select. In this 59 | /// case they will all be passed in as a Map rather than as the named 60 | /// arguments used in Plural/Gender. 61 | static Map argumentsOfInterestFor(MethodInvocation node) { 62 | var casesArgument = node.argumentList.arguments[1] as SetOrMapLiteral; 63 | // ignore: prefer_for_elements_to_map_fromiterable 64 | return Map.fromIterable( 65 | casesArgument.elements, 66 | key: (element) => _keyForm(element.key), 67 | value: (element) => element.value, 68 | ); 69 | } 70 | 71 | // The key might already be a simple string, or it might be 72 | // something else, in which case we convert it to a string 73 | // and take the portion after the period, if present. 74 | // This is to handle enums as select keys. 75 | static String _keyForm(key) { 76 | return (key is SimpleStringLiteral) ? key.value : '$key'.split('.').last; 77 | } 78 | 79 | @override 80 | void validate() { 81 | if (this['other'] == null) { 82 | throw MessageExtractionException( 83 | 'Missing keyword other for Intl.select $this'); 84 | } 85 | } 86 | 87 | /// Write out the generated representation of this message. This differs 88 | /// from Plural/Gender in that it prints a literal map rather than 89 | /// named arguments. 90 | @override 91 | String toCode() { 92 | var out = StringBuffer(); 93 | out.write('\${'); 94 | out.write(dartMessageName); 95 | out.write('('); 96 | out.write(mainArgument); 97 | var args = codeAttributeNames; 98 | out.write(', {'); 99 | args.fold(out, 100 | (buffer, arg) => buffer..write("'$arg': '${this[arg]!.toCode()}', ")); 101 | out.write('})}'); 102 | return out.toString(); 103 | } 104 | 105 | /// We represent this in JSON as a List with the name of the message 106 | /// (e.g. Intl.select), the index in the arguments list of the main argument, 107 | /// and then a Map from the cases to the List of strings or sub-messages. 108 | @override 109 | List toJson() { 110 | var json = []; 111 | json.add(dartMessageName); 112 | json.add(arguments.indexOf(mainArgument)); 113 | var attributes = {}; 114 | for (var arg in codeAttributeNames) { 115 | attributes[arg] = this[arg]!.toJson(); 116 | } 117 | json.add(attributes); 118 | return json; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/src/messages/submessages/submessage.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:analyzer/dart/ast/ast.dart'; 6 | import '../complex_message.dart'; 7 | import '../composite_message.dart'; 8 | import '../literal_string_message.dart'; 9 | import '../message.dart'; 10 | import '../pair_message.dart'; 11 | 12 | /// An abstract class to represent sub-sections of a message, primarily 13 | /// plurals and genders. 14 | abstract class SubMessage extends ComplexMessage { 15 | /// Creates the sub-message, given a list of [clauses] in the sort of form 16 | /// that we're likely to get them from parsing a translation file format, 17 | /// as a list of [key, value] where value may in turn be a list. 18 | SubMessage.from(this.mainArgument, List clauses, super.parent) { 19 | for (var clause in clauses) { 20 | String key; 21 | Object? value; 22 | if (clause is List && clause[0] is String && clause.length == 2) { 23 | //If trying to parse a string 24 | key = clause[0]; 25 | value = (clause[1] is List) ? clause[1] : [(clause[1])]; 26 | } else if (clause is PairMessage) { 27 | //If trying to parse a message 28 | key = clause.first.string; 29 | var second = clause.second; 30 | value = second is CompositeMessage ? second.pieces : [second]; 31 | } else { 32 | throw Exception( 33 | 'The clauses argument supplied must be a list of pairs, i.e. list of lists of length 2 or PairMessages.'); 34 | } 35 | this[key] = value; 36 | } 37 | } 38 | 39 | @override 40 | String toString() => expanded(); 41 | 42 | /// The name of the main argument, which is expected to have the value which 43 | /// is one of [attributeNames] and is used to decide which clause to use. 44 | String mainArgument; 45 | 46 | /// Return the arguments that affect this SubMessage as a map of 47 | /// argument names and values. 48 | static Map argumentsOfInterestFor(MethodInvocation node) { 49 | return { 50 | for (var node in node.argumentList.arguments.whereType()) 51 | node.name.label.token.value() as String: node.expression, 52 | }; 53 | } 54 | 55 | /// Return the list of attribute names to use when generating code. This 56 | /// may be different from [attributeNames] if there are multiple aliases 57 | /// that map to the same clause. 58 | List get codeAttributeNames; 59 | 60 | @override 61 | String expanded( 62 | [String Function(dynamic, dynamic) transform = nullTransform]) { 63 | String fullMessageForClause(String key) => 64 | '$key{${transform(parent, this[key])}}'; 65 | var clauses = attributeNames 66 | .where((key) => this[key] != null) 67 | .map(fullMessageForClause) 68 | .toList(); 69 | return "{$mainArgument,$icuMessageName, ${clauses.join("")}}"; 70 | } 71 | 72 | @override 73 | String toCode() { 74 | var out = StringBuffer(); 75 | out.write('\${'); 76 | out.write(dartMessageName); 77 | out.write('('); 78 | out.write(mainArgument); 79 | var args = codeAttributeNames.where((attribute) => this[attribute] != null); 80 | args.fold( 81 | out, 82 | (buffer, arg) => 83 | buffer..write(", $arg: '${(this[arg] as Message).toCode()}'")); 84 | out.write(')}'); 85 | return out.toString(); 86 | } 87 | 88 | /// We represent this in JSON as a list with [dartMessageName], the index in 89 | /// the arguments list at which we will find the main argument (e.g. howMany 90 | /// for a plural), and then the values of all the possible arguments, in the 91 | /// order that they appear in codeAttributeNames. Any missing arguments are 92 | /// saved as an explicit null. 93 | @override 94 | List toJson() { 95 | var json = []; 96 | json.add(dartMessageName); 97 | json.add(arguments.indexOf(mainArgument)); 98 | for (var arg in codeAttributeNames) { 99 | json.add(this[arg]?.toJson()); 100 | } 101 | return json; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/messages/variable_substitution_message.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'message.dart'; 6 | 7 | /// Represents an interpolation of a variable value in a message. We expect 8 | /// this to be specified as an [index] into the list of variables, or else 9 | /// as the name of a variable that exists in [arguments] and we will 10 | /// compute the variable name or the index based on the value of the other. 11 | class VariableSubstitution extends Message { 12 | VariableSubstitution(this._index, [Message? parent]) : super(parent); 13 | 14 | /// Create a substitution based on the name rather than the index. The name 15 | /// may have been used as all upper-case in the translation tool, so we 16 | /// save it separately and look it up case-insensitively once the parent 17 | /// (and its arguments) are definitely available. 18 | VariableSubstitution.named(String name, [Message? parent]) : super(parent) { 19 | _variableName = name; 20 | _variableNameUpper = name.toUpperCase(); 21 | } 22 | 23 | /// The index in the list of parameters of the containing function. 24 | int? _index; 25 | int? get index { 26 | if (_index != null) return _index; 27 | if (arguments.isEmpty) return null; 28 | // We may have been given an all-uppercase version of the name, so compare 29 | // case-insensitive. 30 | _index = arguments 31 | .map((x) => x.toUpperCase()) 32 | .toList() 33 | .indexOf(_variableNameUpper!); 34 | if (_index == -1) { 35 | throw ArgumentError( 36 | "Cannot find parameter named '$_variableNameUpper' in " 37 | "message named '$name'. Available " 38 | 'parameters are $arguments'); 39 | } 40 | return _index; 41 | } 42 | 43 | /// The variable name we get from parsing. This may be an all uppercase 44 | /// version of the Dart argument name. 45 | String? _variableNameUpper; 46 | 47 | /// The name of the variable in the parameter list of the containing function. 48 | /// Used when generating code for the interpolation. 49 | String? get variableName { 50 | return index != null ? arguments[index!] : _variableName; 51 | } 52 | 53 | String? _variableName; 54 | // Although we only allow simple variable references, we always enclose them 55 | // in curly braces so that there's no possibility of ambiguity with 56 | // surrounding text. 57 | @override 58 | String toCode() => '\${$variableName}'; 59 | @override 60 | int? toJson() => index; 61 | @override 62 | String toString() => 'VariableSubstitution(${index ?? _variableName})'; 63 | @override 64 | String expanded( 65 | [String Function(dynamic, dynamic) transform = nullTransform]) => 66 | transform(this, index); 67 | } 68 | -------------------------------------------------------------------------------- /lib/visitors/interpolation_visitor.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:analyzer/dart/ast/ast.dart'; 6 | import 'package:analyzer/dart/ast/visitor.dart'; 7 | 8 | import '../extract_messages.dart'; 9 | import '../src/messages/complex_message.dart'; 10 | import '../src/messages/message.dart'; 11 | import '../src/messages/message_extraction_exception.dart'; 12 | import 'plural_gender_visitor.dart'; 13 | 14 | /// Given an interpolation, find all of its chunks, validate that they are only 15 | /// simple variable substitutions or else Intl.plural/gender calls, 16 | /// and keep track of the pieces of text so that other parts 17 | /// of the program can deal with the simple string sections and the generated 18 | /// parts separately. Note that this is a SimpleAstVisitor, so it only 19 | /// traverses one level of children rather than automatically recursing. If we 20 | /// find a plural or gender, which requires recursion, we do it with a separate 21 | /// special-purpose visitor. 22 | class InterpolationVisitor extends SimpleAstVisitor { 23 | final Message message; 24 | 25 | /// The message extraction in which we are running. 26 | final MessageExtraction extraction; 27 | 28 | InterpolationVisitor(this.message, this.extraction); 29 | 30 | final List pieces = []; 31 | String get extractedMessage => pieces.join(); 32 | 33 | @override 34 | void visitAdjacentStrings(AdjacentStrings node) { 35 | node.visitChildren(this); 36 | } 37 | 38 | @override 39 | void visitStringInterpolation(StringInterpolation node) { 40 | node.visitChildren(this); 41 | } 42 | 43 | @override 44 | void visitSimpleStringLiteral(SimpleStringLiteral node) { 45 | pieces.add(node.value); 46 | } 47 | 48 | @override 49 | void visitInterpolationString(InterpolationString node) { 50 | pieces.add(node.value); 51 | } 52 | 53 | @override 54 | void visitInterpolationExpression(InterpolationExpression node) { 55 | if (node.expression is SimpleIdentifier) { 56 | handleSimpleInterpolation(node); 57 | } else { 58 | lookForPluralOrGender(node); 59 | } 60 | } 61 | 62 | void lookForPluralOrGender(InterpolationExpression node) { 63 | var visitor = PluralAndGenderVisitor( 64 | pieces, 65 | message as ComplexMessage, 66 | extraction, 67 | ); 68 | node.accept(visitor); 69 | if (!visitor.foundPluralOrGender) { 70 | throw MessageExtractionException( 71 | 'Only simple identifiers and Intl.plural/gender/select expressions ' 72 | 'are allowed in message ' 73 | 'interpolation expressions.\nError at $node'); 74 | } 75 | } 76 | 77 | void handleSimpleInterpolation(InterpolationExpression node) { 78 | var index = arguments.indexOf(node.expression.toString()); 79 | if (index == -1) { 80 | throw MessageExtractionException( 81 | 'Cannot find argument ${node.expression}'); 82 | } 83 | pieces.add(index); 84 | } 85 | 86 | List get arguments => message.arguments; 87 | } 88 | -------------------------------------------------------------------------------- /lib/visitors/plural_gender_visitor.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:analyzer/dart/ast/ast.dart'; 6 | import 'package:analyzer/dart/ast/visitor.dart'; 7 | 8 | import '../extract_messages.dart'; 9 | import '../src/messages/complex_message.dart'; 10 | import '../src/messages/message.dart'; 11 | import '../src/messages/message_extraction_exception.dart'; 12 | import '../src/messages/submessages/gender.dart'; 13 | import '../src/messages/submessages/plural.dart'; 14 | import '../src/messages/submessages/select.dart'; 15 | import '../src/messages/submessages/submessage.dart'; 16 | import 'interpolation_visitor.dart'; 17 | 18 | /// A visitor to extract information from Intl.plural/gender sends. Note that 19 | /// this is a SimpleAstVisitor, so it doesn't automatically recurse. So this 20 | /// needs to be called where we expect a plural or gender immediately below. 21 | class PluralAndGenderVisitor extends SimpleAstVisitor { 22 | /// The message extraction in which we are running. 23 | final MessageExtraction extraction; 24 | 25 | /// A plural or gender always exists in the context of a parent message, 26 | /// which could in turn also be a plural or gender. 27 | final ComplexMessage parent; 28 | 29 | /// The pieces of the message. We are given an initial version of this 30 | /// from our parent and we add to it as we find additional information. 31 | List pieces; 32 | 33 | /// This will be set to true if we find a plural or gender. 34 | bool foundPluralOrGender = false; 35 | 36 | PluralAndGenderVisitor(this.pieces, this.parent, this.extraction) : super(); 37 | 38 | @override 39 | void visitInterpolationExpression(InterpolationExpression node) { 40 | // TODO(alanknight): Provide better errors for malformed expressions. 41 | if (!looksLikePluralOrGender(node.expression)) return; 42 | var nodeMethod = node.expression as MethodInvocation; 43 | var reason = checkValidity(nodeMethod); 44 | if (reason != null) { 45 | throw reason; 46 | } 47 | var message = messageFromMethodInvocation(nodeMethod); 48 | foundPluralOrGender = true; 49 | pieces.add(message); 50 | } 51 | 52 | @override 53 | void visitMethodInvocation(MethodInvocation node) { 54 | pieces.add(messageFromMethodInvocation(node)); 55 | } 56 | 57 | /// Return true if [node] matches the pattern for plural or gender message. 58 | bool looksLikePluralOrGender(Expression expression) { 59 | if (expression is! MethodInvocation) return false; 60 | final node = expression; 61 | if (!['plural', 'gender', 'select'].contains(node.methodName.name)) { 62 | return false; 63 | } 64 | if (node.target is! SimpleIdentifier) return false; 65 | var target = node.target as SimpleIdentifier; 66 | return target.token.toString() == 'Intl'; 67 | } 68 | 69 | /// Returns a String describing why the node is invalid, or null if no 70 | /// reason is found, so it's presumed valid. 71 | String? checkValidity(MethodInvocation node) { 72 | // TODO(alanknight): Add reasonable validity checks. 73 | return null; 74 | } 75 | 76 | /// Create a MainMessage from [node] using the name and 77 | /// parameters of the last function/method declaration we encountered 78 | /// and the parameters to the Intl.message call. 79 | Message? messageFromMethodInvocation(MethodInvocation node) { 80 | SubMessage message; 81 | Map arguments; 82 | switch (node.methodName.name) { 83 | case 'gender': 84 | message = Gender(); 85 | arguments = SubMessage.argumentsOfInterestFor(node); 86 | break; 87 | case 'plural': 88 | message = Plural(); 89 | arguments = SubMessage.argumentsOfInterestFor(node); 90 | break; 91 | case 'select': 92 | message = Select(); 93 | arguments = Select.argumentsOfInterestFor(node); 94 | break; 95 | default: 96 | throw MessageExtractionException( 97 | 'Invalid plural/gender/select message ${node.methodName.name} ' 98 | 'in $node'); 99 | } 100 | message.parent = parent; 101 | 102 | var buildNotJustCollectErrors = true; 103 | arguments.forEach((key, Expression value) { 104 | try { 105 | var interpolation = InterpolationVisitor( 106 | message, 107 | extraction, 108 | ); 109 | value.accept(interpolation); 110 | if (buildNotJustCollectErrors) { 111 | message[key] = interpolation.pieces; 112 | } 113 | } on MessageExtractionException catch (e) { 114 | buildNotJustCollectErrors = false; 115 | var errString = (StringBuffer() 116 | ..writeAll(['Error ', e, '\nProcessing <', node, '>']) 117 | ..write(extraction.reportErrorLocation(node))) 118 | .toString(); 119 | extraction.onMessage(errString); 120 | extraction.warnings.add(errString); 121 | } 122 | }); 123 | var mainArg = node.argumentList.arguments 124 | .firstWhere((each) => each is! NamedExpression); 125 | if (mainArg is SimpleStringLiteral) { 126 | message.mainArgument = mainArg.toString(); 127 | } else if (mainArg is SimpleIdentifier) { 128 | message.mainArgument = mainArg.name; 129 | } else { 130 | var errString = (StringBuffer() 131 | ..write('Error (Invalid argument to plural/gender/select, ' 132 | 'must be simple variable reference) ' 133 | '\nProcessing <$node>') 134 | ..write(extraction.reportErrorLocation(node))) 135 | .toString(); 136 | extraction.onMessage(errString); 137 | extraction.warnings.add(errString); 138 | } 139 | 140 | return message; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: intl_translation 2 | version: 0.18.1 3 | description: >- 4 | Contains code to deal with internationalized/localized messages, 5 | date and number formatting and parsing, bi-directional text, and 6 | other internationalization issues. 7 | repository: https://github.com/dart-lang/intl_translation 8 | 9 | environment: 10 | sdk: '>=2.18.0 <3.0.0' 11 | 12 | dependencies: 13 | analyzer: ^5.2.0 14 | args: ^2.0.0 15 | dart_style: ^2.0.0 16 | intl: ^0.18.0 17 | path: ^1.0.0 18 | 19 | dev_dependencies: 20 | lints: ^2.0.0 21 | test: ^1.0.0 22 | -------------------------------------------------------------------------------- /test/data_directory.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// A utility function for test and tools that compensates (at least for very 6 | /// simple cases) for file-dependent programs being run from different 7 | /// directories. The important cases are 8 | /// - running in the directory that contains the test itself, i.e. 9 | /// test/ or a sub-directory. 10 | /// - running in root of this package, which is where the editor and bots will 11 | /// run things by default 12 | library data_directory; 13 | 14 | import 'dart:io'; 15 | 16 | import 'package:path/path.dart' as path; 17 | 18 | /// Returns whether [dir] is the root of the `intl` package. We validate that it 19 | /// is by looking for a pubspec file with the entry `name: intl`. 20 | bool _isIntlRoot(String dir) { 21 | var file = File(path.join(dir, 'pubspec.yaml')); 22 | if (!file.existsSync()) return false; 23 | return file.readAsStringSync().contains('name: intl_translation\n'); 24 | } 25 | 26 | String get packageDirectory { 27 | // Try the current directory. 28 | if (_isIntlRoot(Directory.current.path)) return Directory.current.path; 29 | 30 | // Search upwards from the script location. 31 | var dir = path.fromUri(Platform.script); 32 | var root = path.rootPrefix(dir); 33 | 34 | while (dir != root) { 35 | if (_isIntlRoot(dir)) return dir; 36 | dir = path.dirname(dir); 37 | } 38 | throw UnsupportedError( 39 | 'Cannot find the root directory of the `intl_translation` package.'); 40 | } 41 | -------------------------------------------------------------------------------- /test/generate_localized/README.txt: -------------------------------------------------------------------------------- 1 | A test for messages generated with code-map. 2 | 3 | The translation ARB files are hard-coded, and the generated files are 4 | checked in, so there's minimum infrastructure required, but if the 5 | files need to be regenerated, then the regenerate.sh script will need 6 | to be run. 7 | -------------------------------------------------------------------------------- /test/generate_localized/app_translation_getfromthelocale.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "fr", 3 | "Hello from application": "Bonjour de l'application", 4 | "@Hello from application": null 5 | } 6 | -------------------------------------------------------------------------------- /test/generate_localized/code_map_messages_all.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | import 'package:intl/intl.dart'; 6 | export 'code_map_messages_all_locales.dart' show initializeMessages; 7 | 8 | /// Turn the JSON template into a string. 9 | /// 10 | /// We expect one of the following forms for the template. 11 | /// * null -> null 12 | /// * String s -> s 13 | /// * int n -> '${args[n]}' 14 | /// * List list, one of 15 | /// * ['Intl.plural', int howMany, (templates for zero, one, ...)] 16 | /// * ['Intl.gender', String gender, (templates for female, male, other)] 17 | /// * ['Intl.select', String choice, { 'case' : template, ...} ] 18 | /// * ['text alternating with ', 0 , ' indexes in the argument list'] 19 | String? evaluateJsonTemplate(dynamic input, List args) { 20 | if (input == null) return null; 21 | if (input is String) return input; 22 | if (input is int) { 23 | return '${args[input]}'; 24 | } 25 | 26 | var template = input as List; 27 | var messageName = template.first; 28 | if (messageName == 'Intl.plural') { 29 | var howMany = args[template[1] as int] as num; 30 | return evaluateJsonTemplate( 31 | Intl.pluralLogic(howMany, 32 | zero: template[2], 33 | one: template[3], 34 | two: template[4], 35 | few: template[5], 36 | many: template[6], 37 | other: template[7]), 38 | args); 39 | } 40 | if (messageName == 'Intl.gender') { 41 | var gender = args[template[1] as int] as String; 42 | return evaluateJsonTemplate( 43 | Intl.genderLogic(gender, 44 | female: template[2], male: template[3], other: template[4]), 45 | args); 46 | } 47 | if (messageName == 'Intl.select') { 48 | var select = args[template[1] as int] as Object; 49 | var choices = template[2] as Map; 50 | return evaluateJsonTemplate(Intl.selectLogic(select, choices), args); 51 | } 52 | 53 | // If we get this far, then we are a basic interpolation, just strings and 54 | // ints. 55 | var output = StringBuffer(); 56 | for (var entry in template) { 57 | if (entry is int) { 58 | output.write('${args[entry]}'); 59 | } else { 60 | output.write('$entry'); 61 | } 62 | } 63 | return output.toString(); 64 | } 65 | -------------------------------------------------------------------------------- /test/generate_localized/code_map_messages_all_locales.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | // Ignore issues from commonly used lints in this file. 6 | // ignore_for_file:implementation_imports, file_names 7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering 8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment 9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases 10 | // ignore_for_file:comment_references 11 | 12 | import 'package:intl/intl.dart'; 13 | import 'package:intl/message_lookup_by_library.dart'; 14 | import 'package:intl/src/intl_helpers.dart'; 15 | 16 | import 'code_map_messages_fr.dart' deferred as messages_fr; 17 | 18 | typedef Future LibraryLoader(); 19 | Map _deferredLibraries = { 20 | 'fr': messages_fr.loadLibrary, 21 | }; 22 | 23 | MessageLookupByLibrary? _findExact(String localeName) { 24 | switch (localeName) { 25 | case 'fr': 26 | return messages_fr.messages; 27 | default: 28 | return null; 29 | } 30 | } 31 | 32 | /// User programs should call this before using [localeName] for messages. 33 | Future initializeMessages(String? localeName) async { 34 | var availableLocale = Intl.verifiedLocale( 35 | localeName, (locale) => _deferredLibraries[locale] != null, 36 | onFailure: (_) => null); 37 | if (availableLocale == null) { 38 | return Future.value(false); 39 | } 40 | var lib = _deferredLibraries[availableLocale]; 41 | await (lib == null ? Future.value(false) : lib()); 42 | initializeInternalMessageLookup(() => CompositeMessageLookup()); 43 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); 44 | return Future.value(true); 45 | } 46 | 47 | bool _messagesExistFor(String locale) { 48 | try { 49 | return _findExact(locale) != null; 50 | } catch (e) { 51 | return false; 52 | } 53 | } 54 | 55 | MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { 56 | var actualLocale = 57 | Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); 58 | if (actualLocale == null) return null; 59 | return _findExact(actualLocale); 60 | } 61 | -------------------------------------------------------------------------------- /test/generate_localized/code_map_messages_fr.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that provides messages for a fr locale. All the 3 | // messages from the main program should be duplicated here with the same 4 | // function name. 5 | 6 | // Ignore issues from commonly used lints in this file. 7 | // ignore_for_file:unnecessary_brace_in_string_interps 8 | // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering 9 | // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases 10 | // ignore_for_file:unused_import, file_names 11 | 12 | import 'package:intl/intl.dart'; 13 | import 'package:intl/message_lookup_by_library.dart'; 14 | import 'dart:convert'; 15 | 16 | import 'code_map_messages_all.dart' show evaluateJsonTemplate; 17 | 18 | import 'dart:collection'; 19 | 20 | final messages = MessageLookup(); 21 | 22 | typedef String? MessageIfAbsent(String? messageStr, List? args); 23 | 24 | class MessageLookup extends MessageLookupByLibrary { 25 | @override 26 | String get localeName => 'fr'; 27 | 28 | String? evaluateMessage(dynamic translation, List args) { 29 | return evaluateJsonTemplate(translation, args); 30 | } 31 | 32 | Map get messages => _constMessages; 33 | static const _constMessages = { 34 | "Hello from application": "Bonjour de l'application" 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /test/generate_localized/code_map_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// An application using the code map messages. 6 | import 'package:intl/intl.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | import 'code_map_messages_all.dart'; 10 | 11 | String appMessage() => Intl.message('Hello from application', desc: 'hi'); 12 | 13 | void main() async { 14 | Intl.defaultLocale = 'fr'; 15 | await initializeMessages('fr'); 16 | test('String lookups should provide translation to French', () { 17 | expect(appMessage(), 'Bonjour de l\'application'); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /test/generate_localized/regenerate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Regenerate the messages Dart files. 3 | dart ../../bin/generate_from_arb.dart \ 4 | --code-map --generated-file-prefix=code_map_ \ 5 | code_map_test.dart app_translation_getfromthelocale.arb \ 6 | --null-safety 7 | -------------------------------------------------------------------------------- /test/intl_message_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:intl_translation/src/messages/literal_string_message.dart'; 6 | import 'package:intl_translation/src/messages/submessages/plural.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | void main() { 10 | test('Prefer explicit =0 to ZERO in plural', () { 11 | var msg = Plural.from( 12 | 'main', 13 | [ 14 | ['=0', 'explicit'], 15 | ['ZERO', 'general'] 16 | ], 17 | null); 18 | expect((msg['zero'] as LiteralString).string, 'explicit'); 19 | }); 20 | 21 | test('Prefer explicit =1 to ONE in plural', () { 22 | var msg = Plural.from( 23 | 'main', 24 | [ 25 | ['=1', 'explicit'], 26 | ['ONE', 'general'] 27 | ], 28 | null); 29 | expect((msg['one'] as LiteralString).string, 'explicit'); 30 | }); 31 | 32 | test('Prefer explicit =1 to ONE in plural, reverse order', () { 33 | var msg = Plural.from( 34 | 'main', 35 | [ 36 | ['ONE', 'general'], 37 | ['=1', 'explicit'] 38 | ], 39 | null); 40 | expect((msg['one'] as LiteralString).string, 'explicit'); 41 | }); 42 | 43 | test('Prefer explicit =2 to TWO in plural', () { 44 | var msg = Plural.from( 45 | 'main', 46 | [ 47 | ['=2', 'explicit'], 48 | ['TWO', 'general'] 49 | ], 50 | null); 51 | expect((msg['two'] as LiteralString).string, 'explicit'); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /test/message_extraction/arb_list.txt: -------------------------------------------------------------------------------- 1 | translation_fr.arb 2 | french2.arb 3 | translation_de_DE.arb 4 | -------------------------------------------------------------------------------- /test/message_extraction/dart_list.txt: -------------------------------------------------------------------------------- 1 | sample_with_messages.dart 2 | part_of_sample_with_messages.dart 3 | -------------------------------------------------------------------------------- /test/message_extraction/debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # The message_extraction_test.dart test uses a temporary directory and spawns 4 | # separate processes for each step. This can make it very painful to debug the 5 | # steps. 6 | # This script runs the steps individually, putting the files in the current 7 | # directory. You can run the script to run the test locally, or use this to 8 | # run individual steps or create them as launches in the editor. 9 | dart ../../bin/extract_to_arb.dart sample_with_messages.dart \ 10 | part_of_sample_with_messages.dart 11 | 12 | dart make_hardcoded_translation.dart intl_messages.arb 13 | 14 | dart ../../bin/generate_from_arb.dart \ 15 | --json --generated-file-prefix=foo_ \ 16 | --null-safety \ 17 | sample_with_messages.dart part_of_sample_with_messages.dart \ 18 | translation_fr.arb french2.arb translation_de_DE.arb 19 | -------------------------------------------------------------------------------- /test/message_extraction/embedded_plural_text_after.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// A test library that should fail because there is a plural with text 6 | /// following the plural expression. 7 | library embedded_plural_text_after; 8 | 9 | import 'package:intl/intl.dart'; 10 | 11 | String embeddedPlural2(num n) => Intl.message( 12 | "${Intl.plural(n, zero: 'none', one: 'one', other: 'some')} plus text.", 13 | name: 'embeddedPlural2', 14 | desc: 'An embedded plural', 15 | args: [n]); 16 | -------------------------------------------------------------------------------- /test/message_extraction/embedded_plural_text_after_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @Timeout(Duration(seconds: 180)) 6 | 7 | library embedded_plural_text_after_test; 8 | 9 | import 'package:test/test.dart'; 10 | 11 | import 'failed_extraction_test.dart'; 12 | 13 | void main() { 14 | test('Expect failure because of embedded plural with text after it', () { 15 | var specialFiles = ['embedded_plural_text_after.dart']; 16 | runTestWithWarnings( 17 | warningsAreErrors: true, 18 | expectedExitCode: 1, 19 | embeddedPlurals: false, 20 | sourceFiles: specialFiles); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /test/message_extraction/embedded_plural_text_before.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// A test library that should fail because there is a plural with text 6 | /// before the plural expression. 7 | library embedded_plural_text_before; 8 | 9 | import 'package:intl/intl.dart'; 10 | 11 | String embeddedPlural(num n) => Intl.message( 12 | "There are ${Intl.plural(n, zero: 'nothing', one: 'one', other: 'some')}.", 13 | name: 'embeddedPlural', 14 | desc: 'An embedded plural', 15 | args: [n]); 16 | -------------------------------------------------------------------------------- /test/message_extraction/embedded_plural_text_before_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @Timeout(Duration(seconds: 180)) 6 | 7 | library embedded_plural_text_before_test; 8 | 9 | import 'package:test/test.dart'; 10 | 11 | import 'failed_extraction_test.dart'; 12 | 13 | void main() { 14 | test('Expect failure because of embedded plural with text before it', () { 15 | var files = ['embedded_plural_text_before.dart']; 16 | runTestWithWarnings( 17 | warningsAreErrors: true, 18 | expectedExitCode: 1, 19 | embeddedPlurals: false, 20 | sourceFiles: files); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /test/message_extraction/examples_parsing_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @Timeout(Duration(seconds: 180)) 6 | 7 | /// Test for parsing the examples argument from an Intl.message call. Very 8 | /// minimal so far. 9 | 10 | import 'dart:io'; 11 | 12 | import 'package:intl_translation/extract_messages.dart'; 13 | import 'package:path/path.dart' as path; 14 | import 'package:test/test.dart'; 15 | 16 | import '../data_directory.dart'; 17 | 18 | void main() { 19 | test('Message examples are correctly extracted', () { 20 | var file = path.join(packageDirectory, 'test', 'message_extraction', 21 | 'sample_with_messages.dart'); 22 | var extraction = MessageExtraction(); 23 | var messages = extraction.parseFile(File(file)); 24 | expect(messages['message2']!.examples, {'x': 3}); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/message_extraction/failed_extraction_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @Timeout(Duration(seconds: 180)) 6 | 7 | library failed_extraction_test; 8 | 9 | import 'dart:io'; 10 | 11 | import 'package:test/test.dart'; 12 | 13 | import 'message_extraction_test.dart'; 14 | 15 | void main() { 16 | test('Expect warnings but successful extraction', () async { 17 | await runTestWithWarnings(warningsAreErrors: false, expectedExitCode: 0); 18 | }); 19 | } 20 | 21 | const List defaultFiles = [ 22 | 'sample_with_messages.dart', 23 | 'part_of_sample_with_messages.dart' 24 | ]; 25 | 26 | Future runTestWithWarnings( 27 | {required bool warningsAreErrors, 28 | int? expectedExitCode, 29 | bool embeddedPlurals = true, 30 | List sourceFiles = defaultFiles}) async { 31 | void verify(ProcessResult result) { 32 | try { 33 | expect(result.exitCode, expectedExitCode); 34 | } finally { 35 | deleteGeneratedFiles(); 36 | } 37 | } 38 | 39 | await copyFilesToTempDirectory(); 40 | 41 | var program = asTestDirPath('../../bin/extract_to_arb.dart'); 42 | var args = ['--output-dir=$tempDir']; 43 | if (warningsAreErrors) { 44 | args.add('--warnings-are-errors'); 45 | } 46 | if (!embeddedPlurals) { 47 | args.add('--no-embedded-plurals'); 48 | } 49 | var files = sourceFiles.map(asTempDirPath).toList(); 50 | var allArgs = [program, ...args, ...files]; 51 | var callback = expectAsync1(verify); 52 | 53 | run(null, allArgs).then(callback); 54 | } 55 | -------------------------------------------------------------------------------- /test/message_extraction/find_messages_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @Timeout(Duration(seconds: 180)) 6 | 7 | import 'package:intl_translation/extract_messages.dart'; 8 | import 'package:intl_translation/src/message_rewriter.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | void main() { 12 | group('findMessages denied usages', () { 13 | test('fails with message on non-literal examples Map', () { 14 | final messageExtraction = MessageExtraction(); 15 | findMessages(''' 16 | final variable = 'foo'; 17 | 18 | String message(String string) => 19 | Intl.select(string, {'foo': 'foo', 'bar': 'bar'}, 20 | name: 'message', args: [string], examples: {'string': variable}); 21 | ''', '', messageExtraction); 22 | 23 | expect(messageExtraction.warnings, 24 | anyElement(contains('Examples must be a const Map literal.'))); 25 | }); 26 | 27 | test('fails with message on prefixed expression in interpolation', () { 28 | final messageExtraction = MessageExtraction(); 29 | findMessages( 30 | 'String message(object) => Intl.message("\${object.property}");', 31 | '', 32 | messageExtraction); 33 | 34 | expect( 35 | messageExtraction.warnings, 36 | anyElement( 37 | contains('Only simple identifiers and Intl.plural/gender/select ' 38 | 'expressions are allowed in message interpolation ' 39 | 'expressions'))); 40 | }); 41 | 42 | test('fails on call with name referencing variable name inside a function', 43 | () { 44 | final messageExtraction = MessageExtraction(); 45 | findMessages(''' 46 | class MessageTest { 47 | String functionName() { 48 | final String variableName = Intl.message('message string', 49 | name: 'variableName' ); 50 | } 51 | }''', '', messageExtraction); 52 | 53 | expect( 54 | messageExtraction.warnings, 55 | anyElement(contains('The \'name\' argument for Intl.message ' 56 | 'must match either the name of the containing function ' 57 | 'or _'))); 58 | }); 59 | 60 | test('fails on referencing a name from listed fields declaration', () { 61 | final messageExtraction = MessageExtraction(); 62 | findMessages(''' 63 | class MessageTest { 64 | String first, second = Intl.message('message string', 65 | name: 'first' ); 66 | }''', '', messageExtraction); 67 | 68 | expect( 69 | messageExtraction.warnings, 70 | anyElement(contains('The \'name\' argument for Intl.message ' 71 | 'must match either the name of the containing function ' 72 | 'or _'))); 73 | }); 74 | }); 75 | 76 | group('findMessages accepted usages', () { 77 | test('succeeds on Intl call from class getter', () { 78 | final messageExtraction = MessageExtraction(); 79 | var messages = findMessages(''' 80 | class MessageTest { 81 | String get messageName => Intl.message("message string", 82 | name: 'messageName', desc: 'abc'); 83 | }''', '', messageExtraction); 84 | 85 | expect(messages.map((m) => m.name), anyElement(contains('messageName'))); 86 | expect(messageExtraction.warnings, isEmpty); 87 | }); 88 | 89 | test('succeeds on Intl call in top variable declaration', () { 90 | final messageExtraction = MessageExtraction(); 91 | var messages = findMessages( 92 | 'List list = [Intl.message("message string", ' 93 | 'name: "list", desc: "in list")];', 94 | '', 95 | messageExtraction); 96 | 97 | expect(messages.map((m) => m.name), anyElement(contains('list'))); 98 | expect(messageExtraction.warnings, isEmpty); 99 | }); 100 | 101 | test('succeeds on Intl call in member variable declaration', () { 102 | final messageExtraction = MessageExtraction(); 103 | var messages = findMessages(''' 104 | class MessageTest { 105 | final String messageName = Intl.message("message string", 106 | name: 'MessageTest_messageName', desc: 'test'); 107 | }''', '', messageExtraction); 108 | 109 | expect(messages.map((m) => m.name), 110 | anyElement(contains('MessageTest_messageName'))); 111 | expect(messageExtraction.warnings, isEmpty); 112 | }); 113 | 114 | // Note: this type of usage is not recommended. 115 | test('succeeds on Intl call inside a function as variable declaration', () { 116 | final messageExtraction = MessageExtraction(); 117 | var messages = findMessages(''' 118 | class MessageTest { 119 | String functionName() { 120 | final String variableName = Intl.message('message string', 121 | name: 'functionName', desc: 'test' ); 122 | } 123 | }''', '', messageExtraction); 124 | 125 | expect(messages.map((m) => m.name), anyElement(contains('functionName'))); 126 | expect(messageExtraction.warnings, isEmpty); 127 | }); 128 | 129 | test('succeeds on list field declaration', () { 130 | final messageExtraction = MessageExtraction(); 131 | var messages = findMessages(''' 132 | class MessageTest { 133 | String first, second = Intl.message('message string', desc: 'test'); 134 | }''', '', messageExtraction); 135 | 136 | expect( 137 | messages.map((m) => m.name), anyElement(contains('message string'))); 138 | expect(messageExtraction.warnings, isEmpty); 139 | }); 140 | 141 | test('succeeds on prefixed Intl call', () { 142 | final messageExtraction = MessageExtraction(); 143 | final messages = findMessages(''' 144 | class MessageTest { 145 | static final String prefixedMessage = 146 | prefix.Intl.message('message', desc: 'xyz'); 147 | } 148 | ''', '', messageExtraction); 149 | 150 | expect(messages.map((m) => m.name), anyElement(contains('message'))); 151 | expect(messageExtraction.warnings, isEmpty); 152 | }); 153 | }); 154 | 155 | group('messages with the same name', () { 156 | test('are resolved in favour of the earlier one by default', () { 157 | final messageExtraction = MessageExtraction(); 158 | final messages = findMessages(''' 159 | final msg1 = Intl.message('hello there', desc: 'abc'); 160 | final msg2 = Intl.message('hello there', desc: 'def'); 161 | ''', '', messageExtraction); 162 | 163 | expect(messages.map((m) => m.description), equals(['abc'])); 164 | }); 165 | 166 | test('are resolved with custom merger', () { 167 | final messageExtraction = MessageExtraction(); 168 | messageExtraction.mergeMessages = 169 | (m1, m2) => m1..description = '${m1.description}/${m2.description}'; 170 | final messages = findMessages(''' 171 | final msg1 = Intl.message('hello there', desc: 'abc'); 172 | final msg2 = Intl.message('hello there', desc: 'def'); 173 | ''', '', messageExtraction); 174 | 175 | expect(messages.map((m) => m.description), equals(['abc/def'])); 176 | }); 177 | }); 178 | 179 | group('documentation', () { 180 | test('is populated from dartdoc', () { 181 | final messageExtraction = MessageExtraction(); 182 | final messages = findMessages(''' 183 | class MessageTest { 184 | /// Dartdoc. 185 | static final fieldMsg = Intl.message('field msg', desc: 'xyz'); 186 | 187 | /// A long dartdoc. 188 | /// 189 | /// With a paragraph. 190 | String methodMsg(String arg) => Intl.message('method msg', desc: 'xyz'); 191 | } 192 | 193 | /// Hi. 194 | String variable = Intl.message('variable msg', desc: 'xyz'); 195 | 196 | /// Bye. 197 | function() { 198 | return Intl.message('function msg', desc: 'xyz'); 199 | } 200 | ''', '', messageExtraction); 201 | 202 | expect( 203 | messages.map((m) => m.documentation), 204 | unorderedEquals([ 205 | ['/// Dartdoc.'], 206 | ['/// A long dartdoc.', '///', '/// With a paragraph.'], 207 | ['/// Hi.'], 208 | ['/// Bye.'], 209 | ])); 210 | }); 211 | }); 212 | } 213 | -------------------------------------------------------------------------------- /test/message_extraction/foo_messages_all.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | library keep_the_static_analysis_from_complaining; 6 | 7 | Future initializeMessages(_) => 8 | throw UnimplementedError('This entire file is only here to make the static' 9 | ' analysis happy. It will be generated during actual tests.'); 10 | -------------------------------------------------------------------------------- /test/message_extraction/make_hardcoded_translation.dart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dart 2 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 3 | // for details. All rights reserved. Use of this source code is governed by a 4 | // BSD-style license that can be found in the LICENSE file. 5 | 6 | /// This simulates a translation process, reading the messages generated from 7 | /// extract_message.dart for the files sample_with_messages.dart and 8 | /// part_of_sample_with_messages.dart and writing out hard-coded translations 9 | /// for German and French locales. 10 | 11 | import 'dart:convert'; 12 | import 'dart:io'; 13 | 14 | import 'package:args/args.dart'; 15 | import 'package:path/path.dart' as path; 16 | 17 | /// A list of the French translations that we will produce. 18 | Map french = { 19 | 'types': r'{a}, {b}, {c}', 20 | 'This string extends across multiple lines.': 21 | 'Cette message prend plusiers lignes.', 22 | 'message2': r'Un autre message avec un seul paramètre {x}', 23 | 'alwaysTranslated': 'Cette chaîne est toujours traduit', 24 | 'message1': "Il s'agit d'un message", 25 | '"So-called"': '"Soi-disant"', 26 | 'trickyInterpolation': r"L'interpolation est délicate " 27 | r'quand elle se termine une phrase comme {s}.', 28 | 'message3': 'Caractères qui doivent être échapper, par exemple barres \\ ' 29 | 'dollars \${ (les accolades sont ok), et xml/html réservés <& et ' 30 | 'des citations " ' 31 | 'avec quelques paramètres ainsi {a}, {b}, et {c}', 32 | 'notAlwaysTranslated': 'Ce manque certaines traductions', 33 | 'thisNameIsNotInTheOriginal': 'Could this lead to something malicious?', 34 | 'Ancient Greek hangman characters: 𐅆𐅇.': 35 | 'Anciens caractères grecs jeux du pendu: 𐅆𐅇.', 36 | 'escapable': 'Escapes: \n\r\f\b\t\v.', 37 | 'sameContentsDifferentName': 'Bonjour tout le monde', 38 | 'differentNameSameContents': 'Bonjour tout le monde', 39 | 'rentToBePaid': 'loyer', 40 | 'rentAsVerb': 'louer', 41 | 'plurals': "{num,plural, =0{Est-ce que nulle est pluriel?}=1{C'est singulier}" 42 | "other{C'est pluriel ({num}).}}", 43 | 'whereTheyWentMessage': '{gender,select, male{{name} est allé à sa {place}}' 44 | 'female{{name} est allée à sa {place}}other{{name}' 45 | ' est allé à sa {place}}}', 46 | // Gratuitously different translation for testing. Ignoring gender of place. 47 | 'nestedMessage': '{combinedGender,select, ' 48 | 'other{' 49 | '{number,plural, ' 50 | "=0{Personne n'avait allé à la {place}}" 51 | '=1{{names} était allé à la {place}}' 52 | 'other{{names} étaient allés à la {place}}' 53 | '}' 54 | '}' 55 | 'female{' 56 | '{number,plural, ' 57 | '=1{{names} était allée à la {place}}' 58 | 'other{{names} étaient allées à la {place}}' 59 | '}' 60 | '}' 61 | '}', 62 | 'outerPlural': '{n,plural, =0{rien}=1{un}other{quelques-uns}}', 63 | 'outerGender': '{g,select, male {homme} female {femme} other {autre}}', 64 | 'pluralThatFailsParsing': '{noOfThings,plural, ' 65 | '=1{1 chose:}other{{noOfThings} choses:}}', 66 | 'nestedOuter': '{number,plural, other{' 67 | '{gen,select, male{{number} homme}other{{number} autre}}}}', 68 | 'outerSelect': '{currency,select, CDN{{amount} dollars Canadiens}' 69 | 'other{{amount} certaine devise ou autre.}}}', 70 | 'nestedSelect': '{currency,select, CDN{{amount,plural, ' 71 | '=1{{amount} dollar Canadien}' 72 | 'other{{amount} dollars Canadiens}}}' 73 | "other{N'importe quoi}" 74 | '}}', 75 | 'literalDollar': 'Cinq sous est US\$0.05', 76 | r"'<>{}= +-_$()&^%$#@!~`'": r"interessant (fr): '<>{}= +-_$()&^%$#@!~`'", 77 | 'extractable': 'Ce message devrait être extractible', 78 | 'skipMessageExistingTranslation': 'Ce message devrait ignorer la traduction' 79 | }; 80 | 81 | // Used to test having translations in multiple files. 82 | Map frenchExtra = { 83 | 'YouveGotMessages_method': "Cela vient d'une méthode", 84 | 'nonLambda': "Cette méthode n'est pas un lambda", 85 | 'staticMessage': "Cela vient d'une méthode statique", 86 | }; 87 | 88 | /// A list of the German translations that we will produce. 89 | Map german = { 90 | 'types': r'{a}, {b}, {c}', 91 | 'This string extends across multiple lines.': 92 | 'Dieser String erstreckt sich über mehrere Zeilen erstrecken.', 93 | 'message2': r'Eine weitere Meldung mit dem Parameter {x}', 94 | 'alwaysTranslated': 'Diese Zeichenkette wird immer übersetzt', 95 | 'message1': 'Dies ist eine Nachricht', 96 | '"So-called"': '"Sogenannt"', 97 | 'trickyInterpolation': r'Interpolation ist schwierig, wenn es einen Satz ' 98 | 'wie dieser endet {s}.', 99 | 'message3': 'Zeichen, die Flucht benötigen, zB Schrägstriche \\ Dollar ' 100 | '\${ (geschweiften Klammern sind ok) und xml reservierte Zeichen <& und ' 101 | 'Zitate " Parameter {a}, {b} und {c}', 102 | 'YouveGotMessages_method': 'Dies ergibt sich aus einer Methode', 103 | 'nonLambda': 'Diese Methode ist nicht eine Lambda', 104 | 'staticMessage': 'Dies ergibt sich aus einer statischen Methode', 105 | 'thisNameIsNotInTheOriginal': 'Could this lead to something malicious?', 106 | 'Ancient Greek hangman characters: 𐅆𐅇.': 107 | 'Antike griechische Galgenmännchen Zeichen: 𐅆𐅇', 108 | 'escapable': 'Escapes: \n\r\f\b\t\v.', 109 | 'sameContentsDifferentName': 'Hallo Welt', 110 | 'differentNameSameContents': 'Hallo Welt', 111 | 'rentToBePaid': 'Miete', 112 | 'rentAsVerb': 'mieten', 113 | 'plurals': '{num,plural, =0{Ist Null Plural?}=1{Dies ist einmalig}' 114 | 'other{Dies ist Plural ({num}).}}', 115 | 'whereTheyWentMessage': '{gender,select, male{{name} ging zu seinem {place}}' 116 | 'female{{name} ging zu ihrem {place}}other{{name} ging zu seinem {place}}}', 117 | //Note that we're only using the gender of the people. The gender of the 118 | //place also matters, but we're not dealing with that here. 119 | 'nestedMessage': '{combinedGender,select, ' 120 | 'other{' 121 | '{number,plural, ' 122 | '=0{Niemand ging zu {place}}' 123 | '=1{{names} ging zum {place}}' 124 | 'other{{names} gingen zum {place}}' 125 | '}' 126 | '}' 127 | 'female{' 128 | '{number,plural, ' 129 | '=1{{names} ging in dem {place}}' 130 | 'other{{names} gingen zum {place}}' 131 | '}' 132 | '}' 133 | '}', 134 | 'outerPlural': '{n,plural, =0{Null}=1{ein}other{einige}}', 135 | 'outerGender': '{g,select, male{Mann}female{Frau}other{andere}}', 136 | 'pluralThatFailsParsing': '{noOfThings,plural, ' 137 | '=1{eins:}other{{noOfThings} Dinge:}}', 138 | 'nestedOuter': '{number,plural, other{' 139 | '{gen,select, male{{number} Mann}other{{number} andere}}}}', 140 | 'outerSelect': '{currency,select, CDN{{amount} Kanadischen dollar}' 141 | 'other{{amount} einige Währung oder anderen.}}}', 142 | 'nestedSelect': '{currency,select, CDN{{amount,plural, ' 143 | '=1{{amount} Kanadischer dollar}' 144 | 'other{{amount} Kanadischen dollar}}}' 145 | 'other{whatever}' 146 | '}', 147 | 'literalDollar': 'Fünf Cent US \$ 0.05', 148 | r"'<>{}= +-_$()&^%$#@!~`'": r"interessant (de): '<>{}= +-_$()&^%$#@!~`'", 149 | 'extractable': 'Diese Nachricht sollte extrahierbar sein', 150 | 'skipMessageExistingTranslation': 151 | 'Diese Nachricht sollte die Übersetzung überspringen' 152 | }; 153 | 154 | /// The output directory for translated files. 155 | String? targetDir; 156 | 157 | const jsonCodec = JsonCodec(); 158 | 159 | /// Generate a translated json version from [originals] in [locale] looking 160 | /// up the translations in [translations]. 161 | void translate(Map originals, String locale, Map translations, 162 | [String? filename]) { 163 | var translated = {'_locale': locale}; 164 | originals.forEach((name, text) { 165 | if (translations[name] != null) { 166 | translated[name] = translations[name]; 167 | } 168 | }); 169 | var file = File(path.join(targetDir!, filename ?? 'translation_$locale.arb')); 170 | file.writeAsStringSync(jsonCodec.encode(translated)); 171 | } 172 | 173 | void main(List args) { 174 | if (args.isEmpty) { 175 | print('Usage: make_hardcoded_translation [--output-dir=] ' 176 | '[originalFile.arb]'); 177 | exit(0); 178 | } 179 | var parser = ArgParser(); 180 | parser.addOption('output-dir', 181 | defaultsTo: '.', callback: (value) => targetDir = value); 182 | parser.parse(args); 183 | 184 | var fileArgs = args.where((x) => x.contains('.arb')); 185 | 186 | var messages = jsonCodec.decode(File(fileArgs.first).readAsStringSync()); 187 | translate(messages, 'fr', french); 188 | translate(messages, 'fr', frenchExtra, 'french2.arb'); 189 | translate(messages, 'de_DE', german); 190 | } 191 | -------------------------------------------------------------------------------- /test/message_extraction/message_extraction_flutter_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @Timeout(Duration(seconds: 180)) 6 | 7 | /// A test for message extraction and code generation using generated 8 | /// JSON rather than functions 9 | 10 | import 'package:test/test.dart'; 11 | 12 | import 'message_extraction_test.dart' as main_test; 13 | 14 | void main() { 15 | main_test.useJson = true; 16 | main_test.useFlutterLocaleSplit = true; 17 | main_test.main(); 18 | } 19 | -------------------------------------------------------------------------------- /test/message_extraction/message_extraction_json_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @Timeout(Duration(seconds: 180)) 6 | 7 | /// A test for message extraction and code generation using generated 8 | /// JSON rather than functions 9 | 10 | import 'package:test/test.dart'; 11 | 12 | import 'message_extraction_test.dart' as main_test; 13 | 14 | void main() { 15 | main_test.useJson = true; 16 | main_test.main(); 17 | } 18 | -------------------------------------------------------------------------------- /test/message_extraction/message_extraction_no_deferred_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @Timeout(Duration(seconds: 180)) 6 | 7 | /// A test for message extraction and code generation not using deferred 8 | /// loading for the generated code. 9 | library message_extraction_no_deferred_test; 10 | 11 | import 'package:test/test.dart'; 12 | 13 | import 'message_extraction_test.dart' as main_test; 14 | 15 | void main() { 16 | main_test.useDeferredLoading = false; 17 | main_test.main(); 18 | } 19 | -------------------------------------------------------------------------------- /test/message_extraction/message_extraction_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @Timeout(Duration(seconds: 180)) 6 | 7 | library message_extraction_test; 8 | 9 | import 'dart:convert'; 10 | import 'dart:io'; 11 | import 'dart:isolate'; 12 | 13 | import 'package:path/path.dart' as path; 14 | import 'package:test/test.dart'; 15 | 16 | import '../data_directory.dart'; 17 | 18 | /// Should we use deferred loading. 19 | bool useDeferredLoading = true; 20 | 21 | /// Should we generate JSON strings rather than code for messages. 22 | bool useJson = false; 23 | 24 | /// Should we generate the code for Flutter locale split. 25 | /// 26 | /// Note that this is only supported in JSON mode. 27 | bool useFlutterLocaleSplit = false; 28 | 29 | String get _deferredLoadPrefix => useDeferredLoading ? '' : 'no-'; 30 | 31 | String get deferredLoadArg => '--${_deferredLoadPrefix}use-deferred-loading'; 32 | 33 | /// The VM arguments we were given, most important package-root. 34 | final vmArgs = Platform.executableArguments; 35 | 36 | /// For testing we move the files into a temporary directory so as not to leave 37 | /// generated files around after a failed test. For debugging, we omit that 38 | /// step if [useLocalDirectory] is true. The place we move them to is saved as 39 | /// [tempDir]. 40 | String get tempDir => _tempDir ?? (_tempDir = _createTempDir()); 41 | String? _tempDir; 42 | String _createTempDir() => useLocalDirectory 43 | ? '.' 44 | : Directory.systemTemp.createTempSync('message_extraction_test').path; 45 | 46 | bool useLocalDirectory = false; 47 | 48 | /// Translate a relative file path into this test directory. This is 49 | /// applied to all the arguments of [run]. It will ignore a string that 50 | /// is an absolute path or begins with "--", because some of the arguments 51 | /// might be command-line options. 52 | String asTestDirPath(String s) { 53 | if (s.startsWith('--') || path.isAbsolute(s)) return s; 54 | return path.join(packageDirectory, 'test', 'message_extraction', s); 55 | } 56 | 57 | /// Translate a relative file path into our temp directory. This is 58 | /// applied to all the arguments of [run]. It will ignore a string that 59 | /// is an absolute path or begins with "--", because some of the arguments 60 | /// might be command-line options. 61 | String? asTempDirPath([String? s]) { 62 | if (s == null || s.startsWith('--') || path.isAbsolute(s)) return s; 63 | return path.join(tempDir, s); 64 | } 65 | 66 | typedef ThenResult = Future Function(ProcessResult _); 67 | 68 | void main() { 69 | setUp(copyFilesToTempDirectory); 70 | tearDown(deleteGeneratedFiles); 71 | 72 | test( 73 | 'Test round trip message extraction, translation, code generation, ' 74 | 'and printing', () { 75 | var makeSureWeVerify = expectAsync1(runAndVerify); 76 | return extractMessages(null) 77 | .then((result) => generateTranslationFiles(result)) 78 | .then((result) => generateCodeFromTranslation(result)) 79 | .then(makeSureWeVerify) 80 | .then(checkResult); 81 | }); 82 | } 83 | 84 | Future copyFilesToTempDirectory() async { 85 | if (useLocalDirectory) { 86 | return; 87 | } 88 | 89 | var files = [ 90 | asTestDirPath('sample_with_messages.dart'), 91 | asTestDirPath('part_of_sample_with_messages.dart'), 92 | asTestDirPath('verify_messages.dart'), 93 | asTestDirPath('run_and_verify.dart'), 94 | asTestDirPath('embedded_plural_text_before.dart'), 95 | asTestDirPath('embedded_plural_text_after.dart'), 96 | asTestDirPath('print_to_list.dart'), 97 | asTestDirPath('dart_list.txt'), 98 | asTestDirPath('arb_list.txt'), 99 | asTestDirPath('mock_flutter/services.dart'), 100 | ]; 101 | 102 | for (var filePath in files) { 103 | var file = File(filePath); 104 | if (file.existsSync()) { 105 | file.copySync(path.join(tempDir, path.basename(filePath))); 106 | } 107 | } 108 | 109 | // Here we copy the package config file so the test can locate packages. 110 | var configFile = File.fromUri((await Isolate.packageConfig)!); 111 | var destFile = File(path.join(tempDir, '.dart_tool', 'package_config.json')); 112 | if (!destFile.parent.existsSync()) { 113 | destFile.parent.createSync(); 114 | } 115 | configFile.copySync(destFile.path); 116 | } 117 | 118 | void deleteGeneratedFiles() { 119 | if (useLocalDirectory) return; 120 | try { 121 | Directory(tempDir).deleteSync(recursive: true); 122 | } on Error catch (e) { 123 | print('Failed to delete $tempDir'); 124 | print('Exception:\n$e'); 125 | } 126 | } 127 | 128 | /// Run the process with the given list of filenames, which we assume 129 | /// are in dir() and need to be qualified in case that's not our working 130 | /// directory. 131 | Future run( 132 | ProcessResult? previousResult, List filenames) { 133 | // If there's a failure in one of the sub-programs, print its output. 134 | checkResult(previousResult); 135 | var filesInTheRightDirectory = filenames 136 | .map((x) => asTempDirPath(x)) 137 | .map((x) => path.normalize(x!)) 138 | .toList(); 139 | // Inject the script argument --output-dir in between the script and its 140 | // arguments. 141 | var args = [ 142 | ...vmArgs, 143 | filesInTheRightDirectory.first, 144 | '--output-dir=$tempDir', 145 | ...filesInTheRightDirectory.skip(1) 146 | ]; 147 | var result = Process.run(Platform.executable, args, 148 | stdoutEncoding: Utf8Codec(), stderrEncoding: Utf8Codec()); 149 | return result; 150 | } 151 | 152 | void checkResult(ProcessResult? result) { 153 | if (result != null) { 154 | if (result.exitCode != 0) { 155 | print('Error running sub-program:'); 156 | print(result.stdout); 157 | print(result.stderr); 158 | print('exitCode=${result.exitCode}'); 159 | } 160 | 161 | expect(result.exitCode, 0); 162 | } 163 | } 164 | 165 | Future extractMessages(ProcessResult? previousResult) => 166 | run(previousResult, [ 167 | asTestDirPath('../../bin/extract_to_arb.dart'), 168 | '--suppress-warnings', 169 | '--sources-list-file', 170 | 'dart_list.txt' 171 | ]); 172 | 173 | Future generateTranslationFiles(ProcessResult previousResult) => 174 | run(previousResult, [ 175 | asTestDirPath('make_hardcoded_translation.dart'), 176 | 'intl_messages.arb' 177 | ]); 178 | 179 | Future generateCodeFromTranslation( 180 | ProcessResult previousResult) => 181 | run(previousResult, [ 182 | asTestDirPath('../../bin/generate_from_arb.dart'), 183 | deferredLoadArg, 184 | '--${useJson ? '' : 'no-'}json', 185 | '--${useFlutterLocaleSplit ? '' : 'no-'}flutter', 186 | '--flutter-import-path=.', // Mocks package:flutter/services.dart 187 | '--generated-file-prefix=foo_', 188 | '--sources-list-file', 189 | 'dart_list.txt', 190 | '--translations-list-file', 191 | 'arb_list.txt', 192 | ]); 193 | 194 | Future runAndVerify(ProcessResult previousResult) { 195 | return run(previousResult, ['run_and_verify.dart', 'intl_messages.arb']); 196 | } 197 | -------------------------------------------------------------------------------- /test/message_extraction/mock_flutter/foo_messages_de_DE.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | library keep_the_static_analysis_from_complaining; 6 | 7 | class MessageLookup { 8 | String get messages => throw UnimplementedError( 9 | 'This entire file is only here to make the static' 10 | ' analysis happy. It will be generated during actual tests.'); 11 | } 12 | -------------------------------------------------------------------------------- /test/message_extraction/mock_flutter/foo_messages_fr.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | library keep_the_static_analysis_from_complaining; 6 | 7 | class MessageLookup { 8 | String get messages => throw UnimplementedError( 9 | 'This entire file is only here to make the static' 10 | ' analysis happy. It will be generated during actual tests.'); 11 | } 12 | -------------------------------------------------------------------------------- /test/message_extraction/mock_flutter/services.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'foo_messages_de_DE.dart' as de_de; 5 | import 'foo_messages_fr.dart' as fr; 6 | 7 | // Mocks the Flutter interfaces used in the generated messages_flutter.dart. 8 | class SystemChannels { 9 | static const MethodChannel localization = MethodChannel(); 10 | } 11 | 12 | class MethodChannel { 13 | const MethodChannel(); 14 | 15 | Future invokeMethod(String method, [dynamic arguments]) async { 16 | var locale = arguments['locale']; 17 | if (locale == null) { 18 | return null; 19 | } 20 | 21 | // We only have two locales in the test. 22 | if (locale == 'fr') { 23 | return jsonEncode(fr.MessageLookup().messages); 24 | } else if (locale == 'de_DE') { 25 | return jsonEncode(de_de.MessageLookup().messages); 26 | } 27 | return null; 28 | } 29 | } 30 | 31 | class AssetBundle { 32 | Future loadString(String key, {bool cache = true}) async { 33 | // We only have two locales in the test. 34 | if (key.contains('fr')) { 35 | return jsonEncode(fr.MessageLookup().messages); 36 | } else if (key.contains('de-DE')) { 37 | return jsonEncode(de_de.MessageLookup().messages); 38 | } 39 | return null; 40 | } 41 | } 42 | 43 | final AssetBundle rootBundle = AssetBundle(); 44 | -------------------------------------------------------------------------------- /test/message_extraction/part_of_sample_with_messages.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file.part of sample; 4 | 5 | part of sample; 6 | 7 | class Person { 8 | String name; 9 | String? gender; 10 | Person(this.name, this.gender); 11 | } 12 | 13 | class YouveGotMessages { 14 | // A static message, rather than a standalone function. 15 | static String staticMessage() => 16 | Intl.message('This comes from a static method', 17 | name: 'staticMessage', desc: 'Static'); 18 | 19 | // An instance method, rather than a standalone function. 20 | String method() => Intl.message('This comes from a method', 21 | name: 'YouveGotMessages_method', 22 | desc: 'This is a method with a ' 23 | 'long description which spans ' 24 | 'multiple lines.'); 25 | 26 | // A non-lambda, i.e. not using => syntax, and with an additional statement 27 | // before the Intl.message call. 28 | String nonLambda() { 29 | var aTrueValue = true; 30 | var msg = Intl.message('This method is not a lambda', 31 | name: 'nonLambda', desc: 'Not a lambda'); 32 | if (aTrueValue) { 33 | var s = 'Parser should not fail with additional code.'; 34 | s.toString(); 35 | } 36 | return msg; 37 | } 38 | 39 | String plurals(num num) => Intl.message( 40 | """${Intl.plural( 41 | num, 42 | zero: 'Is zero plural?', 43 | one: 'This is singular.', 44 | other: 'This is plural ($num).', 45 | )}""", 46 | name: 'plurals', 47 | args: [num], 48 | desc: 'Basic plurals'); 49 | 50 | dynamic whereTheyWent(Person person, String place) => 51 | whereTheyWentMessage(person.name, person.gender ?? 'other', place); 52 | 53 | String whereTheyWentMessage(String name, String gender, String place) { 54 | return Intl.message( 55 | "${Intl.gender( 56 | gender, 57 | male: '$name went to his $place', 58 | female: '$name went to her $place', 59 | other: '$name went to its $place', 60 | )}", 61 | name: 'whereTheyWentMessage', 62 | args: [name, gender, place], 63 | desc: 'A person went to some place that they own, e.g. their room'); 64 | } 65 | 66 | // English doesn't do enough with genders, so this example is French. 67 | String nested(List people, String place) { 68 | var names = people.map((x) => x.name).join(', '); 69 | var number = people.length; 70 | var combinedGender = 71 | people.every((x) => x.gender == 'female') ? 'female' : 'other'; 72 | if (number == 0) combinedGender = 'other'; 73 | 74 | String nestedMessage(names, number, combinedGender, place) => Intl.message( 75 | '''${Intl.gender( 76 | combinedGender, 77 | other: '${Intl.plural( 78 | number, 79 | zero: "Personne n'est allé au $place", 80 | one: "$names est allé au $place", 81 | other: "$names sont allés au $place", 82 | )}', 83 | female: '${Intl.plural( 84 | number, 85 | one: "$names est allée au $place", 86 | other: "$names sont allées au $place", 87 | )}', 88 | )}''', 89 | desc: 'Nested message example', 90 | name: 'nestedMessage', 91 | args: [names, number, combinedGender, place]); 92 | return nestedMessage(names, number, combinedGender, place); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/message_extraction/print_to_list.dart: -------------------------------------------------------------------------------- 1 | /// This provides a way for a test to print to an internal list so the 2 | /// results can be verified rather than writing to and reading a file. 3 | 4 | library print_to_list.dart; 5 | 6 | List lines = []; 7 | 8 | void printOut(String s) { 9 | lines.add(s); 10 | } 11 | -------------------------------------------------------------------------------- /test/message_extraction/really_fail_extraction_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @Timeout(Duration(seconds: 180)) 6 | 7 | library really_fail_extraction_test; 8 | 9 | import 'package:test/test.dart'; 10 | 11 | import 'failed_extraction_test.dart'; 12 | 13 | void main() { 14 | test('Expect failure because warnings are errors', () { 15 | runTestWithWarnings(warningsAreErrors: true, expectedExitCode: 1); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /test/message_extraction/run_and_verify.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | library verify_and_run; 6 | 7 | import 'dart:convert'; 8 | import 'dart:io'; 9 | 10 | import 'sample_with_messages.dart' as sample; 11 | import 'verify_messages.dart'; 12 | 13 | void main(List args) async { 14 | if (args.isEmpty) { 15 | print('Usage: run_and_verify [message_file.arb]'); 16 | exit(0); 17 | } 18 | 19 | // Verify message translation output 20 | await sample.main(); 21 | verifyResult(); 22 | 23 | // Messages with skipExtraction set should not be extracted 24 | var fileArgs = args.where((x) => x.contains('.arb')); 25 | var messages = jsonDecode(File(fileArgs.first).readAsStringSync()) 26 | as Map; 27 | messages.forEach((name, _) { 28 | // Assume any name with 'skip' in it should not have been extracted. 29 | if (name.contains('skip')) { 30 | throw "A skipped message was extracted ('$name')"; 31 | } 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /test/message_extraction/verify_messages.dart: -------------------------------------------------------------------------------- 1 | library verify_messages; 2 | 3 | import 'print_to_list.dart'; 4 | 5 | void verifyResult() { 6 | late Iterator lineIterator; 7 | 8 | void verify(String expected) { 9 | lineIterator.moveNext(); 10 | var actual = lineIterator.current; 11 | if (expected != actual) { 12 | throw "expected '$expected' but got '$actual'"; 13 | } 14 | } 15 | 16 | var expanded = lines.expand((line) => line.split('\n')).toList(); 17 | lineIterator = expanded.iterator; 18 | verify('-------------------------------------------'); 19 | verify('Printing messages for en_US'); 20 | verify('This is a message'); 21 | verify('Another message with parameter hello'); 22 | verify('Characters that need escaping, e.g slashes \\ dollars \${ ' 23 | '(curly braces are ok) and xml reserved characters <& and ' 24 | 'quotes " parameters 1, 2, and 3'); 25 | verify('This string extends across multiple lines.'); 26 | verify('1, b, [c, d]'); 27 | verify('"So-called"'); 28 | verify('Cette chaîne est toujours traduit'); 29 | verify('Interpolation is tricky when it ends a sentence like this.'); 30 | verify('This comes from a method'); 31 | verify('This method is not a lambda'); 32 | verify('This comes from a static method'); 33 | verify('This is missing some translations'); 34 | verify('Ancient Greek hangman characters: 𐅆𐅇.'); 35 | verify('Escapable characters here: '); 36 | 37 | verify('Is zero plural?'); 38 | verify('This is singular.'); 39 | verify('This is plural (2).'); 40 | verify('This is plural (3).'); 41 | verify('This is plural (4).'); 42 | verify('This is plural (5).'); 43 | verify('This is plural (6).'); 44 | verify('This is plural (7).'); 45 | verify('This is plural (8).'); 46 | verify('This is plural (9).'); 47 | verify('This is plural (10).'); 48 | verify('This is plural (11).'); 49 | verify('This is plural (20).'); 50 | verify('This is plural (100).'); 51 | verify('This is plural (101).'); 52 | verify('This is plural (100000).'); 53 | verify('Alice went to her house'); 54 | verify('Bob went to his house'); 55 | verify('cat went to its litter box'); 56 | verify('Alice, Bob sont allés au magasin'); 57 | verify('Alice est allée au magasin'); 58 | verify('Personne n\'est allé au magasin'); 59 | verify('Bob, Bob sont allés au magasin'); 60 | verify('Alice, Alice sont allées au magasin'); 61 | verify('none'); 62 | verify('one'); 63 | verify('m'); 64 | verify('f'); 65 | verify('7 male'); 66 | verify('7 Canadian dollars'); 67 | verify('5 some currency or other.'); 68 | verify('1 Canadian dollar'); 69 | verify('2 Canadian dollars'); 70 | verify('1 thing:'); 71 | verify('2 things:'); 72 | verify('Hello World'); 73 | verify('Hello World'); 74 | verify('rent'); 75 | verify('rent'); 76 | verify('Five cents is US\$0.05'); 77 | verify(r"'<>{}= +-_$()&^%$#@!~`'"); 78 | verify('This message should be extractable'); 79 | verify('This message should skip extraction'); 80 | verify('Extraction skipped plural one'); 81 | verify('Extraction skipped gender f'); 82 | verify('Extraction skipped select specified Bob!'); 83 | verify('This message should skip translation'); 84 | verify('-------------------------------------------'); 85 | 86 | // French translations. 87 | verify('Printing messages for fr'); 88 | verify("Il s'agit d'un message"); 89 | verify('Un autre message avec un seul paramètre hello'); 90 | verify('Caractères qui doivent être échapper, par exemple barres \\ ' 91 | 'dollars \${ (les accolades sont ok), et xml/html réservés <& et ' 92 | 'des citations " ' 93 | 'avec quelques paramètres ainsi 1, 2, et 3'); 94 | verify('Cette message prend plusiers lignes.'); 95 | verify('1, b, [c, d]'); 96 | verify('"Soi-disant"'); 97 | verify('Cette chaîne est toujours traduit'); 98 | verify("L'interpolation est délicate quand elle se termine une " 99 | 'phrase comme this.'); 100 | verify("Cela vient d'une méthode"); 101 | verify("Cette méthode n'est pas un lambda"); 102 | verify("Cela vient d'une méthode statique"); 103 | verify('Ce manque certaines traductions'); 104 | verify('Anciens caractères grecs jeux du pendu: 𐅆𐅇.'); 105 | verify('Escapes: '); 106 | verify('\r\f\b\t\v.'); 107 | 108 | verify('Est-ce que nulle est pluriel?'); 109 | verify('C\'est singulier'); 110 | verify('C\'est pluriel (2).'); 111 | verify('C\'est pluriel (3).'); 112 | verify('C\'est pluriel (4).'); 113 | verify('C\'est pluriel (5).'); 114 | verify('C\'est pluriel (6).'); 115 | verify('C\'est pluriel (7).'); 116 | verify('C\'est pluriel (8).'); 117 | verify('C\'est pluriel (9).'); 118 | verify('C\'est pluriel (10).'); 119 | verify('C\'est pluriel (11).'); 120 | verify('C\'est pluriel (20).'); 121 | verify('C\'est pluriel (100).'); 122 | verify('C\'est pluriel (101).'); 123 | verify('C\'est pluriel (100000).'); 124 | verify('Alice est allée à sa house'); 125 | verify('Bob est allé à sa house'); 126 | verify('cat est allé à sa litter box'); 127 | verify('Alice, Bob étaient allés à la magasin'); 128 | verify('Alice était allée à la magasin'); 129 | verify('Personne n\'avait allé à la magasin'); 130 | verify('Bob, Bob étaient allés à la magasin'); 131 | verify('Alice, Alice étaient allées à la magasin'); 132 | verify('rien'); 133 | verify('un'); 134 | verify('homme'); 135 | verify('femme'); 136 | verify('7 homme'); 137 | verify('7 dollars Canadiens'); 138 | verify('5 certaine devise ou autre.'); 139 | verify('1 dollar Canadien'); 140 | verify('2 dollars Canadiens'); 141 | verify('1 chose:'); 142 | verify('2 choses:'); 143 | verify('Bonjour tout le monde'); 144 | verify('Bonjour tout le monde'); 145 | verify('louer'); 146 | verify('loyer'); 147 | // Using a non-French format for the currency to test interpolation. 148 | verify('Cinq sous est US\$0.05'); 149 | verify(r"interessant (fr): '<>{}= +-_$()&^%$#@!~`'"); 150 | verify('Ce message devrait être extractible'); 151 | verify('This message should skip extraction'); 152 | verify('Extraction skipped plural one'); 153 | verify('Extraction skipped gender f'); 154 | verify('Extraction skipped select specified Bob!'); 155 | verify('This message should skip translation'); 156 | verify('-------------------------------------------'); 157 | 158 | // German translations. 159 | verify('Printing messages for de_DE'); 160 | verify('Dies ist eine Nachricht'); 161 | verify('Eine weitere Meldung mit dem Parameter hello'); 162 | verify('Zeichen, die Flucht benötigen, zB Schrägstriche \\ Dollar ' 163 | '\${ (geschweiften Klammern sind ok) und xml reservierte Zeichen <& und ' 164 | 'Zitate " Parameter 1, 2 und 3'); 165 | verify('Dieser String erstreckt sich über mehrere ' 166 | 'Zeilen erstrecken.'); 167 | verify('1, b, [c, d]'); 168 | verify('"Sogenannt"'); 169 | // This is correct, the message is forced to French, even in a German locale. 170 | verify('Cette chaîne est toujours traduit'); 171 | verify( 172 | 'Interpolation ist schwierig, wenn es einen Satz wie dieser endet this.'); 173 | verify('Dies ergibt sich aus einer Methode'); 174 | verify('Diese Methode ist nicht eine Lambda'); 175 | verify('Dies ergibt sich aus einer statischen Methode'); 176 | verify('This is missing some translations'); 177 | verify('Antike griechische Galgenmännchen Zeichen: 𐅆𐅇'); 178 | verify('Escapes: '); 179 | verify('\r\f\b\t\v.'); 180 | 181 | verify('Ist Null Plural?'); 182 | verify('Dies ist einmalig'); 183 | verify('Dies ist Plural (2).'); 184 | verify('Dies ist Plural (3).'); 185 | verify('Dies ist Plural (4).'); 186 | verify('Dies ist Plural (5).'); 187 | verify('Dies ist Plural (6).'); 188 | verify('Dies ist Plural (7).'); 189 | verify('Dies ist Plural (8).'); 190 | verify('Dies ist Plural (9).'); 191 | verify('Dies ist Plural (10).'); 192 | verify('Dies ist Plural (11).'); 193 | verify('Dies ist Plural (20).'); 194 | verify('Dies ist Plural (100).'); 195 | verify('Dies ist Plural (101).'); 196 | verify('Dies ist Plural (100000).'); 197 | verify('Alice ging zu ihrem house'); 198 | verify('Bob ging zu seinem house'); 199 | verify('cat ging zu seinem litter box'); 200 | verify('Alice, Bob gingen zum magasin'); 201 | verify('Alice ging in dem magasin'); 202 | verify('Niemand ging zu magasin'); 203 | verify('Bob, Bob gingen zum magasin'); 204 | verify('Alice, Alice gingen zum magasin'); 205 | verify('Null'); 206 | verify('ein'); 207 | verify('Mann'); 208 | verify('Frau'); 209 | verify('7 Mann'); 210 | verify('7 Kanadischen dollar'); 211 | verify('5 einige Währung oder anderen.'); 212 | verify('1 Kanadischer dollar'); 213 | verify('2 Kanadischen dollar'); 214 | verify('eins:'); 215 | verify('2 Dinge:'); 216 | verify('Hallo Welt'); 217 | verify('Hallo Welt'); 218 | verify('mieten'); 219 | verify('Miete'); 220 | verify('Fünf Cent US \$ 0.05'); 221 | verify(r"interessant (de): '<>{}= +-_$()&^%$#@!~`'"); 222 | verify('Diese Nachricht sollte extrahierbar sein'); 223 | verify('This message should skip extraction'); 224 | verify('Extraction skipped plural one'); 225 | verify('Extraction skipped gender f'); 226 | verify('Extraction skipped select specified Bob!'); 227 | verify('This message should skip translation'); 228 | 229 | if (lineIterator.moveNext()) { 230 | throw 'more messages than expected'; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /test/message_parser_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:intl_translation/src/message_parser.dart'; 6 | import 'package:intl_translation/src/messages/composite_message.dart'; 7 | import 'package:intl_translation/src/messages/literal_string_message.dart'; 8 | import 'package:intl_translation/src/messages/message.dart'; 9 | import 'package:intl_translation/src/messages/submessages/gender.dart'; 10 | import 'package:intl_translation/src/messages/submessages/plural.dart'; 11 | import 'package:intl_translation/src/messages/submessages/select.dart'; 12 | import 'package:intl_translation/src/messages/variable_substitution_message.dart'; 13 | import 'package:test/test.dart'; 14 | 15 | void main() { 16 | test('Test escaping', () { 17 | testEscaping(r"interessant (fr): '<>{}= +-_$()&^%$#@!~`'", 18 | r"interessant (fr): \'<>{}= +-_\$()&^%\$#@!~`\'"); 19 | testEscaping('Escapes: \n\r\f\b\t\v.', r'Escapes: \n\r\f\b\t\v.'); 20 | testEscaping("te'{st'}", 'te{st}'); 21 | testEscaping("'{st'}te", '{st}te'); 22 | testEscaping('{st}', r'${st}'); 23 | testEscaping('{st}ts', r'${st}ts'); 24 | testEscaping('te\${st}', r'te\$${st}'); 25 | testEscaping('te{st}', r'te${st}'); 26 | testEscaping("tes''t", "tes\\'t"); 27 | testEscaping("t'e'''{st'}", "t\\'e\\'{st}"); 28 | }); 29 | 30 | test('Gender', () { 31 | var input = 32 | '''{gender_of_host, select, female {test} male {test2} other {test3}}'''; 33 | var parsedMessage = MessageParser(input).pluralGenderSelectParse(); 34 | Message expectedMessage = Gender.from( 35 | 'gender_of_host', 36 | [ 37 | ['female', 'test'], 38 | ['male', 'test2'], 39 | ['other', 'test3'], 40 | ], 41 | null, 42 | ); 43 | expect(parsedMessage.toCode(), expectedMessage.toCode()); 44 | }); 45 | 46 | test('Plural', () { 47 | var input = '''{num_guests, plural, 48 | =0 {Anna does not give a party.} 49 | =1 {Anna invites Bob to her party.} 50 | =2 {Anna invites Bob and one other person to her party.} 51 | other {Anna invites Bob and 2 other people to her party.}}'''; 52 | var parsedMessage = MessageParser(input).pluralGenderSelectParse(); 53 | Message expectedMessage = Plural.from( 54 | 'num_guests', 55 | [ 56 | ['=0', 'Anna does not give a party.'], 57 | ['=1', 'Anna invites Bob to her party.'], 58 | ['=2', 'Anna invites Bob and one other person to her party.'], 59 | ['other', 'Anna invites Bob and 2 other people to her party.'], 60 | ], 61 | null, 62 | ); 63 | expect(parsedMessage.toCode(), expectedMessage.toCode()); 64 | }); 65 | 66 | test('Select', () { 67 | var input = '''{selector, select, 68 | type1 {Anna does not give a party.} 69 | type2 {Anna invites Bob to her party.}}'''; 70 | var parsedMessage = MessageParser(input).pluralGenderSelectParse(); 71 | Message expectedMessage = Select.from( 72 | 'selector', 73 | [ 74 | ['type1', 'Anna does not give a party.'], 75 | ['type2', 'Anna invites Bob to her party.'], 76 | ], 77 | null, 78 | ); 79 | expect(parsedMessage.toCode(), expectedMessage.toCode()); 80 | }); 81 | 82 | test('Plural with args', () { 83 | var input = '''{num_guests, plural, 84 | =0 {{host} does not give a party.} 85 | =1 {{host} invites {guest} to her party.} 86 | =2 {{host} invites {guest} and one other person to her party.} 87 | other {{host} invites {guest} and # other people to her party.}}'''; 88 | var parsedMessage = MessageParser(input).pluralGenderSelectParse(); 89 | Message expectedMessage = Plural.from( 90 | 'num_guests', 91 | [ 92 | [ 93 | '=0', 94 | CompositeMessage( 95 | [ 96 | VariableSubstitution.named('host'), 97 | LiteralString(' does not give a party.'), 98 | ], 99 | ) 100 | ], 101 | [ 102 | '=1', 103 | CompositeMessage( 104 | [ 105 | VariableSubstitution.named('host'), 106 | LiteralString(' invites '), 107 | VariableSubstitution.named('guest'), 108 | LiteralString(' to her party.'), 109 | ], 110 | ) 111 | ], 112 | [ 113 | '=2', 114 | CompositeMessage( 115 | [ 116 | VariableSubstitution.named('host'), 117 | LiteralString(' invites '), 118 | VariableSubstitution.named('guest'), 119 | LiteralString(' and one other person to her party.'), 120 | ], 121 | ) 122 | ], 123 | [ 124 | 'other', 125 | CompositeMessage( 126 | [ 127 | VariableSubstitution.named('host'), 128 | LiteralString(' invites '), 129 | VariableSubstitution.named('guest'), 130 | LiteralString(' and # other people to her party.'), 131 | ], 132 | ) 133 | ], 134 | ], 135 | null, 136 | ); 137 | expect(parsedMessage.toCode(), expectedMessage.toCode()); 138 | }); 139 | } 140 | 141 | void testEscaping(String actual, String expected) { 142 | expect(MessageParser(actual).nonIcuMessageParse().toCode(), expected); 143 | } 144 | -------------------------------------------------------------------------------- /test/two_components/README.txt: -------------------------------------------------------------------------------- 1 | A test for a separate component with its own localized messages, different 2 | from the application. 3 | 4 | The translation ARB files are hard-coded, and the generated files are 5 | checked in, so there's minimum infrastructure required, but if the 6 | files need to be regenerated, then the regenerate.sh script will need 7 | to be run. 8 | -------------------------------------------------------------------------------- /test/two_components/app_messages_all.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | export 'app_messages_all_locales.dart' show initializeMessages; 6 | -------------------------------------------------------------------------------- /test/two_components/app_messages_all_locales.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | // Ignore issues from commonly used lints in this file. 6 | // ignore_for_file:implementation_imports, file_names 7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering 8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment 9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases 10 | // ignore_for_file:comment_references 11 | 12 | import 'package:intl/intl.dart'; 13 | import 'package:intl/message_lookup_by_library.dart'; 14 | import 'package:intl/src/intl_helpers.dart'; 15 | 16 | import 'app_messages_fr.dart' deferred as messages_fr; 17 | 18 | typedef Future LibraryLoader(); 19 | Map _deferredLibraries = { 20 | 'fr': messages_fr.loadLibrary, 21 | }; 22 | 23 | MessageLookupByLibrary? _findExact(String localeName) { 24 | switch (localeName) { 25 | case 'fr': 26 | return messages_fr.messages; 27 | default: 28 | return null; 29 | } 30 | } 31 | 32 | /// User programs should call this before using [localeName] for messages. 33 | Future initializeMessages(String? localeName) async { 34 | var availableLocale = Intl.verifiedLocale( 35 | localeName, (locale) => _deferredLibraries[locale] != null, 36 | onFailure: (_) => null); 37 | if (availableLocale == null) { 38 | return Future.value(false); 39 | } 40 | var lib = _deferredLibraries[availableLocale]; 41 | await (lib == null ? Future.value(false) : lib()); 42 | initializeInternalMessageLookup(() => CompositeMessageLookup()); 43 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); 44 | return Future.value(true); 45 | } 46 | 47 | bool _messagesExistFor(String locale) { 48 | try { 49 | return _findExact(locale) != null; 50 | } catch (e) { 51 | return false; 52 | } 53 | } 54 | 55 | MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { 56 | var actualLocale = 57 | Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); 58 | if (actualLocale == null) return null; 59 | return _findExact(actualLocale); 60 | } 61 | -------------------------------------------------------------------------------- /test/two_components/app_messages_fr.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that provides messages for a fr locale. All the 3 | // messages from the main program should be duplicated here with the same 4 | // function name. 5 | 6 | // Ignore issues from commonly used lints in this file. 7 | // ignore_for_file:unnecessary_brace_in_string_interps 8 | // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering 9 | // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases 10 | // ignore_for_file:unused_import, file_names 11 | 12 | import 'package:intl/intl.dart'; 13 | import 'package:intl/message_lookup_by_library.dart'; 14 | 15 | final messages = MessageLookup(); 16 | 17 | typedef String? MessageIfAbsent(String? messageStr, List? args); 18 | 19 | class MessageLookup extends MessageLookupByLibrary { 20 | @override 21 | String get localeName => 'fr'; 22 | 23 | @override 24 | final Map messages = 25 | _notInlinedMessages(_notInlinedMessages); 26 | 27 | static Map _notInlinedMessages(_) => { 28 | 'Hello from application': 29 | MessageLookupByLibrary.simpleMessage('Bonjour de l\'application') 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /test/two_components/app_translation_getfromthelocale.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "fr", 3 | "Hello from application": "Bonjour de l'application" 4 | } 5 | -------------------------------------------------------------------------------- /test/two_components/component.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// A component which should have its own separate messages, with their own 6 | /// translations. 7 | import 'package:intl/intl.dart'; 8 | 9 | import 'component_messages_all.dart'; 10 | 11 | /// We can just define a normal message, in which case we'll want to pick up 12 | /// our special locale from the zone variable. 13 | String _message1() => Intl.message('Hello from component', desc: 'hi'); 14 | 15 | /// Or we can explicitly code our locale. 16 | String _message2() => Intl.message('Explicit locale', 17 | name: '_message2', desc: 'message two', locale: myParticularLocale); 18 | 19 | String get myParticularLocale => '${Intl.defaultLocale}_$mySuffix'; 20 | 21 | const mySuffix = 'xyz123'; 22 | 23 | /// We can wrap all of our top-level API calls in a zone that stores the locale. 24 | dynamic componentApiFunction() => 25 | Intl.withLocale(myParticularLocale, _message1); 26 | 27 | dynamic directApiCall() => _message2(); 28 | 29 | Future initComponent() async => 30 | await initializeMessages(myParticularLocale); 31 | -------------------------------------------------------------------------------- /test/two_components/component_messages_all.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | export 'component_messages_all_locales.dart' show initializeMessages; 6 | -------------------------------------------------------------------------------- /test/two_components/component_messages_all_locales.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | // Ignore issues from commonly used lints in this file. 6 | // ignore_for_file:implementation_imports, file_names 7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering 8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment 9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases 10 | // ignore_for_file:comment_references 11 | 12 | import 'package:intl/intl.dart'; 13 | import 'package:intl/message_lookup_by_library.dart'; 14 | import 'package:intl/src/intl_helpers.dart'; 15 | 16 | import 'component_messages_fr_xyz123.dart' deferred as messages_fr_xyz123; 17 | 18 | typedef Future LibraryLoader(); 19 | Map _deferredLibraries = { 20 | 'fr_xyz123': messages_fr_xyz123.loadLibrary, 21 | }; 22 | 23 | MessageLookupByLibrary? _findExact(String localeName) { 24 | switch (localeName) { 25 | case 'fr_xyz123': 26 | return messages_fr_xyz123.messages; 27 | default: 28 | return null; 29 | } 30 | } 31 | 32 | /// User programs should call this before using [localeName] for messages. 33 | Future initializeMessages(String? localeName) async { 34 | var availableLocale = Intl.verifiedLocale( 35 | localeName, (locale) => _deferredLibraries[locale] != null, 36 | onFailure: (_) => null); 37 | if (availableLocale == null) { 38 | return Future.value(false); 39 | } 40 | var lib = _deferredLibraries[availableLocale]; 41 | await (lib == null ? Future.value(false) : lib()); 42 | initializeInternalMessageLookup(() => CompositeMessageLookup()); 43 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); 44 | return Future.value(true); 45 | } 46 | 47 | bool _messagesExistFor(String locale) { 48 | try { 49 | return _findExact(locale) != null; 50 | } catch (e) { 51 | return false; 52 | } 53 | } 54 | 55 | MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { 56 | var actualLocale = 57 | Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); 58 | if (actualLocale == null) return null; 59 | return _findExact(actualLocale); 60 | } 61 | -------------------------------------------------------------------------------- /test/two_components/component_messages_fr_xyz123.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that provides messages for a fr_xyz123 locale. All the 3 | // messages from the main program should be duplicated here with the same 4 | // function name. 5 | 6 | // Ignore issues from commonly used lints in this file. 7 | // ignore_for_file:unnecessary_brace_in_string_interps 8 | // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering 9 | // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases 10 | // ignore_for_file:unused_import, file_names 11 | 12 | import 'package:intl/intl.dart'; 13 | import 'package:intl/message_lookup_by_library.dart'; 14 | 15 | final messages = MessageLookup(); 16 | 17 | typedef String? MessageIfAbsent(String? messageStr, List? args); 18 | 19 | class MessageLookup extends MessageLookupByLibrary { 20 | @override 21 | String get localeName => 'fr_xyz123'; 22 | 23 | @override 24 | final Map messages = 25 | _notInlinedMessages(_notInlinedMessages); 26 | 27 | static Map _notInlinedMessages(_) => { 28 | 'Hello from component': 29 | MessageLookupByLibrary.simpleMessage('Bonjour du composant'), 30 | '_message2': MessageLookupByLibrary.simpleMessage('Locale explicite') 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /test/two_components/component_translation_fr.arb: -------------------------------------------------------------------------------- 1 | { 2 | "_locale": "fr_xyz123", 3 | "Hello from component": "Bonjour du composant", 4 | "_message2": "Locale explicite" 5 | } 6 | -------------------------------------------------------------------------------- /test/two_components/initialize_child_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Tests initializing the fr_FR locale when we only have fr available. 6 | /// 7 | /// This is not actually related to the two components testing, but it's 8 | /// convenient to put it here because there's already a hard-coded 9 | /// message here. 10 | import 'package:intl/intl.dart'; 11 | import 'package:test/test.dart'; 12 | 13 | import 'app_messages_all.dart'; 14 | import 'main_app_test.dart'; 15 | 16 | void main() { 17 | test('Initialize sub-locale', () async { 18 | await initializeMessages('fr_FR'); 19 | Intl.withLocale( 20 | 'fr_FR', () => expect(appMessage(), "Bonjour de l'application")); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /test/two_components/main_app_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// An application using the component 6 | import 'package:intl/intl.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | import 'app_messages_all.dart'; 10 | import 'component.dart' as component; 11 | 12 | String appMessage() => Intl.message('Hello from application', desc: 'hi'); 13 | 14 | void main() async { 15 | Intl.defaultLocale = 'fr'; 16 | await initializeMessages('fr'); 17 | await component.initComponent(); 18 | test('Component has its own messages', () { 19 | expect(appMessage(), "Bonjour de l'application"); 20 | expect(component.componentApiFunction(), 'Bonjour du composant'); 21 | expect(component.directApiCall(), 'Locale explicite'); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /test/two_components/regenerate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Regenerate the messages Dart files. 4 | dart ../../bin/generate_from_arb.dart --generated-file-prefix=component_ \ 5 | --null-safety \ 6 | component.dart component_translation_fr.arb 7 | 8 | dart ../../bin/generate_from_arb.dart --generated-file-prefix=app_ \ 9 | --null-safety \ 10 | main_app_test.dart app_translation_getfromthelocale.arb 11 | --------------------------------------------------------------------------------