globalize-rails5 5.1.0
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 +7 -0
- data/CHANGELOG.md +111 -0
- data/CONTRIBUTING.md +37 -0
- data/Gemfile +26 -0
- data/LICENSE +22 -0
- data/README.md +430 -0
- data/Rakefile +55 -0
- data/lib/globalize.rb +90 -0
- data/lib/globalize/active_record.rb +14 -0
- data/lib/globalize/active_record/act_macro.rb +111 -0
- data/lib/globalize/active_record/adapter.rb +99 -0
- data/lib/globalize/active_record/adapter_dirty.rb +54 -0
- data/lib/globalize/active_record/attributes.rb +26 -0
- data/lib/globalize/active_record/class_methods.rb +124 -0
- data/lib/globalize/active_record/exceptions.rb +13 -0
- data/lib/globalize/active_record/instance_methods.rb +218 -0
- data/lib/globalize/active_record/migration.rb +210 -0
- data/lib/globalize/active_record/query_methods.rb +98 -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/uniqueness_validator.rb +42 -0
- data/lib/patches/active_record/rails5/uniqueness_validator.rb +47 -0
- data/lib/patches/active_record/relation.rb +12 -0
- data/lib/patches/active_record/serialization.rb +21 -0
- data/lib/patches/active_record/uniqueness_validator.rb +5 -0
- data/lib/patches/active_record/xml_attribute_serializer.rb +23 -0
- metadata +204 -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,90 @@
|
|
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
|
+
protected
|
62
|
+
|
63
|
+
def read_locale
|
64
|
+
storage[:globalize_locale]
|
65
|
+
end
|
66
|
+
|
67
|
+
def set_locale(locale)
|
68
|
+
storage[:globalize_locale] = locale.try(:to_sym)
|
69
|
+
end
|
70
|
+
|
71
|
+
def read_fallbacks
|
72
|
+
storage[:globalize_fallbacks] || HashWithIndifferentAccess.new
|
73
|
+
end
|
74
|
+
|
75
|
+
def set_fallbacks(locales)
|
76
|
+
fallback_hash = HashWithIndifferentAccess.new
|
77
|
+
|
78
|
+
locales.each do |key, value|
|
79
|
+
fallback_hash[key] = value.presence || [key]
|
80
|
+
end if locales.present?
|
81
|
+
|
82
|
+
storage[:globalize_fallbacks] = fallback_hash
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
ActiveRecord::Base.mattr_accessor :globalize_serialized_attributes, instance_writer: false
|
88
|
+
ActiveRecord::Base.globalize_serialized_attributes = {}
|
89
|
+
|
90
|
+
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 :QueryMethods, 'globalize/active_record/query_methods'
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,111 @@
|
|
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
|
+
check_columns!(attr_names)
|
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
|
+
|
43
|
+
if ::ActiveRecord::VERSION::STRING > "5.0" && table_exists? && translation_class.table_exists?
|
44
|
+
self.ignored_columns += translated_attribute_names.map(&:to_s)
|
45
|
+
reset_column_information
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def check_columns!(attr_names)
|
50
|
+
# If tables do not exist, do not warn about conflicting columns
|
51
|
+
return unless table_exists? && translation_class.table_exists?
|
52
|
+
|
53
|
+
if (overlap = attr_names.map(&:to_s) & column_names).present?
|
54
|
+
ActiveSupport::Deprecation.warn(
|
55
|
+
["You have defined one or more translated attributes with names that conflict with column(s) on the model table. ",
|
56
|
+
"Globalize does not support this configuration anymore, remove or rename column(s) on the model table.\n",
|
57
|
+
"Model name (table name): #{model_name} (#{table_name})\n",
|
58
|
+
"Attribute name(s): #{overlap.join(', ')}\n"].join
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def apply_globalize_options(options)
|
64
|
+
options[:table_name] ||= "#{table_name.singularize}_translations"
|
65
|
+
options[:foreign_key] ||= class_name.foreign_key
|
66
|
+
|
67
|
+
class_attribute :translated_attribute_names, :translation_options, :fallbacks_for_empty_translations
|
68
|
+
self.translated_attribute_names = []
|
69
|
+
self.translation_options = options
|
70
|
+
self.fallbacks_for_empty_translations = options[:fallbacks_for_empty_translations]
|
71
|
+
end
|
72
|
+
|
73
|
+
def enable_serializable_attribute(attr_name)
|
74
|
+
serializer = self.globalize_serialized_attributes[attr_name]
|
75
|
+
if serializer.present?
|
76
|
+
if defined?(::ActiveRecord::Coders::YAMLColumn) &&
|
77
|
+
serializer.is_a?(::ActiveRecord::Coders::YAMLColumn)
|
78
|
+
serializer = serializer.object_class
|
79
|
+
end
|
80
|
+
|
81
|
+
translation_class.send :serialize, attr_name, serializer
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def setup_translates!(options)
|
86
|
+
apply_globalize_options(options)
|
87
|
+
|
88
|
+
include InstanceMethods
|
89
|
+
extend ClassMethods, Migration
|
90
|
+
|
91
|
+
translation_class.table_name = options[:table_name]
|
92
|
+
|
93
|
+
has_many :translations, :class_name => translation_class.name,
|
94
|
+
:foreign_key => options[:foreign_key],
|
95
|
+
:dependent => :destroy,
|
96
|
+
:extend => HasManyExtensions,
|
97
|
+
:autosave => true,
|
98
|
+
:inverse_of => :globalized_model
|
99
|
+
|
100
|
+
before_create :save_translations!
|
101
|
+
before_update :save_translations!
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
module HasManyExtensions
|
106
|
+
def find_or_initialize_by_locale(locale)
|
107
|
+
with_locale(locale.to_s).first || build(:locale => locale.to_s)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,99 @@
|
|
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
|
+
end
|
47
|
+
|
48
|
+
reset
|
49
|
+
end
|
50
|
+
|
51
|
+
def reset
|
52
|
+
stash.clear
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def type_cast(name, value)
|
58
|
+
return value.presence unless column = column_for_attribute(name)
|
59
|
+
|
60
|
+
column.type_cast value
|
61
|
+
end
|
62
|
+
|
63
|
+
def column_for_attribute(name)
|
64
|
+
translation_class.columns_hash[name.to_s]
|
65
|
+
end
|
66
|
+
|
67
|
+
def unserializable_attribute?(name, column)
|
68
|
+
column.text? && translation_class.serialized_attributes[name.to_s]
|
69
|
+
end
|
70
|
+
|
71
|
+
def fetch_attribute(locale, name)
|
72
|
+
translation = record.translation_for(locale, false)
|
73
|
+
if translation
|
74
|
+
translation.send(name)
|
75
|
+
else
|
76
|
+
record.class.translation_class.new.send(name)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def set_metadata(object, metadata)
|
81
|
+
object.translation_metadata.merge!(metadata) if object.respond_to?(:translation_metadata)
|
82
|
+
object
|
83
|
+
end
|
84
|
+
|
85
|
+
def translation_metadata_accessor(object)
|
86
|
+
return if obj.respond_to?(:translation_metadata)
|
87
|
+
class << object; attr_accessor :translation_metadata end
|
88
|
+
object.translation_metadata ||= {}
|
89
|
+
end
|
90
|
+
|
91
|
+
def fallbacks_for?(object)
|
92
|
+
object.nil? || (fallbacks_for_empty_translations? && object.blank?)
|
93
|
+
end
|
94
|
+
|
95
|
+
delegate :fallbacks_for_empty_translations?, :to => :record, :prefix => false
|
96
|
+
prepend AdapterDirty
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,54 @@
|
|
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
|
+
def clear_dirty
|
40
|
+
self.dirty = {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def _reset_attribute name
|
44
|
+
record.send("#{name}=", record.changed_attributes[name])
|
45
|
+
record.original_changed_attributes.except!(name)
|
46
|
+
end
|
47
|
+
|
48
|
+
def reset
|
49
|
+
clear_dirty
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
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
|