galetahub-globalize3 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/README.textile +206 -0
  2. data/Rakefile +22 -0
  3. data/lib/globalize.rb +59 -0
  4. data/lib/globalize/active_record.rb +13 -0
  5. data/lib/globalize/active_record/accessors.rb +22 -0
  6. data/lib/globalize/active_record/act_macro.rb +67 -0
  7. data/lib/globalize/active_record/adapter.rb +101 -0
  8. data/lib/globalize/active_record/attributes.rb +27 -0
  9. data/lib/globalize/active_record/class_methods.rb +125 -0
  10. data/lib/globalize/active_record/exceptions.rb +19 -0
  11. data/lib/globalize/active_record/instance_methods.rb +166 -0
  12. data/lib/globalize/active_record/migration.rb +125 -0
  13. data/lib/globalize/active_record/translation.rb +37 -0
  14. data/lib/globalize/engine.rb +17 -0
  15. data/lib/globalize/utils.rb +142 -0
  16. data/lib/globalize/versioning.rb +5 -0
  17. data/lib/globalize/versioning/paper_trail.rb +41 -0
  18. data/lib/globalize3.rb +1 -0
  19. data/lib/globalize3/version.rb +3 -0
  20. data/lib/i18n/missing_translations_log_handler.rb +41 -0
  21. data/lib/i18n/missing_translations_raise_handler.rb +25 -0
  22. data/lib/patches/active_record/query_method.rb +35 -0
  23. data/lib/patches/active_record/xml_attribute_serializer.rb +13 -0
  24. data/lib/tasks/globalize.rake +13 -0
  25. data/test/all.rb +1 -0
  26. data/test/data/models.rb +68 -0
  27. data/test/data/schema.rb +108 -0
  28. data/test/globalize3/attributes_test.rb +133 -0
  29. data/test/globalize3/clone_test.rb +58 -0
  30. data/test/globalize3/dirty_tracking_test.rb +61 -0
  31. data/test/globalize3/dynamic_finders_test.rb +171 -0
  32. data/test/globalize3/fallbacks_test.rb +146 -0
  33. data/test/globalize3/locale_test.rb +81 -0
  34. data/test/globalize3/migration_test.rb +156 -0
  35. data/test/globalize3/set_translations_test.rb +54 -0
  36. data/test/globalize3/translation_class_test.rb +59 -0
  37. data/test/globalize3/validations_test.rb +92 -0
  38. data/test/globalize3/versioning_test.rb +87 -0
  39. data/test/globalize3_test.rb +159 -0
  40. data/test/i18n/missing_translations_test.rb +35 -0
  41. data/test/test_helper.rb +105 -0
  42. metadata +243 -0
@@ -0,0 +1,206 @@
1
+ h1. Globalize3
2
+
3
+ Globalize3 is the successor of Globalize for Rails. Globalize is targeted at ActiveRecord 3. It is compatible with and builds on the new "I18n API in Ruby on Rails":http://guides.rubyonrails.org/i18n.html and adds model translations to ActiveRecord.
4
+
5
+ Globalize3 is much more lightweight and compatible than its predecessor Globalize for Rails was. Model translations in Globalize3 use default ActiveRecord features and do not limit any ActiveRecord functionality any more.
6
+
7
+ h2. Requirements
8
+
9
+ ActiveRecord > 3.0.0.rc
10
+ I18n
11
+
12
+ h2. Installation
13
+
14
+ To install Globalize3 with its default setup just use:
15
+
16
+ <pre><code>
17
+ gem globalize3
18
+ </code></pre>
19
+
20
+ h2. Model translations
21
+
22
+ Model translations allow you to translate your models' attribute values. E.g.
23
+
24
+ h3. New schema
25
+
26
+ <pre><code>
27
+ class Post < ActiveRecord::Base
28
+ translates "title:string", "content:text"
29
+ end
30
+ </code></pre>
31
+
32
+ h3. Old schema
33
+
34
+ <pre><code>
35
+ class Post < ActiveRecord::Base
36
+ translates :title, :text
37
+ end
38
+ </code></pre>
39
+
40
+ Allows you to translate the attributes :title and :text per locale:
41
+
42
+ <pre><code>
43
+ I18n.locale = :en
44
+ post.title # => Globalize3 rocks!
45
+
46
+ I18n.locale = :he
47
+ post.title # => גלובאלייז2 שולט!
48
+ </code></pre>
49
+
50
+ In order to make this work, you'll need to add the appropriate translation tables. Globalize3 comes with a handy helper method to help you do this. It's called @create_translation_table!@. Here's an example:
51
+
52
+ h3. New schema
53
+
54
+ Synchronize translated columns with database (create/drop table or add/change/remove column)
55
+
56
+ <pre><code>
57
+ rake db:globalize:up
58
+ </code></pre>
59
+
60
+ Drop all globalize translations tables (but be careful: non-refundable with backup)
61
+
62
+ <pre><code>
63
+ rake db:globalize:down
64
+ </code></pre>
65
+
66
+ h3. Old schema
67
+
68
+ <pre><code>
69
+ class CreatePosts < ActiveRecord::Migration
70
+ def self.up
71
+ create_table :posts do |t|
72
+ t.timestamps
73
+ end
74
+ Post.create_translation_table! :title => :string, :text => :text
75
+ end
76
+ def self.down
77
+ drop_table :posts
78
+ Post.drop_translation_table!
79
+ end
80
+ end
81
+ </code></pre>
82
+
83
+ Note that the ActiveRecord model @Post@ must already exist and have a @translates@ directive listing the translated fields.
84
+
85
+ h2. Migrating existing data to and from the translated version
86
+
87
+ As well as creating a translation table, you can also use @create_translation_table!@ to migrate across any
88
+ existing data to the default locale. This can also operate in reverse to restore any translations from the default locale
89
+ back to the model when you don't want to use a translation table anymore using @drop_translation_table!@
90
+
91
+ This feature makes use of @untranslated_attributes@ which allows access to the model's attributes as they were before
92
+ the translation was applied. Here's an example (which assumes you already have a model called @Post@ and its table exists):
93
+
94
+ <pre><code>
95
+ class TranslatePosts < ActiveRecord::Migration
96
+ def self.up
97
+ Post.create_translation_table!({
98
+ :title => :string,
99
+ :text => :text
100
+ }, {
101
+ :migrate_data => true
102
+ })
103
+ end
104
+ def self.down
105
+ Post.drop_translation_table! :migrate_data => true
106
+ end
107
+ end
108
+ </code></pre>
109
+
110
+ h2. Versioning with Globalize3
111
+
112
+ Globalize3 nicely integrates with "vestal_versions":http://github.com/laserlemon/vestal_versions:
113
+
114
+ <pre><code>
115
+ require 'globalize/versioning/vestal_versions'
116
+ </code></pre>
117
+
118
+ As of writing (2010-08-05) the original vestal_versions respository has not been updated to be compatible with Rails 3 though. You can use "this fork":http://github.com/svenfuchs/vestal_versions though. "Globalize3's Gemfile":http://github.com/svenfuchs/globalize3/blob/master/Gemfile#L10 is currently set up accordingly.
119
+
120
+ Please also note that @update_attribute@ currently hides itself from dirty tracking in ActiveRecord >= 3.0.0.beta (which is considered a "regression":http://github.com/rails/rails/commit/01629d180468049d17a8be6900e27a4f0d2b18c4#commitcomment-123199). That means that you currently need to use attribute writers or @update_attributes@ in order to track changes/versions for your models.
121
+
122
+ Also, please see the tests in test/globalize3/versioning_test.rb for some current gotchas.
123
+
124
+ h2. I18n fallbacks for empty translations
125
+
126
+ It is possible to enable fallbacks for empty translations. It will depend on the configuration setting you have set for I18n translations in your Rails config.
127
+
128
+ You can enable them by adding the next line to @config/application.rb@ (or only @config/environments/production.rb@ if you only want them in production)
129
+
130
+ <pre><code>config.i18n.fallbacks = true</code></pre>
131
+
132
+ By default, globalize3 will only use fallbacks when your translation model does not exist or the translation value for the item you've requested is @nil@. However it is possible to also use fallbacks for @blank@ translations by adding @:fallbacks_for_empty_translations => true@ to the @translates@ method.
133
+
134
+ <pre><code>
135
+ class Post < ActiveRecord::Base
136
+ translates :title, :name
137
+ end
138
+
139
+ puts post.translations.inspect
140
+ # => [#<Post::Translation id: 1, post_id: 1, locale: "en", title: "Globalize3 rocks!", name: "Globalize3">, #<Post::Translation id: 2, post_id: 1, locale: "nl", title: '', name: nil>]
141
+
142
+ I18n.locale = :en
143
+ post.title # => 'Globalize3 rocks!'
144
+ post.name # => 'Globalize3'
145
+
146
+ I18n.locale = :nl
147
+ post.title # => ''
148
+ post.name # => 'Globalize3'
149
+ </code></pre>
150
+ <pre><code>
151
+ class Post < ActiveRecord::Base
152
+ translates :title, :name, :fallbacks_for_empty_translations => true
153
+ end
154
+
155
+ puts post.translations.inspect
156
+ # => [#<Post::Translation id: 1, post_id: 1, locale: "en", title: "Globalize3 rocks!", name: "Globalize3">, #<Post::Translation id: 2, post_id: 1, locale: "nl", title: '', name: nil>]
157
+
158
+ I18n.locale = :en
159
+ post.title # => 'Globalize3 rocks!'
160
+ post.name # => 'Globalize3'
161
+
162
+ I18n.locale = :nl
163
+ post.title # => 'Globalize3 rocks!'
164
+ post.name # => 'Globalize3'
165
+ </code></pre>
166
+
167
+
168
+ h2. Scoping objects by those with translations
169
+
170
+ To only return objects that have a translation for the given locale we can use the `with_translations` scope. This will only return records that have a translations for the passed in locale.
171
+
172
+ <pre><code>
173
+
174
+ Post.with_translations('en') # => [#<Post::Translation id: 1, post_id: 1, locale: "en", title: "Globalize3 rocks!", name: "Globalize3">, #<Post::Translation id: 2, post_id: 1, locale: "nl", title: '', name: nil>]
175
+
176
+ Post.with_translations(I18n.locale) # => [#<Post::Translation id: 1, post_id: 1, locale: "en", title: "Globalize3 rocks!", name: "Globalize3">, #<Post::Translation id: 2, post_id: 1, locale: "nl", title: '', name: nil>]
177
+
178
+ Post.with_translations('de') # => []
179
+
180
+ </code></pre>
181
+
182
+ h2. Changes since Globalize2
183
+
184
+ * `translation_table_name` was renamed to `translations_table_name`
185
+ * `available_locales` has been removed. please use `translated_locales`
186
+
187
+ h2. Migration from Globalize for Rails (version 1)
188
+
189
+ See this script by Tomasz Stachewicz: http://gist.github.com/120867
190
+
191
+ h2. Alternative Solutions
192
+
193
+ * "Veger's fork":http://github.com/veger/globalize2 - uses default AR schema for the default locale, delegates to the translations table for other locales only
194
+ * "TranslatableColumns":http://github.com/iain/translatable_columns - have multiple languages of the same attribute in a model (Iain Hecker)
195
+ * "localized_record":http://github.com/glennpow/localized_record - allows records to have localized attributes without any modifications to the database (Glenn Powell)
196
+ * "model_translations":http://github.com/janne/model_translations - Minimal implementation of Globalize2 style model translations (Jan Andersson)
197
+
198
+ h2. Related solutions
199
+
200
+ * "globalize2_versioning":http://github.com/joshmh/globalize2_versioning - acts_as_versioned style versioning for globalize2 (Joshua Harvey)
201
+ * "i18n_multi_locales_validations":http://github.com/ZenCocoon/i18n_multi_locales_validations - multi-locales attributes validations to validates attributes from globalize2 translations models (Sébastien Grosjean)
202
+ * "globalize2 Demo App":http://github.com/svenfuchs/globalize2-demo - demo application for globalize2 (Sven Fuchs)</li>
203
+ * "migrate_from_globalize1":http://gist.github.com/120867 - migrate model translations from Globalize1 to globalize2 (Tomasz Stachewicz)</li>
204
+ * "easy_globalize2_accessors":http://github.com/astropanic/easy_globalize2_accessors - easily access (read and write) globalize2-translated fields (astropanic, Tomasz Stachewicz)</li>
205
+ * "globalize2-easy-translate":http://github.com/bsamman/globalize2-easy-translate - adds methods to easily access or set translated attributes to your model (bsamman)</li>
206
+ * "batch_translations":http://github.com/alvarezrilla/batch_translations - allow saving multiple globalize2 translations in the same request (Jose Alvarez Rilla)</li>
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Run all tests.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'Globalize3'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
@@ -0,0 +1,59 @@
1
+ require 'active_record'
2
+ require 'patches/active_record/xml_attribute_serializer'
3
+ require 'patches/active_record/query_method'
4
+
5
+ module Globalize
6
+ autoload :ActiveRecord, 'globalize/active_record'
7
+ autoload :Versioning, 'globalize/versioning'
8
+ autoload :Utils, 'globalize/utils'
9
+
10
+ mattr_accessor :available_locales
11
+
12
+ class << self
13
+ def locale
14
+ read_locale || I18n.locale
15
+ end
16
+
17
+ def locale=(locale)
18
+ set_locale(locale)
19
+ end
20
+
21
+ def with_locale(locale, &block)
22
+ previous_locale = read_locale
23
+ set_locale(locale)
24
+ result = yield(locale)
25
+ set_locale(previous_locale)
26
+ result
27
+ end
28
+
29
+ def with_locales(*locales, &block)
30
+ locales.flatten.map do |locale|
31
+ with_locale(locale, &block)
32
+ end
33
+ end
34
+
35
+ def fallbacks?
36
+ I18n.respond_to?(:fallbacks)
37
+ end
38
+
39
+ def fallbacks(locale = self.locale)
40
+ fallbacks? ? I18n.fallbacks[locale] : [locale.to_sym]
41
+ end
42
+
43
+ def available_locales
44
+ @@available_locales || I18n.backend.available_locales
45
+ end
46
+
47
+ protected
48
+
49
+ def read_locale
50
+ Thread.current[:globalize_locale]
51
+ end
52
+
53
+ def set_locale(locale)
54
+ Thread.current[:globalize_locale] = locale.to_sym rescue nil
55
+ end
56
+ end
57
+ end
58
+
59
+ require 'globalize/engine'
@@ -0,0 +1,13 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ autoload :ActMacro, 'globalize/active_record/act_macro'
4
+ autoload :Adapter, 'globalize/active_record/adapter'
5
+ autoload :Attributes, 'globalize/active_record/attributes'
6
+ autoload :ClassMethods, 'globalize/active_record/class_methods'
7
+ autoload :Exceptions, 'globalize/active_record/exceptions'
8
+ autoload :InstanceMethods, 'globalize/active_record/instance_methods'
9
+ autoload :Migration, 'globalize/active_record/migration'
10
+ autoload :Translation, 'globalize/active_record/translation'
11
+ autoload :Accessors, 'globalize/active_record/accessors'
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module Accessors
4
+ def self.included(base)
5
+ base.class_eval do
6
+ translated_attribute_names.each do |attr_name|
7
+ Globalize.available_locales.each do |locale|
8
+ define_method :"#{attr_name}_#{locale}" do
9
+ read_attribute(attr_name, {:locale => locale})
10
+ end
11
+
12
+ define_method :"#{attr_name}_#{locale}=" do |value|
13
+ changed_attributes[:"#{attr_name}_#{locale}"] = value unless value == read_attribute(attr_name, locale)
14
+ write_attribute(attr_name, value, {:locale => locale})
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,67 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module ActMacro
4
+ # Translated attributes
5
+ # Example:
6
+ # class Post < ActiveRecord::Base
7
+ # translates "title:string", "content:text", :versioning => true, :table_name => "..."
8
+ # end
9
+ #
10
+ def translates(*attr_columns)
11
+ return if translates?
12
+
13
+ options = attr_columns.extract_options!
14
+ options[:table_name] ||= "#{table_name.singularize}_translations"
15
+
16
+ attrs_hash = Utils.convert_columns(attr_columns)
17
+ attr_names = attrs_hash.keys
18
+
19
+ class_attribute :translated_attribute_names, :translation_options,
20
+ :fallbacks_for_empty_translations, :translated_columns_hash
21
+
22
+ self.translated_attribute_names = attr_names.map(&:to_sym)
23
+ self.translation_options = options
24
+ self.translated_columns_hash = attrs_hash
25
+ self.fallbacks_for_empty_translations = options[:fallbacks_for_empty_translations]
26
+
27
+ include InstanceMethods, Accessors
28
+ extend ClassMethods, Migration
29
+
30
+ has_many :translations, :class_name => translation_class.name,
31
+ :foreign_key => class_name.foreign_key,
32
+ :dependent => :destroy,
33
+ :extend => HasManyExtensions
34
+
35
+ before_save :update_checkers!
36
+ after_create :save_translations!
37
+ after_update :save_translations!
38
+
39
+ if options[:versioning]
40
+ ::ActiveRecord::Base.extend(Globalize::Versioning::PaperTrail)
41
+
42
+ translation_class.has_paper_trail
43
+ delegate :version, :versions, :to => :translation
44
+ end
45
+
46
+ attr_names.each { |attr_name| translated_attr_accessor(attr_name) }
47
+ end
48
+
49
+ def class_name
50
+ @class_name ||= begin
51
+ class_name = table_name[table_name_prefix.length..-(table_name_suffix.length + 1)].downcase.camelize
52
+ pluralize_table_names ? class_name.singularize : class_name
53
+ end
54
+ end
55
+
56
+ def translates?
57
+ included_modules.include?(InstanceMethods)
58
+ end
59
+ end
60
+
61
+ module HasManyExtensions
62
+ def find_or_initialize_by_locale(locale)
63
+ with_locale(locale.to_s).first || build(:locale => locale.to_s)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,101 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ class Adapter
4
+ # The cache caches attributes that already were looked up for read access.
5
+ # The stash keeps track of new or changed values that need to be saved.
6
+ attr_accessor :record, :stash, :translations
7
+ private :record=, :stash=
8
+
9
+ delegate :translation_class, :to => :'record.class'
10
+
11
+ def initialize(record)
12
+ self.record = record
13
+ self.stash = Attributes.new
14
+ end
15
+
16
+ def fetch_stash(locale, name)
17
+ value = stash.read(locale, name)
18
+ return value if value
19
+ return nil
20
+ end
21
+
22
+ def fetch(locale, name)
23
+ Globalize.fallbacks(locale).each do |fallback|
24
+ value = fetch_stash(fallback, name) || fetch_attribute(fallback, name)
25
+
26
+ unless fallbacks_for?(value)
27
+ set_metadata(value, :locale => fallback, :requested_locale => locale)
28
+ return value
29
+ end
30
+ end
31
+ return nil
32
+ end
33
+
34
+ def write(locale, name, value)
35
+ stash.write(locale, name, value)
36
+ end
37
+
38
+ def save_translations!
39
+ stash.each do |locale, attrs|
40
+ translation = record.translations.find_or_initialize_by_locale(locale.to_s)
41
+ attrs.each { |name, value| translation[name] = value }
42
+ translation.save!
43
+ end
44
+ record.translations.reset
45
+ stash.clear
46
+ end
47
+
48
+ def reset
49
+ stash.clear
50
+ end
51
+
52
+ def all_blank?(locale, attrs)
53
+ [attrs].flatten.collect { |name| fetch(locale, name).blank? }.all?
54
+ end
55
+
56
+ protected
57
+
58
+ def type_cast(name, value)
59
+ if value.nil?
60
+ nil
61
+ elsif column = column_for_attribute(name)
62
+ column.type_cast(value)
63
+ else
64
+ value
65
+ end
66
+ end
67
+
68
+ def column_for_attribute(name)
69
+ translation_class.columns_hash[name.to_s]
70
+ end
71
+
72
+ def unserializable_attribute?(name, column)
73
+ column.text? && translation_class.serialized_attributes[name.to_s]
74
+ end
75
+
76
+ def fetch_attribute(locale, name)
77
+ translation = record.translation_for(locale)
78
+ return translation && translation.send(name)
79
+ end
80
+
81
+ def set_metadata(object, metadata)
82
+ object.translation_metadata.merge!(meta_data) if object.respond_to?(:translation_metadata)
83
+ object
84
+ end
85
+
86
+ def translation_metadata_accessor(object)
87
+ return if obj.respond_to?(:translation_metadata)
88
+ class << object; attr_accessor :translation_metadata end
89
+ object.translation_metadata ||= {}
90
+ end
91
+
92
+ def fallbacks_for?(object)
93
+ object.nil? || (fallbacks_for_empty_translations? && object.blank?)
94
+ end
95
+
96
+ def fallbacks_for_empty_translations?
97
+ record.fallbacks_for_empty_translations
98
+ end
99
+ end
100
+ end
101
+ end