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.
@@ -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: {