translate-rails3-plus 0.0.1
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.
- data/MIT-LICENSE +20 -0
- data/README.md +86 -0
- data/Rakefile +35 -0
- data/VERSION +1 -0
- data/app/controllers/translate_controller.rb +222 -0
- data/app/helpers/translate_helper.rb +57 -0
- data/app/views/layouts/translate.html.erb +396 -0
- data/app/views/translate/_array_form.html.erb +42 -0
- data/app/views/translate/_pagination.html.erb +24 -0
- data/app/views/translate/_string_form.html.erb +36 -0
- data/app/views/translate/index.html.erb +96 -0
- data/config/routes.rb +5 -0
- data/init.rb +1 -0
- data/lib/tasks/translate.rake +178 -0
- data/lib/translate/file.rb +35 -0
- data/lib/translate/keys.rb +185 -0
- data/lib/translate/log.rb +35 -0
- data/lib/translate/routes.rb +11 -0
- data/lib/translate/storage.rb +28 -0
- data/lib/translate.rb +17 -0
- data/spec/controllers/translate_controller_spec.rb +130 -0
- data/spec/file_spec.rb +54 -0
- data/spec/files/translate/app/models/article.rb +12 -0
- data/spec/files/translate/app/views/category.erb +1 -0
- data/spec/files/translate/app/views/category.html +1 -0
- data/spec/files/translate/app/views/category.html.erb +1 -0
- data/spec/files/translate/app/views/category.rhtml +5 -0
- data/spec/files/translate/public/javascripts/application.js +1 -0
- data/spec/keys_spec.rb +179 -0
- data/spec/log_spec.rb +47 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/storage_spec.rb +33 -0
- data/translate-rails3-plus.gemspec +70 -0
- metadata +109 -0
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class Hash
|
4
|
+
def deep_merge(other)
|
5
|
+
# deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
|
6
|
+
merger = proc { |key, v1, v2| (Hash === v1 && Hash === v2) ? v1.merge(v2, &merger) : v2 }
|
7
|
+
merge(other, &merger)
|
8
|
+
end
|
9
|
+
|
10
|
+
def set(keys, value)
|
11
|
+
key = keys.shift
|
12
|
+
if keys.empty?
|
13
|
+
self[key] = value
|
14
|
+
else
|
15
|
+
self[key] ||= {}
|
16
|
+
self[key].set keys, value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
if ENV['SORT']
|
21
|
+
# copy of ruby's to_yaml method, prepending sort.
|
22
|
+
# before each so we get an ordered yaml file
|
23
|
+
def to_yaml( opts = {} )
|
24
|
+
YAML::quick_emit( self, opts ) do |out|
|
25
|
+
out.map( taguri, to_yaml_style ) do |map|
|
26
|
+
sort.each do |k, v| #<- Adding sort.
|
27
|
+
map.add( k, v )
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
namespace :translate do
|
36
|
+
desc "Show untranslated keys for locale LOCALE"
|
37
|
+
task :untranslated => :environment do
|
38
|
+
from_locale = I18n.default_locale
|
39
|
+
untranslated = Translate::Keys.new.untranslated_keys
|
40
|
+
|
41
|
+
messages = []
|
42
|
+
untranslated.each do |locale, keys|
|
43
|
+
keys.each do |key|
|
44
|
+
from_text = I18n.backend.send(:lookup, from_locale, key)
|
45
|
+
messages << "#{locale}.#{key} (#{from_locale}.#{key}='#{from_text}')"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
if messages.present?
|
50
|
+
messages.each { |m| puts m }
|
51
|
+
else
|
52
|
+
puts "No untranslated keys"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
desc "Show I18n keys that are missing in the config/locales/default_locale.yml YAML file"
|
57
|
+
task :missing => :environment do
|
58
|
+
missing = Translate::Keys.new.missing_keys.inject([]) do |keys, (key, filename)|
|
59
|
+
keys << "#{key} in \t #{filename} is missing"
|
60
|
+
end
|
61
|
+
puts missing.present? ? missing.join("\n") : "No missing translations in the default locale file"
|
62
|
+
end
|
63
|
+
|
64
|
+
desc "Remove all translation texts that are no longer present in the locale they were translated from"
|
65
|
+
task :remove_obsolete_keys => :environment do
|
66
|
+
I18n.backend.send(:init_translations)
|
67
|
+
master_locale = ENV['LOCALE'] || I18n.default_locale
|
68
|
+
Translate::Keys.translated_locales.each do |locale|
|
69
|
+
texts = {}
|
70
|
+
Translate::Keys.new.i18n_keys(locale).each do |key|
|
71
|
+
if I18n.backend.send(:lookup, master_locale, key).to_s.present?
|
72
|
+
texts[key] = I18n.backend.send(:lookup, locale, key)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
I18n.backend.send(:translations)[locale] = nil # Clear out all current translations
|
76
|
+
I18n.backend.store_translations(locale, Translate::Keys.to_deep_hash(texts))
|
77
|
+
Translate::Storage.new(locale).write_to_file
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
desc "Merge I18n keys from log/translations.yml into config/locales/*.yml (for use with the Rails I18n TextMate bundle)"
|
82
|
+
task :merge_keys => :environment do
|
83
|
+
I18n.backend.send(:init_translations)
|
84
|
+
new_translations = YAML::load(IO.read(File.join(Rails.root, "log", "translations.yml")))
|
85
|
+
raise("Can only merge in translations in single locale") if new_translations.keys.size > 1
|
86
|
+
locale = new_translations.keys.first
|
87
|
+
|
88
|
+
overwrites = false
|
89
|
+
Translate::Keys.to_shallow_hash(new_translations[locale]).keys.each do |key|
|
90
|
+
new_text = key.split(".").inject(new_translations[locale]) { |hash, sub_key| hash[sub_key] }
|
91
|
+
existing_text = I18n.backend.send(:lookup, locale.to_sym, key)
|
92
|
+
if existing_text && new_text != existing_text
|
93
|
+
puts "ERROR: key #{key} already exists with text '#{existing_text.inspect}' and would be overwritten by new text '#{new_text}'. " +
|
94
|
+
"Set environment variable OVERWRITE=1 if you really want to do this."
|
95
|
+
overwrites = true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
if !overwrites || ENV['OVERWRITE']
|
100
|
+
I18n.backend.store_translations(locale, new_translations[locale])
|
101
|
+
Translate::Storage.new(locale).write_to_file
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
desc "Apply Google translate to auto translate all texts in locale ENV['FROM'] to locale ENV['TO']"
|
106
|
+
task :google => :environment do
|
107
|
+
raise "Please specify FROM and TO locales as environment variables" if ENV['FROM'].blank? || ENV['TO'].blank?
|
108
|
+
|
109
|
+
# Depends on httparty gem
|
110
|
+
# http://www.robbyonrails.com/articles/2009/03/16/httparty-goes-foreign
|
111
|
+
class GoogleApi
|
112
|
+
include HTTParty
|
113
|
+
base_uri 'ajax.googleapis.com'
|
114
|
+
def self.translate(string, to, from)
|
115
|
+
tries = 0
|
116
|
+
begin
|
117
|
+
get("/ajax/services/language/translate",
|
118
|
+
:query => {:langpair => "#{from}|#{to}", :q => string, :v => 1.0},
|
119
|
+
:format => :json)
|
120
|
+
rescue
|
121
|
+
tries += 1
|
122
|
+
puts("SLEEPING - retrying in 5...")
|
123
|
+
sleep(5)
|
124
|
+
retry if tries < 10
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
I18n.backend.send(:init_translations)
|
130
|
+
|
131
|
+
start_at = Time.now
|
132
|
+
translations = {}
|
133
|
+
Translate::Keys.new.i18n_keys(ENV['FROM']).each do |key|
|
134
|
+
from_text = I18n.backend.send(:lookup, ENV['FROM'], key).to_s
|
135
|
+
to_text = I18n.backend.send(:lookup, ENV['TO'], key)
|
136
|
+
if !from_text.blank? && to_text.blank?
|
137
|
+
print "#{key}: '#{from_text[0, 40]}' => "
|
138
|
+
if !translations[from_text]
|
139
|
+
response = GoogleApi.translate(from_text, ENV['TO'], ENV['FROM'])
|
140
|
+
translations[from_text] = response["responseData"] && response["responseData"]["translatedText"]
|
141
|
+
end
|
142
|
+
if !(translation = translations[from_text]).blank?
|
143
|
+
translation.gsub!(/\(\(([a-z_.]+)\)\)/i, '{{\1}}')
|
144
|
+
# Google translate sometimes replaces {{foobar}} with (()) foobar. We skip these
|
145
|
+
if translation !~ /\(\(\)\)/
|
146
|
+
puts "'#{translation[0, 40]}'"
|
147
|
+
I18n.backend.store_translations(ENV['TO'].to_sym, Translate::Keys.to_deep_hash({key => translation}))
|
148
|
+
else
|
149
|
+
puts "SKIPPING since interpolations were messed up: '#{translation[0,40]}'"
|
150
|
+
end
|
151
|
+
else
|
152
|
+
puts "NO TRANSLATION - #{response.inspect}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
puts "\nTime elapsed: #{(((Time.now - start_at) / 60) * 10).to_i / 10.to_f} minutes"
|
158
|
+
Translate::Storage.new(ENV['TO'].to_sym).write_to_file
|
159
|
+
end
|
160
|
+
|
161
|
+
desc "List keys that have changed I18n texts between YAML file ENV['FROM_FILE'] and YAML file ENV['TO_FILE']. Set ENV['VERBOSE'] to see changes"
|
162
|
+
task :changed => :environment do
|
163
|
+
from_hash = Translate::Keys.to_shallow_hash(Translate::File.new(ENV['FROM_FILE']).read)
|
164
|
+
to_hash = Translate::Keys.to_shallow_hash(Translate::File.new(ENV['TO_FILE']).read)
|
165
|
+
from_hash.each do |key, from_value|
|
166
|
+
if (to_value = to_hash[key]) && to_value != from_value
|
167
|
+
key_without_locale = key[/^[^.]+\.(.+)$/, 1]
|
168
|
+
if ENV['VERBOSE']
|
169
|
+
puts "KEY: #{key_without_locale}"
|
170
|
+
puts "FROM VALUE: '#{from_value}'"
|
171
|
+
puts "TO VALUE: '#{to_value}'"
|
172
|
+
else
|
173
|
+
puts key_without_locale
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
class Translate::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(Translate::File.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,185 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
class Translate::Keys
|
4
|
+
# Allows keys extracted from lookups in files to be cached
|
5
|
+
def self.files
|
6
|
+
@@files ||= Translate::Keys.new.files
|
7
|
+
end
|
8
|
+
|
9
|
+
# Allows flushing of the files cache
|
10
|
+
def self.files=(files)
|
11
|
+
@@files = files
|
12
|
+
end
|
13
|
+
|
14
|
+
def files
|
15
|
+
@files ||= extract_files
|
16
|
+
end
|
17
|
+
alias_method :to_hash, :files
|
18
|
+
|
19
|
+
def keys
|
20
|
+
files.keys
|
21
|
+
end
|
22
|
+
alias_method :to_a, :keys
|
23
|
+
|
24
|
+
def i18n_keys(locale)
|
25
|
+
I18n.backend.send(:init_translations) unless I18n.backend.initialized?
|
26
|
+
Translate::Keys.to_shallow_hash(I18n.backend.send(:translations)[locale.to_sym]).keys.sort
|
27
|
+
end
|
28
|
+
|
29
|
+
def untranslated_keys
|
30
|
+
Translate::Keys.translated_locales.inject({}) do |missing, locale|
|
31
|
+
missing[locale] = i18n_keys(I18n.default_locale).map do |key|
|
32
|
+
I18n.backend.send(:lookup, locale, key).nil? ? key : nil
|
33
|
+
end.compact
|
34
|
+
missing
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def missing_keys
|
39
|
+
locale = I18n.default_locale; yaml_keys = {}
|
40
|
+
yaml_keys = Translate::Storage.file_paths(locale).inject({}) do |keys, path|
|
41
|
+
keys = keys.deep_merge(Translate::File.new(path).read[locale.to_s])
|
42
|
+
end
|
43
|
+
files.reject { |key, file| self.class.contains_key?(yaml_keys, key) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.translated_locales
|
47
|
+
I18n.available_locales.reject { |locale| [:root, I18n.default_locale.to_sym].include?(locale) }
|
48
|
+
end
|
49
|
+
|
50
|
+
# Checks if a nested hash contains the keys in dot separated I18n key.
|
51
|
+
#
|
52
|
+
# Example:
|
53
|
+
#
|
54
|
+
# hash = {
|
55
|
+
# :foo => {
|
56
|
+
# :bar => {
|
57
|
+
# :baz => 1
|
58
|
+
# }
|
59
|
+
# }
|
60
|
+
# }
|
61
|
+
#
|
62
|
+
# contains_key?("foo", key) # => true
|
63
|
+
# contains_key?("foo.bar", key) # => true
|
64
|
+
# contains_key?("foo.bar.baz", key) # => true
|
65
|
+
# contains_key?("foo.bar.baz.bla", key) # => false
|
66
|
+
#
|
67
|
+
def self.contains_key?(hash, key)
|
68
|
+
keys = key.to_s.split(".")
|
69
|
+
return false if keys.empty?
|
70
|
+
!keys.inject(HashWithIndifferentAccess.new(hash)) do |memo, key|
|
71
|
+
memo.is_a?(Hash) ? memo.try(:[], key) : nil
|
72
|
+
end.nil?
|
73
|
+
end
|
74
|
+
|
75
|
+
# Convert something like:
|
76
|
+
#
|
77
|
+
# {
|
78
|
+
# :pressrelease => {
|
79
|
+
# :label => {
|
80
|
+
# :one => "Pressmeddelande"
|
81
|
+
# }
|
82
|
+
# }
|
83
|
+
# }
|
84
|
+
#
|
85
|
+
# to:
|
86
|
+
#
|
87
|
+
# {'pressrelease.label.one' => "Pressmeddelande"}
|
88
|
+
#
|
89
|
+
def self.to_shallow_hash(hash)
|
90
|
+
hash.inject({}) do |shallow_hash, (key, value)|
|
91
|
+
if value.is_a?(Hash)
|
92
|
+
to_shallow_hash(value).each do |sub_key, sub_value|
|
93
|
+
shallow_hash[[key, sub_key].join(".")] = sub_value
|
94
|
+
end
|
95
|
+
else
|
96
|
+
shallow_hash[key.to_s] = value
|
97
|
+
end
|
98
|
+
shallow_hash
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Convert something like:
|
103
|
+
#
|
104
|
+
# {'pressrelease.label.one' => "Pressmeddelande"}
|
105
|
+
#
|
106
|
+
# to:
|
107
|
+
#
|
108
|
+
# {
|
109
|
+
# :pressrelease => {
|
110
|
+
# :label => {
|
111
|
+
# :one => "Pressmeddelande"
|
112
|
+
# }
|
113
|
+
# }
|
114
|
+
# }
|
115
|
+
def self.to_deep_hash(hash)
|
116
|
+
hash.inject({}) do |deep_hash, (key, value)|
|
117
|
+
keys = key.to_s.split('.').reverse
|
118
|
+
leaf_key = keys.shift
|
119
|
+
key_hash = keys.inject({leaf_key.to_sym => value}) { |hash, key| {key.to_sym => hash} }
|
120
|
+
deep_merge!(deep_hash, key_hash)
|
121
|
+
deep_hash
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
|
126
|
+
def self.deep_merge!(hash1, hash2)
|
127
|
+
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
|
128
|
+
hash1.merge!(hash2, &merger)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Convert something like:
|
132
|
+
#
|
133
|
+
# {'0' => "elem 1", '1' => "elem 2"}
|
134
|
+
#
|
135
|
+
# to:
|
136
|
+
#
|
137
|
+
# ["elem 1", "elem 2"]
|
138
|
+
#
|
139
|
+
def self.arraylize(input_hash)
|
140
|
+
input_hash.inject([]) do |constructed_array, (key, value)|
|
141
|
+
constructed_array << value
|
142
|
+
constructed_array
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def extract_files
|
149
|
+
files_to_scan.inject(HashWithIndifferentAccess.new) do |files, file|
|
150
|
+
keys = IO.read(file)
|
151
|
+
if keys.respond_to? "encode"
|
152
|
+
keys = keys.encode("UTF-8").force_encoding("UTF-8")
|
153
|
+
end
|
154
|
+
error_count = 0
|
155
|
+
begin
|
156
|
+
encoded_keys = keys.scan(i18n_lookup_pattern)
|
157
|
+
rescue => e
|
158
|
+
unless error_count > 1
|
159
|
+
if keys.respond_to? 'encode!'
|
160
|
+
keys.encode!('utf-8', 'utf-8', :invalid => :replace)
|
161
|
+
end
|
162
|
+
error_count += 1
|
163
|
+
retry
|
164
|
+
else
|
165
|
+
puts "cannot fix: #{e} on : #{file}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
encoded_keys.flatten.map(&:to_sym).each do |key|
|
169
|
+
files[key] ||= []
|
170
|
+
path = Pathname.new(File.expand_path(file)).relative_path_from(Pathname.new(Rails.root)).to_s
|
171
|
+
files[key] << path if !files[key].include?(path)
|
172
|
+
end
|
173
|
+
files
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def i18n_lookup_pattern
|
178
|
+
/\b(?:I18n\.t|I18n\.translate|t)(?:\s|\():?'([a-z0-9_]+.[a-z0-9_.]+)'\)?/
|
179
|
+
end
|
180
|
+
|
181
|
+
def files_to_scan
|
182
|
+
Dir.glob(File.join(Translate::Storage.root_dir, "{app,config,lib}", "**","*.{rb,erb,rhtml}")) +
|
183
|
+
Dir.glob(File.join(Translate::Storage.root_dir, "public", "javascripts", "**","*.js"))
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Translate::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 ||= Translate::File.new(file_path)
|
23
|
+
end
|
24
|
+
|
25
|
+
def from_texts
|
26
|
+
Translate::File.deep_stringify_keys(Translate::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,11 @@
|
|
1
|
+
module Translate
|
2
|
+
class Routes
|
3
|
+
def self.translation_ui(map)
|
4
|
+
map.with_options(:controller => 'translate') do |t|
|
5
|
+
t.translate_list 'translate'
|
6
|
+
t.translate 'translate/translate', :action => 'translate'
|
7
|
+
t.translate_reload 'translate/reload', :action => 'reload'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class Translate::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
|
+
Translate::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
|
+
def keys
|
22
|
+
{locale => I18n.backend.send(:translations)[locale]}
|
23
|
+
end
|
24
|
+
|
25
|
+
def file_path
|
26
|
+
File.join(Translate::Storage.root_dir, "config", "locales", "#{locale}.yml")
|
27
|
+
end
|
28
|
+
end
|
data/lib/translate.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'ya2yaml'
|
2
|
+
|
3
|
+
module Translate
|
4
|
+
class Engine < Rails::Engine
|
5
|
+
end if defined?(Rails) && Rails::VERSION::MAJOR == 3
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# For configuring Google Translate API key
|
9
|
+
attr_accessor :api_key
|
10
|
+
# For configuring Bing Application id
|
11
|
+
attr_accessor :app_id
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Dir[File.join(File.dirname(__FILE__), "translate", "*.rb")].each do |file|
|
16
|
+
require file
|
17
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
3
|
+
|
4
|
+
describe TranslateController do
|
5
|
+
describe "index" do
|
6
|
+
before(:each) do
|
7
|
+
controller.stub!(:per_page).and_return(1)
|
8
|
+
I18n.backend.stub!(:translations).and_return(i18n_translations)
|
9
|
+
I18n.backend.instance_eval { @initialized = true }
|
10
|
+
keys = mock(:keys)
|
11
|
+
keys.stub!(:i18n_keys).and_return(['vendor.foobar'])
|
12
|
+
Translate::Keys.should_receive(:new).and_return(keys)
|
13
|
+
Translate::Keys.should_receive(:files).and_return(files)
|
14
|
+
I18n.stub!(:available_locales).and_return([:en, :sv])
|
15
|
+
I18n.stub!(:default_locale).and_return(:sv)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "shows sorted paginated keys from the translate from locale and extracted keys by default" do
|
19
|
+
get_page :index
|
20
|
+
assigns(:from_locale).should == :sv
|
21
|
+
assigns(:to_locale).should == :en
|
22
|
+
assigns(:files).should == files
|
23
|
+
assigns(:keys).sort.should == ['articles.new.page_title', 'home.page_title', 'vendor.foobar']
|
24
|
+
assigns(:paginated_keys).should == ['articles.new.page_title']
|
25
|
+
end
|
26
|
+
|
27
|
+
it "can be paginated with the page param" do
|
28
|
+
get_page :index, :page => 2
|
29
|
+
assigns(:files).should == files
|
30
|
+
assigns(:paginated_keys).should == ['home.page_title']
|
31
|
+
end
|
32
|
+
|
33
|
+
it "accepts a key_pattern param with key_type=starts_with" do
|
34
|
+
get_page :index, :key_pattern => 'articles', :key_type => 'starts_with'
|
35
|
+
assigns(:files).should == files
|
36
|
+
assigns(:paginated_keys).should == ['articles.new.page_title']
|
37
|
+
assigns(:total_entries).should == 1
|
38
|
+
end
|
39
|
+
|
40
|
+
it "accepts a key_pattern param with key_type=contains" do
|
41
|
+
get_page :index, :key_pattern => 'page_', :key_type => 'contains'
|
42
|
+
assigns(:files).should == files
|
43
|
+
assigns(:total_entries).should == 2
|
44
|
+
assigns(:paginated_keys).should == ['articles.new.page_title']
|
45
|
+
end
|
46
|
+
|
47
|
+
it "accepts a filter=untranslated param" do
|
48
|
+
get_page :index, :filter => 'untranslated'
|
49
|
+
assigns(:total_entries).should == 2
|
50
|
+
assigns(:paginated_keys).should == ['articles.new.page_title']
|
51
|
+
end
|
52
|
+
|
53
|
+
it "accepts a filter=translated param" do
|
54
|
+
get_page :index, :filter => 'translated'
|
55
|
+
assigns(:total_entries).should == 1
|
56
|
+
assigns(:paginated_keys).should == ['vendor.foobar']
|
57
|
+
end
|
58
|
+
|
59
|
+
it "accepts a filter=changed param" do
|
60
|
+
log = mock(:log)
|
61
|
+
old_translations = {:home => {:page_title => "Skapar ny artikel"}}
|
62
|
+
log.should_receive(:read).and_return(Translate::File.deep_stringify_keys(old_translations))
|
63
|
+
Translate::Log.should_receive(:new).with(:sv, :en, {}).and_return(log)
|
64
|
+
get_page :index, :filter => 'changed'
|
65
|
+
assigns(:total_entries).should == 1
|
66
|
+
assigns(:keys).should == ["home.page_title"]
|
67
|
+
end
|
68
|
+
|
69
|
+
def i18n_translations
|
70
|
+
HashWithIndifferentAccess.new({
|
71
|
+
:en => {
|
72
|
+
:vendor => {
|
73
|
+
:foobar => "Foo Baar"
|
74
|
+
}
|
75
|
+
},
|
76
|
+
:sv => {
|
77
|
+
:articles => {
|
78
|
+
:new => {
|
79
|
+
:page_title => "Skapa ny artikel"
|
80
|
+
}
|
81
|
+
},
|
82
|
+
:home => {
|
83
|
+
:page_title => "Välkommen till I18n"
|
84
|
+
},
|
85
|
+
:vendor => {
|
86
|
+
:foobar => "Fobar"
|
87
|
+
}
|
88
|
+
}
|
89
|
+
})
|
90
|
+
end
|
91
|
+
|
92
|
+
def files
|
93
|
+
HashWithIndifferentAccess.new({
|
94
|
+
:'home.page_title' => ["app/views/home/index.rhtml"],
|
95
|
+
:'general.back' => ["app/views/articles/new.rhtml", "app/views/categories/new.rhtml"],
|
96
|
+
:'articles.new.page_title' => ["app/views/articles/new.rhtml"]
|
97
|
+
})
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "translate" do
|
102
|
+
it "should store translations to I18n backend and then write them to a YAML file" do
|
103
|
+
session[:from_locale] = :sv
|
104
|
+
session[:to_locale] = :en
|
105
|
+
translations = {
|
106
|
+
:articles => {
|
107
|
+
:new => {
|
108
|
+
:title => "New Article"
|
109
|
+
}
|
110
|
+
},
|
111
|
+
:category => "Category"
|
112
|
+
}
|
113
|
+
key_param = {'articles.new.title' => "New Article", "category" => "Category"}
|
114
|
+
I18n.backend.should_receive(:store_translations).with(:en, translations)
|
115
|
+
storage = mock(:storage)
|
116
|
+
storage.should_receive(:write_to_file)
|
117
|
+
Translate::Storage.should_receive(:new).with(:en).and_return(storage)
|
118
|
+
log = mock(:log)
|
119
|
+
log.should_receive(:write_to_file)
|
120
|
+
Translate::Log.should_receive(:new).with(:sv, :en, key_param.keys).and_return(log)
|
121
|
+
post :translate, "key" => key_param
|
122
|
+
response.should be_redirect
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def get_page(*args)
|
127
|
+
get(*args)
|
128
|
+
response.should be_success
|
129
|
+
end
|
130
|
+
end
|
data/spec/file_spec.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
3
|
+
|
4
|
+
describe Translate::File do
|
5
|
+
describe "write" do
|
6
|
+
before(:each) do
|
7
|
+
@file = Translate::File.new(file_path)
|
8
|
+
end
|
9
|
+
|
10
|
+
after(:each) do
|
11
|
+
FileUtils.rm(file_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "writes all I18n messages for a locale to YAML file" do
|
15
|
+
@file.write(translations)
|
16
|
+
@file.read.should == Translate::File.deep_stringify_keys(translations)
|
17
|
+
end
|
18
|
+
|
19
|
+
def translations
|
20
|
+
{
|
21
|
+
:en => {
|
22
|
+
:article => {
|
23
|
+
:title => "One Article"
|
24
|
+
},
|
25
|
+
:category => "Category"
|
26
|
+
}
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "deep_stringify_keys" do
|
32
|
+
it "should convert all keys in a hash to strings" do
|
33
|
+
Translate::File.deep_stringify_keys({
|
34
|
+
:en => {
|
35
|
+
:article => {
|
36
|
+
:title => "One Article"
|
37
|
+
},
|
38
|
+
:category => "Category"
|
39
|
+
}
|
40
|
+
}).should == {
|
41
|
+
"en" => {
|
42
|
+
"article" => {
|
43
|
+
"title" => "One Article"
|
44
|
+
},
|
45
|
+
"category" => "Category"
|
46
|
+
}
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def file_path
|
52
|
+
File.join(File.dirname(__FILE__), "files", "en.yml")
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class Article < ActiveRecord::Base
|
2
|
+
def validate
|
3
|
+
# t('li')
|
4
|
+
errors.add_to_base([t(:'article.key1') + "#{t('article.key2')}"])
|
5
|
+
I18n.t 'article.key3'
|
6
|
+
I18n.t 'article.key3'
|
7
|
+
I18n.t :'article.key4'
|
8
|
+
I18n.translate :'article.key5'
|
9
|
+
'bla bla t' + "blubba bla" + ' foobar'
|
10
|
+
'bla bla t ' + "blubba bla" + ' foobar'
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= t(:'category_erb.key1') %>
|
@@ -0,0 +1 @@
|
|
1
|
+
t(:'category_html.key1')
|