i18n-tasks 0.2.19 → 0.2.20
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.
- 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?('.')
|