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