mistral_translator 0.3.2 → 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,6 +66,7 @@ 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) }
@@ -82,33 +82,33 @@ module MistralTranslator
82
82
  end
83
83
 
84
84
  # Traduction en lot (plusieurs textes vers une langue)
85
- 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
86
86
  validated_texts = Security::BasicValidator.validate_batch!(texts)
87
87
  source_locale = LocaleHelper.validate_locale!(from)
88
88
  target_locale = LocaleHelper.validate_locale!(to)
89
89
 
90
90
  return handle_same_language_batch(validated_texts) if source_locale == target_locale
91
91
 
92
- 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)
93
95
  end
94
96
 
95
97
  # Auto-détection de la langue source avec support du contexte
96
- 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
+
97
102
  target_locale = LocaleHelper.validate_locale!(to)
98
103
 
99
- # Premier appel pour détecter la langue
100
- detection_prompt = build_language_detection_prompt(text)
101
- 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)
102
106
  detected_language = parse_language_detection(detection_response)
103
-
104
- # Vérifier que la langue détectée est différente de la cible
105
- if detected_language == target_locale
106
- # Si même langue, retourner le texte original
107
- return text
108
- end
107
+ return validated_text if detected_language == target_locale
109
108
 
110
109
  # Puis traduction normale avec contexte
111
- 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)
112
112
  end
113
113
 
114
114
  private
@@ -119,10 +119,8 @@ module MistralTranslator
119
119
  validated_texts.each_with_index.to_h { |text, index| [index, text] }
120
120
  end
121
121
 
122
- def process_mixed_batch(validated_texts, source_locale, target_locale, context, glossary)
122
+ def process_mixed_batch(validated_texts, batch_config)
123
123
  results = {}
124
- batch_config = { source_locale: source_locale, target_locale: target_locale, context: context,
125
- glossary: glossary }
126
124
  requests = build_batch_requests(validated_texts, batch_config, results)
127
125
 
128
126
  process_batch_requests(requests, results) if requests.any?
@@ -147,7 +145,8 @@ module MistralTranslator
147
145
  def create_batch_request(text, batch_config, index)
148
146
  {
149
147
  prompt: build_translation_prompt(text, batch_config[:source_locale], batch_config[:target_locale],
150
- context: batch_config[:context], glossary: batch_config[:glossary]),
148
+ context: batch_config[:context], glossary: batch_config[:glossary],
149
+ preserve_html: batch_config[:preserve_html]),
151
150
  from: batch_config[:source_locale],
152
151
  to: batch_config[:target_locale],
153
152
  index: index,
@@ -172,6 +171,9 @@ module MistralTranslator
172
171
  parsed_result[:translated] if parsed_result
173
172
  rescue EmptyTranslationError
174
173
  ""
174
+ rescue InvalidResponseError
175
+ # Une réponse illisible fait échouer son élément, pas tout le lot
176
+ nil
175
177
  end
176
178
 
177
179
  def translate_with_retry(text, source_locale, target_locale, attempt = 0, **options)
@@ -198,60 +200,10 @@ module MistralTranslator
198
200
  # Log via LoggingHelper
199
201
 
200
202
  def build_translation_prompt(text, source_locale, target_locale, **options)
201
- base_prompt = PromptBuilder.translation_prompt(text, source_locale, target_locale,
202
- preserve_html: options.fetch(:preserve_html, false))
203
-
204
- # Enrichir le prompt avec le contexte et le glossaire
205
- context_present = options[:context] && !options[:context].to_s.strip.empty?
206
- glossary_present =
207
- (options[:glossary].is_a?(Hash) && options[:glossary].any?) ||
208
- (options[:glossary].is_a?(String) && !options[:glossary].to_s.strip.empty?)
209
- if context_present || glossary_present
210
- enrich_prompt_with_context(base_prompt, options[:context], options[:glossary])
211
- else
212
- base_prompt
213
- end
214
- end
215
-
216
- def enrich_prompt_with_context(base_prompt, context, glossary)
217
- enriched_parts = []
218
- context_part = build_context_enrichment(context)
219
- glossary_part = build_glossary_enrichment(glossary)
220
- enriched_parts << context_part if context_part
221
- enriched_parts << glossary_part if glossary_part
222
-
223
- return base_prompt if enriched_parts.empty?
224
-
225
- enriched_context = enriched_parts.join("\n")
226
- base_prompt.sub("RÈGLES :", "#{enriched_context}\n\nRÈGLES :")
227
- end
228
-
229
- def build_context_enrichment(context)
230
- return nil if context.nil? || context.to_s.strip.empty?
231
-
232
- "CONTEXTE : #{context}"
233
- end
234
-
235
- def build_glossary_enrichment(glossary)
236
- return nil if glossary.nil?
237
- return build_glossary_hash(glossary) if glossary.is_a?(Hash)
238
- return build_glossary_string(glossary) if glossary.is_a?(String)
239
-
240
- nil
241
- end
242
-
243
- def build_glossary_hash(glossary_hash)
244
- return nil if glossary_hash.empty?
245
-
246
- glossary_text = glossary_hash.map { |key, value| "#{key} → #{value}" }.join(", ")
247
- "GLOSSAIRE (à respecter strictement) : #{glossary_text}"
248
- end
249
-
250
- def build_glossary_string(glossary_string)
251
- value = glossary_string.to_s.strip
252
- return nil if value.empty?
253
-
254
- "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))
255
207
  end
256
208
  end
257
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
@@ -248,14 +203,20 @@ module MistralTranslator
248
203
  batch_results.each do |result|
249
204
  next unless result[:success]
250
205
 
251
- target_locale = result[:original_request][:to]
252
- parsed_result = ResponseParser.parse_translation_response(result[:result])
253
- 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
254
208
  end
255
209
 
256
210
  results
257
211
  end
258
212
 
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
+
259
220
  def translate_to_multiple_sequential(text, source_locale, target_locales, **options)
260
221
  options = DEFAULT_MULTI_TARGET_OPTIONS.merge(options)
261
222
  results = {}
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MistralTranslator
4
- VERSION = "0.3.2"
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.2
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