i18n-tasks 0.9.37 → 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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +43 -5
  3. data/Rakefile +1 -1
  4. data/config/locales/en.yml +12 -3
  5. data/config/locales/ru.yml +8 -0
  6. data/i18n-tasks.gemspec +14 -7
  7. data/lib/i18n/tasks/cli.rb +8 -7
  8. data/lib/i18n/tasks/command/commander.rb +1 -0
  9. data/lib/i18n/tasks/command/commands/missing.rb +17 -5
  10. data/lib/i18n/tasks/command/options/common.rb +0 -1
  11. data/lib/i18n/tasks/command/options/data.rb +1 -1
  12. data/lib/i18n/tasks/concurrent/cached_value.rb +0 -2
  13. data/lib/i18n/tasks/configuration.rb +13 -7
  14. data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +15 -2
  15. data/lib/i18n/tasks/data/file_formats.rb +1 -1
  16. data/lib/i18n/tasks/data/router/pattern_router.rb +1 -1
  17. data/lib/i18n/tasks/data/tree/node.rb +1 -1
  18. data/lib/i18n/tasks/data/tree/nodes.rb +5 -7
  19. data/lib/i18n/tasks/data/tree/siblings.rb +1 -2
  20. data/lib/i18n/tasks/data/tree/traversal.rb +25 -11
  21. data/lib/i18n/tasks/html_keys.rb +2 -2
  22. data/lib/i18n/tasks/interpolations.rb +7 -3
  23. data/lib/i18n/tasks/key_pattern_matching.rb +2 -0
  24. data/lib/i18n/tasks/locale_pathname.rb +1 -1
  25. data/lib/i18n/tasks/plural_keys.rb +0 -6
  26. data/lib/i18n/tasks/references.rb +3 -3
  27. data/lib/i18n/tasks/reports/base.rb +2 -2
  28. data/lib/i18n/tasks/reports/terminal.rb +3 -3
  29. data/lib/i18n/tasks/scanners/ast_matchers/base_matcher.rb +118 -0
  30. data/lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb +91 -0
  31. data/lib/i18n/tasks/scanners/ast_matchers/rails_model_matcher.rb +69 -0
  32. data/lib/i18n/tasks/scanners/erb_ast_processor.rb +74 -0
  33. data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +48 -0
  34. data/lib/i18n/tasks/scanners/local_ruby_parser.rb +85 -0
  35. data/lib/i18n/tasks/scanners/pattern_mapper.rb +1 -1
  36. data/lib/i18n/tasks/scanners/pattern_scanner.rb +2 -2
  37. data/lib/i18n/tasks/scanners/relative_keys.rb +2 -2
  38. data/lib/i18n/tasks/scanners/results/occurrence.rb +17 -1
  39. data/lib/i18n/tasks/scanners/ruby_ast_call_finder.rb +9 -34
  40. data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +91 -156
  41. data/lib/i18n/tasks/scanners/ruby_key_literals.rb +2 -2
  42. data/lib/i18n/tasks/split_key.rb +1 -1
  43. data/lib/i18n/tasks/translation.rb +4 -1
  44. data/lib/i18n/tasks/translators/base_translator.rb +17 -12
  45. data/lib/i18n/tasks/translators/deepl_translator.rb +34 -11
  46. data/lib/i18n/tasks/translators/google_translator.rb +1 -1
  47. data/lib/i18n/tasks/translators/openai_translator.rb +100 -0
  48. data/lib/i18n/tasks/used_keys.rb +9 -6
  49. data/lib/i18n/tasks/version.rb +1 -1
  50. data/lib/i18n/tasks.rb +11 -0
  51. data/templates/config/i18n-tasks.yml +17 -4
  52. data/templates/minitest/i18n_test.rb +6 -6
  53. metadata +74 -16
@@ -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
@@ -3,6 +3,7 @@
3
3
  require 'find'
4
4
  require 'i18n/tasks/scanners/pattern_with_scope_scanner'
5
5
  require 'i18n/tasks/scanners/ruby_ast_scanner'
6
+ require 'i18n/tasks/scanners/erb_ast_scanner'
6
7
  require 'i18n/tasks/scanners/scanner_multiplexer'
7
8
  require 'i18n/tasks/scanners/files/caching_file_finder_provider'
8
9
  require 'i18n/tasks/scanners/files/caching_file_reader'
@@ -20,14 +21,16 @@ module I18n::Tasks
20
21
  relative_roots: %w[app/controllers app/helpers app/mailers app/presenters app/views].freeze,
21
22
  scanners: [
22
23
  ['::I18n::Tasks::Scanners::RubyAstScanner', { only: %w[*.rb] }],
23
- ['::I18n::Tasks::Scanners::PatternWithScopeScanner', { exclude: %w[*.rb] }]
24
+ ['::I18n::Tasks::Scanners::ErbAstScanner', { only: %w[*.erb] }],
25
+ ['::I18n::Tasks::Scanners::PatternWithScopeScanner', { exclude: %w[*.erb *.rb] }]
24
26
  ],
27
+ ast_matchers: [],
25
28
  strict: true
26
29
  }.freeze
27
30
 
28
- ALWAYS_EXCLUDE = %w[*.jpg *.jpeg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf *.css *.sass *.scss *.less
29
- *.yml *.json *.zip *.tar.gz *.swf *.flv *.mp3 *.wav *.flac *.webm *.mp4 *.ogg *.opus
30
- *.webp *.map].freeze
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
+ *.webp *.map *.xlsx].freeze
31
34
 
32
35
  # Find all keys in the source and return a forest with the keys in absolute form and their occurrences.
33
36
  #
@@ -139,7 +142,7 @@ module I18n::Tasks
139
142
 
140
143
  # keys in the source that end with a ., e.g. t("category.#{ cat.i18n_key }") or t("category." + category.key)
141
144
  # @param [String] replacement for interpolated values.
142
- def expr_key_re(replacement: ':')
145
+ def expr_key_re(replacement: '*:')
143
146
  @expr_key_re ||= begin
144
147
  # disallow patterns with no keys
145
148
  ignore_pattern_re = /\A[.#{replacement}]*\z/
@@ -175,7 +178,7 @@ module I18n::Tasks
175
178
  braces << '{'
176
179
  end
177
180
  end
178
- result << key[scanner.pos..-1] unless scanner.eos?
181
+ result << key[scanner.pos..] unless scanner.eos?
179
182
  result.join
180
183
  end
181
184
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module I18n
4
4
  module Tasks
5
- VERSION = '0.9.37'
5
+ VERSION = '1.0.13'
6
6
  end
7
7
  end
data/lib/i18n/tasks.rb CHANGED
@@ -34,6 +34,17 @@ module I18n
34
34
  ::I18n::Tasks::Commands.send :include, commands_module
35
35
  self
36
36
  end
37
+
38
+ # Add AST-matcher to i18n-tasks
39
+ #
40
+ # @param matcher_class_name
41
+ # @return self
42
+ def add_ast_matcher(matcher_class_name)
43
+ matchers = I18n::Tasks::Configuration::DEFAULTS[:search][:ast_matchers]
44
+ matchers << matcher_class_name
45
+ matchers.uniq!
46
+ self
47
+ end
37
48
  end
38
49
 
39
50
  @verbose = !ENV['VERBOSE'].nil?
@@ -13,7 +13,7 @@ data:
13
13
  ## Provide a custom adapter:
14
14
  # adapter: I18n::Tasks::Data::FileSystem
15
15
 
16
- # Locale files or `File.find` patterns where translations are read from:
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 `File.find` patterns to search in:
55
+ ## Paths or `Find.find` patterns to search in:
56
56
  # paths:
57
57
  # - app/
58
58
 
@@ -72,11 +72,13 @@ search:
72
72
  # -
73
73
 
74
74
  ## Files or `File.fnmatch` patterns to exclude from search. Some files are always excluded regardless of this setting:
75
- ## %w(*.jpg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf *.css *.sass *.scss *.less *.yml *.json *.map)
75
+ ## *.jpg *.jpeg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf *.css *.sass *.scss *.less
76
+ ## *.yml *.json *.zip *.tar.gz *.swf *.flv *.mp3 *.wav *.flac *.webm *.mp4 *.ogg *.opus *.webp *.map *.xlsx
76
77
  exclude:
77
78
  - app/assets/images
78
79
  - app/assets/fonts
79
80
  - app/assets/videos
81
+ - app/assets/builds
80
82
 
81
83
  ## Alternatively, the only files or `File.fnmatch patterns` to search in `paths`:
82
84
  ## If specified, this settings takes priority over `exclude`, but `exclude` still applies.
@@ -85,6 +87,15 @@ search:
85
87
  ## If `strict` is `false`, guess usages such as t("categories.#{category}.title"). The default is `true`.
86
88
  # strict: true
87
89
 
90
+ ## Allows adding ast_matchers for finding translations using the AST-scanners
91
+ ## The available matchers are:
92
+ ## - RailsModelMatcher
93
+ ## Matches ActiveRecord translations like
94
+ ## User.human_attribute_name(:email) and User.model_name.human
95
+ ##
96
+ ## To implement your own, please see `I18n::Tasks::Scanners::AstMatchers::BaseMatcher`.
97
+ # <%# I18n::Tasks.add_ast_matcher('I18n::Tasks::Scanners::AstMatchers::RailsModelMatcher') %>
98
+
88
99
  ## Multiple scanners can be used. Their results are merged.
89
100
  ## The options specified above are passed down to each scanner. Per-scanner options can be specified as well.
90
101
  ## See this example of a custom scanner: https://github.com/glebm/i18n-tasks/wiki/A-custom-scanner-example
@@ -99,7 +110,9 @@ search:
99
110
  # deepl_api_key: "48E92789-57A3-466A-9959-1A1A1A1A1A1A"
100
111
  # # deepl_host: "https://api.deepl.com"
101
112
  # # deepl_version: "v2"
102
-
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
103
116
  ## Do not consider these keys missing:
104
117
  # ignore_missing:
105
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
- assert_empty @missing_keys,
14
- "Missing #{@missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them"
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
- assert_empty @unused_keys,
19
- "#{@unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them"
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: 0.9.37
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: 2021-12-26 00:00:00.000000000 Z
11
+ date: 2023-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -38,6 +38,26 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: 2.1.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: better_html
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: '3.0'
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '1.0'
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.0'
41
61
  - !ruby/object:Gem::Dependency
42
62
  name: erubi
43
63
  requirement: !ruby/object:Gem::Requirement
@@ -86,14 +106,14 @@ dependencies:
86
106
  requirements:
87
107
  - - ">="
88
108
  - !ruby/object:Gem::Version
89
- version: 2.2.3.0
109
+ version: 3.2.2.1
90
110
  type: :runtime
91
111
  prerelease: false
92
112
  version_requirements: !ruby/object:Gem::Requirement
93
113
  requirements:
94
114
  - - ">="
95
115
  - !ruby/object:Gem::Version
96
- version: 2.2.3.0
116
+ version: 3.2.2.1
97
117
  - !ruby/object:Gem::Dependency
98
118
  name: rails-i18n
99
119
  requirement: !ruby/object:Gem::Requirement
@@ -143,12 +163,15 @@ dependencies:
143
163
  - !ruby/object:Gem::Version
144
164
  version: 1.5.1
145
165
  - !ruby/object:Gem::Dependency
146
- name: axlsx
166
+ name: bundler
147
167
  requirement: !ruby/object:Gem::Requirement
148
168
  requirements:
149
169
  - - "~>"
150
170
  - !ruby/object:Gem::Version
151
171
  version: '2.0'
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: 2.0.1
152
175
  type: :development
153
176
  prerelease: false
154
177
  version_requirements: !ruby/object:Gem::Requirement
@@ -156,26 +179,23 @@ dependencies:
156
179
  - - "~>"
157
180
  - !ruby/object:Gem::Version
158
181
  version: '2.0'
182
+ - - ">="
183
+ - !ruby/object:Gem::Version
184
+ version: 2.0.1
159
185
  - !ruby/object:Gem::Dependency
160
- name: bundler
186
+ name: overcommit
161
187
  requirement: !ruby/object:Gem::Requirement
162
188
  requirements:
163
189
  - - "~>"
164
190
  - !ruby/object:Gem::Version
165
- version: '2.0'
166
- - - ">="
167
- - !ruby/object:Gem::Version
168
- version: 2.0.1
191
+ version: 0.58.0
169
192
  type: :development
170
193
  prerelease: false
171
194
  version_requirements: !ruby/object:Gem::Requirement
172
195
  requirements:
173
196
  - - "~>"
174
197
  - !ruby/object:Gem::Version
175
- version: '2.0'
176
- - - ">="
177
- - !ruby/object:Gem::Version
178
- version: 2.0.1
198
+ version: 0.58.0
179
199
  - !ruby/object:Gem::Dependency
180
200
  name: rake
181
201
  requirement: !ruby/object:Gem::Requirement
@@ -210,14 +230,42 @@ dependencies:
210
230
  requirements:
211
231
  - - "~>"
212
232
  - !ruby/object:Gem::Version
213
- version: 1.6.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
214
262
  type: :development
215
263
  prerelease: false
216
264
  version_requirements: !ruby/object:Gem::Requirement
217
265
  requirements:
218
266
  - - "~>"
219
267
  - !ruby/object:Gem::Version
220
- version: 1.6.1
268
+ version: 2.19.0
221
269
  - !ruby/object:Gem::Dependency
222
270
  name: simplecov
223
271
  requirement: !ruby/object:Gem::Requirement
@@ -357,12 +405,18 @@ files:
357
405
  - lib/i18n/tasks/references.rb
358
406
  - lib/i18n/tasks/reports/base.rb
359
407
  - lib/i18n/tasks/reports/terminal.rb
408
+ - lib/i18n/tasks/scanners/ast_matchers/base_matcher.rb
409
+ - lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb
410
+ - lib/i18n/tasks/scanners/ast_matchers/rails_model_matcher.rb
411
+ - lib/i18n/tasks/scanners/erb_ast_processor.rb
412
+ - lib/i18n/tasks/scanners/erb_ast_scanner.rb
360
413
  - lib/i18n/tasks/scanners/file_scanner.rb
361
414
  - lib/i18n/tasks/scanners/files/caching_file_finder.rb
362
415
  - lib/i18n/tasks/scanners/files/caching_file_finder_provider.rb
363
416
  - lib/i18n/tasks/scanners/files/caching_file_reader.rb
364
417
  - lib/i18n/tasks/scanners/files/file_finder.rb
365
418
  - lib/i18n/tasks/scanners/files/file_reader.rb
419
+ - lib/i18n/tasks/scanners/local_ruby_parser.rb
366
420
  - lib/i18n/tasks/scanners/occurrence_from_position.rb
367
421
  - lib/i18n/tasks/scanners/pattern_mapper.rb
368
422
  - lib/i18n/tasks/scanners/pattern_scanner.rb
@@ -382,6 +436,7 @@ files:
382
436
  - lib/i18n/tasks/translators/base_translator.rb
383
437
  - lib/i18n/tasks/translators/deepl_translator.rb
384
438
  - lib/i18n/tasks/translators/google_translator.rb
439
+ - lib/i18n/tasks/translators/openai_translator.rb
385
440
  - lib/i18n/tasks/translators/yandex_translator.rb
386
441
  - lib/i18n/tasks/unused_keys.rb
387
442
  - lib/i18n/tasks/used_keys.rb
@@ -394,11 +449,14 @@ licenses:
394
449
  - MIT
395
450
  metadata:
396
451
  issue_tracker: https://github.com/glebm/i18n-tasks
452
+ rubygems_mfa_required: 'true'
397
453
  post_install_message: |
398
454
  # Install default configuration:
399
455
  cp $(bundle exec i18n-tasks gem-path)/templates/config/i18n-tasks.yml config/
400
456
  # Add an RSpec for missing and unused keys:
401
457
  cp $(bundle exec i18n-tasks gem-path)/templates/rspec/i18n_spec.rb spec/
458
+ # Or for minitest:
459
+ cp $(bundle exec i18n-tasks gem-path)/templates/minitest/i18n_test.rb test/
402
460
  rdoc_options: []
403
461
  require_paths:
404
462
  - lib