i18n-tasks 1.0.13 → 1.0.15

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +125 -38
  3. data/config/locales/en.yml +14 -5
  4. data/config/locales/ru.yml +14 -5
  5. data/i18n-tasks.gemspec +4 -2
  6. data/lib/i18n/tasks/command/commands/data.rb +14 -0
  7. data/lib/i18n/tasks/command/commands/missing.rb +3 -1
  8. data/lib/i18n/tasks/command/option_parsers/enum.rb +4 -3
  9. data/lib/i18n/tasks/command/options/common.rb +1 -1
  10. data/lib/i18n/tasks/command/options/locales.rb +12 -3
  11. data/lib/i18n/tasks/configuration.rb +8 -2
  12. data/lib/i18n/tasks/data/file_system_base.rb +5 -0
  13. data/lib/i18n/tasks/data/router/isolating_router.rb +146 -0
  14. data/lib/i18n/tasks/data/tree/siblings.rb +2 -2
  15. data/lib/i18n/tasks/interpolations.rb +1 -1
  16. data/lib/i18n/tasks/key_pattern_matching.rb +4 -4
  17. data/lib/i18n/tasks/reports/terminal.rb +6 -0
  18. data/lib/i18n/tasks/scanners/ast_matchers/default_i18n_subject_matcher.rb +26 -0
  19. data/lib/i18n/tasks/scanners/ast_matchers/rails_model_matcher.rb +1 -1
  20. data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +55 -25
  21. data/lib/i18n/tasks/scanners/local_ruby_parser.rb +2 -2
  22. data/lib/i18n/tasks/scanners/pattern_scanner.rb +1 -1
  23. data/lib/i18n/tasks/scanners/prism_scanner.rb +83 -0
  24. data/lib/i18n/tasks/scanners/prism_scanners/arguments_visitor.rb +41 -0
  25. data/lib/i18n/tasks/scanners/prism_scanners/nodes.rb +334 -0
  26. data/lib/i18n/tasks/scanners/prism_scanners/visitor.rb +273 -0
  27. data/lib/i18n/tasks/scanners/relative_keys.rb +1 -1
  28. data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +5 -4
  29. data/lib/i18n/tasks/scanners/ruby_key_literals.rb +1 -1
  30. data/lib/i18n/tasks/scanners/ruby_parser_factory.rb +27 -0
  31. data/lib/i18n/tasks/split_key.rb +30 -47
  32. data/lib/i18n/tasks/translation.rb +4 -1
  33. data/lib/i18n/tasks/translators/base_translator.rb +11 -1
  34. data/lib/i18n/tasks/translators/deepl_translator.rb +32 -1
  35. data/lib/i18n/tasks/translators/google_translator.rb +35 -12
  36. data/lib/i18n/tasks/translators/openai_translator.rb +55 -23
  37. data/lib/i18n/tasks/translators/watsonx_translator.rb +155 -0
  38. data/lib/i18n/tasks/translators/yandex_translator.rb +5 -1
  39. data/lib/i18n/tasks/used_keys.rb +1 -0
  40. data/lib/i18n/tasks/version.rb +1 -1
  41. data/lib/i18n/tasks.rb +1 -0
  42. data/templates/config/i18n-tasks.yml +26 -3
  43. metadata +33 -26
  44. data/lib/i18n/tasks/scanners/erb_ast_processor.rb +0 -74
@@ -5,6 +5,11 @@ module I18n
5
5
  module SplitKey
6
6
  module_function
7
7
 
8
+ PARENTHESIS_PAIRS = %w({} [] () <>).freeze
9
+ START_KEYS = PARENTHESIS_PAIRS.to_set { |pair| pair[0] }.freeze
10
+ END_KEYS = PARENTHESIS_PAIRS.to_h { |pair| [pair[0], pair[1]] }.freeze
11
+ private_constant :PARENTHESIS_PAIRS, :START_KEYS, :END_KEYS
12
+
8
13
  # split a key by dots (.)
9
14
  # dots inside braces or parenthesis are not split on
10
15
  #
@@ -12,61 +17,39 @@ module I18n
12
17
  # split_key 'a.#{b.c}' # => ['a', '#{b.c}']
13
18
  # split_key 'a.b.c', 2 # => ['a', 'b.c']
14
19
  def split_key(key, max = Float::INFINITY)
15
- parts = []
16
- pos = 0
17
20
  return [key] if max == 1
18
21
 
19
- key_parts(key) do |part|
20
- parts << part
21
- pos += part.length + 1
22
- if parts.length + 1 >= max
23
- parts << key[pos..] unless pos == key.length
24
- break
22
+ parts = []
23
+ current_parenthesis_end_char = nil
24
+ part = ''
25
+ key.each_char.with_index do |char, index|
26
+ if current_parenthesis_end_char
27
+ part += char
28
+ current_parenthesis_end_char = nil if char == current_parenthesis_end_char
29
+ elsif START_KEYS.include?(char)
30
+ part += char
31
+ current_parenthesis_end_char = END_KEYS[char]
32
+ elsif char == '.'
33
+ parts << part
34
+ if parts.size + 1 == max
35
+ remaining = key[(index + 1)..]
36
+ parts << remaining unless remaining.empty?
37
+ return parts
38
+ end
39
+ part = ''
40
+ else
41
+ part += char
25
42
  end
26
43
  end
27
- parts
28
- end
29
-
30
- def last_key_part(key)
31
- last = nil
32
- key_parts(key) { |part| last = part }
33
- last
34
- end
35
44
 
36
- # yield each key part
37
- # dots inside braces or parenthesis are not split on
38
- def key_parts(key, &block)
39
- return enum_for(:key_parts, key) unless block
45
+ return parts if part.empty?
40
46
 
41
- nesting = PARENS
42
- counts = PARENS_ZEROS # dup'd later if key contains parenthesis
43
- delim = '.'
44
- from = to = 0
45
- key.each_char do |char|
46
- if char == delim && PARENS_ZEROS == counts
47
- block.yield key[from...to]
48
- from = to = (to + 1)
49
- else
50
- nest_i, nest_inc = nesting[char]
51
- if nest_i
52
- counts = counts.dup if counts.frozen?
53
- counts[nest_i] += nest_inc
54
- end
55
- to += 1
56
- end
57
- end
58
- block.yield(key[from...to]) if from < to && to <= key.length
59
- true
47
+ current_parenthesis_end_char ? parts.concat(part.split('.')) : parts << part
60
48
  end
61
49
 
62
- PARENS = %w({} [] ()).each_with_object({}) do |s, h|
63
- i = h.size / 2
64
- h[s[0].freeze] = [i, 1].freeze
65
- h[s[1].freeze] = [i, -1].freeze
66
- end.freeze
67
- PARENS_ZEROS = Array.new(PARENS.size, 0).freeze
68
- private_constant :PARENS
69
- private_constant :PARENS_ZEROS
50
+ def last_key_part(key)
51
+ split_key(key).last
52
+ end
70
53
  end
71
54
  end
72
55
  end
@@ -3,6 +3,7 @@
3
3
  require 'i18n/tasks/translators/deepl_translator'
4
4
  require 'i18n/tasks/translators/google_translator'
5
5
  require 'i18n/tasks/translators/openai_translator'
6
+ require 'i18n/tasks/translators/watsonx_translator'
6
7
  require 'i18n/tasks/translators/yandex_translator'
7
8
 
8
9
  module I18n::Tasks
@@ -11,7 +12,7 @@ module I18n::Tasks
11
12
  # @param [String] from locale
12
13
  # @param [:deepl, :openai, :google, :yandex] backend
13
14
  # @return [I18n::Tasks::Tree::Siblings] translated forest
14
- def translate_forest(forest, from:, backend: :google)
15
+ def translate_forest(forest, from:, backend:)
15
16
  case backend
16
17
  when :deepl
17
18
  Translators::DeeplTranslator.new(self).translate_forest(forest, from)
@@ -19,6 +20,8 @@ module I18n::Tasks
19
20
  Translators::GoogleTranslator.new(self).translate_forest(forest, from)
20
21
  when :openai
21
22
  Translators::OpenAiTranslator.new(self).translate_forest(forest, from)
23
+ when :watsonx
24
+ Translators::WatsonxTranslator.new(self).translate_forest(forest, from)
22
25
  when :yandex
23
26
  Translators::YandexTranslator.new(self).translate_forest(forest, from)
24
27
  else
@@ -14,7 +14,11 @@ module I18n::Tasks
14
14
  # @return [I18n::Tasks::Tree::Siblings] translated forest
15
15
  def translate_forest(forest, from)
16
16
  forest.inject @i18n_tasks.empty_forest do |result, root|
17
- translated = translate_pairs(root.key_values(root: true), to: root.key, from: from)
17
+ pairs = root.key_values(root: true)
18
+
19
+ @progress_bar = ProgressBar.create(total: pairs.flatten.size, format: '%a <%B> %e %c/%C (%p%%)')
20
+
21
+ translated = translate_pairs(pairs, to: root.key, from: from)
18
22
  result.merge! Data::Tree::Siblings.from_flat_pairs(translated)
19
23
  end
20
24
  end
@@ -70,6 +74,9 @@ module I18n::Tasks
70
74
  when Array
71
75
  # dump recursively
72
76
  value.map { |v| dump_value(v, opts) }
77
+ when Hash
78
+ # dump recursively
79
+ value.values.map { |v| dump_value(v, opts) }
73
80
  when String
74
81
  value = CGI.escapeHTML(value) if opts[:html_escape]
75
82
  replace_interpolations value unless value.empty?
@@ -85,6 +92,9 @@ module I18n::Tasks
85
92
  when Array
86
93
  # implode array
87
94
  untranslated.map { |from| parse_value(from, each_translated, opts) }
95
+ when Hash
96
+ # implode hash
97
+ untranslated.transform_values { |value| parse_value(value, each_translated, opts) }
88
98
  when String
89
99
  if untranslated.empty?
90
100
  untranslated
@@ -24,12 +24,19 @@ module I18n::Tasks::Translators
24
24
  def translate_values(list, from:, to:, **options)
25
25
  results = []
26
26
  list.each_slice(BATCH_SIZE) do |parts|
27
- res = DeepL.translate(parts, to_deepl_source_locale(from), to_deepl_target_locale(to), options)
27
+ res = DeepL.translate(
28
+ parts,
29
+ to_deepl_source_locale(from),
30
+ to_deepl_target_locale(to),
31
+ options_with_glossary(options, from, to)
32
+ )
28
33
  if res.is_a?(DeepL::Resources::Text)
29
34
  results << res.text
30
35
  else
31
36
  results += res.map(&:text)
32
37
  end
38
+
39
+ @progress_bar.progress += parts.size
33
40
  end
34
41
  results
35
42
  end
@@ -78,6 +85,9 @@ module I18n::Tasks::Translators
78
85
 
79
86
  # Convert 'es-ES' to 'ES' but warn about locales requiring a specific variant
80
87
  def to_deepl_target_locale(locale)
88
+ locale_aliases = @i18n_tasks.translation_config[:deepl_locale_aliases]
89
+ locale = locale_aliases[locale.to_s.downcase] || locale if locale_aliases.is_a?(Hash)
90
+
81
91
  loc, sub = locale.to_s.split('-')
82
92
  if SPECIFIC_TARGETS.include?(loc)
83
93
  # Must see how the deepl api evolves, so this could be an error in the future
@@ -100,5 +110,26 @@ module I18n::Tasks::Translators
100
110
  config.version = version unless version.blank?
101
111
  end
102
112
  end
113
+
114
+ def options_with_glossary(options, from, to)
115
+ glossary = find_glossary(from, to)
116
+ glossary ? { glossary_id: glossary.id }.merge(options) : options
117
+ end
118
+
119
+ def all_ready_glossaries
120
+ @all_ready_glossaries ||= DeepL.glossaries.list
121
+ end
122
+
123
+ def find_glossary(from, to)
124
+ config_glossary_ids = @i18n_tasks.translation_config[:deepl_glossary_ids]
125
+ return unless config_glossary_ids
126
+
127
+ all_ready_glossaries.find do |glossary|
128
+ glossary.ready \
129
+ && glossary.source_lang == from \
130
+ && glossary.target_lang == to \
131
+ && config_glossary_ids.include?(glossary.id)
132
+ end
133
+ end
103
134
  end
104
135
  end
@@ -4,6 +4,7 @@ require 'i18n/tasks/translators/base_translator'
4
4
 
5
5
  module I18n::Tasks::Translators
6
6
  class GoogleTranslator < BaseTranslator
7
+ NEWLINE_PLACEHOLDER = '<br id=i18n />'
7
8
  def initialize(*)
8
9
  begin
9
10
  require 'easy_translate'
@@ -16,14 +17,25 @@ module I18n::Tasks::Translators
16
17
  protected
17
18
 
18
19
  def translate_values(list, **options)
19
- EasyTranslate.translate(list, options)
20
+ result = restore_newlines(
21
+ EasyTranslate.translate(
22
+ replace_newlines_with_placeholder(list, options[:html]),
23
+ options,
24
+ format: options[:html] ? :html : :text
25
+ ),
26
+ options[:html]
27
+ )
28
+
29
+ @progress_bar.progress += result.size
30
+
31
+ result
20
32
  end
21
33
 
22
34
  def options_for_translate_values(from:, to:, **options)
23
35
  options.merge(
24
36
  api_key: api_key,
25
- from: to_google_translate_compatible_locale(from),
26
- to: to_google_translate_compatible_locale(to)
37
+ from: from,
38
+ to: to
27
39
  )
28
40
  end
29
41
 
@@ -41,15 +53,6 @@ module I18n::Tasks::Translators
41
53
 
42
54
  private
43
55
 
44
- SUPPORTED_LOCALES_WITH_REGION = %w[zh-CN zh-TW].freeze
45
-
46
- # Convert 'es-ES' to 'es'
47
- def to_google_translate_compatible_locale(locale)
48
- return locale unless locale.include?('-') && !SUPPORTED_LOCALES_WITH_REGION.include?(locale)
49
-
50
- locale.split('-', 2).first
51
- end
52
-
53
56
  def api_key
54
57
  @api_key ||= begin
55
58
  key = @i18n_tasks.translation_config[:google_translate_api_key]
@@ -65,5 +68,25 @@ module I18n::Tasks::Translators
65
68
  key
66
69
  end
67
70
  end
71
+
72
+ def replace_newlines_with_placeholder(list, html)
73
+ return list unless html
74
+
75
+ list.map do |value|
76
+ value.gsub(/\n(\s*)/) do
77
+ "<Z__#{::Regexp.last_match(1)&.length || 0}>"
78
+ end
79
+ end
80
+ end
81
+
82
+ def restore_newlines(translations, html)
83
+ return translations unless html
84
+
85
+ translations.map do |translation|
86
+ translation.gsub(/<Z__(\d+)>/) do
87
+ "\n#{' ' * ::Regexp.last_match(1).to_i}"
88
+ end
89
+ end
90
+ end
68
91
  end
69
92
  end
@@ -1,11 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'i18n/tasks/translators/base_translator'
4
+ require 'active_support/core_ext/string/filters'
4
5
 
5
6
  module I18n::Tasks::Translators
6
7
  class OpenAiTranslator < BaseTranslator
7
8
  # max allowed texts per request
8
9
  BATCH_SIZE = 50
10
+ DEFAULT_SYSTEM_PROMPT = <<~PROMPT.squish
11
+ You are a professional translator that translates content from the %{from} locale
12
+ to the %{to} locale in an i18n locale array.
13
+
14
+ The array has a structured format and contains multiple strings. Your task is to translate
15
+ each of these strings and create a new array with the translated strings.
16
+
17
+ HTML markups (enclosed in < and > characters) must not be changed under any circumstance.
18
+ Variables (starting with %%{ and ending with }) must not be changed under any circumstance.
19
+
20
+ Keep in mind the context of all the strings for a more accurate translation.
21
+ It is CRITICAL you output only the result, without any additional information, code block syntax or comments.
22
+ PROMPT
23
+ JSON_FORMAT_INSTRUCTIONS_SYSTEM_PROMPT = <<~PROMPT.squish
24
+ Return the translations as a JSON object with a 'translations' array containing the translated strings.
25
+ PROMPT
9
26
 
10
27
  def initialize(*)
11
28
  begin
@@ -38,7 +55,7 @@ module I18n::Tasks::Translators
38
55
  private
39
56
 
40
57
  def translator
41
- @translator ||= OpenAI::Client.new(access_token: api_key)
58
+ @translator ||= OpenAI::Client.new(access_token: api_key, log_errors: true)
42
59
  end
43
60
 
44
61
  def api_key
@@ -50,26 +67,56 @@ module I18n::Tasks::Translators
50
67
  end
51
68
  end
52
69
 
70
+ def model
71
+ @model ||= @i18n_tasks.translation_config[:openai_model].presence || 'gpt-4o-mini'
72
+ end
73
+
74
+ def system_prompt
75
+ @system_prompt ||=
76
+ (@i18n_tasks.translation_config[:openai_system_prompt].presence || DEFAULT_SYSTEM_PROMPT)
77
+ .concat("\n#{JSON_FORMAT_INSTRUCTIONS_SYSTEM_PROMPT}")
78
+ @system_prompt
79
+ end
80
+
53
81
  def translate_values(list, from:, to:)
54
82
  results = []
55
83
 
56
84
  list.each_slice(BATCH_SIZE) do |batch|
57
85
  translations = translate(batch, from, to)
86
+ result = JSON.parse(translations)
87
+ results << result
58
88
 
59
- results << JSON.parse(translations)
89
+ @progress_bar.progress += result.size
60
90
  end
61
91
 
62
92
  results.flatten
63
93
  end
64
94
 
65
- def translate(values, from, to) # rubocop:disable Metrics/MethodLength
66
- messages = [
95
+ def translate(values, from, to)
96
+ response = translator.chat(
97
+ parameters: {
98
+ model: model,
99
+ messages: build_messages(values, from, to),
100
+ temperature: 0.0,
101
+ response_format: { type: 'json_object' }
102
+ }
103
+ )
104
+
105
+ translations = response.dig('choices', 0, 'message', 'content')
106
+ error = response['error']
107
+
108
+ fail "AI error: #{error}" if error.present?
109
+
110
+ # Extract the array from the JSON object response
111
+ result = JSON.parse(translations)
112
+ result['translations'].to_json
113
+ end
114
+
115
+ def build_messages(values, from, to)
116
+ [
67
117
  {
68
118
  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"
119
+ content: format(system_prompt, from: from, to: to)
73
120
  },
74
121
  {
75
122
  role: 'user',
@@ -80,21 +127,6 @@ module I18n::Tasks::Translators
80
127
  content: values.to_json
81
128
  }
82
129
  ]
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
130
  end
99
131
  end
100
132
  end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/tasks/translators/base_translator'
4
+ require 'active_support/core_ext/string/filters'
5
+
6
+ module I18n::Tasks::Translators
7
+ class WatsonxTranslator < BaseTranslator
8
+ # max allowed texts per request
9
+ BATCH_SIZE = 50
10
+ DEFAULT_SYSTEM_PROMPT = <<~PROMPT.squish
11
+ You are a helpful assistant that translates content from the %{from} locale
12
+ to the %{to} locale in an i18n locale array.
13
+ You always preserve the structure and formatting exactly as it is.
14
+
15
+ The array has a structured format and contains multiple strings. Your task is to translate
16
+ each of these strings and create a new array with the translated strings.
17
+
18
+ Reminder:
19
+ - Translate only the text, preserving the structure and formatting.
20
+ - Do not translate any URLs.
21
+ - Do not translate HTML tags like `<details>` and `<summary>`.
22
+ - HTML markups (enclosed in < and > characters) must not be changed under any circumstance.
23
+ - Variables (starting with %%{ and ending with }) must not be changed under any circumstance.
24
+ - Output only the result, without any additional information or comments.
25
+ PROMPT
26
+
27
+ def options_for_translate_values(from:, to:, **options)
28
+ options.merge(
29
+ from: from,
30
+ to: to
31
+ )
32
+ end
33
+
34
+ def options_for_html
35
+ {}
36
+ end
37
+
38
+ def options_for_plain
39
+ {}
40
+ end
41
+
42
+ def no_results_error_message
43
+ I18n.t('i18n_tasks.watsonx_translate.errors.no_results')
44
+ end
45
+
46
+ private
47
+
48
+ def translator
49
+ @translator ||= WatsonxClient.new(key: api_key)
50
+ end
51
+
52
+ def api_key
53
+ @api_key ||= begin
54
+ key = @i18n_tasks.translation_config[:watsonx_api_key]
55
+ fail ::I18n::Tasks::CommandError, I18n.t('i18n_tasks.watsonx_translate.errors.no_api_key') if key.blank?
56
+
57
+ key
58
+ end
59
+ end
60
+
61
+ def project_id
62
+ @project_id ||= begin
63
+ project_id = @i18n_tasks.translation_config[:watsonx_project_id]
64
+ if project_id.blank?
65
+ fail ::I18n::Tasks::CommandError,
66
+ I18n.t('i18n_tasks.watsonx_translate.errors.no_project_id')
67
+ end
68
+
69
+ project_id
70
+ end
71
+ end
72
+
73
+ def model
74
+ @model ||= @i18n_tasks.translation_config[:watsonx_model].presence || 'meta-llama/llama-3-2-90b-vision-instruct'
75
+ end
76
+
77
+ def system_prompt
78
+ @system_prompt ||= @i18n_tasks.translation_config[:watsonx_system_prompt].presence || DEFAULT_SYSTEM_PROMPT
79
+ end
80
+
81
+ def translate_values(list, from:, to:)
82
+ results = []
83
+
84
+ list.each_slice(BATCH_SIZE) do |batch|
85
+ translations = translate(batch, from, to)
86
+ result = JSON.parse(translations)
87
+ results << result
88
+
89
+ @progress_bar.progress += results.size
90
+ end
91
+
92
+ results.flatten
93
+ end
94
+
95
+ def translate(values, from, to)
96
+ prompt = [
97
+ '<|eot_id|><|start_header_id|>system<|end_header_id|>',
98
+ format(system_prompt, from: from, to: to),
99
+ '<|eot_id|><|start_header_id|>user<|end_header_id|>Translate this array:',
100
+ "<|eot_id|><|start_header_id|>user<|end_header_id|>#{values.to_json}",
101
+ '<|eot_id|><|start_header_id|>assistant<|end_header_id|>'
102
+ ].join
103
+
104
+ response = translator.generate_text(
105
+ model_id: model,
106
+ project_id: project_id,
107
+ input: prompt,
108
+ parameters: {
109
+ decoding_method: :greedy,
110
+ max_new_tokens: 2048,
111
+ repetition_penalty: 1
112
+ }
113
+ )
114
+ response.dig('results', 0, 'generated_text')
115
+ end
116
+ end
117
+ end
118
+
119
+ class WatsonxClient
120
+ WATSONX_BASE_URL = 'https://us-south.ml.cloud.ibm.com/ml/'
121
+ IBM_CLOUD_IAM_URL = 'https://iam.cloud.ibm.com/identity/token'
122
+
123
+ def initialize(key:)
124
+ begin
125
+ require 'faraday'
126
+ rescue LoadError
127
+ raise ::I18n::Tasks::CommandError, "Add gem 'faraday' to your Gemfile to use this command"
128
+ end
129
+
130
+ @http = Faraday.new(url: WATSONX_BASE_URL) do |conn|
131
+ conn.use Faraday::Response::RaiseError
132
+ conn.request :json
133
+ conn.response :json
134
+ conn.options.timeout = 600
135
+ conn.request :authorization, :Bearer, token(key)
136
+ end
137
+ end
138
+
139
+ def generate_text(**opts)
140
+ @http.post('v1/text/generation?version=2024-05-20', **opts).body
141
+ end
142
+
143
+ private
144
+
145
+ def token(key)
146
+ Faraday.new(url: IBM_CLOUD_IAM_URL) do |conn|
147
+ conn.use Faraday::Response::RaiseError
148
+ conn.response :json
149
+ conn.params = {
150
+ grant_type: 'urn:ibm:params:oauth:grant-type:apikey',
151
+ apikey: key
152
+ }
153
+ end.post.body['access_token']
154
+ end
155
+ end
@@ -16,7 +16,11 @@ module I18n::Tasks::Translators
16
16
  protected
17
17
 
18
18
  def translate_values(list, **options)
19
- list.map { |item| translator.translate(item, options) }
19
+ result = list.map { |item| translator.translate(item, options) }
20
+
21
+ @progress_bar.progress += result.size
22
+
23
+ result
20
24
  end
21
25
 
22
26
  def options_for_translate_values(from:, to:, **options)
@@ -4,6 +4,7 @@ require 'find'
4
4
  require 'i18n/tasks/scanners/pattern_with_scope_scanner'
5
5
  require 'i18n/tasks/scanners/ruby_ast_scanner'
6
6
  require 'i18n/tasks/scanners/erb_ast_scanner'
7
+ require 'i18n/tasks/scanners/prism_scanner'
7
8
  require 'i18n/tasks/scanners/scanner_multiplexer'
8
9
  require 'i18n/tasks/scanners/files/caching_file_finder_provider'
9
10
  require 'i18n/tasks/scanners/files/caching_file_reader'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module I18n
4
4
  module Tasks
5
- VERSION = '1.0.13'
5
+ VERSION = '1.0.15'
6
6
  end
7
7
  end
data/lib/i18n/tasks.rb CHANGED
@@ -66,6 +66,7 @@ require 'active_support/core_ext/module/delegation'
66
66
  require 'active_support/core_ext/object/blank'
67
67
  require 'active_support/core_ext/object/try'
68
68
 
69
+ require 'ruby-progressbar'
69
70
  require 'rainbow'
70
71
  require 'erubi'
71
72
 
@@ -13,7 +13,7 @@ data:
13
13
  ## Provide a custom adapter:
14
14
  # adapter: I18n::Tasks::Data::FileSystem
15
15
 
16
- # Locale files or `Find.find` patterns where translations are read from:
16
+ # Locale files or `Dir.glob` patterns where translations are read from:
17
17
  read:
18
18
  ## Default:
19
19
  # - config/locales/%{locale}.yml
@@ -34,7 +34,7 @@ data:
34
34
  ## Example (replace %#= with %=):
35
35
  # - "<%#= %x[bundle info vagrant --path].chomp %>/templates/locales/%{locale}.yml"
36
36
 
37
- ## Specify the router (see Readme for details). Valid values: conservative_router, pattern_router, or a custom class.
37
+ ## Specify the router (see Readme for details). Valid values: conservative_router, pattern_router, isolating_router, or a custom class.
38
38
  # router: conservative_router
39
39
 
40
40
  yaml:
@@ -92,9 +92,13 @@ search:
92
92
  ## - RailsModelMatcher
93
93
  ## Matches ActiveRecord translations like
94
94
  ## User.human_attribute_name(:email) and User.model_name.human
95
+ ## - DefaultI18nSubjectMatcher
96
+ ## Matches ActionMailer's default_i18n_subject method
95
97
  ##
96
98
  ## To implement your own, please see `I18n::Tasks::Scanners::AstMatchers::BaseMatcher`.
97
- # <%# I18n::Tasks.add_ast_matcher('I18n::Tasks::Scanners::AstMatchers::RailsModelMatcher') %>
99
+ # ast_matchers:
100
+ # - 'I18n::Tasks::Scanners::AstMatchers::RailsModelMatcher'
101
+ # - 'I18n::Tasks::Scanners::AstMatchers::DefaultI18nSubjectMatcher'
98
102
 
99
103
  ## Multiple scanners can be used. Their results are merged.
100
104
  ## The options specified above are passed down to each scanner. Per-scanner options can be specified as well.
@@ -110,9 +114,28 @@ search:
110
114
  # deepl_api_key: "48E92789-57A3-466A-9959-1A1A1A1A1A1A"
111
115
  # # deepl_host: "https://api.deepl.com"
112
116
  # # deepl_version: "v2"
117
+ # # deepl_glossary_ids:
118
+ # # - f28106eb-0e06-489e-82c6-8215d6f95089
119
+ # # - 2c6415be-1852-4f54-9e1b-d800463496b4
113
120
  # # add additional options to the DeepL.translate call: https://www.deepl.com/docs-api/translate-text/translate-text/
114
121
  # deepl_options:
115
122
  # formality: prefer_less
123
+ # # OpenAI
124
+ # openai_api_key: "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
125
+ # # openai_model: "gpt-4o-mini" # see https://platform.openai.com/docs/models
126
+ # # may contain `%{from}` and `%{to}`, which will be replaced by source and target locale codes, respectively (using `Kernel.format`)
127
+ # # openai_system_prompt: >-
128
+ # # You are a professional translator that translates content from the %{from} locale
129
+ # # to the %{to} locale in an i18n locale array.
130
+ # #
131
+ # # The array has a structured format and contains multiple strings. Your task is to translate
132
+ # # each of these strings and create a new array with the translated strings.
133
+ # #
134
+ # # HTML markups (enclosed in < and > characters) must not be changed under any circumstance.
135
+ # # Variables (starting with %%{ and ending with }) must not be changed under any circumstance.
136
+ # #
137
+ # # Keep in mind the context of all the strings for a more accurate translation.
138
+
116
139
  ## Do not consider these keys missing:
117
140
  # ignore_missing:
118
141
  # - 'errors.messages.{accepted,blank,invalid,too_short,too_long}'