Rails internationalization - removing validation message prefix

Current ActiveRecord implementation always prepends an error message with the attribute name. This is because of design weakness in the handling of human messages in Rails.

The data should be always interpolated into the message, string concatenation should be never used.

Good:

MY_MESSAGE = '%{attribute} is invalid'
return MY_MESSAGE % {:attribute => 'email'}

Bad:

MY_MESSAGE = 'is invalid'
return MY_MESSAGE + ' ' + 'email'

… because the correct order of the words in a sentence depends on language.

Generally it is not possible to create a message in human language by string concatenation. Only interpolation universally works!

Still bad (from validations.rb):

attr_name + I18n.t('activerecord.errors.format.separator', 
  :default => ' ') + message

I18n.t would not help, if you use +.

Our custom message ‘Gadget should have at least one screenshot.’ is corrupted by a prefix to ‘Screenshots Gadget should have at least one screenshot.’

I hope one day it can be fixed in Rails by substantially refactoring the ActiveRecord validations and internationalization subsystem. For now I’m fixing it with a dash of metaprogramming and monkey patching.

module ActiveRecord
  class Errors

    # allow a proc as a user defined message
    def add(attribute, message = nil, options = {})
      if options[:default].is_a?(Proc) # this is new
        message = options[:default].call(attribute)
        def message.full? do
          true
        end
      end
      message ||= :invalid
      message = generate_message(attribute, message, options) if 
        message.is_a?(Symbol)
      @errors[attribute.to_s] ||= []
      @errors[attribute.to_s] << message
    end

    def full_messages(options = {})
      full_messages = []

      @errors.each_key do |attr|
        @errors[attr].each do |message|
          next unless message

          if attr == "base"
            full_messages << message
          else
            attr_name = @base.class.human_attribute_name(attr)
            if message.respond_to? :full? and message.full? # new switch
              full_messages << message
            else
              # messages for humans based on string concatenation are 
              # inherently broken - should not be used!
              full_messages << attr_name + 
                I18n.t('activerecord.errors.format.separator', 
                  :default => ' ') + message
            end
          end
        end
      end
      full_messages
    end

  end
end

Explanation:

  • copy a paste the code into a initializer (underneath config/initializers folder)
  • all translated messages created by a proc get the special full? tag which is later used in full_messages to bypass the attr_name addition
  • you can use attribute name in your proc to interpolate into the localized error message

This dirty hack is only needed because of design weekness of rails. Some conceptial thoughts:

Short and full error messages

Distinction between short and full error message does not make sence

  • one can not be universarly infered from another for every human language
  • specifing both manually hardly makes sense So there should be just error message with optional placeholder for the attribute name.

String interpolation everywhere

The data should be always interpolated into the error messages, never concatenated, because the order of the words in a sentence and gramatical rules differs from language to language.

Non-programmer capable translation files

YAML files and ruby symbol driven translation keys are unsuitable for non-programmers and people without deep knowledge of the application to be translated.

See GNU gettext for a deeper explanation of internationalization concepts that work!

Comments

blog comments powered by Disqus