i18n-tasks 0.9.29 → 0.9.34

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +25 -5
  3. data/config/locales/en.yml +6 -0
  4. data/config/locales/ru.yml +6 -0
  5. data/i18n-tasks.gemspec +9 -6
  6. data/lib/i18n/tasks/cli.rb +12 -14
  7. data/lib/i18n/tasks/command/commander.rb +1 -1
  8. data/lib/i18n/tasks/command/commands/data.rb +8 -6
  9. data/lib/i18n/tasks/command/commands/eq_base.rb +2 -2
  10. data/lib/i18n/tasks/command/commands/health.rb +7 -6
  11. data/lib/i18n/tasks/command/commands/interpolations.rb +2 -2
  12. data/lib/i18n/tasks/command/commands/meta.rb +1 -1
  13. data/lib/i18n/tasks/command/commands/missing.rb +8 -7
  14. data/lib/i18n/tasks/command/commands/tree.rb +8 -6
  15. data/lib/i18n/tasks/command/commands/usages.rb +7 -6
  16. data/lib/i18n/tasks/command/dsl.rb +4 -4
  17. data/lib/i18n/tasks/command/option_parsers/enum.rb +2 -0
  18. data/lib/i18n/tasks/command/option_parsers/locale.rb +2 -1
  19. data/lib/i18n/tasks/command/options/data.rb +1 -0
  20. data/lib/i18n/tasks/command/options/locales.rb +5 -5
  21. data/lib/i18n/tasks/concurrent/cached_value.rb +2 -0
  22. data/lib/i18n/tasks/configuration.rb +5 -4
  23. data/lib/i18n/tasks/console_context.rb +1 -1
  24. data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +1 -1
  25. data/lib/i18n/tasks/data/file_formats.rb +2 -0
  26. data/lib/i18n/tasks/data/file_system_base.rb +7 -6
  27. data/lib/i18n/tasks/data/router/conservative_router.rb +2 -1
  28. data/lib/i18n/tasks/data/router/pattern_router.rb +2 -0
  29. data/lib/i18n/tasks/data/tree/node.rb +7 -5
  30. data/lib/i18n/tasks/data/tree/nodes.rb +2 -2
  31. data/lib/i18n/tasks/data/tree/siblings.rb +10 -3
  32. data/lib/i18n/tasks/data/tree/traversal.rb +17 -10
  33. data/lib/i18n/tasks/html_keys.rb +2 -4
  34. data/lib/i18n/tasks/ignore_keys.rb +4 -3
  35. data/lib/i18n/tasks/interpolations.rb +4 -2
  36. data/lib/i18n/tasks/key_pattern_matching.rb +3 -2
  37. data/lib/i18n/tasks/missing_keys.rb +33 -7
  38. data/lib/i18n/tasks/plural_keys.rb +6 -1
  39. data/lib/i18n/tasks/references.rb +2 -0
  40. data/lib/i18n/tasks/reports/base.rb +3 -2
  41. data/lib/i18n/tasks/reports/terminal.rb +10 -9
  42. data/lib/i18n/tasks/scanners/file_scanner.rb +4 -3
  43. data/lib/i18n/tasks/scanners/files/caching_file_finder.rb +0 -3
  44. data/lib/i18n/tasks/scanners/files/file_finder.rb +3 -2
  45. data/lib/i18n/tasks/scanners/occurrence_from_position.rb +3 -3
  46. data/lib/i18n/tasks/scanners/pattern_scanner.rb +7 -4
  47. data/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb +4 -2
  48. data/lib/i18n/tasks/scanners/relative_keys.rb +1 -0
  49. data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +18 -15
  50. data/lib/i18n/tasks/scanners/ruby_key_literals.rb +3 -3
  51. data/lib/i18n/tasks/scanners/scanner_multiplexer.rb +2 -0
  52. data/lib/i18n/tasks/split_key.rb +2 -0
  53. data/lib/i18n/tasks/string_interpolation.rb +1 -0
  54. data/lib/i18n/tasks/translation.rb +6 -3
  55. data/lib/i18n/tasks/translators/base_translator.rb +3 -1
  56. data/lib/i18n/tasks/translators/deepl_translator.rb +9 -2
  57. data/lib/i18n/tasks/translators/google_translator.rb +2 -0
  58. data/lib/i18n/tasks/translators/yandex_translator.rb +63 -0
  59. data/lib/i18n/tasks/used_keys.rb +15 -11
  60. data/lib/i18n/tasks/version.rb +1 -1
  61. data/templates/config/i18n-tasks.yml +1 -1
  62. data/templates/rspec/i18n_spec.rb +9 -2
  63. metadata +48 -31
  64. data/lib/i18n/tasks/rainbow_utils.rb +0 -15
@@ -12,6 +12,7 @@ module I18n
12
12
  def absolute_key(key, path, roots: config[:relative_roots], calling_method: nil)
13
13
  return key unless key.start_with?(DOT)
14
14
  fail 'roots argument is required' unless roots.present?
15
+
15
16
  normalized_path = File.expand_path(path)
16
17
  (root = path_root(normalized_path, roots)) ||
17
18
  fail(CommandError, "Cannot resolve relative key \"#{key}\".\n" \
@@ -5,7 +5,7 @@ require 'i18n/tasks/scanners/relative_keys'
5
5
  require 'i18n/tasks/scanners/ruby_ast_call_finder'
6
6
  require 'parser/current'
7
7
 
8
- # rubocop:disable Metrics/AbcSize,Metrics/BlockNesting,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
8
+ # rubocop:disable Metrics/AbcSize,Metrics/BlockNesting,Metrics/PerceivedComplexity
9
9
  # TODO: make this class more readable.
10
10
 
11
11
  module I18n::Tasks::Scanners
@@ -14,11 +14,11 @@ module I18n::Tasks::Scanners
14
14
  include RelativeKeys
15
15
  include AST::Sexp
16
16
 
17
- MAGIC_COMMENT_PREFIX = /\A.\s*i18n-tasks-use\s+/
18
- RECEIVER_MESSAGES = [nil, AST::Node.new(:const, [nil, :I18n])].product(%i[t translate])
17
+ MAGIC_COMMENT_PREFIX = /\A.\s*i18n-tasks-use\s+/.freeze
18
+ RECEIVER_MESSAGES = [nil, AST::Node.new(:const, [nil, :I18n])].product(%i[t t! translate translate!])
19
19
 
20
20
  def initialize(**args)
21
- super(args)
21
+ super(**args)
22
22
  @parser = ::Parser::CurrentRuby.new
23
23
  @magic_comment_parser = ::Parser::CurrentRuby.new
24
24
  @call_finder = RubyAstCallFinder.new(
@@ -71,6 +71,7 @@ module I18n::Tasks::Scanners
71
71
  scope = extract_string(scope_node.children[1],
72
72
  array_join_with: '.', array_flatten: true, array_reject_blank: true)
73
73
  return nil if scope.nil? && scope_node.type != :nil
74
+
74
75
  key = [scope, key].join('.') unless scope == ''
75
76
  end
76
77
  default_arg = if (default_arg_node = extract_hash_pair(second_arg_node, 'default'))
@@ -95,6 +96,7 @@ module I18n::Tasks::Scanners
95
96
  def extract_hash_pair(node, key)
96
97
  node.children.detect do |child|
97
98
  next unless child.type == :pair
99
+
98
100
  key_node = child.children[0]
99
101
  %i[sym str].include?(key_node.type) && key_node.children[0].to_s == key
100
102
  end
@@ -115,15 +117,15 @@ module I18n::Tasks::Scanners
115
117
  def extract_string(node, array_join_with: nil, array_flatten: false, array_reject_blank: false)
116
118
  if %i[sym str int].include?(node.type)
117
119
  node.children[0].to_s
118
- elsif %i[true false].include?(node.type) # rubocop:disable Lint/BooleanSymbol
120
+ elsif %i[true false].include?(node.type)
119
121
  node.type.to_s
120
122
  elsif node.type == :nil
121
123
  ''
122
124
  elsif node.type == :array && array_join_with
123
125
  extract_array_as_string(
124
126
  node,
125
- array_join_with: array_join_with,
126
- array_flatten: array_flatten,
127
+ array_join_with: array_join_with,
128
+ array_flatten: array_flatten,
127
129
  array_reject_blank: array_reject_blank
128
130
  ).tap do |str|
129
131
  # `nil` is returned when a dynamic value is encountered in strict mode. Propagate:
@@ -149,11 +151,12 @@ module I18n::Tasks::Scanners
149
151
  # @return [String, nil] `nil` is returned only when a dynamic value is encountered in strict mode.
150
152
  def extract_array_as_string(node, array_join_with:, array_flatten: false, array_reject_blank: false)
151
153
  children_strings = node.children.map do |child|
152
- if %i[sym str int true false].include?(child.type) # rubocop:disable Lint/BooleanSymbol
154
+ if %i[sym str int true false].include?(child.type)
153
155
  extract_string child
154
156
  else
155
157
  # ignore dynamic argument in strict mode
156
158
  return nil if config[:strict]
159
+
157
160
  if %i[dsym dstr].include?(child.type) || (child.type == :array && array_flatten)
158
161
  extract_string(child, array_join_with: array_join_with)
159
162
  else
@@ -180,12 +183,12 @@ module I18n::Tasks::Scanners
180
183
  # @return [Results::Occurrence]
181
184
  def range_to_occurrence(raw_key, range, default_arg: nil)
182
185
  Results::Occurrence.new(
183
- path: range.source_buffer.name,
184
- pos: range.begin_pos,
185
- line_num: range.line,
186
- line_pos: range.column,
187
- line: range.source_line,
188
- raw_key: raw_key,
186
+ path: range.source_buffer.name,
187
+ pos: range.begin_pos,
188
+ line_num: range.line,
189
+ line_pos: range.column,
190
+ line: range.source_line,
191
+ raw_key: raw_key,
189
192
  default_arg: default_arg
190
193
  )
191
194
  end
@@ -203,4 +206,4 @@ module I18n::Tasks::Scanners
203
206
  end
204
207
  end
205
208
  end
206
- # rubocop:enable Metrics/AbcSize,Metrics/BlockNesting,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
209
+ # rubocop:enable Metrics/AbcSize,Metrics/BlockNesting,Metrics/PerceivedComplexity
@@ -2,7 +2,7 @@
2
2
 
3
3
  module I18n::Tasks::Scanners
4
4
  module RubyKeyLiterals
5
- LITERAL_RE = /:?".+?"|:?'.+?'|:\w+/
5
+ LITERAL_RE = /:?".+?"|:?'.+?'|:\w+/.freeze
6
6
 
7
7
  # Match literals:
8
8
  # * String: '', "#{}"
@@ -20,8 +20,8 @@ module I18n::Tasks::Scanners
20
20
  literal
21
21
  end
22
22
 
23
- VALID_KEY_CHARS = /(?:[[:word:]]|[-.?!:;À-ž])/
24
- VALID_KEY_RE = /^#{VALID_KEY_CHARS}+$/
23
+ VALID_KEY_CHARS = /(?:[[:word:]]|[-.?!:;À-ž])/.freeze
24
+ VALID_KEY_RE = /^#{VALID_KEY_CHARS}+$/.freeze
25
25
 
26
26
  def valid_key?(key)
27
27
  key =~ VALID_KEY_RE && !key.end_with?('.')
@@ -9,6 +9,7 @@ module I18n::Tasks::Scanners
9
9
  class ScannerMultiplexer < Scanner
10
10
  # @param scanners [Array<Scanner>]
11
11
  def initialize(scanners:)
12
+ super()
12
13
  @scanners = scanners
13
14
  end
14
15
 
@@ -25,6 +26,7 @@ module I18n::Tasks::Scanners
25
26
  # @return [Array<Array<Results::KeyOccurrences>>]
26
27
  def collect_results
27
28
  return [@scanners[0].keys] if @scanners.length == 1
29
+
28
30
  Array.new(@scanners.length).tap do |results|
29
31
  results_mutex = Mutex.new
30
32
  @scanners.map.with_index do |scanner, i|
@@ -15,6 +15,7 @@ module I18n
15
15
  parts = []
16
16
  pos = 0
17
17
  return [key] if max == 1
18
+
18
19
  key_parts(key) do |part|
19
20
  parts << part
20
21
  pos += part.length + 1
@@ -36,6 +37,7 @@ module I18n
36
37
  # dots inside braces or parenthesis are not split on
37
38
  def key_parts(key, &block)
38
39
  return enum_for(:key_parts, key) unless block
40
+
39
41
  nesting = PARENS
40
42
  counts = PARENS_ZEROS # dup'd later if key contains parenthesis
41
43
  delim = '.'
@@ -6,6 +6,7 @@ module I18n::Tasks
6
6
 
7
7
  def interpolate_soft(s, t = {})
8
8
  return s unless s
9
+
9
10
  t.each do |k, v|
10
11
  pat = "%{#{k}}"
11
12
  s = s.gsub pat, v.to_s if s.include?(pat)
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/translators/deepl_translator.rb'
4
- require 'i18n/tasks/translators/google_translator.rb'
3
+ require 'i18n/tasks/translators/deepl_translator'
4
+ require 'i18n/tasks/translators/google_translator'
5
+ require 'i18n/tasks/translators/yandex_translator'
5
6
 
6
7
  module I18n::Tasks
7
8
  module Translation
8
9
  # @param [I18n::Tasks::Tree::Siblings] forest to translate to the locales of its root nodes
9
10
  # @param [String] from locale
10
- # @param [:deepl, :google] backend
11
+ # @param [:deepl, :google, :yandex] backend
11
12
  # @return [I18n::Tasks::Tree::Siblings] translated forest
12
13
  def translate_forest(forest, from:, backend: :google)
13
14
  case backend
@@ -15,6 +16,8 @@ module I18n::Tasks
15
16
  Translators::DeeplTranslator.new(self).translate_forest(forest, from)
16
17
  when :google
17
18
  Translators::GoogleTranslator.new(self).translate_forest(forest, from)
19
+ when :yandex
20
+ Translators::YandexTranslator.new(self).translate_forest(forest, from)
18
21
  else
19
22
  fail CommandError, "invalid backend: #{backend}"
20
23
  end
@@ -24,6 +24,7 @@ module I18n::Tasks
24
24
  # @return [Array<[String, Object]>] translated list
25
25
  def translate_pairs(list, opts)
26
26
  return [] if list.empty?
27
+
27
28
  opts = opts.dup
28
29
  key_pos = list.each_with_index.inject({}) { |idx, ((k, _v), i)| idx.update(k => i) }
29
30
  # copy reference keys as is, instead of translating
@@ -92,7 +93,7 @@ module I18n::Tasks
92
93
  end
93
94
  end
94
95
 
95
- INTERPOLATION_KEY_RE = /%\{[^}]+}/
96
+ INTERPOLATION_KEY_RE = /%\{[^}]+}/.freeze
96
97
  UNTRANSLATABLE_STRING = 'zxzxzx'
97
98
 
98
99
  # @param [String] value
@@ -110,6 +111,7 @@ module I18n::Tasks
110
111
  # @return [String] 'hello, <round-trippable string>' => 'hello, %{name}'
111
112
  def restore_interpolations(untranslated, translated)
112
113
  return translated if untranslated !~ INTERPOLATION_KEY_RE
114
+
113
115
  values = untranslated.scan(INTERPOLATION_KEY_RE)
114
116
  translated.gsub(/#{Regexp.escape(UNTRANSLATABLE_STRING)}\d+/i) do |m|
115
117
  values[m[UNTRANSLATABLE_STRING.length..-1].to_i]
@@ -17,7 +17,12 @@ module I18n::Tasks::Translators
17
17
  protected
18
18
 
19
19
  def translate_values(list, from:, to:, **options)
20
- DeepL.translate(list, to_deepl_compatible_locale(from), to_deepl_compatible_locale(to), options).map(&:text)
20
+ result = DeepL.translate(list, to_deepl_compatible_locale(from), to_deepl_compatible_locale(to), options)
21
+ if result.is_a?(DeepL::Resources::Text)
22
+ [result.text]
23
+ else
24
+ result.map(&:text)
25
+ end
21
26
  end
22
27
 
23
28
  def options_for_translate_values(**options)
@@ -43,7 +48,8 @@ module I18n::Tasks::Translators
43
48
  # @return [String] 'hello, <i18n>%{name}</i18n>' => 'hello, %{name}'
44
49
  def restore_interpolations(untranslated, translated)
45
50
  return translated if untranslated !~ INTERPOLATION_KEY_RE
46
- translated.gsub(%r{<\/?i18n>}, '')
51
+
52
+ translated.gsub(%r{</?i18n>}, '')
47
53
  rescue StandardError => e
48
54
  raise_interpolation_error(untranslated, translated, e)
49
55
  end
@@ -62,6 +68,7 @@ module I18n::Tasks::Translators
62
68
  def configure_api_key!
63
69
  api_key = @i18n_tasks.translation_config[:deepl_api_key]
64
70
  fail ::I18n::Tasks::CommandError, I18n.t('i18n_tasks.deepl_translate.errors.no_api_key') if api_key.blank?
71
+
65
72
  DeepL.configure { |config| config.auth_key = api_key }
66
73
  end
67
74
  end
@@ -46,6 +46,7 @@ module I18n::Tasks::Translators
46
46
  # Convert 'es-ES' to 'es'
47
47
  def to_google_translate_compatible_locale(locale)
48
48
  return locale unless locale.include?('-') && !SUPPORTED_LOCALES_WITH_REGION.include?(locale)
49
+
49
50
  locale.split('-', 2).first
50
51
  end
51
52
 
@@ -60,6 +61,7 @@ module I18n::Tasks::Translators
60
61
  key ||= translation_config[:api_key]
61
62
  end
62
63
  fail ::I18n::Tasks::CommandError, I18n.t('i18n_tasks.google_translate.errors.no_api_key') if key.blank?
64
+
63
65
  key
64
66
  end
65
67
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/tasks/translators/base_translator'
4
+
5
+ module I18n::Tasks::Translators
6
+ class YandexTranslator < BaseTranslator
7
+ def initialize(*)
8
+ begin
9
+ require 'yandex-translator'
10
+ rescue LoadError
11
+ raise ::I18n::Tasks::CommandError, "Add gem 'yandex-translator' to your Gemfile to use this command"
12
+ end
13
+ super
14
+ end
15
+
16
+ protected
17
+
18
+ def translate_values(list, **options)
19
+ list.map { |item| translator.translate(item, options) }
20
+ end
21
+
22
+ def options_for_translate_values(from:, to:, **options)
23
+ options.merge(
24
+ from: to_yandex_compatible_locale(from),
25
+ to: to_yandex_compatible_locale(to)
26
+ )
27
+ end
28
+
29
+ def options_for_html
30
+ { format: 'html' }
31
+ end
32
+
33
+ def options_for_plain
34
+ { format: 'plain' }
35
+ end
36
+
37
+ def no_results_error_message
38
+ I18n.t('i18n_tasks.yandex_translate.errors.no_results')
39
+ end
40
+
41
+ private
42
+
43
+ # Convert 'es-ES' to 'es'
44
+ def to_yandex_compatible_locale(locale)
45
+ return locale unless locale.include?('-')
46
+
47
+ locale.split('-', 2).first
48
+ end
49
+
50
+ def translator
51
+ @translator ||= Yandex::Translator.new(api_key)
52
+ end
53
+
54
+ def api_key
55
+ @api_key ||= begin
56
+ key = @i18n_tasks.translation_config[:yandex_api_key]
57
+ fail ::I18n::Tasks::CommandError, I18n.t('i18n_tasks.yandex_translate.errors.no_api_key') if key.blank?
58
+
59
+ key
60
+ end
61
+ end
62
+ end
63
+ end
@@ -18,14 +18,15 @@ module I18n::Tasks
18
18
  paths: %w[app/].freeze,
19
19
  relative_roots: %w[app/controllers app/helpers app/mailers app/presenters app/views].freeze,
20
20
  scanners: [
21
- ['::I18n::Tasks::Scanners::RubyAstScanner', only: %w[*.rb]],
22
- ['::I18n::Tasks::Scanners::PatternWithScopeScanner', exclude: %w[*.rb]]
21
+ ['::I18n::Tasks::Scanners::RubyAstScanner', { only: %w[*.rb] }],
22
+ ['::I18n::Tasks::Scanners::PatternWithScopeScanner', { exclude: %w[*.rb] }]
23
23
  ],
24
24
  strict: true
25
25
  }.freeze
26
26
 
27
27
  ALWAYS_EXCLUDE = %w[*.jpg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf *.css *.sass *.scss *.less
28
- *.yml *.json *.zip *.tar.gz *.swf *.flv *.mp3 *.wav *.flac *.webm *.mp4 *.ogg *.opus].freeze
28
+ *.yml *.json *.zip *.tar.gz *.swf *.flv *.mp3 *.wav *.flac *.webm *.mp4 *.ogg *.opus
29
+ *.webp].freeze
29
30
 
30
31
  # Find all keys in the source and return a forest with the keys in absolute form and their occurrences.
31
32
  #
@@ -55,8 +56,8 @@ module I18n::Tasks
55
56
  keys = keys.select { |k| k.key =~ key_filter_re }
56
57
  end
57
58
  Data::Tree::Node.new(
58
- key: 'used',
59
- data: { key_filter: key_filter },
59
+ key: 'used',
60
+ data: { key_filter: key_filter },
60
61
  children: Data::Tree::Siblings.from_key_occurrences(keys)
61
62
  ).to_siblings
62
63
  end
@@ -72,10 +73,11 @@ module I18n::Tasks
72
73
  if args && args[:strict]
73
74
  fail CommandError, 'the strict option is global and cannot be applied on the scanner level'
74
75
  end
76
+
75
77
  ActiveSupport::Inflector.constantize(class_name).new(
76
- config: merge_scanner_configs(shared_options, args || {}),
78
+ config: merge_scanner_configs(shared_options, args || {}),
77
79
  file_finder_provider: caching_file_finder_provider,
78
- file_reader: caching_file_reader
80
+ file_reader: caching_file_reader
79
81
  )
80
82
  end.tap { |scanners| log_verbose { scanners.map { |s| " #{s.class.name} #{s.config.inspect}" } * "\n" } }
81
83
  )
@@ -123,7 +125,7 @@ module I18n::Tasks
123
125
 
124
126
  # @return [Boolean] whether the key is potentially used in a code expression such as `t("category.#{category_key}")`
125
127
  def used_in_expr?(key)
126
- !!(key =~ expr_key_re) # rubocop:disable Style/DoubleNegation
128
+ !!(key =~ expr_key_re)
127
129
  end
128
130
 
129
131
  private
@@ -139,12 +141,13 @@ module I18n::Tasks
139
141
  def expr_key_re(replacement: ':')
140
142
  @expr_key_re ||= begin
141
143
  # disallow patterns with no keys
142
- ignore_pattern_re = /\A[\.#{replacement}]*\z/
144
+ ignore_pattern_re = /\A[.#{replacement}]*\z/
143
145
  patterns = used_in_source_tree(strict: false).key_names.select do |k|
144
146
  k.end_with?('.') || k =~ /\#{/
145
147
  end.map do |k|
146
148
  pattern = "#{replace_key_exp(k, replacement)}#{replacement if k.end_with?('.')}"
147
149
  next if pattern =~ ignore_pattern_re
150
+
148
151
  pattern
149
152
  end.compact
150
153
  compile_key_pattern "{#{patterns * ','}}"
@@ -160,10 +163,11 @@ module I18n::Tasks
160
163
  braces = []
161
164
  result = []
162
165
  while (match_until = scanner.scan_until(/(?:#?\{|})/))
163
- if scanner.matched == '#{'
166
+ case scanner.matched
167
+ when '#{'
164
168
  braces << scanner.matched
165
169
  result << match_until[0..-3] if braces.length == 1
166
- elsif scanner.matched == '}'
170
+ when '}'
167
171
  prev_brace = braces.pop
168
172
  result << replacement if braces.empty? && prev_brace == '#{'
169
173
  else
@@ -2,6 +2,6 @@
2
2
 
3
3
  module I18n
4
4
  module Tasks
5
- VERSION = '0.9.29'
5
+ VERSION = '0.9.34'
6
6
  end
7
7
  end
@@ -32,7 +32,7 @@ data:
32
32
  # This data is not considered unused and is never written to.
33
33
  external:
34
34
  ## Example (replace %#= with %=):
35
- # - "<%#= %x[bundle show vagrant].chomp %>/templates/locales/%{locale}.yml"
35
+ # - "<%#= %x[bundle info vagrant --path].chomp %>/templates/locales/%{locale}.yml"
36
36
 
37
37
  ## Specify the router (see Readme for details). Valid values: conservative_router, pattern_router, or a custom class.
38
38
  # router: conservative_router
@@ -2,10 +2,11 @@
2
2
 
3
3
  require 'i18n/tasks'
4
4
 
5
- RSpec.describe 'I18n' do
5
+ RSpec.describe I18n do
6
6
  let(:i18n) { I18n::Tasks::BaseTask.new }
7
7
  let(:missing_keys) { i18n.missing_keys }
8
8
  let(:unused_keys) { i18n.unused_keys }
9
+ let(:inconsistent_interpolations) { i18n.inconsistent_interpolations }
9
10
 
10
11
  it 'does not have missing keys' do
11
12
  expect(missing_keys).to be_empty,
@@ -21,7 +22,13 @@ RSpec.describe 'I18n' do
21
22
  non_normalized = i18n.non_normalized_paths
22
23
  error_message = "The following files need to be normalized:\n" \
23
24
  "#{non_normalized.map { |path| " #{path}" }.join("\n")}\n" \
24
- 'Please run `i18n-tasks normalize` to fix'
25
+ "Please run `i18n-tasks normalize' to fix"
25
26
  expect(non_normalized).to be_empty, error_message
26
27
  end
28
+
29
+ it 'does not have inconsistent interpolations' do
30
+ error_message = "#{inconsistent_interpolations.leaves.count} i18n keys have inconsistent interpolations.\n" \
31
+ "Run `i18n-tasks check-consistent-interpolations' to show them"
32
+ expect(inconsistent_interpolations).to be_empty, error_message
33
+ end
27
34
  end