rails-i18nterface 0.1.0
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +22 -0
- data/README.md +109 -0
- data/Rakefile +23 -0
- data/app/assets/javascripts/rails_i18nterface/application.js +8 -0
- data/app/assets/javascripts/rails_i18nterface/base.js +62 -0
- data/app/assets/javascripts/rails_i18nterface/ender.js +3260 -0
- data/app/assets/javascripts/rails_i18nterface/ender.min.js +45 -0
- data/app/assets/stylesheets/rails_i18nterface/application.css +450 -0
- data/app/controllers/rails_i18nterface/application_controller.rb +4 -0
- data/app/controllers/rails_i18nterface/translate_controller.rb +255 -0
- data/app/helpers/rails_i18nterface/application_helper.rb +4 -0
- data/app/helpers/rails_i18nterface/translate_helper.rb +74 -0
- data/app/models/translation.rb +4 -0
- data/app/views/layouts/rails_i18nterface/translate.html.erb +13 -0
- data/app/views/rails_i18nterface/translate/_namespaces.html.erb +1 -0
- data/app/views/rails_i18nterface/translate/_pagination.html.erb +18 -0
- data/app/views/rails_i18nterface/translate/index.html.erb +132 -0
- data/config/routes.rb +7 -0
- data/db/migrate/20110921112044_create_translations.rb +18 -0
- data/lib/rails-i18nterface.rb +8 -0
- data/lib/rails-i18nterface/engine.rb +7 -0
- data/lib/rails-i18nterface/file.rb +35 -0
- data/lib/rails-i18nterface/keys.rb +192 -0
- data/lib/rails-i18nterface/log.rb +35 -0
- data/lib/rails-i18nterface/storage.rb +29 -0
- data/lib/rails-i18nterface/version.rb +3 -0
- data/lib/tasks/rails-i18nterface.rake +4 -0
- data/spec/controllers/translate_controller_spec.rb +135 -0
- data/spec/internal/app/assets/javascripts/application.js +2 -0
- data/spec/internal/app/assets/stylesheets/application.css +3 -0
- data/spec/internal/app/controllers/application_controller.rb +4 -0
- data/spec/internal/app/models/article.rb +11 -0
- data/spec/internal/app/views/application/index.html.erb +5 -0
- data/spec/internal/app/views/categories/category.erb +1 -0
- data/spec/internal/app/views/categories/category.html +1 -0
- data/spec/internal/app/views/categories/category.html.erb +1 -0
- data/spec/internal/app/views/categories/category.rhtml +5 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/routes.rb +4 -0
- data/spec/internal/db/combustion_test.sqlite +0 -0
- data/spec/internal/db/schema.rb +3 -0
- data/spec/internal/log/test.log +521 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/lib/file_spec.rb +53 -0
- data/spec/lib/keys_spec.rb +179 -0
- data/spec/lib/log_spec.rb +46 -0
- data/spec/lib/storage_spec.rb +33 -0
- data/spec/requests/search_spec.rb +62 -0
- data/spec/spec_helper.rb +37 -0
- metadata +200 -0
data/config/routes.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateTranslations < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :translations do |t|
|
4
|
+
t.string :locale
|
5
|
+
t.string :key
|
6
|
+
t.text :value
|
7
|
+
t.text :interpolations
|
8
|
+
t.boolean :is_proc, :default => false
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.down
|
15
|
+
drop_table :translations
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
class RailsI18nterface::File
|
4
|
+
attr_accessor :path
|
5
|
+
|
6
|
+
def initialize(path)
|
7
|
+
self.path = path
|
8
|
+
end
|
9
|
+
|
10
|
+
def write(keys)
|
11
|
+
FileUtils.mkdir_p File.dirname(path)
|
12
|
+
File.open(path, "w") do |file|
|
13
|
+
file.puts keys_to_yaml(self.class.deep_stringify_keys(keys))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def read
|
18
|
+
File.exists?(path) ? YAML::load(IO.read(path)) : {}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Stringifying keys for prettier YAML
|
22
|
+
def self.deep_stringify_keys(hash)
|
23
|
+
hash.inject({}) { |result, (key, value)|
|
24
|
+
value = deep_stringify_keys(value) if value.is_a? Hash
|
25
|
+
result[(key.to_s rescue key) || key] = value
|
26
|
+
result
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def keys_to_yaml(keys)
|
32
|
+
# Using ya2yaml, if available, for UTF8 support
|
33
|
+
keys.respond_to?(:ya2yaml) ? keys.ya2yaml(:escape_as_utf8 => true) : keys.to_yaml
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
#require 'pathname'
|
2
|
+
|
3
|
+
class RailsI18nterface::Keys
|
4
|
+
# Allows keys extracted from lookups in files to be cached
|
5
|
+
def self.files
|
6
|
+
@@files ||= new.files
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.names
|
10
|
+
@@names || new.names
|
11
|
+
end
|
12
|
+
# Allows flushing of the files cache
|
13
|
+
def self.files=(files)
|
14
|
+
@@files = files
|
15
|
+
end
|
16
|
+
|
17
|
+
def files
|
18
|
+
@files ||= extract_files
|
19
|
+
end
|
20
|
+
alias_method :to_hash, :files
|
21
|
+
|
22
|
+
def names
|
23
|
+
@names ||= build_namespaces
|
24
|
+
end
|
25
|
+
alias_method :to_tree, :names
|
26
|
+
|
27
|
+
def keys
|
28
|
+
files.keys
|
29
|
+
end
|
30
|
+
alias_method :to_a, :keys
|
31
|
+
|
32
|
+
def i18n_keys(locale)
|
33
|
+
I18n.backend.send(:init_translations) unless I18n.backend.initialized?
|
34
|
+
self.class.to_shallow_hash(I18n.backend.send(:translations)[locale.to_sym]).keys.sort
|
35
|
+
end
|
36
|
+
|
37
|
+
def untranslated_keys
|
38
|
+
self.class.translated_locales.inject({}) do |missing, locale|
|
39
|
+
missing[locale] = i18n_keys(I18n.default_locale).map do |key|
|
40
|
+
I18n.backend.send(:lookup, locale, key).nil? ? key : nil
|
41
|
+
end.compact
|
42
|
+
missing
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def missing_keys
|
47
|
+
locale = I18n.default_locale
|
48
|
+
yaml_keys = {}
|
49
|
+
yaml_keys = RailsI18nterface::Storage.file_paths(locale).inject({}) do |keys, path|
|
50
|
+
keys = keys.deep_merge(RailsI18nterface::File.new(path).read[locale.to_s])
|
51
|
+
end
|
52
|
+
files.reject { |key, file| self.class.contains_key?(yaml_keys, key) }
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.translated_locales
|
56
|
+
I18n.available_locales.reject { |locale| [:root, I18n.default_locale.to_sym].include?(locale) }
|
57
|
+
end
|
58
|
+
|
59
|
+
# Checks if a nested hash contains the keys in dot separated I18n key.
|
60
|
+
#
|
61
|
+
# Example:
|
62
|
+
#
|
63
|
+
# hash = {
|
64
|
+
# :foo => {
|
65
|
+
# :bar => {
|
66
|
+
# :baz => 1
|
67
|
+
# }
|
68
|
+
# }
|
69
|
+
# }
|
70
|
+
#
|
71
|
+
# contains_key?("foo", key) # => true
|
72
|
+
# contains_key?("foo.bar", key) # => true
|
73
|
+
# contains_key?("foo.bar.baz", key) # => true
|
74
|
+
# contains_key?("foo.bar.baz.bla", key) # => false
|
75
|
+
#
|
76
|
+
def self.contains_key?(hash, key)
|
77
|
+
keys = key.to_s.split(".")
|
78
|
+
return false if keys.empty?
|
79
|
+
!keys.inject(HashWithIndifferentAccess.new(hash)) do |memo, key|
|
80
|
+
memo.is_a?(Hash) ? memo.try(:[], key) : nil
|
81
|
+
end.nil?
|
82
|
+
end
|
83
|
+
|
84
|
+
# Convert something like:
|
85
|
+
#
|
86
|
+
# {
|
87
|
+
# :pressrelease => {
|
88
|
+
# :label => {
|
89
|
+
# :one => "Pressmeddelande"
|
90
|
+
# }
|
91
|
+
# }
|
92
|
+
# }
|
93
|
+
#
|
94
|
+
# to:
|
95
|
+
#
|
96
|
+
# {'pressrelease.label.one' => "Pressmeddelande"}
|
97
|
+
#
|
98
|
+
def self.to_shallow_hash(hash)
|
99
|
+
hash.inject({}) do |shallow_hash, (key, value)|
|
100
|
+
if value.is_a?(Hash)
|
101
|
+
to_shallow_hash(value).each do |sub_key, sub_value|
|
102
|
+
shallow_hash[[key, sub_key].join(".")] = sub_value
|
103
|
+
end
|
104
|
+
else
|
105
|
+
shallow_hash[key.to_s] = value
|
106
|
+
end
|
107
|
+
shallow_hash
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Convert something like:
|
112
|
+
#
|
113
|
+
# {'pressrelease.label.one' => "Pressmeddelande"}
|
114
|
+
#
|
115
|
+
# to:
|
116
|
+
#
|
117
|
+
# {
|
118
|
+
# :pressrelease => {
|
119
|
+
# :label => {
|
120
|
+
# :one => "Pressmeddelande"
|
121
|
+
# }
|
122
|
+
# }
|
123
|
+
# }
|
124
|
+
def self.to_deep_hash(hash)
|
125
|
+
hash.inject({}) do |deep_hash, (key, value)|
|
126
|
+
keys = key.to_s.split('.').reverse
|
127
|
+
leaf_key = keys.shift
|
128
|
+
key_hash = keys.inject({leaf_key.to_sym => value}) { |hash, key| {key.to_sym => hash} }
|
129
|
+
deep_merge!(deep_hash, key_hash)
|
130
|
+
deep_hash
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
|
135
|
+
def self.deep_merge!(hash1, hash2)
|
136
|
+
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
|
137
|
+
hash1.merge!(hash2, &merger)
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def extract_files
|
143
|
+
files_to_scan.inject(HashWithIndifferentAccess.new) do |files, file|
|
144
|
+
begin#hack to avoid UTF-8 error
|
145
|
+
IO.read(file).scan(i18n_lookup_pattern).flatten.map(&:to_sym).each do |key|
|
146
|
+
path = Pathname.new(File.expand_path(file)).relative_path_from(Pathname.new(Rails.root)).to_s
|
147
|
+
#puts path
|
148
|
+
if key[0] == '.'
|
149
|
+
key = guess_namespace(path) + key.to_s
|
150
|
+
end
|
151
|
+
#puts key
|
152
|
+
#puts
|
153
|
+
files[key] ||= []
|
154
|
+
files[key] << path if !files[key].include?(path)
|
155
|
+
end
|
156
|
+
rescue Exception => e
|
157
|
+
puts e.inspect
|
158
|
+
puts e.backtrace
|
159
|
+
puts "bug in Translation plugin, please debug, informations :"
|
160
|
+
puts file.inspect
|
161
|
+
puts i18n_lookup_pattern.inspect
|
162
|
+
end
|
163
|
+
files
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def guess_namespace(path)
|
168
|
+
parts = path.split(/\//)
|
169
|
+
case parts[0]
|
170
|
+
when 'app'
|
171
|
+
parts.shift
|
172
|
+
if parts[0] == 'views'
|
173
|
+
parts.shift
|
174
|
+
parts[-1].gsub!(/\.(erb|haml|slim)$/,'')
|
175
|
+
end
|
176
|
+
end
|
177
|
+
parts.join('.')
|
178
|
+
end
|
179
|
+
|
180
|
+
def build_namespaces
|
181
|
+
i18n_keys
|
182
|
+
end
|
183
|
+
|
184
|
+
def i18n_lookup_pattern
|
185
|
+
/\b(?:I18n\.t|I18n\.translate|t)(?:\s|\():?(?:'|")(\.?[a-z0-9_\.]+)(?:'|")/
|
186
|
+
end
|
187
|
+
|
188
|
+
def files_to_scan
|
189
|
+
Dir.glob(File.join(RailsI18nterface::Storage.root_dir, "{app,config,lib}", "**","*.{rb,erb,haml,slim,rhtml}")) +
|
190
|
+
Dir.glob(File.join(RailsI18nterface::Storage.root_dir, "{public,app/assets}", "javascripts", "**","*.{js,coffee}"))
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class RailsI18nterface::Log
|
2
|
+
attr_accessor :from_locale, :to_locale, :keys
|
3
|
+
|
4
|
+
def initialize(from_locale, to_locale, keys)
|
5
|
+
self.from_locale = from_locale
|
6
|
+
self.to_locale = to_locale
|
7
|
+
self.keys = keys
|
8
|
+
end
|
9
|
+
|
10
|
+
def write_to_file
|
11
|
+
current_texts = File.exists?(file_path) ? file.read : {}
|
12
|
+
current_texts.merge!(from_texts)
|
13
|
+
file.write(current_texts)
|
14
|
+
end
|
15
|
+
|
16
|
+
def read
|
17
|
+
file.read
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def file
|
22
|
+
@file ||= RailsI18nterface::File.new(file_path)
|
23
|
+
end
|
24
|
+
|
25
|
+
def from_texts
|
26
|
+
RailsI18nterface::File.deep_stringify_keys(RailsI18nterface::Keys.to_deep_hash(keys.inject({}) do |hash, key|
|
27
|
+
hash[key] = I18n.backend.send(:lookup, from_locale, key)
|
28
|
+
hash
|
29
|
+
end))
|
30
|
+
end
|
31
|
+
|
32
|
+
def file_path
|
33
|
+
File.join(Rails.root, "config", "locales", "log", "from_#{from_locale}_to_#{to_locale}.yml")
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class RailsI18nterface::Storage
|
2
|
+
attr_accessor :locale
|
3
|
+
|
4
|
+
def initialize(locale)
|
5
|
+
self.locale = locale.to_sym
|
6
|
+
end
|
7
|
+
|
8
|
+
def write_to_file
|
9
|
+
RailsI18nterface::File.new(file_path).write(keys)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.file_paths(locale)
|
13
|
+
Dir.glob(File.join(root_dir, "config", "locales", "**","#{locale}.yml"))
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.root_dir
|
17
|
+
Rails.root
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def keys
|
23
|
+
{locale => I18n.backend.send(:translations)[locale]}
|
24
|
+
end
|
25
|
+
|
26
|
+
def file_path
|
27
|
+
File.join(self.class.root_dir, "config", "locales", "#{locale}.yml")
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RailsI18nterface::TranslateController do
|
4
|
+
describe "index" do
|
5
|
+
before(:each) do
|
6
|
+
controller.stub!(:per_page).and_return(1)
|
7
|
+
I18n.backend.stub!(:translations).and_return(i18n_translations)
|
8
|
+
I18n.backend.instance_eval { @initialized = true }
|
9
|
+
keys = mock(:keys)
|
10
|
+
keys.stub!(:i18n_keys).and_return(['vendor.foobar'])
|
11
|
+
RailsI18nterface::Keys.should_receive(:new).and_return(keys)
|
12
|
+
RailsI18nterface::Keys.should_receive(:files).and_return(files)
|
13
|
+
I18n.stub!(:valid_locales).and_return([:en, :sv])
|
14
|
+
I18n.stub!(:default_locale).and_return(:sv)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "shows sorted paginated keys from the translate from locale and extracted keys by default" do
|
18
|
+
get_page :index, use_route: 'rails-i18nterface'
|
19
|
+
assigns(:from_locale).should == :sv
|
20
|
+
assigns(:to_locale).should == :en
|
21
|
+
assigns(:files).should == files
|
22
|
+
assigns(:keys).sort.should == ['articles.new.page_title', 'home.page_title', 'vendor.foobar']
|
23
|
+
assigns(:paginated_keys).should == ['articles.new.page_title']
|
24
|
+
end
|
25
|
+
|
26
|
+
it "can be paginated with the page param" do
|
27
|
+
get_page :index, :page => 2, use_route: 'rails-i18nterface'
|
28
|
+
assigns(:files).should == files
|
29
|
+
assigns(:paginated_keys).should == ['home.page_title']
|
30
|
+
end
|
31
|
+
|
32
|
+
it "accepts a key_pattern param with key_type=starts_with" do
|
33
|
+
get_page :index, :key_pattern => 'articles', :key_type => 'starts_with', use_route: 'rails-i18nterface'
|
34
|
+
assigns(:files).should == files
|
35
|
+
assigns(:paginated_keys).should == ['articles.new.page_title']
|
36
|
+
assigns(:total_entries).should == 1
|
37
|
+
end
|
38
|
+
|
39
|
+
it "accepts a key_pattern param with key_type=contains" do
|
40
|
+
get_page :index, :key_pattern => 'page_', :key_type => 'contains', use_route: 'rails-i18nterface'
|
41
|
+
assigns(:files).should == files
|
42
|
+
assigns(:total_entries).should == 2
|
43
|
+
assigns(:paginated_keys).should == ['articles.new.page_title']
|
44
|
+
end
|
45
|
+
|
46
|
+
it "accepts a filter=untranslated param" do
|
47
|
+
get_page :index, :filter => 'untranslated', use_route: 'rails-i18nterface'
|
48
|
+
assigns(:total_entries).should == 2
|
49
|
+
assigns(:paginated_keys).should == ['articles.new.page_title']
|
50
|
+
end
|
51
|
+
|
52
|
+
it "accepts a filter=translated param" do
|
53
|
+
get_page :index, :filter => 'translated', use_route: 'rails-i18nterface'
|
54
|
+
assigns(:total_entries).should == 1
|
55
|
+
assigns(:paginated_keys).should == ['vendor.foobar']
|
56
|
+
end
|
57
|
+
|
58
|
+
it "accepts a filter=changed param" do
|
59
|
+
log = mock(:log)
|
60
|
+
old_translations = {:home => {:page_title => "Skapar ny artikel"}}
|
61
|
+
log.should_receive(:read).and_return(RailsI18nterface::File.deep_stringify_keys(old_translations))
|
62
|
+
RailsI18nterface::Log.should_receive(:new).with(:sv, :en, {}).and_return(log)
|
63
|
+
get_page :index, :filter => 'changed', use_route: 'rails-i18nterface'
|
64
|
+
assigns(:total_entries).should == 1
|
65
|
+
assigns(:keys).should == ["home.page_title"]
|
66
|
+
end
|
67
|
+
|
68
|
+
def i18n_translations
|
69
|
+
HashWithIndifferentAccess.new({
|
70
|
+
:en => {
|
71
|
+
:vendor => {
|
72
|
+
:foobar => "Foo Baar"
|
73
|
+
}
|
74
|
+
},
|
75
|
+
:sv => {
|
76
|
+
:articles => {
|
77
|
+
:new => {
|
78
|
+
:page_title => "Skapa ny artikel"
|
79
|
+
}
|
80
|
+
},
|
81
|
+
:home => {
|
82
|
+
:page_title => "Valkommen till I18n"
|
83
|
+
},
|
84
|
+
:vendor => {
|
85
|
+
:foobar => "Fobar"
|
86
|
+
}
|
87
|
+
}
|
88
|
+
})
|
89
|
+
end
|
90
|
+
|
91
|
+
def files
|
92
|
+
HashWithIndifferentAccess.new({
|
93
|
+
:'home.page_title' => ["app/views/home/index.rhtml"],
|
94
|
+
:'general.back' => ["app/views/articles/new.rhtml", "app/views/categories/new.rhtml"],
|
95
|
+
:'articles.new.page_title' => ["app/views/articles/new.rhtml"]
|
96
|
+
})
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "translate" do
|
101
|
+
it "should store translations to I18n backend and then write them to a YAML file" do
|
102
|
+
session[:from_locale] = :sv
|
103
|
+
session[:to_locale] = :en
|
104
|
+
translations = {
|
105
|
+
:articles => {
|
106
|
+
:new => {
|
107
|
+
:title => "New Article"
|
108
|
+
}
|
109
|
+
},
|
110
|
+
:category => "Category"
|
111
|
+
}
|
112
|
+
key_param = {'articles.new.title' => "New Article", "category" => "Category"}
|
113
|
+
#I18n.backend.should_receive(:store_translations).with(:en, translations)
|
114
|
+
storage = mock(:storage)
|
115
|
+
storage.should_receive(:write_to_file)
|
116
|
+
RailsI18nterface::Storage.should_receive(:new).with(:en).and_return(storage)
|
117
|
+
log = mock(:log)
|
118
|
+
log.should_receive(:write_to_file)
|
119
|
+
RailsI18nterface::Log.should_receive(:new).with(:sv, :en, key_param.keys).and_return(log)
|
120
|
+
RailsI18nterface::Storage.stub!(:root_dir).and_return(i18n_files_dir)
|
121
|
+
put :update, key: key_param, version: 1, use_route: 'rails-i18nterface'
|
122
|
+
response.should be_redirect
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def get_page(*args)
|
127
|
+
get(*args, use_route: 'rails-i18nterface')
|
128
|
+
response.should be_success
|
129
|
+
end
|
130
|
+
|
131
|
+
def i18n_files_dir
|
132
|
+
File.expand_path(File.join("..", "..", "..", "spec", "internal"), __FILE__)
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|