i18n-tasks 1.0.12 → 1.0.14
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 -2
- data/Rakefile +1 -1
- data/config/locales/en.yml +11 -4
- data/config/locales/ru.yml +11 -4
- data/i18n-tasks.gemspec +5 -5
- data/lib/i18n/tasks/cli.rb +8 -8
- data/lib/i18n/tasks/command/commands/data.rb +14 -0
- data/lib/i18n/tasks/command/commands/missing.rb +2 -2
- data/lib/i18n/tasks/command/options/common.rb +1 -1
- data/lib/i18n/tasks/configuration.rb +3 -1
- data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +7 -1
- data/lib/i18n/tasks/data/file_system_base.rb +5 -0
- data/lib/i18n/tasks/data/router/isolating_router.rb +146 -0
- data/lib/i18n/tasks/data/tree/siblings.rb +2 -2
- data/lib/i18n/tasks/data/tree/traversal.rb +2 -2
- data/lib/i18n/tasks/interpolations.rb +1 -1
- data/lib/i18n/tasks/key_pattern_matching.rb +5 -3
- data/lib/i18n/tasks/references.rb +3 -3
- data/lib/i18n/tasks/reports/base.rb +1 -1
- data/lib/i18n/tasks/reports/terminal.rb +7 -1
- data/lib/i18n/tasks/scanners/ast_matchers/default_i18n_subject_matcher.rb +26 -0
- data/lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb +4 -4
- data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +55 -25
- data/lib/i18n/tasks/scanners/pattern_scanner.rb +1 -1
- data/lib/i18n/tasks/scanners/relative_keys.rb +1 -1
- data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +3 -1
- data/lib/i18n/tasks/split_key.rb +30 -47
- data/lib/i18n/tasks/translation.rb +4 -1
- data/lib/i18n/tasks/translators/base_translator.rb +22 -11
- data/lib/i18n/tasks/translators/deepl_translator.rb +58 -9
- data/lib/i18n/tasks/translators/google_translator.rb +28 -13
- data/lib/i18n/tasks/translators/openai_translator.rb +118 -0
- data/lib/i18n/tasks/used_keys.rb +5 -5
- data/lib/i18n/tasks/version.rb +1 -1
- data/templates/config/i18n-tasks.yml +28 -3
- data/templates/minitest/i18n_test.rb +6 -6
- metadata +40 -30
- data/lib/i18n/tasks/scanners/erb_ast_processor.rb +0 -74
@@ -90,9 +90,9 @@ module I18n::Tasks
|
|
90
90
|
on_leaves_merge: lambda do |node, other|
|
91
91
|
if node.value != other.value
|
92
92
|
log_warn(
|
93
|
-
'Conflicting references: '\
|
94
|
-
"#{node.full_key(root: false)} ⮕ #{node.value} in #{node.data[:locale]},"\
|
95
|
-
"
|
93
|
+
'Conflicting references: ' \
|
94
|
+
"#{node.full_key(root: false)} ⮕ #{node.value} in #{node.data[:locale]}, " \
|
95
|
+
"but ⮕ #{other.value} in #{other.data[:locale]}"
|
96
96
|
)
|
97
97
|
end
|
98
98
|
end
|
@@ -36,7 +36,7 @@ module I18n::Tasks::Reports
|
|
36
36
|
|
37
37
|
def used_title(keys_nodes, filter)
|
38
38
|
used_n = keys_nodes.map { |_k, node| node.data[:occurrences].size }.reduce(:+).to_i
|
39
|
-
"#{keys_nodes.size} key#{'s' if keys_nodes.size != 1}#{" matching '#{filter}'" if filter}"\
|
39
|
+
"#{keys_nodes.size} key#{'s' if keys_nodes.size != 1}#{" matching '#{filter}'" if filter}" \
|
40
40
|
"#{" (#{used_n} usage#{'s' if used_n != 1})" if used_n.positive?}"
|
41
41
|
end
|
42
42
|
|
@@ -93,6 +93,12 @@ module I18n
|
|
93
93
|
end
|
94
94
|
end
|
95
95
|
|
96
|
+
def cp_results(results)
|
97
|
+
results.each do |(from, to)|
|
98
|
+
print_info "#{Rainbow(from).cyan} #{Rainbow('+').yellow.bright} #{Rainbow(to).green}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
96
102
|
def check_normalized_results(non_normalized)
|
97
103
|
if non_normalized.empty?
|
98
104
|
print_success 'All data is normalized'
|
@@ -112,7 +118,7 @@ module I18n
|
|
112
118
|
when :missing_plural
|
113
119
|
leaf[:data][:missing_keys].join(', ')
|
114
120
|
else
|
115
|
-
"#{Rainbow(leaf[:data][:missing_diff_locale]).cyan} "\
|
121
|
+
"#{Rainbow(leaf[:data][:missing_diff_locale]).cyan} " \
|
116
122
|
"#{format_value(leaf[:value].is_a?(String) ? leaf[:value].strip : leaf[:value])}"
|
117
123
|
end
|
118
124
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'i18n/tasks/scanners/ast_matchers/base_matcher'
|
4
|
+
require 'i18n/tasks/scanners/results/occurrence'
|
5
|
+
|
6
|
+
module I18n::Tasks::Scanners::AstMatchers
|
7
|
+
class DefaultI18nSubjectMatcher < BaseMatcher
|
8
|
+
def convert_to_key_occurrences(send_node, method_name, location: send_node.loc)
|
9
|
+
children = Array(send_node&.children)
|
10
|
+
return unless children[1] == :default_i18n_subject
|
11
|
+
|
12
|
+
key = @scanner.absolute_key(
|
13
|
+
'.subject',
|
14
|
+
location.expression.source_buffer.name,
|
15
|
+
calling_method: method_name
|
16
|
+
)
|
17
|
+
[
|
18
|
+
key,
|
19
|
+
I18n::Tasks::Scanners::Results::Occurrence.from_range(
|
20
|
+
raw_key: key,
|
21
|
+
range: location.expression
|
22
|
+
)
|
23
|
+
]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -79,10 +79,10 @@ module I18n::Tasks::Scanners::AstMatchers
|
|
79
79
|
end
|
80
80
|
if default_arg_node = extract_hash_pair(node, 'default')
|
81
81
|
default_arg = if default_arg_node.children[1]&.type == :hash
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
82
|
+
extract_hash(default_arg_node.children[1])
|
83
|
+
else
|
84
|
+
extract_string(default_arg_node.children[1])
|
85
|
+
end
|
86
86
|
end
|
87
87
|
|
88
88
|
[key, default_arg]
|
@@ -1,16 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'i18n/tasks/scanners/ruby_ast_scanner'
|
4
|
-
require 'i18n/tasks/scanners/
|
5
|
-
require 'better_html/errors'
|
6
|
-
require 'better_html/parser'
|
4
|
+
require 'i18n/tasks/scanners/local_ruby_parser'
|
7
5
|
|
8
6
|
module I18n::Tasks::Scanners
|
9
7
|
# Scan for I18n.translate calls in ERB-file better-html and ASTs
|
10
8
|
class ErbAstScanner < RubyAstScanner
|
9
|
+
DEFAULT_REGEXP = /<%(={1,2}|-|\#|%)?(.*?)([-=])?%>/m.freeze
|
10
|
+
|
11
11
|
def initialize(**args)
|
12
12
|
super(**args)
|
13
|
-
@
|
13
|
+
@ruby_parser = LocalRubyParser.new(ignore_blocks: true)
|
14
14
|
end
|
15
15
|
|
16
16
|
private
|
@@ -20,29 +20,59 @@ module I18n::Tasks::Scanners
|
|
20
20
|
# @param path Path to file to parse
|
21
21
|
# @return [{Parser::AST::Node}, [Parser::Source::Comment]]
|
22
22
|
def path_to_ast_and_comments(path)
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
comments = []
|
24
|
+
buffer = make_buffer(path)
|
25
|
+
|
26
|
+
children = []
|
27
|
+
buffer
|
28
|
+
.source
|
29
|
+
.scan(DEFAULT_REGEXP) do |indicator, code, tailch, _rspace|
|
30
|
+
match = Regexp.last_match
|
31
|
+
character = indicator ? indicator[0] : nil
|
32
|
+
|
33
|
+
start = match.begin(0) + 2 + (character&.size || 0)
|
34
|
+
stop = match.end(0) - 2 - (tailch&.size || 0)
|
35
|
+
|
36
|
+
case character
|
37
|
+
when '=', nil, '-'
|
38
|
+
parsed, parsed_comments = handle_code(buffer, code, start, stop)
|
39
|
+
comments.concat(parsed_comments)
|
40
|
+
children << parsed unless parsed.nil?
|
41
|
+
when '#', '#-'
|
42
|
+
comments << handle_comment(buffer, start, stop)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
[root_node(children, buffer), comments]
|
26
47
|
end
|
27
48
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
49
|
+
def handle_code(buffer, code, start, stop)
|
50
|
+
range = ::Parser::Source::Range.new(buffer, start, stop)
|
51
|
+
location =
|
52
|
+
Parser::Source::Map::Definition.new(
|
53
|
+
range.begin,
|
54
|
+
range.begin,
|
55
|
+
range.begin,
|
56
|
+
range.end
|
57
|
+
)
|
58
|
+
@ruby_parser.parse(code, location: location)
|
59
|
+
end
|
60
|
+
|
61
|
+
def handle_comment(buffer, start, stop)
|
62
|
+
range = ::Parser::Source::Range.new(buffer, start, stop)
|
63
|
+
::Parser::Source::Comment.new(range)
|
64
|
+
end
|
65
|
+
|
66
|
+
def root_node(children, buffer)
|
67
|
+
range = ::Parser::Source::Range.new(buffer, 0, buffer.source.size)
|
68
|
+
location =
|
69
|
+
Parser::Source::Map::Definition.new(
|
70
|
+
range.begin,
|
71
|
+
range.begin,
|
72
|
+
range.begin,
|
73
|
+
range.end
|
74
|
+
)
|
75
|
+
::Parser::AST::Node.new(:erb, children, location: location)
|
46
76
|
end
|
47
77
|
end
|
48
78
|
end
|
@@ -12,7 +12,7 @@ module I18n::Tasks::Scanners
|
|
12
12
|
include OccurrenceFromPosition
|
13
13
|
include RubyKeyLiterals
|
14
14
|
|
15
|
-
TRANSLATE_CALL_RE = /(?<=^|[^\
|
15
|
+
TRANSLATE_CALL_RE = /(?<=^|[^\p{L}'\-.]|[^\p{L}'-]I18n\.|I18n\.)t(?:!|ranslate!?)?/.freeze
|
16
16
|
IGNORE_LINES = {
|
17
17
|
'coffee' => /^\s*#(?!\si18n-tasks-use)/,
|
18
18
|
'erb' => /^\s*<%\s*#(?!\si18n-tasks-use)/,
|
@@ -3,7 +3,9 @@
|
|
3
3
|
require 'i18n/tasks/scanners/file_scanner'
|
4
4
|
require 'i18n/tasks/scanners/relative_keys'
|
5
5
|
require 'i18n/tasks/scanners/ruby_ast_call_finder'
|
6
|
+
require 'i18n/tasks/scanners/ast_matchers/default_i18n_subject_matcher'
|
6
7
|
require 'i18n/tasks/scanners/ast_matchers/message_receivers_matcher'
|
8
|
+
require 'i18n/tasks/scanners/ast_matchers/rails_model_matcher'
|
7
9
|
require 'parser/current'
|
8
10
|
|
9
11
|
module I18n::Tasks::Scanners
|
@@ -79,7 +81,7 @@ module I18n::Tasks::Scanners
|
|
79
81
|
results = []
|
80
82
|
|
81
83
|
# method_name is not available at this stage
|
82
|
-
calls.each do |send_node, _method_name|
|
84
|
+
calls.each do |(send_node, _method_name)|
|
83
85
|
@matchers.each do |matcher|
|
84
86
|
result = matcher.convert_to_key_occurrences(
|
85
87
|
send_node,
|
data/lib/i18n/tasks/split_key.rb
CHANGED
@@ -5,6 +5,11 @@ module I18n
|
|
5
5
|
module SplitKey
|
6
6
|
module_function
|
7
7
|
|
8
|
+
PARENTHESIS_PAIRS = %w({} [] () <>).freeze
|
9
|
+
START_KEYS = PARENTHESIS_PAIRS.to_set { |pair| pair[0] }.freeze
|
10
|
+
END_KEYS = PARENTHESIS_PAIRS.to_h { |pair| [pair[0], pair[1]] }.freeze
|
11
|
+
private_constant :PARENTHESIS_PAIRS, :START_KEYS, :END_KEYS
|
12
|
+
|
8
13
|
# split a key by dots (.)
|
9
14
|
# dots inside braces or parenthesis are not split on
|
10
15
|
#
|
@@ -12,61 +17,39 @@ module I18n
|
|
12
17
|
# split_key 'a.#{b.c}' # => ['a', '#{b.c}']
|
13
18
|
# split_key 'a.b.c', 2 # => ['a', 'b.c']
|
14
19
|
def split_key(key, max = Float::INFINITY)
|
15
|
-
parts = []
|
16
|
-
pos = 0
|
17
20
|
return [key] if max == 1
|
18
21
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
parts = []
|
23
|
+
current_parenthesis_end_char = nil
|
24
|
+
part = ''
|
25
|
+
key.each_char.with_index do |char, index|
|
26
|
+
if current_parenthesis_end_char
|
27
|
+
part += char
|
28
|
+
current_parenthesis_end_char = nil if char == current_parenthesis_end_char
|
29
|
+
elsif START_KEYS.include?(char)
|
30
|
+
part += char
|
31
|
+
current_parenthesis_end_char = END_KEYS[char]
|
32
|
+
elsif char == '.'
|
33
|
+
parts << part
|
34
|
+
if parts.size + 1 == max
|
35
|
+
remaining = key[(index + 1)..]
|
36
|
+
parts << remaining unless remaining.empty?
|
37
|
+
return parts
|
38
|
+
end
|
39
|
+
part = ''
|
40
|
+
else
|
41
|
+
part += char
|
25
42
|
end
|
26
43
|
end
|
27
|
-
parts
|
28
|
-
end
|
29
|
-
|
30
|
-
def last_key_part(key)
|
31
|
-
last = nil
|
32
|
-
key_parts(key) { |part| last = part }
|
33
|
-
last
|
34
|
-
end
|
35
44
|
|
36
|
-
|
37
|
-
# dots inside braces or parenthesis are not split on
|
38
|
-
def key_parts(key, &block)
|
39
|
-
return enum_for(:key_parts, key) unless block
|
45
|
+
return parts if part.empty?
|
40
46
|
|
41
|
-
|
42
|
-
counts = PARENS_ZEROS # dup'd later if key contains parenthesis
|
43
|
-
delim = '.'
|
44
|
-
from = to = 0
|
45
|
-
key.each_char do |char|
|
46
|
-
if char == delim && PARENS_ZEROS == counts
|
47
|
-
block.yield key[from...to]
|
48
|
-
from = to = (to + 1)
|
49
|
-
else
|
50
|
-
nest_i, nest_inc = nesting[char]
|
51
|
-
if nest_i
|
52
|
-
counts = counts.dup if counts.frozen?
|
53
|
-
counts[nest_i] += nest_inc
|
54
|
-
end
|
55
|
-
to += 1
|
56
|
-
end
|
57
|
-
end
|
58
|
-
block.yield(key[from...to]) if from < to && to <= key.length
|
59
|
-
true
|
47
|
+
current_parenthesis_end_char ? parts.concat(part.split('.')) : parts << part
|
60
48
|
end
|
61
49
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
h[s[1].freeze] = [i, -1].freeze
|
66
|
-
end.freeze
|
67
|
-
PARENS_ZEROS = Array.new(PARENS.size, 0).freeze
|
68
|
-
private_constant :PARENS
|
69
|
-
private_constant :PARENS_ZEROS
|
50
|
+
def last_key_part(key)
|
51
|
+
split_key(key).last
|
52
|
+
end
|
70
53
|
end
|
71
54
|
end
|
72
55
|
end
|
@@ -2,13 +2,14 @@
|
|
2
2
|
|
3
3
|
require 'i18n/tasks/translators/deepl_translator'
|
4
4
|
require 'i18n/tasks/translators/google_translator'
|
5
|
+
require 'i18n/tasks/translators/openai_translator'
|
5
6
|
require 'i18n/tasks/translators/yandex_translator'
|
6
7
|
|
7
8
|
module I18n::Tasks
|
8
9
|
module Translation
|
9
10
|
# @param [I18n::Tasks::Tree::Siblings] forest to translate to the locales of its root nodes
|
10
11
|
# @param [String] from locale
|
11
|
-
# @param [:deepl, :google, :yandex] backend
|
12
|
+
# @param [:deepl, :openai, :google, :yandex] backend
|
12
13
|
# @return [I18n::Tasks::Tree::Siblings] translated forest
|
13
14
|
def translate_forest(forest, from:, backend: :google)
|
14
15
|
case backend
|
@@ -16,6 +17,8 @@ module I18n::Tasks
|
|
16
17
|
Translators::DeeplTranslator.new(self).translate_forest(forest, from)
|
17
18
|
when :google
|
18
19
|
Translators::GoogleTranslator.new(self).translate_forest(forest, from)
|
20
|
+
when :openai
|
21
|
+
Translators::OpenAiTranslator.new(self).translate_forest(forest, from)
|
19
22
|
when :yandex
|
20
23
|
Translators::YandexTranslator.new(self).translate_forest(forest, from)
|
21
24
|
else
|
@@ -3,6 +3,7 @@
|
|
3
3
|
module I18n::Tasks
|
4
4
|
module Translators
|
5
5
|
class BaseTranslator
|
6
|
+
include ::I18n::Tasks::Logging
|
6
7
|
# @param [I18n::Tasks::BaseTask] i18n_tasks
|
7
8
|
def initialize(i18n_tasks)
|
8
9
|
@i18n_tasks = i18n_tasks
|
@@ -31,7 +32,7 @@ module I18n::Tasks
|
|
31
32
|
reference_key_vals = list.select { |_k, v| v.is_a? Symbol } || []
|
32
33
|
list -= reference_key_vals
|
33
34
|
result = list.group_by { |k_v| @i18n_tasks.html_key? k_v[0], opts[:from] }.map do |is_html, list_slice|
|
34
|
-
fetch_translations
|
35
|
+
fetch_translations(list_slice, opts.merge(is_html ? options_for_html : options_for_plain))
|
35
36
|
end.reduce(:+) || []
|
36
37
|
result.concat(reference_key_vals)
|
37
38
|
result.sort! { |a, b| key_pos[a[0]] <=> key_pos[b[0]] }
|
@@ -41,34 +42,39 @@ module I18n::Tasks
|
|
41
42
|
# @param [Array<[String, Object]>] list of key-value pairs
|
42
43
|
# @return [Array<[String, Object]>] translated list
|
43
44
|
def fetch_translations(list, opts)
|
44
|
-
|
45
|
+
options = options_for_translate_values(**opts)
|
46
|
+
from_values(list, translate_values(to_values(list, options), **options), options).tap do |result|
|
45
47
|
fail CommandError, no_results_error_message if result.blank?
|
46
48
|
end
|
47
49
|
end
|
48
50
|
|
49
51
|
# @param [Array<[String, Object]>] list of key-value pairs
|
50
52
|
# @return [Array<String>] values for translation extracted from list
|
51
|
-
def to_values(list)
|
52
|
-
list.map { |l| dump_value
|
53
|
+
def to_values(list, opts)
|
54
|
+
list.map { |l| dump_value(l[1], opts) }.flatten.compact
|
53
55
|
end
|
54
56
|
|
55
57
|
# @param [Array<[String, Object]>] list
|
56
58
|
# @param [Array<String>] translated_values
|
57
59
|
# @return [Array<[String, Object]>] translated key-value pairs
|
58
|
-
def from_values(list, translated_values)
|
60
|
+
def from_values(list, translated_values, opts)
|
59
61
|
keys = list.map(&:first)
|
60
62
|
untranslated_values = list.map(&:last)
|
61
|
-
keys.zip parse_value(untranslated_values, translated_values.to_enum)
|
63
|
+
keys.zip parse_value(untranslated_values, translated_values.to_enum, opts)
|
62
64
|
end
|
63
65
|
|
64
66
|
# Prepare value for translation.
|
65
67
|
# @return [String, Array<String, nil>, nil] value for Google Translate or nil for non-string values
|
66
|
-
def dump_value(value)
|
68
|
+
def dump_value(value, opts)
|
67
69
|
case value
|
68
70
|
when Array
|
69
71
|
# dump recursively
|
70
|
-
value.map { |v| dump_value
|
72
|
+
value.map { |v| dump_value(v, opts) }
|
73
|
+
when Hash
|
74
|
+
# dump recursively
|
75
|
+
value.values.map { |v| dump_value(v, opts) }
|
71
76
|
when String
|
77
|
+
value = CGI.escapeHTML(value) if opts[:html_escape]
|
72
78
|
replace_interpolations value unless value.empty?
|
73
79
|
end
|
74
80
|
end
|
@@ -77,16 +83,21 @@ module I18n::Tasks
|
|
77
83
|
# @param [Object] untranslated
|
78
84
|
# @param [Enumerator] each_translated
|
79
85
|
# @return [Object] final translated value
|
80
|
-
def parse_value(untranslated, each_translated)
|
86
|
+
def parse_value(untranslated, each_translated, opts)
|
81
87
|
case untranslated
|
82
88
|
when Array
|
83
89
|
# implode array
|
84
|
-
untranslated.map { |from| parse_value(from, each_translated) }
|
90
|
+
untranslated.map { |from| parse_value(from, each_translated, opts) }
|
91
|
+
when Hash
|
92
|
+
# implode hash
|
93
|
+
untranslated.transform_values { |value| parse_value(value, each_translated, opts) }
|
85
94
|
when String
|
86
95
|
if untranslated.empty?
|
87
96
|
untranslated
|
88
97
|
else
|
89
|
-
|
98
|
+
value = each_translated.next
|
99
|
+
value = CGI.unescapeHTML(value) if opts[:html_escape]
|
100
|
+
restore_interpolations(untranslated, value)
|
90
101
|
end
|
91
102
|
else
|
92
103
|
untranslated
|
@@ -4,6 +4,11 @@ require 'i18n/tasks/translators/base_translator'
|
|
4
4
|
|
5
5
|
module I18n::Tasks::Translators
|
6
6
|
class DeeplTranslator < BaseTranslator
|
7
|
+
# max allowed texts per request
|
8
|
+
BATCH_SIZE = 50
|
9
|
+
# those languages must be specified with their sub-kind e.g en-us
|
10
|
+
SPECIFIC_TARGETS = %w[en pt].freeze
|
11
|
+
|
7
12
|
def initialize(*)
|
8
13
|
begin
|
9
14
|
require 'deepl'
|
@@ -17,16 +22,27 @@ module I18n::Tasks::Translators
|
|
17
22
|
protected
|
18
23
|
|
19
24
|
def translate_values(list, from:, to:, **options)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
+
results = []
|
26
|
+
list.each_slice(BATCH_SIZE) do |parts|
|
27
|
+
res = DeepL.translate(
|
28
|
+
parts,
|
29
|
+
to_deepl_source_locale(from),
|
30
|
+
to_deepl_target_locale(to),
|
31
|
+
options_with_glossary(options, from, to)
|
32
|
+
)
|
33
|
+
if res.is_a?(DeepL::Resources::Text)
|
34
|
+
results << res.text
|
35
|
+
else
|
36
|
+
results += res.map(&:text)
|
37
|
+
end
|
25
38
|
end
|
39
|
+
results
|
26
40
|
end
|
27
41
|
|
28
42
|
def options_for_translate_values(**options)
|
29
|
-
|
43
|
+
extra_options = @i18n_tasks.translation_config[:deepl_options]&.symbolize_keys || {}
|
44
|
+
|
45
|
+
extra_options.merge({ ignore_tags: %w[i18n] }).merge(options)
|
30
46
|
end
|
31
47
|
|
32
48
|
def options_for_html
|
@@ -34,7 +50,7 @@ module I18n::Tasks::Translators
|
|
34
50
|
end
|
35
51
|
|
36
52
|
def options_for_plain
|
37
|
-
{ preserve_formatting: true }
|
53
|
+
{ preserve_formatting: true, tag_handling: 'xml', html_escape: true }
|
38
54
|
end
|
39
55
|
|
40
56
|
# @param [String] value
|
@@ -60,11 +76,23 @@ module I18n::Tasks::Translators
|
|
60
76
|
|
61
77
|
private
|
62
78
|
|
63
|
-
# Convert 'es-ES' to 'ES'
|
64
|
-
def
|
79
|
+
# Convert 'es-ES' to 'ES', en-us to EN
|
80
|
+
def to_deepl_source_locale(locale)
|
65
81
|
locale.to_s.split('-', 2).first.upcase
|
66
82
|
end
|
67
83
|
|
84
|
+
# Convert 'es-ES' to 'ES' but warn about locales requiring a specific variant
|
85
|
+
def to_deepl_target_locale(locale)
|
86
|
+
loc, sub = locale.to_s.split('-')
|
87
|
+
if SPECIFIC_TARGETS.include?(loc)
|
88
|
+
# Must see how the deepl api evolves, so this could be an error in the future
|
89
|
+
warn_deprecated I18n.t('i18n_tasks.deepl_translate.errors.specific_target_missing') unless sub
|
90
|
+
locale.to_s.upcase
|
91
|
+
else
|
92
|
+
loc.upcase
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
68
96
|
def configure_api_key!
|
69
97
|
api_key = @i18n_tasks.translation_config[:deepl_api_key]
|
70
98
|
host = @i18n_tasks.translation_config[:deepl_host]
|
@@ -77,5 +105,26 @@ module I18n::Tasks::Translators
|
|
77
105
|
config.version = version unless version.blank?
|
78
106
|
end
|
79
107
|
end
|
108
|
+
|
109
|
+
def options_with_glossary(options, from, to)
|
110
|
+
glossary = find_glossary(from, to)
|
111
|
+
glossary ? { glossary_id: glossary.id }.merge(options) : options
|
112
|
+
end
|
113
|
+
|
114
|
+
def all_ready_glossaries
|
115
|
+
@all_ready_glossaries ||= DeepL.glossaries.list
|
116
|
+
end
|
117
|
+
|
118
|
+
def find_glossary(from, to)
|
119
|
+
config_glossary_ids = @i18n_tasks.translation_config[:deepl_glossary_ids]
|
120
|
+
return unless config_glossary_ids
|
121
|
+
|
122
|
+
all_ready_glossaries.find do |glossary|
|
123
|
+
glossary.ready \
|
124
|
+
&& glossary.source_lang == from \
|
125
|
+
&& glossary.target_lang == to \
|
126
|
+
&& config_glossary_ids.include?(glossary.id)
|
127
|
+
end
|
128
|
+
end
|
80
129
|
end
|
81
130
|
end
|
@@ -4,6 +4,7 @@ require 'i18n/tasks/translators/base_translator'
|
|
4
4
|
|
5
5
|
module I18n::Tasks::Translators
|
6
6
|
class GoogleTranslator < BaseTranslator
|
7
|
+
NEWLINE_PLACEHOLDER = '<br id=i18n />'
|
7
8
|
def initialize(*)
|
8
9
|
begin
|
9
10
|
require 'easy_translate'
|
@@ -16,14 +17,21 @@ module I18n::Tasks::Translators
|
|
16
17
|
protected
|
17
18
|
|
18
19
|
def translate_values(list, **options)
|
19
|
-
|
20
|
+
restore_newlines(
|
21
|
+
EasyTranslate.translate(
|
22
|
+
replace_newlines_with_placeholder(list, options[:html]),
|
23
|
+
options,
|
24
|
+
format: :text
|
25
|
+
),
|
26
|
+
options[:html]
|
27
|
+
)
|
20
28
|
end
|
21
29
|
|
22
30
|
def options_for_translate_values(from:, to:, **options)
|
23
31
|
options.merge(
|
24
32
|
api_key: api_key,
|
25
|
-
from:
|
26
|
-
to:
|
33
|
+
from: from,
|
34
|
+
to: to
|
27
35
|
)
|
28
36
|
end
|
29
37
|
|
@@ -41,21 +49,12 @@ module I18n::Tasks::Translators
|
|
41
49
|
|
42
50
|
private
|
43
51
|
|
44
|
-
SUPPORTED_LOCALES_WITH_REGION = %w[zh-CN zh-TW].freeze
|
45
|
-
|
46
|
-
# Convert 'es-ES' to 'es'
|
47
|
-
def to_google_translate_compatible_locale(locale)
|
48
|
-
return locale unless locale.include?('-') && !SUPPORTED_LOCALES_WITH_REGION.include?(locale)
|
49
|
-
|
50
|
-
locale.split('-', 2).first
|
51
|
-
end
|
52
|
-
|
53
52
|
def api_key
|
54
53
|
@api_key ||= begin
|
55
54
|
key = @i18n_tasks.translation_config[:google_translate_api_key]
|
56
55
|
# fallback with deprecation warning
|
57
56
|
if @i18n_tasks.translation_config[:api_key]
|
58
|
-
|
57
|
+
warn_deprecated(
|
59
58
|
'Please rename Google Translate API Key from `api_key` to `google_translate_api_key`.'
|
60
59
|
)
|
61
60
|
key ||= translation_config[:api_key]
|
@@ -65,5 +64,21 @@ module I18n::Tasks::Translators
|
|
65
64
|
key
|
66
65
|
end
|
67
66
|
end
|
67
|
+
|
68
|
+
def replace_newlines_with_placeholder(list, html)
|
69
|
+
return list unless html
|
70
|
+
|
71
|
+
list.map do |value|
|
72
|
+
value.gsub("\n", NEWLINE_PLACEHOLDER)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def restore_newlines(translations, html)
|
77
|
+
return translations unless html
|
78
|
+
|
79
|
+
translations.map do |translation|
|
80
|
+
translation.gsub("#{NEWLINE_PLACEHOLDER} ", "\n")
|
81
|
+
end
|
82
|
+
end
|
68
83
|
end
|
69
84
|
end
|