web_translate_it 3.2.2 → 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: 2344f55bed19e435f8334c66229e58266ff1efe3cf54e9e81f4d17287f42a93d
4
- data.tar.gz: 89c6ea08bcc87a43741c8419d5de32e2441f896f12e8338762c15cf84759ab5e
3
+ metadata.gz: 72c1ce39a90e605ff2b39b63440181d91049014e83d2eba2506185ffe901c3c1
4
+ data.tar.gz: 5051fbc9a0f55f21351c821098aef8bd4111f1be4d735706389eab5d231c7f60
5
5
  SHA512:
6
- metadata.gz: c4a9b5b8f2874c7779eedb0d413d826a1d9bccbd8e9db33834ab82a0b8b22b2fb719bf412e7339cb05880018ddc072ee6e70a370824f3fc96f7d6982a2f8396f
7
- data.tar.gz: ad5b96143c805efa324ea6119af80dd24517c2e41691b451920934a3deea7115385c13f859ccff7d314c034680f64b95033aba839b1bc4728ff09aa885395a00
6
+ metadata.gz: 830cf16e7dfdd3ece01f3bce72e5c60d750facaade45aa8626035acf8916b6524590ebd88d0015d7874499177b216c340eb7a0c563665883fb2945ab820d9b97
7
+ data.tar.gz: b631196b93ffb7a6e4e48b207a958d0e626a6f1e68bcbc825cb5e6db971a727e8cb74405af49f36ad1976275efa2d349b1772ee553a401cbd11df12577e79ff9
data/bin/wti CHANGED
@@ -42,11 +42,13 @@ when 'pull'
42
42
  wti pull [filename] - Pull target language file(s)
43
43
  [options] are:
44
44
  BANNER
45
- opt :locale, 'ISO code of locale(s) to pull, space-separated', type: :string
46
- opt :all, 'Pull all files'
47
- opt :force, 'Force pull (bypass conditional requests to WTI)'
48
- opt :config, 'Path to a configuration file', short: '-c', default: '.wti'
49
- opt :debug, 'Display debug information'
45
+ opt :locale, 'ISO code of locale(s) to pull, space-separated', type: :string
46
+ opt :all, 'Pull all files'
47
+ opt :force, 'Force pull (bypass conditional requests to WTI)'
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'
50
+ opt :config, 'Path to a configuration file', short: '-c', default: '.wti'
51
+ opt :debug, 'Display debug information'
50
52
  end
51
53
  when 'push'
52
54
  Optimist.options do
data/history.md CHANGED
@@ -1,3 +1,15 @@
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
+
7
+ ## Version 3.2.3 / 2026-04-09
8
+
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.
10
+ * Add `--threads N` option to `wti pull` to control the number of concurrent download threads. Defaults to 10. Use `--threads 1` for sequential pulls.
11
+ * Modernize code for Ruby 3.0+: use `match?` instead of `!~`, `start_with?`/`delete_prefix` instead of index slicing, array difference instead of `reject`, and remove redundant `|| nil` fallbacks.
12
+
1
13
  ## Version 3.2.2 / 2026-03-03
2
14
 
3
15
  * Replace O(n²) string concatenation loop in `StringUtil#backward_truncate` with `String#ljust`.
@@ -9,9 +9,9 @@ module WebTranslateIt
9
9
  def initialize(params = {}, connection: nil)
10
10
  params = params.transform_keys(&:to_s)
11
11
  self.connection = connection
12
- self.id = params['id'] || nil
13
- self.created_at = params['created_at'] || nil
14
- self.updated_at = params['updated_at'] || nil
12
+ self.id = params['id']
13
+ self.created_at = params['created_at']
14
+ self.updated_at = params['updated_at']
15
15
  self.translations = params['translations'] || []
16
16
  self.new_record = true
17
17
  assign_attributes(params)
@@ -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,7 +29,7 @@ module WebTranslateIt
29
29
  end
30
30
 
31
31
  def valid_request?(env)
32
- env['PATH_INFO'] !~ /\.(js|css|jpeg|jpg|gif|png|woff)$/
32
+ !env['PATH_INFO'].match?(/\.(js|css|jpeg|jpg|gif|png|woff)$/)
33
33
  end
34
34
 
35
35
  end
@@ -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
@@ -29,7 +31,7 @@ module WebTranslateIt
29
31
 
30
32
  def pull_files(files) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength, Naming/PredicateMethod
31
33
  time = Time.now
32
- results, n_threads = Concurrency.concurrent_batch(files) do |batch|
34
+ results, n_threads = Concurrency.concurrent_batch(files, max_threads: command_options.threads) do |batch|
33
35
  with_connection do |conn|
34
36
  batch.map do |file|
35
37
  result = file.fetch(conn, command_options.force)
@@ -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)
@@ -37,7 +37,7 @@ module WebTranslateIt
37
37
  if command_options.locale
38
38
  warn_unknown_locales(command_options.locale.split)
39
39
  elsif command_options.target
40
- configuration.target_locales.reject { |locale| locale == configuration.source_locale }
40
+ configuration.target_locales - [configuration.source_locale]
41
41
  else
42
42
  [configuration.source_locale]
43
43
  end
@@ -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}, ';') }
@@ -16,16 +16,16 @@ module WebTranslateIt
16
16
 
17
17
  protected
18
18
 
19
- def assign_attributes(params) # rubocop:todo Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
20
- self.key = params['key'] || nil
21
- self.plural = params['plural'] || nil
22
- self.type = params['type'] || nil
23
- self.dev_comment = params['dev_comment'] || nil
24
- self.word_count = params['word_count'] || nil
25
- self.status = params['status'] || nil
26
- self.category = params['category'] || nil
27
- self.labels = params['labels'] || nil
28
- self.file = params['file'] || nil
19
+ def assign_attributes(params) # rubocop:todo Metrics/AbcSize
20
+ self.key = params['key']
21
+ self.plural = params['plural']
22
+ self.type = params['type']
23
+ self.dev_comment = params['dev_comment']
24
+ self.word_count = params['word_count']
25
+ self.status = params['status']
26
+ self.category = params['category']
27
+ self.labels = params['labels']
28
+ self.file = params['file']
29
29
  end
30
30
 
31
31
  def parse_translation_response(json)
@@ -13,8 +13,8 @@ module WebTranslateIt
13
13
  protected
14
14
 
15
15
  def assign_attributes(params)
16
- self.text = params['text'] || nil
17
- self.description = params['description'] || nil
16
+ self.text = params['text']
17
+ self.description = params['description']
18
18
  end
19
19
 
20
20
  def parse_translation_response(json)
@@ -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
@@ -4,18 +4,27 @@ module WebTranslateIt
4
4
 
5
5
  module Concurrency
6
6
 
7
- # Execute a block with automatic retry on Timeout::Error.
7
+ # Execute a block with automatic retry on Timeout::Error and RateLimitError.
8
8
  # Returns the block's return value on success, or re-raises after retries are exhausted.
9
9
  def self.with_retries(retries: 3, delay: 5)
10
10
  yield
11
11
  rescue Timeout::Error
12
- puts "Request timeout. Will retry in #{delay} seconds."
13
- if (retries -= 1).positive?
14
- sleep(delay)
15
- retry
16
- end
17
- raise
12
+ raise unless (retries -= 1).positive?
13
+
14
+ log_retry('Request timeout', delay)
15
+ retry
16
+ rescue RateLimitError => e
17
+ raise unless (retries -= 1).positive?
18
+
19
+ log_retry('Rate limited', e.retry_after || delay)
20
+ retry
21
+ end
22
+
23
+ def self.log_retry(message, wait)
24
+ puts "#{message}. Will retry in #{wait} seconds."
25
+ sleep(wait)
18
26
  end
27
+ private_class_method :log_retry
19
28
 
20
29
  # Process items in parallel using a thread pool.
21
30
  # Yields each batch (array of items) to the block; collects return values.
@@ -2,6 +2,17 @@
2
2
 
3
3
  module WebTranslateIt
4
4
 
5
+ class RateLimitError < StandardError
6
+
7
+ attr_reader :retry_after
8
+
9
+ def initialize(retry_after: nil)
10
+ @retry_after = retry_after
11
+ super("Rate limited#{", retry after #{retry_after}s" if retry_after}")
12
+ end
13
+
14
+ end
15
+
5
16
  module HttpResponse
6
17
 
7
18
  STATUS_LABELS = {
@@ -31,15 +42,24 @@ module WebTranslateIt
31
42
 
32
43
  def self.raise_on_error!(response)
33
44
  code = response.code.to_i
34
- if code >= 400 && code < 500
35
- raise "Error: #{MultiJson.load(response.body)['error']}"
36
- elsif code == 500
37
- raise 'Error: Server temporarily unavailable (Error 500).'
38
- elsif code == 503
39
- raise 'Error: Locked (another import in progress)'
40
- end
45
+ raise_on_rate_limit!(response) if code == 429
46
+ raise "Error: #{error_message(response)}" if code >= 400 && code < 500
47
+ raise 'Error: Server temporarily unavailable (Error 500).' if code == 500
48
+ raise 'Error: Locked (another import in progress)' if code == 503
41
49
  end
42
- private_class_method :raise_on_error!
50
+
51
+ def self.raise_on_rate_limit!(response)
52
+ retry_after = response['Retry-After']&.to_i
53
+ raise RateLimitError.new(retry_after: retry_after)
54
+ end
55
+
56
+ def self.error_message(response)
57
+ JSON.parse(response.body)['error']
58
+ rescue StandardError
59
+ response.body.to_s
60
+ end
61
+
62
+ private_class_method :raise_on_error!, :raise_on_rate_limit!, :error_message
43
63
 
44
64
  end
45
65
 
@@ -25,8 +25,8 @@ class StringUtil
25
25
  end
26
26
 
27
27
  def self.array_to_columns(array)
28
- if array[0][0] == '*'
29
- "*#{backward_truncate(array[0][1..])} | #{array[1]} #{array[2]}\n"
28
+ if array[0].start_with?('*')
29
+ "*#{backward_truncate(array[0].delete_prefix('*'))} | #{array[1]} #{array[2]}\n"
30
30
  else
31
31
  " #{backward_truncate(array[0])} | #{array[1]} #{array[2]}\n"
32
32
  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.2
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: 3.6.9
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: []