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.
- data/lib/core_ext/hash/iterate_nested.rb +35 -0
- data/lib/core_ext/hash/slice.rb +20 -0
- data/lib/core_ext/hash/sorted_yaml_style.rb +17 -0
- data/lib/core_ext/hash/symbolize_keys.rb +14 -0
- data/lib/core_ext/module/attribute_accessors.rb +48 -0
- data/lib/core_ext/object/deep_clone.rb +5 -0
- data/lib/core_ext/object/instance_variables.rb +9 -0
- data/lib/core_ext/object/meta_class.rb +5 -0
- data/lib/core_ext/object/tap.rb +6 -0
- data/lib/i18n/backend/simple_storage.rb +119 -0
- data/lib/i18n/commands/keys.rb +84 -0
- data/lib/i18n/exceptions/key_exists.rb +9 -0
- data/lib/i18n/index.rb +33 -0
- data/lib/i18n/index/base.rb +38 -0
- data/lib/i18n/index/file.rb +55 -0
- data/lib/i18n/index/format.rb +49 -0
- data/lib/i18n/index/key.rb +45 -0
- data/lib/i18n/index/occurence.rb +18 -0
- data/lib/i18n/index/simple.rb +69 -0
- data/lib/i18n/index/simple/data.rb +34 -0
- data/lib/i18n/index/simple/storage.rb +79 -0
- data/lib/i18n/ripper2ruby.rb +7 -0
- data/lib/i18n/ripper2ruby/translate_args_list.rb +89 -0
- data/lib/i18n/ripper2ruby/translate_call.rb +66 -0
- data/lib/i18n/translation_properties.rb +38 -0
- data/test/all.rb +1 -1
- data/test/core_ext/hash_iterate_nested.rb +31 -0
- data/test/fixtures/all.rb.src +106 -0
- data/test/fixtures/config.yml +3 -0
- data/test/fixtures/locale/de.yml +4 -0
- data/test/fixtures/locale/en.yml +4 -0
- data/test/fixtures/source_1.rb +2 -1
- data/test/fixtures/translate/double_key.rb +32 -0
- data/test/fixtures/translate/double_scope.rb +32 -0
- data/test/fixtures/translate/single_key.rb +10 -0
- data/test/fixtures/translate/single_scope.rb +32 -0
- data/test/i18n/backend/simple_storage_test.rb +81 -0
- data/test/i18n/backend/translation_properties_test.rb +33 -0
- data/test/i18n/index/all.rb +1 -0
- data/test/i18n/index/args_replace_test.rb +218 -0
- data/test/i18n/index/calls_replace_test.rb +67 -0
- data/test/i18n/index/commands_test.rb +75 -0
- data/test/i18n/index/key_test.rb +32 -0
- data/test/i18n/index/simple_test.rb +67 -0
- data/test/i18n/ripper2ruby/translate_call_test.rb +98 -0
- data/test/test_helper.rb +66 -9
- metadata +49 -32
- data/MIT-LICENSE +0 -20
- data/README.textile +0 -1
- data/bin/i18n-keys +0 -6
- data/lib/ansi.rb +0 -19
- data/lib/i18n/keys.rb +0 -51
- data/lib/i18n/keys/commands.rb +0 -53
- data/lib/i18n/keys/formatter.rb +0 -39
- data/lib/i18n/keys/index.rb +0 -209
- data/lib/i18n/keys/occurence.rb +0 -120
- data/lib/i18n/parser/erb_parser.rb +0 -54
- data/lib/i18n/parser/ruby_parser.rb +0 -93
- data/test/commands_test.rb +0 -1
- data/test/erb_parser_test.rb +0 -31
- data/test/index_test.rb +0 -135
- data/test/keys_test.rb +0 -75
- data/test/occurence_test.rb +0 -130
- 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,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,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
|
data/lib/i18n/index.rb
ADDED
@@ -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
|