galetahub-globalize3 0.2.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.
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,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,125 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module ClassMethods
4
+ delegate :translated_locales, :set_translations_table_name, :to => :translation_class
5
+
6
+ def with_locale(locale)
7
+ scoped.where(:"is_locale_#{locale}" => true)
8
+ end
9
+
10
+ def with_locales(*locales)
11
+ scoped.merge(translation_class.with_locales(*locales))
12
+ end
13
+
14
+ def with_translations(*locales)
15
+ locales = translated_locales if locales.empty?
16
+ includes(:translations).with_locales(locales).with_required_attributes
17
+ end
18
+
19
+ def with_required_attributes
20
+ required_translated_attributes.inject(scoped) do |scope, name|
21
+ scope.where("#{translated_column_name(name)} IS NOT NULL")
22
+ end
23
+ end
24
+
25
+ def with_translated_attribute(name, value, locales = nil)
26
+ locales ||= Globalize.fallbacks
27
+ with_translations.where(
28
+ translated_column_name(name) => value,
29
+ translated_column_name(:locale) => Array(locales).map(&:to_s)
30
+ )
31
+ end
32
+
33
+ def translated?(name)
34
+ translated_attribute_names.include?(name.to_sym)
35
+ end
36
+
37
+ def required_attributes
38
+ validators.map { |v| v.attributes if v.is_a?(ActiveModel::Validations::PresenceValidator) }.flatten
39
+ end
40
+
41
+ def required_translated_attributes
42
+ translated_attribute_names & required_attributes
43
+ end
44
+
45
+ def translation_class
46
+ @translation_class ||= begin
47
+ klass = self.const_get(:Translation) rescue nil
48
+ if klass.nil? || klass.class_name != (self.class_name + "Translation")
49
+ klass = self.const_set(:Translation, Class.new(Globalize::ActiveRecord::Translation))
50
+ end
51
+
52
+ klass.belongs_to name.underscore.gsub('/', '_')
53
+ klass
54
+ end
55
+ end
56
+
57
+ def translations_table_name
58
+ translation_class.table_name
59
+ end
60
+
61
+ def translated_column_name(name)
62
+ "#{translation_class.table_name}.#{name}"
63
+ end
64
+
65
+ if RUBY_VERSION < '1.9'
66
+ def respond_to?(method_id, *args, &block)
67
+ supported_on_missing?(method_id) || super
68
+ end
69
+ else
70
+ def respond_to_missing?(method_id, include_private = false)
71
+ supported_on_missing?(method_id) || super
72
+ end
73
+ end
74
+
75
+ def supported_on_missing?(method_id)
76
+ return super unless RUBY_VERSION < '1.9' || respond_to?(:translated_attribute_names)
77
+ match = ::ActiveRecord::DynamicFinderMatch.match(method_id) || ::ActiveRecord::DynamicScopeMatch.match(method_id)
78
+ return false if match.nil?
79
+
80
+ attribute_names = match.attribute_names.map(&:to_sym)
81
+ translated_attributes = attribute_names & translated_attribute_names
82
+ return false if translated_attributes.empty?
83
+
84
+ untranslated_attributes = attribute_names - translated_attributes
85
+ return false if untranslated_attributes.any?{|unt| ! respond_to?(:"scoped_by_#{unt}")}
86
+ return [match, attribute_names, translated_attributes, untranslated_attributes]
87
+ end
88
+
89
+ def method_missing(method_id, *arguments, &block)
90
+ match, attribute_names, translated_attributes, untranslated_attributes = supported_on_missing?(method_id)
91
+ return super unless match
92
+
93
+ scope = scoped
94
+
95
+ translated_attributes.each do |attr|
96
+ scope = scope.with_translated_attribute(attr, arguments[attribute_names.index(attr)])
97
+ end
98
+
99
+ untranslated_attributes.each do |unt|
100
+ index = attribute_names.index(unt)
101
+ raise StandarError unless index
102
+ scope = scope.send(:"scoped_by_#{unt}", arguments[index])
103
+ end
104
+
105
+ return scope.send(match.finder) if match.is_a?(::ActiveRecord::DynamicFinderMatch)
106
+ return scope
107
+ end
108
+
109
+ protected
110
+
111
+ def translated_attr_accessor(name)
112
+ define_method(:"#{name}=") do |value|
113
+ write_attribute(name, value)
114
+ end
115
+ define_method(name) do |*args|
116
+ read_attribute(name, {:locale => args.first})
117
+ end
118
+ alias_method :"#{name}_before_type_cast", name
119
+ end
120
+
121
+ end
122
+
123
+ end
124
+
125
+ 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,166 @@
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 attributes=(attributes, *args)
15
+ with_given_locale(attributes) { super }
16
+ end
17
+
18
+ def update_attributes!(attributes, *args)
19
+ with_given_locale(attributes) { super }
20
+ end
21
+
22
+ def update_attributes(attributes, *args)
23
+ with_given_locale(attributes) { super }
24
+ end
25
+
26
+ def write_attribute(name, value, options = {})
27
+ # raise 'y' if value.nil? # TODO.
28
+
29
+ if translated?(name)
30
+ # Deprecate old use of locale
31
+ unless options.is_a?(Hash)
32
+ warn "[DEPRECATION] passing 'locale' as #{options.inspect} is deprecated. Please use {:locale => #{options.inspect}} instead."
33
+ options = {:locale => options}
34
+ end
35
+ options = {:locale => nil}.merge(options)
36
+ attribute_will_change! name.to_s
37
+ globalize.write(options[:locale] || Globalize.locale, name, value)
38
+ else
39
+ super(name, value)
40
+ end
41
+ end
42
+
43
+ def read_attribute(name, options = {})
44
+ # Deprecate old use of locale
45
+ unless options.is_a?(Hash)
46
+ warn "[DEPRECATION] passing 'locale' as #{options.inspect} is deprecated. Please use {:locale => #{options.inspect}} instead."
47
+ options = {:locale => options}
48
+ end
49
+
50
+ options = {:translated => true, :locale => nil}.merge(options)
51
+ if self.class.translated?(name) and options[:translated]
52
+ globalize.fetch(options[:locale] || Globalize.locale, name)
53
+ else
54
+ super(name)
55
+ end
56
+ end
57
+
58
+ def attribute_names
59
+ translated_attribute_names.map(&:to_s) + super
60
+ end
61
+
62
+ def translated?(name)
63
+ self.class.translated?(name)
64
+ end
65
+
66
+ def translated_attributes
67
+ translated_attribute_names.inject({}) do |attributes, name|
68
+ attributes.merge(name.to_s => translation.send(name))
69
+ end
70
+ end
71
+
72
+ # This method is basically the method built into Rails
73
+ # but we have to pass {:translated => false}
74
+ def untranslated_attributes
75
+ attrs = {}
76
+ attribute_names.each do |name|
77
+ attrs[name] = read_attribute(name, {:translated => false})
78
+ end
79
+ attrs
80
+ end
81
+
82
+ def set_translations(options)
83
+ options.keys.each do |locale|
84
+ translation = translation_for(locale) ||
85
+ translations.build(:locale => locale.to_s)
86
+ translation.update_attributes!(options[locale])
87
+ end
88
+ end
89
+
90
+ def reload(options = nil)
91
+ translated_attribute_names.each { |name| @attributes.delete(name.to_s) }
92
+ globalize.reset
93
+ super(options)
94
+ end
95
+
96
+ def clone
97
+ obj = super
98
+ return obj unless respond_to?(:translated_attribute_names)
99
+
100
+ obj.instance_variable_set(:@translations, nil) if new_record? # Reset the collection because of rails bug: http://pastie.org/1521874
101
+ obj.instance_variable_set(:@globalize, nil )
102
+ each_locale_and_translated_attribute do |locale, name|
103
+ obj.globalize.write(locale, name, globalize.fetch(locale, name) )
104
+ end
105
+
106
+ return obj
107
+ end
108
+
109
+ def translation
110
+ translation_for(::Globalize.locale)
111
+ end
112
+
113
+ def translation_for(locale)
114
+ @translation_caches ||= {}
115
+ unless @translation_caches[locale]
116
+ # Enumberable#detect is better since we have the translations collection (already) loaded
117
+ # using either Model.includes(:translations) or Model.with_translations
118
+ _translation = translations.detect{|t| t.locale.to_s == locale.to_s}
119
+ _translation ||= translations.build(:locale => locale)
120
+ @translation_caches[locale] = _translation
121
+ end
122
+ @translation_caches[locale]
123
+ end
124
+
125
+ def rollback
126
+ @translation_caches[::Globalize.locale] = translation.previous_version
127
+ end
128
+
129
+ protected
130
+
131
+ def each_locale_and_translated_attribute
132
+ used_locales.each do |locale|
133
+ translated_attribute_names.each do |name|
134
+ yield locale, name
135
+ end
136
+ end
137
+ end
138
+
139
+ def used_locales
140
+ locales = globalize.stash.keys.concat(globalize.stash.keys).concat(translations.translated_locales)
141
+ locales.uniq!
142
+ locales
143
+ end
144
+
145
+ def update_checkers!
146
+ Globalize.available_locales.each do |locale|
147
+ self["is_locale_#{locale}"] = !globalize.all_blank?(locale, translated_attribute_names)
148
+ end
149
+ end
150
+
151
+ def save_translations!
152
+ globalize.save_translations!
153
+ @translation_caches = {}
154
+ end
155
+
156
+ def with_given_locale(attributes, &block)
157
+ attributes.symbolize_keys! if attributes.respond_to?(:symbolize_keys!)
158
+ if locale = attributes.try(:delete, :locale)
159
+ Globalize.with_locale(locale, &block)
160
+ else
161
+ yield
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,125 @@
1
+ require 'digest/sha1'
2
+
3
+ module Globalize
4
+ module ActiveRecord
5
+ module Migration
6
+ attr_reader :globalize_migrator
7
+
8
+ def globalize_migrator
9
+ @globalize_migrator ||= Migrator.new(self)
10
+ end
11
+
12
+ delegate :create_translation_table!, :drop_translation_table!,
13
+ :translation_index_name, :to => :globalize_migrator
14
+
15
+ class Migrator
16
+ include Globalize::ActiveRecord::Exceptions
17
+
18
+ attr_reader :model, :fields
19
+ delegate :translated_attribute_names, :connection, :table_name, :translated_columns_hash,
20
+ :table_name_prefix, :translations_table_name, :columns, :to => :model
21
+
22
+ def initialize(model)
23
+ @model = model
24
+ end
25
+
26
+ def create_translation_table!(fields = {}, options = {})
27
+ @fields = fields
28
+ complete_translated_fields
29
+ validate_translated_fields
30
+
31
+ create_translation_table
32
+ move_data_to_translation_table if options[:migrate_data]
33
+ create_translations_index
34
+ end
35
+
36
+ def drop_translation_table!(options = {})
37
+ move_data_to_model_table if options[:migrate_data]
38
+ drop_translations_index
39
+ drop_translation_table
40
+ end
41
+
42
+ def complete_translated_fields
43
+ translated_attribute_names.each do |name|
44
+ fields[name] = column_type(name) unless fields[name]
45
+ end
46
+ end
47
+
48
+ def create_translation_table
49
+ connection.create_table(translations_table_name) do |t|
50
+ t.references table_name.sub(/^#{table_name_prefix}/, '').singularize
51
+ t.string :locale
52
+ fields.each { |name, type| t.column name, type }
53
+ t.timestamps
54
+ end
55
+ end
56
+
57
+ def create_translations_index
58
+ connection.add_index(
59
+ translations_table_name,
60
+ "#{table_name.sub(/^#{table_name_prefix}/, "").singularize}_id",
61
+ :name => translation_index_name
62
+ )
63
+ end
64
+
65
+ def drop_translation_table
66
+ connection.drop_table(translations_table_name)
67
+ end
68
+
69
+ def drop_translations_index
70
+ connection.remove_index(translations_table_name, :name => translation_index_name) rescue nil
71
+ end
72
+
73
+ def move_data_to_translation_table
74
+ # Find all of the existing untranslated attributes for this model.
75
+ all_model_fields = @model.all
76
+ model_attributes = all_model_fields.collect {|m| m.untranslated_attributes}
77
+ all_model_fields.each do |model_record|
78
+ # Assign the attributes back to the model which will enable globalize3 to translate them.
79
+ model_record.attributes = model_attributes.detect{|a| a['id'] == model_record.id}
80
+ model_record.save!
81
+ end
82
+ end
83
+
84
+ def move_data_to_model_table
85
+ # Find all of the translated attributes for all records in the model.
86
+ all_translated_attributes = @model.all.collect{|m| m.attributes}
87
+ all_translated_attributes.each do |translated_record|
88
+ # Create a hash containing the translated column names and their values.
89
+ translated_attribute_names.inject(fields_to_update={}) do |f, name|
90
+ f.update({name.to_sym => translated_record[name.to_s]})
91
+ end
92
+
93
+ # Now, update the actual model's record with the hash.
94
+ @model.update_all(fields_to_update, {:id => translated_record['id']})
95
+ end
96
+ end
97
+
98
+ def validate_translated_fields
99
+ fields.each do |name, type|
100
+ raise BadFieldName.new(name) unless valid_field_name?(name)
101
+ raise BadFieldType.new(name, type) unless valid_field_type?(name, type)
102
+ end
103
+ end
104
+
105
+ def column_type(name)
106
+ columns.detect { |c| c.name == name.to_s }.try(:type) || translated_columns_hash[name.to_s]
107
+ end
108
+
109
+ def valid_field_name?(name)
110
+ translated_attribute_names.include?(name)
111
+ end
112
+
113
+ def valid_field_type?(name, type)
114
+ !translated_attribute_names.include?(name) || [:string, :text].include?(type)
115
+ end
116
+
117
+ def translation_index_name
118
+ # FIXME what's the max size of an index name?
119
+ index_name = "index_#{translations_table_name}_on_#{table_name.singularize}_id"
120
+ index_name.size < 50 ? index_name : "index_#{Digest::SHA1.hexdigest(index_name)}"
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end