-
Notifications
You must be signed in to change notification settings - Fork 2.5k
I18n
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.
Whether you're in Ruby, ERB, JavaScript, or Handlebars, keep the following in mind:
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).
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
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!"))
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)
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.
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>
.
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.
The following guidelines apply to views and facebook messages (or anywhere we output 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.
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>
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.
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.
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' />"})
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.
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).
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
- Add the language to
config/locales/locales.yml
. If it's crowd-sourced make sure to add that to both the display name of the language, and thecrowdsourced: true
key. I usually just ask Google what to use for the display name (or check what Facebook uses). - Run the import process below with the new language code (e.g.
en-AU
or whatever) - Run
rake i18n:generate_js
to update_core_en.js
. - Ensure a file exists for the locale in
public/javascripts/vendor/timezone/
. These files come from https://github.com/bigeasy/timezone and one may already exist for the locale being added. If not, create the file. You can use theen_US.js
file as a template, then most of the strings in the file can be replaced with values from the YAML file for the locale.
You'll need a transifex username/password. You can get that from dana, or ask brianp for his.
$ rake 'i18n:transifeximport[<user>,<password>,<language code>,https://siteadmin.beta.instructure.com/locales/en.yml]'
If you are importing a language that transifex names incorrectly you can pass what it should be and what transifex has split by '>' and it will download the file correctly
$ rake 'i18n:transifeximport[<user>,<password>,zh_Hant>zh_HK,https://siteadmin.beta.instructure.com/locales/en.yml]'
The last parameter to the en.yml
on beta is used to compare the new import against, to verify translation keys and wrappers and stuff.
The import procedure will do a thorough vetting of the translation file and report problems. We can fix obvious mistakes such as quotes and misspelled placeholders, but all problems need to be sent back to Transifex. Otherwise we'll have to just continually re-fix the problem every time we import.
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.
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.
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.
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"
.
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'))
Are you looking for one of our commercial subscriptions, professional services, support, or our hosted solution? Check out canvaslms.com.