Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
210 views
in Technique[技术] by (71.8m points)

ruby on rails - How to validate translations using locale or fallthrough accessors in mobility?

How can I validate a model with locale or fallthrough accessors with mobility gem using the accessors on creation or updating?

Using mobility gem: v1.0.5

c = Category.new(name_en: 'Cat. 1', name_de: 'Kat. 1')
c.valid?
# => false
c.errors
# Should output the individual validation errors for every single locale accessor.

Example:

  • Name (en): is already taken.
  • Name (de): is valid.
=> #<ActiveModel::Errors:0x00007fb75c488ee0 ...
@messages={:name_en=>["has already been taken"]}, 
@details={:name_en=>[{:error=>:taken, :value=>"Cat. 1"}]}
question from:https://stackoverflow.com/questions/66052774/how-to-validate-translations-using-locale-or-fallthrough-accessors-in-mobility

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Finally I have coded a plugin called ValidatesAccessors. Feedback are welcome.

Config mobility

# config/initializers/mobility.rb

Mobility.configure do
  # PLUGINS
  plugins do
    # ...

    # One of both must be set.
    fallthrough_accessors
    locale_accessors
    
    # Add ValidatesAccessors plugin
    validates_accessors
  end
end

Add mobility plugin code

# lib/mobility/plugins/validates_accessors.rb

module Mobility
  # Mobility plugins.
  module Plugins
    # Adds accessor validation so you can easily validate accessor attributes like +name_en+.
    module ValidatesAccessors
      extend Plugin

      default true

      initialize_hook do |*names|
        if options[:validates_accessors] &&
           !options[:fallthrough_accessors] &&
           !options[:locale_accessors]
          warn 'The Validates Accessors plugin depends on Fallthrough Accessors or Locale '
               'Accessors being enabled, but both options are falsey'
        else
          options[:validates_accessors] = {} unless options[:validates_accessors].is_a?(Hash)
          options[:validates_accessors].reverse_merge!(locales: Mobility.available_locales)

          names.each do |name|
            define_accessors_locales(name, options[:validates_accessors])
          end

          include Mobility::Validations::Concern
        end
      end

      private

      def define_accessors_locales(name, options)
        module_eval <<~RUBY, __FILE__, __LINE__ + 1
          def mobility_accessors_locales_#{name}
            #{options[:locales].map(&:to_sym)}
          end
        RUBY
      end
    end

    register_plugin(:validates_accessors, ValidatesAccessors)
  end
end

Add mobility model concern code

# lib/mobility/validations/concern.rb

module Mobility
  module Validations
    # Mobility validations concern module.
    module Concern
      extend ActiveSupport::Concern

      private

      # This validation will perform a validation round against each mobility locales and add
      # errors for mobility attributes names.
      def validates_mobility_attributes
        # Only validates mobility attributes from the admin locale.
        return unless Mobility.locale.to_sym == I18n.locale.to_sym

        mobility_errors = mobility_errors_for_attributes(self.class.mobility_attributes)

        # Add translated attributes errors back to the object.
        mobility_errors.each do |attribute, attribute_errors|
          attribute_errors[:messages].zip(attribute_errors[:details]).each do |attribute_error|
            errors.add(attribute,
                       attribute_error.second.delete(:error),
                       message: attribute_error.first, **attribute_error.second)
          end
        end
      end

      # Return all translated attributes with errors for the given locales, including their error
      # messages.
      def mobility_errors_for_attributes(attribute_names)
        {}.tap do |mobility_errors|
          locales = mobility_accessors_locales(attribute_names)
          additional_locales = locales - [I18n.locale.to_sym]
          # Track errors for current locale.
          if locales.include?(I18n.locale.to_sym)
            mobility_errors.merge!(mobility_errors_for_locale(attribute_names, I18n.locale.to_sym))
          end

          # Validates the given object against each locale except the current one and track their
          # errors.
          additional_locales.each do |locale|
            Mobility.with_locale(locale) do
              if invalid?
                mobility_errors.merge!(mobility_errors_for_locale(attribute_names,
                                                                  locale))
              end
            end
          end
        end
      end

      # Return all translated attributes with errors for the given locale, including their error
      # messages.
      def mobility_errors_for_locale(attribute_names, locale)
        {}.tap do |mobility_errors|
          attribute_names.each do |attribute|
            next unless mobility_accessors_locales_for(attribute).include?(locale) &&
                        (messages = errors.messages.delete(attribute.to_sym)).present?

            mobility_errors["#{attribute}_#{locale.to_s.underscore}".to_sym] = {
              messages: messages,
              details:  errors.details.delete(attribute.to_sym)
            }
          end
        end
      end

      # Define which locales to validate against all translated attribute.
      def mobility_accessors_locales(attribute_names)
        locales = []
        attribute_names.each { |attribute| locales |= mobility_accessors_locales_for(attribute) }
        locales
      end

      # Define which locales to validate against for a single translated attribute.
      def mobility_accessors_locales_for(attribute)
        locales = if try("mobility_accessors_locales_#{attribute}").respond_to?(:call)
                    try("mobility_accessors_locales_#{attribute}").call(self)
                  else
                    try("mobility_accessors_locales_#{attribute}")
                  end
        locales || []
      end
    end
  end
end

Use the plugin

class Category < ApplicationRecord
  extend Mobility

  translates :name # without config.
  translates :name, validates_accessors: { locales: %i[en de fr] } # specifying locales to validate.
  translates :description, validates_accessors: { locales: %i[en de] } # using another attribute.
  translates :name, :description, validates_accessors: { locales: %i[en de fr] } # same config for both attributes.

  # Add here your translated attributes validations.
  validates :name, presence:   true,
                   uniqueness: true,
                   length:     { maximum: 5 }
    
  # After validations add mobility ValidatesAccessors validation.
  validate  :validates_mobility_attributes

  # ...
end

Example:

  • Name (en): is already taken.
  • Name (de): is valid.
=> #<ActiveModel::Errors:0x00007fb75c488ee0 ...
@messages={:name_en=>["has already been taken"]}, 
@details={:name_en=>[{:error=>:taken, :value=>"Cat. 1"}]}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...