svenfuchs-i18n-tools 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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