i18n-tasks 0.3.6 → 0.3.7
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 -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: {
|