single_table_globalize3 0.0.1

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.
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: []