globalize-danibachar 5.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Appraisals +33 -0
- data/CHANGELOG.md +111 -0
- data/CONTRIBUTING.md +52 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +437 -0
- data/Rakefile +55 -0
- data/lib/globalize.rb +98 -0
- data/lib/globalize/active_record.rb +14 -0
- data/lib/globalize/active_record/act_macro.rb +96 -0
- data/lib/globalize/active_record/adapter.rb +108 -0
- data/lib/globalize/active_record/adapter_dirty.rb +56 -0
- data/lib/globalize/active_record/attributes.rb +26 -0
- data/lib/globalize/active_record/class_methods.rb +129 -0
- data/lib/globalize/active_record/exceptions.rb +13 -0
- data/lib/globalize/active_record/instance_methods.rb +246 -0
- data/lib/globalize/active_record/migration.rb +215 -0
- data/lib/globalize/active_record/translated_attributes_query.rb +181 -0
- data/lib/globalize/active_record/translation.rb +45 -0
- data/lib/globalize/interpolation.rb +28 -0
- data/lib/globalize/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/persistence.rb +17 -0
- data/lib/patches/active_record/query_method.rb +3 -0
- data/lib/patches/active_record/rails4/query_method.rb +35 -0
- data/lib/patches/active_record/rails4/serialization.rb +22 -0
- data/lib/patches/active_record/rails4/uniqueness_validator.rb +42 -0
- data/lib/patches/active_record/rails5/uniqueness_validator.rb +47 -0
- data/lib/patches/active_record/rails5_1/serialization.rb +22 -0
- data/lib/patches/active_record/rails5_1/uniqueness_validator.rb +45 -0
- data/lib/patches/active_record/relation.rb +12 -0
- data/lib/patches/active_record/serialization.rb +5 -0
- data/lib/patches/active_record/uniqueness_validator.rb +7 -0
- data/lib/patches/active_record/xml_attribute_serializer.rb +23 -0
- metadata +264 -0
data/Rakefile
ADDED
@@ -0,0 +1,55 @@
|
|
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 = 'Globalize'
|
19
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
20
|
+
rdoc.rdoc_files.include('README')
|
21
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
22
|
+
end
|
23
|
+
|
24
|
+
task :load_path do
|
25
|
+
%w(lib test).each do |path|
|
26
|
+
$LOAD_PATH.unshift(File.expand_path("../#{path}", __FILE__))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
namespace :db do
|
31
|
+
desc 'Create the database'
|
32
|
+
task :create => :load_path do
|
33
|
+
require 'support/database'
|
34
|
+
|
35
|
+
Globalize::Test::Database.create!
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "Drop the database"
|
39
|
+
task :drop => :load_path do
|
40
|
+
require 'support/database'
|
41
|
+
|
42
|
+
Globalize::Test::Database.drop!
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "Set up the database schema"
|
46
|
+
task :migrate => :load_path do
|
47
|
+
require 'support/database'
|
48
|
+
|
49
|
+
Globalize::Test::Database.migrate!
|
50
|
+
# ActiveRecord::Schema.migrate :up
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "Drop and recreate the database schema"
|
54
|
+
task :reset => [:drop, :create]
|
55
|
+
end
|
data/lib/globalize.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'request_store'
|
2
|
+
require 'active_record'
|
3
|
+
require 'patches/active_record/xml_attribute_serializer'
|
4
|
+
require 'patches/active_record/query_method'
|
5
|
+
require 'patches/active_record/relation'
|
6
|
+
require 'patches/active_record/serialization'
|
7
|
+
require 'patches/active_record/uniqueness_validator'
|
8
|
+
require 'patches/active_record/persistence'
|
9
|
+
|
10
|
+
module Globalize
|
11
|
+
autoload :ActiveRecord, 'globalize/active_record'
|
12
|
+
autoload :Interpolation, 'globalize/interpolation'
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def locale
|
16
|
+
read_locale || I18n.locale
|
17
|
+
end
|
18
|
+
|
19
|
+
def locale=(locale)
|
20
|
+
set_locale(locale)
|
21
|
+
end
|
22
|
+
|
23
|
+
def with_locale(locale, &block)
|
24
|
+
previous_locale = read_locale
|
25
|
+
begin
|
26
|
+
set_locale(locale)
|
27
|
+
result = yield(locale)
|
28
|
+
ensure
|
29
|
+
set_locale(previous_locale)
|
30
|
+
end
|
31
|
+
result
|
32
|
+
end
|
33
|
+
|
34
|
+
def with_locales(*locales, &block)
|
35
|
+
locales.flatten.map do |locale|
|
36
|
+
with_locale(locale, &block)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def fallbacks=(locales)
|
41
|
+
set_fallbacks(locales)
|
42
|
+
end
|
43
|
+
|
44
|
+
def i18n_fallbacks?
|
45
|
+
I18n.respond_to?(:fallbacks)
|
46
|
+
end
|
47
|
+
|
48
|
+
def fallbacks(for_locale = self.locale)
|
49
|
+
read_fallbacks[for_locale] || default_fallbacks(for_locale)
|
50
|
+
end
|
51
|
+
|
52
|
+
def default_fallbacks(for_locale = self.locale)
|
53
|
+
i18n_fallbacks? ? I18n.fallbacks[for_locale] : [for_locale.to_sym]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Thread-safe global storage
|
57
|
+
def storage
|
58
|
+
RequestStore.store
|
59
|
+
end
|
60
|
+
|
61
|
+
def rails_5?
|
62
|
+
::ActiveRecord.version >= Gem::Version.new('5.1.0')
|
63
|
+
end
|
64
|
+
|
65
|
+
def rails_52?
|
66
|
+
::ActiveRecord.version >= Gem::Version.new('5.2.0')
|
67
|
+
end
|
68
|
+
|
69
|
+
protected
|
70
|
+
|
71
|
+
def read_locale
|
72
|
+
storage[:globalize_locale]
|
73
|
+
end
|
74
|
+
|
75
|
+
def set_locale(locale)
|
76
|
+
storage[:globalize_locale] = locale.try(:to_sym)
|
77
|
+
end
|
78
|
+
|
79
|
+
def read_fallbacks
|
80
|
+
storage[:globalize_fallbacks] || HashWithIndifferentAccess.new
|
81
|
+
end
|
82
|
+
|
83
|
+
def set_fallbacks(locales)
|
84
|
+
fallback_hash = HashWithIndifferentAccess.new
|
85
|
+
|
86
|
+
locales.each do |key, value|
|
87
|
+
fallback_hash[key] = value.presence || [key]
|
88
|
+
end if locales.present?
|
89
|
+
|
90
|
+
storage[:globalize_fallbacks] = fallback_hash
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
ActiveRecord::Base.class_attribute :globalize_serialized_attributes, instance_writer: false
|
96
|
+
ActiveRecord::Base.globalize_serialized_attributes = {}
|
97
|
+
|
98
|
+
ActiveRecord::Base.extend(Globalize::ActiveRecord::ActMacro)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Globalize
|
2
|
+
module ActiveRecord
|
3
|
+
autoload :ActMacro, 'globalize/active_record/act_macro'
|
4
|
+
autoload :Adapter, 'globalize/active_record/adapter'
|
5
|
+
autoload :AdapterDirty, 'globalize/active_record/adapter_dirty'
|
6
|
+
autoload :Attributes, 'globalize/active_record/attributes'
|
7
|
+
autoload :ClassMethods, 'globalize/active_record/class_methods'
|
8
|
+
autoload :Exceptions, 'globalize/active_record/exceptions'
|
9
|
+
autoload :InstanceMethods, 'globalize/active_record/instance_methods'
|
10
|
+
autoload :Migration, 'globalize/active_record/migration'
|
11
|
+
autoload :Translation, 'globalize/active_record/translation'
|
12
|
+
autoload :TranslatedAttributesQuery, 'globalize/active_record/translated_attributes_query'
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Globalize
|
2
|
+
module ActiveRecord
|
3
|
+
module ActMacro
|
4
|
+
def translates(*attr_names)
|
5
|
+
options = attr_names.extract_options!
|
6
|
+
# Bypass setup_translates! if the initial bootstrapping is done already.
|
7
|
+
setup_translates!(options) unless translates?
|
8
|
+
|
9
|
+
|
10
|
+
# Add any extra translatable attributes.
|
11
|
+
attr_names = attr_names.map(&:to_sym)
|
12
|
+
attr_names -= translated_attribute_names if defined?(translated_attribute_names)
|
13
|
+
|
14
|
+
allow_translation_of_attributes(attr_names) if attr_names.present?
|
15
|
+
end
|
16
|
+
|
17
|
+
def class_name
|
18
|
+
@class_name ||= begin
|
19
|
+
class_name = table_name[table_name_prefix.length..-(table_name_suffix.length + 1)].downcase.camelize
|
20
|
+
pluralize_table_names ? class_name.singularize : class_name
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def translates?
|
25
|
+
included_modules.include?(InstanceMethods)
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def allow_translation_of_attributes(attr_names)
|
31
|
+
attr_names.each do |attr_name|
|
32
|
+
# Detect and apply serialization.
|
33
|
+
enable_serializable_attribute(attr_name)
|
34
|
+
|
35
|
+
# Create accessors for the attribute.
|
36
|
+
define_translated_attr_accessor(attr_name)
|
37
|
+
define_translations_accessor(attr_name)
|
38
|
+
|
39
|
+
# Add attribute to the list.
|
40
|
+
self.translated_attribute_names << attr_name
|
41
|
+
end
|
42
|
+
if ::ActiveRecord::VERSION::STRING > "5.0" && connected? && table_exists? && translation_class.table_exists?
|
43
|
+
self.ignored_columns += translated_attribute_names.map(&:to_s)
|
44
|
+
reset_column_information
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def apply_globalize_options(options)
|
49
|
+
options[:table_name] ||= "#{table_name.singularize}_translations"
|
50
|
+
options[:foreign_key] ||= class_name.foreign_key
|
51
|
+
|
52
|
+
class_attribute :translated_attribute_names, :translation_options, :fallbacks_for_empty_translations
|
53
|
+
self.translated_attribute_names = []
|
54
|
+
self.translation_options = options
|
55
|
+
self.fallbacks_for_empty_translations = options[:fallbacks_for_empty_translations]
|
56
|
+
end
|
57
|
+
|
58
|
+
def enable_serializable_attribute(attr_name)
|
59
|
+
serializer = self.globalize_serialized_attributes[attr_name]
|
60
|
+
if serializer.present?
|
61
|
+
if defined?(::ActiveRecord::Coders::YAMLColumn) &&
|
62
|
+
serializer.is_a?(::ActiveRecord::Coders::YAMLColumn)
|
63
|
+
serializer = serializer.object_class
|
64
|
+
end
|
65
|
+
|
66
|
+
translation_class.send :serialize, attr_name, serializer
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def setup_translates!(options)
|
71
|
+
apply_globalize_options(options)
|
72
|
+
|
73
|
+
include InstanceMethods
|
74
|
+
extend ClassMethods, Migration
|
75
|
+
|
76
|
+
translation_class.table_name = options[:table_name]
|
77
|
+
|
78
|
+
has_many :translations, :class_name => translation_class.name,
|
79
|
+
:foreign_key => options[:foreign_key],
|
80
|
+
:dependent => :destroy,
|
81
|
+
:extend => HasManyExtensions,
|
82
|
+
:autosave => false,
|
83
|
+
:inverse_of => :globalized_model
|
84
|
+
|
85
|
+
after_create :save_translations!
|
86
|
+
after_update :save_translations!
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
module HasManyExtensions
|
91
|
+
def find_or_initialize_by_locale(locale)
|
92
|
+
with_locale(locale.to_s).first || build(:locale => locale.to_s)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,108 @@
|
|
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
|
7
|
+
private :record=, :stash=
|
8
|
+
|
9
|
+
delegate :translation_class, :to => :'record.class'
|
10
|
+
|
11
|
+
def initialize(record)
|
12
|
+
@record = record
|
13
|
+
@stash = Attributes.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def fetch_stash(locale, name)
|
17
|
+
stash.read(locale, name)
|
18
|
+
end
|
19
|
+
|
20
|
+
delegate :contains?, :to => :stash, :prefix => :stash
|
21
|
+
delegate :write, :to => :stash
|
22
|
+
|
23
|
+
def fetch(locale, name)
|
24
|
+
record.globalize_fallbacks(locale).each do |fallback|
|
25
|
+
value = stash.contains?(fallback, name) ? fetch_stash(fallback, name) : fetch_attribute(fallback, name)
|
26
|
+
|
27
|
+
unless fallbacks_for?(value)
|
28
|
+
set_metadata(value, :locale => fallback, :requested_locale => locale)
|
29
|
+
return value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
return nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def save_translations!
|
37
|
+
stash.each do |locale, attrs|
|
38
|
+
next if attrs.empty?
|
39
|
+
|
40
|
+
translation = record.translations_by_locale[locale] ||
|
41
|
+
record.translations.build(locale: locale.to_s)
|
42
|
+
attrs.each do |name, value|
|
43
|
+
value = value.val if value.is_a?(Arel::Nodes::Casted)
|
44
|
+
translation[name] = value
|
45
|
+
end
|
46
|
+
|
47
|
+
ensure_foreign_key_for(translation)
|
48
|
+
translation.save!
|
49
|
+
end
|
50
|
+
|
51
|
+
reset
|
52
|
+
end
|
53
|
+
|
54
|
+
def reset
|
55
|
+
stash.clear
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
# Sometimes the translation is initialised before a foreign key can be set.
|
61
|
+
def ensure_foreign_key_for(translation)
|
62
|
+
# AR >= 4.1 reflections renamed to _reflections
|
63
|
+
translation[translation.class.reflections.stringify_keys["globalized_model"].foreign_key] = record.id
|
64
|
+
end
|
65
|
+
|
66
|
+
def type_cast(name, value)
|
67
|
+
return value.presence unless column = column_for_attribute(name)
|
68
|
+
|
69
|
+
column.type_cast value
|
70
|
+
end
|
71
|
+
|
72
|
+
def column_for_attribute(name)
|
73
|
+
translation_class.columns_hash[name.to_s]
|
74
|
+
end
|
75
|
+
|
76
|
+
def unserializable_attribute?(name, column)
|
77
|
+
column.text? && translation_class.serialized_attributes[name.to_s]
|
78
|
+
end
|
79
|
+
|
80
|
+
def fetch_attribute(locale, name)
|
81
|
+
translation = record.translation_for(locale, false)
|
82
|
+
if translation
|
83
|
+
translation.send(name)
|
84
|
+
else
|
85
|
+
record.class.translation_class.new.send(name)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def set_metadata(object, metadata)
|
90
|
+
object.translation_metadata.merge!(metadata) if object.respond_to?(:translation_metadata)
|
91
|
+
object
|
92
|
+
end
|
93
|
+
|
94
|
+
def translation_metadata_accessor(object)
|
95
|
+
return if obj.respond_to?(:translation_metadata)
|
96
|
+
class << object; attr_accessor :translation_metadata end
|
97
|
+
object.translation_metadata ||= {}
|
98
|
+
end
|
99
|
+
|
100
|
+
def fallbacks_for?(object)
|
101
|
+
object.nil? || (fallbacks_for_empty_translations? && object.blank?)
|
102
|
+
end
|
103
|
+
|
104
|
+
delegate :fallbacks_for_empty_translations?, :to => :record, :prefix => false
|
105
|
+
prepend AdapterDirty
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Globalize
|
2
|
+
module ActiveRecord
|
3
|
+
module AdapterDirty
|
4
|
+
def write locale, name, value
|
5
|
+
# Dirty tracking, paraphrased from
|
6
|
+
# ActiveRecord::AttributeMethods::Dirty#write_attribute.
|
7
|
+
name = name.to_s
|
8
|
+
store_old_value name, locale
|
9
|
+
old_values = dirty[name]
|
10
|
+
old_value = old_values[locale]
|
11
|
+
is_changed = record.send :attribute_changed?, name
|
12
|
+
if is_changed && value == old_value
|
13
|
+
# If there's already a change, delete it if this undoes the change.
|
14
|
+
old_values.delete locale
|
15
|
+
if old_values.empty?
|
16
|
+
_reset_attribute name
|
17
|
+
end
|
18
|
+
elsif !is_changed
|
19
|
+
# If there's not a change yet, record it.
|
20
|
+
record.send(:attribute_will_change!, name) if old_value != value
|
21
|
+
end
|
22
|
+
|
23
|
+
super locale, name, value
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_writer :dirty
|
27
|
+
def dirty
|
28
|
+
@dirty ||= {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def store_old_value name, locale
|
32
|
+
dirty[name] ||= {}
|
33
|
+
unless dirty[name].key? locale
|
34
|
+
old = fetch(locale, name)
|
35
|
+
old = old.dup if old.duplicable?
|
36
|
+
dirty[name][locale] = old
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear_dirty
|
41
|
+
self.dirty = {}
|
42
|
+
end
|
43
|
+
|
44
|
+
def _reset_attribute name
|
45
|
+
record.send("#{name}=", record.changed_attributes[name])
|
46
|
+
record.send(:clear_attribute_changes, [name])
|
47
|
+
end
|
48
|
+
|
49
|
+
def reset
|
50
|
+
clear_dirty
|
51
|
+
super
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,26 @@
|
|
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
|
+
self[locale][name.to_s] = value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|