rails-i18nterface 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +22 -0
  3. data/README.md +109 -0
  4. data/Rakefile +23 -0
  5. data/app/assets/javascripts/rails_i18nterface/application.js +8 -0
  6. data/app/assets/javascripts/rails_i18nterface/base.js +62 -0
  7. data/app/assets/javascripts/rails_i18nterface/ender.js +3260 -0
  8. data/app/assets/javascripts/rails_i18nterface/ender.min.js +45 -0
  9. data/app/assets/stylesheets/rails_i18nterface/application.css +450 -0
  10. data/app/controllers/rails_i18nterface/application_controller.rb +4 -0
  11. data/app/controllers/rails_i18nterface/translate_controller.rb +255 -0
  12. data/app/helpers/rails_i18nterface/application_helper.rb +4 -0
  13. data/app/helpers/rails_i18nterface/translate_helper.rb +74 -0
  14. data/app/models/translation.rb +4 -0
  15. data/app/views/layouts/rails_i18nterface/translate.html.erb +13 -0
  16. data/app/views/rails_i18nterface/translate/_namespaces.html.erb +1 -0
  17. data/app/views/rails_i18nterface/translate/_pagination.html.erb +18 -0
  18. data/app/views/rails_i18nterface/translate/index.html.erb +132 -0
  19. data/config/routes.rb +7 -0
  20. data/db/migrate/20110921112044_create_translations.rb +18 -0
  21. data/lib/rails-i18nterface.rb +8 -0
  22. data/lib/rails-i18nterface/engine.rb +7 -0
  23. data/lib/rails-i18nterface/file.rb +35 -0
  24. data/lib/rails-i18nterface/keys.rb +192 -0
  25. data/lib/rails-i18nterface/log.rb +35 -0
  26. data/lib/rails-i18nterface/storage.rb +29 -0
  27. data/lib/rails-i18nterface/version.rb +3 -0
  28. data/lib/tasks/rails-i18nterface.rake +4 -0
  29. data/spec/controllers/translate_controller_spec.rb +135 -0
  30. data/spec/internal/app/assets/javascripts/application.js +2 -0
  31. data/spec/internal/app/assets/stylesheets/application.css +3 -0
  32. data/spec/internal/app/controllers/application_controller.rb +4 -0
  33. data/spec/internal/app/models/article.rb +11 -0
  34. data/spec/internal/app/views/application/index.html.erb +5 -0
  35. data/spec/internal/app/views/categories/category.erb +1 -0
  36. data/spec/internal/app/views/categories/category.html +1 -0
  37. data/spec/internal/app/views/categories/category.html.erb +1 -0
  38. data/spec/internal/app/views/categories/category.rhtml +5 -0
  39. data/spec/internal/config/database.yml +3 -0
  40. data/spec/internal/config/routes.rb +4 -0
  41. data/spec/internal/db/combustion_test.sqlite +0 -0
  42. data/spec/internal/db/schema.rb +3 -0
  43. data/spec/internal/log/test.log +521 -0
  44. data/spec/internal/public/favicon.ico +0 -0
  45. data/spec/lib/file_spec.rb +53 -0
  46. data/spec/lib/keys_spec.rb +179 -0
  47. data/spec/lib/log_spec.rb +46 -0
  48. data/spec/lib/storage_spec.rb +33 -0
  49. data/spec/requests/search_spec.rb +62 -0
  50. data/spec/spec_helper.rb +37 -0
  51. metadata +200 -0
@@ -0,0 +1,4 @@
1
+ module RailsI18nterface
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,255 @@
1
+ module RailsI18nterface
2
+ class TranslateController < RailsI18nterface::ApplicationController
3
+
4
+ before_filter :init_translations
5
+ before_filter :set_locale
6
+
7
+ def index
8
+ @dbvalues = {}
9
+ initialize_keys
10
+ load_db_translations
11
+ @deep_keys = RailsI18nterface::Keys.to_deep_hash(@keys)
12
+ filter_by_key_pattern
13
+ filter_by_text_pattern
14
+ filter_by_translated_or_changed
15
+ sort_keys
16
+ paginate_keys
17
+ @total_entries = @keys.size
18
+ end
19
+
20
+ def load_db_translations
21
+ @versions = {}
22
+ @dbvalues = {@to_locale => {}}
23
+ (Translation.where(:locale => @to_locale) || []).each { |translation|
24
+ @versions[translation.key] = translation.updated_at.to_i
25
+ @dbvalues[@to_locale][translation.key] = translation.value
26
+ @keys << translation.key
27
+ }
28
+ @keys.uniq!
29
+ end
30
+
31
+ def export
32
+ locale = params[:locale].to_sym
33
+ keys = {locale => I18n.backend.send(:translations)[locale] || {}}
34
+ Translation.where(:locale => @to_locale).each { |translation|
35
+ next if translation.value == ''
36
+ next if !translation.value
37
+ set_nested(keys[locale], translation.key.split("."), translation.value)
38
+ }
39
+ remove_blanks keys
40
+ yaml = keys_to_yaml(deep_stringify_keys(keys))
41
+ response.headers['Content-Disposition'] = "attachment; filename=#{locale}.yml"
42
+ render :text => yaml
43
+ end
44
+
45
+ def remove_blanks hash
46
+ hash.each { |k, v|
47
+ if !v || v == ''
48
+ hash.delete k
49
+ end
50
+ if v.is_a? Hash
51
+ remove_blanks v
52
+ if v == {}
53
+ hash.delete k
54
+ end
55
+ end
56
+ }
57
+ end
58
+
59
+ def set_nested(hash, key, value)
60
+ if key.length == 1
61
+ hash[key[0]] = value
62
+ else
63
+ k = key.shift
64
+ set_nested(hash[k] ||= {}, key, value)
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ # Stringifying keys for prettier YAML
71
+ def deep_stringify_keys(hash)
72
+ hash.inject({}) { |result, (key, value)|
73
+ value = deep_stringify_keys(value) if value.is_a? Hash
74
+ result[(key.to_s rescue key) || key] = value
75
+ result
76
+ }
77
+ end
78
+
79
+ def keys_to_yaml(keys)
80
+ # Using ya2yaml, if available, for UTF8 support
81
+ if keys.respond_to?(:ya2yaml)
82
+ keys.ya2yaml(:escape_as_utf8 => true)
83
+ else
84
+ keys.to_yaml
85
+ end
86
+ end
87
+
88
+ public
89
+
90
+ def update
91
+ params[:key].each { |k, v|
92
+ t = Translation.where(:key => k, :locale => @to_locale).first
93
+ unless t
94
+ t = Translation.new
95
+ t.key = k
96
+ t.locale = @to_locale
97
+ end
98
+ next if t.value == v
99
+ if params[:version][k].to_i == t.updated_at.to_i
100
+ t.value = v
101
+ end
102
+ t.save
103
+ }
104
+ if I18n.backend.respond_to? :store_translations
105
+ I18n.backend.store_translations(@to_locale, RailsI18nterface::Keys.to_deep_hash(params[:key]))
106
+ end
107
+ RailsI18nterface::Storage.new(@to_locale).write_to_file
108
+ RailsI18nterface::Log.new(@from_locale, @to_locale, params[:key].keys).write_to_file
109
+ force_init_translations # Force reload from YAML file
110
+ flash[:notice] = "Translations stored"
111
+ redirect_to root_path(params.slice(:filter, :sort_by, :key_type, :key_pattern, :text_type, :text_pattern))
112
+ end
113
+
114
+ def reload
115
+ RailsI18nterface::Keys.files = nil
116
+ redirect_to :action => 'index'
117
+ end
118
+
119
+ private
120
+
121
+ def initialize_keys
122
+ @files = RailsI18nterface::Keys.files
123
+ @keys = (@files.keys.map(&:to_s) + RailsI18nterface::Keys.new.i18n_keys(@from_locale)).uniq
124
+ @keys.reject! do |key|
125
+ from_text = lookup(@from_locale, key)
126
+ # When translating from one language to another, make sure there is a text to translate from.
127
+ # Always exclude non string translation objects as we don't support editing them in the UI.
128
+ (@from_locale != @to_locale && !from_text.present?) || (from_text.present? && !from_text.is_a?(String))
129
+ end
130
+ end
131
+
132
+ def lookup(locale, key)
133
+ (@dbvalues[locale] && @dbvalues[locale][key]) || I18n.backend.send(:lookup, locale, key)
134
+ end
135
+ helper_method :lookup
136
+
137
+ def filter_by_translated_or_changed
138
+ params[:filter] ||= 'all'
139
+ return if params[:filter] == 'all'
140
+ @keys.reject! do |key|
141
+ case params[:filter]
142
+ when 'untranslated'
143
+ lookup(@to_locale, key).present?
144
+ when 'translated'
145
+ lookup(@to_locale, key).blank?
146
+ when 'changed'
147
+ old_from_text(key).blank? || lookup(@from_locale, key) == old_from_text(key)
148
+ else
149
+ raise "Unknown filter '#{params[:filter]}'"
150
+ end
151
+ end
152
+ end
153
+
154
+ def filter_by_key_pattern
155
+ return if params[:key_pattern].blank?
156
+ @keys.reject! do |key|
157
+ case params[:key_type]
158
+ when "starts_with"
159
+ if params[:key_pattern] == '.'
160
+ key.match(/\./)
161
+ else
162
+ !key.starts_with?(params[:key_pattern])
163
+ end
164
+ when "contains"
165
+ key.index(params[:key_pattern]).nil?
166
+ else
167
+ raise "Unknown key_type '#{params[:key_type]}'"
168
+ end
169
+ end
170
+ end
171
+
172
+ def filter_by_text_pattern
173
+ return if params[:text_pattern].blank?
174
+ @keys.reject! do |key|
175
+ case params[:text_type]
176
+ when 'contains'
177
+ !lookup(@from_locale, key).present? || !lookup(@from_locale, key).to_s.downcase.index(params[:text_pattern].downcase)
178
+ when 'equals'
179
+ !lookup(@from_locale, key).present? || lookup(@from_locale, key).to_s.downcase != params[:text_pattern].downcase
180
+ else
181
+ raise "Unknown text_type '#{params[:text_type]}'"
182
+ end
183
+ end
184
+ end
185
+
186
+ def sort_keys
187
+ params[:sort_by] ||= "key"
188
+ case params[:sort_by]
189
+ when "key"
190
+ @keys.sort!
191
+ when "text"
192
+ @keys.sort! do |key1, key2|
193
+ if lookup(@from_locale, key1).present? && lookup(@from_locale, key2).present?
194
+ lookup(@from_locale, key1).to_s.downcase <=> lookup(@from_locale, key2).to_s.downcase
195
+ elsif lookup(@from_locale, key1).present?
196
+ -1
197
+ else
198
+ 1
199
+ end
200
+ end
201
+ else
202
+ raise "Unknown sort_by '#{params[:sort_by]}'"
203
+ end
204
+ end
205
+
206
+ def paginate_keys
207
+ params[:page] ||= 1
208
+ @paginated_keys = @keys[offset, per_page]
209
+ end
210
+
211
+ def offset
212
+ (params[:page].to_i - 1) * per_page
213
+ end
214
+
215
+ def per_page
216
+ 50
217
+ end
218
+ helper_method :per_page
219
+
220
+ def init_translations
221
+ I18n.backend.send(:init_translations) unless I18n.backend.initialized?
222
+ end
223
+
224
+ def force_init_translations
225
+ I18n.backend.send(:init_translations) rescue false
226
+ end
227
+
228
+ def default_locale
229
+ I18n.default_locale
230
+ end
231
+
232
+ def set_locale
233
+ session[:from_locale] ||= default_locale
234
+ session[:to_locale] ||= :en
235
+ session[:from_locale] = params[:from_locale] if params[:from_locale].present?
236
+ session[:to_locale] = params[:to_locale] if params[:to_locale].present?
237
+ @from_locale = session[:from_locale].to_sym
238
+ @to_locale = session[:to_locale].to_sym
239
+ end
240
+
241
+ def old_from_text(key)
242
+ return @old_from_text[key] if @old_from_text && @old_from_text[key]
243
+ @old_from_text = {}
244
+ text = key.split(".").inject(log_hash) do |hash, k|
245
+ hash ? hash[k] : nil
246
+ end
247
+ @old_from_text[key] = text
248
+ end
249
+ helper_method :old_from_text
250
+
251
+ def log_hash
252
+ @log_hash ||= RailsI18nterface::Log.new(@from_locale, @to_locale, {}).read
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,4 @@
1
+ module RailsI18nterface
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,74 @@
1
+ module RailsI18nterface
2
+ module TranslateHelper
3
+
4
+ def simple_filter(labels, param_name = 'filter', selected_value = nil)
5
+ selected_value ||= params[param_name]
6
+ filter = []
7
+ labels.each do |item|
8
+ if item.is_a?(Array)
9
+ type, label = item
10
+ else
11
+ type = label = item
12
+ end
13
+ if type.to_s == selected_value.to_s
14
+ filter << "<i>#{label}</i>"
15
+ else
16
+ link_params = params.merge({param_name.to_s => type})
17
+ link_params.merge!({"page" => nil}) if param_name.to_s != "page"
18
+ filter << link_to(label, link_params)
19
+ end
20
+ end
21
+ filter.join(" | ")
22
+ end
23
+
24
+ def n_lines(text, line_size)
25
+ n_lines = 1
26
+ if text.present?
27
+ n_lines = text.split("\n").size
28
+ if n_lines == 1 && text.length > line_size
29
+ n_lines = text.length / line_size + 1
30
+ end
31
+ end
32
+ n_lines
33
+ end
34
+
35
+ def build_namespace(h)
36
+ out = '<ul>'
37
+ dirs = {}
38
+ root = []
39
+ h.each do |k,v|
40
+ if v.is_a? Hash
41
+ dirs[k] = v
42
+ else
43
+ root << k
44
+ end
45
+ end
46
+ out << "<li class=\"dir\"><span class=\"display\" data-id=\".\"></span>ROOT"
47
+ out << " <span class=\"num\">(#{root.length})</span>"
48
+ out << '<ul>'
49
+ root.each do |key|
50
+ out << "<li class=\"item\" data-id=\"#{key.to_s}\">#{key}</li>"
51
+ end
52
+ out << '</ul>'
53
+ out << '</ul>'
54
+ out << list_namespace('',dirs)
55
+ end
56
+
57
+ def list_namespace(k,h)
58
+ out = '<ul>'
59
+ k != '' && k += '.'
60
+ h.each do |key,val|
61
+ if val.is_a? Hash
62
+ out << "<li class=\"dir\"><span class=\"display\" data-id=\"#{k+key.to_s}\"></span>#{key}"
63
+ out << " <span class=\"num\">(#{val.length})</span>"
64
+ out << list_namespace(k+key.to_s,val)
65
+ else
66
+ out << "<li class=\"item\" data-id=\"#{k+key.to_s}\">#{key}"
67
+ end
68
+ out << '</li>'
69
+ end
70
+ out << '</ul>'
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,4 @@
1
+ class Translation < ActiveRecord::Base
2
+ end
3
+
4
+
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
5
+ <title><%= @page_title %></title>
6
+ <%= stylesheet_link_tag 'rails_i18nterface/application' %>
7
+ <%= javascript_include_tag 'rails_i18nterface/application' %>
8
+ </head>
9
+ <body>
10
+ <div id="container">
11
+ <%= yield %>
12
+ </div>
13
+ </html>
@@ -0,0 +1 @@
1
+ <h2>Namespaces</h2>
@@ -0,0 +1,18 @@
1
+ <%
2
+ n_pages = total_entries/per_page + (total_entries % per_page > 0 ? 1 : 0)
3
+ current_page = (params[:page] || 1).to_i
4
+ %>
5
+
6
+ <% if n_pages > 1 %>
7
+ <div class="clearfix">
8
+ <ul class="paging">
9
+ <% (1..n_pages).each do |page_number| %>
10
+ <% if current_page == page_number %>
11
+ <li class="selected"><%= link_to(page_number, params.merge(:page => page_number), :title => "Page #{page_number}" ) %></li>
12
+ <% else %>
13
+ <li><%= link_to(page_number, params.merge(:page => page_number), :title => "Page #{page_number}") %></li>
14
+ <% end %>
15
+ <% end %>
16
+ </ul>
17
+ </div>
18
+ <% end %>
@@ -0,0 +1,132 @@
1
+ <%
2
+ @page_title = "Translate"
3
+ show_filters = ["all", "untranslated", "translated"]
4
+ show_filters << "changed" if @from_locale != @to_locale
5
+ %>
6
+ <%= form_tag(params, :method => :get, :name => 'filter_form') do %>
7
+ <%= hidden_field_tag(:filter, params[:filter]) %>
8
+ <%= hidden_field_tag(:sort_by, params[:sort_by]) %>
9
+ <div id="top">
10
+ <% if @page_title %>
11
+ <h1>
12
+ <div class="right">
13
+ <%= select_tag(:from_locale, options_for_select(I18n.available_locales, @from_locale.to_sym)) %> <span>to</span>
14
+ <%= select_tag(:to_locale, options_for_select(I18n.available_locales, @to_locale.to_sym)) %>
15
+ <%= submit_tag "Display" %>
16
+ </div>
17
+ <%= link_to '.. /', '../' %>
18
+ <%= link_to @page_title, root_path %></a>
19
+ <div class="center">
20
+ <label>Show:</label> <%= simple_filter(show_filters).html_safe %>
21
+ Found <strong><%= @total_entries %></strong> messages
22
+ <%= link_to "Reload messages", translate_reload_path %>
23
+ </div>
24
+
25
+ </h1>
26
+ <% end %>
27
+ <% flash.each do |key, msg| %>
28
+ <div id="<%= key %>"><%= msg %></div>
29
+ <% end %>
30
+ </div>
31
+ <div id="searchbox">
32
+
33
+ <%= select_tag(:key_type, options_for_select([["Key contains", 'contains'], ["Key starts with", 'starts_with']], params[:key_type])) %>
34
+ <%= text_field_tag(:key_pattern, params[:key_pattern], :size => 50, :id => "key_pattern_value", :class => "text-default") %>
35
+
36
+ <%= select_tag(:text_type, options_for_select([['Text contains','contains'], ['Text equals','equals']], params[:text_type])) %>
37
+ <%= text_field_tag(:text_pattern, params[:text_pattern], :size => 50, :id => "text_pattern_value", :class => "text-default") %>
38
+
39
+ <%= link_to "clear", params.merge({:text_pattern => nil, :key_pattern => nil}), class: 'btn' %>
40
+ <%= submit_tag "Search" %>
41
+ <%= link_to "Export language file", translate_export_path(:locale => @to_locale), class: 'btn sep' %>
42
+ </div>
43
+ <% end %>
44
+
45
+ <div id="namespaces">
46
+ <%= build_namespace(@deep_keys).html_safe %>
47
+ </div>
48
+
49
+ <div id="inside">
50
+ <div class="paging">
51
+ <%= render :partial => 'pagination', :locals => {:total_entries => @total_entries, :per_page => per_page} %>
52
+ </div>
53
+
54
+ <% if @total_entries > 0 %>
55
+ <%= form_tag(translate_path, method: :put) do %>
56
+ <div>
57
+ <%= hidden_field_tag(:filter, params[:filter], :id => "hid_filter") %>
58
+ <%= hidden_field_tag(:sort_by, params[:sort_by], :id => "hid_sort_by") %>
59
+ <%= hidden_field_tag(:key_type, params[:key_type], :id => "hid_key_type") %>
60
+ <%= hidden_field_tag(:key_pattern, params[:key_pattern], :id => "hid_key_pattern") %>
61
+ <%= hidden_field_tag(:text_type, params[:text_type], :id => "hid_text_type") %>
62
+ <%= hidden_field_tag(:text_pattern, params[:text_pattern], :id => "hid_text_pattern") %>
63
+ </div>
64
+ <div class="translations">
65
+ <p class="translate">
66
+ Translations from <%= @from_locale %> to <%= @to_locale %>
67
+ <label>Sort by:</label> <%= simple_filter(["key", "text"], 'sort_by').html_safe %>
68
+ <%= submit_tag "Save Translations", style: 'float:right;' %>
69
+ </p>
70
+ <% @paginated_keys.each do |key|
71
+ from_text = lookup(@from_locale, key)
72
+ to_text = lookup(@to_locale, key)
73
+ line_size = 100
74
+ n_lines = n_lines(from_text, line_size)
75
+ field_name = "key[#{key}]"
76
+ tid = key.split('.').join("_")
77
+
78
+ %>
79
+ <div class="translation">
80
+ <p class="edit-form">
81
+ <%= hidden_field_tag("version[#{key}]", @versions[key] || '0') %>
82
+ <% if n_lines > 1 %>
83
+ <span class="right">
84
+ <span class="key"><%=h key %></span>
85
+ <% if @files[key] %>
86
+ <span class="file"><%= @files[key].join("</span><span>") %></span>
87
+ <% end %>
88
+ </span>
89
+ </span>
90
+ <div class="long-translation">
91
+ <div class="translation-text">
92
+ <pre id="<%= tid %>_original"><%= from_text %></pre>
93
+ <div class="clear"></div>
94
+ </div>
95
+ <div class="translation-textarea">
96
+ <%= text_area_tag(field_name, to_text, :rows => n_lines+1, :id => tid) %>
97
+ <div class="clear"></div>
98
+ </div>
99
+ <div class="clear"></div>
100
+ </div>
101
+ <% else %>
102
+ <span class="translation-text">
103
+ <code class="keytext" id="<%= tid %>_original"><%= from_text %></code>
104
+ </span>
105
+ <span class="right">
106
+ <span class="key" ><%=h key %></span>
107
+ <% if @files[key] %>
108
+ <span class="file"><%= @files[key].join("</span><span>") %></span>
109
+ <% end %>
110
+ </span>
111
+ </span>
112
+
113
+ <%= text_field_tag(field_name, to_text, :size => line_size, :id => tid) %>
114
+ <% end %>
115
+ </p>
116
+ <p>
117
+ </p>
118
+ </div>
119
+ <% end %>
120
+ <p class="translate">
121
+ Translations from <%= @from_locale %> to <%= @to_locale %>
122
+ <%= submit_tag "Save Translations", style: 'float:right' %>
123
+ </p>
124
+ </div>
125
+ <% end %>
126
+ <% end %>
127
+
128
+ <div class="paging clear">
129
+ <%= render :partial => 'pagination', :locals => {:total_entries => @total_entries, :per_page => per_page} %>
130
+ </div>
131
+
132
+ </div>