i18n-tasks 0.3.6 → 0.3.7
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 -0
- data/README.md +13 -8
- data/bin/i18n-tasks +13 -9
- data/i18n-tasks.gemspec +2 -7
- data/lib/i18n/tasks.rb +1 -12
- data/lib/i18n/tasks/base_task.rb +6 -9
- data/lib/i18n/tasks/commands.rb +14 -15
- data/lib/i18n/tasks/commands_base.rb +2 -2
- data/lib/i18n/tasks/configuration.rb +20 -7
- data/lib/i18n/tasks/data.rb +78 -0
- data/lib/i18n/tasks/data/file_formats.rb +41 -0
- data/lib/i18n/tasks/data/file_system.rb +2 -3
- data/lib/i18n/tasks/data/file_system_base.rb +90 -0
- data/lib/i18n/tasks/data/locale_tree.rb +85 -0
- data/lib/i18n/tasks/data/router.rb +47 -0
- data/lib/i18n/tasks/key.rb +3 -0
- data/lib/i18n/tasks/key_group.rb +1 -0
- data/lib/i18n/tasks/logging.rb +4 -0
- data/lib/i18n/tasks/missing_keys.rb +18 -14
- data/lib/i18n/tasks/reports/terminal.rb +1 -1
- data/lib/i18n/tasks/scanners/base_scanner.rb +38 -36
- data/lib/i18n/tasks/scanners/pattern_scanner.rb +2 -2
- data/lib/i18n/tasks/unused_keys.rb +11 -8
- data/lib/i18n/tasks/used_keys.rb +15 -6
- data/lib/i18n/tasks/version.rb +1 -1
- data/spec/file_system_data_spec.rb +2 -2
- data/spec/google_translate_spec.rb +1 -1
- data/spec/pattern_scanner_spec.rb +2 -2
- data/spec/relative_keys_spec.rb +3 -3
- data/spec/used_keys_spec.rb +4 -6
- metadata +39 -42
- data/lib/i18n/tasks/data/storage/file_storage.rb +0 -127
- data/lib/i18n/tasks/data_traversal.rb +0 -51
- data/lib/i18n/tasks/translation_data.rb +0 -60
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'i18n/tasks/data/locale_tree'
|
2
|
+
require 'i18n/tasks/data/router'
|
3
|
+
require 'i18n/tasks/data/file_formats'
|
4
|
+
require 'i18n/tasks/key_pattern_matching'
|
5
|
+
|
6
|
+
module I18n::Tasks
|
7
|
+
module Data
|
8
|
+
class FileSystemBase
|
9
|
+
include ::I18n::Tasks::Data::Router
|
10
|
+
include ::I18n::Tasks::Data::FileFormats
|
11
|
+
|
12
|
+
attr_reader :config
|
13
|
+
|
14
|
+
DEFAULTS = {
|
15
|
+
read: ['config/locales/%{locale}.yml'],
|
16
|
+
write: ['config/locales/%{locale}.yml']
|
17
|
+
}.with_indifferent_access
|
18
|
+
|
19
|
+
def initialize(config = {})
|
20
|
+
self.config = config
|
21
|
+
end
|
22
|
+
|
23
|
+
def t(key, locale)
|
24
|
+
get(locale).t(key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def config=(config)
|
28
|
+
@config = DEFAULTS.deep_merge((config || {}).with_indifferent_access)
|
29
|
+
@config[:write] = compile_routes @config[:write]
|
30
|
+
reload
|
31
|
+
end
|
32
|
+
|
33
|
+
# get locale tree
|
34
|
+
def get(locale)
|
35
|
+
locale = locale.to_s
|
36
|
+
@locale_data[locale] ||= begin
|
37
|
+
hash = config[:read].map do |path|
|
38
|
+
Dir.glob path % {locale: locale}
|
39
|
+
end.reduce(:+).map do |locale_file|
|
40
|
+
load_file locale_file
|
41
|
+
end.inject({}.with_indifferent_access) do |hash, locale_data|
|
42
|
+
hash.deep_merge! locale_data || {}
|
43
|
+
hash
|
44
|
+
end[locale.to_s] || {}
|
45
|
+
LocaleTree.new locale, hash.with_indifferent_access
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
alias [] get
|
50
|
+
|
51
|
+
# set locale tree
|
52
|
+
def set(locale, values)
|
53
|
+
locale = locale.to_s
|
54
|
+
route_values config[:write], values, locale do |path, tree|
|
55
|
+
write_tree path, tree
|
56
|
+
end
|
57
|
+
@locale_data[locale] = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
alias []= set
|
61
|
+
|
62
|
+
# @return self
|
63
|
+
def reload
|
64
|
+
@locale_data = {}
|
65
|
+
@available_locales = nil
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
# Get available locales from the list of file names to read
|
70
|
+
def available_locales
|
71
|
+
@available_locales ||= begin
|
72
|
+
locales = Set.new
|
73
|
+
config[:read].map do |pattern|
|
74
|
+
[pattern, Dir.glob(pattern % {locale: '*'})] if pattern.include?('%{locale}')
|
75
|
+
end.compact.each do |pattern, paths|
|
76
|
+
p = pattern.gsub('\\', '\\\\').gsub('/', '\/').gsub('.', '\.')
|
77
|
+
p = p.gsub(/(\*+)/) { $1 == '**' ? '.*' : '[^/]*?' }.gsub('%{locale}', '([^/.]+)')
|
78
|
+
re = /\A#{p}\z/
|
79
|
+
paths.each do |path|
|
80
|
+
if re =~ path
|
81
|
+
locales << $1
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
locales
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module I18n::Tasks
|
2
|
+
module Data
|
3
|
+
# A tree of keys. Roots are locales, leaves are values.
|
4
|
+
class LocaleTree
|
5
|
+
attr_reader :locale, :data
|
6
|
+
|
7
|
+
delegate :tree_data, to: :class
|
8
|
+
|
9
|
+
def initialize(locale, data = {})
|
10
|
+
@locale = locale.to_s
|
11
|
+
data = tree_data(data)
|
12
|
+
@data = data.with_indifferent_access
|
13
|
+
end
|
14
|
+
|
15
|
+
def merge(other)
|
16
|
+
self.class.new locale, data.deep_merge(tree_data(other))
|
17
|
+
end
|
18
|
+
|
19
|
+
alias + merge
|
20
|
+
|
21
|
+
def to_hash
|
22
|
+
{locale => data}
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [String,nil] translation of the key found in the passed hash or nil
|
26
|
+
def t(key)
|
27
|
+
key.to_s.split('.').inject(data) { |r, seg| r[seg] if r }
|
28
|
+
end
|
29
|
+
|
30
|
+
# traverse => map if yield(k, v)
|
31
|
+
# @return [Array] mapped list
|
32
|
+
def traverse_map_if
|
33
|
+
list = []
|
34
|
+
traverse do |k, v|
|
35
|
+
mapped = yield(k, v)
|
36
|
+
list << mapped if mapped
|
37
|
+
end
|
38
|
+
list
|
39
|
+
end
|
40
|
+
|
41
|
+
# traverse data, yielding with full key and value
|
42
|
+
# @yield [full_key, value]
|
43
|
+
# @return self
|
44
|
+
def traverse
|
45
|
+
q = [['', data]]
|
46
|
+
until q.empty?
|
47
|
+
path, value = q.pop
|
48
|
+
if value.is_a?(Hash)
|
49
|
+
value.each { |k, v| q << ["#{path}.#{k}", v] }
|
50
|
+
else
|
51
|
+
yield path[1..-1], value
|
52
|
+
end
|
53
|
+
end
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
class << self
|
58
|
+
def tree_data(any)
|
59
|
+
if any.is_a?(Hash)
|
60
|
+
any
|
61
|
+
elsif any.is_a?(Array)
|
62
|
+
list_to_tree_data any
|
63
|
+
elsif any.is_a?(LocaleTree)
|
64
|
+
any.data
|
65
|
+
else
|
66
|
+
raise "Can't get tree data from #{any.inspect}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def list_to_tree_data(list)
|
71
|
+
key_values = list.sort
|
72
|
+
tree_data = {}
|
73
|
+
key_values.each do |key, value|
|
74
|
+
key_segments = key.to_s.split('.')
|
75
|
+
node = key_segments[0..-2].inject(tree_data) do |subtree, seg|
|
76
|
+
subtree[seg] ||= {}
|
77
|
+
end
|
78
|
+
node[key_segments.last] = value
|
79
|
+
end
|
80
|
+
tree_data
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'i18n/tasks/data/locale_tree'
|
2
|
+
|
3
|
+
module I18n::Tasks
|
4
|
+
module Data
|
5
|
+
module Router
|
6
|
+
include ::I18n::Tasks::KeyPatternMatching
|
7
|
+
|
8
|
+
def compile_routes(routes)
|
9
|
+
routes.map { |x| x.is_a?(String) ? ['*', x] : x }.map { |x|
|
10
|
+
[compile_key_pattern(x[0]), x[1]]
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
# Route keys to destinations
|
15
|
+
# @param routes [Array] of routes
|
16
|
+
# @example
|
17
|
+
# # keys matched top to bottom
|
18
|
+
# [['devise.*', 'config/locales/devise.%{locale}.yml'],
|
19
|
+
# # default catch-all (same as ['*', 'config/locales/%{locale}.yml'])
|
20
|
+
# 'config/locales/%{locale}.yml']
|
21
|
+
# @param tree [I18n::Tasks::Data::Tree] locale tree, where roots are locales.
|
22
|
+
# @param route_args [Hash] route arguments, %-interpolated
|
23
|
+
# @return [Hash] mapping of destination => [ [key, value], ... ]
|
24
|
+
def route_values(routes, values, locale, &block)
|
25
|
+
out = {}
|
26
|
+
values = LocaleTree.new(locale, values) unless values.is_a?(LocaleTree)
|
27
|
+
values.traverse do |key, value|
|
28
|
+
route = routes.detect { |route| route[0] =~ key }
|
29
|
+
key_match = $~
|
30
|
+
path = route[1] % {locale: locale}
|
31
|
+
path.gsub!(/[\\]\d+/) { |m| key_match[m[1..-1].to_i] }
|
32
|
+
(out[path] ||= []) << [key, value]
|
33
|
+
end
|
34
|
+
out.each do |dest, key_values|
|
35
|
+
out[dest] = LocaleTree.new(locale, key_values)
|
36
|
+
end
|
37
|
+
if block
|
38
|
+
out.each(&block)
|
39
|
+
else
|
40
|
+
out
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
data/lib/i18n/tasks/key.rb
CHANGED
@@ -4,6 +4,7 @@ require 'i18n/tasks/key/usages'
|
|
4
4
|
|
5
5
|
module I18n
|
6
6
|
module Tasks
|
7
|
+
# Container for i18n key and its attributes
|
7
8
|
class Key
|
8
9
|
include ::I18n::Tasks::Key::KeyGroup
|
9
10
|
include ::I18n::Tasks::Key::MatchPattern
|
@@ -11,6 +12,8 @@ module I18n
|
|
11
12
|
|
12
13
|
attr_accessor :own_attr
|
13
14
|
|
15
|
+
# @param [Array<Key, Value>|Hash|String] key_or_attr
|
16
|
+
# @param [Hash] attr optional
|
14
17
|
def initialize(key_or_attr, own_attr = {})
|
15
18
|
@own_attr = if key_or_attr.is_a?(Array)
|
16
19
|
{key: key_or_attr[0], value: key_or_attr[1]}.merge(own_attr)
|
data/lib/i18n/tasks/key_group.rb
CHANGED
data/lib/i18n/tasks/logging.rb
CHANGED
@@ -28,11 +28,11 @@ module I18n::Tasks
|
|
28
28
|
# @return [KeyGroup] missing keys, i.e. key that are in the code but are not in the base locale data
|
29
29
|
def keys_missing_from_base
|
30
30
|
@keys_missing_from_base ||= begin
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
31
|
+
keys = used_keys.keys.reject { |k|
|
32
|
+
key = k.key
|
33
|
+
k.expr? || key_value?(key, base_locale) || ignore_key?(key, :missing)
|
34
|
+
}.map(&:clone_orphan)
|
35
|
+
KeyGroup.new keys, type: :missing_from_base, locale: base_locale
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
@@ -40,19 +40,23 @@ module I18n::Tasks
|
|
40
40
|
def keys_missing_from_locale(locale)
|
41
41
|
return keys_missing_from_base if locale == base_locale
|
42
42
|
@keys_missing_from_locale ||= {}
|
43
|
-
@keys_missing_from_locale[locale] ||=
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
43
|
+
@keys_missing_from_locale[locale] ||= begin
|
44
|
+
keys = data[base_locale].traverse_map_if { |key, base_value|
|
45
|
+
key if !ignore_key?(key, :missing) && !key_value?(key, locale) && !key_value?(depluralize_key(key), locale)
|
46
|
+
}
|
47
|
+
KeyGroup.new keys, type: :missing_from_locale, locale: locale
|
48
|
+
end
|
48
49
|
end
|
49
50
|
|
50
51
|
# @return [KeyGroup] keys missing value (but present in base)
|
51
52
|
def keys_eq_base(locale)
|
52
|
-
@keys_eq_base
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
@keys_eq_base ||= {}
|
54
|
+
@keys_eq_base[locale] ||= begin
|
55
|
+
keys = data[base_locale].traverse_map_if { |key, base_value|
|
56
|
+
key if base_value == t(key, locale) && !ignore_key?(key, :eq_base, locale)
|
57
|
+
}
|
58
|
+
KeyGroup.new keys, type: :eq_base, locale: locale
|
59
|
+
end
|
56
60
|
end
|
57
61
|
end
|
58
62
|
end
|
@@ -1,9 +1,12 @@
|
|
1
|
+
require 'i18n/tasks/key_pattern_matching'
|
1
2
|
require 'i18n/tasks/relative_keys'
|
2
3
|
module I18n::Tasks::Scanners
|
3
4
|
class BaseScanner
|
4
5
|
include ::I18n::Tasks::RelativeKeys
|
5
6
|
include ::I18n::Tasks::KeyPatternMatching
|
6
|
-
|
7
|
+
include ::I18n::Tasks::Logging
|
8
|
+
|
9
|
+
attr_reader :config, :key_filter, :record_src_loc
|
7
10
|
|
8
11
|
def initialize(config = {})
|
9
12
|
@config = config.dup.with_indifferent_access.tap do |conf|
|
@@ -16,7 +19,7 @@ module I18n::Tasks::Scanners
|
|
16
19
|
conf[:exclude] = %w(*.jpg *.png *.gif *.svg *.ico *.eot *.ttf *.woff)
|
17
20
|
end
|
18
21
|
end
|
19
|
-
@
|
22
|
+
@record_src_loc = false
|
20
23
|
end
|
21
24
|
|
22
25
|
def key_filter=(value)
|
@@ -26,31 +29,27 @@ module I18n::Tasks::Scanners
|
|
26
29
|
|
27
30
|
# @return [Array] found key usages, absolutized and unique
|
28
31
|
def keys
|
29
|
-
|
30
|
-
keys_with_usages
|
31
|
-
else
|
32
|
-
@keys ||= (traverse_files { |path| scan_file(path, read_file(path)).map(&:key) }.reduce(:+) || []).uniq
|
33
|
-
end
|
32
|
+
@keys ||= (traverse_files { |path| scan_file(path) }.reduce(:+) || []).uniq(&:key)
|
34
33
|
end
|
35
34
|
|
36
|
-
def
|
37
|
-
|
35
|
+
def keys_with_src_locations
|
36
|
+
with_src_locations do
|
38
37
|
keys = traverse_files { |path|
|
39
|
-
::I18n::Tasks::KeyGroup.new(scan_file(path
|
40
|
-
}.
|
41
|
-
keys.group_by(&:key).map { |key,
|
42
|
-
{key: key, usages:
|
38
|
+
::I18n::Tasks::KeyGroup.new(scan_file(path), src_path: path)
|
39
|
+
}.reduce(:+) || []
|
40
|
+
keys.group_by(&:key).map { |key, key_loc|
|
41
|
+
{key: key, usages: key_loc.map { |k| k[:src].merge(path: k[:src_path]) }}
|
43
42
|
}
|
44
43
|
end
|
45
44
|
end
|
46
|
-
|
45
|
+
|
47
46
|
def read_file(path)
|
48
47
|
result = nil
|
49
48
|
File.open(path, 'rb') { |f| result = f.read }
|
50
49
|
result
|
51
50
|
end
|
52
51
|
|
53
|
-
# @return [
|
52
|
+
# @return [Array<Key>] keys found in file
|
54
53
|
def scan_file(path, *args)
|
55
54
|
raise 'Unimplemented'
|
56
55
|
end
|
@@ -61,44 +60,47 @@ module I18n::Tasks::Scanners
|
|
61
60
|
result = []
|
62
61
|
paths = config[:paths].select { |p| File.exists?(p) }
|
63
62
|
if paths.empty?
|
64
|
-
|
63
|
+
log_warn "search.paths #{config[:paths].inspect} do not exist"
|
65
64
|
return result
|
66
65
|
end
|
67
66
|
Find.find(*paths) do |path|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
67
|
+
is_dir = File.directory?(path)
|
68
|
+
hidden = File.basename(path).start_with?('.')
|
69
|
+
not_incl = config[:include] && !path_fnmatch_any?(path, config[:include])
|
70
|
+
excl = path_fnmatch_any?(path, config[:exclude])
|
71
|
+
if is_dir || hidden || not_incl || excl
|
72
|
+
Find.prune if is_dir && (hidden || excl)
|
73
|
+
else
|
74
|
+
result << yield(path)
|
75
|
+
end
|
72
76
|
end
|
73
77
|
result
|
74
78
|
end
|
75
79
|
|
76
|
-
def path_fnmatch_any?(path, globs)
|
77
|
-
globs.any? { |glob| File.fnmatch(glob, path) }
|
78
|
-
end
|
79
|
-
|
80
|
-
protected :path_fnmatch_any?
|
81
|
-
|
82
80
|
def with_key_filter(key_filter = nil)
|
83
81
|
filter_was = @key_filter
|
84
82
|
self.key_filter = key_filter
|
85
|
-
|
83
|
+
yield
|
84
|
+
ensure
|
86
85
|
self.key_filter = filter_was
|
87
|
-
result
|
88
86
|
end
|
89
87
|
|
90
|
-
def
|
91
|
-
was
|
92
|
-
@
|
93
|
-
|
94
|
-
|
95
|
-
|
88
|
+
def with_src_locations
|
89
|
+
was = @record_src_loc
|
90
|
+
@record_src_loc = true
|
91
|
+
yield
|
92
|
+
ensure
|
93
|
+
@record_src_loc = was
|
96
94
|
end
|
97
95
|
|
98
96
|
protected
|
99
97
|
|
100
|
-
def
|
101
|
-
|
98
|
+
def path_fnmatch_any?(path, globs)
|
99
|
+
globs.any? { |glob| File.fnmatch(glob, path) }
|
100
|
+
end
|
101
|
+
|
102
|
+
def src_location(text, src_pos)
|
103
|
+
return nil unless @record_src_loc
|
102
104
|
line_begin = text.rindex(/^/, src_pos - 1)
|
103
105
|
line_end = text.index(/.(?=\n|$)/, src_pos)
|
104
106
|
{src: {
|