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.
- checksums.yaml +4 -4
- data/.env.example +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +125 -114
- data/CLAUDE.md +385 -0
- data/README.md +629 -244
- data/Rakefile +7 -1
- data/Steepfile +29 -0
- data/docs/implementation/00-overview.md +220 -0
- data/docs/implementation/01-setup_dependencies.md +668 -0
- data/docs/implementation/02-error_handling.md +65 -0
- data/docs/implementation/03-core_components.md +457 -0
- data/docs/implementation/03.5-variable_preservation.md +509 -0
- data/docs/implementation/04-provider_architecture.md +571 -0
- data/docs/implementation/05-translation_logic.md +1065 -0
- data/docs/implementation/06-main_module_api.md +122 -0
- data/docs/implementation/07-direct_translation_helpers.md +582 -0
- data/docs/implementation/08-rails_integration.md +323 -0
- data/docs/implementation/09-testing_suite.md +228 -0
- data/docs/implementation/10-documentation_examples.md +150 -0
- data/docs/implementation/11-quality_security.md +65 -0
- data/docs/implementation/12-cli_standalone.md +698 -0
- data/exe/better_translate +9 -0
- data/lib/better_translate/cache.rb +125 -0
- data/lib/better_translate/cli.rb +304 -0
- data/lib/better_translate/configuration.rb +201 -0
- data/lib/better_translate/direct_translator.rb +131 -0
- data/lib/better_translate/errors.rb +101 -0
- data/lib/better_translate/progress_tracker.rb +157 -0
- data/lib/better_translate/provider_factory.rb +45 -0
- data/lib/better_translate/providers/anthropic_provider.rb +154 -0
- data/lib/better_translate/providers/base_http_provider.rb +239 -0
- data/lib/better_translate/providers/chatgpt_provider.rb +138 -44
- data/lib/better_translate/providers/gemini_provider.rb +123 -61
- data/lib/better_translate/railtie.rb +18 -0
- data/lib/better_translate/rate_limiter.rb +90 -0
- data/lib/better_translate/strategies/base_strategy.rb +58 -0
- data/lib/better_translate/strategies/batch_strategy.rb +56 -0
- data/lib/better_translate/strategies/deep_strategy.rb +45 -0
- data/lib/better_translate/strategies/strategy_selector.rb +43 -0
- data/lib/better_translate/translator.rb +115 -284
- data/lib/better_translate/utils/hash_flattener.rb +104 -0
- data/lib/better_translate/validator.rb +105 -0
- data/lib/better_translate/variable_extractor.rb +259 -0
- data/lib/better_translate/version.rb +2 -9
- data/lib/better_translate/yaml_handler.rb +168 -0
- data/lib/better_translate.rb +97 -73
- data/lib/generators/better_translate/analyze/USAGE +12 -0
- data/lib/generators/better_translate/analyze/analyze_generator.rb +94 -0
- data/lib/generators/better_translate/install/USAGE +13 -0
- data/lib/generators/better_translate/install/install_generator.rb +71 -0
- data/lib/generators/better_translate/install/templates/README +20 -0
- data/lib/generators/better_translate/install/templates/initializer.rb.tt +47 -0
- data/lib/generators/better_translate/translate/USAGE +13 -0
- data/lib/generators/better_translate/translate/translate_generator.rb +114 -0
- data/lib/tasks/better_translate.rake +136 -0
- data/sig/better_translate/cache.rbs +28 -0
- data/sig/better_translate/cli.rbs +24 -0
- data/sig/better_translate/configuration.rbs +78 -0
- data/sig/better_translate/direct_translator.rbs +18 -0
- data/sig/better_translate/errors.rbs +46 -0
- data/sig/better_translate/progress_tracker.rbs +29 -0
- data/sig/better_translate/provider_factory.rbs +8 -0
- data/sig/better_translate/providers/anthropic_provider.rbs +27 -0
- data/sig/better_translate/providers/base_http_provider.rbs +44 -0
- data/sig/better_translate/providers/chatgpt_provider.rbs +25 -0
- data/sig/better_translate/providers/gemini_provider.rbs +22 -0
- data/sig/better_translate/railtie.rbs +7 -0
- data/sig/better_translate/rate_limiter.rbs +20 -0
- data/sig/better_translate/strategies/base_strategy.rbs +19 -0
- data/sig/better_translate/strategies/batch_strategy.rbs +13 -0
- data/sig/better_translate/strategies/deep_strategy.rbs +11 -0
- data/sig/better_translate/strategies/strategy_selector.rbs +10 -0
- data/sig/better_translate/translator.rbs +24 -0
- data/sig/better_translate/utils/hash_flattener.rbs +14 -0
- data/sig/better_translate/validator.rbs +14 -0
- data/sig/better_translate/variable_extractor.rbs +40 -0
- data/sig/better_translate/version.rbs +4 -0
- data/sig/better_translate/yaml_handler.rbs +29 -0
- data/sig/better_translate.rbs +32 -2
- data/sig/faraday.rbs +22 -0
- data/sig/generators/better_translate/analyze/analyze_generator.rbs +18 -0
- data/sig/generators/better_translate/install/install_generator.rbs +14 -0
- data/sig/generators/better_translate/translate/translate_generator.rbs +10 -0
- data/sig/optparse.rbs +9 -0
- data/sig/psych.rbs +5 -0
- data/sig/rails.rbs +34 -0
- metadata +89 -203
- data/lib/better_translate/helper.rb +0 -83
- data/lib/better_translate/providers/base_provider.rb +0 -102
- data/lib/better_translate/service.rb +0 -144
- data/lib/better_translate/similarity_analyzer.rb +0 -218
- data/lib/better_translate/utils.rb +0 -55
- data/lib/better_translate/writer.rb +0 -75
- data/lib/generators/better_translate/analyze_generator.rb +0 -57
- data/lib/generators/better_translate/install_generator.rb +0 -14
- data/lib/generators/better_translate/templates/better_translate.rb +0 -56
- 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)
|