Skip to content

Localization (i18n) ​

Avo names everything for you out of the box: resources and actions after their class names, fields after their ids, and the buttons and headings around them from Avo's own locale files, shipped in 19 languages through Rails' I18n module. When you want different copy, there are two ways to set it — and this page covers both.

Drive the copy from your locale files. Everything you build — resources, fields, actions, scopes, cards, dashboards, tabs, panels — resolves its copy from a predictable i18n key before anything else. One YAML file can rename the whole admin:

yaml
# config/locales/avo.es.yml
es:
  avo:
    resource_translations:
      user:
        one: "Usuario"
        other: "Usuarios"
        fields:
          created_at: "Fecha de creación"
    action_translations:
      toggle_inactive:
        name: "Alternar inactivo"

If your app serves multiple languages, definitely work this way: every string lives in the locale files, and adding a language means adding a file, never touching a resource. It's just as useful in a single-language app for renaming things in bulk without editing code.

Or set the copy in code, with the built-in properties. self.name and self.message on an action, name:, help: and placeholder: on a field, label: on a card. Use these when you want the string next to the thing it describes, or when a label needs data — they accept lambdas with Avo's execution context:

ruby
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
  self.name = "Toggle inactive"
  self.message = -> { "Toggle #{record.name}?" }
end

The two compose — a single cascade decides which wins — so you can start with strings in code and move them into locale files when the second language arrives.

Naming records is different

self.title = :name on a resource picks which attribute stands in for a record — the heading on its Show page, its label in a belongs_to select. That's data, not copy, so i18n doesn't enter into it. This page is about the copy around your records.

Translations are used exactly as you write them

Avo renders a resolved resource or field translation verbatim. It only humanizes the name it generates for you when no translation is found. Write one: 'Usuario' and you get Usuario; write one: 'usuario' and you get usuario. This also means acronyms and proper nouns survive -- 'Payment Intent ID' stays Payment Intent ID instead of becoming Payment intent id.

Multi-language URL Support

If you're serving Avo using multiple languages and you're using the locale in your routes (/en/resources/users, /de/resources/users), check out this guide.

Avo ships its own locale files (avo.en.yml plus 18 other languages) bundled inside the gem and loads them into the I18n module automatically, so bin/rails avo:install doesn't copy them into your app. When you want editable copies under config/locales, run the locale generator.

How Avo resolves a label ​

Every label goes through the same cascade. The first layer that has a value wins:

  1. The value at the call site — field :dates, name: "Period", or card Avo::Cards::UsersMetric, label: "Active users".
  2. The i18n key — derived per the key map below, or set explicitly with self.translation_key / translation_key:. For a field, the model's own attribute translation (activerecord.attributes.<model>.<attr>, then attributes.<attr>) is read right after Avo's keys — see Localizing fields.
  3. The class attribute — self.name, self.message, self.label, self.description on actions, scopes, cards, and dashboards.
  4. What Avo generates — the humanized class name or field id, or Avo's own translated string (avo.run, avo.save, …).

Not every layer exists everywhere — resources have no naming attribute, and actions take nothing at the call site — but the order never changes: an explicit call-site value beats the locale file, and the locale file beats a class attribute. That last part is what lets you keep English defaults in code and translate over them without touching the class.

The key map ​

Every key Avo derives, in one place:

You're namingAvo looks upKeys under itComes from
Resourceavo.resource_translations.<class_path>zero/one/other, description, plus the fields, tabs, panels and save subtreesAvo
Fieldavo.resource_translations.<resource>.fields.<field_id>, then avo.field_translations.<field_id>, then activerecord.attributes.<model>.<field_id> and attributes.<field_id>zero/one/other, help, placeholder, include_blank (the model keys take a plain string or one/other)Avo
Actionavo.action_translations.<class_path>name, message, confirm_button_label, cancel_button_label, descriptionAvo
Tabavo.resource_translations.<resource>.tabs.<slug>the title itselfAvo
Panelavo.resource_translations.<resource>.panels.<slug>the title itselfAvo
Save buttonavo.resource_translations.<resource>.save, then avo.savethe label itselfAvo
Scopeavo.scope_translations.<class_path>name, descriptionavo-scopes
Cardavo.card_translations.<class_path>label, description, discreet_descriptionavo-dashboards
Dashboardavo.dashboard_translations.<class_path>name, descriptionavo-dashboards

Filters are the one thing with no derived key yet — name them with a lambda, as shown in Where your own keys go.

How the paths are derived ​

  • <class_path> — the class name minus its DSL container prefix (Avo::Resources::, Avo::Actions::, Avo::Scopes::, Avo::Cards::, Avo::Dashboards::), underscored. A namespace becomes a slash: Avo::Resources::User → user, Avo::Actions::City::Update → city/update.
  • <field_id> — the field's first argument: field :created_at → created_at.
  • <slug> — the tab's or panel's title:, downcased with non-alphanumerics turned into _: title: "Contact information" → contact_information.
  • <resource> — the <class_path> of the resource the field, tab, panel, or save button belongs to.

Every derived path can be replaced with your own: self.translation_key = "..." on the class, or translation_key: "..." on a field, tab, or panel.

Localizing resources ​

Let's say you want to localize a resource. All you need to do is add a self.translation_key class attribute in the Resource file. That will tell Avo to use that translation key to localize this resource. That will change the labels of that resource everywhere in Avo.

ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
  self.title = :name
  self.translation_key = 'avo.resource_translations.user'
end
yaml
# avo.es.yml
es:
  avo:
    dashboard: 'Dashboard'
    # ... other translation keys
    resource_translations:
      user:
        zero: 'Usuarios'
        one: 'Usuario'
        other: 'Usuarios'
        description: 'Los usuarios de la aplicación'

These values are used as written, so capitalize them the way you want them to appear in page titles, breadcrumbs, and the sidebar.

description fills the resource's self.description text. Like everywhere else in the cascade, the locale file beats self.description on the class, so you can leave an English default in code and translate over it. The one thing to watch: if self.description is a block that reads record or view, don't add the key for that resource — a static translation would take its place.

If you don't set self.translation_key, Avo derives it from the resource's class name, namespace included — a namespaced resource like Avo::Resources::Galaxy::Planet defaults to avo.resource_translations.galaxy/planet.

Localizing actions ​

Actions follow the same convention. Avo looks up avo.action_translations.<class_path>.{name,message,confirm_button_label,cancel_button_label,description} before falling back to the class attributes (self.name, self.message, and so on).

ruby
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
  # Optional. Defaults to avo.action_translations.toggle_inactive
  # self.translation_key = "avo.action_translations.toggle_inactive"
end
yaml
# avo.es.yml
es:
  avo:
    action_translations:
      toggle_inactive:
        name: "Alternar inactivo"
        message: "¿Seguro que quieres alternar el estado inactivo?"
        confirm_button_label: "Alternar"
        cancel_button_label: "Cancelar"
        description: "Alterna el estado inactivo del usuario"
      city/update:
        name: "Actualizar ciudad"

Namespaced actions use the same underscored, slash-joined path as resources — Avo::Actions::City::Update resolves to avo.action_translations.city/update.

You can still set self.name, self.message, and the button labels as strings or lambdas. Those remain the fallback when no translation key is present, which is useful for dynamic labels that depend on arguments or record.

Localizing scopes, cards, and dashboards ​

Scopes, dashboard cards, and dashboards follow the same convention, from the add-on gems that provide them — their keys and attributes are in the key map.

yaml
# avo.es.yml
es:
  avo:
    scope_translations:
      admins:
        name: "Administradores"
        description: "Solo administradores"
    card_translations:
      users_metric:
        label: "Número de usuarios"
        description: "En todos los equipos"
      sales/monthly:
        label: "Ventas mensuales"
    dashboard_translations:
      dashy:
        name: "Panel de control"

Each accepts self.translation_key to override the derived path, and each falls back to the class attribute — which may still be a String or a lambda — when no translation is present. Namespaced classes use the same slash-joined path as resources and actions.

A card's per-registration label wins over its key

Unlike actions, a card can be given a label where it is registered:

ruby
card Avo::Cards::UsersMetric, label: "Active users"

That override is more specific than a per-class translation key, so it wins — otherwise a dashboard registering the same card class twice would collapse both to one string. Pass a lambda if such an override also needs translating.

Not to be confused with the gems' own chrome

avo.scope_translations.* and avo.card_translations.* are for your scopes and cards. The strings the gems render themselves — the refresh controls, the count pill — live under avo.scopes.* and avo.cards.*, documented at Scopes and Cards. The namespaces are deliberately separate so a card named refresh cannot overwrite the gem's own refresh label. What you can safely put under avo. covers which paths are yours to fill and which are already taken.

Localizing fields ​

Similarly, you can even localize fields. When you don't set an explicit translation_key: option on the field declaration, Avo resolves the field label through a cascade, using the first key that has a translation:

  1. avo.resource_translations.<resource>.fields.<field_id> — resource-scoped, so you can override a label for a single resource.
  2. avo.field_translations.<field_id> — shared across every resource that uses this field id.
  3. activerecord.attributes.<model>.<field_id>, then attributes.<field_id> — the model's own attribute translation, the one Rails uses for form labels and error messages. Parent classes are consulted too, so an STI subclass inherits its parent's translations.
  4. The humanized field id — the fallback when no key is translated.

Setting translation_key: explicitly bypasses the cascade and uses only that key. When that key is missing, the label is the humanized id, not the model's translation.

Most localized apps already translate their attributes, so their fields come out labeled with no Avo key at all:

yaml
# config/locales/en.yml
en:
  activerecord:
    attributes:
      location:
        street: Street name
ruby
field :street, as: :text # label is "Street name"

A plain string serves the singular and plural label alike; give the key one: and other: when the plural differs. Association fields look up the association name, so a belongs_to :user field reads user, not user_id.

To keep labels exactly as they were before the model's translations were consulted, turn the lookup off in the initializer:

ruby
# config/initializers/avo.rb
config.model_attribute_translations = false
ruby
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
  self.title = :name

  def fields
    field :id, as: :id
    # ... other fields
    field :files, as: :files, translation_key: 'avo.field_translations.file'
  end
end
yaml
# avo.es.yml
es:
  avo:
    dashboard: 'Dashboard'
    # ... other translation keys
    field_translations:
      file:
        zero: 'Archivos'
        one: 'Archivo'
        other: 'Archivos'

To override a field label for a single resource, add a resource-scoped entry. It wins over the shared field_translations key, while other resources keep using the shared one:

yaml
# avo.en.yml
en:
  avo:
    resource_translations:
      product:
        fields:
          title:
            one: "Product title"
            other: "Product titles"
    field_translations:
      title:
        one: "Title" # shared fallback for every other resource
        other: "Titles"

When no explicit help:, placeholder:, or include_blank: is set on the field, Avo also resolves those from sibling keys under the same translation_key:

yaml
# avo.es.yml
es:
  avo:
    field_translations:
      dates:
        one: "Rango de fechas"
        other: "Rangos de fechas"
        help: "Opcional. Período estándar: desde hace 1 semana hasta hoy."
        placeholder: "Elige las fechas"
        include_blank: "Ninguna"
ruby
# No help/placeholder/include_blank needed in Ruby — they come from the locale file
field :dates, as: :date_time

You can point translation_key at a resource-scoped path if you prefer to keep field copy next to the resource:

yaml
avo:
  resource_translations:
    import_guesty:
      fields:
        dates:
          help: Optional. Standard period: 1 week ago to present.
          placeholder: Choose dates
          include_blank: None
ruby
field :dates,
  as: :date_time,
  translation_key: "avo.resource_translations.import_guesty.fields.dates"

Explicit help:, placeholder:, and include_blank: options (strings or lambdas) still win over the locale file.

Localizing tabs and panels ​

Tab and panel titles localize through the resource's translation key too. Avo parameterizes the configured title: (downcased, non-alphanumerics turned into _) and looks it up under a tabs or panels scope:

  • avo.resource_translations.<resource>.tabs.<title> for a tab
  • avo.resource_translations.<resource>.panels.<title> for a panel

So tab title: "Activity" resolves to avo.resource_translations.user.tabs.activity, and panel title: "Contact information" resolves to avo.resource_translations.user.panels.contact_information:

ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
  def fields
    tabs do
      tab title: "Activity" do
        field :last_seen_at, as: :date_time
      end
    end

    panel title: "Contact information" do
      field :email, as: :text
    end
  end
end
yaml
# avo.es.yml
es:
  avo:
    resource_translations:
      user:
        tabs:
          activity: "Actividad"
        panels:
          contact_information: "Información de contacto"

The configured title: stays the fallback whenever the key has no translation. If the generated key doesn't suit you, pass an explicit translation_key: to the tab or panel and Avo uses it verbatim:

ruby
tab title: "Activity", translation_key: "avo.resource_translations.user.tabs.recent_activity" do
  # ...
end

Localizing the delete confirmation ​

Since avo >= v4.3.8

When a delete would also remove dependent records, the confirmation names them. Each association is named by the model's attribute translation, activerecord.attributes.<model>.<association>, the same key Rails uses for form labels. Give it one: and other: so the count reads right:

yaml
# config/locales/de.yml
de:
  activerecord:
    attributes:
      author:
        books:
          one: Buch
          other: Bücher

Deleting an author with 3 books then reads "Dadurch werden auch 3 Bücher gelöscht." Without that key, the humanized and pluralized association name is used ("3 books").

The sentence around the names comes from Avo's own locale files:

  • avo.destroy_also_deletes ("This will also delete %{records}.")
  • avo.checking_dependents ("Checking related records..."), shown while the counts load.

Localizing buttons label ​

The avo.save configuration applies to all save buttons. If you wish to customize the localization for a specific resource, such as Avo::Resources::Product, you can achieve this by:

yml
---
en:
  avo:
    resource_translations:
      product:
        save: "Save the product!"

Setting the locale ​

Setting the locale for Avo is pretty simple. Just use the config.locale = :en config attribute. Default is nil and will fall back to whatever you have configured in as config.i18n.default_locale in application.rb.

ruby
Avo.configure do |config|
  config.locale = :en # default is nil
end

That will change the locale only for Avo requests. The rest of your app will still use your locale set in application.rb.

You can also change Avo's default locale with the set_locale=pt-BR param. That will set the default locale for Avo until you restart your server.

Deprecated

set_locale logs a deprecation warning and will be removed in a future version. It changes Avo.configuration.locale for the whole process, so it switches the language for every user, not only the one who sent the request, until the server restarts. Since avo >= v4.3.1 Avo stores it only after your authenticate_with block lets the request through; earlier versions stored it before authenticating, so an unauthenticated request could switch everyone's language. To let each user pick their own language, use the locale selector. To change the language for everyone, set config.locale.

Suppose you wish to change the locale only for one request using the force_locale=pt-BR param. That will set the locale for that request and keep the force_locale param in all links while you navigate Avo. Remove that param when you want to go back to your configured default_locale.

Locale selector ​

Avo adds a language picker to the top navbar, next to the appearance switcher. It shows the current language code (EN, RO) and opens a list of the available languages, each written in its own language (Deutsch, Română, 日本語, العربية). Lists longer than eight languages get a search box. Picking one reloads the page in that language, and right-to-left languages flip the layout.

The choice is stored per user in a cookie named avo.locale, so it follows that browser across requests and never changes the language for anyone else.

The selector is enabled by default. Use config.locale_selector to choose which languages it lists, or to turn it off:

ruby
Avo.configure do |config|
  # Every locale in I18n.available_locales that Avo has translations for (default)
  config.locale_selector = true

  # Exactly these locales, in this order
  config.locale_selector = [:en, :ro, :de]

  # No selector
  config.locale_selector = false
end

With true, a locale shows up only when both conditions hold: it is in your app's I18n.available_locales, and Avo has an avo translation tree for it, either one Avo ships or one you add in config/locales. Locales that other gems register without Avo translations are left out. With an array, locales missing from I18n.available_locales are dropped. The selector hides itself when fewer than two locales remain.

Avo picks the locale for each request in this order:

  1. The force_locale param.
  2. The set_locale param (deprecated, and stored in Avo.configuration.locale only once the request is authenticated).
  3. The language picked in the locale selector. A cookie naming a locale the selector does not list is ignored, and so is the cookie when the selector is disabled.
  4. config.locale, then config.i18n.default_locale.

A force_locale or set_locale value missing from I18n.available_locales is ignored, and Avo moves on to the next source.

Related:

Right-to-left locales ​

Avo detects right-to-left languages from the active locale and flips its layout automatically — no configuration needed. The built-in RTL locales are ar, he, fa, ur, yi, ps, sd, ku, ckb, ug, and dv. Matching is done on the language segment, so a regional variant like ar-EG counts as RTL too.

Dates and the date picker ​

Dates on the Index and Show views and the month and weekday names in the date picker follow the active locale through the browser's Intl data, so there are no locale files to add for them. The picker's AM/PM labels, tooltips and aria labels stay in English.

A locale you pass through picker_options merges over the generated names instead of replacing them, so a filter that starts the week on Monday keeps the localized names:

ruby
def picker_options(value)
  super.merge(locale: { firstDayOfWeek: 1 })
end

Customize the locale ​

If there's anything in the locale files that you would like to change, run bin/rails generate avo:locales to generate the locale files.

These provide a guide for you for when you want to add more languages.

If you do translate Avo in a new language please consider contributing it to the main repo. Thank you

What you can safely put under avo. ​

Avo's locale files, every add-on gem's, and your own all deep-merge into a single avo tree per locale — and your config/locales loads last. Overriding one of Avo's strings is supported; it's most of what this page is about. What silently breaks an app is changing a key's shape.

  • Replace a string with a string. Set avo.save to whatever you like and every save button follows.
  • Never nest a key beneath a path Avo holds as a string. A String and a Hash can't merge, so the file that loads last replaces the other one outright. There's no error and no warning — the label simply stops rendering.

Plenty of Avo's strings read like namespaces. avo.actions, avo.resources, avo.dashboards, avo.dashboard, avo.filters, avo.pages and avo.tools are all labels Avo renders, and so are one-word keys like avo.all, avo.new, avo.edit, avo.delete, avo.copy, avo.confirm, avo.reset, avo.run, avo.view, avo.save and avo.close. Treat every one of them as taken.

Avo keeps eight namespaces of its own as well — avo.appearance, avo.checkbox_list, avo.global_search, avo.key_value_field, avo.media_library, avo.nested, avo.order and avo.search. They nest a level or two deeper, and the rule holds at every level: override a string, never nest beneath one.

Pluralization hashes accept extra keys ​

Some of the hashes Avo ships are pluralization hashes rather than namespaces. Four sit directly under avo. — avo.file, avo.record, avo.number_of_items and avo.x_items_more — and others are nested, like avo.checkbox_list.hidden_selections. Adding a sibling next to one: and other: in any of them is safe — pluralization reads only the plural keys (zero, one, two, few, many, other) and ignores everything else.

You can tell one apart by its keys, not its depth: a hash whose keys are plural categories is a pluralization hash wherever it appears.

That's why avo.resource_translations.user carries one:/other: and fields:, tabs:, panels: and save: at the same time. It is a plural leaf and a subtree root at once, by design.

The reverse needs a little care. Avo resolves resource and field names with I18n.t(key, count: 1, default: ...), and a subtree holding no plural key at all raises I18n::InvalidPluralizationData — passing a default: doesn't prevent it. Avo rescues that and falls back to the humanized name, so a resource key holding only save: works fine (see Localizing buttons label); you just don't get a translated resource name out of it. Keep one:/other: next to the nested keys when you want both. If your own code reads one of these keys with a count:, add the plural keys or rescue the exception.

Where your own keys go ​

Six roots exist for keys you write — the *_translations roots in the key map. Avo derives those paths itself and ships nothing under them, so nothing of Avo's can collide with what you put there.

avo.resource_translations.<resource> nests one level further — .fields.<field_id>, .tabs.<slug>, .panels.<slug> and .save — and it's the one place Avo expects a subtree of your own.

Filters have no derived root today. Localize a filter by calling I18n.t from its self.name block with a key you choose; a namespace of your own keeps it clear of everything above.

ruby
# app/avo/filters/name.rb
class Avo::Filters::Name < Avo::Filters::TextFilter
  self.name = -> { I18n.t("my_app.filters.name") }
  self.button_label = -> { I18n.t("my_app.filters.name_button") }

  def apply(request, query, value)
    query.where("LOWER(name) LIKE ?", "%#{value}%")
  end
end

See which keys are taken ​

I18n.t("avo") returns the merged tree — Avo's keys, every gem's, and your own overrides. List the ones that are strings and you have the set you must not nest under:

bash
bin/rails runner 'puts I18n.t("avo").select { |_, v| v.is_a?(String) }.keys.sort'

The locale generator shows you the same thing, but it isn't the tool for a look: it copies Avo's locale files into config/locales, where they load last and pin today's wording into your app, so later changes to Avo's own strings never reach you. Run it when you actually want editable copies.

Pagination strings are Pagy's own. Pagy ships a dictionary for each language and reads it through its own loader, so they sit outside the avo.* tree and none of this applies to them.

What a collision looks like ​

avo.dashboards is the sidebar's "Dashboards" heading — a string. Nesting a dashboard's name under it takes the heading away:

yaml
# config/locales/avo.en.yml
en:
  avo:
    dashboards: # Avo holds this as a string
      sales:
        name: "Sales"

Your file loads after Avo's, avo.dashboards becomes a Hash, and the heading is gone — with nothing in the logs to say so. Use the derived root instead:

yaml
# config/locales/avo.en.yml
en:
  avo:
    dashboard_translations:
      sales:
        name: "Sales"

That's exactly why avo-dashboards derives avo.dashboard_translations.* rather than reusing avo.dashboards.

Add-on gems ​

The locale generator covers Avo core. Add-on gems keep their own strings under their own namespace inside avo.*. Most of them ship English only — you supply the other locales in your app, the same way you would for any other key. A gem that ships more says so on its own Localization page: avo-api ships its tree in every locale core does, so the REST API's panel is translated with no work from you.

GemNamespaceReference
Advanced Searchavo.global_search.*
Collaborationavo.collaboration.*
Dashboards & Cardsavo.cards.*Cards · Dashboards
Dynamic Filtersavo.dynamic_filters.*Localization
Forms & Pagesavo.forms.*
Intelligenceavo.intelligence.*
REST APIavo.api.token.*Localization
Scopesavo.scopes.*Localization

The keys deep-merge into the same avo tree as core's, so one es.yml per gem — or a single file carrying all of them — works fine.

Advanced Search is the one gem that ships no locale file at all, and it doesn't need one: every key it renders lives under avo.global_search.* or is avo.all, and Avo core translates all of them in each of its bundled locales.

Four of those keys — avo.global_search.direct_match, avo.global_search.search_results, avo.global_search.searching_on and avo.all — reached core's locale files only after 4.1.6. On 4.1.6 and earlier they fall back to English, so define them in your own locale file until you're on a newer Avo.

Deep-merging can overwrite a core key

Because the merge is into core's own avo tree, nesting keys under a path where core already holds a string replaces that string with a Hash. The live example is avo.dashboards, which core uses for the sidebar's "Dashboards" heading — so avo.dashboards.my_dashboard.name breaks that heading. Add keys under a path core does not already use — see What you can safely put under avo. for the rule and the roots reserved for your own keys.

FAQ ​

If you try to localize your resources and fields and it doesn't seem to work, please be aware of the following.

The I18n.t method defaults to the name of that field/resource ​

Internally the localization works like so I18n.t(translation_key, count: 1, default: default) where the default is the computed field/resource name. So check the structure of your translation keys.

The default is the only value Avo humanizes. When your key resolves, the translation is used exactly as written.

yaml
# config/locales/avo.pt-BR.yml
pt-BR:
  avo:
    field_translations:
      file:
        zero: 'Arquivos'
        one: 'Arquivo'
        other: 'Arquivos'
    resource_translations:
      user:
        zero: 'Usuários'
        one: 'Usuário'
        other: 'Usuários'

Using a Route Scope for Localization ​

To implement a route scope for localization within Avo, refer to this guide. It provides step-by-step instructions on configuring your routes to include a locale scope, enabling seamless localization handling across your application.