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,35 @@
1
+ class Hash
2
+ def each_nested(data = nil, keys = [], &block)
3
+ (data || self).each do |key, value|
4
+ case value
5
+ when Hash
6
+ each_nested(value, keys + [key], &block)
7
+ else
8
+ yield(keys + [key], value)
9
+ end
10
+ end
11
+ end
12
+
13
+ def select_nested(data = self, keys = [], &block)
14
+ case data
15
+ when Hash
16
+ data.inject({}) do |result, (key, value)|
17
+ value = select_nested(value, keys + [key], &block)
18
+ result[key] = value unless value.nil? || value.empty?
19
+ result
20
+ end
21
+ else
22
+ data if yield(keys, data)
23
+ end
24
+ end
25
+
26
+ def delete_nested_if(data = self, keys = [], &block)
27
+ data.each do |key, value|
28
+ if yield(keys + [key], value)
29
+ data.delete(key)
30
+ else
31
+ delete_nested_if(value, keys + [key], &block)
32
+ end
33
+ end if data.is_a?(Hash)
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ # from activesupport
2
+
3
+ class Hash
4
+ # Returns a new hash with only the given keys.
5
+ def slice(*keys)
6
+ hash = self.class.new
7
+ keys.each { |k| hash[k] = self[k] if has_key?(k) }
8
+ hash
9
+ end
10
+
11
+ # Replaces the hash with only the given keys.
12
+ # Returns a hash contained the removed key/value pairs
13
+ # {:a => 1, :b => 2, :c => 3, :d => 4}.slice!(:a, :b) # => {:c => 3, :d =>4}
14
+ def slice!(*keys)
15
+ omit = slice(*self.keys - keys)
16
+ hash = slice(*keys)
17
+ replace(hash)
18
+ omit
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ class Hash
2
+ attr_accessor :to_yaml_style
3
+
4
+ def set_yaml_style(style)
5
+ self.to_yaml_style = style
6
+ each { |key, value| value.set_yaml_style(style) if value.is_a?(Hash) }
7
+ end
8
+
9
+ def to_yaml(opts = {})
10
+ YAML::quick_emit(object_id, opts) do |out|
11
+ out.map(taguri, to_yaml_style) do |map|
12
+ elements = to_yaml_style == :sorted ? sort : self
13
+ elements.each { |k, v| map.add(k, v) }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ class Hash
2
+ # Return a new hash with all keys converted to symbols.
3
+ def symbolize_keys
4
+ inject({}) do |options, (key, value)|
5
+ options[(key.to_sym rescue key) || key] = value
6
+ options
7
+ end
8
+ end
9
+
10
+ # Destructively convert all keys to symbols.
11
+ def symbolize_keys!
12
+ self.replace(self.symbolize_keys)
13
+ end
14
+ end
@@ -0,0 +1,48 @@
1
+ # from activesupport
2
+
3
+ class Module
4
+ def mattr_reader(*syms)
5
+ syms.each do |sym|
6
+ next if sym.is_a?(Hash)
7
+ class_eval(<<-EOS, __FILE__, __LINE__)
8
+ unless defined? @@#{sym} # unless defined? @@pagination_options
9
+ @@#{sym} = nil # @@pagination_options = nil
10
+ end # end
11
+ #
12
+ def self.#{sym} # def self.pagination_options
13
+ @@#{sym} # @@pagination_options
14
+ end # end
15
+ #
16
+ def #{sym} # def pagination_options
17
+ @@#{sym} # @@pagination_options
18
+ end # end
19
+ EOS
20
+ end
21
+ end
22
+
23
+ def mattr_writer(*syms)
24
+ options = syms.last.is_a?(Hash) ? syms.pop : {}
25
+ syms.each do |sym|
26
+ class_eval(<<-EOS, __FILE__, __LINE__)
27
+ unless defined? @@#{sym} # unless defined? @@pagination_options
28
+ @@#{sym} = nil # @@pagination_options = nil
29
+ end # end
30
+ #
31
+ def self.#{sym}=(obj) # def self.pagination_options=(obj)
32
+ @@#{sym} = obj # @@pagination_options = obj
33
+ end # end
34
+ #
35
+ #{" #
36
+ def #{sym}=(obj) # def pagination_options=(obj)
37
+ @@#{sym} = obj # @@pagination_options = obj
38
+ end # end
39
+ " unless options[:instance_writer] == false } # # instance writer above is generated unless options[:instance_writer] == false
40
+ EOS
41
+ end
42
+ end
43
+
44
+ def mattr_accessor(*syms)
45
+ mattr_reader(*syms)
46
+ mattr_writer(*syms)
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ class Object
2
+ def deep_clone
3
+ Marshal::load(Marshal::dump(self))
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ class Object
2
+ def instance_variables_get(*keys)
3
+ keys.inject({}) { |result, key| result[key] = instance_variable_get(:"@#{key}"); result }
4
+ end
5
+
6
+ def instance_variables_set(vars)
7
+ vars.each { |key, value| instance_variable_set(:"@#{key}", value) }
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class Object
2
+ def meta_class
3
+ (class << self; self; end)
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class Object
2
+ def tap
3
+ yield self
4
+ self
5
+ end
6
+ end
@@ -0,0 +1,119 @@
1
+ require 'i18n'
2
+ require 'i18n/exceptions/key_exists'
3
+ require 'i18n/translation_properties'
4
+ require 'core_ext/object/deep_clone'
5
+ require 'core_ext/hash/iterate_nested'
6
+ require 'core_ext/hash/sorted_yaml_style'
7
+
8
+ module I18n
9
+ module Backend
10
+ class SimpleStorage < Simple
11
+ @@sort_keys = true
12
+
13
+ class << self
14
+ def sort_keys
15
+ @@sort_keys
16
+ end
17
+
18
+ def sort_keys=(sort_keys)
19
+ @@sort_keys = sort_keys
20
+ end
21
+ end
22
+
23
+ def interpolate(locale, original, values = {})
24
+ return original unless original.is_a?(String)
25
+ super.tap { |string| set_translation_properties(string, original.properties) }
26
+ end
27
+
28
+ def store_translations(locale, data)
29
+ data.each_nested { |key, value| raise KeyExists.new(locale, key) if lookup(locale, key) }
30
+ super
31
+ end
32
+
33
+ def copy_translations(from, to)
34
+ init_translations unless initialized?
35
+ I18n.available_locales.each do |locale|
36
+ store_translations(locale, to => I18n.t(from, :raise => true))
37
+ end
38
+ end
39
+
40
+ def remove_translation(key)
41
+ init_translations unless initialized?
42
+ key = I18n.send(:normalize_translation_keys, nil, key, nil)
43
+ keys = available_locales.map { |locale| [locale] + key }
44
+ translations.delete_nested_if { |k, v| keys.include?(k) }
45
+ end
46
+
47
+ def save_translations(filenames = I18n.load_path.flatten)
48
+ Array(filenames).each do |filename|
49
+ save_file(filename, by_filename(filename))
50
+ end
51
+ end
52
+
53
+ protected
54
+
55
+ def load_yml(filename)
56
+ YAML::load(IO.read(filename)).tap do |data|
57
+ set_translation_properties(data, :filename => filename) if data
58
+ end
59
+ end
60
+
61
+ def by_filename(filename)
62
+ select_translations { |keys, translation| translation.filename == filename }
63
+ end
64
+
65
+ def select_translations(&block)
66
+ init_translations unless initialized?
67
+ translations.select_nested(&block)
68
+ end
69
+
70
+ def each_translation(&block)
71
+ init_translations unless initialized?
72
+ translations.each_nested { |keys, t| block.call(keys.first, keys[1..-1], t) }
73
+ end
74
+
75
+ def save_file(filename, data)
76
+ type = File.extname(filename).tr('.', '').downcase
77
+ raise UnknownFileType.new(type, filename) unless respond_to?(:"save_#{type}")
78
+ send(:"save_#{type}", filename, data)
79
+ end
80
+
81
+ def save_yml(filename, data)
82
+ data = unset_translation_properties(data.deep_clone)
83
+ data = deep_stringify_keys(data)
84
+ File.open(filename, 'w+') do |f|
85
+ data.set_yaml_style(:sorted) if self.class.sort_keys
86
+ f.write(data.to_yaml)
87
+ end
88
+ end
89
+
90
+ def set_translation_properties(value, properties)
91
+ case value
92
+ when Hash
93
+ value.each_nested { |key, value| set_translation_properties(value, properties) }
94
+ else
95
+ value.meta_class.send(:include, TranslationProperties) unless value.respond_to?(:property_names)
96
+ value.set_properties(properties)
97
+ end
98
+ end
99
+
100
+ def unset_translation_properties(value)
101
+ case value
102
+ when Hash
103
+ value.each_nested { |key, value| value.unset_properties }
104
+ else
105
+ value.unset_properties
106
+ end
107
+ end
108
+
109
+ # Return a new hash with all keys and nested keys converted to strings.
110
+ def deep_stringify_keys(hash)
111
+ hash.inject({}) { |result, (key, value)|
112
+ value = deep_stringify_keys(value) if value.is_a?(Hash)
113
+ result[key.to_s] = value
114
+ result
115
+ }
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,84 @@
1
+ require 'i18n/index'
2
+ require 'i18n/exceptions/key_exists'
3
+ require 'core_ext/hash/slice'
4
+ require 'highline/import'
5
+ require 'highlighters/ansi'
6
+
7
+ module I18n
8
+ module Commands
9
+ class Keys
10
+ def initialize(highlighter = Highlighters::Ansi.new(:bold), out = $stdout)
11
+ @highlighter = highlighter
12
+ @out = out
13
+ end
14
+
15
+ def find(keys, options = {})
16
+ index = index(options)
17
+ index.find_calls(*keys).each do |call|
18
+ log "\n" + call.to_s(:context => options[:context], :highlight => @highlighter).gsub(Dir.pwd, '')
19
+ end
20
+ end
21
+
22
+ def replace(search, replace, options)
23
+ key = search.gsub(/^\*\.|\.\*$/, '')
24
+ interactive = options.has_key?(:'interactive') ? options[:'interactive'] : true
25
+ index = index(options)
26
+ found = false
27
+
28
+ index.find_calls(search).each do |call|
29
+ if replace?(call, replace, :interactive => interactive)
30
+ I18n.backend.copy_translations(key, replace)
31
+ index.replace_key(call, search, replace)
32
+ I18n.backend.remove_translation(key)
33
+ found = true
34
+ end
35
+ break if cancelled?
36
+ end
37
+
38
+ log "No occurences were found or no replacements made for: #{search}." unless found
39
+ rescue I18n::KeyExists => e
40
+ log e.message
41
+ end
42
+
43
+ protected
44
+
45
+ def log(str)
46
+ @out.puts(str)
47
+ end
48
+
49
+ def index(options) # TODO use context + verbose options
50
+ method = options.delete(:index) ? :load_or_create : :new
51
+ options = options.slice(:root_dir, :pattern, :format, :context)
52
+ options[:format] ||= I18n::Index::Format::Stdout.new(@out)
53
+ I18n::Index.send(method, options)
54
+ end
55
+
56
+ def cancelled?
57
+ @cancelled
58
+ end
59
+
60
+ def replace?(call, replace, options = { :interactive => true })
61
+ return true if @all || !options[:interactive]
62
+ return false if @cancelled
63
+ case answer = confirm_replace(call, replace)[0, 1]
64
+ when 'a'
65
+ @all = true
66
+ when 'c'
67
+ @cancelled = true and false
68
+ else
69
+ answer == 'y'
70
+ end
71
+ end
72
+
73
+ def confirm_replace(call, replace)
74
+ log call.to_s
75
+ log call.context(:highlight => @highlighter)
76
+ msg = "Replace this occurence of the key \"#{call.key}\" with \"#{replace}\"? [Y]es [N]o [A]ll [C]ancel"
77
+ answer = ask(msg, %w(y yes n no a all c cancel)) do |q|
78
+ q.case = :downcase
79
+ q.readline = true
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,9 @@
1
+ module I18n
2
+ class KeyExists < ArgumentError
3
+ attr_reader :locale, :key
4
+ def initialize(locale, key)
5
+ @key, @locale = key, locale
6
+ super "key exists: (#{locale}) :#{key.join('.')}"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ require 'core_ext/module/attribute_accessors'
2
+
3
+ require 'i18n/index/file'
4
+ require 'i18n/index/simple'
5
+ require 'i18n/ripper2ruby'
6
+
7
+ module I18n
8
+ module Index
9
+ mattr_accessor :pattern, :implementation, :parser, :filters
10
+
11
+ @@implementation = I18n::Index::Simple
12
+ @@parser = Ripper::RubyBuilder
13
+ @@filters = { '.erb' => lambda { |source| Erb::Stripper.new.to_ruby(source) } }
14
+ @@pattern = '**/*.{rb,erb}'
15
+
16
+ class << self
17
+ def new(*args)
18
+ implementation.new(*args)
19
+ end
20
+
21
+ def load_or_create(*args)
22
+ implementation.load_or_create(*args)
23
+ end
24
+
25
+ def filter(source, filename)
26
+ filters.each do |extname, filter|
27
+ source = filter.call(source) if ::File.extname(filename) == extname
28
+ end
29
+ source
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,38 @@
1
+ require 'core_ext/object/instance_variables'
2
+
3
+ module I18n
4
+ module Index
5
+ class Base
6
+ attr_accessor :root_dir, :pattern
7
+
8
+ def initialize(options = {})
9
+ @root_dir = options[:root_dir] || Dir.pwd
10
+ @pattern = options[:pattern] || Index.pattern
11
+ options[:format].setup(self) if options[:format]
12
+ end
13
+
14
+ def filenames
15
+ Dir[root_dir + '/' + pattern]
16
+ end
17
+
18
+ def files
19
+ @files ||= Files.new
20
+ end
21
+
22
+ protected
23
+
24
+ def marshalled_vars
25
+ # TODO marshalling :files works but makes things much slower during tests - check this for real situations
26
+ [:root_dir, :pattern]
27
+ end
28
+
29
+ def marshal_dump
30
+ instance_variables_get(*marshalled_vars)
31
+ end
32
+
33
+ def marshal_load(data)
34
+ instance_variables_set(data.slice(*marshalled_vars))
35
+ end
36
+ end
37
+ end
38
+ end