fastlane-plugin-translate_gpt_release_notes 0.3.1 → 0.4.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: 583516edebe47c16ac07d08fad7c985ba813ee714465248654dd5dd029bd1d5b
4
- data.tar.gz: 55366af37fecbdf65d587dc31464c5a9b0be8779a819261214f99c7d5affb286
3
+ metadata.gz: 8b804e528e753243e44795157932c4d7fe91dd86f7393fc3c27169bd46ae064a
4
+ data.tar.gz: 46dcb053c8eca62129b0616477a4f0b7287c63dd40de91f40749f35b04758f70
5
5
  SHA512:
6
- metadata.gz: c2db228382b529fa334385d97c39e1daecbd7280894f2aa0b9b870d2a8a1027c15eee79c6253b7c2d07cd0bc228ff684d77a892eaa954842fcc8e03befef0f8e
7
- data.tar.gz: 25021f03188ebbb7551ed18c5da99cce25568ba46e6c92f1c1160207997414ded93b4cbed993f10b30f7eebc1004ced57ae36fbaca6769acfe39638ad10bcb31
6
+ metadata.gz: 3942636066fa49fb840cb6855742b3605e71894b6ce40b235bb25c96d49282da9a7bde5ca458f32b13bb9ac015f74a05efd2995ef7c73ed6119a43aa75a7eab9
7
+ data.tar.gz: f37538e795cfb574c2c7a5a3a025dc4ef9d78b228e27cce6636ccb9ef7fd2fd4984c4304424a0af2f1b15f266e1df0307e283b6fe39f8cb7662d41a9c20ba475
data/README.md CHANGED
@@ -171,6 +171,8 @@ translate_gpt_release_notes(
171
171
 
172
172
  **Note**: DeepL automatically detects free vs paid API keys (free keys end with `:fx`) and uses the appropriate endpoint.
173
173
 
174
+ **Note**: DeepL requires region-qualified targets for English (`EN-US` or `EN-GB`) and Portuguese (`PT-PT` or `PT-BR`). The plugin handles this automatically based on the locale passed — `en` and `en-US` both map to `EN-US`, `en-GB` maps to `EN-GB`, and similarly for Portuguese.
175
+
174
176
  ## Glossary Support (Experimental)
175
177
 
176
178
  > **Note**: Glossary support is an experimental feature. If you encounter any issues, please [open an issue on GitHub](https://github.com/antonkarliner/fastlane-plugin-translate_gpt_release_notes/issues).
@@ -182,7 +184,8 @@ The glossary feature ensures consistent translation of app-specific terms (scree
182
184
  1. The plugin loads glossary terms from a JSON file and/or a directory of localization files
183
185
  2. Before each translation, it fuzzy-matches terms from the glossary against the source text
184
186
  3. Only relevant terms are included in the translation prompt (keeping it concise)
185
- 4. Each provider uses the glossary differently:
187
+ 4. The AI is instructed to use glossary terms as **reference translations** and apply appropriate grammatical forms (declension, conjugation, agreement) as needed — so translations sound natural in inflected languages like Russian, German, or Finnish rather than copying terms verbatim
188
+ 5. Each provider uses the glossary differently:
186
189
  - **OpenAI**: Glossary terms are sent as a system message for strong instruction following
187
190
  - **Anthropic / Gemini**: Glossary terms are included in the translation prompt
188
191
  - **DeepL**: Glossary terms are passed via the `context` parameter
@@ -286,6 +289,7 @@ This keeps the glossary concise and avoids flooding the AI provider with irrelev
286
289
  | `context` | Context for translation to improve accuracy | `GPT_CONTEXT` | - |
287
290
  | `glossary` | Path to a curated JSON glossary file | `GLOSSARY_PATH` | - |
288
291
  | `glossary_dir` | Path to localization files directory for auto-extracting glossary | `GLOSSARY_DIR` | - |
292
+ | `dry_run` | Preview translations and character counts without writing files | `TRANSLATE_DRY_RUN` | `false` |
289
293
 
290
294
  ### Provider-Specific API Keys
291
295
 
@@ -402,14 +406,46 @@ translate_gpt_release_notes(
402
406
  )
403
407
  ```
404
408
 
409
+ ## Dry Run
410
+
411
+ Use `dry_run: true` to call the provider and preview what would be written — without touching any file on disk:
412
+
413
+ ```ruby
414
+ lane :preview_translations do
415
+ translate_gpt_release_notes(
416
+ master_locale: 'en-US',
417
+ platform: 'ios',
418
+ dry_run: true
419
+ )
420
+ end
421
+ ```
422
+
423
+ Output looks like:
424
+
425
+ ```
426
+ DRY RUN: no files will be written.
427
+ fr-FR: 142 chars
428
+ de-DE: 158 chars
429
+ ja-JP: 89 chars
430
+ es-ES: translation FAILED (would be skipped)
431
+ ```
432
+
433
+ For Android, lines exceeding the 500-character limit are flagged:
434
+
435
+ ```
436
+ de-DE: 523 chars — EXCEEDS 500-char Android limit
437
+ ```
438
+
439
+ No files are written and `last_successful_run.txt` is not updated, so the next real run will still translate.
440
+
405
441
  ## Important Notes
406
442
 
407
443
  ### Android 500 Character Limit
408
444
 
409
- Android has a limit of 500 characters for changelogs. The plugin handles this in two ways:
445
+ Android has a hard limit of 500 characters for changelogs. The plugin enforces this in two layers:
410
446
 
411
- 1. **AI Providers (OpenAI, Anthropic, Gemini)**: The character limit is included in the translation prompt, asking the AI to stay within the limit
412
- 2. **DeepL**: Translations are truncated to 500 characters with a warning if they exceed the limit
447
+ 1. **Prompt constraint**: For AI providers, the limit is included near the top of the prompt (alongside core instructions) with explicit wording that the model must count carefully and shorten or summarize if needed. This positions it as a hard constraint rather than an afterthought.
448
+ 2. **Safety-net truncation**: All providers (including DeepL) truncate the result and log a warning if the model still exceeds 500 characters despite the prompt.
413
449
 
414
450
  If you frequently hit the limit, consider shortening your master locale changelog.
415
451
 
@@ -425,6 +461,8 @@ All AI translation APIs cost money. Consider these tips:
425
461
  - Google Gemini is generally the most cost-effective option
426
462
  - DeepL offers competitive pricing for European languages
427
463
  - The plugin skips translation if the source file hasn't changed (tracked via `last_successful_run.txt`)
464
+ - If all translations fail, `last_successful_run.txt` is not updated — the next run will retry rather than skip
465
+ - Locales where translation fails are left with their existing file content untouched
428
466
 
429
467
  ### Service Tiers (OpenAI)
430
468
 
@@ -465,9 +503,9 @@ export OPENAI_API_KEY='your-key-here'
465
503
  **Cause**: The translated text is longer than 500 characters.
466
504
 
467
505
  **Solutions**:
468
- 1. Shorten your source changelog
469
- 2. For DeepL, translations are automatically truncated
470
- 3. For AI providers, the prompt includes the limit but compliance isn't guaranteed
506
+ 1. Shorten your source changelog — the most reliable fix
507
+ 2. All providers will truncate automatically if the limit is exceeded (with a warning), but this may cut mid-sentence
508
+ 3. For AI providers, the prompt strongly instructs the model to stay within the limit and shorten if needed
471
509
 
472
510
  ### API Timeout Errors
473
511
 
@@ -57,10 +57,22 @@ module Fastlane
57
57
  translations[locale] = helper.translate_text(master_texts, locale, params[:platform])
58
58
  end
59
59
 
60
- update_translated_texts(base_directory, translated_texts, is_ios, params)
60
+ if params[:dry_run]
61
+ print_dry_run_preview(translated_texts, params)
62
+ else
63
+ update_translated_texts(base_directory, translated_texts, is_ios, params)
61
64
 
62
- # Store the current time as the last run time
63
- File.write(last_run_file, Time.now.to_i)
65
+ # Only mark the run successful if at least one translation actually succeeded;
66
+ # otherwise a transient failure would suppress the next run's retry.
67
+ any_success = translated_texts.any? do |locale, text|
68
+ locale != params[:master_locale] && !translation_empty?(text)
69
+ end
70
+ if any_success
71
+ File.write(last_run_file, Time.now.to_i)
72
+ else
73
+ UI.error("No translations succeeded; not updating #{last_run_file} so the next run will retry.")
74
+ end
75
+ end
64
76
  end
65
77
 
66
78
  def self.list_locales(base_directory)
@@ -93,17 +105,47 @@ module Fastlane
93
105
  Dir[File.join(directory, '*.txt')].max_by { |f| File.basename(f, '.txt').to_i }.split('/').last
94
106
  end
95
107
 
108
+ def self.translation_empty?(text)
109
+ text.nil? || text.to_s.strip.empty?
110
+ end
111
+
112
+ def self.print_dry_run_preview(translated_texts, params)
113
+ android_limit = 500 # Google Play hard limit; see BaseProvider::ANDROID_CHAR_LIMIT
114
+ UI.important('DRY RUN: no files will be written.')
115
+ translated_texts.each do |locale, text|
116
+ next if locale == params[:master_locale]
117
+
118
+ print_locale_preview(locale, text, android_limit, params[:platform])
119
+ end
120
+ end
121
+
122
+ def self.print_locale_preview(locale, text, android_limit, platform)
123
+ if text.nil? || text.to_s.strip.empty?
124
+ UI.warning(" #{locale}: translation FAILED (would be skipped)")
125
+ return
126
+ end
127
+ length = text.length
128
+ over = platform == 'android' && length > android_limit
129
+ suffix = over ? " — EXCEEDS #{android_limit}-char Android limit" : ''
130
+ UI.message(" #{locale}: #{length} chars#{suffix}")
131
+ end
132
+
96
133
  def self.update_translated_texts(base_directory, translated_texts, is_ios, params)
97
134
  translated_texts.each do |locale, text|
98
135
  next if locale == params[:master_locale] # Skip master locale
99
-
136
+
137
+ if translation_empty?(text)
138
+ UI.warning("Skipping #{locale}: translation failed or returned empty; existing file left unchanged.")
139
+ next
140
+ end
141
+
100
142
  target_path = is_ios ? File.join(base_directory, locale) : File.join(base_directory, locale, 'changelogs')
101
-
143
+
102
144
  # Ensure target path exists or create it
103
145
  FileUtils.mkdir_p(target_path) unless Dir.exist?(target_path)
104
-
146
+
105
147
  filename = is_ios ? 'release_notes.txt' : highest_numbered_file(File.join(base_directory, params[:master_locale], 'changelogs'))
106
-
148
+
107
149
  # Write the translated text to the file
108
150
  File.write(File.join(target_path, filename), text)
109
151
  end
@@ -249,6 +291,14 @@ module Fastlane
249
291
  next if value.nil? || value.to_s.strip.empty?
250
292
  UI.user_error!("Glossary directory not found: #{value}") unless Dir.exist?(value)
251
293
  end
294
+ ),
295
+ FastlaneCore::ConfigItem.new(
296
+ key: :dry_run,
297
+ env_name: 'TRANSLATE_DRY_RUN',
298
+ description: 'Preview translations without writing files',
299
+ type: Boolean,
300
+ optional: true,
301
+ default_value: false
252
302
  )
253
303
  ]
254
304
  end
@@ -82,22 +82,13 @@ module Fastlane
82
82
  # @param glossary_terms [Hash] Optional glossary { source_term => target_translation }
83
83
  # @return [String, nil] Translated text or nil on error
84
84
  def translate(text, source_locale, target_locale, glossary_terms: {})
85
- # Build prompt using inherited method (includes instructions, glossary, and text)
86
- prompt = build_prompt(text, source_locale, target_locale, glossary_terms: glossary_terms)
87
-
88
- # Add Android limitations if needed (appended after the text)
89
- prompt += "\n\n" + android_limitation_instruction if @params[:platform] == 'android'
90
-
91
- # Make API call using ruby-anthropic gem API
92
- response = @client.complete(
93
- model: @params[:model_name] || DEFAULT_MODEL,
94
- max_tokens_to_sample: (@params[:max_tokens] || DEFAULT_MAX_TOKENS).to_i,
95
- temperature: (@params[:temperature] || DEFAULT_TEMPERATURE).to_f,
96
- prompt: "\n\nHuman: #{prompt}\n\nAssistant:"
85
+ system_instruction = build_system_instruction(
86
+ source_locale, target_locale,
87
+ glossary_terms: glossary_terms,
88
+ platform: @params[:platform]
97
89
  )
98
-
99
- # Extract text from response
100
- extract_text_from_response(response)
90
+ response = @client.messages.create(build_create_params(system_instruction, text))
91
+ enforce_android_limit(extract_text_from_response(response))
101
92
  rescue StandardError => e
102
93
  UI.error "Anthropic provider error: #{e.message}"
103
94
  nil
@@ -105,14 +96,22 @@ module Fastlane
105
96
 
106
97
  private
107
98
 
108
- # Extracts translated text from the Anthropic API response
109
- #
110
- # @param response [Hash] The API response hash
111
- # @return [String, nil] The translated text or nil
99
+ def build_create_params(system_instruction, text)
100
+ {
101
+ model: @params[:model_name] || DEFAULT_MODEL,
102
+ max_tokens: (@params[:max_tokens] || DEFAULT_MAX_TOKENS).to_i,
103
+ temperature: (@params[:temperature] || DEFAULT_TEMPERATURE).to_f,
104
+ system_: system_instruction,
105
+ messages: [{ role: 'user', content: text }]
106
+ }
107
+ end
108
+
112
109
  def extract_text_from_response(response)
113
- return nil if response.nil?
110
+ content = response&.content
111
+ return nil if content.to_a.empty?
114
112
 
115
- response['completion']&.strip
113
+ text = content.filter_map { |b| b.text if b.type == :text }.join.strip
114
+ text.empty? ? nil : text
116
115
  end
117
116
  end
118
117
  end
@@ -92,13 +92,19 @@ module Fastlane
92
92
  # @param target_locale [String] Target language code
93
93
  # @param glossary_terms [Hash] Optional glossary { source_term => target_translation }
94
94
  # @return [String] The formatted prompt
95
- def build_prompt(text, source_locale, target_locale, glossary_terms: {})
95
+ def build_prompt(text, source_locale, target_locale, glossary_terms: {}, platform: nil)
96
96
  prompt_parts = []
97
97
 
98
98
  # Instructions first: role, task, and output format
99
99
  prompt_parts << "Translate the following release notes from #{source_locale} to #{target_locale}."
100
100
  prompt_parts << "Respond with ONLY the translated text. Preserve the original formatting, line breaks, and bullet points."
101
101
 
102
+ # Android character limit is a hard constraint — include it with core instructions
103
+ if platform == 'android'
104
+ prompt_parts << ""
105
+ prompt_parts << android_limitation_instruction
106
+ end
107
+
102
108
  # Add context if provided
103
109
  if @params[:context]
104
110
  prompt_parts << ""
@@ -108,7 +114,7 @@ module Fastlane
108
114
  # Add glossary terms before the text so the model reads them first
109
115
  unless glossary_terms.nil? || glossary_terms.empty?
110
116
  prompt_parts << ""
111
- prompt_parts << "Use the following glossary for consistent terminology. Apply these exact translations for the specified terms:"
117
+ prompt_parts << "Use the following glossary for consistent terminology. These are reference translations — apply appropriate grammatical forms (declension, conjugation, agreement) as needed for natural-sounding language in the target language. Do not copy verbatim if grammar requires a different form:"
112
118
  glossary_terms.each do |source_term, target_term|
113
119
  prompt_parts << "- \"#{source_term}\" -> \"#{target_term}\""
114
120
  end
@@ -130,12 +136,18 @@ module Fastlane
130
136
  # @param target_locale [String] Target language code
131
137
  # @param glossary_terms [Hash] Optional glossary { source_term => target_translation }
132
138
  # @return [String] The system instruction
133
- def build_system_instruction(source_locale, target_locale, glossary_terms: {})
139
+ def build_system_instruction(source_locale, target_locale, glossary_terms: {}, platform: nil)
134
140
  parts = []
135
141
 
136
142
  parts << "Translate the following release notes from #{source_locale} to #{target_locale}."
137
143
  parts << "Respond with ONLY the translated text. Preserve the original formatting, line breaks, and bullet points."
138
144
 
145
+ # Android character limit is a hard constraint — include it with core instructions
146
+ if platform == 'android'
147
+ parts << ""
148
+ parts << android_limitation_instruction
149
+ end
150
+
139
151
  if @params[:context]
140
152
  parts << ""
141
153
  parts << "Context: #{@params[:context]}"
@@ -143,7 +155,7 @@ module Fastlane
143
155
 
144
156
  unless glossary_terms.nil? || glossary_terms.empty?
145
157
  parts << ""
146
- parts << "Use the following glossary for consistent terminology. Apply these exact translations for the specified terms:"
158
+ parts << "Use the following glossary for consistent terminology. These are reference translations — apply appropriate grammatical forms (declension, conjugation, agreement) as needed for natural-sounding language in the target language. Do not copy verbatim if grammar requires a different form:"
147
159
  glossary_terms.each do |source_term, target_term|
148
160
  parts << "- \"#{source_term}\" -> \"#{target_term}\""
149
161
  end
@@ -157,17 +169,21 @@ module Fastlane
157
169
  #
158
170
  # @return [String] The Android limitation instruction
159
171
  def android_limitation_instruction
160
- "IMPORTANT: The translated text must not exceed #{ANDROID_CHAR_LIMIT} characters " \
161
- "(Google Play Store release notes limit). Provide a concise translation."
172
+ "CRITICAL: Google Play Store enforces a hard #{ANDROID_CHAR_LIMIT}-character limit for release notes. " \
173
+ "Your translation MUST be #{ANDROID_CHAR_LIMIT} characters or fewer. " \
174
+ "Count carefully and shorten or summarize if needed to stay within this limit."
162
175
  end
163
176
 
164
- # Adds Android character limit constraint to the prompt.
165
- # Google Play has a 500 character limit for release notes.
177
+ # Truncates the translated text to the Android character limit if exceeded.
178
+ # Logs a warning when truncation occurs.
166
179
  #
167
- # @param prompt [String] The existing prompt to append to
168
- # @return [String] The prompt with limitation instruction appended
169
- def apply_android_limitations(prompt)
170
- prompt + android_limitation_instruction
180
+ # @param text [String, nil] The translated text
181
+ # @return [String, nil] The text, truncated to ANDROID_CHAR_LIMIT if necessary
182
+ def enforce_android_limit(text)
183
+ return text unless @params[:platform] == 'android' && text && text.length > ANDROID_CHAR_LIMIT
184
+
185
+ UI.warning("Translation exceeds #{ANDROID_CHAR_LIMIT} characters (#{text.length}), truncating...")
186
+ text[0...ANDROID_CHAR_LIMIT]
171
187
  end
172
188
 
173
189
  # Adds a configuration error to the errors list.
@@ -92,10 +92,8 @@ module Fastlane
92
92
  # @param glossary_terms [Hash] Optional glossary { source_term => target_translation }
93
93
  # @return [String, nil] Translated text or nil on error
94
94
  def translate(text, source_locale, target_locale, glossary_terms: {})
95
- # DeepL uses ISO 639-1 language codes (2-letter codes)
96
- # Convert locales like 'en-US' to 'EN'
97
- source_lang = normalize_locale(source_locale)
98
- target_lang = normalize_locale(target_locale)
95
+ source_lang = normalize_source_locale(source_locale)
96
+ target_lang = normalize_target_locale(target_locale)
99
97
 
100
98
  # Build options hash
101
99
  options = {}
@@ -118,15 +116,7 @@ module Fastlane
118
116
  # Make API call
119
117
  result = DeepL.translate(text, source_lang, target_lang, options)
120
118
 
121
- translated = result.text
122
-
123
- # Handle Android 500 character limit
124
- if @params[:platform] == 'android' && translated.length > 500
125
- UI.warning "DeepL translation exceeds 500 characters (#{translated.length}), truncating..."
126
- translated = translated[0...500]
127
- end
128
-
129
- translated
119
+ enforce_android_limit(result.text)
130
120
  rescue DeepL::Exceptions::RequestError => e
131
121
  UI.error "DeepL API error: #{e.message}"
132
122
  nil
@@ -137,14 +127,26 @@ module Fastlane
137
127
 
138
128
  private
139
129
 
140
- # Normalizes locale codes for DeepL API.
141
- # DeepL uses 2-letter ISO 639-1 codes (e.g., 'EN', 'DE', 'FR').
142
- # Converts 'en-US' 'EN', 'de-DE' 'DE'.
143
- #
144
- # @param locale [String] The locale string to normalize
145
- # @return [String] The normalized 2-letter language code
146
- def normalize_locale(locale)
147
- locale.to_s.split('-').first.upcase
130
+ # Normalizes a locale for use as the DeepL SOURCE language.
131
+ # DeepL source accepts only the bare 2-letter ISO 639-1 code.
132
+ # 'en-US' -> 'EN', 'de-DE' -> 'DE'.
133
+ def normalize_source_locale(locale)
134
+ locale.to_s.split(/[-_]/).first.to_s.upcase
135
+ end
136
+
137
+ # Normalizes a locale for use as the DeepL TARGET language.
138
+ # DeepL requires a region for English ('EN-US'/'EN-GB') and Portuguese
139
+ # ('PT-PT'/'PT-BR'); bare 'EN'/'PT' are deprecated targets and rejected.
140
+ # All other targets use the bare 2-letter code.
141
+ def normalize_target_locale(locale)
142
+ parts = locale.to_s.split(/[-_]/)
143
+ lang = parts[0].to_s.downcase
144
+ region = parts[1].to_s.upcase
145
+ case lang
146
+ when 'en' then region == 'GB' ? 'EN-GB' : 'EN-US'
147
+ when 'pt' then region == 'BR' ? 'PT-BR' : 'PT-PT'
148
+ else lang.upcase
149
+ end
148
150
  end
149
151
  end
150
152
  end
@@ -57,7 +57,7 @@ module Fastlane
57
57
  # @param params [Hash] Configuration parameters for the provider
58
58
  def initialize(params)
59
59
  super
60
- @api_key = params[:api_token]
60
+ @api_key = credential(:api_token)
61
61
  @model = @params[:model_name] || DEFAULT_MODEL
62
62
  @temperature = (@params[:temperature] || DEFAULT_TEMPERATURE).to_f
63
63
  @timeout = (@params[:request_timeout] || DEFAULT_TIMEOUT).to_i
@@ -80,16 +80,17 @@ module Fastlane
80
80
  # @return [String, nil] Translated text or nil on error
81
81
  def translate(text, source_locale, target_locale, glossary_terms: {})
82
82
  # Build prompt using inherited method (includes instructions, glossary, and text)
83
- prompt = build_prompt(text, source_locale, target_locale, glossary_terms: glossary_terms)
84
-
85
- # Add Android limitations if needed (appended after the text)
86
- prompt += "\n\n" + android_limitation_instruction if @params[:platform] == 'android'
83
+ prompt = build_prompt(
84
+ text, source_locale, target_locale,
85
+ glossary_terms: glossary_terms,
86
+ platform: @params[:platform]
87
+ )
87
88
 
88
89
  # Make API call
89
90
  result = make_api_request(prompt)
90
91
 
91
92
  # Extract text from response
92
- extract_text_from_response(result)
93
+ enforce_android_limit(extract_text_from_response(result))
93
94
  rescue StandardError => e
94
95
  UI.error "Gemini provider error: #{e.message}"
95
96
  nil
@@ -82,14 +82,13 @@ module Fastlane
82
82
  # @return [String, nil] Translated text or nil on error
83
83
  def translate(text, source_locale, target_locale, glossary_terms: {})
84
84
  # Build system instruction and user content separately for better results
85
- system_instruction = build_system_instruction(source_locale, target_locale, glossary_terms: glossary_terms)
85
+ system_instruction = build_system_instruction(
86
+ source_locale, target_locale,
87
+ glossary_terms: glossary_terms,
88
+ platform: @params[:platform]
89
+ )
86
90
  user_content = text
87
91
 
88
- # Add Android limitations to system instruction if needed
89
- if @params[:platform] == 'android'
90
- system_instruction += "\n\n" + android_limitation_instruction
91
- end
92
-
93
92
  # Build parameters hash with separate system and user messages
94
93
  parameters = {
95
94
  model: @params[:model_name] || DEFAULT_MODEL,
@@ -112,7 +111,7 @@ module Fastlane
112
111
  UI.error "OpenAI translation error: #{error}"
113
112
  nil
114
113
  else
115
- response.dig('choices', 0, 'message', 'content')&.strip
114
+ enforce_android_limit(response.dig('choices', 0, 'message', 'content')&.strip)
116
115
  end
117
116
  rescue StandardError => e
118
117
  UI.error "OpenAI provider error: #{e.message}"
@@ -44,51 +44,6 @@ module Fastlane
44
44
  @provider.translate(text, source_locale, target_locale, glossary_terms: glossary_terms)
45
45
  end
46
46
 
47
- # Sleep for a specified number of seconds, displaying a progress bar
48
- def wait(seconds = @params[:request_timeout])
49
- sleep_time = 0
50
- while sleep_time < seconds
51
- percent_complete = (sleep_time.to_f / seconds.to_f) * 100.0
52
- progress_bar_width = 20
53
- completed_width = (progress_bar_width * percent_complete / 100.0).round
54
- remaining_width = progress_bar_width - completed_width
55
- print "\rTimeout ["
56
- print Colorizer::code(:green)
57
- print "=" * completed_width
58
- print " " * remaining_width
59
- print Colorizer::code(:reset)
60
- print "]"
61
- print " %.2f%%" % percent_complete
62
- $stdout.flush
63
- sleep(1)
64
- sleep_time += 1
65
- end
66
- print "\r"
67
- $stdout.flush
68
- end
69
- end
70
-
71
- # Helper class for bash colors
72
- class Colorizer
73
- COLORS = {
74
- black: 30,
75
- red: 31,
76
- green: 32,
77
- yellow: 33,
78
- blue: 34,
79
- magenta: 35,
80
- cyan: 36,
81
- white: 37,
82
- reset: 0,
83
- }
84
-
85
- def self.colorize(text, color)
86
- color_code = COLORS[color.to_sym]
87
- "\e[#{color_code}m#{text}\e[0m"
88
- end
89
- def self.code(color)
90
- "\e[#{COLORS[color.to_sym]}m"
91
- end
92
47
  end
93
48
  end
94
49
  end
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module TranslateGptReleaseNotes
3
- VERSION = "0.3.1"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-translate_gpt_release_notes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton Karliner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-12 00:00:00.000000000 Z
11
+ date: 2026-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-openai
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '3.7'
19
+ version: '8.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '3.7'
26
+ version: '8.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: loco_strings
29
29
  requirement: !ruby/object:Gem::Requirement