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,668 @@
1
+ # 01 - Setup Dependencies & Infrastructure
2
+
3
+ [← Previous: 00-Overview](./00-overview.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 02-Error Handling →](./02-error_handling.md)
4
+
5
+ ---
6
+
7
+ ## Setup Dependencies & Infrastructure
8
+
9
+ ### 1.1 Aggiornare `Gemfile`
10
+
11
+ ```ruby
12
+ # frozen_string_literal: true
13
+
14
+ source "https://rubygems.org"
15
+
16
+ gemspec
17
+
18
+ gem "rake", "~> 13.0"
19
+
20
+ # Development & Testing
21
+ gem "rspec", "~> 3.0"
22
+ gem "rubocop", "~> 1.21"
23
+ gem "webmock", "~> 3.18"
24
+ gem "vcr", "~> 6.1"
25
+ gem "yard", "~> 0.9"
26
+ gem "bundler-audit", "~> 0.9"
27
+ ```
28
+
29
+ ### 1.2 Aggiornare `better_translate.gemspec`
30
+
31
+ ```ruby
32
+ # frozen_string_literal: true
33
+
34
+ require_relative "lib/better_translate/version"
35
+
36
+ Gem::Specification.new do |spec|
37
+ spec.name = "better_translate"
38
+ spec.version = BetterTranslate::VERSION
39
+ spec.authors = ["alessiobussolari"]
40
+ spec.email = ["alessio.bussolari@pandev.it"]
41
+
42
+ spec.summary = "AI-powered YAML locale file translator for Rails and Ruby projects"
43
+ spec.description = "Automatically translate YAML locale files using AI providers (ChatGPT, Gemini, Claude). Features intelligent caching, batch processing, and Rails integration."
44
+ spec.homepage = "https://github.com/alessiobussolari/better_translate"
45
+ spec.license = "MIT"
46
+ spec.required_ruby_version = ">= 3.0.0"
47
+
48
+ spec.metadata["homepage_uri"] = spec.homepage
49
+ spec.metadata["source_code_uri"] = "https://github.com/alessiobussolari/better_translate"
50
+ spec.metadata["changelog_uri"] = "https://github.com/alessiobussolari/better_translate/blob/main/CHANGELOG.md"
51
+
52
+ gemspec = File.basename(__FILE__)
53
+ spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
54
+ ls.readlines("\x0", chomp: true).reject do |f|
55
+ (f == gemspec) ||
56
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
57
+ end
58
+ end
59
+ spec.bindir = "exe"
60
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
61
+ spec.require_paths = ["lib"]
62
+
63
+ # Runtime dependencies
64
+ spec.add_dependency "faraday", "~> 2.0"
65
+ end
66
+ ```
67
+
68
+ ### 1.3 Creare `.yardopts`
69
+
70
+ ```
71
+ --markup markdown
72
+ --readme README.md
73
+ --private
74
+ --protected
75
+ --output-dir doc
76
+ lib/**/*.rb
77
+ -
78
+ CHANGELOG.md
79
+ LICENSE.txt
80
+ docs/FEATURES.md
81
+ ```
82
+
83
+ ### 1.4 Configuration File Support (Non-Rails Projects)
84
+
85
+ For pure Ruby projects without Rails initializers, BetterTranslate can load configuration from a `.better_translate.yml` file.
86
+
87
+ **IMPORTANT**: This feature is only used when:
88
+ - No Rails initializer exists (`config/initializers/better_translate.rb`)
89
+ - Running in a pure Ruby project (not Rails)
90
+ - Configuration file is found at project root
91
+
92
+ #### 1.4.1 `lib/better_translate/config_loader.rb`
93
+
94
+ ```ruby
95
+ # frozen_string_literal: true
96
+
97
+ require "yaml"
98
+
99
+ module BetterTranslate
100
+ # Loads configuration from .better_translate.yml file
101
+ #
102
+ # Used for pure Ruby projects without Rails initializers.
103
+ # Searches for .better_translate.yml in:
104
+ # 1. Current working directory
105
+ # 2. Project root (detected by presence of Gemfile, .git, etc.)
106
+ #
107
+ # @example .better_translate.yml
108
+ # provider: chatgpt
109
+ # source_language: en
110
+ # target_languages:
111
+ # - short_name: it
112
+ # name: Italian
113
+ # - short_name: fr
114
+ # name: French
115
+ # openai_key: ENV['OPENAI_API_KEY']
116
+ # verbose: true
117
+ #
118
+ class ConfigLoader
119
+ # Default configuration file name
120
+ CONFIG_FILE_NAME = ".better_translate.yml"
121
+
122
+ # Project root markers
123
+ ROOT_MARKERS = %w[Gemfile .git Rakefile].freeze
124
+
125
+ # @return [String, nil] Path to configuration file if found
126
+ attr_reader :config_file_path
127
+
128
+ # Initialize config loader
129
+ def initialize
130
+ @config_file_path = find_config_file
131
+ end
132
+
133
+ # Check if configuration file exists
134
+ #
135
+ # @return [Boolean] true if config file found
136
+ def config_file_exists?
137
+ !@config_file_path.nil?
138
+ end
139
+
140
+ # Load configuration from file
141
+ #
142
+ # @return [Hash] Configuration hash
143
+ # @raise [ConfigurationError] if file cannot be loaded
144
+ def load
145
+ unless config_file_exists?
146
+ raise ConfigurationError, "Configuration file #{CONFIG_FILE_NAME} not found"
147
+ end
148
+
149
+ content = File.read(@config_file_path)
150
+ config_hash = YAML.safe_load(content, permitted_classes: [Symbol])
151
+
152
+ validate_config_hash!(config_hash)
153
+ resolve_env_vars!(config_hash)
154
+
155
+ config_hash
156
+ rescue Psych::SyntaxError => e
157
+ raise ConfigurationError.new(
158
+ "Invalid YAML syntax in #{@config_file_path}",
159
+ context: { error: e.message }
160
+ )
161
+ rescue Errno::ENOENT => e
162
+ raise ConfigurationError.new(
163
+ "Configuration file not found: #{@config_file_path}",
164
+ context: { error: e.message }
165
+ )
166
+ end
167
+
168
+ # Load and apply configuration to BetterTranslate
169
+ #
170
+ # @return [void]
171
+ def load_and_configure!
172
+ config_hash = load
173
+
174
+ BetterTranslate.configure do |config|
175
+ apply_config_hash(config, config_hash)
176
+ end
177
+ end
178
+
179
+ private
180
+
181
+ # Find configuration file in current directory or project root
182
+ #
183
+ # @return [String, nil] Path to config file or nil
184
+ def find_config_file
185
+ # Check current directory
186
+ current_dir_config = File.join(Dir.pwd, CONFIG_FILE_NAME)
187
+ return current_dir_config if File.exist?(current_dir_config)
188
+
189
+ # Check project root
190
+ project_root = find_project_root
191
+ if project_root
192
+ root_config = File.join(project_root, CONFIG_FILE_NAME)
193
+ return root_config if File.exist?(root_config)
194
+ end
195
+
196
+ nil
197
+ end
198
+
199
+ # Find project root by looking for marker files
200
+ #
201
+ # @return [String, nil] Project root path or nil
202
+ def find_project_root
203
+ current = Dir.pwd
204
+
205
+ loop do
206
+ return current if ROOT_MARKERS.any? { |marker| File.exist?(File.join(current, marker)) }
207
+
208
+ parent = File.dirname(current)
209
+ break if parent == current # Reached filesystem root
210
+
211
+ current = parent
212
+ end
213
+
214
+ nil
215
+ end
216
+
217
+ # Validate configuration hash structure
218
+ #
219
+ # @param config_hash [Hash] Configuration hash
220
+ # @raise [ConfigurationError] if validation fails
221
+ # @return [void]
222
+ def validate_config_hash!(config_hash)
223
+ unless config_hash.is_a?(Hash)
224
+ raise ConfigurationError, "Configuration must be a hash"
225
+ end
226
+
227
+ # Check for required keys
228
+ required_keys = %w[provider source_language target_languages]
229
+ missing_keys = required_keys - config_hash.keys.map(&:to_s)
230
+
231
+ if missing_keys.any?
232
+ raise ConfigurationError,
233
+ "Missing required configuration keys: #{missing_keys.join(', ')}"
234
+ end
235
+
236
+ # Validate target_languages structure
237
+ target_langs = config_hash["target_languages"] || config_hash[:target_languages]
238
+ unless target_langs.is_a?(Array)
239
+ raise ConfigurationError, "target_languages must be an array"
240
+ end
241
+
242
+ target_langs.each do |lang|
243
+ unless lang.is_a?(Hash) && lang.key?("short_name") && lang.key?("name")
244
+ raise ConfigurationError,
245
+ "Each target language must have 'short_name' and 'name' keys"
246
+ end
247
+ end
248
+ end
249
+
250
+ # Resolve ENV variable references in configuration
251
+ #
252
+ # Replaces strings like "ENV['OPENAI_API_KEY']" with actual ENV values
253
+ #
254
+ # @param config_hash [Hash] Configuration hash
255
+ # @return [void]
256
+ def resolve_env_vars!(config_hash)
257
+ config_hash.each do |key, value|
258
+ next unless value.is_a?(String)
259
+
260
+ # Match patterns like ENV['KEY'] or ENV["KEY"]
261
+ if value =~ /\AENV\[['"]([^'"]+)['"]\]\z/
262
+ env_key = Regexp.last_match(1)
263
+ config_hash[key] = ENV.fetch(env_key, nil)
264
+ end
265
+ end
266
+ end
267
+
268
+ # Apply configuration hash to Configuration object
269
+ #
270
+ # @param config [Configuration] Configuration object
271
+ # @param config_hash [Hash] Configuration hash from YAML
272
+ # @return [void]
273
+ def apply_config_hash(config, config_hash)
274
+ config_hash.each do |key, value|
275
+ setter = "#{key}="
276
+
277
+ if config.respond_to?(setter)
278
+ # Convert string keys to symbols for provider
279
+ value = value.to_sym if key.to_s == "provider"
280
+
281
+ # Convert target_languages hashes to symbol keys
282
+ if key.to_s == "target_languages"
283
+ value = value.map { |lang| symbolize_keys(lang) }
284
+ end
285
+
286
+ config.public_send(setter, value)
287
+ end
288
+ end
289
+ end
290
+
291
+ # Convert hash keys to symbols
292
+ #
293
+ # @param hash [Hash] Hash with string keys
294
+ # @return [Hash] Hash with symbol keys
295
+ def symbolize_keys(hash)
296
+ hash.transform_keys(&:to_sym)
297
+ end
298
+ end
299
+ end
300
+ ```
301
+
302
+ #### 1.4.2 Integration with Main Module
303
+
304
+ Update `lib/better_translate.rb` to automatically load configuration file:
305
+
306
+ ```ruby
307
+ # At the end of the module, add:
308
+
309
+ # Auto-load configuration file if present (non-Rails projects)
310
+ #
311
+ # Only loads if:
312
+ # - Not in Rails environment (no Rails initializer)
313
+ # - Configuration file exists
314
+ # - No manual configuration has been set
315
+ def self.auto_load_config_file
316
+ return if defined?(Rails) # Skip in Rails (use initializer)
317
+ return if @config && @config.provider # Already configured
318
+
319
+ loader = ConfigLoader.new
320
+ return unless loader.config_file_exists?
321
+
322
+ loader.load_and_configure!
323
+ puts "[BetterTranslate] Loaded configuration from #{loader.config_file_path}" if @config.verbose
324
+ rescue ConfigurationError => e
325
+ warn "[BetterTranslate] Failed to load configuration file: #{e.message}"
326
+ end
327
+
328
+ # Auto-load on require (for non-Rails projects)
329
+ auto_load_config_file unless defined?(Rails)
330
+ ```
331
+
332
+ #### 1.4.3 Example Configuration File: `.better_translate.yml`
333
+
334
+ ```yaml
335
+ # BetterTranslate Configuration
336
+ # For pure Ruby projects without Rails
337
+
338
+ # Provider selection
339
+ # Options: chatgpt, gemini, anthropic
340
+ provider: chatgpt
341
+
342
+ # Source language
343
+ source_language: en
344
+
345
+ # Target languages
346
+ target_languages:
347
+ - short_name: it
348
+ name: Italian
349
+ - short_name: fr
350
+ name: French
351
+ - short_name: de
352
+ name: German
353
+ - short_name: es
354
+ name: Spanish
355
+
356
+ # API Keys (use ENV variables for security)
357
+ openai_key: ENV['OPENAI_API_KEY']
358
+ google_gemini_key: ENV['GOOGLE_GEMINI_API_KEY']
359
+ anthropic_key: ENV['ANTHROPIC_API_KEY']
360
+
361
+ # Translation settings
362
+ translation_mode: incremental # or: override
363
+ batch_size: 10
364
+ verbose: true
365
+ preserve_variables: true
366
+ dry_run: false
367
+
368
+ # File paths
369
+ input_file: config/locales/en.yml
370
+ output_folder: config/locales
371
+
372
+ # Cache settings
373
+ cache_enabled: true
374
+ cache_ttl: 86400 # 24 hours
375
+ cache_capacity: 1000
376
+
377
+ # Rate limiting
378
+ rate_limit: 10
379
+ rate_limit_period: 60
380
+
381
+ # HTTP settings
382
+ http_timeout: 30
383
+ max_retries: 3
384
+
385
+ # Exclusions
386
+ global_exclusions:
387
+ - debug.test_key
388
+ - internal.private_data
389
+
390
+ # Per-language exclusions
391
+ exclusions_per_language:
392
+ it:
393
+ - specific.italian_key
394
+ fr:
395
+ - specific.french_key
396
+
397
+ # Context for better translations
398
+ context: "This is a web application for e-commerce"
399
+ ```
400
+
401
+ #### 1.4.4 Test: `spec/better_translate/config_loader_spec.rb`
402
+
403
+ ```ruby
404
+ # frozen_string_literal: true
405
+
406
+ require "tmpdir"
407
+
408
+ RSpec.describe BetterTranslate::ConfigLoader do
409
+ let(:config_content) do
410
+ <<~YAML
411
+ provider: chatgpt
412
+ source_language: en
413
+ target_languages:
414
+ - short_name: it
415
+ name: Italian
416
+ - short_name: fr
417
+ name: French
418
+ openai_key: ENV['OPENAI_API_KEY']
419
+ verbose: true
420
+ YAML
421
+ end
422
+
423
+ describe "#config_file_exists?" do
424
+ it "returns false when no config file exists" do
425
+ loader = described_class.new
426
+ allow(loader).to receive(:find_config_file).and_return(nil)
427
+
428
+ expect(loader.config_file_exists?).to be false
429
+ end
430
+
431
+ it "returns true when config file exists" do
432
+ Dir.mktmpdir do |dir|
433
+ config_path = File.join(dir, ".better_translate.yml")
434
+ File.write(config_path, config_content)
435
+
436
+ allow(Dir).to receive(:pwd).and_return(dir)
437
+
438
+ loader = described_class.new
439
+ expect(loader.config_file_exists?).to be true
440
+ end
441
+ end
442
+ end
443
+
444
+ describe "#load" do
445
+ it "loads valid configuration file" do
446
+ Dir.mktmpdir do |dir|
447
+ config_path = File.join(dir, ".better_translate.yml")
448
+ File.write(config_path, config_content)
449
+
450
+ allow(Dir).to receive(:pwd).and_return(dir)
451
+ allow(ENV).to receive(:fetch).with("OPENAI_API_KEY", nil).and_return("test_key")
452
+
453
+ loader = described_class.new
454
+ config = loader.load
455
+
456
+ expect(config["provider"]).to eq("chatgpt")
457
+ expect(config["source_language"]).to eq("en")
458
+ expect(config["target_languages"].size).to eq(2)
459
+ expect(config["openai_key"]).to eq("test_key")
460
+ end
461
+ end
462
+
463
+ it "raises error for missing required keys" do
464
+ invalid_config = "provider: chatgpt\n"
465
+
466
+ Dir.mktmpdir do |dir|
467
+ config_path = File.join(dir, ".better_translate.yml")
468
+ File.write(config_path, invalid_config)
469
+
470
+ allow(Dir).to receive(:pwd).and_return(dir)
471
+
472
+ loader = described_class.new
473
+
474
+ expect {
475
+ loader.load
476
+ }.to raise_error(BetterTranslate::ConfigurationError, /Missing required/)
477
+ end
478
+ end
479
+
480
+ it "resolves ENV variable references" do
481
+ config_with_env = <<~YAML
482
+ provider: gemini
483
+ source_language: en
484
+ target_languages:
485
+ - short_name: it
486
+ name: Italian
487
+ google_gemini_key: ENV['GEMINI_KEY']
488
+ YAML
489
+
490
+ Dir.mktmpdir do |dir|
491
+ config_path = File.join(dir, ".better_translate.yml")
492
+ File.write(config_path, config_with_env)
493
+
494
+ allow(Dir).to receive(:pwd).and_return(dir)
495
+ allow(ENV).to receive(:fetch).with("GEMINI_KEY", nil).and_return("my_gemini_key")
496
+
497
+ loader = described_class.new
498
+ config = loader.load
499
+
500
+ expect(config["google_gemini_key"]).to eq("my_gemini_key")
501
+ end
502
+ end
503
+
504
+ it "raises error for invalid YAML syntax" do
505
+ invalid_yaml = "provider: chatgpt\n invalid: : yaml"
506
+
507
+ Dir.mktmpdir do |dir|
508
+ config_path = File.join(dir, ".better_translate.yml")
509
+ File.write(config_path, invalid_yaml)
510
+
511
+ allow(Dir).to receive(:pwd).and_return(dir)
512
+
513
+ loader = described_class.new
514
+
515
+ expect {
516
+ loader.load
517
+ }.to raise_error(BetterTranslate::ConfigurationError, /Invalid YAML syntax/)
518
+ end
519
+ end
520
+ end
521
+
522
+ describe "#find_project_root" do
523
+ it "finds project root by Gemfile" do
524
+ Dir.mktmpdir do |dir|
525
+ File.write(File.join(dir, "Gemfile"), "")
526
+
527
+ allow(Dir).to receive(:pwd).and_return(File.join(dir, "nested", "deep"))
528
+
529
+ loader = described_class.new
530
+ root = loader.send(:find_project_root)
531
+
532
+ expect(root).to eq(dir)
533
+ end
534
+ end
535
+
536
+ it "returns nil if no root markers found" do
537
+ loader = described_class.new
538
+
539
+ allow(Dir).to receive(:pwd).and_return("/")
540
+
541
+ root = loader.send(:find_project_root)
542
+ expect(root).to be_nil
543
+ end
544
+ end
545
+
546
+ describe "#load_and_configure!" do
547
+ it "applies configuration to BetterTranslate module" do
548
+ Dir.mktmpdir do |dir|
549
+ config_path = File.join(dir, ".better_translate.yml")
550
+ File.write(config_path, config_content)
551
+
552
+ allow(Dir).to receive(:pwd).and_return(dir)
553
+ allow(ENV).to receive(:fetch).with("OPENAI_API_KEY", nil).and_return("test_key")
554
+
555
+ BetterTranslate.reset!
556
+
557
+ loader = described_class.new
558
+ loader.load_and_configure!
559
+
560
+ config = BetterTranslate.config
561
+
562
+ expect(config.provider).to eq(:chatgpt)
563
+ expect(config.source_language).to eq("en")
564
+ expect(config.verbose).to be true
565
+ expect(config.openai_key).to eq("test_key")
566
+ end
567
+ end
568
+ end
569
+ end
570
+ ```
571
+
572
+ ---
573
+
574
+ ## Usage Examples
575
+
576
+ ### Example 1: Pure Ruby Project with Config File
577
+
578
+ ```bash
579
+ # Project structure
580
+ my_ruby_app/
581
+ ├── .better_translate.yml # Configuration file
582
+ ├── Gemfile
583
+ ├── config/
584
+ │ └── locales/
585
+ │ └── en.yml
586
+ └── translate.rb
587
+
588
+ # translate.rb
589
+ require "better_translate"
590
+
591
+ # Configuration is automatically loaded from .better_translate.yml
592
+ BetterTranslate.translate_all
593
+
594
+ puts "Translation complete!"
595
+ ```
596
+
597
+ ### Example 2: Explicit Config File Loading
598
+
599
+ ```ruby
600
+ require "better_translate"
601
+
602
+ # Manually load configuration file
603
+ loader = BetterTranslate::ConfigLoader.new
604
+
605
+ if loader.config_file_exists?
606
+ loader.load_and_configure!
607
+ BetterTranslate.translate_all
608
+ else
609
+ puts "No configuration file found. Please create .better_translate.yml"
610
+ end
611
+ ```
612
+
613
+ ### Example 3: Override Config File Settings
614
+
615
+ ```ruby
616
+ require "better_translate"
617
+
618
+ # Auto-load from file, then override specific settings
619
+ BetterTranslate.configure do |config|
620
+ config.verbose = true
621
+ config.dry_run = true # Override to preview changes
622
+ end
623
+
624
+ BetterTranslate.translate_all
625
+ ```
626
+
627
+ ---
628
+
629
+ ## CLI Integration
630
+
631
+ The standalone CLI (Phase 12) will automatically detect and use `.better_translate.yml`:
632
+
633
+ ```bash
634
+ # CLI automatically loads .better_translate.yml if present
635
+ better_translate translate config/locales/en.yml
636
+
637
+ # Or specify custom config file
638
+ better_translate translate config/locales/en.yml --config my_config.yml
639
+ ```
640
+
641
+ ---
642
+
643
+ ## Benefits
644
+
645
+ 1. **Zero Configuration**: Pure Ruby projects work with just a YAML file
646
+ 2. **Security**: API keys stored in ENV variables
647
+ 3. **Version Control**: Configuration can be committed (without secrets)
648
+ 4. **Portability**: Same config file works across team members
649
+ 5. **Rails Compatible**: Auto-disabled in Rails (use initializer instead)
650
+
651
+ ---
652
+
653
+ ## Implementation Checklist
654
+
655
+ - [ ] Create `lib/better_translate/config_loader.rb`
656
+ - [ ] Update `lib/better_translate.rb` to auto-load config file
657
+ - [ ] Create comprehensive test suite in `spec/better_translate/config_loader_spec.rb`
658
+ - [ ] Add example `.better_translate.yml` to repository
659
+ - [ ] Update CLI to support `--config` flag (Phase 12)
660
+ - [ ] Add YARD documentation for all methods
661
+ - [ ] Update README with configuration file examples
662
+ - [ ] Add `.better_translate.yml` to `.gitignore` template (optional)
663
+
664
+ ---
665
+
666
+ ---
667
+
668
+ [← Previous: 00-Overview](./00-overview.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 02-Error Handling →](./02-error_handling.md)