DEV Community

Brandon Casci
Brandon Casci

Posted on

Internationalize and Humanize your Ruby on Rails application

Image description

Internationalization is the process of designing and developing web applications that can support multiple languages and locales. This is an important consideration for any web application that needs to reach a global audience, as it allows users to interact with the application in their preferred language and format.

Ruby on Rails provides built-in support for I18n, making it easy to create and manage translations for different languages. The humanization helpers of ActiveRecord and ActiveModel I18n, and you can leverage this to make you application support multiple languages, and to also humanize your object names and attributes to generate human readable content and forms.

Take the following two example screens from www.cboflow.com, a Rails application that I maintain. The first is in English, and the second is in Spanish. Using the strategies outlined in this article, I was able to make this application support multiple languages with minimal effort.

Image description

Image description

I18n and Rails

A new Rails application is configured to use I18n by default.

All of the translations are stored in resource files within the project's config/locales directory. Each file has to end with a language code, and .yml. So for example, you might have a file named config/locales/en.yml, with the following content:

  en:
    hello: "Hello, world!"
Enter fullscreen mode Exit fullscreen mode

In this folder, you can have as many subfolder as you like, and prefix the files as you wish. Some people like to have one large locale file for each supported language, while others like to have one file per object or view. For example, keeping all content in config/locales/en.yml, or breaking the content up into several files like config/locales/author.yml and config/locales/post.yml

The Rails Guide has a lot of information about using this feature.

How I use I18n in my own Rails applications

I try to use the same lingo across my applications.

I will create one locale file for each object that I intent to humanize in a view.

So if I have a Person model, I'll have a person.en.yml file, and more files for other languages like person.es.yml.

The default humanization for an attribute name is to the same as String#humanize, so Person#first_name will become First name by default, and if I am happy with that I probably will not make a translation entry.

I also tend to use gems like simple_form to generate my form HTML, and this saves me from having to maintain a lot of view code to outputting translated content onto forms. Also simple_form has it's own i18n convention that compliments the Rails default pretty well.

 # person.en.yml
  en:
    activerecord:
      models:
        person:
          one: Person
          other: People
    attributes:
      person/hispanic:
        true: 'Yes'
        false: 'No'               
      errors:
        models:
          person:
            attributes:
              birth_date:
                must_be_in_the_past: "can't be before today"
                must_be_formatted_correctly: "must be formatted like yyyy-mm-dd"
              full_name:
                blank: "can't be blank"
              email:
                invalid: "is not valid. It should look like name@domain.com"
              phone_number:
                invalid: "is not valid. It should look like 123-456-7890"
Enter fullscreen mode Exit fullscreen mode
# person.es.yml
  es:
    activerecord:
      models:
        person:
          one: "Persona"
          other: "Personas"
      attributes:
        person:
          birth_date: "Fecha de nacimiento"
          email: "Correo electrónico"
          full_name: "Nombre completo"
          hispanic: "Hispano"
          personal_information: "Información personal"
          phone_number: "Número de teléfono"
          self_described_gender: "Género autodescrito"
          self_described_race: "Raza autodescrita" 
        person/hispanic:
          true: "Sí"
          false: "No"
      errors:
        models:
          person:
            attributes:
              birth_date:
                must_be_in_the_past: "no puede ser antes de hoy"
                must_be_formatted_correctly: "debe tener el formato yyyy-mm-dd"
              full_name:
                blank: "no puede estar en blanco"
              email:
                invalid: "no es válido. Debe parecerse a nombre@dominio.com"
              phone_number:
                invalid: "no es válido. Debe parecerse a 123-456-7890"
Enter fullscreen mode Exit fullscreen mode

Gems like simple_form will label your forms with the correct languages, but you will have to call the model humanization helpers when outputting other types of content. You can also clean this up with your pwn helpers or presentation objets.

You manually pull translations like this:

  Person.human_attribute_name(:first_name) # -> First name
  Person.model_name.human # -> Person
  Person.model_name.human(count: 2) # -> People
  Person.human_attribute_name("hispanic.#{person.hispanic}") %> # -> Yes
Enter fullscreen mode Exit fullscreen mode

Humanizing Context and Service Objects with I18n.

Sometimes it will make sense to humanize one object's names and attributes with a totally new name in different parts of an app. For example, maybe it makes sense on one screen to refer to Person ans Profile, or Household Member.

Sometimes it also makes sense to wrap a sequence of steps into a service object that someone can initiate by filling out a form and clicking a button.

I'll use something like what's below to accomplish that.

Now you can do things like Clients::ChangeProfile.model_name.human or Clients::ChangeProfile.human_attribute_name(:first_name).

module Clients
  class ChangeProfile < BaseCommand
    attribute :full_name, :string
    attribute :birth_date, :date
    attribute :email, :string
    attribute :phone_number, :string
    attribute :hispanic, :boolean, default: false
    attribute :race_list, default: []
    attribute :self_described_race, :string
    attribute :gender_list, default: []
    attribute :self_described_gender, :string
    attribute :user

    validates :user, presence: true


    def run
      context = {}
      validate!
      context[:person] = person

      # While the implementation is not shown in this code sample, the attributes
      # for this ActiveModel based Command/Service Object are written across
      # several models within a transaction
      save!

      result(true, **context)
    rescue ActiveRecord::RecordInvalid, ActiveModel::ValidationError
      rollup_errors(person) if person
      result(false, **context)
    end

    # overriding model name here, so this command can be bound to a form
    # with ActionView and humanized with full I18n support

    def self.model_name
      ActiveModel::Name.new(self, nil, 'Profile')
    end

    # other implementation details and methods
    # ...
    # ...
    # ...
  end
end
Enter fullscreen mode Exit fullscreen mode
en:
  activerecord:
    models:
      person:
        one: Person
        other: People
      clients/profile:
        one: Profile
        other: Profiles
      household_member:
        one: Household Member
        other:  Household Members        
    attributes:
      person/hispanic:
        true: 'Yes'
        false: 'No'

    errors:
      models:
        person:
          attributes:  &person_error_attributes
            birth_date:
              must_be_in_the_past: "can't be before today"
              must_be_formatted_correctly: "must be formatted like yyyy-mm-dd"
            full_name:
              blank: "can't be blank"
            email:
              invalid: "is not valid. It should look like name@domain.com"
            phone_number:
              invalid: "is not valid. It should look like 123-456-7890"
        household_member:
          attributes:
            <<: *person_error_attributes
Enter fullscreen mode Exit fullscreen mode

Navigational links and contextual information

I may place all of my navigational and contextual information into a locale file like what's below.

This enables me to do things like this:

 <%= link_to t(:add_action, subject: Person.model_name.human), new_manage_client_care_distribution_path, class: 'btn btn-info' %>
Enter fullscreen mode Exit fullscreen mode
en:
  add: Add
  add_action: "Add %{subject}"
  all: All
  back: Back
  basic_information: Basic Information
  client_profile: Client Profile
  confirm: Are you sure?
  empty_search: "No %{subject} found"
  delete: Delete
  edit: Edit
  hello: "Hello world"
  history_action: "%{subject} History"  
  insights: Insights
  invite_action: "Invite %{subject}"  
  loading: Loading...
  provide_action: "Provide %{subject}"
  missing_object: "No %{subject}"
  new: New
  new_action: "New %{subject}"
  register_action: "Register %{subject}"
  summary: Summary
  view: View
  view_action: "View %{subject}"
Enter fullscreen mode Exit fullscreen mode

Conclusion

I18n is powerful and flexible, and really makes it easy to humanize your applications. There are other complexities that I have not covered here, like right-to-left languages, supporting many languages at once, and long form content, but I hope this article has given you a good starting point.

Top comments (0)