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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 660b650bae69f2f4a4282acb69b7abc72296b7e352660adaf3c1047a3bac04ce
4
- data.tar.gz: acf8cc2ef71f6a897d400cd99793b86c33d28672b8aa6049b49d1f3b3a2f3c16
3
+ metadata.gz: 72c1ce39a90e605ff2b39b63440181d91049014e83d2eba2506185ffe901c3c1
4
+ data.tar.gz: 5051fbc9a0f55f21351c821098aef8bd4111f1be4d735706389eab5d231c7f60
5
5
  SHA512:
6
- metadata.gz: dc29fee26eb5eb394705c81975adcf21f03dbe3ca7918d0e9a622a791ef0163ee9c7aa00f88d9c608eea462b5a75e8b9e4c7d530af276cfa3449ab7824ef53ec
7
- data.tar.gz: 148e6238f00cb1cd2030a8a9f13c1b155ed82c26baac67f6933d86da68463998a8f91f144339f8d538512fe7eee0ec47cab15b23a2de8bdab27df48f77176b76
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
- MultiJson.dump(hash)
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
- def with_connection(&block)
33
- WebTranslateIt::Connection.new(configuration.api_key, &block)
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
- complete_success = pull_files(files)
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}, ';') }
@@ -22,7 +22,7 @@ module WebTranslateIt
22
22
  end
23
23
 
24
24
  def to_json(*_args)
25
- MultiJson.dump(to_hash)
25
+ JSON.generate(to_hash)
26
26
  end
27
27
 
28
28
  protected
@@ -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) # rubocop:todo Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
54
- display = []
55
- if fresh
56
- display.push(file_path)
57
- else
58
- display.push("*#{file_path}")
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
- display.push "#{StringUtil.checksumify(local_checksum.to_s)}..#{StringUtil.checksumify(remote_checksum.to_s)}"
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
- else
72
- display.push StringUtil.success('Skipped')
73
- Result.new(true, display)
74
- end
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
@@ -54,7 +54,7 @@ module WebTranslateIt
54
54
  end
55
55
 
56
56
  def self.error_message(response)
57
- MultiJson.load(response.body)['error']
57
+ JSON.parse(response.body)['error']
58
58
  rescue StandardError
59
59
  response.body.to_s
60
60
  end
@@ -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.2.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: multi_json
13
+ name: json
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '0'
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: '0'
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.6
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: []