translate-rails3 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.
@@ -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