globalize-r5 5.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +423 -0
- data/Rakefile +55 -0
- data/globalize.gemspec +27 -0
- data/issue_template.rb +38 -0
- data/lib/globalize.rb +90 -0
- data/lib/globalize/active_record.rb +14 -0
- data/lib/globalize/active_record/act_macro.rb +95 -0
- data/lib/globalize/active_record/adapter.rb +99 -0
- data/lib/globalize/active_record/adapter_dirty.rb +53 -0
- data/lib/globalize/active_record/attributes.rb +26 -0
- data/lib/globalize/active_record/class_methods.rb +122 -0
- data/lib/globalize/active_record/exceptions.rb +19 -0
- data/lib/globalize/active_record/instance_methods.rb +219 -0
- data/lib/globalize/active_record/migration.rb +192 -0
- data/lib/globalize/active_record/query_methods.rb +113 -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 +39 -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 +206 -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/globalize.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require File.expand_path('../lib/globalize/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'globalize-r5'
|
5
|
+
s.version = Globalize::Version
|
6
|
+
s.authors = ['Sven Fuchs', 'Joshua Harvey', 'Clemens Kofler', 'John-Paul Bader', 'Tomasz Stachewicz', 'Philip Arndt', 'Chris Salzberg']
|
7
|
+
s.email = 'nobody@globalize-rails.org'
|
8
|
+
s.homepage = 'http://github.com/globalize/globalize'
|
9
|
+
s.summary = 'Rails I18n de-facto standard library for ActiveRecord model/data translation'
|
10
|
+
s.description = "#{s.summary}."
|
11
|
+
s.license = "MIT"
|
12
|
+
|
13
|
+
s.files = Dir['{lib/**/*,[A-Z]*}']
|
14
|
+
s.platform = Gem::Platform::RUBY
|
15
|
+
s.require_path = 'lib'
|
16
|
+
s.rubyforge_project = '[none]'
|
17
|
+
|
18
|
+
s.add_dependency 'activerecord', '>= 4.2', '< 5.1'
|
19
|
+
s.add_dependency 'activemodel', '>= 4.2', '< 5.1'
|
20
|
+
s.add_dependency 'request_store', '~> 1.0'
|
21
|
+
|
22
|
+
s.add_development_dependency 'database_cleaner'
|
23
|
+
s.add_development_dependency 'minitest'
|
24
|
+
s.add_development_dependency 'minitest-reporters'
|
25
|
+
s.add_development_dependency 'rdoc'
|
26
|
+
s.add_development_dependency 'rake'
|
27
|
+
end
|
data/issue_template.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# Activate the gem you are reporting the issue against.
|
2
|
+
gem 'activerecord', '4.2.0'
|
3
|
+
gem 'globalize', '5.0.1'
|
4
|
+
require 'active_record'
|
5
|
+
require 'globalize'
|
6
|
+
require 'minitest/autorun'
|
7
|
+
require 'logger'
|
8
|
+
|
9
|
+
# Ensure backward compatibility with Minitest 4
|
10
|
+
Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
|
11
|
+
|
12
|
+
# This connection will do for database-independent bug reports.
|
13
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
14
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
15
|
+
|
16
|
+
ActiveRecord::Schema.define do
|
17
|
+
create_table :posts, force: true do |t|
|
18
|
+
end
|
19
|
+
|
20
|
+
create_table :post_translations, force: true do |t|
|
21
|
+
t.references :post
|
22
|
+
t.string :title
|
23
|
+
t.text :content
|
24
|
+
t.string :locale
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Post < ActiveRecord::Base
|
29
|
+
translates :content, :title
|
30
|
+
end
|
31
|
+
|
32
|
+
class BugTest < Minitest::Test
|
33
|
+
def test_association_stuff
|
34
|
+
post = Post.create!(title: 'HI')
|
35
|
+
|
36
|
+
assert_equal 'HI', post.title
|
37
|
+
end
|
38
|
+
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,95 @@
|
|
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
|
+
# Add any extra translatable attributes.
|
10
|
+
attr_names = attr_names.map(&:to_sym)
|
11
|
+
attr_names -= translated_attribute_names if defined?(translated_attribute_names)
|
12
|
+
|
13
|
+
allow_translation_of_attributes(attr_names) if attr_names.present?
|
14
|
+
end
|
15
|
+
|
16
|
+
def class_name
|
17
|
+
@class_name ||= begin
|
18
|
+
class_name = table_name[table_name_prefix.length..-(table_name_suffix.length + 1)].downcase.camelize
|
19
|
+
pluralize_table_names ? class_name.singularize : class_name
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def translates?
|
24
|
+
included_modules.include?(InstanceMethods)
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def allow_translation_of_attributes(attr_names)
|
30
|
+
attr_names.each do |attr_name|
|
31
|
+
# Detect and apply serialization.
|
32
|
+
enable_serializable_attribute(attr_name)
|
33
|
+
|
34
|
+
if ::ActiveRecord::VERSION::STRING >= "5.0.0"
|
35
|
+
# use virtual attribute
|
36
|
+
attribute attr_name, :string
|
37
|
+
end
|
38
|
+
|
39
|
+
# Create accessors for the attribute.
|
40
|
+
define_translated_attr_accessor(attr_name)
|
41
|
+
define_translations_accessor(attr_name)
|
42
|
+
|
43
|
+
# Add attribute to the list.
|
44
|
+
self.translated_attribute_names << attr_name
|
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 => true
|
83
|
+
|
84
|
+
before_create :save_translations!
|
85
|
+
before_update :save_translations!
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
module HasManyExtensions
|
90
|
+
def find_or_initialize_by_locale(locale)
|
91
|
+
with_locale(locale.to_s).first || build(:locale => locale.to_s)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
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, :translations
|
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,53 @@
|
|
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(:restore_attribute!, name) if record.respond_to? :restore_attribute!, true
|
45
|
+
end
|
46
|
+
|
47
|
+
def reset
|
48
|
+
clear_dirty
|
49
|
+
super
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|