single_table_globalize3 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4d9d9b6bb1b7c9a01b80fa37c207d172b26bf321
4
+ data.tar.gz: 1f6344d88c60c9a226491d3280b2e23ae639e621
5
+ SHA512:
6
+ metadata.gz: 3c82d94486a79cd745cdba46ca13463ad2cda67b321b654263c9e598a4bdb3422a9e286125c61407827c2d55068d1558b0b0c5d421a3b9302ce7ffdce0f2f45e
7
+ data.tar.gz: b22a0c72c1cad41885978ebe89b389afbd727e3bdeb230467beea39f2a3e54c694c76a6be52f30583a8cc01fe820fa077b7ab71b5456a1152a3200ff4050d74d
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
data/Gemfile.lock ADDED
@@ -0,0 +1,61 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ single_table_globalize3 (0.3.0)
5
+ activemodel (>= 3.0.0)
6
+ activerecord (>= 3.0.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activemodel (3.2.12)
12
+ activesupport (= 3.2.12)
13
+ builder (~> 3.0.0)
14
+ activerecord (3.2.12)
15
+ activemodel (= 3.2.12)
16
+ activesupport (= 3.2.12)
17
+ arel (~> 3.0.2)
18
+ tzinfo (~> 0.3.29)
19
+ activesupport (3.2.12)
20
+ i18n (~> 0.6)
21
+ multi_json (~> 1.0)
22
+ arel (3.0.2)
23
+ builder (3.0.4)
24
+ coderay (1.0.8)
25
+ database_cleaner (0.6.7)
26
+ i18n (0.6.1)
27
+ json (1.7.7)
28
+ metaclass (0.0.1)
29
+ method_source (0.8.1)
30
+ mocha (0.13.2)
31
+ metaclass (~> 0.0.1)
32
+ multi_json (1.6.1)
33
+ pathname_local (0.0.2)
34
+ pry (0.9.12)
35
+ coderay (~> 1.0.5)
36
+ method_source (~> 0.8)
37
+ slop (~> 3.4)
38
+ pry-nav (0.2.3)
39
+ pry (~> 0.9.10)
40
+ rake (10.0.3)
41
+ rdoc (3.12.1)
42
+ json (~> 1.4)
43
+ slop (3.4.3)
44
+ sqlite3 (1.3.7)
45
+ test_declarative (0.0.5)
46
+ tzinfo (0.3.35)
47
+
48
+ PLATFORMS
49
+ ruby
50
+
51
+ DEPENDENCIES
52
+ database_cleaner (~> 0.6.0)
53
+ mocha
54
+ pathname_local
55
+ pry
56
+ pry-nav
57
+ rake
58
+ rdoc
59
+ single_table_globalize3!
60
+ sqlite3
61
+ test_declarative
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2008, 2009, 2010, 2012 Sven Fuchs, Joshua Harvey, Clemens Kofler, John-Paul Bader, Trong Tran
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rdoc/task'
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 = 'SingleTableGlobalize3'
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,14 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module SingleTableGlobalize3
4
+ module Generators
5
+ class MigrationGenerator < ActiveRecord::Generators::Base
6
+ source_root File.expand_path("../templates", __FILE__)
7
+ argument :name, :type => :string, :default => "application"
8
+
9
+ def copy_devise_migration
10
+ migration_template "migration.rb", "db/migrate/create_globalize_translations"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ class CreateGlobalizeTranslations < ActiveRecord::Migration
2
+ def up
3
+ create_table :globalize_translations do |t|
4
+ t.integer :translatable_id
5
+ t.string :translatable_type
6
+ t.string :locale
7
+ t.string :attribute_name
8
+ t.text :value
9
+ t.timestamps
10
+ end
11
+ add_index :globalize_translations, [:translatable_id, :translatable_type], :name => :index_globalize_translalation_1
12
+ add_index :globalize_translations, [:translatable_id, :translatable_type, :locale], :name => :index_globalize_translalation_2
13
+ add_index :globalize_translations, [:translatable_id, :translatable_type, :locale, :attribute_name], :name => :index_globalize_translalation_3
14
+ end
15
+
16
+ def down
17
+ remove_index :globalize_translations, :name => :index_globalize_translalation_3
18
+ remove_index :globalize_translations, :name => :index_globalize_translalation_2
19
+ remove_index :globalize_translations, :name => :index_globalize_translalation_1
20
+ drop_table :globalize_translations
21
+ end
22
+ end
@@ -0,0 +1,66 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module ActMacro
4
+ def translates(*attr_names)
5
+
6
+ options = attr_names.extract_options!
7
+ setup_translates!(options) unless translates?
8
+
9
+ attr_names = attr_names.map(&:to_sym)
10
+ attr_names -= translated_attribute_names if defined?(translated_attribute_names)
11
+
12
+ if attr_names.present?
13
+ translation_class.instance_eval %{
14
+ attr_accessible :#{attr_names.join(', :')}
15
+ }
16
+
17
+ attr_names.each do |attr_name|
18
+ # Detect and apply serialization.
19
+ serializer = self.serialized_attributes[attr_name.to_s]
20
+ if serializer.present?
21
+ if defined?(::ActiveRecord::Coders::YAMLColumn) &&
22
+ serializer.is_a?(::ActiveRecord::Coders::YAMLColumn)
23
+
24
+ serializer = serializer.object_class
25
+ end
26
+
27
+ translation_class.send :serialize, attr_name, serializer
28
+ end
29
+
30
+ # Create accessors for the attribute.
31
+ translated_attr_accessor(attr_name)
32
+ translations_accessor(attr_name)
33
+
34
+ # Add attribute to the list.
35
+ self.translated_attribute_names << attr_name
36
+ end
37
+ end
38
+ end
39
+
40
+ def translates?
41
+ included_modules.include?(InstanceMethods)
42
+ end
43
+
44
+ protected
45
+ def setup_translates!(options)
46
+ class_attribute :translated_attribute_names, :translation_options, :fallbacks_for_empty_translations
47
+ self.translated_attribute_names = []
48
+ self.translation_options = options
49
+ self.fallbacks_for_empty_translations = options[:fallbacks_for_empty_translations]
50
+
51
+ include InstanceMethods
52
+ extend ClassMethods
53
+
54
+ has_many :translations, :class_name => translation_class.name,
55
+ :foreign_key => :translatable_id,
56
+ :dependent => :destroy,
57
+ :as => :translatable
58
+
59
+ after_create :save_translations!
60
+ after_update :save_translations!
61
+
62
+ translation_class.instance_eval %{ attr_accessible :locale }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,112 @@
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 stash_contains?(locale, name)
23
+ stash.contains?(locale, name)
24
+ end
25
+
26
+ def fetch(locale, name)
27
+ record.globalize_fallbacks(locale).each do |fallback|
28
+ value = stash.contains?(fallback, name) ? fetch_stash(fallback, name) : fetch_attribute(fallback, name)
29
+
30
+ unless fallbacks_for?(value)
31
+ set_metadata(value, :locale => fallback, :requested_locale => locale)
32
+ return value
33
+ end
34
+ end
35
+
36
+ return nil
37
+ end
38
+
39
+ def write(locale, name, value)
40
+ stash.write(locale, name, value)
41
+ end
42
+
43
+ def save_translations!
44
+ existing_translations = {}
45
+ record.translations.each do |t|
46
+ locale_str = t.locale.to_s
47
+ existing_translations[locale_str] ||= {}
48
+ existing_translations[locale_str][t.attribute_name] = t
49
+ end
50
+
51
+ stash.each do |locale, attrs|
52
+ locale_str = locale.to_s
53
+ attrs.each do |name, value|
54
+ translation = existing_translations[locale_str].try('[]', name) ||
55
+ record.translations.build(:locale => locale_str, :attribute_name => name)
56
+ translation.update_attribute(:value, value)
57
+ end
58
+ end
59
+
60
+ reset
61
+ end
62
+
63
+ def reset
64
+ stash.clear
65
+ end
66
+
67
+ protected
68
+
69
+ def type_cast(name, value)
70
+ if value.nil?
71
+ nil
72
+ elsif column = column_for_attribute(name)
73
+ column.type_cast(value)
74
+ else
75
+ value
76
+ end
77
+ end
78
+
79
+ def column_for_attribute(name)
80
+ translation_class.columns_hash[name.to_s]
81
+ end
82
+
83
+ def unserializable_attribute?(name, column)
84
+ column.text? && translation_class.serialized_attributes[name.to_s]
85
+ end
86
+
87
+ def fetch_attribute(locale, name)
88
+ translation = record.translation_for(locale, name, false)
89
+ return translation && translation.value
90
+ end
91
+
92
+ def set_metadata(object, metadata)
93
+ object.translation_metadata.merge!(metadata) if object.respond_to?(:translation_metadata)
94
+ object
95
+ end
96
+
97
+ def translation_metadata_accessor(object)
98
+ return if obj.respond_to?(:translation_metadata)
99
+ class << object; attr_accessor :translation_metadata end
100
+ object.translation_metadata ||= {}
101
+ end
102
+
103
+ def fallbacks_for?(object)
104
+ object.nil? || (fallbacks_for_empty_translations? && object.blank?)
105
+ end
106
+
107
+ def fallbacks_for_empty_translations?
108
+ record.fallbacks_for_empty_translations
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,27 @@
1
+ # Helper class for storing values per locale. Used by Globalize::Adapter
2
+ # to stash and cache attribute values.
3
+
4
+ module Globalize
5
+ module ActiveRecord
6
+ class Attributes < Hash # TODO: Think about using HashWithIndifferentAccess ?
7
+ def [](locale)
8
+ locale = locale.to_sym
9
+ self[locale] = {} unless has_key?(locale)
10
+ self.fetch(locale)
11
+ end
12
+
13
+ def contains?(locale, name)
14
+ self[locale].has_key?(name.to_s)
15
+ end
16
+
17
+ def read(locale, name)
18
+ self[locale][name.to_s]
19
+ end
20
+
21
+ def write(locale, name, value)
22
+ #raise 'z' if value.nil? # TODO
23
+ self[locale][name.to_s] = value
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,168 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module ClassMethods
4
+ delegate :translated_locales, :to => :translation_class
5
+
6
+ def with_translations(*locales)
7
+ locales = [locales].flatten
8
+ index = locales.pop if locales.last.is_a?(Fixnum)
9
+ locales = Globalize.fallbacks if locales.empty?
10
+ joins("LEFT OUTER JOIN #{translation_class.table_name} #{translations_table_name(index)} ON #{translations_table_name(index)}.translatable_id = #{self.table_name}.id").
11
+ select("distinct #{table_name}.*").
12
+ where(translated_column_name('locale', index) => locales)
13
+ end
14
+
15
+ def with_translated_attribute(name, value, locales = nil)
16
+ locales ||= Globalize.fallbacks
17
+ self.join_index = self.join_index + 1
18
+ with_translations(locales, self.join_index).where(
19
+ translated_column_name('attribute_name', self.join_index) => name.to_s,
20
+ translated_column_name('value', self.join_index) => Array(value).map(&:to_s)
21
+ )
22
+ end
23
+
24
+ def translated?(name)
25
+ translated_attribute_names.include?(name.to_sym)
26
+ end
27
+
28
+ def required_attributes
29
+ validators.map { |v| v.attributes if v.is_a?(ActiveModel::Validations::PresenceValidator) }.flatten
30
+ end
31
+
32
+ def required_translated_attributes
33
+ translated_attribute_names & required_attributes
34
+ end
35
+
36
+ def translation_class
37
+ Globalize::ActiveRecord::Translation
38
+ end
39
+
40
+ def translations_table_name(index = nil)
41
+ "#{translation_class.table_name}#{index}"
42
+ end
43
+
44
+ def translated_column_name(name, index = nil)
45
+ "#{translations_table_name(index)}.#{name}"
46
+ end
47
+
48
+ if RUBY_VERSION < '1.9'
49
+ def respond_to?(method_id, *args, &block)
50
+ supported_on_missing?(method_id) || super
51
+ end
52
+ else
53
+ def respond_to_missing?(method_id, include_private = false)
54
+ supported_on_missing?(method_id) || super
55
+ end
56
+ end
57
+
58
+ def supported_on_missing?(method_id)
59
+ return super unless RUBY_VERSION < '1.9' || respond_to?(:translated_attribute_names)
60
+ match = defined?(::ActiveRecord::DynamicFinderMatch) && (::ActiveRecord::DynamicFinderMatch.match(method_id) || ::ActiveRecord::DynamicScopeMatch.match(method_id))
61
+ return false if match.nil?
62
+
63
+ attribute_names = match.attribute_names.map(&:to_sym)
64
+ translated_attributes = attribute_names & translated_attribute_names
65
+ return false if translated_attributes.empty?
66
+
67
+ untranslated_attributes = attribute_names - translated_attributes
68
+ return false if untranslated_attributes.any?{|unt| ! respond_to?(:"scoped_by_#{unt}")}
69
+ return [match, attribute_names, translated_attributes, untranslated_attributes]
70
+ end
71
+
72
+ def method_missing(method_id, *arguments, &block)
73
+ match, attribute_names, translated_attributes, untranslated_attributes = supported_on_missing?(method_id)
74
+ return super unless match
75
+
76
+ scope = scoped
77
+
78
+ translated_attributes.each do |attr|
79
+ scope = scope.with_translated_attribute(attr, arguments[attribute_names.index(attr)])
80
+ end
81
+
82
+ untranslated_attributes.each do |unt|
83
+ index = attribute_names.index(unt)
84
+ raise StandarError unless index
85
+ scope = scope.send(:"scoped_by_#{unt}", arguments[index])
86
+ end
87
+
88
+ if defined?(::ActiveRecord::DynamicFinderMatch) && match.is_a?(::ActiveRecord::DynamicFinderMatch)
89
+ if match.instantiator? and scope.blank?
90
+ return scope.find_or_instantiator_by_attributes match, attribute_names, *arguments, &block
91
+ end
92
+ match_finder_method = match.finder.to_s
93
+ match_finder_method << "!" if match.bang? && ::ActiveRecord::VERSION::STRING >= "3.1.0"
94
+ return scope.send(match_finder_method).tap do |found|
95
+ found.is_a?(Array) ? found.map { |f| f.translations.reload } : found.translations.reload unless found.nil?
96
+ end
97
+ end
98
+ return scope
99
+ end
100
+
101
+ def find_or_instantiator_by_attributes(match, attributes, *args)
102
+ options = args.size > 1 && args.last(2).all?{ |a| a.is_a?(Hash) } ? args.extract_options! : {}
103
+ protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
104
+ args.each_with_index do |arg, i|
105
+ if arg.is_a?(Hash)
106
+ protected_attributes_for_create = args[i].with_indifferent_access
107
+ else
108
+ unprotected_attributes_for_create[attributes[i]] = args[i]
109
+ end
110
+ end
111
+
112
+ record = if ::ActiveRecord::VERSION::STRING < "3.1.0"
113
+ new do |r|
114
+ r.send(:attributes=, protected_attributes_for_create, true) unless protected_attributes_for_create.empty?
115
+ r.send(:attributes=, unprotected_attributes_for_create, false) unless unprotected_attributes_for_create.empty?
116
+ end
117
+ else
118
+ new(protected_attributes_for_create, options) do |r|
119
+ r.assign_attributes(unprotected_attributes_for_create, :without_protection => true)
120
+ end
121
+ end
122
+ yield(record) if block_given?
123
+ record.send(match.bang? ? :save! : :save) if match.instantiator.eql?(:create)
124
+
125
+ record
126
+ end
127
+
128
+ protected
129
+
130
+ def translated_attr_accessor(name)
131
+ define_method(:"#{name}=") do |value|
132
+ write_attribute(name, value)
133
+ end
134
+ define_method(name) do |*args|
135
+ read_attribute(name, {:locale => args.first})
136
+ end
137
+ alias_method :"#{name}_before_type_cast", name
138
+ end
139
+
140
+ def translations_accessor(name)
141
+ define_method(:"#{name}_translations") do
142
+ result = translations.each_with_object(HashWithIndifferentAccess.new) do |translation, result|
143
+ result[translation.locale] = translation.value
144
+ end
145
+ globalize.stash.keys.each_with_object(result) do |locale, result|
146
+ result[locale] = globalize.fetch_stash(locale, name) if globalize.stash_contains?(locale, name)
147
+ end
148
+ end
149
+ define_method(:"#{name}_translations=") do |value|
150
+ value.each do |(locale, value)|
151
+ write_attribute name, value, :locale => locale
152
+ end
153
+ end
154
+ end
155
+
156
+ def join_index
157
+ @join_index = 0 if @join_index.nil? || @join_index > 100
158
+ @join_index
159
+ end
160
+
161
+ def join_index=(value)
162
+ @join_index = value
163
+ end
164
+ end
165
+
166
+ end
167
+
168
+ end
@@ -0,0 +1,19 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module Exceptions
4
+ class MigrationError < StandardError; end
5
+
6
+ class BadFieldName < MigrationError
7
+ def initialize(field)
8
+ super("Missing translated field #{field.inspect}")
9
+ end
10
+ end
11
+
12
+ class BadFieldType < MigrationError
13
+ def initialize(name, type)
14
+ super("Bad field type for field #{name.inspect} (#{type.inspect}), should be :string or :text")
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,210 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module InstanceMethods
4
+ delegate :translated_locales, :to => :translations
5
+
6
+ def globalize
7
+ @globalize ||= Adapter.new(self)
8
+ end
9
+
10
+ def attributes
11
+ super.merge(translated_attributes)
12
+ end
13
+
14
+ def self.included(base)
15
+ # Maintain Rails 3.0.x compatibility while adding Rails 3.1.x compatibility
16
+ if base.method_defined?(:assign_attributes)
17
+ base.class_eval %{
18
+ def assign_attributes(attributes, options = {})
19
+ with_given_locale(attributes) { super }
20
+ end
21
+ }
22
+ else
23
+ base.class_eval %{
24
+ def attributes=(attributes, *args)
25
+ with_given_locale(attributes) { super }
26
+ end
27
+
28
+ def update_attributes!(attributes, *args)
29
+ with_given_locale(attributes) { super }
30
+ end
31
+
32
+ def update_attributes(attributes, *args)
33
+ with_given_locale(attributes) { super }
34
+ end
35
+ }
36
+ end
37
+ end
38
+
39
+ # Deprecate old use of locale
40
+ def deprecated_options(options)
41
+ unless options.is_a?(Hash)
42
+ warn "[DEPRECATION] passing 'locale' as #{options.inspect} is deprecated. Please use {:locale => #{options.inspect}} instead."
43
+ {:locale => options}
44
+ else
45
+ options
46
+ end
47
+ end
48
+
49
+ def write_attribute(name, value, options = {})
50
+ if translated?(name)
51
+ options = {:locale => Globalize.locale}.merge(deprecated_options(options))
52
+
53
+ # Dirty tracking, paraphrased from
54
+ # ActiveRecord::AttributeMethods::Dirty#write_attribute.
55
+ name_str = name.to_s
56
+ # If there's already a change, delete it if this undoes the change.
57
+ if attribute_changed?(name_str) && value == changed_attributes[name_str]
58
+ changed_attributes.delete(name_str)
59
+ elsif !attribute_changed?(name_str) && value != (old = globalize.fetch(options[:locale], name))
60
+ changed_attributes[name_str] = old
61
+ end
62
+
63
+ globalize.write(options[:locale], name, value)
64
+ else
65
+ super(name, value)
66
+ end
67
+ end
68
+
69
+ def read_attribute(name, options = {})
70
+ options = {:translated => true, :locale => nil}.merge(deprecated_options(options))
71
+ if (self.class.translated?(name) && options[:translated]) && (value = globalize.fetch(options[:locale] || Globalize.locale, name))
72
+ value
73
+ else
74
+ super(name)
75
+ end
76
+ end
77
+
78
+ def attribute_names
79
+ translated_attribute_names.map(&:to_s) + super
80
+ end
81
+
82
+ def translated?(name)
83
+ self.class.translated?(name)
84
+ end
85
+
86
+ def translated_attributes
87
+ translated_attribute_names.inject({}) do |attributes, name|
88
+ attributes.merge(name.to_s => translation[name.to_s].try(:value))
89
+ end
90
+ end
91
+
92
+ # This method is basically the method built into Rails
93
+ # but we have to pass {:translated => false}
94
+ def untranslated_attributes
95
+ attrs = {}
96
+ attribute_names.each do |name|
97
+ attrs[name] = read_attribute(name, {:translated => false})
98
+ end
99
+ attrs
100
+ end
101
+
102
+ def set_translations(options)
103
+ options.keys.each do |locale|
104
+ options[locale].each do |key, value|
105
+ raise NoMethodError.new("unknown attribute: #{key}") unless attribute_names.flatten.include?(key.to_s)
106
+ translation = translation_for(locale, key.to_s)
107
+ translation.value = value
108
+ translation.save
109
+ end
110
+ end
111
+ globalize.reset
112
+ end
113
+
114
+ def reload(options = nil)
115
+ translation_caches.clear
116
+ translated_attribute_names.each { |name| @attributes.delete(name.to_s) }
117
+ globalize.reset
118
+ super(options)
119
+ end
120
+
121
+ def clone
122
+ obj = super
123
+ return obj unless respond_to?(:translated_attribute_names)
124
+
125
+ obj.instance_variable_set(:@translations, nil) if new_record? # Reset the collection because of rails bug: http://pastie.org/1521874
126
+ obj.instance_variable_set(:@globalize, nil )
127
+ each_locale_and_translated_attribute do |locale, name|
128
+ obj.globalize.write(locale, name, globalize.fetch(locale, name) )
129
+ end
130
+
131
+ return obj
132
+ end
133
+
134
+ def translation
135
+ translations_for_locale(::Globalize.locale)
136
+ end
137
+
138
+ def translation_for(locale, name, build_if_missing = true)
139
+ translation_caches[locale] ||= {}
140
+ translation_caches[locale][name] ||= (translations.detect{|t| t.locale == locale && t.attribute_name == name.to_s}) ||
141
+ (translations.attribute(name).with_locale(locale).first) ||
142
+ (translations.build(:locale => locale, :attribute_name => name) if build_if_missing)
143
+ end
144
+
145
+ def translations_for_locale(locale)
146
+ translation_caches[locale] = if translations.loaded?
147
+ translations.select{|t| t.locale.to_s == locale.to_s }
148
+ else
149
+ translations.with_locale(locale)
150
+ end.inject({}) do |hash, t|
151
+ hash.update(t.attribute_name => t)
152
+ end
153
+ end
154
+
155
+ def translation_caches
156
+ @translation_caches ||= {}
157
+ end
158
+
159
+ def globalize_fallbacks(locale)
160
+ Globalize.fallbacks(locale)
161
+ end
162
+
163
+ private
164
+
165
+ def update(*)
166
+ I18n.with_locale(read_attribute(:locale) || I18n.default_locale) do
167
+ super
168
+ end
169
+ end
170
+
171
+ def create(*)
172
+ I18n.with_locale(read_attribute(:locale) || I18n.default_locale) do
173
+ super
174
+ end
175
+ end
176
+
177
+ protected
178
+
179
+ def each_locale_and_translated_attribute
180
+ used_locales.each do |locale|
181
+ translated_attribute_names.each do |name|
182
+ yield locale, name
183
+ end
184
+ end
185
+ end
186
+
187
+ def used_locales
188
+ globalize.stash.keys.concat(globalize.stash.keys).concat(translations.translated_locales).uniq
189
+ end
190
+
191
+ def save_translations!
192
+ globalize.save_translations!
193
+ translation_caches.clear
194
+ end
195
+
196
+ def with_given_locale(attributes, &block)
197
+ attributes.symbolize_keys! if attributes.respond_to?(:symbolize_keys!)
198
+
199
+ locale = respond_to?(:locale=) ? attributes.try(:[], :locale) :
200
+ attributes.try(:delete, :locale)
201
+
202
+ if locale
203
+ Globalize.with_locale(locale, &block)
204
+ else
205
+ yield
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,43 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ class Translation < ::ActiveRecord::Base
4
+ if self.respond_to?(:table_name=)
5
+ self.table_name = :globalize_translations
6
+ else
7
+ set_table_name :globalize_translations
8
+ end
9
+
10
+ attr_accessible :locale, :attribute_name, :value
11
+ validates :locale, :attribute_name, :presence => true
12
+ belongs_to :translatable, :polymorphic => true
13
+
14
+ class << self
15
+ def with_locales(*locales)
16
+ # Avoid using "IN" with SQL queries when only using one locale.
17
+ locales = locales.flatten.map(&:to_s)
18
+ locales = locales.first if locales.one?
19
+ where :locale => locales
20
+ end
21
+ alias with_locale with_locales
22
+
23
+ def translated_locales
24
+ select('DISTINCT locale').map(&:locale).sort { |l,r| l.to_s <=> r.to_s }
25
+ end
26
+
27
+ def attribute(attribute)
28
+ attribute = attribute.to_s
29
+ where :attribute_name => attribute
30
+ end
31
+ end
32
+
33
+ def locale
34
+ _locale = read_attribute :locale
35
+ _locale.present? ? _locale.to_sym : _locale
36
+ end
37
+
38
+ def locale=(locale)
39
+ write_attribute :locale, locale.to_s
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,12 @@
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
+ end
12
+ end
data/lib/globalize.rb ADDED
@@ -0,0 +1,74 @@
1
+ require 'active_record'
2
+ require 'patches/active_record/xml_attribute_serializer'
3
+ require 'patches/active_record/query_method'
4
+ require 'patches/active_record/uniqueness_validator'
5
+
6
+ module Globalize
7
+ autoload :ActiveRecord, 'globalize/active_record'
8
+
9
+ class << self
10
+ def locale
11
+ read_locale || I18n.locale
12
+ end
13
+
14
+ def locale=(locale)
15
+ set_locale(locale)
16
+ end
17
+
18
+ def with_locale(locale, &block)
19
+ previous_locale = read_locale
20
+ set_locale(locale)
21
+ result = yield(locale)
22
+ set_locale(previous_locale)
23
+ result
24
+ end
25
+
26
+ def with_locales(*locales, &block)
27
+ locales.flatten.map do |locale|
28
+ with_locale(locale, &block)
29
+ end
30
+ end
31
+
32
+ def fallbacks=(locales)
33
+ set_fallbacks(locales)
34
+ end
35
+
36
+ def i18n_fallbacks?
37
+ I18n.respond_to?(:fallbacks)
38
+ end
39
+
40
+ def fallbacks(for_locale = self.locale)
41
+ read_fallbacks[for_locale] || default_fallbacks(for_locale)
42
+ end
43
+
44
+ def default_fallbacks(for_locale = self.locale)
45
+ i18n_fallbacks? ? I18n.fallbacks[for_locale] : [for_locale.to_sym]
46
+ end
47
+
48
+ protected
49
+
50
+ def read_locale
51
+ Thread.current[:globalize_locale]
52
+ end
53
+
54
+ def set_locale(locale)
55
+ Thread.current[:globalize_locale] = locale.try(:to_sym)
56
+ end
57
+
58
+ def read_fallbacks
59
+ Thread.current[:fallbacks] || HashWithIndifferentAccess.new
60
+ end
61
+
62
+ def set_fallbacks(locales)
63
+ fallback_hash = HashWithIndifferentAccess.new
64
+
65
+ locales.each do |key, value|
66
+ fallback_hash[key] = value.presence || [key]
67
+ end if locales.present?
68
+
69
+ Thread.current[:fallbacks] = fallback_hash
70
+ end
71
+ end
72
+ end
73
+
74
+ ActiveRecord::Base.extend(Globalize::ActiveRecord::ActMacro)
@@ -0,0 +1,41 @@
1
+ # A simple exception handler that behaves like the default exception handler
2
+ # but additionally logs missing translations to a given log.
3
+ #
4
+ # Useful for identifying missing translations during testing.
5
+ #
6
+ # E.g.
7
+ #
8
+ # require 'globalize/i18n/missing_translations_log_handler'
9
+ # I18n.missing_translations_logger = RAILS_DEFAULT_LOGGER
10
+ # I18n.exception_handler = :missing_translations_log_handler
11
+ #
12
+ # To set up a different log file:
13
+ #
14
+ # logger = Logger.new("#{RAILS_ROOT}/log/missing_translations.log")
15
+ # I18n.missing_translations_logger = logger
16
+
17
+ module I18n
18
+ @@missing_translations_logger = nil
19
+
20
+ class << self
21
+ def missing_translations_logger
22
+ @@missing_translations_logger ||= begin
23
+ require 'logger' unless defined?(Logger)
24
+ Logger.new(STDOUT)
25
+ end
26
+ end
27
+
28
+ def missing_translations_logger=(logger)
29
+ @@missing_translations_logger = logger
30
+ end
31
+
32
+ def missing_translations_log_handler(exception, locale, key, options)
33
+ if MissingTranslationData === exception
34
+ missing_translations_logger.warn(exception.message)
35
+ return exception.message
36
+ else
37
+ raise exception
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ # A simple exception handler that behaves like the default exception handler
2
+ # but also raises on missing translations.
3
+ #
4
+ # Useful for identifying missing translations during testing.
5
+ #
6
+ # E.g.
7
+ #
8
+ # require 'globalize/i18n/missing_translations_raise_handler'
9
+ # I18n.exception_handler = :missing_translations_raise_handler
10
+ module I18n
11
+ class << self
12
+ def missing_translations_raise_handler(exception, locale, key, options)
13
+ raise exception
14
+ end
15
+ end
16
+ end
17
+
18
+ I18n.exception_handler = :missing_translations_raise_handler
19
+
20
+ ActionView::Helpers::TranslationHelper.module_eval do
21
+ def translate(key, options = {})
22
+ I18n.translate(key, options)
23
+ end
24
+ alias :t :translate
25
+ end
@@ -0,0 +1,35 @@
1
+ require 'active_record/attribute_methods/query'
2
+
3
+ module ActiveRecord
4
+ module AttributeMethods
5
+ module Query
6
+ def query_attribute(attr_name)
7
+ unless value = read_attribute(attr_name)
8
+ false
9
+ else
10
+ column = self.class.columns_hash[attr_name]
11
+ if column.nil?
12
+
13
+ # TODO submit a rails patch
14
+
15
+ # not sure what active_record tests say but i guess this should mean:
16
+ # call to_i and check zero? if the value is a Numeric or starts with
17
+ # a digit, so it can meaningfully be typecasted by to_i
18
+
19
+ # if Numeric === value || value !~ /[^0-9]/
20
+ if Numeric === value || value.to_s =~ /^[0-9]/
21
+ !value.to_i.zero?
22
+ else
23
+ return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
24
+ !value.blank?
25
+ end
26
+ elsif column.number?
27
+ !value.zero?
28
+ else
29
+ !value.blank?
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,61 @@
1
+ require 'active_record/validations/uniqueness.rb'
2
+
3
+ ActiveRecord::Validations::UniquenessValidator.class_eval do
4
+ def validate_each_with_translations(record, attribute, value)
5
+ klass = record.class
6
+ if klass.translates? && klass.translated?(attribute)
7
+ if methods.include?(:build_relation) || respond_to?(:build_relation)
8
+ finder_class = klass.translation_class
9
+ table = finder_class.arel_table
10
+
11
+ relation = build_relation(finder_class, table, 'attribute_name', attribute).
12
+ and(table[:locale].eq(Globalize.locale)).
13
+ and(table[:value].eq(value)).
14
+ and(table[:translatable_type].eq(klass.name))
15
+ relation = relation.and(table[klass.reflect_on_association(:translations).foreign_key].not_eq(record.send(:id))) if record.persisted?
16
+
17
+ # TODO: add scope with translated attributes
18
+ if options[:scope]
19
+ ActiveRecord::Base.logger.warn("WARNING: SingleTableGlobalize3 does not currently support `scope` option on uniqueness validation for translated attributes.")
20
+ end
21
+
22
+ if finder_class.unscoped.where(relation).exists?
23
+ record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value))
24
+ end
25
+ else
26
+ finder_class = klass.translation_class
27
+ table = finder_class.unscoped
28
+
29
+ table_name = finder_class.quoted_table_name
30
+
31
+ if value && klass.serialized_attributes.key?(attribute.to_s)
32
+ value = YAML.dump value
33
+ end
34
+
35
+ sql, params = mount_sql_and_params(finder_class, table_name, 'value', value)
36
+
37
+ relation = table.where(sql, *params).
38
+ where(:locale => Globalize.locale).
39
+ where(:attribute_name => attribute).
40
+ where(:translatable_type => klass.name)
41
+
42
+ Array.wrap(options[:scope]).each do |scope_item|
43
+ scope_value = record.send(scope_item)
44
+ relation = relation.where(scope_item => scope_value)
45
+ end
46
+
47
+ if record.persisted?
48
+ fk = klass.reflect_on_association(:translations).options[:foreign_key]
49
+ relation = relation.where(finder_class.arel_table[fk].not_eq(record.send(:id)))
50
+ end
51
+
52
+ if relation.exists?
53
+ record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value))
54
+ end
55
+ end
56
+ else
57
+ validate_each_without_translations(record, attribute, value)
58
+ end
59
+ end
60
+ alias_method_chain :validate_each, :translations
61
+ end
@@ -0,0 +1,13 @@
1
+ require 'active_record/serializers/xml_serializer'
2
+
3
+ ActiveRecord::XmlSerializer::Attribute.class_eval do
4
+ def compute_type_with_translations
5
+ klass = @serializable.class
6
+ if klass.translates? && klass.translated_attribute_names.include?(name.to_sym)
7
+ :string
8
+ else
9
+ compute_type_without_translations
10
+ end
11
+ end
12
+ alias_method_chain :compute_type, :translations
13
+ end
@@ -0,0 +1,3 @@
1
+ module SingleTableGlobalize3
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1 @@
1
+ require 'globalize'
metadata ADDED
@@ -0,0 +1,198 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: single_table_globalize3
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Sven Fuchs
8
+ - Joshua Harvey
9
+ - Clemens Kofler
10
+ - John-Paul Bader
11
+ - Tomasz Stachewicz
12
+ - Philip Arndt
13
+ - Trong Tran
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+ date: 2013-03-04 00:00:00.000000000 Z
18
+ dependencies:
19
+ - !ruby/object:Gem::Dependency
20
+ name: activerecord
21
+ requirement: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - '>='
24
+ - !ruby/object:Gem::Version
25
+ version: 3.0.0
26
+ type: :runtime
27
+ prerelease: false
28
+ version_requirements: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 3.0.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: activemodel
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '>='
38
+ - !ruby/object:Gem::Version
39
+ version: 3.0.0
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '>='
45
+ - !ruby/object:Gem::Version
46
+ version: 3.0.0
47
+ - !ruby/object:Gem::Dependency
48
+ name: database_cleaner
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 0.6.0
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ version: 0.6.0
61
+ - !ruby/object:Gem::Dependency
62
+ name: mocha
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: pathname_local
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: test_declarative
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: sqlite3
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - '>='
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rake
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - '>='
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ - !ruby/object:Gem::Dependency
132
+ name: rdoc
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - '>='
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ description: Rails I18n de-facto standard library for ActiveRecord 3 model/data translation
146
+ with single table.
147
+ email: trong.v.tran@gmail.com
148
+ executables: []
149
+ extensions: []
150
+ extra_rdoc_files: []
151
+ files:
152
+ - lib/generators/globalize3/migration_generator.rb
153
+ - lib/generators/globalize3/templates/migration.rb
154
+ - lib/globalize/active_record/act_macro.rb
155
+ - lib/globalize/active_record/adapter.rb
156
+ - lib/globalize/active_record/attributes.rb
157
+ - lib/globalize/active_record/class_methods.rb
158
+ - lib/globalize/active_record/exceptions.rb
159
+ - lib/globalize/active_record/instance_methods.rb
160
+ - lib/globalize/active_record/translation.rb
161
+ - lib/globalize/active_record.rb
162
+ - lib/globalize.rb
163
+ - lib/i18n/missing_translations_log_handler.rb
164
+ - lib/i18n/missing_translations_raise_handler.rb
165
+ - lib/patches/active_record/query_method.rb
166
+ - lib/patches/active_record/uniqueness_validator.rb
167
+ - lib/patches/active_record/xml_attribute_serializer.rb
168
+ - lib/single_table_globalize3/version.rb
169
+ - lib/single_table_globalize3.rb
170
+ - Gemfile
171
+ - Gemfile.lock
172
+ - LICENSE
173
+ - Rakefile
174
+ homepage: http://github.com/trongrg/single_table_globalize3
175
+ licenses: []
176
+ metadata: {}
177
+ post_install_message:
178
+ rdoc_options: []
179
+ require_paths:
180
+ - lib
181
+ required_ruby_version: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - '>='
184
+ - !ruby/object:Gem::Version
185
+ version: '0'
186
+ required_rubygems_version: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - '>='
189
+ - !ruby/object:Gem::Version
190
+ version: '0'
191
+ requirements: []
192
+ rubyforge_project: '[none]'
193
+ rubygems_version: 2.0.0
194
+ signing_key:
195
+ specification_version: 4
196
+ summary: Rails I18n de-facto standard library for ActiveRecord 3 model/data translation
197
+ with single table
198
+ test_files: []