i18n-tasks 0.9.33 → 1.0.12

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -11
  3. data/config/locales/en.yml +5 -3
  4. data/config/locales/ru.yml +1 -0
  5. data/i18n-tasks.gemspec +12 -6
  6. data/lib/i18n/tasks/base_task.rb +2 -1
  7. data/lib/i18n/tasks/cli.rb +27 -17
  8. data/lib/i18n/tasks/command/commander.rb +1 -0
  9. data/lib/i18n/tasks/command/commands/data.rb +8 -6
  10. data/lib/i18n/tasks/command/commands/eq_base.rb +2 -2
  11. data/lib/i18n/tasks/command/commands/health.rb +4 -3
  12. data/lib/i18n/tasks/command/commands/interpolations.rb +1 -1
  13. data/lib/i18n/tasks/command/commands/meta.rb +1 -1
  14. data/lib/i18n/tasks/command/commands/missing.rb +22 -9
  15. data/lib/i18n/tasks/command/commands/tree.rb +8 -6
  16. data/lib/i18n/tasks/command/commands/usages.rb +5 -4
  17. data/lib/i18n/tasks/command/dsl.rb +4 -4
  18. data/lib/i18n/tasks/command/option_parsers/enum.rb +2 -0
  19. data/lib/i18n/tasks/command/option_parsers/locale.rb +2 -1
  20. data/lib/i18n/tasks/command/options/common.rb +5 -0
  21. data/lib/i18n/tasks/command/options/data.rb +4 -1
  22. data/lib/i18n/tasks/command/options/locales.rb +5 -5
  23. data/lib/i18n/tasks/concurrent/cached_value.rb +2 -2
  24. data/lib/i18n/tasks/configuration.rb +17 -10
  25. data/lib/i18n/tasks/console_context.rb +1 -1
  26. data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +9 -2
  27. data/lib/i18n/tasks/data/file_formats.rb +3 -1
  28. data/lib/i18n/tasks/data/file_system_base.rb +7 -6
  29. data/lib/i18n/tasks/data/router/conservative_router.rb +2 -1
  30. data/lib/i18n/tasks/data/router/pattern_router.rb +3 -1
  31. data/lib/i18n/tasks/data/tree/node.rb +6 -3
  32. data/lib/i18n/tasks/data/tree/nodes.rb +6 -7
  33. data/lib/i18n/tasks/data/tree/siblings.rb +10 -4
  34. data/lib/i18n/tasks/data/tree/traversal.rb +34 -11
  35. data/lib/i18n/tasks/html_keys.rb +4 -6
  36. data/lib/i18n/tasks/ignore_keys.rb +4 -3
  37. data/lib/i18n/tasks/interpolations.rb +10 -4
  38. data/lib/i18n/tasks/key_pattern_matching.rb +3 -2
  39. data/lib/i18n/tasks/locale_pathname.rb +1 -1
  40. data/lib/i18n/tasks/missing_keys.rb +4 -0
  41. data/lib/i18n/tasks/plural_keys.rb +5 -6
  42. data/lib/i18n/tasks/references.rb +4 -2
  43. data/lib/i18n/tasks/reports/base.rb +4 -3
  44. data/lib/i18n/tasks/reports/terminal.rb +8 -6
  45. data/lib/i18n/tasks/scanners/ast_matchers/base_matcher.rb +118 -0
  46. data/lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb +91 -0
  47. data/lib/i18n/tasks/scanners/ast_matchers/rails_model_matcher.rb +69 -0
  48. data/lib/i18n/tasks/scanners/erb_ast_processor.rb +74 -0
  49. data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +48 -0
  50. data/lib/i18n/tasks/scanners/file_scanner.rb +4 -3
  51. data/lib/i18n/tasks/scanners/files/caching_file_finder.rb +0 -3
  52. data/lib/i18n/tasks/scanners/files/file_finder.rb +3 -2
  53. data/lib/i18n/tasks/scanners/local_ruby_parser.rb +85 -0
  54. data/lib/i18n/tasks/scanners/occurrence_from_position.rb +3 -3
  55. data/lib/i18n/tasks/scanners/pattern_mapper.rb +1 -1
  56. data/lib/i18n/tasks/scanners/pattern_scanner.rb +8 -5
  57. data/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb +4 -2
  58. data/lib/i18n/tasks/scanners/relative_keys.rb +19 -4
  59. data/lib/i18n/tasks/scanners/results/occurrence.rb +17 -1
  60. data/lib/i18n/tasks/scanners/ruby_ast_call_finder.rb +9 -34
  61. data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +91 -154
  62. data/lib/i18n/tasks/scanners/ruby_key_literals.rb +4 -4
  63. data/lib/i18n/tasks/scanners/scanner_multiplexer.rb +2 -0
  64. data/lib/i18n/tasks/split_key.rb +3 -1
  65. data/lib/i18n/tasks/string_interpolation.rb +1 -0
  66. data/lib/i18n/tasks/translation.rb +3 -3
  67. data/lib/i18n/tasks/translators/base_translator.rb +5 -3
  68. data/lib/i18n/tasks/translators/deepl_translator.rb +10 -2
  69. data/lib/i18n/tasks/translators/google_translator.rb +2 -0
  70. data/lib/i18n/tasks/translators/yandex_translator.rb +2 -0
  71. data/lib/i18n/tasks/used_keys.rb +21 -14
  72. data/lib/i18n/tasks/version.rb +1 -1
  73. data/lib/i18n/tasks.rb +17 -7
  74. data/templates/config/i18n-tasks.yml +21 -1
  75. metadata +44 -15
@@ -28,6 +28,11 @@ module I18n::Tasks
28
28
  '--value VALUE',
29
29
  t('i18n_tasks.cmd.args.desc.value')
30
30
 
31
+ arg :config,
32
+ '-c',
33
+ '--config FILE',
34
+ t('i18n_tasks.cmd.args.desc.config')
35
+
31
36
  def arg_or_pos!(key, opts)
32
37
  opts[key] ||= opts[:arguments].try(:shift)
33
38
  end
@@ -9,7 +9,7 @@ module I18n::Tasks
9
9
  include Command::DSL
10
10
 
11
11
  DATA_FORMATS = %w[yaml json keys].freeze
12
- OUT_FORMATS = ['terminal-table', *DATA_FORMATS, 'inspect'].freeze
12
+ OUT_FORMATS = ['terminal-table', *DATA_FORMATS, 'inspect', 'key-values'].freeze
13
13
 
14
14
  format_arg = proc do |type, values|
15
15
  default = values.first
@@ -64,6 +64,7 @@ module I18n::Tasks
64
64
  # @return [I18n::Tasks::Data::Tree::Siblings]
65
65
  def parse_forest(src, format)
66
66
  fail CommandError, I18n.t('i18n_tasks.cmd.errors.pass_forest') unless src
67
+
67
68
  if format == 'keys'
68
69
  ::I18n::Tasks::Data::Tree::Siblings.from_key_names parse_keys(src)
69
70
  else
@@ -84,6 +85,8 @@ module I18n::Tasks
84
85
  puts forest.inspect
85
86
  when 'keys'
86
87
  puts forest.key_names(root: true)
88
+ when 'key-values'
89
+ puts(forest.key_values(root: true).map { |kv| kv.join("\t") })
87
90
  when *DATA_FORMATS
88
91
  puts i18n.data.adapter_dump forest.to_hash(true), format
89
92
  end
@@ -13,22 +13,22 @@ module I18n::Tasks
13
13
  '--locales en,es,ru',
14
14
  Array,
15
15
  t('i18n_tasks.cmd.args.desc.locales_filter'),
16
- parser: OptionParsers::Locale::ListParser,
17
- default: 'all',
16
+ parser: OptionParsers::Locale::ListParser,
17
+ default: 'all',
18
18
  consume_positional: true
19
19
 
20
20
  arg :locale,
21
21
  '-l',
22
22
  '--locale en',
23
23
  t('i18n_tasks.cmd.args.desc.locale'),
24
- parser: OptionParsers::Locale::Parser,
24
+ parser: OptionParsers::Locale::Parser,
25
25
  default: 'base'
26
26
 
27
27
  arg :locale_to_translate_from,
28
28
  '-f',
29
29
  '--from en',
30
30
  t('i18n_tasks.cmd.args.desc.locale_to_translate_from'),
31
- parser: OptionParsers::Locale::Parser,
31
+ parser: OptionParsers::Locale::Parser,
32
32
  default: 'base'
33
33
 
34
34
  TRANSLATION_BACKENDS = %w[google deepl].freeze
@@ -36,7 +36,7 @@ module I18n::Tasks
36
36
  '-b',
37
37
  '--backend BACKEND',
38
38
  t('i18n_tasks.cmd.args.desc.translation_backend'),
39
- parser: OptionParsers::Locale::Parser,
39
+ parser: OptionParsers::Locale::Parser,
40
40
  default: TRANSLATION_BACKENDS[0]
41
41
  end
42
42
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/concurrent/cached_value'
4
-
5
3
  module I18n::Tasks::Concurrent
6
4
  # A thread-safe memoized value.
7
5
  # The given computation is guaranteed to be invoked at most once.
@@ -19,8 +17,10 @@ module I18n::Tasks::Concurrent
19
17
  # @return [Object] Result of the computation.
20
18
  def get
21
19
  return get_result_volatile unless get_result_volatile == NULL
20
+
22
21
  @mutex.synchronize do
23
22
  next unless get_result_volatile == NULL
23
+
24
24
  set_result_volatile @computation.call
25
25
  @computation = nil
26
26
  end
@@ -2,10 +2,10 @@
2
2
 
3
3
  module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
4
4
  DEFAULTS = {
5
- base_locale: 'en',
5
+ base_locale: 'en',
6
6
  internal_locale: 'en',
7
- search: ::I18n::Tasks::UsedKeys::SEARCH_DEFAULTS,
8
- data: ::I18n::Tasks::Data::DATA_DEFAULTS
7
+ search: ::I18n::Tasks::UsedKeys::SEARCH_DEFAULTS,
8
+ data: ::I18n::Tasks::Data::DATA_DEFAULTS
9
9
  }.freeze
10
10
 
11
11
  # i18n-tasks config (defaults + config/i18n-tasks.yml)
@@ -20,7 +20,7 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
20
20
  ].freeze
21
21
 
22
22
  def file_config
23
- file = CONFIG_FILES.detect { |f| File.exist?(f) }
23
+ file = @config_override || CONFIG_FILES.detect { |f| File.exist?(f) }
24
24
  # rubocop:disable Security/Eval
25
25
  config = file && YAML.load(eval(Erubi::Engine.new(File.read(file, encoding: 'UTF-8')).src))
26
26
  # rubocop:enable Security/Eval
@@ -30,6 +30,13 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
30
30
  warn_deprecated 'Please move relative_roots under search in config/i18n-tasks.yml.'
31
31
  c[:search][:relative_roots] = c.delete(:relative_roots)
32
32
  end
33
+
34
+ if c.dig(:search, :exclude_method_name_paths)
35
+ warn_deprecated(
36
+ 'Please rename exclude_method_name_paths to relative_exclude_method_name_paths in config/i18n-tasks.yml.'
37
+ )
38
+ c[:search][:relative_exclude_method_name_paths] = c[:search].delete(:exclude_method_name_paths)
39
+ end
33
40
  end
34
41
  else
35
42
  {}.with_indifferent_access
@@ -44,12 +51,10 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
44
51
  # data config
45
52
  # @return [Hash<adapter: String, options: Hash>]
46
53
  def data_config
47
- @config_sections[:data] ||= begin
48
- {
49
- adapter: data.class.name,
50
- config: data.config
51
- }
52
- end
54
+ @config_sections[:data] ||= {
55
+ adapter: data.class.name,
56
+ config: data.config
57
+ }
53
58
  end
54
59
 
55
60
  # translation config
@@ -59,6 +64,8 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
59
64
  conf = (config[:translation] || {}).with_indifferent_access
60
65
  conf[:google_translate_api_key] = ENV['GOOGLE_TRANSLATE_API_KEY'] if ENV.key?('GOOGLE_TRANSLATE_API_KEY')
61
66
  conf[:deepl_api_key] = ENV['DEEPL_AUTH_KEY'] if ENV.key?('DEEPL_AUTH_KEY')
67
+ conf[:deepl_host] = ENV['DEEPL_HOST'] if ENV.key?('DEEPL_HOST')
68
+ conf[:deepl_version] = ENV['DEEPL_VERSION'] if ENV.key?('DEEPL_VERSION')
62
69
  conf[:yandex_api_key] = ENV['YANDEX_API_KEY'] if ENV.key?('YANDEX_API_KEY')
63
70
  conf
64
71
  end
@@ -34,7 +34,7 @@ module I18n::Tasks
34
34
  end
35
35
 
36
36
  def guide
37
- Rainbow('i18n-tasks IRB Quick Start guide').green.bright + "\n" + <<~TEXT
37
+ "#{Rainbow('i18n-tasks IRB Quick Start guide').green.bright}\n#{<<~TEXT}"
38
38
  #{Rainbow('Data as trees').yellow}
39
39
  tree(locale)
40
40
  used_tree(key_filter: nil, strict: nil)
@@ -5,11 +5,13 @@ module I18n::Tasks
5
5
  module Data
6
6
  module Adapter
7
7
  module YamlAdapter
8
+ EMOJI_REGEX = /\\u[\da-f]{8}/i.freeze
9
+
8
10
  class << self
9
11
  # @return [Hash] locale tree
10
12
  def parse(str, options)
11
13
  if YAML.method(:load).arity.abs == 2
12
- YAML.load(str, **(options || {}))
14
+ YAML.safe_load(str, **(options || {}), permitted_classes: [Symbol], aliases: true)
13
15
  else
14
16
  # older jruby and rbx 2.2.7 do not accept options
15
17
  YAML.load(str)
@@ -18,7 +20,12 @@ module I18n::Tasks
18
20
 
19
21
  # @return [String]
20
22
  def dump(tree, options)
21
- tree.to_yaml(options || {})
23
+ restore_emojis(tree.to_yaml(options || {}))
24
+ end
25
+
26
+ # @return [String]
27
+ def restore_emojis(yaml)
28
+ yaml.gsub(EMOJI_REGEX) { |m| [m[-8..].to_i(16)].pack('U') }
22
29
  end
23
30
  end
24
31
  end
@@ -53,12 +53,14 @@ module I18n
53
53
  content = adapter_dump(hash, adapter)
54
54
  # Ignore unchanged data
55
55
  return if File.file?(path) && content == read_file(path)
56
+
56
57
  ::FileUtils.mkpath(File.dirname(path))
57
- ::File.open(path, 'w') { |f| f.write content }
58
+ ::File.write(path, content)
58
59
  end
59
60
 
60
61
  def normalized?(path, tree)
61
62
  return false unless File.file?(path)
63
+
62
64
  read_file(path) == adapter_dump(tree.to_hash(true), self.class.adapter_name_for_path(path))
63
65
  end
64
66
 
@@ -12,11 +12,12 @@ module I18n::Tasks
12
12
  include ::I18n::Tasks::Data::FileFormats
13
13
  include ::I18n::Tasks::Logging
14
14
 
15
- attr_reader :config, :base_locale, :locales
16
- attr_writer :locales
15
+ attr_accessor :locales
16
+ attr_reader :config, :base_locale
17
+ attr_writer :router
17
18
 
18
19
  DEFAULTS = {
19
- read: ['config/locales/%{locale}.yml'],
20
+ read: ['config/locales/%{locale}.yml'],
20
21
  write: ['config/locales/%{locale}.yml']
21
22
  }.freeze
22
23
 
@@ -131,11 +132,12 @@ module I18n::Tasks
131
132
  def t(key, locale)
132
133
  tree = self[locale.to_s]
133
134
  return unless tree
135
+
134
136
  tree[locale][key].try(:value_or_children_hash)
135
137
  end
136
138
 
137
139
  def config=(config)
138
- @config = DEFAULTS.deep_merge((config || {}).reject { |_k, v| v.nil? })
140
+ @config = DEFAULTS.deep_merge((config || {}).compact)
139
141
  reload
140
142
  end
141
143
 
@@ -159,7 +161,6 @@ module I18n::Tasks
159
161
  router_class.new(self, @config.merge(base_locale: base_locale, locales: locales))
160
162
  end
161
163
  end
162
- attr_writer :router
163
164
 
164
165
  protected
165
166
 
@@ -188,7 +189,7 @@ module I18n::Tasks
188
189
  The following unquoted YAML keys result in a nil key:
189
190
  #{%w[null Null NULL ~].join(', ')}
190
191
  See http://yaml.org/type/null.html
191
- TEXT
192
+ TEXT
192
193
  elsif value.is_a?(Hash)
193
194
  filter_nil_keys! path, value, suffix + [key]
194
195
  end
@@ -13,8 +13,9 @@ module I18n::Tasks
13
13
  super
14
14
  end
15
15
 
16
- def route(locale, forest, &block)
16
+ def route(locale, forest, &block) # rubocop:disable Metrics/AbcSize
17
17
  return to_enum(:route, locale, forest) unless block
18
+
18
19
  out = Hash.new { |hash, key| hash[key] = Set.new }
19
20
  not_found = Set.new
20
21
  forest.keys do |key, _node|
@@ -10,6 +10,7 @@ module I18n::Tasks
10
10
  include ::I18n::Tasks::KeyPatternMatching
11
11
 
12
12
  attr_reader :routes
13
+
13
14
  # @option data_config write [Array] of routes
14
15
  # @example
15
16
  # {write:
@@ -29,6 +30,7 @@ module I18n::Tasks
29
30
  # @return [Hash] mapping of destination => [ [key, value], ... ]
30
31
  def route(locale, forest, &block)
31
32
  return to_enum(:route, locale, forest) unless block
33
+
32
34
  locale = locale.to_s
33
35
  out = {}
34
36
  forest.keys do |key, _node|
@@ -36,7 +38,7 @@ module I18n::Tasks
36
38
  if pattern
37
39
  key_match = $~
38
40
  path = format(path, locale: locale)
39
- path.gsub!(/\\\d+/) { |m| key_match[m[1..-1].to_i] }
41
+ path.gsub!(/\\\d+/) { |m| key_match[m[1..].to_i] }
40
42
  (out[path] ||= Set.new) << "#{locale}.#{key}"
41
43
  else
42
44
  fail CommandError, "Cannot route key #{key}. Routes are #{@routes_config.inspect}"
@@ -18,7 +18,7 @@ module I18n::Tasks::Data::Tree
18
18
  @data = data
19
19
  @parent = parent
20
20
  @warn_about_add_children_to_leaf = warn_about_add_children_to_leaf
21
- self.children = (children if children)
21
+ self.children = children if children
22
22
  end
23
23
  # rubocop:enable Metrics/ParameterLists
24
24
 
@@ -48,6 +48,7 @@ module I18n::Tasks::Data::Tree
48
48
 
49
49
  def each(&block)
50
50
  return to_enum(:each) { 1 } unless block
51
+
51
52
  block.yield(self)
52
53
  self
53
54
  end
@@ -112,6 +113,7 @@ module I18n::Tasks::Data::Tree
112
113
 
113
114
  def walk_to_root(&visitor)
114
115
  return to_enum(:walk_to_root) unless visitor
116
+
115
117
  visitor.yield self
116
118
  parent.walk_to_root(&visitor) if parent?
117
119
  end
@@ -124,6 +126,7 @@ module I18n::Tasks::Data::Tree
124
126
 
125
127
  def walk_from_root(&visitor)
126
128
  return to_enum(:walk_from_root) unless visitor
129
+
127
130
  walk_to_root.reverse_each do |node|
128
131
  visitor.yield node
129
132
  end
@@ -165,11 +168,11 @@ module I18n::Tasks::Data::Tree
165
168
  label = if key.nil?
166
169
  Rainbow('∅').faint
167
170
  else
168
- [Rainbow(key).color(1 + level % 15),
171
+ [Rainbow(key).color(1 + (level % 15)),
169
172
  (": #{format_value_for_inspect(value)}" if leaf?),
170
173
  (" #{data}" if data?)].compact.join
171
174
  end
172
- [' ' * level, label, ("\n" + children.map { |c| c.inspect(level + 1) }.join("\n") if children?)].compact.join
175
+ [' ' * level, label, ("\n#{children.map { |c| c.inspect(level + 1) }.join("\n")}" if children?)].compact.join
173
176
  end
174
177
 
175
178
  def format_value_for_inspect(value)
@@ -31,13 +31,11 @@ module I18n::Tasks::Data::Tree
31
31
  end
32
32
 
33
33
  def to_hash(sort = false)
34
- (@hash ||= {})[sort] ||= begin
35
- if sort
36
- sort_by(&:key)
37
- else
38
- self
39
- end.map { |node| node.to_hash(sort) }.reduce({}, :deep_merge!)
40
- end
34
+ (@hash ||= {})[sort] ||= if sort
35
+ sort_by(&:key)
36
+ else
37
+ self
38
+ end.map { |node| node.to_hash(sort) }.reduce({}, :deep_merge!)
41
39
  end
42
40
 
43
41
  delegate :to_json, to: :to_hash
@@ -80,6 +78,7 @@ module I18n::Tasks::Data::Tree
80
78
 
81
79
  def children(&block)
82
80
  return to_enum(:children) { map { |c| c.children ? c.children.size : 0 }.reduce(:+) } unless block
81
+
83
82
  each do |node|
84
83
  node.children.each(&block) if node.children?
85
84
  end
@@ -3,7 +3,6 @@
3
3
  require 'set'
4
4
  require 'i18n/tasks/split_key'
5
5
  require 'i18n/tasks/data/tree/nodes'
6
- require 'i18n/tasks/data/tree/node'
7
6
 
8
7
  module I18n::Tasks::Data::Tree
9
8
  # Siblings represents a subtree sharing a common parent
@@ -50,7 +49,7 @@ module I18n::Tasks::Data::Tree
50
49
  next
51
50
  end
52
51
  match = $~
53
- new_key = to_pattern.gsub(/\\\d+/) { |m| match[m[1..-1].to_i] }
52
+ new_key = to_pattern.gsub(/\\\d+/) { |m| match[m[1..].to_i] }
54
53
  old_key_to_new_key[full_key] = new_key
55
54
  moved_forest.merge!(Siblings.new.tap do |forest|
56
55
  forest[[(node.root.try(:key) unless root), new_key].compact.join('.')] =
@@ -62,6 +61,7 @@ module I18n::Tasks::Data::Tree
62
61
  # TODO: support nested references better
63
62
  nodes do |node|
64
63
  next unless node.reference?
64
+
65
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
@@ -92,6 +92,7 @@ module I18n::Tasks::Data::Tree
92
92
  # add or replace node by full key
93
93
  def set(full_key, node)
94
94
  fail 'value should be a I18n::Tasks::Data::Tree::Node' unless node.is_a?(Node)
95
+
95
96
  key_part, rest = split_key(full_key, 2)
96
97
  child = key_to_node[key_part]
97
98
 
@@ -131,6 +132,7 @@ module I18n::Tasks::Data::Tree
131
132
  def append!(nodes)
132
133
  nodes = nodes.map do |node|
133
134
  fail "already has a child with key '#{node.key}'" if key_to_node.key?(node.key)
135
+
134
136
  key_to_node[node.key] = (node.parent == parent ? node : node.derive(parent: parent))
135
137
  end
136
138
  super(nodes)
@@ -172,16 +174,18 @@ module I18n::Tasks::Data::Tree
172
174
 
173
175
  def set_root_key!(new_key, data = nil)
174
176
  return self if empty?
177
+
175
178
  rename_key first.key, new_key
176
179
  leaves { |node| node.data.merge! data } if data
177
180
  self
178
181
  end
179
182
 
180
183
  # @param on_leaves_merge [Proc] invoked when a leaf is merged with another leaf
181
- def merge_node!(node, on_leaves_merge: nil) # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity
184
+ def merge_node!(node, on_leaves_merge: nil) # rubocop:disable Metrics/AbcSize
182
185
  if key_to_node.key?(node.key)
183
186
  our = key_to_node[node.key]
184
187
  return if our == node
188
+
185
189
  our.value = node.value if node.leaf?
186
190
  our.data.merge!(node.data) if node.data?
187
191
  if node.children?
@@ -262,7 +266,7 @@ module I18n::Tasks::Data::Tree
262
266
  build_forest(warn_about_add_children_to_leaf: false) do |forest|
263
267
  key_occurrences.each do |key_occurrence|
264
268
  forest[key_occurrence.key] = ::I18n::Tasks::Data::Tree::Node.new(
265
- key: split_key(key_occurrence.key).last,
269
+ key: split_key(key_occurrence.key).last,
266
270
  data: { occurrences: key_occurrence.occurrences }
267
271
  )
268
272
  end
@@ -273,6 +277,7 @@ module I18n::Tasks::Data::Tree
273
277
  build_forest(opts) do |forest|
274
278
  key_attrs.each do |(full_key, attr)|
275
279
  fail "Invalid key #{full_key.inspect}" if full_key.end_with?('.')
280
+
276
281
  node = ::I18n::Tasks::Data::Tree::Node.new(**attr.merge(key: split_key(full_key).last))
277
282
  yield(full_key, node) if block
278
283
  forest[full_key] = node
@@ -295,6 +300,7 @@ module I18n::Tasks::Data::Tree
295
300
  def from_nested_hash(hash, opts = {})
296
301
  parse_parent_opt!(opts)
297
302
  fail I18n::Tasks::CommandError, "invalid tree #{hash.inspect}" unless hash.respond_to?(:map)
303
+
298
304
  opts[:nodes] = hash.map { |key, value| Node.from_key_value key, value }
299
305
  Siblings.new(opts)
300
306
  end
@@ -12,6 +12,7 @@ module I18n::Tasks
12
12
 
13
13
  def leaves(&visitor)
14
14
  return to_enum(:leaves) unless visitor
15
+
15
16
  nodes do |node|
16
17
  visitor.yield(node) if node.leaf?
17
18
  end
@@ -20,6 +21,7 @@ module I18n::Tasks
20
21
 
21
22
  def levels(&block)
22
23
  return to_enum(:levels) unless block
24
+
23
25
  nodes = to_nodes
24
26
  unless nodes.empty?
25
27
  block.yield nodes
@@ -35,6 +37,7 @@ module I18n::Tasks
35
37
 
36
38
  def breadth_first(&visitor)
37
39
  return to_enum(:breadth_first) unless visitor
40
+
38
41
  levels do |nodes|
39
42
  nodes.each { |node| visitor.yield(node) }
40
43
  end
@@ -43,9 +46,11 @@ module I18n::Tasks
43
46
 
44
47
  def depth_first(&visitor)
45
48
  return to_enum(:depth_first) unless visitor
49
+
46
50
  each do |node|
47
51
  visitor.yield node
48
52
  next unless node.children?
53
+
49
54
  node.children.each do |child|
50
55
  child.depth_first(&visitor)
51
56
  end
@@ -56,6 +61,7 @@ module I18n::Tasks
56
61
  # @option root include root in full key
57
62
  def keys(root: false, &visitor)
58
63
  return to_enum(:keys, root: root) unless visitor
64
+
59
65
  leaves { |node| visitor.yield(node.full_key(root: root), node) }
60
66
  self
61
67
  end
@@ -70,13 +76,13 @@ module I18n::Tasks
70
76
 
71
77
  def root_key_values(sort = false)
72
78
  result = keys(root: false).map { |key, node| [node.root.key, key, node.value] }
73
- result.sort! { |a, b| a[0] != b[0] ? a[0] <=> b[0] : a[1] <=> b[1] } if sort
79
+ result.sort! { |a, b| a[0] == b[0] ? a[1] <=> b[1] : a[0] <=> b[0] } if sort
74
80
  result
75
81
  end
76
82
 
77
83
  def root_key_value_data(sort = false)
78
84
  result = keys(root: false).map { |key, node| [node.root.key, key, node.value, node.data] }
79
- result.sort! { |a, b| a[0] != b[0] ? a[0] <=> b[0] : a[1] <=> b[1] } if sort
85
+ result.sort! { |a, b| a[0] == b[0] ? a[1] <=> b[1] : a[0] <=> b[0] } if sort
80
86
  result
81
87
  end
82
88
 
@@ -88,6 +94,7 @@ module I18n::Tasks
88
94
  tree = Siblings.new
89
95
  each do |node|
90
96
  next unless block.yield(node)
97
+
91
98
  tree.append! node.derive(
92
99
  parent: tree.parent,
93
100
  children: (node.children.select_nodes(&block).to_a if node.children)
@@ -163,22 +170,38 @@ module I18n::Tasks
163
170
  value_proc ||= proc do |node|
164
171
  node_value = node.value
165
172
  next node_value if node.reference?
173
+
166
174
  human_key = ActiveSupport::Inflector.humanize(node.key.to_s)
167
175
  full_key = node.full_key
168
176
  default = (node.data[:occurrences] || []).detect { |o| o.default_arg.presence }.try(:default_arg)
169
- StringInterpolation.interpolate_soft(
170
- val_pattern,
171
- value: node_value,
172
- human_key: human_key,
173
- key: full_key,
174
- default: default,
175
- value_or_human_key: node_value.presence || human_key,
176
- value_or_default_or_human_key: node_value.presence || default || human_key
177
- )
177
+ if default.is_a?(Hash)
178
+ default.each_with_object({}) do |(k, v), h|
179
+ h[k] = StringInterpolation.interpolate_soft(
180
+ val_pattern,
181
+ value: node_value,
182
+ human_key: human_key,
183
+ key: full_key,
184
+ default: v,
185
+ value_or_human_key: node_value.presence || human_key,
186
+ value_or_default_or_human_key: node_value.presence || v || human_key
187
+ )
188
+ end
189
+ else
190
+ StringInterpolation.interpolate_soft(
191
+ val_pattern,
192
+ value: node_value,
193
+ human_key: human_key,
194
+ key: full_key,
195
+ default: default,
196
+ value_or_human_key: node_value.presence || human_key,
197
+ value_or_default_or_human_key: node_value.presence || default || human_key
198
+ )
199
+ end
178
200
  end
179
201
  pattern_re = I18n::Tasks::KeyPatternMatching.compile_key_pattern(key_pattern) if key_pattern.present?
180
202
  keys.each do |key, node|
181
203
  next if pattern_re && key !~ pattern_re
204
+
182
205
  node.value = value_proc.call(node)
183
206
  end
184
207
  self
@@ -2,15 +2,13 @@
2
2
 
3
3
  module I18n::Tasks
4
4
  module HtmlKeys
5
- HTML_KEY_PATTERN = /[.\-_]html\z/
6
- MAYBE_PLURAL_HTML_KEY_PATTERN = /[.\-_]html\.[^.]+\z/
5
+ HTML_KEY_PATTERN = /[.\-_]html\z/.freeze
6
+ MAYBE_PLURAL_HTML_KEY_PATTERN = /[.\-_]html\.[^.]+\z/.freeze
7
7
 
8
8
  def html_key?(full_key, locale)
9
- # rubocop:disable Style/DoubleNegation
10
9
  !!(full_key =~ HTML_KEY_PATTERN ||
11
- full_key =~ MAYBE_PLURAL_HTML_KEY_PATTERN &&
12
- depluralize_key(split_key(full_key, 2)[1], locale) =~ HTML_KEY_PATTERN)
13
- # rubocop:enable Style/DoubleNegation
10
+ (full_key =~ MAYBE_PLURAL_HTML_KEY_PATTERN &&
11
+ depluralize_key(split_key(full_key, 2)[1], locale) =~ HTML_KEY_PATTERN))
14
12
  end
15
13
  end
16
14
  end
@@ -17,12 +17,13 @@ module I18n::Tasks::IgnoreKeys
17
17
  @ignore_patterns[type][locale] ||= begin
18
18
  global = ignore_config.presence || []
19
19
  type_ignore = ignore_config(type).presence || []
20
- patterns = if type_ignore.is_a?(Array)
20
+ patterns = case type_ignore
21
+ when Array
21
22
  global + type_ignore
22
- elsif type_ignore.is_a?(Hash)
23
+ when Hash
23
24
  # ignore per locale
24
25
  global + (type_ignore['all'] || []) +
25
- type_ignore.select { |k, _v| k.to_s =~ /\b#{locale}\b/ }.values.flatten(1).compact
26
+ type_ignore.select { |k, _v| k.to_s =~ /\b#{locale}\b/ }.values.flatten(1).compact
26
27
  end
27
28
  compile_patterns_re patterns
28
29
  end
@@ -2,20 +2,26 @@
2
2
 
3
3
  module I18n::Tasks
4
4
  module Interpolations
5
- VARIABLE_REGEX = /%{[^}]+}/
5
+ class << self
6
+ attr_accessor :variable_regex
7
+ end
8
+ @variable_regex = /%{[^}]+}/.freeze
6
9
 
7
10
  def inconsistent_interpolations(locales: nil, base_locale: nil) # rubocop:disable Metrics/AbcSize
8
11
  locales ||= self.locales
9
12
  base_locale ||= self.base_locale
10
13
  result = empty_forest
14
+ variable_regex = I18n::Tasks::Interpolations.variable_regex
11
15
 
12
16
  data[base_locale].key_values.each do |key, value|
13
17
  next if !value.is_a?(String) || ignore_key?(key, :inconsistent_interpolations)
14
- base_vars = Set.new(value.scan(VARIABLE_REGEX))
18
+
19
+ base_vars = Set.new(value.scan(variable_regex))
15
20
  (locales - [base_locale]).each do |current_locale|
16
21
  node = data[current_locale].first.children[key]
17
- next unless node&.value&.is_a?(String)
18
- if base_vars != Set.new(node.value.scan(VARIABLE_REGEX))
22
+ next unless node&.value.is_a?(String)
23
+
24
+ if base_vars != Set.new(node.value.scan(variable_regex))
19
25
  result.merge!(node.walk_to_root.reduce(nil) { |c, p| [p.derive(children: c)] })
20
26
  end
21
27
  end
@@ -5,7 +5,7 @@ require 'strscan'
5
5
  module I18n::Tasks::KeyPatternMatching
6
6
  extend self # rubocop:disable Style/ModuleFunction
7
7
 
8
- MATCH_NOTHING = /\z\A/
8
+ MATCH_NOTHING = /\z\A/.freeze
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
 
@@ -24,6 +24,7 @@ module I18n::Tasks::KeyPatternMatching
24
24
  # { a, b.c } match any in set, can use : and *, match is captured
25
25
  def compile_key_pattern(key_pattern)
26
26
  return key_pattern if key_pattern.is_a?(Regexp)
27
+
27
28
  /\A#{key_pattern_re_body(key_pattern)}\z/
28
29
  end
29
30