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.
- data/README.textile +206 -0
- data/Rakefile +22 -0
- data/lib/globalize.rb +59 -0
- data/lib/globalize/active_record.rb +13 -0
- data/lib/globalize/active_record/accessors.rb +22 -0
- data/lib/globalize/active_record/act_macro.rb +67 -0
- data/lib/globalize/active_record/adapter.rb +101 -0
- data/lib/globalize/active_record/attributes.rb +27 -0
- data/lib/globalize/active_record/class_methods.rb +125 -0
- data/lib/globalize/active_record/exceptions.rb +19 -0
- data/lib/globalize/active_record/instance_methods.rb +166 -0
- data/lib/globalize/active_record/migration.rb +125 -0
- data/lib/globalize/active_record/translation.rb +37 -0
- data/lib/globalize/engine.rb +17 -0
- data/lib/globalize/utils.rb +142 -0
- data/lib/globalize/versioning.rb +5 -0
- data/lib/globalize/versioning/paper_trail.rb +41 -0
- data/lib/globalize3.rb +1 -0
- data/lib/globalize3/version.rb +3 -0
- data/lib/i18n/missing_translations_log_handler.rb +41 -0
- data/lib/i18n/missing_translations_raise_handler.rb +25 -0
- data/lib/patches/active_record/query_method.rb +35 -0
- data/lib/patches/active_record/xml_attribute_serializer.rb +13 -0
- data/lib/tasks/globalize.rake +13 -0
- data/test/all.rb +1 -0
- data/test/data/models.rb +68 -0
- data/test/data/schema.rb +108 -0
- data/test/globalize3/attributes_test.rb +133 -0
- data/test/globalize3/clone_test.rb +58 -0
- data/test/globalize3/dirty_tracking_test.rb +61 -0
- data/test/globalize3/dynamic_finders_test.rb +171 -0
- data/test/globalize3/fallbacks_test.rb +146 -0
- data/test/globalize3/locale_test.rb +81 -0
- data/test/globalize3/migration_test.rb +156 -0
- data/test/globalize3/set_translations_test.rb +54 -0
- data/test/globalize3/translation_class_test.rb +59 -0
- data/test/globalize3/validations_test.rb +92 -0
- data/test/globalize3/versioning_test.rb +87 -0
- data/test/globalize3_test.rb +159 -0
- data/test/i18n/missing_translations_test.rb +35 -0
- data/test/test_helper.rb +105 -0
- 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
|