martes, 1 de abril de 2008

Rails en español

Existen muchas opciones para hacer localización e internacionalización en Ruby on Rails. Entre todas ellas, la que me parece más sencilla es el plugin Localization Simplified. Sin embargo, me interesaban principalmente dos cosas: (1) agregar código sólo para español a mi aplicación, y (2) meterle la mano en los bolsillos a Rails y averiguar bien qué se debía hacer y por qué. En menos palabras: porque soy un maniático y un idiota ;-)

Hay diferentes cosas que hay que ajustar en Rails para que toda la información que colocamos en nuestras aplicaciones salga correctamente en español. Buscando en internet, encontré bastantes sitios donde aparecen consejos sobre cómo hacer muchas de estas cosas. Pero cada consejo, cada nueva solución, generaba un nuevo error, o advertencia, y tenía que seguir buscando. Con un poco de lectura y trabajo, fui uniendo y ajustando las diferentes soluciones que iba encontrando. A continuación les muestro la solución final que logré.

En primer lugar creamos un subdirectorio app\overrides, y allí colocamos los siguientes archivos:

  • errores.rb
module ActiveRecord
  class Errors
    begin
      @@default_error_messages = {
        :inclusion => "no está incluido en la lista",
        :exclusion => "está reservado",
        :invalid => "no es válido",
        :confirmation => "no es una confirmación",
        :accepted  => "debe ser aceptado",
        :empty => "no puede estar vacío",
        :blank => "no puede estar en blanco",
        :too_long => "es demasiado largo (máximo %d caracteres)",
        :too_short => "es demasiado corto (mínimo %d caracteres)",
        :wrong_length => "no tiene la longitud correcta (debería tener %d caracteres)",
        :taken => "ya ha sido incluido",
        :not_a_number => "debe ser un número" }
    end
  end
end

module ActionView #nodoc
  module Helpers
    module ActiveRecordHelper
      def error_messages_for(object_name, options = {})
        options = options.symbolize_keys
        object = instance_variable_get("@#{object_name}")
        unless object.errors.empty?
          content_tag("div", content_tag(options[:header_tag] || "h2", "Hay errores que impiden guardar el registro") +
          content_tag("p", "Compruebe los siguientes campos:") +
          content_tag("ul", object.errors.full_messages.collect { |msg| content_tag("li", msg) }), "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation" )
        end
      end
    end
  end
end
  • date.rb
require 'date'

class Date
  remove_const(:MONTHNAMES)
  MONTHNAMES = [nil] + %w(Enero Febrero Marzo Abril Mayo Junio Julio Agosto Septiembre Octubre Noviembre Diciembre)
  remove_const(:ABBR_MONTHNAMES)
  ABBR_MONTHNAMES = [nil] + %w(Ene Feb Mar Abr May Jun Jul Ago Sep Oct Nov Dic)
  remove_const(:DAYNAMES)
  DAYNAMES = %w(Domingo Lunes Martes Miercoles Jueves Viernes Sábado)
  remove_const(:ABBR_DAYNAMES)
  ABBR_DAYNAMES = %w(Dom Lun Mar Mié Jue Vie Sáb)
end

class Time
  alias :strftime_nolocale :strftime

  def strftime(format)
    format = format.dup
    format.gsub!(/%a/, Date::ABBR_DAYNAMES[self.wday])
    format.gsub!(/%A/, Date::DAYNAMES[self.wday])
    format.gsub!(/%b/, Date::ABBR_MONTHNAMES[self.mon])
    format.gsub!(/%B/, Date::MONTHNAMES[self.mon])
    self.strftime_nolocale(format)
  end
end

module ActionView
  module Helpers
    module DateHelper

      def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
        from_time = from_time.to_time if from_time.respond_to?(:to_time)
        to_time = to_time.to_time if to_time.respond_to?(:to_time)
        distance_in_minutes = (((to_time - from_time).abs)/60).round
        distance_in_seconds = ((to_time - from_time).abs).round

        case distance_in_minutes
          when 0..1
            return (distance_in_minutes == 0) ? 'menos de un minuto' : '1 minuto' unless include_seconds
            case distance_in_seconds
              when 0..4   then 'menos de 5 segundos'
              when 5..9   then 'menos de 10 segundos'
              when 10..19 then 'menos de 20 segundos'
              when 20..39 then 'medio minuto'
              when 40..59 then 'menos de un minuto'
              else             '1 minuto'
            end

          when 2..44           then "#{distance_in_minutes} minutos"
          when 45..89          then 'aprox. 1 hora'
          when 90..1439        then "aprox. #{(distance_in_minutes.to_f / 60.0).round} horas"
          when 1440..2879      then '1 día'
          when 2880..43199     then "#{(distance_in_minutes / 1440).round} días"
          when 43200..86399    then 'aprox. 1 mes'
          when 86400..525599   then "#{(distance_in_minutes / 43200).round} months"
          when 525600..1051199 then 'aprox. 1 año'
          else                      "over #{(distance_in_minutes / 525600).round} years"
        end
      end
      
    end
  end
end
  • all.rb
Dir[File.dirname(__FILE__) + "/**/*.rb"].each { |file| require(file) }

Luego colocamos, al final del archivo config\environment.rb:

require "#{RAILS_ROOT}/app/overrides/all"

Por último, en el archivo config\initializers\inflections.rb:

Inflector.inflections do |inflect|
  inflect.plural /([aeiou])([A-Z]|_|$)/, '\1s\2'
  inflect.plural /([rlnd])([A-Z]|_|$)/, '\1es\2'
  inflect.singular /([aeiou])s([A-Z]|_|$)/, '\1\2'
  inflect.singular /([rlnd])es([A-Z]|_|$)/, '\1\2'
  inflect.irregular 'user', 'users'
  inflect.irregular 'account', 'accounts'
  inflect.irregular 'password', 'passwords'
  inflect.irregular 'session', 'sessions'
end

Noten que el final del código de pluralizaciones agrego cuatro líneas para user, account, password y session. Esto se debe a que quiero que todas las pluralizaciones que haga Rails en mi aplicación, las haga con las reglas del español, así que tengo que agregar excepciones para los modelos que están en inglés (en este caso, los modelos para el manejo de usuarios, que vienen del plugin Restful Authentication). Si tienen algún modelo en inglés, pueden agregar la regla para pluralizarlo de la misma manera allí.

Todo esto se debe encargar de colocar en español los mensajes de error, las fechas (meses, días), y las pluralizaciones de Rails.

ACTUALIZACIÓN (12/04/2008): Agregué la traducción de la función distance_of_time_in_words al archivo app\overrides\date.rb. También es recomendable traducir las plantillas del generador scaffold que se encuentran en {RUBY}\lib\ruby\gems\{VERSION DE RUBY}\gems\rails-{VERSION DE RAILS}\lib\rails_generator\generators\components\scaffold\templates

Fuentes del código:

12 comentarios:

Unknown dijo...

Pana que alegría encontrar un Venezolano que le guste llegar al fondo de los problemas con soluciones en mano. Voy a probar todo este código que de entrada se ve muy bien.

Estoy iniciando un proyecto con rails 2.x.x y la creación de los modelos se me ha hecho dificil por este tipo de problemas.

Gracias por tan buen aporte!

Unknown dijo...

Mis felicitaciones
Cristian
http://z-nexus.net

Anónimo dijo...

Gracias por el post. Sólo un detalle para los que utilicen Ruby on Rails 2.2.2; hay que hacer una pequeña modificación. Siguiendo con el ejemplo citado arriba, quedaría:

ActiveSupport::Inflector.inflections do |inflect|
inflect.plural /([aeiou])([A-Z]|_|$)/, '\1s\2'
inflect.plural /([rlnd])([A-Z]|_|$)/, '\1es\2'
inflect.singular /([aeiou])s([A-Z]|_|$)/, '\1\2'
inflect.singular /([rlnd])es([A-Z]|_|$)/, '\1\2'
inflect.irregular 'user', 'users'
inflect.irregular 'account', 'accounts'
inflect.irregular 'password', 'passwords'
inflect.irregular 'session', 'sessions'
end

Saludos.

Cvielma dijo...

coye muchísimas gracias chamo! que bueno tu post, y sin tener que instalar plugins ni nada. Te felicito por lo fajado y por compartir tu fajación!

Saludos,

vladimir prieto dijo...

tengo dos problemillas:

1.- me funciona a medias todo esto. es decir, me salen algunos mensajes pero otros no. ejemplo:

"Nombre can't be blank"

para "validates_presence_of :nombre"

2.- la modificación a config\initializers\inflections.rb, me genera un error. será porque previamente ya tengo modelos pluralizados con las reglas en inglés (ej. institucions, en vez de instituciones)? o será porque estoy con ror 2.3.2?

como sea, si lo agrego se cae WEBrick con el siguiente error:

=> Rails 2.3.2 application starting on http://0.0.0.0:3001
/home/vladimir/.gem/ruby/1.8/gems/activesupport-2.3.2/lib/active_support/dependencies.rb:443:in `load_missing_constant': uninitialized constant Inflector (NameError)...(sigue)

cualquier ayuda se las agradecerpé.

saludos!

vladimir prieto dijo...

el 2° link está roto. el correcto es:

http://www.jorgebernal.info/dev-random/better-error_messages_for-i

Anónimo dijo...

Fantástica aportación. Muchas gracias a Jesús y también al anónimo por el matiz para los que usamos rails 2.2.2+

Alfredo Rico dijo...

Para Vladimir Prieto.
No sé si ya resolvistes el problema con el load_missing.. Pero si aún lo tienes verifica que la línea require "#{RAILS_ROOT}/app/overrides/all"
se encuentra después del "end"
en el archivo inflections.rb
Probablemente tengas:

.
.
.
require "#{RAILS_ROOT}/app/overrides/all"
end


Y debe ser:

.
.
.
end
require "#{RAILS_ROOT}/app/overrides/all"

Alfredo Rico dijo...

Hola.. Uno de mis socios creo este archivo de pluralizació para nombres de tablas con 1 o 2 palabras conformando el nombre. Si alguien lo puede hacer para cualaquier cantidad de palabras bienvenido es..
ActiveSupport::Inflector.inflections do |inflect|
inflect.plural /([aeiou])([A-Z]|_|$)/, '\1s\2'
inflect.plural /([rlnd])([A-Z]|_|$)/, '\1es\2'
inflect.plural /([aeiou])([A-Z]|_|$)([a-z]+)([rlnd])($)/, '\1s\2\3\4es\5'
inflect.plural /([rlnd])([A-Z]|_|$)([a-z]+)([aeiou])($)/, '\1es\2\3\4s\5'
inflect.singular /([aeiou])s([A-Z]|_|$)/, '\1\2'
inflect.singular /([rlnd])es([A-Z]|_|$)/, '\1\2'
inflect.singular /([aeiou])s([A-Z]|_)([a-z]+)([rlnd])es($)/, '\1\2\3\4\5'
inflect.singular /([rlnd])es([A-Z]|_)([a-z]+)([aeiou])s($)/, '\1\2\3\4\5'
inflect.irregular 'user', 'users'
inflect.irregular 'account', 'accounts'
inflect.irregular 'password', 'passwords'
inflect.irregular 'session', 'sessions'
end

Anónimo dijo...

hola, tu código me da un error en la linea 11 del date.rb:

el error es :
date.rb:11: unterminated string meets end of file (syntaxError)

te agradezco ayuda

saludos

SamuelVega dijo...

Hola, yo utilizo esto y me funciona bien tanto con una o mas palabras, underscore y CamelCase. Espero que os sirva de ayuda :)


ActiveSupport::Inflector.inflections do |inflect|
inflect.plural /([^djlnrs])([A-Z]|_|$)/, '\1s\2'
inflect.plural /([djlnrs])([A-Z]|_|$)/, '\1es\2'
inflect.plural /(.*)z([A-Z]|_|$)$/i, '\1ces\2'

inflect.singular /([^djlnrs])s([A-Z]|_|$)/, '\1\2'
inflect.singular /([djlnrs])es([A-Z]|_|$)/, '\1\2'
inflect.singular /(.*)ces([A-Z]|_|$)$/i, '\1z\2'

inflect.irregular 'user', 'users'
inflect.irregular 'account', 'accounts'
inflect.irregular 'password', 'passwords'
inflect.irregular 'session', 'sessions'
inflect.irregular 'ud', 'uds'
end

Juan Fuentes dijo...

Pana te debo una! Te invito a revisar la página que estoy migrando de php a rails www.joincic.com.ve