i18n-tasks 0.9.33 → 1.0.12

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