couch_i18n 0.0.4 → 0.0.5

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.
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])