Rails: How to use i18n with Rails 4 enums
Asked Answered
S

18

93

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

Suavity answered 3/4, 2014 at 3:25 Comment(0)
K
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}"
Kazan answered 16/4, 2014 at 9:9 Comment(1)
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 translationSuavity
J
94

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"
Jaclin answered 31/3, 2016 at 13:58 Comment(8)
How would you handle using this in a dropdown (ie when not displaying a single value)?Croix
@Croix you can handle a dropdown like this: <%= f.select :status, User.statuses.keys.collect { |status| [User.human_enum_name(:status, status), status] } %>.Sententious
+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)Osmunda
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] }Saito
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?Lockwood
@Lockwood 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.Sententious
@Jaclin 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?Lockwood
@Lockwood here's an example: gist.github.com/repoles/e798a915a0df49e3bcce0b7932478728. Let me know if you have any question.Sententious
K
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}"
Kazan answered 16/4, 2014 at 9:9 Comment(1)
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 translationSuavity
C
36

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}")
Conjuncture answered 20/5, 2015 at 23:19 Comment(4)
This is visually appealing but it breaks the rails convention of activerecord.attributes.<fieldname> being the label translation for form helpersSuavity
@ChrisBeck it appears this follows the convention described in the Rails I18n Guide: guides.rubyonrails.org/…Henotheism
In my experience this works without using the role key. You can nest coordinator and teacher directly under user.Dineen
what is human_attribute_name?Lockwood
E
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
Erbium answered 17/5, 2014 at 10:31 Comment(5)
but how to get translation for single record in this case? Because .human_attribute_name('genders.male') don't workDiscord
Thank you, works like charm in my case!Disputatious
I've made lightweight gem for these purposes github.com/shlima/translate_enumErbium
FML - it is 2021 and this still doesn't properly work with simple_form. But - thanks to your comment I have a good workaround :-)Fane
What is the genders of genders.map? I keep getting undefined local variable or method `genders'Lockwood
E
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])
Erogenous answered 17/9, 2014 at 19:35 Comment(1)
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.Moslemism
D
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
Dismuke answered 16/7, 2014 at 14:52 Comment(0)
M
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) %>
Mincing answered 24/2, 2017 at 18:42 Comment(1)
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).invertUboat
C
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

Cutlass answered 16/9, 2014 at 18:11 Comment(0)
E
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 
Erbium answered 7/3, 2017 at 19:52 Comment(1)
We also use this gem. Has the cleanest approach from all options we evaluated and is well maintained.Sethsethi
S
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
Scientist answered 30/9, 2016 at 0:22 Comment(3)
How about @user.role, because that is the main issue.Metanephros
The most straight forward, clean and elegant way.Hermetic
AnyModel.human_attribute_name(:i_dont_exist) => "I dont exist"Enslave
S
2

Try the enum_help gem. From its description:

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

Stroh answered 24/4, 2015 at 13:18 Comment(0)
D
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"
Donaghue answered 25/2, 2016 at 18:0 Comment(0)
U
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
Unique answered 31/8, 2016 at 4:49 Comment(0)
C
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é"
Carpospore answered 10/5, 2017 at 8:28 Comment(0)
R
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: 'Безпечна'
Referential answered 10/8, 2022 at 18:7 Comment(0)
L
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

Lachesis answered 20/1, 2017 at 12:57 Comment(0)
E
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
Embayment answered 28/11, 2017 at 2:32 Comment(0)
E
-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

Editheditha answered 23/2, 2022 at 16:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.