fastlane-plugin-translate_gpt_release_notes 0.3.1 → 0.3.2

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: 34d44bece5c1bdb823c405512f9cd7123c61584c7839d832eeae0f3b8c730d3f
4
+ data.tar.gz: '0851f02c45a3fcce10a4313fbb6a55f4334f770fc0aa78fdf4a98b0d2fc25fb8'
5
5
  SHA512:
6
- metadata.gz: c2db228382b529fa334385d97c39e1daecbd7280894f2aa0b9b870d2a8a1027c15eee79c6253b7c2d07cd0bc228ff684d77a892eaa954842fcc8e03befef0f8e
7
- data.tar.gz: 25021f03188ebbb7551ed18c5da99cce25568ba46e6c92f1c1160207997414ded93b4cbed993f10b30f7eebc1004ced57ae36fbaca6769acfe39638ad10bcb31
6
+ metadata.gz: 3572a763d2202c00cb26e826f44b141e1d4042ea33214fd6182c3f2dcb6d672b0d36625c42f8ccc454a406460d49f04efeff391bde9e8c12affe73f33676c606
7
+ data.tar.gz: 52a36475a11bb478ba51f8823916649272a4bd7e3fb42113a5462b2f5dd58051df34dee849460de3c40859a97e0712f8f30f47e579cf30017cfe7a3903075871
data/README.md CHANGED
@@ -182,7 +182,8 @@ The glossary feature ensures consistent translation of app-specific terms (scree
182
182
  1. The plugin loads glossary terms from a JSON file and/or a directory of localization files
183
183
  2. Before each translation, it fuzzy-matches terms from the glossary against the source text
184
184
  3. Only relevant terms are included in the translation prompt (keeping it concise)
185
- 4. Each provider uses the glossary differently:
185
+ 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
186
+ 5. Each provider uses the glossary differently:
186
187
  - **OpenAI**: Glossary terms are sent as a system message for strong instruction following
187
188
  - **Anthropic / Gemini**: Glossary terms are included in the translation prompt
188
189
  - **DeepL**: Glossary terms are passed via the `context` parameter
@@ -406,10 +407,10 @@ translate_gpt_release_notes(
406
407
 
407
408
  ### Android 500 Character Limit
408
409
 
409
- Android has a limit of 500 characters for changelogs. The plugin handles this in two ways:
410
+ Android has a hard limit of 500 characters for changelogs. The plugin enforces this in two layers:
410
411
 
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
412
+ 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.
413
+ 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
414
 
414
415
  If you frequently hit the limit, consider shortening your master locale changelog.
415
416
 
@@ -465,9 +466,9 @@ export OPENAI_API_KEY='your-key-here'
465
466
  **Cause**: The translated text is longer than 500 characters.
466
467
 
467
468
  **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
469
+ 1. Shorten your source changelog — the most reliable fix
470
+ 2. All providers will truncate automatically if the limit is exceeded (with a warning), but this may cut mid-sentence
471
+ 3. For AI providers, the prompt strongly instructs the model to stay within the limit and shorten if needed
471
472
 
472
473
  ### API Timeout Errors
473
474
 
@@ -83,10 +83,11 @@ module Fastlane
83
83
  # @return [String, nil] Translated text or nil on error
84
84
  def translate(text, source_locale, target_locale, glossary_terms: {})
85
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'
86
+ prompt = build_prompt(
87
+ text, source_locale, target_locale,
88
+ glossary_terms: glossary_terms,
89
+ platform: @params[:platform]
90
+ )
90
91
 
91
92
  # Make API call using ruby-anthropic gem API
92
93
  response = @client.complete(
@@ -97,7 +98,7 @@ module Fastlane
97
98
  )
98
99
 
99
100
  # Extract text from response
100
- extract_text_from_response(response)
101
+ enforce_android_limit(extract_text_from_response(response))
101
102
  rescue StandardError => e
102
103
  UI.error "Anthropic provider error: #{e.message}"
103
104
  nil
@@ -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.
@@ -118,15 +118,7 @@ module Fastlane
118
118
  # Make API call
119
119
  result = DeepL.translate(text, source_lang, target_lang, options)
120
120
 
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
121
+ enforce_android_limit(result.text)
130
122
  rescue DeepL::Exceptions::RequestError => e
131
123
  UI.error "DeepL API error: #{e.message}"
132
124
  nil
@@ -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}"
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module TranslateGptReleaseNotes
3
- VERSION = "0.3.1"
3
+ VERSION = "0.3.2"
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.3.2
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-04-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-openai