KDE Programmer's i18n howto
version 3.3

Lukáš Tinkl
Matthias Kiefer
Gary Cramblitt

Abstract:

This document gives a brief introduction how to prepare KDE applications to successfully support other languages and to be used in other countries. It also lists some common problems and how to avoid them. It is targeted towards KDE programmers.


Contents


1 Introduction

Internationalization is the process of writing your application so that it can be run in any locale, taking into account data output (string messages), data input (including input methods), format of dates, numbers, currency, etc. 1

Localization is the actual process of taking an internationalized application and adapting it for a specific locale. 2

Roughly speaking, KDE programmer's internationalize their applications and KDE translation teams localize them.

Internationalization is usually abbreviated as i18n, which is the leading i and trailing n with 18 characters in between removed. Localization is abbreviated as l10n.


2 How to prepare the code

In KDE, all strings should be translated before they are displayed on the user's screen. What you should not translate are debug messages or similar.

To support i18n and l10n, KDE offers the class KLocale which is contained in kdelibs/kdecore. KLocale has a lot of functionality to make it as easy as possible for developers to make their code i18n aware. Nevertheless there are some things to take care of so that applications are usable in other languages and countries, too.

To use KLocale, you don't have to create an instance of it, but you have access to the global KLocale object with KGlobal::locale(). The object you receive takes care of all user settings and will be deleted automatically on application exit. The next few paragraphs will show in more detail how to use the functionality of KLocale and what you have to take care of.

To make translations possible, the programmer has to use i18n() on all strings that should be displayed. i18n() takes a const char* as an argument and returns a translated QString or the original string if no translation was found. If the string to be translated contains any non-UTF8 characters, use the utf8() method to get a char*. As you see on the returned value, which is of type QString, you should use QString for all strings that can be translated, because it uses internally Unicode characters and therefore can handle strings in all languages, while using the classical char* string, you will have to take care of the different possible charsets on your own3.

Since all equal strings are summarized and are therefore translated with the same string, there is an additional extended function i18n() which takes two const char* arguments. The first argument is an additional description of the second string (which is shown to the user) and both strings together are used to find the corresponding translation. In addition the first argument is shown to the translators as a hint of the meaning or context of the string.


2.1 Context information

Think of a file manager, where you can open a context menu on a file and select View to view the file. In this context View is a verb. Additionally the window of the file manager has a menu View in the menubar. In this context View is a noun. In the English version of the application everything looks fine, but in most other languages one of the strings will be wrong and confuses the user. Even if equal strings would not be translated with the same string, it would be difficult for the translators to find out, which of these strings is used as verb and which as noun. The solution for this is to use the extended i18n() function: For the context menu something like i18n("verb, to view something", "View") and for the menu in the menubar i18n("noun, the view", "View"). This way both strings are translated as separate strings and the translator has a hint how to translate these strings. Even shortening to i18n("verb", "View") and i18n("noun", "View") will be helpful to the translators.

In general it is a very good idea to use this extended function, if the string to translate is short and the meaning is hard to find out, when the context is not exactly known.

You may have seen another translation function in application templates or other existing code: I18N_NOOP(). This function does not actually translate a string, but just marks the string for translation, so that the string will get extracted and included in the po files. If you want to translate the string, you still have to use i18n() with exactly the same string afterwards. I18N_NOOP() is typically used for strings given to KAboutData, because it is constructed before the KApplication and you can use i18n() only after the construction of the KApplication. So it is safe to use always i18n() and never I18N_NOOP() if you are sure, that the code will be executed after construction of KApplication.

There is also a I18N_NOOP2("context","text") macro for providing untranslated strings with a context.


2.2 Plural handling

When the string you want translated refers to more than one thing, use code like this:

msgStr = i18n("Creating index file: 1 message done", 
              "Creating index file: %n messages done", num);

This form of i18n() gets expanded to as many cases as required by the user's language. In English, this is just two forms; in other languages it may be more depending on the num variable value4.

Note that this form should be used even if the string always refers to more than one "item". Some languages use a singular form even when referring to a multiple (typically for 21, 31, etc.). So something like i18n("%1 files were deleted") is incorrect, even if always deleting more than one file.


2.3 All about numbers

When a program needs to present numbers to the user, it should take care of decimal separator, thousand separator or currency symbol being used. These symbols may be different in various regions; in English speaking countries, a dot (.) is used to separate the fractional part of a number while in some European countries, a comma (,) is used instead. Below is a short summary of functions that will help you format the numbers correctly, taking the local conventions into account.



Function Arguments
formatMoney() double num, const QString & currency, int digits=-1
formatMoney() const QString & numStr
formatNumber() double num, int precision=-1
formatNumber() const QString & numStr
formatDate() const QDate & pDate, bool shortFormat=false
formatTime() const QTime & pTime, bool includeSecs=false
formatDateTime() const QDateTime &pDateTime, bool shortFormat=true, bool includeSecs=false



Similar functions exist to read information from user, e.g. readNumber() or readMoney().


2.4 Calendaring

Developing applications dealing with date and time, or calendars in general is a very complex area. Not only the resulting string containing a date or time may look different in different countries but also one has to take care of other aspects like:

KLocale provides, among others, these methods:



Function Arguments
formatDate() const QDate & pDate, bool shortFormat=false
formatTime() const QTime & pTime, bool includeSecs=false
formatDateTime() const QDateTime &pDateTime, bool shortFormat=true, bool includeSecs=false



TODO -- provide more info on the different calendar systems


2.5 Technical details

The user's configuration is stored in kdeglobals and the different country profiles are stored in share/locale/l10n/<country_code>. <country_code> is replaced with the short version of ISO 3166 country code for the country in lower case.


2.6 Things to take care of in your code

Although using i18n() for all visible messages is the main work to get your application translated, there are some traps, which prevent your application from being usable in some languages. Mainly these are layout problems and problems with string concatenation.


2.6.1 Use layout management

You might know that English text is often very compact while in other languages the translated text might be a lot longer. If you developed the GUI of your application carefully, you will not have a problem with this because you might anyway have used a layoutmanager to take care of the widget geometry for you. Of course you can implement geometry management on your own but it is very likely that you might get in trouble with the widget geometry when you use functions like QWidget::setGeometry() or similar. To get rid of this problem and let your application look nice also when used in other languages, it is highly recommended to use layout manager as they are provided by Qt with QLayout and derived classes. For more information look at the documentation of QLayout.


2.6.2 No word puzzles

Another thing to take care of is to not concatenate strings together like this:

QString msg=i18n("Do you want to replace ")+oldFile+i18n(" with ")+newFile+"?"

The result of constructs like this is, that it is only very hard or impossible to translate, because the structure of the sentence may be completely different in another language and the translator will only see parts of the sentences and has to guess what belongs together. The solution for this problem is to use QString::arg() which lets the translator not only make good translations because he sees the whole sentence but also let him change the order of the arguments freely. Because of this last advantage, QString::arg is also recommended to use instead of sprintf and similar functions. The above example would then look like this:

QString msg=i18n("Do you want to replace %1 with %2?").arg(oldFile).arg(newFile) 

Note: Don't insert anything else than numbers, names or similar with this method, since in some languages the translation depends on the inserted words. Better use more complete strings instead, if possible.

Similarly, messages that contain a version string or other often changing things should use QString::arg() to insert this string into the message. Otherwise every change will cause the translators to change the translated messages, too. Of course only names, numbers and such things should be inserted with QString::arg(). (Since for example KDE is translated into more than 50 languages, a single change causes at least 50 people to open the file, find the changed message, look carefully if this is the only thing that has changed, change the translation, save the file again and commit the changed file into the code repository. All in all such a small change might cause work of more than one hour. Just to give you an idea, so please think carefully about the messages you use.)


2.6.3 Be Unicode friendly

Don't ever call QString::latin1() or QString::ascii() on translated strings unless you really know what you are doing! This also applies to information submitted by user, like passwords, URLs, filenames etc...

KDE applications are translated in many languages which use other charsets than Latin1. If you really need a char* representation of a string, better use QString::utf8()5. For more information on charsets and Unicode, see the KDE Unicode Howto.

Since KDE 3.0, the KIO local filesystem can use UTF-8 encoded filenames6. You may turn it on globally by exporting KDE_UTF8_FILENAMES in your shell's startup file (e.g. .bashrc). You, as a programmer, have to take care of passing a properly encoded filename to any method in question -- the correct way is not to guess user's filesystem encoding but to use QFile::encodeName() and QFile::decodeName().


3 Makefile.am

You must add a messages target to your application's Makefile.am.

Example messages target (taken from kdebase/konqueror, shortened):

messages: rc.cpp
        $(EXTRACTRC) *.rc */*.rc >> rc.cpp
        $(EXTRACTRC) sidebar/trees/history_module/history_dlg.ui >> rc.cpp
        $(XGETTEXT) rc.cpp *.h *.cc *view/*h *view/*cc kedit*/*.cpp \
        -o $(podir)/konqueror.pot

Line 1 extracts messages from ui, rc, and kcfg files in the current folder (folder containing the Makefile.am). On line 2, we extract messages from rc files, on line 3 from Qt Designer UI files and finally on line 4, we put them all (together with C++ sources and headers) into konqueror.pot.

Important: You must use tab characters for indentation. make will not be happy with spaces.

$EXTRACTRC and $EXTRACTATTR can be used to extract messages from xml files. These programs are in the kdesdk module. Assuming you have them installed in the $PATH, type the following commands in a Konsole to learn more.

extractrc --help
extractattr --help

If the source tree contains many nested subdirectories, the find and grep commands can help. Here's an example from kdenetwork (shortened).

messages:
        $(EXTRACTRC) `find . -name \*.ui -o -name \*.rc | egrep -v
        '(libkopete/compat|protocols/testbed)'` > rc.cpp
        LIST=`find . -name \*.h -o -name \*.cpp -o -name \*.c | egrep -v
        '(libkopete/compat|protocols/testbed)'`; \
        if test -n "$$LIST"; then \
                $(XGETTEXT) $$LIST -o $(podir)/kopete.pot; \
        fi

You cannot pick any arbitrary name you want for the .pot file. The name you code must match the name of the catalogue that will be read at runtime. See Naming .pot files [*].

See the Makefile.am HOWTO for more information about messages targets.

If your application is a small helper application in a larger application, you can use the translations from that app. Let's take the example of KEditBookmark (the KDE bookmark editor) to explain this topic. The bookmark editor sources reside in Konqueror's subfolder keditbookmark. Notice that we extract its messages into the konqueror.pot file on the line 3 as well. What's left now is to tell the bookmark editor to load the konqueror message catalog and use it as its own catalog (usually done in the main entry point of your binary):

#include <klocale.h>

int main(int argc, char ** argv)
{
  KLocale::setMainCatalogue("konqueror");

  <some code>

  KApplication app;
  <your code here>

  return app.exec();
}

The method KLocale::setMainCatalogue("konqueror") on line 5 does the job for us. Another similar issue is pre-loading a different catalog at application's startup (usually done for e.g. Kicker applets, multimedia plugins or KOffice filters):

extern "C"
{
  KPanelApplet* init(QWidget *parent, const QString& configFile)
  {
    KGlobal::locale()->insertCatalogue("clockapplet");
    KGlobal::locale()->insertCatalogue("timezones"); // For time zone translations
    return new ClockApplet(configFile, KPanelApplet::Normal,
                           KPanelApplet::Preferences, parent, "clockapplet");
  }
}

If your application consists of many plugins, parts, and libraries, all the components should share a single .pot file. The reason for this is the components are likely to contain duplicate strings for translation. If you created a separate .pot file for each component, the translators would have more work. If you use a single .pot file for all the components, the translators need only translate each unique string once.

It is best if your .pot file has the same name as the top-level folder of your project. If so, the svn2dist script will automatically include translations into the tarball it creates for your app.

So how do you set up your project to share a single .pot file? In the top-level Makefile.am of your project, create a messages target as usual, except you enter commands to extract strings from all the component subfolders as well. The example above for Konqueror does just that, extracting strings from all the *view and kedit* folders as well as the sidebar/trees/history_module/history_dlg.ui file. Do not put messages targets into the Makefile.am's of the subfolders. You should also make sure each component reads from the translation catalog having the same name as the .pot file, but before we get into that, let's discuss the translation process.


4 The translation process from the programmer's viewpoint

Assuming you've set up your application as described above, the following outlines the process that occurs to translate your application. If your application is in the KDE code repository, all this happens automatically. If your code is not in the KDE repository, you must perform these steps yourself. See Distributing message catalogs [*].

  1. Periodically, the script kde-common/makemessages (a.k.a. scripty) runs on the KDE server. This program extracts all the i18n messages from your application per the messages targets in your Makefile.am's. The extracted messages are stored in template (.pot) files in the templates folder of the l10n module. See What Is Scripty for more information.
  2. The KDE translation teams translate the messages and commit them into a messages folder corresponding to the language, which is further broken down by module. For example, German translated messages for konqueror are committed to l10n/de/messages/kdebase/konqueror.po.
  3. When the l10n module is built, the .po files are compiled into a binary format for fast lookup and installed as .mo files to $KDEDIR/share/locale/xx/LC_MESSAGES/, where xx is the two-letter ISO 639 code for the language. These are called the message catalogs.
  4. At runtime, the i18n() function, using the original string you coded, looks up the string in the message catalog of the user's desktop language and returns the translated string. If the message catalog is missing, or the specific string is not found, i18n falls back to the original string in your code.

.desktop files in your project are handled separately. makemessages extracts strings, such as Name and Comment from the .desktop files and places them into a file named desktop_mmmm.pot, where mmmm is the module name, in the templates folder. Once translators have translated this file, makemessages inserts the translated strings back into the .desktop files. The list of strings extracted is in l10n/scripts/apply.cc. Here's the code that checks for them.

if (checkTag("Name", in, argc, argv, newFile))
    continue;
if (checkTag("Comment", in, argc, argv, newFile))
    continue;
if (checkTag("Language", in, argc, argv, newFile))
    continue;
if (checkTag("Keywords", in, argc, argv, newFile))
    continue;
if (checkTag("About", in, argc, argv, newFile))
    continue;
if (checkTag("Description", in, argc, argv, newFile))
    continue;
if (checkTag("GenericName", in, argc, argv, newFile))
    continue;
if (checkTag("Query", in, argc, argv, newFile))
    continue;
if (checkTag("ExtraNames", in, argc, argv, newFile))


5 Naming .pot files

As can be seen in the process above, the name of your .pot file that you code in Makefile.am must match the name of the message catalog (.mo) file that your application will read at runtime. But what name will be used at runtime?

In general, it will be the instanceName() returned by the application or component's KInstance. But what will that be?

For simple standalone applications, the message catalog will default to the name of the application passed as the first argument to KAboutData.

If your code calls setMainCatalogue, that name will be used instead.

If your code calls insertCatalogue, the specified catalog will also be searched in addition to the main catalog.

If your code does not call setMainCatalogue or insertCatalogue and your application is a plugin, it gets a little more complicated. If your code exports your plugin using the K_EXPORT_COMPONENT_FACTORY macro, like this

K_EXPORT_COMPONENT_FACTORY( libkhtmlkttsdplugin,
  KGenericFactory<KHTMLPluginKTTSD>("khtmlkttsd") )

then the catalogue name will be the name passed in the KGenericFactory constructor - in the example above khtmlkttsd. This is because the macro creates a KInstance for you with the specified name. However, some classes, such as KTextEditor create a KPart containing your component and the name passed in the macro is not used. In this case, you should call insertCatalogue in the component constructor.

If in doubt, the safest thing to do is to call insertCatalogue.

Note: If you're not using the export macro for your plugin, you probably should be. See KGenericFactory in the KDE Library API documentation.

Note: Calling insertCatalogue if the catalog has already been inserted does nothing. However, calling removeCatalogue removes the catalog no matter how many times it has been inserted. Therefore, take care that you do not inadvertently remove the catalog when multiple components are sharing a single catalog. For example, the following sequence will break translations:

// Component A loads and uses catalog xyz by calling
insertCatalogue("xyz");
// Component B loads and also uses the same catalog.
insertCatalogue("xyz");
// component B unloads and calls
removeCatalogue("xyz")
// Component A now doesn't translate properly.

Notice that a sequence such as above will occur automatically if both components A and B are loaded using the K_EXPORT_COMPONENT_FACTORY macro and pass the same name argument to the KGenericFactory constructor.

TODO" How are KParts handled? Is the catalog the name in the KInstance, or the name in the KPart's KAboutData?

The name that you settle on for both the .pot file and catalog should be governed by where your code resides. If it is a component of another application and resides in that application's source tree, you'll want to share with the main catalog of the application. If is a component of another application but resides elsewhere in the code repository, you will probably have to create a separate .pot, but keep in mind that .pot filenames must be unique across all of KDE. If it is a component of another application, but resides in the source tree of a second application, you should share with the second application. For example, KTTSD includes a plugin for embedded Kate in the kdeaccessibility/kttsd source tree. Since the plugin is not installed unless kttsd is installed, it shares its catalog with kttsd and calls insertCatalogue("kttsd") to do this.


6 Distributing message catalogs

If your application is in the KDE code repository, you don't have to do anything to distribute message catalogs. Users will either download the l10n source and build it (see Building the l10n module [*]), or install binaries prepared by packagers. For example, Debian users install the l10n package.

In addition, if your top-level folder and .pot file have the same name, svn2dist will automatically include .po files when you use it to make a source tarball for your app.

If your application is not in the KDE code repository, you have to extract the messages yourself and provide the translated PO files within your distribution. To extract the messages you need to have xgettext installed and extractrc from kdesdk has to be in your path.7 You also must have the KDE build system (admin folder) as part of your distribution. A copy of admin can be found in kde-common/admin. Then do the following commands 8 in the base folder of your distribution.

cd myapp
mkdir po
make -f admin/Makefile.common package-messages
echo po>>SUBDIRS
echo "POFILES = AUTO">po/Makefile.am

After that you will find a file <appname>.pot, which contains all messages of your application in the subfolder po.

TODO: How can messages for .desktop files be extracted?

Now you just have to find translators to translate the extracted messages into the various languages. ;-) Translated PO files then have to be stored in the po folder with the naming scheme <languagecode>.po.

TODO: How can translated .desktop messages be inserted back into .desktop files?


6.1 Technical details

The actual translation is done with the gettext package. It contains tools for extracting messages from source files and to handle changed messages, so that translators do not have to start over and over again. The extracted messages and the translations are stored in so called ``PO files'' using different encodings. These files are then compiled into a binary format (MO files) which then get installed.

All translations of a language have to be stored in the same encoding which is defined in the charset file. KLocale reads this file when constructed and uses this information to decode the translations.

Nowadays, UTF-8 is required as the PO files encoding in KDE code repository.


7 Building the l10n module

As a developer, you should build some languages from the l10n module so that you can test whether your application is getting properly translated.

First, download the kdesdk module from the repository, then build and install.

cd kdesdk
make -f Makefile.cvs
./configure
make
make install

If you have trouble building the kdesdk module, you can skip the troublesome parts. The main thing you need from the kdesdk module is the poxml folder. KBabel is also nice to have but not essential.

cd kdesdk
echo poxml>inst-apps
echo kbabel>>inst-apps
make -f Makefile.cvs
./configure
make
make install

You can also use the DO_NOT_COMPILE variable to skip a top-level folder. Let's say you are having trouble building kbugbuster.

cd kdesdk
export DO_NOT_COMPILE="kbugbuster"
make -f Makefile.cvs
./configure
make
make install

You must download, build and install each language in the l10n module separately. You can check out and build just the German, and XX languages using the following procedure (assuming the code repository uses Subversion 9).

svn co -N svn://anonsvn.kde.org/home/kde/trunk/l10n
cd l10n
svn up scripts
svn up templates
svn up xx
svn up de
echo xx>inst-apps
echo de>>inst-apps
./xx/internal/update_xx.sh
./scripts/autogen.sh
cd xx
./configure
make
make install
cd ../de
./configure
make
make install

Important: Do not combine make and make install in a single step. It won't install anything.

The ./xx/internal/update_xx.sh step uses the transxx program in kdesdk/poxml to automatically produce the XX language translation. If you were not able to build the kdesdk/poxml files, skip the ./xx/internal/update_xx.sh step. You'll have to rely on someone else running it and committing to the code repository.

To run your application using the German catalogs

KDE_LANG=de myapp

The German language is a good choice for testing the layout of your application, as German translated strings tend to be quite long. French is also a good choice. The XX language is useful for identifying strings in your application that are not translated. See XX language [*].


8 Handbooks

Handbooks, which are written in docbook format, are handled different from applications. The translated Handbooks are committed into the l10n module in the docs folder under each language. In addition, the docs folder is broken down by module and application. For example, the German Kate Handbook is committed to the l10n/de/docs/kdebase/kate/ folder. When compiled, the German Kate Handbook is installed to $KDEDIR/share/doc/HTML/de/kate/index.cache.bz2.

Note that it is up to each translation team to generate the translated Handbook when they feel it is complete.


9 Message freezes and KDE Policy

If your application is part of KDE that is periodically announced and released, you must pay attention to announcements on the mailing lists for message freezes. When a message freeze is in place, you must not change any of the translatable strings in your application or its Handbook.

Message freezes permit the translation teams to complete their work. If you change i18n strings in your application, it creates more work (and confusion) for the teams and could prevent your application from being released with translations.

Message freezes for KDE do not apply to modules outside the KDE folder in the code repository, therefore, they do not apply to the following modules. These modules typically announce their own release cycles and messages freezes:

If you find a GUI string that you feel must be fixed or added during a message freeze, ask first on the kde-i18n-doc mailing list. Changing a string or adding a new one will not invalidate the entire translation, but annoys the translation teams, who are trying to be 100 percent done.

See Fixes during message freeze [*] for more information.


9.1 Handbooks

Handbooks especially should not be touched during a message freeze. This is because changing a Handbook (even a small change) can prevent the entire translated Handbook from being released. For this reason, you should watch the release cycle plans carefully and try to complete your Handbook at least 3 to 4 weeks before the message freeze.

If you do make a late change to a Handbook, it will be up to the translation team whether to apply the fix to their translation or not. If the team has already generated the translated Handbook, the uncorrected version may end up being released.

Freezes are often announced in two phases. A feature freeze means GUI string changes should not be made without first asking permission on the kde-i18n-doc mailing list. Handbook changes are OK at this time. A hard freeze means you must not make any more changes to code or to the Handbook.

If you find a Handbook string that you feel must be fixed or added during a freeze, ask first on the kde-doc-english mailing list. Changing the Handbook, even a tiny change, can invalidate the entire Handbook. Once a hard freeze is in place, fixes should be sent to kde-doc-english as patch files (svn diff). The fixes will not be translated, but they will be incorporated into the English-only version of the Handbook.

Questions about Handbook translation should be sent to the kde-doc-english mailing list.


9.2 Independent releases

If you application is not part of an official release cycle, it will be up to you to plan releases and coordinate with the translation teams. When you want to do a release, send an email to the kde-i18n-doc mailing list with the following information:

You should provide at least 3 weeks or more between the message freeze date and release date - more if your application is large and complex. If your application has a Handbook, you should provide even more time.

It is a good idea to avoid time periods when KDE or KOffice are in message freeze, since the translators will be very busy at these times.

If you are distributing your code to translators via a source tarball, you should first ask for volunteers. Of course, the message freeze occurs when you send them the tarball. Your tarball must have all the .pot files and prepared po folder (see Distributing message catalogs [*]).


9.3 Moving extragear applications to stable

Applications in the extragear module have a special procedure for handling translations. When an extragear application becomes stable (mostly at message freeze), the maintainer should copy the application to the appropriate extragear directory in branches/stable/extragear. In addition the whole set of translation template files (.pot) and translation files (.po), including eventual documentation, of the application must be copied to the appropriate places in branches/stable/l10n. This serves as a signal to translators that the application is ready for final translation work in preparation for release.


10 Useful tools

There are some very well hidden tools that can help you debug your application and stay compliant with i18n standards in KDE.


10.1 Dr. Klash

This little utility, once activated, can present a report about conflicting shortcuts in menus. Not only this is helpful for translators but also for developers. A little hand editing of /.kde/share/config/kdeglobals is needed:

  [Development]
  CheckAccelerators=F12
  AutoCheckAccelerators=false
  AlwaysShowCheckAccelerators=false

Now pressing F12 will show you the current accelerators conflicts and insert suggested new accelerators. If no accelerators conflicts exist or Dr. Klash has nothing to suggest, the popup dialog will not be displayed at all.

With AutoCheckAccelerators enabled the dialog will popup automatically if a conflict exists and AlwaysShowCheckAccelerators can force the dialog to always popup.


10.2 XX language

This helper language serves as a debugging aid for people finding untranslated strings in applications. If you start your application with the ``xx'' locale, all translated strings will appear with x's. First you have to check out these ``translations'' from l10n/xx and install them. See Building the l10n module [*].

After you've built l10n/xx and installed it, run your application using the XX locale.

  > KDE_LANG=xx kspread

This will start KSpread using the XX locale no matter what is your current language preference set in the Control Center. Look for strings that do not have the x's in them. If x's are missing from a string, it could be one of the following problems:

When checking for untranslated strings, be sure to check all the WhatsThis help and tooltips.


10.3 Examining .po files

You can look at the translated .po files to see if a string is present and has been translated. For example, here's a portion of l10n/de/messages/kdebase/konqueror.po:

#: konq_mainwindow.cc:3707 konq_tabs.cc:84
msgid "&New Tab"
msgstr "Neues &Unterfenster"

The string to be translated is given on the msgid line. The translated string is on the msgstr line. Notice that the exact same string to translate came from konq_mainwindow.cc and also from konq_tabs.cc. This saves the translators from having to translate the same identical string twice.

You can also use KBabel to examine .po files. KBabel is part of the kdesdk module.


11 Common problems and solutions


11.1 Translating data

If your application uses data that you want the translators to translate for you, there are a couple of solutions. If the data is a single string, and you want to use a translation of the string that is different from the user's desktop language setting, you can create a .desktop file and extract the translated string from it. For example, KTTSD has the following kcmkttsd_testmessage.desktop file:

[Desktop Entry]
Encoding=UTF-8
NoDisplay=true
Name=The text-to-speech system seems to be functioning properly.

Notice the NoDisplay=true line. The translators will translate the Name line and scripty will insert the translations back into the .desktop file. At runtime, KTTSD uses the following code to get a test message to speak in the language of the synthesizer's voice, not the user's desktop language:

QString key = "Name[" + languageCode + "]";
QString result;
QString def;
QFile file(locate("data", "kttsd/kcmkttsd_testmessage.desktop"));
if (file.open(IO_ReadOnly))
{
  QTextStream stream(&file);
  stream.setEncoding(QTextStream::UnicodeUTF8);
  while ( !stream.atEnd() ) {
    QString line = stream.readLine(); // line of text excluding '\n'
    QStringList keyAndValue = QStringList::split("=", line);
    if (keyAndValue.count() == 2)
    {
      if (keyAndValue[0] == key)
      {
        result = keyAndValue[1];
        break;
      }
      // Use English default if not found.
      if (keyAndValue[0] == "Name") def = keyAndValue[1];
    }
  }
  file.close();
}
if (result.isEmpty()) {
  result = def;
}
return result;

If your program has larger amounts of data, an XML file is a good way to go. For example, KTTSD maintains a list of Festival synthesizer voices in an XML file called voices. Here's a sample portion from that file:

<?xml version="1.0" encoding="UTF-8"?>
<voices>
<voice>
  <code>kal_diphone</code>
  <language>en_US</language>
  <codec>ISO 8859-1</codec>
  <gender>male</gender>
  <name>American Male</name>
</voice>

The Makefile.am extracts the name strings from this file like this:

messages: rc.cpp
    $(EXTRACTRC) */*.rc */*/*.rc >> rc.cpp
    $(EXTRACTRC) */*.ui */*/*.ui >> rc.cpp
    $(EXTRACTRC) --tag=name --context=FestivalVoiceName plugins/festivalint/voices >> rc.cpp
    $(XGETTEXT) rc.cpp */*.cpp */*.h */*/*.cpp */*/*.h -o $(podir)/kttsd.pot

Notice that the translators are given a context to help them to translate the voice names. At runtime, KTTSD parses the voices file and displays the translated name of each voice (in the user's desktop language) using code like this:

QDomNode childNode = node.namedItem("name");
if (!childNode.isNull())
  QString name = i18n("FestivalVoiceName",
    childNode.toElement().text().utf8());

11.2 Coding in a foreign language

If you are not comfortable with the English language, you may have coded your application using strings in your native language. You must convert all your code to English before it can be translated by the translation teams. One solution is to get someone who understands both English and your language to help you with this. You can also ask for a volunteer on the kde-i18n-doc mailing list. You will create your .pot file (see Distributing message catalogs [*]) and send it to them. They will translate it to English and send you back a .po file. Using cut and paste, put the English strings into your code. If you do this, it is still a good idea to have an English speaker review your code for any mistakes.


11.3 Merging .po files

Question: I didn't follow your advice above and create a single .pot file for all the components in my application. The translators have already created the .po files and I don't want to wipe out all their hard work. What should I do now?

Answer: Go ahead and correct all your code and Makefile.am's. Commit the changes to the KDE code repository. Ask one of the KDE admins to merge the existing .po files. She will need to know the exact names of all the .po files, which module they are in, and the name of the desired merged .po file. The admin will have to wait for scripty to generate your new .pot file before doing the merge.

11.4 Deleting obsolete .pot files

If you change your Makefile.am so that it no longer generates a .pot file, or generates a .pot file with a different name, the old .pot file will not automatically disappear from the code repository. You must delete it yourself from the templates folder in the l10n module. You should also send an email to the kde-i18n-doc mailing list advising translators that they may delete the .po file. If you do not do this, the translators will waste their time working on an obsolete file.


11.5 Fixes during message freeze

It's message freeze time and you forgot to code with the i18n() function or forgot to include a file in the Makefile.am messages target, or you need to make some changes to existing strings. What should you do?

If you need to change an existing string, ask the translator's on the kde-i18n-doc mailing list for permission first before you make the fixes.

If you need to add untranslated strings (forgotten i18n() or mis-coded Makefile.am), go ahead and make the fixes. When you commit the fixes, write "untranslated strings" in the commit message so that translators will know why you are committing string changes during a message freeze. It is a good idea to email kde-i18n-doc to let the translators know that you've added additional strings. If the translator's don't have time to translate the additional strings, they will display in English at runtime.

An untranslated string is a string that exists in your code but is not included in a .pot file because the i18n() is missing or the Makefile.am is mis-coded. It is considered a violation of the message freeze to add a brand new translated string to your code where the string did not exist before. So changing

QMessageBox::warning(0, "Starting KTTSD Failed", error );

to

QMessageBox::warning(0, i18n( "Starting KTTSD Failed"), error );

is OK, but adding the call to QMessageBox where it didn't exist before will raise objections from the translators. In the latter case, ask on the kde-i18n-doc mailing list first. In general, the criteria for modifying or adding strings is potentially severe misunderstanding of a message, especially if it means a risk of losing data.

Note that changes to the Handbook should be avoided during a message freeze. If you really must make a change, ask on the kde-doc-english mailing list first.

By the way, you may see translators refer to "fuzzy" strings. Fuzzy strings come about when a string has already been translated and then is changed a little bit later. xgettext leaves the original translation in the .po file, but marks the string as fuzzy. The translator must look at the translation to determine if a change is needed and remove the fuzzy flag, otherwise the translation will not be used at runtime. In short, if you change strings in your app, they will generate fuzzies for the translators.

11.6 Handbook entity errors

Sometimes, when building Handbooks in the l10n/xx/docs folders, you may get entity errors. First of all, make sure your kdelibs/kdoctools folder is up-to-date and installed. If that doesn't fix the problem, it may be that the translator made an error, leaving an entity in the .docbook file that is defined in English but not in the translated language. In many cases, the translators haven't finished the translation. Contact the translation team.

For example, the English entity eg is defined in the kdelibs/kdoctools/customizations/en/user.entities file but is not defined in the German kdelibs/kdoctools/customizations/de/user.entities. It must be replaced by the translator with the entity zb (Zum Beispiel).


12 Links and additional information




We encourage you to read these in-depth materials by Markus Kuhn:


13 Revision history

Version 3.3 (cramblitt, 2006 Feb):

Version 3.2 (cramblitt, 2005 Sep):

Version 3.1 (cramblitt, 2005 May):

Version 3.0 (cramblitt, 2005 Mar):

Version 2.0 (lukas):




TODO:



Footnotes

... etc.1
A formal definition of internationalization may be found at http://www.lisa.org/term/termdefinitions/termShow.php?id=6.
... locale.2
A formal definition of localization may be found at http://www.lisa.org/term/termdefinitions/termShow.php?id=1.
... own3
If you are wondering where to find documentation of the i18n() function: it is defined as a global function in klocale.h and uses KLocale::translate() internally.
... value4
Notice usage of %n here, it must be part of the string
...QString::utf8()5
QString::local8bit() should be avoided, except in rare cases.
... filenames6
This feature is still regarded as experimental.
... path.7
KDE uses a patched version of xgettext to use the above mentioned additional context information. You can find this patch in kdesdk/scripts.
... commands8
As of Sep 2005, the KDE project is in the process of building KDE version 4. The standard build command has changed from make to unsermake. In general, the steps are the same; just substitute unsermake for make. Unsermake is available in the kdenonbeta module.
... Subversion9
As of May 2005, there are problems with the -N option in Subversion. The "svn up scripts" command in the procedure should get the scripts/admin folder. If not, or there are other problems, you'll have to check out the entire l10n module.
... Qt10
Only some information is relevant, e.g. encodings


Gary Cramblitt 2006-02-13