i18n-tasks 1.0.15 → 1.1.0

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +85 -13
  3. data/Rakefile +4 -4
  4. data/bin/i18n-tasks +3 -3
  5. data/config/locales/en.yml +6 -0
  6. data/config/locales/ru.yml +7 -0
  7. data/i18n-tasks.gemspec +28 -41
  8. data/lib/i18n/tasks/base_task.rb +19 -19
  9. data/lib/i18n/tasks/cli.rb +37 -30
  10. data/lib/i18n/tasks/command/collection.rb +4 -4
  11. data/lib/i18n/tasks/command/commander.rb +5 -5
  12. data/lib/i18n/tasks/command/commands/check_prism.rb +126 -0
  13. data/lib/i18n/tasks/command/commands/data.rb +33 -33
  14. data/lib/i18n/tasks/command/commands/eq_base.rb +3 -3
  15. data/lib/i18n/tasks/command/commands/health.rb +6 -5
  16. data/lib/i18n/tasks/command/commands/interpolations.rb +14 -3
  17. data/lib/i18n/tasks/command/commands/meta.rb +6 -6
  18. data/lib/i18n/tasks/command/commands/missing.rb +25 -25
  19. data/lib/i18n/tasks/command/commands/tree.rb +33 -33
  20. data/lib/i18n/tasks/command/commands/usages.rb +24 -24
  21. data/lib/i18n/tasks/command/dsl.rb +1 -1
  22. data/lib/i18n/tasks/command/option_parsers/enum.rb +5 -5
  23. data/lib/i18n/tasks/command/option_parsers/locale.rb +4 -4
  24. data/lib/i18n/tasks/command/options/common.rb +16 -16
  25. data/lib/i18n/tasks/command/options/data.rb +18 -18
  26. data/lib/i18n/tasks/command/options/locales.rb +32 -32
  27. data/lib/i18n/tasks/commands.rb +14 -12
  28. data/lib/i18n/tasks/concurrent/cache.rb +1 -1
  29. data/lib/i18n/tasks/concurrent/cached_value.rb +1 -1
  30. data/lib/i18n/tasks/configuration.rb +22 -21
  31. data/lib/i18n/tasks/console_context.rb +11 -11
  32. data/lib/i18n/tasks/data/adapter/json_adapter.rb +1 -1
  33. data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +5 -5
  34. data/lib/i18n/tasks/data/file_formats.rb +3 -3
  35. data/lib/i18n/tasks/data/file_system.rb +5 -5
  36. data/lib/i18n/tasks/data/file_system_base.rb +26 -26
  37. data/lib/i18n/tasks/data/language_names.rb +202 -0
  38. data/lib/i18n/tasks/data/router/conservative_router.rb +3 -3
  39. data/lib/i18n/tasks/data/router/isolating_router.rb +19 -19
  40. data/lib/i18n/tasks/data/router/pattern_router.rb +5 -5
  41. data/lib/i18n/tasks/data/tree/node.rb +27 -27
  42. data/lib/i18n/tasks/data/tree/nodes.rb +10 -10
  43. data/lib/i18n/tasks/data/tree/siblings.rb +20 -20
  44. data/lib/i18n/tasks/data/tree/traversal.rb +5 -5
  45. data/lib/i18n/tasks/data.rb +4 -4
  46. data/lib/i18n/tasks/html_keys.rb +2 -2
  47. data/lib/i18n/tasks/ignore_keys.rb +9 -9
  48. data/lib/i18n/tasks/interpolations.rb +21 -1
  49. data/lib/i18n/tasks/key_pattern_matching.rb +8 -8
  50. data/lib/i18n/tasks/logging.rb +2 -1
  51. data/lib/i18n/tasks/missing_keys.rb +24 -8
  52. data/lib/i18n/tasks/plural_keys.rb +6 -4
  53. data/lib/i18n/tasks/references.rb +4 -4
  54. data/lib/i18n/tasks/reports/base.rb +18 -14
  55. data/lib/i18n/tasks/reports/terminal.rb +64 -47
  56. data/lib/i18n/tasks/scanners/ast_matchers/base_matcher.rb +3 -3
  57. data/lib/i18n/tasks/scanners/ast_matchers/default_i18n_subject_matcher.rb +3 -3
  58. data/lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb +10 -10
  59. data/lib/i18n/tasks/scanners/ast_matchers/rails_model_matcher.rb +1 -1
  60. data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +69 -10
  61. data/lib/i18n/tasks/scanners/file_scanner.rb +5 -5
  62. data/lib/i18n/tasks/scanners/files/caching_file_finder.rb +3 -3
  63. data/lib/i18n/tasks/scanners/files/caching_file_finder_provider.rb +3 -3
  64. data/lib/i18n/tasks/scanners/files/caching_file_reader.rb +2 -2
  65. data/lib/i18n/tasks/scanners/files/file_finder.rb +8 -8
  66. data/lib/i18n/tasks/scanners/files/file_reader.rb +1 -1
  67. data/lib/i18n/tasks/scanners/local_ruby_parser.rb +8 -8
  68. data/lib/i18n/tasks/scanners/occurrence_from_position.rb +1 -1
  69. data/lib/i18n/tasks/scanners/pattern_mapper.rb +7 -7
  70. data/lib/i18n/tasks/scanners/pattern_scanner.rb +20 -20
  71. data/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb +8 -8
  72. data/lib/i18n/tasks/scanners/prism_scanners/arguments_visitor.rb +8 -1
  73. data/lib/i18n/tasks/scanners/prism_scanners/nodes.rb +101 -61
  74. data/lib/i18n/tasks/scanners/prism_scanners/visitor.rb +169 -105
  75. data/lib/i18n/tasks/scanners/relative_keys.rb +8 -8
  76. data/lib/i18n/tasks/scanners/results/key_occurrences.rb +3 -3
  77. data/lib/i18n/tasks/scanners/results/occurrence.rb +14 -10
  78. data/lib/i18n/tasks/scanners/ruby_ast_call_finder.rb +1 -1
  79. data/lib/i18n/tasks/scanners/ruby_key_literals.rb +6 -6
  80. data/lib/i18n/tasks/scanners/ruby_parser_factory.rb +1 -1
  81. data/lib/i18n/tasks/scanners/ruby_scanner.rb +225 -0
  82. data/lib/i18n/tasks/scanners/scanner.rb +2 -2
  83. data/lib/i18n/tasks/scanners/scanner_multiplexer.rb +1 -1
  84. data/lib/i18n/tasks/split_key.rb +4 -4
  85. data/lib/i18n/tasks/stats.rb +3 -3
  86. data/lib/i18n/tasks/translation.rb +5 -5
  87. data/lib/i18n/tasks/translators/base_translator.rb +40 -14
  88. data/lib/i18n/tasks/translators/deepl_translator.rb +17 -14
  89. data/lib/i18n/tasks/translators/google_translator.rb +169 -25
  90. data/lib/i18n/tasks/translators/openai_translator.rb +34 -23
  91. data/lib/i18n/tasks/translators/watsonx_translator.rb +16 -16
  92. data/lib/i18n/tasks/translators/yandex_translator.rb +8 -8
  93. data/lib/i18n/tasks/unused_keys.rb +1 -1
  94. data/lib/i18n/tasks/used_keys.rb +32 -33
  95. data/lib/i18n/tasks/version.rb +1 -1
  96. data/lib/i18n/tasks.rb +17 -17
  97. data/templates/config/i18n-tasks.yml +12 -0
  98. data/templates/minitest/i18n_test.rb +3 -3
  99. data/templates/rspec/i18n_spec.rb +7 -7
  100. metadata +25 -185
  101. data/lib/i18n/tasks/scanners/prism_scanner.rb +0 -83
  102. data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +0 -145
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/data/tree/traversal'
3
+ require "i18n/tasks/data/tree/traversal"
4
4
  module I18n::Tasks::Data::Tree
5
5
  # A list of nodes
6
6
  class Nodes
@@ -20,7 +20,7 @@ module I18n::Tasks::Data::Tree
20
20
  end
21
21
 
22
22
  def attributes
23
- { nodes: @list }
23
+ {nodes: @list}
24
24
  end
25
25
 
26
26
  def derive(new_attr = {})
@@ -32,10 +32,10 @@ module I18n::Tasks::Data::Tree
32
32
 
33
33
  def to_hash(sort = false)
34
34
  (@hash ||= {})[sort] ||= if sort
35
- sort_by(&:key)
36
- else
37
- self
38
- end.map { |node| node.to_hash(sort) }.reduce({}, :deep_merge!)
35
+ sort_by(&:key)
36
+ else
37
+ self
38
+ end.map { |node| node.to_hash(sort) }.reduce({}, :deep_merge!)
39
39
  end
40
40
 
41
41
  delegate :to_json, to: :to_hash
@@ -45,7 +45,7 @@ module I18n::Tasks::Data::Tree
45
45
  if present?
46
46
  map(&:inspect) * "\n"
47
47
  else
48
- Rainbow('{∅}').faint
48
+ Rainbow("{∅}").faint
49
49
  end
50
50
  end
51
51
 
@@ -67,14 +67,14 @@ module I18n::Tasks::Data::Tree
67
67
  derive.append!(other)
68
68
  end
69
69
 
70
- alias << append
70
+ alias_method :<<, :append
71
71
 
72
72
  def merge!(nodes)
73
73
  @list += nodes.to_a
74
74
  dirty!
75
75
  self
76
76
  end
77
- alias + merge!
77
+ alias_method :+, :merge!
78
78
 
79
79
  def children(&block)
80
80
  return to_enum(:children) { map { |c| c.children ? c.children.size : 0 }.reduce(:+) } unless block
@@ -84,7 +84,7 @@ module I18n::Tasks::Data::Tree
84
84
  end
85
85
  end
86
86
 
87
- alias children? any?
87
+ alias_method :children?, :any?
88
88
 
89
89
  protected
90
90
 
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
- require 'i18n/tasks/split_key'
5
- require 'i18n/tasks/data/tree/nodes'
3
+ require "set"
4
+ require "i18n/tasks/split_key"
5
+ require "i18n/tasks/data/tree/nodes"
6
6
 
7
7
  module I18n::Tasks::Data::Tree
8
8
  # Siblings represents a subtree sharing a common parent
@@ -17,7 +17,7 @@ module I18n::Tasks::Data::Tree
17
17
  def initialize(opts = {})
18
18
  super(nodes: opts[:nodes])
19
19
  @parent = opts[:parent] || first.try(:parent)
20
- @list.map! { |node| node.parent == @parent ? node : node.derive(parent: @parent) }
20
+ @list.map! { |node| (node.parent == @parent) ? node : node.derive(parent: @parent) }
21
21
  @key_to_node = @list.each_with_object({}) { |node, h| h[node.key] = node }
22
22
  @warn_about_add_children_to_leaf = opts.fetch(:warn_about_add_children_to_leaf, true)
23
23
  end
@@ -52,7 +52,7 @@ module I18n::Tasks::Data::Tree
52
52
  new_key = to_pattern.gsub(/\\\d+/) { |m| match[m[1..].to_i] }
53
53
  old_key_to_new_key[full_key] = new_key
54
54
  moved_forest.merge!(Siblings.new.tap do |forest|
55
- forest[[(node.root.try(:key) unless root), new_key].compact.join('.')] =
55
+ forest[[(node.root.try(:key) unless root), new_key].compact.join(".")] =
56
56
  node.derive(key: split_key(new_key).last)
57
57
  end)
58
58
  end
@@ -62,10 +62,10 @@ module I18n::Tasks::Data::Tree
62
62
  nodes do |node|
63
63
  next unless node.reference?
64
64
 
65
- old_target = [(node.root.key if root), node.value.to_s].compact.join('.')
65
+ old_target = [(node.root.key if root), node.value.to_s].compact.join(".")
66
66
  new_target = old_key_to_new_key[old_target]
67
67
  if new_target
68
- new_target = new_target.sub(/\A[^.]*\./, '') if root
68
+ new_target = new_target.sub(/\A[^.]*\./, "") if root
69
69
  node.value = new_target.to_sym
70
70
  end
71
71
  end
@@ -75,26 +75,26 @@ module I18n::Tasks::Data::Tree
75
75
  end
76
76
 
77
77
  def replace_node!(node, new_node)
78
- @list[@list.index(node)] = new_node
78
+ @list[@list.index(node)] = new_node
79
79
  key_to_node[new_node.key] = new_node
80
80
  end
81
81
 
82
82
  # @return [Node] by full key
83
83
  def get(full_key)
84
84
  first_key, rest = split_key(full_key.to_s, 2)
85
- node = key_to_node[first_key]
85
+ node = key_to_node[first_key]
86
86
  node = node.children.try(:get, rest) if rest && node
87
87
  node
88
88
  end
89
89
 
90
- alias [] get
90
+ alias_method :[], :get
91
91
 
92
92
  # add or replace node by full key
93
93
  def set(full_key, node)
94
- fail 'value should be a I18n::Tasks::Data::Tree::Node' unless node.is_a?(Node)
94
+ fail "value should be a I18n::Tasks::Data::Tree::Node" unless node.is_a?(Node)
95
95
 
96
96
  key_part, rest = split_key(full_key, 2)
97
- child = key_to_node[key_part]
97
+ child = key_to_node[key_part]
98
98
 
99
99
  if rest
100
100
  unless child
@@ -119,7 +119,7 @@ module I18n::Tasks::Data::Tree
119
119
  node
120
120
  end
121
121
 
122
- alias []= set
122
+ alias_method :[]=, :set
123
123
 
124
124
  # methods below change state
125
125
 
@@ -133,9 +133,9 @@ module I18n::Tasks::Data::Tree
133
133
  nodes = nodes.map do |node|
134
134
  fail "already has a child with key '#{node.key}'" if key_to_node.key?(node.key)
135
135
 
136
- key_to_node[node.key] = (node.parent == parent ? node : node.derive(parent: parent))
136
+ key_to_node[node.key] = ((node.parent == parent) ? node : node.derive(parent: parent))
137
137
  end
138
- super(nodes)
138
+ super
139
139
  self
140
140
  end
141
141
 
@@ -267,7 +267,7 @@ module I18n::Tasks::Data::Tree
267
267
  key_occurrences.each do |key_occurrence|
268
268
  forest[key_occurrence.key] = ::I18n::Tasks::Data::Tree::Node.new(
269
269
  key: split_key(key_occurrence.key).last,
270
- data: { occurrences: key_occurrence.occurrences }
270
+ data: {occurrences: key_occurrence.occurrences}
271
271
  )
272
272
  end
273
273
  end
@@ -276,9 +276,9 @@ module I18n::Tasks::Data::Tree
276
276
  def from_key_attr(key_attrs, opts = {}, &block)
277
277
  build_forest(opts) do |forest|
278
278
  key_attrs.each do |(full_key, attr)|
279
- fail "Invalid key #{full_key.inspect}" if full_key.end_with?('.')
279
+ fail "Invalid key #{full_key.inspect}" if full_key.end_with?(".")
280
280
 
281
- node = ::I18n::Tasks::Data::Tree::Node.new(**attr.merge(key: split_key(full_key).last))
281
+ node = ::I18n::Tasks::Data::Tree::Node.new(**attr, key: split_key(full_key).last)
282
282
  yield(full_key, node) if block
283
283
  forest[full_key] = node
284
284
  end
@@ -305,7 +305,7 @@ module I18n::Tasks::Data::Tree
305
305
  Siblings.new(opts)
306
306
  end
307
307
 
308
- alias [] from_nested_hash
308
+ alias_method :[], :from_nested_hash
309
309
 
310
310
  # build forest from [[Full Key, Value]]
311
311
  def from_flat_pairs(pairs)
@@ -323,7 +323,7 @@ module I18n::Tasks::Data::Tree
323
323
  opts[:parent] = ::I18n::Tasks::Data::Tree::Node.new(opts[:parent_attr]) if opts[:parent_attr]
324
324
  if opts[:parent_locale]
325
325
  opts[:parent] = ::I18n::Tasks::Data::Tree::Node.new(
326
- key: opts[:parent_locale], data: { locale: opts[:parent_locale] }
326
+ key: opts[:parent_locale], data: {locale: opts[:parent_locale]}
327
327
  )
328
328
  end
329
329
  end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
3
+ require "set"
4
4
 
5
5
  module I18n::Tasks
6
6
  module Data::Tree
7
7
  # Any Enumerable that yields nodes can mix in this module
8
8
  module Traversal # rubocop:disable Metrics/ModuleLength
9
- def nodes(&block)
10
- depth_first(&block)
9
+ def nodes(&)
10
+ depth_first(&)
11
11
  end
12
12
 
13
13
  def leaves(&visitor)
@@ -76,13 +76,13 @@ module I18n::Tasks
76
76
 
77
77
  def root_key_values(sort = false)
78
78
  result = keys(root: false).map { |key, node| [node.root.key, key, node.value] }
79
- result.sort! { |a, b| a[0] == b[0] ? a[1] <=> b[1] : a[0] <=> b[0] } if sort
79
+ result.sort! { |a, b| (a[0] == b[0]) ? a[1] <=> b[1] : a[0] <=> b[0] } if sort
80
80
  result
81
81
  end
82
82
 
83
83
  def root_key_value_data(sort = false)
84
84
  result = keys(root: false).map { |key, node| [node.root.key, key, node.value, node.data] }
85
- result.sort! { |a, b| a[0] == b[0] ? a[1] <=> b[1] : a[0] <=> b[0] } if sort
85
+ result.sort! { |a, b| (a[0] == b[0]) ? a[1] <=> b[1] : a[0] <=> b[0] } if sort
86
86
  result
87
87
  end
88
88
 
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/data/file_system'
3
+ require "i18n/tasks/data/file_system"
4
4
 
5
5
  module I18n::Tasks
6
6
  module Data
7
7
  DATA_DEFAULTS = {
8
- adapter: 'I18n::Tasks::Data::FileSystem'
8
+ adapter: "I18n::Tasks::Data::FileSystem"
9
9
  }.freeze
10
10
 
11
11
  # I18n data provider
@@ -17,7 +17,7 @@ module I18n::Tasks
17
17
  data_config[:locales] = config[:locales]
18
18
  adapter_class = data_config[:adapter].presence || data_config[:class].presence || DATA_DEFAULTS[:adapter]
19
19
  adapter_class = adapter_class.to_s
20
- adapter_class = 'I18n::Tasks::Data::FileSystem' if adapter_class == 'file_system'
20
+ adapter_class = "I18n::Tasks::Data::FileSystem" if adapter_class == "file_system"
21
21
  data_config.except!(:adapter, :class)
22
22
  ActiveSupport::Inflector.constantize(adapter_class).new data_config
23
23
  end
@@ -50,7 +50,7 @@ module I18n::Tasks
50
50
  end
51
51
 
52
52
  def t_proc(locale = base_locale)
53
- @t_proc ||= {}
53
+ @t_proc ||= {}
54
54
  @t_proc[locale] ||= proc { |key| t(key, locale) }
55
55
  end
56
56
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  module I18n::Tasks
4
4
  module HtmlKeys
5
- HTML_KEY_PATTERN = /[.\-_]html\z/.freeze
6
- MAYBE_PLURAL_HTML_KEY_PATTERN = /[.\-_]html\.[^.]+\z/.freeze
5
+ HTML_KEY_PATTERN = /[.\-_]html\z/
6
+ MAYBE_PLURAL_HTML_KEY_PATTERN = /[.\-_]html\.[^.]+\z/
7
7
 
8
8
  def html_key?(full_key, locale)
9
9
  !!(full_key =~ HTML_KEY_PATTERN ||
@@ -12,19 +12,19 @@ module I18n::Tasks::IgnoreKeys
12
12
  # @param locale [String] only when type is :eq_base
13
13
  # @return [Regexp] a regexp that matches all the keys ignored for the type (and locale)
14
14
  def ignore_pattern(type, locale = nil)
15
- @ignore_patterns ||= HashWithIndifferentAccess.new
16
- @ignore_patterns[type] ||= {}
15
+ @ignore_patterns ||= HashWithIndifferentAccess.new
16
+ @ignore_patterns[type] ||= {}
17
17
  @ignore_patterns[type][locale] ||= begin
18
18
  global = ignore_config.presence || []
19
19
  type_ignore = ignore_config(type).presence || []
20
20
  patterns = case type_ignore
21
- when Array
22
- global + type_ignore
23
- when Hash
24
- # ignore per locale
25
- global + (type_ignore['all'] || []) +
26
- type_ignore.select { |k, _v| k.to_s =~ /\b#{locale}\b/ }.values.flatten(1).compact
27
- end
21
+ when Array
22
+ global + type_ignore
23
+ when Hash
24
+ # ignore per locale
25
+ global + (type_ignore["all"] || []) +
26
+ type_ignore.select { |k, _v| k.to_s =~ /\b#{locale}\b/ }.values.flatten(1).compact
27
+ end
28
28
  compile_patterns_re patterns
29
29
  end
30
30
  end
@@ -5,7 +5,7 @@ module I18n::Tasks
5
5
  class << self
6
6
  attr_accessor :variable_regex
7
7
  end
8
- @variable_regex = /(?<!%)%{[^}]+}/.freeze
8
+ @variable_regex = /(?<!%)%{[^}]+}/
9
9
 
10
10
  def inconsistent_interpolations(locales: nil, base_locale: nil) # rubocop:disable Metrics/AbcSize
11
11
  locales ||= self.locales
@@ -30,5 +30,25 @@ module I18n::Tasks
30
30
  result.each { |root| root.data[:type] = :inconsistent_interpolations }
31
31
  result
32
32
  end
33
+
34
+ def reserved_interpolations(locales: nil)
35
+ locales ||= self.locales
36
+ result = empty_forest
37
+
38
+ locales.each do |current_locale|
39
+ data[current_locale].key_values.each do |key, value|
40
+ next unless value.is_a?(String)
41
+
42
+ reserved_variables = value.scan(::I18n.reserved_keys_pattern).flatten
43
+ next if reserved_variables.empty?
44
+
45
+ result.merge!(data[current_locale].first.children[key].walk_to_root.reduce(nil) do |c, p|
46
+ [p.derive(children: c, value: reserved_variables)]
47
+ end)
48
+ end
49
+ end
50
+ result.each { |root| root.data[:type] = :inconsistent_interpolations }
51
+ result
52
+ end
33
53
  end
34
54
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'strscan'
3
+ require "strscan"
4
4
 
5
5
  module I18n::Tasks::KeyPatternMatching
6
6
  extend self # rubocop:disable Style/ModuleFunction
7
7
 
8
- MATCH_NOTHING = /\z\A/.freeze
8
+ MATCH_NOTHING = /\z\A/
9
9
 
10
10
  # one regex to match any
11
11
  def compile_patterns_re(key_patterns)
@@ -13,7 +13,7 @@ module I18n::Tasks::KeyPatternMatching
13
13
  # match nothing
14
14
  MATCH_NOTHING
15
15
  else
16
- /(?:#{key_patterns.map { |p| compile_key_pattern p } * '|'})/m
16
+ /(?:#{key_patterns.map { |p| compile_key_pattern p } * "|"})/m
17
17
  end
18
18
  end
19
19
 
@@ -31,10 +31,10 @@ module I18n::Tasks::KeyPatternMatching
31
31
 
32
32
  def key_pattern_re_body(key_pattern)
33
33
  key_pattern
34
- .gsub('.', '\.')
35
- .gsub('*:', '[^.]+?')
36
- .gsub('*', '.*')
37
- .gsub(':', '(?<=^|\.)[^.]+?(?=\.|$)')
38
- .gsub(/\{(.*?)}/) { "(#{Regexp.last_match(1).strip.gsub(/\s*,\s*/, '|')})" }
34
+ .gsub(".", '\.')
35
+ .gsub("*:", "[^.]+?")
36
+ .gsub("*", ".*")
37
+ .gsub(":", '(?<=^|\.)[^.]+?(?=\.|$)')
38
+ .gsub(/\{(.*?)}/) { "(#{Regexp.last_match(1).strip.gsub(/\s*,\s*/, "|")})" }
39
39
  end
40
40
  end
@@ -25,7 +25,8 @@ module I18n::Tasks::Logging
25
25
  def log_stderr(*args)
26
26
  # We don't want output from different threads to get intermixed.
27
27
  MUTEX.synchronize do
28
- $stderr.puts(*args)
28
+ # Use $stderr directly to avoid issues with JRuby and thread safety
29
+ $stderr.puts(*args) # rubocop:disable Style/StderrPuts
29
30
  end
30
31
  end
31
32
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
3
+ require "set"
4
4
  module I18n::Tasks
5
5
  module MissingKeys # rubocop:disable Metrics/ModuleLength
6
6
  MISSING_TYPES = %w[
@@ -21,7 +21,7 @@ module I18n::Tasks
21
21
  # @return [Siblings]
22
22
  def missing_keys(locales: nil, types: nil, base_locale: nil)
23
23
  locales ||= self.locales
24
- types ||= missing_keys_types
24
+ types ||= missing_keys_types
25
25
  base = base_locale || self.base_locale
26
26
  types.inject(empty_forest) do |f, type|
27
27
  f.merge! send(:"missing_#{type}_forest", locales, base)
@@ -88,7 +88,7 @@ module I18n::Tasks
88
88
 
89
89
  # Loads rails-i18n pluralization config for the given locale.
90
90
  def load_rails_i18n_pluralization!(locale)
91
- path = File.join(Gem::Specification.find_by_name('rails-i18n').gem_dir, 'rails', 'pluralization', "#{locale}.rb")
91
+ path = File.join(Gem::Specification.find_by_name("rails-i18n").gem_dir, "rails", "pluralization", "#{locale}.rb")
92
92
  eval(File.read(path), binding, path) # rubocop:disable Security/Eval
93
93
  end
94
94
 
@@ -98,7 +98,7 @@ module I18n::Tasks
98
98
  locale_key_missing? locale, depluralize_key(key, compared_to)
99
99
  end.set_root_key!(locale, type: :missing_diff).keys do |_key, node|
100
100
  # change path and locale to base
101
- data = { locale: locale, missing_diff_locale: node.data[:locale] }
101
+ data = {locale: locale, missing_diff_locale: node.data[:locale]}
102
102
  if node.data.key?(:path)
103
103
  data[:path] = LocalePathname.replace_locale(node.data[:path], node.data[:locale], locale)
104
104
  end
@@ -108,8 +108,24 @@ module I18n::Tasks
108
108
 
109
109
  # keys used in the code missing translations in locale
110
110
  def missing_used_tree(locale)
111
- used_tree(strict: true).select_keys do |key, _node|
112
- locale_key_missing?(locale, key)
111
+ used_tree(strict: true).select_keys do |key, node|
112
+ occurrences = node.data[:occurrences] || []
113
+
114
+ # An occurrence may carry candidate keys (for relative lookups). If any
115
+ # candidate key exists in the locale, the usage is considered present.
116
+ occurrences_all_missing = occurrences.all? do |occ|
117
+ candidates = if occ.respond_to?(:candidate_keys) && occ.candidate_keys.present?
118
+ occ.candidate_keys
119
+ else
120
+ # fallback to the scanned key
121
+ [key]
122
+ end
123
+
124
+ # Occurrence is missing iff all its candidates are missing
125
+ candidates.all? { |c| locale_key_missing?(locale, c) }
126
+ end
127
+
128
+ occurrences_all_missing
113
129
  end.set_root_key!(locale, type: :missing_used)
114
130
  end
115
131
 
@@ -130,7 +146,7 @@ module I18n::Tasks
130
146
  # @yieldreturn [Boolean] whether to collapse the node
131
147
  def collapse_same_key_in_locales!(forest)
132
148
  locales_and_node_by_key = {}
133
- to_remove = []
149
+ to_remove = []
134
150
  forest.each do |root|
135
151
  locale = root.key
136
152
  root.keys do |key, node|
@@ -146,7 +162,7 @@ module I18n::Tasks
146
162
  end
147
163
  forest.remove_nodes_and_emptied_ancestors! to_remove
148
164
  locales_and_node_by_key.each_with_object({}) do |(key, (locales, node)), inv|
149
- (inv[locales.sort.join('+')] ||= []) << [key, node]
165
+ (inv[locales.sort.join("+")] ||= []) << [key, node]
150
166
  end.map do |locales, keys_nodes|
151
167
  keys_nodes.each do |(key, node)|
152
168
  forest["#{locales}.#{key}"] = node
@@ -1,18 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
3
+ require "set"
4
4
  module I18n::Tasks::PluralKeys
5
5
  # Ref: http://cldr.unicode.org/index/cldr-spec/plural-rules
6
6
  CLDR_CATEGORY_KEYS = %w[zero one two few many other].freeze
7
7
  PLURAL_KEY_SUFFIXES = Set.new CLDR_CATEGORY_KEYS
8
- PLURAL_KEY_RE = /\.(?:#{CLDR_CATEGORY_KEYS * '|'})$/.freeze
8
+ PLURAL_KEY_RE = /\.(?:#{CLDR_CATEGORY_KEYS * "|"})$/
9
+ # Ref: https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
10
+ EXPLICIT_0_1 = %w[0 1].freeze
9
11
 
10
12
  def collapse_plural_nodes!(tree)
11
13
  tree.leaves.map(&:parent).compact.uniq.each do |node|
12
14
  children = node.children
13
15
  next unless plural_forms?(children)
14
16
 
15
- node.value = children.to_hash
17
+ node.value = children.to_hash
16
18
  node.children = nil
17
19
  node.data.merge! children.first.data
18
20
  end
@@ -56,6 +58,6 @@ module I18n::Tasks::PluralKeys
56
58
  end
57
59
 
58
60
  def plural_suffix?(key)
59
- PLURAL_KEY_SUFFIXES.include?(key)
61
+ PLURAL_KEY_SUFFIXES.include?(key) || EXPLICIT_0_1.include?(key)
60
62
  end
61
63
  end
@@ -7,9 +7,9 @@ module I18n::Tasks
7
7
  # 2. Resolved references -- all the used references in their fully resolved form.
8
8
  # 3. Reference keys -- all the used reference keys.
9
9
  def process_references(usages,
10
- data_refs = merge_reference_trees(data_forest.select_keys { |_, node| node.reference? }))
11
- fail ArgumentError, 'usages must be a Data::Tree::Instance' unless usages.is_a?(Data::Tree::Siblings)
12
- fail ArgumentError, 'all_references must be a Data::Tree::Instance' unless data_refs.is_a?(Data::Tree::Siblings)
10
+ data_refs = merge_reference_trees(data_forest.select_keys { |_, node| node.reference? }))
11
+ fail ArgumentError, "usages must be a Data::Tree::Instance" unless usages.is_a?(Data::Tree::Siblings)
12
+ fail ArgumentError, "all_references must be a Data::Tree::Instance" unless data_refs.is_a?(Data::Tree::Siblings)
13
13
 
14
14
  raw_refs = empty_forest
15
15
  resolved_refs = empty_forest
@@ -90,7 +90,7 @@ 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: ' \
93
+ "Conflicting references: " \
94
94
  "#{node.full_key(root: false)} ⮕ #{node.value} in #{node.data[:locale]}, " \
95
95
  "but ⮕ #{other.value} in #{other.data[:locale]}"
96
96
  )
@@ -15,55 +15,59 @@ module I18n::Tasks::Reports
15
15
  protected
16
16
 
17
17
  def missing_type_info(type)
18
- ::I18n::Tasks::MissingKeys::MISSING_TYPES[type.to_s.sub(/\Amissing_/, '').to_sym]
18
+ ::I18n::Tasks::MissingKeys::MISSING_TYPES[type.to_s.sub(/\Amissing_/, "").to_sym]
19
19
  end
20
20
 
21
21
  def missing_title(forest)
22
- "Missing translations (#{forest.leaves.count || ''})"
22
+ "Missing translations (#{forest.leaves.count || ""})"
23
23
  end
24
24
 
25
25
  def inconsistent_interpolations_title(forest)
26
- "Inconsistent interpolations (#{forest.leaves.count || ''})"
26
+ "Inconsistent interpolations (#{forest.leaves.count || ""})"
27
+ end
28
+
29
+ def reserved_interpolations_title(forest)
30
+ "Reserved interpolations (#{forest.leaves.count || "∅"})"
27
31
  end
28
32
 
29
33
  def unused_title(key_values)
30
- "Unused keys (#{key_values.count || ''})"
34
+ "Unused keys (#{key_values.count || ""})"
31
35
  end
32
36
 
33
37
  def eq_base_title(key_values, locale = base_locale)
34
- "Same value as #{locale} (#{key_values.count || ''})"
38
+ "Same value as #{locale} (#{key_values.count || ""})"
35
39
  end
36
40
 
37
41
  def used_title(keys_nodes, filter)
38
42
  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}" \
40
- "#{" (#{used_n} usage#{'s' if used_n != 1})" if used_n.positive?}"
43
+ "#{keys_nodes.size} key#{"s" if keys_nodes.size != 1}#{" matching '#{filter}'" if filter}" \
44
+ "#{" (#{used_n} usage#{"s" if used_n != 1})" if used_n.positive?}"
41
45
  end
42
46
 
43
47
  # Sort keys by their attributes in order
44
48
  # @param [Hash] order e.g. {locale: :asc, type: :desc, key: :asc}
45
- def sort_by_attr!(objects, order = { locale: :asc, key: :asc })
49
+ def sort_by_attr!(objects, order = {locale: :asc, key: :asc})
46
50
  order_keys = order.keys
47
51
  objects.sort! do |a, b|
48
52
  by = order_keys.detect { |k| a[k] != b[k] }
49
- order[by] == :desc ? b[by] <=> a[by] : a[by] <=> b[by]
53
+ (order[by] == :desc) ? b[by] <=> a[by] : a[by] <=> b[by]
50
54
  end
51
55
  objects
52
56
  end
53
57
 
54
58
  def forest_to_attr(forest)
55
59
  forest.keys(root: false).map do |key, node|
56
- { key: key, value: node.value, type: node.data[:type], locale: node.root.key, data: node.data }
60
+ {key: key, value: node.value, type: node.data[:type], locale: node.root.key, data: node.data}
57
61
  end
58
62
  end
59
63
 
60
64
  def format_locale(locale)
61
- return '' unless locale
65
+ return "" unless locale
62
66
 
63
- if locale.split('+') == task.locales.sort
64
- 'all'
67
+ if locale.split("+") == task.locales.sort
68
+ "all"
65
69
  else
66
- locale.tr '+', ' '
70
+ locale.tr "+", " "
67
71
  end
68
72
  end
69
73