Skip to content

Internationalization (i18n) Standards

Bilka edited this page Sep 21, 2024 · 22 revisions

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.)

Table of Contents

Basics

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.

General guidelines

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

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.

HTML in translations

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>

Variable interpolation

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!

Hyperlinks

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>.
Note that in the example above, the variable is the key for the linked text (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.

URLs

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

Pluralization

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

Mailers

Additionally to the general guidelines, there are some guidelines for internationalization that are specific to mailers.

Styling work links and titles

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.

Styling collection links

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.

HTML and text versions of keys

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}.

Translating subjects

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"

Mailer previews

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 for UserMailer.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 of batch_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.
  • Make sure the I18n.with_locale call for the email is outside of the mailer class. Instead it should be where deliver/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 moving with_locale to the correct spot.

Models

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.

errors.add

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."

Validation errors

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.

Overriding a default error message

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}"

Error message format

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.

Overriding a default error message key

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}"

i18n-tasks

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

Manually marking keys as used

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

Renaming locale keys

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 the config/locales/ subfolders.
  • Update the view files to actually use the changed keys.
Clone this wiki locally