i18n-tasks 0.2.19 → 0.2.20
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +5 -1
- data/LICENSE.txt +1 -1
- data/README.md +51 -39
- data/doc/img/i18n-usages.png +0 -0
- data/i18n-tasks.gemspec +1 -1
- data/lib/i18n/tasks.rb +7 -2
- data/lib/i18n/tasks/base_task.rb +2 -4
- data/lib/i18n/tasks/configuration.rb +2 -4
- data/lib/i18n/tasks/data/adapter/json_adapter.rb +22 -0
- data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +21 -0
- data/lib/i18n/tasks/data/file_system.rb +13 -0
- data/lib/i18n/tasks/data/storage/file_storage.rb +105 -0
- data/lib/i18n/tasks/data/yaml.rb +6 -65
- data/lib/i18n/tasks/data_traversal.rb +2 -2
- data/lib/i18n/tasks/fill_tasks.rb +2 -11
- data/lib/i18n/tasks/ignore_keys.rb +3 -1
- data/lib/i18n/tasks/key.rb +60 -0
- data/lib/i18n/tasks/key_group.rb +65 -0
- data/lib/i18n/tasks/missing_keys.rb +51 -18
- data/lib/i18n/tasks/reports/base.rb +8 -4
- data/lib/i18n/tasks/reports/spreadsheet.rb +8 -8
- data/lib/i18n/tasks/reports/terminal.rb +39 -16
- data/lib/i18n/tasks/scanners/base_scanner.rb +36 -2
- data/lib/i18n/tasks/scanners/pattern_scanner.rb +9 -7
- data/lib/i18n/tasks/translation_data.rb +4 -1
- data/lib/i18n/tasks/unused_keys.rb +9 -7
- data/lib/i18n/tasks/{source_keys.rb → used_keys.rb} +5 -6
- data/lib/i18n/tasks/version.rb +1 -1
- data/lib/tasks/i18n-tasks.rake +24 -20
- data/spec/file_system_data_spec.rb +68 -0
- data/spec/fixtures/config/i18n-tasks.yml +2 -2
- data/spec/i18n_tasks_spec.rb +5 -5
- data/spec/key_group_spec.rb +48 -0
- data/spec/used_keys_spec.rb +22 -0
- metadata +17 -7
- data/lib/i18n/tasks/untranslated_keys.rb +0 -50
- data/spec/yaml_adapter_spec.rb +0 -33
@@ -3,11 +3,11 @@ module I18n::Tasks::DataTraversal
|
|
3
3
|
# @return [String,nil]
|
4
4
|
def t(hash = data[base_locale], key)
|
5
5
|
if hash.is_a?(String)
|
6
|
-
#
|
6
|
+
# hash is a locale
|
7
7
|
raise ArgumentError.new("invalid locale: #{hash}") if hash =~ /[^A-z\d-]/
|
8
8
|
hash = data[hash]
|
9
9
|
end
|
10
|
-
key.split('.').inject(hash) { |r, seg| r[seg] if r }
|
10
|
+
key.to_s.split('.').inject(hash) { |r, seg| r[seg] if r }
|
11
11
|
end
|
12
12
|
|
13
13
|
# traverse => map if yield(k, v)
|
@@ -22,7 +22,7 @@ module I18n::Tasks::FillTasks
|
|
22
22
|
locales = non_base_locales(locales)
|
23
23
|
normalize_store! locales
|
24
24
|
locales.each do |locale|
|
25
|
-
blank_keys =
|
25
|
+
blank_keys = keys_missing_from_locale(locale).key_names.select { |k|
|
26
26
|
tr = t(k)
|
27
27
|
tr.present? && tr.is_a?(String)
|
28
28
|
}
|
@@ -48,17 +48,8 @@ module I18n::Tasks::FillTasks
|
|
48
48
|
# fill blank values with values from passed block
|
49
49
|
# @param [String] locale
|
50
50
|
def set_blank_values!(locale = base_locale, &fill_with)
|
51
|
-
blank_keys =
|
51
|
+
blank_keys = keys_missing_from_locale(locale).key_names
|
52
52
|
list = blank_keys.zip fill_with.call(blank_keys)
|
53
53
|
data[locale] = data[locale].deep_merge(list_to_tree(list))
|
54
54
|
end
|
55
|
-
|
56
|
-
|
57
|
-
def find_blank_keys(locale, include_missing = (locale == base_locale))
|
58
|
-
blank_keys = traverse_map_if(data[base_locale]) { |key, value|
|
59
|
-
key if !key_value?(key, locale) && !ignore_key?(key, :missing)
|
60
|
-
}
|
61
|
-
blank_keys += keys_not_in_base if include_missing
|
62
|
-
blank_keys.uniq
|
63
|
-
end
|
64
55
|
end
|
@@ -10,7 +10,9 @@ module I18n::Tasks::IgnoreKeys
|
|
10
10
|
# @param locale [String] only when type is :eq_base
|
11
11
|
# @return [Regexp] a regexp that matches all the keys ignored for the type (and locale)
|
12
12
|
def ignore_pattern(type, locale = nil)
|
13
|
-
|
13
|
+
@ignore_patterns ||= HashWithIndifferentAccess.new
|
14
|
+
@ignore_patterns[type] ||= {}
|
15
|
+
@ignore_patterns[type][locale] ||= begin
|
14
16
|
global, type_ignore = config[:ignore].presence || [], config["ignore_#{type}"].presence || []
|
15
17
|
if type_ignore.is_a?(Array)
|
16
18
|
patterns = global + type_ignore
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module I18n
|
2
|
+
module Tasks
|
3
|
+
class Key
|
4
|
+
attr_accessor :own_attr, :key_group
|
5
|
+
|
6
|
+
def initialize(key_or_attr, own_attr = {})
|
7
|
+
@own_attr = if key_or_attr.is_a?(Array)
|
8
|
+
{key: key_or_attr[0], value: key_or_attr[1]}.merge(own_attr)
|
9
|
+
elsif key_or_attr.is_a?(Hash)
|
10
|
+
key_or_attr.merge(own_attr)
|
11
|
+
else
|
12
|
+
own_attr.merge(key: key_or_attr)
|
13
|
+
end
|
14
|
+
|
15
|
+
@own_attr[:key] = @own_attr[:key].to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](prop)
|
19
|
+
@own_attr[prop] || key_group.attr[prop]
|
20
|
+
end
|
21
|
+
|
22
|
+
def attr
|
23
|
+
key_group.attr.merge @own_attr
|
24
|
+
end
|
25
|
+
|
26
|
+
def ==(other)
|
27
|
+
self.attr == other.attr
|
28
|
+
end
|
29
|
+
|
30
|
+
def inspect
|
31
|
+
"#<#{self.class.name}#{attr.inspect}>"
|
32
|
+
end
|
33
|
+
|
34
|
+
def clone_orphan
|
35
|
+
clone.tap { |k| k.key_group = nil }
|
36
|
+
end
|
37
|
+
|
38
|
+
def key
|
39
|
+
@own_attr[:key]
|
40
|
+
end
|
41
|
+
alias to_s key
|
42
|
+
|
43
|
+
def value
|
44
|
+
self[:value]
|
45
|
+
end
|
46
|
+
|
47
|
+
def locale
|
48
|
+
self[:locale]
|
49
|
+
end
|
50
|
+
|
51
|
+
def type
|
52
|
+
self[:type]
|
53
|
+
end
|
54
|
+
|
55
|
+
def src_pos
|
56
|
+
self[:src_pos]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'set'
|
2
|
+
module I18n
|
3
|
+
module Tasks
|
4
|
+
class KeyGroup
|
5
|
+
attr_reader :keys, :attr, :key_names
|
6
|
+
|
7
|
+
delegate :size, :length, :each, :[], to: :keys
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
def initialize(keys, attr = {})
|
11
|
+
@keys = if keys && !keys[0].is_a?(::I18n::Tasks::Key)
|
12
|
+
keys.map { |key| I18n::Tasks::Key.new(key) }
|
13
|
+
else
|
14
|
+
keys
|
15
|
+
end
|
16
|
+
@keys.each { |key| key.key_group ||= self } unless attr.delete(:orphan)
|
17
|
+
|
18
|
+
@keys_by_name = @keys.inject({}) { |h, k| h[k.key.to_s] = k; h }
|
19
|
+
@key_names = @keys.map(&:key)
|
20
|
+
@attr = attr
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_by_name(key)
|
24
|
+
@keys_by_name[key.to_s]
|
25
|
+
end
|
26
|
+
|
27
|
+
def key_names_set
|
28
|
+
@key_names_set ||= Set.new(@key_names)
|
29
|
+
end
|
30
|
+
|
31
|
+
def include?(key)
|
32
|
+
key_names_set.include?(key.to_s)
|
33
|
+
end
|
34
|
+
|
35
|
+
# order, e.g: {locale: :asc, type: :desc, key: :asc}
|
36
|
+
def sort!(&block)
|
37
|
+
@keys.sort!(&block)
|
38
|
+
@key_names = @keys.map(&:to_s)
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def sort_by_attr!(order)
|
43
|
+
order_keys = order.keys
|
44
|
+
sort! { |a, b|
|
45
|
+
by = order_keys.detect { |by| a[by] != b[by] }
|
46
|
+
order[by] == :desc ? b[by] <=> a[by] : a[by] <=> b[by]
|
47
|
+
}
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_a
|
52
|
+
@array ||= keys.map(&:attr)
|
53
|
+
end
|
54
|
+
|
55
|
+
alias as_json to_a
|
56
|
+
|
57
|
+
def merge(other)
|
58
|
+
KeyGroup.new(keys + other.keys,
|
59
|
+
type: [attr[:type], other.attr[:type]].flatten.compact)
|
60
|
+
end
|
61
|
+
|
62
|
+
alias + merge
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -1,23 +1,56 @@
|
|
1
|
-
module I18n::Tasks
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
1
|
+
module I18n::Tasks
|
2
|
+
module MissingKeys
|
3
|
+
# @param [:missing_from_base, :missing_from_locale, :eq_base] type (default nil)
|
4
|
+
# @return [KeyGroup]
|
5
|
+
def missing_keys(opts = {})
|
6
|
+
type = opts[:type]
|
7
|
+
locales = non_base_locales(opts[:locales])
|
8
|
+
if type
|
9
|
+
if type == :missing_from_base
|
10
|
+
keys_missing_from_base
|
11
|
+
else
|
12
|
+
locales.map { |locale| send("keys_#{type}", locale) }.reduce(:+)
|
13
|
+
end
|
14
|
+
else
|
15
|
+
missing_keys(type: :missing_from_base) +
|
16
|
+
missing_keys(type: :eq_base, locales: locales) +
|
17
|
+
missing_keys(type: :missing_from_locale, locales: locales)
|
18
|
+
end
|
19
|
+
end
|
8
20
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
key if !ignore_key?(key, :missing) && !key_value?(key, locale) && !key_value?(depluralize_key(key), locale)
|
21
|
+
def untranslated_keys(locales = nil)
|
22
|
+
I18n::Tasks.warn_deprecated("#untranslated_keys. Please use #missing_keys instead")
|
23
|
+
missing_keys(locales: locales)
|
13
24
|
end
|
14
|
-
end
|
15
25
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
26
|
+
# @return [KeyGroup] missing keys, i.e. key that are in the code but are not in the base locale data
|
27
|
+
def keys_missing_from_base
|
28
|
+
@keys_missing_from_base ||= begin
|
29
|
+
KeyGroup.new(
|
30
|
+
used_keys.keys.reject { |k|
|
31
|
+
key = k.key
|
32
|
+
key_value?(key, base_locale) || pattern_key?(key) || ignore_key?(key, :missing)
|
33
|
+
}.map(&:clone_orphan), type: :missing_from_base, locale: base_locale)
|
34
|
+
end
|
20
35
|
end
|
21
|
-
end
|
22
36
|
|
23
|
-
|
37
|
+
# @return [KeyGroup] keys missing (nil or blank?) in locale but present in base
|
38
|
+
def keys_missing_from_locale(locale)
|
39
|
+
return keys_missing_from_base if locale == base_locale
|
40
|
+
@keys_missing_from_locale ||= {}
|
41
|
+
@keys_missing_from_locale[locale] ||= KeyGroup.new(
|
42
|
+
traverse_map_if(data[base_locale]) { |key, base_value|
|
43
|
+
key if !ignore_key?(key, :missing) && !key_value?(key, locale) && !key_value?(depluralize_key(key), locale)
|
44
|
+
}, type: :missing_from_locale, locale: locale)
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [KeyGroup] keys missing value (but present in base)
|
49
|
+
def keys_eq_base(locale)
|
50
|
+
@keys_eq_base ||= KeyGroup.new(
|
51
|
+
traverse_map_if(data[base_locale]) { |key, base_value|
|
52
|
+
key if base_value == t(locale, key) && !ignore_key?(key, :eq_base, locale)
|
53
|
+
}, type: :eq_base, locale: locale)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -9,9 +9,9 @@ module I18n::Tasks::Reports
|
|
9
9
|
attr_reader :task
|
10
10
|
|
11
11
|
MISSING_TYPES = {
|
12
|
-
|
13
|
-
|
14
|
-
eq_base: {glyph: '=', summary: 'value
|
12
|
+
missing_from_base: {glyph: '✗', summary: 'missing from base locale'},
|
13
|
+
missing_from_locale: {glyph: '∅', summary: 'missing from locale but present in base locale'},
|
14
|
+
eq_base: {glyph: '=', summary: 'value equals base value'}
|
15
15
|
}
|
16
16
|
|
17
17
|
def missing_types
|
@@ -25,5 +25,9 @@ module I18n::Tasks::Reports
|
|
25
25
|
def unused_title(recs)
|
26
26
|
"Unused keys (#{recs.length})"
|
27
27
|
end
|
28
|
+
|
29
|
+
def used_title(keys)
|
30
|
+
"#{keys.length} keys used #{keys.map { |k| k[:usages].size }.reduce(:+)} times"
|
31
|
+
end
|
28
32
|
end
|
29
|
-
end
|
33
|
+
end
|
@@ -17,17 +17,17 @@ module I18n::Tasks::Reports
|
|
17
17
|
private
|
18
18
|
|
19
19
|
def add_missing_sheet(wb)
|
20
|
-
|
20
|
+
keys = task.missing_keys
|
21
21
|
wb.styles do |s|
|
22
22
|
type_cell = s.add_style :alignment => {:horizontal => :center}
|
23
23
|
locale_cell = s.add_style :alignment => {:horizontal => :center}
|
24
24
|
regular_style = s.add_style
|
25
|
-
wb.add_worksheet(name: missing_title(
|
25
|
+
wb.add_worksheet(name: missing_title(keys)) { |sheet|
|
26
26
|
sheet.page_setup.fit_to :width => 1
|
27
27
|
sheet.add_row ['Type', 'Locale', 'Key', 'Base Value']
|
28
28
|
style_header sheet
|
29
|
-
|
30
|
-
sheet.add_row [missing_types[
|
29
|
+
keys.each do |key|
|
30
|
+
sheet.add_row [missing_types[key.type][:summary], key.locale, key.key, task.t(key)],
|
31
31
|
styles: [type_cell, locale_cell, regular_style, regular_style]
|
32
32
|
end
|
33
33
|
}
|
@@ -35,12 +35,12 @@ module I18n::Tasks::Reports
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def add_unused_sheet(wb)
|
38
|
-
|
39
|
-
wb.add_worksheet name: unused_title(
|
38
|
+
keys = task.unused_keys
|
39
|
+
wb.add_worksheet name: unused_title(keys) do |sheet|
|
40
40
|
sheet.add_row ['Key', 'Base Value']
|
41
41
|
style_header sheet
|
42
|
-
|
43
|
-
sheet.add_row
|
42
|
+
keys.each do |key|
|
43
|
+
sheet.add_row [key.key, task.t(key)]
|
44
44
|
end
|
45
45
|
end
|
46
46
|
end
|
@@ -7,26 +7,27 @@ module I18n
|
|
7
7
|
class Terminal < Base
|
8
8
|
include Term::ANSIColor
|
9
9
|
|
10
|
-
def
|
11
|
-
|
12
|
-
|
10
|
+
def missing_keys(keys = task.missing_keys)
|
11
|
+
keys.sort_by_attr!(locale: :asc, type: :asc, key: :asc)
|
12
|
+
print_title missing_title(keys)
|
13
|
+
if keys.present?
|
13
14
|
|
14
15
|
$stderr.puts "#{bold 'Types:'} #{missing_types.values.map { |t| "#{t[:glyph]} #{t[:summary]}" } * ', '}"
|
15
16
|
|
16
17
|
print_table headings: [magenta(bold('Locale')), bold('Type'), magenta('i18n Key'), bold(cyan "Base value (#{base_locale})")] do |t|
|
17
|
-
t.rows =
|
18
|
-
glyph = missing_types[
|
19
|
-
glyph = {
|
20
|
-
if
|
21
|
-
locale = magenta bold
|
18
|
+
t.rows = keys.map { |key|
|
19
|
+
glyph = missing_types[key.type][:glyph]
|
20
|
+
glyph = {missing_from_base: red(glyph), missing_from_locale: yellow(glyph), eq_base: bold(blue(glyph))}[key.type]
|
21
|
+
if key[:type] == :missing_from_base
|
22
|
+
locale = magenta bold key.locale
|
22
23
|
base_value = ''
|
23
24
|
else
|
24
|
-
locale = magenta
|
25
|
-
base_value =
|
25
|
+
locale = magenta key.locale
|
26
|
+
base_value = task.t(key.key).to_s.strip
|
26
27
|
end
|
27
28
|
[{value: locale, alignment: :center},
|
28
29
|
{value: glyph, alignment: :center},
|
29
|
-
magenta(
|
30
|
+
magenta(key[:key]),
|
30
31
|
base_value]
|
31
32
|
}
|
32
33
|
end
|
@@ -35,11 +36,30 @@ module I18n
|
|
35
36
|
end
|
36
37
|
end
|
37
38
|
|
38
|
-
def
|
39
|
-
print_title
|
40
|
-
|
39
|
+
def used_keys(keys = task.used_keys)
|
40
|
+
print_title used_title(keys)
|
41
|
+
keys.sort_by_attr!(key: :asc)
|
42
|
+
if keys.present?
|
43
|
+
keys.each do |k|
|
44
|
+
puts "#{bold k.key} (#{k[:usages].size}):"
|
45
|
+
k[:usages].each do |u|
|
46
|
+
line = faint u[:line].dup.tap { |line|
|
47
|
+
line.sub!(k[:key], underline(k[:key]))
|
48
|
+
line.strip!
|
49
|
+
}
|
50
|
+
puts " #{u[:path]}:#{u[:line_num]}: #{faint line}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
else
|
54
|
+
print_error 'No key usages found. Please check configuration in config/i18n-tasks.yml'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def unused_keys(keys = task.unused_keys)
|
59
|
+
print_title unused_title(keys)
|
60
|
+
if keys.present?
|
41
61
|
print_table headings: [bold(magenta('i18n Key')), cyan("Base value (#{base_locale})")] do |t|
|
42
|
-
t.rows =
|
62
|
+
t.rows = keys.map { |k| [magenta(k.key), cyan(k.value)] }
|
43
63
|
end
|
44
64
|
else
|
45
65
|
print_success 'Good job! Every translation is used!'
|
@@ -56,7 +76,10 @@ module I18n
|
|
56
76
|
$stderr.puts(bold green message)
|
57
77
|
end
|
58
78
|
|
59
|
-
|
79
|
+
def print_error(message)
|
80
|
+
$stderr.puts(bold red message)
|
81
|
+
end
|
82
|
+
|
60
83
|
def indent(txt, n = 2)
|
61
84
|
spaces = ' ' * n
|
62
85
|
txt.gsub /^/, spaces
|
@@ -14,11 +14,36 @@ module I18n::Tasks::Scanners
|
|
14
14
|
|
15
15
|
# @return [Array] found key usages, absolutized and unique
|
16
16
|
def keys
|
17
|
-
@keys ||=
|
17
|
+
@keys ||= begin
|
18
|
+
@keys_by_name = {}
|
19
|
+
keys = []
|
20
|
+
usages = traverse_files { |path|
|
21
|
+
::I18n::Tasks::KeyGroup.new(scan_file(path), src_path: path) }.map(&:keys).flatten
|
22
|
+
usages.group_by(&:key).each do |key, instances|
|
23
|
+
key = {
|
24
|
+
key: key,
|
25
|
+
usages: instances.map { |inst| inst[:src].merge(path: inst[:src_path]) }
|
26
|
+
}
|
27
|
+
@keys_by_name[key.to_s] = key
|
28
|
+
keys << key
|
29
|
+
end
|
30
|
+
keys
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def key_usages(key)
|
35
|
+
keys
|
36
|
+
@keys_by_name[key.to_s][:usages]
|
37
|
+
end
|
38
|
+
|
39
|
+
def read_file(path)
|
40
|
+
result = nil
|
41
|
+
File.open(path, 'rb') { |f| result = f.read }
|
42
|
+
result
|
18
43
|
end
|
19
44
|
|
20
45
|
# @return [String] keys used in file (unimplemented)
|
21
|
-
def scan_file(path)
|
46
|
+
def scan_file(path, *args)
|
22
47
|
raise 'Unimplemented'
|
23
48
|
end
|
24
49
|
|
@@ -37,6 +62,15 @@ module I18n::Tasks::Scanners
|
|
37
62
|
|
38
63
|
protected
|
39
64
|
|
65
|
+
def usage_context(text, src_pos)
|
66
|
+
line_begin = text.rindex(/^/, src_pos - 1)
|
67
|
+
line_end = text.index(/.(?=\n|$)/, src_pos)
|
68
|
+
{pos: src_pos,
|
69
|
+
line_num: text[0..src_pos].count("\n") + 1,
|
70
|
+
line_pos: src_pos - line_begin + 1,
|
71
|
+
line: text[line_begin..line_end]}
|
72
|
+
end
|
73
|
+
|
40
74
|
def extract_key_from_match(match, path)
|
41
75
|
key = strip_literal(match[0])
|
42
76
|
key = absolutize_key(key, path) if path && key.start_with?('.')
|