(Minor reorg... I clearly did not read ahead to the next section ;)) |
|||
| (23 intermediate revisions not shown) | |||
| Line 1: | Line 1: | ||
| - | + | =Internationalization= | |
| - | + | This is a developer's guide to internationalization (i18n) within the MeeGo UX project. | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ==Localizable Strings== | |
| - | + | The most basic step in internationalization is to identify any user-visible strings in your application and replace them with code that looks up the appropriate string for the current locale. The way this is accomplished in Qt C++ code is to simply wrap the string with <code>tr()</code>: | |
| - | + | <pre>QString label = tr("Ring tone:");</pre> | |
| - | + | This is accomplished in Qt Quick (QML) code by wrapping the string with qsTr(): | |
| - | + | ||
| - | + | ||
| - | + | <pre>Text { | |
| - | + | id: dialogTitle | |
| + | text: qsTr("Find contacts") | ||
| + | ...</pre> | ||
| - | + | There is a Qt command-line tool ''lupdate'' that scans source code for these <code>tr()</code> and <code>qsTr()</code> macros and pulls out the identified strings. It writes them into a ".ts file," an XML-format file that lists information about where the strings were found. In UX projects, we generate these TS files automatically when creating the dist tarball that we push to OBS. | |
| - | + | Your top-level .pro file should have lines like these: | |
| - | + | ||
| - | + | <pre>TRANSLATIONS += .qml lib/.h lib/*.cpp | |
| - | + | PROJECT_NAME = meego-app-example | |
| - | + | dist.commands += rm -fR $${PROJECT_NAME}-$${VERSION} && | |
| - | + | dist.commands += git clone . $${PROJECT_NAME}-$${VERSION} && | |
| - | + | dist.commands += rm -fR $${PROJECT_NAME}-$${VERSION}/.git && | |
| - | + | dist.commands += rm -f $${PROJECT_NAME}-$${VERSION}/.gitignore && | |
| - | + | dist.commands += mkdir -p $${PROJECT_NAME}-$${VERSION}/ts && | |
| - | + | dist.commands += lupdate $${TRANSLATIONS} -ts $${PROJECT_NAME}-$${VERSION}/ts/$${PROJECT_NAME}.ts && | |
| - | + | dist.commands += tar jcpvf $${PROJECT_NAME}-$${VERSION}.tar.bz2 $${PROJECT_NAME}-$${VERSION} && | |
| - | + | dist.commands += rm -fR $${PROJECT_NAME}-$${VERSION} && | |
| - | = | + | dist.commands += echo; echo Created $${PROJECT_NAME}-$${VERSION}.tar.bz2</pre> |
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | Make sure that <code>TRANSLATIONS</code> is set to include all files with translatable strings (you may need to include other subdirectories and .js files, for instance). Make sure that <code>PROJECT_NAME</code> is set to your application name. | |
| - | + | Whenever you check in an update to OBS, you should create a clean tarball from git source. To create this tarball, run the following commands from the toplevel directory in your git tree: | |
| - | + | <pre>$ qmake | |
| + | $ make dist | ||
| + | </pre> | ||
| - | + | Then copy the generated tarball up to OBS. Bump the version up once a week. You should never submit an "sr" for the same version twice. But you can push to OBS multiple times throughout the week without doing an "sr." | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | Later these TS files are translated into various languages, and each one is compiled into a .qm binary file that allows efficient string lookup at runtime. Elsewhere you need to load a translator for each .qm file in your app, but this is handled automatically by our meego-qml-launcher for QML applications, so you generally don't have to think about. You just name your .ts file with your package name and it will be handled correctly. | |
| - | + | ||
| - | + | ==String Concatentation== | |
| - | + | ||
| - | + | ||
| - | + | A common pitfall for i18n is building up strings by concatenation. You do this using assumptions about grammar and order that are true for your own language, but may not be true for another language. For example, consider this code: | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | <pre>function getBestPepperColor() { | |
| - | + | return qsTr("red") | |
| - | + | } | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | = | + | var message = qsTr("Tabasco is made from peppers selected using a ") + getBestPepperColor() + qsTr(" stick")</pre> |
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | But in French, "red stick" is "bâton rouge". If you use concatenation as above, it will come out as "rouge bâton" in translation... incorrect! | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | So here's the solution: | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | <pre>var message = qsTr("Tabasco is made from peppers selected using a %1 stick").arg(getBestPepperColor())</pre> | |
| - | + | ||
| - | + | Now the French translator will come up with something like: | |
| - | + | <source>Tabasco est faite à partir de piments sélectionnés à l'aide d'un bâton %1.</source> | |
| + | They are able to move the <code>%1</code> around to the right place for their language. | ||
| - | + | Another example is where there are multiple variables in the string. The order of the variables may need to change in translation. This cannot happen if you're concatenating. | |
| - | + | ||
| - | + | Wrong: | |
| - | + | ||
| - | + | <pre>// dateHeader will be e.g. "March 14th" | |
| - | + | var dateHeader = getLocalizedMonth() + " " + getLocalizedDay()</pre> | |
| - | + | ||
| - | + | This looks right because the month and day are localized, but in another language maybe the day needs to come first, like "14. Марта" in Russian. | |
| - | + | ||
| - | + | Right: | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | <pre>// localized date header: %1 is month, %2 is day of month | |
| - | + | var dateHeader = qsTr("%1 %2").arg(getLocalizedMonth()).arg(getLocalizedDay())</pre> | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | ||
| - | + | '''Note:''' There is a problem with the tools in QML in that the translator will only see "%1 %2" so they won't know what to do with it. In Qt, you can pass a second argument to tr() to give the translator a hint, for example: | |
| - | + | ||
| - | + | <pre>QString dateFormat(tr("%1 %2", "%1 is the month, %2 is the day of the month")); | |
| - | + | QString dateHeader = dateFormat.arg(getLocalizedMonth(), getLocalizedDay());</pre> | |
| - | + | ||
| - | + | Also notice that the "<code>arg()</code>" function on QString can take multiple arguments, unlike in QML (to my knowledge). | |
| - | + | ||
| - | + | ==Translator comments== | |
| - | + | ||
| - | + | Strings like "%1 %2" cannot get correctly translated by translators without knowing some context. It is possible to add a comment intended for translators to help them. | |
| - | + | ||
| - | + | See http://doc.qt.nokia.com/latest/i18n-source-translation.html#translator-comments for more information. | |
| - | + | ||
| - | + | In QML, a translator comment must be added to the line immediately preceding the affected string. For example: | |
| - | + | ||
| - | + | <pre>//: This is a date range (start date - end date) | |
| - | + | string: qsTr("%1 - %2") | |
| - | + | </pre> | |
| - | + | ||
| + | For more information on this topic, see http://lists.meego.com/pipermail/meego-il10n/2011-June/000516.html | ||
| + | |||
| + | ==Disambiguation of strings == | ||
| + | |||
| + | Imagine an application that uses the translatable string "Read" in one place as an action (e.g. a button that enables the user to read something), and in another place uses the translatable string "Read" to mark an element that has been read. The two different meanings (and translations to other languages) require disambiguation as otherwise they will end up as the same string in the translation files and can only receive exactly one translation that will be wrong in 50% of the cases. | ||
| + | |||
| + | See http://doc.qt.nokia.com/latest/i18n-source-translation.html#disambiguation for more information how to solve this. | ||
| + | |||
| + | An example for QML: | ||
| + | |||
| + | <pre>//: %1 is the number of transactions | ||
| + | text: qsTr("Total volume is %1", "transactions").arg(num) | ||
| + | |||
| + | //: %1 is the volume of water | ||
| + | text: qsTr("Total volume is %1", "watervolume").arg(vol) | ||
| + | </pre> | ||
| + | |||
| + | ==Plural handling== | ||
| + | |||
| + | Many languages have more than one plural form (e.g. Czech has one plural form for 2-4 and another one for >=5) or complex rules (e.g. Arabic uses the singular form for amounts of 1, 11, 21, 31, ...), hence code like | ||
| + | <pre>n == 1 ? tr("%n message saved") : tr("%n messages saved")</pre> | ||
| + | will fail for these languages. | ||
| + | |||
| + | See http://doc.qt.nokia.com/latest/i18n-source-translation.html#handling-plurals for more information how to solve this. | ||
| + | |||
| + | ==Supported locales== | ||
| + | |||
| + | The locales which must be enabled, per http://bugs.meego.com, are: | ||
| + | * American English (en_US) | ||
| + | * British English (en_GB) | ||
| + | * French (fr) | ||
| + | * Spanish (es) | ||
| + | * German (de) | ||
| + | * Italian (it) | ||
| + | * Polish (pl) | ||
| + | * Dutch (nl) | ||
| + | * Russian (ru) | ||
| + | * Swedish (sv) | ||
| + | * Finnish (fi) | ||
| + | * Brazilian-Portuguese (pt_BR) | ||
| + | * Canadian-French (fr_CA) | ||
| + | * Portuguese (pt) | ||
| + | * Japanese (ja) | ||
| + | * Korean (ko) | ||
| + | * Chinese Simplified (zh_CN) | ||
| + | * Chinese Traditional (zh_TW) | ||
| - | + | If in doubt of a language code, see here: http://www.transifex.net/languages/ | |
| - | + | ||
| - | + | ||
| - | + | [[Category: Localization]] | |
Contents |
This is a developer's guide to internationalization (i18n) within the MeeGo UX project.
The most basic step in internationalization is to identify any user-visible strings in your application and replace them with code that looks up the appropriate string for the current locale. The way this is accomplished in Qt C++ code is to simply wrap the string with tr():
QString label = tr("Ring tone:");
This is accomplished in Qt Quick (QML) code by wrapping the string with qsTr():
Text {
id: dialogTitle
text: qsTr("Find contacts")
...
There is a Qt command-line tool lupdate that scans source code for these tr() and qsTr() macros and pulls out the identified strings. It writes them into a ".ts file," an XML-format file that lists information about where the strings were found. In UX projects, we generate these TS files automatically when creating the dist tarball that we push to OBS.
Your top-level .pro file should have lines like these:
TRANSLATIONS += .qml lib/.h lib/*.cpp
PROJECT_NAME = meego-app-example
dist.commands += rm -fR $${PROJECT_NAME}-$${VERSION} &&
dist.commands += git clone . $${PROJECT_NAME}-$${VERSION} &&
dist.commands += rm -fR $${PROJECT_NAME}-$${VERSION}/.git &&
dist.commands += rm -f $${PROJECT_NAME}-$${VERSION}/.gitignore &&
dist.commands += mkdir -p $${PROJECT_NAME}-$${VERSION}/ts &&
dist.commands += lupdate $${TRANSLATIONS} -ts $${PROJECT_NAME}-$${VERSION}/ts/$${PROJECT_NAME}.ts &&
dist.commands += tar jcpvf $${PROJECT_NAME}-$${VERSION}.tar.bz2 $${PROJECT_NAME}-$${VERSION} &&
dist.commands += rm -fR $${PROJECT_NAME}-$${VERSION} &&
dist.commands += echo; echo Created $${PROJECT_NAME}-$${VERSION}.tar.bz2
Make sure that TRANSLATIONS is set to include all files with translatable strings (you may need to include other subdirectories and .js files, for instance). Make sure that PROJECT_NAME is set to your application name.
Whenever you check in an update to OBS, you should create a clean tarball from git source. To create this tarball, run the following commands from the toplevel directory in your git tree:
$ qmake $ make dist
Then copy the generated tarball up to OBS. Bump the version up once a week. You should never submit an "sr" for the same version twice. But you can push to OBS multiple times throughout the week without doing an "sr."
Later these TS files are translated into various languages, and each one is compiled into a .qm binary file that allows efficient string lookup at runtime. Elsewhere you need to load a translator for each .qm file in your app, but this is handled automatically by our meego-qml-launcher for QML applications, so you generally don't have to think about. You just name your .ts file with your package name and it will be handled correctly.
A common pitfall for i18n is building up strings by concatenation. You do this using assumptions about grammar and order that are true for your own language, but may not be true for another language. For example, consider this code:
function getBestPepperColor() {
return qsTr("red")
}
var message = qsTr("Tabasco is made from peppers selected using a ") + getBestPepperColor() + qsTr(" stick")
But in French, "red stick" is "bâton rouge". If you use concatenation as above, it will come out as "rouge bâton" in translation... incorrect!
So here's the solution:
var message = qsTr("Tabasco is made from peppers selected using a %1 stick").arg(getBestPepperColor())
Now the French translator will come up with something like:
<source>Tabasco est faite à partir de piments sélectionnés à l'aide d'un bâton %1.</source>
They are able to move the %1 around to the right place for their language.
Another example is where there are multiple variables in the string. The order of the variables may need to change in translation. This cannot happen if you're concatenating.
Wrong:
// dateHeader will be e.g. "March 14th" var dateHeader = getLocalizedMonth() + " " + getLocalizedDay()
This looks right because the month and day are localized, but in another language maybe the day needs to come first, like "14. Марта" in Russian.
Right:
// localized date header: %1 is month, %2 is day of month
var dateHeader = qsTr("%1 %2").arg(getLocalizedMonth()).arg(getLocalizedDay())
Note: There is a problem with the tools in QML in that the translator will only see "%1 %2" so they won't know what to do with it. In Qt, you can pass a second argument to tr() to give the translator a hint, for example:
QString dateFormat(tr("%1 %2", "%1 is the month, %2 is the day of the month"));
QString dateHeader = dateFormat.arg(getLocalizedMonth(), getLocalizedDay());
Also notice that the "arg()" function on QString can take multiple arguments, unlike in QML (to my knowledge).
Strings like "%1 %2" cannot get correctly translated by translators without knowing some context. It is possible to add a comment intended for translators to help them.
See http://doc.qt.nokia.com/latest/i18n-source-translation.html#translator-comments for more information.
In QML, a translator comment must be added to the line immediately preceding the affected string. For example:
//: This is a date range (start date - end date)
string: qsTr("%1 - %2")
For more information on this topic, see http://lists.meego.com/pipermail/meego-il10n/2011-June/000516.html
Imagine an application that uses the translatable string "Read" in one place as an action (e.g. a button that enables the user to read something), and in another place uses the translatable string "Read" to mark an element that has been read. The two different meanings (and translations to other languages) require disambiguation as otherwise they will end up as the same string in the translation files and can only receive exactly one translation that will be wrong in 50% of the cases.
See http://doc.qt.nokia.com/latest/i18n-source-translation.html#disambiguation for more information how to solve this.
An example for QML:
//: %1 is the number of transactions
text: qsTr("Total volume is %1", "transactions").arg(num)
//: %1 is the volume of water
text: qsTr("Total volume is %1", "watervolume").arg(vol)
Many languages have more than one plural form (e.g. Czech has one plural form for 2-4 and another one for >=5) or complex rules (e.g. Arabic uses the singular form for amounts of 1, 11, 21, 31, ...), hence code like
n == 1 ? tr("%n message saved") : tr("%n messages saved")
will fail for these languages.
See http://doc.qt.nokia.com/latest/i18n-source-translation.html#handling-plurals for more information how to solve this.
The locales which must be enabled, per http://bugs.meego.com, are:
If in doubt of a language code, see here: http://www.transifex.net/languages/