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,70 @@
1
+ module I18n::Tasks
2
+ module Command
3
+ module Commands
4
+ module Usages
5
+ include Command::Collection
6
+
7
+ cmd_opt :strict, {
8
+ short: :s,
9
+ long: :strict,
10
+ desc: I18n.t('i18n_tasks.cmd.args.desc.strict')
11
+ }
12
+
13
+ cmd :find,
14
+ args: '[pattern]',
15
+ desc: I18n.t('i18n_tasks.cmd.desc.find'),
16
+ opt: cmd_opts(:out_format, :pattern)
17
+
18
+ def find(opt = {})
19
+ opt_output_format! opt
20
+ opt[:filter] ||= opt.delete(:pattern) || opt[:arguments].try(:first)
21
+ print_forest i18n.used_tree(key_filter: opt[:filter].presence, source_occurrences: true), opt, :used_keys
22
+ end
23
+
24
+ cmd :unused,
25
+ args: '[locale ...]',
26
+ desc: I18n.t('i18n_tasks.cmd.desc.unused'),
27
+ opt: cmd_opts(:locales, :out_format, :strict)
28
+
29
+ def unused(opt = {})
30
+ opt_locales! opt
31
+ opt_output_format! opt
32
+ print_forest i18n.unused_keys(opt), opt, :unused_keys
33
+ end
34
+
35
+ cmd :remove_unused,
36
+ args: '[locale ...]',
37
+ desc: I18n.t('i18n_tasks.cmd.desc.remove_unused'),
38
+ opt: cmd_opts(:locales, :out_format, :strict, :confirm)
39
+
40
+ def remove_unused(opt = {})
41
+ opt_locales! opt
42
+ opt_output_format! opt
43
+ unused_keys = i18n.unused_keys(opt)
44
+ if unused_keys.present?
45
+ terminal_report.unused_keys(unused_keys)
46
+ confirm_remove_unused!(unused_keys, opt)
47
+ removed = i18n.data.remove_by_key!(unused_keys)
48
+ log_stderr I18n.t('i18n_tasks.remove_unused.removed', count: unused_keys.leaves.count)
49
+ print_forest removed, opt
50
+ else
51
+ log_stderr bold green I18n.t('i18n_tasks.remove_unused.noop')
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def confirm_remove_unused!(unused_keys, opt)
58
+ return if ENV['CONFIRM'] || opt[:confirm]
59
+ locales = bold(opt[:locales] * ', ')
60
+ msg = [
61
+ red(I18n.t('i18n_tasks.remove_unused.confirm', count: unused_keys.leaves.count, locales: locales)),
62
+ yellow(I18n.t('i18n_tasks.common.continue_q')),
63
+ yellow('(yes/no)')
64
+ ] * ' '
65
+ exit 1 unless agree msg
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,27 @@
1
+ module I18n::Tasks
2
+ module Command
3
+ module Commands
4
+ module XLSX
5
+ include Command::Collection
6
+
7
+ cmd :xlsx_report,
8
+ args: '[locale...]',
9
+ desc: 'save missing and unused translations to an Excel file',
10
+ opt: [cmd_opt(:locales),
11
+ {short: :p, long: :path=, desc: 'Destination path', conf: {default: 'tmp/i18n-report.xlsx'}}]
12
+
13
+ def xlsx_report(opt = {})
14
+ begin
15
+ require 'axlsx'
16
+ rescue LoadError
17
+ message = %Q(For spreadsheet report please add axlsx gem to Gemfile:\ngem 'axlsx', '~> 2.0')
18
+ log_stderr Term::ANSIColor.red Term::ANSIColor.bold message
19
+ exit 1
20
+ end
21
+ opt_locales! opt
22
+ spreadsheet_report.save_report opt[:path], opt.except(:path)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ require 'i18n/tasks/command/dsl/cmd'
2
+ require 'i18n/tasks/command/dsl/cmd_opt'
3
+ require 'i18n/tasks/command/dsl/enum_opt'
4
+
5
+ module I18n::Tasks
6
+ module Command
7
+ module DSL
8
+ include DSL::Cmd
9
+ include DSL::CmdOpt
10
+ include DSL::EnumOpt
11
+
12
+ def self.extended(base)
13
+ base.instance_variable_set :@dsl, HashWithIndifferentAccess.new { |h, k|
14
+ h[k] = HashWithIndifferentAccess.new
15
+ }
16
+ end
17
+
18
+ def included(base)
19
+ base.instance_variable_get(:@dsl).deep_merge!(@dsl)
20
+ end
21
+
22
+ def dsl(key)
23
+ @dsl[key]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module I18n::Tasks
2
+ module Command
3
+ module DSL
4
+ module Cmd
5
+ def cmd(name, args = nil)
6
+ if args
7
+ dsl(:cmds)[name] = args
8
+ else
9
+ dsl(:cmds)[name]
10
+ end
11
+ end
12
+
13
+ def cmds
14
+ dsl(:cmds)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module I18n::Tasks
2
+ module Command
3
+ module DSL
4
+ module CmdOpt
5
+ def cmd_opts(*args)
6
+ dsl(:cmd_opts).values_at(*args)
7
+ end
8
+
9
+ def cmd_opt(arg, opts = nil)
10
+ if opts
11
+ dsl(:cmd_opts)[arg] = opts
12
+ else
13
+ dsl(:cmd_opts)[arg]
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ module I18n::Tasks
2
+ module Command
3
+ module DSL
4
+ module EnumOpt
5
+ def enum_opt(name, list = nil)
6
+ if list
7
+ dsl(:enum_valid)[name] = list
8
+ else
9
+ dsl(:enum_valid)[name]
10
+ end
11
+ end
12
+
13
+ DEFAULT_ENUM_OPT_DESC = proc { |valid, default|
14
+ I18n.t('i18n_tasks.cmd.enum_opt.desc.default', valid_text: valid, default_text: default)
15
+ }
16
+
17
+ def enum_opt_attr(short, long, valid, &desc)
18
+ desc ||= DEFAULT_ENUM_OPT_DESC
19
+ {short: short, long: long.to_sym,
20
+ desc: desc.call(valid * ', ', I18n.t('i18n_tasks.cmd.args.default_text', value: valid.first)),
21
+ conf: {default: valid.first, argument: true, optional: false}}
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,48 @@
1
+ require 'i18n/tasks/command/options/enum_opt'
2
+ require 'i18n/tasks/command/options/list_opt'
3
+
4
+ module I18n::Tasks
5
+ module Command
6
+ module Options
7
+ module Common
8
+ extend Command::DSL
9
+ include Options::EnumOpt
10
+ include Options::ListOpt
11
+
12
+ VALID_LOCALE_RE = /\A\w[\w\-_\.]*\z/i
13
+
14
+ cmd_opt :nostdin, {
15
+ short: :S,
16
+ long: :nostdin,
17
+ desc: I18n.t('i18n_tasks.cmd.args.desc.nostdin'),
18
+ conf: {default: false}
19
+ }
20
+
21
+ cmd_opt :confirm, {
22
+ short: :y,
23
+ long: :confirm,
24
+ desc: I18n.t('i18n_tasks.cmd.args.desc.confirm'),
25
+ conf: {default: false}
26
+ }
27
+
28
+ cmd_opt :pattern, {
29
+ short: :p,
30
+ long: :pattern=,
31
+ desc: I18n.t('i18n_tasks.cmd.args.desc.key_pattern'),
32
+ conf: {argument: true, optional: false}
33
+ }
34
+
35
+ cmd_opt :value, {
36
+ short: :v,
37
+ long: :value=,
38
+ desc: I18n.t('i18n_tasks.cmd.args.desc.value'),
39
+ conf: {argument: true, optional: false}
40
+ }
41
+
42
+ def opt_or_arg!(key, opt)
43
+ opt[key] ||= opt[:arguments].try(:shift)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,44 @@
1
+ module I18n::Tasks
2
+ module Command
3
+ module Options
4
+ module EnumOpt
5
+ delegate :enum_opt, to: :class
6
+
7
+ DEFAULT_ENUM_OPT_ERROR = proc { |bad, good|
8
+ I18n.t('i18n_tasks.cmd.enum_opt.invalid_one', invalid: bad, valid: good * ', ')
9
+ }
10
+
11
+ def parse_enum_opt(value, valid, &error_msg)
12
+ valid = enum_opt(valid) if Symbol === valid
13
+ return valid.first unless value.present?
14
+ if enum_opt_valid?(valid, value)
15
+ value
16
+ else
17
+ error_msg ||= DEFAULT_ENUM_OPT_ERROR
18
+ raise CommandError.new error_msg.call(value, valid)
19
+ end
20
+ end
21
+
22
+ def enum_opt_valid?(valid, value)
23
+ valid = enum_opt(valid) if Symbol === valid
24
+ valid.include?(value)
25
+ end
26
+
27
+ DEFAULT_ENUM_LIST_ERROR = proc { |bad, good|
28
+ I18n.t('i18n_tasks.cmd.enum_opt.invalid_list', invalid: bad * ', ', valid: good * ', ')
29
+ }
30
+
31
+ def parse_enum_list_opt(values, valid, &error_msg)
32
+ values = explode_list_opt(values)
33
+ invalid = values - valid.map(&:to_s)
34
+ if invalid.empty?
35
+ values
36
+ else
37
+ error_msg ||= DEFAULT_ENUM_LIST_ERROR
38
+ raise CommandError.new error_msg.call(invalid, valid)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,11 @@
1
+ module I18n::Tasks
2
+ module Command
3
+ module Options
4
+ module ListOpt
5
+ def explode_list_opt(list_opt, delim = /\s*[,]\s*/)
6
+ Array(list_opt).compact.map { |v| v.strip.split(delim).compact.presence }.flatten.map(&:presence).compact
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,47 @@
1
+ module I18n::Tasks
2
+ module Command
3
+ module Options
4
+ module Locales
5
+ extend Command::DSL
6
+
7
+ cmd_opt :locales, {
8
+ short: :l,
9
+ long: :locales=,
10
+ desc: I18n.t('i18n_tasks.cmd.args.desc.locales_filter'),
11
+ conf: {as: Array, delimiter: /\s*[+:,]\s*/, default: 'all', argument: true, optional: false}
12
+ }
13
+ cmd_opt :locale, {
14
+ short: :l,
15
+ long: :locale=,
16
+ desc: I18n.t('i18n_tasks.cmd.args.desc.locale'),
17
+ conf: {default: 'base', argument: true, optional: false}
18
+ }
19
+
20
+ def opt_locales!(opt)
21
+ argv = Array(opt[:arguments]) + Array(opt[:locales])
22
+ locales = if argv == ['all'] || argv == 'all' || argv.blank?
23
+ i18n.locales
24
+ else
25
+ explode_list_opt(argv).map { |v| v == 'base' ? base_locale : v }
26
+ end
27
+ locales.each { |locale| validate_locale!(locale) }
28
+ log_verbose "locales for the command are #{locales.inspect}"
29
+ opt[:locales] = locales
30
+ end
31
+
32
+ def opt_locale!(opt, key = :locale)
33
+ val = opt[key]
34
+ opt[key] = base_locale if val.blank? || val == 'base'
35
+ opt[key]
36
+ end
37
+
38
+
39
+ VALID_LOCALE_RE = /\A\w[\w\-_\.]*\z/i
40
+
41
+ def validate_locale!(locale)
42
+ raise CommandError.new(I18n.t('i18n_tasks.cmd.errors.invalid_locale', invalid: locale)) if VALID_LOCALE_RE !~ locale
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,101 @@
1
+ module I18n::Tasks
2
+ module Command
3
+ module Options
4
+ module Trees
5
+ extend Command::DSL
6
+
7
+ enum_opt :data_format, %w(yaml json keys)
8
+ cmd_opt :data_format, enum_opt_attr(:f, :format=, enum_opt(:data_format)) { |valid_text, default_text|
9
+ I18n.t('i18n_tasks.cmd.args.desc.data_format', valid_text: valid_text, default_text: default_text)
10
+ }
11
+
12
+ enum_opt :out_format, ['terminal-table', *enum_opt(:data_format), 'inspect']
13
+ cmd_opt :out_format, enum_opt_attr(:f, :format=, enum_opt(:out_format)) { |valid_text, default_text|
14
+ I18n.t('i18n_tasks.cmd.args.desc.out_format', valid_text: valid_text, default_text: default_text)
15
+ }
16
+
17
+ cmd_opt :keys, {
18
+ short: :k,
19
+ long: :keys=,
20
+ desc: I18n.t('i18n_tasks.cmd.args.desc.keys'),
21
+ conf: {as: Array, delimiter: /[+:,]/, argument: true, optional: false}
22
+ }
23
+
24
+ def print_forest(forest, opt, version = :show_tree)
25
+ format = opt[:format].to_s
26
+
27
+ case format
28
+ when 'terminal-table'
29
+ terminal_report.send(version, forest)
30
+ when 'inspect'
31
+ puts forest.inspect
32
+ when 'keys'
33
+ puts forest.key_names(root: true)
34
+ when *enum_opt(:data_format)
35
+ puts i18n.data.adapter_dump forest, format
36
+ end
37
+ end
38
+
39
+ INVALID_FORMAT_MSG = proc do |value, valid|
40
+ I18n.t('i18n_tasks.cmd.errors.invalid_format', invalid: value, valid: valid * ', ')
41
+ end
42
+
43
+ def opt_output_format!(opt = {}, key = :format)
44
+ opt[key] = parse_enum_opt opt[key], :out_format, &INVALID_FORMAT_MSG
45
+ end
46
+
47
+ def opt_data_format!(opt = {}, key = :format)
48
+ opt[key] = parse_enum_opt opt[key], :data_format, &INVALID_FORMAT_MSG
49
+ end
50
+
51
+ def opt_args_keys!(opt = {})
52
+ opt[:keys] = explode_list_opt(opt[:keys]) + Array(opt[:arguments])
53
+ end
54
+
55
+ def opt_forest_arg_or_stdin!(opt)
56
+ src = opt[:arguments].try(:shift) || $stdin.read
57
+ parse_forest(src, opt)
58
+ end
59
+
60
+ def opt_forests_stdin_args!(opt, num = false)
61
+ args = opt[:arguments] || []
62
+ if opt[:nostdin]
63
+ sources = []
64
+ else
65
+ sources = [$stdin.read]
66
+ num -= 1 if num
67
+ end
68
+ if num
69
+ num.times { sources << args.shift }
70
+ else
71
+ sources += args
72
+ args.clear
73
+ end
74
+ sources.map { |src| parse_forest(src, opt) }
75
+ end
76
+
77
+ def opt_forests_merged_stdin_args!(opt)
78
+ opt_forests_stdin_args!(opt).inject(i18n.empty_forest) { |result, forest|
79
+ result.merge! forest
80
+ }
81
+ end
82
+
83
+ def parse_forest(src, opt = {})
84
+ if !src
85
+ raise CommandError.new(I18n.t('i18n_tasks.cmd.errors.pass_forest'))
86
+ end
87
+ format = opt_data_format!(opt)
88
+ if format == 'keys'
89
+ Data::Tree::Siblings.from_key_names parse_keys(src)
90
+ else
91
+ Data::Tree::Siblings.from_nested_hash i18n.data.adapter_parse(src, format)
92
+ end
93
+ end
94
+
95
+ def parse_keys(src)
96
+ explode_list_opt(src, /\s*[,\s\n]\s*/)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end