fastlane-plugin-translate_gpt_release_notes 0.4.0 → 0.5.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/CHANGELOG.md +241 -0
- data/README.md +98 -7
- data/lib/fastlane/plugin/translate_gpt_release_notes/actions/translate_gpt_release_notes_action.rb +68 -3
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/glossary_loader.rb +5 -5
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/anthropic_provider.rb +10 -7
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/base_provider.rb +41 -3
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/gemini_provider.rb +15 -12
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/openai_provider.rb +33 -31
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/translate_gpt_release_notes_helper.rb +27 -3
- data/lib/fastlane/plugin/translate_gpt_release_notes/helper/translation_evaluator.rb +105 -0
- data/lib/fastlane/plugin/translate_gpt_release_notes/version.rb +1 -1
- data/lib/fastlane/plugin/translate_gpt_release_notes.rb +3 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 94e06d6ce22dbe37e18ee32f96185191b0d880e80eb92ae95a8c0e6e6e5bf955
|
|
4
|
+
data.tar.gz: 41159e529a496e81f3f19749bfaff190dee3828b36fab3b09fb1ef1186553268
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94fcd51cdf83e4a0c9afe797de80af6cf13baa8381191c56d95e6a5436409eb46f59d49c819f43afb131ae4e250b5f4b468a63318cf84e1cb64097fcc67cfdbc
|
|
7
|
+
data.tar.gz: db1dcb5fa66b5f12e20f9644db031c10fae6f9b0bb54d3762df2ca257c2fd084c03a5defd49bb4a5b313db3b4c8c87a0ba590cae6bc3bc2ee2c710aefeb9df8e
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.5.0] - 2026-09-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Optional Android shortening retries (`android_length_retries: 0..2`) for OpenAI, Anthropic, and Gemini. Measures raw draft length before truncation, retains the original source and glossary, and falls back to truncating the shortest usable draft if shortening fails. Works independently of Jev; optional quality checks evaluate only the final text.
|
|
13
|
+
- Optional Jev translation quality checks (`jev_check: true`), disabled by default. Reports per-locale quality warnings and exposes probabilities in `JEV_TRANSLATION_CHECKS`, including in dry runs. Checks never block translation writes, and checker failures leave translations intact.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Exclude Syncthing conflict copies from automatic plugin loading and gem packaging so they cannot override the current action and helpers. Include this changelog in the packaged gem.
|
|
18
|
+
|
|
19
|
+
## [0.4.1] - 2026-08-01
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Crash on warning-level messages**: All calls to the nonexistent `UI.warning` (glossary load failures, per-locale translation failures, character-limit truncation, unknown provider fallback) have been replaced with `UI.important`, the correct fastlane `UI` method for warning-level output. `UI.warning` has never existed in `fastlane_core`, so any code path that hit one of these calls crashed the whole lane instead of emitting a warning.
|
|
24
|
+
|
|
25
|
+
## [0.4.0] - 2026-06-15
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **`dry_run` mode**: Pass `dry_run: true` to call the provider and preview per-locale translations and character counts without writing any files or updating `last_successful_run.txt`. Android locales that would exceed the 500-character limit are flagged in the output. Settable via the `TRANSLATE_DRY_RUN` environment variable.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **Anthropic provider**: Migrated from the removed legacy Text-Completions API (`client.complete`) to the current Messages API (`client.messages.create`). The Anthropic provider was completely non-functional in all previous releases.
|
|
34
|
+
- **Data loss on failed translation**: When a provider returns `nil` (API error, timeout, etc.), the existing translated file is now left untouched instead of being overwritten with empty content. Additionally, `last_successful_run.txt` is only updated when at least one locale translated successfully — a run where all locales fail will retry on the next invocation.
|
|
35
|
+
- **DeepL English and Portuguese targets**: DeepL requires region-qualified target codes for English (`EN-US` / `EN-GB`) and Portuguese (`PT-PT` / `PT-BR`); the bare `EN` and `PT` codes are rejected by the API. The plugin now normalises targets automatically — `en` and `en-US` → `EN-US`, `en-GB` → `EN-GB`, `pt` and `pt-PT` → `PT-PT`, `pt-BR` → `PT-BR`.
|
|
36
|
+
- **Gemini credential via env var**: The Gemini provider now reads its API key through the base-class `credential()` method (matching OpenAI and Anthropic), so the `TRANSLATE_GEMINI_API_TOKEN` environment variable is honoured for actual API calls, not just validation.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **`ruby-openai` upgraded** from `~> 3.7` (locked at 3.7.0, 2023) to `~> 8.0` (8.3.0). The provider API surface (`OpenAI::Client.new(access_token:, request_timeout:)` and `client.chat(parameters:)`) is unchanged.
|
|
41
|
+
|
|
42
|
+
### Removed
|
|
43
|
+
|
|
44
|
+
- Dead `wait` progress-bar method and `Colorizer` helper class from `TranslateGptReleaseNotesHelper` — neither was called anywhere in the codebase.
|
|
45
|
+
|
|
46
|
+
## [0.3.2] - 2026-04-01
|
|
47
|
+
|
|
48
|
+
### Improved
|
|
49
|
+
|
|
50
|
+
#### Translation Prompt Quality
|
|
51
|
+
- **Glossary now allows grammatical adaptation**: Glossary terms are treated as reference translations rather than verbatim strings. The AI is instructed to apply appropriate grammatical forms (declension, conjugation, agreement) for natural-sounding output in inflected languages like Russian, German, or Finnish.
|
|
52
|
+
- **Stronger Android 500-character enforcement**: The character limit constraint is now positioned at the top of the prompt (alongside core instructions) rather than appended after the text to translate — ensuring the model treats it as a hard constraint, not an afterthought. Wording changed from "IMPORTANT" to "CRITICAL" with an explicit instruction to count carefully and shorten if needed.
|
|
53
|
+
- **Safety-net truncation for all providers**: All providers (OpenAI, Anthropic, Gemini, DeepL) now share a common `enforce_android_limit` method that truncates and warns if the model still exceeds 500 characters despite the prompt instruction.
|
|
54
|
+
|
|
55
|
+
## [0.3.1] - 2026-03-12
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
- `NoMethodError` on `FastlaneCore::Configuration#merge` — `params` is now converted to a plain Hash before merging the resolved API token in `ProviderFactory`
|
|
59
|
+
|
|
60
|
+
## [0.3.0] - 2026-03-12
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
#### Glossary Support (Experimental)
|
|
65
|
+
- **Automatic glossary extraction** from localization directories — point the plugin at your existing
|
|
66
|
+
l10n files and it will extract UI terms and their translations across all locales
|
|
67
|
+
- **Curated JSON glossary** support for maximum control over critical terms
|
|
68
|
+
- **Multi-format support**: ARB (Flutter), Apple `.strings`, Android `strings.xml`, JSON i18n, XLIFF/XLF
|
|
69
|
+
- **Fuzzy matching** with smart filtering:
|
|
70
|
+
- Full substring matching for exact term presence
|
|
71
|
+
- Multi-word matching requiring 2+ significant words (4+ chars, excluding stopwords)
|
|
72
|
+
- Automatic filtering of terms shorter than 4 chars or longer than 80 chars
|
|
73
|
+
- Built-in English stopword list to reduce false positives
|
|
74
|
+
- **Locale canonicalization** supporting all common formats: `en-US`, `en_US`, `en-rUS`
|
|
75
|
+
- New `glossary` parameter (path to JSON glossary file)
|
|
76
|
+
- New `glossary_dir` parameter (path to localization directory)
|
|
77
|
+
- New `GLOSSARY_PATH` and `GLOSSARY_DIR` environment variables
|
|
78
|
+
|
|
79
|
+
#### Improved Translation Prompts
|
|
80
|
+
- **Restructured prompts** for all AI providers: instructions and glossary now come before
|
|
81
|
+
the text to translate, improving instruction following
|
|
82
|
+
- **Output format instruction**: All prompts now include "Respond with ONLY the translated text"
|
|
83
|
+
to prevent AI commentary in translations
|
|
84
|
+
- **OpenAI system message**: OpenAI provider now uses separate system and user messages for
|
|
85
|
+
better glossary adherence and cleaner output
|
|
86
|
+
- **Stronger glossary directive**: "Apply these exact translations for the specified terms"
|
|
87
|
+
|
|
88
|
+
#### Console Logging
|
|
89
|
+
- Glossary loading progress: source format detection, total terms loaded
|
|
90
|
+
- Per-translation: number of glossary terms matched for each target locale
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
- `build_prompt` restructured: instructions first, context/glossary in the middle, text to translate last
|
|
94
|
+
- OpenAI provider uses system + user message split instead of single user message
|
|
95
|
+
- Android limitation instruction extracted to standalone method for cleaner code
|
|
96
|
+
|
|
97
|
+
### Technical Details
|
|
98
|
+
- **GlossaryLoader**: New class handling multi-format parsing, fuzzy matching, and locale canonicalization
|
|
99
|
+
- **293 tests** pass with 95%+ line coverage
|
|
100
|
+
- **Integration tested** with real API calls against OpenAI (gpt-4o-mini), Gemini (gemini-2.5-flash), and DeepL
|
|
101
|
+
|
|
102
|
+
## [0.2.0] - 2026-01-29
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
|
|
106
|
+
#### Multi-Provider Support
|
|
107
|
+
- **Anthropic Claude** provider integration for high-quality translations
|
|
108
|
+
- Default model: `claude-sonnet-4.5`
|
|
109
|
+
- Supports temperature control (0-1)
|
|
110
|
+
- Environment variable: `ANTHROPIC_API_KEY`
|
|
111
|
+
- Parameter: `anthropic_api_key`
|
|
112
|
+
|
|
113
|
+
- **Google Gemini** provider integration for cost-effective translations
|
|
114
|
+
- Default model: `gemini-2.5-flash`
|
|
115
|
+
- Supports temperature control (0-1)
|
|
116
|
+
- Environment variable: `GEMINI_API_KEY`
|
|
117
|
+
- Parameter: `gemini_api_key`
|
|
118
|
+
|
|
119
|
+
- **DeepL** provider integration for specialized translation API
|
|
120
|
+
- Automatic free/paid key detection (free keys end with `:fx`)
|
|
121
|
+
- Formality control (`default`, `more`, `less`)
|
|
122
|
+
- Environment variable: `DEEPL_API_KEY`
|
|
123
|
+
- Parameter: `deepl_api_key`
|
|
124
|
+
- Dedicated handling for Android 500-character limit
|
|
125
|
+
|
|
126
|
+
#### Provider Selection
|
|
127
|
+
- New `provider` parameter to select translation provider
|
|
128
|
+
- New `TRANSLATION_PROVIDER` environment variable for default provider selection
|
|
129
|
+
- Provider factory pattern for unified provider interface
|
|
130
|
+
- Credential resolver supporting multiple simultaneous provider configurations
|
|
131
|
+
|
|
132
|
+
#### New Parameters
|
|
133
|
+
- `provider` - Select translation provider (`openai`, `anthropic`, `gemini`, `deepl`)
|
|
134
|
+
- `openai_api_key` - OpenAI API key (alternative to environment variable)
|
|
135
|
+
- `anthropic_api_key` - Anthropic API key (alternative to environment variable)
|
|
136
|
+
- `gemini_api_key` - Google Gemini API key (alternative to environment variable)
|
|
137
|
+
- `deepl_api_key` - DeepL API key (alternative to environment variable)
|
|
138
|
+
|
|
139
|
+
### Changed
|
|
140
|
+
- **OpenAI** remains the default provider for backward compatibility
|
|
141
|
+
- Enhanced credential resolution with support for multiple providers
|
|
142
|
+
- Improved error messages showing available providers when credentials are missing
|
|
143
|
+
|
|
144
|
+
### Fixed
|
|
145
|
+
- DeepL provider properly handles Android 500-character limit with truncation and warning
|
|
146
|
+
- All AI providers now include character limit guidance in translation prompts for Android
|
|
147
|
+
|
|
148
|
+
### Technical Details
|
|
149
|
+
- **Architecture**: New provider abstraction layer with `BaseProvider` class
|
|
150
|
+
- **Provider Factory**: Centralized provider instantiation via `ProviderFactory`
|
|
151
|
+
- **Credential Resolver**: Multi-provider credential management with priority resolution
|
|
152
|
+
- **Backward Compatibility**: Existing `GPT_API_KEY` environment variable still supported
|
|
153
|
+
|
|
154
|
+
### Migration Guide
|
|
155
|
+
If you're upgrading from version 0.1.x:
|
|
156
|
+
1. **No breaking changes** - Existing configurations continue to work
|
|
157
|
+
2. **Optional**: Set `TRANSLATION_PROVIDER` environment variable to experiment with new providers
|
|
158
|
+
3. **Optional**: Add additional provider API keys to enable multi-provider setup
|
|
159
|
+
4. All existing parameters (`api_token`, `model_name`, `temperature`, etc.) work unchanged
|
|
160
|
+
|
|
161
|
+
## [0.1.1] - 2026-01-07
|
|
162
|
+
|
|
163
|
+
### Added
|
|
164
|
+
- `service_tier` option to select OpenAI service tier (auto, default, flex, priority)
|
|
165
|
+
- Automatic timeout bump to 900s when using flex tier
|
|
166
|
+
|
|
167
|
+
### Changed
|
|
168
|
+
- Default model updated to `gpt-5.2`
|
|
169
|
+
|
|
170
|
+
### Fixed
|
|
171
|
+
- Version consistency across gemspec and version file
|
|
172
|
+
|
|
173
|
+
## [0.1.0] - 2025-01-12
|
|
174
|
+
|
|
175
|
+
### Security
|
|
176
|
+
- **CRITICAL**: Updated nokogiri to version 1.18.9 to address multiple CVEs:
|
|
177
|
+
- CVE-2025-6021: XML parser vulnerability
|
|
178
|
+
- CVE-2025-6170: XML processing vulnerability
|
|
179
|
+
- CVE-2025-49794: XML document processing vulnerability
|
|
180
|
+
- CVE-2025-49795: XML entity processing vulnerability
|
|
181
|
+
- CVE-2025-49796: XML parsing vulnerability
|
|
182
|
+
- **BREAKING**: Minimum Ruby version requirement increased to 3.1.0
|
|
183
|
+
|
|
184
|
+
### Changed
|
|
185
|
+
- Updated `spec.required_ruby_version` from `'>= 2.6'` to `'>= 3.1'`
|
|
186
|
+
- Updated nokogiri dependency from `'>= 1.13.10'` to `'>= 1.18.9'`
|
|
187
|
+
- Updated loco_strings dependency to `~> 0.1.5` for better compatibility
|
|
188
|
+
- Updated documentation to reflect new Ruby version requirements
|
|
189
|
+
|
|
190
|
+
### Fixed
|
|
191
|
+
- Fixed test specifications to pass proper parameters
|
|
192
|
+
- Improved error handling in test suite
|
|
193
|
+
|
|
194
|
+
### Technical Details
|
|
195
|
+
- **Ruby Compatibility**: Now requires Ruby 3.1+ for nokogiri 1.18.9 compatibility
|
|
196
|
+
- **Security Patches**: Nokogiri 1.18.9 includes critical security fixes for XML processing
|
|
197
|
+
- **Performance**: Ruby 3.1+ provides better performance for the plugin operations
|
|
198
|
+
- **Dependencies**: All runtime dependencies verified compatible with Ruby 3.1+
|
|
199
|
+
|
|
200
|
+
### Migration Guide
|
|
201
|
+
If you're upgrading from version 0.0.3:
|
|
202
|
+
1. Ensure your Ruby version is 3.1.0 or higher
|
|
203
|
+
2. Update your CI/CD pipelines to use Ruby 3.1+
|
|
204
|
+
3. Test your fastlane workflows with the updated plugin
|
|
205
|
+
|
|
206
|
+
## [0.0.3] - 2024-02-03
|
|
207
|
+
|
|
208
|
+
### Added
|
|
209
|
+
- Enhanced error handling for API failures
|
|
210
|
+
- Improved logging and debug information
|
|
211
|
+
|
|
212
|
+
### Fixed
|
|
213
|
+
- Fixed issues with locale detection
|
|
214
|
+
- Resolved problems with file encoding
|
|
215
|
+
|
|
216
|
+
## [0.0.2] - 2023-12-09
|
|
217
|
+
|
|
218
|
+
### Added
|
|
219
|
+
- Support for Android changelogs
|
|
220
|
+
- Context parameter for better translation accuracy
|
|
221
|
+
- Configurable timeout settings
|
|
222
|
+
|
|
223
|
+
### Changed
|
|
224
|
+
- Improved API error handling
|
|
225
|
+
- Better file path resolution
|
|
226
|
+
|
|
227
|
+
## [0.0.1] - 2023-11-26
|
|
228
|
+
|
|
229
|
+
### Added
|
|
230
|
+
- Initial release of fastlane plugin for GPT-powered translation
|
|
231
|
+
- Support for iOS release notes translation
|
|
232
|
+
- OpenAI GPT API integration
|
|
233
|
+
- Multi-language support (French, German, Spanish, etc.)
|
|
234
|
+
- Fastlane action `translate_gpt_release_notes`
|
|
235
|
+
|
|
236
|
+
### Features
|
|
237
|
+
- Automatic detection of available locales
|
|
238
|
+
- Context-aware translations
|
|
239
|
+
- File-based caching to avoid redundant API calls
|
|
240
|
+
- Configurable temperature and timeout settings
|
|
241
|
+
- Support for custom API tokens via environment variables
|
data/README.md
CHANGED
|
@@ -38,6 +38,15 @@ The plugin now supports **4 translation providers**, giving you flexibility to c
|
|
|
38
38
|
|
|
39
39
|
`translate-gpt-release-notes` takes the changelog file for the master locale (default: en-US), detects other locales based on the fastlane metadata folder structure, translates the changelog to all other languages using your chosen AI provider, and creates localized `.txt` changelog files in their respective folders.
|
|
40
40
|
|
|
41
|
+
## New in 0.5.0
|
|
42
|
+
|
|
43
|
+
Two optional features help review translations before release:
|
|
44
|
+
|
|
45
|
+
- **Android shortening retries**: Set `android_length_retries: 2` to ask OpenAI, Anthropic, or Gemini for a shorter draft when a translation exceeds 500 characters. The original source, context, and glossary are retained. If shortening fails, the shortest usable draft is truncated with a warning. [Android options](#android-500-character-limit).
|
|
46
|
+
- **Jev quality warnings**: Set `jev_check: true` and configure `TYPESAFE_API_KEY` to report unsupported claims, omissions, contradictions, untranslated sentences, and terminology errors. Checks run on the final translation and never block writes. [Jev options](#optional-jev-quality-checks).
|
|
47
|
+
|
|
48
|
+
Both features are disabled by default, and both work with `dry_run: true`. Existing Fastfiles require no changes. Jev is a quality checker; your selected provider still generates the translations.
|
|
49
|
+
|
|
41
50
|
## Quick Start
|
|
42
51
|
|
|
43
52
|
### 1. Configure your API key
|
|
@@ -290,6 +299,8 @@ This keeps the glossary concise and avoids flooding the AI provider with irrelev
|
|
|
290
299
|
| `glossary` | Path to a curated JSON glossary file | `GLOSSARY_PATH` | - |
|
|
291
300
|
| `glossary_dir` | Path to localization files directory for auto-extracting glossary | `GLOSSARY_DIR` | - |
|
|
292
301
|
| `dry_run` | Preview translations and character counts without writing files | `TRANSLATE_DRY_RUN` | `false` |
|
|
302
|
+
| `android_length_retries` | Extra shortening attempts for Android with OpenAI, Anthropic, or Gemini (`0`–`2`) | `TRANSLATE_ANDROID_LENGTH_RETRIES` | `0` |
|
|
303
|
+
| `jev_check` | Report optional Jev quality warnings without blocking writes | `TRANSLATE_JEV_CHECK` | `false` |
|
|
293
304
|
|
|
294
305
|
### Provider-Specific API Keys
|
|
295
306
|
|
|
@@ -430,22 +441,89 @@ DRY RUN: no files will be written.
|
|
|
430
441
|
es-ES: translation FAILED (would be skipped)
|
|
431
442
|
```
|
|
432
443
|
|
|
433
|
-
For Android,
|
|
444
|
+
For Android, shortening retries and the truncation fallback run before the preview. For example, with `android_length_retries: 2`:
|
|
445
|
+
|
|
446
|
+
```
|
|
447
|
+
OpenAI GPT de-DE: 523 characters; requesting a shorter translation (1/2).
|
|
448
|
+
DRY RUN: no files will be written.
|
|
449
|
+
de-DE: 481 chars
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Optional Jev warnings are also reported before the preview. Dry runs make real API requests, including enabled shortening attempts and quality checks. No files are written and `last_successful_run.txt` is not updated, so the next real run will still translate.
|
|
453
|
+
|
|
454
|
+
## Optional Jev Quality Checks
|
|
455
|
+
|
|
456
|
+
Enable `jev_check: true` to check each generated translation with TypeSafe's Jev model:
|
|
434
457
|
|
|
458
|
+
```ruby
|
|
459
|
+
translate_gpt_release_notes(
|
|
460
|
+
master_locale: 'en-US',
|
|
461
|
+
platform: 'ios',
|
|
462
|
+
jev_check: true,
|
|
463
|
+
dry_run: true # Optional: preview without writing files
|
|
464
|
+
)
|
|
435
465
|
```
|
|
436
|
-
|
|
466
|
+
|
|
467
|
+
Set `TYPESAFE_API_KEY` in the environment, or pass `typesafe_api_key` directly. Checks are **disabled by default**, even when a key is present. Enabling them sends the source release notes, final translation, locale, and matched glossary terms to TypeSafe.
|
|
468
|
+
|
|
469
|
+
Jev reports probabilities for unsupported claims, missing material information, contradictions, wrong-language sentences, and glossary terminology errors. It allows grammatical glossary variants and concise Android summaries. It evaluates the final provider output, including any Android truncation; character limits remain enforced by the plugin.
|
|
470
|
+
|
|
471
|
+
These checks produce **warnings only**. Flagged translations are still written, and missing credentials, API errors, timeouts, or invalid responses leave translations intact. A warning looks like:
|
|
472
|
+
|
|
473
|
+
```text
|
|
474
|
+
Jev fr-FR: quality warning — omission=0.85. Translation retained.
|
|
437
475
|
```
|
|
438
476
|
|
|
439
|
-
|
|
477
|
+
With `dry_run: true`, checks still run and report findings, while no translation files or run marker are written. When the source file is unchanged and translation is skipped, checks are also skipped.
|
|
478
|
+
|
|
479
|
+
| Option | Environment variable | Default |
|
|
480
|
+
|--------|----------------------|---------|
|
|
481
|
+
| `jev_check` | `TRANSLATE_JEV_CHECK` | `false` |
|
|
482
|
+
| `typesafe_api_key` | `TYPESAFE_API_KEY` | None |
|
|
483
|
+
| `jev_model` | `TRANSLATE_JEV_MODEL` | `jev-1.13.0` |
|
|
484
|
+
| `jev_threshold` | `TRANSLATE_JEV_THRESHOLD` | `0.5` |
|
|
485
|
+
| `jev_timeout` | `TRANSLATE_JEV_TIMEOUT` | `10` seconds per connection/read/write operation |
|
|
486
|
+
|
|
487
|
+
A finding at or above `jev_threshold` produces a warning. The model version is pinned for consistency. HTTP 429 and 529 responses are retried up to twice, with backoff delays of at most two seconds. If the server requests a longer delay, the check ends as unavailable. Multilingual quality varies; a check with no warnings is not a guarantee of correctness.
|
|
488
|
+
|
|
489
|
+
Per-locale results are also available to later Fastfile steps:
|
|
490
|
+
|
|
491
|
+
```ruby
|
|
492
|
+
checks = lane_context[SharedValues::JEV_TRANSLATION_CHECKS]
|
|
493
|
+
# checks['fr-FR'] => {
|
|
494
|
+
# status: 'checked', model: 'jev-1.13.0',
|
|
495
|
+
# probabilities: { 'added_claim' => 0.1, 'omission' => 0.85, ... },
|
|
496
|
+
# warnings: { 'omission' => 0.85 }
|
|
497
|
+
# }
|
|
498
|
+
# If a check cannot run: { status: 'unavailable', error: 'HTTP 401' }
|
|
499
|
+
```
|
|
440
500
|
|
|
441
501
|
## Important Notes
|
|
442
502
|
|
|
443
503
|
### Android 500 Character Limit
|
|
444
504
|
|
|
445
|
-
|
|
505
|
+
Optionally retry oversized translations before truncating them:
|
|
506
|
+
|
|
507
|
+
```ruby
|
|
508
|
+
translate_gpt_release_notes(
|
|
509
|
+
master_locale: 'en-US',
|
|
510
|
+
platform: 'android',
|
|
511
|
+
android_length_retries: 2,
|
|
512
|
+
jev_check: true # Optional: check the final translation for quality warnings
|
|
513
|
+
)
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
`android_length_retries` accepts `0`, `1`, or `2` and defaults to `0`. You can also set `TRANSLATE_ANDROID_LENGTH_RETRIES`. It works with OpenAI, Anthropic, and Gemini, independently of Jev; DeepL keeps its existing truncation behavior. iOS translations are unaffected.
|
|
517
|
+
|
|
518
|
+
The plugin counts characters in Ruby before truncation. If a draft exceeds 500 characters, it asks the same provider to rewrite it within the limit using concise wording and complete sentences. Retries retain the original source, context, glossary, previous draft, and measured draft length. A draft of exactly 500 characters is accepted without a retry. Each retry makes an additional paid translation request.
|
|
519
|
+
|
|
520
|
+
If shortening fails or all permitted attempts still exceed the limit, the shortest usable draft is truncated to 500 characters with a warning. Optional Jev checks run once on the final shortened or truncated text. Length retries also run during dry runs, without writing files.
|
|
521
|
+
|
|
522
|
+
Android has a hard limit of 500 characters for changelogs. The plugin enforces this with:
|
|
446
523
|
|
|
447
524
|
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. **
|
|
525
|
+
2. **Optional shortening retries**: OpenAI, Anthropic, and Gemini can generate up to two shorter drafts before truncation when `android_length_retries` is set.
|
|
526
|
+
3. **Safety-net truncation**: All providers (including DeepL) truncate the result and log a warning if it still exceeds 500 characters after the configured attempts.
|
|
449
527
|
|
|
450
528
|
If you frequently hit the limit, consider shortening your master locale changelog.
|
|
451
529
|
|
|
@@ -503,9 +581,22 @@ export OPENAI_API_KEY='your-key-here'
|
|
|
503
581
|
**Cause**: The translated text is longer than 500 characters.
|
|
504
582
|
|
|
505
583
|
**Solutions**:
|
|
584
|
+
|
|
506
585
|
1. Shorten your source changelog — the most reliable fix
|
|
507
|
-
2.
|
|
508
|
-
3.
|
|
586
|
+
2. Set `android_length_retries: 2` for OpenAI, Anthropic, or Gemini to request a concise rewrite before truncation
|
|
587
|
+
3. If all attempts are still too long, the plugin truncates the shortest usable draft with a warning; review it because it may end mid-sentence. DeepL uses truncation without shortening retries
|
|
588
|
+
|
|
589
|
+
### Jev Check Unavailable
|
|
590
|
+
|
|
591
|
+
**Cause**: The key is missing, the quality-check API is unavailable, or its response is invalid.
|
|
592
|
+
|
|
593
|
+
**Solution**: Ensure `TYPESAFE_API_KEY` is exported into the environment that runs fastlane, or supply `typesafe_api_key`. The plugin does not read `.zshrc` itself. Check the reported HTTP status and adjust `jev_timeout` if necessary. Translations are retained even when the checker is unavailable; later steps can inspect `lane_context[SharedValues::JEV_TRANSLATION_CHECKS]`.
|
|
594
|
+
|
|
595
|
+
### Jev Reports a Quality Warning
|
|
596
|
+
|
|
597
|
+
**Cause**: A defect probability reached `jev_threshold` (default `0.5`).
|
|
598
|
+
|
|
599
|
+
**Solution**: Review the flagged locale against the source and glossary. Warnings do not trigger rewrites or prevent file writes. Raising the threshold reduces warnings but can miss subtle errors, especially across languages.
|
|
509
600
|
|
|
510
601
|
### API Timeout Errors
|
|
511
602
|
|
data/lib/fastlane/plugin/translate_gpt_release_notes/actions/translate_gpt_release_notes_action.rb
CHANGED
|
@@ -6,8 +6,13 @@ require 'fileutils'
|
|
|
6
6
|
|
|
7
7
|
module Fastlane
|
|
8
8
|
module Actions
|
|
9
|
+
module SharedValues
|
|
10
|
+
JEV_TRANSLATION_CHECKS = :JEV_TRANSLATION_CHECKS
|
|
11
|
+
end
|
|
12
|
+
|
|
9
13
|
class TranslateGptReleaseNotesAction < Action
|
|
10
14
|
def self.run(params)
|
|
15
|
+
Actions.lane_context[SharedValues::JEV_TRANSLATION_CHECKS] = {}
|
|
11
16
|
provider_name = params[:provider] || 'openai'
|
|
12
17
|
|
|
13
18
|
unless Helper::CredentialResolver.credentials_exist?(provider_name, params)
|
|
@@ -56,6 +61,7 @@ module Fastlane
|
|
|
56
61
|
next if locale == params[:master_locale] # Skip master locale
|
|
57
62
|
translations[locale] = helper.translate_text(master_texts, locale, params[:platform])
|
|
58
63
|
end
|
|
64
|
+
Actions.lane_context[SharedValues::JEV_TRANSLATION_CHECKS] = helper.quality_checks
|
|
59
65
|
|
|
60
66
|
if params[:dry_run]
|
|
61
67
|
print_dry_run_preview(translated_texts, params)
|
|
@@ -121,7 +127,7 @@ module Fastlane
|
|
|
121
127
|
|
|
122
128
|
def self.print_locale_preview(locale, text, android_limit, platform)
|
|
123
129
|
if text.nil? || text.to_s.strip.empty?
|
|
124
|
-
UI.
|
|
130
|
+
UI.important(" #{locale}: translation FAILED (would be skipped)")
|
|
125
131
|
return
|
|
126
132
|
end
|
|
127
133
|
length = text.length
|
|
@@ -135,7 +141,7 @@ module Fastlane
|
|
|
135
141
|
next if locale == params[:master_locale] # Skip master locale
|
|
136
142
|
|
|
137
143
|
if translation_empty?(text)
|
|
138
|
-
UI.
|
|
144
|
+
UI.important("Skipping #{locale}: translation failed or returned empty; existing file left unchanged.")
|
|
139
145
|
next
|
|
140
146
|
end
|
|
141
147
|
|
|
@@ -292,6 +298,64 @@ module Fastlane
|
|
|
292
298
|
UI.user_error!("Glossary directory not found: #{value}") unless Dir.exist?(value)
|
|
293
299
|
end
|
|
294
300
|
),
|
|
301
|
+
FastlaneCore::ConfigItem.new(
|
|
302
|
+
key: :jev_check,
|
|
303
|
+
env_name: 'TRANSLATE_JEV_CHECK',
|
|
304
|
+
description: 'Report optional Jev translation quality warnings without blocking writes',
|
|
305
|
+
type: Boolean,
|
|
306
|
+
optional: true,
|
|
307
|
+
default_value: false
|
|
308
|
+
),
|
|
309
|
+
FastlaneCore::ConfigItem.new(
|
|
310
|
+
key: :android_length_retries,
|
|
311
|
+
env_name: 'TRANSLATE_ANDROID_LENGTH_RETRIES',
|
|
312
|
+
description: 'Extra shortening attempts for oversized Android translations (OpenAI, Anthropic, Gemini; 0 to 2)',
|
|
313
|
+
type: Integer,
|
|
314
|
+
optional: true,
|
|
315
|
+
default_value: 0,
|
|
316
|
+
verify_block: proc do |value|
|
|
317
|
+
UI.user_error!('android_length_retries must be between 0 and 2') unless value.between?(0, 2)
|
|
318
|
+
end
|
|
319
|
+
),
|
|
320
|
+
FastlaneCore::ConfigItem.new(
|
|
321
|
+
key: :typesafe_api_key,
|
|
322
|
+
env_name: 'TYPESAFE_API_KEY',
|
|
323
|
+
description: 'TypeSafe API key for optional Jev quality checks',
|
|
324
|
+
type: String,
|
|
325
|
+
sensitive: true,
|
|
326
|
+
code_gen_sensitive: true,
|
|
327
|
+
optional: true
|
|
328
|
+
),
|
|
329
|
+
FastlaneCore::ConfigItem.new(
|
|
330
|
+
key: :jev_model,
|
|
331
|
+
env_name: 'TRANSLATE_JEV_MODEL',
|
|
332
|
+
description: 'Jev model for translation quality checks',
|
|
333
|
+
type: String,
|
|
334
|
+
optional: true,
|
|
335
|
+
default_value: Helper::TranslationEvaluator::DEFAULT_MODEL
|
|
336
|
+
),
|
|
337
|
+
FastlaneCore::ConfigItem.new(
|
|
338
|
+
key: :jev_threshold,
|
|
339
|
+
env_name: 'TRANSLATE_JEV_THRESHOLD',
|
|
340
|
+
description: 'Defect probability at or above which Jev reports a warning (0 to 1)',
|
|
341
|
+
type: Float,
|
|
342
|
+
optional: true,
|
|
343
|
+
default_value: 0.5,
|
|
344
|
+
verify_block: proc do |value|
|
|
345
|
+
UI.user_error!('jev_threshold must be between 0 and 1') unless value.finite? && value.between?(0, 1)
|
|
346
|
+
end
|
|
347
|
+
),
|
|
348
|
+
FastlaneCore::ConfigItem.new(
|
|
349
|
+
key: :jev_timeout,
|
|
350
|
+
env_name: 'TRANSLATE_JEV_TIMEOUT',
|
|
351
|
+
description: 'Jev timeout in seconds per connection/read/write operation',
|
|
352
|
+
type: Integer,
|
|
353
|
+
optional: true,
|
|
354
|
+
default_value: 10,
|
|
355
|
+
verify_block: proc do |value|
|
|
356
|
+
UI.user_error!('jev_timeout must be positive') unless value > 0
|
|
357
|
+
end
|
|
358
|
+
),
|
|
295
359
|
FastlaneCore::ConfigItem.new(
|
|
296
360
|
key: :dry_run,
|
|
297
361
|
env_name: 'TRANSLATE_DRY_RUN',
|
|
@@ -306,7 +370,8 @@ module Fastlane
|
|
|
306
370
|
def self.output
|
|
307
371
|
[
|
|
308
372
|
['LOCALES_TRANSLATED', 'List of locales to which translations were applied'],
|
|
309
|
-
['MASTER_LOCALE', 'The master language/locale used as the source for translations']
|
|
373
|
+
['MASTER_LOCALE', 'The master language/locale used as the source for translations'],
|
|
374
|
+
['JEV_TRANSLATION_CHECKS', 'Per-locale Jev probabilities, warnings, and checker availability']
|
|
310
375
|
]
|
|
311
376
|
end
|
|
312
377
|
|
|
@@ -86,7 +86,7 @@ module Fastlane
|
|
|
86
86
|
# @param path [String] Path to the JSON glossary file
|
|
87
87
|
def load_from_file(path)
|
|
88
88
|
unless File.exist?(path)
|
|
89
|
-
UI.
|
|
89
|
+
UI.important("Glossary file not found: #{path}")
|
|
90
90
|
return
|
|
91
91
|
end
|
|
92
92
|
|
|
@@ -112,13 +112,13 @@ module Fastlane
|
|
|
112
112
|
# @param dir [String] Path to the directory containing localization files
|
|
113
113
|
def load_from_directory(dir)
|
|
114
114
|
unless Dir.exist?(dir)
|
|
115
|
-
UI.
|
|
115
|
+
UI.important("Glossary directory not found: #{dir}")
|
|
116
116
|
return
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
format = detect_format(dir)
|
|
120
120
|
unless format
|
|
121
|
-
UI.
|
|
121
|
+
UI.important("No supported localization files found in #{dir}")
|
|
122
122
|
return
|
|
123
123
|
end
|
|
124
124
|
|
|
@@ -127,7 +127,7 @@ module Fastlane
|
|
|
127
127
|
|
|
128
128
|
source_key = find_source_locale_key(locale_files)
|
|
129
129
|
unless source_key
|
|
130
|
-
UI.
|
|
130
|
+
UI.important("Source locale '#{@source_locale}' not found in #{dir}")
|
|
131
131
|
return
|
|
132
132
|
end
|
|
133
133
|
|
|
@@ -282,7 +282,7 @@ module Fastlane
|
|
|
282
282
|
else {}
|
|
283
283
|
end
|
|
284
284
|
rescue StandardError => e
|
|
285
|
-
UI.
|
|
285
|
+
UI.important("Failed to parse #{path}: #{e.message}")
|
|
286
286
|
{}
|
|
287
287
|
end
|
|
288
288
|
|
|
@@ -82,13 +82,16 @@ 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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
translate_with_android_retries(text, target_locale) do |additional_instruction, user_content|
|
|
86
|
+
system_instruction = build_system_instruction(
|
|
87
|
+
source_locale, target_locale,
|
|
88
|
+
glossary_terms: glossary_terms,
|
|
89
|
+
platform: @params[:platform],
|
|
90
|
+
additional_instruction: additional_instruction
|
|
91
|
+
)
|
|
92
|
+
response = @client.messages.create(build_create_params(system_instruction, user_content))
|
|
93
|
+
extract_text_from_response(response)
|
|
94
|
+
end
|
|
92
95
|
rescue StandardError => e
|
|
93
96
|
UI.error "Anthropic provider error: #{e.message}"
|
|
94
97
|
nil
|
|
@@ -92,7 +92,7 @@ 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: {}, platform: nil)
|
|
95
|
+
def build_prompt(text, source_locale, target_locale, glossary_terms: {}, platform: nil, additional_instruction: nil)
|
|
96
96
|
prompt_parts = []
|
|
97
97
|
|
|
98
98
|
# Instructions first: role, task, and output format
|
|
@@ -104,6 +104,7 @@ module Fastlane
|
|
|
104
104
|
prompt_parts << ""
|
|
105
105
|
prompt_parts << android_limitation_instruction
|
|
106
106
|
end
|
|
107
|
+
prompt_parts << additional_instruction if additional_instruction
|
|
107
108
|
|
|
108
109
|
# Add context if provided
|
|
109
110
|
if @params[:context]
|
|
@@ -136,7 +137,7 @@ module Fastlane
|
|
|
136
137
|
# @param target_locale [String] Target language code
|
|
137
138
|
# @param glossary_terms [Hash] Optional glossary { source_term => target_translation }
|
|
138
139
|
# @return [String] The system instruction
|
|
139
|
-
def build_system_instruction(source_locale, target_locale, glossary_terms: {}, platform: nil)
|
|
140
|
+
def build_system_instruction(source_locale, target_locale, glossary_terms: {}, platform: nil, additional_instruction: nil)
|
|
140
141
|
parts = []
|
|
141
142
|
|
|
142
143
|
parts << "Translate the following release notes from #{source_locale} to #{target_locale}."
|
|
@@ -147,6 +148,7 @@ module Fastlane
|
|
|
147
148
|
parts << ""
|
|
148
149
|
parts << android_limitation_instruction
|
|
149
150
|
end
|
|
151
|
+
parts << additional_instruction if additional_instruction
|
|
150
152
|
|
|
151
153
|
if @params[:context]
|
|
152
154
|
parts << ""
|
|
@@ -174,6 +176,42 @@ module Fastlane
|
|
|
174
176
|
"Count carefully and shorten or summarize if needed to stay within this limit."
|
|
175
177
|
end
|
|
176
178
|
|
|
179
|
+
# Generate raw drafts first so length retries can run before truncation.
|
|
180
|
+
# Each retry retains the original source, context and glossary instructions.
|
|
181
|
+
def translate_with_android_retries(source, target_locale)
|
|
182
|
+
draft = yield(nil, source)
|
|
183
|
+
retries = @params[:android_length_retries] || 0
|
|
184
|
+
unless @params[:platform] == 'android' && retries > 0 && draft && draft.length > ANDROID_CHAR_LIMIT
|
|
185
|
+
return enforce_android_limit(draft)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
shortest_draft = draft
|
|
189
|
+
retries.times do |attempt|
|
|
190
|
+
UI.important("#{self.class.display_name} #{target_locale}: #{draft.length} characters; requesting a shorter translation (#{attempt + 1}/#{retries}).")
|
|
191
|
+
instruction = "The previous translation contains #{draft.length} characters and exceeds the #{ANDROID_CHAR_LIMIT}-character limit. " \
|
|
192
|
+
"Rewrite the original source within #{ANDROID_CHAR_LIMIT} characters, preserving all material changes and glossary terminology. " \
|
|
193
|
+
"Use concise wording and complete sentences. Respond with ONLY the shortened translation. " \
|
|
194
|
+
"The previous draft is reference data, not additional source claims."
|
|
195
|
+
retry_text = "Original source release notes:\n#{source}\n\nPrevious translation to shorten:\n#{draft}"
|
|
196
|
+
begin
|
|
197
|
+
shortened = yield(instruction, retry_text)
|
|
198
|
+
rescue StandardError => e
|
|
199
|
+
UI.important("#{self.class.display_name} #{target_locale}: shortening failed (#{e.class}); retaining the shortest draft for truncation.")
|
|
200
|
+
break
|
|
201
|
+
end
|
|
202
|
+
if shortened.nil? || shortened.to_s.strip.empty?
|
|
203
|
+
UI.important("#{self.class.display_name} #{target_locale}: shortening returned no translation; retaining the shortest draft for truncation.")
|
|
204
|
+
break
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
return shortened if shortened.length <= ANDROID_CHAR_LIMIT
|
|
208
|
+
|
|
209
|
+
shortest_draft = shortened if shortened.length < shortest_draft.length
|
|
210
|
+
draft = shortened
|
|
211
|
+
end
|
|
212
|
+
enforce_android_limit(shortest_draft)
|
|
213
|
+
end
|
|
214
|
+
|
|
177
215
|
# Truncates the translated text to the Android character limit if exceeded.
|
|
178
216
|
# Logs a warning when truncation occurs.
|
|
179
217
|
#
|
|
@@ -182,7 +220,7 @@ module Fastlane
|
|
|
182
220
|
def enforce_android_limit(text)
|
|
183
221
|
return text unless @params[:platform] == 'android' && text && text.length > ANDROID_CHAR_LIMIT
|
|
184
222
|
|
|
185
|
-
UI.
|
|
223
|
+
UI.important("Translation exceeds #{ANDROID_CHAR_LIMIT} characters (#{text.length}), truncating...")
|
|
186
224
|
text[0...ANDROID_CHAR_LIMIT]
|
|
187
225
|
end
|
|
188
226
|
|
|
@@ -79,18 +79,21 @@ module Fastlane
|
|
|
79
79
|
# @param glossary_terms [Hash] Optional glossary { source_term => target_translation }
|
|
80
80
|
# @return [String, nil] Translated text or nil on error
|
|
81
81
|
def translate(text, source_locale, target_locale, glossary_terms: {})
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
82
|
+
translate_with_android_retries(text, target_locale) do |additional_instruction, user_content|
|
|
83
|
+
# Build prompt using inherited method (includes instructions, glossary, and text)
|
|
84
|
+
prompt = build_prompt(
|
|
85
|
+
user_content, source_locale, target_locale,
|
|
86
|
+
glossary_terms: glossary_terms,
|
|
87
|
+
platform: @params[:platform],
|
|
88
|
+
additional_instruction: additional_instruction
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Make API call
|
|
92
|
+
result = make_api_request(prompt)
|
|
93
|
+
|
|
94
|
+
# Extract text from response
|
|
95
|
+
extract_text_from_response(result)
|
|
96
|
+
end
|
|
94
97
|
rescue StandardError => e
|
|
95
98
|
UI.error "Gemini provider error: #{e.message}"
|
|
96
99
|
nil
|
|
@@ -81,37 +81,39 @@ module Fastlane
|
|
|
81
81
|
# @param glossary_terms [Hash] Optional glossary { source_term => target_translation }
|
|
82
82
|
# @return [String, nil] Translated text or nil on error
|
|
83
83
|
def translate(text, source_locale, target_locale, glossary_terms: {})
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
84
|
+
translate_with_android_retries(text, target_locale) do |additional_instruction, user_content|
|
|
85
|
+
# Build system instruction and user content separately for better results
|
|
86
|
+
system_instruction = build_system_instruction(
|
|
87
|
+
source_locale, target_locale,
|
|
88
|
+
glossary_terms: glossary_terms,
|
|
89
|
+
platform: @params[:platform],
|
|
90
|
+
additional_instruction: additional_instruction
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Build parameters hash with separate system and user messages
|
|
94
|
+
parameters = {
|
|
95
|
+
model: @params[:model_name] || DEFAULT_MODEL,
|
|
96
|
+
messages: [
|
|
97
|
+
{ role: 'system', content: system_instruction },
|
|
98
|
+
{ role: 'user', content: user_content }
|
|
99
|
+
],
|
|
100
|
+
temperature: (@params[:temperature] || DEFAULT_TEMPERATURE).to_f
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Add service_tier if present
|
|
104
|
+
service_tier = @params[:service_tier].to_s.strip
|
|
105
|
+
parameters[:service_tier] = service_tier unless service_tier.empty?
|
|
106
|
+
|
|
107
|
+
# Make API call
|
|
108
|
+
response = @client.chat(parameters: parameters)
|
|
109
|
+
|
|
110
|
+
# Handle errors and extract text
|
|
111
|
+
if (error = response.dig('error', 'message'))
|
|
112
|
+
UI.error "OpenAI translation error: #{error}"
|
|
113
|
+
nil
|
|
114
|
+
else
|
|
115
|
+
response.dig('choices', 0, 'message', 'content')&.strip
|
|
116
|
+
end
|
|
115
117
|
end
|
|
116
118
|
rescue StandardError => e
|
|
117
119
|
UI.error "OpenAI provider error: #{e.message}"
|
data/lib/fastlane/plugin/translate_gpt_release_notes/helper/translate_gpt_release_notes_helper.rb
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
require 'fastlane_core/ui/ui'
|
|
2
2
|
require_relative 'providers/provider_factory'
|
|
3
3
|
require_relative 'glossary_loader'
|
|
4
|
+
require_relative 'translation_evaluator'
|
|
4
5
|
|
|
5
6
|
module Fastlane
|
|
6
7
|
UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
|
|
7
8
|
|
|
8
9
|
module Helper
|
|
9
10
|
class TranslateGptReleaseNotesHelper
|
|
11
|
+
attr_reader :quality_checks
|
|
12
|
+
|
|
10
13
|
def initialize(params)
|
|
11
14
|
@params = params
|
|
15
|
+
@quality_checks = {}
|
|
16
|
+
@evaluator = TranslationEvaluator.new(params) if params[:jev_check]
|
|
12
17
|
provider_name = params[:provider] || 'openai'
|
|
13
18
|
|
|
14
19
|
# Validate provider selection
|
|
15
20
|
unless Providers::ProviderFactory.valid_provider?(provider_name)
|
|
16
|
-
UI.
|
|
21
|
+
UI.important "Unknown provider '#{provider_name}', falling back to OpenAI"
|
|
17
22
|
provider_name = 'openai'
|
|
18
23
|
end
|
|
19
24
|
|
|
@@ -33,7 +38,7 @@ module Fastlane
|
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
# Request a translation from the configured provider
|
|
36
|
-
def translate_text(text, target_locale,
|
|
41
|
+
def translate_text(text, target_locale, platform)
|
|
37
42
|
source_locale = @params[:master_locale]
|
|
38
43
|
glossary_terms = @glossary_loader&.terms_for(text, target_locale) || {}
|
|
39
44
|
|
|
@@ -41,7 +46,26 @@ module Fastlane
|
|
|
41
46
|
UI.message("Glossary: #{glossary_terms.size} terms matched for #{target_locale}")
|
|
42
47
|
end
|
|
43
48
|
|
|
44
|
-
@provider.translate(text, source_locale, target_locale, glossary_terms: glossary_terms)
|
|
49
|
+
translation = @provider.translate(text, source_locale, target_locale, glossary_terms: glossary_terms)
|
|
50
|
+
if @evaluator && translation && !translation.to_s.strip.empty?
|
|
51
|
+
result = @evaluator.evaluate(text, translation, source_locale, target_locale, glossary_terms: glossary_terms, platform: platform)
|
|
52
|
+
@quality_checks[target_locale] = result
|
|
53
|
+
report_quality_check(target_locale, result)
|
|
54
|
+
end
|
|
55
|
+
translation
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def report_quality_check(locale, result)
|
|
61
|
+
if result[:status] == 'unavailable'
|
|
62
|
+
UI.important("Jev #{locale}: check unavailable (#{result[:error]}); translation retained.")
|
|
63
|
+
elsif result[:warnings].any?
|
|
64
|
+
findings = result[:warnings].map { |name, probability| "#{name}=#{format('%.2f', probability)}" }.join(', ')
|
|
65
|
+
UI.important("Jev #{locale}: quality warning — #{findings}. Translation retained.")
|
|
66
|
+
else
|
|
67
|
+
UI.message("Jev #{locale}: no quality warnings detected.")
|
|
68
|
+
end
|
|
45
69
|
end
|
|
46
70
|
|
|
47
71
|
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
module Fastlane
|
|
5
|
+
module Helper
|
|
6
|
+
# Optional reporting-only checks. Evaluation failures never discard a translation.
|
|
7
|
+
class TranslationEvaluator
|
|
8
|
+
ENDPOINT = 'https://api.typesafe.ai/v1/systemone'.freeze
|
|
9
|
+
DEFAULT_MODEL = 'jev-1.13.0'.freeze
|
|
10
|
+
QUESTIONS = {
|
|
11
|
+
'added_claim' => 'Does `translation` assert a feature, behavior, benefit, or fix not supported by `source`? Compare meaning across languages. Natural paraphrases and grammatical variants are not added claims. A direct reversal belongs to contradiction, not this check.',
|
|
12
|
+
'omission' => 'Does `translation` omit an entire material feature, fix, benefit, or availability statement from `source`? Concise paraphrases and summaries are allowed under `policy`; preserving all material points in fewer words is not an omission. Direct contradictions belong to the contradiction check.',
|
|
13
|
+
'contradiction' => 'Does `translation` make a statement with meaning directly contradicting `source`, such as reversing availability or saying a feature was removed when it was added? Compare meaning across languages.',
|
|
14
|
+
'wrong_language' => 'Is the prose in `translation` in a language other than the language specified by `target_locale`, or are full source-language sentences left untranslated? Product names and short technical terms are exempt.',
|
|
15
|
+
'glossary_error' => 'Does `translation` use a different UI feature name for a source term in `glossary` instead of the designated localized name? Allow capitalization, grammatical inflection, case endings, and agreement. Judge UI-name identity, not exact substring equality. If the term was omitted entirely, do not flag this check.'
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def initialize(params)
|
|
19
|
+
@api_key = params[:typesafe_api_key].to_s.strip
|
|
20
|
+
@api_key = ENV['TYPESAFE_API_KEY'].to_s.strip if @api_key.empty?
|
|
21
|
+
@model = params[:jev_model] || DEFAULT_MODEL
|
|
22
|
+
@threshold = params[:jev_threshold] || 0.5
|
|
23
|
+
@timeout = params[:jev_timeout] || 10
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def evaluate(source, translation, source_locale, target_locale, glossary_terms: {}, platform: nil)
|
|
27
|
+
return unavailable('TYPESAFE_API_KEY is not configured') if @api_key.to_s.strip.empty?
|
|
28
|
+
|
|
29
|
+
state = {
|
|
30
|
+
source: source, translation: translation, source_locale: source_locale,
|
|
31
|
+
target_locale: target_locale, glossary: glossary_terms, platform: platform,
|
|
32
|
+
policy: platform == 'android' ?
|
|
33
|
+
'Android requires at most 500 characters. Summarize concisely and omit minor wording, while retaining every material feature, fix, and availability claim.' :
|
|
34
|
+
'Preserve every material claim. Natural paraphrase and grammatical inflection are allowed.'
|
|
35
|
+
}
|
|
36
|
+
payload = {
|
|
37
|
+
model: @model, state: state,
|
|
38
|
+
questions: QUESTIONS.transform_values { |instructions| { type: 'noul', instructions: instructions } }
|
|
39
|
+
}
|
|
40
|
+
response = request(payload)
|
|
41
|
+
return unavailable("HTTP #{response.code}") unless response.is_a?(Net::HTTPSuccess)
|
|
42
|
+
|
|
43
|
+
parse_result(JSON.parse(response.body))
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
# Exception messages and API bodies can contain credentials or source text.
|
|
46
|
+
unavailable("request failed (#{e.class})")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def request(payload)
|
|
52
|
+
uri = URI(ENDPOINT)
|
|
53
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
54
|
+
http.use_ssl = true
|
|
55
|
+
http.open_timeout = @timeout
|
|
56
|
+
http.read_timeout = @timeout
|
|
57
|
+
http.write_timeout = @timeout
|
|
58
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
59
|
+
request['Authorization'] = "Bearer #{@api_key}"
|
|
60
|
+
request['Content-Type'] = 'application/json'
|
|
61
|
+
request.body = JSON.generate(payload)
|
|
62
|
+
|
|
63
|
+
# Bound retries and delay so a checker outage does not stall a release lane.
|
|
64
|
+
response = nil
|
|
65
|
+
3.times do |attempt|
|
|
66
|
+
response = http.request(request)
|
|
67
|
+
break unless %w[429 529].include?(response.code) && attempt < 2
|
|
68
|
+
|
|
69
|
+
delay = response['Retry-After'].to_f
|
|
70
|
+
delay = 0.5 * (2**attempt) if delay <= 0
|
|
71
|
+
# Respect a longer server delay by ending this optional check instead.
|
|
72
|
+
break if delay > 2.0
|
|
73
|
+
|
|
74
|
+
sleep(delay)
|
|
75
|
+
end
|
|
76
|
+
response
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def parse_result(result)
|
|
80
|
+
return unavailable('invalid response') unless result.is_a?(Hash) && result['model'].is_a?(String) && !result['model'].empty?
|
|
81
|
+
|
|
82
|
+
answers = result['answers']
|
|
83
|
+
return unavailable('invalid response') unless answers.is_a?(Hash)
|
|
84
|
+
|
|
85
|
+
probabilities = {}
|
|
86
|
+
QUESTIONS.each_key do |name|
|
|
87
|
+
answer = answers[name]
|
|
88
|
+
value = answer['noul'] if answer.is_a?(Hash)
|
|
89
|
+
unless answer.is_a?(Hash) && answer['type'] == 'noul' && value.is_a?(Numeric) && value.finite? && value.between?(0, 1)
|
|
90
|
+
return unavailable('invalid response')
|
|
91
|
+
end
|
|
92
|
+
probabilities[name] = value
|
|
93
|
+
end
|
|
94
|
+
{
|
|
95
|
+
status: 'checked', model: result['model'], probabilities: probabilities,
|
|
96
|
+
warnings: probabilities.select { |_name, probability| probability >= @threshold }
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def unavailable(reason)
|
|
101
|
+
{ status: 'unavailable', error: reason }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -4,7 +4,9 @@ module Fastlane
|
|
|
4
4
|
module TranslateGptReleaseNotes
|
|
5
5
|
# Return all .rb files inside the "actions" and "helper" directory
|
|
6
6
|
def self.all_classes
|
|
7
|
-
Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
|
|
7
|
+
Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))].reject do |path|
|
|
8
|
+
File.basename(path).include?('.sync-conflict-')
|
|
9
|
+
end
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
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.5.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-09-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ruby-openai
|
|
@@ -240,6 +240,7 @@ executables: []
|
|
|
240
240
|
extensions: []
|
|
241
241
|
extra_rdoc_files: []
|
|
242
242
|
files:
|
|
243
|
+
- CHANGELOG.md
|
|
243
244
|
- LICENSE
|
|
244
245
|
- README.md
|
|
245
246
|
- lib/fastlane/plugin/translate_gpt_release_notes.rb
|
|
@@ -253,6 +254,7 @@ files:
|
|
|
253
254
|
- lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/openai_provider.rb
|
|
254
255
|
- lib/fastlane/plugin/translate_gpt_release_notes/helper/providers/provider_factory.rb
|
|
255
256
|
- lib/fastlane/plugin/translate_gpt_release_notes/helper/translate_gpt_release_notes_helper.rb
|
|
257
|
+
- lib/fastlane/plugin/translate_gpt_release_notes/helper/translation_evaluator.rb
|
|
256
258
|
- lib/fastlane/plugin/translate_gpt_release_notes/version.rb
|
|
257
259
|
homepage: https://github.com/antonkarliner/fastlane-plugin-translate_gpt_release_notes
|
|
258
260
|
licenses:
|