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,122 @@
1
+ # 06 - Main Module & API
2
+
3
+ [← Previous: 05-Translation Logic](./05-translation_logic.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 07-Direct Translation Helpers →](./07-direct_translation_helpers.md)
4
+
5
+ ---
6
+
7
+ ## Main Module & API
8
+
9
+ ### 6.1 Aggiornare `lib/better_translate.rb`
10
+
11
+ ```ruby
12
+ # frozen_string_literal: true
13
+
14
+ require_relative "better_translate/version"
15
+ require_relative "better_translate/errors"
16
+ require_relative "better_translate/configuration"
17
+ require_relative "better_translate/validator"
18
+ require_relative "better_translate/cache"
19
+ require_relative "better_translate/rate_limiter"
20
+ require_relative "better_translate/utils/hash_flattener"
21
+ require_relative "better_translate/yaml_handler"
22
+ require_relative "better_translate/progress_tracker"
23
+ require_relative "better_translate/providers/base_http_provider"
24
+ require_relative "better_translate/providers/chatgpt_provider"
25
+ require_relative "better_translate/providers/gemini_provider"
26
+ require_relative "better_translate/providers/anthropic_provider"
27
+ require_relative "better_translate/provider_factory"
28
+ require_relative "better_translate/strategies/base_strategy"
29
+ require_relative "better_translate/strategies/deep_strategy"
30
+ require_relative "better_translate/strategies/batch_strategy"
31
+ require_relative "better_translate/strategies/strategy_selector"
32
+ require_relative "better_translate/translator"
33
+
34
+ # BetterTranslate - AI-powered YAML locale file translator
35
+ #
36
+ # Automatically translate YAML locale files using AI providers (ChatGPT, Gemini, Claude).
37
+ # Features intelligent caching, batch processing, and Rails integration.
38
+ #
39
+ # @example Basic usage
40
+ # BetterTranslate.configure do |config|
41
+ # config.provider = :chatgpt
42
+ # config.openai_key = ENV['OPENAI_API_KEY']
43
+ # config.source_language = "en"
44
+ # config.target_languages = [{ short_name: "it", name: "Italian" }]
45
+ # config.input_file = "config/locales/en.yml"
46
+ # config.output_folder = "config/locales"
47
+ # end
48
+ #
49
+ # BetterTranslate.translate_all
50
+ #
51
+ # @example With advanced options
52
+ # BetterTranslate.configure do |config|
53
+ # config.provider = :anthropic
54
+ # config.anthropic_key = ENV['ANTHROPIC_API_KEY']
55
+ # config.source_language = "en"
56
+ # config.target_languages = [
57
+ # { short_name: "it", name: "Italian" },
58
+ # { short_name: "fr", name: "French" }
59
+ # ]
60
+ # config.input_file = "config/locales/en.yml"
61
+ # config.output_folder = "config/locales"
62
+ # config.translation_mode = :incremental
63
+ # config.translation_context = "E-commerce product descriptions"
64
+ # config.cache_enabled = true
65
+ # config.verbose = true
66
+ # config.global_exclusions = ["app.name"]
67
+ # end
68
+ #
69
+ # results = BetterTranslate.translate_all
70
+ # puts "Success: #{results[:success_count]}, Failures: #{results[:failure_count]}"
71
+ #
72
+ module BetterTranslate
73
+ class << self
74
+ # @return [Configuration, nil] Current configuration
75
+ attr_accessor :configuration
76
+
77
+ # Configure BetterTranslate
78
+ #
79
+ # @yieldparam config [Configuration] Configuration object to customize
80
+ # @return [Configuration] The configuration object
81
+ #
82
+ # @example
83
+ # BetterTranslate.configure do |config|
84
+ # config.provider = :chatgpt
85
+ # config.openai_key = ENV['OPENAI_API_KEY']
86
+ # end
87
+ def configure
88
+ self.configuration ||= Configuration.new
89
+ yield(configuration) if block_given?
90
+ configuration
91
+ end
92
+
93
+ # Translate all target languages
94
+ #
95
+ # @return [Hash] Results hash with :success_count, :failure_count, :errors
96
+ # @raise [ConfigurationError] if configuration is invalid
97
+ #
98
+ # @example
99
+ # results = BetterTranslate.translate_all
100
+ # puts "Translated #{results[:success_count]} languages"
101
+ def translate_all
102
+ raise ConfigurationError, "BetterTranslate not configured" unless configuration
103
+
104
+ translator = Translator.new(configuration)
105
+ translator.translate_all
106
+ end
107
+
108
+ # Reset configuration
109
+ #
110
+ # @return [void]
111
+ def reset!
112
+ self.configuration = nil
113
+ end
114
+ end
115
+ end
116
+ ```
117
+
118
+ ---
119
+
120
+ ---
121
+
122
+ [← Previous: 05-Translation Logic](./05-translation_logic.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 07-Direct Translation Helpers →](./07-direct_translation_helpers.md)
@@ -0,0 +1,582 @@
1
+ # 07 - Direct Translation Helpers
2
+
3
+ [← Previous: 06-Main Module Api](./06-main_module_api.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 08-Rails Integration →](./08-rails_integration.md)
4
+
5
+ ---
6
+
7
+ ## Direct Translation Helpers
8
+
9
+ Questa sezione implementa helper pubblici per tradurre stringhe direttamente, senza usare file YAML.
10
+
11
+ ### 6.5.1 `lib/better_translate/helpers.rb`
12
+
13
+ ```ruby
14
+ # frozen_string_literal: true
15
+
16
+ module BetterTranslate
17
+ # Helper methods for direct text translation
18
+ #
19
+ # Provides convenient methods to translate text programmatically without YAML files.
20
+ module Helpers
21
+ # Translate a single text string
22
+ #
23
+ # @param text [String] Text to translate
24
+ # @param from [String] Source language code (e.g., "en")
25
+ # @param to [String, Array<String>] Target language code(s)
26
+ # @param provider [Symbol, nil] Provider to use (:chatgpt, :gemini, :anthropic). Uses configured provider if nil.
27
+ # @param context [String, nil] Optional translation context
28
+ # @return [String, Hash] Translated text (String if single target, Hash if multiple targets)
29
+ # @raise [ConfigurationError] if BetterTranslate is not configured and no provider specified
30
+ # @raise [ValidationError] if input is invalid
31
+ # @raise [TranslationError] if translation fails
32
+ #
33
+ # @example Single target language
34
+ # BetterTranslate.translate_text("Hello", from: "en", to: "it")
35
+ # #=> "Ciao"
36
+ #
37
+ # @example Multiple target languages
38
+ # BetterTranslate.translate_text("Hello", from: "en", to: ["it", "fr", "de"])
39
+ # #=> { "it" => "Ciao", "fr" => "Bonjour", "de" => "Hallo" }
40
+ #
41
+ # @example With custom provider
42
+ # BetterTranslate.translate_text("Hello", from: "en", to: "it", provider: :anthropic)
43
+ #
44
+ # @example With context
45
+ # BetterTranslate.translate_text(
46
+ # "The patient presents with symptoms",
47
+ # from: "en",
48
+ # to: "it",
49
+ # context: "Medical terminology"
50
+ # )
51
+ #
52
+ def self.translate_text(text, from:, to:, provider: nil, context: nil)
53
+ Validator.validate_text!(text)
54
+ Validator.validate_language_code!(from)
55
+
56
+ # Normalize to array
57
+ target_langs = Array(to)
58
+ target_langs.each { |lang| Validator.validate_language_code!(lang) }
59
+
60
+ # Get provider
61
+ translation_provider = get_provider(provider, context)
62
+
63
+ # Translate
64
+ if target_langs.size == 1
65
+ # Single target - return string
66
+ translate_single(text, target_langs.first, translation_provider)
67
+ else
68
+ # Multiple targets - return hash
69
+ translate_multiple(text, target_langs, translation_provider)
70
+ end
71
+ end
72
+
73
+ # Translate multiple texts to a single target language
74
+ #
75
+ # @param texts [Array<String>] Texts to translate
76
+ # @param from [String] Source language code
77
+ # @param to [String] Target language code
78
+ # @param provider [Symbol, nil] Provider to use
79
+ # @param context [String, nil] Optional translation context
80
+ # @return [Array<String>] Translated texts
81
+ # @raise [ValidationError] if input is invalid
82
+ # @raise [TranslationError] if translation fails
83
+ #
84
+ # @example
85
+ # BetterTranslate.translate_texts(
86
+ # ["Hello", "Goodbye", "Thank you"],
87
+ # from: "en",
88
+ # to: "it"
89
+ # )
90
+ # #=> ["Ciao", "Arrivederci", "Grazie"]
91
+ #
92
+ def self.translate_texts(texts, from:, to:, provider: nil, context: nil)
93
+ raise ValidationError, "texts must be an Array" unless texts.is_a?(Array)
94
+ raise ValidationError, "texts cannot be empty" if texts.empty?
95
+
96
+ Validator.validate_language_code!(from)
97
+ Validator.validate_language_code!(to)
98
+
99
+ texts.each { |text| Validator.validate_text!(text) }
100
+
101
+ # Get provider
102
+ translation_provider = get_provider(provider, context)
103
+
104
+ # Translate each text
105
+ texts.map do |text|
106
+ translate_single(text, to, translation_provider)
107
+ end
108
+ end
109
+
110
+ # Translate a text to multiple languages (batch)
111
+ #
112
+ # @param text [String] Text to translate
113
+ # @param from [String] Source language code
114
+ # @param to [Array<Hash>] Target languages with format: [{ short_name: "it", name: "Italian" }]
115
+ # @param provider [Symbol, nil] Provider to use
116
+ # @param context [String, nil] Optional translation context
117
+ # @return [Hash] Hash with language codes as keys and translations as values
118
+ #
119
+ # @example
120
+ # languages = [
121
+ # { short_name: "it", name: "Italian" },
122
+ # { short_name: "fr", name: "French" }
123
+ # ]
124
+ # BetterTranslate.translate_text_to_languages("Hello", from: "en", to: languages)
125
+ # #=> { "it" => "Ciao", "fr" => "Bonjour" }
126
+ #
127
+ def self.translate_text_to_languages(text, from:, to:, provider: nil, context: nil)
128
+ raise ValidationError, "to must be an Array of Hashes" unless to.is_a?(Array)
129
+
130
+ Validator.validate_text!(text)
131
+ Validator.validate_language_code!(from)
132
+
133
+ # Get provider
134
+ translation_provider = get_provider(provider, context)
135
+
136
+ # Translate to each language
137
+ to.each_with_object({}) do |lang_hash, result|
138
+ lang_code = lang_hash[:short_name]
139
+ lang_name = lang_hash[:name] || lang_code
140
+
141
+ Validator.validate_language_code!(lang_code)
142
+ result[lang_code] = translation_provider.translate_text(text, lang_code, lang_name)
143
+ end
144
+ end
145
+
146
+ # Translate multiple texts to multiple languages
147
+ #
148
+ # @param texts [Array<String>] Texts to translate
149
+ # @param from [String] Source language code
150
+ # @param to [Array<Hash>] Target languages
151
+ # @param provider [Symbol, nil] Provider to use
152
+ # @param context [String, nil] Optional translation context
153
+ # @return [Hash] Nested hash: { "it" => ["Ciao", "Arrivederci"], "fr" => [...] }
154
+ #
155
+ # @example
156
+ # languages = [
157
+ # { short_name: "it", name: "Italian" },
158
+ # { short_name: "fr", name: "French" }
159
+ # ]
160
+ # BetterTranslate.translate_texts_to_languages(
161
+ # ["Hello", "Goodbye"],
162
+ # from: "en",
163
+ # to: languages
164
+ # )
165
+ # #=> { "it" => ["Ciao", "Arrivederci"], "fr" => ["Bonjour", "Au revoir"] }
166
+ #
167
+ def self.translate_texts_to_languages(texts, from:, to:, provider: nil, context: nil)
168
+ raise ValidationError, "texts must be an Array" unless texts.is_a?(Array)
169
+ raise ValidationError, "texts cannot be empty" if texts.empty?
170
+ raise ValidationError, "to must be an Array of Hashes" unless to.is_a?(Array)
171
+
172
+ Validator.validate_language_code!(from)
173
+ texts.each { |text| Validator.validate_text!(text) }
174
+
175
+ # Get provider
176
+ translation_provider = get_provider(provider, context)
177
+
178
+ # Translate to each language
179
+ to.each_with_object({}) do |lang_hash, result|
180
+ lang_code = lang_hash[:short_name]
181
+ lang_name = lang_hash[:name] || lang_code
182
+
183
+ Validator.validate_language_code!(lang_code)
184
+
185
+ # Translate all texts for this language
186
+ result[lang_code] = texts.map do |text|
187
+ translation_provider.translate_text(text, lang_code, lang_name)
188
+ end
189
+ end
190
+ end
191
+
192
+ private_class_method def self.translate_single(text, target_lang, translation_provider)
193
+ # Use language code as name if needed
194
+ lang_name = target_lang.upcase
195
+
196
+ translation_provider.translate_text(text, target_lang, lang_name)
197
+ end
198
+
199
+ private_class_method def self.translate_multiple(text, target_langs, translation_provider)
200
+ target_langs.each_with_object({}) do |lang_code, result|
201
+ result[lang_code] = translate_single(text, lang_code, translation_provider)
202
+ end
203
+ end
204
+
205
+ private_class_method def self.get_provider(provider_symbol, context)
206
+ if provider_symbol
207
+ # Use specified provider with temporary config
208
+ config = build_temp_config(provider_symbol, context)
209
+ ProviderFactory.create(provider_symbol, config)
210
+ elsif BetterTranslate.configuration
211
+ # Use existing configuration
212
+ config = BetterTranslate.configuration
213
+ config.translation_context = context if context
214
+ ProviderFactory.create(config.provider, config)
215
+ else
216
+ raise ConfigurationError, "BetterTranslate not configured. Either configure it or specify a provider."
217
+ end
218
+ end
219
+
220
+ private_class_method def self.build_temp_config(provider_symbol, context)
221
+ config = Configuration.new
222
+ config.provider = provider_symbol
223
+ config.translation_context = context if context
224
+
225
+ # Set API keys from environment
226
+ case provider_symbol
227
+ when :chatgpt
228
+ config.openai_key = ENV["OPENAI_API_KEY"]
229
+ when :gemini
230
+ config.google_gemini_key = ENV["GEMINI_API_KEY"]
231
+ when :anthropic
232
+ config.anthropic_key = ENV["ANTHROPIC_API_KEY"]
233
+ end
234
+
235
+ # Set minimal required fields for validation
236
+ config.source_language = "en"
237
+ config.target_languages = [{ short_name: "it", name: "Italian" }]
238
+ config.input_file = "/tmp/dummy.yml" # Not used for direct translation
239
+ config.output_folder = "/tmp"
240
+
241
+ config
242
+ end
243
+ end
244
+ end
245
+ ```
246
+
247
+ ### 6.5.2 Aggiornare `lib/better_translate.rb`
248
+
249
+ Aggiungere dopo la sezione dei require:
250
+
251
+ ```ruby
252
+ require_relative "better_translate/helpers"
253
+ ```
254
+
255
+ E aggiungere i metodi convenience al modulo principale:
256
+
257
+ ```ruby
258
+ module BetterTranslate
259
+ class << self
260
+ # ... existing methods ...
261
+
262
+ # Translate a text string directly
263
+ #
264
+ # @see Helpers.translate_text
265
+ def translate_text(text, from:, to:, provider: nil, context: nil)
266
+ Helpers.translate_text(text, from: from, to: to, provider: provider, context: context)
267
+ end
268
+
269
+ # Translate multiple texts directly
270
+ #
271
+ # @see Helpers.translate_texts
272
+ def translate_texts(texts, from:, to:, provider: nil, context: nil)
273
+ Helpers.translate_texts(texts, from: from, to: to, provider: provider, context: context)
274
+ end
275
+
276
+ # Translate text to multiple languages
277
+ #
278
+ # @see Helpers.translate_text_to_languages
279
+ def translate_text_to_languages(text, from:, to:, provider: nil, context: nil)
280
+ Helpers.translate_text_to_languages(text, from: from, to: to, provider: provider, context: context)
281
+ end
282
+
283
+ # Translate multiple texts to multiple languages
284
+ #
285
+ # @see Helpers.translate_texts_to_languages
286
+ def translate_texts_to_languages(texts, from:, to:, provider: nil, context: nil)
287
+ Helpers.translate_texts_to_languages(texts, from: from, to: to, provider: provider, context: context)
288
+ end
289
+ end
290
+ end
291
+ ```
292
+
293
+ ### 6.5.3 Test: `spec/better_translate/helpers_spec.rb`
294
+
295
+ ```ruby
296
+ # frozen_string_literal: true
297
+
298
+ RSpec.describe BetterTranslate::Helpers do
299
+ describe ".translate_text" do
300
+ let(:config) { build_config }
301
+
302
+ before do
303
+ BetterTranslate.configure do |c|
304
+ c.provider = :chatgpt
305
+ c.openai_key = "test-key"
306
+ c.source_language = "en"
307
+ c.target_languages = [{ short_name: "it", name: "Italian" }]
308
+ c.input_file = create_temp_yaml("en" => { "test" => "test" })
309
+ c.output_folder = Dir.mktmpdir
310
+ end
311
+ end
312
+
313
+ context "with single target language" do
314
+ it "returns translated string", :vcr do
315
+ result = described_class.translate_text("Hello", from: "en", to: "it")
316
+ expect(result).to be_a(String)
317
+ expect(result).to eq("Ciao")
318
+ end
319
+ end
320
+
321
+ context "with multiple target languages" do
322
+ it "returns hash of translations", :vcr do
323
+ result = described_class.translate_text("Hello", from: "en", to: ["it", "fr"])
324
+ expect(result).to be_a(Hash)
325
+ expect(result.keys).to contain_exactly("it", "fr")
326
+ expect(result["it"]).to eq("Ciao")
327
+ expect(result["fr"]).to eq("Bonjour")
328
+ end
329
+ end
330
+
331
+ context "with custom provider" do
332
+ it "uses specified provider", :vcr do
333
+ result = described_class.translate_text(
334
+ "Hello",
335
+ from: "en",
336
+ to: "it",
337
+ provider: :gemini
338
+ )
339
+ expect(result).to eq("Ciao")
340
+ end
341
+ end
342
+
343
+ context "with context" do
344
+ it "includes context in translation", :vcr do
345
+ result = described_class.translate_text(
346
+ "The patient presents with symptoms",
347
+ from: "en",
348
+ to: "it",
349
+ context: "Medical terminology"
350
+ )
351
+ expect(result).to be_a(String)
352
+ end
353
+ end
354
+
355
+ it "validates text input" do
356
+ expect {
357
+ described_class.translate_text("", from: "en", to: "it")
358
+ }.to raise_error(BetterTranslate::ValidationError, /cannot be empty/)
359
+ end
360
+
361
+ it "validates language codes" do
362
+ expect {
363
+ described_class.translate_text("Hello", from: "invalid", to: "it")
364
+ }.to raise_error(BetterTranslate::ValidationError, /must be 2 letters/)
365
+ end
366
+ end
367
+
368
+ describe ".translate_texts" do
369
+ before do
370
+ BetterTranslate.configure do |c|
371
+ c.provider = :chatgpt
372
+ c.openai_key = "test-key"
373
+ c.source_language = "en"
374
+ c.target_languages = [{ short_name: "it", name: "Italian" }]
375
+ c.input_file = create_temp_yaml("en" => { "test" => "test" })
376
+ c.output_folder = Dir.mktmpdir
377
+ end
378
+ end
379
+
380
+ it "translates array of texts", :vcr do
381
+ result = described_class.translate_texts(
382
+ ["Hello", "Goodbye"],
383
+ from: "en",
384
+ to: "it"
385
+ )
386
+ expect(result).to be_an(Array)
387
+ expect(result).to eq(["Ciao", "Arrivederci"])
388
+ end
389
+
390
+ it "validates input is array" do
391
+ expect {
392
+ described_class.translate_texts("Hello", from: "en", to: "it")
393
+ }.to raise_error(BetterTranslate::ValidationError, /must be an Array/)
394
+ end
395
+
396
+ it "validates array is not empty" do
397
+ expect {
398
+ described_class.translate_texts([], from: "en", to: "it")
399
+ }.to raise_error(BetterTranslate::ValidationError, /cannot be empty/)
400
+ end
401
+ end
402
+
403
+ describe ".translate_text_to_languages" do
404
+ let(:languages) do
405
+ [
406
+ { short_name: "it", name: "Italian" },
407
+ { short_name: "fr", name: "French" }
408
+ ]
409
+ end
410
+
411
+ before do
412
+ BetterTranslate.configure do |c|
413
+ c.provider = :chatgpt
414
+ c.openai_key = "test-key"
415
+ c.source_language = "en"
416
+ c.target_languages = languages
417
+ c.input_file = create_temp_yaml("en" => { "test" => "test" })
418
+ c.output_folder = Dir.mktmpdir
419
+ end
420
+ end
421
+
422
+ it "translates to multiple languages", :vcr do
423
+ result = described_class.translate_text_to_languages(
424
+ "Hello",
425
+ from: "en",
426
+ to: languages
427
+ )
428
+ expect(result).to be_a(Hash)
429
+ expect(result.keys).to contain_exactly("it", "fr")
430
+ expect(result["it"]).to eq("Ciao")
431
+ expect(result["fr"]).to eq("Bonjour")
432
+ end
433
+ end
434
+
435
+ describe ".translate_texts_to_languages" do
436
+ let(:languages) do
437
+ [
438
+ { short_name: "it", name: "Italian" },
439
+ { short_name: "fr", name: "French" }
440
+ ]
441
+ end
442
+
443
+ before do
444
+ BetterTranslate.configure do |c|
445
+ c.provider = :chatgpt
446
+ c.openai_key = "test-key"
447
+ c.source_language = "en"
448
+ c.target_languages = languages
449
+ c.input_file = create_temp_yaml("en" => { "test" => "test" })
450
+ c.output_folder = Dir.mktmpdir
451
+ end
452
+ end
453
+
454
+ it "translates multiple texts to multiple languages", :vcr do
455
+ result = described_class.translate_texts_to_languages(
456
+ ["Hello", "Goodbye"],
457
+ from: "en",
458
+ to: languages
459
+ )
460
+ expect(result).to be_a(Hash)
461
+ expect(result["it"]).to eq(["Ciao", "Arrivederci"])
462
+ expect(result["fr"]).to eq(["Bonjour", "Au revoir"])
463
+ end
464
+ end
465
+ end
466
+
467
+ # Test module-level convenience methods
468
+ RSpec.describe BetterTranslate do
469
+ describe ".translate_text" do
470
+ it "delegates to Helpers.translate_text" do
471
+ expect(BetterTranslate::Helpers).to receive(:translate_text).with(
472
+ "Hello",
473
+ from: "en",
474
+ to: "it",
475
+ provider: nil,
476
+ context: nil
477
+ )
478
+
479
+ BetterTranslate.translate_text("Hello", from: "en", to: "it")
480
+ end
481
+ end
482
+ end
483
+ ```
484
+
485
+ ### 6.5.4 Example: `examples/direct_translation.rb`
486
+
487
+ ```ruby
488
+ #!/usr/bin/env ruby
489
+ # frozen_string_literal: true
490
+
491
+ require "bundler/setup"
492
+ require "better_translate"
493
+
494
+ # Configure BetterTranslate
495
+ BetterTranslate.configure do |config|
496
+ config.provider = :chatgpt
497
+ config.openai_key = ENV["OPENAI_API_KEY"]
498
+ # Minimal configuration for direct translation
499
+ config.source_language = "en"
500
+ config.target_languages = [{ short_name: "it", name: "Italian" }]
501
+ config.input_file = "/tmp/dummy.yml"
502
+ config.output_folder = "/tmp"
503
+ end
504
+
505
+ puts "=" * 80
506
+ puts "Direct Translation Examples"
507
+ puts "=" * 80
508
+
509
+ # Example 1: Single text, single target
510
+ puts "\n1. Translate to single language:"
511
+ result = BetterTranslate.translate_text("Hello, world!", from: "en", to: "it")
512
+ puts " EN: Hello, world!"
513
+ puts " IT: #{result}"
514
+
515
+ # Example 2: Single text, multiple targets
516
+ puts "\n2. Translate to multiple languages:"
517
+ result = BetterTranslate.translate_text("Good morning", from: "en", to: ["it", "fr", "de"])
518
+ puts " EN: Good morning"
519
+ result.each do |lang, translation|
520
+ puts " #{lang.upcase}: #{translation}"
521
+ end
522
+
523
+ # Example 3: Multiple texts, single target
524
+ puts "\n3. Translate multiple texts:"
525
+ texts = ["Hello", "Goodbye", "Thank you"]
526
+ results = BetterTranslate.translate_texts(texts, from: "en", to: "it")
527
+ texts.each_with_index do |text, i|
528
+ puts " #{text} => #{results[i]}"
529
+ end
530
+
531
+ # Example 4: With translation context
532
+ puts "\n4. Translation with context (medical):"
533
+ result = BetterTranslate.translate_text(
534
+ "The patient presents with acute symptoms",
535
+ from: "en",
536
+ to: "it",
537
+ context: "Medical terminology for healthcare professionals"
538
+ )
539
+ puts " EN: The patient presents with acute symptoms"
540
+ puts " IT: #{result}"
541
+
542
+ # Example 5: Using language hashes (like in YAML translation)
543
+ puts "\n5. Translate with full language info:"
544
+ languages = [
545
+ { short_name: "it", name: "Italian" },
546
+ { short_name: "fr", name: "French" },
547
+ { short_name: "de", name: "German" }
548
+ ]
549
+
550
+ result = BetterTranslate.translate_text_to_languages(
551
+ "Welcome to our application",
552
+ from: "en",
553
+ to: languages
554
+ )
555
+
556
+ puts " EN: Welcome to our application"
557
+ result.each do |lang_code, translation|
558
+ lang_name = languages.find { |l| l[:short_name] == lang_code }[:name]
559
+ puts " #{lang_name} (#{lang_code}): #{translation}"
560
+ end
561
+
562
+ # Example 6: Without global configuration (using provider parameter)
563
+ puts "\n6. Direct translation without global config:"
564
+ BetterTranslate.reset! # Clear configuration
565
+
566
+ result = BetterTranslate.translate_text(
567
+ "Hello",
568
+ from: "en",
569
+ to: "it",
570
+ provider: :anthropic # Uses ENV['ANTHROPIC_API_KEY']
571
+ )
572
+ puts " Using Anthropic provider directly"
573
+ puts " Hello => #{result}"
574
+
575
+ puts "\n" + "=" * 80
576
+ ```
577
+
578
+ ---
579
+
580
+ ---
581
+
582
+ [← Previous: 06-Main Module Api](./06-main_module_api.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 08-Rails Integration →](./08-rails_integration.md)