i18n-tasks 1.0.15 → 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/README.md +85 -13
- data/Rakefile +4 -4
- data/bin/i18n-tasks +3 -3
- data/config/locales/en.yml +6 -0
- data/config/locales/ru.yml +7 -0
- data/i18n-tasks.gemspec +28 -41
- data/lib/i18n/tasks/base_task.rb +19 -19
- data/lib/i18n/tasks/cli.rb +37 -30
- data/lib/i18n/tasks/command/collection.rb +4 -4
- data/lib/i18n/tasks/command/commander.rb +5 -5
- data/lib/i18n/tasks/command/commands/check_prism.rb +126 -0
- data/lib/i18n/tasks/command/commands/data.rb +33 -33
- data/lib/i18n/tasks/command/commands/eq_base.rb +3 -3
- data/lib/i18n/tasks/command/commands/health.rb +6 -5
- data/lib/i18n/tasks/command/commands/interpolations.rb +14 -3
- data/lib/i18n/tasks/command/commands/meta.rb +6 -6
- data/lib/i18n/tasks/command/commands/missing.rb +25 -25
- data/lib/i18n/tasks/command/commands/tree.rb +33 -33
- data/lib/i18n/tasks/command/commands/usages.rb +24 -24
- data/lib/i18n/tasks/command/dsl.rb +1 -1
- data/lib/i18n/tasks/command/option_parsers/enum.rb +5 -5
- data/lib/i18n/tasks/command/option_parsers/locale.rb +4 -4
- data/lib/i18n/tasks/command/options/common.rb +16 -16
- data/lib/i18n/tasks/command/options/data.rb +18 -18
- data/lib/i18n/tasks/command/options/locales.rb +32 -32
- data/lib/i18n/tasks/commands.rb +14 -12
- data/lib/i18n/tasks/concurrent/cache.rb +1 -1
- data/lib/i18n/tasks/concurrent/cached_value.rb +1 -1
- data/lib/i18n/tasks/configuration.rb +22 -21
- data/lib/i18n/tasks/console_context.rb +11 -11
- data/lib/i18n/tasks/data/adapter/json_adapter.rb +1 -1
- data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +5 -5
- data/lib/i18n/tasks/data/file_formats.rb +3 -3
- data/lib/i18n/tasks/data/file_system.rb +5 -5
- data/lib/i18n/tasks/data/file_system_base.rb +26 -26
- data/lib/i18n/tasks/data/language_names.rb +202 -0
- data/lib/i18n/tasks/data/router/conservative_router.rb +3 -3
- data/lib/i18n/tasks/data/router/isolating_router.rb +19 -19
- data/lib/i18n/tasks/data/router/pattern_router.rb +5 -5
- data/lib/i18n/tasks/data/tree/node.rb +27 -27
- data/lib/i18n/tasks/data/tree/nodes.rb +10 -10
- data/lib/i18n/tasks/data/tree/siblings.rb +20 -20
- data/lib/i18n/tasks/data/tree/traversal.rb +5 -5
- data/lib/i18n/tasks/data.rb +4 -4
- data/lib/i18n/tasks/html_keys.rb +2 -2
- data/lib/i18n/tasks/ignore_keys.rb +9 -9
- data/lib/i18n/tasks/interpolations.rb +21 -1
- data/lib/i18n/tasks/key_pattern_matching.rb +8 -8
- data/lib/i18n/tasks/logging.rb +2 -1
- data/lib/i18n/tasks/missing_keys.rb +24 -8
- data/lib/i18n/tasks/plural_keys.rb +6 -4
- data/lib/i18n/tasks/references.rb +4 -4
- data/lib/i18n/tasks/reports/base.rb +18 -14
- data/lib/i18n/tasks/reports/terminal.rb +64 -47
- data/lib/i18n/tasks/scanners/ast_matchers/base_matcher.rb +3 -3
- data/lib/i18n/tasks/scanners/ast_matchers/default_i18n_subject_matcher.rb +3 -3
- data/lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb +10 -10
- data/lib/i18n/tasks/scanners/ast_matchers/rails_model_matcher.rb +1 -1
- data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +69 -10
- data/lib/i18n/tasks/scanners/file_scanner.rb +5 -5
- data/lib/i18n/tasks/scanners/files/caching_file_finder.rb +3 -3
- data/lib/i18n/tasks/scanners/files/caching_file_finder_provider.rb +3 -3
- data/lib/i18n/tasks/scanners/files/caching_file_reader.rb +2 -2
- data/lib/i18n/tasks/scanners/files/file_finder.rb +8 -8
- data/lib/i18n/tasks/scanners/files/file_reader.rb +1 -1
- data/lib/i18n/tasks/scanners/local_ruby_parser.rb +8 -8
- data/lib/i18n/tasks/scanners/occurrence_from_position.rb +1 -1
- data/lib/i18n/tasks/scanners/pattern_mapper.rb +7 -7
- data/lib/i18n/tasks/scanners/pattern_scanner.rb +20 -20
- data/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb +8 -8
- data/lib/i18n/tasks/scanners/prism_scanners/arguments_visitor.rb +8 -1
- data/lib/i18n/tasks/scanners/prism_scanners/nodes.rb +101 -61
- data/lib/i18n/tasks/scanners/prism_scanners/visitor.rb +169 -105
- data/lib/i18n/tasks/scanners/relative_keys.rb +8 -8
- data/lib/i18n/tasks/scanners/results/key_occurrences.rb +3 -3
- data/lib/i18n/tasks/scanners/results/occurrence.rb +14 -10
- data/lib/i18n/tasks/scanners/ruby_ast_call_finder.rb +1 -1
- data/lib/i18n/tasks/scanners/ruby_key_literals.rb +6 -6
- data/lib/i18n/tasks/scanners/ruby_parser_factory.rb +1 -1
- data/lib/i18n/tasks/scanners/ruby_scanner.rb +225 -0
- data/lib/i18n/tasks/scanners/scanner.rb +2 -2
- data/lib/i18n/tasks/scanners/scanner_multiplexer.rb +1 -1
- data/lib/i18n/tasks/split_key.rb +4 -4
- data/lib/i18n/tasks/stats.rb +3 -3
- data/lib/i18n/tasks/translation.rb +5 -5
- data/lib/i18n/tasks/translators/base_translator.rb +40 -14
- data/lib/i18n/tasks/translators/deepl_translator.rb +17 -14
- data/lib/i18n/tasks/translators/google_translator.rb +169 -25
- data/lib/i18n/tasks/translators/openai_translator.rb +34 -23
- data/lib/i18n/tasks/translators/watsonx_translator.rb +16 -16
- data/lib/i18n/tasks/translators/yandex_translator.rb +8 -8
- data/lib/i18n/tasks/unused_keys.rb +1 -1
- data/lib/i18n/tasks/used_keys.rb +32 -33
- data/lib/i18n/tasks/version.rb +1 -1
- data/lib/i18n/tasks.rb +17 -17
- data/templates/config/i18n-tasks.yml +12 -0
- data/templates/minitest/i18n_test.rb +3 -3
- data/templates/rspec/i18n_spec.rb +7 -7
- metadata +25 -185
- data/lib/i18n/tasks/scanners/prism_scanner.rb +0 -83
- data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +0 -145
|
@@ -1,29 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "json"
|
|
7
|
+
require "i18n/tasks/translators/base_translator"
|
|
8
|
+
|
|
9
|
+
# https://cloud.google.com/translate/docs/reference/rest/v2/translate
|
|
4
10
|
|
|
5
11
|
module I18n::Tasks::Translators
|
|
6
12
|
class GoogleTranslator < BaseTranslator
|
|
7
|
-
|
|
13
|
+
API_ENDPOINT = "https://translation.googleapis.com/language/translate/v2"
|
|
14
|
+
MAX_TEXTS_PER_REQUEST = 128
|
|
15
|
+
MAX_CHARS_PER_REQUEST = 30_000
|
|
16
|
+
|
|
17
|
+
class Error < StandardError; end
|
|
18
|
+
|
|
19
|
+
class RateLimitError < Error; end
|
|
20
|
+
|
|
21
|
+
class QuotaExceededError < Error; end
|
|
22
|
+
|
|
8
23
|
def initialize(*)
|
|
9
|
-
begin
|
|
10
|
-
require 'easy_translate'
|
|
11
|
-
rescue LoadError
|
|
12
|
-
raise ::I18n::Tasks::CommandError, "Add gem 'easy_translate' to your Gemfile to use this command"
|
|
13
|
-
end
|
|
14
24
|
super
|
|
15
25
|
end
|
|
16
26
|
|
|
17
27
|
protected
|
|
18
28
|
|
|
19
29
|
def translate_values(list, **options)
|
|
30
|
+
html = options[:html].present?
|
|
20
31
|
result = restore_newlines(
|
|
21
|
-
|
|
22
|
-
replace_newlines_with_placeholder(list
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
translate(
|
|
33
|
+
texts: replace_newlines_with_placeholder(list),
|
|
34
|
+
api_key: api_key,
|
|
35
|
+
to: options[:to],
|
|
36
|
+
from: options[:from],
|
|
37
|
+
html:
|
|
25
38
|
),
|
|
26
|
-
|
|
39
|
+
html:
|
|
27
40
|
)
|
|
28
41
|
|
|
29
42
|
@progress_bar.progress += result.size
|
|
@@ -40,15 +53,15 @@ module I18n::Tasks::Translators
|
|
|
40
53
|
end
|
|
41
54
|
|
|
42
55
|
def options_for_html
|
|
43
|
-
{
|
|
56
|
+
{html: true}
|
|
44
57
|
end
|
|
45
58
|
|
|
46
59
|
def options_for_plain
|
|
47
|
-
{
|
|
60
|
+
{format: "text"}
|
|
48
61
|
end
|
|
49
62
|
|
|
50
63
|
def no_results_error_message
|
|
51
|
-
I18n.t(
|
|
64
|
+
I18n.t("i18n_tasks.google_translate.errors.no_results")
|
|
52
65
|
end
|
|
53
66
|
|
|
54
67
|
private
|
|
@@ -59,19 +72,17 @@ module I18n::Tasks::Translators
|
|
|
59
72
|
# fallback with deprecation warning
|
|
60
73
|
if @i18n_tasks.translation_config[:api_key]
|
|
61
74
|
warn_deprecated(
|
|
62
|
-
|
|
75
|
+
"Please rename Google Translate API Key from `api_key` to `google_translate_api_key`."
|
|
63
76
|
)
|
|
64
77
|
key ||= translation_config[:api_key]
|
|
65
78
|
end
|
|
66
|
-
fail ::I18n::Tasks::CommandError, I18n.t(
|
|
79
|
+
fail ::I18n::Tasks::CommandError, I18n.t("i18n_tasks.google_translate.errors.no_api_key") if key.blank?
|
|
67
80
|
|
|
68
81
|
key
|
|
69
82
|
end
|
|
70
83
|
end
|
|
71
84
|
|
|
72
|
-
def replace_newlines_with_placeholder(list
|
|
73
|
-
return list unless html
|
|
74
|
-
|
|
85
|
+
def replace_newlines_with_placeholder(list)
|
|
75
86
|
list.map do |value|
|
|
76
87
|
value.gsub(/\n(\s*)/) do
|
|
77
88
|
"<Z__#{::Regexp.last_match(1)&.length || 0}>"
|
|
@@ -79,13 +90,146 @@ module I18n::Tasks::Translators
|
|
|
79
90
|
end
|
|
80
91
|
end
|
|
81
92
|
|
|
82
|
-
def restore_newlines(translations, html)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
translations.map do |translation|
|
|
93
|
+
def restore_newlines(translations, html:)
|
|
94
|
+
restored = translations.map do |translation|
|
|
86
95
|
translation.gsub(/<Z__(\d+)>/) do
|
|
87
|
-
"\n#{
|
|
96
|
+
"\n#{" " * ::Regexp.last_match(1).to_i}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Need to unescape if translating HTML content
|
|
101
|
+
html ? restored.map { |t| CGI.unescapeHTML(t) } : restored
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @param texts [Array<String>] texts to translate
|
|
105
|
+
# @param api_key [String] Google Translate API key (required)
|
|
106
|
+
# @param to [String] target language code (required)
|
|
107
|
+
# @param format [Symbol] :text or :html (required)
|
|
108
|
+
# @param from [String] source language code
|
|
109
|
+
# @return [Array<String>] translated texts
|
|
110
|
+
def translate(texts:, api_key:, to:, html:, from: nil)
|
|
111
|
+
texts = Array(texts)
|
|
112
|
+
|
|
113
|
+
return [] if texts.empty?
|
|
114
|
+
|
|
115
|
+
raise ArgumentError, "api_key is required" if api_key.nil? || api_key.empty?
|
|
116
|
+
|
|
117
|
+
raise ArgumentError, "target language (:to) is required" if to.nil? || to.empty?
|
|
118
|
+
|
|
119
|
+
# Split into batches if needed
|
|
120
|
+
batches = batch_texts(texts)
|
|
121
|
+
|
|
122
|
+
batches.flat_map do |batch|
|
|
123
|
+
translate_batch(texts: batch, api_key:, to:, from:, html:)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def batch_texts(texts)
|
|
128
|
+
batches = []
|
|
129
|
+
current_batch = []
|
|
130
|
+
current_chars = 0
|
|
131
|
+
|
|
132
|
+
texts.each do |text|
|
|
133
|
+
text_length = text.to_s.length
|
|
134
|
+
|
|
135
|
+
# If adding this text would exceed limits, start new batch
|
|
136
|
+
if current_batch.size >= MAX_TEXTS_PER_REQUEST ||
|
|
137
|
+
(current_chars + text_length > MAX_CHARS_PER_REQUEST && current_batch.any?)
|
|
138
|
+
batches << current_batch
|
|
139
|
+
current_batch = []
|
|
140
|
+
current_chars = 0
|
|
88
141
|
end
|
|
142
|
+
|
|
143
|
+
current_batch << text
|
|
144
|
+
current_chars += text_length
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
batches << current_batch if current_batch.any?
|
|
148
|
+
batches
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def translate_batch(texts:, api_key:, to:, from:, html:)
|
|
152
|
+
uri = URI(API_ENDPOINT)
|
|
153
|
+
# Provide API key via query string as expected by v2 API
|
|
154
|
+
uri.query = URI.encode_www_form(key: api_key)
|
|
155
|
+
|
|
156
|
+
body = {
|
|
157
|
+
q: texts,
|
|
158
|
+
target: to,
|
|
159
|
+
format: html ? "html" : "text"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Add source language if specified
|
|
163
|
+
if from
|
|
164
|
+
body[:source] = from
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
response = make_request(uri, body)
|
|
168
|
+
parse_response(response, texts.size)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def make_request(uri, body)
|
|
172
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
173
|
+
http.use_ssl = true
|
|
174
|
+
http.read_timeout = 60
|
|
175
|
+
http.open_timeout = 30
|
|
176
|
+
|
|
177
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
178
|
+
request["Content-Type"] = "application/json"
|
|
179
|
+
request["Accept"] = "application/json"
|
|
180
|
+
request["User-Agent"] = "i18n-tasks/GoogleTranslateApi"
|
|
181
|
+
request.body = JSON.generate(body)
|
|
182
|
+
|
|
183
|
+
response = http.request(request)
|
|
184
|
+
|
|
185
|
+
handle_error_response(response) unless response.is_a?(Net::HTTPSuccess)
|
|
186
|
+
response
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def parse_response(response, expected_count)
|
|
190
|
+
data = JSON.parse(response.body)
|
|
191
|
+
|
|
192
|
+
unless data["data"] && data["data"]["translations"]
|
|
193
|
+
raise Error, "Unexpected API response format"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
translations = data["data"]["translations"].map do |translation|
|
|
197
|
+
translation["translatedText"]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
if translations.size != expected_count
|
|
201
|
+
raise Error, "Expected #{expected_count} translations, got #{translations.size}"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
translations
|
|
205
|
+
rescue JSON::ParserError => e
|
|
206
|
+
raise Error, "Failed to parse API response: #{e.message}"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def handle_error_response(response)
|
|
210
|
+
begin
|
|
211
|
+
error_data = JSON.parse(response.body)
|
|
212
|
+
error_message = error_data.dig("error", "message") || "Unknown error"
|
|
213
|
+
error_code = error_data.dig("error", "code")
|
|
214
|
+
rescue JSON::ParserError
|
|
215
|
+
error_message = response.body
|
|
216
|
+
error_code = response.code
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
status = response.code.to_i
|
|
220
|
+
diagnostic = "(status=#{status}, code=#{error_code})"
|
|
221
|
+
case status
|
|
222
|
+
when 400
|
|
223
|
+
raise Error, "Bad request #{diagnostic}: #{error_message}"
|
|
224
|
+
when 401, 403
|
|
225
|
+
raise Error, "Authentication/authorization failed #{diagnostic}: #{error_message}. " \
|
|
226
|
+
"Ensure the Cloud Translation API v2 is enabled, billing is active, and the key is unrestricted for this API."
|
|
227
|
+
when 429
|
|
228
|
+
raise RateLimitError, "Rate limit exceeded #{diagnostic}: #{error_message}"
|
|
229
|
+
when 503
|
|
230
|
+
raise QuotaExceededError, "Quota exceeded #{diagnostic}: #{error_message}"
|
|
231
|
+
else
|
|
232
|
+
raise Error, "API error #{diagnostic}: #{error_message}"
|
|
89
233
|
end
|
|
90
234
|
end
|
|
91
235
|
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "i18n/tasks/translators/base_translator"
|
|
4
|
+
require "active_support/core_ext/string/filters"
|
|
5
5
|
|
|
6
6
|
module I18n::Tasks::Translators
|
|
7
7
|
class OpenAiTranslator < BaseTranslator
|
|
8
|
+
include ::I18n::Tasks::Data::LanguageNames
|
|
9
|
+
|
|
8
10
|
# max allowed texts per request
|
|
9
11
|
BATCH_SIZE = 50
|
|
10
12
|
DEFAULT_SYSTEM_PROMPT = <<~PROMPT.squish
|
|
@@ -26,7 +28,7 @@ module I18n::Tasks::Translators
|
|
|
26
28
|
|
|
27
29
|
def initialize(*)
|
|
28
30
|
begin
|
|
29
|
-
require
|
|
31
|
+
require "openai"
|
|
30
32
|
rescue LoadError
|
|
31
33
|
raise ::I18n::Tasks::CommandError, "Add gem 'ruby-openai' to your Gemfile to use this command"
|
|
32
34
|
end
|
|
@@ -49,7 +51,7 @@ module I18n::Tasks::Translators
|
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
def no_results_error_message
|
|
52
|
-
I18n.t(
|
|
54
|
+
I18n.t("i18n_tasks.openai_translate.errors.no_results")
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
private
|
|
@@ -61,29 +63,39 @@ module I18n::Tasks::Translators
|
|
|
61
63
|
def api_key
|
|
62
64
|
@api_key ||= begin
|
|
63
65
|
key = @i18n_tasks.translation_config[:openai_api_key]
|
|
64
|
-
fail ::I18n::Tasks::CommandError, I18n.t(
|
|
66
|
+
fail ::I18n::Tasks::CommandError, I18n.t("i18n_tasks.openai_translate.errors.no_api_key") if key.blank?
|
|
65
67
|
|
|
66
68
|
key
|
|
67
69
|
end
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
def model
|
|
71
|
-
@model ||= @i18n_tasks.translation_config[:openai_model].presence ||
|
|
73
|
+
@model ||= @i18n_tasks.translation_config[:openai_model].presence || "gpt-4o-mini"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def temperature
|
|
77
|
+
@temperature ||= @i18n_tasks.translation_config[:openai_temperature].presence || 0.0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def system_prompt(to_locale)
|
|
81
|
+
prompt = if locale_prompts[to_locale].present?
|
|
82
|
+
locale_prompts[to_locale]
|
|
83
|
+
else
|
|
84
|
+
@i18n_tasks.translation_config[:openai_system_prompt].presence || DEFAULT_SYSTEM_PROMPT
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
prompt.concat("\n#{JSON_FORMAT_INSTRUCTIONS_SYSTEM_PROMPT}")
|
|
72
88
|
end
|
|
73
89
|
|
|
74
|
-
def
|
|
75
|
-
@
|
|
76
|
-
(@i18n_tasks.translation_config[:openai_system_prompt].presence || DEFAULT_SYSTEM_PROMPT)
|
|
77
|
-
.concat("\n#{JSON_FORMAT_INSTRUCTIONS_SYSTEM_PROMPT}")
|
|
78
|
-
@system_prompt
|
|
90
|
+
def locale_prompts
|
|
91
|
+
@locale_prompts ||= @i18n_tasks.translation_config[:openai_locale_prompts] || {}
|
|
79
92
|
end
|
|
80
93
|
|
|
81
94
|
def translate_values(list, from:, to:)
|
|
82
95
|
results = []
|
|
83
96
|
|
|
84
97
|
list.each_slice(BATCH_SIZE) do |batch|
|
|
85
|
-
|
|
86
|
-
result = JSON.parse(translations)
|
|
98
|
+
result = translate(batch, from, to)
|
|
87
99
|
results << result
|
|
88
100
|
|
|
89
101
|
@progress_bar.progress += result.size
|
|
@@ -97,33 +109,32 @@ module I18n::Tasks::Translators
|
|
|
97
109
|
parameters: {
|
|
98
110
|
model: model,
|
|
99
111
|
messages: build_messages(values, from, to),
|
|
100
|
-
temperature:
|
|
101
|
-
response_format: {
|
|
112
|
+
temperature: temperature,
|
|
113
|
+
response_format: {type: "json_object"}
|
|
102
114
|
}
|
|
103
115
|
)
|
|
104
116
|
|
|
105
|
-
translations = response.dig(
|
|
106
|
-
error = response[
|
|
117
|
+
translations = response.dig("choices", 0, "message", "content")
|
|
118
|
+
error = response["error"]
|
|
107
119
|
|
|
108
120
|
fail "AI error: #{error}" if error.present?
|
|
109
121
|
|
|
110
122
|
# Extract the array from the JSON object response
|
|
111
|
-
|
|
112
|
-
result['translations'].to_json
|
|
123
|
+
JSON.parse(translations)["translations"]
|
|
113
124
|
end
|
|
114
125
|
|
|
115
126
|
def build_messages(values, from, to)
|
|
116
127
|
[
|
|
117
128
|
{
|
|
118
|
-
role:
|
|
119
|
-
content: format(system_prompt, from: from, to: to)
|
|
129
|
+
role: "system",
|
|
130
|
+
content: format(system_prompt(to), from: language_name(from), to: language_name(to))
|
|
120
131
|
},
|
|
121
132
|
{
|
|
122
|
-
role:
|
|
133
|
+
role: "user",
|
|
123
134
|
content: "Translate this array: \n\n\n"
|
|
124
135
|
},
|
|
125
136
|
{
|
|
126
|
-
role:
|
|
137
|
+
role: "user",
|
|
127
138
|
content: values.to_json
|
|
128
139
|
}
|
|
129
140
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "i18n/tasks/translators/base_translator"
|
|
4
|
+
require "active_support/core_ext/string/filters"
|
|
5
5
|
|
|
6
6
|
module I18n::Tasks::Translators
|
|
7
7
|
class WatsonxTranslator < BaseTranslator
|
|
@@ -40,7 +40,7 @@ module I18n::Tasks::Translators
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def no_results_error_message
|
|
43
|
-
I18n.t(
|
|
43
|
+
I18n.t("i18n_tasks.watsonx_translate.errors.no_results")
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
private
|
|
@@ -52,7 +52,7 @@ module I18n::Tasks::Translators
|
|
|
52
52
|
def api_key
|
|
53
53
|
@api_key ||= begin
|
|
54
54
|
key = @i18n_tasks.translation_config[:watsonx_api_key]
|
|
55
|
-
fail ::I18n::Tasks::CommandError, I18n.t(
|
|
55
|
+
fail ::I18n::Tasks::CommandError, I18n.t("i18n_tasks.watsonx_translate.errors.no_api_key") if key.blank?
|
|
56
56
|
|
|
57
57
|
key
|
|
58
58
|
end
|
|
@@ -63,7 +63,7 @@ module I18n::Tasks::Translators
|
|
|
63
63
|
project_id = @i18n_tasks.translation_config[:watsonx_project_id]
|
|
64
64
|
if project_id.blank?
|
|
65
65
|
fail ::I18n::Tasks::CommandError,
|
|
66
|
-
|
|
66
|
+
I18n.t("i18n_tasks.watsonx_translate.errors.no_project_id")
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
project_id
|
|
@@ -71,7 +71,7 @@ module I18n::Tasks::Translators
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def model
|
|
74
|
-
@model ||= @i18n_tasks.translation_config[:watsonx_model].presence ||
|
|
74
|
+
@model ||= @i18n_tasks.translation_config[:watsonx_model].presence || "meta-llama/llama-3-2-90b-vision-instruct"
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def system_prompt
|
|
@@ -94,11 +94,11 @@ module I18n::Tasks::Translators
|
|
|
94
94
|
|
|
95
95
|
def translate(values, from, to)
|
|
96
96
|
prompt = [
|
|
97
|
-
|
|
97
|
+
"<|eot_id|><|start_header_id|>system<|end_header_id|>",
|
|
98
98
|
format(system_prompt, from: from, to: to),
|
|
99
|
-
|
|
99
|
+
"<|eot_id|><|start_header_id|>user<|end_header_id|>Translate this array:",
|
|
100
100
|
"<|eot_id|><|start_header_id|>user<|end_header_id|>#{values.to_json}",
|
|
101
|
-
|
|
101
|
+
"<|eot_id|><|start_header_id|>assistant<|end_header_id|>"
|
|
102
102
|
].join
|
|
103
103
|
|
|
104
104
|
response = translator.generate_text(
|
|
@@ -111,18 +111,18 @@ module I18n::Tasks::Translators
|
|
|
111
111
|
repetition_penalty: 1
|
|
112
112
|
}
|
|
113
113
|
)
|
|
114
|
-
response.dig(
|
|
114
|
+
response.dig("results", 0, "generated_text")
|
|
115
115
|
end
|
|
116
116
|
end
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
class WatsonxClient
|
|
120
|
-
WATSONX_BASE_URL =
|
|
121
|
-
IBM_CLOUD_IAM_URL =
|
|
120
|
+
WATSONX_BASE_URL = "https://us-south.ml.cloud.ibm.com/ml/"
|
|
121
|
+
IBM_CLOUD_IAM_URL = "https://iam.cloud.ibm.com/identity/token"
|
|
122
122
|
|
|
123
123
|
def initialize(key:)
|
|
124
124
|
begin
|
|
125
|
-
require
|
|
125
|
+
require "faraday"
|
|
126
126
|
rescue LoadError
|
|
127
127
|
raise ::I18n::Tasks::CommandError, "Add gem 'faraday' to your Gemfile to use this command"
|
|
128
128
|
end
|
|
@@ -137,7 +137,7 @@ class WatsonxClient
|
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
def generate_text(**opts)
|
|
140
|
-
@http.post(
|
|
140
|
+
@http.post("v1/text/generation?version=2024-05-20", **opts).body
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
private
|
|
@@ -147,9 +147,9 @@ class WatsonxClient
|
|
|
147
147
|
conn.use Faraday::Response::RaiseError
|
|
148
148
|
conn.response :json
|
|
149
149
|
conn.params = {
|
|
150
|
-
grant_type:
|
|
150
|
+
grant_type: "urn:ibm:params:oauth:grant-type:apikey",
|
|
151
151
|
apikey: key
|
|
152
152
|
}
|
|
153
|
-
end.post.body[
|
|
153
|
+
end.post.body["access_token"]
|
|
154
154
|
end
|
|
155
155
|
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "i18n/tasks/translators/base_translator"
|
|
4
4
|
|
|
5
5
|
module I18n::Tasks::Translators
|
|
6
6
|
class YandexTranslator < BaseTranslator
|
|
7
7
|
def initialize(*)
|
|
8
8
|
begin
|
|
9
|
-
require
|
|
9
|
+
require "yandex-translator"
|
|
10
10
|
rescue LoadError
|
|
11
11
|
raise ::I18n::Tasks::CommandError, "Add gem 'yandex-translator' to your Gemfile to use this command"
|
|
12
12
|
end
|
|
@@ -31,24 +31,24 @@ module I18n::Tasks::Translators
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def options_for_html
|
|
34
|
-
{
|
|
34
|
+
{format: "html"}
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def options_for_plain
|
|
38
|
-
{
|
|
38
|
+
{format: "plain"}
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def no_results_error_message
|
|
42
|
-
I18n.t(
|
|
42
|
+
I18n.t("i18n_tasks.yandex_translate.errors.no_results")
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
private
|
|
46
46
|
|
|
47
47
|
# Convert 'es-ES' to 'es'
|
|
48
48
|
def to_yandex_compatible_locale(locale)
|
|
49
|
-
return locale unless locale.include?(
|
|
49
|
+
return locale unless locale.include?("-")
|
|
50
50
|
|
|
51
|
-
locale.split(
|
|
51
|
+
locale.split("-", 2).first
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def translator
|
|
@@ -58,7 +58,7 @@ module I18n::Tasks::Translators
|
|
|
58
58
|
def api_key
|
|
59
59
|
@api_key ||= begin
|
|
60
60
|
key = @i18n_tasks.translation_config[:yandex_api_key]
|
|
61
|
-
fail ::I18n::Tasks::CommandError, I18n.t(
|
|
61
|
+
fail ::I18n::Tasks::CommandError, I18n.t("i18n_tasks.yandex_translate.errors.no_api_key") if key.blank?
|
|
62
62
|
|
|
63
63
|
key
|
|
64
64
|
end
|