i18n-tasks 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -1
  3. data/lib/i18n/tasks.rb +1 -0
  4. data/lib/i18n/tasks/base_task.rb +3 -0
  5. data/lib/i18n/tasks/cli.rb +1 -0
  6. data/lib/i18n/tasks/command/collection.rb +1 -0
  7. data/lib/i18n/tasks/command/commander.rb +1 -0
  8. data/lib/i18n/tasks/command/commands/data.rb +1 -0
  9. data/lib/i18n/tasks/command/commands/eq_base.rb +1 -0
  10. data/lib/i18n/tasks/command/commands/health.rb +1 -0
  11. data/lib/i18n/tasks/command/commands/meta.rb +1 -0
  12. data/lib/i18n/tasks/command/commands/missing.rb +1 -0
  13. data/lib/i18n/tasks/command/commands/tree.rb +1 -0
  14. data/lib/i18n/tasks/command/commands/usages.rb +2 -1
  15. data/lib/i18n/tasks/command/commands/xlsx.rb +1 -0
  16. data/lib/i18n/tasks/command/dsl.rb +1 -0
  17. data/lib/i18n/tasks/command/option_parsers/enum.rb +1 -0
  18. data/lib/i18n/tasks/command/option_parsers/locale.rb +1 -0
  19. data/lib/i18n/tasks/command/options/common.rb +1 -0
  20. data/lib/i18n/tasks/command/options/data.rb +1 -0
  21. data/lib/i18n/tasks/command/options/locales.rb +1 -0
  22. data/lib/i18n/tasks/command_error.rb +1 -0
  23. data/lib/i18n/tasks/commands.rb +1 -0
  24. data/lib/i18n/tasks/configuration.rb +1 -0
  25. data/lib/i18n/tasks/console_context.rb +1 -0
  26. data/lib/i18n/tasks/data.rb +2 -1
  27. data/lib/i18n/tasks/data/adapter/json_adapter.rb +1 -0
  28. data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +1 -0
  29. data/lib/i18n/tasks/data/file_formats.rb +1 -0
  30. data/lib/i18n/tasks/data/file_system.rb +1 -0
  31. data/lib/i18n/tasks/data/file_system_base.rb +1 -0
  32. data/lib/i18n/tasks/data/router/conservative_router.rb +1 -0
  33. data/lib/i18n/tasks/data/router/pattern_router.rb +1 -0
  34. data/lib/i18n/tasks/data/tree/node.rb +5 -0
  35. data/lib/i18n/tasks/data/tree/nodes.rb +1 -0
  36. data/lib/i18n/tasks/data/tree/siblings.rb +23 -6
  37. data/lib/i18n/tasks/data/tree/traversal.rb +2 -0
  38. data/lib/i18n/tasks/google_translation.rb +5 -0
  39. data/lib/i18n/tasks/html_keys.rb +1 -0
  40. data/lib/i18n/tasks/ignore_keys.rb +1 -0
  41. data/lib/i18n/tasks/key_pattern_matching.rb +3 -17
  42. data/lib/i18n/tasks/locale_list.rb +1 -0
  43. data/lib/i18n/tasks/locale_pathname.rb +1 -0
  44. data/lib/i18n/tasks/logging.rb +1 -0
  45. data/lib/i18n/tasks/missing_keys.rb +2 -1
  46. data/lib/i18n/tasks/plural_keys.rb +1 -0
  47. data/lib/i18n/tasks/references.rb +68 -0
  48. data/lib/i18n/tasks/reports/base.rb +4 -5
  49. data/lib/i18n/tasks/reports/spreadsheet.rb +1 -0
  50. data/lib/i18n/tasks/reports/terminal.rb +29 -6
  51. data/lib/i18n/tasks/scanners/file_scanner.rb +1 -0
  52. data/lib/i18n/tasks/scanners/files/caching_file_finder.rb +1 -0
  53. data/lib/i18n/tasks/scanners/files/caching_file_finder_provider.rb +1 -0
  54. data/lib/i18n/tasks/scanners/files/caching_file_reader.rb +1 -0
  55. data/lib/i18n/tasks/scanners/files/file_finder.rb +1 -0
  56. data/lib/i18n/tasks/scanners/files/file_reader.rb +1 -0
  57. data/lib/i18n/tasks/scanners/occurrence_from_position.rb +4 -2
  58. data/lib/i18n/tasks/scanners/pattern_scanner.rb +3 -2
  59. data/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb +1 -0
  60. data/lib/i18n/tasks/scanners/relative_keys.rb +1 -0
  61. data/lib/i18n/tasks/scanners/results/key_occurrences.rb +1 -0
  62. data/lib/i18n/tasks/scanners/results/occurrence.rb +10 -3
  63. data/lib/i18n/tasks/scanners/ruby_ast_call_finder.rb +1 -0
  64. data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +5 -2
  65. data/lib/i18n/tasks/scanners/scanner.rb +1 -0
  66. data/lib/i18n/tasks/scanners/scanner_multiplexer.rb +1 -0
  67. data/lib/i18n/tasks/split_key.rb +1 -0
  68. data/lib/i18n/tasks/stats.rb +1 -0
  69. data/lib/i18n/tasks/string_interpolation.rb +1 -0
  70. data/lib/i18n/tasks/unused_keys.rb +5 -1
  71. data/lib/i18n/tasks/used_keys.rb +56 -16
  72. data/lib/i18n/tasks/version.rb +2 -1
  73. data/templates/rspec/i18n_spec.rb +1 -0
  74. metadata +4 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d0b8ce2a23eb7fc9f59cdc7b993e17a0a1f4eb29
4
- data.tar.gz: aedad8826903bf2d429f7486dcea8cbdf2b2492d
3
+ metadata.gz: b5b15ad428fdc29cc404d033f363c08708f5b815
4
+ data.tar.gz: c5aaaecace6c6024d544ff552c06097b9ec2f252
5
5
  SHA512:
6
- metadata.gz: 42fb46c4e26ef0f09bdf641b332110b43c43caa3a402371d334e54773cdfa31cfc48da0a5c5fcd7b8baad0695519071aeab4634fec85065a3733e6bcc8e0fbe6
7
- data.tar.gz: 0be2ce49e12dc3f2f77222cd8028dcd04de5584435c859bed50a60dcee3c3c685aeb5878c8e5d5959baecf4b91ff1999ec71f93a5f13ab969871ed7fd8c4df41
6
+ metadata.gz: 54586d8f4fb14861a755a75325ccf73230b9e180bc590f2f7fd383dc6c10c3529943179709d1d8045873148530684625c92a53d9b997281b9078acac51c40699
7
+ data.tar.gz: 693ff45562b53f09a0c5baf81b0d5418227e35d182f81e51cdff99c740b2a4666c39bbf3ea84f9d64daafb5ce34b454a59410b3c73a03fadf35c3b9ee98244bb
data/README.md CHANGED
@@ -24,7 +24,7 @@ i18n-tasks can be used with any project using the ruby [i18n gem][i18n-gem] (def
24
24
  Add i18n-tasks to the Gemfile:
25
25
 
26
26
  ```ruby
27
- gem 'i18n-tasks', '~> 0.9.1'
27
+ gem 'i18n-tasks', '~> 0.9.3'
28
28
  ```
29
29
 
30
30
  Copy the default [configuration file](#configuration):
@@ -159,6 +159,11 @@ See the full list of tasks with `i18n-tasks --help`.
159
159
 
160
160
  ✔ Plural keys, such as `key.{one,many,other,...}` are fully supported.
161
161
 
162
+ #### Reference keys
163
+
164
+ ✔ Reference keys (keys with `:symbol` values) are fully supported. These keys are copied as-is in
165
+ `add/translate-missing`, and can be looked up by reference or value in `find`.
166
+
162
167
  #### `t()` keyword arguments
163
168
 
164
169
  ✔ `scope` keyword argument is fully supported by the AST scanner, and also by the Regexp scanner but only when it is the first argument.
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # define all the modules to be able to use ::
2
3
  module I18n
3
4
  module Tasks
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/command_error'
2
3
  require 'i18n/tasks/split_key'
3
4
  require 'i18n/tasks/key_pattern_matching'
4
5
  require 'i18n/tasks/logging'
5
6
  require 'i18n/tasks/plural_keys'
7
+ require 'i18n/tasks/references'
6
8
  require 'i18n/tasks/html_keys'
7
9
  require 'i18n/tasks/used_keys'
8
10
  require 'i18n/tasks/ignore_keys'
@@ -22,6 +24,7 @@ module I18n
22
24
  include SplitKey
23
25
  include KeyPatternMatching
24
26
  include PluralKeys
27
+ include References
25
28
  include HtmlKeys
26
29
  include UsedKeys
27
30
  include IgnoreKeys
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks'
2
3
  require 'i18n/tasks/commands'
3
4
  require 'optparse'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/command/dsl'
2
3
  require 'i18n/tasks/command/options/common'
3
4
  require 'i18n/tasks/command/options/locales'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/cli'
2
3
  require 'i18n/tasks/reports/terminal'
3
4
  require 'i18n/tasks/reports/spreadsheet'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module Command
3
4
  module Commands
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module Command
3
4
  module Commands
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module Command
3
4
  module Commands
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module Command
3
4
  module Commands
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/command/collection'
2
3
 
3
4
  module I18n::Tasks
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module Command
3
4
  module Commands
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module Command
3
4
  module Commands
@@ -15,7 +16,7 @@ module I18n::Tasks
15
16
 
16
17
  def find(opt = {})
17
18
  opt[:filter] ||= opt.delete(:pattern) || opt[:arguments].try(:first)
18
- print_forest i18n.used_tree(strict: opt[:strict], key_filter: opt[:filter].presence), opt, :used_keys
19
+ print_forest i18n.used_tree(strict: opt[:strict], key_filter: opt[:filter].presence, include_raw_references: true), opt, :used_keys
19
20
  end
20
21
 
21
22
  cmd :unused,
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module Command
3
4
  module Commands
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module Command
3
4
  module DSL
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module Command
3
4
  module OptionParsers
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module Command
3
4
  module OptionParsers
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/command/dsl'
2
3
 
3
4
  module I18n::Tasks
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/command/option_parsers/enum'
2
3
 
3
4
  module I18n::Tasks
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/command/option_parsers/locale'
2
3
 
3
4
  module I18n::Tasks
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n
2
3
  module Tasks
3
4
  # When this type of error is caught:
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/command/dsl'
2
3
  require 'i18n/tasks/command/collection'
3
4
  require 'i18n/tasks/command/commands/health'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks::Configuration
2
3
  DEFAULTS = {
3
4
  base_locale: 'en'.freeze,
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  class ConsoleContext < BaseTask
3
4
  def to_s
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/data/file_system'
2
3
 
3
4
  module I18n::Tasks
@@ -10,7 +11,7 @@ module I18n::Tasks
10
11
  # @see I18n::Tasks::Data::FileSystem
11
12
  def data
12
13
  @data ||= begin
13
- data_config = (config[:data] || {}).deep_symbolize_keys
14
+ data_config = (config[:data] || {}).deep_symbolize_keys
14
15
  data_config.merge!(base_locale: base_locale, locales: config[:locales])
15
16
  adapter_class = data_config[:adapter].presence || data_config[:class].presence || DATA_DEFAULTS[:adapter]
16
17
  adapter_class = adapter_class.to_s
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'json'
2
3
 
3
4
  module I18n::Tasks
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'yaml'
2
3
  module I18n::Tasks
3
4
  module Data
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'fileutils'
2
3
 
3
4
  module I18n
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/data/file_system_base'
2
3
  require 'i18n/tasks/data/adapter/json_adapter'
3
4
  require 'i18n/tasks/data/adapter/yaml_adapter'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/data/tree/node'
2
3
  require 'i18n/tasks/data/router/pattern_router'
3
4
  require 'i18n/tasks/data/router/conservative_router'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/data/router/pattern_router'
2
3
 
3
4
  module I18n::Tasks
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/key_pattern_matching'
2
3
  require 'i18n/tasks/data/tree/node'
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  require 'i18n/tasks/data/tree/traversal'
3
4
  require 'i18n/tasks/data/tree/siblings'
@@ -73,6 +74,10 @@ module I18n::Tasks::Data::Tree
73
74
  @data.present?
74
75
  end
75
76
 
77
+ def reference?
78
+ value.is_a?(Symbol)
79
+ end
80
+
76
81
  def get(key)
77
82
  children.get(key)
78
83
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  require 'i18n/tasks/data/tree/traversal'
3
4
  module I18n::Tasks::Data::Tree
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'set'
2
3
  require 'i18n/tasks/split_key'
3
4
  require 'i18n/tasks/data/tree/nodes'
@@ -108,13 +109,12 @@ module I18n::Tasks::Data::Tree
108
109
  derive.append!(nodes)
109
110
  end
110
111
 
111
- def merge!(nodes)
112
+ # @param on_leaves_merge [Proc] invoked when a leaf is merged with another leaf
113
+ def merge!(nodes, on_leaves_merge: nil)
112
114
  nodes = Siblings.from_nested_hash(nodes) if nodes.is_a?(Hash)
113
115
  nodes.each do |node|
114
- merge_node! node
116
+ merge_node! node, on_leaves_merge: on_leaves_merge
115
117
  end
116
- @list = key_to_node.values
117
- dirty!
118
118
  self
119
119
  end
120
120
 
@@ -131,10 +131,23 @@ module I18n::Tasks::Data::Tree
131
131
  remove_nodes_collapsing_emptied_ancestors to_remove
132
132
  end
133
133
 
134
+ def subtract_keys!(keys)
135
+ to_remove = Set.new
136
+ keys.each do |full_key|
137
+ node = get full_key
138
+ to_remove << node if node
139
+ end
140
+ remove_nodes_collapsing_emptied_ancestors! to_remove
141
+ end
142
+
134
143
  def subtract_by_key(other)
135
144
  subtract_keys other.key_names(root: true)
136
145
  end
137
146
 
147
+ def subtract_by_key!(other)
148
+ subtract_keys! other.key_names(root: true)
149
+ end
150
+
138
151
  def set_root_key!(new_key, data = nil)
139
152
  return self if empty?
140
153
  rename_key first.key, new_key
@@ -142,7 +155,8 @@ module I18n::Tasks::Data::Tree
142
155
  self
143
156
  end
144
157
 
145
- def merge_node!(node)
158
+ # @param on_leaves_merge [Proc] invoked when a leaf is merged with another leaf
159
+ def merge_node!(node, on_leaves_merge: nil)
146
160
  if key_to_node.key?(node.key)
147
161
  our = key_to_node[node.key]
148
162
  return if our == node
@@ -155,9 +169,12 @@ module I18n::Tasks::Data::Tree
155
169
  warn_add_children_to_leaf our
156
170
  our.children = node.children
157
171
  end
172
+ elsif on_leaves_merge
173
+ on_leaves_merge.call(our, node)
158
174
  end
159
175
  else
160
- key_to_node[node.key] = node.derive(parent: parent)
176
+ @list << (key_to_node[node.key] = node.derive(parent: parent))
177
+ dirty!
161
178
  end
162
179
  end
163
180
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module Data::Tree
3
4
  # Any Enumerable that yields nodes can mix in this module
@@ -145,6 +146,7 @@ module I18n::Tasks
145
146
  def set_each_value!(val_pattern, key_pattern = nil, &value_proc)
146
147
  value_proc ||= proc { |node|
147
148
  node_value = node.value
149
+ next node_value if node.reference?
148
150
  human_key = ActiveSupport::Inflector.humanize(node.key.to_s)
149
151
  full_key = node.full_key
150
152
  StringInterpolation.interpolate_soft(
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'easy_translate'
2
3
  require 'i18n/tasks/html_keys'
3
4
 
@@ -22,9 +23,13 @@ module I18n::Tasks
22
23
  opts[:key] ||= translation_config[:api_key]
23
24
  validate_google_translate_api_key! opts[:key]
24
25
  key_pos = list.each_with_index.inject({}) { |idx, ((k, _v), i)| idx[k] = i; idx }
26
+ # copy reference keys as is, instead of translating
27
+ reference_key_vals = list.select { |_k, v| v.is_a? Symbol } || []
28
+ list -= reference_key_vals
25
29
  result = list.group_by { |k_v| HtmlKeys.html_key? k_v[0] }.map { |is_html, list_slice|
26
30
  fetch_google_translations list_slice, opts.merge(is_html ? {html: true} : {format: 'text'})
27
31
  }.reduce(:+) || []
32
+ result.concat(reference_key_vals)
28
33
  result.sort! { |a, b| key_pos[a[0]] <=> key_pos[b[0]] }
29
34
  result
30
35
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module HtmlKeys
3
4
  extend self
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks::IgnoreKeys
2
3
  # whether to ignore the key
3
4
  # will also apply global ignore rules
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+ require 'strscan'
3
+
1
4
  module I18n::Tasks::KeyPatternMatching
2
5
  extend self
3
6
  MATCH_NOTHING = /\z\A/.freeze
@@ -29,21 +32,4 @@ module I18n::Tasks::KeyPatternMatching
29
32
  gsub(/:/, '(?<=^|\.)[^.]+?(?=\.|$)'.freeze).
30
33
  gsub(/\{(.*?)}/) { "(#{$1.strip.gsub /\s*,\s*/, '|'.freeze})" }
31
34
  end
32
-
33
- def key_match_pattern(k)
34
- @key_match_pattern ||= {}
35
- @key_match_pattern[k] ||= begin
36
- "#{k.gsub(KEY_INTERPOLATION_RE, ':'.freeze)}#{':'.freeze if k.end_with?('.'.freeze)}"
37
- end
38
- end
39
-
40
- # @return true if the key looks like an expression
41
- KEY_INTERPOLATION_RE = /(?:\#{.*?}|\*+|\:+)/.freeze
42
- def key_expression?(k)
43
- @key_is_expr ||= {}
44
- if @key_is_expr[k].nil?
45
- @key_is_expr[k] = (k =~ KEY_INTERPOLATION_RE || k.end_with?('.'.freeze))
46
- end
47
- @key_is_expr[k]
48
- end
49
35
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module LocaleList
3
4
  extend self
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module LocalePathname
3
4
  extend self
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks::Logging
2
3
  extend self
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'set'
2
3
  module I18n::Tasks
3
4
  module MissingKeys
@@ -79,7 +80,7 @@ module I18n::Tasks
79
80
  base = data[compare_to].first.children
80
81
  data[locale].select_keys(root: false) { |key, node|
81
82
  other_node = base[key]
82
- other_node && node.value == other_node.value && !ignore_key?(key, :eq_base, locale)
83
+ other_node && !node.reference? && node.value == other_node.value && !ignore_key?(key, :eq_base, locale)
83
84
  }.set_root_key!(locale, type: :eq_base)
84
85
  end
85
86
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'set'
2
3
  module I18n::Tasks::PluralKeys
3
4
  PLURAL_KEY_SUFFIXES = Set.new %w(zero one two few many other)
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+ module I18n::Tasks
3
+ module References
4
+ # Given a raw usage tree and a tree of reference keys in the data, return 3 trees:
5
+ # 1. Raw references -- a subset of the usages tree with keys that are reference key usages.
6
+ # 2. Resolved references -- all the used references in their fully resolved form.
7
+ # 3. Reference keys -- all the used reference keys.
8
+ def process_references(usages, data_references = merge_reference_trees(data_forest.select_keys { |_, node| node.reference? }))
9
+ fail ArgumentError.new('usages must be a Data::Tree::Instance') unless usages.is_a?(Data::Tree::Siblings)
10
+ fail ArgumentError.new('all_references must be a Data::Tree::Instance') unless data_references.is_a?(Data::Tree::Siblings)
11
+ raw_refs = empty_forest
12
+ resolved_refs = empty_forest
13
+ refs = empty_forest
14
+ data_references.key_to_node.each do |ref_key_part, ref_node|
15
+ usages.each do |usage_node|
16
+ next unless usage_node.key == ref_key_part
17
+ if ref_node.leaf?
18
+ unless refs.key_to_node.key?(ref_node.key)
19
+ refs.merge_node!(Data::Tree::Node.new(key: ref_node.key, data: usage_node.data))
20
+ end
21
+ resolved_refs.merge!(
22
+ Data::Tree::Siblings.from_key_names([ref_node.value.to_s]) { |_, resolved_node|
23
+ raw_refs.merge_node!(usage_node)
24
+ if usage_node.leaf?
25
+ resolved_node.data.merge!(usage_node.data)
26
+ else
27
+ resolved_node.children = usage_node.children
28
+ end
29
+ }.tap { |new_resolved_refs|
30
+ refs.key_to_node[ref_node.key].data.tap { |ref_data|
31
+ ref_data[:occurrences] ||= []
32
+ new_resolved_refs.leaves { |leaf| ref_data[:occurrences].concat(leaf.data[:occurrences] || []) }
33
+ ref_data[:occurrences].sort_by!(&:path)
34
+ ref_data[:occurrences].uniq!
35
+ }
36
+ })
37
+ else
38
+ child_raw_refs, child_resolved_refs, child_refs = process_references(usage_node.children, ref_node.children)
39
+ raw_refs.merge_node! Data::Tree::Node.new(key: ref_node.key, children: child_raw_refs) unless child_raw_refs.empty?
40
+ resolved_refs.merge! child_resolved_refs
41
+ refs.merge_node! Data::Tree::Node.new(key: ref_node.key, children: child_refs) unless child_refs.empty?
42
+ end
43
+ end
44
+ end
45
+ [raw_refs, resolved_refs, refs]
46
+ end
47
+
48
+ # Given a forest of references, merge trees into one tree, ensuring there are no conflicting references.
49
+ # @param roots [Data::Tree::Siblings]
50
+ # @return [Data::Tree::Siblings]
51
+ def merge_reference_trees(roots)
52
+ roots.inject(empty_forest) do |forest, root|
53
+ root.keys { |full_key, node|
54
+ log_warn(
55
+ "Self-referencing key #{node.full_key(root: false).inspect} in #{node.data[:locale].inspect}"
56
+ ) if full_key == node.value.to_s
57
+ }
58
+ forest.merge!(
59
+ root.children,
60
+ on_leaves_merge: -> (node, other) {
61
+ log_warn(
62
+ "Conflicting references: #{node.full_key(root: false)} ⮕ #{node.value} in #{node.data[:locale]}, but ⮕ #{other.value} in #{other.data[:locale]}"
63
+ ) if node.value != other.value
64
+ })
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks::Reports
2
3
  class Base
3
4
  include I18n::Tasks::Logging
@@ -27,11 +28,9 @@ module I18n::Tasks::Reports
27
28
  "Same value as #{locale} (#{key_values.count || '∅'})"
28
29
  end
29
30
 
30
- def used_title(used_tree)
31
- leaves = used_tree.leaves.to_a
32
- filter = used_tree.first.root.data[:key_filter]
33
- used_n = leaves.map { |node| node.data[:occurrences].size }.reduce(:+).to_i
34
- "#{leaves.length} key#{'s' if leaves.size != 1}#{" matching '#{filter}'" if filter}#{" (#{used_n} usage#{'s' if used_n != 1})" if used_n > 0}"
31
+ def used_title(keys_nodes, filter)
32
+ used_n = keys_nodes.map { |_k, node| node.data[:occurrences].size }.reduce(:+).to_i
33
+ "#{keys_nodes.size} key#{'s' if keys_nodes.size != 1}#{" matching '#{filter}'" if filter}#{" (#{used_n} usage#{'s' if used_n != 1})" if used_n > 0}"
35
34
  end
36
35
 
37
36
  # Sort keys by their attributes in order
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/reports/base'
2
3
  require 'fileutils'
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/reports/base'
2
3
  require 'terminal-table'
3
4
  module I18n
@@ -29,8 +30,12 @@ module I18n
29
30
  end
30
31
 
31
32
  def used_keys(used_tree = task.used_tree)
32
- print_title used_title(used_tree)
33
- keys_nodes = used_tree.keys.to_a
33
+ # For the used tree we may have usage nodes that are not leaves as references.
34
+ keys_nodes = used_tree.nodes.select { |node| !!node.data[:occurrences] }.map { |node|
35
+ [node.full_key(root: false), node]
36
+ }
37
+ print_title used_title(keys_nodes, used_tree.first.root.data[:key_filter])
38
+ # Group multiple nodes
34
39
  if keys_nodes.present?
35
40
  keys_nodes.sort! { |a, b| a[0] <=> b[0] }.each do |key, node|
36
41
  print_occurrences node, key
@@ -80,13 +85,31 @@ module I18n
80
85
  if leaf[:type] == :missing_used
81
86
  first_occurrence leaf
82
87
  else
83
- "#{cyan leaf[:data][:missing_diff_locale]} #{leaf[:value].to_s.strip}"
88
+ "#{cyan leaf[:data][:missing_diff_locale]} #{format_value(leaf[:value].is_a?(String) ? leaf[:value].strip : leaf[:value])}"
84
89
  end
85
90
  end
86
91
 
92
+ def format_value(val)
93
+ val.is_a?(Symbol) ? "#{bold(yellow('⮕ '))}#{yellow(val.to_s)}" : val.to_s.strip
94
+ end
95
+
96
+ def format_reference_desc(node)
97
+ case node.data[:type]
98
+ when :reference_usage
99
+ bold(yellow('(reference)'))
100
+ when :reference_usage_resolved
101
+ bold(yellow('(resolved reference)'))
102
+ when :reference_usage_key
103
+ bold(yellow('(reference key)'))
104
+ end
105
+ end
106
+
87
107
  def print_occurrences(node, full_key = node.full_key)
88
108
  occurrences = node.data[:occurrences]
89
- puts "#{bold "#{full_key}"} #{green(occurrences.size.to_s) if occurrences.size > 1}"
109
+ puts [bold("#{full_key}"),
110
+ format_reference_desc(node),
111
+ (green(occurrences.size.to_s) if occurrences.size > 1)
112
+ ].compact.join ' '
90
113
  occurrences.each do |occurrence|
91
114
  puts " #{key_occurrence full_key, occurrence}"
92
115
  end
@@ -98,7 +121,7 @@ module I18n
98
121
  bold(cyan(I18n.t('i18n_tasks.common.key'))),
99
122
  I18n.t('i18n_tasks.common.value')] do |t|
100
123
  t.rows = locale_key_values.map { |(locale, k, v)|
101
- [{value: cyan(locale), alignment: :center}, cyan(k), v.to_s]
124
+ [{value: cyan(locale), alignment: :center}, cyan(k), format_value(v)]
102
125
  }
103
126
  end
104
127
  else
@@ -133,7 +156,7 @@ module I18n
133
156
 
134
157
  def key_occurrence(full_key, occurrence)
135
158
  location = green "#{occurrence.path}:#{occurrence.line_num}"
136
- source = highlight_key(full_key, occurrence.line, occurrence.line_pos..-1).strip
159
+ source = highlight_key(occurrence.raw_key || full_key, occurrence.line, occurrence.line_pos..-1).strip
137
160
  "#{location} #{source}"
138
161
  end
139
162
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/scanners/scanner'
2
3
 
3
4
  module I18n::Tasks::Scanners
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/scanners/files/file_finder'
2
3
  module I18n::Tasks::Scanners::Files
3
4
  # Finds the files in the specified search paths with support for exclusion / inclusion patterns.
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/scanners/files/caching_file_finder'
2
3
 
3
4
  module I18n::Tasks::Scanners::Files
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/scanners/files/file_reader'
2
3
  module I18n::Tasks::Scanners::Files
3
4
  # Reads the files in 'rb' mode and UTF-8 encoding.
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks::Scanners::Files
2
3
  # Finds the files in the specified search paths with support for exclusion / inclusion patterns.
3
4
  #
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks::Scanners::Files
2
3
  # Reads the files in 'rb' mode and UTF-8 encoding.
3
4
  #
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n
2
3
  module Tasks
3
4
  module Scanners
@@ -9,7 +10,7 @@ module I18n
9
10
  # @param contents [String] contents of the file at the path.
10
11
  # @param position [Fixnum] position just before the beginning of the match.
11
12
  # @return [Results::Occurrence]
12
- def occurrence_from_position(path, contents, position)
13
+ def occurrence_from_position(path, contents, position, raw_key: nil)
13
14
  line_begin = contents.rindex(/^/, position - 1)
14
15
  line_end = contents.index(/.(?=\r?\n|$)/, position)
15
16
  Results::Occurrence.new(
@@ -17,7 +18,8 @@ module I18n
17
18
  pos: position,
18
19
  line_num: contents[0..position].count("\n".freeze) + 1,
19
20
  line_pos: position - line_begin + 1,
20
- line: contents[line_begin..line_end])
21
+ line: contents[line_begin..line_end],
22
+ raw_key: raw_key)
21
23
  end
22
24
  end
23
25
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/scanners/file_scanner'
2
3
  require 'i18n/tasks/scanners/relative_keys'
3
4
  require 'i18n/tasks/scanners/occurrence_from_position'
@@ -17,13 +18,13 @@ module I18n::Tasks::Scanners
17
18
  protected
18
19
 
19
20
  # Extract i18n keys from file based on the pattern which must capture the key literal.
20
- # @return [Array<[key, Results::KeyOccurrence]>] each occurrence found in the file
21
+ # @return [Array<[key, Results::Occurrence]>] each occurrence found in the file
21
22
  def scan_file(path)
22
23
  keys = []
23
24
  text = read_file(path)
24
25
  text.scan(@pattern) do |match|
25
26
  src_pos = Regexp.last_match.offset(0).first
26
- location = occurrence_from_position(path, text, src_pos)
27
+ location = occurrence_from_position(path, text, src_pos, raw_key: strip_literal(match[0]))
27
28
  next if exclude_line?(location.line, path)
28
29
  key = match_to_key(match, path, location)
29
30
  next unless key
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/scanners/pattern_scanner'
2
3
 
3
4
  module I18n::Tasks::Scanners
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n
2
3
  module Tasks
3
4
  module Scanners
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/scanners/results/occurrence'
2
3
 
3
4
  module I18n::Tasks::Scanners::Results
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module Scanners
3
4
  module Results
@@ -23,27 +24,33 @@ module I18n::Tasks
23
24
  # @return [String, nil] the value of the `default:` argument of the translate call.
24
25
  attr_reader :default_arg
25
26
 
27
+ # @return [String, nil] the raw key (for relative keys and references)
28
+ attr_accessor :raw_key
29
+
26
30
  # @param path [String]
27
31
  # @param pos [Fixnum]
28
32
  # @param line_num [Fixnum]
29
33
  # @param line_pos [Fixnum]
30
34
  # @param line [String]
35
+ # @param raw_key [String, nil]
31
36
  # @param default_arg [String, nil]
32
- def initialize(path:, pos:, line_num:, line_pos:, line:, default_arg: nil)
37
+ def initialize(path:, pos:, line_num:, line_pos:, line:, raw_key: nil, default_arg: nil)
33
38
  @path = path
34
39
  @pos = pos
35
40
  @line_num = line_num
36
41
  @line_pos = line_pos
37
42
  @line = line
43
+ @raw_key = raw_key
38
44
  @default_arg = default_arg
39
45
  end
40
46
 
41
47
  def inspect
42
- "Occurrence(#{@path}:#{@line_num}:#{@line_pos}:(#{@pos})"
48
+ "Occurrence(#{@path}:#{@line_num}:#{@line_pos}:#{@pos}:#{@raw_key}:#{@default_arg})"
43
49
  end
44
50
 
45
51
  def ==(other)
46
- other.path == @path && other.pos == @pos && other.line_num == @line_num && other.line == @line
52
+ other.path == @path && other.pos == @pos && other.line_num == @line_num && other.line == @line &&
53
+ other.raw_key == @raw_key && other.default_arg == @default_arg
47
54
  end
48
55
 
49
56
  def eql?(other)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'ast'
2
3
  require 'set'
3
4
  module I18n::Tasks::Scanners
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/scanners/file_scanner'
2
3
  require 'i18n/tasks/scanners/relative_keys'
3
4
  require 'i18n/tasks/scanners/ruby_ast_call_finder'
@@ -75,7 +76,7 @@ module I18n::Tasks::Scanners
75
76
  else
76
77
  key
77
78
  end
78
- [full_key, range_to_occurrence(location.expression, default_arg: default_arg)]
79
+ [full_key, range_to_occurrence(key, location.expression, default_arg: default_arg)]
79
80
  end
80
81
  end
81
82
 
@@ -163,16 +164,18 @@ module I18n::Tasks::Scanners
163
164
  /controllers|mailers/.match(path)
164
165
  end
165
166
 
167
+ # @param raw_key [String]
166
168
  # @param range [Parser::Source::Range]
167
169
  # @param default_arg [String, nil]
168
170
  # @return [Results::Occurrence]
169
- def range_to_occurrence(range, default_arg: nil)
171
+ def range_to_occurrence(raw_key, range, default_arg: nil)
170
172
  Results::Occurrence.new(
171
173
  path: range.source_buffer.name,
172
174
  pos: range.begin_pos,
173
175
  line_num: range.line,
174
176
  line_pos: range.column,
175
177
  line: range.source_line,
178
+ raw_key: raw_key,
176
179
  default_arg: default_arg)
177
180
  end
178
181
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/scanners/results/key_occurrences'
2
3
 
3
4
  module I18n::Tasks::Scanners
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks/scanners/scanner'
2
3
 
3
4
  module I18n::Tasks::Scanners
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n
2
3
  module Tasks
3
4
  module SplitKey
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module Stats
3
4
  def forest_stats(forest)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module I18n::Tasks
2
3
  module StringInterpolation
3
4
  extend self
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'set'
2
3
 
3
4
  module I18n
@@ -11,10 +12,13 @@ module I18n
11
12
  # @param [String] locale
12
13
  # @param [Boolean] strict if true, do not match dynamic keys
13
14
  def unused_tree(locale: base_locale, strict: nil)
15
+ used_key_names = used_tree(strict: true).keys.reject {|_key, node|
16
+ node.data[:type] == :used_reference_key_raw
17
+ }.map(&:first)
14
18
  collapse_plural_nodes! data[locale].select_keys { |key, _node|
15
19
  !ignore_key?(key, :unused) &&
16
20
  (strict || !used_in_expr?(key)) &&
17
- !used_key?(depluralize_key(key, locale))
21
+ !used_key_names.include?(depluralize_key(key, locale))
18
22
  }
19
23
  end
20
24
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'find'
2
3
  require 'i18n/tasks/scanners/pattern_with_scope_scanner'
3
4
  require 'i18n/tasks/scanners/ruby_ast_scanner'
@@ -30,8 +31,26 @@ module I18n::Tasks
30
31
  # @param key_filter [String] only return keys matching this pattern.
31
32
  # @param strict [Boolean] if true, dynamic keys are excluded (e.g. `t("category.#{ category.key }")`)
32
33
  # @return [Data::Tree::Siblings]
33
- def used_tree(key_filter: nil, strict: nil)
34
- keys = ((@used_tree ||= {})[strict?(strict)] ||= scanner(strict: strict).keys.freeze)
34
+ def used_tree(key_filter: nil, strict: nil, include_raw_references: false)
35
+ src_tree = used_in_source_tree(key_filter: key_filter, strict: strict)
36
+
37
+ raw_refs, resolved_refs, used_refs = process_references(src_tree['used'].children)
38
+ raw_refs.leaves { |node| node.data[:type] = :reference_usage }
39
+ resolved_refs.leaves { |node| node.data[:type] = :reference_usage_resolved }
40
+ used_refs.leaves { |node| node.data[:type] = :reference_usage_key }
41
+ src_tree.tap do |result|
42
+ tree = result['used'].children
43
+ tree.subtract_by_key!(raw_refs)
44
+ if include_raw_references
45
+ tree.merge!(raw_refs)
46
+ end
47
+ tree.merge!(used_refs).merge!(resolved_refs)
48
+ end
49
+ end
50
+
51
+ def used_in_source_tree(key_filter: nil, strict: nil)
52
+ keys = ((@keys_used_in_source_tree ||= {})[strict?(strict)] ||=
53
+ scanner(strict: strict).keys.freeze)
35
54
  if key_filter
36
55
  key_filter_re = compile_key_pattern(key_filter)
37
56
  keys = keys.reject { |k| k.key !~ key_filter_re }
@@ -43,6 +62,7 @@ module I18n::Tasks
43
62
  ).to_siblings
44
63
  end
45
64
 
65
+
46
66
  def scanner(strict: nil)
47
67
  (@scanner ||= {})[strict?(strict)] ||= begin
48
68
  shared_options = search_config.dup
@@ -102,20 +122,13 @@ module I18n::Tasks
102
122
  @caching_file_reader ||= Scanners::Files::CachingFileReader.new
103
123
  end
104
124
 
105
- def used_key_names(strict: nil)
106
- (@used_key_names ||= {})[strict?(strict)] ||= used_tree(strict: strict).key_names
107
- end
108
-
109
- # whether the key is used in the source
110
- def used_key?(key)
111
- used_key_names(strict: true).include?(key)
112
- end
113
-
114
125
  # @return whether the key is potentially used in a code expression such as `t("category.#{ category_key }")`
115
126
  def used_in_expr?(key)
116
127
  !!(key =~ expr_key_re)
117
128
  end
118
129
 
130
+ private
131
+
119
132
  # @param strict [Boolean, nil]
120
133
  # @return [Boolean]
121
134
  def strict?(strict)
@@ -123,16 +136,43 @@ module I18n::Tasks
123
136
  end
124
137
 
125
138
  # keys in the source that end with a ., e.g. t("category.#{ cat.i18n_key }") or t("category." + category.key)
126
- def expr_key_re
139
+ # @param [String] replacement for interpolated values.
140
+ def expr_key_re(replacement: ':'.freeze)
127
141
  @expr_key_re ||= begin
128
- patterns = used_key_names(strict: false).select { |k| key_expression?(k) }.map { |k|
129
- pattern = key_match_pattern(k)
130
- # disallow patterns with no keys
131
- next if pattern =~ /\A(:\.)*:\z/
142
+ # disallow patterns with no keys
143
+ ignore_pattern_re = /\A[\.#{replacement}]*\z/
144
+ patterns = used_in_source_tree(strict: false).key_names.select { |k|
145
+ k.end_with?('.'.freeze) || k =~ /\#{/.freeze
146
+ }.map { |k|
147
+ pattern = "#{replace_key_exp(k, replacement)}#{replacement if k.end_with?('.'.freeze)}"
148
+ next if pattern =~ ignore_pattern_re
132
149
  pattern
133
150
  }.compact
134
151
  compile_key_pattern "{#{patterns * ','}}"
135
152
  end
136
153
  end
154
+
155
+ # Replace interpolations in dynamic keys such as "category.#{category.i18n_key}".
156
+ # @param key [String]
157
+ # @param replacement [String]
158
+ # @return [String]
159
+ def replace_key_exp(key, replacement)
160
+ scanner = StringScanner.new(key)
161
+ braces = []
162
+ result = []
163
+ while (match_until = scanner.scan_until(/(?:#?\{|})/.freeze))
164
+ if scanner.matched == '#{'.freeze
165
+ braces << scanner.matched
166
+ result << match_until[0..-3] if braces.length == 1
167
+ elsif scanner.matched == '}'
168
+ prev_brace = braces.pop
169
+ result << replacement if braces.empty? && prev_brace == '#{'.freeze
170
+ else
171
+ braces << '{'.freeze
172
+ end
173
+ end
174
+ result << key[scanner.pos..-1] unless scanner.eos?
175
+ result.join
176
+ end
137
177
  end
138
178
  end
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  module I18n
2
3
  module Tasks
3
- VERSION = '0.9.2'
4
+ VERSION = '0.9.3'
4
5
  end
5
6
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'i18n/tasks'
2
3
 
3
4
  RSpec.describe 'I18n' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: i18n-tasks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - glebm
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-11-23 00:00:00.000000000 Z
11
+ date: 2016-02-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -270,6 +270,7 @@ files:
270
270
  - lib/i18n/tasks/logging.rb
271
271
  - lib/i18n/tasks/missing_keys.rb
272
272
  - lib/i18n/tasks/plural_keys.rb
273
+ - lib/i18n/tasks/references.rb
273
274
  - lib/i18n/tasks/reports/base.rb
274
275
  - lib/i18n/tasks/reports/spreadsheet.rb
275
276
  - lib/i18n/tasks/reports/terminal.rb
@@ -322,7 +323,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
322
323
  version: '0'
323
324
  requirements: []
324
325
  rubyforge_project:
325
- rubygems_version: 2.4.8
326
+ rubygems_version: 2.5.1
326
327
  signing_key:
327
328
  specification_version: 4
328
329
  summary: Manage localization and translation with the awesome power of static analysis