web_translate_it 3.2.3 → 3.3.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 +4 -4
- data/bin/wti +1 -0
- data/history.md +6 -0
- data/lib/web_translate_it/api_resource.rb +1 -1
- data/lib/web_translate_it/commands/base.rb +6 -2
- data/lib/web_translate_it/commands/pull.rb +75 -1
- data/lib/web_translate_it/project.rb +10 -0
- data/lib/web_translate_it/translation_base.rb +1 -1
- data/lib/web_translate_it/translation_file.rb +48 -20
- data/lib/web_translate_it/util/http_response.rb +1 -1
- data/lib/web_translate_it.rb +2 -1
- data/man/wti.1.ron +11 -1
- data/readme.md +4 -0
- metadata +31 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72c1ce39a90e605ff2b39b63440181d91049014e83d2eba2506185ffe901c3c1
|
|
4
|
+
data.tar.gz: 5051fbc9a0f55f21351c821098aef8bd4111f1be4d735706389eab5d231c7f60
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 830cf16e7dfdd3ece01f3bce72e5c60d750facaade45aa8626035acf8916b6524590ebd88d0015d7874499177b216c340eb7a0c563665883fb2945ab820d9b97
|
|
7
|
+
data.tar.gz: b631196b93ffb7a6e4e48b207a958d0e626a6f1e68bcbc825cb5e6db971a727e8cb74405af49f36ad1976275efa2d349b1772ee553a401cbd11df12577e79ff9
|
data/bin/wti
CHANGED
|
@@ -46,6 +46,7 @@ when 'pull'
|
|
|
46
46
|
opt :all, 'Pull all files'
|
|
47
47
|
opt :force, 'Force pull (bypass conditional requests to WTI)'
|
|
48
48
|
opt :threads, 'Number of threads for parallel downloads', type: :integer, default: 10
|
|
49
|
+
opt :zip, 'Download the project as a zip archive instead of one request per file'
|
|
49
50
|
opt :config, 'Path to a configuration file', short: '-c', default: '.wti'
|
|
50
51
|
opt :debug, 'Display debug information'
|
|
51
52
|
end
|
data/history.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## Version 3.3.0 / 2026-09-17
|
|
2
|
+
|
|
3
|
+
* Add `--zip` option to `wti pull`. It downloads the language files through the zip file endpoint: one request per locale instead of one request per file, and a single request when the whole project is pulled. Much faster on projects with many files, for instance when pulling translations in a build. Files already up to date are left alone, only the locales needing an update are requested, and an archive which fails to download fails its own files instead of aborting the pull. #473
|
|
4
|
+
* **`wti pull` now reports download and write failures in its exit code.** `Commands::Base#with_connection` returned the connection rather than the block's value, so `pull` discarded its per-file results and always exited `0`. Builds which were silently green on a partially failed pull will start failing.
|
|
5
|
+
* Support json 3. `multi_json` 1.19 passes parse options positionally, which json 3 rejects, so API error messages came back as raw JSON. Replace `multi_json` with the `json` gem, which ships with Ruby, and run CI against both json 2 and json 3.
|
|
6
|
+
|
|
1
7
|
## Version 3.2.3 / 2026-04-09
|
|
2
8
|
|
|
3
9
|
* Handle HTTP 429 (rate limit) errors: add `RateLimitError` class, retry with backoff respecting the `Retry-After` header, and fix garbled error messages when the response body is not JSON.
|
|
@@ -123,7 +123,7 @@ module WebTranslateIt
|
|
|
123
123
|
def to_json(*_args, with_translations: false)
|
|
124
124
|
hash = to_hash
|
|
125
125
|
hash['translations'] = translations.map(&:to_hash) if translations.any? && with_translations
|
|
126
|
-
|
|
126
|
+
JSON.generate(hash)
|
|
127
127
|
end
|
|
128
128
|
|
|
129
129
|
private
|
|
@@ -29,8 +29,12 @@ module WebTranslateIt
|
|
|
29
29
|
exit 1
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
# Yields a connection and returns the block's value. `Connection.new`
|
|
33
|
+
# returns the connection itself, so the block's value has to be captured.
|
|
34
|
+
def with_connection
|
|
35
|
+
result = nil
|
|
36
|
+
WebTranslateIt::Connection.new(configuration.api_key) { |conn| result = yield conn }
|
|
37
|
+
result
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
def run_hook(hook_command, label)
|
|
@@ -13,7 +13,9 @@ module WebTranslateIt
|
|
|
13
13
|
if files.empty?
|
|
14
14
|
puts 'No files to pull.'
|
|
15
15
|
else
|
|
16
|
-
|
|
16
|
+
# `command_options[:zip]`, not `.zip`: Optimist's option hash falls back
|
|
17
|
+
# to `method_missing`, and `Enumerable#zip` gets there first.
|
|
18
|
+
complete_success = command_options[:zip] ? pull_archives(files) : pull_files(files)
|
|
17
19
|
run_hook(configuration.after_pull, 'after_pull')
|
|
18
20
|
end
|
|
19
21
|
complete_success
|
|
@@ -43,6 +45,78 @@ module WebTranslateIt
|
|
|
43
45
|
results.all?
|
|
44
46
|
end
|
|
45
47
|
|
|
48
|
+
# Pull through the zip file endpoint: one request returns every language
|
|
49
|
+
# file of the project, which beats one request per file when a project has
|
|
50
|
+
# many files or many locales.
|
|
51
|
+
def pull_archives(files)
|
|
52
|
+
time = Time.now
|
|
53
|
+
outdated = files.select { |file| file.outdated?(command_options.force) }
|
|
54
|
+
locales = archive_locales(outdated)
|
|
55
|
+
saved = save_archives(outdated, locales)
|
|
56
|
+
results = files.map { |file| saved.fetch(file) { file.skipped } }
|
|
57
|
+
report_archives(results, locales.size, Time.now - time)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def report_archives(results, requests, elapsed) # rubocop:todo Naming/PredicateMethod
|
|
61
|
+
results.each { |result| print StringUtil.array_to_columns(result.output) }
|
|
62
|
+
puts "Pulled #{results.size} files in #{elapsed.round(1)}s, using #{requests} archive request(s)."
|
|
63
|
+
results.all?(&:success)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The zip file endpoint serves either the whole project or a single
|
|
67
|
+
# locale, so ask for the whole project when every locale is wanted and
|
|
68
|
+
# fall back to one request per locale otherwise.
|
|
69
|
+
def archive_locales(files)
|
|
70
|
+
return [] if files.empty?
|
|
71
|
+
|
|
72
|
+
locales = files.map(&:locale).uniq
|
|
73
|
+
(configuration.target_locales + [configuration.source_locale] - locales).empty? ? [nil] : locales
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Returns the result of writing each file, keyed by file.
|
|
77
|
+
def save_archives(files, locales)
|
|
78
|
+
return {} if locales.empty?
|
|
79
|
+
|
|
80
|
+
by_path = files.to_h { |file| [file.file_path, file] }
|
|
81
|
+
results, = Concurrency.concurrent_batch(locales, batch_size: 1, max_threads: command_options.threads) do |batch|
|
|
82
|
+
with_connection { |conn| batch.flat_map { |locale| save_archive(conn, locale, by_path) } }
|
|
83
|
+
end
|
|
84
|
+
report_missing(files, results.to_h)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# An archive which can't be downloaded or read — an unknown locale, a
|
|
88
|
+
# server error, a truncated response — fails the files it was carrying
|
|
89
|
+
# rather than aborting the whole pull and leaving the other threads to be
|
|
90
|
+
# killed mid-write.
|
|
91
|
+
def save_archive(connection, locale, by_path)
|
|
92
|
+
extract(Project.fetch_zip(connection, locale: locale), by_path)
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
by_path.each_value.select { |file| locale.nil? || file.locale == locale }
|
|
95
|
+
.map { |file| [file, file.failed("An error occured: #{e.message}")] }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def extract(archive, by_path)
|
|
99
|
+
Tempfile.create(['wti-pull', '.zip']) do |tempfile|
|
|
100
|
+
tempfile.binmode
|
|
101
|
+
tempfile.write(archive)
|
|
102
|
+
tempfile.close
|
|
103
|
+
Zip::File.open(tempfile.path) { |zip| zip.filter_map { |entry| save_entry(entry, by_path) } }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def save_entry(entry, by_path)
|
|
108
|
+
# rubyzip hands entry names back as binary strings, which never match a
|
|
109
|
+
# path holding an accented or non-Latin character.
|
|
110
|
+
file = by_path[entry.name.dup.force_encoding(Encoding::UTF_8)]
|
|
111
|
+
[file, file.save(entry.get_input_stream(&:read))] if file
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# A file listed by the project but absent from the archive was deleted
|
|
115
|
+
# between the two requests. Report it rather than silently skipping it.
|
|
116
|
+
def report_missing(files, saved)
|
|
117
|
+
saved.merge(files.difference(saved.keys).to_h { |file| [file, file.save(nil)] })
|
|
118
|
+
end
|
|
119
|
+
|
|
46
120
|
def fetch_locales # rubocop:todo Metrics/AbcSize
|
|
47
121
|
locales = if command_options.locale
|
|
48
122
|
warn_unknown_locales(command_options.locale.split)
|
|
@@ -21,6 +21,16 @@ module WebTranslateIt
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# Fetch every language file of a project in a single request, as a zip
|
|
25
|
+
# archive. Pass a locale to narrow the archive down to that locale.
|
|
26
|
+
def self.fetch_zip(connection, locale: nil)
|
|
27
|
+
url = "/api/projects/#{connection.api_key}/zip_file"
|
|
28
|
+
url += "?locale=#{URI.encode_www_form_component(locale)}" if locale
|
|
29
|
+
Concurrency.with_retries do
|
|
30
|
+
HttpResponse.handle_response(connection.get(url))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
24
34
|
def self.create_locale(connection, locale_code)
|
|
25
35
|
Concurrency.with_retries do
|
|
26
36
|
response = connection.post("/api/projects/#{connection.api_key}/locales") { |req| req.set_form_data({'id' => locale_code}, ';') }
|
|
@@ -50,28 +50,44 @@ module WebTranslateIt
|
|
|
50
50
|
# file.fetch # returns nothing, with a status 304 Not Modified
|
|
51
51
|
# file.fetch(true) # force to re-download the file, will return the content of the file with a 200 OK
|
|
52
52
|
#
|
|
53
|
-
def fetch(connection, force = false)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
def fetch(connection, force = false)
|
|
54
|
+
return skipped unless outdated?(force)
|
|
55
|
+
|
|
56
|
+
make_directory
|
|
57
|
+
with_display(display_columns) do
|
|
58
|
+
response = connection.get(api_url)
|
|
59
|
+
File.open(file_path, 'wb') { |file| file << response.body } if response.code.to_i == 200
|
|
60
|
+
response
|
|
59
61
|
end
|
|
60
|
-
|
|
61
|
-
if !File.exist?(file_path) || force || (remote_checksum != local_checksum)
|
|
62
|
-
|
|
63
|
-
dir = File.dirname(file_path)
|
|
64
|
-
FileUtils.mkpath(dir) unless File.exist?(file_path) || dir == '.'
|
|
65
|
-
with_display(display) do
|
|
66
|
-
response = connection.get(api_url)
|
|
67
|
-
File.open(file_path, 'wb') { |file| file << response.body } if response.code.to_i == 200
|
|
68
|
-
response
|
|
69
|
-
end
|
|
62
|
+
end
|
|
70
63
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
64
|
+
# Write a language file taken out of a project archive, as downloaded by
|
|
65
|
+
# `wti pull --zip`. `content` is nil when the archive didn't carry the file,
|
|
66
|
+
# which happens when it was deleted between listing the project and
|
|
67
|
+
# downloading the archive.
|
|
68
|
+
def save(content)
|
|
69
|
+
return failed('Missing from archive') if content.nil?
|
|
70
|
+
|
|
71
|
+
make_directory
|
|
72
|
+
File.open(file_path, 'wb') { |file| file << content }
|
|
73
|
+
Result.new(true, display_columns.push(StringUtil.success('OK')))
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
failed("An error occured: #{e.message}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The result of a file which could not be written.
|
|
79
|
+
def failed(message)
|
|
80
|
+
Result.new(false, display_columns.push(StringUtil.failure(message)))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Whether the local file is missing or differs from the copy on WebTranslateIt.
|
|
84
|
+
def outdated?(force = false)
|
|
85
|
+
!File.exist?(file_path) || force || remote_checksum != local_checksum
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The result of leaving an already up-to-date file alone.
|
|
89
|
+
def skipped
|
|
90
|
+
Result.new(true, display_columns.push(StringUtil.success('Skipped')))
|
|
75
91
|
end
|
|
76
92
|
|
|
77
93
|
def fetch_remote_content(connection)
|
|
@@ -191,6 +207,18 @@ module WebTranslateIt
|
|
|
191
207
|
|
|
192
208
|
private
|
|
193
209
|
|
|
210
|
+
# The first two columns of a pull or push line: the file path, prefixed with
|
|
211
|
+
# a star when its translations are not up to date, and the local and remote
|
|
212
|
+
# checksums.
|
|
213
|
+
def display_columns
|
|
214
|
+
[fresh ? file_path : "*#{file_path}", "#{StringUtil.checksumify(local_checksum.to_s)}..#{StringUtil.checksumify(remote_checksum.to_s)}"]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def make_directory
|
|
218
|
+
dir = File.dirname(file_path)
|
|
219
|
+
FileUtils.mkpath(dir) unless File.exist?(file_path) || dir == '.'
|
|
220
|
+
end
|
|
221
|
+
|
|
194
222
|
def with_display(display)
|
|
195
223
|
Concurrency.with_retries do
|
|
196
224
|
response = yield
|
data/lib/web_translate_it.rb
CHANGED
|
@@ -8,9 +8,10 @@ require 'net/http'
|
|
|
8
8
|
require 'net/https'
|
|
9
9
|
require 'openssl'
|
|
10
10
|
require 'uri'
|
|
11
|
-
require 'multi_json'
|
|
12
11
|
require 'digest/sha1'
|
|
13
12
|
require 'English'
|
|
13
|
+
require 'tempfile'
|
|
14
|
+
require 'zip'
|
|
14
15
|
|
|
15
16
|
require 'web_translate_it/connection'
|
|
16
17
|
require 'web_translate_it/util'
|
data/man/wti.1.ron
CHANGED
|
@@ -45,7 +45,17 @@ These options can be used to change this behaviour:
|
|
|
45
45
|
The Web Translate It API use HTTP caching to be efficient, and check if your file needs
|
|
46
46
|
to be updated by checking its modification date against the project’s latest activity.
|
|
47
47
|
Use this option to bypass this check.
|
|
48
|
-
|
|
48
|
+
|
|
49
|
+
* `--zip`:
|
|
50
|
+
Pull the language files as zip archives, one request per locale instead of one
|
|
51
|
+
request per file. Pulling the whole project, with `--all` and no ignored or needed
|
|
52
|
+
locales configured, takes a single request. Much faster on projects with many files.
|
|
53
|
+
Files already up to date are left alone, and only the locales needing an update
|
|
54
|
+
are requested.
|
|
55
|
+
|
|
56
|
+
* `--threads`:
|
|
57
|
+
Number of parallel downloads. Defaults to 10.
|
|
58
|
+
|
|
49
59
|
You may additionally ask for help:
|
|
50
60
|
|
|
51
61
|
* `-h`, `--help`:
|
data/readme.md
CHANGED
|
@@ -177,6 +177,10 @@ Append `--help` for each command for more information. For instance:
|
|
|
177
177
|
<td>wti pull --force</td>
|
|
178
178
|
<td>Force pull (to bypass WebTranslateIt’s HTTP caching)</td>
|
|
179
179
|
</tr>
|
|
180
|
+
<tr>
|
|
181
|
+
<td>wti pull --zip</td>
|
|
182
|
+
<td>Download the language files as zip archives: one request per locale instead of one per file, or a single request when the whole project is pulled. Much faster on projects with many files</td>
|
|
183
|
+
</tr>
|
|
180
184
|
<tr>
|
|
181
185
|
<td>wti addlocale fr</td>
|
|
182
186
|
<td>Add a new locale to the project</td>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: web_translate_it
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Edouard Briere
|
|
@@ -10,19 +10,25 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: json
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '2.5'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '4'
|
|
19
22
|
type: :runtime
|
|
20
23
|
prerelease: false
|
|
21
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
25
|
requirements:
|
|
23
26
|
- - ">="
|
|
24
27
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
28
|
+
version: '2.5'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '4'
|
|
26
32
|
- !ruby/object:Gem::Dependency
|
|
27
33
|
name: optimist
|
|
28
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -37,6 +43,26 @@ dependencies:
|
|
|
37
43
|
- - "~>"
|
|
38
44
|
- !ruby/object:Gem::Version
|
|
39
45
|
version: '3.0'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: rubyzip
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '2.3'
|
|
53
|
+
- - "<"
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '4'
|
|
56
|
+
type: :runtime
|
|
57
|
+
prerelease: false
|
|
58
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '2.3'
|
|
63
|
+
- - "<"
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '4'
|
|
40
66
|
description: A Command Line Interface tool to push and pull language files to WebTranslateIt.com.
|
|
41
67
|
email: support@webtranslateit.com
|
|
42
68
|
executables:
|
|
@@ -113,7 +139,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
113
139
|
- !ruby/object:Gem::Version
|
|
114
140
|
version: '0'
|
|
115
141
|
requirements: []
|
|
116
|
-
rubygems_version: 4.0.
|
|
142
|
+
rubygems_version: 4.0.20
|
|
117
143
|
specification_version: 4
|
|
118
144
|
summary: A CLI tool to sync locale files with WebTranslateIt.com.
|
|
119
145
|
test_files: []
|