Translating Rails Fields

When we launched TaskRabbit in London, one of our goals was to have a fully localized product. There are enough differences between British English and US English to not cut (m)any corners. Of course, it’s even more important in completely different languages.

An issue that I noticed one day was in our signup flow. It said “Last name is required.” This was caused by a blank field and a model-level validation.

1
2
3
class User
  validates :last_name, presence: true
end

It was working as expected, but it should have said “Surname is required.” That’s what they want across the pond.

We translate all of our “en” locale into “en-GB” but this wasn’t there because it more or less works automatically. Active Model does a humanize call on the field name. To translate this field, which we had done on stranger field names, you add it to a YML locale file.

1
2
3
4
5
en:
  activerecord:
    attributes:
      user:
        last_name: 'Last Name'

This is for the one model, but there is fallback code in Active Model that also allows you to define the name for all models. You can do this:

1
2
3
en:
  attributes:
    last_name: 'Last Name'

This has the advantage of also working for another model as well as an Active Model service objects in one shot.

Generate a File

So I wrote some code to make sure all fields were defined in our “en” locale. This would ensure that they would get translated into “en-GB” and others. The values are just what Active Model would have done anyway, so there’s no functional difference. It does. however, prevent us from missing anything because of Rails magic.

Now we have a rake job that calls Translation::Fields.new.generate! to:

  • load all the models that have attributes/validations
  • clear out the file and reload I18n
  • go through all the models and look at each one’s attributes
  • if not already defined, add the humanized version to the file

Here is the code. It’s been working well so far.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
module Translation
  class Fields
    def initialize
      load_all_models
    end

    def generate!
      clear_file
      I18n.backend.send(:init_translations) # make sure init'd

      old_locale = I18n.locale
      I18n.locale = "en"
      load_children(ActiveRecord::Base)

      hash = build_hash
      write_file(hash)

      puts ""
      hash
    ensure
      I18n.locale = old_locale if old_locale
    end

    def file_name
      @file_name ||= Rails.root.join("config",
                                     "locales",
                                     "generated_default_fields.en.yml"
                                    ).to_s
    end

    def clear_file
      write_file({})
    end

    def write_file(to_file)
      File.open(file_name, 'w') do |file|
        file.write({'en' => {'attributes' => to_file}}.ya2yaml)
      end
    end

    def load_all_models
      # models
      Dir[Rails.root.join('/app/models/**/*.rb')].each do |path|
        require_dependency path
      end

      # can load more objects that have validations here
    end

    def load_children(klass)
      klass.subclasses.each do |klass|
        attributes = klass.attribute_names

        attributes.each do |attribute|
          current = I18n.t("attributes.#{attribute}", default: "")
          next if current.present?
          all_keys[attribute.to_s] = attribute
        end
      end
    end

    def build_hash
      out = {}
      all_keys.each do |key, value|
        out[key] = key.humanize
      end
      out
    end

    def all_keys
      @all_keys ||= {}
    end
  end
end
Copyright © 2017 Brian Leonard