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,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterTranslate
4
+ # LRU (Least Recently Used) Cache implementation
5
+ #
6
+ # Thread-safe cache with configurable capacity and optional TTL.
7
+ #
8
+ # @example Basic usage
9
+ # cache = Cache.new(capacity: 100)
10
+ # cache.set("hello:it", "ciao")
11
+ # cache.get("hello:it") #=> "ciao"
12
+ #
13
+ # @example With TTL
14
+ # cache = Cache.new(capacity: 100, ttl: 3600)
15
+ # cache.set("key", "value")
16
+ # # After 3600 seconds, get("key") will return nil
17
+ #
18
+ class Cache
19
+ # @return [Integer] Maximum number of items in cache
20
+ attr_reader :capacity
21
+
22
+ # @return [Integer, nil] Time to live in seconds
23
+ attr_reader :ttl
24
+
25
+ # Initialize a new cache
26
+ #
27
+ # @param capacity [Integer] Maximum cache size
28
+ # @param ttl [Integer, nil] Time to live in seconds (nil = no expiration)
29
+ #
30
+ # @example Create cache with capacity and TTL
31
+ # cache = Cache.new(capacity: 500, ttl: 1800)
32
+ #
33
+ def initialize(capacity: 1000, ttl: nil)
34
+ @capacity = capacity
35
+ @ttl = ttl
36
+ @cache = {}
37
+ @mutex = Mutex.new
38
+ end
39
+
40
+ # Get a value from the cache
41
+ #
42
+ # @param key [String] Cache key
43
+ # @return [String, nil] Cached value or nil if not found/expired
44
+ #
45
+ # @example Get cached value
46
+ # value = cache.get("translation:en:it:hello")
47
+ #
48
+ def get(key)
49
+ @mutex.synchronize do
50
+ return nil unless @cache.key?(key)
51
+
52
+ entry = @cache[key]
53
+
54
+ # Check TTL
55
+ if @ttl && entry && Time.now - entry[:timestamp] > @ttl
56
+ @cache.delete(key)
57
+ return nil
58
+ end
59
+
60
+ # Move to end (most recently used)
61
+ @cache.delete(key)
62
+ @cache[key] = entry
63
+ entry[:value]
64
+ end
65
+ end
66
+
67
+ # Set a value in the cache
68
+ #
69
+ # @param key [String] Cache key
70
+ # @param value [String] Value to cache
71
+ # @return [String] The cached value
72
+ #
73
+ # @example Store value in cache
74
+ # cache.set("translation:en:it:hello", "ciao")
75
+ #
76
+ def set(key, value)
77
+ @mutex.synchronize do
78
+ # Remove oldest entry if at capacity
79
+ @cache.shift if @cache.size >= @capacity && !@cache.key?(key)
80
+
81
+ @cache[key] = {
82
+ value: value,
83
+ timestamp: Time.now
84
+ }
85
+ value
86
+ end
87
+ end
88
+
89
+ # Clear the cache
90
+ #
91
+ # @return [void]
92
+ #
93
+ # @example Clear all cached values
94
+ # cache.clear
95
+ #
96
+ def clear
97
+ @mutex.synchronize { @cache.clear }
98
+ end
99
+
100
+ # Get cache size
101
+ #
102
+ # @return [Integer] Number of items in cache
103
+ #
104
+ # @example Check cache size
105
+ # puts "Cache contains #{cache.size} items"
106
+ #
107
+ def size
108
+ @mutex.synchronize { @cache.size }
109
+ end
110
+
111
+ # Check if key exists in cache
112
+ #
113
+ # @param key [String] Cache key
114
+ # @return [Boolean] true if key exists and not expired
115
+ #
116
+ # @example Check if key exists
117
+ # if cache.key?("translation:en:it:hello")
118
+ # puts "Translation is cached"
119
+ # end
120
+ #
121
+ def key?(key)
122
+ !get(key).nil?
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,304 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "yaml"
5
+
6
+ module BetterTranslate
7
+ # Command-line interface for BetterTranslate
8
+ #
9
+ # Provides standalone CLI commands for translation without Rails.
10
+ #
11
+ # @example Run translation from config file
12
+ # cli = CLI.new(["translate", "--config", "config.yml"])
13
+ # cli.run
14
+ #
15
+ # @example Generate config file
16
+ # cli = CLI.new(["generate", "config.yml"])
17
+ # cli.run
18
+ #
19
+ # @example Direct translation
20
+ # cli = CLI.new(["direct", "Hello", "--to", "it", "--provider", "chatgpt"])
21
+ # cli.run
22
+ #
23
+ class CLI
24
+ # @return [Array<String>] Command-line arguments
25
+ attr_reader :args
26
+
27
+ # Initialize CLI
28
+ #
29
+ # @param args [Array<String>] Command-line arguments
30
+ #
31
+ # @example
32
+ # cli = CLI.new(ARGV)
33
+ #
34
+ def initialize(args)
35
+ @args = args
36
+ end
37
+
38
+ # Run CLI command
39
+ #
40
+ # @return [void]
41
+ #
42
+ # @example
43
+ # cli = CLI.new(ARGV)
44
+ # cli.run
45
+ #
46
+ def run
47
+ command = args.first
48
+
49
+ case command
50
+ when "translate"
51
+ run_translate
52
+ when "generate"
53
+ run_generate
54
+ when "direct"
55
+ run_direct
56
+ when "--version", "-v"
57
+ puts "BetterTranslate version #{VERSION}"
58
+ when "--help", "-h", nil
59
+ show_help
60
+ else
61
+ puts "Unknown command: #{command}"
62
+ puts
63
+ show_help
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ # Show help message
70
+ #
71
+ # @return [void]
72
+ # @api private
73
+ #
74
+ def show_help
75
+ puts <<~HELP
76
+ Usage: better_translate COMMAND [OPTIONS]
77
+
78
+ Commands:
79
+ translate Translate YAML files using config file
80
+ generate OUTPUT_FILE Generate sample config file
81
+ direct TEXT Translate text directly
82
+
83
+ Options:
84
+ --help, -h Show this help message
85
+ --version, -v Show version
86
+
87
+ Examples:
88
+ better_translate translate --config config.yml
89
+ better_translate generate config.yml
90
+ better_translate direct "Hello" --to it --provider chatgpt --api-key KEY
91
+ HELP
92
+ end
93
+
94
+ # Run translate command
95
+ #
96
+ # @return [void]
97
+ # @api private
98
+ #
99
+ def run_translate
100
+ # @type var options: Hash[Symbol, String]
101
+ options = {}
102
+ OptionParser.new do |opts|
103
+ opts.on("--config FILE", "Config file path") { |v| options[:config] = v }
104
+ end.parse!(args[1..])
105
+
106
+ unless options[:config]
107
+ puts "Error: --config is required"
108
+ return
109
+ end
110
+
111
+ unless File.exist?(options[:config])
112
+ puts "Error: Config file not found: #{options[:config]}"
113
+ return
114
+ end
115
+
116
+ # Load configuration from YAML
117
+ yaml_config = YAML.load_file(options[:config])
118
+
119
+ # Configure BetterTranslate
120
+ BetterTranslate.configure do |config|
121
+ config.provider = yaml_config["provider"]&.to_sym
122
+ config.openai_key = yaml_config["openai_key"] || ENV["OPENAI_API_KEY"]
123
+ config.gemini_key = yaml_config["gemini_key"] || ENV["GEMINI_API_KEY"]
124
+ config.anthropic_key = yaml_config["anthropic_key"] || ENV["ANTHROPIC_API_KEY"]
125
+
126
+ config.source_language = yaml_config["source_language"]
127
+ config.target_languages = yaml_config["target_languages"]&.map do |lang|
128
+ if lang.is_a?(Hash)
129
+ { short_name: lang["short_name"], name: lang["name"] }
130
+ else
131
+ lang
132
+ end
133
+ end
134
+
135
+ config.input_file = yaml_config["input_file"]
136
+ config.output_folder = yaml_config["output_folder"]
137
+ config.verbose = yaml_config.fetch("verbose", true)
138
+ config.dry_run = yaml_config.fetch("dry_run", false)
139
+
140
+ # Map "full" to :override for backward compatibility
141
+ translation_mode = yaml_config.fetch("translation_mode", "override")
142
+ translation_mode = "override" if translation_mode == "full"
143
+ config.translation_mode = translation_mode.to_sym
144
+
145
+ config.preserve_variables = yaml_config.fetch("preserve_variables", true)
146
+
147
+ # Exclusions
148
+ config.global_exclusions = yaml_config["global_exclusions"] || []
149
+ config.exclusions_per_language = yaml_config["exclusions_per_language"] || {}
150
+
151
+ # Provider options
152
+ config.model = yaml_config["model"] if yaml_config["model"]
153
+ config.temperature = yaml_config["temperature"] if yaml_config["temperature"]
154
+ config.max_tokens = yaml_config["max_tokens"] if yaml_config["max_tokens"]
155
+ config.timeout = yaml_config["timeout"] if yaml_config["timeout"]
156
+ config.max_retries = yaml_config["max_retries"] if yaml_config["max_retries"]
157
+ config.rate_limit = yaml_config["rate_limit"] if yaml_config["rate_limit"]
158
+ end
159
+
160
+ # Perform translation
161
+ puts "Starting translation..."
162
+ puts "Provider: #{BetterTranslate.configuration.provider}"
163
+ puts "Source: #{BetterTranslate.configuration.source_language}"
164
+ puts "Targets: #{BetterTranslate.configuration.target_languages.map { |l| l[:name] }.join(", ")}"
165
+ puts
166
+
167
+ results = BetterTranslate.translate_files
168
+
169
+ # Report results
170
+ puts
171
+ puts "=" * 60
172
+ puts "Translation Complete"
173
+ puts "=" * 60
174
+ puts "Success: #{results[:success_count]}"
175
+ puts "Failure: #{results[:failure_count]}"
176
+
177
+ return unless results[:errors].any?
178
+
179
+ puts
180
+ puts "Errors:"
181
+ results[:errors].each do |error|
182
+ puts " - #{error[:language]}: #{error[:error]}"
183
+ end
184
+ end
185
+
186
+ # Run generate command
187
+ #
188
+ # @return [void]
189
+ # @api private
190
+ #
191
+ def run_generate
192
+ output_file = args[1]
193
+ force = args.include?("--force")
194
+
195
+ unless output_file
196
+ puts "Error: Output file path is required"
197
+ puts "Usage: better_translate generate OUTPUT_FILE [--force]"
198
+ return
199
+ end
200
+
201
+ if File.exist?(output_file) && !force
202
+ puts "Config file already exists at #{output_file}"
203
+ puts "Use --force to overwrite"
204
+ return
205
+ end
206
+
207
+ sample_config = {
208
+ "provider" => "chatgpt",
209
+ "openai_key" => "YOUR_OPENAI_API_KEY",
210
+ "gemini_key" => "YOUR_GEMINI_API_KEY",
211
+ "anthropic_key" => "YOUR_ANTHROPIC_API_KEY",
212
+ "source_language" => "en",
213
+ "target_languages" => [
214
+ { "short_name" => "it", "name" => "Italian" },
215
+ { "short_name" => "es", "name" => "Spanish" },
216
+ { "short_name" => "fr", "name" => "French" }
217
+ ],
218
+ "input_file" => "locales/en.yml",
219
+ "output_folder" => "locales",
220
+ "verbose" => true,
221
+ "dry_run" => false,
222
+ "translation_mode" => "override",
223
+ "preserve_variables" => true,
224
+ "global_exclusions" => Array.new,
225
+ "exclusions_per_language" => Hash.new,
226
+ "model" => nil,
227
+ "temperature" => 0.3,
228
+ "max_tokens" => 2000,
229
+ "timeout" => 30,
230
+ "max_retries" => 3,
231
+ "rate_limit" => 10
232
+ }
233
+
234
+ File.write(output_file, sample_config.to_yaml)
235
+ puts "Generated configuration file at #{output_file}"
236
+ puts "Please edit it with your API keys and preferences."
237
+ end
238
+
239
+ # Run direct translation command
240
+ #
241
+ # @return [void]
242
+ # @api private
243
+ #
244
+ def run_direct
245
+ text = args[1]
246
+ # @type var options: Hash[Symbol, String]
247
+ options = {}
248
+
249
+ remaining_args = args[2..] || []
250
+ OptionParser.new do |opts|
251
+ opts.on("--to LANG", "Target language code") { |v| options[:to] = v }
252
+ opts.on("--language-name NAME", "Target language name") { |v| options[:language_name] = v }
253
+ opts.on("--provider PROVIDER", "Provider (chatgpt, gemini, anthropic)") { |v| options[:provider] = v }
254
+ opts.on("--api-key KEY", "API key") { |v| options[:api_key] = v }
255
+ end.parse!(remaining_args)
256
+
257
+ unless text
258
+ puts "Error: Text is required"
259
+ puts "Usage: better_translate direct TEXT --to LANG --provider PROVIDER --api-key KEY"
260
+ return
261
+ end
262
+
263
+ unless options[:to]
264
+ puts "Error: --to is required"
265
+ return
266
+ end
267
+
268
+ unless options[:provider]
269
+ puts "Error: --provider is required"
270
+ return
271
+ end
272
+
273
+ # Default language name
274
+ options[:language_name] ||= options[:to].upcase
275
+
276
+ # Configure
277
+ BetterTranslate.configure do |config|
278
+ config.provider = options[:provider].to_sym
279
+ config.source_language = "en"
280
+
281
+ case options[:provider].to_sym
282
+ when :chatgpt
283
+ config.openai_key = options[:api_key] || ENV["OPENAI_API_KEY"]
284
+ when :gemini
285
+ config.gemini_key = options[:api_key] || ENV["GEMINI_API_KEY"]
286
+ when :anthropic
287
+ config.anthropic_key = options[:api_key] || ENV["ANTHROPIC_API_KEY"]
288
+ end
289
+ end
290
+
291
+ # Translate
292
+ translator = DirectTranslator.new(BetterTranslate.configuration)
293
+ result = translator.translate(
294
+ text,
295
+ to: options[:to],
296
+ language_name: options[:language_name]
297
+ )
298
+
299
+ puts result
300
+ rescue StandardError => e
301
+ puts "Error: #{e.message}"
302
+ end
303
+ end
304
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterTranslate
4
+ # Configuration class for BetterTranslate
5
+ #
6
+ # Manages all configuration options with type safety and validation.
7
+ #
8
+ # @example Basic configuration
9
+ # config = Configuration.new
10
+ # config.provider = :chatgpt
11
+ # config.openai_key = ENV['OPENAI_API_KEY']
12
+ # config.source_language = "en"
13
+ # config.target_languages = [{ short_name: "it", name: "Italian" }]
14
+ # config.validate!
15
+ #
16
+ class Configuration
17
+ # @return [Symbol] The translation provider (:chatgpt, :gemini, :anthropic)
18
+ attr_accessor :provider
19
+
20
+ # @return [String, nil] OpenAI API key
21
+ attr_accessor :openai_key
22
+
23
+ # @return [String, nil] Google Gemini API key
24
+ attr_accessor :google_gemini_key
25
+
26
+ # Alias for google_gemini_key (for convenience in configuration)
27
+ alias gemini_key google_gemini_key
28
+ alias gemini_key= google_gemini_key=
29
+
30
+ # @return [String, nil] Anthropic API key (Claude)
31
+ attr_accessor :claude_key
32
+
33
+ # Alias for claude_key (for convenience in configuration)
34
+ alias anthropic_key claude_key
35
+ alias anthropic_key= claude_key=
36
+
37
+ # @return [String] Source language code (e.g., "en")
38
+ attr_accessor :source_language
39
+
40
+ # @return [Array<Hash>] Target languages with :short_name and :name
41
+ attr_accessor :target_languages
42
+
43
+ # @return [String] Path to input YAML file
44
+ attr_accessor :input_file
45
+
46
+ # @return [String] Output folder for translated files
47
+ attr_accessor :output_folder
48
+
49
+ # @return [Symbol] Translation mode (:override or :incremental)
50
+ attr_accessor :translation_mode
51
+
52
+ # @return [String, nil] Translation context for domain-specific terminology
53
+ attr_accessor :translation_context
54
+
55
+ # @return [Integer] Maximum concurrent requests
56
+ attr_accessor :max_concurrent_requests
57
+
58
+ # @return [Integer] Request timeout in seconds
59
+ attr_accessor :request_timeout
60
+
61
+ # @return [Integer] Maximum number of retries
62
+ attr_accessor :max_retries
63
+
64
+ # @return [Float] Retry delay in seconds
65
+ attr_accessor :retry_delay
66
+
67
+ # @return [Boolean] Enable/disable caching
68
+ attr_accessor :cache_enabled
69
+
70
+ # @return [Integer] Cache size (LRU capacity)
71
+ attr_accessor :cache_size
72
+
73
+ # @return [Integer, nil] Cache TTL in seconds (nil = no expiration)
74
+ attr_accessor :cache_ttl
75
+
76
+ # @return [Boolean] Verbose logging
77
+ attr_accessor :verbose
78
+
79
+ # @return [Boolean] Dry run mode (no files written)
80
+ attr_accessor :dry_run
81
+
82
+ # @return [Array<String>] Global exclusions (apply to all languages)
83
+ attr_accessor :global_exclusions
84
+
85
+ # @return [Hash] Language-specific exclusions
86
+ attr_accessor :exclusions_per_language
87
+
88
+ # @return [Boolean] Preserve interpolation variables during translation (default: true)
89
+ attr_accessor :preserve_variables
90
+
91
+ # Initialize a new configuration with defaults
92
+ def initialize
93
+ @translation_mode = :override
94
+ @max_concurrent_requests = 3
95
+ @request_timeout = 30
96
+ @max_retries = 3
97
+ @retry_delay = 2.0
98
+ @cache_enabled = true
99
+ @cache_size = 1000
100
+ @cache_ttl = nil
101
+ @verbose = false
102
+ @dry_run = false
103
+ @global_exclusions = []
104
+ @exclusions_per_language = {}
105
+ @target_languages = []
106
+ @preserve_variables = true
107
+ end
108
+
109
+ # Validate the configuration
110
+ #
111
+ # @raise [ConfigurationError] if configuration is invalid
112
+ # @return [true] if configuration is valid
113
+ def validate!
114
+ validate_provider!
115
+ validate_api_keys!
116
+ validate_languages!
117
+ validate_files!
118
+ validate_optional_settings!
119
+ true
120
+ end
121
+
122
+ private
123
+
124
+ # Validate provider configuration
125
+ #
126
+ # @raise [ConfigurationError] if provider is not set or not a Symbol
127
+ # @return [void]
128
+ # @api private
129
+ def validate_provider!
130
+ raise ConfigurationError, "Provider must be set" if provider.nil?
131
+ raise ConfigurationError, "Provider must be a Symbol" unless provider.is_a?(Symbol)
132
+ end
133
+
134
+ # Validate API keys based on selected provider
135
+ #
136
+ # @raise [ConfigurationError] if required API key is missing
137
+ # @return [void]
138
+ # @api private
139
+ def validate_api_keys!
140
+ case provider
141
+ when :chatgpt
142
+ if openai_key.nil? || openai_key.empty?
143
+ raise ConfigurationError, "OpenAI API key is required for ChatGPT provider"
144
+ end
145
+ when :gemini
146
+ if google_gemini_key.nil? || google_gemini_key.empty?
147
+ raise ConfigurationError, "Google Gemini API key is required for Gemini provider"
148
+ end
149
+ when :anthropic
150
+ if anthropic_key.nil? || anthropic_key.empty?
151
+ raise ConfigurationError, "Anthropic API key is required for Anthropic provider"
152
+ end
153
+ end
154
+ end
155
+
156
+ # Validate language configuration
157
+ #
158
+ # @raise [ConfigurationError] if source or target languages are invalid
159
+ # @return [void]
160
+ # @api private
161
+ def validate_languages!
162
+ raise ConfigurationError, "Source language must be set" if source_language.nil? || source_language.empty?
163
+ raise ConfigurationError, "Target languages must be an array" unless target_languages.is_a?(Array)
164
+ raise ConfigurationError, "At least one target language is required" if target_languages.empty?
165
+
166
+ target_languages.each do |lang|
167
+ raise ConfigurationError, "Each target language must be a Hash" unless lang.is_a?(Hash)
168
+ raise ConfigurationError, "Target language must have :short_name" unless lang.key?(:short_name)
169
+ raise ConfigurationError, "Target language must have :name" unless lang.key?(:name)
170
+ end
171
+ end
172
+
173
+ # Validate file paths
174
+ #
175
+ # @raise [ConfigurationError] if input file or output folder are invalid
176
+ # @return [void]
177
+ # @api private
178
+ def validate_files!
179
+ raise ConfigurationError, "Input file must be set" if input_file.nil? || input_file.empty?
180
+ raise ConfigurationError, "Output folder must be set" if output_folder.nil? || output_folder.empty?
181
+ raise ConfigurationError, "Input file does not exist: #{input_file}" unless File.exist?(input_file)
182
+ end
183
+
184
+ # Validate optional settings (timeouts, retries, cache, etc.)
185
+ #
186
+ # @raise [ConfigurationError] if optional settings have invalid values
187
+ # @return [void]
188
+ # @api private
189
+ def validate_optional_settings!
190
+ valid_modes = %i[override incremental]
191
+ unless valid_modes.include?(translation_mode)
192
+ raise ConfigurationError, "Translation mode must be :override or :incremental"
193
+ end
194
+
195
+ raise ConfigurationError, "Max concurrent requests must be positive" if max_concurrent_requests <= 0
196
+ raise ConfigurationError, "Request timeout must be positive" if request_timeout <= 0
197
+ raise ConfigurationError, "Max retries must be non-negative" if max_retries.negative?
198
+ raise ConfigurationError, "Cache size must be positive" if cache_size <= 0
199
+ end
200
+ end
201
+ end