better_translate 1.0.0.1 → 1.1.1
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/.rubocop.yml +28 -0
- data/.rubocop_todo.yml +291 -0
- data/CHANGELOG.md +88 -0
- data/CLAUDE.md +12 -7
- data/CONTRIBUTING.md +432 -0
- data/README.md +240 -1
- data/Rakefile +14 -1
- data/SECURITY.md +160 -0
- data/Steepfile +0 -1
- data/brakeman.yml +37 -0
- data/codecov.yml +34 -0
- data/lib/better_translate/analyzer/code_scanner.rb +149 -0
- data/lib/better_translate/analyzer/key_scanner.rb +109 -0
- data/lib/better_translate/analyzer/orphan_detector.rb +91 -0
- data/lib/better_translate/analyzer/reporter.rb +155 -0
- data/lib/better_translate/cli.rb +81 -2
- data/lib/better_translate/configuration.rb +76 -3
- data/lib/better_translate/errors.rb +9 -0
- data/lib/better_translate/json_handler.rb +227 -0
- data/lib/better_translate/translator.rb +205 -23
- data/lib/better_translate/version.rb +1 -1
- data/lib/better_translate/yaml_handler.rb +59 -0
- data/lib/better_translate.rb +7 -0
- data/lib/generators/better_translate/install/install_generator.rb +2 -2
- data/lib/generators/better_translate/install/templates/initializer.rb.tt +22 -34
- data/lib/generators/better_translate/translate/translate_generator.rb +65 -46
- data/lib/tasks/better_translate.rake +62 -45
- data/sig/better_translate/analyzer/code_scanner.rbs +59 -0
- data/sig/better_translate/analyzer/key_scanner.rbs +40 -0
- data/sig/better_translate/analyzer/orphan_detector.rbs +43 -0
- data/sig/better_translate/analyzer/reporter.rbs +70 -0
- data/sig/better_translate/cli.rbs +2 -0
- data/sig/better_translate/configuration.rbs +6 -0
- data/sig/better_translate/errors.rbs +4 -0
- data/sig/better_translate/json_handler.rbs +65 -0
- data/sig/better_translate/progress_tracker.rbs +1 -1
- data/sig/better_translate/translator.rbs +12 -1
- data/sig/better_translate/yaml_handler.rbs +6 -0
- data/sig/better_translate.rbs +4 -0
- data/sig/csv.rbs +16 -0
- metadata +32 -3
- data/regenerate_vcr.rb +0 -47
|
@@ -34,12 +34,19 @@ module BetterTranslate
|
|
|
34
34
|
@config.validate!
|
|
35
35
|
provider_name = config.provider || :chatgpt
|
|
36
36
|
@provider = ProviderFactory.create(provider_name, config)
|
|
37
|
-
|
|
37
|
+
|
|
38
|
+
# Resolve input files (supports input_file, input_files array, or glob pattern)
|
|
39
|
+
@input_files = resolve_input_files
|
|
40
|
+
|
|
41
|
+
# We'll determine handler per file during translation
|
|
38
42
|
@progress_tracker = ProgressTracker.new(enabled: config.verbose)
|
|
39
43
|
end
|
|
40
44
|
|
|
41
45
|
# Translate to all target languages
|
|
42
46
|
#
|
|
47
|
+
# Uses parallel execution when max_concurrent_requests > 1,
|
|
48
|
+
# sequential execution otherwise. Supports multiple input files.
|
|
49
|
+
#
|
|
43
50
|
# @return [Hash] Results hash with :success_count, :failure_count, :errors
|
|
44
51
|
#
|
|
45
52
|
# @example
|
|
@@ -47,8 +54,128 @@ module BetterTranslate
|
|
|
47
54
|
# #=> { success_count: 2, failure_count: 0, errors: [] }
|
|
48
55
|
#
|
|
49
56
|
def translate_all
|
|
50
|
-
|
|
57
|
+
# @type var combined_results: translation_results
|
|
58
|
+
combined_results = {
|
|
59
|
+
success_count: 0,
|
|
60
|
+
failure_count: 0,
|
|
61
|
+
errors: []
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@input_files.each do |input_file|
|
|
65
|
+
# Create appropriate handler for this file
|
|
66
|
+
handler = if input_file.end_with?(".json")
|
|
67
|
+
JsonHandler.new(config)
|
|
68
|
+
else
|
|
69
|
+
YAMLHandler.new(config)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Temporarily set input_file for this iteration
|
|
73
|
+
original_input_file = config.input_file
|
|
74
|
+
config.input_file = input_file
|
|
75
|
+
|
|
76
|
+
source_strings = handler.get_source_strings
|
|
77
|
+
|
|
78
|
+
results = if config.max_concurrent_requests > 1
|
|
79
|
+
translate_parallel(source_strings, handler)
|
|
80
|
+
else
|
|
81
|
+
translate_sequential(source_strings, handler)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Restore original config
|
|
85
|
+
config.input_file = original_input_file
|
|
86
|
+
|
|
87
|
+
# Accumulate results
|
|
88
|
+
combined_results[:success_count] += results[:success_count]
|
|
89
|
+
combined_results[:failure_count] += results[:failure_count]
|
|
90
|
+
combined_results[:errors].concat(results[:errors])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
combined_results
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# Resolve input files from config
|
|
99
|
+
#
|
|
100
|
+
# Handles input_file, input_files array, or glob patterns
|
|
101
|
+
#
|
|
102
|
+
# @return [Array<String>] Resolved file paths
|
|
103
|
+
# @raise [FileError] if no files found
|
|
104
|
+
# @api private
|
|
105
|
+
#
|
|
106
|
+
def resolve_input_files
|
|
107
|
+
files = if config.input_files
|
|
108
|
+
# Handle input_files (array or glob pattern)
|
|
109
|
+
if config.input_files.is_a?(Array)
|
|
110
|
+
config.input_files
|
|
111
|
+
else
|
|
112
|
+
# Glob pattern
|
|
113
|
+
Dir.glob(config.input_files)
|
|
114
|
+
end
|
|
115
|
+
elsif config.input_file
|
|
116
|
+
# Backward compatibility with single input_file
|
|
117
|
+
[config.input_file]
|
|
118
|
+
else
|
|
119
|
+
[] # : Array[String]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Validate files exist (unless glob pattern that found nothing)
|
|
123
|
+
if files.empty?
|
|
124
|
+
if config.input_files && !config.input_files.is_a?(Array)
|
|
125
|
+
raise FileError, "No files found matching pattern: #{config.input_files}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
raise FileError, "No input files specified"
|
|
129
|
+
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
files.each do |file|
|
|
133
|
+
Validator.validate_file_exists!(file)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
files
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Translate languages in parallel
|
|
140
|
+
#
|
|
141
|
+
# @param source_strings [Hash] Source strings (flattened)
|
|
142
|
+
# @param handler [YAMLHandler, JsonHandler] File handler for this file
|
|
143
|
+
# @return [Hash] Results hash with counts and errors
|
|
144
|
+
# @api private
|
|
145
|
+
#
|
|
146
|
+
def translate_parallel(source_strings, handler)
|
|
147
|
+
# @type var results: translation_results
|
|
148
|
+
results = {
|
|
149
|
+
success_count: 0,
|
|
150
|
+
failure_count: 0,
|
|
151
|
+
errors: []
|
|
152
|
+
}
|
|
153
|
+
mutex = Mutex.new
|
|
154
|
+
|
|
155
|
+
# Process languages in batches of max_concurrent_requests
|
|
156
|
+
config.target_languages.each_slice(config.max_concurrent_requests) do |batch|
|
|
157
|
+
threads = batch.map do |lang|
|
|
158
|
+
Thread.new do
|
|
159
|
+
translate_language(source_strings, lang, handler)
|
|
160
|
+
mutex.synchronize { results[:success_count] += 1 }
|
|
161
|
+
rescue StandardError => e
|
|
162
|
+
mutex.synchronize { record_error(results, lang, e) }
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
threads.each(&:join)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
results
|
|
169
|
+
end
|
|
51
170
|
|
|
171
|
+
# Translate languages sequentially
|
|
172
|
+
#
|
|
173
|
+
# @param source_strings [Hash] Source strings (flattened)
|
|
174
|
+
# @param handler [YAMLHandler, JsonHandler] File handler for this file
|
|
175
|
+
# @return [Hash] Results hash with counts and errors
|
|
176
|
+
# @api private
|
|
177
|
+
#
|
|
178
|
+
def translate_sequential(source_strings, handler)
|
|
52
179
|
# @type var results: translation_results
|
|
53
180
|
results = {
|
|
54
181
|
success_count: 0,
|
|
@@ -57,42 +184,53 @@ module BetterTranslate
|
|
|
57
184
|
}
|
|
58
185
|
|
|
59
186
|
config.target_languages.each do |lang|
|
|
60
|
-
translate_language(source_strings, lang)
|
|
187
|
+
translate_language(source_strings, lang, handler)
|
|
61
188
|
results[:success_count] += 1
|
|
62
189
|
rescue StandardError => e
|
|
63
|
-
results
|
|
64
|
-
# @type var error_context: Hash[Symbol, untyped]
|
|
65
|
-
error_context = if e.is_a?(BetterTranslate::Error)
|
|
66
|
-
e.context
|
|
67
|
-
else
|
|
68
|
-
{}
|
|
69
|
-
end
|
|
70
|
-
results[:errors] << {
|
|
71
|
-
language: lang[:name],
|
|
72
|
-
error: e.message,
|
|
73
|
-
context: error_context
|
|
74
|
-
}
|
|
75
|
-
@progress_tracker.error(lang[:name], e)
|
|
190
|
+
record_error(results, lang, e)
|
|
76
191
|
end
|
|
77
192
|
|
|
78
193
|
results
|
|
79
194
|
end
|
|
80
195
|
|
|
81
|
-
|
|
196
|
+
# Record translation error in results
|
|
197
|
+
#
|
|
198
|
+
# @param results [Hash] Results hash to update
|
|
199
|
+
# @param lang [Hash] Language config
|
|
200
|
+
# @param error [StandardError] The error that occurred
|
|
201
|
+
# @return [void]
|
|
202
|
+
# @api private
|
|
203
|
+
#
|
|
204
|
+
def record_error(results, lang, error)
|
|
205
|
+
results[:failure_count] += 1
|
|
206
|
+
# @type var error_context: Hash[Symbol, untyped]
|
|
207
|
+
error_context = if error.is_a?(BetterTranslate::Error)
|
|
208
|
+
error.context
|
|
209
|
+
else
|
|
210
|
+
{}
|
|
211
|
+
end
|
|
212
|
+
results[:errors] << {
|
|
213
|
+
language: lang[:name],
|
|
214
|
+
error: error.message,
|
|
215
|
+
context: error_context
|
|
216
|
+
}
|
|
217
|
+
@progress_tracker.error(lang[:name], error)
|
|
218
|
+
end
|
|
82
219
|
|
|
83
220
|
# Translate to a single language
|
|
84
221
|
#
|
|
85
222
|
# @param source_strings [Hash] Source strings (flattened)
|
|
86
223
|
# @param lang [Hash] Language config with :short_name and :name
|
|
224
|
+
# @param handler [YAMLHandler, JsonHandler] File handler for this file
|
|
87
225
|
# @return [void]
|
|
88
226
|
# @api private
|
|
89
227
|
#
|
|
90
|
-
def translate_language(source_strings, lang)
|
|
228
|
+
def translate_language(source_strings, lang, handler)
|
|
91
229
|
target_lang_code = lang[:short_name]
|
|
92
230
|
target_lang_name = lang[:name]
|
|
93
231
|
|
|
94
232
|
# Filter exclusions
|
|
95
|
-
strings_to_translate =
|
|
233
|
+
strings_to_translate = handler.filter_exclusions(source_strings, target_lang_code)
|
|
96
234
|
|
|
97
235
|
return if strings_to_translate.empty?
|
|
98
236
|
|
|
@@ -108,20 +246,64 @@ module BetterTranslate
|
|
|
108
246
|
@progress_tracker.reset
|
|
109
247
|
translated = strategy.translate(strings_to_translate, target_lang_code, target_lang_name)
|
|
110
248
|
|
|
111
|
-
# Save
|
|
112
|
-
|
|
249
|
+
# Save - generate output path with proper filename
|
|
250
|
+
current_input_file = config.input_file or raise "No input file set"
|
|
251
|
+
output_path = build_output_path_for_file(current_input_file, target_lang_code)
|
|
113
252
|
|
|
114
253
|
final_translations = if config.translation_mode == :incremental
|
|
115
|
-
|
|
254
|
+
handler.merge_translations(output_path, translated)
|
|
116
255
|
else
|
|
117
256
|
Utils::HashFlattener.unflatten(translated)
|
|
118
257
|
end
|
|
119
258
|
|
|
120
259
|
# Wrap in language key (e.g., "it:")
|
|
121
260
|
wrapped = { target_lang_code => final_translations }
|
|
122
|
-
|
|
261
|
+
|
|
262
|
+
# Write using appropriate handler method (write_yaml or write_json)
|
|
263
|
+
if handler.is_a?(JsonHandler)
|
|
264
|
+
handler.write_json(output_path, wrapped)
|
|
265
|
+
else
|
|
266
|
+
handler.write_yaml(output_path, wrapped)
|
|
267
|
+
end
|
|
123
268
|
|
|
124
269
|
@progress_tracker.complete(target_lang_name, translated.size)
|
|
125
270
|
end
|
|
271
|
+
|
|
272
|
+
# Build output path for a specific file and language
|
|
273
|
+
#
|
|
274
|
+
# Replaces source language code with target language code in filename
|
|
275
|
+
# and preserves directory structure
|
|
276
|
+
#
|
|
277
|
+
# @param input_file [String] Input file path
|
|
278
|
+
# @param target_lang_code [String] Target language code
|
|
279
|
+
# @return [String] Output file path
|
|
280
|
+
# @api private
|
|
281
|
+
#
|
|
282
|
+
# @example
|
|
283
|
+
# build_output_path_for_file("config/locales/common.en.yml", "it")
|
|
284
|
+
# #=> "config/locales/common.it.yml"
|
|
285
|
+
#
|
|
286
|
+
def build_output_path_for_file(input_file, target_lang_code)
|
|
287
|
+
# Get file basename and directory
|
|
288
|
+
dir = File.dirname(input_file)
|
|
289
|
+
basename = File.basename(input_file)
|
|
290
|
+
ext = File.extname(basename)
|
|
291
|
+
name_without_ext = File.basename(basename, ext)
|
|
292
|
+
|
|
293
|
+
# Replace source language code with target language code
|
|
294
|
+
# Handles patterns like: common.en.yml -> common.it.yml
|
|
295
|
+
new_basename = name_without_ext.gsub(/\.#{config.source_language}$/, ".#{target_lang_code}") + ext
|
|
296
|
+
|
|
297
|
+
# If no language code in filename, use simple pattern
|
|
298
|
+
new_basename = "#{target_lang_code}#{ext}" if new_basename == basename
|
|
299
|
+
|
|
300
|
+
# Build output path
|
|
301
|
+
if config.output_folder
|
|
302
|
+
# Use output_folder but preserve relative directory structure if input was nested
|
|
303
|
+
File.join(config.output_folder, new_basename)
|
|
304
|
+
else
|
|
305
|
+
File.join(dir, new_basename)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
126
308
|
end
|
|
127
309
|
end
|
|
@@ -81,6 +81,9 @@ module BetterTranslate
|
|
|
81
81
|
|
|
82
82
|
return summary if config.dry_run
|
|
83
83
|
|
|
84
|
+
# Create backup if enabled and file exists
|
|
85
|
+
create_backup_file(file_path) if config.create_backup && File.exist?(file_path)
|
|
86
|
+
|
|
84
87
|
# Ensure output directory exists
|
|
85
88
|
FileUtils.mkdir_p(File.dirname(file_path))
|
|
86
89
|
|
|
@@ -164,5 +167,61 @@ module BetterTranslate
|
|
|
164
167
|
|
|
165
168
|
File.join(config.output_folder, "#{target_lang_code}.yml")
|
|
166
169
|
end
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
|
|
173
|
+
# Create backup file with rotation support
|
|
174
|
+
#
|
|
175
|
+
# @param file_path [String] Path to file to backup
|
|
176
|
+
# @return [void]
|
|
177
|
+
# @api private
|
|
178
|
+
#
|
|
179
|
+
def create_backup_file(file_path)
|
|
180
|
+
return unless File.exist?(file_path)
|
|
181
|
+
|
|
182
|
+
# Rotate existing backups if max_backups > 1
|
|
183
|
+
rotate_backups(file_path) if config.max_backups > 1
|
|
184
|
+
|
|
185
|
+
# Create primary backup
|
|
186
|
+
backup_path = "#{file_path}.bak"
|
|
187
|
+
FileUtils.cp(file_path, backup_path)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Rotate backup files, keeping only max_backups
|
|
191
|
+
#
|
|
192
|
+
# Rotation strategy:
|
|
193
|
+
# - .bak is always the most recent
|
|
194
|
+
# - .bak.1, .bak.2, etc. are progressively older
|
|
195
|
+
# - When we reach max_backups, oldest is deleted
|
|
196
|
+
#
|
|
197
|
+
# @param file_path [String] Base file path
|
|
198
|
+
# @return [void]
|
|
199
|
+
# @api private
|
|
200
|
+
#
|
|
201
|
+
def rotate_backups(file_path)
|
|
202
|
+
primary_backup = "#{file_path}.bak"
|
|
203
|
+
return unless File.exist?(primary_backup)
|
|
204
|
+
|
|
205
|
+
# Clean up ANY backups that would exceed max_backups after rotation
|
|
206
|
+
# max_backups includes .bak itself, so numbered backups go from 1 to max_backups-1
|
|
207
|
+
# After rotation, .bak -> .bak.1, so we can have at most .bak.1 through .bak.(max_backups-1)
|
|
208
|
+
10.downto(config.max_backups) do |i|
|
|
209
|
+
numbered_backup = "#{file_path}.bak.#{i}"
|
|
210
|
+
FileUtils.rm_f(numbered_backup) if File.exist?(numbered_backup)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Rotate numbered backups from high to low to avoid overwrites
|
|
214
|
+
# max_backups=2: nothing to rotate (only .bak -> .bak.1)
|
|
215
|
+
# max_backups=3: .bak.1 -> .bak.2 (if exists)
|
|
216
|
+
(config.max_backups - 2).downto(1) do |i|
|
|
217
|
+
old_path = "#{file_path}.bak.#{i}"
|
|
218
|
+
new_path = "#{file_path}.bak.#{i + 1}"
|
|
219
|
+
|
|
220
|
+
FileUtils.mv(old_path, new_path) if File.exist?(old_path)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Move primary backup to .bak.1
|
|
224
|
+
FileUtils.mv(primary_backup, "#{file_path}.bak.1")
|
|
225
|
+
end
|
|
167
226
|
end
|
|
168
227
|
end
|
data/lib/better_translate.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "csv"
|
|
4
|
+
|
|
3
5
|
require_relative "better_translate/version"
|
|
4
6
|
require_relative "better_translate/errors"
|
|
5
7
|
require_relative "better_translate/configuration"
|
|
@@ -14,6 +16,7 @@ require_relative "better_translate/providers/gemini_provider"
|
|
|
14
16
|
require_relative "better_translate/providers/anthropic_provider"
|
|
15
17
|
require_relative "better_translate/provider_factory"
|
|
16
18
|
require_relative "better_translate/yaml_handler"
|
|
19
|
+
require_relative "better_translate/json_handler"
|
|
17
20
|
require_relative "better_translate/progress_tracker"
|
|
18
21
|
require_relative "better_translate/strategies/base_strategy"
|
|
19
22
|
require_relative "better_translate/strategies/deep_strategy"
|
|
@@ -21,6 +24,10 @@ require_relative "better_translate/strategies/batch_strategy"
|
|
|
21
24
|
require_relative "better_translate/strategies/strategy_selector"
|
|
22
25
|
require_relative "better_translate/translator"
|
|
23
26
|
require_relative "better_translate/direct_translator"
|
|
27
|
+
require_relative "better_translate/analyzer/key_scanner"
|
|
28
|
+
require_relative "better_translate/analyzer/code_scanner"
|
|
29
|
+
require_relative "better_translate/analyzer/orphan_detector"
|
|
30
|
+
require_relative "better_translate/analyzer/reporter"
|
|
24
31
|
require_relative "better_translate/cli"
|
|
25
32
|
|
|
26
33
|
# Load Rails integration if Rails is present
|
|
@@ -47,8 +47,8 @@ module BetterTranslate
|
|
|
47
47
|
"dry_run" => false,
|
|
48
48
|
"translation_mode" => "override",
|
|
49
49
|
"preserve_variables" => true,
|
|
50
|
-
"global_exclusions" => [],
|
|
51
|
-
"exclusions_per_language" => {},
|
|
50
|
+
"global_exclusions" => [], # : Array[String]
|
|
51
|
+
"exclusions_per_language" => {}, # : Hash[String, Array[String]]
|
|
52
52
|
"model" => nil,
|
|
53
53
|
"temperature" => 0.3,
|
|
54
54
|
"max_tokens" => 2000,
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
# BetterTranslate Configuration
|
|
4
4
|
#
|
|
5
|
+
# IMPORTANT: I18n configuration is not yet available when initializers load.
|
|
6
|
+
# You must manually set source_language and target_languages to match your
|
|
7
|
+
# Rails I18n configuration in config/application.rb
|
|
8
|
+
#
|
|
5
9
|
# For more configuration options, see config/better_translate.yml
|
|
6
10
|
|
|
7
11
|
BetterTranslate.configure do |config|
|
|
@@ -14,43 +18,27 @@ BetterTranslate.configure do |config|
|
|
|
14
18
|
config.anthropic_key = ENV["ANTHROPIC_API_KEY"]
|
|
15
19
|
|
|
16
20
|
# Source and target languages
|
|
17
|
-
#
|
|
18
|
-
#
|
|
21
|
+
#
|
|
22
|
+
# IMPORTANT: These must be set manually to match your Rails I18n config
|
|
23
|
+
# (I18n.default_locale and I18n.available_locales are not yet available)
|
|
24
|
+
#
|
|
25
|
+
# Example: If your config/application.rb has:
|
|
19
26
|
# config.i18n.default_locale = :it
|
|
20
|
-
# config.i18n.available_locales = [:it, :en, :
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
available_targets = (I18n.available_locales - [I18n.default_locale]).map(&:to_s)
|
|
27
|
+
# config.i18n.available_locales = [:it, :en, :fr, :ja, :ru]
|
|
28
|
+
#
|
|
29
|
+
# Then set:
|
|
30
|
+
# config.source_language = "it" # matches default_locale
|
|
31
|
+
# config.target_languages = [...] # matches available_locales (excluding source)
|
|
26
32
|
|
|
27
|
-
#
|
|
28
|
-
language_names = {
|
|
29
|
-
"en" => "English", "it" => "Italian", "es" => "Spanish", "fr" => "French",
|
|
30
|
-
"de" => "German", "pt" => "Portuguese", "ru" => "Russian", "zh" => "Chinese",
|
|
31
|
-
"ja" => "Japanese", "ko" => "Korean", "ar" => "Arabic", "nl" => "Dutch",
|
|
32
|
-
"pl" => "Polish", "tr" => "Turkish", "sv" => "Swedish", "da" => "Danish",
|
|
33
|
-
"fi" => "Finnish", "no" => "Norwegian", "cs" => "Czech", "el" => "Greek",
|
|
34
|
-
"he" => "Hebrew", "hi" => "Hindi", "th" => "Thai", "vi" => "Vietnamese"
|
|
35
|
-
}
|
|
33
|
+
config.source_language = "en" # TODO: Change to match your default_locale
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
end
|
|
45
|
-
else
|
|
46
|
-
# Fallback: suggest common languages
|
|
47
|
-
# Uncomment and modify the languages you want to translate to
|
|
48
|
-
config.target_languages = [
|
|
49
|
-
{ short_name: "it", name: "Italian" },
|
|
50
|
-
{ short_name: "es", name: "Spanish" },
|
|
51
|
-
{ short_name: "fr", name: "French" }
|
|
52
|
-
]
|
|
53
|
-
end
|
|
35
|
+
# Target languages (excluding source language)
|
|
36
|
+
# TODO: Change to match your available_locales
|
|
37
|
+
config.target_languages = [
|
|
38
|
+
{ short_name: "it", name: "Italian" },
|
|
39
|
+
{ short_name: "es", name: "Spanish" },
|
|
40
|
+
{ short_name: "fr", name: "French" }
|
|
41
|
+
]
|
|
54
42
|
|
|
55
43
|
# File paths
|
|
56
44
|
# Uses source_language for input file
|
|
@@ -29,57 +29,75 @@ module BetterTranslate
|
|
|
29
29
|
#
|
|
30
30
|
# @return [void]
|
|
31
31
|
#
|
|
32
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
32
33
|
def run_translation
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
# Check if configuration is already loaded (from initializer)
|
|
35
|
+
if BetterTranslate.configuration.provider.nil?
|
|
36
|
+
# No initializer configuration found, try loading from YAML
|
|
37
|
+
config_file = Rails.root.join("config", "better_translate.yml")
|
|
38
|
+
|
|
39
|
+
unless File.exist?(config_file)
|
|
40
|
+
say "No configuration found", :red
|
|
41
|
+
say "Either:"
|
|
42
|
+
say " 1. Create config/initializers/better_translate.rb (recommended)"
|
|
43
|
+
say " 2. Run 'rails generate better_translate:install' to create YAML config"
|
|
44
|
+
return
|
|
45
|
+
end
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
# Load configuration from YAML
|
|
48
|
+
yaml_config = YAML.load_file(config_file)
|
|
49
|
+
|
|
50
|
+
# Configure BetterTranslate
|
|
51
|
+
BetterTranslate.configure do |config|
|
|
52
|
+
config.provider = yaml_config["provider"]&.to_sym
|
|
53
|
+
config.openai_key = yaml_config["openai_key"] || ENV["OPENAI_API_KEY"]
|
|
54
|
+
config.gemini_key = yaml_config["gemini_key"] || ENV["GEMINI_API_KEY"]
|
|
55
|
+
config.anthropic_key = yaml_config["anthropic_key"] || ENV["ANTHROPIC_API_KEY"]
|
|
56
|
+
|
|
57
|
+
config.source_language = yaml_config["source_language"]
|
|
58
|
+
config.target_languages = yaml_config["target_languages"]&.map do |lang|
|
|
59
|
+
if lang.is_a?(Hash)
|
|
60
|
+
{ short_name: lang["short_name"], name: lang["name"] }
|
|
61
|
+
else
|
|
62
|
+
lang
|
|
63
|
+
end
|
|
57
64
|
end
|
|
65
|
+
|
|
66
|
+
config.input_file = Rails.root.join(yaml_config["input_file"]).to_s
|
|
67
|
+
config.output_folder = Rails.root.join(yaml_config["output_folder"]).to_s
|
|
68
|
+
config.verbose = yaml_config.fetch("verbose", true)
|
|
69
|
+
config.dry_run = options[:dry_run] || yaml_config.fetch("dry_run", false)
|
|
70
|
+
|
|
71
|
+
# Map "full" to :override for backward compatibility
|
|
72
|
+
translation_mode = yaml_config.fetch("translation_mode", "override")
|
|
73
|
+
translation_mode = "override" if translation_mode == "full"
|
|
74
|
+
config.translation_mode = translation_mode.to_sym
|
|
75
|
+
|
|
76
|
+
config.preserve_variables = yaml_config.fetch("preserve_variables", true)
|
|
77
|
+
|
|
78
|
+
# Exclusions
|
|
79
|
+
config.global_exclusions = yaml_config["global_exclusions"] || []
|
|
80
|
+
config.exclusions_per_language = yaml_config["exclusions_per_language"] || {}
|
|
81
|
+
|
|
82
|
+
# Provider options
|
|
83
|
+
config.model = yaml_config["model"] if yaml_config["model"]
|
|
84
|
+
config.temperature = yaml_config["temperature"] if yaml_config["temperature"]
|
|
85
|
+
config.max_tokens = yaml_config["max_tokens"] if yaml_config["max_tokens"]
|
|
86
|
+
config.timeout = yaml_config["timeout"] if yaml_config["timeout"]
|
|
87
|
+
config.max_retries = yaml_config["max_retries"] if yaml_config["max_retries"]
|
|
88
|
+
config.rate_limit = yaml_config["rate_limit"] if yaml_config["rate_limit"]
|
|
58
89
|
end
|
|
90
|
+
elsif options[:dry_run]
|
|
91
|
+
# Configuration from initializer exists, but apply dry_run option if provided
|
|
92
|
+
BetterTranslate.configuration.dry_run = true
|
|
93
|
+
end
|
|
59
94
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
translation_mode = yaml_config.fetch("translation_mode", "override")
|
|
67
|
-
translation_mode = "override" if translation_mode == "full"
|
|
68
|
-
config.translation_mode = translation_mode.to_sym
|
|
69
|
-
|
|
70
|
-
config.preserve_variables = yaml_config.fetch("preserve_variables", true)
|
|
71
|
-
|
|
72
|
-
# Exclusions
|
|
73
|
-
config.global_exclusions = yaml_config["global_exclusions"] || []
|
|
74
|
-
config.exclusions_per_language = yaml_config["exclusions_per_language"] || {}
|
|
75
|
-
|
|
76
|
-
# Provider options
|
|
77
|
-
config.model = yaml_config["model"] if yaml_config["model"]
|
|
78
|
-
config.temperature = yaml_config["temperature"] if yaml_config["temperature"]
|
|
79
|
-
config.max_tokens = yaml_config["max_tokens"] if yaml_config["max_tokens"]
|
|
80
|
-
config.timeout = yaml_config["timeout"] if yaml_config["timeout"]
|
|
81
|
-
config.max_retries = yaml_config["max_retries"] if yaml_config["max_retries"]
|
|
82
|
-
config.rate_limit = yaml_config["rate_limit"] if yaml_config["rate_limit"]
|
|
95
|
+
# Validate configuration (whether from initializer or YAML)
|
|
96
|
+
begin
|
|
97
|
+
BetterTranslate.configuration.validate!
|
|
98
|
+
rescue BetterTranslate::ConfigurationError => e
|
|
99
|
+
say "Invalid configuration: #{e.message}", :red
|
|
100
|
+
return
|
|
83
101
|
end
|
|
84
102
|
|
|
85
103
|
# Perform translation
|
|
@@ -110,6 +128,7 @@ module BetterTranslate
|
|
|
110
128
|
say " - #{error[:language]}: #{error[:error]}", :red
|
|
111
129
|
end
|
|
112
130
|
end
|
|
131
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
113
132
|
end
|
|
114
133
|
end
|
|
115
134
|
end
|