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,698 @@
|
|
|
1
|
+
# 12 - CLI Standalone
|
|
2
|
+
|
|
3
|
+
[← Previous: 11-Quality Security](./11-quality_security.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## CLI Standalone Tool
|
|
8
|
+
|
|
9
|
+
**PRIORITY: HIGH** - Enables BetterTranslate usage from command line without Ruby code.
|
|
10
|
+
|
|
11
|
+
### Overview
|
|
12
|
+
|
|
13
|
+
A standalone command-line interface for BetterTranslate that works:
|
|
14
|
+
- **Outside Rails**: Use in any Ruby project or standalone
|
|
15
|
+
- **CI/CD Integration**: Automate translations in pipelines
|
|
16
|
+
- **Quick Testing**: Test translations without writing code
|
|
17
|
+
- **Batch Operations**: Process multiple files easily
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 12.1 `exe/better_translate`
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
#!/usr/bin/env ruby
|
|
25
|
+
# frozen_string_literal: true
|
|
26
|
+
|
|
27
|
+
require "bundler/setup"
|
|
28
|
+
require "better_translate"
|
|
29
|
+
require_relative "../lib/better_translate/cli"
|
|
30
|
+
|
|
31
|
+
BetterTranslate::CLI.start(ARGV)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Make executable:
|
|
35
|
+
```bash
|
|
36
|
+
chmod +x exe/better_translate
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 12.2 `lib/better_translate/cli.rb`
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
# frozen_string_literal: true
|
|
45
|
+
|
|
46
|
+
require "optparse"
|
|
47
|
+
|
|
48
|
+
module BetterTranslate
|
|
49
|
+
# Command-line interface for BetterTranslate
|
|
50
|
+
#
|
|
51
|
+
# Provides commands for translation, analysis, and configuration.
|
|
52
|
+
#
|
|
53
|
+
# @example
|
|
54
|
+
# better_translate translate config/locales/en.yml --to it,fr
|
|
55
|
+
# better_translate text "Hello world" --from en --to it
|
|
56
|
+
# better_translate analyze config/locales/*.yml
|
|
57
|
+
#
|
|
58
|
+
class CLI
|
|
59
|
+
# CLI version
|
|
60
|
+
VERSION = BetterTranslate::VERSION
|
|
61
|
+
|
|
62
|
+
# Available commands
|
|
63
|
+
COMMANDS = %w[translate text analyze version help].freeze
|
|
64
|
+
|
|
65
|
+
# Start CLI with arguments
|
|
66
|
+
#
|
|
67
|
+
# @param args [Array<String>] Command-line arguments
|
|
68
|
+
# @return [void]
|
|
69
|
+
def self.start(args)
|
|
70
|
+
new(args).execute
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Initialize CLI
|
|
74
|
+
#
|
|
75
|
+
# @param args [Array<String>] Command-line arguments
|
|
76
|
+
def initialize(args)
|
|
77
|
+
@args = args
|
|
78
|
+
@options = {}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Execute the CLI command
|
|
82
|
+
#
|
|
83
|
+
# @return [void]
|
|
84
|
+
def execute
|
|
85
|
+
return show_help if @args.empty?
|
|
86
|
+
|
|
87
|
+
command = @args.shift
|
|
88
|
+
return show_help unless COMMANDS.include?(command)
|
|
89
|
+
|
|
90
|
+
send("command_#{command}")
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
error "Error: #{e.message}"
|
|
93
|
+
exit 1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# Command: translate
|
|
99
|
+
#
|
|
100
|
+
# Translate YAML file(s)
|
|
101
|
+
def command_translate
|
|
102
|
+
parse_translate_options
|
|
103
|
+
|
|
104
|
+
# Load configuration file if exists
|
|
105
|
+
load_config_file
|
|
106
|
+
|
|
107
|
+
# Configure BetterTranslate
|
|
108
|
+
BetterTranslate.configure do |config|
|
|
109
|
+
config.provider = @options[:provider]&.to_sym || :chatgpt
|
|
110
|
+
config.source_language = @options[:from] || "en"
|
|
111
|
+
config.target_languages = build_target_languages(@options[:to])
|
|
112
|
+
config.input_file = @options[:input]
|
|
113
|
+
config.output_folder = @options[:output] || File.dirname(@options[:input])
|
|
114
|
+
config.verbose = @options[:verbose]
|
|
115
|
+
config.dry_run = @options[:dry_run]
|
|
116
|
+
|
|
117
|
+
# Set API keys from environment
|
|
118
|
+
config.openai_key = ENV["OPENAI_API_KEY"]
|
|
119
|
+
config.google_gemini_key = ENV["GEMINI_API_KEY"]
|
|
120
|
+
config.anthropic_key = ENV["ANTHROPIC_API_KEY"]
|
|
121
|
+
|
|
122
|
+
# Optional settings
|
|
123
|
+
config.translation_context = @options[:context] if @options[:context]
|
|
124
|
+
config.cache_enabled = !@options[:no_cache]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Run translation
|
|
128
|
+
success "Translating #{@options[:input]}..."
|
|
129
|
+
results = BetterTranslate.translate_all
|
|
130
|
+
|
|
131
|
+
# Show results
|
|
132
|
+
success "✓ Successfully translated #{results[:success_count]} language(s)"
|
|
133
|
+
if results[:failure_count] > 0
|
|
134
|
+
error "✗ Failed to translate #{results[:failure_count]} language(s)"
|
|
135
|
+
results[:errors].each do |err|
|
|
136
|
+
error " - #{err[:language]}: #{err[:error]}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Command: text
|
|
142
|
+
#
|
|
143
|
+
# Translate text directly
|
|
144
|
+
def command_text
|
|
145
|
+
parse_text_options
|
|
146
|
+
|
|
147
|
+
text = @args.shift
|
|
148
|
+
return error("Error: Text argument required") unless text
|
|
149
|
+
|
|
150
|
+
success "Translating text..."
|
|
151
|
+
result = BetterTranslate.translate_text(
|
|
152
|
+
text,
|
|
153
|
+
from: @options[:from] || "en",
|
|
154
|
+
to: @options[:to].split(","),
|
|
155
|
+
provider: @options[:provider]&.to_sym,
|
|
156
|
+
context: @options[:context]
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Display results
|
|
160
|
+
if result.is_a?(String)
|
|
161
|
+
puts "\n#{result}"
|
|
162
|
+
else
|
|
163
|
+
puts "\nTranslations:"
|
|
164
|
+
result.each do |lang, translation|
|
|
165
|
+
puts " #{lang}: #{translation}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Command: analyze
|
|
171
|
+
#
|
|
172
|
+
# Analyze translation quality
|
|
173
|
+
def command_analyze
|
|
174
|
+
parse_analyze_options
|
|
175
|
+
|
|
176
|
+
files = @args
|
|
177
|
+
return error("Error: No files specified") if files.empty?
|
|
178
|
+
|
|
179
|
+
success "Analyzing translations..."
|
|
180
|
+
|
|
181
|
+
# Load first file as source
|
|
182
|
+
source_file = files.first
|
|
183
|
+
source_data = YAML.load_file(source_file)
|
|
184
|
+
source_lang = source_data.keys.first
|
|
185
|
+
source_strings = flatten_hash(source_data[source_lang] || source_data)
|
|
186
|
+
|
|
187
|
+
# Compare with other files
|
|
188
|
+
files[1..].each do |file|
|
|
189
|
+
target_data = YAML.load_file(file)
|
|
190
|
+
target_lang = target_data.keys.first
|
|
191
|
+
target_strings = flatten_hash(target_data[target_lang] || target_data)
|
|
192
|
+
|
|
193
|
+
puts "\n#{File.basename(file)} (#{target_lang}):"
|
|
194
|
+
analyze_similarity(source_strings, target_strings)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Command: version
|
|
199
|
+
#
|
|
200
|
+
# Show version
|
|
201
|
+
def command_version
|
|
202
|
+
puts "BetterTranslate version #{VERSION}"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Command: help
|
|
206
|
+
#
|
|
207
|
+
# Show help
|
|
208
|
+
def command_help
|
|
209
|
+
show_help
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Parse options for translate command
|
|
213
|
+
def parse_translate_options
|
|
214
|
+
OptionParser.new do |opts|
|
|
215
|
+
opts.banner = "Usage: better_translate translate FILE [options]"
|
|
216
|
+
|
|
217
|
+
opts.on("-t", "--to LANGUAGES", "Target languages (comma-separated)") do |langs|
|
|
218
|
+
@options[:to] = langs
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
opts.on("-f", "--from LANG", "Source language (default: en)") do |lang|
|
|
222
|
+
@options[:from] = lang
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
opts.on("-o", "--output DIR", "Output directory") do |dir|
|
|
226
|
+
@options[:output] = dir
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
opts.on("-p", "--provider PROVIDER", "Translation provider (chatgpt, gemini, anthropic)") do |provider|
|
|
230
|
+
@options[:provider] = provider
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
opts.on("-c", "--context TEXT", "Translation context") do |context|
|
|
234
|
+
@options[:context] = context
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
opts.on("-v", "--verbose", "Verbose output") do
|
|
238
|
+
@options[:verbose] = true
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
opts.on("-d", "--dry-run", "Dry run (don't write files)") do
|
|
242
|
+
@options[:dry_run] = true
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
opts.on("--no-cache", "Disable caching") do
|
|
246
|
+
@options[:no_cache] = true
|
|
247
|
+
end
|
|
248
|
+
end.parse!(@args)
|
|
249
|
+
|
|
250
|
+
@options[:input] = @args.shift
|
|
251
|
+
return error("Error: Input file required") unless @options[:input]
|
|
252
|
+
return error("Error: --to languages required") unless @options[:to]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Parse options for text command
|
|
256
|
+
def parse_text_options
|
|
257
|
+
OptionParser.new do |opts|
|
|
258
|
+
opts.banner = "Usage: better_translate text TEXT [options]"
|
|
259
|
+
|
|
260
|
+
opts.on("-t", "--to LANGUAGES", "Target languages (comma-separated)") do |langs|
|
|
261
|
+
@options[:to] = langs
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
opts.on("-f", "--from LANG", "Source language (default: en)") do |lang|
|
|
265
|
+
@options[:from] = lang
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
opts.on("-p", "--provider PROVIDER", "Translation provider") do |provider|
|
|
269
|
+
@options[:provider] = provider
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
opts.on("-c", "--context TEXT", "Translation context") do |context|
|
|
273
|
+
@options[:context] = context
|
|
274
|
+
end
|
|
275
|
+
end.parse!(@args)
|
|
276
|
+
|
|
277
|
+
return error("Error: --to languages required") unless @options[:to]
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Parse options for analyze command
|
|
281
|
+
def parse_analyze_options
|
|
282
|
+
OptionParser.new do |opts|
|
|
283
|
+
opts.banner = "Usage: better_translate analyze FILES..."
|
|
284
|
+
|
|
285
|
+
opts.on("-t", "--threshold PERCENT", Float, "Similarity threshold (default: 80)") do |threshold|
|
|
286
|
+
@options[:threshold] = threshold
|
|
287
|
+
end
|
|
288
|
+
end.parse!(@args)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Build target languages array from comma-separated string
|
|
292
|
+
#
|
|
293
|
+
# @param langs_str [String] Comma-separated language codes
|
|
294
|
+
# @return [Array<Hash>] Array of language hashes
|
|
295
|
+
def build_target_languages(langs_str)
|
|
296
|
+
langs_str.split(",").map do |lang_code|
|
|
297
|
+
{
|
|
298
|
+
short_name: lang_code.strip,
|
|
299
|
+
name: lang_code.strip.upcase
|
|
300
|
+
}
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Load configuration file if exists
|
|
305
|
+
def load_config_file
|
|
306
|
+
config_file = ".better_translate.yml"
|
|
307
|
+
return unless File.exist?(config_file)
|
|
308
|
+
|
|
309
|
+
success "Loading configuration from #{config_file}..."
|
|
310
|
+
config_data = YAML.load_file(config_file)
|
|
311
|
+
|
|
312
|
+
# Merge config file options with CLI options (CLI takes precedence)
|
|
313
|
+
config_data.each do |key, value|
|
|
314
|
+
@options[key.to_sym] ||= value
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Flatten nested hash
|
|
319
|
+
#
|
|
320
|
+
# @param hash [Hash] Nested hash
|
|
321
|
+
# @param prefix [String] Key prefix
|
|
322
|
+
# @return [Hash] Flattened hash
|
|
323
|
+
def flatten_hash(hash, prefix = "")
|
|
324
|
+
result = {}
|
|
325
|
+
hash.each do |key, value|
|
|
326
|
+
full_key = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
|
|
327
|
+
if value.is_a?(Hash)
|
|
328
|
+
result.merge!(flatten_hash(value, full_key))
|
|
329
|
+
else
|
|
330
|
+
result[full_key] = value
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
result
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Analyze similarity between source and target strings
|
|
337
|
+
#
|
|
338
|
+
# @param source [Hash] Source strings
|
|
339
|
+
# @param target [Hash] Target strings
|
|
340
|
+
def analyze_similarity(source, target)
|
|
341
|
+
threshold = @options[:threshold] || 80.0
|
|
342
|
+
suspicious = []
|
|
343
|
+
|
|
344
|
+
source.each do |key, source_value|
|
|
345
|
+
target_value = target[key]
|
|
346
|
+
next unless target_value
|
|
347
|
+
|
|
348
|
+
similarity = calculate_similarity(source_value.to_s, target_value.to_s)
|
|
349
|
+
if similarity > threshold / 100.0
|
|
350
|
+
suspicious << {
|
|
351
|
+
key: key,
|
|
352
|
+
source: source_value,
|
|
353
|
+
target: target_value,
|
|
354
|
+
similarity: (similarity * 100).round(1)
|
|
355
|
+
}
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
if suspicious.empty?
|
|
360
|
+
success " ✓ No suspicious translations found"
|
|
361
|
+
else
|
|
362
|
+
warning " ⚠ Found #{suspicious.size} potentially untranslated strings:"
|
|
363
|
+
suspicious.each do |item|
|
|
364
|
+
puts " #{item[:key]}: #{item[:similarity]}% similar"
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Calculate Levenshtein similarity
|
|
370
|
+
#
|
|
371
|
+
# @param str1 [String] First string
|
|
372
|
+
# @param str2 [String] Second string
|
|
373
|
+
# @return [Float] Similarity score (0.0 to 1.0)
|
|
374
|
+
def calculate_similarity(str1, str2)
|
|
375
|
+
return 1.0 if str1 == str2
|
|
376
|
+
return 0.0 if str1.empty? || str2.empty?
|
|
377
|
+
|
|
378
|
+
distance = levenshtein_distance(str1.downcase, str2.downcase)
|
|
379
|
+
max_length = [str1.length, str2.length].max
|
|
380
|
+
1.0 - (distance.to_f / max_length)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Calculate Levenshtein distance
|
|
384
|
+
#
|
|
385
|
+
# @param str1 [String] First string
|
|
386
|
+
# @param str2 [String] Second string
|
|
387
|
+
# @return [Integer] Edit distance
|
|
388
|
+
def levenshtein_distance(str1, str2)
|
|
389
|
+
matrix = Array.new(str1.length + 1) { Array.new(str2.length + 1) }
|
|
390
|
+
|
|
391
|
+
(0..str1.length).each { |i| matrix[i][0] = i }
|
|
392
|
+
(0..str2.length).each { |j| matrix[0][j] = j }
|
|
393
|
+
|
|
394
|
+
(1..str1.length).each do |i|
|
|
395
|
+
(1..str2.length).each do |j|
|
|
396
|
+
cost = str1[i - 1] == str2[j - 1] ? 0 : 1
|
|
397
|
+
matrix[i][j] = [
|
|
398
|
+
matrix[i - 1][j] + 1,
|
|
399
|
+
matrix[i][j - 1] + 1,
|
|
400
|
+
matrix[i - 1][j - 1] + cost
|
|
401
|
+
].min
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
matrix[str1.length][str2.length]
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Show help message
|
|
409
|
+
def show_help
|
|
410
|
+
puts <<~HELP
|
|
411
|
+
BetterTranslate CLI - AI-powered YAML translation tool
|
|
412
|
+
|
|
413
|
+
Usage: better_translate COMMAND [options]
|
|
414
|
+
|
|
415
|
+
Commands:
|
|
416
|
+
translate FILE Translate YAML file to target languages
|
|
417
|
+
text TEXT Translate text directly
|
|
418
|
+
analyze FILES Analyze translation quality
|
|
419
|
+
version Show version
|
|
420
|
+
help Show this help
|
|
421
|
+
|
|
422
|
+
Examples:
|
|
423
|
+
# Translate YAML file
|
|
424
|
+
better_translate translate config/locales/en.yml --to it,fr,de
|
|
425
|
+
|
|
426
|
+
# Translate text
|
|
427
|
+
better_translate text "Hello world" --from en --to it,fr
|
|
428
|
+
|
|
429
|
+
# Analyze translations
|
|
430
|
+
better_translate analyze config/locales/*.yml
|
|
431
|
+
|
|
432
|
+
Translate Options:
|
|
433
|
+
-t, --to LANGUAGES Target languages (comma-separated, required)
|
|
434
|
+
-f, --from LANG Source language (default: en)
|
|
435
|
+
-o, --output DIR Output directory
|
|
436
|
+
-p, --provider PROVIDER Provider: chatgpt, gemini, anthropic (default: chatgpt)
|
|
437
|
+
-c, --context TEXT Translation context for better accuracy
|
|
438
|
+
-v, --verbose Verbose output with progress
|
|
439
|
+
-d, --dry-run Preview changes without writing files
|
|
440
|
+
--no-cache Disable translation caching
|
|
441
|
+
|
|
442
|
+
Text Options:
|
|
443
|
+
-t, --to LANGUAGES Target languages (comma-separated, required)
|
|
444
|
+
-f, --from LANG Source language (default: en)
|
|
445
|
+
-p, --provider PROVIDER Provider: chatgpt, gemini, anthropic
|
|
446
|
+
-c, --context TEXT Translation context
|
|
447
|
+
|
|
448
|
+
Environment Variables:
|
|
449
|
+
OPENAI_API_KEY API key for ChatGPT provider
|
|
450
|
+
GEMINI_API_KEY API key for Google Gemini provider
|
|
451
|
+
ANTHROPIC_API_KEY API key for Anthropic Claude provider
|
|
452
|
+
|
|
453
|
+
Configuration File:
|
|
454
|
+
Create .better_translate.yml in project root for default settings.
|
|
455
|
+
See: https://github.com/alessiobussolari/better_translate
|
|
456
|
+
|
|
457
|
+
More info: https://github.com/alessiobussolari/better_translate
|
|
458
|
+
HELP
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Print success message in green
|
|
462
|
+
def success(message)
|
|
463
|
+
puts "\e[32m#{message}\e[0m"
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Print error message in red
|
|
467
|
+
def error(message)
|
|
468
|
+
puts "\e[31m#{message}\e[0m"
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Print warning message in yellow
|
|
472
|
+
def warning(message)
|
|
473
|
+
puts "\e[33m#{message}\e[0m"
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## 12.3 Update `better_translate.gemspec`
|
|
482
|
+
|
|
483
|
+
Add executable specification:
|
|
484
|
+
|
|
485
|
+
```ruby
|
|
486
|
+
spec.bindir = "exe"
|
|
487
|
+
spec.executables = ["better_translate"]
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## 12.4 Test: `spec/better_translate/cli_spec.rb`
|
|
493
|
+
|
|
494
|
+
```ruby
|
|
495
|
+
# frozen_string_literal: true
|
|
496
|
+
|
|
497
|
+
RSpec.describe BetterTranslate::CLI do
|
|
498
|
+
describe ".start" do
|
|
499
|
+
it "shows help when no arguments" do
|
|
500
|
+
expect { described_class.start([]) }.to output(/Usage/).to_stdout
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
it "shows version" do
|
|
504
|
+
expect { described_class.start(["version"]) }.to output(/BetterTranslate version/).to_stdout
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
it "shows help for help command" do
|
|
508
|
+
expect { described_class.start(["help"]) }.to output(/Usage/).to_stdout
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
describe "translate command" do
|
|
513
|
+
let(:temp_file) { create_temp_yaml("en" => { "hello" => "Hello" }) }
|
|
514
|
+
|
|
515
|
+
it "requires input file" do
|
|
516
|
+
expect {
|
|
517
|
+
described_class.start(["translate", "--to", "it"])
|
|
518
|
+
}.to output(/Error: Input file required/).to_stdout.and raise_error(SystemExit)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
it "requires --to option" do
|
|
522
|
+
expect {
|
|
523
|
+
described_class.start(["translate", temp_file])
|
|
524
|
+
}.to output(/Error: --to languages required/).to_stdout.and raise_error(SystemExit)
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
it "translates file", :vcr do
|
|
528
|
+
ENV["OPENAI_API_KEY"] = "test-key"
|
|
529
|
+
|
|
530
|
+
expect {
|
|
531
|
+
described_class.start(["translate", temp_file, "--to", "it", "--verbose"])
|
|
532
|
+
}.to output(/Successfully translated/).to_stdout
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
describe "text command" do
|
|
537
|
+
it "requires text argument" do
|
|
538
|
+
expect {
|
|
539
|
+
described_class.start(["text", "--to", "it"])
|
|
540
|
+
}.to output(/Error: Text argument required/).to_stdout.and raise_error(SystemExit)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
it "requires --to option" do
|
|
544
|
+
expect {
|
|
545
|
+
described_class.start(["text", "Hello"])
|
|
546
|
+
}.to output(/Error: --to languages required/).to_stdout.and raise_error(SystemExit)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
it "translates text", :vcr do
|
|
550
|
+
ENV["OPENAI_API_KEY"] = "test-key"
|
|
551
|
+
|
|
552
|
+
expect {
|
|
553
|
+
described_class.start(["text", "Hello", "--from", "en", "--to", "it"])
|
|
554
|
+
}.to output(/Ciao/).to_stdout
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## Usage Examples
|
|
563
|
+
|
|
564
|
+
### Example 1: Translate YAML File
|
|
565
|
+
|
|
566
|
+
```bash
|
|
567
|
+
# Basic translation
|
|
568
|
+
better_translate translate config/locales/en.yml --to it,fr,de
|
|
569
|
+
|
|
570
|
+
# With custom output directory
|
|
571
|
+
better_translate translate config/locales/en.yml --to it --output locales/
|
|
572
|
+
|
|
573
|
+
# Dry run to preview
|
|
574
|
+
better_translate translate config/locales/en.yml --to it --dry-run
|
|
575
|
+
|
|
576
|
+
# With context for better accuracy
|
|
577
|
+
better_translate translate config/locales/en.yml --to it \
|
|
578
|
+
--context "Medical terminology for healthcare app"
|
|
579
|
+
|
|
580
|
+
# Verbose output
|
|
581
|
+
better_translate translate config/locales/en.yml --to it,fr --verbose
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### Example 2: Translate Text Directly
|
|
585
|
+
|
|
586
|
+
```bash
|
|
587
|
+
# Single language
|
|
588
|
+
better_translate text "Hello world" --from en --to it
|
|
589
|
+
|
|
590
|
+
# Multiple languages
|
|
591
|
+
better_translate text "Good morning" --from en --to it,fr,de,es
|
|
592
|
+
|
|
593
|
+
# With context
|
|
594
|
+
better_translate text "The patient presents with symptoms" \
|
|
595
|
+
--from en --to it --context "Medical terminology"
|
|
596
|
+
|
|
597
|
+
# Using different provider
|
|
598
|
+
better_translate text "Hello" --from en --to it --provider anthropic
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Example 3: Analyze Translation Quality
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
# Analyze all locale files
|
|
605
|
+
better_translate analyze config/locales/*.yml
|
|
606
|
+
|
|
607
|
+
# With custom similarity threshold
|
|
608
|
+
better_translate analyze config/locales/*.yml --threshold 90
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### Example 4: Using Configuration File
|
|
612
|
+
|
|
613
|
+
Create `.better_translate.yml`:
|
|
614
|
+
```yaml
|
|
615
|
+
provider: chatgpt
|
|
616
|
+
source_language: en
|
|
617
|
+
output_folder: config/locales
|
|
618
|
+
verbose: true
|
|
619
|
+
cache_enabled: true
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Then simply:
|
|
623
|
+
```bash
|
|
624
|
+
better_translate translate config/locales/en.yml --to it,fr
|
|
625
|
+
# Loads settings from .better_translate.yml
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## CI/CD Integration
|
|
631
|
+
|
|
632
|
+
### GitHub Actions Example
|
|
633
|
+
|
|
634
|
+
```yaml
|
|
635
|
+
name: Translate Locales
|
|
636
|
+
|
|
637
|
+
on:
|
|
638
|
+
push:
|
|
639
|
+
paths:
|
|
640
|
+
- 'config/locales/en.yml'
|
|
641
|
+
|
|
642
|
+
jobs:
|
|
643
|
+
translate:
|
|
644
|
+
runs-on: ubuntu-latest
|
|
645
|
+
steps:
|
|
646
|
+
- uses: actions/checkout@v2
|
|
647
|
+
|
|
648
|
+
- name: Set up Ruby
|
|
649
|
+
uses: ruby/setup-ruby@v1
|
|
650
|
+
with:
|
|
651
|
+
ruby-version: 3.2
|
|
652
|
+
|
|
653
|
+
- name: Install BetterTranslate
|
|
654
|
+
run: gem install better_translate
|
|
655
|
+
|
|
656
|
+
- name: Translate locales
|
|
657
|
+
env:
|
|
658
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
659
|
+
run: |
|
|
660
|
+
better_translate translate config/locales/en.yml \
|
|
661
|
+
--to it,fr,de,es \
|
|
662
|
+
--verbose
|
|
663
|
+
|
|
664
|
+
- name: Commit translations
|
|
665
|
+
run: |
|
|
666
|
+
git config user.name "GitHub Actions"
|
|
667
|
+
git config user.email "actions@github.com"
|
|
668
|
+
git add config/locales/*.yml
|
|
669
|
+
git commit -m "Auto-translate locales [skip ci]"
|
|
670
|
+
git push
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## Benefits
|
|
676
|
+
|
|
677
|
+
1. **No Code Required**: Use without writing Ruby code
|
|
678
|
+
2. **CI/CD Ready**: Easy integration into automation pipelines
|
|
679
|
+
3. **Quick Testing**: Test translations instantly
|
|
680
|
+
4. **Standalone**: Works outside Rails/Bundler
|
|
681
|
+
5. **Scriptable**: Perfect for automation scripts
|
|
682
|
+
6. **Configuration File**: Reusable settings via `.better_translate.yml`
|
|
683
|
+
|
|
684
|
+
---
|
|
685
|
+
|
|
686
|
+
## Implementation Checklist
|
|
687
|
+
|
|
688
|
+
- [ ] Create `exe/better_translate` executable
|
|
689
|
+
- [ ] Create `lib/better_translate/cli.rb` with all commands
|
|
690
|
+
- [ ] Update gemspec to include executable
|
|
691
|
+
- [ ] Create comprehensive CLI tests
|
|
692
|
+
- [ ] Update README with CLI documentation
|
|
693
|
+
- [ ] Add CI/CD integration examples
|
|
694
|
+
- [ ] Test on multiple Ruby versions
|
|
695
|
+
|
|
696
|
+
---
|
|
697
|
+
|
|
698
|
+
[← Previous: 11-Quality Security](./11-quality_security.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md)
|