better_translate 0.5.0 → 1.0.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 +4 -4
- data/.env.example +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +125 -114
- data/CLAUDE.md +385 -0
- data/README.md +629 -244
- data/Rakefile +7 -1
- data/Steepfile +29 -0
- data/docs/implementation/00-overview.md +220 -0
- data/docs/implementation/01-setup_dependencies.md +668 -0
- data/docs/implementation/02-error_handling.md +65 -0
- data/docs/implementation/03-core_components.md +457 -0
- data/docs/implementation/03.5-variable_preservation.md +509 -0
- data/docs/implementation/04-provider_architecture.md +571 -0
- data/docs/implementation/05-translation_logic.md +1065 -0
- data/docs/implementation/06-main_module_api.md +122 -0
- data/docs/implementation/07-direct_translation_helpers.md +582 -0
- data/docs/implementation/08-rails_integration.md +323 -0
- data/docs/implementation/09-testing_suite.md +228 -0
- data/docs/implementation/10-documentation_examples.md +150 -0
- data/docs/implementation/11-quality_security.md +65 -0
- data/docs/implementation/12-cli_standalone.md +698 -0
- data/exe/better_translate +9 -0
- data/lib/better_translate/cache.rb +125 -0
- data/lib/better_translate/cli.rb +304 -0
- data/lib/better_translate/configuration.rb +201 -0
- data/lib/better_translate/direct_translator.rb +131 -0
- data/lib/better_translate/errors.rb +101 -0
- data/lib/better_translate/progress_tracker.rb +157 -0
- data/lib/better_translate/provider_factory.rb +45 -0
- data/lib/better_translate/providers/anthropic_provider.rb +154 -0
- data/lib/better_translate/providers/base_http_provider.rb +239 -0
- data/lib/better_translate/providers/chatgpt_provider.rb +138 -44
- data/lib/better_translate/providers/gemini_provider.rb +123 -61
- data/lib/better_translate/railtie.rb +18 -0
- data/lib/better_translate/rate_limiter.rb +90 -0
- data/lib/better_translate/strategies/base_strategy.rb +58 -0
- data/lib/better_translate/strategies/batch_strategy.rb +56 -0
- data/lib/better_translate/strategies/deep_strategy.rb +45 -0
- data/lib/better_translate/strategies/strategy_selector.rb +43 -0
- data/lib/better_translate/translator.rb +115 -284
- data/lib/better_translate/utils/hash_flattener.rb +104 -0
- data/lib/better_translate/validator.rb +105 -0
- data/lib/better_translate/variable_extractor.rb +259 -0
- data/lib/better_translate/version.rb +2 -9
- data/lib/better_translate/yaml_handler.rb +168 -0
- data/lib/better_translate.rb +97 -73
- data/lib/generators/better_translate/analyze/USAGE +12 -0
- data/lib/generators/better_translate/analyze/analyze_generator.rb +94 -0
- data/lib/generators/better_translate/install/USAGE +13 -0
- data/lib/generators/better_translate/install/install_generator.rb +71 -0
- data/lib/generators/better_translate/install/templates/README +20 -0
- data/lib/generators/better_translate/install/templates/initializer.rb.tt +47 -0
- data/lib/generators/better_translate/translate/USAGE +13 -0
- data/lib/generators/better_translate/translate/translate_generator.rb +114 -0
- data/lib/tasks/better_translate.rake +136 -0
- data/sig/better_translate/cache.rbs +28 -0
- data/sig/better_translate/cli.rbs +24 -0
- data/sig/better_translate/configuration.rbs +78 -0
- data/sig/better_translate/direct_translator.rbs +18 -0
- data/sig/better_translate/errors.rbs +46 -0
- data/sig/better_translate/progress_tracker.rbs +29 -0
- data/sig/better_translate/provider_factory.rbs +8 -0
- data/sig/better_translate/providers/anthropic_provider.rbs +27 -0
- data/sig/better_translate/providers/base_http_provider.rbs +44 -0
- data/sig/better_translate/providers/chatgpt_provider.rbs +25 -0
- data/sig/better_translate/providers/gemini_provider.rbs +22 -0
- data/sig/better_translate/railtie.rbs +7 -0
- data/sig/better_translate/rate_limiter.rbs +20 -0
- data/sig/better_translate/strategies/base_strategy.rbs +19 -0
- data/sig/better_translate/strategies/batch_strategy.rbs +13 -0
- data/sig/better_translate/strategies/deep_strategy.rbs +11 -0
- data/sig/better_translate/strategies/strategy_selector.rbs +10 -0
- data/sig/better_translate/translator.rbs +24 -0
- data/sig/better_translate/utils/hash_flattener.rbs +14 -0
- data/sig/better_translate/validator.rbs +14 -0
- data/sig/better_translate/variable_extractor.rbs +40 -0
- data/sig/better_translate/version.rbs +4 -0
- data/sig/better_translate/yaml_handler.rbs +29 -0
- data/sig/better_translate.rbs +32 -2
- data/sig/faraday.rbs +22 -0
- data/sig/generators/better_translate/analyze/analyze_generator.rbs +18 -0
- data/sig/generators/better_translate/install/install_generator.rbs +14 -0
- data/sig/generators/better_translate/translate/translate_generator.rbs +10 -0
- data/sig/optparse.rbs +9 -0
- data/sig/psych.rbs +5 -0
- data/sig/rails.rbs +34 -0
- metadata +89 -203
- data/lib/better_translate/helper.rb +0 -83
- data/lib/better_translate/providers/base_provider.rb +0 -102
- data/lib/better_translate/service.rb +0 -144
- data/lib/better_translate/similarity_analyzer.rb +0 -218
- data/lib/better_translate/utils.rb +0 -55
- data/lib/better_translate/writer.rb +0 -75
- data/lib/generators/better_translate/analyze_generator.rb +0 -57
- data/lib/generators/better_translate/install_generator.rb +0 -14
- data/lib/generators/better_translate/templates/better_translate.rb +0 -56
- data/lib/generators/better_translate/translate_generator.rb +0 -84
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
# 04 - Provider Architecture
|
|
2
|
+
|
|
3
|
+
[← Previous: 03-Core Components](./03-core_components.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 05-Translation Logic →](./05-translation_logic.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Provider Architecture
|
|
8
|
+
|
|
9
|
+
### 4.1 `lib/better_translate/providers/base_http_provider.rb`
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# frozen_string_literal: true
|
|
13
|
+
|
|
14
|
+
require "faraday"
|
|
15
|
+
require "json"
|
|
16
|
+
|
|
17
|
+
module BetterTranslate
|
|
18
|
+
module Providers
|
|
19
|
+
# Base class for HTTP-based translation providers
|
|
20
|
+
#
|
|
21
|
+
# Implements common functionality:
|
|
22
|
+
# - Faraday HTTP client with retry logic
|
|
23
|
+
# - Exponential backoff with jitter
|
|
24
|
+
# - Rate limiting
|
|
25
|
+
# - Caching
|
|
26
|
+
# - Error handling
|
|
27
|
+
#
|
|
28
|
+
# @abstract Subclasses must implement {#translate_text} and {#translate_batch}
|
|
29
|
+
class BaseHttpProvider
|
|
30
|
+
# @return [Configuration] The configuration object
|
|
31
|
+
attr_reader :config
|
|
32
|
+
|
|
33
|
+
# @return [Cache] The cache instance
|
|
34
|
+
attr_reader :cache
|
|
35
|
+
|
|
36
|
+
# @return [RateLimiter] The rate limiter instance
|
|
37
|
+
attr_reader :rate_limiter
|
|
38
|
+
|
|
39
|
+
# Initialize the provider
|
|
40
|
+
#
|
|
41
|
+
# @param config [Configuration] Configuration object
|
|
42
|
+
def initialize(config)
|
|
43
|
+
@config = config
|
|
44
|
+
@cache = Cache.new(capacity: config.cache_size, ttl: config.cache_ttl)
|
|
45
|
+
@rate_limiter = RateLimiter.new(delay: 0.5)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Translate a single text string
|
|
49
|
+
#
|
|
50
|
+
# @param text [String] Text to translate
|
|
51
|
+
# @param target_lang_code [String] Target language code
|
|
52
|
+
# @param target_lang_name [String] Target language name
|
|
53
|
+
# @return [String] Translated text
|
|
54
|
+
# @raise [NotImplementedError] Must be implemented by subclasses
|
|
55
|
+
def translate_text(text, target_lang_code, target_lang_name)
|
|
56
|
+
raise NotImplementedError, "#{self.class} must implement #translate_text"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Translate multiple texts in a batch
|
|
60
|
+
#
|
|
61
|
+
# @param texts [Array<String>] Texts to translate
|
|
62
|
+
# @param target_lang_code [String] Target language code
|
|
63
|
+
# @param target_lang_name [String] Target language name
|
|
64
|
+
# @return [Array<String>] Translated texts
|
|
65
|
+
# @raise [NotImplementedError] Must be implemented by subclasses
|
|
66
|
+
def translate_batch(texts, target_lang_code, target_lang_name)
|
|
67
|
+
raise NotImplementedError, "#{self.class} must implement #translate_batch"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
protected
|
|
71
|
+
|
|
72
|
+
# Make an HTTP request with retry logic
|
|
73
|
+
#
|
|
74
|
+
# @param method [Symbol] HTTP method (:get, :post, etc.)
|
|
75
|
+
# @param url [String] Request URL
|
|
76
|
+
# @param body [Hash, nil] Request body
|
|
77
|
+
# @param headers [Hash] Request headers
|
|
78
|
+
# @return [Faraday::Response] HTTP response
|
|
79
|
+
# @raise [ApiError] if request fails after retries
|
|
80
|
+
def make_request(method, url, body: nil, headers: {})
|
|
81
|
+
attempt = 0
|
|
82
|
+
last_error = nil
|
|
83
|
+
|
|
84
|
+
loop do
|
|
85
|
+
attempt += 1
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
rate_limiter.wait
|
|
89
|
+
response = http_client.send(method, url) do |req|
|
|
90
|
+
req.headers.merge!(headers)
|
|
91
|
+
req.body = body.to_json if body
|
|
92
|
+
end
|
|
93
|
+
rate_limiter.record_request
|
|
94
|
+
|
|
95
|
+
handle_response(response)
|
|
96
|
+
return response
|
|
97
|
+
|
|
98
|
+
rescue RateLimitError => e
|
|
99
|
+
last_error = e
|
|
100
|
+
if attempt < config.max_retries
|
|
101
|
+
delay = calculate_backoff(attempt)
|
|
102
|
+
log_retry(attempt, delay, e)
|
|
103
|
+
sleep(delay)
|
|
104
|
+
next
|
|
105
|
+
end
|
|
106
|
+
raise
|
|
107
|
+
|
|
108
|
+
rescue ApiError => e
|
|
109
|
+
last_error = e
|
|
110
|
+
if attempt < config.max_retries
|
|
111
|
+
delay = calculate_backoff(attempt)
|
|
112
|
+
log_retry(attempt, delay, e)
|
|
113
|
+
sleep(delay)
|
|
114
|
+
next
|
|
115
|
+
end
|
|
116
|
+
raise
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Handle HTTP response
|
|
122
|
+
#
|
|
123
|
+
# @param response [Faraday::Response] HTTP response
|
|
124
|
+
# @raise [RateLimitError] if rate limited (429)
|
|
125
|
+
# @raise [ApiError] if response indicates error
|
|
126
|
+
# @return [void]
|
|
127
|
+
def handle_response(response)
|
|
128
|
+
case response.status
|
|
129
|
+
when 200..299
|
|
130
|
+
# Success
|
|
131
|
+
when 429
|
|
132
|
+
raise RateLimitError.new(
|
|
133
|
+
"Rate limit exceeded",
|
|
134
|
+
context: { status: response.status, body: response.body }
|
|
135
|
+
)
|
|
136
|
+
when 400..499
|
|
137
|
+
raise ApiError.new(
|
|
138
|
+
"Client error: #{response.status}",
|
|
139
|
+
context: { status: response.status, body: response.body }
|
|
140
|
+
)
|
|
141
|
+
when 500..599
|
|
142
|
+
raise ApiError.new(
|
|
143
|
+
"Server error: #{response.status}",
|
|
144
|
+
context: { status: response.status, body: response.body }
|
|
145
|
+
)
|
|
146
|
+
else
|
|
147
|
+
raise ApiError.new(
|
|
148
|
+
"Unexpected status: #{response.status}",
|
|
149
|
+
context: { status: response.status, body: response.body }
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Calculate exponential backoff with jitter
|
|
155
|
+
#
|
|
156
|
+
# @param attempt [Integer] Current attempt number
|
|
157
|
+
# @return [Float] Delay in seconds
|
|
158
|
+
def calculate_backoff(attempt)
|
|
159
|
+
base_delay = config.retry_delay
|
|
160
|
+
max_delay = 60.0
|
|
161
|
+
jitter = rand * 0.3 # 0-30% jitter
|
|
162
|
+
|
|
163
|
+
delay = base_delay * (2 ** (attempt - 1)) * (1 + jitter)
|
|
164
|
+
[delay, max_delay].min
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Log retry attempt
|
|
168
|
+
#
|
|
169
|
+
# @param attempt [Integer] Current attempt number
|
|
170
|
+
# @param delay [Float] Delay before retry
|
|
171
|
+
# @param error [StandardError] The error that triggered retry
|
|
172
|
+
# @return [void]
|
|
173
|
+
def log_retry(attempt, delay, error)
|
|
174
|
+
return unless config.verbose
|
|
175
|
+
|
|
176
|
+
puts "[BetterTranslate] Retry #{attempt}/#{config.max_retries} after #{delay.round(2)}s (#{error.class}: #{error.message})"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Get or create HTTP client
|
|
180
|
+
#
|
|
181
|
+
# @return [Faraday::Connection] HTTP client
|
|
182
|
+
def http_client
|
|
183
|
+
@http_client ||= Faraday.new do |f|
|
|
184
|
+
f.options.timeout = config.request_timeout
|
|
185
|
+
f.options.open_timeout = 10
|
|
186
|
+
f.adapter Faraday.default_adapter
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Get from cache or execute block
|
|
191
|
+
#
|
|
192
|
+
# @param cache_key [String] Cache key
|
|
193
|
+
# @yieldreturn [String] Value to cache if not found
|
|
194
|
+
# @return [String] Cached or newly computed value
|
|
195
|
+
def with_cache(cache_key)
|
|
196
|
+
return yield unless config.cache_enabled
|
|
197
|
+
|
|
198
|
+
cached = cache.get(cache_key)
|
|
199
|
+
return cached if cached
|
|
200
|
+
|
|
201
|
+
result = yield
|
|
202
|
+
cache.set(cache_key, result)
|
|
203
|
+
result
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Build cache key
|
|
207
|
+
#
|
|
208
|
+
# @param text [String] Text being translated
|
|
209
|
+
# @param target_lang_code [String] Target language code
|
|
210
|
+
# @return [String] Cache key
|
|
211
|
+
def build_cache_key(text, target_lang_code)
|
|
212
|
+
"#{text}:#{target_lang_code}"
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 4.2 `lib/better_translate/providers/chatgpt_provider.rb`
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
# frozen_string_literal: true
|
|
223
|
+
|
|
224
|
+
module BetterTranslate
|
|
225
|
+
module Providers
|
|
226
|
+
# OpenAI ChatGPT translation provider
|
|
227
|
+
#
|
|
228
|
+
# Uses GPT-5-nano model with temperature=1.0
|
|
229
|
+
class ChatGPTProvider < BaseHttpProvider
|
|
230
|
+
API_URL = "https://api.openai.com/v1/chat/completions"
|
|
231
|
+
MODEL = "gpt-5-nano"
|
|
232
|
+
TEMPERATURE = 1.0
|
|
233
|
+
|
|
234
|
+
# Translate a single text
|
|
235
|
+
#
|
|
236
|
+
# @param text [String] Text to translate
|
|
237
|
+
# @param target_lang_code [String] Target language code (e.g., "it")
|
|
238
|
+
# @param target_lang_name [String] Target language name (e.g., "Italian")
|
|
239
|
+
# @return [String] Translated text
|
|
240
|
+
# @raise [ValidationError] if input is invalid
|
|
241
|
+
# @raise [TranslationError] if translation fails
|
|
242
|
+
def translate_text(text, target_lang_code, target_lang_name)
|
|
243
|
+
Validator.validate_text!(text)
|
|
244
|
+
Validator.validate_language_code!(target_lang_code)
|
|
245
|
+
|
|
246
|
+
cache_key = build_cache_key(text, target_lang_code)
|
|
247
|
+
|
|
248
|
+
with_cache(cache_key) do
|
|
249
|
+
messages = build_messages(text, target_lang_name)
|
|
250
|
+
response = make_chat_completion_request(messages)
|
|
251
|
+
extract_translation(response)
|
|
252
|
+
end
|
|
253
|
+
rescue ApiError => e
|
|
254
|
+
raise TranslationError.new(
|
|
255
|
+
"Failed to translate text with ChatGPT: #{e.message}",
|
|
256
|
+
context: { text: text, target_lang: target_lang_code, original_error: e }
|
|
257
|
+
)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Translate multiple texts in a batch
|
|
261
|
+
#
|
|
262
|
+
# @param texts [Array<String>] Texts to translate
|
|
263
|
+
# @param target_lang_code [String] Target language code
|
|
264
|
+
# @param target_lang_name [String] Target language name
|
|
265
|
+
# @return [Array<String>] Translated texts
|
|
266
|
+
def translate_batch(texts, target_lang_code, target_lang_name)
|
|
267
|
+
texts.map { |text| translate_text(text, target_lang_code, target_lang_name) }
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
private
|
|
271
|
+
|
|
272
|
+
def build_messages(text, target_lang_name)
|
|
273
|
+
system_message = build_system_message(target_lang_name)
|
|
274
|
+
|
|
275
|
+
[
|
|
276
|
+
{ role: "system", content: system_message },
|
|
277
|
+
{ role: "user", content: text }
|
|
278
|
+
]
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def build_system_message(target_lang_name)
|
|
282
|
+
base_message = "You are a professional translator. Translate the following text to #{target_lang_name}. " \
|
|
283
|
+
"Return ONLY the translated text, without any explanations or additional text."
|
|
284
|
+
|
|
285
|
+
if config.translation_context && !config.translation_context.empty?
|
|
286
|
+
base_message += "\n\nContext: #{config.translation_context}"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
base_message
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def make_chat_completion_request(messages)
|
|
293
|
+
body = {
|
|
294
|
+
model: MODEL,
|
|
295
|
+
messages: messages,
|
|
296
|
+
temperature: TEMPERATURE
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
headers = {
|
|
300
|
+
"Content-Type" => "application/json",
|
|
301
|
+
"Authorization" => "Bearer #{config.openai_key}"
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
make_request(:post, API_URL, body: body, headers: headers)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def extract_translation(response)
|
|
308
|
+
parsed = JSON.parse(response.body)
|
|
309
|
+
translation = parsed.dig("choices", 0, "message", "content")
|
|
310
|
+
|
|
311
|
+
raise TranslationError, "No translation in response" if translation.nil? || translation.empty?
|
|
312
|
+
|
|
313
|
+
translation.strip
|
|
314
|
+
rescue JSON::ParserError => e
|
|
315
|
+
raise TranslationError.new(
|
|
316
|
+
"Failed to parse ChatGPT response",
|
|
317
|
+
context: { error: e.message, body: response.body }
|
|
318
|
+
)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### 4.3 `lib/better_translate/providers/gemini_provider.rb`
|
|
326
|
+
|
|
327
|
+
```ruby
|
|
328
|
+
# frozen_string_literal: true
|
|
329
|
+
|
|
330
|
+
module BetterTranslate
|
|
331
|
+
module Providers
|
|
332
|
+
# Google Gemini translation provider
|
|
333
|
+
#
|
|
334
|
+
# Uses gemini-2.0-flash-exp model
|
|
335
|
+
class GeminiProvider < BaseHttpProvider
|
|
336
|
+
API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent"
|
|
337
|
+
MODEL = "gemini-2.0-flash-exp"
|
|
338
|
+
|
|
339
|
+
# Translate a single text
|
|
340
|
+
#
|
|
341
|
+
# @param text [String] Text to translate
|
|
342
|
+
# @param target_lang_code [String] Target language code
|
|
343
|
+
# @param target_lang_name [String] Target language name
|
|
344
|
+
# @return [String] Translated text
|
|
345
|
+
def translate_text(text, target_lang_code, target_lang_name)
|
|
346
|
+
Validator.validate_text!(text)
|
|
347
|
+
Validator.validate_language_code!(target_lang_code)
|
|
348
|
+
|
|
349
|
+
cache_key = build_cache_key(text, target_lang_code)
|
|
350
|
+
|
|
351
|
+
with_cache(cache_key) do
|
|
352
|
+
prompt = build_prompt(text, target_lang_name)
|
|
353
|
+
response = make_generation_request(prompt)
|
|
354
|
+
extract_translation(response)
|
|
355
|
+
end
|
|
356
|
+
rescue ApiError => e
|
|
357
|
+
raise TranslationError.new(
|
|
358
|
+
"Failed to translate text with Gemini: #{e.message}",
|
|
359
|
+
context: { text: text, target_lang: target_lang_code, original_error: e }
|
|
360
|
+
)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Translate multiple texts in a batch
|
|
364
|
+
#
|
|
365
|
+
# @param texts [Array<String>] Texts to translate
|
|
366
|
+
# @param target_lang_code [String] Target language code
|
|
367
|
+
# @param target_lang_name [String] Target language name
|
|
368
|
+
# @return [Array<String>] Translated texts
|
|
369
|
+
def translate_batch(texts, target_lang_code, target_lang_name)
|
|
370
|
+
texts.map { |text| translate_text(text, target_lang_code, target_lang_name) }
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
private
|
|
374
|
+
|
|
375
|
+
def build_prompt(text, target_lang_name)
|
|
376
|
+
base_prompt = "Translate the following text to #{target_lang_name}. " \
|
|
377
|
+
"Return ONLY the translated text, without any explanations.\n\n" \
|
|
378
|
+
"Text: #{text}"
|
|
379
|
+
|
|
380
|
+
if config.translation_context && !config.translation_context.empty?
|
|
381
|
+
base_prompt = "Context: #{config.translation_context}\n\n#{base_prompt}"
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
base_prompt
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def make_generation_request(prompt)
|
|
388
|
+
url = "#{API_URL}?key=#{config.google_gemini_key}"
|
|
389
|
+
|
|
390
|
+
body = {
|
|
391
|
+
contents: [
|
|
392
|
+
{
|
|
393
|
+
parts: [
|
|
394
|
+
{ text: prompt }
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
headers = {
|
|
401
|
+
"Content-Type" => "application/json"
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
make_request(:post, url, body: body, headers: headers)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def extract_translation(response)
|
|
408
|
+
parsed = JSON.parse(response.body)
|
|
409
|
+
translation = parsed.dig("candidates", 0, "content", "parts", 0, "text")
|
|
410
|
+
|
|
411
|
+
raise TranslationError, "No translation in response" if translation.nil? || translation.empty?
|
|
412
|
+
|
|
413
|
+
translation.strip
|
|
414
|
+
rescue JSON::ParserError => e
|
|
415
|
+
raise TranslationError.new(
|
|
416
|
+
"Failed to parse Gemini response",
|
|
417
|
+
context: { error: e.message, body: response.body }
|
|
418
|
+
)
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### 4.4 `lib/better_translate/providers/anthropic_provider.rb`
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
# frozen_string_literal: true
|
|
429
|
+
|
|
430
|
+
module BetterTranslate
|
|
431
|
+
module Providers
|
|
432
|
+
# Anthropic Claude translation provider
|
|
433
|
+
#
|
|
434
|
+
# Uses claude-3-5-sonnet-20241022 model
|
|
435
|
+
class AnthropicProvider < BaseHttpProvider
|
|
436
|
+
API_URL = "https://api.anthropic.com/v1/messages"
|
|
437
|
+
MODEL = "claude-3-5-sonnet-20241022"
|
|
438
|
+
API_VERSION = "2023-06-01"
|
|
439
|
+
|
|
440
|
+
# Translate a single text
|
|
441
|
+
#
|
|
442
|
+
# @param text [String] Text to translate
|
|
443
|
+
# @param target_lang_code [String] Target language code
|
|
444
|
+
# @param target_lang_name [String] Target language name
|
|
445
|
+
# @return [String] Translated text
|
|
446
|
+
def translate_text(text, target_lang_code, target_lang_name)
|
|
447
|
+
Validator.validate_text!(text)
|
|
448
|
+
Validator.validate_language_code!(target_lang_code)
|
|
449
|
+
|
|
450
|
+
cache_key = build_cache_key(text, target_lang_code)
|
|
451
|
+
|
|
452
|
+
with_cache(cache_key) do
|
|
453
|
+
messages = build_messages(text, target_lang_name)
|
|
454
|
+
response = make_messages_request(messages)
|
|
455
|
+
extract_translation(response)
|
|
456
|
+
end
|
|
457
|
+
rescue ApiError => e
|
|
458
|
+
raise TranslationError.new(
|
|
459
|
+
"Failed to translate text with Anthropic: #{e.message}",
|
|
460
|
+
context: { text: text, target_lang: target_lang_code, original_error: e }
|
|
461
|
+
)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# Translate multiple texts in a batch
|
|
465
|
+
#
|
|
466
|
+
# @param texts [Array<String>] Texts to translate
|
|
467
|
+
# @param target_lang_code [String] Target language code
|
|
468
|
+
# @param target_lang_name [String] Target language name
|
|
469
|
+
# @return [Array<String>] Translated texts
|
|
470
|
+
def translate_batch(texts, target_lang_code, target_lang_name)
|
|
471
|
+
texts.map { |text| translate_text(text, target_lang_code, target_lang_name) }
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
private
|
|
475
|
+
|
|
476
|
+
def build_messages(text, target_lang_name)
|
|
477
|
+
system_message = build_system_message(target_lang_name)
|
|
478
|
+
|
|
479
|
+
{
|
|
480
|
+
system: system_message,
|
|
481
|
+
messages: [
|
|
482
|
+
{ role: "user", content: text }
|
|
483
|
+
]
|
|
484
|
+
}
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def build_system_message(target_lang_name)
|
|
488
|
+
base_message = "You are a professional translator. Translate the following text to #{target_lang_name}. " \
|
|
489
|
+
"Return ONLY the translated text, without any explanations or additional text."
|
|
490
|
+
|
|
491
|
+
if config.translation_context && !config.translation_context.empty?
|
|
492
|
+
base_message += "\n\nContext: #{config.translation_context}"
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
base_message
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def make_messages_request(message_data)
|
|
499
|
+
body = {
|
|
500
|
+
model: MODEL,
|
|
501
|
+
max_tokens: 1024,
|
|
502
|
+
system: message_data[:system],
|
|
503
|
+
messages: message_data[:messages]
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
headers = {
|
|
507
|
+
"Content-Type" => "application/json",
|
|
508
|
+
"x-api-key" => config.anthropic_key,
|
|
509
|
+
"anthropic-version" => API_VERSION
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
make_request(:post, API_URL, body: body, headers: headers)
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def extract_translation(response)
|
|
516
|
+
parsed = JSON.parse(response.body)
|
|
517
|
+
translation = parsed.dig("content", 0, "text")
|
|
518
|
+
|
|
519
|
+
raise TranslationError, "No translation in response" if translation.nil? || translation.empty?
|
|
520
|
+
|
|
521
|
+
translation.strip
|
|
522
|
+
rescue JSON::ParserError => e
|
|
523
|
+
raise TranslationError.new(
|
|
524
|
+
"Failed to parse Anthropic response",
|
|
525
|
+
context: { error: e.message, body: response.body }
|
|
526
|
+
)
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### 4.5 `lib/better_translate/provider_factory.rb`
|
|
534
|
+
|
|
535
|
+
```ruby
|
|
536
|
+
# frozen_string_literal: true
|
|
537
|
+
|
|
538
|
+
module BetterTranslate
|
|
539
|
+
# Factory for creating translation providers
|
|
540
|
+
#
|
|
541
|
+
# @example
|
|
542
|
+
# provider = ProviderFactory.create(:chatgpt, config)
|
|
543
|
+
#
|
|
544
|
+
class ProviderFactory
|
|
545
|
+
# Create a provider instance
|
|
546
|
+
#
|
|
547
|
+
# @param provider_name [Symbol] Provider name (:chatgpt, :gemini, :anthropic)
|
|
548
|
+
# @param config [Configuration] Configuration object
|
|
549
|
+
# @return [Providers::BaseHttpProvider] Provider instance
|
|
550
|
+
# @raise [ProviderNotFoundError] if provider is unknown
|
|
551
|
+
def self.create(provider_name, config)
|
|
552
|
+
case provider_name
|
|
553
|
+
when :chatgpt
|
|
554
|
+
Providers::ChatGPTProvider.new(config)
|
|
555
|
+
when :gemini
|
|
556
|
+
Providers::GeminiProvider.new(config)
|
|
557
|
+
when :anthropic
|
|
558
|
+
Providers::AnthropicProvider.new(config)
|
|
559
|
+
else
|
|
560
|
+
raise ProviderNotFoundError, "Unknown provider: #{provider_name}. Supported: :chatgpt, :gemini, :anthropic"
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
[← Previous: 03-Core Components](./03-core_components.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 05-Translation Logic →](./05-translation_logic.md)
|