In order to ensure that the Oppia website is understandable to learners around the world, we provide internationalization (i18n) support on Oppia for learner-facing pages. This enables learners to view the site in different languages using the language-selector dropdown in the navbar.
Our i18n support uses the angular-translate library, documentation for which can be found here. All platform string translations are provided through our partner, translatewiki.net.
This wiki page explains how to:
- Fill in missing platform translations
- Translate lessons and other dynamic text
- Create an i18n-compliant PR
- Fetch new translations from translatewiki
- Add a new translation language
All our translations are contributed through translatewiki.net. If you see missing translations in a language you're familiar with, please follow these steps to help fix the gap (do not create a PR):
- Visit the Oppia Translatewiki page. Read the notes on that page regarding plural rules and special markup syntax.
- Click "Translate this project".
- Select a language to contribute translations for. (Also, read the notes below describing the translation formats used for variables and plurals.)
Changes will then be pushed to Oppia automatically by the Translatewiki admins, and they will show up in future Oppia releases. We typically update new translations to the Oppia.org website on a monthly cadence.
The translatewiki admins have requested that translators do not rely on machine auto-translation, especially if they don't know the language and cannot fix its mistakes.
In cases where fixing a translation is absolutely necessary for technical reasons (e.g. if a translatewiki string has errors and it's breaking the tests), they recommend doing the following:
- Add the string
!!FUZZY!!
to the beginning of the machine-translated string. This will update the translation in Oppia's source tree, and also mark it as "needing update" for the translators on translatewiki. - Ask a translator for that language to fix the translation. You can find active translators by going to Special:ActiveLanguages and clicking on a language name.
Within translations, you could add variables that would be later replaced with personalized content. For example, in the text
You have 3 new notifications.
the number of notifications is a variable, and thus cannot be included directly in the translation. Angular translate solves this problem by using an interpolation service. Your translated phrase should look like:
You have <[notification_number]> new notifications.
In the html page, the value of notification_number
will be substituted accordingly by angular-translate. For more details, please refer to the angular-translate documentation. IMPORTANT: the default mechanism of indicating an expression in Angular is using the symbols {{ and }}, however in Oppia these symbols have been replaced by <[ and ]>.
In the example above, if there is only one notification, then we should change "notifications" for "notification". Furthermore, some languages may have more plural forms than English. To handle this, we use a different interpolation service, called messageformat. In this case, the translation should look like this:
{notification_number, plural, =0{You have no notifications.} one{You have one notification.} other{You have # notifications.}}
In this example, the # symbol will be replaced by the value of the notification_number
variable. For a more elaborate tutorial, please refer to the angular-translate guide for pluralization.
If you'd like to help translate Oppia's lessons, please get in touch via our volunteer form. Successful applicants will be invited to join one of Oppia's translation teams.
We really appreciate help with translations to make the lessons accessible for students whose first language isn't English. Thank you for helping out!
When developing learner-facing functionality, you must use I18N_...
strings as placeholders for text content. This enables such strings to be translated. You can see the translations in the i18n directory, which map translation keys like I18N_MODULE_STRING_NAME
to the translated strings. When a page is loaded, angular-translate traverses the page's html code and changes the translation key to the appropriate translated string.
Please consider i18n while developing. Not all languages are the same: words have different lengths, pluralization rules differ, sentences have different structures and the direction of writing can be from right to left. As a result, sometimes development practices that are generally good (like code reuse) turn out to be less than ideal for i18n. Here are some important points to take into account:
- Do not include raw strings, always use a translation key. Even if it is just an exclamation mark (!)
- When designing a page, plan for the case where strings are twice the length in other languages. Also, think about how the page would look like if the language is written from right to left.
- Try to include as much text as you can in a single key, so that the translator can provide a more coherent translation. Do not divide a paragraph into multiple keys unless you cannot avoid it. Don't split strings up and concatenate them, since different languages will use a different grammatical order.
- If you need to include html in your string, such as
<a></a>
tags, try and pass the code as an argument to the translation service. - Note that other languages may have more plural forms or genres than English. So, for example, if you include a sentence that is always going to be plural in English, add pluralization support regardless in your translation (see [Note 2 on pluralization](#note-2-pluralization] above).
To add a new translation key:
-
Choose a translation key name. These key names are always written in uppercase, with the following parts:
- Prefix: the key MUST start with
I18N_
(otherwise some tests will break). - Module name: such as
SIGN_UP_
. Be consistent with existing names, and keep the keys grouped by the module name. - String name: a meaningful name representing the function of the string in the page, like
PAGE_TITLE
.
Note that long translation keys are fine -- the key objective is that the role of the string is well described.
- Prefix: the key MUST start with
-
Add the new translation key to both the
assets/i18n/en.json
andassets/i18n/qqq.json
files. Inqqq.json
you should provide translators with a descriptive context for the string. Please see this page for more information on what goes into these descriptions.
You do not need to modify the other JSON files. Translatewiki will do that after you merge the PR.
When updating the English text for a translation key, first determine whether the new English text has a significantly different meaning to the previous English text.
- If the new text conveys a different meaning from the previous text: do not reuse the translation key. Create a new translation key instead, and delete the previous one.
- If the new text is similar to the previous text, and previous translations are probably still valid: just update the English value directly in
assets/i18n/en.json
. Translatewiki will pick up the updates and push new translations in due course.
If a translation key is not used any more, you must delete it from assets/i18n/en.json
and all other translation JSON files. This is to preserve the property that the keys in other JSON files are a subset of those in en.json
.
To verify your changes to translation files locally, run the following command in a terminal:
Python:
python -m scripts.run_backend_tests --test_targets=core.controllers.base_test.I18nDictsTests
Docker:
make run_tests.backend PYTHON_ARGS="--test_targets=core.controllers.base_test.I18nDictsTests"
This validates the translation JSON files by verifying that the keys are correctly sorted, that the keys in en.json and qqq.json match, that every other translation JSON file has a subset of the keys in en.json, and so on.
Sometimes, the page is rendered in the browser before the locale file with the translations is loaded. As a result, the user can see the translation keys briefly before they're replaced with the corresponding translations, which is not a good user experience. This problem is known as the "Flash of Untranslated Content", or FoUC.
When adding a new string to Oppia's HTML code, please take into account the following tips to prevent FoUC:
- The FoUC behaves differently in the preferred language (English) and all the other languages. Please manually check that there is no FoUC in both cases.
- Add the translation key inside the html tag as the value of the translate attribute. This will prevent the key from being shown briefly -- instead, the location of the string will remain empty in the interim.
- If the string is in a very visible location and there is FoUC in the preferred language, add the key and the translation into the
DEFAULT_TRANSLATIONS
constant defined in the file i18n.js.
Remember to translate placeholders and tooltips! For tooltips, use the translate filter on the tooltip attribute value. For example:
tooltip="<['I18N_GALLERY_VIEWS_TOOLTIP' | translate]>"
For placeholders, use the attribute ng-attr-placeholder
instead of placeholder
. As a value for this attribute, apply a translate filter to the key. For example:
ng-attr-placeholder="<['I18N_FORMS_TYPE_NUMBER' | translate]>"
Angular translate supports pluralization and representation of different genders using the messageformat library. For example, if you need to translate the html code
<span ng-if="choices > 1">Select <[choices]> choices.</span>
<span ng-if="choices == 1">Select one choice.</span>
<span ng-if="choices == 0">Select no choice.</span>
you must replace this code with:
<span translate=”TRANSLATION_KEY” translate-values=”{choicesValue:<[choices]>}” translate-interpolation="messageformat"></span>
and add the translation into the en.json file with the following format:
“TRANSLATION_KEY”: “{choicesValue, plural, =0{Select no choice.} one{Select one choice.} other{Select # choices.}}”
For a more complete tutorial, refer to the angular translate guide and the messageformat documentation.
In e2e tests, to check that a page has no untranslated keys: call the helper function ensurePageHasNoTranslationIds
, which is located in webdriverio_utils/general.js.
Also, Karma tests may generate 404 warnings, as the required locale files aren't available in the Karma test environment. To overcome this, add the following line in the first part of your Karma test:
beforeEach(module('oppia', GLOBALS.TRANSLATOR_PROVIDER_FOR_TESTS));
Note that we generally do this on a monthly basis. Here are the steps that we follow:
- Checkout the
translatewiki-prs
branch. - Merge
develop
intotranslatewiki-prs
and resolve all conflicts (usually by accepting the changes fromtranslatewiki-prs
). - Run
python -m scripts.run_backend_tests --test_target=core.controllers.base_test
on that branch. This will validate the I18n files. If any errors arise, they need to be fixed. - Create a PR (similar to this one) that brings the new translations from translatewiki into develop.
The reason we cannot fully automate this yet is because of step 3. Sometimes, the crowdsourced translations on translatewiki incorrectly handle syntax (e.g. HTML tags in the original strings do not show up in the translated text) and this is an issue that needs to be fixed manually.
When a language has reached 90+% completion on translatewiki, we can add it to the website.
To do this: in feconf.py, add a new entry to the variable SUPPORTED_SITE_LANGUAGES
representing the language code and the language name.
After this, you should be able to see the new language listed in the Oppia splash page and translate the site using the language dropdown in the footer.