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.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +14 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +8 -0
  5. data/.yardopts +10 -0
  6. data/CHANGELOG.md +125 -114
  7. data/CLAUDE.md +385 -0
  8. data/README.md +629 -244
  9. data/Rakefile +7 -1
  10. data/Steepfile +29 -0
  11. data/docs/implementation/00-overview.md +220 -0
  12. data/docs/implementation/01-setup_dependencies.md +668 -0
  13. data/docs/implementation/02-error_handling.md +65 -0
  14. data/docs/implementation/03-core_components.md +457 -0
  15. data/docs/implementation/03.5-variable_preservation.md +509 -0
  16. data/docs/implementation/04-provider_architecture.md +571 -0
  17. data/docs/implementation/05-translation_logic.md +1065 -0
  18. data/docs/implementation/06-main_module_api.md +122 -0
  19. data/docs/implementation/07-direct_translation_helpers.md +582 -0
  20. data/docs/implementation/08-rails_integration.md +323 -0
  21. data/docs/implementation/09-testing_suite.md +228 -0
  22. data/docs/implementation/10-documentation_examples.md +150 -0
  23. data/docs/implementation/11-quality_security.md +65 -0
  24. data/docs/implementation/12-cli_standalone.md +698 -0
  25. data/exe/better_translate +9 -0
  26. data/lib/better_translate/cache.rb +125 -0
  27. data/lib/better_translate/cli.rb +304 -0
  28. data/lib/better_translate/configuration.rb +201 -0
  29. data/lib/better_translate/direct_translator.rb +131 -0
  30. data/lib/better_translate/errors.rb +101 -0
  31. data/lib/better_translate/progress_tracker.rb +157 -0
  32. data/lib/better_translate/provider_factory.rb +45 -0
  33. data/lib/better_translate/providers/anthropic_provider.rb +154 -0
  34. data/lib/better_translate/providers/base_http_provider.rb +239 -0
  35. data/lib/better_translate/providers/chatgpt_provider.rb +138 -44
  36. data/lib/better_translate/providers/gemini_provider.rb +123 -61
  37. data/lib/better_translate/railtie.rb +18 -0
  38. data/lib/better_translate/rate_limiter.rb +90 -0
  39. data/lib/better_translate/strategies/base_strategy.rb +58 -0
  40. data/lib/better_translate/strategies/batch_strategy.rb +56 -0
  41. data/lib/better_translate/strategies/deep_strategy.rb +45 -0
  42. data/lib/better_translate/strategies/strategy_selector.rb +43 -0
  43. data/lib/better_translate/translator.rb +115 -284
  44. data/lib/better_translate/utils/hash_flattener.rb +104 -0
  45. data/lib/better_translate/validator.rb +105 -0
  46. data/lib/better_translate/variable_extractor.rb +259 -0
  47. data/lib/better_translate/version.rb +2 -9
  48. data/lib/better_translate/yaml_handler.rb +168 -0
  49. data/lib/better_translate.rb +97 -73
  50. data/lib/generators/better_translate/analyze/USAGE +12 -0
  51. data/lib/generators/better_translate/analyze/analyze_generator.rb +94 -0
  52. data/lib/generators/better_translate/install/USAGE +13 -0
  53. data/lib/generators/better_translate/install/install_generator.rb +71 -0
  54. data/lib/generators/better_translate/install/templates/README +20 -0
  55. data/lib/generators/better_translate/install/templates/initializer.rb.tt +47 -0
  56. data/lib/generators/better_translate/translate/USAGE +13 -0
  57. data/lib/generators/better_translate/translate/translate_generator.rb +114 -0
  58. data/lib/tasks/better_translate.rake +136 -0
  59. data/sig/better_translate/cache.rbs +28 -0
  60. data/sig/better_translate/cli.rbs +24 -0
  61. data/sig/better_translate/configuration.rbs +78 -0
  62. data/sig/better_translate/direct_translator.rbs +18 -0
  63. data/sig/better_translate/errors.rbs +46 -0
  64. data/sig/better_translate/progress_tracker.rbs +29 -0
  65. data/sig/better_translate/provider_factory.rbs +8 -0
  66. data/sig/better_translate/providers/anthropic_provider.rbs +27 -0
  67. data/sig/better_translate/providers/base_http_provider.rbs +44 -0
  68. data/sig/better_translate/providers/chatgpt_provider.rbs +25 -0
  69. data/sig/better_translate/providers/gemini_provider.rbs +22 -0
  70. data/sig/better_translate/railtie.rbs +7 -0
  71. data/sig/better_translate/rate_limiter.rbs +20 -0
  72. data/sig/better_translate/strategies/base_strategy.rbs +19 -0
  73. data/sig/better_translate/strategies/batch_strategy.rbs +13 -0
  74. data/sig/better_translate/strategies/deep_strategy.rbs +11 -0
  75. data/sig/better_translate/strategies/strategy_selector.rbs +10 -0
  76. data/sig/better_translate/translator.rbs +24 -0
  77. data/sig/better_translate/utils/hash_flattener.rbs +14 -0
  78. data/sig/better_translate/validator.rbs +14 -0
  79. data/sig/better_translate/variable_extractor.rbs +40 -0
  80. data/sig/better_translate/version.rbs +4 -0
  81. data/sig/better_translate/yaml_handler.rbs +29 -0
  82. data/sig/better_translate.rbs +32 -2
  83. data/sig/faraday.rbs +22 -0
  84. data/sig/generators/better_translate/analyze/analyze_generator.rbs +18 -0
  85. data/sig/generators/better_translate/install/install_generator.rbs +14 -0
  86. data/sig/generators/better_translate/translate/translate_generator.rbs +10 -0
  87. data/sig/optparse.rbs +9 -0
  88. data/sig/psych.rbs +5 -0
  89. data/sig/rails.rbs +34 -0
  90. metadata +89 -203
  91. data/lib/better_translate/helper.rb +0 -83
  92. data/lib/better_translate/providers/base_provider.rb +0 -102
  93. data/lib/better_translate/service.rb +0 -144
  94. data/lib/better_translate/similarity_analyzer.rb +0 -218
  95. data/lib/better_translate/utils.rb +0 -55
  96. data/lib/better_translate/writer.rb +0 -75
  97. data/lib/generators/better_translate/analyze_generator.rb +0 -57
  98. data/lib/generators/better_translate/install_generator.rb +0 -14
  99. data/lib/generators/better_translate/templates/better_translate.rb +0 -56
  100. 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)