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,65 @@
|
|
|
1
|
+
# 02 - Error Handling
|
|
2
|
+
|
|
3
|
+
[← Previous: 01-Setup Dependencies](./01-setup_dependencies.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 03-Core Components →](./03-core_components.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Error Handling
|
|
8
|
+
|
|
9
|
+
### 2.1 `lib/better_translate/errors.rb`
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# frozen_string_literal: true
|
|
13
|
+
|
|
14
|
+
module BetterTranslate
|
|
15
|
+
# Base error class for all BetterTranslate errors
|
|
16
|
+
#
|
|
17
|
+
# @abstract
|
|
18
|
+
class Error < StandardError
|
|
19
|
+
# @return [Hash] Additional context about the error
|
|
20
|
+
attr_reader :context
|
|
21
|
+
|
|
22
|
+
# Initialize a new error with optional context
|
|
23
|
+
#
|
|
24
|
+
# @param message [String] The error message
|
|
25
|
+
# @param context [Hash] Additional context information
|
|
26
|
+
def initialize(message = nil, context: {})
|
|
27
|
+
@context = context
|
|
28
|
+
super(message)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Raised when configuration is invalid or incomplete
|
|
33
|
+
class ConfigurationError < Error; end
|
|
34
|
+
|
|
35
|
+
# Raised when input validation fails
|
|
36
|
+
class ValidationError < Error; end
|
|
37
|
+
|
|
38
|
+
# Raised when translation fails
|
|
39
|
+
class TranslationError < Error; end
|
|
40
|
+
|
|
41
|
+
# Raised when a provider encounters an error
|
|
42
|
+
class ProviderError < Error; end
|
|
43
|
+
|
|
44
|
+
# Raised when an API call fails
|
|
45
|
+
class ApiError < Error; end
|
|
46
|
+
|
|
47
|
+
# Raised when rate limit is exceeded
|
|
48
|
+
class RateLimitError < ApiError; end
|
|
49
|
+
|
|
50
|
+
# Raised when file operations fail
|
|
51
|
+
class FileError < Error; end
|
|
52
|
+
|
|
53
|
+
# Raised when YAML parsing fails
|
|
54
|
+
class YamlError < Error; end
|
|
55
|
+
|
|
56
|
+
# Raised when a provider is not found
|
|
57
|
+
class ProviderNotFoundError < Error; end
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
[← Previous: 01-Setup Dependencies](./01-setup_dependencies.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 03-Core Components →](./03-core_components.md)
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
# 03 - Core Components
|
|
2
|
+
|
|
3
|
+
[← Previous: 02-Error Handling](./02-error_handling.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 04-Provider Architecture →](./04-provider_architecture.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Core Components
|
|
8
|
+
|
|
9
|
+
### 3.1 `lib/better_translate/configuration.rb`
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# frozen_string_literal: true
|
|
13
|
+
|
|
14
|
+
module BetterTranslate
|
|
15
|
+
# Configuration class for BetterTranslate
|
|
16
|
+
#
|
|
17
|
+
# Manages all configuration options with type safety and validation.
|
|
18
|
+
#
|
|
19
|
+
# @example Basic configuration
|
|
20
|
+
# config = Configuration.new
|
|
21
|
+
# config.provider = :chatgpt
|
|
22
|
+
# config.openai_key = ENV['OPENAI_API_KEY']
|
|
23
|
+
# config.source_language = "en"
|
|
24
|
+
# config.target_languages = [{ short_name: "it", name: "Italian" }]
|
|
25
|
+
# config.validate!
|
|
26
|
+
#
|
|
27
|
+
class Configuration
|
|
28
|
+
# @return [Symbol] The translation provider (:chatgpt, :gemini, etc.)
|
|
29
|
+
attr_accessor :provider
|
|
30
|
+
|
|
31
|
+
# @return [String, nil] OpenAI API key
|
|
32
|
+
attr_accessor :openai_key
|
|
33
|
+
|
|
34
|
+
# @return [String, nil] Google Gemini API key
|
|
35
|
+
attr_accessor :google_gemini_key
|
|
36
|
+
|
|
37
|
+
# @return [String, nil] Anthropic API key
|
|
38
|
+
attr_accessor :anthropic_key
|
|
39
|
+
|
|
40
|
+
# @return [String] Source language code (e.g., "en")
|
|
41
|
+
attr_accessor :source_language
|
|
42
|
+
|
|
43
|
+
# @return [Array<Hash>] Target languages with :short_name and :name
|
|
44
|
+
attr_accessor :target_languages
|
|
45
|
+
|
|
46
|
+
# @return [String] Path to input YAML file
|
|
47
|
+
attr_accessor :input_file
|
|
48
|
+
|
|
49
|
+
# @return [String] Output folder for translated files
|
|
50
|
+
attr_accessor :output_folder
|
|
51
|
+
|
|
52
|
+
# @return [Symbol] Translation mode (:override or :incremental)
|
|
53
|
+
attr_accessor :translation_mode
|
|
54
|
+
|
|
55
|
+
# @return [String, nil] Translation context for domain-specific terminology
|
|
56
|
+
attr_accessor :translation_context
|
|
57
|
+
|
|
58
|
+
# @return [Integer] Maximum concurrent requests
|
|
59
|
+
attr_accessor :max_concurrent_requests
|
|
60
|
+
|
|
61
|
+
# @return [Integer] Request timeout in seconds
|
|
62
|
+
attr_accessor :request_timeout
|
|
63
|
+
|
|
64
|
+
# @return [Integer] Maximum number of retries
|
|
65
|
+
attr_accessor :max_retries
|
|
66
|
+
|
|
67
|
+
# @return [Float] Retry delay in seconds
|
|
68
|
+
attr_accessor :retry_delay
|
|
69
|
+
|
|
70
|
+
# @return [Boolean] Enable/disable caching
|
|
71
|
+
attr_accessor :cache_enabled
|
|
72
|
+
|
|
73
|
+
# @return [Integer] Cache size (LRU capacity)
|
|
74
|
+
attr_accessor :cache_size
|
|
75
|
+
|
|
76
|
+
# @return [Integer, nil] Cache TTL in seconds (nil = no expiration)
|
|
77
|
+
attr_accessor :cache_ttl
|
|
78
|
+
|
|
79
|
+
# @return [Boolean] Verbose logging
|
|
80
|
+
attr_accessor :verbose
|
|
81
|
+
|
|
82
|
+
# @return [Boolean] Dry run mode (no files written)
|
|
83
|
+
attr_accessor :dry_run
|
|
84
|
+
|
|
85
|
+
# @return [Array<String>] Global exclusions (apply to all languages)
|
|
86
|
+
attr_accessor :global_exclusions
|
|
87
|
+
|
|
88
|
+
# @return [Hash] Language-specific exclusions
|
|
89
|
+
attr_accessor :exclusions_per_language
|
|
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
|
+
end
|
|
107
|
+
|
|
108
|
+
# Validate the configuration
|
|
109
|
+
#
|
|
110
|
+
# @raise [ConfigurationError] if configuration is invalid
|
|
111
|
+
# @return [true] if configuration is valid
|
|
112
|
+
def validate!
|
|
113
|
+
validate_provider!
|
|
114
|
+
validate_api_keys!
|
|
115
|
+
validate_languages!
|
|
116
|
+
validate_files!
|
|
117
|
+
validate_optional_settings!
|
|
118
|
+
true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def validate_provider!
|
|
124
|
+
raise ConfigurationError, "Provider must be set" if provider.nil?
|
|
125
|
+
raise ConfigurationError, "Provider must be a Symbol" unless provider.is_a?(Symbol)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def validate_api_keys!
|
|
129
|
+
case provider
|
|
130
|
+
when :chatgpt
|
|
131
|
+
raise ConfigurationError, "OpenAI API key is required for ChatGPT provider" if openai_key.nil? || openai_key.empty?
|
|
132
|
+
when :gemini
|
|
133
|
+
raise ConfigurationError, "Google Gemini API key is required for Gemini provider" if google_gemini_key.nil? || google_gemini_key.empty?
|
|
134
|
+
when :anthropic
|
|
135
|
+
raise ConfigurationError, "Anthropic API key is required for Anthropic provider" if anthropic_key.nil? || anthropic_key.empty?
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def validate_languages!
|
|
140
|
+
raise ConfigurationError, "Source language must be set" if source_language.nil? || source_language.empty?
|
|
141
|
+
raise ConfigurationError, "Target languages must be an array" unless target_languages.is_a?(Array)
|
|
142
|
+
raise ConfigurationError, "At least one target language is required" if target_languages.empty?
|
|
143
|
+
|
|
144
|
+
target_languages.each do |lang|
|
|
145
|
+
raise ConfigurationError, "Each target language must be a Hash" unless lang.is_a?(Hash)
|
|
146
|
+
raise ConfigurationError, "Target language must have :short_name" unless lang.key?(:short_name)
|
|
147
|
+
raise ConfigurationError, "Target language must have :name" unless lang.key?(:name)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def validate_files!
|
|
152
|
+
raise ConfigurationError, "Input file must be set" if input_file.nil? || input_file.empty?
|
|
153
|
+
raise ConfigurationError, "Output folder must be set" if output_folder.nil? || output_folder.empty?
|
|
154
|
+
raise ConfigurationError, "Input file does not exist: #{input_file}" unless File.exist?(input_file)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def validate_optional_settings!
|
|
158
|
+
valid_modes = [:override, :incremental]
|
|
159
|
+
raise ConfigurationError, "Translation mode must be :override or :incremental" unless valid_modes.include?(translation_mode)
|
|
160
|
+
|
|
161
|
+
raise ConfigurationError, "Max concurrent requests must be positive" if max_concurrent_requests <= 0
|
|
162
|
+
raise ConfigurationError, "Request timeout must be positive" if request_timeout <= 0
|
|
163
|
+
raise ConfigurationError, "Max retries must be non-negative" if max_retries < 0
|
|
164
|
+
raise ConfigurationError, "Cache size must be positive" if cache_size <= 0
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 3.2 `lib/better_translate/cache.rb`
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# frozen_string_literal: true
|
|
174
|
+
|
|
175
|
+
module BetterTranslate
|
|
176
|
+
# LRU (Least Recently Used) Cache implementation
|
|
177
|
+
#
|
|
178
|
+
# Thread-safe cache with configurable capacity and optional TTL.
|
|
179
|
+
#
|
|
180
|
+
# @example Basic usage
|
|
181
|
+
# cache = Cache.new(capacity: 100)
|
|
182
|
+
# cache.set("hello:it", "ciao")
|
|
183
|
+
# cache.get("hello:it") #=> "ciao"
|
|
184
|
+
#
|
|
185
|
+
class Cache
|
|
186
|
+
# @return [Integer] Maximum number of items in cache
|
|
187
|
+
attr_reader :capacity
|
|
188
|
+
|
|
189
|
+
# @return [Integer, nil] Time to live in seconds
|
|
190
|
+
attr_reader :ttl
|
|
191
|
+
|
|
192
|
+
# Initialize a new cache
|
|
193
|
+
#
|
|
194
|
+
# @param capacity [Integer] Maximum cache size
|
|
195
|
+
# @param ttl [Integer, nil] Time to live in seconds
|
|
196
|
+
def initialize(capacity: 1000, ttl: nil)
|
|
197
|
+
@capacity = capacity
|
|
198
|
+
@ttl = ttl
|
|
199
|
+
@cache = {}
|
|
200
|
+
@mutex = Mutex.new
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Get a value from the cache
|
|
204
|
+
#
|
|
205
|
+
# @param key [String] Cache key
|
|
206
|
+
# @return [String, nil] Cached value or nil if not found/expired
|
|
207
|
+
def get(key)
|
|
208
|
+
@mutex.synchronize do
|
|
209
|
+
return nil unless @cache.key?(key)
|
|
210
|
+
|
|
211
|
+
entry = @cache[key]
|
|
212
|
+
|
|
213
|
+
# Check TTL
|
|
214
|
+
if @ttl && Time.now - entry[:timestamp] > @ttl
|
|
215
|
+
@cache.delete(key)
|
|
216
|
+
return nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Move to end (most recently used)
|
|
220
|
+
@cache.delete(key)
|
|
221
|
+
@cache[key] = entry
|
|
222
|
+
entry[:value]
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Set a value in the cache
|
|
227
|
+
#
|
|
228
|
+
# @param key [String] Cache key
|
|
229
|
+
# @param value [String] Value to cache
|
|
230
|
+
# @return [String] The cached value
|
|
231
|
+
def set(key, value)
|
|
232
|
+
@mutex.synchronize do
|
|
233
|
+
# Remove oldest entry if at capacity
|
|
234
|
+
@cache.shift if @cache.size >= @capacity && !@cache.key?(key)
|
|
235
|
+
|
|
236
|
+
@cache[key] = {
|
|
237
|
+
value: value,
|
|
238
|
+
timestamp: Time.now
|
|
239
|
+
}
|
|
240
|
+
value
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Clear the cache
|
|
245
|
+
#
|
|
246
|
+
# @return [void]
|
|
247
|
+
def clear
|
|
248
|
+
@mutex.synchronize { @cache.clear }
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Get cache size
|
|
252
|
+
#
|
|
253
|
+
# @return [Integer] Number of items in cache
|
|
254
|
+
def size
|
|
255
|
+
@mutex.synchronize { @cache.size }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Check if key exists in cache
|
|
259
|
+
#
|
|
260
|
+
# @param key [String] Cache key
|
|
261
|
+
# @return [Boolean] true if key exists and not expired
|
|
262
|
+
def key?(key)
|
|
263
|
+
!get(key).nil?
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### 3.3 `lib/better_translate/rate_limiter.rb`
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
# frozen_string_literal: true
|
|
273
|
+
|
|
274
|
+
module BetterTranslate
|
|
275
|
+
# Thread-safe rate limiter
|
|
276
|
+
#
|
|
277
|
+
# Ensures requests are spaced out by a minimum delay.
|
|
278
|
+
#
|
|
279
|
+
# @example
|
|
280
|
+
# limiter = RateLimiter.new(delay: 0.5)
|
|
281
|
+
# limiter.wait # Waits if needed
|
|
282
|
+
#
|
|
283
|
+
class RateLimiter
|
|
284
|
+
# @return [Float] Delay between requests in seconds
|
|
285
|
+
attr_reader :delay
|
|
286
|
+
|
|
287
|
+
# Initialize a new rate limiter
|
|
288
|
+
#
|
|
289
|
+
# @param delay [Float] Delay in seconds between requests
|
|
290
|
+
def initialize(delay: 0.5)
|
|
291
|
+
@delay = delay
|
|
292
|
+
@last_request_time = nil
|
|
293
|
+
@mutex = Mutex.new
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Wait if necessary to respect rate limit
|
|
297
|
+
#
|
|
298
|
+
# @return [void]
|
|
299
|
+
def wait
|
|
300
|
+
@mutex.synchronize do
|
|
301
|
+
return if @last_request_time.nil?
|
|
302
|
+
|
|
303
|
+
elapsed = Time.now - @last_request_time
|
|
304
|
+
sleep_time = @delay - elapsed
|
|
305
|
+
|
|
306
|
+
sleep(sleep_time) if sleep_time > 0
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Record that a request was made
|
|
311
|
+
#
|
|
312
|
+
# @return [void]
|
|
313
|
+
def record_request
|
|
314
|
+
@mutex.synchronize { @last_request_time = Time.now }
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Reset the rate limiter
|
|
318
|
+
#
|
|
319
|
+
# @return [void]
|
|
320
|
+
def reset
|
|
321
|
+
@mutex.synchronize { @last_request_time = nil }
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### 3.4 `lib/better_translate/validator.rb`
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
# frozen_string_literal: true
|
|
331
|
+
|
|
332
|
+
module BetterTranslate
|
|
333
|
+
# Input validation utilities
|
|
334
|
+
#
|
|
335
|
+
# Validates language codes, text, paths, and other inputs.
|
|
336
|
+
class Validator
|
|
337
|
+
# Validate a language code
|
|
338
|
+
#
|
|
339
|
+
# @param code [String] Language code to validate
|
|
340
|
+
# @raise [ValidationError] if code is invalid
|
|
341
|
+
# @return [true] if valid
|
|
342
|
+
def self.validate_language_code!(code)
|
|
343
|
+
raise ValidationError, "Language code cannot be nil" if code.nil?
|
|
344
|
+
raise ValidationError, "Language code must be a String" unless code.is_a?(String)
|
|
345
|
+
raise ValidationError, "Language code cannot be empty" if code.empty?
|
|
346
|
+
raise ValidationError, "Language code must be 2 letters" unless code.match?(/^[a-z]{2}$/i)
|
|
347
|
+
true
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Validate text for translation
|
|
351
|
+
#
|
|
352
|
+
# @param text [String] Text to validate
|
|
353
|
+
# @raise [ValidationError] if text is invalid
|
|
354
|
+
# @return [true] if valid
|
|
355
|
+
def self.validate_text!(text)
|
|
356
|
+
raise ValidationError, "Text cannot be nil" if text.nil?
|
|
357
|
+
raise ValidationError, "Text must be a String" unless text.is_a?(String)
|
|
358
|
+
raise ValidationError, "Text cannot be empty" if text.strip.empty?
|
|
359
|
+
true
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Validate a file path exists
|
|
363
|
+
#
|
|
364
|
+
# @param path [String] File path to validate
|
|
365
|
+
# @raise [FileError] if path is invalid
|
|
366
|
+
# @return [true] if valid
|
|
367
|
+
def self.validate_file_exists!(path)
|
|
368
|
+
raise FileError, "File path cannot be nil" if path.nil?
|
|
369
|
+
raise FileError, "File path must be a String" unless path.is_a?(String)
|
|
370
|
+
raise FileError, "File does not exist: #{path}" unless File.exist?(path)
|
|
371
|
+
true
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Validate an API key
|
|
375
|
+
#
|
|
376
|
+
# @param key [String] API key to validate
|
|
377
|
+
# @param provider [Symbol] Provider name for error message
|
|
378
|
+
# @raise [ConfigurationError] if key is invalid
|
|
379
|
+
# @return [true] if valid
|
|
380
|
+
def self.validate_api_key!(key, provider:)
|
|
381
|
+
raise ConfigurationError, "API key for #{provider} cannot be nil" if key.nil?
|
|
382
|
+
raise ConfigurationError, "API key for #{provider} must be a String" unless key.is_a?(String)
|
|
383
|
+
raise ConfigurationError, "API key for #{provider} cannot be empty" if key.strip.empty?
|
|
384
|
+
true
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### 3.5 `lib/better_translate/utils/hash_flattener.rb`
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
# frozen_string_literal: true
|
|
394
|
+
|
|
395
|
+
module BetterTranslate
|
|
396
|
+
module Utils
|
|
397
|
+
# Utilities for flattening and unflattening nested hashes
|
|
398
|
+
#
|
|
399
|
+
# Used to convert nested YAML structures to flat key-value pairs
|
|
400
|
+
# and back again.
|
|
401
|
+
#
|
|
402
|
+
# @example
|
|
403
|
+
# nested = { "user" => { "name" => "John", "age" => 30 } }
|
|
404
|
+
# flat = HashFlattener.flatten(nested)
|
|
405
|
+
# #=> { "user.name" => "John", "user.age" => 30 }
|
|
406
|
+
#
|
|
407
|
+
# HashFlattener.unflatten(flat)
|
|
408
|
+
# #=> { "user" => { "name" => "John", "age" => 30 } }
|
|
409
|
+
#
|
|
410
|
+
class HashFlattener
|
|
411
|
+
# Flatten a nested hash to dot-notation keys
|
|
412
|
+
#
|
|
413
|
+
# @param hash [Hash] Nested hash to flatten
|
|
414
|
+
# @param parent_key [String] Parent key prefix
|
|
415
|
+
# @param separator [String] Key separator
|
|
416
|
+
# @return [Hash] Flattened hash
|
|
417
|
+
def self.flatten(hash, parent_key = "", separator = ".")
|
|
418
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
419
|
+
new_key = parent_key.empty? ? key.to_s : "#{parent_key}#{separator}#{key}"
|
|
420
|
+
|
|
421
|
+
if value.is_a?(Hash)
|
|
422
|
+
result.merge!(flatten(value, new_key, separator))
|
|
423
|
+
else
|
|
424
|
+
result[new_key] = value
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Unflatten a hash with dot-notation keys to nested structure
|
|
430
|
+
#
|
|
431
|
+
# @param hash [Hash] Flattened hash
|
|
432
|
+
# @param separator [String] Key separator
|
|
433
|
+
# @return [Hash] Nested hash
|
|
434
|
+
def self.unflatten(hash, separator = ".")
|
|
435
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
436
|
+
keys = key.split(separator)
|
|
437
|
+
last_key = keys.pop
|
|
438
|
+
|
|
439
|
+
# Build nested structure
|
|
440
|
+
nested = keys.reduce(result) do |memo, k|
|
|
441
|
+
memo[k] ||= {}
|
|
442
|
+
memo[k]
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
nested[last_key] = value
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
[← Previous: 02-Error Handling](./02-error_handling.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 04-Provider Architecture →](./04-provider_architecture.md)
|