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.
@@ -9,7 +9,6 @@ module MistralTranslator
9
9
  include TranslatorHelpers::InputValidator
10
10
  include TranslatorHelpers::RetryHandler
11
11
  include TranslatorHelpers::LoggingHelper
12
- include TranslatorHelpers::PromptHandler
13
12
  include TranslatorHelpers::AnalysisHelper
14
13
  include TranslatorHelpers::RequestHelper
15
14
  include TranslatorHelpers::MultiTargetHelper
@@ -67,47 +66,49 @@ module MistralTranslator
67
66
  # Traduction vers plusieurs langues avec support du batch
68
67
  def translate_to_multiple(text, from:, to:, **options)
69
68
  validate_translation_inputs!(text, from, to)
69
+ Security::BasicValidator.validate_text!(text)
70
70
 
71
71
  source_locale = LocaleHelper.validate_locale!(from)
72
72
  target_locales = Array(to).map { |locale| LocaleHelper.validate_locale!(locale) }
73
73
 
74
+ translation_options = { context: options[:context], glossary: options[:glossary] }
75
+ translation_options[:preserve_html] = true if options[:preserve_html]
76
+
74
77
  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])
78
+ translate_to_multiple_batch(text, source_locale, target_locales, **translation_options)
77
79
  else
78
- translate_to_multiple_sequential(text, source_locale, target_locales, context: options[:context],
79
- glossary: options[:glossary])
80
+ translate_to_multiple_sequential(text, source_locale, target_locales, **translation_options)
80
81
  end
81
82
  end
82
83
 
83
84
  # Traduction en lot (plusieurs textes vers une langue)
84
- def translate_batch(texts, from:, to:, context: nil, glossary: nil)
85
+ def translate_batch(texts, from:, to:, context: nil, glossary: nil, preserve_html: false) # rubocop:disable Metrics/ParameterLists
85
86
  validated_texts = Security::BasicValidator.validate_batch!(texts)
86
87
  source_locale = LocaleHelper.validate_locale!(from)
87
88
  target_locale = LocaleHelper.validate_locale!(to)
88
89
 
89
90
  return handle_same_language_batch(validated_texts) if source_locale == target_locale
90
91
 
91
- process_mixed_batch(validated_texts, source_locale, target_locale, context, glossary)
92
+ batch_config = { source_locale: source_locale, target_locale: target_locale, context: context,
93
+ glossary: glossary, preserve_html: preserve_html }
94
+ process_mixed_batch(validated_texts, batch_config)
92
95
  end
93
96
 
94
97
  # Auto-détection de la langue source avec support du contexte
95
- def translate_auto(text, to:, context: nil, glossary: nil)
98
+ def translate_auto(text, to:, context: nil, glossary: nil, preserve_html: false)
99
+ validated_text = Security::BasicValidator.validate_text!(text)
100
+ return "" if validated_text.empty?
101
+
96
102
  target_locale = LocaleHelper.validate_locale!(to)
97
103
 
98
- # Premier appel pour détecter la langue
99
- detection_prompt = build_language_detection_prompt(text)
100
- detection_response = @client.complete(detection_prompt)
104
+ # Premier appel pour détecter la langue ; un échec lève une erreur
105
+ detection_response = @client.complete(build_language_detection_prompt(validated_text), json: true)
101
106
  detected_language = parse_language_detection(detection_response)
102
-
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
107
+ return validated_text if detected_language == target_locale
108
108
 
109
109
  # Puis traduction normale avec contexte
110
- translate(text, from: detected_language, to: target_locale, context: context, glossary: glossary)
110
+ translate(validated_text, from: detected_language, to: target_locale, context: context, glossary: glossary,
111
+ preserve_html: preserve_html)
111
112
  end
112
113
 
113
114
  private
@@ -118,10 +119,8 @@ module MistralTranslator
118
119
  validated_texts.each_with_index.to_h { |text, index| [index, text] }
119
120
  end
120
121
 
121
- def process_mixed_batch(validated_texts, source_locale, target_locale, context, glossary)
122
+ def process_mixed_batch(validated_texts, batch_config)
122
123
  results = {}
123
- batch_config = { source_locale: source_locale, target_locale: target_locale, context: context,
124
- glossary: glossary }
125
124
  requests = build_batch_requests(validated_texts, batch_config, results)
126
125
 
127
126
  process_batch_requests(requests, results) if requests.any?
@@ -146,7 +145,8 @@ module MistralTranslator
146
145
  def create_batch_request(text, batch_config, index)
147
146
  {
148
147
  prompt: build_translation_prompt(text, batch_config[:source_locale], batch_config[:target_locale],
149
- context: batch_config[:context], glossary: batch_config[:glossary]),
148
+ context: batch_config[:context], glossary: batch_config[:glossary],
149
+ preserve_html: batch_config[:preserve_html]),
150
150
  from: batch_config[:source_locale],
151
151
  to: batch_config[:target_locale],
152
152
  index: index,
@@ -171,6 +171,9 @@ module MistralTranslator
171
171
  parsed_result[:translated] if parsed_result
172
172
  rescue EmptyTranslationError
173
173
  ""
174
+ rescue InvalidResponseError
175
+ # Une réponse illisible fait échouer son élément, pas tout le lot
176
+ nil
174
177
  end
175
178
 
176
179
  def translate_with_retry(text, source_locale, target_locale, attempt = 0, **options)
@@ -184,9 +187,12 @@ module MistralTranslator
184
187
  translate_with_retry(text, source_locale, target_locale, attempt + 1, **options)
185
188
  end
186
189
  rescue RateLimitError
190
+ # Le client a déjà relancé avec backoff : quelques tentatives de plus suffisent.
191
+ raise if attempt >= DEFAULT_RETRY_COUNT
192
+
187
193
  log_rate_limit_hit(source_locale, target_locale)
188
194
  sleep(DEFAULT_RETRY_DELAY)
189
- retry
195
+ translate_with_retry(text, source_locale, target_locale, attempt + 1, **options)
190
196
  end
191
197
 
192
198
  # Ces méthodes sont héritées via RequestHelper
@@ -194,60 +200,10 @@ module MistralTranslator
194
200
  # Log via LoggingHelper
195
201
 
196
202
  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))
199
-
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
210
- end
211
-
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 :")
223
- end
224
-
225
- def build_context_enrichment(context)
226
- return nil if context.nil? || context.to_s.strip.empty?
227
-
228
- "CONTEXTE : #{context}"
229
- end
230
-
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)
235
-
236
- nil
237
- end
238
-
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}"
244
- end
245
-
246
- def build_glossary_string(glossary_string)
247
- value = glossary_string.to_s.strip
248
- return nil if value.empty?
249
-
250
- "GLOSSAIRE : #{value}"
203
+ PromptBuilder.translation_prompt(text, source_locale, target_locale,
204
+ context: options[:context],
205
+ glossary: options[:glossary],
206
+ preserve_html: options.fetch(:preserve_html, false))
251
207
  end
252
208
  end
253
209
  end
@@ -91,34 +91,6 @@ module MistralTranslator
91
91
  end
92
92
  end
93
93
 
94
- # Helper pour la gestion des prompts
95
- module PromptHandler
96
- def enrich_prompt_with_context(base_prompt, context, glossary)
97
- context_section = build_context_section(context, glossary)
98
- base_prompt + context_section
99
- end
100
-
101
- def build_context_section(context, glossary)
102
- context_section = ""
103
- context_section += add_context_text(context) if context && !context.to_s.strip.empty?
104
- context_section += add_glossary_text(glossary) if glossary && !glossary.to_s.strip.empty?
105
- context_section
106
- end
107
-
108
- def add_context_text(context)
109
- "\nCONTEXTE : #{context}\n"
110
- end
111
-
112
- def add_glossary_text(glossary)
113
- if glossary.is_a?(Hash) && glossary.any?
114
- glossary_text = glossary.map { |key, value| "#{key} → #{value}" }.join(", ")
115
- "\nGLOSSAIRE (à respecter strictement) : #{glossary_text}\n"
116
- else
117
- "\nGLOSSAIRE : #{glossary}\n"
118
- end
119
- end
120
- end
121
-
122
94
  # Helper pour l'analyse et la détection de langue
123
95
  module AnalysisHelper
124
96
  def calculate_confidence_score(original, translated, from_locale, to_locale)
@@ -152,16 +124,17 @@ module MistralTranslator
152
124
  PromptBuilder.language_detection_prompt(text)
153
125
  end
154
126
 
127
+ # Fail-closed : une détection illisible ou une langue non supportée lève une erreur,
128
+ # au lieu de supposer une langue et de renvoyer le texte non traduit.
155
129
  def parse_language_detection(response)
156
- json_content = response.match(/\{.*\}/m)&.[](0)
157
- return "en" unless json_content
158
-
159
- data = JSON.parse(json_content)
160
- detected = data.dig("metadata", "detected_language")
130
+ data = JSON.parse(response.to_s)
131
+ metadata = data["metadata"] if data.is_a?(Hash)
132
+ detected = LocaleHelper.normalize_locale(metadata["detected_language"]) if metadata.is_a?(Hash)
133
+ return detected if detected && LocaleHelper.locale_supported?(detected)
161
134
 
162
- LocaleHelper.locale_supported?(detected) ? detected : "en"
135
+ raise InvalidResponseError.new("Language detection failed", response)
163
136
  rescue JSON::ParserError
164
- "en"
137
+ raise InvalidResponseError.new("Language detection failed", response), cause: nil
165
138
  end
166
139
  end
167
140
 
@@ -187,31 +160,13 @@ module MistralTranslator
187
160
  end
188
161
 
189
162
  def perform_client_request(prompt, request_context)
190
- if @client.respond_to?(:complete)
191
- begin
192
- return @client.complete(prompt, context: request_context)
193
- rescue StandardError => e
194
- raise e unless e.class.name.include?("MockExpectationError") && @client.respond_to?(:make_request)
195
-
196
- return @client.make_request(prompt, nil, nil)
197
- end
198
- end
199
-
200
- return @client.make_request(prompt, nil, nil) if @client.respond_to?(:make_request)
201
-
202
- @client.complete(prompt, context: request_context)
163
+ @client.complete(prompt, context: request_context, json: true)
203
164
  end
204
165
 
166
+ # Une réponse inexploitable lève une erreur : jamais de réponse brute enregistrée comme traduction.
205
167
  def extract_translated_text!(raw_response)
206
- begin
207
- result = ResponseParser.parse_translation_response(raw_response)
208
- return result[:translated] if result && !result[:translated].to_s.empty?
209
- rescue EmptyTranslationError, InvalidResponseError
210
- # Fallback: accepter une réponse texte brute non vide
211
- fallback_text = raw_response.to_s.strip
212
- return fallback_text unless fallback_text.empty?
213
- # sinon on relance l'erreur plus bas
214
- end
168
+ result = ResponseParser.parse_translation_response(raw_response)
169
+ return result[:translated] if result && !result[:translated].to_s.empty?
215
170
 
216
171
  raise EmptyTranslationError
217
172
  end
@@ -228,10 +183,14 @@ module MistralTranslator
228
183
 
229
184
  # Helper pour traductions multi-cibles
230
185
  module MultiTargetHelper
231
- def translate_to_multiple_batch(text, source_locale, target_locales, context: nil, glossary: nil)
186
+ DEFAULT_MULTI_TARGET_OPTIONS = { context: nil, glossary: nil }.freeze
187
+
188
+ # options : context, glossary, preserve_html
189
+ def translate_to_multiple_batch(text, source_locale, target_locales, **options)
190
+ options = DEFAULT_MULTI_TARGET_OPTIONS.merge(options)
232
191
  requests = target_locales.map do |target_locale|
233
192
  {
234
- prompt: build_translation_prompt(text, source_locale, target_locale, context: context, glossary: glossary),
193
+ prompt: build_translation_prompt(text, source_locale, target_locale, **options),
235
194
  from: source_locale,
236
195
  to: target_locale,
237
196
  original_text: text
@@ -244,21 +203,28 @@ module MistralTranslator
244
203
  batch_results.each do |result|
245
204
  next unless result[:success]
246
205
 
247
- target_locale = result[:original_request][:to]
248
- parsed_result = ResponseParser.parse_translation_response(result[:result])
249
- results[target_locale] = parsed_result[:translated] if parsed_result
206
+ translated = parse_batch_translation(result[:result])
207
+ results[result[:original_request][:to]] = translated if translated
250
208
  end
251
209
 
252
210
  results
253
211
  end
254
212
 
255
- def translate_to_multiple_sequential(text, source_locale, target_locales, context: nil, glossary: nil)
213
+ # Une réponse illisible fait échouer sa langue, pas tout le lot
214
+ def parse_batch_translation(response)
215
+ ResponseParser.parse_translation_response(response)&.fetch(:translated)
216
+ rescue EmptyTranslationError, InvalidResponseError
217
+ nil
218
+ end
219
+
220
+ def translate_to_multiple_sequential(text, source_locale, target_locales, **options)
221
+ options = DEFAULT_MULTI_TARGET_OPTIONS.merge(options)
256
222
  results = {}
257
223
 
258
224
  target_locales.each_with_index do |target_locale, index|
259
225
  sleep(MistralTranslator::Translator::DEFAULT_RETRY_DELAY) if index.positive?
260
226
  results[target_locale] =
261
- translate_with_retry(text, source_locale, target_locale, context: context, glossary: glossary)
227
+ translate_with_retry(text, source_locale, target_locale, **options)
262
228
  end
263
229
 
264
230
  results
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MistralTranslator
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
 
6
6
  # Informations additionnelles sur la gem
7
7
  API_VERSION = "v1"
@@ -107,7 +107,7 @@ module MistralTranslator
107
107
 
108
108
  # Health check
109
109
  def health_check
110
- client.complete("Hello", max_tokens: 10, context: {})
110
+ client.chat("Hello", max_tokens: 10)
111
111
  { status: :ok, message: "API connection successful" }
112
112
  rescue AuthenticationError
113
113
  { status: :error, message: "Authentication failed - check your API key" }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mistral_translator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pierre Cherbero
@@ -159,15 +159,18 @@ files:
159
159
  - lib/mistral_translator/client.rb
160
160
  - lib/mistral_translator/client_helpers.rb
161
161
  - lib/mistral_translator/configuration.rb
162
+ - lib/mistral_translator/data/languages.json
162
163
  - lib/mistral_translator/errors.rb
163
164
  - lib/mistral_translator/helpers.rb
164
165
  - lib/mistral_translator/helpers_extensions.rb
165
166
  - lib/mistral_translator/levenshtein_helpers.rb
166
167
  - lib/mistral_translator/locale_helper.rb
167
168
  - lib/mistral_translator/logger.rb
169
+ - lib/mistral_translator/prompt.rb
168
170
  - lib/mistral_translator/prompt_builder.rb
169
171
  - lib/mistral_translator/prompt_helpers.rb
170
172
  - lib/mistral_translator/prompt_metadata_helpers.rb
173
+ - lib/mistral_translator/proxy.rb
171
174
  - lib/mistral_translator/response_parser.rb
172
175
  - lib/mistral_translator/security.rb
173
176
  - lib/mistral_translator/summarizer.rb