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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34d44bece5c1bdb823c405512f9cd7123c61584c7839d832eeae0f3b8c730d3f
4
- data.tar.gz: '0851f02c45a3fcce10a4313fbb6a55f4334f770fc0aa78fdf4a98b0d2fc25fb8'
3
+ metadata.gz: 8b804e528e753243e44795157932c4d7fe91dd86f7393fc3c27169bd46ae064a
4
+ data.tar.gz: 46dcb053c8eca62129b0616477a4f0b7287c63dd40de91f40749f35b04758f70
5
5
  SHA512:
6
- metadata.gz: 3572a763d2202c00cb26e826f44b141e1d4042ea33214fd6182c3f2dcb6d672b0d36625c42f8ccc454a406460d49f04efeff391bde9e8c12affe73f33676c606
7
- data.tar.gz: 52a36475a11bb478ba51f8823916649272a4bd7e3fb42113a5462b2f5dd58051df34dee849460de3c40859a97e0712f8f30f47e579cf30017cfe7a3903075871
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
 
@@ -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,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
- # Build prompt using inherited method (includes instructions, glossary, and text)
86
- prompt = build_prompt(
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
- # Extracts translated text from the Anthropic API response
110
- #
111
- # @param response [Hash] The API response hash
112
- # @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
+
113
109
  def extract_text_from_response(response)
114
- return nil if response.nil?
110
+ content = response&.content
111
+ return nil if content.to_a.empty?
115
112
 
116
- response['completion']&.strip
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
- # 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 = {}
@@ -129,14 +127,26 @@ module Fastlane
129
127
 
130
128
  private
131
129
 
132
- # Normalizes locale codes for DeepL API.
133
- # DeepL uses 2-letter ISO 639-1 codes (e.g., 'EN', 'DE', 'FR').
134
- # Converts 'en-US' 'EN', 'de-DE' 'DE'.
135
- #
136
- # @param locale [String] The locale string to normalize
137
- # @return [String] The normalized 2-letter language code
138
- def normalize_locale(locale)
139
- 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
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 = 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
@@ -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.2"
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.2
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-04-01 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