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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 278978acb1df0d447293200f13ff58745640a62f5bf43a41ebc7b31db2d50b98
4
- data.tar.gz: c86fc021d49a554fd4d44883e1383a5ea4ba82251074b4b358b3b5cf97f27fd2
3
+ metadata.gz: 9ecf7ff10300ed8793126b3417e1b26444daf4c6778ac13d95224880175cb96c
4
+ data.tar.gz: 5ade2f503013b9b539831e9dfddb830fdd68ca6e8a98762a1a0cd97f2d0d1f63
5
5
  SHA512:
6
- metadata.gz: ca6e0c3b2e2346ab8432dfc24080adcb994f75df453111851d6f8441465d09107d59216fc84efc1f84acf823a4a4ec284c5d1b1212c8ceda9a3d8861e95a2208
7
- data.tar.gz: 39b55072eb24870acd861b8c7ec7c8aa279a9f7b03b4a84f0cdabd4982c2d9f04da1b39b2eea05024d6e3a2c1bc7737ee95557f0eaa588c61ec3fbc5d04af540
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 retry and fallback
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` `en` `es` `pt` `de` `it` `nl` `ru` `mg` `ja` `ko` `zh` `ar`
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
- # => ["fr", "en", "es", "pt", "de", "it", "nl", "ru", "mg", "ja", "ko", "zh", "ar"]
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
- **Description:** Violation de sécurité détectée.
326
+ **Statut:** définie pour usage futur ; la gem ne la lève actuellement nulle part. Inutile de la rattraper.
325
327
 
326
- **Causes courantes:**
328
+ Les contrôles de sécurité existants lèvent d'autres erreurs :
327
329
 
328
- - Texte dépassant la taille limite (50,000 caractères)
329
- - Contenu potentiellement malveillant détecté
330
- - Tentative d'injection de prompt
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
- **Description:** Limite de taux personnalisée dépassée.
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
- **Différence avec `RateLimitError`:**
362
+ **Comportement réel des limites de débit:**
362
363
 
363
- - `RateLimitError` : Limite API Mistral (HTTP 429)
364
- - `RateLimitExceededError` : Limite locale de la gem
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
- begin
378
- # Trop de requêtes locales
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: ["fr", "en", "es"])
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
 
@@ -39,15 +39,21 @@ MistralTranslator.translate_auto("Guten Tag", to: "fr")
39
39
  ## 🌍 Langues Supportées
40
40
 
41
41
  ```ruby
42
- # Vérifier les langues disponibles
42
+ # Les 182 langues ISO 639-1 sont disponibles
43
43
  MistralTranslator::LocaleHelper.supported_locales
44
- # => ["fr", "en", "es", "pt", "de", "it", "nl", "ru", "mg", "ja", "ko", "zh", "ar"]
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
- # Gestion des rich text
47
- if defined?(ActionText::RichText) && value.is_a?(ActionText::RichText)
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
- # Gestion des rich text - préserver le HTML
59
- if defined?(ActionText::RichText) && value.is_a?(ActionText::RichText)
60
- value.body.to_s
61
- else
62
- value.to_s
63
- end
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
- def initialize(record, translatable_fields, adapter: nil, source_locale: nil)
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
- true
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
- def translate_to_locale(field, content, source_locale, target_locale)
210
- translated_content = MistralTranslator.translate(
211
- content,
212
- from: source_locale.to_s,
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
- if translated_content && !translated_content.strip.empty?
217
- @adapter.set_field_value(field, target_locale,
218
- translated_content)
219
- end
220
- rescue MistralTranslator::RateLimitError => e
221
- Rails.logger.warn "Rate limit: #{e.message}" if defined?(Rails)
222
- sleep(2)
223
- retry
224
- rescue StandardError => e
225
- if defined?(Rails)
226
- Rails.logger.error "Translation error for #{field} #{source_locale}->#{target_locale}: #{e.message}"
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
- def complete(prompt, max_tokens: nil, temperature: nil, context: {})
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
- response = make_request_with_retry(prompt, max_tokens, temperature, ctx)
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
- def handle_json_parse_error(error, context)
126
- MistralTranslator.configuration.trigger_translation_error(
127
- context[:from_locale],
128
- context[:to_locale],
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 InvalidResponseError, "Invalid response structure: #{error.message}"
146
+ raise error, cause: nil
148
147
  end
149
148
  end
150
149
  end