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 +4 -4
- data/README.md +45 -7
- data/lib/fastlane/plugin/translate_gpt_release_notes/actions/translate_gpt_release_notes_action.rb +57 -7
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/anthropic_provider.rb +20 -21
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/base_provider.rb +28 -12
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/deepl_provider.rb +23 -21
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/gemini_provider.rb +7 -6
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/openai_provider.rb +6 -7
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/translate_gpt_release_notes_helper.rb +0 -45
- data/lib/fastlane/plugin/translate_gpt_release_notes/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b804e528e753243e44795157932c4d7fe91dd86f7393fc3c27169bd46ae064a
|
|
4
|
+
data.tar.gz: 46dcb053c8eca62129b0616477a4f0b7287c63dd40de91f40749f35b04758f70
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
445
|
+
Android has a hard limit of 500 characters for changelogs. The plugin enforces this in two layers:
|
|
410
446
|
|
|
411
|
-
1. **
|
|
412
|
-
2. **
|
|
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.
|
|
470
|
-
3. For AI providers, the prompt
|
|
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
|
|
data/lib/fastlane/plugin/translate_gpt_release_notes/actions/translate_gpt_release_notes_action.rb
CHANGED
|
@@ -57,10 +57,22 @@ module Fastlane
|
|
|
57
57
|
translations[locale] = helper.translate_text(master_texts, locale, params[:platform])
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
110
|
+
content = response&.content
|
|
111
|
+
return nil if content.to_a.empty?
|
|
114
112
|
|
|
115
|
-
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
161
|
-
"
|
|
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
|
-
#
|
|
165
|
-
#
|
|
177
|
+
# Truncates the translated text to the Android character limit if exceeded.
|
|
178
|
+
# Logs a warning when truncation occurs.
|
|
166
179
|
#
|
|
167
|
-
# @param
|
|
168
|
-
# @return [String] The
|
|
169
|
-
def
|
|
170
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
|
141
|
-
# DeepL
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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 =
|
|
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(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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(
|
|
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}"
|
data/lib/fastlane/plugin/translate_gpt_release_notes/helper/translate_gpt_release_notes_helper.rb
CHANGED
|
@@ -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
|
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.
|
|
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-
|
|
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: '
|
|
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: '
|
|
26
|
+
version: '8.0'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: loco_strings
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|