better_translate 0.5.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +14 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +8 -0
  5. data/.yardopts +10 -0
  6. data/CHANGELOG.md +125 -114
  7. data/CLAUDE.md +385 -0
  8. data/README.md +629 -244
  9. data/Rakefile +7 -1
  10. data/Steepfile +29 -0
  11. data/docs/implementation/00-overview.md +220 -0
  12. data/docs/implementation/01-setup_dependencies.md +668 -0
  13. data/docs/implementation/02-error_handling.md +65 -0
  14. data/docs/implementation/03-core_components.md +457 -0
  15. data/docs/implementation/03.5-variable_preservation.md +509 -0
  16. data/docs/implementation/04-provider_architecture.md +571 -0
  17. data/docs/implementation/05-translation_logic.md +1065 -0
  18. data/docs/implementation/06-main_module_api.md +122 -0
  19. data/docs/implementation/07-direct_translation_helpers.md +582 -0
  20. data/docs/implementation/08-rails_integration.md +323 -0
  21. data/docs/implementation/09-testing_suite.md +228 -0
  22. data/docs/implementation/10-documentation_examples.md +150 -0
  23. data/docs/implementation/11-quality_security.md +65 -0
  24. data/docs/implementation/12-cli_standalone.md +698 -0
  25. data/exe/better_translate +9 -0
  26. data/lib/better_translate/cache.rb +125 -0
  27. data/lib/better_translate/cli.rb +304 -0
  28. data/lib/better_translate/configuration.rb +201 -0
  29. data/lib/better_translate/direct_translator.rb +131 -0
  30. data/lib/better_translate/errors.rb +101 -0
  31. data/lib/better_translate/progress_tracker.rb +157 -0
  32. data/lib/better_translate/provider_factory.rb +45 -0
  33. data/lib/better_translate/providers/anthropic_provider.rb +154 -0
  34. data/lib/better_translate/providers/base_http_provider.rb +239 -0
  35. data/lib/better_translate/providers/chatgpt_provider.rb +138 -44
  36. data/lib/better_translate/providers/gemini_provider.rb +123 -61
  37. data/lib/better_translate/railtie.rb +18 -0
  38. data/lib/better_translate/rate_limiter.rb +90 -0
  39. data/lib/better_translate/strategies/base_strategy.rb +58 -0
  40. data/lib/better_translate/strategies/batch_strategy.rb +56 -0
  41. data/lib/better_translate/strategies/deep_strategy.rb +45 -0
  42. data/lib/better_translate/strategies/strategy_selector.rb +43 -0
  43. data/lib/better_translate/translator.rb +115 -284
  44. data/lib/better_translate/utils/hash_flattener.rb +104 -0
  45. data/lib/better_translate/validator.rb +105 -0
  46. data/lib/better_translate/variable_extractor.rb +259 -0
  47. data/lib/better_translate/version.rb +2 -9
  48. data/lib/better_translate/yaml_handler.rb +168 -0
  49. data/lib/better_translate.rb +97 -73
  50. data/lib/generators/better_translate/analyze/USAGE +12 -0
  51. data/lib/generators/better_translate/analyze/analyze_generator.rb +94 -0
  52. data/lib/generators/better_translate/install/USAGE +13 -0
  53. data/lib/generators/better_translate/install/install_generator.rb +71 -0
  54. data/lib/generators/better_translate/install/templates/README +20 -0
  55. data/lib/generators/better_translate/install/templates/initializer.rb.tt +47 -0
  56. data/lib/generators/better_translate/translate/USAGE +13 -0
  57. data/lib/generators/better_translate/translate/translate_generator.rb +114 -0
  58. data/lib/tasks/better_translate.rake +136 -0
  59. data/sig/better_translate/cache.rbs +28 -0
  60. data/sig/better_translate/cli.rbs +24 -0
  61. data/sig/better_translate/configuration.rbs +78 -0
  62. data/sig/better_translate/direct_translator.rbs +18 -0
  63. data/sig/better_translate/errors.rbs +46 -0
  64. data/sig/better_translate/progress_tracker.rbs +29 -0
  65. data/sig/better_translate/provider_factory.rbs +8 -0
  66. data/sig/better_translate/providers/anthropic_provider.rbs +27 -0
  67. data/sig/better_translate/providers/base_http_provider.rbs +44 -0
  68. data/sig/better_translate/providers/chatgpt_provider.rbs +25 -0
  69. data/sig/better_translate/providers/gemini_provider.rbs +22 -0
  70. data/sig/better_translate/railtie.rbs +7 -0
  71. data/sig/better_translate/rate_limiter.rbs +20 -0
  72. data/sig/better_translate/strategies/base_strategy.rbs +19 -0
  73. data/sig/better_translate/strategies/batch_strategy.rbs +13 -0
  74. data/sig/better_translate/strategies/deep_strategy.rbs +11 -0
  75. data/sig/better_translate/strategies/strategy_selector.rbs +10 -0
  76. data/sig/better_translate/translator.rbs +24 -0
  77. data/sig/better_translate/utils/hash_flattener.rbs +14 -0
  78. data/sig/better_translate/validator.rbs +14 -0
  79. data/sig/better_translate/variable_extractor.rbs +40 -0
  80. data/sig/better_translate/version.rbs +4 -0
  81. data/sig/better_translate/yaml_handler.rbs +29 -0
  82. data/sig/better_translate.rbs +32 -2
  83. data/sig/faraday.rbs +22 -0
  84. data/sig/generators/better_translate/analyze/analyze_generator.rbs +18 -0
  85. data/sig/generators/better_translate/install/install_generator.rbs +14 -0
  86. data/sig/generators/better_translate/translate/translate_generator.rbs +10 -0
  87. data/sig/optparse.rbs +9 -0
  88. data/sig/psych.rbs +5 -0
  89. data/sig/rails.rbs +34 -0
  90. metadata +89 -203
  91. data/lib/better_translate/helper.rb +0 -83
  92. data/lib/better_translate/providers/base_provider.rb +0 -102
  93. data/lib/better_translate/service.rb +0 -144
  94. data/lib/better_translate/similarity_analyzer.rb +0 -218
  95. data/lib/better_translate/utils.rb +0 -55
  96. data/lib/better_translate/writer.rb +0 -75
  97. data/lib/generators/better_translate/analyze_generator.rb +0 -57
  98. data/lib/generators/better_translate/install_generator.rb +0 -14
  99. data/lib/generators/better_translate/templates/better_translate.rb +0 -56
  100. data/lib/generators/better_translate/translate_generator.rb +0 -84
@@ -0,0 +1,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)
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Add lib to load path
5
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
6
+
7
+ require "better_translate"
8
+
9
+ BetterTranslate::CLI.new(ARGV).run