i18n-tasks 1.0.12 → 1.0.14

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +85 -2
  3. data/Rakefile +1 -1
  4. data/config/locales/en.yml +11 -4
  5. data/config/locales/ru.yml +11 -4
  6. data/i18n-tasks.gemspec +5 -5
  7. data/lib/i18n/tasks/cli.rb +8 -8
  8. data/lib/i18n/tasks/command/commands/data.rb +14 -0
  9. data/lib/i18n/tasks/command/commands/missing.rb +2 -2
  10. data/lib/i18n/tasks/command/options/common.rb +1 -1
  11. data/lib/i18n/tasks/configuration.rb +3 -1
  12. data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +7 -1
  13. data/lib/i18n/tasks/data/file_system_base.rb +5 -0
  14. data/lib/i18n/tasks/data/router/isolating_router.rb +146 -0
  15. data/lib/i18n/tasks/data/tree/siblings.rb +2 -2
  16. data/lib/i18n/tasks/data/tree/traversal.rb +2 -2
  17. data/lib/i18n/tasks/interpolations.rb +1 -1
  18. data/lib/i18n/tasks/key_pattern_matching.rb +5 -3
  19. data/lib/i18n/tasks/references.rb +3 -3
  20. data/lib/i18n/tasks/reports/base.rb +1 -1
  21. data/lib/i18n/tasks/reports/terminal.rb +7 -1
  22. data/lib/i18n/tasks/scanners/ast_matchers/default_i18n_subject_matcher.rb +26 -0
  23. data/lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb +4 -4
  24. data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +55 -25
  25. data/lib/i18n/tasks/scanners/pattern_scanner.rb +1 -1
  26. data/lib/i18n/tasks/scanners/relative_keys.rb +1 -1
  27. data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +3 -1
  28. data/lib/i18n/tasks/split_key.rb +30 -47
  29. data/lib/i18n/tasks/translation.rb +4 -1
  30. data/lib/i18n/tasks/translators/base_translator.rb +22 -11
  31. data/lib/i18n/tasks/translators/deepl_translator.rb +58 -9
  32. data/lib/i18n/tasks/translators/google_translator.rb +28 -13
  33. data/lib/i18n/tasks/translators/openai_translator.rb +118 -0
  34. data/lib/i18n/tasks/used_keys.rb +5 -5
  35. data/lib/i18n/tasks/version.rb +1 -1
  36. data/templates/config/i18n-tasks.yml +28 -3
  37. data/templates/minitest/i18n_test.rb +6 -6
  38. metadata +40 -30
  39. data/lib/i18n/tasks/scanners/erb_ast_processor.rb +0 -74
@@ -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
- " but ⮕ #{other.value} in #{other.data[:locale]}"
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
 
@@ -93,6 +93,12 @@ module I18n
93
93
  end
94
94
  end
95
95
 
96
+ def cp_results(results)
97
+ results.each do |(from, to)|
98
+ print_info "#{Rainbow(from).cyan} #{Rainbow('+').yellow.bright} #{Rainbow(to).green}"
99
+ end
100
+ end
101
+
96
102
  def check_normalized_results(non_normalized)
97
103
  if non_normalized.empty?
98
104
  print_success 'All data is normalized'
@@ -112,7 +118,7 @@ module I18n
112
118
  when :missing_plural
113
119
  leaf[:data][:missing_keys].join(', ')
114
120
  else
115
- "#{Rainbow(leaf[:data][:missing_diff_locale]).cyan} "\
121
+ "#{Rainbow(leaf[:data][:missing_diff_locale]).cyan} " \
116
122
  "#{format_value(leaf[:value].is_a?(String) ? leaf[:value].strip : leaf[:value])}"
117
123
  end
118
124
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/tasks/scanners/ast_matchers/base_matcher'
4
+ require 'i18n/tasks/scanners/results/occurrence'
5
+
6
+ module I18n::Tasks::Scanners::AstMatchers
7
+ class DefaultI18nSubjectMatcher < BaseMatcher
8
+ def convert_to_key_occurrences(send_node, method_name, location: send_node.loc)
9
+ children = Array(send_node&.children)
10
+ return unless children[1] == :default_i18n_subject
11
+
12
+ key = @scanner.absolute_key(
13
+ '.subject',
14
+ location.expression.source_buffer.name,
15
+ calling_method: method_name
16
+ )
17
+ [
18
+ key,
19
+ I18n::Tasks::Scanners::Results::Occurrence.from_range(
20
+ raw_key: key,
21
+ range: location.expression
22
+ )
23
+ ]
24
+ end
25
+ end
26
+ end
@@ -79,10 +79,10 @@ module I18n::Tasks::Scanners::AstMatchers
79
79
  end
80
80
  if default_arg_node = extract_hash_pair(node, 'default')
81
81
  default_arg = if default_arg_node.children[1]&.type == :hash
82
- extract_hash(default_arg_node.children[1])
83
- else
84
- extract_string(default_arg_node.children[1])
85
- end
82
+ extract_hash(default_arg_node.children[1])
83
+ else
84
+ extract_string(default_arg_node.children[1])
85
+ end
86
86
  end
87
87
 
88
88
  [key, default_arg]
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'i18n/tasks/scanners/ruby_ast_scanner'
4
- require 'i18n/tasks/scanners/erb_ast_processor'
5
- require 'better_html/errors'
6
- require 'better_html/parser'
4
+ require 'i18n/tasks/scanners/local_ruby_parser'
7
5
 
8
6
  module I18n::Tasks::Scanners
9
7
  # Scan for I18n.translate calls in ERB-file better-html and ASTs
10
8
  class ErbAstScanner < RubyAstScanner
9
+ DEFAULT_REGEXP = /<%(={1,2}|-|\#|%)?(.*?)([-=])?%>/m.freeze
10
+
11
11
  def initialize(**args)
12
12
  super(**args)
13
- @erb_ast_processor = ErbAstProcessor.new
13
+ @ruby_parser = LocalRubyParser.new(ignore_blocks: true)
14
14
  end
15
15
 
16
16
  private
@@ -20,29 +20,59 @@ module I18n::Tasks::Scanners
20
20
  # @param path Path to file to parse
21
21
  # @return [{Parser::AST::Node}, [Parser::Source::Comment]]
22
22
  def path_to_ast_and_comments(path)
23
- parser = BetterHtml::Parser.new(make_buffer(path))
24
- ast = convert_better_html(parser.ast)
25
- @erb_ast_processor.process_and_extract_comments(ast)
23
+ comments = []
24
+ buffer = make_buffer(path)
25
+
26
+ children = []
27
+ buffer
28
+ .source
29
+ .scan(DEFAULT_REGEXP) do |indicator, code, tailch, _rspace|
30
+ match = Regexp.last_match
31
+ character = indicator ? indicator[0] : nil
32
+
33
+ start = match.begin(0) + 2 + (character&.size || 0)
34
+ stop = match.end(0) - 2 - (tailch&.size || 0)
35
+
36
+ case character
37
+ when '=', nil, '-'
38
+ parsed, parsed_comments = handle_code(buffer, code, start, stop)
39
+ comments.concat(parsed_comments)
40
+ children << parsed unless parsed.nil?
41
+ when '#', '#-'
42
+ comments << handle_comment(buffer, start, stop)
43
+ end
44
+ end
45
+
46
+ [root_node(children, buffer), comments]
26
47
  end
27
48
 
28
- # Convert BetterHtml nodes to Parser::AST::Node
29
- #
30
- # @param node BetterHtml::Parser::AST::Node
31
- # @return Parser::AST::Node
32
- def convert_better_html(node)
33
- definition = Parser::Source::Map::Definition.new(
34
- node.location.begin,
35
- node.location.begin,
36
- node.location.begin,
37
- node.location.end
38
- )
39
- Parser::AST::Node.new(
40
- node.type,
41
- node.children.map { |child| child.is_a?(BetterHtml::AST::Node) ? convert_better_html(child) : child },
42
- {
43
- location: definition
44
- }
45
- )
49
+ def handle_code(buffer, code, start, stop)
50
+ range = ::Parser::Source::Range.new(buffer, start, stop)
51
+ location =
52
+ Parser::Source::Map::Definition.new(
53
+ range.begin,
54
+ range.begin,
55
+ range.begin,
56
+ range.end
57
+ )
58
+ @ruby_parser.parse(code, location: location)
59
+ end
60
+
61
+ def handle_comment(buffer, start, stop)
62
+ range = ::Parser::Source::Range.new(buffer, start, stop)
63
+ ::Parser::Source::Comment.new(range)
64
+ end
65
+
66
+ def root_node(children, buffer)
67
+ range = ::Parser::Source::Range.new(buffer, 0, buffer.source.size)
68
+ location =
69
+ Parser::Source::Map::Definition.new(
70
+ range.begin,
71
+ range.begin,
72
+ range.begin,
73
+ range.end
74
+ )
75
+ ::Parser::AST::Node.new(:erb, children, location: location)
46
76
  end
47
77
  end
48
78
  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'\-]I18n\.|I18n\.)t(?:!|ranslate!?)?/.freeze
15
+ TRANSLATE_CALL_RE = /(?<=^|[^\p{L}'\-.]|[^\p{L}'-]I18n\.|I18n\.)t(?:!|ranslate!?)?/.freeze
16
16
  IGNORE_LINES = {
17
17
  'coffee' => /^\s*#(?!\si18n-tasks-use)/,
18
18
  'erb' => /^\s*<%\s*#(?!\si18n-tasks-use)/,
@@ -61,7 +61,7 @@ module I18n
61
61
  "#{file_key.sub(/_controller$/, '')}.#{calling_method}"
62
62
  else
63
63
  # Remove _ prefix from partials
64
- file_key.gsub(/\._/, DOT)
64
+ file_key.gsub('._', DOT)
65
65
  end
66
66
  end
67
67
  end
@@ -3,7 +3,9 @@
3
3
  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
+ require 'i18n/tasks/scanners/ast_matchers/default_i18n_subject_matcher'
6
7
  require 'i18n/tasks/scanners/ast_matchers/message_receivers_matcher'
8
+ require 'i18n/tasks/scanners/ast_matchers/rails_model_matcher'
7
9
  require 'parser/current'
8
10
 
9
11
  module I18n::Tasks::Scanners
@@ -79,7 +81,7 @@ module I18n::Tasks::Scanners
79
81
  results = []
80
82
 
81
83
  # method_name is not available at this stage
82
- calls.each do |send_node, _method_name|
84
+ calls.each do |(send_node, _method_name)|
83
85
  @matchers.each do |matcher|
84
86
  result = matcher.convert_to_key_occurrences(
85
87
  send_node,
@@ -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
@@ -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 list_slice, opts.merge(is_html ? options_for_html : options_for_plain)
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,39 @@ 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
- from_values(list, translate_values(to_values(list), **options_for_translate_values(**opts))).tap do |result|
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 l[1] }.flatten.compact
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 v }
72
+ value.map { |v| dump_value(v, opts) }
73
+ when Hash
74
+ # dump recursively
75
+ value.values.map { |v| dump_value(v, opts) }
71
76
  when String
77
+ value = CGI.escapeHTML(value) if opts[:html_escape]
72
78
  replace_interpolations value unless value.empty?
73
79
  end
74
80
  end
@@ -77,16 +83,21 @@ module I18n::Tasks
77
83
  # @param [Object] untranslated
78
84
  # @param [Enumerator] each_translated
79
85
  # @return [Object] final translated value
80
- def parse_value(untranslated, each_translated)
86
+ def parse_value(untranslated, each_translated, opts)
81
87
  case untranslated
82
88
  when Array
83
89
  # implode array
84
- untranslated.map { |from| parse_value(from, each_translated) }
90
+ untranslated.map { |from| parse_value(from, each_translated, opts) }
91
+ when Hash
92
+ # implode hash
93
+ untranslated.transform_values { |value| parse_value(value, each_translated, opts) }
85
94
  when String
86
95
  if untranslated.empty?
87
96
  untranslated
88
97
  else
89
- restore_interpolations untranslated, each_translated.next
98
+ value = each_translated.next
99
+ value = CGI.unescapeHTML(value) if opts[:html_escape]
100
+ restore_interpolations(untranslated, value)
90
101
  end
91
102
  else
92
103
  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,27 @@ module I18n::Tasks::Translators
17
22
  protected
18
23
 
19
24
  def translate_values(list, from:, to:, **options)
20
- result = DeepL.translate(list, to_deepl_compatible_locale(from), to_deepl_compatible_locale(to), options)
21
- if result.is_a?(DeepL::Resources::Text)
22
- [result.text]
23
- else
24
- result.map(&:text)
25
+ results = []
26
+ list.each_slice(BATCH_SIZE) do |parts|
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
+ )
33
+ if res.is_a?(DeepL::Resources::Text)
34
+ results << res.text
35
+ else
36
+ results += res.map(&:text)
37
+ end
25
38
  end
39
+ results
26
40
  end
27
41
 
28
42
  def options_for_translate_values(**options)
29
- { ignore_tags: %w[i18n] }.merge(options)
43
+ extra_options = @i18n_tasks.translation_config[:deepl_options]&.symbolize_keys || {}
44
+
45
+ extra_options.merge({ ignore_tags: %w[i18n] }).merge(options)
30
46
  end
31
47
 
32
48
  def options_for_html
@@ -34,7 +50,7 @@ module I18n::Tasks::Translators
34
50
  end
35
51
 
36
52
  def options_for_plain
37
- { preserve_formatting: true }
53
+ { preserve_formatting: true, tag_handling: 'xml', html_escape: true }
38
54
  end
39
55
 
40
56
  # @param [String] value
@@ -60,11 +76,23 @@ module I18n::Tasks::Translators
60
76
 
61
77
  private
62
78
 
63
- # Convert 'es-ES' to 'ES'
64
- def to_deepl_compatible_locale(locale)
79
+ # Convert 'es-ES' to 'ES', en-us to EN
80
+ def to_deepl_source_locale(locale)
65
81
  locale.to_s.split('-', 2).first.upcase
66
82
  end
67
83
 
84
+ # Convert 'es-ES' to 'ES' but warn about locales requiring a specific variant
85
+ def to_deepl_target_locale(locale)
86
+ loc, sub = locale.to_s.split('-')
87
+ if SPECIFIC_TARGETS.include?(loc)
88
+ # Must see how the deepl api evolves, so this could be an error in the future
89
+ warn_deprecated I18n.t('i18n_tasks.deepl_translate.errors.specific_target_missing') unless sub
90
+ locale.to_s.upcase
91
+ else
92
+ loc.upcase
93
+ end
94
+ end
95
+
68
96
  def configure_api_key!
69
97
  api_key = @i18n_tasks.translation_config[:deepl_api_key]
70
98
  host = @i18n_tasks.translation_config[:deepl_host]
@@ -77,5 +105,26 @@ module I18n::Tasks::Translators
77
105
  config.version = version unless version.blank?
78
106
  end
79
107
  end
108
+
109
+ def options_with_glossary(options, from, to)
110
+ glossary = find_glossary(from, to)
111
+ glossary ? { glossary_id: glossary.id }.merge(options) : options
112
+ end
113
+
114
+ def all_ready_glossaries
115
+ @all_ready_glossaries ||= DeepL.glossaries.list
116
+ end
117
+
118
+ def find_glossary(from, to)
119
+ config_glossary_ids = @i18n_tasks.translation_config[:deepl_glossary_ids]
120
+ return unless config_glossary_ids
121
+
122
+ all_ready_glossaries.find do |glossary|
123
+ glossary.ready \
124
+ && glossary.source_lang == from \
125
+ && glossary.target_lang == to \
126
+ && config_glossary_ids.include?(glossary.id)
127
+ end
128
+ end
80
129
  end
81
130
  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,21 @@ module I18n::Tasks::Translators
16
17
  protected
17
18
 
18
19
  def translate_values(list, **options)
19
- EasyTranslate.translate(list, options)
20
+ restore_newlines(
21
+ EasyTranslate.translate(
22
+ replace_newlines_with_placeholder(list, options[:html]),
23
+ options,
24
+ format: :text
25
+ ),
26
+ options[:html]
27
+ )
20
28
  end
21
29
 
22
30
  def options_for_translate_values(from:, to:, **options)
23
31
  options.merge(
24
32
  api_key: api_key,
25
- from: to_google_translate_compatible_locale(from),
26
- to: to_google_translate_compatible_locale(to)
33
+ from: from,
34
+ to: to
27
35
  )
28
36
  end
29
37
 
@@ -41,21 +49,12 @@ module I18n::Tasks::Translators
41
49
 
42
50
  private
43
51
 
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
52
  def api_key
54
53
  @api_key ||= begin
55
54
  key = @i18n_tasks.translation_config[:google_translate_api_key]
56
55
  # fallback with deprecation warning
57
56
  if @i18n_tasks.translation_config[:api_key]
58
- @i18n_tasks.warn_deprecated(
57
+ warn_deprecated(
59
58
  'Please rename Google Translate API Key from `api_key` to `google_translate_api_key`.'
60
59
  )
61
60
  key ||= translation_config[:api_key]
@@ -65,5 +64,21 @@ module I18n::Tasks::Translators
65
64
  key
66
65
  end
67
66
  end
67
+
68
+ def replace_newlines_with_placeholder(list, html)
69
+ return list unless html
70
+
71
+ list.map do |value|
72
+ value.gsub("\n", NEWLINE_PLACEHOLDER)
73
+ end
74
+ end
75
+
76
+ def restore_newlines(translations, html)
77
+ return translations unless html
78
+
79
+ translations.map do |translation|
80
+ translation.gsub("#{NEWLINE_PLACEHOLDER} ", "\n")
81
+ end
82
+ end
68
83
  end
69
84
  end