-
Notifications
You must be signed in to change notification settings - Fork 264
/
Copy pathbase_translator.rb
156 lines (136 loc) · 5.27 KB
/
base_translator.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# frozen_string_literal: true
module I18n::Tasks
module Translators
class BaseTranslator
# @param [I18n::Tasks::BaseTask] i18n_tasks
def initialize(i18n_tasks)
@i18n_tasks = i18n_tasks
end
# @param [I18n::Tasks::Tree::Siblings] forest to translate to the locales of its root nodes
# @param [String] from locale
# @return [I18n::Tasks::Tree::Siblings] translated forest
def translate_forest(forest, from)
forest.inject @i18n_tasks.empty_forest do |result, root|
translated = translate_pairs(root.key_values(root: true), to: root.key, from: from)
result.merge! Data::Tree::Siblings.from_flat_pairs(translated)
end
end
protected
# @param [Array<[String, Object]>] list of key-value pairs
# @return [Array<[String, Object]>] translated list
def translate_pairs(list, opts)
return [] if list.empty?
opts = opts.dup
key_pos = list.each_with_index.inject({}) { |idx, ((k, _v), i)| idx.update(k => i) }
# copy reference keys as is, instead of translating
reference_key_vals = list.select { |_k, v| v.is_a? Symbol } || []
list -= reference_key_vals
result = list.group_by { |k_v| @i18n_tasks.html_key? k_v[0], opts[:from] }.map do |is_html, list_slice|
fetch_translations list_slice, opts.merge(is_html ? options_for_html : options_for_plain)
end.reduce(:+) || []
result.concat(reference_key_vals)
result.sort! { |a, b| key_pos[a[0]] <=> key_pos[b[0]] }
result
end
# @param [Array<[String, Object]>] list of key-value pairs
# @return [Array<[String, Object]>] translated list
def fetch_translations(list, opts)
from_values(list, translate_values(to_values(list), **options_for_translate_values(**opts))).tap do |result|
fail CommandError, no_results_error_message if result.blank?
end
end
# @param [Array<[String, Object]>] list of key-value pairs
# @return [Array<String>] values for translation extracted from list
def to_values(list)
list.map { |l| dump_value l[1] }.flatten.compact
end
# @param [Array<[String, Object]>] list
# @param [Array<String>] translated_values
# @return [Array<[String, Object]>] translated key-value pairs
def from_values(list, translated_values)
keys = list.map(&:first)
untranslated_values = list.map(&:last)
keys.zip parse_value(untranslated_values, translated_values.to_enum)
end
# Prepare value for translation.
# @return [String, Array<String, nil>, nil] value for Google Translate or nil for non-string values
def dump_value(value)
case value
when Array
# dump recursively
value.map { |v| dump_value v }
when String
replace_interpolations value unless value.empty?
end
end
# Parse translated value from the each_translated enumerator
# @param [Object] untranslated
# @param [Enumerator] each_translated
# @return [Object] final translated value
def parse_value(untranslated, each_translated)
case untranslated
when Array
# implode array
untranslated.map { |from| parse_value(from, each_translated) }
when String
if untranslated.empty?
untranslated
else
restore_interpolations untranslated, each_translated.next
end
else
untranslated
end
end
INTERPOLATION_KEY_RE = /%\{[^}]+}/.freeze
UNTRANSLATABLE_STRING = 'X__'
# @param [String] value
# @return [String] 'hello, %{name}' => 'hello, <round-trippable string>'
def replace_interpolations(value)
i = -1
value.gsub INTERPOLATION_KEY_RE do
i += 1
"#{UNTRANSLATABLE_STRING}#{i}"
end
end
# @param [String] untranslated
# @param [String] translated
# @return [String] 'hello, <round-trippable string>' => 'hello, %{name}'
def restore_interpolations(untranslated, translated)
return translated if untranslated !~ INTERPOLATION_KEY_RE
values = untranslated.scan(INTERPOLATION_KEY_RE)
translated.gsub(/#{Regexp.escape(UNTRANSLATABLE_STRING)}\d+/i) do |m|
values[m[UNTRANSLATABLE_STRING.length..].to_i]
end
rescue StandardError => e
raise_interpolation_error(untranslated, translated, e)
end
def raise_interpolation_error(untranslated, translated, e)
fail CommandError.new(e, <<~TEXT.strip)
Error when restoring interpolations:
original: "#{untranslated}"
response: "#{translated}"
error: #{e.message} (#{e.class.name})
TEXT
end
# @param [Array<String>] list
# @param [Hash] options
# @return [Array<String>]
# @abstract
def translate_values(list, **options); end
# @param [Hash] options
# @return [Hash]
# @abstract
def options_for_translate_values(options); end
# @return [Hash]
# @abstract
def options_for_html; end
# @return [Hash]
# @abstract
def options_for_plain; end
# @return [String]
# @abstract
def no_results_error_message; end
end
end
end