Skip to content

Internationaliztion and Localization

Brut uses Ruby's i18n gem to provide support for localization and internationalization.

Overview

Brut::I18n::BaseMethods provides the core implementation of Brut's i18n support, and it largely wraps the t and l methods of the i18n gem.

Consider this:

ruby
t("my.key", foo: "Bar")

This will locate the string with the key my.key and return it, replacing %{foo} with "Bar", if %{foo} is present in the string.

The keys are located in files in app/config/i18n. The directories there correspond to the locales your app supports, e.g .app/config/i18n/en would hold translations for English.

The translation files themselves are 🎉NOT YAML🎊. They are Ruby files. By default, there are two files: app/config/i18n/en/1_defaults.rb and app/config/i18n/en/2_app.rb (noting that /en/ is for English and other langauges are obviously supported).

1_defaults.rb provides values for keys Brut may require or use, such as for front-end constraint violations. 2_app.rb provides your app's keys. If this file contains the same keys as 1_defaults.rb, your file's values will be used.

The file is a giant Hash, so the key above might look like so:

ruby
{
  my: {
    key: "Hello there %{foo}",
  },
}

Enhancements

Brut's t tries to balance predictability with flexibility. It will always try to tell you what keys it was checking when it cannot find a translation or what interpolated values were missing.

Basic Usage

Often, keys contain dynamic elements. Rather that creating a key like widget.status.#{widget.status}, you can pass an array in and it'll be joined for you:

ruby
t([ :widget, :status, widget.status ]) #=> widget.status.active e.g.

t can also take a block that will be evaluated and substituted into the block interpolation value:

ruby
{
  supportlink: "To contact support %{block}',
}

t(:supportlink) do
  "<a href='https://support.example.com'>Contact Support</a>"
end

See below for how this affects HTML generation.

Page- and Component-specific Values

If you call t inside page_template, or inside view_template of a page private component, you can simplify the key specification.

ruby
t(:nevermind)

If this is used on, say, NewWidgetPage, Brut will try to locate the key pages.NewWidgetPage.nevermind. This works on page private components as well.

Inside a normal component, it works simliarly. Suppose FlashComponent had this:

ruby
if !flash
  t(:default_message)
end

This would locate components.FlashComponent.default_message.

This saves some typing, but it can also assist refactoring. If you rename a page or component, calls to t will blow up, reminding you to move the translations inside 2_app.rb.

HTML Escaping

Brut::I18n::BaseMethods cannot be used directly. One of three submodules must be used: ForHTML, ForCLI, or ForBackend. This is to allow the ForHTML module to properly escape HTML.

When a translation accepts a block, that block could be HTML and you would want that HTML be included in the page, un-escaped. Brut achieves this by first using Phlex's capture method, then marking it as HTML safe using safe.

Thus, as long as you aren't introducing injections in your translations file or source code, it is safe to do the following:

ruby
def view_template
  div do
    raw(
      t(page: :contact_support) do
        a(href: "https://support.example.com") do
          t(page: :support_link_name)
        end
      end
    )
  end
end

The result of t is safe HTML, so you must use raw to avoid escaping it.

In a CLI or back-end context, HTML escaping is not relevant and can actually create problems, so ForCLI and ForBackend no-op safe and capture.

When using ForHTML, all interpolated values are HTML-escaped. ForCLI and ForBackend are not.

Localizing Dates and Times

l can be called and this defers to the Ruby I18n library.

Date and time formats can be configured in the translation files. l does not accept a full key for the format. It is created dynamically by the library, so you must take care in which one you use. If you pass a Date into l, date.formats.«format» is used. If you pass a Time in, time.formats.«format» is used.

The values of the formats are strings suitable for strftime. The site strif.me can be helpful in conjuring the right value.

Brut includes translations for various formats that you can inspect in app/config/i18n/«lang»/1_defaults.rb.

Displaying Dates and Times in HTML

While l will return a string you can use anywhere, you are most likely going to show dates and times in HTML. For that, you should use a <time> element. Brut provides Brut::FrontEnd::Components::TimeTag (remember that if you include Brut::FrontEnd::Components, it's a Phlex kit and thus you can use TimeTag(...) directly) to do this. It contains additional behavior to make friendly dates and times.

  • You can give it a timestamp: or date: to control which formatting style is used.
  • skip_year_if_same, if true, will omit the year from any format if the current year is the same as the year being displayed. This is true by default
  • skip_dow_if_not_this_week, if true, will omit the day of week if the date or time is more than 7 days in the past. This is true by default.

The way skip_year_if_same and skip_dow_if_not_this_week work is to append no_year and/or no_dow to existing format strings which are assumed to omit this elements.

If you wish to create your own formats, you can add them as well.

Constraint Violations and Field Names

The interpolated value {field} is special. It is assumed to be the name of a field in a constraint violation message, e.g. "%{field} is required". It is the only interpolated value that can be omitted without causing an error.

If included, it will work as normal:

ruby
t("cv.be.required", field: "Email") # => Email is required

If omitted, the value of "cv.this_field" is used. This is included in 1_default.rb, but if it's missing, Brut will raise. Assuming the value is "This field":

ruby
t("cv.be.required") # => This field is required

Testing

In tests, you can call t and l to examine values as needed. You may find the have_i18n_string matcher usefult to check generated HTML for I18n values (see Brut::SpecSupport::Matchers::HaveI18nString).

WARNING

Brut hardcodes English for tests, which you may not want. This will be addressed in the future.

None at this time, however Brut's I18n has not been battle-tested.

Technical Notes

IMPORTANT

Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's internals, the source code is always more correct.

Last Updated May 7, 2025

None at this time.