mistral_translator 0.1.0 → 0.2.1

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README.md +189 -121
  4. data/README_TESTING.md +33 -0
  5. data/SECURITY.md +157 -0
  6. data/docs/.nojekyll +2 -0
  7. data/docs/404.html +30 -0
  8. data/docs/README.md +153 -0
  9. data/docs/advanced-usage/batch-processing.md +158 -0
  10. data/docs/advanced-usage/error-handling.md +106 -0
  11. data/docs/advanced-usage/monitoring.md +133 -0
  12. data/docs/advanced-usage/summarization.md +86 -0
  13. data/docs/advanced-usage/translations.md +141 -0
  14. data/docs/api-reference/callbacks.md +231 -0
  15. data/docs/api-reference/configuration.md +74 -0
  16. data/docs/api-reference/errors.md +673 -0
  17. data/docs/api-reference/methods.md +539 -0
  18. data/docs/getting-started.md +179 -0
  19. data/docs/index.html +27 -0
  20. data/docs/installation.md +142 -0
  21. data/docs/migration-0.1.0-to-0.2.0.md +61 -0
  22. data/docs/rails-integration/adapters.md +84 -0
  23. data/docs/rails-integration/controllers.md +107 -0
  24. data/docs/rails-integration/jobs.md +97 -0
  25. data/docs/rails-integration/setup.md +339 -0
  26. data/examples/basic_usage.rb +129 -102
  27. data/examples/batch-job.rb +511 -0
  28. data/examples/monitoring-setup.rb +499 -0
  29. data/examples/rails-model.rb +399 -0
  30. data/lib/mistral_translator/adapters.rb +261 -0
  31. data/lib/mistral_translator/client.rb +103 -100
  32. data/lib/mistral_translator/client_helpers.rb +161 -0
  33. data/lib/mistral_translator/configuration.rb +171 -1
  34. data/lib/mistral_translator/errors.rb +16 -0
  35. data/lib/mistral_translator/helpers.rb +292 -0
  36. data/lib/mistral_translator/helpers_extensions.rb +150 -0
  37. data/lib/mistral_translator/levenshtein_helpers.rb +40 -0
  38. data/lib/mistral_translator/logger.rb +28 -4
  39. data/lib/mistral_translator/prompt_builder.rb +93 -41
  40. data/lib/mistral_translator/prompt_helpers.rb +83 -0
  41. data/lib/mistral_translator/prompt_metadata_helpers.rb +42 -0
  42. data/lib/mistral_translator/response_parser.rb +194 -23
  43. data/lib/mistral_translator/security.rb +72 -0
  44. data/lib/mistral_translator/summarizer.rb +41 -2
  45. data/lib/mistral_translator/translator.rb +174 -98
  46. data/lib/mistral_translator/translator_helpers.rb +268 -0
  47. data/lib/mistral_translator/version.rb +1 -1
  48. data/lib/mistral_translator.rb +51 -25
  49. metadata +39 -3
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Module de sécurité optionnel - chargé seulement si nécessaire
4
+ module MistralTranslator
5
+ module Security
6
+ # Validation basique des entrées (version légère)
7
+ module BasicValidator
8
+ MAX_TEXT_LENGTH = 50_000
9
+ MIN_TEXT_LENGTH = 1
10
+
11
+ class << self
12
+ def validate_text!(text)
13
+ # Accepter nil et texte vide - ce sont des cas d'usage légitimes
14
+ return "" if text.nil? || text.empty?
15
+
16
+ text_str = text.to_s
17
+ return "" if text_str.strip.empty?
18
+
19
+ raise ArgumentError, "Text too long (max #{MAX_TEXT_LENGTH} chars)" if text_str.length > MAX_TEXT_LENGTH
20
+
21
+ text_str
22
+ end
23
+
24
+ def validate_batch!(texts)
25
+ raise ArgumentError, "Batch cannot be nil" if texts.nil?
26
+ raise ArgumentError, "Batch must be an array" unless texts.is_a?(Array)
27
+ raise ArgumentError, "Batch cannot be empty" if texts.empty?
28
+ raise ArgumentError, "Batch too large (max 20 items)" if texts.size > 20
29
+
30
+ texts.each { |text| validate_text!(text) }
31
+ texts
32
+ end
33
+ end
34
+ end
35
+
36
+ # Rate limiter basique (version légère)
37
+ class BasicRateLimiter
38
+ def initialize(max_requests: 50, window_seconds: 60)
39
+ @max_requests = max_requests
40
+ @window_seconds = window_seconds
41
+ @requests = []
42
+ @mutex = Mutex.new
43
+ end
44
+
45
+ def wait_and_record!
46
+ @mutex.synchronize do
47
+ cleanup_old_requests
48
+ if @requests.size >= @max_requests
49
+ wait_time = calculate_wait_time
50
+ sleep(wait_time) if wait_time.positive?
51
+ end
52
+ @requests << Time.now
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def cleanup_old_requests
59
+ cutoff_time = Time.now - @window_seconds
60
+ @requests.reject! { |request_time| request_time < cutoff_time }
61
+ end
62
+
63
+ def calculate_wait_time
64
+ return 0 if @requests.empty?
65
+
66
+ oldest_request = @requests.min
67
+ time_until_oldest_expires = (oldest_request + @window_seconds) - Time.now
68
+ [time_until_oldest_expires, 0].max
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "logger"
4
+ require_relative "security"
4
5
 
5
6
  module MistralTranslator
6
7
  class Summarizer
@@ -16,12 +17,18 @@ module MistralTranslator
16
17
  # Résumé simple dans une langue donnée
17
18
  def summarize(text, language: "fr", max_words: DEFAULT_MAX_WORDS)
18
19
  log_debug("Starting summarize - language: #{language}, max_words: #{max_words}")
19
- validate_summarize_inputs!(text, language, max_words)
20
+
21
+ # Validation basique des entrées
22
+ validated_text = Security::BasicValidator.validate_text!(text)
23
+ raise ArgumentError, "Max words must be positive" unless max_words.is_a?(Integer) && max_words.positive?
24
+
25
+ # Si le texte est vide, retourner directement une chaîne vide
26
+ return "" if validated_text.empty?
20
27
 
21
28
  target_locale = LocaleHelper.validate_locale!(language)
22
29
  log_debug("Target locale validated: #{target_locale}")
23
30
 
24
- cleaned_text = clean_document_content(text)
31
+ cleaned_text = clean_document_content(validated_text)
25
32
  log_debug("Text cleaned, length: #{cleaned_text&.length}")
26
33
 
27
34
  result = summarize_with_retry(cleaned_text, target_locale, max_words)
@@ -50,6 +57,21 @@ module MistralTranslator
50
57
  raw_response = @client.complete(prompt)
51
58
 
52
59
  result = ResponseParser.parse_summary_response(raw_response)
60
+ if result.nil? || result[:summary].nil? || result[:summary].empty?
61
+ # Fallback: si le JSON est manquant ou vide, on tente un résumé simple puis traduction
62
+ log_error(
63
+ "Empty or invalid summary response for summarize_and_translate. Using fallback. " \
64
+ "Raw length: #{raw_response.to_s.length}"
65
+ )
66
+ summary_only = summarize(cleaned_text, language: source_locale, max_words: max_words)
67
+ begin
68
+ return MistralTranslator.translate(summary_only, from: source_locale, to: target_locale)
69
+ rescue EmptyTranslationError, InvalidResponseError, RateLimitError => e
70
+ log_error("Translation fallback failed: #{e.class.name} - returning source summary")
71
+ return summary_only
72
+ end
73
+ end
74
+
53
75
  result[:summary]
54
76
  end
55
77
 
@@ -95,14 +117,20 @@ module MistralTranslator
95
117
 
96
118
  private
97
119
 
120
+ # rubocop:disable Metrics/PerceivedComplexity
98
121
  def summarize_with_retry(text, target_locale, max_words, attempt = 0)
99
122
  log_debug("Summarize attempt #{attempt + 1} for #{target_locale}")
100
123
 
101
124
  prompt = PromptBuilder.summary_prompt(text, max_words, target_locale)
102
125
  raw_response = @client.complete(prompt)
126
+ response_len = raw_response&.length || 0
103
127
 
104
128
  result = ResponseParser.parse_summary_response(raw_response)
105
129
  if result.nil? || result[:summary].nil? || result[:summary].empty?
130
+ log_error(
131
+ "Empty or invalid summary response for #{target_locale}. " \
132
+ "Raw response length: #{response_len}"
133
+ )
106
134
  raise EmptyTranslationError, "Empty summary received"
107
135
  end
108
136
 
@@ -123,6 +151,7 @@ module MistralTranslator
123
151
  sleep(DEFAULT_RETRY_DELAY)
124
152
  retry
125
153
  end
154
+ # rubocop:enable Metrics/PerceivedComplexity
126
155
 
127
156
  def build_summary_translation_prompt(text, source_locale, target_locale, max_words)
128
157
  source_name = LocaleHelper.locale_to_language(source_locale)
@@ -222,5 +251,15 @@ module MistralTranslator
222
251
 
223
252
  puts "[MistralTranslator] #{message}"
224
253
  end
254
+
255
+ def log_error(message)
256
+ # Utiliser le logger centralisé
257
+ Logger.warn(message, sensitive: false)
258
+
259
+ # Pour les tests, optionnellement afficher en stdout
260
+ return unless ENV["MISTRAL_TRANSLATOR_TEST_OUTPUT"] == "true"
261
+
262
+ puts "[MistralTranslator] ERROR: #{message}"
263
+ end
225
264
  end
226
265
  end
@@ -1,9 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "logger"
4
+ require_relative "translator_helpers"
5
+ require_relative "security"
4
6
 
5
7
  module MistralTranslator
6
8
  class Translator
9
+ include TranslatorHelpers::InputValidator
10
+ include TranslatorHelpers::RetryHandler
11
+ include TranslatorHelpers::LoggingHelper
12
+ include TranslatorHelpers::PromptHandler
13
+ include TranslatorHelpers::AnalysisHelper
14
+ include TranslatorHelpers::RequestHelper
15
+ include TranslatorHelpers::MultiTargetHelper
16
+
7
17
  DEFAULT_RETRY_COUNT = 3
8
18
  DEFAULT_RETRY_DELAY = 2
9
19
 
@@ -12,50 +22,77 @@ module MistralTranslator
12
22
  end
13
23
 
14
24
  # Traduction simple d'un texte vers une langue
15
- def translate(text, from:, to:)
16
- validate_inputs!(text, from, to)
25
+ def translate(text, from:, to:, **options)
26
+ # Validation basique des entrées
27
+ validated_text = Security::BasicValidator.validate_text!(text)
28
+
29
+ # Si le texte est vide, retourner directement une chaîne vide
30
+ return "" if validated_text.empty?
17
31
 
18
32
  source_locale = LocaleHelper.validate_locale!(from)
19
33
  target_locale = LocaleHelper.validate_locale!(to)
20
34
 
21
- translate_with_retry(text, source_locale, target_locale)
35
+ # Si même langue source et cible, retourner le texte original
36
+ return validated_text if source_locale == target_locale
37
+
38
+ translate_with_retry(validated_text, source_locale, target_locale,
39
+ context: options[:context],
40
+ glossary: options[:glossary],
41
+ preserve_html: options.fetch(:preserve_html, false))
42
+ end
43
+
44
+ # Traduction avec score de confiance (expérimental)
45
+ def translate_with_confidence(text, from:, to:, context: nil, glossary: nil)
46
+ result = begin
47
+ translate(text, from: from, to: to, context: context, glossary: glossary)
48
+ rescue EmptyTranslationError
49
+ ""
50
+ end
51
+
52
+ # Score de confiance basique basé sur la longueur et la cohérence
53
+ confidence = calculate_confidence_score(text, result, from, to)
54
+
55
+ {
56
+ translation: result,
57
+ confidence: confidence,
58
+ metadata: {
59
+ source_locale: from,
60
+ target_locale: to,
61
+ original_length: text.length,
62
+ translated_length: result.length
63
+ }
64
+ }
22
65
  end
23
66
 
24
- # Traduction vers plusieurs langues
25
- def translate_to_multiple(text, from:, to:)
67
+ # Traduction vers plusieurs langues avec support du batch
68
+ def translate_to_multiple(text, from:, to:, **options)
26
69
  validate_translation_inputs!(text, from, to)
27
70
 
28
71
  source_locale = LocaleHelper.validate_locale!(from)
29
72
  target_locales = Array(to).map { |locale| LocaleHelper.validate_locale!(locale) }
30
73
 
31
- results = {}
32
-
33
- target_locales.each_with_index do |target_locale, index|
34
- # Délai entre les requêtes, mais pas avant la première
35
- sleep(DEFAULT_RETRY_DELAY) if index.positive?
36
- results[target_locale] = translate_with_retry(text, source_locale, target_locale)
74
+ if options[:use_batch] && target_locales.size > 3
75
+ translate_to_multiple_batch(text, source_locale, target_locales, context: options[:context],
76
+ glossary: options[:glossary])
77
+ else
78
+ translate_to_multiple_sequential(text, source_locale, target_locales, context: options[:context],
79
+ glossary: options[:glossary])
37
80
  end
38
-
39
- results
40
81
  end
41
82
 
42
83
  # Traduction en lot (plusieurs textes vers une langue)
43
- def translate_batch(texts, from:, to:)
44
- validate_batch_inputs!(texts, from, to)
45
-
84
+ def translate_batch(texts, from:, to:, context: nil, glossary: nil)
85
+ validated_texts = Security::BasicValidator.validate_batch!(texts)
46
86
  source_locale = LocaleHelper.validate_locale!(from)
47
87
  target_locale = LocaleHelper.validate_locale!(to)
48
88
 
49
- # Pour des lots importants, on peut les découper
50
- if texts.length > 10
51
- translate_large_batch(texts, source_locale, target_locale)
52
- else
53
- translate_small_batch(texts, source_locale, target_locale)
54
- end
89
+ return handle_same_language_batch(validated_texts) if source_locale == target_locale
90
+
91
+ process_mixed_batch(validated_texts, source_locale, target_locale, context, glossary)
55
92
  end
56
93
 
57
- # Auto-détection de la langue source (utilise l'API pour détecter)
58
- def translate_auto(text, to:)
94
+ # Auto-détection de la langue source avec support du contexte
95
+ def translate_auto(text, to:, context: nil, glossary: nil)
59
96
  target_locale = LocaleHelper.validate_locale!(to)
60
97
 
61
98
  # Premier appel pour détecter la langue
@@ -63,115 +100,154 @@ module MistralTranslator
63
100
  detection_response = @client.complete(detection_prompt)
64
101
  detected_language = parse_language_detection(detection_response)
65
102
 
66
- # Puis traduction normale
67
- translate(text, from: detected_language, to: target_locale)
103
+ # Vérifier que la langue détectée est différente de la cible
104
+ if detected_language == target_locale
105
+ # Si même langue, retourner le texte original
106
+ return text
107
+ end
108
+
109
+ # Puis traduction normale avec contexte
110
+ translate(text, from: detected_language, to: target_locale, context: context, glossary: glossary)
68
111
  end
69
112
 
70
113
  private
71
114
 
72
- def translate_with_retry(text, source_locale, target_locale, attempt = 0)
73
- prompt = PromptBuilder.translation_prompt(text, source_locale, target_locale)
74
- raw_response = @client.complete(prompt)
115
+ # Ces méthodes sont héritées via MultiTargetHelper
116
+
117
+ def handle_same_language_batch(validated_texts)
118
+ validated_texts.each_with_index.to_h { |text, index| [index, text] }
119
+ end
75
120
 
76
- result = ResponseParser.parse_translation_response(raw_response)
77
- raise EmptyTranslationError if result.nil? || result[:translated].nil?
121
+ def process_mixed_batch(validated_texts, source_locale, target_locale, context, glossary)
122
+ results = {}
123
+ batch_config = { source_locale: source_locale, target_locale: target_locale, context: context,
124
+ glossary: glossary }
125
+ requests = build_batch_requests(validated_texts, batch_config, results)
78
126
 
79
- result[:translated]
80
- rescue EmptyTranslationError, InvalidResponseError => e
81
- raise e unless attempt < DEFAULT_RETRY_COUNT
127
+ process_batch_requests(requests, results) if requests.any?
82
128
 
83
- wait_time = DEFAULT_RETRY_DELAY * (2**attempt) # Backoff exponentiel
84
- log_retry(e, attempt + 1, wait_time)
85
- sleep(wait_time)
86
- translate_with_retry(text, source_locale, target_locale, attempt + 1)
87
- rescue RateLimitError => e
88
- log_rate_limit_hit(source_locale, target_locale)
89
- sleep(DEFAULT_RETRY_DELAY)
90
- retry
129
+ results
91
130
  end
92
131
 
93
- def translate_small_batch(texts, source_locale, target_locale)
94
- prompt = PromptBuilder.bulk_translation_prompt(texts, source_locale, target_locale)
95
- raw_response = @client.complete(prompt)
132
+ def build_batch_requests(validated_texts, batch_config, results)
133
+ requests = []
134
+
135
+ validated_texts.each_with_index do |text, index|
136
+ if text.nil? || text.empty?
137
+ results[index] = ""
138
+ else
139
+ requests << create_batch_request(text, batch_config, index)
140
+ end
141
+ end
96
142
 
97
- results = ResponseParser.parse_bulk_translation_response(raw_response)
143
+ requests
144
+ end
145
+
146
+ def create_batch_request(text, batch_config, index)
147
+ {
148
+ prompt: build_translation_prompt(text, batch_config[:source_locale], batch_config[:target_locale],
149
+ context: batch_config[:context], glossary: batch_config[:glossary]),
150
+ from: batch_config[:source_locale],
151
+ to: batch_config[:target_locale],
152
+ index: index,
153
+ original_text: text
154
+ }
155
+ end
98
156
 
99
- # Retourner un hash indexé par l'ordre original
100
- results.each_with_object({}) do |result, hash|
101
- original_index = result[:index] - 1 # L'API retourne 1-indexed
102
- hash[original_index] = result[:translated]
157
+ def process_batch_requests(requests, results)
158
+ batch_results = @client.translate_batch(requests, batch_size: 10)
159
+ batch_results.each do |result|
160
+ process_batch_result(result, results)
103
161
  end
104
162
  end
105
163
 
106
- def translate_large_batch(texts, source_locale, target_locale)
107
- results = {}
164
+ def process_batch_result(result, results)
165
+ index = result[:original_request][:index]
166
+ results[index] = (parse_batch_response(result[:result]) if result[:success])
167
+ end
108
168
 
109
- texts.each_slice(10).with_index do |batch, batch_index|
110
- sleep(DEFAULT_RETRY_DELAY) if batch_index.positive? # Délai entre les batches
169
+ def parse_batch_response(response)
170
+ parsed_result = ResponseParser.parse_translation_response(response)
171
+ parsed_result[:translated] if parsed_result
172
+ rescue EmptyTranslationError
173
+ ""
174
+ end
111
175
 
112
- batch_results = translate_small_batch(batch, source_locale, target_locale)
176
+ def translate_with_retry(text, source_locale, target_locale, attempt = 0, **options)
177
+ prompt = build_prompt_for_retry(text, source_locale, target_locale, **options)
178
+ request_context = build_request_context(source_locale, target_locale, attempt)
113
179
 
114
- # Ajuster les index pour le batch
115
- batch_results.each do |local_index, translation|
116
- global_index = (batch_index * 10) + local_index
117
- results[global_index] = translation
118
- end
180
+ raw_response = perform_client_request(prompt, request_context)
181
+ extract_translated_text!(raw_response)
182
+ rescue EmptyTranslationError, InvalidResponseError => e
183
+ handle_retryable_error!(e, attempt) do
184
+ translate_with_retry(text, source_locale, target_locale, attempt + 1, **options)
119
185
  end
120
-
121
- results
186
+ rescue RateLimitError
187
+ log_rate_limit_hit(source_locale, target_locale)
188
+ sleep(DEFAULT_RETRY_DELAY)
189
+ retry
122
190
  end
123
191
 
124
- def build_language_detection_prompt(text)
125
- PromptBuilder.language_detection_prompt(text)
126
- end
192
+ # Ces méthodes sont héritées via RequestHelper
127
193
 
128
- def parse_language_detection(response)
129
- json_content = response.match(/\{.*\}/m)&.[](0)
130
- return "en" unless json_content # Défaut en anglais si détection échoue
194
+ # Log via LoggingHelper
131
195
 
132
- data = JSON.parse(json_content)
133
- detected = data["detected_language"]
196
+ def build_translation_prompt(text, source_locale, target_locale, **options)
197
+ base_prompt = PromptBuilder.translation_prompt(text, source_locale, target_locale,
198
+ preserve_html: options.fetch(:preserve_html, false))
134
199
 
135
- LocaleHelper.locale_supported?(detected) ? detected : "en"
136
- rescue JSON::ParserError
137
- "en" # Défaut en anglais si parsing échoue
200
+ # Enrichir le prompt avec le contexte et le glossaire
201
+ context_present = options[:context] && !options[:context].to_s.strip.empty?
202
+ glossary_present =
203
+ (options[:glossary].is_a?(Hash) && options[:glossary].any?) ||
204
+ (options[:glossary].is_a?(String) && !options[:glossary].to_s.strip.empty?)
205
+ if context_present || glossary_present
206
+ enrich_prompt_with_context(base_prompt, options[:context], options[:glossary])
207
+ else
208
+ base_prompt
209
+ end
138
210
  end
139
211
 
140
- def validate_inputs!(text, from, to)
141
- raise ArgumentError, "Text cannot be nil or empty" if text.nil? || text.empty?
142
- raise ArgumentError, "Source language cannot be nil" if from.nil?
143
- raise ArgumentError, "Target language cannot be nil" if to.nil?
144
- raise ArgumentError, "Source and target languages cannot be the same" if from == to
212
+ def enrich_prompt_with_context(base_prompt, context, glossary)
213
+ enriched_parts = []
214
+ context_part = build_context_enrichment(context)
215
+ glossary_part = build_glossary_enrichment(glossary)
216
+ enriched_parts << context_part if context_part
217
+ enriched_parts << glossary_part if glossary_part
218
+
219
+ return base_prompt if enriched_parts.empty?
220
+
221
+ enriched_context = enriched_parts.join("\n")
222
+ base_prompt.sub("RÈGLES :", "#{enriched_context}\n\nRÈGLES :")
145
223
  end
146
224
 
147
- def validate_translation_inputs!(text, from, to)
148
- # Convertir to en array pour la validation
149
- target_languages = Array(to)
150
- raise ArgumentError, "Target languages cannot be empty" if target_languages.empty?
225
+ def build_context_enrichment(context)
226
+ return nil if context.nil? || context.to_s.strip.empty?
151
227
 
152
- validate_inputs!(text, from, target_languages.first)
228
+ "CONTEXTE : #{context}"
153
229
  end
154
230
 
155
- def validate_batch_inputs!(texts, from, to)
156
- raise ArgumentError, "Texts array cannot be nil or empty" if texts.nil? || texts.empty?
157
- raise ArgumentError, "Source language cannot be nil" if from.nil?
158
- raise ArgumentError, "Target language cannot be nil" if to.nil?
231
+ def build_glossary_enrichment(glossary)
232
+ return nil if glossary.nil?
233
+ return build_glossary_hash(glossary) if glossary.is_a?(Hash)
234
+ return build_glossary_string(glossary) if glossary.is_a?(String)
159
235
 
160
- texts.each_with_index do |text, index|
161
- raise ArgumentError, "Text at index #{index} cannot be nil or empty" if text.nil? || text.empty?
162
- end
236
+ nil
163
237
  end
164
238
 
165
- def log_retry(error, attempt, wait_time)
166
- message = "#{error.class.name}: #{error.message}. Retry #{attempt}/#{DEFAULT_RETRY_COUNT} in #{wait_time}s"
167
- # Log une seule fois par type d'erreur pour éviter le spam
168
- Logger.warn_once(message, key: "retry_#{error.class.name}", sensitive: false, ttl: 120)
239
+ def build_glossary_hash(glossary_hash)
240
+ return nil if glossary_hash.empty?
241
+
242
+ glossary_text = glossary_hash.map { |key, value| "#{key} → #{value}" }.join(", ")
243
+ "GLOSSAIRE (à respecter strictement) : #{glossary_text}"
169
244
  end
170
245
 
171
- def log_rate_limit_hit(source, target)
172
- message = "Rate limit hit for translation #{source} -> #{target}, retrying..."
173
- # Log une seule fois par paire de langues
174
- Logger.warn_once(message, key: "rate_limit_#{source}_#{target}", sensitive: false, ttl: 300)
246
+ def build_glossary_string(glossary_string)
247
+ value = glossary_string.to_s.strip
248
+ return nil if value.empty?
249
+
250
+ "GLOSSAIRE : #{value}"
175
251
  end
176
252
  end
177
253
  end