translate-rails3 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,24 @@
1
+ <%
2
+ # Expects locals:
3
+ #
4
+ # total_entries
5
+ # per_page
6
+
7
+ n_pages = total_entries/per_page + (total_entries % per_page > 0 ? 1 : 0)
8
+ current_page = (params[:page] || 1).to_i
9
+ %>
10
+
11
+ <% if n_pages > 1 %>
12
+ <h2>Pages:</h2>
13
+ <div class="clearfix">
14
+ <ul class="paging">
15
+ <% (1..n_pages).each do |page_number| %>
16
+ <% if current_page == page_number %>
17
+ <li class="selected"><%= link_to(page_number, params.merge(:page => page_number), :title => "Page #{page_number}" ) %></li>
18
+ <% else %>
19
+ <li><%= link_to(page_number, params.merge(:page => page_number), :title => "Page #{page_number}") %></li>
20
+ <% end %>
21
+ <% end %>
22
+ </ul>
23
+ </div>
24
+ <% end %>
@@ -0,0 +1,114 @@
1
+ <%
2
+ @page_title = "Translate"
3
+ show_filters = ["all", "untranslated", "translated"]
4
+ show_filters << "changed" if @from_locale != @to_locale
5
+ %>
6
+
7
+ <fieldset>
8
+ <legend>Search filter</legend>
9
+ <div id="show-sort">
10
+ <p>
11
+ <label>Show:</label> <%= raw simple_filter(show_filters) %>
12
+ </p>
13
+ <p>
14
+ <label>Sort by:</label> <%= raw simple_filter(["key", "text"], 'sort_by') %>
15
+ </p>
16
+ </div>
17
+ <%= form_tag(params, :method => :get) do %>
18
+ <div id="languages">
19
+ <p>
20
+ <%= hidden_field_tag(:filter, params[:filter]) %>
21
+ <%= hidden_field_tag(:sort_by, params[:sort_by]) %>
22
+ <label>Translate from</label>
23
+ <%= select_tag(:from_locale, options_for_select(I18n.available_locales, @from_locale.to_sym)) %> <span>to</span>
24
+ <%= select_tag(:to_locale, options_for_select(I18n.available_locales, @to_locale.to_sym)) %>
25
+ <%= submit_tag "Display" %>
26
+ </p>
27
+ </div>
28
+ <div id="filter-pattern">
29
+ <p>
30
+ <label for="key_pattern_value">Key</label>
31
+ <%= select_tag(:key_type, options_for_select([["contains", 'contains'], ["starts with", 'starts_with']], params[:key_type])) %>
32
+ <%= text_field_tag(:key_pattern, params[:key_pattern], :size => 50, :id => "key_pattern_value", :class => "text-default") %>
33
+ </p>
34
+ <p>
35
+ <label for="text_pattern_value">Text</label>
36
+ <%= select_tag(:text_type, options_for_select(['contains', 'equals'], params[:text_type])) %>
37
+ <%= text_field_tag(:text_pattern, params[:text_pattern], :size => 50, :id => "text_pattern_value", :class => "text-default") %>
38
+ </p>
39
+ <p>
40
+ <%= submit_tag "Search" %>
41
+ <%= link_to "clear", params.merge({:text_pattern => nil, :key_pattern => nil}) %>
42
+ </p>
43
+ </div>
44
+ <% end %>
45
+ <p class="hits">
46
+ Found <strong><%= @total_entries %></strong> messages
47
+ </p>
48
+ <p>
49
+ <%= link_to "Reload messages", translate_reload_path %>
50
+ </p>
51
+ </fieldset>
52
+
53
+
54
+ <div class="paging">
55
+ <%= render :partial => 'pagination', :locals => {:total_entries => @total_entries, :per_page => per_page} %>
56
+ </div>
57
+
58
+ <% if @total_entries > 0 %>
59
+ <%= form_tag(translate_path) do %>
60
+ <div>
61
+ <%= hidden_field_tag(:filter, params[:filter], :id => "hid_filter") %>
62
+ <%= hidden_field_tag(:sort_by, params[:sort_by], :id => "hid_sort_by") %>
63
+ <%= hidden_field_tag(:key_type, params[:key_type], :id => "hid_key_type") %>
64
+ <%= hidden_field_tag(:key_pattern, params[:key_pattern], :id => "hid_key_pattern") %>
65
+ <%= hidden_field_tag(:text_type, params[:text_type], :id => "hid_text_type") %>
66
+ <%= hidden_field_tag(:text_pattern, params[:text_pattern], :id => "hid_text_pattern") %>
67
+ </div>
68
+ <div class="translations">
69
+ <h2>Translations from <%= @from_locale %> to <%= @to_locale %></h2>
70
+ <p class="translate">
71
+ <%= submit_tag "Save Translations" %>
72
+ </p>
73
+ <% @paginated_keys.each do |key|
74
+ from_text = lookup(@from_locale, key)
75
+ to_text = lookup(@to_locale, key)
76
+ line_size = 100
77
+ n_lines = n_lines(from_text, line_size)
78
+ field_name = "key[#{key}]"
79
+ %>
80
+ <div class="translation">
81
+ <% if from_text.present? %>
82
+ <p class="translation-text">
83
+ <%= simple_format(h(from_text)) %>
84
+ </p>
85
+ <% end %>
86
+ <p class="edit-form">
87
+ <% if n_lines > 1 %>
88
+ <%= text_area_tag(field_name, to_text, :size => "#{line_size}x#{n_lines}", :id => key) %>
89
+ <% else %>
90
+ <%= text_field_tag(field_name, to_text, :size => line_size, :id => key) %>
91
+ <% end %>
92
+ </p>
93
+ <p>
94
+ <em>
95
+ <%= link_to_function 'Auto Translate', "getGoogleTranslation('#{key}', \"#{escape_javascript(from_text)}\", '#{@from_locale}', '#{@to_locale}')", :style => 'padding: 0; margin: 0;' %>
96
+ <br/>
97
+ <strong>Key:</strong><%=h key %><br/>
98
+ <% if @files[key] %>
99
+ <strong>File:</strong><%= @files[key].join("<br/>") %>
100
+ <% end %>
101
+ </em>
102
+ </p>
103
+ </div>
104
+ <% end %>
105
+ <p class="translate">
106
+ <%= submit_tag "Save Translations" %>
107
+ </p>
108
+ </div>
109
+ <% end %>
110
+ <% end %>
111
+
112
+ <div class="paging">
113
+ <%= render :partial => 'pagination', :locals => {:total_entries => @total_entries, :per_page => per_page} %>
114
+ </div>
@@ -0,0 +1,5 @@
1
+ Rails.application.routes.draw do
2
+ match 'translate' => 'translate#index', :as => :translate_list
3
+ match 'translate/translate' => 'translate#translate', :as => :translate
4
+ match 'translate/reload' => 'translate#reload', :as => :translate_reload
5
+ end if Rails.env.development?
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'translate'
@@ -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,8 @@
1
+ module Translate
2
+ class Engine < Rails::Engine
3
+ end if defined?(Rails) && Rails::VERSION::MAJOR == 3
4
+ end
5
+
6
+ Dir[File.join(File.dirname(__FILE__), "translate", "*.rb")].each do |file|
7
+ require file
8
+ 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,152 @@
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
+ private
132
+
133
+ def extract_files
134
+ files_to_scan.inject(HashWithIndifferentAccess.new) do |files, file|
135
+ IO.read(file).scan(i18n_lookup_pattern).flatten.map(&:to_sym).each do |key|
136
+ files[key] ||= []
137
+ path = Pathname.new(File.expand_path(file)).relative_path_from(Pathname.new(Rails.root)).to_s
138
+ files[key] << path if !files[key].include?(path)
139
+ end
140
+ files
141
+ end
142
+ end
143
+
144
+ def i18n_lookup_pattern
145
+ /\b(?:I18n\.t|I18n\.translate|t)(?:\s|\():?'([a-z0-9_]+.[a-z0-9_.]+)'\)?/
146
+ end
147
+
148
+ def files_to_scan
149
+ Dir.glob(File.join(Translate::Storage.root_dir, "{app,config,lib}", "**","*.{rb,erb,rhtml}")) +
150
+ Dir.glob(File.join(Translate::Storage.root_dir, "public", "javascripts", "**","*.js"))
151
+ end
152
+ end