globalize-r5 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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +111 -0
  3. data/CONTRIBUTING.md +37 -0
  4. data/Gemfile +26 -0
  5. data/LICENSE +22 -0
  6. data/README.md +423 -0
  7. data/Rakefile +55 -0
  8. data/globalize.gemspec +27 -0
  9. data/issue_template.rb +38 -0
  10. data/lib/globalize.rb +90 -0
  11. data/lib/globalize/active_record.rb +14 -0
  12. data/lib/globalize/active_record/act_macro.rb +95 -0
  13. data/lib/globalize/active_record/adapter.rb +99 -0
  14. data/lib/globalize/active_record/adapter_dirty.rb +53 -0
  15. data/lib/globalize/active_record/attributes.rb +26 -0
  16. data/lib/globalize/active_record/class_methods.rb +122 -0
  17. data/lib/globalize/active_record/exceptions.rb +19 -0
  18. data/lib/globalize/active_record/instance_methods.rb +219 -0
  19. data/lib/globalize/active_record/migration.rb +192 -0
  20. data/lib/globalize/active_record/query_methods.rb +113 -0
  21. data/lib/globalize/active_record/translation.rb +45 -0
  22. data/lib/globalize/interpolation.rb +28 -0
  23. data/lib/globalize/version.rb +3 -0
  24. data/lib/i18n/missing_translations_log_handler.rb +41 -0
  25. data/lib/i18n/missing_translations_raise_handler.rb +25 -0
  26. data/lib/patches/active_record/persistence.rb +17 -0
  27. data/lib/patches/active_record/query_method.rb +3 -0
  28. data/lib/patches/active_record/rails4/query_method.rb +35 -0
  29. data/lib/patches/active_record/rails4/uniqueness_validator.rb +39 -0
  30. data/lib/patches/active_record/rails5/uniqueness_validator.rb +47 -0
  31. data/lib/patches/active_record/relation.rb +12 -0
  32. data/lib/patches/active_record/serialization.rb +21 -0
  33. data/lib/patches/active_record/uniqueness_validator.rb +5 -0
  34. data/lib/patches/active_record/xml_attribute_serializer.rb +23 -0
  35. metadata +206 -0
@@ -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
@@ -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
@@ -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
@@ -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