is_translatable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2011 Alex Dixon
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,41 @@
1
+ # is\_translatable
2
+
3
+ (development in progress)
4
+
5
+ This is a simple library geared towards translating dynamic text in your database.
6
+ Unlike most similar libaries, is_translatable uses a single table for all translations, so once it's hooked up you won't
7
+ need to do anything fancy in your DB migrations to add support for translations.
8
+
9
+ ## Usage
10
+
11
+ Add the 'translatable' declaration to your model and specify the columns you want translated:
12
+
13
+ class TranslateMe < ActiveRecord::Base
14
+ translatable :title, :description
15
+ end
16
+
17
+ Now you can get/set translations on those columns (defaulting to the current locale):
18
+
19
+ I18n.locale = :es
20
+ t = TranslateMe.new
21
+ t.set_translation(:title, 'In Spanish')
22
+ t.get_translation(:title) # 'In Spanish'
23
+
24
+ Or if you want to override the locale:
25
+
26
+ t.set_translation(:title, 'In French', :fr) # specific override
27
+ t.get_translation(:title, :fr) # 'In French'
28
+
29
+ TODO: hookup behavior for default locale and fallbacking.
30
+
31
+ ## Installation
32
+
33
+ TODO: create a migration generator, and make 'gem install is_translatable' work.
34
+
35
+ ## Credits
36
+
37
+ Inspired by the is\_taggable gem at https://github.com/jamesgolick/is_taggable
38
+
39
+ ## License
40
+
41
+ is\_translatable is available under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ Bundler::GemHelper.install_tasks
9
+
10
+ require 'rspec/core/rake_task'
11
+
12
+ RSpec::Core::RakeTask.new('spec')
13
+
14
+ task :default => :spec
@@ -0,0 +1,3 @@
1
+ module IsTranslatable
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,67 @@
1
+ require 'active_record'
2
+ require 'translation'
3
+
4
+ module IsTranslatable
5
+ module ActiveRecordExtension
6
+ def translatable(*kinds)
7
+ class_attribute :translatable_kinds
8
+ self.translatable_kinds ||= {}
9
+ self.translatable_kinds = kinds.map(&:to_s)
10
+
11
+ include IsTranslatable::Methods
12
+ end
13
+ end
14
+
15
+ module Methods
16
+ def self.included(klass)
17
+ klass.class_eval do
18
+ include IsTranslatable::Methods::InstanceMethods
19
+
20
+ has_many :translations, :as => :translatable, :dependent => :destroy, :autosave => true
21
+ accepts_nested_attributes_for :translations
22
+ end
23
+ end
24
+
25
+ module InstanceMethods
26
+ def set_translation(kind, t, locale=nil)
27
+ validate_kind(kind)
28
+ locale ||= I18n.locale
29
+ t_obj = find_translation(kind, locale)
30
+ if t_obj.nil?
31
+ translations.build({:kind => kind.to_s, :translation => t, :locale => locale.to_s})
32
+ else
33
+ t_obj.translation = t
34
+ end
35
+ end
36
+
37
+ def get_translation(kind, locale=nil)
38
+ validate_kind(kind)
39
+ locale ||= I18n.locale
40
+ t = translations.find_by_kind(kind.to_s, :conditions => {:locale => locale.to_s})
41
+ t ||= find_translation(kind, locale)
42
+ t.translation unless t.nil?
43
+ end
44
+
45
+ def remove_translation(kind, locale = nil)
46
+ validate_kind(kind)
47
+ locale ||= I18n.locale
48
+ t = find_translation(kind, locale)
49
+ t.mark_for_destruction unless t.nil?
50
+ end
51
+
52
+ protected
53
+ def find_translation(kind, locale)
54
+ translations.each do |t|
55
+ return t if t.kind == kind.to_s && t.locale == locale.to_s
56
+ end
57
+ nil
58
+ end
59
+
60
+ def validate_kind(kind)
61
+ raise ArgumentError.new("#{kind} is not a translatable field") unless translatable_kinds.include?(kind.to_s)
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ ActiveRecord::Base.send(:extend, IsTranslatable::ActiveRecordExtension)
@@ -0,0 +1,24 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ # This article helps figure out what's going on with Rails 3 generators:
5
+ # http://www.themodestrubyist.com/2010/03/16/rails-3-plugins---part-3---rake-tasks-generators-initializers-oh-my/
6
+ class IsTranslatableMigrationGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+ source_root File.expand_path('../templates', __FILE__)
9
+
10
+ # Ugh :(
11
+ # Implement the required interface for Rails::Generators::Migration.
12
+ # taken from http://github.com/rails/rails/blob/master/activerecord/lib/generators/active_record.rb
13
+ def self.next_migration_number(dirname)
14
+ if ActiveRecord::Base.timestamped_migrations
15
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
16
+ else
17
+ "%.3d" % (current_migration_number(dirname) + 1)
18
+ end
19
+ end
20
+
21
+ def create_translatable_migration
22
+ migration_template 'migration.rb', 'db/migrate/is_translatable_migration'
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ class IsTranslatableMigration < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :translations do |t|
4
+ t.string :translatable_type , :default => ''
5
+ t.integer :translatable_id
6
+ t.string :kind
7
+ t.string :locale, :limit => 5
8
+ t.text :translation
9
+ end
10
+
11
+ add_index :translations, [:translatable_id, :translatable_type, :kind, :locale], :name => 'index_translations'
12
+ end
13
+
14
+ def self.down
15
+ drop_table :translations
16
+ end
17
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :is_translatable do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,3 @@
1
+ class Translation < ActiveRecord::Base
2
+ belongs_to :translatable, :polymorphic => true
3
+ end
@@ -0,0 +1,180 @@
1
+ require 'spec_helper'
2
+
3
+ # NOTE: trying to switch closer to this style (mostly for the nicer errors)
4
+ # http://eggsonbread.com/2010/03/28/my-rspec-best-practices-and-tips/
5
+ describe IsTranslatable do
6
+ before :each do
7
+ @titles = {
8
+ :en => 'Translations now easier',
9
+ :es => 'Traducciones ahora facil',
10
+ :fr => 'Traductions maintenant plus facile'
11
+ }
12
+ end
13
+
14
+ it 'should test default locale (just using the actual field)'
15
+ it 'should be even more awesome'
16
+
17
+ context 'article translations' do
18
+ before {@article = Article.new(:title => 'Translations now easier', :body => 'is_translatable plugin makes translating easier...')}
19
+ subject {@article}
20
+
21
+ specify {subject.save.should be_true}
22
+ it {should be_valid}
23
+
24
+ context 'with spanish locale' do
25
+ before {I18n.locale = :es}
26
+
27
+ context 'with translated title' do
28
+ before {@article.set_translation(:title, @titles[:es])}
29
+ it {should be_valid}
30
+
31
+ it {subject.get_translation(:title).should == @titles[:es]}
32
+
33
+ context 'set twice' do
34
+ before {@article.set_translation(:title, 'spanish override')}
35
+
36
+ it {subject.translations.length.should == 1}
37
+ it {subject.get_translation(:title).should == 'spanish override'}
38
+ end
39
+
40
+ context 'loaded from db' do
41
+ before :each do
42
+ @article.save!
43
+ @loaded_article = Article.find(@article.id)
44
+ end
45
+ subject{@loaded_article}
46
+
47
+ it {should be_valid}
48
+
49
+ it {subject.get_translation(:title).should == @titles[:es]}
50
+
51
+ context 'and removed' do
52
+ before :each do
53
+ @loaded_article.remove_translation(:title)
54
+ @loaded_article.save!
55
+ end
56
+
57
+ it {subject.get_translation(:title).should be_nil}
58
+ end
59
+ end
60
+ end
61
+
62
+ it 'should not translate if not translatable' do
63
+ lambda {@article.set_translation(:non_translatable, 'foo')}.should raise_error(ArgumentError)
64
+ end
65
+
66
+ context 'with translated title and locale override' do
67
+ before {@article.set_translation(:title, @titles[:fr], :fr)}
68
+ it {should be_valid}
69
+
70
+ it 'should allow specifying fallback behavior?'
71
+ it {subject.get_translation(:title).should == nil}
72
+ it {subject.get_translation(:title, :fr).should == @titles[:fr]}
73
+ end
74
+
75
+ end
76
+ end
77
+
78
+ context 'multiple translations' do
79
+ before :each do
80
+ # Creating these in a couple of different ways to excersize the code a little better.
81
+ # Also putting in overlapping ids to make sure we're loading for the right table.
82
+ @article1 = Article.new(:title => 'Article Title', :body => 'Article Body')
83
+ @article1.id = 1
84
+ @article1.set_translation(:title, 'es A1T', :es)
85
+ @article1.set_translation(:title, 'pt-BR A1T', :'pt-BR')
86
+ @article1.set_translation(:body, 'pt-BR A1B', :'pt-BR')
87
+ @article1.save!
88
+
89
+ article2 = Article.new(:title => 'Article2 Title', :body => 'Article2 Body')
90
+ article2.id = 2
91
+ article2.set_translation(:title, 'es A2T', :es)
92
+ article2.set_translation(:title, 'pt-BR A2T', :'pt-BR')
93
+ article2.set_translation(:body, 'pt-BR A2B', :'pt-BR')
94
+ article2.save!
95
+ @article2 = Article.find(article2.id)
96
+
97
+ @note1 = Note.create!(:body => 'Note Body')
98
+ @note1.set_translation(:body, 'pt-BR N1B', :'pt-BR')
99
+ @note1.set_translation(:body, 'es N1B', :'es')
100
+ @note1.save!
101
+
102
+ @note2 = Note.new(:body => 'Note2 Body')
103
+ @note2.set_translation(:body, 'pt-BR N2B', :'pt-BR')
104
+ @note2.set_translation(:body, 'es N2B', :'es')
105
+ @note2.save!
106
+ end
107
+
108
+ context 'article1' do
109
+ subject {@article1}
110
+
111
+ it {should be_valid}
112
+ it {subject.get_translation(:title, :es).should == 'es A1T'}
113
+ it {subject.get_translation(:title, :'pt-BR').should == 'pt-BR A1T'}
114
+ it {subject.get_translation(:body, :'pt-BR').should == 'pt-BR A1B'}
115
+ it {subject.get_translation(:body, :es).should be_nil}
116
+ end
117
+
118
+ context 'article2' do
119
+ subject {@article2}
120
+
121
+ it {should be_valid}
122
+ it {subject.get_translation(:title, :es).should == 'es A2T'}
123
+ it {subject.get_translation(:title, :'pt-BR').should == 'pt-BR A2T'}
124
+ it {subject.get_translation(:body, :'pt-BR').should == 'pt-BR A2B'}
125
+ it {subject.get_translation(:body, :es).should be_nil}
126
+ end
127
+
128
+ context 'note1' do
129
+ subject {@note1}
130
+
131
+ it {should be_valid}
132
+ it {subject.get_translation(:body, :'pt-BR').should == 'pt-BR N1B'}
133
+ it {subject.get_translation(:body, :es).should == 'es N1B'}
134
+ end
135
+
136
+ context 'note2' do
137
+ subject {@note2}
138
+
139
+ it {should be_valid}
140
+ it {subject.get_translation(:body, :'pt-BR').should == 'pt-BR N2B'}
141
+ it {subject.get_translation(:body, :es).should == 'es N2B'}
142
+ end
143
+
144
+ context 'article1 body translations deleted' do
145
+ before :each do
146
+ @article1.remove_translation(:body, :es)
147
+ @article1.remove_translation(:body, :'pt-BR')
148
+ @article1.save!
149
+ end
150
+
151
+ context 'article1 reloaded' do
152
+ before { @article1_loaded = Article.find(@article1.id) }
153
+ subject { @article1_loaded }
154
+
155
+ it {subject.get_translation(:title, :es).should == 'es A1T'}
156
+ it {subject.get_translation(:title, :'pt-BR').should == 'pt-BR A1T'}
157
+ it {subject.get_translation(:body, :es).should be_nil}
158
+ it {subject.get_translation(:body, :'pt-BR').should be_nil}
159
+ end
160
+
161
+ context 'article2' do
162
+ subject {@article2}
163
+ it {subject.get_translation(:body, :'pt-BR').should == 'pt-BR A2B'}
164
+ it {subject.get_translation(:body, :es).should be_nil}
165
+ end
166
+
167
+ context 'note1' do
168
+ subject {@note1}
169
+ it {subject.get_translation(:body, :'pt-BR').should == 'pt-BR N1B'}
170
+ it {subject.get_translation(:body, :es).should == 'es N1B'}
171
+ end
172
+
173
+ context 'note2' do
174
+ subject {@note2}
175
+ it {subject.get_translation(:body, :'pt-BR').should == 'pt-BR N2B'}
176
+ it {subject.get_translation(:body, :es).should == 'es N2B'}
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'is_translatable'
5
+ require 'sqlite3'
6
+ require 'active_record'
7
+
8
+ # Create test schema and models
9
+
10
+ ActiveRecord::Base.configurations = {'sqlite3' => {:adapter => 'sqlite3', :database => ':memory:'}}
11
+ ActiveRecord::Base.establish_connection('sqlite3')
12
+
13
+ ActiveRecord::Schema.define(:version => 0) do
14
+ create_table :articles do |t|
15
+ t.string :title
16
+ t.string :body
17
+ t.string :non_translatable
18
+ end
19
+
20
+ create_table :notes do |t|
21
+ t.string :body # for dupe tests with articles
22
+ end
23
+
24
+ create_table :translations do |t|
25
+ t.string :translatable_type , :default => ''
26
+ t.integer :translatable_id
27
+ t.string :kind
28
+ t.string :locale, :limit => 5
29
+ t.text :translation
30
+ end
31
+
32
+ class Article < ActiveRecord::Base
33
+ translatable :title, :body
34
+ end
35
+
36
+ class Note < ActiveRecord::Base
37
+ translatable :body
38
+ end
39
+ end
40
+
41
+ RSpec.configure do |config|
42
+ config.mock_with :rspec
43
+ config.before :each do
44
+ Article.destroy_all
45
+ Note.destroy_all
46
+ Translation.destroy_all
47
+ end
48
+ end
49
+
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: is_translatable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alex Dixon
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-12-15 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: &70343138412680 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.1.2
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70343138412680
25
+ - !ruby/object:Gem::Dependency
26
+ name: sqlite3
27
+ requirement: &70343138409820 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70343138409820
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &70343138394480 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70343138394480
47
+ description:
48
+ email:
49
+ - dixo0015+is_translatable@gmail.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - lib/is_translatable/version.rb
55
+ - lib/is_translatable.rb
56
+ - lib/rails/generators/is_translatable_migration/is_translatable_migration_generator.rb
57
+ - lib/rails/generators/is_translatable_migration/templates/migration.rb
58
+ - lib/tasks/is_translatable_tasks.rake
59
+ - lib/translation.rb
60
+ - MIT-LICENSE
61
+ - Rakefile
62
+ - README.markdown
63
+ - spec/lib/is_translatable_spec.rb
64
+ - spec/spec_helper.rb
65
+ homepage: https://github.com/alexdixon/is_translatable
66
+ licenses: []
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 1.8.12
86
+ signing_key:
87
+ specification_version: 3
88
+ summary: Simple translation of dynamic db fields.
89
+ test_files:
90
+ - spec/lib/is_translatable_spec.rb
91
+ - spec/spec_helper.rb