├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── bin ├── generate.dart ├── localizely_download.dart └── localizely_upload_main.dart ├── doc └── getting_started.md ├── example └── README.md ├── lib ├── intl_utils.dart └── src │ ├── config │ ├── config_exception.dart │ ├── credentials_config.dart │ └── pubspec_config.dart │ ├── constants │ └── constants.dart │ ├── generator │ ├── generator.dart │ ├── generator_exception.dart │ ├── intl_translation_helper.dart │ ├── label.dart │ └── templates.dart │ ├── intl_translation │ ├── extract_messages.dart │ ├── generate_localized.dart │ └── src │ │ ├── icu_parser.dart │ │ └── intl_message.dart │ ├── localizely │ ├── api │ │ ├── api.dart │ │ └── api_exception.dart │ ├── model │ │ ├── download_response.dart │ │ └── file_data.dart │ └── service │ │ ├── service.dart │ │ └── service_exception.dart │ ├── parser │ ├── icu_parser.dart │ └── message_format.dart │ └── utils │ ├── file_utils.dart │ └── utils.dart ├── pubspec.yaml └── test ├── download_response_test.dart ├── download_response_test.mocks.dart ├── label_test.dart ├── parser_test.dart └── utils_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "flutter-intl" extension will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## 2.8.10 - 2025-02-11 9 | 10 | - Update `analyzer` dependency 11 | 12 | ## 2.8.9 - 2025-01-23 13 | 14 | - Update docs 15 | 16 | - Update `dart_style` dependency 17 | Note: This update introduces 'tall style' formatting for the generated code. This new formatting style is also set to become the [default in Dart 3.7 and later versions](https://pub.dev/packages/dart_style/changelog#300). 18 | 19 | ## 2.8.8 - 2025-01-08 20 | 21 | - Update `intl` dependency 22 | 23 | - Update `archive` dependency 24 | 25 | - Fix lint warnings 26 | 27 | ## 2.8.7 - 2023-12-25 28 | 29 | - Update `intl` dependency 30 | 31 | - Fix deprecation and lint warnings 32 | 33 | ## 2.8.6 - 2023-11-22 34 | 35 | - Update `analyzer` dependency 36 | 37 | - Update `mockito` dependency 38 | 39 | ## 2.8.5 - 2023-10-13 40 | 41 | - Update `petitparser` dependency 42 | 43 | ## 2.8.4 - 2023-08-04 44 | 45 | - Add support for locale formats based on ISO 639-2/3 languages and UN-M49 regions 46 | 47 | ## 2.8.3 - 2023-06-01 48 | 49 | - Update `http` dependency 50 | 51 | - Update `.gitignore` file 52 | 53 | ## 2.8.2 - 2023-02-22 54 | 55 | - Update `intl` dependency 56 | 57 | ## 2.8.1 - 2022-11-17 58 | 59 | - Fix black frames caused by async initialization of localization messages when deferred loading is not enabled 60 | 61 | ## 2.8.0 - 2022-11-15 62 | 63 | - Update `analyzer` and `lints` dependencies 64 | 65 | ## 2.7.0 - 2022-07-07 66 | 67 | - Update `analyzer` and `petitparser` dependencies 68 | 69 | ## 2.6.1 - 2022-01-14 70 | 71 | - Improve error handling for invalid config files 72 | 73 | - Update `analyzer` dependency 74 | 75 | ## 2.6.0 - 2021-12-24 76 | 77 | - Add custom date-time format option 78 | 79 | ## 2.5.1 - 2021-11-08 80 | 81 | - Fix optional parameters string issue 82 | 83 | ## 2.5.0 - 2021-11-05 84 | 85 | - Add support for json strings 86 | 87 | - Add number and date-time format options 88 | 89 | - Move from pedantic to lints package 90 | 91 | ## 2.4.1 - 2021-10-01 92 | 93 | - Update `analyzer` dependency 94 | 95 | ## 2.4.0 - 2021-07-13 96 | 97 | - Add support for tagging uploaded string keys to Localizely 98 | 99 | - Add support for download tagged string keys from Localizely 100 | 101 | - Fix issue with translations that contain tags 102 | 103 | ## 2.3.0 - 2021-05-18 104 | 105 | - Add missing upload and download command line arg options 106 | 107 | ## 2.2.0 - 2021-04-27 108 | 109 | - Add support for compound messages 110 | 111 | - Format generated files 112 | 113 | - Add missing return types in generated files 114 | 115 | - Ignore avoid_escaping_inner_quotes lint rule in generated files 116 | 117 | - Fix escaping special chars 118 | 119 | ## 2.1.0 - 2021-03-09 120 | 121 | - Make `of(context)` non-null 122 | 123 | ## 2.0.0 - 2021-03-05 124 | 125 | - Migrate to null-safety 126 | 127 | ## 1.9.0 - 2020-10-19 128 | 129 | - Make generated directory path configurable 130 | 131 | - Extend configuration with deferred loading parameter 132 | 133 | - Ignore common lint warnings for the `l10n.dart` file 134 | 135 | ## 1.8.0 - 2020-10-09 136 | 137 | - Extend Localizely configuration with the download_empty_as parameter used for setting a fallback for empty translations during download 138 | 139 | ## 1.7.0 - 2020-09-29 140 | 141 | - Make ARB directory path configurable 142 | 143 | ## 1.6.5 - 2020-09-18 144 | 145 | - Fix unzipping issues during download 146 | 147 | ## 1.6.4 - 2020-09-03 148 | 149 | - Extend Localizely configuration with the branch parameter 150 | 151 | ## 1.6.3 - 2020-08-06 152 | 153 | - Update `petitparser` dependency 154 | 155 | ## 1.6.2 - 2020-06-22 156 | 157 | - Update file logic 158 | 159 | - Code cleanup 160 | 161 | ## 1.6.1 - 2020-06-17 162 | 163 | - Add useful error message for invalid ARB files 164 | 165 | ## 1.6.0 - 2020-06-03 166 | 167 | - Reference the key without passing the context 168 | 169 | - Provide default value of term as Dart doc on getters in `l10n.dart` file 170 | 171 | - Suppress lint warnings for getters that do not follow the lowerCamelCase style within `l10n.dart` file 172 | 173 | ## 1.5.0 - 2020-05-11 174 | 175 | - Add support for the Localizely SDK 176 | 177 | - Fix lint warnings for the `l10n.dart` file 178 | 179 | ## 1.4.0 - 2020-05-04 180 | 181 | - Add integration with Localizely 182 | 183 | ## 1.3.0 - 2020-04-21 184 | 185 | - Support select messages 186 | 187 | - Make order of supported locales consistent 188 | 189 | ## 1.2.2 - 2020-04-13 190 | 191 | - Make generated files consistent 192 | 193 | ## 1.2.1 - 2020-03-30 194 | 195 | - Update order of supported locales 196 | 197 | - Replace `dynamic` with concrete type for generated Dart methods 198 | 199 | - Handle empty plural and gender forms 200 | 201 | - Update `l10n.dart` file template (remove `localeName`) 202 | 203 | ## 1.2.0 - 2020-03-16 204 | 205 | - Add support for locales with script code 206 | 207 | - Fix locale loading issue when country code is not provided 208 | 209 | ## 1.1.0 - 2020-02-04 210 | 211 | - Make main locale configurable 212 | 213 | ## 1.0.2 - 2020-01-21 214 | 215 | - Add curly-braces around placeholders when they are followed by alphanumeric or underscore character 216 | 217 | ## 1.0.1 - 2020-01-15 218 | 219 | - Fix trailing comma issue (l10n.dart) 220 | 221 | - Remove unused dependencies 222 | 223 | ## 1.0.0 - 2020-01-12 224 | 225 | - Initial release 226 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 The Localizely Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | * Neither the name of Localizely Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Intl Utils 2 | 3 | [![pub package](https://img.shields.io/pub/v/intl_utils.svg)](https://pub.dev/packages/intl_utils) 4 | [![Twitter Follow](https://img.shields.io/twitter/follow/localizely?label=Follow%20us&style=social)](https://twitter.com/intent/follow?screen_name=localizely) 5 | 6 | Dart package that creates a binding between your translations from .arb files and your Flutter app. It generates boilerplate code for official Dart Intl library and adds auto-complete for keys in Dart code. 7 | 8 | ## Usage 9 | 10 | You can use this package directly (i.e. for Continuous Integration tools or via CLI) or leave it to [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=localizely.flutter-intl) or [IntelliJ/Android Studio](https://plugins.jetbrains.com/plugin/13666-flutter-intl) plugins to run it automatically whenever you modify ARB files. 11 | 12 | Follow these steps to get started: 13 | 14 | ### Configure package 15 | 16 | Add package configuration to your `pubspec.yaml` file. Here is a full configuration for the package: 17 | 18 |
19 | flutter_intl:
20 |   enabled: true # Required. Must be set to true to activate the package. Default: false
21 |   class_name: S # Optional. Sets the name for the generated localization class. Default: S
22 |   main_locale: en # Optional. Sets the main locale used for generating localization files. Provided value should consist of language code and optional script and country codes separated with underscore (e.g. 'en', 'en_GB', 'zh_Hans', 'zh_Hans_CN'). Default: en
23 |   arb_dir: lib/l10n # Optional. Sets the directory of your ARB resource files. Provided value should be a valid path on your system. Default: lib/l10n
24 |   output_dir: lib/generated # Optional. Sets the directory of generated localization files. Provided value should be a valid path on your system. Default: lib/generated
25 |   use_deferred_loading: false # Optional. Must be set to true to generate localization code that is loaded with deferred loading. Default: false
26 |   localizely: # Optional settings if you use Localizely platform. Read more: https://localizely.com/blog/flutter-localization-step-by-step/?tab=automated-using-flutter-intl
27 |     project_id: # Get it from the https://app.localizely.com/projects page.
28 |     branch: # Get it from the “Branches” page on the Localizely platform, in case branching is enabled and you want to use a non-main branch.
29 |     upload_overwrite: # Set to true if you want to overwrite translations with upload. Default: false
30 |     upload_as_reviewed: # Set to true if you want to mark uploaded translations as reviewed. Default: false
31 |     upload_tag_added: # Optional list of tags to add to new translations with upload (e.g. ['new', 'New translation']). Default: []
32 |     upload_tag_updated: # Optional list of tags to add to updated translations with upload (e.g. ['updated', 'Updated translation']). Default: []
33 |     upload_tag_removed: # Optional list of tags to add to removed translations with upload (e.g. ['removed', 'Removed translation']). Default: []
34 |     download_empty_as: # Set to empty|main|skip, to configure how empty translations should be exported from the Localizely platform. Default: empty
35 |     download_include_tags: # Optional list of tags to be downloaded (e.g. ['include', 'Include key']). If not set, all string keys will be considered for download. Default: []
36 |     download_exclude_tags: # Optional list of tags to be excluded from download (e.g. ['exclude', 'Exclude key']). If not set, all string keys will be considered for download. Default: []
37 |     ota_enabled: # Set to true if you want to use Localizely Over-the-air functionality. Default: false
38 | 
39 | 40 | ### Add ARB files 41 | 42 | Add one ARB file for each locale you need to support in your Flutter app. 43 | Add them to `lib/l10n` folder inside your project, and name them in a following way: `intl_.arb`. 44 | For example: `intl_en.arb` or `intl_en_GB.arb`. 45 | You can also change the ARB folder from `lib/l10n` to a custom directory by adding the `arb_dir` line in your `pubspec.yaml` file. 46 | 47 | If you wonder how to format key-values content inside ARB files, [here](https://localizely.com/flutter-arb/) is detailed explanation. 48 | 49 | ### Run command 50 | 51 | To generate boilerplate code for localization, run the `generate` program inside directory where your `pubspec.yaml` file is located: 52 | 53 | dart run intl_utils:generate 54 | 55 | This will produce files inside `lib/generated` directory. 56 | You can also change the output folder from `lib/generated` to a custom directory by adding the `output_dir` line in your `pubspec.yaml` file. 57 | 58 | ### Integration with Localizely 59 | 60 | #### Upload main ARB file 61 | 62 | dart run intl_utils:localizely_upload_main [--project-id --api-token --arb-dir --main-locale --branch --[no-]upload-overwrite --[no-]upload-as-reviewed] --upload-tag-added --upload-tag-updated --upload-tag-removed 63 | 64 | This will upload your main ARB file to Localizely.
All args are optional. If not provided, the `intl_utils` will use configuration from the `pubspec.yaml` file or default values (check the [Configure package](#configure-package) section for more details). 65 | 66 | #### Download ARB files 67 | 68 | dart run intl_utils:localizely_download [--project-id --api-token --arb-dir --download-empty-as --download-include-tags --download-exclude-tags --branch ] 69 | 70 | This will download all available ARB files from the Localizely platform and put them under `arb-dir` folder.
All args are optional. If not provided, the `intl_utils` will use configuration from the `pubspec.yaml` file or default values (check the [Configure package](#configure-package) section for more details). 71 | 72 | Notes: 73 | Argument `project-id` can be omitted if `pubspec.yaml` file contains `project_id` configuration under `flutter_intl/localizely` section. 74 | Argument `api-token` can be omitted if `~/.localizely/credentials.yaml` file contains `api_token` configuration (e.g. `api_token: xxxxxx`). 75 | Optional argument `arb-dir` has the value `lib/l10n` as default and needs only to be set, if you want to place your ARB files in a custom directory. 76 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | analyzer: 4 | exclude: 5 | - test/*.mocks.dart 6 | -------------------------------------------------------------------------------- /bin/generate.dart: -------------------------------------------------------------------------------- 1 | library intl_utils; 2 | 3 | import 'package:intl_utils/intl_utils.dart'; 4 | import 'package:intl_utils/src/generator/generator_exception.dart'; 5 | import 'package:intl_utils/src/utils/utils.dart'; 6 | 7 | Future main(List args) async { 8 | try { 9 | var generator = Generator(); 10 | await generator.generateAsync(); 11 | } on GeneratorException catch (e) { 12 | exitWithError(e.message); 13 | } catch (e) { 14 | exitWithError('Failed to generate localization files.\n$e'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /bin/localizely_download.dart: -------------------------------------------------------------------------------- 1 | library intl_utils; 2 | 3 | import 'dart:io'; 4 | 5 | import 'package:args/args.dart' as args; 6 | import 'package:intl_utils/src/config/config_exception.dart'; 7 | import 'package:intl_utils/src/config/credentials_config.dart'; 8 | import 'package:intl_utils/src/config/pubspec_config.dart'; 9 | import 'package:intl_utils/src/constants/constants.dart'; 10 | import 'package:intl_utils/src/localizely/api/api_exception.dart'; 11 | import 'package:intl_utils/src/localizely/service/service.dart'; 12 | import 'package:intl_utils/src/localizely/service/service_exception.dart'; 13 | import 'package:intl_utils/src/utils/file_utils.dart'; 14 | import 'package:intl_utils/src/utils/utils.dart'; 15 | 16 | Future main(List arguments) async { 17 | late String? projectId; 18 | late String? apiToken; 19 | late String arbDir; 20 | late String downloadEmptyAs; 21 | late List? downloadIncludeTags; 22 | late List? downloadExcludeTags; 23 | late String? branch; 24 | 25 | final argParser = args.ArgParser(); 26 | 27 | try { 28 | final pubspecConfig = PubspecConfig(); 29 | final credentialsConfig = CredentialsConfig(); 30 | 31 | argParser 32 | ..addFlag( 33 | 'help', 34 | abbr: 'h', 35 | help: 'Print this usage information.', 36 | negatable: false, 37 | defaultsTo: false, 38 | ) 39 | ..addOption( 40 | 'project-id', 41 | help: 'Localizely project ID.', 42 | callback: ((x) => projectId = x), 43 | defaultsTo: pubspecConfig.localizelyConfig?.projectId, 44 | ) 45 | ..addOption( 46 | 'api-token', 47 | help: 'Localizely API token.', 48 | callback: ((x) => apiToken = x), 49 | ) 50 | ..addOption( 51 | 'arb-dir', 52 | help: 'Directory of the arb files.', 53 | callback: ((x) => arbDir = x!), 54 | defaultsTo: pubspecConfig.arbDir ?? defaultArbDir, 55 | ) 56 | ..addOption( 57 | 'branch', 58 | help: 59 | 'Get it from the “Branches” page on the Localizely platform, in case branching is enabled and you want to use a non-main branch.', 60 | callback: ((x) => branch = x), 61 | defaultsTo: pubspecConfig.localizelyConfig?.branch, 62 | ) 63 | ..addOption( 64 | 'download-empty-as', 65 | help: 66 | "Config parameter 'download_empty_as' expects one of the following values: 'empty', 'main' or 'skip'.", 67 | callback: ((x) => downloadEmptyAs = x!), 68 | defaultsTo: pubspecConfig.localizelyConfig?.downloadEmptyAs ?? 69 | defaultDownloadEmptyAs, 70 | ) 71 | ..addMultiOption( 72 | 'download-include-tags', 73 | help: 'Optional list of tags to be downloaded.', 74 | callback: ((x) => downloadIncludeTags = x), 75 | defaultsTo: pubspecConfig.localizelyConfig?.downloadIncludeTags, 76 | ) 77 | ..addMultiOption( 78 | 'download-exclude-tags', 79 | help: 'Optional list of tags to be excluded from download.', 80 | callback: ((x) => downloadExcludeTags = x), 81 | defaultsTo: pubspecConfig.localizelyConfig?.downloadExcludeTags, 82 | ); 83 | 84 | final argResults = argParser.parse(arguments); 85 | if (argResults['help'] == true) { 86 | stdout.writeln(argParser.usage); 87 | exit(0); 88 | } 89 | 90 | if (projectId == null) { 91 | throw ConfigException( 92 | "Argument 'project-id' was not provided, nor 'project_id' config was set within the 'flutter_intl/localizely' section of the 'pubspec.yaml' file."); 93 | } 94 | 95 | apiToken ??= credentialsConfig.apiToken; 96 | if (apiToken == null) { 97 | throw ConfigException( 98 | "Argument 'api-token' was not provided, nor 'api_token' config was set within the '${getLocalizelyCredentialsFilePath()}' file."); 99 | } 100 | 101 | if (!isValidDownloadEmptyAsParam(downloadEmptyAs)) { 102 | throw ConfigException( 103 | "Config parameter 'download_empty_as' expects one of the following values: 'empty', 'main' or 'skip'.", 104 | ); 105 | } 106 | 107 | await LocalizelyService.download(projectId!, apiToken!, arbDir, 108 | downloadEmptyAs, branch, downloadIncludeTags, downloadExcludeTags); 109 | } on args.ArgParserException catch (e) { 110 | exitWithError('${e.message}\n\n${argParser.usage}'); 111 | } on ConfigException catch (e) { 112 | exitWithError(e.message); 113 | } on ServiceException catch (e) { 114 | exitWithError(e.message); 115 | } on ApiException catch (e) { 116 | exitWithError(e.getFormattedMessage()); 117 | } catch (e) { 118 | exitWithError('Failed to download ARB files from Localizely.\n$e'); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /bin/localizely_upload_main.dart: -------------------------------------------------------------------------------- 1 | library intl_utils; 2 | 3 | import 'dart:io'; 4 | 5 | import 'package:args/args.dart' as args; 6 | import 'package:intl_utils/src/config/config_exception.dart'; 7 | import 'package:intl_utils/src/config/credentials_config.dart'; 8 | import 'package:intl_utils/src/config/pubspec_config.dart'; 9 | import 'package:intl_utils/src/constants/constants.dart'; 10 | import 'package:intl_utils/src/localizely/api/api_exception.dart'; 11 | import 'package:intl_utils/src/localizely/service/service.dart'; 12 | import 'package:intl_utils/src/localizely/service/service_exception.dart'; 13 | import 'package:intl_utils/src/utils/file_utils.dart'; 14 | import 'package:intl_utils/src/utils/utils.dart'; 15 | 16 | Future main(List arguments) async { 17 | late String? projectId; 18 | late String? apiToken; 19 | late String arbDir; 20 | late String mainLocale; 21 | late String? branch; 22 | late bool uploadOverwrite; 23 | late bool uploadAsReviewed; 24 | late List? uploadTagAdded; 25 | late List? uploadTagUpdated; 26 | late List? uploadTagRemoved; 27 | 28 | final argParser = args.ArgParser(); 29 | 30 | try { 31 | final pubspecConfig = PubspecConfig(); 32 | final credentialsConfig = CredentialsConfig(); 33 | 34 | argParser 35 | ..addFlag( 36 | 'help', 37 | abbr: 'h', 38 | help: 'Print this usage information.', 39 | negatable: false, 40 | defaultsTo: false, 41 | ) 42 | ..addOption( 43 | 'project-id', 44 | help: 'Localizely project ID.', 45 | callback: ((x) => projectId = x), 46 | defaultsTo: pubspecConfig.localizelyConfig?.projectId, 47 | ) 48 | ..addOption( 49 | 'api-token', 50 | help: 'Localizely API token.', 51 | callback: ((x) => apiToken = x), 52 | ) 53 | ..addOption( 54 | 'arb-dir', 55 | help: 'Directory of the arb files.', 56 | callback: ((x) => arbDir = x!), 57 | defaultsTo: pubspecConfig.arbDir ?? defaultArbDir, 58 | ) 59 | ..addOption( 60 | 'main-locale', 61 | help: 62 | "Optional. Sets the main locale used for generating localization files. Provided value should consist of language code and optional script and country codes separated with underscore (e.g. 'en', 'en_GB', 'zh_Hans', 'zh_Hans_CN')", 63 | callback: ((x) => mainLocale = x!), 64 | defaultsTo: pubspecConfig.mainLocale ?? defaultMainLocale, 65 | ) 66 | ..addOption( 67 | 'branch', 68 | help: 69 | 'Get it from the “Branches” page on the Localizely platform, in case branching is enabled and you want to use a non-main branch.', 70 | callback: ((x) => branch = x), 71 | defaultsTo: pubspecConfig.localizelyConfig?.branch, 72 | ) 73 | ..addFlag( 74 | 'upload-overwrite', 75 | help: 'Set to true if you want to overwrite translations with upload.', 76 | callback: ((x) => uploadOverwrite = x), 77 | defaultsTo: pubspecConfig.localizelyConfig?.uploadOverwrite ?? 78 | defaultUploadOverwrite, 79 | ) 80 | ..addFlag( 81 | 'upload-as-reviewed', 82 | help: 83 | 'Set to true if you want to mark uploaded translations as reviewed.', 84 | callback: ((x) => uploadAsReviewed = x), 85 | defaultsTo: pubspecConfig.localizelyConfig?.uploadAsReviewed ?? 86 | defaultUploadAsReviewed, 87 | ) 88 | ..addMultiOption( 89 | 'upload-tag-added', 90 | help: 'Optional list of tags to add to new translations with upload.', 91 | callback: ((x) => uploadTagAdded = x), 92 | defaultsTo: pubspecConfig.localizelyConfig?.uploadTagAdded, 93 | ) 94 | ..addMultiOption( 95 | 'upload-tag-updated', 96 | help: 97 | 'Optional list of tags to add to updated translations with upload.', 98 | callback: ((x) => uploadTagUpdated = x), 99 | defaultsTo: pubspecConfig.localizelyConfig?.uploadTagUpdated, 100 | ) 101 | ..addMultiOption( 102 | 'upload-tag-removed', 103 | help: 104 | 'Optional list of tags to add to removed translations with upload.', 105 | callback: ((x) => uploadTagRemoved = x), 106 | defaultsTo: pubspecConfig.localizelyConfig?.uploadTagRemoved, 107 | ); 108 | 109 | final argResults = argParser.parse(arguments); 110 | if (argResults['help'] == true) { 111 | stdout.writeln(argParser.usage); 112 | exit(0); 113 | } 114 | 115 | if (projectId == null) { 116 | throw ConfigException( 117 | "Argument 'project-id' was not provided, nor 'project_id' config was set within the 'flutter_intl/localizely' section of the 'pubspec.yaml' file."); 118 | } 119 | 120 | apiToken ??= credentialsConfig.apiToken; 121 | if (apiToken == null) { 122 | throw ConfigException( 123 | "Argument 'api-token' was not provided, nor 'api_token' config was set within the '${getLocalizelyCredentialsFilePath()}' file."); 124 | } 125 | 126 | if (!isValidLocale(mainLocale)) { 127 | throw ConfigException( 128 | "Config parameter 'main_locale' requires value consisted of language code and optional script and country codes separated with underscore (e.g. 'en', 'en_GB', 'zh_Hans', 'zh_Hans_CN').", 129 | ); 130 | } 131 | 132 | await LocalizelyService.uploadMainArbFile( 133 | projectId!, 134 | apiToken!, 135 | arbDir, 136 | mainLocale, 137 | branch, 138 | uploadOverwrite, 139 | uploadAsReviewed, 140 | uploadTagAdded, 141 | uploadTagUpdated, 142 | uploadTagRemoved); 143 | } on args.ArgParserException catch (e) { 144 | exitWithError('${e.message}\n\n${argParser.usage}'); 145 | } on ConfigException catch (e) { 146 | exitWithError(e.message); 147 | } on ServiceException catch (e) { 148 | exitWithError(e.message); 149 | } on ApiException catch (e) { 150 | exitWithError(e.getFormattedMessage()); 151 | } catch (e) { 152 | exitWithError('Failed to upload the main ARB file on Localizely.\n$e'); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /doc/getting_started.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localizely/intl_utils/36bad3ec481de41a9a2b44e4a2134c82e2226d88/doc/getting_started.md -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | Please see [Usage](https://pub.dev/packages/intl_utils#usage). 2 | -------------------------------------------------------------------------------- /lib/intl_utils.dart: -------------------------------------------------------------------------------- 1 | library intl_utils; 2 | 3 | export 'src/generator/generator.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/config/config_exception.dart: -------------------------------------------------------------------------------- 1 | class ConfigException implements Exception { 2 | final String message; 3 | 4 | ConfigException(this.message); 5 | 6 | @override 7 | String toString() => 'ConfigException: $message'; 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/config/credentials_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:yaml/yaml.dart' as yaml; 2 | 3 | import './config_exception.dart'; 4 | import '../utils/file_utils.dart'; 5 | 6 | class CredentialsConfig { 7 | String? _apiToken; 8 | 9 | CredentialsConfig() { 10 | var credentialsFile = getLocalizelyCredentialsFile(); 11 | if (credentialsFile == null) { 12 | return; 13 | } 14 | 15 | var credentialsFileContent = credentialsFile.readAsStringSync(); 16 | var credentialsYaml = yaml.loadYaml(credentialsFileContent); 17 | if (credentialsYaml is! yaml.YamlMap) { 18 | throw ConfigException( 19 | "Failed to extract 'api_token' from the '${getLocalizelyCredentialsFilePath()}' file.\nExpected YAML map (e.g. api_token: xxxxxx) but got ${credentialsYaml.runtimeType}."); 20 | } 21 | 22 | _apiToken = credentialsYaml['api_token'] is String 23 | ? credentialsYaml['api_token'] 24 | : null; 25 | } 26 | 27 | String? get apiToken => _apiToken; 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/config/pubspec_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:yaml/yaml.dart' as yaml; 2 | 3 | import '../utils/file_utils.dart'; 4 | import 'config_exception.dart'; 5 | 6 | class PubspecConfig { 7 | bool? _enabled; 8 | String? _className; 9 | String? _mainLocale; 10 | String? _arbDir; 11 | String? _outputDir; 12 | bool? _useDeferredLoading; 13 | LocalizelyConfig? _localizelyConfig; 14 | 15 | PubspecConfig() { 16 | var pubspecFile = getPubspecFile(); 17 | if (pubspecFile == null) { 18 | throw ConfigException("Can't find 'pubspec.yaml' file."); 19 | } 20 | 21 | var pubspecFileContent = pubspecFile.readAsStringSync(); 22 | var pubspecYaml = yaml.loadYaml(pubspecFileContent); 23 | 24 | if (pubspecYaml is! yaml.YamlMap) { 25 | throw ConfigException( 26 | "Failed to extract config from the 'pubspec.yaml' file.\nExpected YAML map but got ${pubspecYaml.runtimeType}."); 27 | } 28 | 29 | var flutterIntlConfig = pubspecYaml['flutter_intl']; 30 | if (flutterIntlConfig == null) { 31 | return; 32 | } 33 | 34 | _enabled = flutterIntlConfig['enabled'] is bool 35 | ? flutterIntlConfig['enabled'] 36 | : null; 37 | _className = flutterIntlConfig['class_name'] is String 38 | ? flutterIntlConfig['class_name'] 39 | : null; 40 | _mainLocale = flutterIntlConfig['main_locale'] is String 41 | ? flutterIntlConfig['main_locale'] 42 | : null; 43 | _arbDir = flutterIntlConfig['arb_dir'] is String 44 | ? flutterIntlConfig['arb_dir'] 45 | : null; 46 | _outputDir = flutterIntlConfig['output_dir'] is String 47 | ? flutterIntlConfig['output_dir'] 48 | : null; 49 | _useDeferredLoading = flutterIntlConfig['use_deferred_loading'] is bool 50 | ? flutterIntlConfig['use_deferred_loading'] 51 | : null; 52 | _localizelyConfig = 53 | LocalizelyConfig.fromConfig(flutterIntlConfig['localizely']); 54 | } 55 | 56 | bool? get enabled => _enabled; 57 | 58 | String? get className => _className; 59 | 60 | String? get mainLocale => _mainLocale; 61 | 62 | String? get arbDir => _arbDir; 63 | 64 | String? get outputDir => _outputDir; 65 | 66 | bool? get useDeferredLoading => _useDeferredLoading; 67 | 68 | LocalizelyConfig? get localizelyConfig => _localizelyConfig; 69 | } 70 | 71 | class LocalizelyConfig { 72 | String? _projectId; 73 | String? _branch; 74 | bool? _uploadAsReviewed; 75 | bool? _uploadOverwrite; 76 | List? _uploadTagAdded; 77 | List? _uploadTagUpdated; 78 | List? _uploadTagRemoved; 79 | String? _downloadEmptyAs; 80 | List? _downloadIncludeTags; 81 | List? _downloadExcludeTags; 82 | bool? _otaEnabled; 83 | 84 | LocalizelyConfig.fromConfig(yaml.YamlMap? localizelyConfig) { 85 | if (localizelyConfig == null) { 86 | return; 87 | } 88 | 89 | _projectId = localizelyConfig['project_id'] is String 90 | ? localizelyConfig['project_id'] 91 | : null; 92 | _branch = localizelyConfig['branch'] is String 93 | ? localizelyConfig['branch'] 94 | : null; 95 | _uploadAsReviewed = localizelyConfig['upload_as_reviewed'] is bool 96 | ? localizelyConfig['upload_as_reviewed'] 97 | : null; 98 | _uploadOverwrite = localizelyConfig['upload_overwrite'] is bool 99 | ? localizelyConfig['upload_overwrite'] 100 | : null; 101 | _uploadTagAdded = localizelyConfig['upload_tag_added'] is yaml.YamlList 102 | ? List.from(localizelyConfig['upload_tag_added']) 103 | : null; 104 | _uploadTagUpdated = localizelyConfig['upload_tag_updated'] is yaml.YamlList 105 | ? List.from(localizelyConfig['upload_tag_updated']) 106 | : null; 107 | _uploadTagRemoved = localizelyConfig['upload_tag_removed'] is yaml.YamlList 108 | ? List.from(localizelyConfig['upload_tag_removed']) 109 | : null; 110 | _downloadEmptyAs = localizelyConfig['download_empty_as'] is String 111 | ? localizelyConfig['download_empty_as'] 112 | : null; 113 | _downloadIncludeTags = 114 | localizelyConfig['download_include_tags'] is yaml.YamlList 115 | ? List.from(localizelyConfig['download_include_tags']) 116 | : null; 117 | _downloadExcludeTags = 118 | localizelyConfig['download_exclude_tags'] is yaml.YamlList 119 | ? List.from(localizelyConfig['download_exclude_tags']) 120 | : null; 121 | _otaEnabled = localizelyConfig['ota_enabled'] is bool 122 | ? localizelyConfig['ota_enabled'] 123 | : null; 124 | } 125 | 126 | String? get projectId => _projectId; 127 | 128 | String? get branch => _branch; 129 | 130 | bool? get uploadAsReviewed => _uploadAsReviewed; 131 | 132 | bool? get uploadOverwrite => _uploadOverwrite; 133 | 134 | List? get uploadTagAdded => _uploadTagAdded; 135 | 136 | List? get uploadTagUpdated => _uploadTagUpdated; 137 | 138 | List? get uploadTagRemoved => _uploadTagRemoved; 139 | 140 | String? get downloadEmptyAs => _downloadEmptyAs; 141 | 142 | List? get downloadIncludeTags => _downloadIncludeTags; 143 | 144 | List? get downloadExcludeTags => _downloadExcludeTags; 145 | 146 | bool? get otaEnabled => _otaEnabled; 147 | } 148 | -------------------------------------------------------------------------------- /lib/src/constants/constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:path/path.dart'; 2 | 3 | const defaultClassName = 'S'; 4 | const defaultMainLocale = 'en'; 5 | final defaultArbDir = join('lib', 'l10n'); 6 | final defaultOutputDir = join('lib', 'generated'); 7 | const defaultUseDeferredLoading = false; 8 | const defaultUploadOverwrite = false; 9 | const defaultUploadAsReviewed = false; 10 | const defaultDownloadEmptyAs = 'empty'; 11 | const defaultOtaEnabled = false; 12 | -------------------------------------------------------------------------------- /lib/src/generator/generator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import '../config/pubspec_config.dart'; 4 | import '../constants/constants.dart'; 5 | import '../utils/file_utils.dart'; 6 | import '../utils/utils.dart'; 7 | import 'generator_exception.dart'; 8 | import 'intl_translation_helper.dart'; 9 | import 'label.dart'; 10 | import 'templates.dart'; 11 | 12 | /// The generator of localization files. 13 | class Generator { 14 | late String _className; 15 | late String _mainLocale; 16 | late String _arbDir; 17 | late String _outputDir; 18 | late bool _useDeferredLoading; 19 | late bool _otaEnabled; 20 | 21 | /// Creates a new generator with configuration from the 'pubspec.yaml' file. 22 | Generator() { 23 | var pubspecConfig = PubspecConfig(); 24 | 25 | _className = defaultClassName; 26 | if (pubspecConfig.className != null) { 27 | if (isValidClassName(pubspecConfig.className!)) { 28 | _className = pubspecConfig.className!; 29 | } else { 30 | warning( 31 | "Config parameter 'class_name' requires valid 'UpperCamelCase' value."); 32 | } 33 | } 34 | 35 | _mainLocale = defaultMainLocale; 36 | if (pubspecConfig.mainLocale != null) { 37 | if (isValidLocale(pubspecConfig.mainLocale!)) { 38 | _mainLocale = pubspecConfig.mainLocale!; 39 | } else { 40 | warning( 41 | "Config parameter 'main_locale' requires value consisted of language code and optional script and country codes separated with underscore (e.g. 'en', 'en_GB', 'zh_Hans', 'zh_Hans_CN')."); 42 | } 43 | } 44 | 45 | _arbDir = defaultArbDir; 46 | if (pubspecConfig.arbDir != null) { 47 | if (isValidPath(pubspecConfig.arbDir!)) { 48 | _arbDir = pubspecConfig.arbDir!; 49 | } else { 50 | warning( 51 | "Config parameter 'arb_dir' requires valid path value (e.g. 'lib', 'res/', 'lib\\l10n')."); 52 | } 53 | } 54 | 55 | _outputDir = defaultOutputDir; 56 | if (pubspecConfig.outputDir != null) { 57 | if (isValidPath(pubspecConfig.outputDir!)) { 58 | _outputDir = pubspecConfig.outputDir!; 59 | } else { 60 | warning( 61 | "Config parameter 'output_dir' requires valid path value (e.g. 'lib', 'lib\\generated')."); 62 | } 63 | } 64 | 65 | _useDeferredLoading = 66 | pubspecConfig.useDeferredLoading ?? defaultUseDeferredLoading; 67 | 68 | _otaEnabled = 69 | pubspecConfig.localizelyConfig?.otaEnabled ?? defaultOtaEnabled; 70 | } 71 | 72 | /// Generates localization files. 73 | Future generateAsync() async { 74 | await _updateL10nDir(); 75 | await _updateGeneratedDir(); 76 | await _generateDartFiles(); 77 | } 78 | 79 | Future _updateL10nDir() async { 80 | var mainArbFile = getArbFileForLocale(_mainLocale, _arbDir); 81 | if (mainArbFile == null) { 82 | await createArbFileForLocale(_mainLocale, _arbDir); 83 | } 84 | } 85 | 86 | Future _updateGeneratedDir() async { 87 | var labels = _getLabelsFromMainArbFile(); 88 | var locales = _orderLocales(getLocales(_arbDir)); 89 | var content = 90 | generateL10nDartFileContent(_className, labels, locales, _otaEnabled); 91 | var formattedContent = formatDartContent(content, 'l10n.dart'); 92 | 93 | await updateL10nDartFile(formattedContent, _outputDir); 94 | 95 | var intlDir = getIntlDirectory(_outputDir); 96 | if (intlDir == null) { 97 | await createIntlDirectory(_outputDir); 98 | } 99 | 100 | await removeUnusedGeneratedDartFiles(locales, _outputDir); 101 | } 102 | 103 | List