phraseapp_updater 3.3.0 → 3.3.2

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: a32efb606f930f644881f15a2c3c1edf0bf72eddefd4de9de61524145e8b74c0
4
- data.tar.gz: 3fd1b4164ee16b056b8ecc964863d66dac48a459aee374f1056268b524d2bbaf
3
+ metadata.gz: b44812204928f9576633c2d59bd8c4deb220cdd70e7cfb2f52cdc059368b7b8a
4
+ data.tar.gz: 13e282dcfc436150b6edecf384ae6b54c4133c8cb19a73bd5f723fe90bbb923b
5
5
  SHA512:
6
- metadata.gz: c03c6ffd3ef4c33825884d9e14552445d5a86d3e7248d3295e3998a66b482f496bf716961bb61df602f293681ad4a755082813ce997e41a3507414f7973f586e
7
- data.tar.gz: 0b08b5b4ddec6a3fd38675acf3d00c79f9bf3bc51dba94290cfde4a2f5223414b87ce50d7d9e06834c9c64872b111469625f4419afee17012c4dcc1171e3e3ea
6
+ metadata.gz: 5bd3fe2ab57e511236b31902d21d31d3e80d2e07029c1c7f2d52c4983bf263d5839ab85a6eef904967f6560f387af4312170d3032307b73a2456a816ef97f59b
7
+ data.tar.gz: c0d4677afd153d1b8f0aa95d0f58bb2b1c1568abd6ee68b646610316c8cd7f28a82e0670e5302d6e39c4f4543a025fa867f4850e05922892d22276b5ba771f11
@@ -4,6 +4,7 @@ require 'phraseapp_updater/locale_file'
4
4
  require 'phraseapp_updater/index_by'
5
5
  require 'uri'
6
6
  require 'phrase'
7
+ require 'concurrent'
7
8
  require 'parallel'
8
9
  require 'tempfile'
9
10
 
@@ -96,39 +97,61 @@ class PhraseAppUpdater
96
97
  end
97
98
  end
98
99
 
99
- # Empirically, PhraseApp fails to parse the uploaded files when uploaded in
100
- # parallel. Give it a better chance by uploading them one at a time.
101
100
  def upload_files(locale_files, default_locale:)
102
- is_default = ->(l) { l.locale_name == default_locale }
101
+ locale_files = locale_files.sort_by(&:locale_name)
102
+ default_locale_file = locale_files.detect { |l| l.locale_name == default_locale }
103
+ locale_files.delete(default_locale_file) if default_locale_file
103
104
 
104
- # Ensure the locales all exist
105
- STDERR.puts('Creating locales')
106
105
  known_locales = fetch_locales.index_by(&:name)
106
+
107
+ # Phraseapp appears to use to use the first file uploaded to resolve conflicts
108
+ # between pluralized and non-pluralized keys. Upload and verify the canonical
109
+ # default locale first before uploading translated locales.
110
+ if default_locale_file
111
+ unless known_locales.has_key?(default_locale_file.locale_name)
112
+ STDERR.puts("Creating default locale (#{default_locale_file})")
113
+ create_locale(default_locale_file.locale_name, default: true)
114
+ end
115
+
116
+ STDERR.puts("Uploading default locale (#{default_locale_file})")
117
+ upload_id = upload_file(default_locale_file)
118
+
119
+ successful_default_upload = verify_uploads({ upload_id => default_locale_file })
120
+ else
121
+ STDERR.puts("No upload for default locale (#{default_locale})")
122
+ end
123
+
124
+ # Ensure the locales all exist
125
+ STDERR.puts('Creating translation locales')
107
126
  threaded_request(locale_files) do |locale_file|
108
127
  unless known_locales.has_key?(locale_file.locale_name)
109
- create_locale(locale_file.locale_name, default: is_default.(locale_file))
128
+ create_locale(locale_file.locale_name, default: false)
110
129
  end
111
130
  end
112
131
 
113
- # Upload the files in a stable order, ensuring the default locale is first.
114
- locale_files.sort! do |a, b|
115
- next -1 if is_default.(a)
116
- next 1 if is_default.(b)
132
+ uploads = Concurrent::Hash.new
117
133
 
118
- a.locale_name <=> b.locale_name
134
+ threaded_request(locale_files) do |locale_file|
135
+ STDERR.puts("Uploading #{locale_file}")
136
+ upload_id = upload_file(locale_file)
137
+ uploads[upload_id] = locale_file
119
138
  end
120
139
 
121
- uploads = {}
140
+ successful_uploads = verify_uploads(uploads)
122
141
 
123
- uploads = locale_files.to_h do |locale_file|
124
- STDERR.puts("Uploading #{locale_file}")
125
- upload_id = upload_file(locale_file)
126
- [upload_id, locale_file]
142
+ if default_locale_file
143
+ successful_uploads = successful_uploads.merge(successful_default_upload)
127
144
  end
128
145
 
129
- # Validate the uploads, retrying failures as necessary
130
- successful_upload_ids = {}
146
+ successful_uploads
147
+ end
131
148
 
149
+ # Given a map of {upload_id => locale_file} pairs, use the upload_show
150
+ # API to verify that they're complete, and re-upload them if they failed.
151
+ # Return a map of locale name to upload id.
152
+ def verify_uploads(uploads)
153
+ successful_upload_ids = Concurrent::Hash.new
154
+ attempts = 1
132
155
  STDERR.puts('Verifying uploads...')
133
156
  until uploads.empty?
134
157
  threaded_request(uploads.to_a) do |upload_id, locale_file|
@@ -151,12 +174,18 @@ class PhraseAppUpdater
151
174
  end
152
175
  end
153
176
 
154
- sleep(2) unless uploads.empty?
177
+ unless uploads.empty?
178
+ delay = attempts ** 1.6 + 1
179
+ STDERR.puts("#{uploads.size} remaining, waiting #{delay.round} seconds...")
180
+ sleep(delay)
181
+ attempts += 1
182
+ end
155
183
  end
156
184
 
157
185
  successful_upload_ids
158
186
  end
159
187
 
188
+
160
189
  def remove_keys_not_in_uploads(upload_ids)
161
190
  threaded_request(upload_ids) do |upload_id|
162
191
  STDERR.puts "Removing keys not in upload #{upload_id}"
@@ -209,17 +238,30 @@ class PhraseAppUpdater
209
238
  phraseapp_request(Phrase::TagsApi,:tag_create, @project_id, params)
210
239
  end
211
240
 
212
- def wrap_phrase_errors
213
- yield
214
- rescue Phrase::ApiError => e
215
- if e.code == 401
216
- raise BadAPIKeyError.new(e)
217
- elsif e.message.match?(/not found/)
218
- raise BadProjectIDError.new(e, @project_id)
219
- elsif e.message.match?(/has already been taken/)
220
- raise ProjectNameTakenError.new(e)
221
- else
222
- raise
241
+ def wrap_phrase_errors(retries: 10)
242
+ begin
243
+ yield
244
+ rescue Phrase::ApiError => e
245
+ if e.code == 401
246
+ raise BadAPIKeyError.new(e)
247
+ elsif e.code == 429
248
+ # If we bail mid-sync, it can be expensive to recover from a partial
249
+ # merge. Instead, aggressively try to retry.
250
+ if retries >= 0
251
+ retries -= 1
252
+ STDERR.puts('Rate limited, retrying in 5...')
253
+ sleep(5)
254
+ retry
255
+ end
256
+
257
+ raise RateLimitError.new(e)
258
+ elsif e.message.match?(/not found/)
259
+ raise BadProjectIDError.new(e, @project_id)
260
+ elsif e.message.match?(/has already been taken/)
261
+ raise ProjectNameTakenError.new(e)
262
+ else
263
+ raise
264
+ end
223
265
  end
224
266
  end
225
267
 
@@ -324,5 +366,11 @@ class PhraseAppUpdater
324
366
  super(original_error.message)
325
367
  end
326
368
  end
369
+
370
+ class RateLimitError < RuntimeError
371
+ def initialize(original_error)
372
+ super(original_error.message)
373
+ end
374
+ end
327
375
  end
328
376
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class PhraseAppUpdater
4
- VERSION = '3.3.0'
4
+ VERSION = '3.3.2'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phraseapp_updater
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 3.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - iKnow Team
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-24 00:00:00.000000000 Z
11
+ date: 2025-04-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '1.23'
97
+ - !ruby/object:Gem::Dependency
98
+ name: concurrent-ruby
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 1.0.2
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 1.0.2
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: bundler
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -178,7 +192,7 @@ homepage: https://github.com/iknow/phraseapp_updater
178
192
  licenses:
179
193
  - MIT
180
194
  metadata: {}
181
- post_install_message:
195
+ post_install_message:
182
196
  rdoc_options: []
183
197
  require_paths:
184
198
  - lib
@@ -194,7 +208,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
194
208
  version: '0'
195
209
  requirements: []
196
210
  rubygems_version: 3.3.27
197
- signing_key:
211
+ signing_key:
198
212
  specification_version: 4
199
213
  summary: A three-way differ for PhraseApp projects.
200
214
  test_files: []