better_translate 1.0.0 → 1.1.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/.rubocop.yml +28 -0
- data/.rubocop_todo.yml +291 -0
- data/CHANGELOG.md +88 -0
- data/README.md +262 -2
- data/RELEASE_NOTES_v1.0.0.md +240 -0
- data/Steepfile +2 -2
- data/docs/implementation/00-overview.md +1 -1
- data/docs/implementation/04-provider_architecture.md +5 -5
- data/lib/better_translate/analyzer/code_scanner.rb +151 -0
- data/lib/better_translate/analyzer/key_scanner.rb +109 -0
- data/lib/better_translate/analyzer/orphan_detector.rb +88 -0
- data/lib/better_translate/analyzer/reporter.rb +155 -0
- data/lib/better_translate/cache.rb +2 -1
- data/lib/better_translate/cli.rb +81 -2
- data/lib/better_translate/configuration.rb +48 -2
- data/lib/better_translate/errors.rb +9 -0
- data/lib/better_translate/json_handler.rb +227 -0
- data/lib/better_translate/providers/anthropic_provider.rb +4 -3
- data/lib/better_translate/providers/chatgpt_provider.rb +2 -1
- data/lib/better_translate/providers/gemini_provider.rb +5 -4
- data/lib/better_translate/railtie.rb +2 -1
- data/lib/better_translate/rate_limiter.rb +4 -1
- data/lib/better_translate/strategies/batch_strategy.rb +1 -1
- data/lib/better_translate/strategies/deep_strategy.rb +1 -1
- data/lib/better_translate/translator.rb +204 -19
- data/lib/better_translate/utils/hash_flattener.rb +2 -2
- data/lib/better_translate/variable_extractor.rb +7 -7
- data/lib/better_translate/version.rb +1 -1
- data/lib/better_translate/yaml_handler.rb +59 -0
- data/lib/better_translate.rb +5 -0
- data/lib/generators/better_translate/analyze/analyze_generator.rb +2 -1
- data/lib/generators/better_translate/install/install_generator.rb +4 -3
- data/lib/generators/better_translate/install/templates/initializer.rb.tt +39 -7
- data/lib/generators/better_translate/translate/translate_generator.rb +2 -1
- data/regenerate_vcr.rb +47 -0
- data/sig/better_translate/configuration.rbs +13 -2
- data/sig/better_translate/errors.rbs +4 -0
- data/sig/better_translate/providers/base_http_provider.rbs +1 -1
- data/sig/better_translate/translator.rbs +12 -1
- data/sig/better_translate/variable_extractor.rbs +1 -1
- data/sig/better_translate/yaml_handler.rbs +6 -0
- data/sig/better_translate.rbs +2 -1
- metadata +9 -1
|
@@ -13,6 +13,15 @@ module BetterTranslate
|
|
|
13
13
|
# config.target_languages = [{ short_name: "it", name: "Italian" }]
|
|
14
14
|
# config.validate!
|
|
15
15
|
#
|
|
16
|
+
# @example Provider-specific options
|
|
17
|
+
# config.model = "gpt-5-nano" # Specify model (optional, provider-specific)
|
|
18
|
+
# config.temperature = 0.7 # Control creativity (0.0-2.0, default: 0.3)
|
|
19
|
+
# config.max_tokens = 1500 # Limit response length (default: 2000)
|
|
20
|
+
#
|
|
21
|
+
# @example Backup configuration
|
|
22
|
+
# config.create_backup = true # Enable automatic backups (default: true)
|
|
23
|
+
# config.max_backups = 5 # Keep up to 5 backup files (default: 3)
|
|
24
|
+
#
|
|
16
25
|
class Configuration
|
|
17
26
|
# @return [Symbol] The translation provider (:chatgpt, :gemini, :anthropic)
|
|
18
27
|
attr_accessor :provider
|
|
@@ -40,9 +49,12 @@ module BetterTranslate
|
|
|
40
49
|
# @return [Array<Hash>] Target languages with :short_name and :name
|
|
41
50
|
attr_accessor :target_languages
|
|
42
51
|
|
|
43
|
-
# @return [String] Path to input YAML file
|
|
52
|
+
# @return [String] Path to input YAML file (for backward compatibility, use input_files for multiple files)
|
|
44
53
|
attr_accessor :input_file
|
|
45
54
|
|
|
55
|
+
# @return [Array<String>, String] Multiple input files (array or glob pattern)
|
|
56
|
+
attr_accessor :input_files
|
|
57
|
+
|
|
46
58
|
# @return [String] Output folder for translated files
|
|
47
59
|
attr_accessor :output_folder
|
|
48
60
|
|
|
@@ -88,6 +100,21 @@ module BetterTranslate
|
|
|
88
100
|
# @return [Boolean] Preserve interpolation variables during translation (default: true)
|
|
89
101
|
attr_accessor :preserve_variables
|
|
90
102
|
|
|
103
|
+
# @return [String, nil] AI model to use (provider-specific, e.g., "gpt-5-nano", "gemini-2.0-flash-exp")
|
|
104
|
+
attr_accessor :model
|
|
105
|
+
|
|
106
|
+
# @return [Float] Temperature for AI generation (0.0-2.0, higher = more creative)
|
|
107
|
+
attr_accessor :temperature
|
|
108
|
+
|
|
109
|
+
# @return [Integer] Maximum tokens for AI response
|
|
110
|
+
attr_accessor :max_tokens
|
|
111
|
+
|
|
112
|
+
# @return [Boolean] Create backup files before overwriting (default: true)
|
|
113
|
+
attr_accessor :create_backup
|
|
114
|
+
|
|
115
|
+
# @return [Integer] Maximum number of backup files to keep (default: 3)
|
|
116
|
+
attr_accessor :max_backups
|
|
117
|
+
|
|
91
118
|
# Initialize a new configuration with defaults
|
|
92
119
|
def initialize
|
|
93
120
|
@translation_mode = :override
|
|
@@ -104,6 +131,11 @@ module BetterTranslate
|
|
|
104
131
|
@exclusions_per_language = {}
|
|
105
132
|
@target_languages = []
|
|
106
133
|
@preserve_variables = true
|
|
134
|
+
@model = nil
|
|
135
|
+
@temperature = 0.3
|
|
136
|
+
@max_tokens = 2000
|
|
137
|
+
@create_backup = true
|
|
138
|
+
@max_backups = 3
|
|
107
139
|
end
|
|
108
140
|
|
|
109
141
|
# Validate the configuration
|
|
@@ -176,8 +208,14 @@ module BetterTranslate
|
|
|
176
208
|
# @return [void]
|
|
177
209
|
# @api private
|
|
178
210
|
def validate_files!
|
|
179
|
-
|
|
211
|
+
# Check if either input_file or input_files is set
|
|
212
|
+
has_input = (input_file && !input_file.empty?) || input_files
|
|
213
|
+
|
|
214
|
+
raise ConfigurationError, "Input file or input_files must be set" unless has_input
|
|
180
215
|
raise ConfigurationError, "Output folder must be set" if output_folder.nil? || output_folder.empty?
|
|
216
|
+
|
|
217
|
+
# Only validate input_file exists if using single file mode (not glob pattern or array)
|
|
218
|
+
return unless input_file && !input_file.empty? && !input_files
|
|
181
219
|
raise ConfigurationError, "Input file does not exist: #{input_file}" unless File.exist?(input_file)
|
|
182
220
|
end
|
|
183
221
|
|
|
@@ -196,6 +234,14 @@ module BetterTranslate
|
|
|
196
234
|
raise ConfigurationError, "Request timeout must be positive" if request_timeout <= 0
|
|
197
235
|
raise ConfigurationError, "Max retries must be non-negative" if max_retries.negative?
|
|
198
236
|
raise ConfigurationError, "Cache size must be positive" if cache_size <= 0
|
|
237
|
+
|
|
238
|
+
# Validate temperature range (AI providers typically accept 0.0-2.0)
|
|
239
|
+
if temperature && (temperature < 0.0 || temperature > 2.0)
|
|
240
|
+
raise ConfigurationError, "Temperature must be between 0.0 and 2.0"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Validate max_tokens is positive
|
|
244
|
+
raise ConfigurationError, "Max tokens must be positive" if max_tokens && max_tokens <= 0
|
|
199
245
|
end
|
|
200
246
|
end
|
|
201
247
|
end
|
|
@@ -90,6 +90,15 @@ module BetterTranslate
|
|
|
90
90
|
# )
|
|
91
91
|
class YamlError < Error; end
|
|
92
92
|
|
|
93
|
+
# Raised when JSON parsing fails
|
|
94
|
+
#
|
|
95
|
+
# @example JSON syntax error
|
|
96
|
+
# raise JsonError.new(
|
|
97
|
+
# "Invalid JSON syntax",
|
|
98
|
+
# context: { file_path: "config/locales/en.json", error: "unexpected token" }
|
|
99
|
+
# )
|
|
100
|
+
class JsonError < Error; end
|
|
101
|
+
|
|
93
102
|
# Raised when a provider is not found
|
|
94
103
|
#
|
|
95
104
|
# @example Provider not found
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module BetterTranslate
|
|
7
|
+
# Handles JSON file operations
|
|
8
|
+
#
|
|
9
|
+
# Provides methods for:
|
|
10
|
+
# - Reading and parsing JSON files
|
|
11
|
+
# - Writing JSON files with proper formatting
|
|
12
|
+
# - Merging translations (incremental mode)
|
|
13
|
+
# - Handling exclusions
|
|
14
|
+
# - Flattening/unflattening nested structures
|
|
15
|
+
#
|
|
16
|
+
# @example Reading a JSON file
|
|
17
|
+
# handler = JsonHandler.new(config)
|
|
18
|
+
# data = handler.read_json("config/locales/en.json")
|
|
19
|
+
#
|
|
20
|
+
# @example Writing translations
|
|
21
|
+
# handler.write_json("config/locales/it.json", { "it" => { "greeting" => "Ciao" } })
|
|
22
|
+
#
|
|
23
|
+
class JsonHandler
|
|
24
|
+
# @return [Configuration] Configuration object
|
|
25
|
+
attr_reader :config
|
|
26
|
+
|
|
27
|
+
# Initialize JSON handler
|
|
28
|
+
#
|
|
29
|
+
# @param config [Configuration] Configuration object
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# config = Configuration.new
|
|
33
|
+
# handler = JsonHandler.new(config)
|
|
34
|
+
#
|
|
35
|
+
def initialize(config)
|
|
36
|
+
@config = config
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Read and parse JSON file
|
|
40
|
+
#
|
|
41
|
+
# @param file_path [String] Path to JSON file
|
|
42
|
+
# @return [Hash] Parsed JSON content
|
|
43
|
+
# @raise [FileError] if file cannot be read
|
|
44
|
+
# @raise [JsonError] if JSON is invalid
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
# data = handler.read_json("config/locales/en.json")
|
|
48
|
+
# #=> { "en" => { "greeting" => "Hello" } }
|
|
49
|
+
#
|
|
50
|
+
def read_json(file_path)
|
|
51
|
+
Validator.validate_file_exists!(file_path)
|
|
52
|
+
|
|
53
|
+
content = File.read(file_path)
|
|
54
|
+
return {} if content.strip.empty?
|
|
55
|
+
|
|
56
|
+
JSON.parse(content)
|
|
57
|
+
rescue Errno::ENOENT => e
|
|
58
|
+
raise FileError.new("File does not exist: #{file_path}", context: { error: e.message })
|
|
59
|
+
rescue JSON::ParserError => e
|
|
60
|
+
raise JsonError.new("Invalid JSON syntax in #{file_path}", context: { error: e.message })
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Write hash to JSON file
|
|
64
|
+
#
|
|
65
|
+
# @param file_path [String] Output file path
|
|
66
|
+
# @param data [Hash] Data to write
|
|
67
|
+
# @param diff_preview [DiffPreview, nil] Optional diff preview instance
|
|
68
|
+
# @return [Hash, nil] Summary hash if dry_run, nil otherwise
|
|
69
|
+
# @raise [FileError] if file cannot be written
|
|
70
|
+
#
|
|
71
|
+
# @example
|
|
72
|
+
# handler.write_json("config/locales/it.json", { "it" => { "greeting" => "Ciao" } })
|
|
73
|
+
#
|
|
74
|
+
def write_json(file_path, data, diff_preview: nil)
|
|
75
|
+
summary = nil
|
|
76
|
+
|
|
77
|
+
# Show diff preview if in dry run mode
|
|
78
|
+
if config.dry_run && diff_preview
|
|
79
|
+
existing_data = File.exist?(file_path) ? read_json(file_path) : {}
|
|
80
|
+
summary = diff_preview.show_diff(existing_data, data, file_path)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
return summary if config.dry_run
|
|
84
|
+
|
|
85
|
+
# Create backup if enabled and file exists
|
|
86
|
+
create_backup_file(file_path) if config.create_backup && File.exist?(file_path)
|
|
87
|
+
|
|
88
|
+
# Ensure output directory exists
|
|
89
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
|
90
|
+
|
|
91
|
+
# Write JSON with proper indentation
|
|
92
|
+
File.write(file_path, JSON.pretty_generate(data))
|
|
93
|
+
|
|
94
|
+
nil
|
|
95
|
+
rescue Errno::EACCES => e
|
|
96
|
+
raise FileError.new("Permission denied: #{file_path}", context: { error: e.message })
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
raise FileError.new("Failed to write JSON: #{file_path}", context: { error: e.message })
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get translatable strings from source JSON
|
|
102
|
+
#
|
|
103
|
+
# Reads the input file and returns a flattened hash of strings.
|
|
104
|
+
# Removes the root language key if present.
|
|
105
|
+
#
|
|
106
|
+
# @return [Hash] Flattened hash of translatable strings
|
|
107
|
+
#
|
|
108
|
+
# @example
|
|
109
|
+
# strings = handler.get_source_strings
|
|
110
|
+
# #=> { "greeting" => "Hello", "nav.home" => "Home" }
|
|
111
|
+
#
|
|
112
|
+
def get_source_strings
|
|
113
|
+
return {} unless config.input_file
|
|
114
|
+
|
|
115
|
+
source_data = read_json(config.input_file)
|
|
116
|
+
# Remove root language key if present (e.g., "en:")
|
|
117
|
+
source_data = source_data[config.source_language] || source_data
|
|
118
|
+
|
|
119
|
+
Utils::HashFlattener.flatten(source_data)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Filter out excluded keys for a specific language
|
|
123
|
+
#
|
|
124
|
+
# @param strings [Hash] Flattened strings
|
|
125
|
+
# @param target_lang_code [String] Target language code
|
|
126
|
+
# @return [Hash] Filtered strings
|
|
127
|
+
#
|
|
128
|
+
# @example
|
|
129
|
+
# filtered = handler.filter_exclusions(strings, "it")
|
|
130
|
+
#
|
|
131
|
+
def filter_exclusions(strings, target_lang_code)
|
|
132
|
+
excluded_keys = config.global_exclusions.dup
|
|
133
|
+
excluded_keys += config.exclusions_per_language[target_lang_code] || []
|
|
134
|
+
|
|
135
|
+
strings.reject { |key, _| excluded_keys.include?(key) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Merge translated strings with existing file (incremental mode)
|
|
139
|
+
#
|
|
140
|
+
# @param file_path [String] Existing file path
|
|
141
|
+
# @param new_translations [Hash] New translations (flattened)
|
|
142
|
+
# @return [Hash] Merged translations (nested)
|
|
143
|
+
#
|
|
144
|
+
# @example
|
|
145
|
+
# merged = handler.merge_translations("config/locales/it.json", new_translations)
|
|
146
|
+
#
|
|
147
|
+
def merge_translations(file_path, new_translations)
|
|
148
|
+
if File.exist?(file_path)
|
|
149
|
+
existing = read_json(file_path)
|
|
150
|
+
# Extract actual translations (remove language wrapper if present)
|
|
151
|
+
target_lang = config.target_languages.first[:short_name]
|
|
152
|
+
existing = existing[target_lang] || existing
|
|
153
|
+
else
|
|
154
|
+
existing = {}
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
existing_flat = Utils::HashFlattener.flatten(existing)
|
|
158
|
+
|
|
159
|
+
# Merge: existing takes precedence
|
|
160
|
+
merged = new_translations.merge(existing_flat)
|
|
161
|
+
|
|
162
|
+
Utils::HashFlattener.unflatten(merged)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Build output file path for target language
|
|
166
|
+
#
|
|
167
|
+
# @param target_lang_code [String] Target language code
|
|
168
|
+
# @return [String] Output file path
|
|
169
|
+
#
|
|
170
|
+
# @example
|
|
171
|
+
# path = handler.build_output_path("it")
|
|
172
|
+
# #=> "config/locales/it.json"
|
|
173
|
+
#
|
|
174
|
+
def build_output_path(target_lang_code)
|
|
175
|
+
return "#{target_lang_code}.json" unless config.output_folder
|
|
176
|
+
|
|
177
|
+
File.join(config.output_folder, "#{target_lang_code}.json")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
# Create backup file with rotation support
|
|
183
|
+
#
|
|
184
|
+
# @param file_path [String] Path to file to backup
|
|
185
|
+
# @return [void]
|
|
186
|
+
# @api private
|
|
187
|
+
#
|
|
188
|
+
def create_backup_file(file_path)
|
|
189
|
+
return unless File.exist?(file_path)
|
|
190
|
+
|
|
191
|
+
# Rotate existing backups if max_backups > 1
|
|
192
|
+
rotate_backups(file_path) if config.max_backups > 1
|
|
193
|
+
|
|
194
|
+
# Create primary backup
|
|
195
|
+
backup_path = "#{file_path}.bak"
|
|
196
|
+
FileUtils.cp(file_path, backup_path)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Rotate backup files, keeping only max_backups
|
|
200
|
+
#
|
|
201
|
+
# @param file_path [String] Base file path
|
|
202
|
+
# @return [void]
|
|
203
|
+
# @api private
|
|
204
|
+
#
|
|
205
|
+
def rotate_backups(file_path)
|
|
206
|
+
primary_backup = "#{file_path}.bak"
|
|
207
|
+
return unless File.exist?(primary_backup)
|
|
208
|
+
|
|
209
|
+
# Clean up ANY backups that would exceed max_backups after rotation
|
|
210
|
+
10.downto(config.max_backups) do |i|
|
|
211
|
+
numbered_backup = "#{file_path}.bak.#{i}"
|
|
212
|
+
FileUtils.rm_f(numbered_backup) if File.exist?(numbered_backup)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Rotate numbered backups from high to low to avoid overwrites
|
|
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
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -4,7 +4,7 @@ module BetterTranslate
|
|
|
4
4
|
module Providers
|
|
5
5
|
# Anthropic Claude translation provider
|
|
6
6
|
#
|
|
7
|
-
# Uses claude-
|
|
7
|
+
# Uses claude-haiku-4-5 model for fast, efficient translations.
|
|
8
8
|
#
|
|
9
9
|
# @example Basic usage
|
|
10
10
|
# config = Configuration.new
|
|
@@ -18,7 +18,7 @@ module BetterTranslate
|
|
|
18
18
|
API_URL = "https://api.anthropic.com/v1/messages"
|
|
19
19
|
|
|
20
20
|
# Model to use for translations
|
|
21
|
-
MODEL = "claude-
|
|
21
|
+
MODEL = "claude-haiku-4-5"
|
|
22
22
|
|
|
23
23
|
# API version
|
|
24
24
|
API_VERSION = "2023-06-01"
|
|
@@ -97,7 +97,8 @@ module BetterTranslate
|
|
|
97
97
|
#
|
|
98
98
|
def build_system_message(target_lang_name)
|
|
99
99
|
base_message = "You are a professional translator. Translate the following text to #{target_lang_name}. " \
|
|
100
|
-
"Return ONLY the translated text, without any explanations or additional text."
|
|
100
|
+
"Return ONLY the translated text, without any explanations or additional text. " \
|
|
101
|
+
"Words like VARIABLE_0, VARIABLE_1, etc. are placeholders and must be kept unchanged in the translation."
|
|
101
102
|
|
|
102
103
|
if config.translation_context && !config.translation_context.empty?
|
|
103
104
|
base_message += "\n\nContext: #{config.translation_context}"
|
|
@@ -95,7 +95,8 @@ module BetterTranslate
|
|
|
95
95
|
#
|
|
96
96
|
def build_system_message(target_lang_name)
|
|
97
97
|
base_message = "You are a professional translator. Translate the following text to #{target_lang_name}. " \
|
|
98
|
-
"Return ONLY the translated text, without any explanations or additional text."
|
|
98
|
+
"Return ONLY the translated text, without any explanations or additional text. " \
|
|
99
|
+
"Words like VARIABLE_0, VARIABLE_1, etc. are placeholders and must be kept unchanged in the translation."
|
|
99
100
|
|
|
100
101
|
if config.translation_context && !config.translation_context.empty?
|
|
101
102
|
base_message += "\n\nContext: #{config.translation_context}"
|
|
@@ -4,7 +4,7 @@ module BetterTranslate
|
|
|
4
4
|
module Providers
|
|
5
5
|
# Google Gemini translation provider
|
|
6
6
|
#
|
|
7
|
-
# Uses gemini-2.
|
|
7
|
+
# Uses gemini-2.5-flash-lite model for fast, high-quality translations.
|
|
8
8
|
#
|
|
9
9
|
# @example Basic usage
|
|
10
10
|
# config = Configuration.new
|
|
@@ -15,10 +15,10 @@ module BetterTranslate
|
|
|
15
15
|
#
|
|
16
16
|
class GeminiProvider < BaseHttpProvider
|
|
17
17
|
# Google Gemini API endpoint
|
|
18
|
-
API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.
|
|
18
|
+
API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent"
|
|
19
19
|
|
|
20
20
|
# Model to use for translations
|
|
21
|
-
MODEL = "gemini-2.
|
|
21
|
+
MODEL = "gemini-2.5-flash-lite"
|
|
22
22
|
|
|
23
23
|
# Translate a single text
|
|
24
24
|
#
|
|
@@ -77,7 +77,8 @@ module BetterTranslate
|
|
|
77
77
|
#
|
|
78
78
|
def build_prompt(text, target_lang_name)
|
|
79
79
|
base_prompt = "Translate the following text to #{target_lang_name}. " \
|
|
80
|
-
"Return ONLY the translated text, without any explanations
|
|
80
|
+
"Return ONLY the translated text, without any explanations. " \
|
|
81
|
+
"Words like VARIABLE_0, VARIABLE_1, etc. are placeholders and must be kept unchanged in the translation.\n\n" \
|
|
81
82
|
"Text: #{text}"
|
|
82
83
|
|
|
83
84
|
if config.translation_context && !config.translation_context.empty?
|
|
@@ -11,7 +11,8 @@ module BetterTranslate
|
|
|
11
11
|
#
|
|
12
12
|
class Railtie < Rails::Railtie
|
|
13
13
|
rake_tasks do
|
|
14
|
-
|
|
14
|
+
dir = __dir__
|
|
15
|
+
rake_file = File.expand_path("../tasks/better_translate.rake", dir) if dir
|
|
15
16
|
load rake_file if rake_file
|
|
16
17
|
end
|
|
17
18
|
end
|
|
@@ -52,7 +52,10 @@ module BetterTranslate
|
|
|
52
52
|
@mutex.synchronize do
|
|
53
53
|
return if @last_request_time.nil?
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
last_time = @last_request_time
|
|
56
|
+
return unless last_time
|
|
57
|
+
|
|
58
|
+
elapsed = Time.now - last_time
|
|
56
59
|
sleep_time = @delay - elapsed.to_f
|
|
57
60
|
|
|
58
61
|
sleep(sleep_time) if sleep_time.positive?
|
|
@@ -37,7 +37,7 @@ module BetterTranslate
|
|
|
37
37
|
progress_tracker.update(
|
|
38
38
|
language: target_lang_name,
|
|
39
39
|
current_key: "Batch #{batch_index + 1}/#{total_batches}",
|
|
40
|
-
progress: ((batch_index + 1).to_f / total_batches * 100.0).round(1)
|
|
40
|
+
progress: ((batch_index + 1).to_f / total_batches * 100.0).round(1).to_f
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
translated_batch = provider.translate_batch(batch, target_lang_code, target_lang_name)
|
|
@@ -32,7 +32,7 @@ module BetterTranslate
|
|
|
32
32
|
progress_tracker.update(
|
|
33
33
|
language: target_lang_name,
|
|
34
34
|
current_key: key,
|
|
35
|
-
progress: ((index + 1).to_f / total * 100.0).round(1)
|
|
35
|
+
progress: ((index + 1).to_f / total * 100.0).round(1).to_f
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
translated[key] = provider.translate_text(value, target_lang_code, target_lang_name)
|