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.
- 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
| @@ -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
         | 
| @@ -0,0 +1,122 @@ | |
| 1 | 
            +
            module Globalize
         | 
| 2 | 
            +
              module ActiveRecord
         | 
| 3 | 
            +
                module ClassMethods
         | 
| 4 | 
            +
                  delegate :translated_locales, :set_translations_table_name, :to => :translation_class
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  def with_locales(*locales)
         | 
| 7 | 
            +
                    all.merge translation_class.with_locales(*locales)
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def with_where(opts = {})
         | 
| 11 | 
            +
                    all.merge translation_class.where(opts)
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def with_translations(*locales)
         | 
| 15 | 
            +
                    locales = translated_locales if locales.empty?
         | 
| 16 | 
            +
                    preload(:translations).joins(:translations).readonly(false).with_locales(locales).tap do |query|
         | 
| 17 | 
            +
                      query.distinct! unless locales.flatten.one?
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  def with_required_attributes
         | 
| 22 | 
            +
                    warn 'with_required_attributes is deprecated and will be removed in the next release of Globalize.'
         | 
| 23 | 
            +
                    required_translated_attributes.inject(all) do |scope, name|
         | 
| 24 | 
            +
                      scope.where("#{translated_column_name(name)} IS NOT NULL")
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def with_translated_attribute(name, value, locales = Globalize.fallbacks)
         | 
| 29 | 
            +
                    with_translations.where(
         | 
| 30 | 
            +
                      translated_column_name(name)    => value,
         | 
| 31 | 
            +
                      translated_column_name(:locale) => Array(locales).map(&:to_s)
         | 
| 32 | 
            +
                    )
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def translated?(name)
         | 
| 36 | 
            +
                    translated_attribute_names.include?(name.to_sym)
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def required_attributes
         | 
| 40 | 
            +
                    warn 'required_attributes is deprecated and will be removed in the next release of Globalize.'
         | 
| 41 | 
            +
                    validators.map { |v| v.attributes if v.is_a?(ActiveModel::Validations::PresenceValidator) }.flatten
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  def required_translated_attributes
         | 
| 45 | 
            +
                    warn 'required_translated_attributes is deprecated and will be removed in the next release of Globalize.'
         | 
| 46 | 
            +
                    translated_attribute_names & required_attributes
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def translation_class
         | 
| 50 | 
            +
                    @translation_class ||= begin
         | 
| 51 | 
            +
                      if self.const_defined?(:Translation, false)
         | 
| 52 | 
            +
                        klass = self.const_get(:Translation, false)
         | 
| 53 | 
            +
                      else
         | 
| 54 | 
            +
                        klass = self.const_set(:Translation, Class.new(Globalize::ActiveRecord::Translation))
         | 
| 55 | 
            +
                      end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                      klass.belongs_to :globalized_model, :class_name => self.name, :foreign_key => translation_options[:foreign_key]
         | 
| 58 | 
            +
                      klass
         | 
| 59 | 
            +
                    end
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  def translations_table_name
         | 
| 63 | 
            +
                    translation_class.table_name
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  def translated_column_name(name)
         | 
| 67 | 
            +
                    "#{translation_class.table_name}.#{name}"
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  private
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  # Override the default relation method in order to return a subclass
         | 
| 73 | 
            +
                  # of ActiveRecord::Relation with custom finder methods for translated
         | 
| 74 | 
            +
                  # attributes.
         | 
| 75 | 
            +
                  def relation
         | 
| 76 | 
            +
                    super.extending!(QueryMethods)
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  protected
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  def define_translated_attr_reader(name)
         | 
| 82 | 
            +
                    define_method(name) do |*args|
         | 
| 83 | 
            +
                      Globalize::Interpolation.interpolate(name, self, args)
         | 
| 84 | 
            +
                    end
         | 
| 85 | 
            +
                    alias_method :"#{name}_before_type_cast", name
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  def define_translated_attr_writer(name)
         | 
| 89 | 
            +
                    define_method(:"#{name}=") do |value|
         | 
| 90 | 
            +
                      write_attribute(name, value)
         | 
| 91 | 
            +
                    end
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  def define_translated_attr_accessor(name)
         | 
| 95 | 
            +
                    define_translated_attr_reader(name)
         | 
| 96 | 
            +
                    define_translated_attr_writer(name)
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                  def define_translations_reader(name)
         | 
| 100 | 
            +
                    define_method(:"#{name}_translations") do
         | 
| 101 | 
            +
                      hash = translated_attribute_by_locale(name)
         | 
| 102 | 
            +
                      globalize.stash.keys.each_with_object(hash) do |locale, result|
         | 
| 103 | 
            +
                        result[locale] = globalize.fetch_stash(locale, name) if globalize.stash_contains?(locale, name)
         | 
| 104 | 
            +
                      end
         | 
| 105 | 
            +
                    end
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  def define_translations_writer(name)
         | 
| 109 | 
            +
                    define_method(:"#{name}_translations=") do |value|
         | 
| 110 | 
            +
                      value.each do |(locale, _value)|
         | 
| 111 | 
            +
                        write_attribute name, _value, :locale => locale
         | 
| 112 | 
            +
                      end
         | 
| 113 | 
            +
                    end
         | 
| 114 | 
            +
                  end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                  def define_translations_accessor(name)
         | 
| 117 | 
            +
                    define_translations_reader(name)
         | 
| 118 | 
            +
                    define_translations_writer(name)
         | 
| 119 | 
            +
                  end
         | 
| 120 | 
            +
                end
         | 
| 121 | 
            +
              end
         | 
| 122 | 
            +
            end
         | 
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            module Globalize
         | 
| 2 | 
            +
              module ActiveRecord
         | 
| 3 | 
            +
                module Exceptions
         | 
| 4 | 
            +
                  class MigrationError < StandardError; end
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  class BadFieldName < MigrationError
         | 
| 7 | 
            +
                    def initialize(field)
         | 
| 8 | 
            +
                      super("Missing translated field #{field.inspect}")
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  class BadFieldType < MigrationError
         | 
| 13 | 
            +
                    def initialize(name, type)
         | 
| 14 | 
            +
                      super("Bad field type for field #{name.inspect} (#{type.inspect}), should be :string or :text")
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| @@ -0,0 +1,219 @@ | |
| 1 | 
            +
            module Globalize
         | 
| 2 | 
            +
              module ActiveRecord
         | 
| 3 | 
            +
                module InstanceMethods
         | 
| 4 | 
            +
                  delegate :translated_locales, :to => :translations
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  def globalize
         | 
| 7 | 
            +
                    @globalize ||= Adapter.new(self)
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def attributes
         | 
| 11 | 
            +
                    super.merge(translated_attributes)
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def attributes=(attributes, *args)
         | 
| 15 | 
            +
                    with_given_locale(attributes) { super(attributes_without_locale(attributes)) }
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def assign_attributes(attributes, *args)
         | 
| 19 | 
            +
                    with_given_locale(attributes) { super(attributes_without_locale(attributes)) }
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
             | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def write_attribute(name, value, options = {})
         | 
| 25 | 
            +
                    return super(name, value) unless translated?(name)
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    options = {:locale => Globalize.locale}.merge(options)
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                    globalize.write(options[:locale], name, value)
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def [](attr_name)
         | 
| 33 | 
            +
                    if translated?(attr_name)
         | 
| 34 | 
            +
                      read_attribute(attr_name)
         | 
| 35 | 
            +
                    else
         | 
| 36 | 
            +
                      read_attribute(attr_name) { |n| missing_attribute(n, caller) }
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  def read_attribute(name, options = {})
         | 
| 41 | 
            +
                    options = {:translated => true, :locale => nil}.merge(options)
         | 
| 42 | 
            +
                    return super(name) unless options[:translated]
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                    if translated?(name)
         | 
| 45 | 
            +
                      if !(value = globalize.fetch(options[:locale] || Globalize.locale, name)).nil?
         | 
| 46 | 
            +
                        value
         | 
| 47 | 
            +
                      else
         | 
| 48 | 
            +
                        super(name)
         | 
| 49 | 
            +
                      end
         | 
| 50 | 
            +
                    else
         | 
| 51 | 
            +
                      super(name)
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def attribute_names
         | 
| 56 | 
            +
                    translated_attribute_names.map(&:to_s) + super
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  delegate :translated?, :to => :class
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  def translated_attributes
         | 
| 62 | 
            +
                    translated_attribute_names.inject({}) do |attributes, name|
         | 
| 63 | 
            +
                      attributes.merge(name.to_s => send(name))
         | 
| 64 | 
            +
                    end
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  # This method is basically the method built into Rails
         | 
| 68 | 
            +
                  # but we have to pass {:translated => false}
         | 
| 69 | 
            +
                  def untranslated_attributes
         | 
| 70 | 
            +
                    attribute_names.inject({}) do |attrs, name|
         | 
| 71 | 
            +
                      attrs[name] = read_attribute(name, {:translated => false}); attrs
         | 
| 72 | 
            +
                    end
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  def set_translations(options)
         | 
| 76 | 
            +
                    options.keys.each do |locale|
         | 
| 77 | 
            +
                      translation = translation_for(locale) ||
         | 
| 78 | 
            +
                                    translations.build(:locale => locale.to_s)
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                      options[locale].each do |key, value|
         | 
| 81 | 
            +
                        translation.send :"#{key}=", value
         | 
| 82 | 
            +
                      end
         | 
| 83 | 
            +
                      translation.save
         | 
| 84 | 
            +
                    end
         | 
| 85 | 
            +
                    globalize.reset
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  def reload(options = nil)
         | 
| 89 | 
            +
                    translation_caches.clear
         | 
| 90 | 
            +
                    translated_attribute_names.each { |name| @attributes.reset(name.to_s) }
         | 
| 91 | 
            +
                    globalize.reset
         | 
| 92 | 
            +
                    super(options)
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  def initialize_dup(other)
         | 
| 96 | 
            +
                    @globalize = nil
         | 
| 97 | 
            +
                    @translation_caches = nil
         | 
| 98 | 
            +
                    super
         | 
| 99 | 
            +
                    other.each_locale_and_translated_attribute do |locale, name|
         | 
| 100 | 
            +
                      globalize.write(locale, name, other.globalize.fetch(locale, name) )
         | 
| 101 | 
            +
                    end
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  def translation
         | 
| 105 | 
            +
                    translation_for(::Globalize.locale)
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  def translation_for(locale, build_if_missing = true)
         | 
| 109 | 
            +
                    unless translation_caches[locale]
         | 
| 110 | 
            +
                      # Fetch translations from database as those in the translation collection may be incomplete
         | 
| 111 | 
            +
                      _translation = translations.detect{|t| t.locale.to_s == locale.to_s}
         | 
| 112 | 
            +
                      _translation ||= translations.with_locale(locale).first unless translations.loaded?
         | 
| 113 | 
            +
                      _translation ||= translations.build(:locale => locale) if build_if_missing
         | 
| 114 | 
            +
                      translation_caches[locale] = _translation if _translation
         | 
| 115 | 
            +
                    end
         | 
| 116 | 
            +
                    translation_caches[locale]
         | 
| 117 | 
            +
                  end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                  def translation_caches
         | 
| 120 | 
            +
                    @translation_caches ||= {}
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  def translations_by_locale
         | 
| 124 | 
            +
                    translations.each_with_object(HashWithIndifferentAccess.new) do |t, hash|
         | 
| 125 | 
            +
                      hash[t.locale] = block_given? ? yield(t) : t
         | 
| 126 | 
            +
                    end
         | 
| 127 | 
            +
                  end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                  def translated_attribute_by_locale(name)
         | 
| 130 | 
            +
                    translations_by_locale(&:"#{name}")
         | 
| 131 | 
            +
                  end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                  # Get available locales from translations association, without a separate distinct query
         | 
| 134 | 
            +
                  def available_locales
         | 
| 135 | 
            +
                    translations.map(&:locale).uniq
         | 
| 136 | 
            +
                  end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                  def globalize_fallbacks(locale)
         | 
| 139 | 
            +
                    Globalize.fallbacks(locale)
         | 
| 140 | 
            +
                  end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                  def rollback
         | 
| 143 | 
            +
                    translation_caches[::Globalize.locale] = translation.previous_version
         | 
| 144 | 
            +
                  end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                  def save(*)
         | 
| 147 | 
            +
                    result = Globalize.with_locale(translation.locale || I18n.default_locale) do
         | 
| 148 | 
            +
                      without_fallbacks do
         | 
| 149 | 
            +
                        super
         | 
| 150 | 
            +
                      end
         | 
| 151 | 
            +
                    end
         | 
| 152 | 
            +
                    if result
         | 
| 153 | 
            +
                      globalize.clear_dirty
         | 
| 154 | 
            +
                    end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                    result
         | 
| 157 | 
            +
                  end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                  def column_for_attribute name
         | 
| 160 | 
            +
                    return super if translated_attribute_names.exclude?(name)
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                    globalize.send(:column_for_attribute, name)
         | 
| 163 | 
            +
                  end
         | 
| 164 | 
            +
             | 
| 165 | 
            +
                  def cache_key
         | 
| 166 | 
            +
                    [super, translation.cache_key].join("/")
         | 
| 167 | 
            +
                  end
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                  def changed?
         | 
| 170 | 
            +
                    changed_attributes.present? || translations.any?(&:changed?)
         | 
| 171 | 
            +
                  end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                protected
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                  def attributes_without_locale(attributes)
         | 
| 176 | 
            +
                    attributes.try(:except, :locale)
         | 
| 177 | 
            +
                  end
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                  def each_locale_and_translated_attribute
         | 
| 180 | 
            +
                    used_locales.each do |locale|
         | 
| 181 | 
            +
                      translated_attribute_names.each do |name|
         | 
| 182 | 
            +
                        yield locale, name
         | 
| 183 | 
            +
                      end
         | 
| 184 | 
            +
                    end
         | 
| 185 | 
            +
                  end
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                  def used_locales
         | 
| 188 | 
            +
                    locales = globalize.stash.keys.concat(globalize.stash.keys).concat(translations.translated_locales)
         | 
| 189 | 
            +
                    locales.uniq!
         | 
| 190 | 
            +
                    locales
         | 
| 191 | 
            +
                  end
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                  def save_translations!
         | 
| 194 | 
            +
                    globalize.save_translations!
         | 
| 195 | 
            +
                    translation_caches.clear
         | 
| 196 | 
            +
                  end
         | 
| 197 | 
            +
             | 
| 198 | 
            +
                  def with_given_locale(_attributes, &block)
         | 
| 199 | 
            +
                    attributes = _attributes.dup
         | 
| 200 | 
            +
                    attributes.symbolize_keys! if attributes.respond_to?(:symbolize_keys)
         | 
| 201 | 
            +
             | 
| 202 | 
            +
                    if locale = attributes.try(:delete, :locale)
         | 
| 203 | 
            +
                      Globalize.with_locale(locale, &block)
         | 
| 204 | 
            +
                    else
         | 
| 205 | 
            +
                      yield
         | 
| 206 | 
            +
                    end
         | 
| 207 | 
            +
                  end
         | 
| 208 | 
            +
             | 
| 209 | 
            +
                  def without_fallbacks
         | 
| 210 | 
            +
                    before = self.fallbacks_for_empty_translations
         | 
| 211 | 
            +
                    self.fallbacks_for_empty_translations = false
         | 
| 212 | 
            +
                    yield
         | 
| 213 | 
            +
                  ensure
         | 
| 214 | 
            +
                    self.fallbacks_for_empty_translations = before
         | 
| 215 | 
            +
                  end
         | 
| 216 | 
            +
             | 
| 217 | 
            +
                end
         | 
| 218 | 
            +
              end
         | 
| 219 | 
            +
            end
         | 
| @@ -0,0 +1,192 @@ | |
| 1 | 
            +
            require 'digest/sha1'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Globalize
         | 
| 4 | 
            +
              module ActiveRecord
         | 
| 5 | 
            +
                module Migration
         | 
| 6 | 
            +
                  def globalize_migrator
         | 
| 7 | 
            +
                    @globalize_migrator ||= Migrator.new(self)
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  delegate :create_translation_table!, :add_translation_fields!,
         | 
| 11 | 
            +
                    :drop_translation_table!, :translation_index_name,
         | 
| 12 | 
            +
                    :translation_locale_index_name, :to => :globalize_migrator
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  class Migrator
         | 
| 15 | 
            +
                    include Globalize::ActiveRecord::Exceptions
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    attr_reader :model
         | 
| 18 | 
            +
                    delegate :translated_attribute_names, :connection, :table_name,
         | 
| 19 | 
            +
                      :table_name_prefix, :translations_table_name, :columns, :to => :model
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    def initialize(model)
         | 
| 22 | 
            +
                      @model = model
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    def fields
         | 
| 26 | 
            +
                      @fields ||= complete_translated_fields
         | 
| 27 | 
            +
                    end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                    def create_translation_table!(fields = {}, options = {})
         | 
| 30 | 
            +
                      @fields = fields
         | 
| 31 | 
            +
                      # If we have fields we only want to create the translation table with those fields
         | 
| 32 | 
            +
                      complete_translated_fields if fields.blank?
         | 
| 33 | 
            +
                      validate_translated_fields
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                      create_translation_table
         | 
| 36 | 
            +
                      add_translation_fields!(fields, options)
         | 
| 37 | 
            +
                      create_translations_index
         | 
| 38 | 
            +
                      clear_schema_cache!
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    def add_translation_fields!(fields, options = {})
         | 
| 42 | 
            +
                      @fields = fields
         | 
| 43 | 
            +
                      validate_translated_fields
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                      add_translation_fields
         | 
| 46 | 
            +
                      clear_schema_cache!
         | 
| 47 | 
            +
                      move_data_to_translation_table if options[:migrate_data]
         | 
| 48 | 
            +
                      remove_source_columns if options[:remove_source_columns]
         | 
| 49 | 
            +
                      clear_schema_cache!
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                    def remove_source_columns
         | 
| 53 | 
            +
                      connection.remove_columns(table_name, *fields.keys)
         | 
| 54 | 
            +
                    end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    def drop_translation_table!(options = {})
         | 
| 57 | 
            +
                      move_data_to_model_table if options[:migrate_data]
         | 
| 58 | 
            +
                      drop_translations_index
         | 
| 59 | 
            +
                      drop_translation_table
         | 
| 60 | 
            +
                      clear_schema_cache!
         | 
| 61 | 
            +
                    end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                    # This adds all the current translated attributes of the model
         | 
| 64 | 
            +
                    # It's a problem because in early migrations would add all the translated attributes
         | 
| 65 | 
            +
                    def complete_translated_fields
         | 
| 66 | 
            +
                      translated_attribute_names.each do |name|
         | 
| 67 | 
            +
                        @fields[name] ||= column_type(name)
         | 
| 68 | 
            +
                      end
         | 
| 69 | 
            +
                    end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                    def create_translation_table
         | 
| 72 | 
            +
                      connection.create_table(translations_table_name) do |t|
         | 
| 73 | 
            +
                        t.references table_name.sub(/^#{table_name_prefix}/, '').singularize, :null => false, :index => false, :type => column_type(model.primary_key).to_sym
         | 
| 74 | 
            +
                        t.string :locale, :null => false
         | 
| 75 | 
            +
                        t.timestamps :null => false
         | 
| 76 | 
            +
                      end
         | 
| 77 | 
            +
                    end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                    def add_translation_fields
         | 
| 80 | 
            +
                      connection.change_table(translations_table_name) do |t|
         | 
| 81 | 
            +
                        fields.each do |name, options|
         | 
| 82 | 
            +
                          if options.is_a? Hash
         | 
| 83 | 
            +
                            t.column name, options.delete(:type), options
         | 
| 84 | 
            +
                          else
         | 
| 85 | 
            +
                            t.column name, options
         | 
| 86 | 
            +
                          end
         | 
| 87 | 
            +
                        end
         | 
| 88 | 
            +
                      end
         | 
| 89 | 
            +
                    end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    def create_translations_index
         | 
| 92 | 
            +
                      connection.add_index(
         | 
| 93 | 
            +
                        translations_table_name,
         | 
| 94 | 
            +
                        "#{table_name.sub(/^#{table_name_prefix}/, "").singularize}_id",
         | 
| 95 | 
            +
                        :name => translation_index_name
         | 
| 96 | 
            +
                      )
         | 
| 97 | 
            +
                      # index for select('DISTINCT locale') call in translation.rb
         | 
| 98 | 
            +
                      connection.add_index(
         | 
| 99 | 
            +
                        translations_table_name,
         | 
| 100 | 
            +
                        :locale,
         | 
| 101 | 
            +
                        :name => translation_locale_index_name
         | 
| 102 | 
            +
                      )
         | 
| 103 | 
            +
                    end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                    def drop_translation_table
         | 
| 106 | 
            +
                      connection.drop_table(translations_table_name)
         | 
| 107 | 
            +
                    end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                    def drop_translations_index
         | 
| 110 | 
            +
                      if connection.indexes(translations_table_name).map(&:name).include?(translation_index_name)
         | 
| 111 | 
            +
                        connection.remove_index(translations_table_name, :name => translation_index_name)
         | 
| 112 | 
            +
                      end
         | 
| 113 | 
            +
                    end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                    def move_data_to_translation_table
         | 
| 116 | 
            +
                      model.find_each do |record|
         | 
| 117 | 
            +
                        translation = record.translation_for(I18n.locale) || record.translations.build(:locale => I18n.locale)
         | 
| 118 | 
            +
                        fields.each do |attribute_name, attribute_type|
         | 
| 119 | 
            +
                          translation[attribute_name] = record.read_attribute(attribute_name, {:translated => false})
         | 
| 120 | 
            +
                        end
         | 
| 121 | 
            +
                        translation.save!
         | 
| 122 | 
            +
                      end
         | 
| 123 | 
            +
                    end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                    def move_data_to_model_table
         | 
| 126 | 
            +
                      add_missing_columns
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                      # Find all of the translated attributes for all records in the model.
         | 
| 129 | 
            +
                      all_translated_attributes = model.all.collect{|m| m.attributes}
         | 
| 130 | 
            +
                      all_translated_attributes.each do |translated_record|
         | 
| 131 | 
            +
                        # Create a hash containing the translated column names and their values.
         | 
| 132 | 
            +
                        translated_attribute_names.inject(fields_to_update={}) do |f, name|
         | 
| 133 | 
            +
                          f.update({name.to_sym => translated_record[name.to_s]})
         | 
| 134 | 
            +
                        end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                        # Now, update the actual model's record with the hash.
         | 
| 137 | 
            +
                        model.where(model.primary_key.to_sym => translated_record[model.primary_key]).update_all(fields_to_update)
         | 
| 138 | 
            +
                      end
         | 
| 139 | 
            +
                    end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                    def validate_translated_fields
         | 
| 142 | 
            +
                      fields.each do |name, options|
         | 
| 143 | 
            +
                        raise BadFieldName.new(name) unless valid_field_name?(name)
         | 
| 144 | 
            +
                        type = (options.is_a? Hash) ? options[:type] : options
         | 
| 145 | 
            +
                        raise BadFieldType.new(name, type) unless valid_field_type?(name, type)
         | 
| 146 | 
            +
                      end
         | 
| 147 | 
            +
                    end
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                    def column_type(name)
         | 
| 150 | 
            +
                      columns.detect { |c| c.name == name.to_s }.try(:type)
         | 
| 151 | 
            +
                    end
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                    def valid_field_name?(name)
         | 
| 154 | 
            +
                      translated_attribute_names.include?(name)
         | 
| 155 | 
            +
                    end
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                    def valid_field_type?(name, type)
         | 
| 158 | 
            +
                      !translated_attribute_names.include?(name) || [:string, :text, :citext].include?(type)
         | 
| 159 | 
            +
                    end
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                    def translation_index_name
         | 
| 162 | 
            +
                      index_name = "index_#{translations_table_name}_on_#{table_name.singularize}_id"
         | 
| 163 | 
            +
                      index_name.size < connection.index_name_length ? index_name :
         | 
| 164 | 
            +
                        "index_#{Digest::SHA1.hexdigest(index_name)}"[0, connection.index_name_length]
         | 
| 165 | 
            +
                    end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                    def translation_locale_index_name
         | 
| 168 | 
            +
                      index_name = "index_#{translations_table_name}_on_locale"
         | 
| 169 | 
            +
                      index_name.size < connection.index_name_length ? index_name :
         | 
| 170 | 
            +
                        "index_#{Digest::SHA1.hexdigest(index_name)}"[0, connection.index_name_length]
         | 
| 171 | 
            +
                    end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                    def clear_schema_cache!
         | 
| 174 | 
            +
                      connection.schema_cache.clear! if connection.respond_to? :schema_cache
         | 
| 175 | 
            +
                      model::Translation.reset_column_information
         | 
| 176 | 
            +
                      model.reset_column_information
         | 
| 177 | 
            +
                    end
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                    private
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                    def add_missing_columns
         | 
| 182 | 
            +
                      clear_schema_cache!
         | 
| 183 | 
            +
                      translated_attribute_names.map(&:to_s).each do |attribute|
         | 
| 184 | 
            +
                        unless model.column_names.include?(attribute)
         | 
| 185 | 
            +
                          connection.add_column(table_name, attribute, model::Translation.columns_hash[attribute].type)
         | 
| 186 | 
            +
                        end
         | 
| 187 | 
            +
                      end
         | 
| 188 | 
            +
                    end
         | 
| 189 | 
            +
                  end
         | 
| 190 | 
            +
                end
         | 
| 191 | 
            +
              end
         | 
| 192 | 
            +
            end
         |