fastlane-plugin-translate 0.1.0 → 0.2.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: d4b55852f8515ef6942b0bf6819379eb2c3cb81e20ef872a924470a2e9b6d886
4
- data.tar.gz: 7d1d3b603837c68bd8fd548dca30b171ff074e42218956b910f9446109a3fea2
3
+ metadata.gz: '094cd9a767279fbff36838d5bea6a4adb7e6f64381df5cf2ae934e1a37c7038e'
4
+ data.tar.gz: f0de0b5a62196fc84570c1d8cd44651b16a75193f9659ac4eca2baea203ade18
5
5
  SHA512:
6
- metadata.gz: 83b931afb04769b0d6bd57fe7b686a772e707d350239dbcef6d191633078d2cd205f5ebf2f0229a88aab838ef824dd078f3326b7489ed633bd6462af633d3e91
7
- data.tar.gz: 1be7fd87de5af70a4ea87a8c05fc8900606e3fa07327c2f0694997a0f614163be7fdcec98c170ad7e4d2bee798d53b4d396838ec57fd398c603fcf7fec9f67a1
6
+ metadata.gz: 75d924a65adfdb29515f747fb4513eed4bdc85eec3d378c299656f7aaecb0934e1ec33494714a2e4296141a0d6b2478083b02b5b1dc01fa2a2bfb9df9abfacfa
7
+ data.tar.gz: 0cca73dcf50da91a43bb3ff19e838ff0da44331e8a44b6e32551009d824516c29e87873750bc3a46d7b0ef4dc700d54567b943f81551854fd4866977a1b9fcec
data/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  </div>
6
6
 
7
7
  [![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-translate)
8
+ [![Gem Version](https://badge.fury.io/rb/fastlane-plugin-translate.svg)](https://badge.fury.io/rb/fastlane-plugin-translate)
8
9
  [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow.svg?style=flat)](https://buymeacoffee.com/xocrgwybbs)
9
10
 
10
11
  ## About translate
@@ -13,16 +14,13 @@ Automatically translate iOS `Localizable.xcstrings` files using DeepL API. This
13
14
 
14
15
  ## Features
15
16
 
16
- - 🔍 **Auto-discovery**: Automatically finds your `Localizable.xcstrings` file
17
- - 📊 **Translation analysis**: Shows translation progress for each language
18
- - 🎯 **Smart targeting**: Only translates untranslated strings (state: "new" with empty values)
19
- - 💾 **Progress tracking**: Saves progress between runs to avoid re-translating
20
- - 🎭 **Formality support**: Automatically detects and offers formality options for supported languages
21
- - 📝 **Context extraction**: Uses comments from xcstrings as translation context
22
- - 🔄 **Batch processing**: Efficiently handles large numbers of strings
23
- - 🛡️ **Error recovery**: Comprehensive error handling with user choices
24
- - 📄 **Automatic backups**: Creates timestamped backups before translation
25
- - ✅ **Validation**: Ensures output file is valid JSON
17
+ - **Language selection with progress**: Shows translation completeness for each language
18
+ - **Smart targeting**: Only translates missing strings, preserves existing translations
19
+ - **Formality options**: Formal/informal translation styles for supported languages
20
+ - **Context-aware translation**: Uses xcstrings comments to improve translation quality
21
+ - **Progress tracking**: Resume interrupted translations without starting over
22
+ - **Automatic backups**: Safe translation with rollback capability
23
+ - **Error recovery**: Handle API failures gracefully with retry options
26
24
 
27
25
  ## Getting Started
28
26
 
@@ -59,9 +57,29 @@ bundle install
59
57
 
60
58
  ### Basic Usage
61
59
 
60
+ After installation, create a lane in your Fastfile and run it:
61
+
62
+ ```bash
63
+ fastlane ios translate
64
+ ```
65
+
66
+ **Simple dedicated lane:**
67
+
62
68
  ```ruby
63
- # Automatically detect xcstrings file and show language selection
64
- translate_with_deepl
69
+ lane :translate do
70
+ translate_with_deepl
71
+ end
72
+ ```
73
+
74
+ **Within complex workflows:**
75
+
76
+ ```ruby
77
+ lane :prepare_release do
78
+ build_app
79
+ translate_with_deepl(target_language: "de")
80
+ upload_to_testflight
81
+ slack(message: "New build with German translations available!")
82
+ end
65
83
  ```
66
84
 
67
85
  ### Specify Target Language
@@ -17,6 +17,7 @@ module Fastlane
17
17
  # Setup and validation
18
18
  setup_deepl_client(params)
19
19
  xcstrings_path = find_xcstrings_file(params[:xcstrings_path])
20
+
20
21
  backup_file = create_backup(xcstrings_path)
21
22
 
22
23
  # Parse xcstrings file
@@ -144,12 +145,36 @@ module Fastlane
144
145
  # Sort by most untranslated first (prioritize languages that need work)
145
146
  language_options.sort_by! { |opt| -opt[:stats][:untranslated] }
146
147
 
148
+ # Show all languages with their translation status
147
149
  UI.message('📋 Available languages for translation:')
148
- selected_display = UI.select('Choose target language:', language_options.map { |opt| opt[:display] })
149
150
 
150
- # Find the corresponding language code
151
- selected_option = language_options.find { |opt| opt[:display] == selected_display }
152
- selected_option[:code]
151
+ # Display numbered list
152
+ language_options.each_with_index do |option, index|
153
+ UI.message(" #{index + 1}. #{option[:display]}")
154
+ end
155
+
156
+ # Force interactive mode and ensure we wait for user input
157
+ $stdout.flush
158
+ $stderr.flush
159
+
160
+ # Use a loop to ensure we get valid input
161
+ loop do
162
+ choice = UI.input("Choose target language (1-#{language_options.count}): ").strip
163
+
164
+ # Validate numeric input
165
+ if choice.match?(/^\d+$/)
166
+ choice_num = choice.to_i
167
+ if choice_num >= 1 && choice_num <= language_options.count
168
+ selected_option = language_options[choice_num - 1]
169
+ UI.message("✅ Selected: #{selected_option[:display]}")
170
+ return selected_option[:code]
171
+ end
172
+ end
173
+
174
+ UI.error("❌ Invalid selection '#{choice}'. Please enter a number between 1 and #{language_options.count}.")
175
+ rescue Interrupt
176
+ UI.user_error!('👋 Translation cancelled by user')
177
+ end
153
178
  end
154
179
 
155
180
  def self.calculate_translation_stats(xcstrings_data, languages)
@@ -161,8 +186,8 @@ module Fastlane
161
186
  skipped_dont_translate = 0
162
187
 
163
188
  xcstrings_data['strings'].each do |string_key, string_data|
189
+ # Skip empty string keys as they're usually not real translatable content
164
190
  next if string_key.empty?
165
- next unless string_data.dig('localizations', lang_code)
166
191
 
167
192
  # Skip strings marked as "Don't translate" from statistics
168
193
  if string_data['shouldTranslate'] == false
@@ -170,11 +195,27 @@ module Fastlane
170
195
  next
171
196
  end
172
197
 
173
- total += 1
198
+ # Check if this string has any localizations at all
199
+ if !string_data['localizations'] || string_data['localizations'].empty?
200
+ # String has no localizations - count as untranslated for this language
201
+ total += 1
202
+ next
203
+ end
204
+
205
+ # Check if this string has a localization for the target language
174
206
  localization = string_data.dig('localizations', lang_code, 'stringUnit')
207
+ unless localization
208
+ # String exists but has no localization for this specific language - count as untranslated
209
+ total += 1
210
+ next
211
+ end
175
212
 
176
- if localization && localization['state'] == 'translated' &&
177
- localization['value'] && !localization['value'].empty?
213
+ total += 1
214
+
215
+ # Count as translated ONLY if state is 'translated' AND has non-empty value
216
+ # Strings with state 'new', 'needs_review', or empty values are untranslated
217
+ if localization['state'] == 'translated' &&
218
+ localization['value'] && !localization['value'].strip.empty?
178
219
  translated += 1
179
220
  end
180
221
  end
@@ -195,16 +236,41 @@ module Fastlane
195
236
  return nil unless Helper::DeeplLanguageMapperHelper.supports_formality?(target_language)
196
237
 
197
238
  lang_name = Helper::LanguageRegistryHelper.language_name(target_language)
198
- choice = UI.select(
199
- "🎭 #{lang_name} supports formality options. Choose style:",
200
- ['default', 'more (formal)', 'less (informal)', 'prefer_more (formal if possible)', 'prefer_less (informal if possible)']
201
- )
239
+ options = [
240
+ { display: 'default (no formality preference)', value: nil },
241
+ { display: 'more (formal)', value: 'more' },
242
+ { display: 'less (informal)', value: 'less' },
243
+ { display: 'prefer_more (formal if possible)', value: 'prefer_more' },
244
+ { display: 'prefer_less (informal if possible)', value: 'prefer_less' }
245
+ ]
202
246
 
203
- case choice
204
- when 'more (formal)' then 'more'
205
- when 'less (informal)' then 'less'
206
- when 'prefer_more (formal if possible)' then 'prefer_more'
207
- when 'prefer_less (informal if possible)' then 'prefer_less'
247
+ # Display numbered list
248
+ UI.message("🎭 #{lang_name} supports formality options. Choose style:")
249
+ options.each_with_index do |option, index|
250
+ UI.message(" #{index + 1}. #{option[:display]}")
251
+ end
252
+
253
+ # Force interactive mode and ensure we wait for user input
254
+ $stdout.flush
255
+ $stderr.flush
256
+
257
+ # Use a loop to ensure we get valid input
258
+ loop do
259
+ choice = UI.input("Choose formality style (1-#{options.count}): ").strip
260
+
261
+ # Validate numeric input
262
+ if choice.match?(/^\d+$/)
263
+ choice_num = choice.to_i
264
+ if choice_num >= 1 && choice_num <= options.count
265
+ selected_option = options[choice_num - 1]
266
+ UI.message("✅ Selected: #{selected_option[:display]}")
267
+ return selected_option[:value]
268
+ end
269
+ end
270
+
271
+ UI.error("❌ Invalid selection '#{choice}'. Please enter a number between 1 and #{options.count}.")
272
+ rescue Interrupt
273
+ UI.user_error!('👋 Translation cancelled by user')
208
274
  end
209
275
  end
210
276
 
@@ -224,8 +290,34 @@ module Fastlane
224
290
  if progress.has_progress?
225
291
  summary = progress.progress_summary
226
292
  UI.message("📈 Found existing progress: #{summary[:translated_count]} strings translated")
227
- choice = UI.select('Continue from where you left off?', ['Yes, continue', 'No, start fresh'])
228
- progress.cleanup if choice == 'No, start fresh'
293
+
294
+ # Display numbered options
295
+ UI.message('Continue from where you left off?')
296
+ UI.message(' 1. Yes, continue')
297
+ UI.message(' 2. No, start fresh')
298
+
299
+ # Force interactive mode and ensure we wait for user input
300
+ $stdout.flush
301
+ $stderr.flush
302
+
303
+ # Use a loop to ensure we get valid input
304
+ loop do
305
+ choice = UI.input('Choose option (1-2): ').strip
306
+
307
+ case choice
308
+ when '1'
309
+ UI.message('✅ Continuing from existing progress')
310
+ break
311
+ when '2'
312
+ UI.message('✅ Starting fresh')
313
+ progress.cleanup
314
+ break
315
+ else
316
+ UI.error("❌ Invalid selection '#{choice}'. Please enter 1 or 2.")
317
+ end
318
+ rescue Interrupt
319
+ UI.user_error!('👋 Translation cancelled by user')
320
+ end
229
321
  end
230
322
 
231
323
  # Extract untranslated strings
@@ -270,21 +362,45 @@ module Fastlane
270
362
  next
271
363
  end
272
364
 
365
+ # Skip if already translated in progress
366
+ next if already_translated[string_key]
367
+
368
+ # Check if this string has any localizations at all
369
+ if !string_data['localizations'] || string_data['localizations'].empty?
370
+ # String has no localizations - it's completely new and needs translation
371
+ # Use the string key itself as the source text since there's no source localization
372
+ untranslated[string_key] = {
373
+ 'source_text' => string_key,
374
+ 'context' => extract_string_context(string_key, string_data)
375
+ }
376
+ next
377
+ end
378
+
379
+ # Check if target language has a localization
273
380
  localization = string_data.dig('localizations', target_language, 'stringUnit')
274
- next unless localization
381
+ unless localization
382
+ # String exists but has no localization for target language
383
+ # Get source text from source language or use string key as fallback
384
+ source_text = string_data.dig('localizations', source_language, 'stringUnit', 'value')
385
+ source_text = string_key if source_text.nil? || source_text.strip.empty?
386
+
387
+ untranslated[string_key] = {
388
+ 'source_text' => source_text,
389
+ 'context' => extract_string_context(string_key, string_data)
390
+ }
391
+ next
392
+ end
275
393
 
276
394
  # Check if NOT fully translated (inverse of the translation stats logic)
277
395
  # A string is considered translated only if: state == 'translated' AND has non-empty value
278
396
  is_fully_translated = localization['state'] == 'translated' &&
279
- localization['value'] && !localization['value'].empty?
397
+ localization['value'] && !localization['value'].strip.empty?
280
398
  next if is_fully_translated
281
399
 
282
- # Skip if already translated in progress
283
- next if already_translated[string_key]
284
-
285
400
  # Get source text from source language
286
401
  source_text = string_data.dig('localizations', source_language, 'stringUnit', 'value')
287
- next if source_text.nil? || source_text.empty?
402
+ # Use string key as fallback if no source text available
403
+ source_text = string_key if source_text.nil? || source_text.strip.empty?
288
404
 
289
405
  context = extract_string_context(string_key, string_data)
290
406
  untranslated[string_key] = {
@@ -312,45 +428,19 @@ module Fastlane
312
428
  batches.each_with_index do |batch, index|
313
429
  UI.message("🔄 Translating batch #{index + 1}/#{batches.count} (#{batch.count} strings)...")
314
430
 
315
- # Prepare batch for DeepL API
316
- texts_to_translate = batch.map { |_, data| data['source_text'] }
317
-
318
431
  retry_count = 0
319
432
  max_retries = 3
320
433
 
321
434
  begin
322
- # Build translation options (exclude source_lang and target_lang)
323
- translation_options = {}
324
- translation_options[:formality] = formality if formality
325
-
326
- # Get context from first item if available (DeepL applies to all)
327
- first_context = batch.first&.last&.dig('context')
328
- translation_options[:context] = first_context if first_context
329
-
330
- # Call DeepL with positional arguments for source_lang and target_lang
331
- translations = DeepL.translate(texts_to_translate, source_lang, target_lang, translation_options)
332
-
333
- # Save translations to progress (only save non-empty translations)
334
- translated_batch = {}
335
- skipped_empty = 0
336
-
337
- batch.each_with_index do |(string_key, _), text_index|
338
- translated_text = translations.is_a?(Array) ? translations[text_index].text : translations.text
339
-
340
- # Only save non-empty translations
341
- if translated_text && !translated_text.strip.empty?
342
- translated_batch[string_key] = translated_text
343
- else
344
- skipped_empty += 1
345
- UI.important("⚠️ Skipping empty translation for: \"#{string_key}\"")
346
- end
347
- end
435
+ # Process the batch using the helper
436
+ result = Helper::BatchTranslationProcessor.process_batch(
437
+ batch, source_lang, target_lang, formality, progress
438
+ )
348
439
 
349
- progress.save_translated_strings(translated_batch)
350
- total_translated += translated_batch.size
440
+ total_translated += result[:translated_count]
351
441
 
352
- success_msg = "✅ Batch #{index + 1} completed (#{translated_batch.size} strings translated"
353
- success_msg += ", #{skipped_empty} empty translations skipped" if skipped_empty.positive?
442
+ success_msg = "✅ Batch #{index + 1} completed (#{result[:translated_count]} strings translated"
443
+ success_msg += ", #{result[:skipped_count]} empty translations skipped" if result[:skipped_count].positive?
354
444
  success_msg += ')'
355
445
  UI.success(success_msg)
356
446
  rescue DeepL::Exceptions::AuthorizationFailed
@@ -358,19 +448,25 @@ module Fastlane
358
448
  rescue DeepL::Exceptions::QuotaExceeded
359
449
  UI.user_error!('❌ DeepL quota exceeded. Upgrade your plan or wait for reset.')
360
450
  rescue DeepL::Exceptions::LimitExceeded
361
- action = handle_rate_limit_error(batch, index, batches.count)
362
- if action == :retry && retry_count < max_retries
451
+ action = Helper::TranslationErrorHandler.handle_rate_limit_error(batch, index, batches.count)
452
+ result = Helper::TranslationErrorHandler.handle_batch_result(action, retry_count, max_retries)
453
+
454
+ case result
455
+ when :retry
363
456
  retry_count += 1
364
457
  retry
365
- elsif action == :skip
458
+ when :skip
366
459
  next
367
460
  end
368
461
  rescue StandardError => e
369
- action = handle_translation_error(e, batch, index, batches.count)
370
- if action == :retry && retry_count < max_retries
462
+ action = Helper::TranslationErrorHandler.handle_translation_error(e, batch, index, batches.count)
463
+ result = Helper::TranslationErrorHandler.handle_batch_result(action, retry_count, max_retries)
464
+
465
+ case result
466
+ when :retry
371
467
  retry_count += 1
372
468
  retry
373
- elsif action == :skip
469
+ when :skip
374
470
  next
375
471
  end
376
472
  end
@@ -379,63 +475,46 @@ module Fastlane
379
475
  total_translated
380
476
  end
381
477
 
382
- def self.handle_rate_limit_error(_batch, index, total_batches)
383
- choice = UI.select("⚠️ Rate limit exceeded for batch #{index + 1}/#{total_batches}",
384
- ['Wait 60s and retry', 'Skip this batch', 'Abort translation'])
385
- case choice
386
- when 'Wait 60s and retry'
387
- UI.message('⏳ Waiting 60 seconds...')
388
- sleep(60)
389
- :retry
390
- when 'Skip this batch'
391
- UI.important("⏭️ Skipping batch #{index + 1}")
392
- :skip
393
- when 'Abort translation'
394
- UI.user_error!('❌ Translation aborted by user')
395
- end
396
- end
397
-
398
- def self.handle_translation_error(error, _batch, index, total_batches)
399
- UI.error("❌ Translation error for batch #{index + 1}/#{total_batches}: #{error.message}")
400
- choice = UI.select('Choose action:', ['Skip this batch', 'Retry batch', 'Abort translation'])
401
-
402
- case choice
403
- when 'Skip this batch'
404
- UI.important("⏭️ Skipping batch #{index + 1}")
405
- :skip
406
- when 'Retry batch'
407
- UI.message("🔄 Retrying batch #{index + 1}")
408
- :retry
409
- when 'Abort translation'
410
- UI.user_error!('❌ Translation aborted by user')
411
- end
412
- end
413
-
414
478
  def self.update_xcstrings_file(xcstrings_path, xcstrings_data, target_language, translated_strings)
415
479
  UI.message("📝 Updating xcstrings file with #{translated_strings.size} translations...")
416
480
 
417
481
  # Update the JSON structure
418
482
  actually_updated = 0
483
+ empty_translations = 0
419
484
  translated_strings.each do |string_key, translated_text|
420
- localization = xcstrings_data.dig('strings', string_key, 'localizations', target_language, 'stringUnit')
421
- next unless localization
485
+ # Ensure the string exists in the xcstrings structure
486
+ xcstrings_data['strings'][string_key] ||= {}
487
+ string_data = xcstrings_data['strings'][string_key]
488
+
489
+ # Ensure localizations structure exists
490
+ string_data['localizations'] ||= {}
491
+
492
+ # Ensure target language localization exists
493
+ string_data['localizations'][target_language] ||= {}
494
+
495
+ # Ensure stringUnit exists
496
+ string_data['localizations'][target_language]['stringUnit'] ||= {}
497
+
498
+ localization = string_data['localizations'][target_language]['stringUnit']
422
499
 
423
500
  # Double-check: only mark as translated if we have actual content
424
501
  if translated_text && !translated_text.strip.empty?
425
502
  localization['value'] = translated_text
426
503
  localization['state'] = 'translated'
427
504
  actually_updated += 1
505
+ UI.message("✅ Updated \"#{string_key}\" -> \"#{translated_text}\"")
428
506
  else
429
507
  # Keep as 'new' if translation is empty
430
508
  localization['value'] = translated_text || ''
431
509
  localization['state'] = 'new'
432
- UI.important("⚠️ Keeping empty translation as 'new' state: \"#{string_key}\"")
510
+ empty_translations += 1
511
+ UI.important("⚠️ Empty translation for \"#{string_key}\" (received: \"#{translated_text || 'nil'}\")")
433
512
  end
434
513
  end
435
514
 
436
515
  # Write updated JSON back to file
437
516
  File.write(xcstrings_path, JSON.pretty_generate(xcstrings_data))
438
- UI.success("💾 Updated xcstrings file (#{actually_updated} marked as translated)")
517
+ UI.success("💾 Updated xcstrings file (#{actually_updated} marked as translated, #{empty_translations} empty)")
439
518
  end
440
519
 
441
520
  def self.validate_json_file(xcstrings_path)
@@ -492,7 +571,7 @@ module Fastlane
492
571
  FastlaneCore::ConfigItem.new(
493
572
  key: :free_api,
494
573
  description: 'Use DeepL Free API endpoint instead of Pro',
495
- type: Boolean,
574
+ is_string: false,
496
575
  default_value: false
497
576
  ),
498
577
  FastlaneCore::ConfigItem.new(
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'deepl'
4
+ require 'fastlane_core/ui/ui'
5
+
6
+ module Fastlane
7
+ UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
8
+
9
+ module Helper
10
+ class BatchTranslationProcessor
11
+ def self.process_batch(batch, source_lang, target_lang, formality, progress)
12
+ # Prepare batch for DeepL API
13
+ texts_to_translate = batch.map { |_, data| data['source_text'] }
14
+
15
+ # Build translation options
16
+ translation_options = build_translation_options(formality, batch)
17
+
18
+ # Call DeepL API
19
+ translations = DeepL.translate(texts_to_translate, source_lang, target_lang, translation_options)
20
+
21
+ # Process and save translations
22
+ save_batch_translations(batch, translations, progress)
23
+ end
24
+
25
+ def self.build_translation_options(formality, batch)
26
+ translation_options = {}
27
+ translation_options[:formality] = formality if formality
28
+
29
+ # Get context from first item if available (DeepL applies to all)
30
+ first_context = batch.first&.last&.dig('context')
31
+ translation_options[:context] = first_context if first_context
32
+
33
+ translation_options
34
+ end
35
+
36
+ def self.save_batch_translations(batch, translations, progress)
37
+ translated_batch = {}
38
+ skipped_empty = 0
39
+
40
+ batch.each_with_index do |(string_key, _), text_index|
41
+ translated_text = translations.is_a?(Array) ? translations[text_index].text : translations.text
42
+
43
+ # Save all translations, including empty ones - let the update logic handle them
44
+ translated_batch[string_key] = translated_text || ''
45
+
46
+ # Count empty translations for reporting
47
+ if !translated_text || translated_text.strip.empty?
48
+ skipped_empty += 1
49
+ UI.important("⚠️ DeepL returned empty translation for: \"#{string_key}\"")
50
+ end
51
+ end
52
+
53
+ progress.save_translated_strings(translated_batch)
54
+
55
+ {
56
+ translated_count: translated_batch.size - skipped_empty, # Only count non-empty as "translated"
57
+ skipped_count: skipped_empty
58
+ }
59
+ end
60
+
61
+ private_class_method :build_translation_options, :save_batch_translations
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastlaneCore
4
+ class UI
5
+ unless respond_to?(:select)
6
+ def self.select(message, options)
7
+ # Fallback implementation for testing
8
+ puts "#{message} (#{options.join(', ')})"
9
+ options.first
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ module Fastlane
16
+ module Helper
17
+ class TranslationErrorHandler
18
+ def self.handle_rate_limit_error(_batch, batch_index, total_batches)
19
+ options = ['Wait 60s and retry', 'Skip this batch', 'Abort translation', '❌ Quit']
20
+ choice = UI.select("⚠️ Rate limit exceeded for batch #{batch_index + 1}/#{total_batches}",
21
+ options)
22
+
23
+ case choice
24
+ when 'Wait 60s and retry'
25
+ UI.message('⏳ Waiting 60 seconds...')
26
+ sleep(60)
27
+ :retry
28
+ when 'Skip this batch'
29
+ UI.important("⏭️ Skipping batch #{batch_index + 1}")
30
+ :skip
31
+ when 'Abort translation'
32
+ UI.user_error!('❌ Translation aborted by user')
33
+ when '❌ Quit'
34
+ :quit
35
+ end
36
+ end
37
+
38
+ def self.handle_translation_error(error, _batch, batch_index, total_batches)
39
+ UI.error("❌ Translation error for batch #{batch_index + 1}/#{total_batches}: #{error.message}")
40
+ options = ['Skip this batch', 'Retry batch', 'Abort translation', '❌ Quit']
41
+ choice = UI.select('Choose action:', options)
42
+
43
+ case choice
44
+ when 'Skip this batch'
45
+ UI.important("⏭️ Skipping batch #{batch_index + 1}")
46
+ :skip
47
+ when 'Retry batch'
48
+ UI.message("🔄 Retrying batch #{batch_index + 1}")
49
+ :retry
50
+ when 'Abort translation'
51
+ UI.user_error!('❌ Translation aborted by user')
52
+ when '❌ Quit'
53
+ :quit
54
+ end
55
+ end
56
+
57
+ def self.handle_batch_result(action, retry_count, max_retries)
58
+ case action
59
+ when :retry
60
+ return :retry if retry_count < max_retries
61
+
62
+ UI.error("❌ Max retries (#{max_retries}) exceeded")
63
+ :skip
64
+ when :skip
65
+ :skip
66
+ when :quit
67
+ :quit
68
+ else
69
+ :continue
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Fastlane
4
4
  module Translate
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-translate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tijs Teulings
@@ -175,8 +175,10 @@ files:
175
175
  - README.md
176
176
  - lib/fastlane/plugin/translate.rb
177
177
  - lib/fastlane/plugin/translate/actions/translate_with_deepl.rb
178
+ - lib/fastlane/plugin/translate/helper/batch_translation_processor.rb
178
179
  - lib/fastlane/plugin/translate/helper/deepl_language_mapper_helper.rb
179
180
  - lib/fastlane/plugin/translate/helper/language_registry_helper.rb
181
+ - lib/fastlane/plugin/translate/helper/translation_error_handler.rb
180
182
  - lib/fastlane/plugin/translate/helper/translation_progress_helper.rb
181
183
  - lib/fastlane/plugin/translate/version.rb
182
184
  homepage: https://github.com/tijs/fastlane-plugin-translate