couch_i18n 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. data/README.rdoc +15 -31
  2. data/app/assets/images/couch_i18n/alert.png +0 -0
  3. data/app/assets/images/couch_i18n/notice.png +0 -0
  4. data/app/assets/javascripts/{application.js → couch_i18n/application.js} +2 -4
  5. data/app/assets/javascripts/couch_i18n/structure.js +2 -0
  6. data/app/assets/stylesheets/couch_i18n/alerts.css.scss +32 -0
  7. data/app/assets/stylesheets/{application.css → couch_i18n/application.css} +0 -0
  8. data/app/assets/stylesheets/couch_i18n/structure.css.scss +69 -0
  9. data/app/controllers/couch_i18n/application_controller.rb +1 -0
  10. data/app/controllers/couch_i18n/translations_controller.rb +143 -0
  11. data/app/helpers/couch_i18n/application_helper.rb +33 -0
  12. data/app/models/couch_i18n/translation.rb +51 -0
  13. data/app/views/couch_i18n/application/_alerts.html.haml +6 -0
  14. data/app/views/couch_i18n/application/_error_messages.html.haml +6 -0
  15. data/app/views/couch_i18n/translations/_form.html.haml +13 -0
  16. data/app/views/couch_i18n/translations/edit.html.haml +4 -0
  17. data/app/views/couch_i18n/translations/index.html.haml +54 -0
  18. data/app/views/couch_i18n/{stores → translations}/new.html.haml +0 -0
  19. data/app/views/layouts/couch_i18n/application.html.haml +5 -1
  20. data/config/routes.rb +10 -0
  21. data/lib/couch_i18n/backend.rb +3 -3
  22. data/lib/couch_i18n/engine.rb +1 -2
  23. data/lib/couch_i18n/store.rb +13 -52
  24. data/lib/couch_i18n.rb +12 -8
  25. metadata +18 -9
  26. data/app/controllers/couch_i18n/stores_controller.rb +0 -127
  27. data/app/views/couch_i18n/stores/_form.html.haml +0 -10
  28. data/app/views/couch_i18n/stores/edit.html.haml +0 -4
  29. data/app/views/couch_i18n/stores/index.html.haml +0 -46
data/README.rdoc CHANGED
@@ -1,5 +1,7 @@
1
1
  = CouchI18n
2
2
 
3
+ <b>Note this is a Rails >= 3.1 engine</b>
4
+
3
5
  This projects is created to make translations editable. It is created using
4
6
  the simply_stored gem. To use the web frontend please read the README section
5
7
  on this carefully. Important to know is that this system sits *on top* of the
@@ -15,30 +17,7 @@ Your Gemfile should look like:
15
17
  gem 'couch_i18n'
16
18
  gem 'kaminari'
17
19
  And in your config routes put:
18
- namespace :couch_i18n do
19
- root :to => "stores#index"
20
- resources :stores do
21
- collection do
22
- post :export
23
- post :import
24
- delete :destroy_offset
25
- end
26
- end
27
- end
28
- Or to put is at a different location:
29
- namespace :translations, :module => :couch_i18n, :as => 'couch_i18n' do
30
- root :to => "stores#index"
31
- resources :stores do
32
- collection do
33
- post :export
34
- post :import
35
- delete :destroy_offset
36
- end
37
- end
38
- end
39
- The intention is to make this a mountable application, but this will not happen
40
- before Rails 3.1 is officially out. But beware of using this if your application
41
- will not be Rails 3.1 compatible.
20
+ mount CouchI18n::Engine => '/couch_i18n'
42
21
 
43
22
  == Security!!!
44
23
  By default there is no security activated for editing the translation. This is
@@ -81,10 +60,10 @@ the following yamlL
81
60
  activemodel:
82
61
  models:
83
62
  couch_i18n:
84
- store: Translation
63
+ translation: Translation
85
64
  attributes:
86
65
  couch_i18n:
87
- store:
66
+ translation:
88
67
  key: Key
89
68
  value: Value
90
69
  action:
@@ -94,16 +73,18 @@ the following yamlL
94
73
  successful: %{model} successfully updated
95
74
  destroy:
96
75
  successful: %{model} successfully removed
97
- chouch_i18n:
98
- store:
76
+ couch_i18n:
77
+ translation:
99
78
  index title: Translations
100
79
  none found: No translations present
101
80
  new title: New translation
102
81
  show title: Translation
103
82
  edit title: Edit translation
104
83
  go to offset: Go to
105
- got to zero offset: x
106
- export: Export
84
+ go to zero offset: x
85
+ export:
86
+ execute: Export
87
+ untranslated: Untranslated
107
88
  import: Import
108
89
  offset deleted: "%{count} translations with offset %{offset} are deleted"
109
90
  no proper import extension: Files with extension%{extension} cannot be imported
@@ -159,13 +140,16 @@ It should <tt>yield</tt> the following parts:
159
140
  !!!
160
141
  %html
161
142
  %head
162
- %title= defined?(site_title) ? site_title : I18n.t('couch_i18n.store.site_title')
143
+ %title= defined?(site_title) ? site_title : I18n.t('couch_i18n.translation.site_title')
163
144
  = csrf_meta_tag
145
+ = stylesheet_link_tag 'couch_i18n/application'
146
+ = javascript_include_tag 'couch_i18n/application'
164
147
  = yield :head
165
148
  %body
166
149
  #page-wrapper
167
150
  #page-content
168
151
  %h1= yield :title
152
+ = render 'alerts'
169
153
  = yield
170
154
  #page-links= yield :page_links
171
155
  == TODO
@@ -4,8 +4,6 @@
4
4
  // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
5
5
  // the compiled file.
6
6
  //
7
- <% unless options[:skip_javascript] -%>
8
- //= require <%= options[:javascript] %>
9
- //= require <%= options[:javascript] %>_ujs
10
- <% end -%>
7
+ //= require jquery
8
+ //= require jquery_ujs
11
9
  //= require_tree .
@@ -0,0 +1,2 @@
1
+ $(function(){
2
+ });
@@ -0,0 +1,32 @@
1
+ $green: #729A37;
2
+ $red: #D65819;
3
+ .alert-message, #error_explanation{
4
+ margin: 10px 40px;
5
+ border-width: 2px;
6
+ border-style: solid;
7
+ background-repeat: no-repeat;
8
+ background-position: left center;
9
+ font-weight: bold;
10
+ }
11
+ .alert-message .message, #error_explanation .errors-content{
12
+ margin-left: 40px;
13
+ padding: 20px;
14
+ }
15
+ .notice{
16
+ background-color: lighten($green, 50%);
17
+ border-color: darken($green, 10%);
18
+ color: darken($green, 20%);
19
+ background-image: url('/assets/couch_i18n/notice.png');
20
+ }
21
+ .alert, #error_explanation{
22
+ background-color: lighten($red, 50%);
23
+ border-color: darken($red, 10%);
24
+ color: darken($red, 20%);
25
+ background-image: url('/assets/couch_i18n/alert.png');
26
+ }
27
+ .field_with_errors{
28
+ padding: 2px 5px;
29
+ background-color: $red;
30
+ display: table;
31
+ color: white;
32
+ }
@@ -0,0 +1,69 @@
1
+ html, body{
2
+ margin: 0;
3
+ padding: 0;
4
+ }
5
+ body{
6
+ background-color: #ddf;
7
+ }
8
+ h1, h2, h3, h4, h5, h6{
9
+ margin: 0;
10
+ padding: 0;
11
+ }
12
+ h1{
13
+ font-size: 22px;
14
+ }
15
+ #page-wrapper{
16
+ width: 922px;
17
+ margin: 10px auto;
18
+ border: 1px solid #555;
19
+ background-color: white;
20
+ }
21
+ #page-content{
22
+ padding: 8px;
23
+ a{
24
+ color: black;
25
+ text-decoration: none;
26
+ }
27
+ .destroy-link{
28
+ float: right;
29
+ a{
30
+ background-color: #800;
31
+ color: white;
32
+ font-weight: bold;
33
+ padding: 4px 10px;
34
+ }
35
+ }
36
+ }
37
+ tr.even{
38
+ background-color: #ddf;
39
+ }
40
+ #page-links{
41
+ margin-top: 8px;
42
+ margin-bottom: 8px;
43
+ a{
44
+ padding: 4px;
45
+ border: 1px dotted #555;
46
+ &:hover {
47
+ background-color: #ddd;
48
+ border-style: solid;
49
+ }
50
+ }
51
+ }
52
+
53
+ .offset-navigation-block{
54
+ border-style: double;
55
+ border-color: #444;
56
+ border-left-width: 0;
57
+ border-right-width: 0;
58
+ .offset-navigation-deeper, .offset-navigation-higher{
59
+ a{
60
+ padding: 4px;
61
+ border: 1px solid #aaa;
62
+ &:hover {
63
+ background-color: #ddd;
64
+ border-color: black;
65
+ }
66
+ }
67
+ padding: 8px;
68
+ }
69
+ }
@@ -1,4 +1,5 @@
1
1
  module CouchI18n
2
2
  class ApplicationController < ActionController::Base
3
+ layout 'couch_i18n/application'
3
4
  end
4
5
  end
@@ -0,0 +1,143 @@
1
+ module CouchI18n
2
+ class TranslationsController < CouchI18n::ApplicationController
3
+ def index
4
+ @available_higher_offsets = []
5
+ if params[:offset].present?
6
+ @levels = params[:offset].split('.')
7
+ # Add higher levels. Do not add the last level, since it is the current one => 0..-2
8
+ @levels[0..-2].each_with_index do |level_name, i|
9
+ @available_higher_offsets << {
10
+ :name => level_name,
11
+ :offset => @levels[0..i].join('.')
12
+ }
13
+ end
14
+ @translations = CouchI18n::Translation.with_offset(params[:offset], :page => params[:page], :per_page => 30)
15
+ @available_deeper_offsets = CouchI18n::Translation.get_keys_by_level(@levels.size, :startkey => @levels, :endkey => @levels + [{}]).
16
+ map{|dl| {:name => dl, :offset => [params[:offset], dl].join('.')}}
17
+ else
18
+ @translations = CouchI18n::Translation.all(:page => params[:page], :per_page => 30)
19
+ @available_deeper_offsets = CouchI18n::Translation.get_keys_by_level(0).
20
+ map{|dl| {:name => dl, :offset => dl}}
21
+ end
22
+ end
23
+
24
+ def show
25
+ @translation = CouchI18n::Translation.find(params[:id])
26
+ end
27
+
28
+ def new
29
+ @translation = CouchI18n::Translation.new :key => params[:offset]
30
+ end
31
+
32
+ def create
33
+ @translation = CouchI18n::Translation.new params[:translation]
34
+ if @translation.value.present? && params[:is_json].present?
35
+ @translation.value = JSON.parse(@translation.value)
36
+ end
37
+ if @translation.save
38
+ redirect_to({:action => :index, :offset => @translation.key.to_s.sub(/\.[\w\s-]+$/, '')}, :notice => I18n.t('action.create.successful', :model => CouchI18n::Translation.model_name.human))
39
+ else
40
+ render :action => :new
41
+ end
42
+ end
43
+
44
+ # GET /couch_i18n/translations/:id/edit
45
+ def edit
46
+ @translation = CouchI18n::Translation.find(params[:id])
47
+ end
48
+
49
+ # PUT /couch_i18n/translations/:id
50
+ def update
51
+ @translation = CouchI18n::Translation.find(params[:id])
52
+ @translation.translated = true
53
+ if params[:translation]["value"].present? && params[:is_json].present?
54
+ params[:translation]["value"] = JSON.parse(params[:translation]["value"])
55
+ end
56
+ if @translation.update_attributes(params[:translation])
57
+ redirect_to({:action => :index, :offset => @translation.key.to_s.sub(/\.[\w\s-]+$/, '')}, :notice => I18n.t('action.update.successful', :model => CouchI18n::Translation.model_name.human))
58
+ else
59
+ render :action => :edit
60
+ end
61
+ end
62
+
63
+ def destroy
64
+ @translation = CouchI18n::Translation.find(params[:id])
65
+ if @translation.destroy
66
+ flash[:notice] = I18n.t('action.destroy.successful', :model => CouchI18n::Translation.model_name.human)
67
+ end
68
+ redirect_to({:action => :index, :offset => @translation.key.to_s.sub(/\.\w+$/, '')})
69
+ end
70
+
71
+ # POST /couch_i18n/translations/export
72
+ # Export to yml, csv or json
73
+ def export
74
+ if params[:offset].present?
75
+ if params[:untranslated].present?
76
+ @translations = CouchI18n::Translation.unstranslated_with_offset(params[:offset])
77
+ else
78
+ @translations = CouchI18n::Translation.with_offset(params[:offset])
79
+ end
80
+ else
81
+ if params[:untranslated].present?
82
+ @translations = CouchI18n::Translation.untranslated
83
+ else
84
+ @translations = CouchI18n::Translation.all
85
+ end
86
+ end
87
+ base_filename = "export#{Time.now.strftime('%Y%m%d%H%M')}"
88
+ if params[:exportformat] == 'csv'
89
+ response.headers['Content-Type'] = 'text/csv'
90
+ response.headers['Content-Disposition'] = %{attachment; filename="#{base_filename}.csv"}
91
+ render :text => @translations.map{|s| [s.key, s.value.to_json].join(',')}.join("\n")
92
+ elsif params[:exportformat] == 'json'
93
+ response.headers['Content-Type'] = 'application/json'
94
+ response.headers['Content-Disposition'] = %{attachment; filename="#{base_filename}.json"}
95
+ # render :text => CouchI18n.indent_keys(@translations).to_json # for indented json
96
+ render :json => @translations.map{|s| {s.key => s.value}}.to_json
97
+ else #yaml
98
+ response.headers['Content-Type'] = 'application/x-yaml'
99
+ response.headers['Content-Disposition'] = %{attachment; filename="#{base_filename}.yml"}
100
+ render :text => CouchI18n.indent_keys(@translations).to_yaml
101
+ end
102
+ end
103
+
104
+ # POST /couch_i18n/translations/import
105
+ # Import yml files
106
+ def import
107
+ redirect_to({:action => :index, :offset => params[:offset]}, :alert => I18n.t('couch_i18n.translation.no import file given')) and return unless params[:importfile].present?
108
+ filename = params[:importfile].original_filename
109
+ extension = filename.sub(/.*\./, '')
110
+ if extension == 'yml'
111
+ hash = YAML.load_file(params[:importfile].tempfile.path) rescue nil
112
+ redirect_to({:action => :index, :offset => params[:offset]}, :alert => I18n.t('couch_i18n.translation.cannot parse yaml')) and return unless hash
113
+ CouchI18n.traverse_flatten_keys(hash).each do |key, value|
114
+ existing = CouchI18n::Translation.find_by_key(key)
115
+ if existing
116
+ if existing.value != value
117
+ existing.value = value
118
+ existing.translated = true
119
+ existing.save
120
+ end
121
+ else
122
+ CouchI18n::Translation.create :key => key, :value => value
123
+ end
124
+ end
125
+ else
126
+ redirect_to({:action => :index, :offset => params[:offset]}, :alert => I18n.t('couch_i18n.translation.no proper import extension', :extension => extension)) and return
127
+ end
128
+ redirect_to({:action => :index, :offset => params[:offset]}, :notice => I18n.t('couch_i18n.translation.file imported', :filename => filename))
129
+ end
130
+
131
+ # Very dangarous action, please handle this with care, large removals are supported!
132
+ # DELETE /couch_i18n/translations/destroy_offset?...
133
+ def destroy_offset
134
+ if params[:offset].present?
135
+ @translations = CouchI18n::Translation.with_offset(params[:offset])
136
+ else
137
+ @translations = CouchI18n::Translation.all
138
+ end
139
+ @translations.map(&:destroy)
140
+ redirect_to({:action => :index}, :notice => I18n.t('couch_i18n.translation.offset deleted', :count => @translations.size, :offset => params[:offset]))
141
+ end
142
+ end
143
+ end
@@ -1,4 +1,37 @@
1
1
  module CouchI18n
2
2
  module ApplicationHelper
3
+ def title(msg)
4
+ content_for :title do
5
+ msg
6
+ end
7
+ end
8
+
9
+ def link_to_new_content(obj)
10
+ t('New')
11
+ end
12
+ def link_to_edit_content(obj = nil)
13
+ t('Edit')
14
+ end
15
+ def link_to_show_content(obj = nil)
16
+ t('Show')
17
+ end
18
+ def link_to_index_content(singular_name)
19
+ t('Back')
20
+ end
21
+ def link_to_destroy_content(obj = nil)
22
+ t('Delete')
23
+ end
24
+ def update_button_text(obj = nil)
25
+ t('Save')
26
+ end
27
+ def create_button_text(obj = nil)
28
+ t('Create')
29
+ end
30
+ def boolean_show(truefalse)
31
+ truefalse ? t('boolean true') : t('boolean false')
32
+ end
33
+ def are_you_sure(obj = nil)
34
+ t('Are you sure')
35
+ end
3
36
  end
4
37
  end
@@ -0,0 +1,51 @@
1
+ module CouchI18n
2
+ class Translation
3
+ include SimplyStored::Couch
4
+ per_page_method :limit_value
5
+
6
+ property :key
7
+ property :value
8
+ property :translated, :type => :boolean, :default => true
9
+
10
+ validates_uniqueness_of :key
11
+
12
+ after_save :reload_i18n
13
+
14
+ view :all_documents, :key => :key
15
+ view :by_key, :key => :key
16
+
17
+ view :with_key_array, :map => %|function(doc){
18
+ if(doc.ruby_class && doc.ruby_class == 'CouchI18n::Translation') {
19
+ emit(doc.key.split('.').slice(0, -1), 1);
20
+ }
21
+ }|, :type => :raw, :reduce => '_sum'
22
+
23
+ view :untranslated_view, :key => :key, :conditions => "!doc['translated']"
24
+
25
+ def self.get_keys_by_level(level = 0, options = {})
26
+ data = database.view(with_key_array(options.merge(:group_level => level.succ)))["rows"]
27
+ # data = data.select{|h| h["key"].size > level } # Only select ones that have a deeper nesting
28
+ data.map{|h| h['key'][level].try(:to_sym)}.compact
29
+ end
30
+
31
+ # Shorthand for selecting all stored with a given offset
32
+ def self.with_offset(offset, options = {})
33
+ CouchI18n::Translation.find_all_by_key("#{offset}.".."#{offset}.ZZZZZZZZZ", options)
34
+ end
35
+
36
+ def self.untranslated(*args)
37
+ database.view(untranslated_view(*args))
38
+ end
39
+
40
+ def self.untranslated_with_offset(offset, options = {})
41
+ CouchI18n::Translation.untranslated("#{offset}.".."#{offset}.ZZZZZZZZZ", options)
42
+ end
43
+
44
+ # Expire I18n when record is update
45
+ def reload_i18n
46
+ Rails.cache.write(key, value)
47
+ I18n.reload!
48
+ I18n.cache_store.clear if I18n.respond_to?(:cache_store) && I18n.cache_store.respond_to?(:clear)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,6 @@
1
+ - if flash[:notice]
2
+ .alert-message.notice
3
+ .message= flash[:notice]
4
+ - if flash[:alert]
5
+ .alert-message.alert
6
+ .message= flash[:alert]
@@ -0,0 +1,6 @@
1
+ -if target.errors.any?
2
+ #errorExplanation
3
+ %h2= t('Errors title', :count => target.errors.count)
4
+ %ul
5
+ - target.errors.full_messages.each do |msg|
6
+ %li= msg
@@ -0,0 +1,13 @@
1
+ = form_for @translation do |f|
2
+ = render 'error_messages', :target => @translation
3
+ .field
4
+ = f.label :key
5
+ = f.text_field :key, :size => 70
6
+ .field
7
+ = f.label :value
8
+ = f.text_field :value, :size => 70
9
+ .field
10
+ = label_tag :is_json
11
+ = check_box_tag :is_json, 1, ['{', '['].include?(@translation.value.to_s.first)
12
+ .actions
13
+ = f.submit @submit || update_button_text
@@ -0,0 +1,4 @@
1
+ - title t("couch_i18n.translation.edit title")
2
+ = render 'form'
3
+ - content_for :page_links do
4
+ = link_to link_to_index_content(:translations), couch_i18n.translations_path(:offset => params[:offset] || @translation.key.to_s.sub(/\.[\w\s-]+$/, ''))
@@ -0,0 +1,54 @@
1
+ - if params[:offset].present?
2
+ - title params[:offset]
3
+ - else
4
+ - title t("couch_i18n.translation.index title")
5
+ .destroy-link
6
+ = link_to t('couch_i18n.translation.destroy offset link'), couch_i18n.destroy_offset_translations_path(:offset => params[:offset]), :method => :delete, :confirm => (defined?(:are_you_sure) ? are_you_sure : t('couch_i18n.translation.are you sure'))
7
+ .import-form
8
+ = form_tag({:action => :import}, :multipart => true) do
9
+ = file_field_tag :importfile
10
+ = submit_tag I18n.t('couch_i18n.translation.import')
11
+ .export-form
12
+ = form_tag :action => :export do
13
+ = hidden_field_tag :offset, params[:offset]
14
+ = select_tag :exportformat, options_for_select(%w[yml csv json], params[:exportformat])
15
+ = check_box_tag :untranslated
16
+ = label_tag :untranslated, t('couch_i18n.translation.export.untranslated')
17
+ = submit_tag I18n.t('couch_i18n.translation.export.execute')
18
+ .offset-navigation-block.block
19
+ .offset-navigation-higher
20
+ - for offset in @available_higher_offsets
21
+ = link_to offset[:name], :offset => offset[:offset]
22
+ .offset-navigation-form
23
+ = form_tag({}, :method => :get )do
24
+ - if params[:offset].present?
25
+ = link_to I18n.t('couch_i18n.translation.go to zero offset'), :offset => nil
26
+ = text_field_tag :offset, params[:offset], :size => 60
27
+ = "(#{@translations.total_entries})"
28
+ = submit_tag I18n.t('couch_i18n.translation.go to offset')
29
+ .offset-navigation-deeper
30
+ - for offset in @available_deeper_offsets
31
+ = link_to offset[:name], :offset => offset[:offset]
32
+ - if @translations.any?
33
+ = paginate @translations, :right => 3, :left => 3
34
+ %table.index-table
35
+ %thead
36
+ %tr
37
+ %th= CouchI18n::Translation.human_attribute_name(:key)
38
+ %th= CouchI18n::Translation.human_attribute_name(:value)
39
+ %th= CouchI18n::Translation.human_attribute_name(:translated)
40
+ %th.action.edit
41
+ %th.action.destroy
42
+ %tbody
43
+ - @translations.each do |translation|
44
+ %tr{:class => cycle('odd', 'even')}
45
+ %td= link_to translation.key[(params[:offset].try(:size) || 0)..-1].sub(/^\./, ''), couch_i18n.edit_translation_path(translation)
46
+ %td= link_to translation.value, couch_i18n.edit_translation_path(translation)
47
+ %td.boolean= boolean_show(translation.translated)
48
+ %td.action.edit= link_to link_to_edit_content(translation), couch_i18n.edit_translation_path(translation)
49
+ %td.action.destroy= link_to link_to_destroy_content(translation),couch_i18n.translation_path(translation), :confirm => are_you_sure, :method => :delete
50
+ - else
51
+ %h3= t("translation.none found")
52
+
53
+ - content_for :page_links do
54
+ = link_to link_to_new_content(:translation), couch_i18n.new_translation_path(:offset => params[:offset])
@@ -3,10 +3,14 @@
3
3
  %head
4
4
  %title= defined?(site_title) ? site_title : I18n.t('couch_i18n.store.site_title')
5
5
  = csrf_meta_tag
6
+ = stylesheet_link_tag 'couch_i18n/application'
7
+ = javascript_include_tag 'couch_i18n/application'
6
8
  = yield :head
7
9
  %body
8
10
  #page-wrapper
9
- #page-content
11
+ #page-header
10
12
  %h1= yield :title
13
+ #page-content
14
+ = render 'alerts'
11
15
  = yield
12
16
  #page-links= yield :page_links
data/config/routes.rb ADDED
@@ -0,0 +1,10 @@
1
+ CouchI18n::Engine.routes.draw do
2
+ root :to => "translations#index"
3
+ resources :translations do
4
+ collection do
5
+ post :export
6
+ post :import
7
+ delete :destroy_offset
8
+ end
9
+ end
10
+ end
@@ -1,7 +1,7 @@
1
1
  require 'i18n/backend/base'
2
2
  require 'active_support/json'
3
3
  require 'active_support/ordered_hash' # active_support/json/encoding uses ActiveSupport::OrderedHash but does not require it
4
-
4
+ require "i18n/backend/cache"
5
5
  module CouchI18n
6
6
  class Backend
7
7
  # This is a basic backend for key value stores. It receives on
@@ -52,7 +52,7 @@ module CouchI18n
52
52
  attr_accessor :store
53
53
 
54
54
  include I18n::Backend::Base, I18n::Backend::Flatten
55
-
55
+ include I18n::Backend::Cache
56
56
  def initialize(store, subtrees=true)
57
57
  @store, @subtrees = store, subtrees
58
58
  end
@@ -89,7 +89,7 @@ module CouchI18n
89
89
  def lookup(locale, key, scope = [], options = {})
90
90
  key = normalize_flat_keys(locale, key, scope, options[:separator])
91
91
  value = @store["#{locale}.#{key}"]
92
- #value = ActiveSupport::JSON.decode(value) if value
92
+ value = ActiveSupport::JSON.decode(value) rescue value
93
93
  value.is_a?(Hash) ? value.deep_symbolize_keys : value
94
94
  end
95
95
  end
@@ -1,6 +1,5 @@
1
1
  module CouchI18n
2
2
  class Engine < Rails::Engine
3
- # Commented out be be used in rails < 3.1
4
- #isolate_namespace CouchI18n
3
+ isolate_namespace CouchI18n
5
4
  end
6
5
  end
@@ -1,69 +1,30 @@
1
- # First comment out backend declaration in config/initializers/i18n_backend.rb
2
- # this looks like:
3
- # #I18n.backend = I18n::Backend::Chain.new(I18n::Backend::KeyValue.new(CouchI18n::Store), I18n.backend)
4
- # Then load the translations:
5
- # I18n.t('New')
6
- # Then create the translations
7
- # I18n.t('New');I18n.backend.send(:translations).flatten_keys.each{|k, v| CouchI18n::Store.create(:key => k, :value => v)}
8
1
  module CouchI18n
9
2
  class Store
10
- include SimplyStored::Couch
11
- per_page_method :limit_value
12
3
 
13
- property :key
14
- property :value
15
-
16
- validates_uniqueness_of :key
17
-
18
- after_save :reload_i18n
19
-
20
- view :all_documents, :key => :key
21
- view :by_key, :key => :key
22
-
23
- view :with_key_array, :map => %|function(doc){
24
- if(doc.ruby_class && doc.ruby_class == 'CouchI18n::Store') {
25
- emit(doc.key.split('.').slice(0,-1), 1);
26
- }
27
- }|, :type => :raw, :reduce => '_sum'
28
-
29
- def self.available_locales
30
- get_keys_by_level
31
- end
32
-
33
- def self.get_keys_by_level(level = 0, options = {})
34
- data = database.view(with_key_array(options.merge(:group_level => level.succ)))["rows"]
35
- # data = data.select{|h| h["key"].size > level } # Only select ones that have a deeper nesting
36
- data.map{|h| h['key'][level].try(:to_sym)}.compact
4
+ def available_locales
5
+ CouchI18n::Translation.get_keys_by_level(0)
37
6
  end
38
7
 
39
8
  # Now the store features
40
- def self.[]=(key, value, options = {})
9
+ def []=(key, value, options = {})
41
10
  key = key.to_s
42
- existing = find_by_key(key) rescue nil
43
- translation = existing || new(:key => key)
11
+ existing = CouchI18n::Translation.find_by_key(key) rescue nil
12
+ translation = existing || CouchI18n::Translation.new(:key => key)
44
13
  translation.value = value
45
14
  translation.save
46
15
  end
47
16
 
48
- # Shorthand for selecting all stored with a given offset
49
- def self.with_offset(offset, options = {})
50
- find_all_by_key("#{offset}.".."#{offset}.ZZZZZZZZZ", options)
51
- end
52
-
53
17
  # alias for read
54
- def self.[](key, options=nil)
55
- find_by_key(key.to_s).try(:value) rescue nil
18
+ def [](key, options=nil)
19
+ Rails.cache.fetch(key) do
20
+ translation = CouchI18n::Translation.find_by_key(key.to_s)
21
+ translation ||= CouchI18n::Translation.create(:key => key, :value => key.to_s.split('.').last, :translated => false)
22
+ translation.value
23
+ end
56
24
  end
57
25
 
58
- def self.keys
59
- all.map(&:key)
60
- end
61
-
62
- private
63
-
64
- def reload_i18n
65
- I18n.reload!
66
- I18n.cache_store.clear if I18n.respond_to?(:cache_store) && I18n.cache_store.respond_to?(:clear)
26
+ def keys
27
+ CouchI18n::Translation.all.map(&:key)
67
28
  end
68
29
  end
69
30
  end
data/lib/couch_i18n.rb CHANGED
@@ -1,8 +1,6 @@
1
- require "i18n/backend/cache"
2
1
  require "couch_i18n/engine"
3
2
  require 'couch_i18n/store'
4
3
  require 'couch_i18n/backend'
5
- I18n::Backend::KeyValue.send(:include, I18n::Backend::Cache)
6
4
  module CouchI18n
7
5
  # This method imports yaml translations to the couchdb version. When run again new ones will
8
6
  # be added. Translations already stored in the couchdb database are not overwritten
@@ -15,7 +13,7 @@ module CouchI18n
15
13
  yml_backend.load_translations
16
14
  flattened_hash = traverse_flatten_keys(yml_backend.send(:translations))
17
15
  flattened_hash.each do |key, value|
18
- CouchI18n::Store.create :key => key, :value => value
16
+ CouchI18n::Translation.create :key => key, :value => value
19
17
  end
20
18
  end
21
19
 
@@ -26,7 +24,7 @@ module CouchI18n
26
24
  obj.each{|k, v| traverse_flatten_keys(v, store, [path, k].compact.join('.'))}
27
25
  when Array
28
26
  # Do not store array for now. There is no good solution yet
29
- # store[path] = obj # Keeyp arrays intact
27
+ store[path] = obj # Keeyp arrays intact
30
28
  # store[path] = obj.to_json
31
29
  # obj.each_with_index{|v, i| traverse_flatten_keys(store, v, [path, i].compact.join('.'))}
32
30
  else
@@ -38,7 +36,7 @@ module CouchI18n
38
36
  # This will return an indented strucutre of a collection of stores. If no argument is given
39
37
  # by default all the translations are indented. So a command:
40
38
  # CouchI18n.indent_keys.to_yaml will return one big yaml string of the translations
41
- def self.indent_keys(selection = Store.all)
39
+ def self.indent_keys(selection = CouchI18n::Translation.all)
42
40
  traverse_indent_keys(selection.map{|kv| [kv.key.split('.'), kv.value]})
43
41
  end
44
42
 
@@ -49,13 +47,19 @@ module CouchI18n
49
47
  h[pair.first.first] = pair.last
50
48
  else
51
49
  h[pair.first.first] ||= {}
50
+ next unless h[pair.first.first].is_a?(Hash) # In case there is a translation en.a: A, en.a.b: B this is invalid
52
51
  traverse_indent_keys([[pair.first[1..-1], pair.last]], h[pair.first.first])
53
52
  end
54
53
  end
55
54
  h
56
55
  end
57
- end
58
56
 
57
+ # Add all translations to the cache to avoid one by one loading and caching
58
+ def self.cache_all
59
+ CouchI18n::Translation.all.each do |t|
60
+ Rails.cache.write(t.key, t.value)
61
+ end
62
+ end
63
+ end
59
64
  # Now extend the I18n backend
60
- I18n.backend = I18n::Backend::Chain.new(CouchI18n::Backend.new(CouchI18n::Store), I18n.backend)
61
- I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
65
+ I18n.backend = I18n::Backend::Chain.new(CouchI18n::Backend.new(CouchI18n::Store.new), I18n.backend)
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: couch_i18n
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.0.4
5
+ version: 0.0.5
6
6
  platform: ruby
7
7
  authors:
8
8
  - Benjamin ter Kuile
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-05-21 00:00:00 +02:00
13
+ date: 2011-07-10 00:00:00 +02:00
14
14
  default_executable:
15
15
  dependencies: []
16
16
 
@@ -28,16 +28,25 @@ files:
28
28
  - lib/couch_i18n/store.rb
29
29
  - lib/couch_i18n.rb
30
30
  - lib/tasks/couch_i18n_tasks.rake
31
- - app/controllers/couch_i18n/stores_controller.rb
31
+ - app/models/couch_i18n/translation.rb
32
32
  - app/controllers/couch_i18n/application_controller.rb
33
- - app/assets/javascripts/application.js
34
- - app/assets/stylesheets/application.css
33
+ - app/controllers/couch_i18n/translations_controller.rb
34
+ - app/assets/javascripts/couch_i18n/structure.js
35
+ - app/assets/javascripts/couch_i18n/application.js
36
+ - app/assets/images/couch_i18n/alert.png
37
+ - app/assets/images/couch_i18n/notice.png
38
+ - app/assets/stylesheets/couch_i18n/alerts.css.scss
39
+ - app/assets/stylesheets/couch_i18n/structure.css.scss
40
+ - app/assets/stylesheets/couch_i18n/application.css
35
41
  - app/helpers/couch_i18n/application_helper.rb
36
- - app/views/couch_i18n/stores/_form.html.haml
37
- - app/views/couch_i18n/stores/index.html.haml
38
- - app/views/couch_i18n/stores/edit.html.haml
39
- - app/views/couch_i18n/stores/new.html.haml
42
+ - app/views/couch_i18n/application/_alerts.html.haml
43
+ - app/views/couch_i18n/application/_error_messages.html.haml
44
+ - app/views/couch_i18n/translations/_form.html.haml
45
+ - app/views/couch_i18n/translations/index.html.haml
46
+ - app/views/couch_i18n/translations/edit.html.haml
47
+ - app/views/couch_i18n/translations/new.html.haml
40
48
  - app/views/layouts/couch_i18n/application.html.haml
49
+ - config/routes.rb
41
50
  - MIT-LICENSE
42
51
  - Rakefile
43
52
  - README.rdoc
@@ -1,127 +0,0 @@
1
- module CouchI18n
2
- class StoresController < ApplicationController
3
- def index
4
- @available_higher_offsets = []
5
- if params[:offset].present?
6
- @levels = params[:offset].split('.')
7
- # Add higher levels. Do not add the last level, since it is the current one => 0..-2
8
- @levels[0..-2].each_with_index do |level_name, i|
9
- @available_higher_offsets << {
10
- :name => level_name,
11
- :offset => @levels[0..i].join('.')
12
- }
13
- end
14
- @couch_i18n_stores = CouchI18n::Store.with_offset(params[:offset], :page => params[:page], :per_page => 30)
15
- @available_deeper_offsets = CouchI18n::Store.get_keys_by_level(@levels.size, :startkey => @levels, :endkey => @levels + [{}]).
16
- map{|dl| {:name => dl, :offset => [params[:offset], dl].join('.')}}
17
- else
18
- @couch_i18n_stores = CouchI18n::Store.all(:page => params[:page], :per_page => 30)
19
- @available_deeper_offsets = CouchI18n::Store.get_keys_by_level(0).
20
- map{|dl| {:name => dl, :offset => dl}}
21
- end
22
- end
23
-
24
- def show
25
- @couch_i18n_store = CouchI18n::Store.find(params[:id])
26
- end
27
-
28
- def new
29
- @couch_i18n_store = CouchI18n::Store.new :key => params[:offset]
30
- end
31
-
32
- def create
33
- @couch_i18n_store = CouchI18n::Store.new params[:couch_i18n_store]
34
- if @couch_i18n_store.save
35
- redirect_to({:action => :index, :offset => @couch_i18n_store.key.to_s.sub(/\.[\w\s-]+$/, '')}, :notice => I18n.t('action.create.successful', :model => CouchI18n::Store.model_name.human))
36
- else
37
- render :action => :new
38
- end
39
- end
40
-
41
- # GET /couch_i18n/stores/:id/edit
42
- def edit
43
- @couch_i18n_store = CouchI18n::Store.find(params[:id])
44
- end
45
-
46
- # PUT /couch_i18n/stores/:id
47
- def update
48
- @couch_i18n_store = CouchI18n::Store.find(params[:id])
49
- if @couch_i18n_store.update_attributes(params[:couch_i18n_store])
50
- redirect_to({:action => :index, :offset => @couch_i18n_store.key.to_s.sub(/\.[\w\s-]+$/, '')}, :notice => I18n.t('action.update.successful', :model => CouchI18n::Store.model_name.human))
51
- else
52
- render :action => :edit
53
- end
54
- end
55
-
56
- def destroy
57
- @couch_i18n_store = CouchI18n::Store.find(params[:id])
58
- if @couch_i18n_store.destroy
59
- flash[:notice] = I18n.t('action.destroy.successful', :model => CouchI18n::Store.model_name.human)
60
- end
61
- redirect_to({:action => :index, :offset => @couch_i18n_store.key.to_s.sub(/\.\w+$/, '')})
62
- end
63
-
64
- # POST /couch_i18n/stores/export
65
- # Export to yml, csv or json
66
- def export
67
- if params[:offset].present?
68
- @couch_i18n_stores = CouchI18n::Store.with_offset(params[:offset])
69
- else
70
- @couch_i18n_stores = CouchI18n::Store.all
71
- end
72
- base_filename = "export#{Time.now.strftime('%Y%m%d%H%M')}"
73
- if params[:exportformat] == 'csv'
74
- response.headers['Content-Type'] = 'text/csv'
75
- response.headers['Content-Disposition'] = %{attachment; filename="#{base_filename}.csv"}
76
- render :text => @couch_i18n_stores.map{|s| [s.key, s.value.to_json].join(',')}.join("\n")
77
- elsif params[:exportformat] == 'json'
78
- response.headers['Content-Type'] = 'application/json'
79
- response.headers['Content-Disposition'] = %{attachment; filename="#{base_filename}.json"}
80
- # render :text => CouchI18n.indent_keys(@couch_i18n_stores).to_json # for indented json
81
- render :json => @couch_i18n_stores.map{|s| {s.key => s.value}}.to_json
82
- else #yaml
83
- response.headers['Content-Type'] = 'application/x-yaml'
84
- response.headers['Content-Disposition'] = %{attachment; filename="#{base_filename}.yml"}
85
- render :text => CouchI18n.indent_keys(@couch_i18n_stores).to_yaml
86
- end
87
- end
88
-
89
- # POST /couch_i18n/stores/import
90
- # Import yml files
91
- def import
92
- redirect_to({:action => :index, :offset => params[:offset]}, :alert => I18n.t('couch_i18n.store.no import file given')) and return unless params[:importfile].present?
93
- filename = params[:importfile].original_filename
94
- extension = filename.sub(/.*\./, '')
95
- if extension == 'yml'
96
- hash = YAML.load_file(params[:importfile].tempfile.path) rescue nil
97
- redirect_to({:action => :index, :offset => params[:offset]}, :alert => I18n.t('couch_i18n.store.cannot parse yaml')) and return unless hash
98
- CouchI18n.traverse_flatten_keys(hash).each do |key, value|
99
- existing = CouchI18n::Store.find_by_key(key)
100
- if existing
101
- if existing.value != value
102
- existing.value = value
103
- existing.save
104
- end
105
- else
106
- CouchI18n::Store.create :key => key, :value => value
107
- end
108
- end
109
- else
110
- redirect_to({:action => :index, :offset => params[:offset]}, :alert => I18n.t('couch_i18n.store.no proper import extension', :extension => extension)) and return
111
- end
112
- redirect_to({:action => :index, :offset => params[:offset]}, :notice => I18n.t('couch_i18n.store.file imported', :filename => filename))
113
- end
114
-
115
- # Very dangarous action, please handle this with care, large removals are supported!
116
- # DELETE /couch_i18n/stores/destroy_offset?...
117
- def destroy_offset
118
- if params[:offset].present?
119
- @couch_i18n_stores = CouchI18n::Store.with_offset(params[:offset])
120
- else
121
- @couch_i18n_stores = CouchI18n::Store.all
122
- end
123
- @couch_i18n_stores.map(&:destroy)
124
- redirect_to({:action => :index}, :notice => I18n.t('couch_i18n.store.offset deleted', :count => @couch_i18n_stores, :offset => params[:offset]))
125
- end
126
- end
127
- end
@@ -1,10 +0,0 @@
1
- = form_for @couch_i18n_store do |f|
2
- = render 'shared/error_messages', :target => @couch_i18n_store
3
- .field
4
- = f.label :key
5
- = f.text_field :key, :size => 70
6
- .field
7
- = f.label :value
8
- = f.text_field :value, :size => 70
9
- .actions
10
- = f.submit @submit || update_button_text
@@ -1,4 +0,0 @@
1
- - title t("couch_i18n.store.edit title")
2
- = render 'form'
3
- - content_for :page_links do
4
- = link_to link_to_index_content(:couch_i18n_stores), couch_i18n_stores_path(:offset => params[:offset] || @couch_i18n_store.key.to_s.sub(/\.[\w\s-]+$/, ''))
@@ -1,46 +0,0 @@
1
- - title t("couch_i18n.store.index title")
2
- .destroy-link
3
- = link_to t('couch_i18n.store.destroy offset link'), destroy_offset_couch_i18n_stores_path(:offset => params[:offset]), :method => :delete, :confirm => (defined?(:are_you_sure) ? are_you_sure : t('couch_i18n.store.are you sure'))
4
- .import-form
5
- = form_tag({:action => :import}, :multipart => true) do
6
- = file_field_tag :importfile
7
- = submit_tag I18n.t('couch_i18n.store.import')
8
- .export-form
9
- = form_tag :action => :export do
10
- = hidden_field_tag :offset, params[:offset]
11
- = select_tag :exportformat, options_for_select(%w[yml csv json], params[:exportformat])
12
- = submit_tag I18n.t('couch_i18n.store.export')
13
- .offset-navigation-block.block
14
- .offset-navigation-higher
15
- - for offset in @available_higher_offsets
16
- = link_to offset[:name], :offset => offset[:offset]
17
- .offset-navigation-form
18
- = form_tag({}, :method => :get )do
19
- - if params[:offset].present?
20
- = link_to I18n.t('couch_i18n.store.go to zero offset'), :offset => nil
21
- = text_field_tag :offset, params[:offset], :size => 60
22
- = "(#{@couch_i18n_stores.total_entries})"
23
- = submit_tag I18n.t('couch_i18n.store.go to offset')
24
- .offset-navigation-deeper
25
- - for offset in @available_deeper_offsets
26
- = link_to offset[:name], :offset => offset[:offset]
27
- - if @couch_i18n_stores.any?
28
- = paginate @couch_i18n_stores, :right => 3, :left => 3
29
- %table.index-table
30
- %thead
31
- %tr
32
- %th= CouchI18n::Store.human_attribute_name(:key)
33
- %th= CouchI18n::Store.human_attribute_name(:value)
34
- %th
35
- %tbody
36
- - @couch_i18n_stores.each do |couch_i18n_store|
37
- %tr
38
- %td= link_to couch_i18n_store.key[(params[:offset].try(:size) || 0)..-1].sub(/^\./, ''), edit_couch_i18n_store_path(couch_i18n_store)
39
- %td= link_to couch_i18n_store.value, edit_couch_i18n_store_path(couch_i18n_store)
40
- %td= link_to link_to_edit_content(couch_i18n_store), edit_couch_i18n_store_path(couch_i18n_store)
41
- %td= link_to link_to_destroy_content(couch_i18n_store),couch_i18n_store_path(couch_i18n_store), :confirm => are_you_sure, :method => :delete
42
- - else
43
- %h3= t("couch_i18n_store.none found")
44
-
45
- - content_for :page_links do
46
- = link_to link_to_new_content(:couch_i18n_store), new_couch_i18n_store_path(:offset => params[:offset])