i18n-tasks 0.9.37 → 1.0.13

Sign up to get free protection for your applications and to get access to all the features.
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