better_translate 1.0.0.1 → 1.1.1

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +28 -0
  3. data/.rubocop_todo.yml +291 -0
  4. data/CHANGELOG.md +88 -0
  5. data/CLAUDE.md +12 -7
  6. data/CONTRIBUTING.md +432 -0
  7. data/README.md +240 -1
  8. data/Rakefile +14 -1
  9. data/SECURITY.md +160 -0
  10. data/Steepfile +0 -1
  11. data/brakeman.yml +37 -0
  12. data/codecov.yml +34 -0
  13. data/lib/better_translate/analyzer/code_scanner.rb +149 -0
  14. data/lib/better_translate/analyzer/key_scanner.rb +109 -0
  15. data/lib/better_translate/analyzer/orphan_detector.rb +91 -0
  16. data/lib/better_translate/analyzer/reporter.rb +155 -0
  17. data/lib/better_translate/cli.rb +81 -2
  18. data/lib/better_translate/configuration.rb +76 -3
  19. data/lib/better_translate/errors.rb +9 -0
  20. data/lib/better_translate/json_handler.rb +227 -0
  21. data/lib/better_translate/translator.rb +205 -23
  22. data/lib/better_translate/version.rb +1 -1
  23. data/lib/better_translate/yaml_handler.rb +59 -0
  24. data/lib/better_translate.rb +7 -0
  25. data/lib/generators/better_translate/install/install_generator.rb +2 -2
  26. data/lib/generators/better_translate/install/templates/initializer.rb.tt +22 -34
  27. data/lib/generators/better_translate/translate/translate_generator.rb +65 -46
  28. data/lib/tasks/better_translate.rake +62 -45
  29. data/sig/better_translate/analyzer/code_scanner.rbs +59 -0
  30. data/sig/better_translate/analyzer/key_scanner.rbs +40 -0
  31. data/sig/better_translate/analyzer/orphan_detector.rbs +43 -0
  32. data/sig/better_translate/analyzer/reporter.rbs +70 -0
  33. data/sig/better_translate/cli.rbs +2 -0
  34. data/sig/better_translate/configuration.rbs +6 -0
  35. data/sig/better_translate/errors.rbs +4 -0
  36. data/sig/better_translate/json_handler.rbs +65 -0
  37. data/sig/better_translate/progress_tracker.rbs +1 -1
  38. data/sig/better_translate/translator.rbs +12 -1
  39. data/sig/better_translate/yaml_handler.rbs +6 -0
  40. data/sig/better_translate.rbs +4 -0
  41. data/sig/csv.rbs +16 -0
  42. metadata +32 -3
  43. data/regenerate_vcr.rb +0 -47
@@ -34,12 +34,19 @@ module BetterTranslate
34
34
  @config.validate!
35
35
  provider_name = config.provider || :chatgpt
36
36
  @provider = ProviderFactory.create(provider_name, config)
37
- @yaml_handler = YAMLHandler.new(config)
37
+
38
+ # Resolve input files (supports input_file, input_files array, or glob pattern)
39
+ @input_files = resolve_input_files
40
+
41
+ # We'll determine handler per file during translation
38
42
  @progress_tracker = ProgressTracker.new(enabled: config.verbose)
39
43
  end
40
44
 
41
45
  # Translate to all target languages
42
46
  #
47
+ # Uses parallel execution when max_concurrent_requests > 1,
48
+ # sequential execution otherwise. Supports multiple input files.
49
+ #
43
50
  # @return [Hash] Results hash with :success_count, :failure_count, :errors
44
51
  #
45
52
  # @example
@@ -47,8 +54,128 @@ module BetterTranslate
47
54
  # #=> { success_count: 2, failure_count: 0, errors: [] }
48
55
  #
49
56
  def translate_all
50
- source_strings = @yaml_handler.get_source_strings
57
+ # @type var combined_results: translation_results
58
+ combined_results = {
59
+ success_count: 0,
60
+ failure_count: 0,
61
+ errors: []
62
+ }
63
+
64
+ @input_files.each do |input_file|
65
+ # Create appropriate handler for this file
66
+ handler = if input_file.end_with?(".json")
67
+ JsonHandler.new(config)
68
+ else
69
+ YAMLHandler.new(config)
70
+ end
71
+
72
+ # Temporarily set input_file for this iteration
73
+ original_input_file = config.input_file
74
+ config.input_file = input_file
75
+
76
+ source_strings = handler.get_source_strings
77
+
78
+ results = if config.max_concurrent_requests > 1
79
+ translate_parallel(source_strings, handler)
80
+ else
81
+ translate_sequential(source_strings, handler)
82
+ end
83
+
84
+ # Restore original config
85
+ config.input_file = original_input_file
86
+
87
+ # Accumulate results
88
+ combined_results[:success_count] += results[:success_count]
89
+ combined_results[:failure_count] += results[:failure_count]
90
+ combined_results[:errors].concat(results[:errors])
91
+ end
92
+
93
+ combined_results
94
+ end
95
+
96
+ private
97
+
98
+ # Resolve input files from config
99
+ #
100
+ # Handles input_file, input_files array, or glob patterns
101
+ #
102
+ # @return [Array<String>] Resolved file paths
103
+ # @raise [FileError] if no files found
104
+ # @api private
105
+ #
106
+ def resolve_input_files
107
+ files = if config.input_files
108
+ # Handle input_files (array or glob pattern)
109
+ if config.input_files.is_a?(Array)
110
+ config.input_files
111
+ else
112
+ # Glob pattern
113
+ Dir.glob(config.input_files)
114
+ end
115
+ elsif config.input_file
116
+ # Backward compatibility with single input_file
117
+ [config.input_file]
118
+ else
119
+ [] # : Array[String]
120
+ end
121
+
122
+ # Validate files exist (unless glob pattern that found nothing)
123
+ if files.empty?
124
+ if config.input_files && !config.input_files.is_a?(Array)
125
+ raise FileError, "No files found matching pattern: #{config.input_files}"
126
+ end
127
+
128
+ raise FileError, "No input files specified"
129
+
130
+ end
131
+
132
+ files.each do |file|
133
+ Validator.validate_file_exists!(file)
134
+ end
135
+
136
+ files
137
+ end
138
+
139
+ # Translate languages in parallel
140
+ #
141
+ # @param source_strings [Hash] Source strings (flattened)
142
+ # @param handler [YAMLHandler, JsonHandler] File handler for this file
143
+ # @return [Hash] Results hash with counts and errors
144
+ # @api private
145
+ #
146
+ def translate_parallel(source_strings, handler)
147
+ # @type var results: translation_results
148
+ results = {
149
+ success_count: 0,
150
+ failure_count: 0,
151
+ errors: []
152
+ }
153
+ mutex = Mutex.new
154
+
155
+ # Process languages in batches of max_concurrent_requests
156
+ config.target_languages.each_slice(config.max_concurrent_requests) do |batch|
157
+ threads = batch.map do |lang|
158
+ Thread.new do
159
+ translate_language(source_strings, lang, handler)
160
+ mutex.synchronize { results[:success_count] += 1 }
161
+ rescue StandardError => e
162
+ mutex.synchronize { record_error(results, lang, e) }
163
+ end
164
+ end
165
+ threads.each(&:join)
166
+ end
167
+
168
+ results
169
+ end
51
170
 
171
+ # Translate languages sequentially
172
+ #
173
+ # @param source_strings [Hash] Source strings (flattened)
174
+ # @param handler [YAMLHandler, JsonHandler] File handler for this file
175
+ # @return [Hash] Results hash with counts and errors
176
+ # @api private
177
+ #
178
+ def translate_sequential(source_strings, handler)
52
179
  # @type var results: translation_results
53
180
  results = {
54
181
  success_count: 0,
@@ -57,42 +184,53 @@ module BetterTranslate
57
184
  }
58
185
 
59
186
  config.target_languages.each do |lang|
60
- translate_language(source_strings, lang)
187
+ translate_language(source_strings, lang, handler)
61
188
  results[:success_count] += 1
62
189
  rescue StandardError => e
63
- results[:failure_count] += 1
64
- # @type var error_context: Hash[Symbol, untyped]
65
- error_context = if e.is_a?(BetterTranslate::Error)
66
- e.context
67
- else
68
- {}
69
- end
70
- results[:errors] << {
71
- language: lang[:name],
72
- error: e.message,
73
- context: error_context
74
- }
75
- @progress_tracker.error(lang[:name], e)
190
+ record_error(results, lang, e)
76
191
  end
77
192
 
78
193
  results
79
194
  end
80
195
 
81
- private
196
+ # Record translation error in results
197
+ #
198
+ # @param results [Hash] Results hash to update
199
+ # @param lang [Hash] Language config
200
+ # @param error [StandardError] The error that occurred
201
+ # @return [void]
202
+ # @api private
203
+ #
204
+ def record_error(results, lang, error)
205
+ results[:failure_count] += 1
206
+ # @type var error_context: Hash[Symbol, untyped]
207
+ error_context = if error.is_a?(BetterTranslate::Error)
208
+ error.context
209
+ else
210
+ {}
211
+ end
212
+ results[:errors] << {
213
+ language: lang[:name],
214
+ error: error.message,
215
+ context: error_context
216
+ }
217
+ @progress_tracker.error(lang[:name], error)
218
+ end
82
219
 
83
220
  # Translate to a single language
84
221
  #
85
222
  # @param source_strings [Hash] Source strings (flattened)
86
223
  # @param lang [Hash] Language config with :short_name and :name
224
+ # @param handler [YAMLHandler, JsonHandler] File handler for this file
87
225
  # @return [void]
88
226
  # @api private
89
227
  #
90
- def translate_language(source_strings, lang)
228
+ def translate_language(source_strings, lang, handler)
91
229
  target_lang_code = lang[:short_name]
92
230
  target_lang_name = lang[:name]
93
231
 
94
232
  # Filter exclusions
95
- strings_to_translate = @yaml_handler.filter_exclusions(source_strings, target_lang_code)
233
+ strings_to_translate = handler.filter_exclusions(source_strings, target_lang_code)
96
234
 
97
235
  return if strings_to_translate.empty?
98
236
 
@@ -108,20 +246,64 @@ module BetterTranslate
108
246
  @progress_tracker.reset
109
247
  translated = strategy.translate(strings_to_translate, target_lang_code, target_lang_name)
110
248
 
111
- # Save
112
- output_path = @yaml_handler.build_output_path(target_lang_code)
249
+ # Save - generate output path with proper filename
250
+ current_input_file = config.input_file or raise "No input file set"
251
+ output_path = build_output_path_for_file(current_input_file, target_lang_code)
113
252
 
114
253
  final_translations = if config.translation_mode == :incremental
115
- @yaml_handler.merge_translations(output_path, translated)
254
+ handler.merge_translations(output_path, translated)
116
255
  else
117
256
  Utils::HashFlattener.unflatten(translated)
118
257
  end
119
258
 
120
259
  # Wrap in language key (e.g., "it:")
121
260
  wrapped = { target_lang_code => final_translations }
122
- @yaml_handler.write_yaml(output_path, wrapped)
261
+
262
+ # Write using appropriate handler method (write_yaml or write_json)
263
+ if handler.is_a?(JsonHandler)
264
+ handler.write_json(output_path, wrapped)
265
+ else
266
+ handler.write_yaml(output_path, wrapped)
267
+ end
123
268
 
124
269
  @progress_tracker.complete(target_lang_name, translated.size)
125
270
  end
271
+
272
+ # Build output path for a specific file and language
273
+ #
274
+ # Replaces source language code with target language code in filename
275
+ # and preserves directory structure
276
+ #
277
+ # @param input_file [String] Input file path
278
+ # @param target_lang_code [String] Target language code
279
+ # @return [String] Output file path
280
+ # @api private
281
+ #
282
+ # @example
283
+ # build_output_path_for_file("config/locales/common.en.yml", "it")
284
+ # #=> "config/locales/common.it.yml"
285
+ #
286
+ def build_output_path_for_file(input_file, target_lang_code)
287
+ # Get file basename and directory
288
+ dir = File.dirname(input_file)
289
+ basename = File.basename(input_file)
290
+ ext = File.extname(basename)
291
+ name_without_ext = File.basename(basename, ext)
292
+
293
+ # Replace source language code with target language code
294
+ # Handles patterns like: common.en.yml -> common.it.yml
295
+ new_basename = name_without_ext.gsub(/\.#{config.source_language}$/, ".#{target_lang_code}") + ext
296
+
297
+ # If no language code in filename, use simple pattern
298
+ new_basename = "#{target_lang_code}#{ext}" if new_basename == basename
299
+
300
+ # Build output path
301
+ if config.output_folder
302
+ # Use output_folder but preserve relative directory structure if input was nested
303
+ File.join(config.output_folder, new_basename)
304
+ else
305
+ File.join(dir, new_basename)
306
+ end
307
+ end
126
308
  end
127
309
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module BetterTranslate
4
4
  # Current version of BetterTranslate gem
5
- VERSION = "1.0.0.1"
5
+ VERSION = "1.1.1"
6
6
  end
@@ -81,6 +81,9 @@ module BetterTranslate
81
81
 
82
82
  return summary if config.dry_run
83
83
 
84
+ # Create backup if enabled and file exists
85
+ create_backup_file(file_path) if config.create_backup && File.exist?(file_path)
86
+
84
87
  # Ensure output directory exists
85
88
  FileUtils.mkdir_p(File.dirname(file_path))
86
89
 
@@ -164,5 +167,61 @@ module BetterTranslate
164
167
 
165
168
  File.join(config.output_folder, "#{target_lang_code}.yml")
166
169
  end
170
+
171
+ private
172
+
173
+ # Create backup file with rotation support
174
+ #
175
+ # @param file_path [String] Path to file to backup
176
+ # @return [void]
177
+ # @api private
178
+ #
179
+ def create_backup_file(file_path)
180
+ return unless File.exist?(file_path)
181
+
182
+ # Rotate existing backups if max_backups > 1
183
+ rotate_backups(file_path) if config.max_backups > 1
184
+
185
+ # Create primary backup
186
+ backup_path = "#{file_path}.bak"
187
+ FileUtils.cp(file_path, backup_path)
188
+ end
189
+
190
+ # Rotate backup files, keeping only max_backups
191
+ #
192
+ # Rotation strategy:
193
+ # - .bak is always the most recent
194
+ # - .bak.1, .bak.2, etc. are progressively older
195
+ # - When we reach max_backups, oldest is deleted
196
+ #
197
+ # @param file_path [String] Base file path
198
+ # @return [void]
199
+ # @api private
200
+ #
201
+ def rotate_backups(file_path)
202
+ primary_backup = "#{file_path}.bak"
203
+ return unless File.exist?(primary_backup)
204
+
205
+ # Clean up ANY backups that would exceed max_backups after rotation
206
+ # max_backups includes .bak itself, so numbered backups go from 1 to max_backups-1
207
+ # After rotation, .bak -> .bak.1, so we can have at most .bak.1 through .bak.(max_backups-1)
208
+ 10.downto(config.max_backups) do |i|
209
+ numbered_backup = "#{file_path}.bak.#{i}"
210
+ FileUtils.rm_f(numbered_backup) if File.exist?(numbered_backup)
211
+ end
212
+
213
+ # Rotate numbered backups from high to low to avoid overwrites
214
+ # max_backups=2: nothing to rotate (only .bak -> .bak.1)
215
+ # max_backups=3: .bak.1 -> .bak.2 (if exists)
216
+ (config.max_backups - 2).downto(1) do |i|
217
+ old_path = "#{file_path}.bak.#{i}"
218
+ new_path = "#{file_path}.bak.#{i + 1}"
219
+
220
+ FileUtils.mv(old_path, new_path) if File.exist?(old_path)
221
+ end
222
+
223
+ # Move primary backup to .bak.1
224
+ FileUtils.mv(primary_backup, "#{file_path}.bak.1")
225
+ end
167
226
  end
168
227
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "csv"
4
+
3
5
  require_relative "better_translate/version"
4
6
  require_relative "better_translate/errors"
5
7
  require_relative "better_translate/configuration"
@@ -14,6 +16,7 @@ require_relative "better_translate/providers/gemini_provider"
14
16
  require_relative "better_translate/providers/anthropic_provider"
15
17
  require_relative "better_translate/provider_factory"
16
18
  require_relative "better_translate/yaml_handler"
19
+ require_relative "better_translate/json_handler"
17
20
  require_relative "better_translate/progress_tracker"
18
21
  require_relative "better_translate/strategies/base_strategy"
19
22
  require_relative "better_translate/strategies/deep_strategy"
@@ -21,6 +24,10 @@ require_relative "better_translate/strategies/batch_strategy"
21
24
  require_relative "better_translate/strategies/strategy_selector"
22
25
  require_relative "better_translate/translator"
23
26
  require_relative "better_translate/direct_translator"
27
+ require_relative "better_translate/analyzer/key_scanner"
28
+ require_relative "better_translate/analyzer/code_scanner"
29
+ require_relative "better_translate/analyzer/orphan_detector"
30
+ require_relative "better_translate/analyzer/reporter"
24
31
  require_relative "better_translate/cli"
25
32
 
26
33
  # Load Rails integration if Rails is present
@@ -47,8 +47,8 @@ module BetterTranslate
47
47
  "dry_run" => false,
48
48
  "translation_mode" => "override",
49
49
  "preserve_variables" => true,
50
- "global_exclusions" => [],
51
- "exclusions_per_language" => {},
50
+ "global_exclusions" => [], # : Array[String]
51
+ "exclusions_per_language" => {}, # : Hash[String, Array[String]]
52
52
  "model" => nil,
53
53
  "temperature" => 0.3,
54
54
  "max_tokens" => 2000,
@@ -2,6 +2,10 @@
2
2
 
3
3
  # BetterTranslate Configuration
4
4
  #
5
+ # IMPORTANT: I18n configuration is not yet available when initializers load.
6
+ # You must manually set source_language and target_languages to match your
7
+ # Rails I18n configuration in config/application.rb
8
+ #
5
9
  # For more configuration options, see config/better_translate.yml
6
10
 
7
11
  BetterTranslate.configure do |config|
@@ -14,43 +18,27 @@ BetterTranslate.configure do |config|
14
18
  config.anthropic_key = ENV["ANTHROPIC_API_KEY"]
15
19
 
16
20
  # Source and target languages
17
- # Automatically uses Rails I18n configuration
18
- # To configure I18n in your Rails app, set in config/application.rb:
21
+ #
22
+ # IMPORTANT: These must be set manually to match your Rails I18n config
23
+ # (I18n.default_locale and I18n.available_locales are not yet available)
24
+ #
25
+ # Example: If your config/application.rb has:
19
26
  # config.i18n.default_locale = :it
20
- # config.i18n.available_locales = [:it, :en, :es, :fr]
21
- config.source_language = I18n.default_locale.to_s
22
-
23
- # Target languages: automatically derived from I18n.available_locales
24
- # Excludes the source language from targets
25
- available_targets = (I18n.available_locales - [I18n.default_locale]).map(&:to_s)
27
+ # config.i18n.available_locales = [:it, :en, :fr, :ja, :ru]
28
+ #
29
+ # Then set:
30
+ # config.source_language = "it" # matches default_locale
31
+ # config.target_languages = [...] # matches available_locales (excluding source)
26
32
 
27
- # Language name mapping for common languages
28
- language_names = {
29
- "en" => "English", "it" => "Italian", "es" => "Spanish", "fr" => "French",
30
- "de" => "German", "pt" => "Portuguese", "ru" => "Russian", "zh" => "Chinese",
31
- "ja" => "Japanese", "ko" => "Korean", "ar" => "Arabic", "nl" => "Dutch",
32
- "pl" => "Polish", "tr" => "Turkish", "sv" => "Swedish", "da" => "Danish",
33
- "fi" => "Finnish", "no" => "Norwegian", "cs" => "Czech", "el" => "Greek",
34
- "he" => "Hebrew", "hi" => "Hindi", "th" => "Thai", "vi" => "Vietnamese"
35
- }
33
+ config.source_language = "en" # TODO: Change to match your default_locale
36
34
 
37
- if available_targets.any?
38
- # Use I18n available locales
39
- config.target_languages = available_targets.map do |locale|
40
- {
41
- short_name: locale,
42
- name: language_names[locale] || locale.capitalize
43
- }
44
- end
45
- else
46
- # Fallback: suggest common languages
47
- # Uncomment and modify the languages you want to translate to
48
- config.target_languages = [
49
- { short_name: "it", name: "Italian" },
50
- { short_name: "es", name: "Spanish" },
51
- { short_name: "fr", name: "French" }
52
- ]
53
- end
35
+ # Target languages (excluding source language)
36
+ # TODO: Change to match your available_locales
37
+ config.target_languages = [
38
+ { short_name: "it", name: "Italian" },
39
+ { short_name: "es", name: "Spanish" },
40
+ { short_name: "fr", name: "French" }
41
+ ]
54
42
 
55
43
  # File paths
56
44
  # Uses source_language for input file
@@ -29,57 +29,75 @@ module BetterTranslate
29
29
  #
30
30
  # @return [void]
31
31
  #
32
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
32
33
  def run_translation
33
- config_file = Rails.root.join("config", "better_translate.yml")
34
-
35
- unless File.exist?(config_file)
36
- say "Config file not found at #{config_file}", :red
37
- say "Run 'rails generate better_translate:install' first"
38
- return
39
- end
34
+ # Check if configuration is already loaded (from initializer)
35
+ if BetterTranslate.configuration.provider.nil?
36
+ # No initializer configuration found, try loading from YAML
37
+ config_file = Rails.root.join("config", "better_translate.yml")
38
+
39
+ unless File.exist?(config_file)
40
+ say "No configuration found", :red
41
+ say "Either:"
42
+ say " 1. Create config/initializers/better_translate.rb (recommended)"
43
+ say " 2. Run 'rails generate better_translate:install' to create YAML config"
44
+ return
45
+ end
40
46
 
41
- # Load configuration from YAML
42
- yaml_config = YAML.load_file(config_file)
43
-
44
- # Configure BetterTranslate
45
- BetterTranslate.configure do |config|
46
- config.provider = yaml_config["provider"]&.to_sym
47
- config.openai_key = yaml_config["openai_key"] || ENV["OPENAI_API_KEY"]
48
- config.gemini_key = yaml_config["gemini_key"] || ENV["GEMINI_API_KEY"]
49
- config.anthropic_key = yaml_config["anthropic_key"] || ENV["ANTHROPIC_API_KEY"]
50
-
51
- config.source_language = yaml_config["source_language"]
52
- config.target_languages = yaml_config["target_languages"]&.map do |lang|
53
- if lang.is_a?(Hash)
54
- { short_name: lang["short_name"], name: lang["name"] }
55
- else
56
- lang
47
+ # Load configuration from YAML
48
+ yaml_config = YAML.load_file(config_file)
49
+
50
+ # Configure BetterTranslate
51
+ BetterTranslate.configure do |config|
52
+ config.provider = yaml_config["provider"]&.to_sym
53
+ config.openai_key = yaml_config["openai_key"] || ENV["OPENAI_API_KEY"]
54
+ config.gemini_key = yaml_config["gemini_key"] || ENV["GEMINI_API_KEY"]
55
+ config.anthropic_key = yaml_config["anthropic_key"] || ENV["ANTHROPIC_API_KEY"]
56
+
57
+ config.source_language = yaml_config["source_language"]
58
+ config.target_languages = yaml_config["target_languages"]&.map do |lang|
59
+ if lang.is_a?(Hash)
60
+ { short_name: lang["short_name"], name: lang["name"] }
61
+ else
62
+ lang
63
+ end
57
64
  end
65
+
66
+ config.input_file = Rails.root.join(yaml_config["input_file"]).to_s
67
+ config.output_folder = Rails.root.join(yaml_config["output_folder"]).to_s
68
+ config.verbose = yaml_config.fetch("verbose", true)
69
+ config.dry_run = options[:dry_run] || yaml_config.fetch("dry_run", false)
70
+
71
+ # Map "full" to :override for backward compatibility
72
+ translation_mode = yaml_config.fetch("translation_mode", "override")
73
+ translation_mode = "override" if translation_mode == "full"
74
+ config.translation_mode = translation_mode.to_sym
75
+
76
+ config.preserve_variables = yaml_config.fetch("preserve_variables", true)
77
+
78
+ # Exclusions
79
+ config.global_exclusions = yaml_config["global_exclusions"] || []
80
+ config.exclusions_per_language = yaml_config["exclusions_per_language"] || {}
81
+
82
+ # Provider options
83
+ config.model = yaml_config["model"] if yaml_config["model"]
84
+ config.temperature = yaml_config["temperature"] if yaml_config["temperature"]
85
+ config.max_tokens = yaml_config["max_tokens"] if yaml_config["max_tokens"]
86
+ config.timeout = yaml_config["timeout"] if yaml_config["timeout"]
87
+ config.max_retries = yaml_config["max_retries"] if yaml_config["max_retries"]
88
+ config.rate_limit = yaml_config["rate_limit"] if yaml_config["rate_limit"]
58
89
  end
90
+ elsif options[:dry_run]
91
+ # Configuration from initializer exists, but apply dry_run option if provided
92
+ BetterTranslate.configuration.dry_run = true
93
+ end
59
94
 
60
- config.input_file = Rails.root.join(yaml_config["input_file"]).to_s
61
- config.output_folder = Rails.root.join(yaml_config["output_folder"]).to_s
62
- config.verbose = yaml_config.fetch("verbose", true)
63
- config.dry_run = options[:dry_run] || yaml_config.fetch("dry_run", false)
64
-
65
- # Map "full" to :override for backward compatibility
66
- translation_mode = yaml_config.fetch("translation_mode", "override")
67
- translation_mode = "override" if translation_mode == "full"
68
- config.translation_mode = translation_mode.to_sym
69
-
70
- config.preserve_variables = yaml_config.fetch("preserve_variables", true)
71
-
72
- # Exclusions
73
- config.global_exclusions = yaml_config["global_exclusions"] || []
74
- config.exclusions_per_language = yaml_config["exclusions_per_language"] || {}
75
-
76
- # Provider options
77
- config.model = yaml_config["model"] if yaml_config["model"]
78
- config.temperature = yaml_config["temperature"] if yaml_config["temperature"]
79
- config.max_tokens = yaml_config["max_tokens"] if yaml_config["max_tokens"]
80
- config.timeout = yaml_config["timeout"] if yaml_config["timeout"]
81
- config.max_retries = yaml_config["max_retries"] if yaml_config["max_retries"]
82
- config.rate_limit = yaml_config["rate_limit"] if yaml_config["rate_limit"]
95
+ # Validate configuration (whether from initializer or YAML)
96
+ begin
97
+ BetterTranslate.configuration.validate!
98
+ rescue BetterTranslate::ConfigurationError => e
99
+ say "Invalid configuration: #{e.message}", :red
100
+ return
83
101
  end
84
102
 
85
103
  # Perform translation
@@ -110,6 +128,7 @@ module BetterTranslate
110
128
  say " - #{error[:language]}: #{error[:error]}", :red
111
129
  end
112
130
  end
131
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
113
132
  end
114
133
  end
115
134
  end