lokalise_manager 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/CODE_OF_CONDUCT.md +46 -0
- data/.github/CONTRIBUTING.md +14 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +11 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +5 -0
- data/LICENSE +22 -0
- data/README.md +266 -0
- data/Rakefile +21 -0
- data/lib/lokalise_manager/error.rb +10 -0
- data/lib/lokalise_manager/global_config.rb +89 -0
- data/lib/lokalise_manager/task_definitions/base.rb +98 -0
- data/lib/lokalise_manager/task_definitions/exporter.rb +79 -0
- data/lib/lokalise_manager/task_definitions/importer.rb +106 -0
- data/lib/lokalise_manager/version.rb +5 -0
- data/lib/lokalise_manager.rb +34 -0
- data/lokalise_manager.gemspec +38 -0
- data/spec/lib/lokalise_manager/global_config_spec.rb +99 -0
- data/spec/lib/lokalise_manager/task_definitions/base_spec.rb +102 -0
- data/spec/lib/lokalise_manager/task_definitions/exporter_spec.rb +197 -0
- data/spec/lib/lokalise_manager/task_definitions/importer_spec.rb +168 -0
- data/spec/lib/lokalise_manager_spec.rb +15 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/file_manager.rb +64 -0
- data/spec/support/spec_addons.rb +16 -0
- data/spec/support/vcr.rb +11 -0
- metadata +233 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f0640f0599d5b4967f79d2ced69f63d7cb7edde3e2a3a49336d08d17e5fb4c0f
|
4
|
+
data.tar.gz: 0766d8cb7f6b478e427f8ef7dd1409d86072a74a4606b026eaa718f6fe4761b5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 38a3adfcc0bf4a78f595547cdc3ba2962a4122d8bbeaa7e132d47dc4e76992711e653710a802e395fafcb0c316743607876a10f0914c2633472994891fd4850e
|
7
|
+
data.tar.gz: 98b2edd83de044615db56ee71d090afa03fcfc7793c55854e322ab466996e0eb46c74f3b22dbdbd44e570f31746d40394912ba39c29dd17337b17e08d0417761
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
6
|
+
|
7
|
+
## Our Standards
|
8
|
+
|
9
|
+
Examples of behavior that contributes to creating a positive environment include:
|
10
|
+
|
11
|
+
* Using welcoming and inclusive language
|
12
|
+
* Being respectful of differing viewpoints and experiences
|
13
|
+
* Gracefully accepting constructive criticism
|
14
|
+
* Focusing on what is best for the community
|
15
|
+
* Showing empathy towards other community members
|
16
|
+
|
17
|
+
Examples of unacceptable behavior by participants include:
|
18
|
+
|
19
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
20
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
21
|
+
* Public or private harassment
|
22
|
+
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
23
|
+
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
24
|
+
|
25
|
+
## Our Responsibilities
|
26
|
+
|
27
|
+
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
28
|
+
|
29
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
30
|
+
|
31
|
+
## Scope
|
32
|
+
|
33
|
+
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
34
|
+
|
35
|
+
## Enforcement
|
36
|
+
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@lokalise.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
38
|
+
|
39
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
40
|
+
|
41
|
+
## Attribution
|
42
|
+
|
43
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
44
|
+
|
45
|
+
[homepage]: http://contributor-covenant.org
|
46
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
1. [Fork the repository.][fork]
|
4
|
+
2. [Create a topic branch.][branch]
|
5
|
+
3. Implement your feature or bug fix.
|
6
|
+
4. Don't forget to add specs and make sure they pass by running `rspec .`.
|
7
|
+
5. Make sure your code complies with the style guide by running `rubocop`. `rubocop -a` can automatically fix most issues for you.
|
8
|
+
6. If necessary, add documentation for your feature or bug fix.
|
9
|
+
7. Commit and push your changes.
|
10
|
+
8. [Submit a pull request.][pr]
|
11
|
+
|
12
|
+
[fork]: http://help.github.com/fork-a-repo/
|
13
|
+
[branch]: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-branches
|
14
|
+
[pr]: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests
|
@@ -0,0 +1,11 @@
|
|
1
|
+
### Summary
|
2
|
+
|
3
|
+
Provide a general description of the code changes in your pull
|
4
|
+
request. Were there any bugs you had fixed? If so, mention them. If
|
5
|
+
these bugs have open GitHub issues, be sure to tag them here as well,
|
6
|
+
to keep the conversation linked together.
|
7
|
+
|
8
|
+
### Other Information
|
9
|
+
|
10
|
+
If there's anything else that's important and relevant to your pull
|
11
|
+
request, mention that information here.
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 Lokalise team, Ilya Bodrov
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,266 @@
|
|
1
|
+
# LokaliseManager
|
2
|
+
|
3
|
+
![Gem](https://img.shields.io/gem/v/lokalise_manager)
|
4
|
+
[![Build Status](https://travis-ci.com/bodrovis/lokalise_manager.svg?branch=master)](https://travis-ci.com/github/bodrovis/lokalise_manager)
|
5
|
+
[![Test Coverage](https://codecov.io/gh/bodrovis/lokalise_manager/graph/badge.svg)](https://codecov.io/gh/bodrovis/lokalise_manager)
|
6
|
+
![Downloads total](https://img.shields.io/gem/dt/lokalise_manager)
|
7
|
+
|
8
|
+
This gem provides [Lokalise](http://lokalise.com) integration for Ruby and allows to exchange translation files between your project and TMS easily. It relies on [ruby-lokalise-api](https://lokalise.github.io/ruby-lokalise-api) to send APIv2 requests.
|
9
|
+
|
10
|
+
If you are looking for a Rails integration, please check [lokalise_rails](https://github.com/bodrovis/lokalise_rails) which provides a set of Rake tasks for importing/exporting.
|
11
|
+
|
12
|
+
## Getting started
|
13
|
+
|
14
|
+
### Requirements
|
15
|
+
|
16
|
+
This gem requires Ruby 2.5+. You will also need to [setup a Lokalise account](https://app.lokalise.com/signup) and create a [translation project](https://docs.lokalise.com/en/articles/1400460-projects). Finally, you will need to generate a [read/write API token](https://docs.lokalise.com/en/articles/1929556-api-tokens) at your Lokalise profile.
|
17
|
+
|
18
|
+
### Installation
|
19
|
+
|
20
|
+
Add the gem to your `Gemfile`:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'lokalise_manager'
|
24
|
+
```
|
25
|
+
|
26
|
+
and run:
|
27
|
+
|
28
|
+
```
|
29
|
+
bundle
|
30
|
+
```
|
31
|
+
|
32
|
+
### Creating a client
|
33
|
+
|
34
|
+
To import or export translation files, you'll have to create the corresponding client:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
importer = LokaliseManager.importer api_token: '1234abc', project_id: '123.abc'
|
38
|
+
|
39
|
+
# OR
|
40
|
+
|
41
|
+
exporter = LokaliseManager.exporter api_token: '1234abc', project_id: '123.abc'
|
42
|
+
```
|
43
|
+
|
44
|
+
You *must* provide an API token and a project ID (your project ID can be found under Lokalise project settings). [Other options can be customized as well (see below)](https://github.com/bodrovis/lokalise_manager#configuration) but they have sensible defaults.
|
45
|
+
|
46
|
+
### Importing files from Lokalise into your project
|
47
|
+
|
48
|
+
To download translation files from Lokalise into your project (by default all files will be stored under the `locales/` directory), run the following code:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
result = importer.import!
|
52
|
+
```
|
53
|
+
|
54
|
+
The `result` will contain a boolean value which says whether the operation was successfull or not.
|
55
|
+
|
56
|
+
Please note that upon importing translations any duplicating files inside the `locales` directory (or any other directory that you've specified in the options) will be overwritten! You can enable [safe mode](https://github.com/bodrovis/lokalise_manager#import-config) to check whether the folder is empty or not.
|
57
|
+
|
58
|
+
### Exporting files from your project to Lokalise
|
59
|
+
|
60
|
+
To upload your translation files from a local directory (defaults to `locales/`) to a Lokalise project, run the following code:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
processes = exporter.export!
|
64
|
+
```
|
65
|
+
|
66
|
+
`processes` will contain an array of queued background processes as uploading is done in the background on Lokalise. You can perform periodic checks to read the status of the process. Here's a very simple example:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
def uploaded?(process)
|
70
|
+
5.times do # try to check the status 5 times
|
71
|
+
process = process.reload_data # load new data
|
72
|
+
return(true) if process.status == 'finished' # return true is the upload has finished
|
73
|
+
sleep 1 # wait for 1 second, adjust this number with regards to the upload size
|
74
|
+
end
|
75
|
+
|
76
|
+
false # if all 5 checks failed, return false (probably something is wrong)
|
77
|
+
end
|
78
|
+
|
79
|
+
processes = exporter.export!
|
80
|
+
uploaded? processes[0]
|
81
|
+
```
|
82
|
+
|
83
|
+
Please don't forget that Lokalise API has rate limiting and you cannot send more than six requests per second.
|
84
|
+
|
85
|
+
## Configuration
|
86
|
+
|
87
|
+
### Common config
|
88
|
+
|
89
|
+
* `api_token` (`string`, required) — Lokalise API token with read/write permissions.
|
90
|
+
* `project_id` (`string`, required) — Lokalise project ID. You must have import/export permissions in the specified project.
|
91
|
+
* `locales_path` (`string`) — path to the directory with your translation files. Defaults to `"#{Dir.getwd}/locales"`.
|
92
|
+
* `branch` (`string`) — Lokalise project branch to use. Defaults to `""` (no branch is provided).
|
93
|
+
* `timeouts` (`hash`) — set [request timeouts for the Lokalise API client](https://lokalise.github.io/ruby-lokalise-api/additional_info/customization#setting-timeouts). By default, requests have no timeouts: `{open_timeout: nil, timeout: nil}`. Both values are in seconds.
|
94
|
+
|
95
|
+
### Import config
|
96
|
+
|
97
|
+
* `import_opts` (`hash`) — options that will be passed to Lokalise API when downloading translations to your app. Here are the default options:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
{
|
101
|
+
format: 'yaml',
|
102
|
+
placeholder_format: :icu,
|
103
|
+
yaml_include_root: true,
|
104
|
+
original_filenames: true,
|
105
|
+
directory_prefix: '',
|
106
|
+
indentation: '2sp'
|
107
|
+
}
|
108
|
+
```
|
109
|
+
|
110
|
+
Full list of available import options [can be found in the official API documentation](https://app.lokalise.com/api2docs/curl/#transition-download-files-post).
|
111
|
+
|
112
|
+
You can provide additional options, and they will be merged with the default ones. For example:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
importer = LokaliseManager.importer api_token: '1234abc',
|
116
|
+
project_id: '123.abc',
|
117
|
+
import_opts: {original_filenames: true}
|
118
|
+
```
|
119
|
+
|
120
|
+
In this case the `import_opts` will have `original_filenames` set to `true` and will also contain all the defaults (`format`, `placeholder_format`, and others). Of course, you can override defaults as well:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
importer = LokaliseManager.importer api_token: '1234abc',
|
124
|
+
project_id: '123.abc',
|
125
|
+
import_opts: {indentation: '4sp}
|
126
|
+
```
|
127
|
+
|
128
|
+
* `import_safe_mode` (`boolean`) — default to `false`. When this option is enabled, the import task will check whether the directory set with `locales_path` is empty or not. If it is not empty, you will be prompted to continue.
|
129
|
+
* `max_retries_import` (`integer`) — this option is introduced to properly handle Lokalise API rate limiting. If the HTTP status code 429 (too many requests) has been received, this gem will apply an exponential backoff mechanism with a very simple formula: `2 ** retries`. If the maximum number of retries has been reached, a `Lokalise::Error::TooManyRequests` exception will be raised and the operation will be halted.
|
130
|
+
|
131
|
+
### Export config
|
132
|
+
|
133
|
+
* `export_opts` (`hash`) — options that will be passed to Lokalise API when uploading translations. Full list of available export options [can be found in the official documentation](https://app.lokalise.com/api2docs/curl/#transition-upload-a-file-post). By default, the following options are provided:
|
134
|
+
+ `data` (`string`, required) — base64-encoded contents of the translation file.
|
135
|
+
+ `filename` (`string`, required) — translation file name. If the file is stored under a subdirectory (for example, `nested/en.yml` inside the `locales/` directory), the whole path acts as a name. Later when importing files with such names, they will be placed into the proper subdirectories.
|
136
|
+
+ `lang_iso` (`string`, required) — language ISO code which is determined using the root key inside your YAML file. For example, in this case the `lang_iso` is `en_US`:
|
137
|
+
|
138
|
+
```yaml
|
139
|
+
en_US:
|
140
|
+
my_key: "my value"
|
141
|
+
```
|
142
|
+
|
143
|
+
You can provide additional options, and they will be merged with the default ones. For example:
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
exporter = LokaliseManager.exporter api_token: '1234abc',
|
147
|
+
project_id: '123.abc',
|
148
|
+
export_opts: {detect_icu_plurals: true}
|
149
|
+
```
|
150
|
+
|
151
|
+
In this case the `export_opts` will have `detect_icu_plurals` set to `true` and will also contain all the defaults (`data`, `filename`, and `lang_iso`).
|
152
|
+
|
153
|
+
**Please note** that if your Lokalise project does not have a language with the specified `lang_iso` code, the export will fail. It means that you first have to add all the locales to the project and then start the exporting process.
|
154
|
+
|
155
|
+
* `skip_file_export` (`lambda` or `proc`) — specify additional exclusion criteria for the exported files. By default, the rake task will ignore all non-file entries and all files with improper extensions (the latter is controlled by the `file_ext_regexp`). Lambda passed to this option should accept a single argument which is full path to the file (instance of the [`Pathname` class](https://ruby-doc.org/stdlib-2.7.1/libdoc/pathname/rdoc/Pathname.html)). For example, to exclude all files that have `fr` part in their names, add the following config:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
c.skip_file_export = ->(file) { f.split[1].to_s.include?('fr') }
|
159
|
+
```
|
160
|
+
|
161
|
+
* `max_retries_export` (`integer`) — this option is introduced to properly handle Lokalise API rate limiting. If the HTTP status code 429 (too many requests) has been received, LokaliseRails will apply an exponential backoff mechanism with a very simple formula: `2 ** retries` (initially `retries` is `0`). If the maximum number of retries has been reached, a `Lokalise::Error::TooManyRequests` exception will be raised and the export operation will be halted. By default, LokaliseRails will make up to `5` retries which potentially means `1 + 2 + 4 + 8 + 16 + 32 = 63` seconds of waiting time. If the `max_retries_export` is less than `1`, LokaliseRails will not perform any retries and give up immediately after receiving error 429.
|
162
|
+
|
163
|
+
### Config to work with formats other than YAML
|
164
|
+
|
165
|
+
If your translation files are not in YAML format, you will need to adjust the following options:
|
166
|
+
|
167
|
+
* `file_ext_regexp` (`regexp`) — regular expression applied to file extensions to determine which files should be imported and exported. Defaults to `/\.ya?ml\z/i` (YAML files).
|
168
|
+
* `translations_loader` (`lambda` or `proc`) — loads translations data and makes sure they are valid before saving them to a translation file. Defaults to `->(raw_data) { YAML.safe_load raw_data }`. In the simplest case you may just return the data back, for example `-> (raw_data) { raw_data }`.
|
169
|
+
* `translations_converter` (`lambda` or `proc`) — converts translations data to a proper format before saving them to a translation file. Defaults to `->(raw_data) { raw_data.to_yaml }`. In the simplest case you may just return the data back, for example `-> (raw_data) { raw_data }`.
|
170
|
+
* `lang_iso_inferer` (`lambda` or `proc`) — infers language ISO code based on the translation file data before uploading it to Lokalise. Defaults to `->(data) { YAML.safe_load(data)&.keys&.first }`.
|
171
|
+
|
172
|
+
## Providing config options
|
173
|
+
|
174
|
+
### Per-client
|
175
|
+
|
176
|
+
The simplest way to provide your config is on per-client basis, for example:
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
importer = LokaliseManager.importer api_token: '1234abc',
|
180
|
+
project_id: '123.abc',
|
181
|
+
import_opts: {original_filenames: true},
|
182
|
+
export_opts: {detect_icu_plurals: true},
|
183
|
+
translations_converter: -> (raw_data) { raw_data }
|
184
|
+
```
|
185
|
+
|
186
|
+
These options will be merged with the default ones. Please note that per-client config has the highest priority.
|
187
|
+
|
188
|
+
You can also adjust individual options later using the `#config` instance variable (it contains an OpenStruct object):
|
189
|
+
|
190
|
+
```
|
191
|
+
importer.config.project_id = '678xyz'
|
192
|
+
importer.config.branch = 'develop'
|
193
|
+
```
|
194
|
+
|
195
|
+
### Globally
|
196
|
+
|
197
|
+
You can also provide config globally. To achieve that, call `.config` on `LokaliseManager::Config` class:
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
LokaliseManager::GlobalConfig.config do |c|
|
201
|
+
c.api_token = '12345'
|
202
|
+
c.project_id = '123.abc'
|
203
|
+
|
204
|
+
c.branch = 'develop'
|
205
|
+
end
|
206
|
+
```
|
207
|
+
|
208
|
+
Global config takes precedence over the default options, however per-client config has higher precedence.
|
209
|
+
|
210
|
+
### Overriding defaults
|
211
|
+
|
212
|
+
You can even subclass the default `GlobalConfig` class and provide the new defaults:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
class CustomConfig < LokaliseManager::GlobalConfig
|
216
|
+
class << self
|
217
|
+
def branch
|
218
|
+
@branch || 'develop'
|
219
|
+
end
|
220
|
+
|
221
|
+
def locales_path
|
222
|
+
@locales_path || "#{Dir.getwd}/i18n/locales"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
```
|
227
|
+
|
228
|
+
Check [`global_config.rb` source](https://github.com/bodrovis/lokalise_manager/blob/master/lib/lokalise_manager/global_config.rb) to find other defaults.
|
229
|
+
|
230
|
+
Now when you have created a custom config, you can also set some global options (this is not required though):
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
CustomConfig.config do |c|
|
234
|
+
c.api_token = '123'
|
235
|
+
c.project_id = '456.abc'
|
236
|
+
end
|
237
|
+
```
|
238
|
+
|
239
|
+
However, it's required to pass your new class when instantiating the clients:
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
importer = LokaliseManager.importer({}, CustomConfig)
|
243
|
+
```
|
244
|
+
|
245
|
+
Please note that round brackets are required in this case.
|
246
|
+
|
247
|
+
The first argument is your per-client options, whereas the second argument contains the class with all the defaults.
|
248
|
+
|
249
|
+
Of course, you can still provide options on per-client basis:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
importer = LokaliseManager.importer({api_token: '890abcdef'}, CustomConfig)
|
253
|
+
|
254
|
+
# Now we can run the import process as before:
|
255
|
+
|
256
|
+
importer.import!
|
257
|
+
```
|
258
|
+
|
259
|
+
## Running tests
|
260
|
+
|
261
|
+
1. Copypaste `.env.example` file as `.env`. Put your Lokalise API token and project ID inside. The `.env` file is excluded from version control so your data is safe. All in all, we use pre-recorded VCR cassettes, so the actual API requests won’t be sent. However, providing at least some values is required.
|
262
|
+
2. Run `rspec .`. Observe test results and code coverage.
|
263
|
+
|
264
|
+
## License
|
265
|
+
|
266
|
+
Copyright (c) [Lokalise team](http://lokalise.com), [Ilya Bodrov](http://bodrovis.tech). License type is [MIT](https://github.com/bodrovis/lokalise_manager/blob/master/LICENSE).
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'bundler/setup'
|
7
|
+
Bundler::GemHelper.install_tasks
|
8
|
+
rescue LoadError
|
9
|
+
puts 'although not required, bundler is recommened for running the tests'
|
10
|
+
end
|
11
|
+
|
12
|
+
task default: :spec
|
13
|
+
|
14
|
+
require 'rspec/core/rake_task'
|
15
|
+
RSpec::Core::RakeTask.new(:spec)
|
16
|
+
|
17
|
+
require 'rubocop/rake_task'
|
18
|
+
RuboCop::RakeTask.new do |task|
|
19
|
+
task.requires << 'rubocop-performance'
|
20
|
+
task.requires << 'rubocop-rspec'
|
21
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LokaliseManager
|
4
|
+
class GlobalConfig
|
5
|
+
class << self
|
6
|
+
attr_accessor :api_token, :project_id
|
7
|
+
attr_writer :import_opts, :import_safe_mode, :export_opts, :locales_path,
|
8
|
+
:file_ext_regexp, :skip_file_export, :branch, :timeouts,
|
9
|
+
:translations_loader, :translations_converter, :lang_iso_inferer,
|
10
|
+
:max_retries_export, :max_retries_import
|
11
|
+
|
12
|
+
# Main interface to provide configuration options
|
13
|
+
def config
|
14
|
+
yield self
|
15
|
+
end
|
16
|
+
|
17
|
+
# Full path to directory with translation files
|
18
|
+
def locales_path
|
19
|
+
@locales_path || "#{Dir.getwd}/locales"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Project branch to use
|
23
|
+
def branch
|
24
|
+
@branch || ''
|
25
|
+
end
|
26
|
+
|
27
|
+
# Set request timeouts for the Lokalise API client
|
28
|
+
def timeouts
|
29
|
+
@timeouts || {}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Maximum number of retries for file exporting
|
33
|
+
def max_retries_export
|
34
|
+
@max_retries_export || 5
|
35
|
+
end
|
36
|
+
|
37
|
+
# Maximum number of retries for file importing
|
38
|
+
def max_retries_import
|
39
|
+
@max_retries_import || 5
|
40
|
+
end
|
41
|
+
|
42
|
+
# Regular expression used to select translation files with proper extensions
|
43
|
+
def file_ext_regexp
|
44
|
+
@file_ext_regexp || /\.ya?ml\z/i
|
45
|
+
end
|
46
|
+
|
47
|
+
# Options for import rake task
|
48
|
+
def import_opts
|
49
|
+
@import_opts || {
|
50
|
+
format: 'yaml',
|
51
|
+
placeholder_format: :icu,
|
52
|
+
yaml_include_root: true,
|
53
|
+
original_filenames: true,
|
54
|
+
directory_prefix: '',
|
55
|
+
indentation: '2sp'
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Options for export rake task
|
60
|
+
def export_opts
|
61
|
+
@export_opts || {}
|
62
|
+
end
|
63
|
+
|
64
|
+
# Enables safe mode for import. When enabled, will check whether the target folder is empty or not
|
65
|
+
def import_safe_mode
|
66
|
+
@import_safe_mode.nil? ? false : @import_safe_mode
|
67
|
+
end
|
68
|
+
|
69
|
+
# Additional file skip criteria to apply when performing export
|
70
|
+
def skip_file_export
|
71
|
+
@skip_file_export || ->(_) { false }
|
72
|
+
end
|
73
|
+
|
74
|
+
def translations_loader
|
75
|
+
@translations_loader || ->(raw_data) { YAML.safe_load raw_data }
|
76
|
+
end
|
77
|
+
|
78
|
+
# Converts translations data to the proper format
|
79
|
+
def translations_converter
|
80
|
+
@translations_converter || ->(raw_data) { raw_data.to_yaml }
|
81
|
+
end
|
82
|
+
|
83
|
+
# Infers lang ISO for the given translation file
|
84
|
+
def lang_iso_inferer
|
85
|
+
@lang_iso_inferer || ->(data) { YAML.safe_load(data)&.keys&.first }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ruby-lokalise-api'
|
4
|
+
require 'pathname'
|
5
|
+
require 'ostruct'
|
6
|
+
|
7
|
+
module LokaliseManager
|
8
|
+
module TaskDefinitions
|
9
|
+
class Base
|
10
|
+
attr_accessor :config
|
11
|
+
|
12
|
+
# Creates a new importer or exporter. It accepts custom config and merges it
|
13
|
+
# with the global config (custom config take precendence)
|
14
|
+
#
|
15
|
+
# @param custom_opts [Hash]
|
16
|
+
# @param global_config [Object]
|
17
|
+
def initialize(custom_opts = {}, global_config = LokaliseManager::GlobalConfig)
|
18
|
+
primary_opts = global_config.
|
19
|
+
singleton_methods.
|
20
|
+
filter { |m| m.to_s.end_with?('=') }.
|
21
|
+
each_with_object({}) do |method, opts|
|
22
|
+
reader = method.to_s.delete_suffix('=')
|
23
|
+
opts[reader.to_sym] = global_config.send(reader)
|
24
|
+
end
|
25
|
+
|
26
|
+
@config = OpenStruct.new primary_opts.merge(custom_opts)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Creates a Lokalise API client
|
30
|
+
#
|
31
|
+
# @return [Lokalise::Client]
|
32
|
+
def api_client
|
33
|
+
@api_client ||= ::Lokalise.client config.api_token, {enable_compression: true}.merge(config.timeouts)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Resets API client
|
37
|
+
def reset_api_client!
|
38
|
+
Lokalise.reset_client!
|
39
|
+
@api_client = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Checks task options
|
45
|
+
#
|
46
|
+
# @return Array
|
47
|
+
def check_options_errors!
|
48
|
+
errors = []
|
49
|
+
errors << 'Project ID is not set!' if config.project_id.nil? || config.project_id.empty?
|
50
|
+
errors << 'Lokalise API token is not set!' if config.api_token.nil? || config.api_token.empty?
|
51
|
+
|
52
|
+
raise(LokaliseManager::Error, errors.join(' ')) if errors.any?
|
53
|
+
end
|
54
|
+
|
55
|
+
# Checks whether the provided file has a proper extension as dictated by the `file_ext_regexp` option
|
56
|
+
#
|
57
|
+
# @return Boolean
|
58
|
+
# @param raw_path [String, Pathname]
|
59
|
+
def proper_ext?(raw_path)
|
60
|
+
path = raw_path.is_a?(Pathname) ? raw_path : Pathname.new(raw_path)
|
61
|
+
config.file_ext_regexp.match? path.extname
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns directory and filename for the given entry
|
65
|
+
#
|
66
|
+
# @return Array
|
67
|
+
# @param entry [String]
|
68
|
+
def subdir_and_filename_for(entry)
|
69
|
+
Pathname.new(entry).split
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns Lokalise project ID and branch, semicolumn separated
|
73
|
+
#
|
74
|
+
# @return [String]
|
75
|
+
def project_id_with_branch
|
76
|
+
return config.project_id.to_s if config.branch.to_s.strip.empty?
|
77
|
+
|
78
|
+
"#{config.project_id}:#{config.branch}"
|
79
|
+
end
|
80
|
+
|
81
|
+
# Sends request with exponential backoff mechanism
|
82
|
+
def with_exp_backoff(max_retries)
|
83
|
+
return unless block_given?
|
84
|
+
|
85
|
+
retries = 0
|
86
|
+
begin
|
87
|
+
yield
|
88
|
+
rescue Lokalise::Error::TooManyRequests => e
|
89
|
+
raise(e.class, "Gave up after #{retries} retries") if retries >= max_retries
|
90
|
+
|
91
|
+
sleep 2**retries
|
92
|
+
retries += 1
|
93
|
+
retry
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|