mistral_translator 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +64 -0
- data/README.md +9 -2
- data/docs/api-reference/configuration.md +19 -2
- data/docs/api-reference/errors.md +15 -24
- data/docs/api-reference/methods.md +10 -4
- data/docs/getting-started.md +8 -2
- data/lib/mistral_translator/adapters.rb +73 -33
- data/lib/mistral_translator/client.rb +18 -19
- data/lib/mistral_translator/client_helpers.rb +41 -23
- data/lib/mistral_translator/configuration.rb +11 -1
- data/lib/mistral_translator/data/languages.json +912 -0
- data/lib/mistral_translator/helpers.rb +20 -16
- data/lib/mistral_translator/helpers_extensions.rb +1 -1
- data/lib/mistral_translator/locale_helper.rb +19 -31
- data/lib/mistral_translator/prompt.rb +48 -0
- data/lib/mistral_translator/prompt_builder.rb +40 -60
- data/lib/mistral_translator/prompt_helpers.rb +25 -12
- data/lib/mistral_translator/prompt_metadata_helpers.rb +16 -17
- data/lib/mistral_translator/proxy.rb +75 -0
- data/lib/mistral_translator/response_parser.rb +120 -140
- data/lib/mistral_translator/security.rb +16 -14
- data/lib/mistral_translator/summarizer.rb +37 -110
- data/lib/mistral_translator/translator.rb +33 -77
- data/lib/mistral_translator/translator_helpers.rb +30 -64
- data/lib/mistral_translator/version.rb +1 -1
- data/lib/mistral_translator.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9ecf7ff10300ed8793126b3417e1b26444daf4c6778ac13d95224880175cb96c
|
|
4
|
+
data.tar.gz: 5ade2f503013b9b539831e9dfddb830fdd68ca6e8a98762a1a0cd97f2d0d1f63
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 23afe648e77f352c771231bee584b0a2cee50c1bf3a2c4c747bf24874a0aeca4c9304fd1f0c8e34cd732591d4551235258d0091a4624f82879235bab737708f2
|
|
7
|
+
data.tar.gz: cd5c9ab42a3369c70ac0d9716e263f25eaf38e7541c8dc18533b1c603db14d6d96c6e628c074bc13e9471ebd2e51b034e946432e8bdca0d0ed1d511fea9bc9cf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-10-28
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
These changes alter behaviour that callers may rely on:
|
|
8
|
+
|
|
9
|
+
- Unusable responses now raise instead of being returned. A response without JSON, with a target that is not text, or with a blank target raises `InvalidResponseError` or `EmptyTranslationError` after the usual retries. Previously the raw response text could be stored as the translation or the summary.
|
|
10
|
+
- `summarize_and_translate` no longer falls back to an untranslated summary in the source language; it retries like the other summaries, then raises.
|
|
11
|
+
- `translate_auto` raises `InvalidResponseError` when language detection is unreadable or names an unsupported language. It used to assume English, which returned the source text untranslated when the target was English.
|
|
12
|
+
- `PromptBuilder` methods return a `MistralTranslator::Prompt` (`instructions` and `text`) instead of a String. `Prompt#to_s` gives the previous single-text form.
|
|
13
|
+
- Prompts are sent as two messages: instructions in the system message, the text alone in the user message, between delimiters, with a rule telling the model to treat it as data.
|
|
14
|
+
- `Client#complete` takes `json: false`. The gem's own calls pass `json: true`, which sends `response_format: json_object`.
|
|
15
|
+
- 182 languages instead of 13: every ISO 639-1 code named by CLDR. `SUPPORTED_LANGUAGES` keeps the native names, unchanged for the original 13, and `supported_languages` returns a much longer string. Quality depends on the model and is weaker for rare languages.
|
|
16
|
+
- In `translate_batch` and the batch path of `translate_to_multiple`, an unreadable response fails its own item (`nil`) instead of the whole batch.
|
|
17
|
+
- `translate_auto`, `translate_to_multiple` and the summaries enforce the same 50,000 character limit as `translate`.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `config.proxy`: an `http://` proxy URL or URI, or `:env` to read `HTTPS_PROXY`, then `HTTP_PROXY`, honouring `NO_PROXY`. Disabled by default.
|
|
22
|
+
- `preserve_html:` in `translate_batch` and `translate_auto`.
|
|
23
|
+
- `context:` and `style:` in every summary method.
|
|
24
|
+
- English and French language names are accepted in place of codes (`"German"`, `"allemand"`), and locale suggestions are based on language names.
|
|
25
|
+
- `bin/generate_languages` regenerates `lib/mistral_translator/data/languages.json` from CLDR (macOS).
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- `Helpers.smart_summarize` raised `ArgumentError` on every call.
|
|
30
|
+
- A translated text mentioning "rate limit" or "quota exceeded" was taken for a rate limit, retried, then failed. Only the 429 status counts now.
|
|
31
|
+
- Context and glossary were formatted three different ways; an empty glossary printed `GLOSSAIRE : {}` in quality check prompts, and the `has_context` / `has_glossary` metadata were never set.
|
|
32
|
+
- `translate_batch` silently ignored misspelled options.
|
|
33
|
+
- Locale suggestions returned unrelated codes once every ISO 639-1 language was supported.
|
|
34
|
+
|
|
35
|
+
### Security
|
|
36
|
+
|
|
37
|
+
- User text and API responses no longer reach logs or error messages: the verbose request log records sizes only, error messages carry no response content, errors no longer keep the original Ruby error as `cause`, and the error callback receives the sanitized error. The response stays available through `InvalidResponseError#raw_response`.
|
|
38
|
+
- Delimiters found in the text are neutralised so it cannot close its block, and `Prompt#inspect` shows lengths only.
|
|
39
|
+
- `https://` proxies are rejected: net-http-persistent talks to the proxy in clear, so their credentials would travel unencrypted.
|
|
40
|
+
|
|
41
|
+
### Removed
|
|
42
|
+
|
|
43
|
+
- `TranslatorHelpers::PromptHandler` and the test-only `make_request` fallback in `perform_client_request`.
|
|
44
|
+
|
|
45
|
+
## [0.3.2] - 2026-10-28
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- Rich text adapters send the stored ActionText HTML (`to_html`) instead of the rendered one (`to_s`), which wrapped every translation in an extra `<div class="trix-content">` and inlined the rendered attachments. `ActionText::Content` values are now recognised too, not only `ActionText::RichText`.
|
|
50
|
+
- `RecordTranslationService` passes `preserve_html: true` for rich text fields.
|
|
51
|
+
- `RecordTranslationService#translate_to_all_locales` returns `false` when a translation fails. It returned `true`, so failures went unnoticed; successful translations are still saved.
|
|
52
|
+
- `RecordTranslationService`, `Translator` and `Summarizer` stop retrying after a few `RateLimitError`s and raise, instead of retrying forever on top of the client's own backoff.
|
|
53
|
+
- `translate_to_multiple` passes `preserve_html` to each translation; the option was ignored.
|
|
54
|
+
- `translate_with_mistral` raised `ArgumentError` on every call. `from:` is now the source locale and `to:` the target locales, through a new `target_locales:` option of `RecordTranslationService`.
|
|
55
|
+
- A glossary given as a string no longer raises `NoMethodError` in `PromptBuilder`.
|
|
56
|
+
- `BasicRateLimiter` no longer sleeps while holding its lock, which blocked every other thread.
|
|
57
|
+
- HTTP error messages no longer end with a stray brace.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- CI token restricted to read access, and development dependencies updated (addressable, json, uri).
|
|
62
|
+
|
|
63
|
+
### Removed
|
|
64
|
+
|
|
65
|
+
- Unused helpers `log_prompt_debug` and `build_html_validation_instruction`.
|
|
66
|
+
|
|
3
67
|
## [0.3.1] - 2026-10-27
|
|
4
68
|
|
|
5
69
|
### Fixed
|
data/README.md
CHANGED
|
@@ -30,14 +30,16 @@ MistralTranslator.translate("Bonjour le monde", from: "fr", to: "en")
|
|
|
30
30
|
- **Native Rails integration** (Mobility, Globalize, I18n attributes)
|
|
31
31
|
- **Batch processing** for high-volume translations
|
|
32
32
|
- **Multi-level summarization** with translation
|
|
33
|
-
- **Robust error handling** with automatic
|
|
33
|
+
- **Robust error handling** with automatic retries; unusable responses raise instead of being returned
|
|
34
34
|
- **Complete monitoring** with metrics and callbacks
|
|
35
35
|
- **Thread-safe & concurrent** with connection pooling ([docs](https://peyochanchan.github.io/mistral_translator/#/advanced-usage/concurrent-async))
|
|
36
36
|
- **Built-in rate limiting** and security
|
|
37
37
|
|
|
38
38
|
## Supported Languages
|
|
39
39
|
|
|
40
|
-
`fr
|
|
40
|
+
All 182 ISO 639-1 languages named by CLDR (`fr`, `en`, `es`, `de`, `ja`, `br`, `wo`, `mg`…). `MistralTranslator.supported_locales` lists them, and English or French names are accepted in place of codes (`"German"`, `"allemand"`).
|
|
41
|
+
|
|
42
|
+
Translation quality depends on the Mistral model: excellent for widely used languages, weaker for rare ones. Check the output before relying on a rare language.
|
|
41
43
|
|
|
42
44
|
## Installation
|
|
43
45
|
|
|
@@ -162,6 +164,8 @@ MistralTranslator.configure do |config|
|
|
|
162
164
|
config.model = "mistral-small-latest" # AI model to use (an alias that follows the current Mistral Small)
|
|
163
165
|
config.retry_delays = [1, 2, 4, 8, 16] # Retry delays in seconds
|
|
164
166
|
config.enable_metrics = true # Enable performance metrics
|
|
167
|
+
config.proxy = :env # HTTP proxy: nil (default), "http://host:port", or :env
|
|
168
|
+
# to read HTTPS_PROXY / HTTP_PROXY and NO_PROXY
|
|
165
169
|
|
|
166
170
|
# Callbacks for monitoring
|
|
167
171
|
config.on_translation_complete = ->(from, to, orig_len, trans_len, duration) {
|
|
@@ -188,6 +192,9 @@ rescue MistralTranslator::AuthenticationError
|
|
|
188
192
|
rescue MistralTranslator::UnsupportedLanguageError => e
|
|
189
193
|
# Language not supported
|
|
190
194
|
Rails.logger.error "Language '#{e.language}' not supported"
|
|
195
|
+
rescue MistralTranslator::InvalidResponseError, MistralTranslator::EmptyTranslationError
|
|
196
|
+
# The model kept returning an unusable answer after the retries: nothing is returned
|
|
197
|
+
Rails.logger.error "Translation unavailable"
|
|
191
198
|
rescue MistralTranslator::Error => e
|
|
192
199
|
# General error handling
|
|
193
200
|
Rails.logger.error "Translation failed: #{e.message}"
|
|
@@ -17,9 +17,26 @@ MistralTranslator.configure do |config|
|
|
|
17
17
|
config.default_temperature = 0.3 # Créativité (0-1)
|
|
18
18
|
config.retry_delays = [2, 4, 8, 16, 32] # Délais retry (secondes)
|
|
19
19
|
config.enable_metrics = false # Suivi des stats
|
|
20
|
+
config.proxy = nil # Proxy HTTP (désactivé par défaut)
|
|
20
21
|
end
|
|
21
22
|
```
|
|
22
23
|
|
|
24
|
+
## Proxy
|
|
25
|
+
|
|
26
|
+
Désactivé par défaut. Trois valeurs possibles :
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
config.proxy = nil # Aucun proxy (défaut)
|
|
30
|
+
config.proxy = "http://user:password@proxy:3128" # Ce proxy (String ou URI)
|
|
31
|
+
config.proxy = :env # HTTPS_PROXY, puis HTTP_PROXY ; NO_PROXY respecté
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- Seuls les proxys `http://` sont acceptés. La bibliothèque HTTP utilisée parle toujours en clair au proxy : avec `https://`, les identifiants du proxy circuleraient sans chiffrement. Le flux vers l'API Mistral reste chiffré de bout en bout (tunnel `CONNECT` puis TLS).
|
|
35
|
+
- Avec `:env`, `NO_PROXY` suit les règles de curl : `*`, un domaine et ses sous-domaines (`mistral.ai`, `.mistral.ai`, `*.mistral.ai`), éventuellement avec le port 443.
|
|
36
|
+
- Une valeur invalide lève `ConfigurationError` dès l'affectation ; le message ne reprend jamais l'URL, pour ne pas exposer les identifiants.
|
|
37
|
+
- `api_url` reste verrouillée sur `https://api.mistral.ai`.
|
|
38
|
+
- Définissez le proxy avant la première traduction : le client et sa connexion sont créés une fois puis réutilisés.
|
|
39
|
+
|
|
23
40
|
## Callbacks
|
|
24
41
|
|
|
25
42
|
```ruby
|
|
@@ -63,9 +80,9 @@ config.setup_rails_logging # Active les logs Rails standard
|
|
|
63
80
|
MistralTranslator.health_check
|
|
64
81
|
# => { status: :ok, message: "API connection successful" }
|
|
65
82
|
|
|
66
|
-
# Langues supportées
|
|
83
|
+
# Langues supportées : les 182 codes ISO 639-1
|
|
67
84
|
MistralTranslator.supported_locales
|
|
68
|
-
# => ["
|
|
85
|
+
# => ["aa", "ab", "ae", "af", ..., "zh", "zu"]
|
|
69
86
|
```
|
|
70
87
|
|
|
71
88
|
---
|
|
@@ -210,8 +210,10 @@ end
|
|
|
210
210
|
|
|
211
211
|
**Attributs:**
|
|
212
212
|
|
|
213
|
-
- `message` (String) - Description de l'erreur
|
|
214
|
-
- `raw_response` (String, optionnel) - Réponse brute reçue
|
|
213
|
+
- `message` (String) - Description de l'erreur. Elle ne contient jamais la réponse de l'API, pour qu'aucun texte traduit ne finisse dans les journaux.
|
|
214
|
+
- `raw_response` (String, optionnel) - Réponse brute reçue, accessible au code appelant uniquement
|
|
215
|
+
|
|
216
|
+
L'erreur Ruby d'origine (`JSON::ParserError`, `NoMethodError`…) n'est pas conservée dans `cause` : son message cite la réponse.
|
|
215
217
|
|
|
216
218
|
**Causes courantes:**
|
|
217
219
|
|
|
@@ -321,13 +323,13 @@ end
|
|
|
321
323
|
|
|
322
324
|
### `MistralTranslator::SecurityError`
|
|
323
325
|
|
|
324
|
-
**
|
|
326
|
+
**Statut:** définie pour usage futur ; la gem ne la lève actuellement nulle part. Inutile de la rattraper.
|
|
325
327
|
|
|
326
|
-
|
|
328
|
+
Les contrôles de sécurité existants lèvent d'autres erreurs :
|
|
327
329
|
|
|
328
|
-
- Texte
|
|
329
|
-
-
|
|
330
|
-
-
|
|
330
|
+
- Texte de plus de 50 000 caractères : `ArgumentError` (traductions, détection automatique et résumés)
|
|
331
|
+
- Proxy invalide : `ConfigurationError`
|
|
332
|
+
- Instructions glissées dans le texte à traduire : rien n'est levé. Le texte est envoyé à part des consignes (message `user`), entre délimiteurs, avec une règle demandant au modèle de le traiter comme une donnée.
|
|
331
333
|
|
|
332
334
|
**Exemple:**
|
|
333
335
|
|
|
@@ -344,44 +346,33 @@ end
|
|
|
344
346
|
**Solutions:**
|
|
345
347
|
|
|
346
348
|
- Diviser les textes longs en plusieurs parties
|
|
347
|
-
- Valider le contenu avant traduction
|
|
348
349
|
- Utiliser `translate_batch` pour les lots importants
|
|
349
350
|
|
|
350
351
|
---
|
|
351
352
|
|
|
352
353
|
### `MistralTranslator::RateLimitExceededError`
|
|
353
354
|
|
|
354
|
-
**
|
|
355
|
+
**Statut:** définie pour usage futur ; la gem ne la lève actuellement nulle part.
|
|
355
356
|
|
|
356
357
|
**Attributs:**
|
|
357
358
|
|
|
358
359
|
- `wait_time` (Integer, optionnel) - Temps d'attente suggéré
|
|
359
360
|
- `retry_after` (Integer, optionnel) - Timestamp de retry
|
|
360
361
|
|
|
361
|
-
**
|
|
362
|
+
**Comportement réel des limites de débit:**
|
|
362
363
|
|
|
363
|
-
- `
|
|
364
|
-
-
|
|
365
|
-
|
|
366
|
-
**Exemple:**
|
|
364
|
+
- Limite locale (`Security::BasicRateLimiter`) : la requête **attend** qu'un créneau se libère, sans lever d'erreur.
|
|
365
|
+
- Limite de l'API Mistral (HTTP 429) : le client relance avec les délais de `config.retry_delays`, puis lève `RateLimitError`.
|
|
367
366
|
|
|
368
367
|
```ruby
|
|
369
|
-
# Configuration du rate limiter local
|
|
370
368
|
rate_limiter = MistralTranslator::Security::BasicRateLimiter.new(
|
|
371
369
|
max_requests: 10,
|
|
372
370
|
window_seconds: 60
|
|
373
371
|
)
|
|
374
|
-
|
|
375
372
|
client = MistralTranslator::Client.new(rate_limiter: rate_limiter)
|
|
376
373
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
15.times { client.complete("Hello") }
|
|
380
|
-
rescue MistralTranslator::RateLimitExceededError => e
|
|
381
|
-
puts "Local rate limit exceeded"
|
|
382
|
-
sleep(e.wait_time) if e.wait_time
|
|
383
|
-
retry
|
|
384
|
-
end
|
|
374
|
+
# Au-delà de 10 requêtes par minute, les appels attendent leur tour
|
|
375
|
+
15.times { client.complete("Hello") }
|
|
385
376
|
```
|
|
386
377
|
|
|
387
378
|
---
|
|
@@ -62,8 +62,13 @@ Traduit un texte avec détection automatique de la langue source.
|
|
|
62
62
|
- `to` (String|Symbol) - Code de langue cible
|
|
63
63
|
- `context` (String, optionnel) - Contexte pour la traduction
|
|
64
64
|
- `glossary` (Hash, optionnel) - Dictionnaire de termes
|
|
65
|
+
- `preserve_html` (Boolean, optionnel) - Préserve les balises HTML (défaut: false)
|
|
65
66
|
|
|
66
|
-
**Retour:** String - Le texte traduit
|
|
67
|
+
**Retour:** String - Le texte traduit (le texte d'origine si la langue détectée est déjà la langue cible)
|
|
68
|
+
|
|
69
|
+
**Exceptions:**
|
|
70
|
+
|
|
71
|
+
- `InvalidResponseError` - Détection illisible ou langue non supportée : aucune langue n'est supposée
|
|
67
72
|
|
|
68
73
|
**Exemple:**
|
|
69
74
|
|
|
@@ -118,8 +123,9 @@ Traduit plusieurs textes en une fois pour optimiser les performances.
|
|
|
118
123
|
- `to` (String|Symbol) - Code de langue cible
|
|
119
124
|
- `context` (String, optionnel) - Contexte commun pour tous les textes
|
|
120
125
|
- `glossary` (Hash, optionnel) - Dictionnaire de termes
|
|
126
|
+
- `preserve_html` (Boolean, optionnel) - Préserve les balises HTML (défaut: false)
|
|
121
127
|
|
|
122
|
-
**Retour:** Hash - Hash avec les index comme clés et traductions comme valeurs
|
|
128
|
+
**Retour:** Hash - Hash avec les index comme clés et traductions comme valeurs. Un élément dont la requête ou la réponse échoue vaut `nil`, sans faire échouer le lot.
|
|
123
129
|
|
|
124
130
|
**Exemple:**
|
|
125
131
|
|
|
@@ -292,7 +298,7 @@ Remet la configuration aux valeurs par défaut.
|
|
|
292
298
|
|
|
293
299
|
Retourne la liste des codes de langue supportés.
|
|
294
300
|
|
|
295
|
-
**Retour:** Array<String> - Liste des codes (ex: ["
|
|
301
|
+
**Retour:** Array<String> - Liste des 182 codes ISO 639-1 (ex: ["aa", "ab", ..., "zu"])
|
|
296
302
|
|
|
297
303
|
### `supported_languages`
|
|
298
304
|
|
|
@@ -399,7 +405,7 @@ Client HTTP pour l'API Mistral.
|
|
|
399
405
|
|
|
400
406
|
**Méthodes d'instance:**
|
|
401
407
|
|
|
402
|
-
- `complete(prompt, max_tokens: nil, temperature: nil, context: {})` - Complétion
|
|
408
|
+
- `complete(prompt, max_tokens: nil, temperature: nil, context: {}, json: false)` - Complétion ; `json: true` demande un objet JSON à l'API (`response_format: json_object`), le prompt devant décrire le JSON attendu
|
|
403
409
|
- `chat(prompt, max_tokens: nil, temperature: nil, context: {})` - Chat
|
|
404
410
|
- `translate_batch(requests, batch_size: 5)` - Traduction par lots
|
|
405
411
|
|
data/docs/getting-started.md
CHANGED
|
@@ -39,15 +39,21 @@ MistralTranslator.translate_auto("Guten Tag", to: "fr")
|
|
|
39
39
|
## 🌍 Langues Supportées
|
|
40
40
|
|
|
41
41
|
```ruby
|
|
42
|
-
#
|
|
42
|
+
# Les 182 langues ISO 639-1 sont disponibles
|
|
43
43
|
MistralTranslator::LocaleHelper.supported_locales
|
|
44
|
-
# => ["
|
|
44
|
+
# => ["aa", "ab", "ae", "af", ..., "zh", "zu"]
|
|
45
|
+
|
|
46
|
+
# Les noms anglais et français sont acceptés à la place du code
|
|
47
|
+
MistralTranslator::LocaleHelper.language_to_locale("allemand")
|
|
48
|
+
# => "de"
|
|
45
49
|
|
|
46
50
|
# Vérifier une langue spécifique
|
|
47
51
|
MistralTranslator::LocaleHelper.locale_supported?("ja")
|
|
48
52
|
# => true
|
|
49
53
|
```
|
|
50
54
|
|
|
55
|
+
La qualité de traduction dépend du modèle Mistral : excellente pour les langues les plus parlées, plus faible pour les langues rares. Vérifiez le résultat avant de vous appuyer sur une langue rare.
|
|
56
|
+
|
|
51
57
|
## 🚨 Gestion d'Erreurs Simple
|
|
52
58
|
|
|
53
59
|
```ruby
|
|
@@ -43,24 +43,35 @@ module MistralTranslator
|
|
|
43
43
|
value = get_field_value(field, locale)
|
|
44
44
|
return false if value.nil?
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
!value.to_plain_text.to_s.strip.empty?
|
|
49
|
-
else
|
|
50
|
-
!value.to_s.strip.empty?
|
|
51
|
-
end
|
|
46
|
+
text = rich_text?(value) ? value.to_plain_text : value
|
|
47
|
+
!text.to_s.strip.empty?
|
|
52
48
|
end
|
|
53
49
|
|
|
54
50
|
def get_translatable_content(field, locale)
|
|
55
51
|
value = get_field_value(field, locale)
|
|
56
52
|
return nil if value.nil?
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
rich_text?(value) ? rich_text_html(value) : value.to_s
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# ActionText::RichText (has_rich_text) ou ActionText::Content (son body)
|
|
58
|
+
def rich_text?(value)
|
|
59
|
+
(defined?(ActionText::RichText) && value.is_a?(ActionText::RichText)) ||
|
|
60
|
+
(defined?(ActionText::Content) && value.is_a?(ActionText::Content))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def rich_text_field?(field, locale)
|
|
64
|
+
rich_text?(get_field_value(field, locale))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# HTML tel que stocké, pièces jointes réduites à <action-text-attachment>.
|
|
70
|
+
# to_s rendrait le layout (<div class="trix-content">) et le contenu des
|
|
71
|
+
# pièces jointes, qui seraient alors enregistrés dans la traduction.
|
|
72
|
+
def rich_text_html(value)
|
|
73
|
+
content = value.respond_to?(:body) ? value.body : value
|
|
74
|
+
content&.to_html.to_s
|
|
64
75
|
end
|
|
65
76
|
end
|
|
66
77
|
|
|
@@ -167,16 +178,22 @@ module MistralTranslator
|
|
|
167
178
|
# Service de traduction utilisant les adaptateurs
|
|
168
179
|
module Adapters
|
|
169
180
|
class RecordTranslationService
|
|
170
|
-
|
|
181
|
+
MAX_RATE_LIMIT_RETRIES = 3
|
|
182
|
+
|
|
183
|
+
# target_locales : toutes les locales de l'adaptateur par défaut
|
|
184
|
+
def initialize(record, translatable_fields, adapter: nil, source_locale: nil, target_locales: nil)
|
|
171
185
|
@record = record
|
|
172
186
|
@translatable_fields = Array(translatable_fields)
|
|
173
187
|
@adapter = adapter || Adapters::AdapterFactory.build_for(record)
|
|
174
188
|
@source_locale = source_locale
|
|
189
|
+
@target_locales = target_locales&.map(&:to_sym)
|
|
175
190
|
end
|
|
176
191
|
|
|
192
|
+
# Enregistre les traductions obtenues ; renvoie false si l'une a échoué.
|
|
177
193
|
def translate_to_all_locales
|
|
178
194
|
return false if @translatable_fields.empty?
|
|
179
195
|
|
|
196
|
+
@failed = false
|
|
180
197
|
ActiveRecord::Base.transaction do
|
|
181
198
|
@translatable_fields.each do |field|
|
|
182
199
|
translate_field(field)
|
|
@@ -184,7 +201,7 @@ module MistralTranslator
|
|
|
184
201
|
@adapter.save_record!
|
|
185
202
|
end
|
|
186
203
|
|
|
187
|
-
|
|
204
|
+
!@failed
|
|
188
205
|
rescue StandardError => e
|
|
189
206
|
Rails.logger.error "Translation failed: #{e.message}" if defined?(Rails)
|
|
190
207
|
false
|
|
@@ -198,34 +215,57 @@ module MistralTranslator
|
|
|
198
215
|
|
|
199
216
|
return if source_content.nil? || source_content.strip.empty?
|
|
200
217
|
|
|
218
|
+
options = rich_text_field?(field, source_locale) ? { preserve_html: true } : {}
|
|
201
219
|
target_locales = @adapter.available_locales - [source_locale]
|
|
220
|
+
target_locales &= @target_locales if @target_locales
|
|
202
221
|
|
|
203
222
|
target_locales.each do |target_locale|
|
|
204
|
-
translate_to_locale(field, source_content, source_locale, target_locale)
|
|
205
|
-
sleep(2) # Rate limiting basique
|
|
223
|
+
translated = translate_to_locale(field, source_content, source_locale, target_locale, options)
|
|
224
|
+
sleep(2) if translated # Rate limiting basique
|
|
206
225
|
end
|
|
207
226
|
end
|
|
208
227
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
to: target_locale.to_s
|
|
214
|
-
)
|
|
228
|
+
# Les adaptateurs fournis par l'application n'héritent pas forcément de BaseAdapter.
|
|
229
|
+
def rich_text_field?(field, locale)
|
|
230
|
+
@adapter.respond_to?(:rich_text_field?) && @adapter.rich_text_field?(field, locale)
|
|
231
|
+
end
|
|
215
232
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
233
|
+
def translate_to_locale(field, content, source_locale, target_locale, options)
|
|
234
|
+
rate_limit_retries = 0
|
|
235
|
+
begin
|
|
236
|
+
translated_content = MistralTranslator.translate(
|
|
237
|
+
content,
|
|
238
|
+
from: source_locale.to_s,
|
|
239
|
+
to: target_locale.to_s,
|
|
240
|
+
**options
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if translated_content && !translated_content.strip.empty?
|
|
244
|
+
@adapter.set_field_value(field, target_locale,
|
|
245
|
+
translated_content)
|
|
246
|
+
end
|
|
247
|
+
true
|
|
248
|
+
rescue MistralTranslator::RateLimitError => e
|
|
249
|
+
Rails.logger.warn "Rate limit: #{e.message}" if defined?(Rails)
|
|
250
|
+
rate_limit_retries += 1
|
|
251
|
+
if rate_limit_retries <= MAX_RATE_LIMIT_RETRIES
|
|
252
|
+
sleep(2)
|
|
253
|
+
retry
|
|
254
|
+
end
|
|
255
|
+
record_failure(field, source_locale, target_locale, e)
|
|
256
|
+
false
|
|
257
|
+
rescue StandardError => e
|
|
258
|
+
record_failure(field, source_locale, target_locale, e)
|
|
259
|
+
false
|
|
227
260
|
end
|
|
228
261
|
end
|
|
262
|
+
|
|
263
|
+
def record_failure(field, source_locale, target_locale, error)
|
|
264
|
+
@failed = true
|
|
265
|
+
return unless defined?(Rails)
|
|
266
|
+
|
|
267
|
+
Rails.logger.error "Translation error for #{field} #{source_locale}->#{target_locale}: #{error.message}"
|
|
268
|
+
end
|
|
229
269
|
end
|
|
230
270
|
end
|
|
231
271
|
|
|
@@ -22,7 +22,9 @@ module MistralTranslator
|
|
|
22
22
|
@rate_limiter = rate_limiter || Security::BasicRateLimiter.new
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
# json: true demande un objet JSON à l'API (response_format json_object) ;
|
|
26
|
+
# le prompt doit alors décrire le JSON attendu.
|
|
27
|
+
def complete(prompt, max_tokens: nil, temperature: nil, context: {}, json: false)
|
|
26
28
|
# Vérifier le rate limit avant de faire la requête
|
|
27
29
|
@rate_limiter.wait_and_record!
|
|
28
30
|
|
|
@@ -30,14 +32,15 @@ module MistralTranslator
|
|
|
30
32
|
start_time = Time.now
|
|
31
33
|
trigger_translation_start_callback(ctx, prompt)
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
request_options = { max_tokens: max_tokens, temperature: temperature, json: json }
|
|
36
|
+
response = make_request_with_retry(prompt, request_options, ctx)
|
|
34
37
|
content = extract_content_from_response(response)
|
|
35
38
|
|
|
36
39
|
trigger_translation_complete_callback(ctx, prompt, content, start_time)
|
|
37
40
|
content
|
|
38
41
|
rescue JSON::ParserError => e
|
|
39
42
|
handle_json_parse_error(e, ctx)
|
|
40
|
-
rescue NoMethodError => e
|
|
43
|
+
rescue NoMethodError, TypeError => e
|
|
41
44
|
handle_response_structure_error(e, ctx)
|
|
42
45
|
end
|
|
43
46
|
|
|
@@ -49,14 +52,14 @@ module MistralTranslator
|
|
|
49
52
|
start_time = Time.now
|
|
50
53
|
trigger_translation_start_callback(ctx, prompt)
|
|
51
54
|
|
|
52
|
-
response = make_request_with_retry(prompt, max_tokens, temperature, ctx)
|
|
55
|
+
response = make_request_with_retry(prompt, { max_tokens: max_tokens, temperature: temperature }, ctx)
|
|
53
56
|
content = extract_chat_content_from_response(response)
|
|
54
57
|
|
|
55
58
|
trigger_translation_complete_callback(ctx, prompt, content, start_time)
|
|
56
59
|
content
|
|
57
60
|
rescue JSON::ParserError => e
|
|
58
61
|
handle_json_parse_error(e, ctx)
|
|
59
|
-
rescue NoMethodError => e
|
|
62
|
+
rescue NoMethodError, TypeError => e
|
|
60
63
|
handle_response_structure_error(e, ctx)
|
|
61
64
|
end
|
|
62
65
|
|
|
@@ -122,29 +125,25 @@ module MistralTranslator
|
|
|
122
125
|
)
|
|
123
126
|
end
|
|
124
127
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
error,
|
|
130
|
-
context[:attempt] || 0
|
|
131
|
-
)
|
|
132
|
-
message = if context[:operation] == :chat
|
|
133
|
-
"JSON parse error: #{error.message}"
|
|
134
|
-
else
|
|
135
|
-
"Invalid JSON in API response: #{error.message}"
|
|
136
|
-
end
|
|
137
|
-
raise InvalidResponseError, message
|
|
128
|
+
# Le message de JSON::ParserError, NoMethodError ou TypeError cite la réponse de l'API :
|
|
129
|
+
# ni lui ni l'erreur d'origine (cause) ne sont repris, et le callback reçoit l'erreur nettoyée.
|
|
130
|
+
def handle_json_parse_error(_error, context)
|
|
131
|
+
raise_invalid_response("Invalid JSON in API response", context)
|
|
138
132
|
end
|
|
139
133
|
|
|
140
134
|
def handle_response_structure_error(error, context)
|
|
135
|
+
raise_invalid_response("Invalid response structure (#{error.class})", context)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def raise_invalid_response(message, context)
|
|
139
|
+
error = InvalidResponseError.new(message)
|
|
141
140
|
MistralTranslator.configuration.trigger_translation_error(
|
|
142
141
|
context[:from_locale],
|
|
143
142
|
context[:to_locale],
|
|
144
143
|
error,
|
|
145
144
|
context[:attempt] || 0
|
|
146
145
|
)
|
|
147
|
-
raise
|
|
146
|
+
raise error, cause: nil
|
|
148
147
|
end
|
|
149
148
|
end
|
|
150
149
|
end
|