Rails: How to use i18n with Rails 4 enums

38,756

Solution 1

I didn't find any specific pattern either, so I simply added:

en:
  user_status:
    active:   Active
    pending:  Pending...
    archived: Archived

to an arbitrary .yml file. Then in my views:

I18n.t :"user_status.#{user.status}"

Solution 2

Starting from Rails 5, all models will inherit from ApplicationRecord.

class User < ApplicationRecord
  enum status: [:active, :pending, :archived]
end

I use this superclass to implement a generic solution for translating enums:

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  def self.human_enum_name(enum_name, enum_value)
    I18n.t("activerecord.attributes.#{model_name.i18n_key}.#{enum_name.to_s.pluralize}.#{enum_value}")
  end
end

Then I add the translations in my .yml file:

en:
  activerecord:
    attributes:
      user:
        statuses:
          active: "Active"
          pending: "Pending"
          archived: "Archived"

Finally, to get the translation I use:

User.human_enum_name(:status, :pending)
=> "Pending"

Solution 3

Here is a view:

select_tag :gender, options_for_select(Profile.gender_attributes_for_select)

Here is a model (you can move this code into a helper or a decorator actually)

class Profile < ActiveRecord::Base
  enum gender: {male: 1, female: 2, trans: 3}

  # @return [Array<Array>]
  def self.gender_attributes_for_select
    genders.map do |gender, _|
      [I18n.t("activerecord.attributes.#{model_name.i18n_key}.genders.#{gender}"), gender]
    end
  end
end

And here is locale file:

en:
  activerecord:
    attributes:
      profile:
        genders:
          male: Male
          female: Female
          trans: Trans

Solution 4

To keep the internationalization similar as any other attribute I followed the nested attribute way as you can see here.

If you have a class User:

class User < ActiveRecord::Base
  enum role: [ :teacher, :coordinator ]
end

And a yml like this:

pt-BR:
  activerecord:
    attributes:
      user/role: # You need to nest the values under model_name/attribute_name
        coordinator: Coordenador
        teacher: Professor

You can use:

User.human_attribute_name("role.#{@user.role}")

Solution 5

Elaborating on user3647358's answer, you can accomplish that very closely to what you're used to when translating attributes names.

Locale file:

en:
  activerecord:
    attributes:
      profile:
        genders:
          male: Male
          female: Female
          trans: Trans

Translate by calling I18n#t:

profile = Profile.first
I18n.t(profile.gender, scope: [:activerecord, :attributes, :profile, :genders])
Share:
38,756

Related videos on Youtube

Chris Beck
Author by

Chris Beck

Software Architect and Rails developer at www.directra.com

Updated on February 24, 2022

Comments

  • Chris Beck
    Chris Beck about 2 years

    Rails 4 Active Record Enums are great, but what is the right pattern for translating with i18n?

  • Chris Beck
    Chris Beck about 10 years
    i did something similar, but i put it under {locale}.activerecord.attributes.{model}.{attribute}and wrote a t_enum(model, enum, value) helper method so the enum translations would be adjacent to the label translation
  • Chris Beck
    Chris Beck almost 9 years
    This is visually appealing but it breaks the rails convention of activerecord.attributes.<fieldname> being the label translation for form helpers
  • Stiig
    Stiig over 7 years
    but how to get translation for single record in this case? Because .human_attribute_name('genders.male') don't work
  • Code-MonKy
    Code-MonKy over 7 years
    How about @user.role, because that is the main issue.
  • tirdadc
    tirdadc over 7 years
    How would you handle using this in a dropdown (ie when not displaying a single value)?
  • matiss
    matiss about 7 years
    Thank you, works like charm in my case!
  • Aliaksandr
    Aliaksandr about 7 years
    I've made lightweight gem for these purposes github.com/shlima/translate_enum
  • Fabian Winkler
    Fabian Winkler about 7 years
    The most straight forward, clean and elegant way.
  • Shiyason
    Shiyason over 6 years
    AnyModel.human_attribute_name(:i_dont_exist) => "I dont exist"
  • Repolês
    Repolês over 6 years
    @tirdadc you can handle a dropdown like this: <%= f.select :status, User.statuses.keys.collect { |status| [User.human_enum_name(:status, status), status] } %>.
  • Abe Voelker
    Abe Voelker over 6 years
    +1 good answer. I tweaked it for my use to be a view helper method since I feel this is more of a view concern, and to not pluralize the attribute name: gist.github.com/abevoelker/fed59c2ec908de15acd27965e4725762 Call it in a view like human_enum_name(@user, :status)
  • danblaker
    danblaker over 6 years
    @ChrisBeck it appears this follows the convention described in the Rails I18n Guide: guides.rubyonrails.org/…
  • armchairdj
    armchairdj about 6 years
    Per Repolês, you could also add another class method to your base model for dropdowns: self.human_enum_collection(enum_name). Code would be send(enum_name.to_s.pluralize).keys.collect { |val| [human_enum_name(enum_name, val), val] }
  • cseelus
    cseelus about 6 years
    We also use this gem. Has the cleanest approach from all options we evaluated and is well maintained.
  • Ryenski
    Ryenski over 3 years
    In my experience this works without using the role key. You can nest coordinator and teacher directly under user.
  • Hendrik
    Hendrik about 3 years
    FML - it is 2021 and this still doesn't properly work with simple_form. But - thanks to your comment I have a good workaround :-)
  • schmijos
    schmijos almost 3 years
    This is the minimalist solution only using framework tools and therefore the best one in my eyes. Maybe add a test so that you cover all genders in your translations.
  • Jorge Sampayo
    Jorge Sampayo over 2 years
    This worked perfectly for me translating enums. There was only one change I needed to do to use it on selects, to put as value the key of the enum and as text the translation, instead of the map in translate_enum_collection: enum_values.each_with_object({}) do |enum_value, acc| acc[enum_value] = self.translate_enum_name(enum_name, enum_value) end And then in the view add an invert: User.translate_enum_collection(:status).invert
  • TPR
    TPR about 2 years
    Usually there is more than one enum name and enum value, does this mean that I have to def self.human_enum_name2 and def self.human_enum_name3 and so on?
  • TPR
    TPR about 2 years
    What is the genders of genders.map? I keep getting undefined local variable or method `genders'
  • TPR
    TPR about 2 years
    what is human_attribute_name?
  • Repolês
    Repolês about 2 years
    @TPR to use the method self.human_enum_name you have to specify the enum name and the enum value. This means only one generic class method will be enough, doesn't matter how many enums your ActiveRecord class has.
  • TPR
    TPR about 2 years
    @Repolês I see that the code has only one (enum_name, enum_value), if I have more than one enum name and enum value, how should I write this without confusion?
  • Repolês
    Repolês about 2 years
    @TPR here's an example: gist.github.com/repoles/e798a915a0df49e3bcce0b7932478728. Let me know if you have any question.