i18n-tasks 1.0.12 → 1.0.13
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 +25 -2
- data/Rakefile +1 -1
- data/config/locales/en.yml +8 -0
- data/config/locales/ru.yml +8 -0
- data/i18n-tasks.gemspec +5 -4
- data/lib/i18n/tasks/cli.rb +8 -8
- data/lib/i18n/tasks/command/commands/missing.rb +2 -2
- data/lib/i18n/tasks/configuration.rb +2 -1
- data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +7 -1
- data/lib/i18n/tasks/data/tree/traversal.rb +2 -2
- data/lib/i18n/tasks/key_pattern_matching.rb +2 -0
- data/lib/i18n/tasks/references.rb +3 -3
- data/lib/i18n/tasks/reports/base.rb +1 -1
- data/lib/i18n/tasks/reports/terminal.rb +1 -1
- data/lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb +4 -4
- data/lib/i18n/tasks/scanners/pattern_scanner.rb +1 -1
- data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +1 -0
- data/lib/i18n/tasks/translation.rb +4 -1
- data/lib/i18n/tasks/translators/base_translator.rb +16 -11
- data/lib/i18n/tasks/translators/deepl_translator.rb +32 -9
- data/lib/i18n/tasks/translators/google_translator.rb +1 -1
- data/lib/i18n/tasks/translators/openai_translator.rb +100 -0
- data/lib/i18n/tasks/used_keys.rb +3 -3
- data/lib/i18n/tasks/version.rb +1 -1
- data/templates/config/i18n-tasks.yml +6 -4
- data/templates/minitest/i18n_test.rb +6 -6
- metadata +38 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12e19e4d7fe61bd20e000b0b6e4db5bdf233443e8e8cf965b62e634bdfa3e5a2
|
|
4
|
+
data.tar.gz: 3dab05338d079c4defad590466ab2985faf43245d616f86af4c212003831a6e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2cf99aec1c6d5738c4a71964a7ce67e70c8ade3150042c4e381fef19b9eaa82cf6638979bf97c6e9941f119304973f9e6f3fd31b3bab0fd635520344448b5587
|
|
7
|
+
data.tar.gz: 2de5ab47951585b298e43bece202f3ad885ae8eb03edb2b6e860b30e3172ea213947509b9543bbf31df06ef03079ff1e7cfd1b7a32cd964428d9f9612fd7e171
|
data/README.md
CHANGED
|
@@ -24,7 +24,7 @@ i18n-tasks can be used with any project using the ruby [i18n gem][i18n-gem] (def
|
|
|
24
24
|
Add i18n-tasks to the Gemfile:
|
|
25
25
|
|
|
26
26
|
```ruby
|
|
27
|
-
gem 'i18n-tasks', '~> 1.0.
|
|
27
|
+
gem 'i18n-tasks', '~> 1.0.13'
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Copy the default [configuration file](#configuration):
|
|
@@ -117,6 +117,17 @@ $ i18n-tasks translate-missing --backend=yandex
|
|
|
117
117
|
$ i18n-tasks translate-missing --from=en es fr
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
### OpenAI Translate missing keys
|
|
121
|
+
|
|
122
|
+
Translate missing values with OpenAI ([more below on the API key](#openai-translation-config)).
|
|
123
|
+
|
|
124
|
+
```console
|
|
125
|
+
$ i18n-tasks translate-missing --backend=openai
|
|
126
|
+
|
|
127
|
+
# accepts from and locales options:
|
|
128
|
+
$ i18n-tasks translate-missing --from=en es fr
|
|
129
|
+
```
|
|
130
|
+
|
|
120
131
|
### Find usages
|
|
121
132
|
|
|
122
133
|
See where the keys are used with `i18n-tasks find`:
|
|
@@ -335,6 +346,7 @@ A special syntax similar to file glob patterns is used throughout i18n-tasks to
|
|
|
335
346
|
|:------------:|:----------------------------------------------------------|
|
|
336
347
|
| `*` | matches everything |
|
|
337
348
|
| `:` | matches a single key |
|
|
349
|
+
| `*:` | matches part of a single key |
|
|
338
350
|
| `{a, b.c}` | match any in set, can use `:` and `*`, match is captured |
|
|
339
351
|
|
|
340
352
|
Example of usage:
|
|
@@ -423,6 +435,17 @@ translation:
|
|
|
423
435
|
yandex_api_key: <Yandex API key>
|
|
424
436
|
```
|
|
425
437
|
|
|
438
|
+
<a name="openai-translation-config"></a>
|
|
439
|
+
### OpenAI Translate
|
|
440
|
+
|
|
441
|
+
`i18n-tasks translate-missing` requires a OpenAI API key, get it at [OpenAI](https://openai.com/).
|
|
442
|
+
|
|
443
|
+
```yaml
|
|
444
|
+
# config/i18n-tasks.yml
|
|
445
|
+
translation:
|
|
446
|
+
openai_api_key: <OpenAI API key>
|
|
447
|
+
```
|
|
448
|
+
|
|
426
449
|
## Interactive console
|
|
427
450
|
|
|
428
451
|
`i18n-tasks irb` starts an IRB session in i18n-tasks context. Type `guide` for more information.
|
|
@@ -440,7 +463,7 @@ Custom tasks can be added easily, see the examples [on the wiki](https://github.
|
|
|
440
463
|
|
|
441
464
|
- Install dependencies using `bundle install`
|
|
442
465
|
- Run tests using `bundle exec rspec`
|
|
443
|
-
- Install [Overcommit](overcommit) by running `overcommit --install`
|
|
466
|
+
- Install [Overcommit](https://github.com/sds/overcommit) by running `overcommit --install`
|
|
444
467
|
|
|
445
468
|
## Skip Overcommit-hooks
|
|
446
469
|
|
data/Rakefile
CHANGED
data/config/locales/en.yml
CHANGED
|
@@ -96,6 +96,8 @@ en:
|
|
|
96
96
|
Setup DeepL Pro API key via DEEPL_AUTH_KEY environment variable or translation.deepl_api_key
|
|
97
97
|
in config/i18n-tasks.yml. Get the key at https://www.deepl.com/pro.
|
|
98
98
|
no_results: DeepL returned no results.
|
|
99
|
+
specific_target_missing: You must supply a specific variant for the given target language
|
|
100
|
+
e.g. en-us instead of en.
|
|
99
101
|
google_translate:
|
|
100
102
|
errors:
|
|
101
103
|
no_api_key: >-
|
|
@@ -110,6 +112,12 @@ en:
|
|
|
110
112
|
missing:
|
|
111
113
|
details_title: Value in other locales or source
|
|
112
114
|
none: No translations are missing.
|
|
115
|
+
openai_translate:
|
|
116
|
+
errors:
|
|
117
|
+
no_api_key: >-
|
|
118
|
+
Set OpenAI API key via OPENAI_API_KEY environment variable or translation.openai_api_key
|
|
119
|
+
in config/i18n-tasks.yml. Get the key at https://openai.com/.
|
|
120
|
+
no_results: OpenAI returned no results.
|
|
113
121
|
remove_unused:
|
|
114
122
|
confirm:
|
|
115
123
|
one: "%{count} translation will be removed from %{locales}."
|
data/config/locales/ru.yml
CHANGED
|
@@ -94,6 +94,8 @@ ru:
|
|
|
94
94
|
Задайте ключ API DeepL через переменную окружения DEEPL_AUTH_KEY или translation.deepl_api_key
|
|
95
95
|
Получите ключ через https://www.deepl.com/pro.
|
|
96
96
|
no_results: DeepL не дал результатов.
|
|
97
|
+
specific_target_missing: You must supply a specific variant for the given target language
|
|
98
|
+
e.g. en-us instead of en.
|
|
97
99
|
google_translate:
|
|
98
100
|
errors:
|
|
99
101
|
no_api_key: >-
|
|
@@ -109,6 +111,12 @@ ru:
|
|
|
109
111
|
missing:
|
|
110
112
|
details_title: На других языках или в коде
|
|
111
113
|
none: Всё переведено.
|
|
114
|
+
openai_translate:
|
|
115
|
+
errors:
|
|
116
|
+
no_api_key: |-
|
|
117
|
+
Установить ключ API Яндекса с помощью переменной среды OPENAI_API_KEY или translation.openai_api_key
|
|
118
|
+
в config / i18n-tasks.yml. Получите ключ по адресу https://openai.com/.
|
|
119
|
+
no_results: Яндекс не дал результатов.
|
|
112
120
|
remove_unused:
|
|
113
121
|
confirm:
|
|
114
122
|
few: Переводы (%{count}) будут удалены из %{locales}.
|
data/i18n-tasks.gemspec
CHANGED
|
@@ -4,7 +4,7 @@ lib = File.expand_path('lib', __dir__)
|
|
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
5
|
require 'i18n/tasks/version'
|
|
6
6
|
|
|
7
|
-
Gem::Specification.new do |s|
|
|
7
|
+
Gem::Specification.new do |s|
|
|
8
8
|
s.name = 'i18n-tasks'
|
|
9
9
|
s.version = I18n::Tasks::VERSION
|
|
10
10
|
s.authors = ['glebm']
|
|
@@ -35,7 +35,6 @@ Gem::Specification.new do |s| # rubocop:disable Metrics/BlockLength
|
|
|
35
35
|
s.files = `git ls-files`.split($/)
|
|
36
36
|
s.files -= s.files.grep(%r{^(doc/|\.|spec/)}) + %w[CHANGES.md config/i18n-tasks.yml Gemfile]
|
|
37
37
|
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } - %w[i18n-tasks.cmd]
|
|
38
|
-
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
|
39
38
|
s.require_paths = ['lib']
|
|
40
39
|
|
|
41
40
|
s.add_dependency 'activesupport', '>= 4.0.2'
|
|
@@ -44,7 +43,7 @@ Gem::Specification.new do |s| # rubocop:disable Metrics/BlockLength
|
|
|
44
43
|
s.add_dependency 'erubi'
|
|
45
44
|
s.add_dependency 'highline', '>= 2.0.0'
|
|
46
45
|
s.add_dependency 'i18n'
|
|
47
|
-
s.add_dependency 'parser', '>= 2.2.
|
|
46
|
+
s.add_dependency 'parser', '>= 3.2.2.1'
|
|
48
47
|
s.add_dependency 'rails-i18n'
|
|
49
48
|
s.add_dependency 'rainbow', '>= 2.2.2', '< 4.0'
|
|
50
49
|
s.add_dependency 'terminal-table', '>= 1.5.1'
|
|
@@ -52,7 +51,9 @@ Gem::Specification.new do |s| # rubocop:disable Metrics/BlockLength
|
|
|
52
51
|
s.add_development_dependency 'overcommit', '~> 0.58.0'
|
|
53
52
|
s.add_development_dependency 'rake'
|
|
54
53
|
s.add_development_dependency 'rspec', '~> 3.3'
|
|
55
|
-
s.add_development_dependency 'rubocop', '~> 1.
|
|
54
|
+
s.add_development_dependency 'rubocop', '~> 1.50.1'
|
|
55
|
+
s.add_development_dependency 'rubocop-rake', '~> 0.6.0'
|
|
56
|
+
s.add_development_dependency 'rubocop-rspec', '~> 2.19.0'
|
|
56
57
|
s.add_development_dependency 'simplecov'
|
|
57
58
|
s.add_development_dependency 'yard'
|
|
58
59
|
|
data/lib/i18n/tasks/cli.rb
CHANGED
|
@@ -35,14 +35,14 @@ class I18n::Tasks::CLI
|
|
|
35
35
|
|
|
36
36
|
def run(argv)
|
|
37
37
|
argv.each_with_index do |arg, i|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
next unless ['--config', '-c'].include?(arg)
|
|
39
|
+
|
|
40
|
+
_, config_file = argv.slice!(i, 2)
|
|
41
|
+
if File.exist?(config_file)
|
|
42
|
+
@config_file = config_file
|
|
43
|
+
break
|
|
44
|
+
else
|
|
45
|
+
error "Config file doesn't exist: #{config_file}", 128
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -43,7 +43,7 @@ module I18n::Tasks
|
|
|
43
43
|
args: [:locales, :locale_to_translate_from, arg(:out_format).from(1), :translation_backend, :pattern]
|
|
44
44
|
|
|
45
45
|
def translate_missing(opt = {})
|
|
46
|
-
missing
|
|
46
|
+
missing = i18n.missing_diff_forest opt[:locales], opt[:from]
|
|
47
47
|
if opt[:pattern]
|
|
48
48
|
pattern_re = i18n.compile_key_pattern(opt[:pattern])
|
|
49
49
|
missing.select_keys! { |full_key, _node| full_key =~ pattern_re }
|
|
@@ -61,7 +61,7 @@ module I18n::Tasks
|
|
|
61
61
|
['--nil-value', 'Set value to nil. Takes precedence over the value argument.']]
|
|
62
62
|
|
|
63
63
|
# Merge base locale first, as this may affect the value for the other locales
|
|
64
|
-
def add_missing(opt = {})
|
|
64
|
+
def add_missing(opt = {}) # rubocop:disable Metrics/AbcSize
|
|
65
65
|
[
|
|
66
66
|
[i18n.base_locale] & opt[:locales],
|
|
67
67
|
opt[:locales] - [i18n.base_locale]
|
|
@@ -66,6 +66,7 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
|
|
|
66
66
|
conf[:deepl_api_key] = ENV['DEEPL_AUTH_KEY'] if ENV.key?('DEEPL_AUTH_KEY')
|
|
67
67
|
conf[:deepl_host] = ENV['DEEPL_HOST'] if ENV.key?('DEEPL_HOST')
|
|
68
68
|
conf[:deepl_version] = ENV['DEEPL_VERSION'] if ENV.key?('DEEPL_VERSION')
|
|
69
|
+
conf[:openai_api_key] = ENV['OPENAI_API_KEY'] if ENV.key?('OPENAI_API_KEY')
|
|
69
70
|
conf[:yandex_api_key] = ENV['YANDEX_API_KEY'] if ENV.key?('YANDEX_API_KEY')
|
|
70
71
|
conf
|
|
71
72
|
end
|
|
@@ -87,7 +88,7 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
|
|
|
87
88
|
valid_locales = Dir[File.join(I18n::Tasks.gem_path, 'config', 'locales', '*.yml')]
|
|
88
89
|
.map { |f| File.basename(f, '.yml') }
|
|
89
90
|
unless valid_locales.include?(internal_locale)
|
|
90
|
-
log_warn "invalid internal_locale #{internal_locale.inspect}. "\
|
|
91
|
+
log_warn "invalid internal_locale #{internal_locale.inspect}. " \
|
|
91
92
|
"Available internal locales: #{valid_locales * ', '}."
|
|
92
93
|
internal_locale = DEFAULTS[:internal_locale].to_s
|
|
93
94
|
end
|
|
@@ -6,6 +6,7 @@ module I18n::Tasks
|
|
|
6
6
|
module Adapter
|
|
7
7
|
module YamlAdapter
|
|
8
8
|
EMOJI_REGEX = /\\u[\da-f]{8}/i.freeze
|
|
9
|
+
TRAILING_SPACE_REGEX = / $/.freeze
|
|
9
10
|
|
|
10
11
|
class << self
|
|
11
12
|
# @return [Hash] locale tree
|
|
@@ -20,13 +21,18 @@ module I18n::Tasks
|
|
|
20
21
|
|
|
21
22
|
# @return [String]
|
|
22
23
|
def dump(tree, options)
|
|
23
|
-
restore_emojis(tree.to_yaml(options || {}))
|
|
24
|
+
strip_trailing_spaces(restore_emojis(tree.to_yaml(options || {})))
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
# @return [String]
|
|
27
28
|
def restore_emojis(yaml)
|
|
28
29
|
yaml.gsub(EMOJI_REGEX) { |m| [m[-8..].to_i(16)].pack('U') }
|
|
29
30
|
end
|
|
31
|
+
|
|
32
|
+
# @return [String]
|
|
33
|
+
def strip_trailing_spaces(yaml)
|
|
34
|
+
yaml.gsub(TRAILING_SPACE_REGEX, '')
|
|
35
|
+
end
|
|
30
36
|
end
|
|
31
37
|
end
|
|
32
38
|
end
|
|
@@ -161,12 +161,12 @@ module I18n::Tasks
|
|
|
161
161
|
end
|
|
162
162
|
|
|
163
163
|
def grep_keys(match, opts = {})
|
|
164
|
-
select_keys(opts) do |full_key, _node|
|
|
164
|
+
select_keys(**opts) do |full_key, _node|
|
|
165
165
|
match === full_key # rubocop:disable Style/CaseEquality
|
|
166
166
|
end
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
-
def set_each_value!(val_pattern, key_pattern = nil, &value_proc)
|
|
169
|
+
def set_each_value!(val_pattern, key_pattern = nil, &value_proc) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
170
170
|
value_proc ||= proc do |node|
|
|
171
171
|
node_value = node.value
|
|
172
172
|
next node_value if node.reference?
|
|
@@ -21,6 +21,7 @@ module I18n::Tasks::KeyPatternMatching
|
|
|
21
21
|
# In patterns:
|
|
22
22
|
# * is like .* in regexs
|
|
23
23
|
# : matches a single key
|
|
24
|
+
# *: matches part of a single key, equivalent to `[^.]+?` regex
|
|
24
25
|
# { a, b.c } match any in set, can use : and *, match is captured
|
|
25
26
|
def compile_key_pattern(key_pattern)
|
|
26
27
|
return key_pattern if key_pattern.is_a?(Regexp)
|
|
@@ -31,6 +32,7 @@ module I18n::Tasks::KeyPatternMatching
|
|
|
31
32
|
def key_pattern_re_body(key_pattern)
|
|
32
33
|
key_pattern
|
|
33
34
|
.gsub(/\./, '\.')
|
|
35
|
+
.gsub(/\*:/, '[^.]+?')
|
|
34
36
|
.gsub(/\*/, '.*')
|
|
35
37
|
.gsub(/:/, '(?<=^|\.)[^.]+?(?=\.|$)')
|
|
36
38
|
.gsub(/\{(.*?)}/) { "(#{Regexp.last_match(1).strip.gsub(/\s*,\s*/, '|')})" }
|
|
@@ -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
|
|
|
@@ -112,7 +112,7 @@ module I18n
|
|
|
112
112
|
when :missing_plural
|
|
113
113
|
leaf[:data][:missing_keys].join(', ')
|
|
114
114
|
else
|
|
115
|
-
"#{Rainbow(leaf[:data][:missing_diff_locale]).cyan} "\
|
|
115
|
+
"#{Rainbow(leaf[:data][:missing_diff_locale]).cyan} " \
|
|
116
116
|
"#{format_value(leaf[:value].is_a?(String) ? leaf[:value].strip : leaf[:value])}"
|
|
117
117
|
end
|
|
118
118
|
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]
|
|
@@ -12,7 +12,7 @@ module I18n::Tasks::Scanners
|
|
|
12
12
|
include OccurrenceFromPosition
|
|
13
13
|
include RubyKeyLiterals
|
|
14
14
|
|
|
15
|
-
TRANSLATE_CALL_RE = /(?<=^|[^\w'\-.]|[^\w'
|
|
15
|
+
TRANSLATE_CALL_RE = /(?<=^|[^\w'\-.]|[^\w'-]I18n\.|I18n\.)t(?:!|ranslate!?)?/.freeze
|
|
16
16
|
IGNORE_LINES = {
|
|
17
17
|
'coffee' => /^\s*#(?!\si18n-tasks-use)/,
|
|
18
18
|
'erb' => /^\s*<%\s*#(?!\si18n-tasks-use)/,
|
|
@@ -4,6 +4,7 @@ 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
6
|
require 'i18n/tasks/scanners/ast_matchers/message_receivers_matcher'
|
|
7
|
+
require 'i18n/tasks/scanners/ast_matchers/rails_model_matcher'
|
|
7
8
|
require 'parser/current'
|
|
8
9
|
|
|
9
10
|
module I18n::Tasks::Scanners
|
|
@@ -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,36 @@ 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) }
|
|
71
73
|
when String
|
|
74
|
+
value = CGI.escapeHTML(value) if opts[:html_escape]
|
|
72
75
|
replace_interpolations value unless value.empty?
|
|
73
76
|
end
|
|
74
77
|
end
|
|
@@ -77,16 +80,18 @@ module I18n::Tasks
|
|
|
77
80
|
# @param [Object] untranslated
|
|
78
81
|
# @param [Enumerator] each_translated
|
|
79
82
|
# @return [Object] final translated value
|
|
80
|
-
def parse_value(untranslated, each_translated)
|
|
83
|
+
def parse_value(untranslated, each_translated, opts)
|
|
81
84
|
case untranslated
|
|
82
85
|
when Array
|
|
83
86
|
# implode array
|
|
84
|
-
untranslated.map { |from| parse_value(from, each_translated) }
|
|
87
|
+
untranslated.map { |from| parse_value(from, each_translated, opts) }
|
|
85
88
|
when String
|
|
86
89
|
if untranslated.empty?
|
|
87
90
|
untranslated
|
|
88
91
|
else
|
|
89
|
-
|
|
92
|
+
value = each_translated.next
|
|
93
|
+
value = CGI.unescapeHTML(value) if opts[:html_escape]
|
|
94
|
+
restore_interpolations(untranslated, value)
|
|
90
95
|
end
|
|
91
96
|
else
|
|
92
97
|
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,22 @@ 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(parts, to_deepl_source_locale(from), to_deepl_target_locale(to), options)
|
|
28
|
+
if res.is_a?(DeepL::Resources::Text)
|
|
29
|
+
results << res.text
|
|
30
|
+
else
|
|
31
|
+
results += res.map(&:text)
|
|
32
|
+
end
|
|
25
33
|
end
|
|
34
|
+
results
|
|
26
35
|
end
|
|
27
36
|
|
|
28
37
|
def options_for_translate_values(**options)
|
|
29
|
-
|
|
38
|
+
extra_options = @i18n_tasks.translation_config[:deepl_options]&.symbolize_keys || {}
|
|
39
|
+
|
|
40
|
+
extra_options.merge({ ignore_tags: %w[i18n] }).merge(options)
|
|
30
41
|
end
|
|
31
42
|
|
|
32
43
|
def options_for_html
|
|
@@ -34,7 +45,7 @@ module I18n::Tasks::Translators
|
|
|
34
45
|
end
|
|
35
46
|
|
|
36
47
|
def options_for_plain
|
|
37
|
-
{ preserve_formatting: true }
|
|
48
|
+
{ preserve_formatting: true, tag_handling: 'xml', html_escape: true }
|
|
38
49
|
end
|
|
39
50
|
|
|
40
51
|
# @param [String] value
|
|
@@ -60,11 +71,23 @@ module I18n::Tasks::Translators
|
|
|
60
71
|
|
|
61
72
|
private
|
|
62
73
|
|
|
63
|
-
# Convert 'es-ES' to 'ES'
|
|
64
|
-
def
|
|
74
|
+
# Convert 'es-ES' to 'ES', en-us to EN
|
|
75
|
+
def to_deepl_source_locale(locale)
|
|
65
76
|
locale.to_s.split('-', 2).first.upcase
|
|
66
77
|
end
|
|
67
78
|
|
|
79
|
+
# Convert 'es-ES' to 'ES' but warn about locales requiring a specific variant
|
|
80
|
+
def to_deepl_target_locale(locale)
|
|
81
|
+
loc, sub = locale.to_s.split('-')
|
|
82
|
+
if SPECIFIC_TARGETS.include?(loc)
|
|
83
|
+
# Must see how the deepl api evolves, so this could be an error in the future
|
|
84
|
+
warn_deprecated I18n.t('i18n_tasks.deepl_translate.errors.specific_target_missing') unless sub
|
|
85
|
+
locale.to_s.upcase
|
|
86
|
+
else
|
|
87
|
+
loc.upcase
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
68
91
|
def configure_api_key!
|
|
69
92
|
api_key = @i18n_tasks.translation_config[:deepl_api_key]
|
|
70
93
|
host = @i18n_tasks.translation_config[:deepl_host]
|
|
@@ -55,7 +55,7 @@ module I18n::Tasks::Translators
|
|
|
55
55
|
key = @i18n_tasks.translation_config[:google_translate_api_key]
|
|
56
56
|
# fallback with deprecation warning
|
|
57
57
|
if @i18n_tasks.translation_config[:api_key]
|
|
58
|
-
|
|
58
|
+
warn_deprecated(
|
|
59
59
|
'Please rename Google Translate API Key from `api_key` to `google_translate_api_key`.'
|
|
60
60
|
)
|
|
61
61
|
key ||= translation_config[:api_key]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'i18n/tasks/translators/base_translator'
|
|
4
|
+
|
|
5
|
+
module I18n::Tasks::Translators
|
|
6
|
+
class OpenAiTranslator < BaseTranslator
|
|
7
|
+
# max allowed texts per request
|
|
8
|
+
BATCH_SIZE = 50
|
|
9
|
+
|
|
10
|
+
def initialize(*)
|
|
11
|
+
begin
|
|
12
|
+
require 'openai'
|
|
13
|
+
rescue LoadError
|
|
14
|
+
raise ::I18n::Tasks::CommandError, "Add gem 'ruby-openai' to your Gemfile to use this command"
|
|
15
|
+
end
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def options_for_translate_values(from:, to:, **options)
|
|
20
|
+
options.merge(
|
|
21
|
+
from: from,
|
|
22
|
+
to: to
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def options_for_html
|
|
27
|
+
{}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def options_for_plain
|
|
31
|
+
{}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def no_results_error_message
|
|
35
|
+
I18n.t('i18n_tasks.openai_translate.errors.no_results')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def translator
|
|
41
|
+
@translator ||= OpenAI::Client.new(access_token: api_key)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def api_key
|
|
45
|
+
@api_key ||= begin
|
|
46
|
+
key = @i18n_tasks.translation_config[:openai_api_key]
|
|
47
|
+
fail ::I18n::Tasks::CommandError, I18n.t('i18n_tasks.openai_translate.errors.no_api_key') if key.blank?
|
|
48
|
+
|
|
49
|
+
key
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def translate_values(list, from:, to:)
|
|
54
|
+
results = []
|
|
55
|
+
|
|
56
|
+
list.each_slice(BATCH_SIZE) do |batch|
|
|
57
|
+
translations = translate(batch, from, to)
|
|
58
|
+
|
|
59
|
+
results << JSON.parse(translations)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
results.flatten
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def translate(values, from, to) # rubocop:disable Metrics/MethodLength
|
|
66
|
+
messages = [
|
|
67
|
+
{
|
|
68
|
+
role: 'system',
|
|
69
|
+
content: "You are a helpful assistant that translates content from the #{from} to #{to} locale in an i18n
|
|
70
|
+
locale array. The array has a structured format and contains multiple strings. Your task is to translate
|
|
71
|
+
each of these strings and create a new array with the translated strings. Keep in mind the context of all
|
|
72
|
+
the strings for a more accurate translation.\n"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
role: 'user',
|
|
76
|
+
content: "Translate this array: \n\n\n"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
role: 'user',
|
|
80
|
+
content: values.to_json
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
response = translator.chat(
|
|
85
|
+
parameters: {
|
|
86
|
+
model: 'gpt-3.5-turbo',
|
|
87
|
+
messages: messages,
|
|
88
|
+
temperature: 0.7
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
translations = response.dig('choices', 0, 'message', 'content')
|
|
93
|
+
error = response['error']
|
|
94
|
+
|
|
95
|
+
fail "AI error: #{error}" if error.present?
|
|
96
|
+
|
|
97
|
+
translations
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/i18n/tasks/used_keys.rb
CHANGED
|
@@ -28,8 +28,8 @@ module I18n::Tasks
|
|
|
28
28
|
strict: true
|
|
29
29
|
}.freeze
|
|
30
30
|
|
|
31
|
-
ALWAYS_EXCLUDE = %w[*.jpg *.jpeg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf *.css *.sass *.scss
|
|
32
|
-
*.yml *.json *.zip *.tar.gz *.swf *.flv *.mp3 *.wav *.flac *.webm *.mp4 *.ogg *.opus
|
|
31
|
+
ALWAYS_EXCLUDE = %w[*.jpg *.jpeg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf *.css *.sass *.scss
|
|
32
|
+
*.less *.yml *.json *.zip *.tar.gz *.swf *.flv *.mp3 *.wav *.flac *.webm *.mp4 *.ogg *.opus
|
|
33
33
|
*.webp *.map *.xlsx].freeze
|
|
34
34
|
|
|
35
35
|
# Find all keys in the source and return a forest with the keys in absolute form and their occurrences.
|
|
@@ -142,7 +142,7 @@ module I18n::Tasks
|
|
|
142
142
|
|
|
143
143
|
# keys in the source that end with a ., e.g. t("category.#{ cat.i18n_key }") or t("category." + category.key)
|
|
144
144
|
# @param [String] replacement for interpolated values.
|
|
145
|
-
def expr_key_re(replacement: '
|
|
145
|
+
def expr_key_re(replacement: '*:')
|
|
146
146
|
@expr_key_re ||= begin
|
|
147
147
|
# disallow patterns with no keys
|
|
148
148
|
ignore_pattern_re = /\A[.#{replacement}]*\z/
|
data/lib/i18n/tasks/version.rb
CHANGED
|
@@ -13,7 +13,7 @@ data:
|
|
|
13
13
|
## Provide a custom adapter:
|
|
14
14
|
# adapter: I18n::Tasks::Data::FileSystem
|
|
15
15
|
|
|
16
|
-
# Locale files or `
|
|
16
|
+
# Locale files or `Find.find` patterns where translations are read from:
|
|
17
17
|
read:
|
|
18
18
|
## Default:
|
|
19
19
|
# - config/locales/%{locale}.yml
|
|
@@ -52,7 +52,7 @@ data:
|
|
|
52
52
|
|
|
53
53
|
# Find translate calls
|
|
54
54
|
search:
|
|
55
|
-
## Paths or `
|
|
55
|
+
## Paths or `Find.find` patterns to search in:
|
|
56
56
|
# paths:
|
|
57
57
|
# - app/
|
|
58
58
|
|
|
@@ -94,7 +94,7 @@ search:
|
|
|
94
94
|
## User.human_attribute_name(:email) and User.model_name.human
|
|
95
95
|
##
|
|
96
96
|
## To implement your own, please see `I18n::Tasks::Scanners::AstMatchers::BaseMatcher`.
|
|
97
|
-
<%# I18n::Tasks.add_ast_matcher('I18n::Tasks::Scanners::AstMatchers::RailsModelMatcher') %>
|
|
97
|
+
# <%# I18n::Tasks.add_ast_matcher('I18n::Tasks::Scanners::AstMatchers::RailsModelMatcher') %>
|
|
98
98
|
|
|
99
99
|
## Multiple scanners can be used. Their results are merged.
|
|
100
100
|
## The options specified above are passed down to each scanner. Per-scanner options can be specified as well.
|
|
@@ -110,7 +110,9 @@ search:
|
|
|
110
110
|
# deepl_api_key: "48E92789-57A3-466A-9959-1A1A1A1A1A1A"
|
|
111
111
|
# # deepl_host: "https://api.deepl.com"
|
|
112
112
|
# # deepl_version: "v2"
|
|
113
|
-
|
|
113
|
+
# # add additional options to the DeepL.translate call: https://www.deepl.com/docs-api/translate-text/translate-text/
|
|
114
|
+
# deepl_options:
|
|
115
|
+
# formality: prefer_less
|
|
114
116
|
## Do not consider these keys missing:
|
|
115
117
|
# ignore_missing:
|
|
116
118
|
# - 'errors.messages.{accepted,blank,invalid,too_short,too_long}'
|
|
@@ -5,18 +5,18 @@ require 'i18n/tasks'
|
|
|
5
5
|
class I18nTest < ActiveSupport::TestCase
|
|
6
6
|
def setup
|
|
7
7
|
@i18n = I18n::Tasks::BaseTask.new
|
|
8
|
-
@missing_keys = @i18n.missing_keys
|
|
9
|
-
@unused_keys = @i18n.unused_keys
|
|
10
8
|
end
|
|
11
9
|
|
|
12
10
|
def test_no_missing_keys
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
missing_keys = @i18n.missing_keys
|
|
12
|
+
assert_empty missing_keys,
|
|
13
|
+
"Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them"
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
def test_no_unused_keys
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
unused_keys = @i18n.unused_keys
|
|
18
|
+
assert_empty unused_keys,
|
|
19
|
+
"#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them"
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def test_files_are_normalized
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: i18n-tasks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- glebm
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2023-10-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -106,14 +106,14 @@ dependencies:
|
|
|
106
106
|
requirements:
|
|
107
107
|
- - ">="
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
|
-
version: 2.2.
|
|
109
|
+
version: 3.2.2.1
|
|
110
110
|
type: :runtime
|
|
111
111
|
prerelease: false
|
|
112
112
|
version_requirements: !ruby/object:Gem::Requirement
|
|
113
113
|
requirements:
|
|
114
114
|
- - ">="
|
|
115
115
|
- !ruby/object:Gem::Version
|
|
116
|
-
version: 2.2.
|
|
116
|
+
version: 3.2.2.1
|
|
117
117
|
- !ruby/object:Gem::Dependency
|
|
118
118
|
name: rails-i18n
|
|
119
119
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -230,14 +230,42 @@ dependencies:
|
|
|
230
230
|
requirements:
|
|
231
231
|
- - "~>"
|
|
232
232
|
- !ruby/object:Gem::Version
|
|
233
|
-
version: 1.
|
|
233
|
+
version: 1.50.1
|
|
234
234
|
type: :development
|
|
235
235
|
prerelease: false
|
|
236
236
|
version_requirements: !ruby/object:Gem::Requirement
|
|
237
237
|
requirements:
|
|
238
238
|
- - "~>"
|
|
239
239
|
- !ruby/object:Gem::Version
|
|
240
|
-
version: 1.
|
|
240
|
+
version: 1.50.1
|
|
241
|
+
- !ruby/object:Gem::Dependency
|
|
242
|
+
name: rubocop-rake
|
|
243
|
+
requirement: !ruby/object:Gem::Requirement
|
|
244
|
+
requirements:
|
|
245
|
+
- - "~>"
|
|
246
|
+
- !ruby/object:Gem::Version
|
|
247
|
+
version: 0.6.0
|
|
248
|
+
type: :development
|
|
249
|
+
prerelease: false
|
|
250
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
251
|
+
requirements:
|
|
252
|
+
- - "~>"
|
|
253
|
+
- !ruby/object:Gem::Version
|
|
254
|
+
version: 0.6.0
|
|
255
|
+
- !ruby/object:Gem::Dependency
|
|
256
|
+
name: rubocop-rspec
|
|
257
|
+
requirement: !ruby/object:Gem::Requirement
|
|
258
|
+
requirements:
|
|
259
|
+
- - "~>"
|
|
260
|
+
- !ruby/object:Gem::Version
|
|
261
|
+
version: 2.19.0
|
|
262
|
+
type: :development
|
|
263
|
+
prerelease: false
|
|
264
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
265
|
+
requirements:
|
|
266
|
+
- - "~>"
|
|
267
|
+
- !ruby/object:Gem::Version
|
|
268
|
+
version: 2.19.0
|
|
241
269
|
- !ruby/object:Gem::Dependency
|
|
242
270
|
name: simplecov
|
|
243
271
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -408,6 +436,7 @@ files:
|
|
|
408
436
|
- lib/i18n/tasks/translators/base_translator.rb
|
|
409
437
|
- lib/i18n/tasks/translators/deepl_translator.rb
|
|
410
438
|
- lib/i18n/tasks/translators/google_translator.rb
|
|
439
|
+
- lib/i18n/tasks/translators/openai_translator.rb
|
|
411
440
|
- lib/i18n/tasks/translators/yandex_translator.rb
|
|
412
441
|
- lib/i18n/tasks/unused_keys.rb
|
|
413
442
|
- lib/i18n/tasks/used_keys.rb
|
|
@@ -445,8 +474,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
445
474
|
- !ruby/object:Gem::Version
|
|
446
475
|
version: '0'
|
|
447
476
|
requirements: []
|
|
448
|
-
rubygems_version: 3.
|
|
449
|
-
signing_key:
|
|
477
|
+
rubygems_version: 3.2.3
|
|
478
|
+
signing_key:
|
|
450
479
|
specification_version: 4
|
|
451
480
|
summary: Manage localization and translation with the awesome power of static analysis
|
|
452
481
|
test_files: []
|