multilocale 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +33 -0
- data/LICENSE +21 -0
- data/README.md +230 -0
- data/exe/multilocale-ruby +6 -0
- data/lib/multilocale/cli.rb +215 -0
- data/lib/multilocale/client.rb +256 -0
- data/lib/multilocale/config_file.rb +130 -0
- data/lib/multilocale/dictionary.rb +94 -0
- data/lib/multilocale/encoding.rb +45 -0
- data/lib/multilocale/errors.rb +79 -0
- data/lib/multilocale/locale_file.rb +118 -0
- data/lib/multilocale/phrase.rb +103 -0
- data/lib/multilocale/phrases.rb +96 -0
- data/lib/multilocale/project.rb +95 -0
- data/lib/multilocale/projects.rb +42 -0
- data/lib/multilocale/sync.rb +135 -0
- data/lib/multilocale/version.rb +5 -0
- data/lib/multilocale.rb +38 -0
- metadata +68 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 75db6f0b36c95b3bbcb2c43a76c1764d40bb1cb4d4cb329b7f1ef541915588cb
|
|
4
|
+
data.tar.gz: 17c4ba2fd230cc8078a2ecc47a941ff59c01a89868ca9e2ff8cc96bb3ef0088d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b55ec4bcc946785519ff30b691531acfe4a39179918f6fd89050ef1126c9ea09bcb8f81ff017fc943a45f358df7199ef77cf7c0a44c9777431e14df02a3548a0
|
|
7
|
+
data.tar.gz: 77ac28ae81c76322f0b97caee8be408f7f8554acd248017cf32c0c12b78256745110f5b8df5c43911887dd1479af8e4432a6579472c7136fb66f51afb886273b
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this gem are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the version is
|
|
5
|
+
the one in `lib/multilocale/version.rb`.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-08-04
|
|
10
|
+
|
|
11
|
+
First release of the rewritten gem. The repository previously held nothing but
|
|
12
|
+
a licence file; nothing published under this name before.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `Multilocale::Client` for the REST API: `Authorization: Basic base64(secret)`,
|
|
17
|
+
timeouts, exponential-backoff retries on 429 and 5xx, and errors mapped onto
|
|
18
|
+
`AuthenticationError` / `PermissionError` / `NotFoundError` /
|
|
19
|
+
`RateLimitedError` / `ServerError` / `ConnectionError`.
|
|
20
|
+
- `client.projects` — list, find by id or name, create, update.
|
|
21
|
+
- `client.phrases` — list with filters and paging, upsert (batched), update,
|
|
22
|
+
delete, plus `client.dictionary` / `client.dictionaries`.
|
|
23
|
+
- `Multilocale::LocaleFile` — reads and writes i18n-shaped
|
|
24
|
+
`config/locales/*.yml` (and JSON), nesting dotted keys on the way out and
|
|
25
|
+
flattening them on the way back.
|
|
26
|
+
- `Multilocale::Sync` — `pull` and `push`, driven by `multilocale.json`.
|
|
27
|
+
- `multilocale-ruby` command: `pull`, `push`, `projects`, `phrases`, `version`,
|
|
28
|
+
with `--json` output. Credentials come from the environment only.
|
|
29
|
+
- `example/` — a four-language Sinatra application with its translations
|
|
30
|
+
committed, so a clone runs offline.
|
|
31
|
+
|
|
32
|
+
[unreleased]: https://github.com/multilocale/multilocale-ruby-gem/compare/v0.1.0...HEAD
|
|
33
|
+
[0.1.0]: https://github.com/multilocale/multilocale-ruby-gem/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Multilocale
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# multilocale (Ruby)
|
|
2
|
+
|
|
3
|
+
Ruby client for [Multilocale](https://www.multilocale.com), the translation
|
|
4
|
+
management platform. It reads and writes projects and phrases over the REST
|
|
5
|
+
API, and syncs them into the locale files the [i18n
|
|
6
|
+
gem](https://github.com/ruby-i18n/i18n) — and therefore Rails — already load.
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
client = Multilocale.client # MULTILOCALE_API_KEY
|
|
10
|
+
client.dictionary(project: "website", language: "es").to_h
|
|
11
|
+
#=> { "cart.title" => "Carrito", "nav.language" => "Idioma", … }
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```console
|
|
15
|
+
$ multilocale-ruby pull
|
|
16
|
+
website: 412 phrases in 6 languages
|
|
17
|
+
wrote config/locales/en.yml
|
|
18
|
+
wrote config/locales/es.yml
|
|
19
|
+
…
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- No runtime dependencies: `net/http`, `json` and `yaml` from the standard library.
|
|
23
|
+
- No network call while a page is being served. Translations are downloaded at
|
|
24
|
+
build time and committed, so the site renders offline and stays up when
|
|
25
|
+
multilocale.com does not.
|
|
26
|
+
- Ruby >= 3.2.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# Gemfile
|
|
32
|
+
gem "multilocale", "~> 0.1"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```console
|
|
36
|
+
gem install multilocale
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
> **0.1.0 is not on RubyGems yet.** Until it is, install from a checkout —
|
|
40
|
+
> `gem "multilocale", path: "…"` in your Gemfile, or `rake build && gem install
|
|
41
|
+
> pkg/multilocale-0.1.0.gem`. `example/` in this repository already resolves the
|
|
42
|
+
> gem from `path: ".."`, so it needs nothing extra.
|
|
43
|
+
|
|
44
|
+
## The workflow this gem is for
|
|
45
|
+
|
|
46
|
+
1. **Phrases live in a project on multilocale.com.** A project has a name, a
|
|
47
|
+
default locale and a complete locale list. A phrase is one
|
|
48
|
+
`{key, value, language}` row, so a key translated into 12 locales is 12 rows.
|
|
49
|
+
2. **`multilocale-ruby pull` writes them to disk** as
|
|
50
|
+
`config/locales/<locale>.yml`, nested the way i18n expects.
|
|
51
|
+
3. **You commit the result.** The files are part of the application, reviewed
|
|
52
|
+
in the same pull request as the code that renders them.
|
|
53
|
+
4. **The i18n gem renders them.** Nothing in the request path talks to
|
|
54
|
+
Multilocale.
|
|
55
|
+
|
|
56
|
+
Editing a locale file by hand and sending it back up is `multilocale-ruby push
|
|
57
|
+
--yes`.
|
|
58
|
+
|
|
59
|
+
## Authenticate
|
|
60
|
+
|
|
61
|
+
Create a key at [app.multilocale.com](https://app.multilocale.com) → **API
|
|
62
|
+
keys**, and export the secret:
|
|
63
|
+
|
|
64
|
+
```console
|
|
65
|
+
export MULTILOCALE_API_KEY=…
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The API expects `Authorization: Basic base64(secret)` — the secret alone,
|
|
69
|
+
base64'd, **with no colon and no key/secret pair**. This gem does that for you;
|
|
70
|
+
it is worth knowing because a hand-rolled `curl` with `-u key:secret` is the
|
|
71
|
+
single most common first failure.
|
|
72
|
+
|
|
73
|
+
The secret selects the organization *and* the project, so there is no tenant
|
|
74
|
+
parameter to pass, and a key scoped to one project cannot read another's
|
|
75
|
+
phrases even if you ask for it.
|
|
76
|
+
|
|
77
|
+
Scopes are `projects:read`, `projects:write`, `phrases:read`, `phrases:write`.
|
|
78
|
+
New keys get the read scopes only; `pull` needs `projects:read` +
|
|
79
|
+
`phrases:read`, `push` also needs `phrases:write`.
|
|
80
|
+
|
|
81
|
+
There is deliberately **no `--api-key` flag**. A secret on the command line is
|
|
82
|
+
readable in the process table and lives in your shell history forever.
|
|
83
|
+
|
|
84
|
+
## Configure
|
|
85
|
+
|
|
86
|
+
`multilocale.json` in the repository root, the same file the
|
|
87
|
+
[npm CLI](https://www.npmjs.com/package/multilocale) reads:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"projectId": "website",
|
|
92
|
+
"defaultLocale": "en",
|
|
93
|
+
"locales": ["en", "es", "fr", "it"],
|
|
94
|
+
"paths": ["config/locales/%lang%.yml"]
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- `projectId` accepts an id **or** a name. Names are unique per organization,
|
|
99
|
+
which is what makes this portable between accounts.
|
|
100
|
+
- `paths` are resolved relative to `multilocale.json`, so `rake` from a
|
|
101
|
+
subdirectory writes the same files as `rake` from the root.
|
|
102
|
+
- `%lang%` is replaced with each locale.
|
|
103
|
+
|
|
104
|
+
Every command works with no flags once this file exists. Nothing ever prompts —
|
|
105
|
+
an interactive picker in a CI job is a hung build.
|
|
106
|
+
|
|
107
|
+
## Command line
|
|
108
|
+
|
|
109
|
+
```console
|
|
110
|
+
multilocale-ruby pull # API -> config/locales/*.yml
|
|
111
|
+
multilocale-ruby push --yes # files -> API (overwrites remote values)
|
|
112
|
+
multilocale-ruby projects # what this credential can see
|
|
113
|
+
multilocale-ruby phrases --language es # one dictionary, key = value
|
|
114
|
+
multilocale-ruby pull --json # machine-readable report
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The executable is `multilocale-ruby`, not `multilocale`: that name belongs to
|
|
118
|
+
the npm CLI, and two tools fighting over one name on `PATH` helps nobody.
|
|
119
|
+
|
|
120
|
+
## In a Rails application
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
# lib/tasks/multilocale.rake
|
|
124
|
+
require "multilocale"
|
|
125
|
+
|
|
126
|
+
namespace :multilocale do
|
|
127
|
+
desc "Download the project's phrases into config/locales"
|
|
128
|
+
task :pull do
|
|
129
|
+
result = Multilocale::Sync.new(
|
|
130
|
+
client: Multilocale.client,
|
|
131
|
+
config: Multilocale::ConfigFile.discover(Rails.root)
|
|
132
|
+
).pull
|
|
133
|
+
|
|
134
|
+
puts "#{result.phrases} phrases in #{result.languages.size} languages"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`config/locales/*.yml` is already on `I18n.load_path`, so `t("cart.title")`
|
|
140
|
+
works with no further wiring. `example/` in this repository is the same thing in
|
|
141
|
+
Sinatra, small enough to read in one sitting.
|
|
142
|
+
|
|
143
|
+
## Library
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
client = Multilocale::Client.new(api_key: ENV["MULTILOCALE_API_KEY"])
|
|
147
|
+
|
|
148
|
+
client.projects.list # => [Multilocale::Project]
|
|
149
|
+
client.projects.find("website") # by id or by name
|
|
150
|
+
client.projects.create(name: "docs", default_locale: "en", locales: %w[en fr])
|
|
151
|
+
client.projects.update(id, locales: %w[en fr de]) # replaces the locale list
|
|
152
|
+
|
|
153
|
+
client.phrases.list(project: "website", language: "es")
|
|
154
|
+
client.phrases.list(project: "website", key: "cart.title")
|
|
155
|
+
client.phrases.upsert(key: "cart.title", value: "Cart", language: "en", projects: ["website"])
|
|
156
|
+
client.phrases.update(phrase_id, value: "Basket")
|
|
157
|
+
client.phrases.delete(key: "cart.title", project: "website")
|
|
158
|
+
|
|
159
|
+
client.dictionary(project: "website", language: "es") # => Multilocale::Dictionary
|
|
160
|
+
client.dictionaries(project: "website") # => { "en" => …, "es" => … }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Every call raises a `Multilocale::Error` subclass on failure —
|
|
164
|
+
`AuthenticationError` (401), `PermissionError` (403), `NotFoundError` (404),
|
|
165
|
+
`RateLimitedError` (429), `ServerError` (5xx), `ConnectionError`. 429 and 5xx
|
|
166
|
+
are retried with exponential backoff; 4xx is not, because retrying a missing
|
|
167
|
+
scope only burns rate limit.
|
|
168
|
+
|
|
169
|
+
### Things worth knowing before you write
|
|
170
|
+
|
|
171
|
+
- **`locales:` on a project update is a replacement, not a merge.** Read the
|
|
172
|
+
project, add to `project.locales`, send the whole list back. Omitting a
|
|
173
|
+
locale removes it.
|
|
174
|
+
- **`delete` removes every language of a key**, not one row — and rows shared
|
|
175
|
+
with other projects are deleted, not detached from this one.
|
|
176
|
+
- **`upsert` overwrites.** It is not a patch: send the whole row.
|
|
177
|
+
- **A phrase needs `projects: [name]`.** Without it the row exists in the
|
|
178
|
+
organization but belongs to no project, and no download will include it.
|
|
179
|
+
- **`list` without `limit` returns everything.** Pass `limit`/`skip` only when
|
|
180
|
+
you want a page; they cap at 2001 and 10000.
|
|
181
|
+
|
|
182
|
+
## Locale files
|
|
183
|
+
|
|
184
|
+
`config/locales/es.yml`, as written by `pull`:
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
# Generated by `multilocale-ruby pull`. Edit the phrases on multilocale.com, not here.
|
|
188
|
+
---
|
|
189
|
+
es:
|
|
190
|
+
cart:
|
|
191
|
+
title: Carrito
|
|
192
|
+
greeting: "¡Hola, %{name}!"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Two details that matter to Ruby specifically:
|
|
196
|
+
|
|
197
|
+
- **Dotted keys are nested.** i18n resolves `t("cart.title")` by walking
|
|
198
|
+
hashes, so a flat `"cart.title":` key would never be found. `push` flattens
|
|
199
|
+
them again on the way back.
|
|
200
|
+
- **No `locale:` entry is injected.** The npm CLI writes one into every
|
|
201
|
+
dictionary it generates; in a Rails locale file it would show up as the
|
|
202
|
+
translation `t("locale")`.
|
|
203
|
+
|
|
204
|
+
`%{name}` and `%{count}` survive machine translation: Multilocale checks that
|
|
205
|
+
every `{…}` placeholder in the source is still present in the translation and
|
|
206
|
+
repairs it if not.
|
|
207
|
+
|
|
208
|
+
Pluralisation is ordinary i18n — store `items.one` and `items.other` as two
|
|
209
|
+
keys and call `t("items", count: 3)`.
|
|
210
|
+
|
|
211
|
+
## Example application
|
|
212
|
+
|
|
213
|
+
```console
|
|
214
|
+
cd example
|
|
215
|
+
bundle install
|
|
216
|
+
bundle exec ruby app.rb # http://localhost:4567
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Four locales, translations committed, no credentials needed. `/en/`, `/es/`,
|
|
220
|
+
`/fr/`, `/it/`. It resolves the gem from `path: ".."`, so it runs against the
|
|
221
|
+
code in this checkout and works before the gem is on RubyGems.
|
|
222
|
+
|
|
223
|
+
## Documentation
|
|
224
|
+
|
|
225
|
+
- REST API and CLI guides: <https://www.multilocale.com/developers/>
|
|
226
|
+
- Issues: <https://github.com/multilocale/multilocale-ruby-gem/issues>
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
require_relative "../multilocale"
|
|
7
|
+
|
|
8
|
+
module Multilocale
|
|
9
|
+
# `multilocale-ruby` — the thin command wrapper around this gem.
|
|
10
|
+
#
|
|
11
|
+
# The name is not `multilocale` on purpose: that binary belongs to the npm
|
|
12
|
+
# CLI (`npm i -g multilocale`), and two tools fighting over one name on PATH
|
|
13
|
+
# is a support ticket nobody enjoys.
|
|
14
|
+
#
|
|
15
|
+
# Credentials come from the environment only. A secret passed as
|
|
16
|
+
# `--api-key=…` is in the process table for every user on the machine and in
|
|
17
|
+
# ~/.zsh_history forever, so there is no such flag.
|
|
18
|
+
class CLI
|
|
19
|
+
BANNER = <<~USAGE
|
|
20
|
+
multilocale-ruby #{VERSION}
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
multilocale-ruby pull [options] download phrases into locale files
|
|
24
|
+
multilocale-ruby push [options] upload locale files as phrases
|
|
25
|
+
multilocale-ruby projects [options] list the projects this credential can see
|
|
26
|
+
multilocale-ruby phrases [options] print one language as key = value
|
|
27
|
+
multilocale-ruby version
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
-p, --project NAME project id or name (default: multilocale.json)
|
|
31
|
+
-c, --config PATH path to multilocale.json (default: nearest one)
|
|
32
|
+
--path TEMPLATE output path containing %lang% (repeatable)
|
|
33
|
+
-l, --language LANG restrict to one language (repeatable)
|
|
34
|
+
--flat write flat "a.b" keys instead of nested hashes
|
|
35
|
+
--json machine-readable output
|
|
36
|
+
--yes required by push, which overwrites remote rows
|
|
37
|
+
-h, --help
|
|
38
|
+
|
|
39
|
+
Environment:
|
|
40
|
+
MULTILOCALE_API_KEY API key secret (app.multilocale.com -> API keys)
|
|
41
|
+
MULTILOCALE_ACCESS_TOKEN operator session token, as an alternative
|
|
42
|
+
MULTILOCALE_API_URL override the API host
|
|
43
|
+
MULTILOCALE_PROJECT default project id or name
|
|
44
|
+
USAGE
|
|
45
|
+
|
|
46
|
+
def initialize(stdout: $stdout, stderr: $stderr)
|
|
47
|
+
@stdout = stdout
|
|
48
|
+
@stderr = stderr
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns a process exit status: 0 ok, 1 failure, 2 usage.
|
|
52
|
+
def run(argv)
|
|
53
|
+
command = argv.first
|
|
54
|
+
|
|
55
|
+
return usage(0) if command.nil? || %w[-h --help help].include?(command)
|
|
56
|
+
return version if %w[version --version -v].include?(command)
|
|
57
|
+
|
|
58
|
+
options = parse(argv[1..] || [])
|
|
59
|
+
|
|
60
|
+
case command
|
|
61
|
+
when "pull" then pull(options)
|
|
62
|
+
when "push" then push(options)
|
|
63
|
+
when "projects" then projects(options)
|
|
64
|
+
when "phrases" then phrases(options)
|
|
65
|
+
else
|
|
66
|
+
@stderr.puts("Unknown command: #{command}")
|
|
67
|
+
usage(2)
|
|
68
|
+
end
|
|
69
|
+
rescue ConfigurationError, ApiError, ConnectionError, Error, ArgumentError => error
|
|
70
|
+
@stderr.puts(error.message)
|
|
71
|
+
1
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def parse(argv)
|
|
77
|
+
options = { paths: [], languages: [] }
|
|
78
|
+
|
|
79
|
+
parser = OptionParser.new do |parser|
|
|
80
|
+
parser.banner = BANNER
|
|
81
|
+
parser.on("-p", "--project NAME") { |value| options[:project] = value }
|
|
82
|
+
parser.on("-c", "--config PATH") { |value| options[:config] = value }
|
|
83
|
+
parser.on("--path TEMPLATE") { |value| options[:paths] << value }
|
|
84
|
+
parser.on("-l", "--language LANG") { |value| options[:languages] << value }
|
|
85
|
+
parser.on("--flat") { options[:flat] = true }
|
|
86
|
+
parser.on("--json") { options[:json] = true }
|
|
87
|
+
parser.on("--yes") { options[:yes] = true }
|
|
88
|
+
parser.on("-h", "--help") { options[:help] = true }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
parser.parse(argv)
|
|
92
|
+
options
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def usage(status)
|
|
96
|
+
(status.zero? ? @stdout : @stderr).puts(BANNER)
|
|
97
|
+
status
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def version
|
|
101
|
+
@stdout.puts("multilocale-ruby #{VERSION}")
|
|
102
|
+
0
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def client(options)
|
|
106
|
+
# Passing `project: nil` would override the client's own MULTILOCALE_PROJECT
|
|
107
|
+
# default with nil, so the keyword is only supplied when there is one.
|
|
108
|
+
options[:project] ? Client.new(project: options[:project]) : Client.new
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def config(options)
|
|
112
|
+
return ConfigFile.load(options[:config]) if options[:config]
|
|
113
|
+
|
|
114
|
+
ConfigFile.discover
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def sync(options)
|
|
118
|
+
Sync.new(
|
|
119
|
+
client: client(options),
|
|
120
|
+
config: config(options),
|
|
121
|
+
project: options[:project],
|
|
122
|
+
paths: (options[:paths].empty? ? nil : options[:paths]),
|
|
123
|
+
nested: (options[:flat] ? false : nil)
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def pull(options)
|
|
128
|
+
return usage(0) if options[:help]
|
|
129
|
+
|
|
130
|
+
result = sync(options).pull(languages: languages(options))
|
|
131
|
+
|
|
132
|
+
if options[:json]
|
|
133
|
+
@stdout.puts(JSON.pretty_generate(
|
|
134
|
+
ok: true,
|
|
135
|
+
project: result.project.name,
|
|
136
|
+
phrases: result.phrases,
|
|
137
|
+
languages: result.languages,
|
|
138
|
+
empty_locales: result.empty_locales,
|
|
139
|
+
files: result.files
|
|
140
|
+
))
|
|
141
|
+
else
|
|
142
|
+
@stdout.puts("#{result.project.name}: #{result.phrases} phrases in #{result.languages.size} languages")
|
|
143
|
+
result.files.each { |file| @stdout.puts(" wrote #{relative(file)}") }
|
|
144
|
+
unless result.empty_locales.empty?
|
|
145
|
+
@stdout.puts(" no phrases yet for: #{result.empty_locales.join(', ')} (files left untouched)")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
0
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def push(options)
|
|
153
|
+
return usage(0) if options[:help]
|
|
154
|
+
|
|
155
|
+
unless options[:yes]
|
|
156
|
+
@stderr.puts("push overwrites the remote value of every key in the files it reads. Re-run with --yes.")
|
|
157
|
+
return 2
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
rows = sync(options).push(languages: languages(options))
|
|
161
|
+
|
|
162
|
+
if options[:json]
|
|
163
|
+
@stdout.puts(JSON.pretty_generate(ok: true, phrases: rows.size))
|
|
164
|
+
else
|
|
165
|
+
@stdout.puts("pushed #{rows.size} phrases")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
0
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def projects(options)
|
|
172
|
+
return usage(0) if options[:help]
|
|
173
|
+
|
|
174
|
+
list = client(options).projects.list
|
|
175
|
+
|
|
176
|
+
if options[:json]
|
|
177
|
+
@stdout.puts(JSON.pretty_generate(list.map(&:to_h)))
|
|
178
|
+
else
|
|
179
|
+
list.sort_by { |project| project.name.to_s }.each do |project|
|
|
180
|
+
@stdout.puts("#{project.name} #{project.id} #{project.locales.size} locales")
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
0
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def phrases(options)
|
|
188
|
+
return usage(0) if options[:help]
|
|
189
|
+
|
|
190
|
+
language = languages(options)&.first
|
|
191
|
+
raise ArgumentError, "phrases needs --language" if language.nil?
|
|
192
|
+
|
|
193
|
+
dictionary = client(options).dictionary(
|
|
194
|
+
project: options[:project] || config(options)&.project,
|
|
195
|
+
language: language
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if options[:json]
|
|
199
|
+
@stdout.puts(JSON.pretty_generate(dictionary.to_h))
|
|
200
|
+
else
|
|
201
|
+
dictionary.each { |key, value| @stdout.puts("#{key} = #{value}") }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
0
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def languages(options)
|
|
208
|
+
options[:languages].empty? ? nil : options[:languages]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def relative(path)
|
|
212
|
+
path.sub("#{Dir.pwd}/", "")
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|