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:
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:
{
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:
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:
{
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.
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:
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:
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:
ordate:
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 defaultskip_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:
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"
:
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.
Recommended Practices
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.