globalize-r5 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
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