92

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

Promise Preston
  • 24,334
  • 12
  • 145
  • 143
Chris Beck
  • 1,899
  • 2
  • 19
  • 26

18 Answers18

92

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"
Repolês
  • 1,563
  • 12
  • 14
  • 3
    How would you handle using this in a dropdown (ie when not displaying a single value)? – tirdadc Jan 16 '17 at 16:13
  • 7
    @tirdadc you can handle a dropdown like this: `<%= f.select :status, User.statuses.keys.collect { |status| [User.human_enum_name(:status, status), status] } %>`. – Repolês Aug 10 '17 at 13:34
  • 6
    +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: https://gist.github.com/abevoelker/fed59c2ec908de15acd27965e4725762 Call it in a view like `human_enum_name(@user, :status)` – Abe Voelker Oct 12 '17 at 02:54
  • 1
    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] }``` – armchairdj Feb 17 '18 at 21:14
  • 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 Mar 10 '22 at 19:57
  • @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. – Repolês Mar 11 '22 at 20:51
  • @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? – TPR Mar 13 '22 at 06:43
  • 1
    @TPR here's an example: https://gist.github.com/repoles/e798a915a0df49e3bcce0b7932478728. Let me know if you have any question. – Repolês Mar 21 '22 at 14:07
57

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}"
pierrea
  • 1,368
  • 14
  • 18
  • 5
    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 Apr 17 '14 at 13:13
34

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}")
Doguita
  • 15,403
  • 3
  • 27
  • 36
  • 2
    This is visually appealing but it breaks the rails convention of `activerecord.attributes.` being the `label` translation for form helpers – Chris Beck Jul 11 '15 at 15:00
  • 7
    @ChrisBeck it appears this follows the convention described in the Rails I18n Guide: http://guides.rubyonrails.org/i18n.html#translations-for-active-record-models – danblaker Dec 29 '17 at 01:21
  • 1
    In my experience this works without using the `role` key. You can nest `coordinator` and `teacher` directly under `user`. – Ryenski Oct 03 '20 at 22:03
  • what is human_attribute_name? – TPR Mar 10 '22 at 20:11
33

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
Community
  • 1
  • 1
Aliaksandr
  • 651
  • 6
  • 7
  • 1
    but how to get translation for single record in this case? Because `.human_attribute_name('genders.male')` don't work – Stiig Sep 30 '16 at 09:52
  • Thank you, works like charm in [my case](http://stackoverflow.com/q/42185293/6873497)! – matiss Feb 12 '17 at 16:35
  • I've made lightweight gem for these purposes https://github.com/shlima/translate_enum – Aliaksandr Mar 07 '17 at 19:49
  • FML - it is 2021 and this still doesn't properly work with simple_form. But - thanks to your comment I have a good workaround :-) – Hendrik Feb 08 '21 at 17:21
  • What is the genders of ```genders.map```? I keep getting ```undefined local variable or method `genders'``` – TPR Mar 10 '22 at 20:10
8

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])
Rafael Valverde
  • 301
  • 3
  • 2
  • 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. – schmijos Jun 21 '21 at 09:32
7

Model:

enum stage: { starting: 1, course: 2, ending: 3 }

def self.i18n_stages(hash = {})
  stages.keys.each { |key| hash[I18n.t("checkpoint_stages.#{key}")] = key }
  hash
end

Locale:

checkpoint_stages:
    starting: Saída
    course: Percurso
    ending: Chegada

And on the view (.slim):

= f.input_field :stage, collection: Checkpoint.i18n_stages, as: :radio_buttons
Oswaldo Ferreira
  • 1,339
  • 16
  • 15
7

Combining the answers from Repolês and Aliaksandr, for Rails 5, we can build 2 methods that allow you to translate a single value or a collection of values from an enum attribute.

Set up the translations in your .yml file:

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

In the ApplicationRecord class, from which all models inherit, we define a method that handles translations for a single value and another one that handles arrays by calling it:

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

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

  def self.translate_enum_collection(enum_name)
    enum_values = self.send(enum_name.to_s.pluralize).keys
    enum_values.map do |enum_value|
      self.translate_enum_name enum_name, enum_value
    end
  end
end 

In our views, we can then translate single values:

<p>User Status: <%= User.translate_enum_name :status, @user.status %></p>

Or the entire collection of enum values:

<%= f.select(:status, User.translate_enum_collection :status) %>
Community
  • 1
  • 1
ne5t
  • 71
  • 1
  • 4
  • 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` – Jorge Sampayo Aug 08 '21 at 02:34
4

I've created a gem for this.

http://rubygems.org/gems/translated_attribute_value

Add to your gemfile:

gem 'translated_attribute_value'

If you have a status field for user:

pt-BR:
  activerecord:
    attributes:
      user:
        status_translation:
          value1: 'Translation for value1'
          value2: 'Translation for value2'

And in your view you can call like this:

user.status_translated

It works with active record, mongoid or any other class with getter/setters:

https://github.com/viniciusoyama/translated_attribute_value

onebree
  • 1,853
  • 1
  • 17
  • 44
4

Try using TranslateEnum gem for these purposes

class Post < ActiveRecord::Base
  enum status: { published: 0, archive: 1 }
  translate_enum :status
end


Post.translated_status(:published)
Post.translated_statuses

@post = Post.new(status: :published)
@post.translated_status 
Aliaksandr
  • 651
  • 6
  • 7
  • 1
    We also use this gem. Has the cleanest approach from all options we evaluated and is well maintained. – cseelus Mar 07 '18 at 03:55
3

The model:

class User < ActiveRecord::Base
  enum role: [:master, :apprentice]
end

The locale file:

en:
  activerecord:
    attributes:
      user:
        master: Master
        apprentice: Apprentice

Usage:

User.human_attribute_name(:master) # => Master
User.human_attribute_name(:apprentice) # => Apprentice
Dmitry Shvetsov
  • 651
  • 10
  • 19
2

Try the enum_help gem. From its description:

Help ActiveRecord::Enum feature to work fine with I18n and simple_form.

Kukunin
  • 853
  • 7
  • 22
2

Heres a t_enum helper method that I use.

<%= t_enum(@user, :status) %>

enum_helper.rb:

module EnumHelper

  def t_enum(inst, enum)
    value = inst.send(enum);
    t_enum_class(inst.class, enum, value)
  end

  def t_enum_class(klass, enum, value)
    unless value.blank?
      I18n.t("activerecord.enums.#{klass.to_s.demodulize.underscore}.#{enum}.#{value}")
    end
  end

end

user.rb:

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

en.yml:

en:
  activerecord:
    enums:
      user:
        status:
          active:   "Active"
          pending:  "Pending..."
          archived: "Archived"
cweston
  • 11,297
  • 19
  • 82
  • 107
1

Yet another way, I find it a bit more convenient using a concern in models

Concern :

module EnumTranslation
  extend ActiveSupport::Concern

  def t_enum(enum)
    I18n.t "activerecord.attributes.#{self.class.name.underscore}.enums.#{enum}.#{self.send(enum)}"
  end
end

YML:

fr:
    activerecord:
      attributes:
        campaign:
          title: Titre
          short_description: Description courte
          enums:
            status:
              failed: "Echec"

View :

<% @campaigns.each do |c| %>
  <%= c.t_enum("status") %>
<% end %>

Don't forget to add concern in your model :

class Campaign < ActiveRecord::Base
  include EnumTranslation

  enum status: [:designed, :created, :active, :failed, :success]
end
Geoffrey H
  • 1,280
  • 2
  • 10
  • 33
1

I prefer a simple helper in application_helper

  def translate_enum(object, enum_name)
    I18n.t("activerecord.attributes.#{object.model_name.i18n_key}.#{enum_name.to_s.pluralize}.#{object.send(enum_name)}")
  end

Then in my YML file :

fr:
  activerecord:
    attributes:
      my_model:
        my_enum_plural:
          pending:  "En cours"
          accepted: "Accepté"
          refused:  "Refusé"
Snake
  • 1,157
  • 1
  • 10
  • 21
1

Model Order:

 enum order_type: {normal: false, security: true}.freeze, _default: :normal

OrdersController

    @order_type = Order.order_types.except(:security) unless secure_present # optional code
    @order_type.transform_keys! { |key| I18n.t("orders.new.order_types.#{key}")}

Locales (Ukrainian)

uk:
  orders:
    new:
      order_types:
        normal: 'Звичайна'
        security: 'Безпечна'
Secretpray
  • 11
  • 1
  • 3
0

You can simply add a helper:

def my_something_list
  modes = 'activerecord.attributes.mymodel.my_somethings'
  I18n.t(modes).map {|k, v| [v, k]}
end

and set it up as usually:

en:
  activerecord:
    attributes:
      mymodel:
        my_somethings:
           my_enum_value: "My enum Value!"

then use it with your select: my_something_list

ftshtw
  • 629
  • 1
  • 5
  • 19
0
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  def self.enum(definitions)
    defind_i18n_text(definitions) if definitions.delete(:_human)
    super(definitions)
  end

  def self.defind_i18n_text(definitions)
    scope = i18n_scope
    definitions.each do |name, values|
      next if name.to_s.start_with?('_')
      define_singleton_method("human_#{name.to_s.tableize}") do
        p values
        values.map { |key, _value| [key, I18n.t("#{scope}.enums.#{model_name.i18n_key}.#{name}.#{key}")] }.to_h
      end

      define_method("human_#{name}") do
        I18n.t("#{scope}.enums.#{model_name.i18n_key}.#{name}.#{send(name)}")
      end
    end
  end
end


en:
  activerecord:
    enums:
      mymodel:
        my_somethings:
           my_enum_value: "My enum Value!"

enum status: [:unread, :down], _human: true
Kevin.Xin
  • 86
  • 5
-1

Here is the simplest solution I have found.

Model file 'house.rb':

enum status: { unavailable: 0, available: 1 }

In the view, a simple_form select:

<%= simple_form_for(@house) do |f| %>
...
<%= f.input :status,
        collection: House.statuses.keys.map { |s| [t("house_enum_status_#{s}"), s] }
...
<% end %>

The collection creates an array with [key, value] expected for the select, with the correct translation.

And here is both locales yml files used:

'fr.yml'

house_enum_status_unavailable: "Indisponible"
house_enum_status_available: "Disponible"

'en.yml'

house_enum_status_unavailable: "Not available"
house_enum_status_available: "Available"

Here is the result in French

Here is the result in English

Franfran
  • 1
  • 1