mistral_translator 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +493 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +205 -0
- data/Rakefile +12 -0
- data/examples/basic_usage.rb +135 -0
- data/lib/mistral_translator/client.rb +147 -0
- data/lib/mistral_translator/configuration.rb +39 -0
- data/lib/mistral_translator/errors.rb +53 -0
- data/lib/mistral_translator/locale_helper.rb +99 -0
- data/lib/mistral_translator/logger.rb +55 -0
- data/lib/mistral_translator/prompt_builder.rb +219 -0
- data/lib/mistral_translator/response_parser.rb +125 -0
- data/lib/mistral_translator/summarizer.rb +226 -0
- data/lib/mistral_translator/translator.rb +177 -0
- data/lib/mistral_translator/version.rb +20 -0
- data/lib/mistral_translator.rb +138 -0
- data/sig/mistral_translator.rbs +4 -0
- metadata +150 -0
data/README.md
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
# MistralTranslator
|
2
|
+
|
3
|
+
Une gem Ruby pour traduire et résumer du texte en utilisant l'API Mistral AI.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Ajoutez cette ligne à votre Gemfile :
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'mistral_translator'
|
11
|
+
```
|
12
|
+
|
13
|
+
Puis exécutez :
|
14
|
+
|
15
|
+
```bash
|
16
|
+
bundle install
|
17
|
+
```
|
18
|
+
|
19
|
+
Ou installez directement :
|
20
|
+
|
21
|
+
```bash
|
22
|
+
gem install mistral_translator
|
23
|
+
```
|
24
|
+
|
25
|
+
## Configuration
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require 'mistral_translator'
|
29
|
+
|
30
|
+
MistralTranslator.configure do |config|
|
31
|
+
config.api_key = 'votre_clé_api_mistral'
|
32
|
+
config.api_url = 'https://api.mistral.ai' # optionnel
|
33
|
+
config.model = 'mistral-small' # optionnel
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
### Variables d'environnement
|
38
|
+
|
39
|
+
Vous pouvez aussi utiliser des variables d'environnement :
|
40
|
+
|
41
|
+
```bash
|
42
|
+
export MISTRAL_API_KEY=votre_clé_api
|
43
|
+
export MISTRAL_API_URL=https://api.mistral.ai
|
44
|
+
```
|
45
|
+
|
46
|
+
## Utilisation
|
47
|
+
|
48
|
+
### Traduction simple
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
# Traduction de base
|
52
|
+
result = MistralTranslator.translate("Bonjour le monde", from: 'fr', to: 'en')
|
53
|
+
# => "Hello world"
|
54
|
+
|
55
|
+
# Vers plusieurs langues
|
56
|
+
results = MistralTranslator.translate_to_multiple(
|
57
|
+
"Bonjour le monde",
|
58
|
+
from: 'fr',
|
59
|
+
to: ['en', 'es', 'de']
|
60
|
+
)
|
61
|
+
# => { 'en' => "Hello world", 'es' => "Hola mundo", 'de' => "Hallo Welt" }
|
62
|
+
```
|
63
|
+
|
64
|
+
### Traduction en lot
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
texts = ["Bonjour", "Au revoir", "Merci"]
|
68
|
+
results = MistralTranslator.translate_batch(texts, from: 'fr', to: 'en')
|
69
|
+
# => { 0 => "Hello", 1 => "Goodbye", 2 => "Thank you" }
|
70
|
+
```
|
71
|
+
|
72
|
+
### Auto-détection de langue
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
result = MistralTranslator.translate_auto("Hello world", to: 'fr')
|
76
|
+
# => "Bonjour le monde"
|
77
|
+
```
|
78
|
+
|
79
|
+
### Résumés
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
long_text = "Un très long texte à résumer..."
|
83
|
+
|
84
|
+
# Résumé simple
|
85
|
+
summary = MistralTranslator.summarize(long_text, language: 'fr', max_words: 100)
|
86
|
+
|
87
|
+
# Résumé avec traduction
|
88
|
+
summary = MistralTranslator.summarize_and_translate(
|
89
|
+
long_text,
|
90
|
+
from: 'fr',
|
91
|
+
to: 'en',
|
92
|
+
max_words: 150
|
93
|
+
)
|
94
|
+
|
95
|
+
# Résumés par niveaux
|
96
|
+
summaries = MistralTranslator.summarize_tiered(
|
97
|
+
long_text,
|
98
|
+
language: 'fr',
|
99
|
+
short: 50,
|
100
|
+
medium: 150,
|
101
|
+
long: 300
|
102
|
+
)
|
103
|
+
# => { short: "...", medium: "...", long: "..." }
|
104
|
+
```
|
105
|
+
|
106
|
+
### Extensions String (optionnel)
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
# Activer les extensions String
|
110
|
+
ENV['MISTRAL_TRANSLATOR_EXTEND_STRING'] = 'true'
|
111
|
+
require 'mistral_translator'
|
112
|
+
|
113
|
+
"Bonjour".mistral_translate(from: 'fr', to: 'en')
|
114
|
+
# => "Hello"
|
115
|
+
|
116
|
+
"Long texte...".mistral_summarize(language: 'fr', max_words: 50)
|
117
|
+
# => "Résumé..."
|
118
|
+
```
|
119
|
+
|
120
|
+
## Langues supportées
|
121
|
+
|
122
|
+
La gem supporte les langues suivantes :
|
123
|
+
- Français (fr)
|
124
|
+
- Anglais (en)
|
125
|
+
- Espagnol (es)
|
126
|
+
- Portugais (pt)
|
127
|
+
- Allemand (de)
|
128
|
+
- Italien (it)
|
129
|
+
- Néerlandais (nl)
|
130
|
+
- Russe (ru)
|
131
|
+
- Japonais (ja)
|
132
|
+
- Coréen (ko)
|
133
|
+
- Chinois (zh)
|
134
|
+
- Arabe (ar)
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
# Vérifier les langues supportées
|
138
|
+
MistralTranslator.supported_languages
|
139
|
+
MistralTranslator.locale_supported?('fr') # => true
|
140
|
+
```
|
141
|
+
|
142
|
+
## Gestion d'erreurs
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
begin
|
146
|
+
result = MistralTranslator.translate("Hello", from: 'en', to: 'fr')
|
147
|
+
rescue MistralTranslator::RateLimitError
|
148
|
+
puts "Rate limit dépassé, réessayez plus tard"
|
149
|
+
rescue MistralTranslator::AuthenticationError
|
150
|
+
puts "Clé API invalide"
|
151
|
+
rescue MistralTranslator::UnsupportedLanguageError => e
|
152
|
+
puts "Langue non supportée: #{e.language}"
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
## Utilisation avancée
|
157
|
+
|
158
|
+
### Client personnalisé
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
client = MistralTranslator::Client.new(api_key: 'autre_clé')
|
162
|
+
translator = MistralTranslator::Translator.new(client: client)
|
163
|
+
result = translator.translate("Hello", from: 'en', to: 'fr')
|
164
|
+
```
|
165
|
+
|
166
|
+
### Health check
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
status = MistralTranslator.health_check
|
170
|
+
# => { status: :ok, message: "API connection successful" }
|
171
|
+
```
|
172
|
+
|
173
|
+
## Développement
|
174
|
+
|
175
|
+
Après avoir cloné le repo :
|
176
|
+
|
177
|
+
```bash
|
178
|
+
bundle install
|
179
|
+
bin/setup
|
180
|
+
```
|
181
|
+
|
182
|
+
Pour lancer les tests :
|
183
|
+
|
184
|
+
```bash
|
185
|
+
bundle exec rspec
|
186
|
+
```
|
187
|
+
|
188
|
+
Pour lancer RuboCop :
|
189
|
+
|
190
|
+
```bash
|
191
|
+
bundle exec rubocop
|
192
|
+
```
|
193
|
+
|
194
|
+
## Contribution
|
195
|
+
|
196
|
+
Les contributions sont les bienvenues ! Merci de :
|
197
|
+
1. Forker le projet
|
198
|
+
2. Créer une branche pour votre feature
|
199
|
+
3. Commiter vos changements
|
200
|
+
4. Pousser vers la branche
|
201
|
+
5. Ouvrir une Pull Request
|
202
|
+
|
203
|
+
## Licence
|
204
|
+
|
205
|
+
Cette gem est disponible sous la licence MIT.
|
data/Rakefile
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Exemple d'utilisation de base de la gem MistralTranslator
|
5
|
+
require "mistral_translator"
|
6
|
+
|
7
|
+
# Configuration
|
8
|
+
MistralTranslator.configure do |config|
|
9
|
+
config.api_key = ENV["MISTRAL_API_KEY"] || "your_api_key_here"
|
10
|
+
# config.api_url = 'https://api.mistral.ai' # optionnel
|
11
|
+
# config.model = 'mistral-small' # optionnel
|
12
|
+
end
|
13
|
+
|
14
|
+
puts "=== MistralTranslator Examples ==="
|
15
|
+
puts "Version: #{MistralTranslator.version}"
|
16
|
+
puts
|
17
|
+
|
18
|
+
# Vérification de la santé de l'API
|
19
|
+
puts "🔍 Health Check:"
|
20
|
+
health = MistralTranslator.health_check
|
21
|
+
puts "Status: #{health[:status]} - #{health[:message]}"
|
22
|
+
puts
|
23
|
+
|
24
|
+
# Exemple 1: Traduction simple
|
25
|
+
puts "📝 Traduction simple:"
|
26
|
+
begin
|
27
|
+
result = MistralTranslator.translate("Bonjour le monde", from: "fr", to: "en")
|
28
|
+
puts "FR → EN: 'Bonjour le monde' → '#{result}'"
|
29
|
+
rescue MistralTranslator::Error => e
|
30
|
+
puts "Erreur: #{e.message}"
|
31
|
+
end
|
32
|
+
puts
|
33
|
+
|
34
|
+
# Exemple 2: Traduction vers plusieurs langues
|
35
|
+
puts "🌍 Traduction vers plusieurs langues:"
|
36
|
+
begin
|
37
|
+
results = MistralTranslator.translate_to_multiple(
|
38
|
+
"Hello world",
|
39
|
+
from: "en",
|
40
|
+
to: %w[fr es de]
|
41
|
+
)
|
42
|
+
|
43
|
+
results.each do |locale, translation|
|
44
|
+
puts "EN → #{locale.upcase}: '#{translation}'"
|
45
|
+
end
|
46
|
+
rescue MistralTranslator::Error => e
|
47
|
+
puts "Erreur: #{e.message}"
|
48
|
+
end
|
49
|
+
puts
|
50
|
+
|
51
|
+
# Exemple 3: Traduction en lot
|
52
|
+
puts "📦 Traduction en lot:"
|
53
|
+
begin
|
54
|
+
texts = ["Good morning", "Good afternoon", "Good evening"]
|
55
|
+
results = MistralTranslator.translate_batch(texts, from: "en", to: "fr")
|
56
|
+
|
57
|
+
results.each do |index, translation|
|
58
|
+
puts "#{index}: '#{texts[index]}' → '#{translation}'"
|
59
|
+
end
|
60
|
+
rescue MistralTranslator::Error => e
|
61
|
+
puts "Erreur: #{e.message}"
|
62
|
+
end
|
63
|
+
puts
|
64
|
+
|
65
|
+
# Exemple 4: Auto-détection de langue
|
66
|
+
puts "🔍 Auto-détection de langue:"
|
67
|
+
begin
|
68
|
+
result = MistralTranslator.translate_auto("¡Hola mundo!", to: "en")
|
69
|
+
puts "Auto-détection → EN: '#{result}'"
|
70
|
+
rescue MistralTranslator::Error => e
|
71
|
+
puts "Erreur: #{e.message}"
|
72
|
+
end
|
73
|
+
puts
|
74
|
+
|
75
|
+
# Exemple 5: Résumé de texte
|
76
|
+
puts "📄 Résumé de texte:"
|
77
|
+
long_text = <<~TEXT
|
78
|
+
Ruby on Rails est un framework de développement web écrit en Ruby qui suit le paradigme#{" "}
|
79
|
+
Modèle-Vue-Contrôleur (MVC). Il privilégie la convention plutôt que la configuration,#{" "}
|
80
|
+
ce qui permet aux développeurs de créer rapidement des applications web robustes.#{" "}
|
81
|
+
Rails comprend de nombreux outils intégrés comme Active Record pour l'ORM,#{" "}
|
82
|
+
Action View pour les templates, Action Controller pour la logique métier,#{" "}
|
83
|
+
et bien d'autres composants. Le framework a été créé par David Heinemeier Hansson#{" "}
|
84
|
+
en 2004 et continue d'évoluer avec une communauté active de développeurs.
|
85
|
+
TEXT
|
86
|
+
|
87
|
+
begin
|
88
|
+
summary = MistralTranslator.summarize(long_text, language: "fr", max_words: 50)
|
89
|
+
puts "Texte original: #{long_text.length} caractères"
|
90
|
+
puts "Résumé (50 mots max): #{summary}"
|
91
|
+
rescue MistralTranslator::Error => e
|
92
|
+
puts "Erreur: #{e.message}"
|
93
|
+
end
|
94
|
+
puts
|
95
|
+
|
96
|
+
# Exemple 6: Résumé avec traduction
|
97
|
+
puts "🔄 Résumé + Traduction:"
|
98
|
+
begin
|
99
|
+
result = MistralTranslator.summarize_and_translate(
|
100
|
+
long_text,
|
101
|
+
from: "fr",
|
102
|
+
to: "en",
|
103
|
+
max_words: 75
|
104
|
+
)
|
105
|
+
puts "Résumé traduit EN (75 mots max): #{result}"
|
106
|
+
rescue MistralTranslator::Error => e
|
107
|
+
puts "Erreur: #{e.message}"
|
108
|
+
end
|
109
|
+
puts
|
110
|
+
|
111
|
+
# Exemple 7: Résumés par niveaux
|
112
|
+
puts "📊 Résumés par niveaux:"
|
113
|
+
begin
|
114
|
+
tiered = MistralTranslator.summarize_tiered(
|
115
|
+
long_text,
|
116
|
+
language: "fr",
|
117
|
+
short: 25,
|
118
|
+
medium: 75,
|
119
|
+
long: 150
|
120
|
+
)
|
121
|
+
|
122
|
+
puts "Court (25 mots): #{tiered[:short]}"
|
123
|
+
puts "Moyen (75 mots): #{tiered[:medium]}"
|
124
|
+
puts "Long (150 mots): #{tiered[:long]}"
|
125
|
+
rescue MistralTranslator::Error => e
|
126
|
+
puts "Erreur: #{e.message}"
|
127
|
+
end
|
128
|
+
puts
|
129
|
+
|
130
|
+
# Informations sur les langues supportées
|
131
|
+
puts "🌐 Langues supportées:"
|
132
|
+
puts MistralTranslator.supported_languages
|
133
|
+
puts
|
134
|
+
|
135
|
+
puts "✅ Exemples terminés!"
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "json"
|
5
|
+
require "uri"
|
6
|
+
require_relative "logger"
|
7
|
+
|
8
|
+
module MistralTranslator
|
9
|
+
class Client
|
10
|
+
def initialize(api_key: nil)
|
11
|
+
@api_key = api_key || MistralTranslator.configuration.api_key!
|
12
|
+
@base_uri = MistralTranslator.configuration.api_url
|
13
|
+
@model = MistralTranslator.configuration.model
|
14
|
+
@retry_delays = MistralTranslator.configuration.retry_delays
|
15
|
+
end
|
16
|
+
|
17
|
+
def complete(prompt, max_tokens: nil, temperature: nil)
|
18
|
+
response = make_request_with_retry(prompt, max_tokens, temperature)
|
19
|
+
parsed_response = JSON.parse(response.body)
|
20
|
+
|
21
|
+
content = parsed_response.dig("choices", 0, "message", "content")
|
22
|
+
raise InvalidResponseError, "No content in API response" if content.nil? || content.empty?
|
23
|
+
|
24
|
+
content
|
25
|
+
rescue JSON::ParserError => e
|
26
|
+
raise InvalidResponseError, "Invalid JSON in API response: #{e.message}"
|
27
|
+
rescue NoMethodError => e
|
28
|
+
raise InvalidResponseError, "Invalid response structure: #{e.message}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def chat(prompt, max_tokens: nil, temperature: nil)
|
32
|
+
response = make_request_with_retry(prompt, max_tokens, temperature)
|
33
|
+
parsed_response = JSON.parse(response.body)
|
34
|
+
|
35
|
+
parsed_response.dig("choices", 0, "message", "content")
|
36
|
+
rescue JSON::ParserError => e
|
37
|
+
raise InvalidResponseError, "JSON parse error: #{e.message}"
|
38
|
+
rescue NoMethodError => e
|
39
|
+
raise InvalidResponseError, "Invalid response structure: #{e.message}"
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def make_request_with_retry(prompt, max_tokens, temperature, attempt = 0)
|
45
|
+
response = make_request(prompt, max_tokens, temperature)
|
46
|
+
|
47
|
+
# Vérifier les erreurs dans la réponse
|
48
|
+
check_response_for_errors(response)
|
49
|
+
|
50
|
+
if rate_limit_exceeded?(response)
|
51
|
+
handle_rate_limit(prompt, max_tokens, temperature, attempt)
|
52
|
+
else
|
53
|
+
response
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def make_request(prompt, max_tokens, temperature)
|
58
|
+
uri = URI("#{@base_uri}/v1/chat/completions")
|
59
|
+
|
60
|
+
request_body = build_request_body(prompt, max_tokens, temperature)
|
61
|
+
|
62
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
63
|
+
http.use_ssl = (uri.scheme == "https")
|
64
|
+
http.read_timeout = 60 # 60 secondes de timeout
|
65
|
+
|
66
|
+
request = Net::HTTP::Post.new(uri.path, headers)
|
67
|
+
request.body = request_body.to_json
|
68
|
+
|
69
|
+
response = http.request(request)
|
70
|
+
log_request_response(request_body, response)
|
71
|
+
|
72
|
+
response
|
73
|
+
rescue Net::ReadTimeout, Timeout::Error => e
|
74
|
+
raise ApiError, "Request timeout: #{e.message}"
|
75
|
+
rescue Net::HTTPError => e
|
76
|
+
raise ApiError, "HTTP error: #{e.message}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def build_request_body(prompt, max_tokens, temperature)
|
80
|
+
body = {
|
81
|
+
model: @model,
|
82
|
+
messages: [{ role: "user", content: prompt }]
|
83
|
+
}
|
84
|
+
|
85
|
+
body[:max_tokens] = max_tokens if max_tokens
|
86
|
+
body[:temperature] = temperature if temperature
|
87
|
+
|
88
|
+
body
|
89
|
+
end
|
90
|
+
|
91
|
+
def headers
|
92
|
+
{
|
93
|
+
"Authorization" => "Bearer #{@api_key}",
|
94
|
+
"Content-Type" => "application/json",
|
95
|
+
"User-Agent" => "mistral-translator-gem/#{MistralTranslator::VERSION}"
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def check_response_for_errors(response)
|
100
|
+
case response.code.to_i
|
101
|
+
when 401
|
102
|
+
raise AuthenticationError, "Invalid API key"
|
103
|
+
when 429
|
104
|
+
# Rate limit sera géré séparément
|
105
|
+
nil
|
106
|
+
when 400..499
|
107
|
+
raise ApiError, "Client error (#{response.code})}"
|
108
|
+
when 500..599
|
109
|
+
raise ApiError, "Server error (#{response.code})}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def rate_limit_exceeded?(response)
|
114
|
+
return true if response.code.to_i == 429
|
115
|
+
|
116
|
+
return false unless response.code.to_i == 200
|
117
|
+
|
118
|
+
body_content = response.body.to_s
|
119
|
+
return false if body_content.length > 1000
|
120
|
+
|
121
|
+
body_content.match?(/rate.?limit|quota.?exceeded/i)
|
122
|
+
end
|
123
|
+
|
124
|
+
def handle_rate_limit(prompt, max_tokens, temperature, attempt)
|
125
|
+
unless attempt < @retry_delays.length
|
126
|
+
raise RateLimitError, "API rate limit exceeded after #{@retry_delays.length} retries"
|
127
|
+
end
|
128
|
+
|
129
|
+
wait_time = @retry_delays[attempt]
|
130
|
+
log_rate_limit_retry(wait_time, attempt)
|
131
|
+
sleep(wait_time)
|
132
|
+
make_request_with_retry(prompt, max_tokens, temperature, attempt + 1)
|
133
|
+
end
|
134
|
+
|
135
|
+
def log_request_response(_request_body, response)
|
136
|
+
# Log seulement si mode verbose activé
|
137
|
+
Logger.debug_if_verbose("Request sent to API", sensitive: true)
|
138
|
+
Logger.debug_if_verbose("Response received: #{response.code}", sensitive: false)
|
139
|
+
end
|
140
|
+
|
141
|
+
def log_rate_limit_retry(wait_time, attempt)
|
142
|
+
message = "Rate limit exceeded, retrying in #{wait_time} seconds (attempt #{attempt + 1})"
|
143
|
+
# Log une seule fois par session pour éviter le spam
|
144
|
+
Logger.warn_once(message, key: "rate_limit_retry", sensitive: false, ttl: 60)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MistralTranslator
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :api_key, :api_url, :model, :default_max_tokens, :default_temperature, :retry_delays
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@api_key = nil
|
9
|
+
@api_url = "https://api.mistral.ai"
|
10
|
+
@model = "mistral-small"
|
11
|
+
@default_max_tokens = nil
|
12
|
+
@default_temperature = nil
|
13
|
+
@retry_delays = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
|
14
|
+
end
|
15
|
+
|
16
|
+
def api_key!
|
17
|
+
if @api_key.nil?
|
18
|
+
raise ConfigurationError,
|
19
|
+
"API key is required. Set it with MistralTranslator.configure { |c| c.api_key = 'your_key' }"
|
20
|
+
end
|
21
|
+
|
22
|
+
@api_key
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class << self
|
27
|
+
def configuration
|
28
|
+
@configuration ||= Configuration.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def configure
|
32
|
+
yield(configuration)
|
33
|
+
end
|
34
|
+
|
35
|
+
def reset_configuration!
|
36
|
+
@configuration = Configuration.new
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MistralTranslator
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class ConfigurationError < Error; end
|
7
|
+
|
8
|
+
class ApiError < Error
|
9
|
+
attr_reader :response, :status_code
|
10
|
+
|
11
|
+
def initialize(message, response = nil, status_code = nil)
|
12
|
+
super(message)
|
13
|
+
@response = response
|
14
|
+
@status_code = status_code
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class RateLimitError < ApiError
|
19
|
+
def initialize(message = "API rate limit exceeded", response = nil, status_code = 429)
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class AuthenticationError < ApiError
|
25
|
+
def initialize(message = "Invalid API key", response = nil, status_code = 401)
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class InvalidResponseError < Error
|
31
|
+
attr_reader :raw_response
|
32
|
+
|
33
|
+
def initialize(message, raw_response = nil)
|
34
|
+
super(message)
|
35
|
+
@raw_response = raw_response
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class EmptyTranslationError < Error
|
40
|
+
def initialize(message = "Empty translation received from API")
|
41
|
+
super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class UnsupportedLanguageError < Error
|
46
|
+
attr_reader :language
|
47
|
+
|
48
|
+
def initialize(language)
|
49
|
+
@language = language
|
50
|
+
super("Unsupported language: #{language}")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MistralTranslator
|
4
|
+
module LocaleHelper
|
5
|
+
SUPPORTED_LANGUAGES = {
|
6
|
+
"fr" => "français",
|
7
|
+
"en" => "english",
|
8
|
+
"es" => "español",
|
9
|
+
"pt" => "português",
|
10
|
+
"de" => "deutsch",
|
11
|
+
"it" => "italiano",
|
12
|
+
"nl" => "nederlands",
|
13
|
+
"ru" => "русский",
|
14
|
+
"mg" => "malagasy",
|
15
|
+
"ja" => "日本語",
|
16
|
+
"ko" => "한국어",
|
17
|
+
"zh" => "中文",
|
18
|
+
"ar" => "العربية"
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
# Mapping des noms alternatifs vers les codes de langue
|
22
|
+
LANGUAGE_ALIASES = {
|
23
|
+
"french" => "fr",
|
24
|
+
"german" => "de",
|
25
|
+
"spanish" => "es",
|
26
|
+
"portuguese" => "pt",
|
27
|
+
"italian" => "it",
|
28
|
+
"dutch" => "nl",
|
29
|
+
"russian" => "ru",
|
30
|
+
"malagasy" => "mg",
|
31
|
+
"japanese" => "ja",
|
32
|
+
"korean" => "ko",
|
33
|
+
"chinese" => "zh",
|
34
|
+
"arabic" => "ar"
|
35
|
+
}.freeze
|
36
|
+
|
37
|
+
class << self
|
38
|
+
def locale_to_language(locale)
|
39
|
+
locale_code = normalize_locale(locale)
|
40
|
+
SUPPORTED_LANGUAGES[locale_code] || locale_code.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
def language_to_locale(language)
|
44
|
+
normalized_language = language.to_s.downcase.strip
|
45
|
+
|
46
|
+
# Vérifier d'abord les alias
|
47
|
+
return LANGUAGE_ALIASES[normalized_language] if LANGUAGE_ALIASES.key?(normalized_language)
|
48
|
+
|
49
|
+
# Recherche directe par valeur
|
50
|
+
found_locale = SUPPORTED_LANGUAGES.find { |_, lang| lang.downcase == normalized_language }
|
51
|
+
return found_locale[0] if found_locale
|
52
|
+
|
53
|
+
# Retourne la langue telle quelle si pas trouvée
|
54
|
+
normalized_language
|
55
|
+
end
|
56
|
+
|
57
|
+
def supported_locales
|
58
|
+
SUPPORTED_LANGUAGES.keys
|
59
|
+
end
|
60
|
+
|
61
|
+
def supported_languages
|
62
|
+
SUPPORTED_LANGUAGES.values
|
63
|
+
end
|
64
|
+
|
65
|
+
def locale_supported?(locale)
|
66
|
+
SUPPORTED_LANGUAGES.key?(normalize_locale(locale))
|
67
|
+
end
|
68
|
+
|
69
|
+
def validate_locale!(locale)
|
70
|
+
normalized = normalize_locale(locale)
|
71
|
+
raise UnsupportedLanguageError, normalized unless locale_supported?(normalized)
|
72
|
+
|
73
|
+
normalized
|
74
|
+
end
|
75
|
+
|
76
|
+
def normalize_locale(locale)
|
77
|
+
return "" if locale.nil? || locale.to_s.empty?
|
78
|
+
|
79
|
+
normalized = locale.to_s.downcase
|
80
|
+
return "" if normalized.empty?
|
81
|
+
|
82
|
+
# Diviser par "-" et prendre la première partie
|
83
|
+
first_part = normalized.split("-").first
|
84
|
+
return "" if first_part.nil? || first_part.empty?
|
85
|
+
|
86
|
+
# Diviser par "_" et prendre la première partie
|
87
|
+
result = first_part.split("_").first
|
88
|
+
return "" if result.nil? || result.empty?
|
89
|
+
|
90
|
+
result
|
91
|
+
end
|
92
|
+
|
93
|
+
# Méthode pour obtenir une liste formatée des langues supportées
|
94
|
+
def supported_languages_list
|
95
|
+
SUPPORTED_LANGUAGES.map { |code, name| "#{code} (#{name})" }.join(", ")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|