fastlane-plugin-translate_gpt_release_notes 0.3.2 → 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 +37 -0
- 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 +17 -19
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/deepl_provider.rb +22 -12
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/gemini_provider.rb +1 -1
- 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).
|
|
@@ -287,6 +289,7 @@ This keeps the glossary concise and avoids flooding the AI provider with irrelev
|
|
|
287
289
|
| `context` | Context for translation to improve accuracy | `GPT_CONTEXT` | - |
|
|
288
290
|
| `glossary` | Path to a curated JSON glossary file | `GLOSSARY_PATH` | - |
|
|
289
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` |
|
|
290
293
|
|
|
291
294
|
### Provider-Specific API Keys
|
|
292
295
|
|
|
@@ -403,6 +406,38 @@ translate_gpt_release_notes(
|
|
|
403
406
|
)
|
|
404
407
|
```
|
|
405
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
|
+
|
|
406
441
|
## Important Notes
|
|
407
442
|
|
|
408
443
|
### Android 500 Character Limit
|
|
@@ -426,6 +461,8 @@ All AI translation APIs cost money. Consider these tips:
|
|
|
426
461
|
- Google Gemini is generally the most cost-effective option
|
|
427
462
|
- DeepL offers competitive pricing for European languages
|
|
428
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
|
|
429
466
|
|
|
430
467
|
### Service Tiers (OpenAI)
|
|
431
468
|
|
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,12 @@ 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
|
-
text, source_locale, target_locale,
|
|
85
|
+
system_instruction = build_system_instruction(
|
|
86
|
+
source_locale, target_locale,
|
|
88
87
|
glossary_terms: glossary_terms,
|
|
89
88
|
platform: @params[:platform]
|
|
90
89
|
)
|
|
91
|
-
|
|
92
|
-
# Make API call using ruby-anthropic gem API
|
|
93
|
-
response = @client.complete(
|
|
94
|
-
model: @params[:model_name] || DEFAULT_MODEL,
|
|
95
|
-
max_tokens_to_sample: (@params[:max_tokens] || DEFAULT_MAX_TOKENS).to_i,
|
|
96
|
-
temperature: (@params[:temperature] || DEFAULT_TEMPERATURE).to_f,
|
|
97
|
-
prompt: "\n\nHuman: #{prompt}\n\nAssistant:"
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
# Extract text from response
|
|
90
|
+
response = @client.messages.create(build_create_params(system_instruction, text))
|
|
101
91
|
enforce_android_limit(extract_text_from_response(response))
|
|
102
92
|
rescue StandardError => e
|
|
103
93
|
UI.error "Anthropic provider error: #{e.message}"
|
|
@@ -106,14 +96,22 @@ module Fastlane
|
|
|
106
96
|
|
|
107
97
|
private
|
|
108
98
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
|
|
113
109
|
def extract_text_from_response(response)
|
|
114
|
-
|
|
110
|
+
content = response&.content
|
|
111
|
+
return nil if content.to_a.empty?
|
|
115
112
|
|
|
116
|
-
|
|
113
|
+
text = content.filter_map { |b| b.text if b.type == :text }.join.strip
|
|
114
|
+
text.empty? ? nil : text
|
|
117
115
|
end
|
|
118
116
|
end
|
|
119
117
|
end
|
|
@@ -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 = {}
|
|
@@ -129,14 +127,26 @@ module Fastlane
|
|
|
129
127
|
|
|
130
128
|
private
|
|
131
129
|
|
|
132
|
-
# Normalizes locale
|
|
133
|
-
# DeepL
|
|
134
|
-
#
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
140
150
|
end
|
|
141
151
|
end
|
|
142
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
|
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
|