-
Notifications
You must be signed in to change notification settings - Fork 510
Internationalization (i18n) Standards
The internationalization of our code is a work in progress, as is the development of our I18n standards. The code may not reflect what is currently in the standard. We do our best to keep this page up-to-date with our current standards, but if you're unsure what method to use, please ask! Similarly, please get in touch if you have suggestions. You can comment on a relevant pull request or use the mailing list address on the bottom of this wiki page.
While it is in no way expected, we are very grateful when contributors internationalize existing code their pull requests touch. (However, if your pull request is particularly large, it may be best to refrain from doing so and making it bigger.)
Usually, internationalized text is generated by calling the t()
method with a locale key. The English text has to be placed in the corresponding locale file for that key, for example config/locales/views/en.yml. The t()
method is provided by the Rails I18n API.
Translations into languages other than English are done separately by the Translation team and should not be added or modified by coders. Instead, there are specific pull requests that update the locale files in this repository with the translations from the Translation team's tool, such as #4517.
In Ruby and view files, use double quotes and parentheses for the t(".key")
call. In view files, code should be formatted like <%= t(".key") %>
.
In the locale (.yml
) files, use quotation marks only when required. Double quotes are required when the translation starts with a variable interpolation "%{foo} bar"
, single quotes are required when the translation contains a colon 'baz:'
. i18n-tasks can standardize this formatting, see its section for the normalize command.
Use lazy lookup. For example, use t(".key")
instead of t("full.path.to.key")
.
Do not use the default
option when calling the t()
method, for example t(".key", default: "foo")
. This English text in the default
option cannot be translated because it's not in a locale file.
Some locations in the code use the legacy ts
helper, which looks like a translation helper but is not one. Instead, use the Rails built-in t()
helper as documented here.
Avoid reusing the exact same locale across different contexts. Translators may want to translate text fragments differently depending on context, even when the text fragments are the same in English. That is only possible when different locale keys are used for each occurrence. There are exceptions to this, for example the mailer greetings are designed to be reusable without issues.
Locale key names should be descriptive, avoid numbered keys like part1
or para1
. Furthermore, Translation finds it helpful for variable names in keys to closely correspond with the key for the text the variable represents.
Update the key name when modifying text. If the key is not modified, changes to the text may not be brought to Translation's attention, or old versions of the text may be included in the pull requests our translation tool exports, reverting desired changes.
Keep all HTML in the view files. Newlines and any HTML like paragraph tags (<p>
) or links and urls should be in not the locale files. This will reduce the risk of encountering issues with an email's markup when the locale files are automatically updated.
Very rarely it is not possible to pull the HTML out of the translation string. In that case, the locale key needs to be marked as HTML safe by using the html
suffix. Do not use the html_safe
method for this.
<!-- View file like index.html.erb -->
<p><strong><%= t(".completely_highlighted_sentence") %></strong></p>
<p><%= t(".long_sentence_html") %></p>
# Locale file like config/locales/views/en.yml
completely_highlighted_sentence: A sentence, all of it is highlighted.
long_sentence_html: Long sentence with a <strong>highlighted</strong> word in the middle.
<!-- Result -->
<p><strong>A sentence, all of it is highlighted.</strong></p>
<p>Long sentence with a <strong>highlighted</strong> word in the middle.</p>
Use variable interpolation. If you have a sentence with a variable in it, use interpolation rather than multiple strings or concatenation in the view file.
<!-- View file like index.html.erb -->
<%= t(".greeting", name: @user.login) %>
# Locale file like config/locales/views/en.yml
greeting: Hi, %{name}!
<!-- Result -->
Hi, Username!
If the variable represents a hyperlink, its name should end with _link
. This helps our translators identify links and the linked text.
<!-- View file like index.html.erb -->
<%= t(".questions_html", contact_support_link: link_to(t(".contact_support"), new_feedback_report_url)) %>
# Locale file like config/locales/views/en.yml
questions_html: If you have questions, please %{contact_support_link}.
contact_support: contact Support
<!-- Result -->
If you have questions, please <a href="https://archiveofourown.org/support">contact Support</a>.
contact_support
) plus the suffix _link
.
Note that because the translated text includes HTML (for the link), you need to mark the key as HTML safe by using the html
suffix.
If the variable represents a URL, its name should end with _url
. Particularly with mailers, this ensures links and URLs have distinct keys.
<!-- View file like index.html.erb -->
<%= t(".questions", support_url: new_feedback_report_url) %>
# Locale file like config/locales/views/en.yml
questions: If you have questions, please contact Support: %{support_url}.
<!-- Result -->
If you have questions, please contact Support: https://archiveofourown.org/support
If the variable represents a number, its name should be count
. This enables automatic flexible pluralization.
<!-- View file like index.html.erb -->
<%= t(".word_count", count: creation.word_count) %>
# Locale file like config/locales/views/en.yml
word_count:
one: "%{count} word"
other: "%{count} words"
<!-- Result -->
46 words
Additionally to the general guidelines, there are some guidelines for internationalization that are specific to mailers.
In HTML emails, use style_creation_link(@work.title, work_url(@work))
to link to works. In text emails, format references to works as "Work Title" (https://archiveofourown.org/works/000)
.
If the work is no longer on the site, like in deleted work emails, use style_creation_title(@work.title)
in HTML emails and "Work Title"
in text emails.
In HTML emails, use style_link(@collection.title, collection_url(@collection))
to link to collections. In text emails, format references to collections as "Collection Title" (https://archiveofourown.org/collections/collection_name)
.
If a word like "collection" or "challenge" follows the collection title, exclude the word "collection" or "challenge" from the hyperlink in the HTML email, but format it as "Collection Title" challenge (https://archiveofourown.org/collections/collection_name)
in the text email.
In mailers, separate html and text email version of the mailer keys by adding .text
/.html
at the end.
<!-- app/views/user_mailer/claim_notification.html.erb -->
<p><%= t(".questions.html", contact_support_link: support_link(t(".questions.contact_support"))) %></p>
<!-- app/views/user_mailer/claim_notification.text.erb -->
<%= t(".questions.text", support_url: new_feedback_report_url) %>
# config/locales/mailers/en.yml
user_mailer:
claim_notification:
questions:
contact_support: contact AO3 Support
html: For other inquiries, please %{contact_support_link}.
text: For other inquiries, please contact AO3 Support at %{support_url}.
The subjects of mailers should be translated using the default_i18n_subject
method, because it can be used to provide interpolation variables.
# app/mailers/user_mailer.rb
def admin_hidden_work_notification(creation_id, user_id)
# ...
I18n.with_locale(@user.preference.locale.iso) do
mail(
to: @user.email,
subject: default_i18n_subject(app_name: ArchiveConfig.APP_SHORT_NAME)
)
end
# config/locales/mailers/en.yml
user_mailer:
admin_hidden_work_notification:
subject: "[%{app_name}] Your work has been hidden by the Policy & Abuse team"
When rewriting an email to use Rails I18n, it is helpful to add it to the mailer previews to make visual inspection easier. We do not require this for I18n PRs, but it is strongly recommended.
When adding a mailer preview, there are the following guidelines:
- Give the preview method the same name as the mailer method, e.g. create
UserMailerPreview.change_email
forUserMailer.change_email
. - If the email has variants:
- If there are finite variants, e.g. different subscription types like Work and Chapter for the subscription email, create separate methods for each variant and tack the variant onto the end of the preview method name, e.g.
batch_subscription_notification_work
for the Work variant ofbatch_subscription_notification
. - If there are infinite variants, e.g. amount of guest kudos in a kudos email, use URL parameters instead.
- Because previews are only accessible in local dev and in the staging environment and will mostly be used by scripts, input validation is not a major concern.
- However, the URL parameters are strings. So when using URL parameters as numbers (e.g. for pluralization), make sure to convert them to a number (e.g. with
to_i
) before passing them to the mailer.
- If there are finite variants, e.g. different subscription types like Work and Chapter for the subscription email, create separate methods for each variant and tack the variant onto the end of the preview method name, e.g.
- Make sure the
I18n.with_locale
call for the email is outside of the mailer class. Instead it should be wheredeliver
/deliver_later
is called on the mailer. Otherwise the locale dropdown in the preview will not have any effect. See #4875 for an example of movingwith_locale
to the correct spot.
See for the Rails I18n guide regarding translating model names and model attributes, e.g. using human_attribute_name
.
Errors generated in models should also be internationalized. Rails provides translated default error messages for validations. These errors can be customized by providing translation keys or overriding translations, without calling the t()
method directly. Usually, these errors are scoped to activemodel.errors.models
in the locale file, see error message scopes.
When creating an error with errors.add, the translation key should be provided as the second argument. Variable interpolations can be provided as keyword arguments.
# app/models/work.rb
validate :new_recipients_allow_gifts
def new_recipients_allow_gifts
# ...
errors.add(:base, :blocked_gifts, byline: gift.pseud.byline)
end
# config/locales/models/en.yml
activerecord:
errors:
models:
work:
blocked_gifts: "%{byline} does not accept gifts."
When an error is directly generated by an active record validation like :validates
, there are a few options for internationalizing the error message.
The simplest is to use the default validation error generated by Rails, as it will also have default translations. The default validation error will automatically be used unless it is overridden with one of the below methods.
The default error message can be overridden by creating a translation for the right key in the locale file. The keys for the error messages and variables provided for interpolation are described in the I18n guide on error message interpolation.
# app/models/block.rb
validates :blocked_id, uniqueness: { scope: :blocker_id }
# config/locales/models/en.yml
activerecord:
errors:
models:
block:
attributes:
blocked_id:
taken: You have already blocked that user.
format: "%{message}"
By default, error messages for model attributes will be prefixed with the name of the attribute that the validation is on. For example, when validating the attribute title
, the error message in the locale file could be cannot contain underscores.
, which would result in the error Title cannot contain underscores.
.
For that reason, the above example overrides the format of the message to be %{message}
. This means it will be exactly the error message set in the locale file, no prefix.
Another way to disable the automatic prefix of the error message for an attribute validation is to start the error message itself with a caret ^
. For example, if the error message was ^You have already left kudos here. :)
then the format
would not need to be set in the locale file.
When using errors.add
to create an error related to the whole object, :base
should be used as the attribute: errors.add(:base, :custom_key)
. This will also disable the automatic prefix of the error message.
Sometimes, a completely custom error message with a custom locale key is wanted. In that case, a completely separate locale key can be used by setting it in the :message
option.
# Model file like app/models/user.rb
validates :name, presence: { message: :question }
# View file like config/locales/models/en.yml
activerecord:
errors:
models:
user:
attributes:
name:
question: What is your name?
format: "%{message}"
We use the i18n-tasks gem to help us manage translations.
Before submitting a pull request with i18n changes, it's a good idea to run the specs that check for missing keys and unused translations and ensure the English locale file is normalized:
RAILS_ENV=test bundle exec rspec spec/lib/i18n/i18n_tasks_spec.rb
You can also run the associated tasks before running the tests:
bundle exec i18n-tasks missing -l en -t used,plural # Check en.yml for missing keys bundle exec i18n-tasks unused -l en # Check en.yml for unused keys bundle exec i18n-tasks normalize -l en # Normalize formatting of en.yml
Rarely, the i18n-tasks gem cannot parse a call to the t()
method and incorrectly reports the locale key as unused. Usually this happens when the locale key is dynamically created. In that case, a comment with i18n-tasks-use
will mark the locale key as used.
# app/controllers/comments_controller.rb
before_action :check_permission_to_modify_hidden_status, only: [:hide, :unhide]
def check_permission_to_modify_hidden_status
# ...
# i18n-tasks-use t('comments.hide.permission_denied')
# i18n-tasks-use t('comments.unhide.permission_denied')
flash[:error] = t("comments.#{action_name}.permission_denied")
# ...
end
The i18n-tasks gem can be used to easily rename (move) locale keys. To rename locale keys, perform the following steps:
- Uncomment the data write config in
config/i18n-tasks.yml
line 25. - Move the locale keys with
bundle exec i18n-tasks mv FROM_KEY_PATTERN TO_KEY_PATTERN
. - Turn line 25 in
config/i18n-tasks.yml
into a comment again. - Move the locale from
config/locales/phrase-exports/en.yml
into to correct en.yml files in theconfig/locales/
subfolders. - Update the view files to actually use the changed keys.
If you have any questions regarding code development, please don't hesitate to send an email to [email protected] and we will try to get back to you as soon as possible!
- Home
- Set Up Instructions
- Docker (All platforms)
- Gitpod (Cloud-based development)
- Linux
- OS X
- Creating Development Data
- Writing and Tracking Code
- Automated Testing
- Architecture
-
Getting Started Guide
- Getting Set Up
- Your First Pull Request
- More About Git
- Jira