i18n-tools 0.0.4 → 0.0.5

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.
Files changed (64) hide show
  1. data/lib/core_ext/hash/iterate_nested.rb +35 -0
  2. data/lib/core_ext/hash/slice.rb +20 -0
  3. data/lib/core_ext/hash/sorted_yaml_style.rb +17 -0
  4. data/lib/core_ext/hash/symbolize_keys.rb +14 -0
  5. data/lib/core_ext/module/attribute_accessors.rb +48 -0
  6. data/lib/core_ext/object/deep_clone.rb +5 -0
  7. data/lib/core_ext/object/instance_variables.rb +9 -0
  8. data/lib/core_ext/object/meta_class.rb +5 -0
  9. data/lib/core_ext/object/tap.rb +6 -0
  10. data/lib/i18n/backend/simple_storage.rb +119 -0
  11. data/lib/i18n/commands/keys.rb +84 -0
  12. data/lib/i18n/exceptions/key_exists.rb +9 -0
  13. data/lib/i18n/index.rb +33 -0
  14. data/lib/i18n/index/base.rb +38 -0
  15. data/lib/i18n/index/file.rb +55 -0
  16. data/lib/i18n/index/format.rb +49 -0
  17. data/lib/i18n/index/key.rb +45 -0
  18. data/lib/i18n/index/occurence.rb +18 -0
  19. data/lib/i18n/index/simple.rb +69 -0
  20. data/lib/i18n/index/simple/data.rb +34 -0
  21. data/lib/i18n/index/simple/storage.rb +79 -0
  22. data/lib/i18n/ripper2ruby.rb +7 -0
  23. data/lib/i18n/ripper2ruby/translate_args_list.rb +89 -0
  24. data/lib/i18n/ripper2ruby/translate_call.rb +66 -0
  25. data/lib/i18n/translation_properties.rb +38 -0
  26. data/test/all.rb +1 -1
  27. data/test/core_ext/hash_iterate_nested.rb +31 -0
  28. data/test/fixtures/all.rb.src +106 -0
  29. data/test/fixtures/config.yml +3 -0
  30. data/test/fixtures/locale/de.yml +4 -0
  31. data/test/fixtures/locale/en.yml +4 -0
  32. data/test/fixtures/source_1.rb +2 -1
  33. data/test/fixtures/translate/double_key.rb +32 -0
  34. data/test/fixtures/translate/double_scope.rb +32 -0
  35. data/test/fixtures/translate/single_key.rb +10 -0
  36. data/test/fixtures/translate/single_scope.rb +32 -0
  37. data/test/i18n/backend/simple_storage_test.rb +81 -0
  38. data/test/i18n/backend/translation_properties_test.rb +33 -0
  39. data/test/i18n/index/all.rb +1 -0
  40. data/test/i18n/index/args_replace_test.rb +218 -0
  41. data/test/i18n/index/calls_replace_test.rb +67 -0
  42. data/test/i18n/index/commands_test.rb +75 -0
  43. data/test/i18n/index/key_test.rb +32 -0
  44. data/test/i18n/index/simple_test.rb +67 -0
  45. data/test/i18n/ripper2ruby/translate_call_test.rb +98 -0
  46. data/test/test_helper.rb +66 -9
  47. metadata +49 -32
  48. data/MIT-LICENSE +0 -20
  49. data/README.textile +0 -1
  50. data/bin/i18n-keys +0 -6
  51. data/lib/ansi.rb +0 -19
  52. data/lib/i18n/keys.rb +0 -51
  53. data/lib/i18n/keys/commands.rb +0 -53
  54. data/lib/i18n/keys/formatter.rb +0 -39
  55. data/lib/i18n/keys/index.rb +0 -209
  56. data/lib/i18n/keys/occurence.rb +0 -120
  57. data/lib/i18n/parser/erb_parser.rb +0 -54
  58. data/lib/i18n/parser/ruby_parser.rb +0 -93
  59. data/test/commands_test.rb +0 -1
  60. data/test/erb_parser_test.rb +0 -31
  61. data/test/index_test.rb +0 -135
  62. data/test/keys_test.rb +0 -75
  63. data/test/occurence_test.rb +0 -130
  64. data/test/ruby_parser_test.rb +0 -54
@@ -0,0 +1,55 @@
1
+ module I18n
2
+ module Index
3
+ class Files < Hash
4
+ def [](filename)
5
+ fetch(filename)
6
+ rescue
7
+ store(filename, File.new(filename))
8
+ end
9
+ end
10
+
11
+ class File
12
+ attr_accessor :filename
13
+
14
+ def initialize(filename)
15
+ self.filename = filename
16
+ end
17
+
18
+ def source
19
+ @source ||= begin
20
+ source = ::File.read(filename) # TODO srsly ... how am i supposed to open a file in ruby 1.9?
21
+ source = ::File.open(filename, 'r:iso-8859-1:utf-8') { |f| f.read } unless source.valid_encoding?
22
+ source
23
+ end
24
+ end
25
+
26
+ def ruby
27
+ @ruby ||= begin
28
+ parser.parse
29
+ rescue Ripper::RubyBuilder::ParseError => e
30
+ puts "\nWARNING Ruby 1.9 incompatible syntax in: " + e.message.gsub(Dir.pwd, '') + ". Can not index file."
31
+ Ruby::Program.new(source, filename)
32
+ end
33
+ end
34
+
35
+ def calls
36
+ @calls ||= ruby.select_translate_calls
37
+ end
38
+
39
+ def update(source)
40
+ @source = source
41
+ save
42
+ end
43
+
44
+ def save
45
+ ::File.open(filename, 'w+') { |f| f.write(source) } # TODO need to modify/write unfiltered source for ERB
46
+ end
47
+
48
+ protected
49
+
50
+ def parser
51
+ @parser ||= Index.parser.new(source, filename)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,49 @@
1
+ # wicked. but i can't see a better pattern right now. i want a module to include
2
+ # into the meta_class. and i don't want to set the io object globally (or on
3
+ # any class or module var) so i need to instantiate something, too.
4
+
5
+ module I18n
6
+ module Index
7
+ module Format
8
+ class Base
9
+ attr_accessor :io
10
+
11
+ def initialize(io = nil)
12
+ self.io = io || $stdout
13
+ end
14
+
15
+ def out(str)
16
+ io.print(str)
17
+ end
18
+
19
+ def setup(target)
20
+ $stdout.sync = true
21
+ (class << target; self; end).send(:include, self.class::Hooks)
22
+ target.format = self
23
+ end
24
+ end
25
+
26
+ class Stdout < Base
27
+ module Hooks
28
+ attr_accessor :format
29
+
30
+ def build(*args)
31
+ format.out "indexing files ... "
32
+ super
33
+ format.out "found #{occurences.size} occurences of #{keys.size} keys in #{filenames.size} files in total\n"
34
+ end
35
+
36
+ def save
37
+ format.out "saving index\n"
38
+ super
39
+ end
40
+
41
+ def parse(file)
42
+ format.out '.'
43
+ super
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+ module I18n
2
+ module Index
3
+ class Key
4
+ class << self
5
+ def patterns(*keys)
6
+ keys.inject({}) { |result, key| result[key] = pattern(key); result }
7
+ end
8
+
9
+ def pattern(key)
10
+ key = key.to_s.dup
11
+ match_start = key.gsub!(/^\*/, '') ? '' : '^'
12
+ match_end = key.gsub!(/\*$/, '') ? '' : '$'
13
+ pattern = Regexp.escape("#{key}")
14
+ /#{match_start}#{pattern}#{match_end}/
15
+ end
16
+ end
17
+
18
+ attr_accessor :key
19
+
20
+ def initialize(key)
21
+ self.key = key
22
+ end
23
+
24
+ def matches?(*keys)
25
+ patterns = Key.patterns(*keys)
26
+ patterns.empty? || patterns.any? do |key, pattern|
27
+ self.key == key.to_sym || self.key.to_s =~ pattern
28
+ end
29
+ end
30
+
31
+ def <=>(other)
32
+ key <=> other.key
33
+ end
34
+
35
+ def ==(other)
36
+ key == (other.respond_to?(:key) ? other.key : other)
37
+ end
38
+ alias eql? ==
39
+
40
+ def hash
41
+ key.hash
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,18 @@
1
+ module I18n
2
+ module Index
3
+ class Occurence
4
+ attr_reader :key, :filename, :position
5
+
6
+ def initialize(key, filename, position)
7
+ @key = key
8
+ @filename = filename
9
+ @position = position
10
+ end
11
+
12
+ def ==(other)
13
+ key == other.key && filename == other.filename && position == other.position
14
+ end
15
+ alias eql? ==
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,69 @@
1
+ require 'i18n/index/base'
2
+ require 'i18n/index/key'
3
+ require 'i18n/index/occurence'
4
+ require 'i18n/index/format'
5
+ require 'i18n/index/simple/storage'
6
+ require 'i18n/index/simple/data'
7
+ require 'erb/stripper'
8
+
9
+ module I18n
10
+ module Index
11
+ class Simple < Base
12
+ include Storage
13
+
14
+ def find_call(*keys)
15
+ return unless key = data.keys.detect { |key, data| key.matches?(*keys) }
16
+ occurence = data.occurences(key).first
17
+ files[occurence.filename].ruby.select_translate_calls(:position => occurence.position).first
18
+ end
19
+
20
+ def find_calls(*keys)
21
+ keys = data.keys.select { |key, data| key.matches?(*keys) }
22
+ occurences = keys.map { |key| data.occurences(key) }.flatten
23
+ occurences.map do |occurence|
24
+ files[occurence.filename].ruby.select_translate_calls(:position => occurence.position)
25
+ end.flatten
26
+ end
27
+
28
+ def replace_key(call, search, replacement)
29
+ data.remove(call)
30
+ call.replace_key(search.to_s.gsub(/[^\w\.]/, ''), replacement.to_sym)
31
+ files[call.filename].update(call.root.src)
32
+ data.add(call)
33
+ save if built?
34
+ end
35
+
36
+ def data
37
+ @data ||= Data.new
38
+ build unless built?
39
+ @data
40
+ end
41
+
42
+ def keys
43
+ data.keys
44
+ end
45
+
46
+ def occurences
47
+ data.values.map { |value| value[:occurences] }.flatten
48
+ end
49
+
50
+ protected
51
+
52
+ def reset!
53
+ @built = false
54
+ @data = nil
55
+ end
56
+
57
+ def built?
58
+ @built
59
+ end
60
+
61
+ def build
62
+ reset!
63
+ @built = true
64
+ calls = filenames.map { |filename| files[filename].calls }.flatten
65
+ calls.each { |call| data.add(call) }
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,34 @@
1
+ module I18n
2
+ module Index
3
+ class Simple < Base
4
+ class Data < Hash
5
+ def add(call)
6
+ key = Key.new(call.full_key(true))
7
+ occurences(key) << Occurence.new(key, call.filename, call.position)
8
+ end
9
+
10
+ def remove(call)
11
+ key = Key.new(call.full_key(true))
12
+ occurences = occurences(key)
13
+ occurences.delete(Occurence.new(key, call.filename, call.position))
14
+ self.delete(key) if occurences.empty?
15
+ end
16
+
17
+ def occurences(key)
18
+ self[key] ||= { :occurences => [] }
19
+ self[key][:occurences]
20
+ end
21
+
22
+ def [](key)
23
+ key = Key.new(key) unless key.is_a?(Key)
24
+ super
25
+ end
26
+
27
+ def []=(key, value)
28
+ key = Key.new(key) unless key.is_a?(Key)
29
+ super
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,79 @@
1
+ module I18n
2
+ module Index
3
+ class Simple < Base
4
+ module Storage
5
+ class << self
6
+ def included(target)
7
+ target.send(:extend, ClassMethods)
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ def load_or_create(options = {})
13
+ exists?(options) ? load(options) : create(options)
14
+ end
15
+
16
+ protected
17
+
18
+ def create(options = {})
19
+ index = Simple.new(options)
20
+ index.update
21
+ index
22
+ end
23
+
24
+ def load(options)
25
+ ::File.open(filename(options), 'r') { |f| ::Marshal.load(f) }
26
+ end
27
+
28
+ def exists?(options)
29
+ ::File.exists?(filename(options))
30
+ end
31
+
32
+ def filename(options)
33
+ store_dir(options) + "/index.marshal"
34
+ end
35
+
36
+ def store_dir(options)
37
+ root_dir = options[:root_dir] || Dir.pwd
38
+ ::File.expand_path(root_dir + '/.i18n')
39
+ end
40
+ end
41
+ end
42
+
43
+ def exists?
44
+ ::File.exists?(filename)
45
+ end
46
+
47
+ def update
48
+ reset!
49
+ build
50
+ save
51
+ end
52
+
53
+ def save
54
+ mkdir
55
+ ::File.open(filename, 'w+') { |f| ::Marshal.dump(self, f) }
56
+ end
57
+
58
+ def delete
59
+ FileUtils.rm(filename) if exists? rescue Errno::ENOENT
60
+ end
61
+
62
+ def filename
63
+ self.class.send(:filename, :root_dir => root_dir)
64
+ end
65
+
66
+ def store_dir
67
+ self.class.send(:store_dir, :root_dir => root_dir)
68
+ end
69
+
70
+ def mkdir
71
+ FileUtils.mkdir_p(store_dir) unless ::File.exists?(store_dir)
72
+ end
73
+
74
+ def marshalled_vars
75
+ super + [:built, :data]
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,7 @@
1
+ require 'core_ext/object/meta_class'
2
+
3
+ require 'ripper/ruby_builder'
4
+ require 'ruby/call'
5
+
6
+ require 'i18n/ripper2ruby/translate_call'
7
+ require 'i18n/ripper2ruby/translate_args_list'
@@ -0,0 +1,89 @@
1
+ module Ruby
2
+ class ArgsList
3
+ def to_translate_args_list
4
+ meta_class.send(:include, TranslateArgsList)
5
+ self
6
+ end
7
+ end
8
+
9
+ module TranslateArgsList
10
+ def full_key(joined = false)
11
+ full_key = normalize_keys(scope, key)
12
+ joined ? join_key(full_key) : full_key
13
+ end
14
+
15
+ def key
16
+ first.arg.value if first
17
+ end
18
+
19
+ def scope
20
+ last.arg.is_a?(Ruby::Hash) ? last.arg.value[:scope] : nil
21
+ end
22
+
23
+ def key_matches?(keys)
24
+ keys = normalize_keys(keys)
25
+ keys == full_key[0, keys.length]
26
+ end
27
+
28
+ def replace_key(search, replace)
29
+ original_length = length
30
+ self.key, self.scope = compute_replace_keys(search, replace)
31
+ root.replace_src(row, column, original_length, to_ruby)
32
+ end
33
+
34
+ protected
35
+
36
+ def key=(key)
37
+ self[0] = key
38
+ end
39
+
40
+ def scope=(scope)
41
+ if scope
42
+ set_option(:scope, scope)
43
+ elsif options
44
+ options.delete(:scope)
45
+ pop if options.arg.empty?
46
+ end
47
+ end
48
+
49
+ def compute_replace_keys(search, replace)
50
+ search = normalize_keys(search)
51
+ replace = normalize_keys(replace)
52
+ key = normalize_keys(self.key)
53
+ scope = normalize_keys(self.scope)
54
+ all = scope + key
55
+
56
+ all[key_index(search), search.length] = replace
57
+
58
+ if scope.empty?
59
+ key = all
60
+ else
61
+ key = all.slice!(-[key.length, all.size].min..-1) # i.e. we preserve the key length, this is debatable
62
+ scope = all.empty? ? nil : all
63
+ # scope = all.slice!(0, scope.length) # this would preserve the scope length
64
+ # key = all # comment this in, previous lines out and observe the tests
65
+ end
66
+
67
+ key = [scope.pop] if key.empty?
68
+ scope = scope.first if scope && scope.size == 1
69
+ scope = nil if scope && scope.empty?
70
+
71
+ [join_key(key), scope]
72
+ end
73
+
74
+ def key_index(search)
75
+ (all = full_key).each_index { |ix| return ix if all[ix, search.length] == search } and nil
76
+ end
77
+
78
+ def join_key(key)
79
+ key.map { |k| k.to_s }.join('.').to_sym
80
+ end
81
+
82
+ def normalize_keys(*args)
83
+ args.flatten.
84
+ map { |k| k.to_s.split(/\./) }.flatten.
85
+ reject { |k| k.empty? }.
86
+ map { |k| k.to_sym }
87
+ end
88
+ end
89
+ end