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.
@@ -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
+
@@ -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)
@@ -1,6 +1,7 @@
1
1
  require 'set'
2
2
  module I18n
3
3
  module Tasks
4
+ # Container for keys with shared attributes
4
5
  class KeyGroup
5
6
  attr_reader :keys, :attr, :key_names
6
7
 
@@ -9,6 +9,10 @@ module I18n::Tasks::Logging
9
9
  end
10
10
  end
11
11
 
12
+ def log_warn(message)
13
+ log_stderr Term::ANSIColor.yellow "i18n-tasks: [WARN] #{message}"
14
+ end
15
+
12
16
  def log_stderr(*args)
13
17
  STDERR.puts *args
14
18
  end
@@ -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
- KeyGroup.new(
32
- used_keys.keys.reject { |k|
33
- key = k.key
34
- k.expr? || key_value?(key, base_locale) || ignore_key?(key, :missing)
35
- }.map(&:clone_orphan), type: :missing_from_base, locale: base_locale)
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] ||= KeyGroup.new(
44
- traverse_map_if(data[base_locale]) { |key, base_value|
45
- key if !ignore_key?(key, :missing) && !key_value?(key, locale) && !key_value?(depluralize_key(key), locale)
46
- }, type: :missing_from_locale, locale: locale)
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 ||= KeyGroup.new(
53
- traverse_map_if(data[base_locale]) { |key, base_value|
54
- key if base_value == t(key, locale) && !ignore_key?(key, :eq_base, locale)
55
- }, type: :eq_base, locale: locale)
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
@@ -36,7 +36,7 @@ module I18n
36
36
  end
37
37
  end
38
38
 
39
- def used_keys(keys = task.used_keys(true))
39
+ def used_keys(keys = task.used_keys(src_locations: true))
40
40
  print_title used_title(keys)
41
41
  keys.sort_by_attr!(key: :asc)
42
42
  if keys.present?
@@ -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
- attr_reader :config, :key_filter, :record_usages
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
- @record_usages = false
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
- if @record_usages
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 keys_with_usages
37
- with_usages do
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, read_file(path)), src_path: path)
40
- }.map(&:keys).reduce(:+) || []
41
- keys.group_by(&:key).map { |key, key_usages|
42
- {key: key, usages: key_usages.map { |usage| usage[:src].merge(path: usage[:src_path]) }}
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 [String] keys used in file (unimplemented)
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
- STDERR.puts Term::ANSIColor.yellow("i18n-tasks: [WARN] search.paths (#{config[:paths]}) do not exist")
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
- next if File.directory?(path) ||
69
- config[:include] && !path_fnmatch_any?(path, config[:include]) ||
70
- path_fnmatch_any?(path, config[:exclude])
71
- result << yield(path)
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
- result = yield
83
+ yield
84
+ ensure
86
85
  self.key_filter = filter_was
87
- result
88
86
  end
89
87
 
90
- def with_usages
91
- was = @record_usages
92
- @record_usages = true
93
- result = yield
94
- @record_usages = was
95
- result
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 usage_context(text, src_pos)
101
- return nil unless @record_usages
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: {