i18n-tasks 1.0.11 → 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 +12 -3
- data/config/locales/ru.yml +8 -0
- data/i18n-tasks.gemspec +6 -5
- data/lib/i18n/tasks/cli.rb +8 -8
- data/lib/i18n/tasks/command/commands/missing.rb +17 -5
- 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 +25 -11
- data/lib/i18n/tasks/key_pattern_matching.rb +2 -0
- data/lib/i18n/tasks/locale_pathname.rb +1 -1
- data/lib/i18n/tasks/plural_keys.rb +0 -6
- 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/base_matcher.rb +16 -0
- data/lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb +7 -2
- 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 +43 -8
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
@@ -33,7 +33,7 @@ en:
|
|
33
33
|
Value. Interpolates: %{value}, %{human_key}, %{key}, %{default}, %{value_or_human_key},
|
34
34
|
%{value_or_default_or_human_key}
|
35
35
|
desc:
|
36
|
-
add_missing: add missing keys to locale data
|
36
|
+
add_missing: add missing keys to locale data, optionally match a pattern
|
37
37
|
check_consistent_interpolations: verify that all translations use correct interpolation variables
|
38
38
|
check_normalized: verify that all translation data is normalized
|
39
39
|
config: display i18n-tasks configuration
|
@@ -46,12 +46,13 @@ en:
|
|
46
46
|
gem_path: show path to the gem
|
47
47
|
health: is everything OK?
|
48
48
|
irb: start REPL session within i18n-tasks context
|
49
|
-
missing: show missing translations
|
49
|
+
missing: show missing translations, optionally match a pattern
|
50
50
|
mv: rename/merge the keys in locale data that match the given pattern
|
51
51
|
normalize: 'normalize translation data: sort and move to the right files'
|
52
52
|
remove_unused: remove unused keys
|
53
53
|
rm: remove the keys in locale data that match the given pattern
|
54
|
-
translate_missing: translate missing keys with Google Translate or DeepL Pro
|
54
|
+
translate_missing: translate missing keys with Google Translate or DeepL Pro, optionally match
|
55
|
+
a pattern
|
55
56
|
tree_convert: convert tree between formats
|
56
57
|
tree_filter: filter tree by key pattern
|
57
58
|
tree_merge: merge trees
|
@@ -95,6 +96,8 @@ en:
|
|
95
96
|
Setup DeepL Pro API key via DEEPL_AUTH_KEY environment variable or translation.deepl_api_key
|
96
97
|
in config/i18n-tasks.yml. Get the key at https://www.deepl.com/pro.
|
97
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.
|
98
101
|
google_translate:
|
99
102
|
errors:
|
100
103
|
no_api_key: >-
|
@@ -109,6 +112,12 @@ en:
|
|
109
112
|
missing:
|
110
113
|
details_title: Value in other locales or source
|
111
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.
|
112
121
|
remove_unused:
|
113
122
|
confirm:
|
114
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,16 +35,15 @@ 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'
|
42
41
|
s.add_dependency 'ast', '>= 2.1.0'
|
43
|
-
s.add_dependency 'better_html', '
|
42
|
+
s.add_dependency 'better_html', '>= 1.0', '< 3.0'
|
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
|
|
@@ -25,10 +25,14 @@ module I18n::Tasks
|
|
25
25
|
cmd :missing,
|
26
26
|
pos: '[locale ...]',
|
27
27
|
desc: t('i18n_tasks.cmd.desc.missing'),
|
28
|
-
args: %i[locales out_format missing_types]
|
28
|
+
args: %i[locales out_format missing_types pattern]
|
29
29
|
|
30
30
|
def missing(opt = {})
|
31
31
|
forest = i18n.missing_keys(**opt.slice(:locales, :base_locale, :types))
|
32
|
+
if opt[:pattern]
|
33
|
+
pattern_re = i18n.compile_key_pattern(opt[:pattern])
|
34
|
+
forest.select_keys! { |full_key, _node| full_key =~ pattern_re }
|
35
|
+
end
|
32
36
|
print_forest forest, opt, :missing_keys
|
33
37
|
:exit1 unless forest.empty?
|
34
38
|
end
|
@@ -36,10 +40,14 @@ module I18n::Tasks
|
|
36
40
|
cmd :translate_missing,
|
37
41
|
pos: '[locale ...]',
|
38
42
|
desc: t('i18n_tasks.cmd.desc.translate_missing'),
|
39
|
-
args: [:locales, :locale_to_translate_from, arg(:out_format).from(1), :translation_backend]
|
43
|
+
args: [:locales, :locale_to_translate_from, arg(:out_format).from(1), :translation_backend, :pattern]
|
40
44
|
|
41
45
|
def translate_missing(opt = {})
|
42
|
-
missing
|
46
|
+
missing = i18n.missing_diff_forest opt[:locales], opt[:from]
|
47
|
+
if opt[:pattern]
|
48
|
+
pattern_re = i18n.compile_key_pattern(opt[:pattern])
|
49
|
+
missing.select_keys! { |full_key, _node| full_key =~ pattern_re }
|
50
|
+
end
|
43
51
|
translated = i18n.translate_forest missing, from: opt[:from], backend: opt[:backend].to_sym
|
44
52
|
i18n.data.merge! translated
|
45
53
|
log_stderr t('i18n_tasks.translate_missing.translated', count: translated.leaves.count)
|
@@ -49,17 +57,21 @@ module I18n::Tasks
|
|
49
57
|
cmd :add_missing,
|
50
58
|
pos: '[locale ...]',
|
51
59
|
desc: t('i18n_tasks.cmd.desc.add_missing'),
|
52
|
-
args: [:locales, :out_format, arg(:value) + [{ default: '%{value_or_default_or_human_key}' }],
|
60
|
+
args: [:locales, :out_format, :pattern, arg(:value) + [{ default: '%{value_or_default_or_human_key}' }],
|
53
61
|
['--nil-value', 'Set value to nil. Takes precedence over the value argument.']]
|
54
62
|
|
55
63
|
# Merge base locale first, as this may affect the value for the other locales
|
56
|
-
def add_missing(opt = {})
|
64
|
+
def add_missing(opt = {}) # rubocop:disable Metrics/AbcSize
|
57
65
|
[
|
58
66
|
[i18n.base_locale] & opt[:locales],
|
59
67
|
opt[:locales] - [i18n.base_locale]
|
60
68
|
].reject(&:empty?).each_with_object(i18n.empty_forest) do |locales, added|
|
61
69
|
forest = i18n.missing_keys(locales: locales, **opt.slice(:types, :base_locale))
|
62
70
|
.set_each_value!(opt[:'nil-value'] ? nil : opt[:value])
|
71
|
+
if opt[:pattern]
|
72
|
+
pattern_re = i18n.compile_key_pattern(opt[:pattern])
|
73
|
+
forest.select_keys! { |full_key, _node| full_key =~ pattern_re }
|
74
|
+
end
|
63
75
|
i18n.data.merge! forest
|
64
76
|
added.merge! forest
|
65
77
|
end.tap do |added|
|
@@ -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?
|
@@ -174,15 +174,29 @@ module I18n::Tasks
|
|
174
174
|
human_key = ActiveSupport::Inflector.humanize(node.key.to_s)
|
175
175
|
full_key = node.full_key
|
176
176
|
default = (node.data[:occurrences] || []).detect { |o| o.default_arg.presence }.try(:default_arg)
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
177
|
+
if default.is_a?(Hash)
|
178
|
+
default.each_with_object({}) do |(k, v), h|
|
179
|
+
h[k] = StringInterpolation.interpolate_soft(
|
180
|
+
val_pattern,
|
181
|
+
value: node_value,
|
182
|
+
human_key: human_key,
|
183
|
+
key: full_key,
|
184
|
+
default: v,
|
185
|
+
value_or_human_key: node_value.presence || human_key,
|
186
|
+
value_or_default_or_human_key: node_value.presence || v || human_key
|
187
|
+
)
|
188
|
+
end
|
189
|
+
else
|
190
|
+
StringInterpolation.interpolate_soft(
|
191
|
+
val_pattern,
|
192
|
+
value: node_value,
|
193
|
+
human_key: human_key,
|
194
|
+
key: full_key,
|
195
|
+
default: default,
|
196
|
+
value_or_human_key: node_value.presence || human_key,
|
197
|
+
value_or_default_or_human_key: node_value.presence || default || human_key
|
198
|
+
)
|
199
|
+
end
|
186
200
|
end
|
187
201
|
pattern_re = I18n::Tasks::KeyPatternMatching.compile_key_pattern(key_pattern) if key_pattern.present?
|
188
202
|
keys.each do |key, node|
|
@@ -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*/, '|')})" }
|
@@ -52,15 +52,9 @@ module I18n::Tasks::PluralKeys
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def plural_forms?(s)
|
55
|
-
return false if non_plural_other?(s)
|
56
|
-
|
57
55
|
s.present? && s.all? { |node| node.leaf? && plural_suffix?(node.key) }
|
58
56
|
end
|
59
57
|
|
60
|
-
def non_plural_other?(s)
|
61
|
-
s.size == 1 && s.first.leaf? && (!s.first.value.is_a?(String) || !s.first.value.include?('%{count}'))
|
62
|
-
end
|
63
|
-
|
64
58
|
def plural_suffix?(key)
|
65
59
|
PLURAL_KEY_SUFFIXES.include?(key)
|
66
60
|
end
|
@@ -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
|
@@ -54,6 +54,22 @@ module I18n::Tasks::Scanners::AstMatchers
|
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
57
|
+
# Extract the whole hash from a node of type `:hash`
|
58
|
+
#
|
59
|
+
# @param node [AST::Node] a node of type `:hash`.
|
60
|
+
# @return [Hash] the whole hash from the node
|
61
|
+
def extract_hash(node)
|
62
|
+
return {} if node.nil?
|
63
|
+
|
64
|
+
if node.type == :hash
|
65
|
+
node.children.each_with_object({}) do |pair, h|
|
66
|
+
key = pair.children[0].children[0].to_s
|
67
|
+
value = pair.children[1].children[0]
|
68
|
+
h[key] = value
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
57
73
|
# Extract a hash pair with a given literal key.
|
58
74
|
#
|
59
75
|
# @param node [AST::Node] a node of type `:hash`.
|
@@ -77,8 +77,13 @@ module I18n::Tasks::Scanners::AstMatchers
|
|
77
77
|
|
78
78
|
key = [scope, key].join('.') unless scope == ''
|
79
79
|
end
|
80
|
-
default_arg_node = extract_hash_pair(node, 'default')
|
81
|
-
|
80
|
+
if default_arg_node = extract_hash_pair(node, 'default')
|
81
|
+
default_arg = if default_arg_node.children[1]&.type == :hash
|
82
|
+
extract_hash(default_arg_node.children[1])
|
83
|
+
else
|
84
|
+
extract_string(default_arg_node.children[1])
|
85
|
+
end
|
86
|
+
end
|
82
87
|
|
83
88
|
[key, default_arg]
|
84
89
|
end
|
@@ -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
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
|
@@ -42,16 +42,22 @@ dependencies:
|
|
42
42
|
name: better_html
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '1.0'
|
48
|
+
- - "<"
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '3.0'
|
48
51
|
type: :runtime
|
49
52
|
prerelease: false
|
50
53
|
version_requirements: !ruby/object:Gem::Requirement
|
51
54
|
requirements:
|
52
|
-
- - "
|
55
|
+
- - ">="
|
53
56
|
- !ruby/object:Gem::Version
|
54
57
|
version: '1.0'
|
58
|
+
- - "<"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '3.0'
|
55
61
|
- !ruby/object:Gem::Dependency
|
56
62
|
name: erubi
|
57
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -100,14 +106,14 @@ dependencies:
|
|
100
106
|
requirements:
|
101
107
|
- - ">="
|
102
108
|
- !ruby/object:Gem::Version
|
103
|
-
version: 2.2.
|
109
|
+
version: 3.2.2.1
|
104
110
|
type: :runtime
|
105
111
|
prerelease: false
|
106
112
|
version_requirements: !ruby/object:Gem::Requirement
|
107
113
|
requirements:
|
108
114
|
- - ">="
|
109
115
|
- !ruby/object:Gem::Version
|
110
|
-
version: 2.2.
|
116
|
+
version: 3.2.2.1
|
111
117
|
- !ruby/object:Gem::Dependency
|
112
118
|
name: rails-i18n
|
113
119
|
requirement: !ruby/object:Gem::Requirement
|
@@ -224,14 +230,42 @@ dependencies:
|
|
224
230
|
requirements:
|
225
231
|
- - "~>"
|
226
232
|
- !ruby/object:Gem::Version
|
227
|
-
version: 1.
|
233
|
+
version: 1.50.1
|
234
|
+
type: :development
|
235
|
+
prerelease: false
|
236
|
+
version_requirements: !ruby/object:Gem::Requirement
|
237
|
+
requirements:
|
238
|
+
- - "~>"
|
239
|
+
- !ruby/object:Gem::Version
|
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
|
228
262
|
type: :development
|
229
263
|
prerelease: false
|
230
264
|
version_requirements: !ruby/object:Gem::Requirement
|
231
265
|
requirements:
|
232
266
|
- - "~>"
|
233
267
|
- !ruby/object:Gem::Version
|
234
|
-
version:
|
268
|
+
version: 2.19.0
|
235
269
|
- !ruby/object:Gem::Dependency
|
236
270
|
name: simplecov
|
237
271
|
requirement: !ruby/object:Gem::Requirement
|
@@ -402,6 +436,7 @@ files:
|
|
402
436
|
- lib/i18n/tasks/translators/base_translator.rb
|
403
437
|
- lib/i18n/tasks/translators/deepl_translator.rb
|
404
438
|
- lib/i18n/tasks/translators/google_translator.rb
|
439
|
+
- lib/i18n/tasks/translators/openai_translator.rb
|
405
440
|
- lib/i18n/tasks/translators/yandex_translator.rb
|
406
441
|
- lib/i18n/tasks/unused_keys.rb
|
407
442
|
- lib/i18n/tasks/used_keys.rb
|