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.
Files changed (33) 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 +430 -0
  7. data/Rakefile +55 -0
  8. data/lib/globalize.rb +90 -0
  9. data/lib/globalize/active_record.rb +14 -0
  10. data/lib/globalize/active_record/act_macro.rb +111 -0
  11. data/lib/globalize/active_record/adapter.rb +99 -0
  12. data/lib/globalize/active_record/adapter_dirty.rb +54 -0
  13. data/lib/globalize/active_record/attributes.rb +26 -0
  14. data/lib/globalize/active_record/class_methods.rb +124 -0
  15. data/lib/globalize/active_record/exceptions.rb +13 -0
  16. data/lib/globalize/active_record/instance_methods.rb +218 -0
  17. data/lib/globalize/active_record/migration.rb +210 -0
  18. data/lib/globalize/active_record/query_methods.rb +98 -0
  19. data/lib/globalize/active_record/translation.rb +45 -0
  20. data/lib/globalize/interpolation.rb +28 -0
  21. data/lib/globalize/version.rb +3 -0
  22. data/lib/i18n/missing_translations_log_handler.rb +41 -0
  23. data/lib/i18n/missing_translations_raise_handler.rb +25 -0
  24. data/lib/patches/active_record/persistence.rb +17 -0
  25. data/lib/patches/active_record/query_method.rb +3 -0
  26. data/lib/patches/active_record/rails4/query_method.rb +35 -0
  27. data/lib/patches/active_record/rails4/uniqueness_validator.rb +42 -0
  28. data/lib/patches/active_record/rails5/uniqueness_validator.rb +47 -0
  29. data/lib/patches/active_record/relation.rb +12 -0
  30. data/lib/patches/active_record/serialization.rb +21 -0
  31. data/lib/patches/active_record/uniqueness_validator.rb +5 -0
  32. data/lib/patches/active_record/xml_attribute_serializer.rb +23 -0
  33. metadata +204 -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,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