Skip to content
Bangbay Siboliban edited this page May 14, 2020 · 10 revisions

We're using I18nliner with a few add-ons of our own to make I18n as painless as possible. TL;DR: to internationalize "Hello World", just change it to t("Hello World"); you don't need to go edit any .yml files.

General Guidelines

Whether you're in Ruby, ERB, JavaScript, or Handlebars, keep the following in mind:

Always Use Literals

Because we are not manually managing the en.yml translation file, we need to take care in how we do our translate calls. A rake task, i18n:generate, extracts the English translations from the source code and generate the en.yml file. This means that you should only ever pass a literal as the default translation (and key, if provided), never a variable/expression/function/lambda. So stuff like t("Hello"), never t(coming ? "Hello" : "Goodbye"). If you do it incorrectly, the Jenkins build will fail and the output will tell you the problem(s).

Translate at Runtime

Always call I18n.t right before you need the string; don't cache the result so you can reuse it later (especially in a constant/singleton).

For example, instead of this:

class Foo
  # oh noes this runs when the file is first loaded, so everybody will get
  # English /o\
  SOME_STRING = I18n.t("Hello World")

  def gimme_a_string
    SOME_STRING
  end
end

do this:

class Foo
  def gimme_a_string
    I18n.t("Hello World")
  end
end

Interpolate, Don't Concatenate

Phrasing varies widely in different languages, so we do not want to force any particular word order. Additionally, translators have a much easier task when they are given entire sentences to translate (with placeholders), rather than individual phrases or words with less context.

So stuff like this:

  • @course.name + " conferences"
  • "This conference will be begin at " + datetime_string(date_and_time) + ", unless it doesn't."
  • "If you're happy and you know it, " + (first_verse ? "clap your hands" : (second_verse ? "stomp your feet" : "twirl your mustache")) + "!"

should become something like this:

  • t "%{course_name} conferences", :course_name => @course.name
  • t "This conference will be begin at %{date_and_time}, unless it doesn't.", :date_and_time => date_and_time
  • first_verse ? t("If you're happy and you know it, clap your hands!") : (second_verse ? t("If you're happy and you know it, stomp your feet!") : t("If you're happy and you know it, twirl your mustache!"))

Pluralization

Some languages don't have plural forms of nouns, some differentiate between singular/plural, and some differentiate between one/two/three+. Rather than use pluralize, you should use the magical count fu. You would change pluralize(count, "minute") to t("minute", :count => count), and it will just work.

If you have a phrase or sentence, and the possibly-pluralized word makes up part of it, you should pass in a hash for the English translations with appropriate :one/:other (and possibly :zero) values, e.g. t({:one => "It was offline for 1 minute", :other => "It was offline for %{count} minutes"}, :count => count)

Contexts

Sometimes we have context names interpolated into sentences, e.g. You can create a widget for this #{@context.class.to_s.downcase}. This has some issues: 1. we need to translate the class name, 2. single words are problematic to translate (e.g. suppose the class name is "Lead" ... is that an noun or verb?) and 3. it may not be safe to interpolate it in all languages anyway. e.g. Person and Group are feminine and masculine respectively in romance languages, which presents a problem for the preceding "this". It's safer to have a different translation of the sentence for each context.

Lists

If you have an inline list like this: These are the users: #{users.join(', ')}, you can just set up your translation like so: t "These are the users: %{list_of_users}", :list_of_users => users.to_sentence. By default Array#to_sentence uses your activesupport translations for words_connector, two_words_connector, last_word_connector. There's an :or boolean option you can pass if you want the last connector to be "or" instead of "and".

Another option if you are in a view is just to rework it as a <ul>.

Dates/Times/Numbers

Our custom date/time formatters (e.g. date_string) have been rewritten to use localize (l), so you can use them as you do today. Anywhere you have an unformatted date/time (or manually formatted, e.g. strftime), you should change it to use those helpers (or do a vanilla localize call).

The rails helpers number_to_currency, number_with_precision, number_to_percentage, number_with_delimiter, and number_to_human_size all use the format settings located in the number scope, so use them.

ERB

The following guidelines apply to views and facebook messages (or anywhere we output HTML):

Interpolating HTML

Sometimes you have inputs or other markup in the middle of a phrase/sentence, e.g. Make this available for <%= f.text_field(:duration) %> minutes. You can interpolate it normally, e.g. t("Make this available for %{text_field} minutes", :text_field => f.text_field(:duration)). By default this would get double-escaped in views, but we have some html_safe fu that ensures this doesn't happen. Note that for inline links, you should use a wrapper or block (see below), since they have content that needs to be translated in the context of the entire sentence.

Blocks

I18nliner adds an ERB pre-processor that will create wrappers and placeholders for you if you use the block syntax. So you can just do stuff like this:

<p>
  <%= t do %>
    <b>Ohai <%= user.name %>,</b>
    you can <%= link_to "lead", new_discussion_path %> a new discussion or
    <%= link_to "join", discussion_search_path %> an existing one.
  <% end %>
</p>

Wrappers

You can also use explicit wrappers (e.g. if you want more control or are not in ERB land). If you have a sentence where one portion is wrapped in a span or link_to or something, you can use the :wrapper key to wrap it in html. For instance: Submissions for <span class='assignment_name'><%= assignment.name %></span>, can be extracted as: <%= t("Submissions for *%{assignment_name}*", :assignment_name => assignment.name, :wrapper => '<span class="assignment_name">\1</span>') %>. If you have multiple things to wrap, use a different symbol for each and pass a hash for :wrapper, e.g. :wrapper => { '*' => '...', '#' => '...' }

It's assumed you're going for HTML output when you use :wrapper, so the translation will be html escaped if it's not already and the :wrapper text is marked as html_safe implicitly.

JavaScript / Coffeescript

i18nliner-js enhances i18n-js, so it works more or less as it does in ruby. To translate a string or format a date/number, just use the I18n.t/I18n.l methods as you would in Ruby.

Note that to facilitate bundling client-side translations, you need to use the i18n require-js plugin, and should give it a meaningful scope, e.g.:

define([
  'i18n!collaborations',
  ...
]), function(I18n, ...) {
  ...

  // Simple
  I18n.t("The name is required");

  // Interpolation
  I18n.t("Please use %{maxLength} characters or fewer for the name.",
    {maxLength: max_allowed_length});
}

As with our Ruby translate calls, the JS ones should only ever use literal keys as the first argument, never variables/expressions/etc.

Handlebars

I18n in Handlebars works similarly to the Ruby/JS variants, but both an inline and block helper.

Inline:

{{t "Date"}}

Block Style:

{{#t}}Date{{/t}}

With links or buttons, the translation generation will automatically extract wrappers so that you don't have to:

{{#t}}Click <a href='#'>here</a> if you are happy!{{/t}}

The translators would just see something like Click *here* if you are happy!

With more complicated substitutions like inputs.

{{#t}}I want to order {{{how_many}}} pies for my party.{{/t}}

Then in the Backbone view (or template caller), the "how_many" can be passed as the HTML for the input.

myTemplate({how_many: "<input name='pies' value='1' />"})

Validating I18n

Local Localization

Localization does not occur in development or test Rails environments by default (to keep canvas snappy). To load other locales, you need to run canvas with: RAILS_LOAD_ALL_LOCALES=true, otherwise you will always see English no matter the locale you select.

Additionally, JS/Coffee/Handlebars localization will not happen unless you either run canvas with optimized js, i.e. USE_OPTIMIZED_JS=true or you pass include_js_translations=1 in the query string. If you do the latter, you also need to run $ rake i18n:generate_js beforehand to create the necessary JS localization files.

Lolcalization

If you just want to see which strings have been extracted and which have not, run canvas with LOLCALIZE=true (no, that's not a typo). Anything passing through ruby or js I18n.t will be lol-calized (schizo-case, spurious lols, and exclamation marks).

Checking Your Changes

If you want to validate stuff yourself rather than waiting on Jenkins, just do:

$ rake i18n:check

or

$ rake i18n:check ONLY=path/to/stuff

Deprecated Features

Prior to I18nliner, we had a pile of hacks to make I18n easier. We've kept a compatibility layer, so that all of these things still work, and you'll see them in the codebase for some time. But you shouldn't add new code with this, as eventually these things will all go away.

Explicit Keys

While I18nliner allows explicit keys, they aren't needed. e.g. instead of t :heading, "Hello there" do t "Hello there".

Note that keys need to be unique; if you use the same key for a different translation, the rake task will fail.

Scoping

If you do provide a key with your translate call, the key will be automagically scoped to the controller/view/model/message/whatever.

If you are in a scoped file and want to reuse a translation from somewhere else (e.g. you're in shared/conference and you want something in conferences/show), you can specify the full key, prefixed with a '#', e.g. t '#conferences.show.status.new', ....

Note that things like lib modules do not have any automatic scoping, so you'll be doing raw I18n.t calls with the full key (# is not required). In those cases you should name your keys sensibly (e.g. 'lib.text_helper.quoted_text_toggle').

Additionally, base STI models (e.g. Enrollment) need to use absolute keys.

Plugins (vendored and external) should follow the outlines above (e.g. if a plugin (re)defines some /users/index translations, they should be scoped under 'users.index'). Plugin translations are loaded after the main canvas-lms ones, allowing us to overwrite strings.

Labels

We often have labels of the style f.label :name, "Name:". Colons are not used in all languages, but leaving it in the translated string might lead to inconsistencies (e.g. a translator might not be religious in preserving them in translations). Instead use the blabel (before-label) helper and omit the colon. This will auto-interpolate the passed-in text into the before_label_wrapper translation (which will have a colon, depending on the language).

If you pass in a symbol as the second argument, label/blabel will do the t() call for you (scoping it under <current_scope>.labels). As is the case with translate calls, you do need to pass in the English text. So as not to mess up the method signature, this should be passed as the :en option in the options hash, e.g. f.blabel :title, :name, :en => "Name". If the method/field_name is the same as the localization key, the key can be omitted. So you could just do: f.blabel :name, :en => "Name".

Markdown Translations

The mt helper allows you to have markdown in your translations, that will be converted to HTML at runtime. e.g. mt("You can [lead](%{url}) the discussion", :url => url_for(:action => 'lead_it'))