---
title: "How should I structure Rails i18n YAML files?"
description: "Split `config/locales` by concern, such as `models/user/en.yml`, `views/users/en.yml` and `defaults/en.yml`; Rails 7.0+ loads subdirectories automatically. Name view keys after the view path so `t(\".title\")` works, and keep model and attribute names under `activerecord`."
category: localization-files
date: 2026-10-09
canonical: https://localizely.com/i18n-questions/localization-files/how-should-i-structure-rails-i18n-yaml-files/
---

# How should I structure Rails i18n YAML files?

Split the files by concern instead of keeping one `en.yml` per language, and let the key hierarchy follow the code: view keys mirror the view path, model and attribute names live under `activerecord`, and shared words go into a defaults file. Rails merges every file into one dictionary per locale, so the split is for people, not for the framework. The [Rails i18n guide](https://guides.rubyonrails.org/i18n.html#organization-of-locale-files) suggests a layout like this:

```text
config/locales/
  defaults/en.yml       # shared words, date and number formats
  models/user/en.yml    # activerecord.models and activerecord.attributes
  views/users/en.yml    # users.index.title, users.create.success
  views/layout/en.yml   # navigation, footer
```

Since Rails 7.0, `config/locales` is loaded recursively (the registered glob is `**/*.{rb,yml}`), so this works with no configuration. Rails 6.1 and older read only the top level; there you add the nested files yourself in `config/application.rb`:

```ruby
config.i18n.load_path += Dir[Rails.root.join("config", "locales", "**", "*.{rb,yml}")]
```

Every file starts with the locale key (`en:`), and a key defined in two files is silently overwritten by the file loaded later, so give each file its own branch of the tree.

View keys follow the template path, which is what lazy lookup expects. In `app/views/users/index.html.erb`, `t(".title")` resolves to `users.index.title`, and in `UsersController#create`, `t(".success")` resolves to `users.create.success`:

```yaml
en:
  users:
    index:
      title: "Users"
    create:
      success: "User created"
```

Model and attribute names belong under `activerecord`. `User.model_name.human` and `User.human_attribute_name(:name)` read them, and so do `form.label :name` and validation messages such as "Name can't be blank". Error messages are looked up from the most specific key, `activerecord.errors.models.user.attributes.name.blank`, down to `errors.messages.blank`. Form helpers have their own namespace: `helpers.label.user.name` overrides the label text, and `helpers.submit.user.create` and `update` set the button text with `%{model}` available for interpolation.

```yaml
en:
  activerecord:
    models:
      user: "Customer"
    attributes:
      user:
        name: "Full name"
  helpers:
    submit:
      user:
        create: "Add %{model}"
        update: "Save %{model}"
```

Naming rules that keep the files maintainable:

- Name keys after their meaning and place (`users.form.save`), never after the English text, which changes while the key stays.
- Keep keys static in code. `t("status.#{state}")` works, but static analysis cannot see the keys (i18n-tasks only infers them with `--no-strict`), so use such dynamic keys sparingly.
- Use the same tree in every language file; only the values differ.

The `i18n-tasks` gem checks all of this: `i18n-tasks missing` lists keys used in code but absent from a locale, `i18n-tasks unused` finds dead keys, `i18n-tasks normalize` sorts and formats the files, and its test template fails CI when a key is missing. A TMS such as Localizely reads and writes these per-language Rails YAML files, so the layout you pick is kept through translation. The file format itself is described at [Rails YAML file](/rails-yaml-file/).

Source: https://localizely.com/i18n-questions/localization-files/how-should-i-structure-rails-i18n-yaml-files/
