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,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