i18n-tasks 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -0
  3. data/Gemfile +2 -1
  4. data/README.md +80 -78
  5. data/bin/i18n-tasks +24 -30
  6. data/config/i18n-tasks.yml +87 -0
  7. data/config/locales/en.yml +95 -0
  8. data/i18n-tasks.gemspec +1 -0
  9. data/lib/i18n/tasks.rb +10 -0
  10. data/lib/i18n/tasks/base_task.rb +6 -2
  11. data/lib/i18n/tasks/command/collection.rb +18 -0
  12. data/lib/i18n/tasks/command/commander.rb +72 -0
  13. data/lib/i18n/tasks/command/commands/data.rb +73 -0
  14. data/lib/i18n/tasks/command/commands/eq_base.rb +20 -0
  15. data/lib/i18n/tasks/command/commands/health.rb +26 -0
  16. data/lib/i18n/tasks/command/commands/meta.rb +35 -0
  17. data/lib/i18n/tasks/command/commands/missing.rb +73 -0
  18. data/lib/i18n/tasks/command/commands/tree.rb +92 -0
  19. data/lib/i18n/tasks/command/commands/usages.rb +70 -0
  20. data/lib/i18n/tasks/command/commands/xlsx.rb +27 -0
  21. data/lib/i18n/tasks/command/dsl.rb +27 -0
  22. data/lib/i18n/tasks/command/dsl/cmd.rb +19 -0
  23. data/lib/i18n/tasks/command/dsl/cmd_opt.rb +19 -0
  24. data/lib/i18n/tasks/command/dsl/enum_opt.rb +26 -0
  25. data/lib/i18n/tasks/command/options/common.rb +48 -0
  26. data/lib/i18n/tasks/command/options/enum_opt.rb +44 -0
  27. data/lib/i18n/tasks/command/options/list_opt.rb +11 -0
  28. data/lib/i18n/tasks/command/options/locales.rb +47 -0
  29. data/lib/i18n/tasks/command/options/trees.rb +101 -0
  30. data/lib/i18n/tasks/command_error.rb +3 -0
  31. data/lib/i18n/tasks/commands.rb +22 -169
  32. data/lib/i18n/tasks/configuration.rb +1 -16
  33. data/lib/i18n/tasks/console_context.rb +1 -1
  34. data/lib/i18n/tasks/data.rb +13 -8
  35. data/lib/i18n/tasks/data/file_formats.rb +29 -18
  36. data/lib/i18n/tasks/data/file_system_base.rb +35 -4
  37. data/lib/i18n/tasks/data/router/conservative_router.rb +18 -11
  38. data/lib/i18n/tasks/data/tree/node.rb +5 -15
  39. data/lib/i18n/tasks/data/tree/nodes.rb +0 -3
  40. data/lib/i18n/tasks/data/tree/siblings.rb +32 -2
  41. data/lib/i18n/tasks/data/tree/traversal.rb +117 -96
  42. data/lib/i18n/tasks/google_translation.rb +25 -25
  43. data/lib/i18n/tasks/html_keys.rb +10 -0
  44. data/lib/i18n/tasks/key_pattern_matching.rb +1 -0
  45. data/lib/i18n/tasks/locale_list.rb +19 -0
  46. data/lib/i18n/tasks/missing_keys.rb +32 -33
  47. data/lib/i18n/tasks/plural_keys.rb +1 -1
  48. data/lib/i18n/tasks/reports/base.rb +4 -9
  49. data/lib/i18n/tasks/reports/spreadsheet.rb +5 -5
  50. data/lib/i18n/tasks/reports/terminal.rb +62 -38
  51. data/lib/i18n/tasks/scanners/base_scanner.rb +5 -4
  52. data/lib/i18n/tasks/slop_command.rb +27 -0
  53. data/lib/i18n/tasks/stats.rb +20 -0
  54. data/lib/i18n/tasks/string_interpolation.rb +14 -0
  55. data/lib/i18n/tasks/unused_keys.rb +0 -10
  56. data/lib/i18n/tasks/version.rb +1 -1
  57. data/spec/commands/data_commands_spec.rb +38 -0
  58. data/spec/commands/tree_commands_spec.rb +68 -0
  59. data/spec/fixtures/app/views/index.html.slim +1 -0
  60. data/spec/google_translate_spec.rb +5 -3
  61. data/spec/i18n_spec.rb +18 -0
  62. data/spec/i18n_tasks_spec.rb +8 -8
  63. data/spec/spec_helper.rb +3 -3
  64. data/spec/support/test_codebase.rb +4 -1
  65. data/spec/used_keys_spec.rb +7 -7
  66. data/templates/config/i18n-tasks.yml +2 -2
  67. metadata +48 -4
  68. data/lib/i18n/tasks/commands_base.rb +0 -107
  69. data/lib/i18n/tasks/fill_tasks.rb +0 -40
@@ -0,0 +1,10 @@
1
+ module I18n::Tasks
2
+ module HtmlKeys
3
+ extend self
4
+ HTML_KEY_PATTERN = /[.\-_]html\z/.freeze
5
+
6
+ def html_key?(full_key)
7
+ !!(full_key =~ HTML_KEY_PATTERN)
8
+ end
9
+ end
10
+ end
@@ -1,5 +1,6 @@
1
1
  # coding: utf-8
2
2
  module I18n::Tasks::KeyPatternMatching
3
+ extend self
3
4
  MATCH_NOTHING = /\z\A/
4
5
 
5
6
  # one regex to match any
@@ -0,0 +1,19 @@
1
+ module I18n::Tasks
2
+ module LocaleList
3
+ extend self
4
+
5
+ # @return locales converted to strings, with base locale first, the rest sorted alphabetically
6
+ def normalize_locale_list(locales, base_locale, include_base = false)
7
+ locales = Array(locales).map(&:to_s).sort
8
+ if locales.include?(base_locale)
9
+ [base_locale] + (locales - [base_locale])
10
+ else
11
+ if include_base
12
+ [base_locale] + locales
13
+ else
14
+ locales
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,34 +1,40 @@
1
1
  # coding: utf-8
2
2
  module I18n::Tasks
3
3
  module MissingKeys
4
+
5
+ MISSING_TYPES = {
6
+ used: {glyph: '✗', summary: 'used in code but missing from base locale'},
7
+ diff: {glyph: '∅', summary: 'translated in one locale but not in the other'}
8
+ }
9
+
10
+ def self.missing_keys_types
11
+ @missing_keys_types ||= MISSING_TYPES.keys
12
+ end
13
+
4
14
  def missing_keys_types
5
- @missing_keys_types ||= [:used, :diff]
15
+ MissingKeys.missing_keys_types
6
16
  end
7
17
 
8
- # @param [:missing_used, :missing_diff] type (default nil)
18
+ # @param [:missing_used, :missing_diff] types (default nil)
9
19
  # @return [Siblings]
10
20
  def missing_keys(opts = {})
11
- locales = Array(opts[:locales]).presence || self.locales
12
- types = (Array(opts[:types]).presence || missing_keys_types).map(&:to_s)
13
- validate_missing_types! types
21
+ locales = opts[:locales].presence || self.locales
22
+ types = opts[:types].presence || missing_keys_types
14
23
  base = opts[:base_locale] || base_locale
15
- tree = Data::Tree::Siblings.new
16
-
17
- types.each do |type|
18
- tree.merge! send(:"missing_#{type}_forest", locales, base)
24
+ types.inject(empty_forest) do |f, type|
25
+ f.merge! send(:"missing_#{type}_forest", locales, base)
19
26
  end
20
- tree
21
27
  end
22
28
 
23
29
  def eq_base_keys(opts = {})
24
30
  locales = Array(opts[:locales]).presence || self.locales
25
- (locales - [base_locale]).inject(Data::Tree::Siblings.new) { |tree, locale|
31
+ (locales - [base_locale]).inject(empty_forest) { |tree, locale|
26
32
  tree.merge! equal_values_tree(locale, base_locale)
27
33
  }
28
34
  end
29
35
 
30
36
  def missing_diff_forest(locales, base = base_locale)
31
- tree = Data::Tree::Siblings.new
37
+ tree = empty_forest
32
38
  # present in base but not locale
33
39
  (locales - [base]).each { |locale|
34
40
  tree.merge! missing_diff_tree(locale, base)
@@ -36,7 +42,7 @@ module I18n::Tasks
36
42
  if locales.include?(base)
37
43
  # present in locale but not base
38
44
  (self.locales - [base]).each { |locale|
39
- tree.merge! missing_diff_tree(base, locale).set_root_key(base)
45
+ tree.merge! missing_diff_tree(base, locale)
40
46
  }
41
47
  end
42
48
  tree
@@ -46,30 +52,35 @@ module I18n::Tasks
46
52
  if locales.include?(base)
47
53
  missing_used_tree(base)
48
54
  else
49
- Data::Tree::Siblings.new
55
+ empty_forest
50
56
  end
51
57
  end
52
58
 
53
- def missing_tree(locale, compared_to, collapse_plural = true)
59
+ def missing_tree(locale, compared_to)
54
60
  if locale == compared_to
55
61
  missing_used_tree locale
56
62
  else
57
- missing_diff_tree locale, compared_to, collapse_plural
63
+ missing_diff_tree locale, compared_to
58
64
  end
59
65
  end
60
66
 
61
67
  # keys present in compared_to, but not in locale
62
- def missing_diff_tree(locale, compared_to = base_locale, collapse_plural = true)
68
+ def missing_diff_tree(locale, compared_to = base_locale)
63
69
  data[compared_to].select_keys { |key, _node|
64
- locale_key_missing?(locale, key)
65
- }.set_root_key(locale, type: :missing_diff).tap { |t| collapse_plural_nodes!(t) if collapse_plural }
70
+ locale_key_missing? locale, depluralize_key(key, locale)
71
+ }.set_root_key!(locale, type: :missing_diff).keys { |_key, node|
72
+ if node.data.key?(:path)
73
+ # change path and locale to base
74
+ node.data.update path: LocalePathname.replace_locale(node.data[:path], node.data[:locale], locale), locale: locale
75
+ end
76
+ }
66
77
  end
67
78
 
68
79
  # keys used in the code missing translations in locale
69
80
  def missing_used_tree(locale)
70
81
  used_tree(strict: true).select_keys { |key, _node|
71
82
  locale_key_missing?(locale, key)
72
- }.set_root_key(locale, type: :missing_used)
83
+ }.set_root_key!(locale, type: :missing_used)
73
84
  end
74
85
 
75
86
  def equal_values_tree(locale, compare_to = base_locale)
@@ -77,23 +88,11 @@ module I18n::Tasks
77
88
  data[locale].select_keys(root: false) { |key, node|
78
89
  other_node = base[key]
79
90
  other_node && node.value == other_node.value && !ignore_key?(key, :eq_base, locale)
80
- }.set_root_key(locale, type: :eq_base)
91
+ }.set_root_key!(locale, type: :eq_base)
81
92
  end
82
93
 
83
94
  def locale_key_missing?(locale, key)
84
95
  !key_value?(key, locale) && !ignore_key?(key, :missing)
85
96
  end
86
-
87
- private
88
-
89
- def validate_missing_types!(types)
90
- valid_types = missing_keys_types.map(&:to_s)
91
- types = types.map(&:to_s)
92
- invalid_types = types - valid_types
93
- if invalid_types.present?
94
- raise CommandError.new("Unknown types: #{invalid_types * ', '}. Valid types are: #{valid_types * ', '}.")
95
- end
96
- true
97
- end
98
97
  end
99
98
  end
@@ -5,7 +5,7 @@ module I18n::Tasks::PluralKeys
5
5
  PLURAL_KEY_RE = /\.(?:#{PLURAL_KEY_SUFFIXES.to_a * '|'})$/
6
6
 
7
7
  def collapse_plural_nodes!(tree)
8
- tree.leaves.map(&:parent).uniq.each do |node|
8
+ tree.leaves.select(&:parent?).map(&:parent).uniq.each do |node|
9
9
  children = node.children
10
10
  if children.present? && children.all? { |c| PLURAL_KEY_SUFFIXES.include?(c.key) }
11
11
  node.value = children.to_hash
@@ -12,13 +12,8 @@ module I18n::Tasks::Reports
12
12
 
13
13
  protected
14
14
 
15
- MISSING_TYPES = {
16
- missing_used: {glyph: '', summary: 'used in code but missing from base locale'},
17
- missing_diff: {glyph: '∅', summary: 'translated in one locale but not in the other'}
18
- }
19
-
20
- def missing_types
21
- MISSING_TYPES
15
+ def missing_type_info(type)
16
+ ::I18n::Tasks::MissingKeys::MISSING_TYPES[type.to_s.sub(/\Amissing_/, '').to_sym]
22
17
  end
23
18
 
24
19
  def missing_title(forest)
@@ -36,13 +31,13 @@ module I18n::Tasks::Reports
36
31
  def used_title(used_tree)
37
32
  leaves = used_tree.leaves.to_a
38
33
  filter = used_tree.first.root.data[:key_filter]
39
- used_n = leaves.map { |node| node.data[:source_locations].size }.reduce(:+).to_i
34
+ used_n = leaves.map { |node| node.data[:source_occurrences].size }.reduce(:+).to_i
40
35
  "#{leaves.length} key#{'s' if leaves.size != 1}#{" matching '#{filter}'" if filter}#{" (#{used_n} usage#{'s' if used_n != 1})" if used_n > 0}"
41
36
  end
42
37
 
43
38
  # Sort keys by their attributes in order
44
39
  # @param [Hash] order e.g. {locale: :asc, type: :desc, key: :asc}
45
- def sort_by_attr!(objects, order)
40
+ def sort_by_attr!(objects, order = {locale: :asc, key: :asc})
46
41
  order_keys = order.keys
47
42
  objects.sort! { |a, b|
48
43
  by = order_keys.detect { |by| a[by] != b[by] }
@@ -5,8 +5,8 @@ require 'fileutils'
5
5
  module I18n::Tasks::Reports
6
6
  class Spreadsheet < Base
7
7
 
8
- def save_report(path = nil)
9
- path = 'tmp/i18n-report.xlsx' if path.blank?
8
+ def save_report(path, opts)
9
+ path = path.presence || 'tmp/i18n-report.xlsx'
10
10
  p = Axlsx::Package.new
11
11
  add_missing_sheet p.workbook
12
12
  add_unused_sheet p.workbook
@@ -27,11 +27,11 @@ module I18n::Tasks::Reports
27
27
  regular_style = s.add_style
28
28
  wb.add_worksheet(name: missing_title(tree)) { |sheet|
29
29
  sheet.page_setup.fit_to :width => 1
30
- sheet.add_row ['Type', 'Locale', 'Key', 'Base Value']
30
+ sheet.add_row [I18n.t('i18n_tasks.common.type'), I18n.t('i18n_tasks.common.locale'), I18n.t('i18n_tasks.common.key'), I18n.t('i18n_tasks.common.base_value')]
31
31
  style_header sheet
32
32
  tree.keys do |key, node|
33
33
  locale, type = node.root.data[:locale], node.data[:type]
34
- sheet.add_row [missing_types[type][:summary], locale, key, task.t(key)],
34
+ sheet.add_row [missing_type_info(type)[:summary], locale, key, task.t(key)],
35
35
  styles: [type_cell, locale_cell, regular_style, regular_style]
36
36
  end
37
37
  }
@@ -52,7 +52,7 @@ module I18n::Tasks::Reports
52
52
 
53
53
  def add_locale_key_value_table(wb, keys, worksheet_opts = {})
54
54
  wb.add_worksheet worksheet_opts do |sheet|
55
- sheet.add_row ['Locale', 'Key', 'Value']
55
+ sheet.add_row [I18n.t('i18n_tasks.common.locale'), I18n.t('i18n_tasks.common.key'), I18n.t('i18n_tasks.common.value')]
56
56
  style_header sheet
57
57
  keys.each do |locale_k_v|
58
58
  sheet.add_row locale_k_v
@@ -8,71 +8,50 @@ module I18n
8
8
  include Term::ANSIColor
9
9
 
10
10
  def missing_keys(forest = task.missing_keys)
11
- print_title missing_title(forest)
12
-
11
+ forest = task.collapse_plural_nodes!(forest)
13
12
  if forest.present?
14
- keys_attr = sort_by_attr! forest_to_attr(forest), {locale: :asc, type: :desc, key: :asc}
15
- print_table headings: [cyan(bold('Locale')), cyan(bold 'Key'), 'Details'] do |t|
16
- t.rows = keys_attr.map do |a|
17
- locale, key = a[:locale], a[:key], a[:type]
18
- if a[:type] == :missing_used
19
- occ = a[:data][:source_locations]
20
- first = occ.first
21
- info = [green("#{first[:src_path]}:#{first[:line_num]}"),
22
- ("(#{occ.length - 1} more)" if occ.length > 1)].compact.join(' ')
23
- else
24
- info = a[:value].to_s.strip
25
- end
26
- [{value: cyan(locale), alignment: :center},
27
- cyan(key),
28
- wrap_string(info, 60)]
13
+ print_title missing_title(forest)
14
+ print_table headings: [cyan(bold(I18n.t('i18n_tasks.common.locale'))), cyan(bold I18n.t('i18n_tasks.common.key')), I18n.t('i18n_tasks.common.details')] do |t|
15
+ t.rows = sort_by_attr!(forest_to_attr(forest)).map do |a|
16
+ [{value: cyan(a[:locale]), alignment: :center}, cyan(a[:key]), wrap_string(key_info(a), 60)]
29
17
  end
30
18
  end
31
19
  else
32
- print_success 'No translations missing!'
20
+ print_success I18n.t('i18n_tasks.missing.none')
33
21
  end
34
22
  end
35
23
 
36
24
  def icon(type)
37
- glyph = missing_types[type][:glyph]
25
+ glyph = missing_type_info(type)[:glyph]
38
26
  {missing_used: red(glyph), missing_diff: yellow(glyph)}[type]
39
27
  end
40
28
 
41
- def used_keys(used_tree = task.used_tree(source_locations: true))
29
+ def used_keys(used_tree = task.used_tree(source_occurrences: true))
42
30
  print_title used_title(used_tree)
43
31
  keys_nodes = used_tree.keys.to_a
44
32
  if keys_nodes.present?
45
- keys_nodes.sort! { |a, b| a[0] <=> b[0] }
46
- keys_nodes.each do |key, node|
47
- usages = node.data[:source_locations]
48
- puts "#{bold "#{key}"} #{green(usages.size.to_s) if usages.size > 1}"
49
- usages.each do |u|
50
- line = u[:line].dup.tap { |line|
51
- line.strip!
52
- line.sub!(/(.*?)(#{key})(.*)$/) { dark($1) + underline($2) + dark($3) }
53
- }
54
- puts " #{green "#{u[:src_path]}:#{u[:line_num]}"} #{line}"
55
- end
33
+ keys_nodes.sort! { |a, b| a[0] <=> b[0] }.each do |key, node|
34
+ print_occurrences node, key
56
35
  end
57
36
  else
58
- print_error 'No key usages found'
37
+ print_error I18n.t('i18n_tasks.usages.none')
59
38
  end
60
39
  end
61
40
 
62
41
  def unused_keys(tree = task.unused_keys)
63
42
  keys = tree.root_key_values(true)
64
- print_title unused_title(keys)
65
43
  if keys.present?
44
+ print_title unused_title(keys)
66
45
  print_locale_key_value_table keys
67
46
  else
68
- print_success 'Every translation is used!'
47
+ print_success I18n.t('i18n_tasks.unused.none')
69
48
  end
70
49
  end
71
50
 
72
51
  def eq_base_keys(tree = task.eq_base_keys)
73
52
  keys = tree.root_key_values(true)
74
- print_title eq_base_title(keys)
75
53
  if keys.present?
54
+ print_title eq_base_title(keys)
76
55
  print_locale_key_value_table keys
77
56
  else
78
57
  print_info cyan('No translations are the same as base value')
@@ -83,11 +62,29 @@ module I18n
83
62
  print_locale_key_value_table tree.root_key_values(true)
84
63
  end
85
64
 
65
+ def forest_stats(forest, stats = task.forest_stats(forest))
66
+ text = if stats[:locale_count] == 1
67
+ I18n.t('i18n_tasks.data_stats.text_single_locale', stats)
68
+ else
69
+ I18n.t('i18n_tasks.data_stats.text', stats)
70
+ end
71
+ title = bold(I18n.t('i18n_tasks.data_stats.title', stats.slice(:locales)))
72
+ print_info "#{cyan title} #{cyan text}"
73
+ end
74
+
86
75
  private
87
76
 
77
+ def print_occurrences(node, full_key = node.full_key)
78
+ occurrences = node.data[:source_occurrences]
79
+ puts "#{bold "#{full_key}"} #{green(occurrences.size.to_s) if occurrences.size > 1}"
80
+ occurrences.each do |occurrence|
81
+ puts " #{key_occurrence full_key, occurrence}"
82
+ end
83
+ end
84
+
88
85
  def print_locale_key_value_table(locale_key_values)
89
86
  if locale_key_values.present?
90
- print_table headings: [bold(cyan('Locale')), bold(cyan('Key')), 'Value'] do |t|
87
+ print_table headings: [bold(cyan(I18n.t('i18n_tasks.common.locale'))), bold(cyan(I18n.t('i18n_tasks.common.key'))), I18n.t('i18n_tasks.common.value')] do |t|
91
88
  t.rows = locale_key_values.map { |(locale, k, v)| [{value: cyan(locale), alignment: :center}, cyan(k), v.to_s] }
92
89
  end
93
90
  else
@@ -100,7 +97,7 @@ module I18n
100
97
  end
101
98
 
102
99
  def print_success(message)
103
- log_stderr(bold green ['Good job!', 'Well done!'].sample + ' ' + message)
100
+ log_stderr bold(green "✓ #{I18n.t('i18n_tasks.cmd.encourage').sample} #{message}")
104
101
  end
105
102
 
106
103
  def print_error(message)
@@ -120,9 +117,36 @@ module I18n
120
117
  puts ::Terminal::Table.new(opts, &block)
121
118
  end
122
119
 
120
+ def key_occurrence(full_key, info)
121
+ location = green "#{info[:src_path]}:#{info[:line_num]}"
122
+ source = highlight_key(full_key, info[:line], info[:line_pos]..-1).strip
123
+ "#{location} #{source}"
124
+ end
125
+
126
+ def highlight_key(full_key, line, range = (0..-1))
127
+ result = line.dup
128
+ result[range] = result[range].sub!(full_key) { |m| underline m }
129
+ result
130
+ end
131
+
132
+ def key_info(leaf)
133
+ if leaf[:type] == :missing_used
134
+ first_occurrence leaf
135
+ else
136
+ leaf[:value].to_s.strip
137
+ end
138
+ end
139
+
140
+ def first_occurrence(leaf)
141
+ usages = leaf[:data][:source_occurrences]
142
+ first = usages.first
143
+ [green("#{first[:src_path]}:#{first[:line_num]}"),
144
+ ("(#{I18n.t 'i18n_tasks.common.n_more', count: usages.length - 1})" if usages.length > 1)].compact.join(' ')
145
+ end
146
+
123
147
  def wrap_string(s, max)
124
148
  chars = []
125
- dist = 0
149
+ dist = 0
126
150
  s.chars.each do |c|
127
151
  chars << c
128
152
  dist += 1
@@ -36,13 +36,13 @@ module I18n::Tasks::Scanners
36
36
  @key_filter_pattern = compile_key_pattern(value) if @key_filter
37
37
  end
38
38
 
39
- # @return [Array<{key,data:{source_locations:[]}}]
39
+ # @return [Array<{key,data:{source_occurrences:[]}}]
40
40
  def keys(opts = {})
41
41
  keys = traverse_files { |path|
42
42
  scan_file(path, opts)
43
43
  }.reduce(:+) || []
44
44
  keys.group_by(&:first).map { |key, key_loc|
45
- [key, data: {source_locations: key_loc.map { |(k, attr)| attr[:data] }}]
45
+ [key, data: {source_occurrences: key_loc.map { |(k, attr)| attr[:data] }}]
46
46
  }
47
47
  end
48
48
 
@@ -115,8 +115,9 @@ module I18n::Tasks::Scanners
115
115
  key
116
116
  end
117
117
 
118
- VALID_KEY_RE = /^[-\w.\#{}]+$/
119
- VALID_KEY_RE_STRICT = /^[-\w.]+$/
118
+ VALID_KEY_CHARS = /[-\w.?!;:]/
119
+ VALID_KEY_RE_STRICT = /^#{VALID_KEY_CHARS}+$/
120
+ VALID_KEY_RE = /^(#{VALID_KEY_CHARS}|[\#{}])+$/
120
121
 
121
122
  def valid_key?(key, strict = false)
122
123
  return false if @key_filter && @key_filter_pattern !~ key
@@ -0,0 +1,27 @@
1
+ module I18n::Tasks::SlopCommand
2
+ extend self
3
+
4
+ def slop_command(name, attr, &block)
5
+ proc {
6
+ command name.tr('_', '-') do
7
+ opts = attr[:opt]
8
+ args = attr[:args]
9
+ banner "Usage: i18n-tasks #{name} [options] #{args}" if args.present?
10
+ desc = attr[:desc]
11
+ description desc if desc
12
+ if opts
13
+ opts.each do |opt|
14
+ on *[:short, :long, :desc, :conf].map { |k| opt[k] }.compact
15
+ end
16
+ end
17
+ run { |opts, args| block.call(name, opts, args) }
18
+ end
19
+ }
20
+ end
21
+
22
+ def parse_slop_opts_args(opts, args)
23
+ opts = opts.to_hash(true).reject { |k, v| v.nil? }
24
+ opts.merge!(arguments: args) unless args.empty?
25
+ opts
26
+ end
27
+ end