Skip to content
Jon Jensen edited this page Aug 19, 2013 · 10 revisions

For the most part we're doing standard Rails i18n stuff, though we are using some tweaks to make things easier. To extract a heading/sentence/paragraph/whatever, simply change it to t(:descriptive_key, "The actual sentence here"). Note that the English translation must always be passed in as the second argument (this differs from vanilla i18n).

General Guidelines

Scoping

Our translation keys are automagically scoped to the controller/view/model/message/whatever (inspired by the translator gem). So while the key should have a meaningful name, it only needs to be unique/meaningful within its context. For example, if the /things/index view has some instructions on how to create a thing, the key you use can just be "create_instructions", since the real key that the translators will see is actually something like "things.index.create_instructions".

The following table outlines how things are scoped:

Type Example i18n scope
View conferences/index.html.erb conferences.index
Controller ConferencesController conferences
Model web_conference.rb web_conference
Message assignment_changed.facebook.erb messages.assignment_changed.facebook
JavaScript / Coffeescript conferences.js conferences (not automatic, see below)

If you are in a scope 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.

Some gotchas

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 keys and English translations from the source code and generate the en.yml file. This means that you should only every pass a string/symbol as the first argument to a translate call, never a variable/expression/function/lambda. So stuff like t(:foo), never t(a ? :asdf : :qwerty). If you do it incorrectly, the Jenkins build will fail and the output will tell you the problem(s).

You should also take care not to reuse a key for a different translation (e.g. "#button.save" : "Save" and "#button.save" : "Save Progress"). The rake task will also fail the build in these scenarios.

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, " + (quadriplegic ? "blink your eyes" : (paraplegic ? "clap your hands" : "stomp your feet")) + "!"

should become something like this:

  • t :conferences_title, "%{course_name} conferences", :course_name => @course.name
  • t :conference_open_description, "This conference will be begin at %{date_and_time}, unless it doesn't.", :date_and_time => date_and_time
  • quadriplegic ? t(:happy_quadriplegic, "If you're happy and you know it, blink your eyes!") : (paraplegic ? t(:happy_paraplegic, "If you're happy and you know it, clap your hands!") : t(:happy_aplegic, "If you're happy and you know it, stomp your feet!"))

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(:minutes, "minute", :count => count), and it will just work.

If you have an entire 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(:how_long, {: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 (see "lead" example below) 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: These are the users: %{list_of_users} and pass in users.to_sentence. By default Array#to_sentence uses your activesupport translations for words_connector, two_words_connector, last_word_connector, though you can override these if needed (e.g. if you want "or" instead of "and", you could pass in the appropriate translation to Array#to_sentence)

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

Formatting

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(:duration_foo, :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 follow the simple markup guidelines below, since they have content that needs to be translated in the context of the entire sentence.

HTML wrapping

If you have a sentence where one portion is wrapped in a span or em 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(:key, "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 has 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.

Simple Markup

If you have a sentence that has a link, emphasis, or some other simple markup with translatable content, you should use markdown syntax. For example, suppose you have this sentence: You can <%= link_to("lead", :action => :lead_it) %> the discussion.. You might be tempted to do something like this: <%= t(:lead_instructions, "You can %{link} the discussion.", :link => link_to(t(:lead, "lead"), :action => :lead_it)) %>. This would result in two strings to translate. The sentence may be mistranslated since the verb is not present, and the translator also wouldn't know if the standalone "lead" should be translated as a verb or a noun. A better approach is to do: mt(:lead_instructions, "You can [lead](%{url}) the discussion", :url => url_for(:action => 'lead_it')). Translators will be given detailed guidelines on what should be translated and what constitutes markdown/placeholders.

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

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.

JavaScript / Coffeescript

We're using a modified i18n.js file from the i18n-js gem, 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, making sure to pass in the English text as the second argument.

Note that we've implemented the same scoping magic for i18n.js, though it does require you to specify the scope (since there's no way for a javascript to reliably know its source file name). Just do your define/require block like so (where the part after "i18n!" is a meaningful scope), with your translation calls inside:

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

  // Simple
  I18n.t('errors.title_required', "The name is required");

  // Substitution
  I18n.t('errors.title_too_long', 
    "Please use %{maxLength} characters or less 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 with a handlebars-y block helper.

Simple version:

{{#t "date"}}Date{{/t}}

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

{{#t "happy_prompt"}}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 "how_happy_pies"}}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' />"})

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 with optimized js, 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).

Importing From Transifex

From transifex download screen, we want the "download for use" option.

$ rake i18n:import
    - the task will ask for the translational filepath
    - and the commit hash of the version of canvas-lms to compare against

(i.e. the en.yml in that commit will be compared, so that the translation file will be compared with a file at about the same time as when we asked for the translations).

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.

If adding a brand new language, you'll also need to add the language to config/locales/locales.yml

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

Cherry Picking Transifex Updates

Check out the release branch, and then run:

for hash in `git log --format=%h --reverse origin/stable/2013-08-03..origin/master config/locales`; do git cherry-pick $hash; done

Eventually this will be automated by Transifex.

Clone this wiki locally