galetahub-globalize3 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/README.textile +206 -0
  2. data/Rakefile +22 -0
  3. data/lib/globalize.rb +59 -0
  4. data/lib/globalize/active_record.rb +13 -0
  5. data/lib/globalize/active_record/accessors.rb +22 -0
  6. data/lib/globalize/active_record/act_macro.rb +67 -0
  7. data/lib/globalize/active_record/adapter.rb +101 -0
  8. data/lib/globalize/active_record/attributes.rb +27 -0
  9. data/lib/globalize/active_record/class_methods.rb +125 -0
  10. data/lib/globalize/active_record/exceptions.rb +19 -0
  11. data/lib/globalize/active_record/instance_methods.rb +166 -0
  12. data/lib/globalize/active_record/migration.rb +125 -0
  13. data/lib/globalize/active_record/translation.rb +37 -0
  14. data/lib/globalize/engine.rb +17 -0
  15. data/lib/globalize/utils.rb +142 -0
  16. data/lib/globalize/versioning.rb +5 -0
  17. data/lib/globalize/versioning/paper_trail.rb +41 -0
  18. data/lib/globalize3.rb +1 -0
  19. data/lib/globalize3/version.rb +3 -0
  20. data/lib/i18n/missing_translations_log_handler.rb +41 -0
  21. data/lib/i18n/missing_translations_raise_handler.rb +25 -0
  22. data/lib/patches/active_record/query_method.rb +35 -0
  23. data/lib/patches/active_record/xml_attribute_serializer.rb +13 -0
  24. data/lib/tasks/globalize.rake +13 -0
  25. data/test/all.rb +1 -0
  26. data/test/data/models.rb +68 -0
  27. data/test/data/schema.rb +108 -0
  28. data/test/globalize3/attributes_test.rb +133 -0
  29. data/test/globalize3/clone_test.rb +58 -0
  30. data/test/globalize3/dirty_tracking_test.rb +61 -0
  31. data/test/globalize3/dynamic_finders_test.rb +171 -0
  32. data/test/globalize3/fallbacks_test.rb +146 -0
  33. data/test/globalize3/locale_test.rb +81 -0
  34. data/test/globalize3/migration_test.rb +156 -0
  35. data/test/globalize3/set_translations_test.rb +54 -0
  36. data/test/globalize3/translation_class_test.rb +59 -0
  37. data/test/globalize3/validations_test.rb +92 -0
  38. data/test/globalize3/versioning_test.rb +87 -0
  39. data/test/globalize3_test.rb +159 -0
  40. data/test/i18n/missing_translations_test.rb +35 -0
  41. data/test/test_helper.rb +105 -0
  42. metadata +243 -0
@@ -0,0 +1,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