Development/Tutorials/Localization/i18n Build Systems/Outside KDE repositories

From KDE TechBase

Handling i18n outside KDE's repositories

If your application is developed outside of KDE's repositories, you need to take care of generating and installing message catalogs yourself. This is not too hard to do, but you should make sure you have a basic understanding of all steps involved, so you will probably want to read The xgettext toolchain, first.

Also, there is more than one way to deal with i18n in your application. We will outline one approach to do so for simple applications, but you may want to diverge from this, if you like to.

General considerations

The i18n generation process can roughly be divided into two steps:

  1. Extracting and merging messages
  2. Compiling and installing message catalogs

The first step really concerns the sources, while the second step is a natural part of compiling and installing an application. Hence, while the second step should definitely be incorporated into the build system for your application (we assume CMake, here), there is no striking reason for the first step to be handled by the build system.

In fact, the extraction and merging of messages does not map well onto the CMake concept of out-of-source builds, and CMake can't provide too much help for this step, either.

Hence, in this tutorial, we will handle the first step with a standalone shell script, and only the second step will be handled by CMake.

Extracting and merging messages

If you have read the section Handling i18n in KDE's repositories you will know that in the KDE repository, message extraction and merging is handled by a simple script called Messages.sh, which is invoked by a script called extract-messages.sh. Outside of KDE's repository, you will need to take care of both parts, but fortunately, this is easy enough. Here's a sample script:

#!/bin/sh
BASEDIR="../rkward/"	# root of translatable sources
PROJECT="rkward"	# project name
BUGADDR="http://sourceforge.net/tracker/?group_id=50231&atid=459007"	# MSGID-Bugs
WDIR=`pwd`		# working dir


echo "Preparing rc files"
cd ${BASEDIR}
# we use simple sorting to make sure the lines do not jump around too much from system to system
find . -name '*.rc' -o -name '*.ui' -o -name '*.kcfg' | sort > ${WDIR}/rcfiles.list
xargs --arg-file=${WDIR}/rcfiles.list extractrc > ${WDIR}/rc.cpp
# additional string for KAboutData
echo 'i18nc("NAME OF TRANSLATORS","Your names");' >> ${WDIR}/rc.cpp
echo 'i18nc("EMAIL OF TRANSLATORS","Your emails");' >> ${WDIR}/rc.cpp
cd ${WDIR}
echo "Done preparing rc files"


echo "Extracting messages"
cd ${BASEDIR}
# see above on sorting
find . -name '*.cpp' -o -name '*.h' -o -name '*.c' | sort > ${WDIR}/infiles.list
echo "rc.cpp" >> ${WDIR}/infiles.list
cd ${WDIR}
xgettext --from-code=UTF-8 -C -kde -ci18n -ki18n:1 -ki18nc:1c,2 -ki18np:1,2 -ki18ncp:1c,2,3 -ktr2i18n:1 \
	-kI18N_NOOP:1 -kI18N_NOOP2:1c,2 -kaliasLocale -kki18n:1 -kki18nc:1c,2 -kki18np:1,2 -kki18ncp:1c,2,3 \
	--msgid-bugs-address="${BUGADDR}" \
	--files-from=infiles.list -D ${BASEDIR} -D ${WDIR} -o ${PROJECT}.pot || { echo "error while calling xgettext. aborting."; exit 1; }
echo "Done extracting messages"


echo "Merging translations"
catalogs=`find . -name '*.po'`
for cat in $catalogs; do
  echo $cat
  msgmerge -o $cat.new $cat ${PROJECT}.pot
  mv $cat.new $cat
done
echo "Done merging translations"


echo "Cleaning up"
cd ${WDIR}
rm rcfiles.list
rm infiles.list
rm rc.cpp
echo "Done"

Of course you will want to adjust the variable definitions at the top, and -- if needed -- add code to extract tips-of-the-day or other additional strings.

The example script assumes that all .po files and the .pot file are kept in a single directory, which is appropriate for most projects.

Compiling and installing message catalogs

Assuming you use the script from the previous section, you can place the following CMakeLists.txt in the directory containing the .po files:

FIND_PROGRAM(GETTEXT_MSGFMT_EXECUTABLE msgfmt)

IF(NOT GETTEXT_MSGFMT_EXECUTABLE)
	MESSAGE(
"------
                 NOTE: msgfmt not found. Translations will *not* be installed
------")
ELSE(NOT GETTEXT_MSGFMT_EXECUTABLE)

        SET(catalogname rkward)

        FILE(GLOB PO_FILES *.po)
        SET(GMO_FILES)

        FOREACH(_poFile ${PO_FILES})
                GET_FILENAME_COMPONENT(_poFileName ${_poFile} NAME)
                STRING(REGEX REPLACE "^${catalogname}_?" "" _langCode ${_poFileName} )
                STRING(REGEX REPLACE "\\.po$" "" _langCode ${_langCode} )

                IF( _langCode )
                        GET_FILENAME_COMPONENT(_lang ${_poFile} NAME_WE)
                        SET(_gmoFile ${CMAKE_CURRENT_BINARY_DIR}/${_lang}.gmo)

                        ADD_CUSTOM_COMMAND(TARGET ${_gmoFile}
                                COMMAND ${GETTEXT_MSGFMT_EXECUTABLE} --check -o ${_gmoFile} ${_poFile}
                                DEPENDS ${_poFile})
                        INSTALL(FILES ${_gmoFile} DESTINATION ${LOCALE_INSTALL_DIR}/${_langCode}/LC_MESSAGES/ RENAME ${catalogname}.mo)
                        LIST(APPEND GMO_FILES ${_gmoFile})
                ENDIF( _langCode )

        ENDFOREACH(_poFile ${PO_FILES})

        ADD_CUSTOM_TARGET(translations ALL DEPENDS ${GMO_FILES})

ENDIF(NOT GETTEXT_MSGFMT_EXECUTABLE)

This iterates over all .po files in the directory, compiles them using msgfmt, and installs them to the standard location. You will want to change "catalogname" to the name of your application.

Getting translations

Now you just have to find translators to translate the extracted messages into the various languages. As your application gets used by more people, you will find that translators will volunteer to do this. Translated PO files then have to be stored in the po folder with the naming scheme <languagecode>.po.

Translating .desktop Files

Translating .desktop files is a bit tricky because the translation of .desktop files isn't fetched from .po/.gmo files, but must be part of the .desktop file itself. So we have 2 tasks:

  • extract messages from the .desktop file
  • merge the translations in the .desktop file

Our friends from Gnome provide the intltool package. While this tools is mainly written for the old autotools build system (but KDE uses CMake since KDE 4), we can use this package nevertheless for our purpose. Here is a step-by-step guide:

  1. Install the intltool package
  2. We assume that your .desktop file is named "MYPROGRAM.desktop".
  3. Make a copy of "MYPROGRAM.desktop" which is named "MYPROGRAM.desktop.template".
  4. In MYPROGRAM.desktop.template substitute the keys "Name" by "_Name", "GenericName" by "_GenericName" and "Comment" by "_Comment".
  5. Remove all translations from "MYPROGRAM.desktop.template".
  6. Add the following lines to the above described build script, at the end of the section "Preparing rc files":
intltool-extract --quiet --type=gettext/ini PATH/MYPROGRAM.desktop.template
mv PATH/MYPROGRAM.desktop.template.h ${WDIR}/rc.cpp
cd ${WDIR}

intltool-extract will create a dummy C++ header (PATH/MYPROGRAM.desktop.template.h) with the strings whose key starts with "_". We append the content of this header file to our "rc.cpp" and remove the file then. (We assume that "PATH" is the path to "MYPROGRAM.desktop.template", relative to the working directory of the script.)

  1. Add "-kN_:1" as additional parameter to the xgettext call in the section "Extracting messages". This is necessary because dummy code that intltool-extract produces uses "N_()" instead of "i18n()".
  2. Add the following lines to the above described build script, at the end of the section "Merging translations":
cd ${WDIR}
intltool-merge --quiet --desktop-style ${WDIR} PATH/MYPROGRAM.desktop.template PATH2/MYPROGRAM.desktop

This will produce MYPROGRAM.desktop from MYPROGRAM.desktop.template, inserting all available translations in the .po files in the directory ${WDIR}. (We assume that "PATH" is the path to "MYPROGRAM.desktop.template", relative to the working directory of the script. We assume that "PATH2" is the path to the final location of "MYPROGRAM.desktop", relative to the working directory of the script.)

Note
Names of .po files):For intltool, it is required that the .po files have the name <languagecode>.po without other characters (NOT programname_<languagecode>.po). This is important because intltool-merge simply uses the .po file name (without ".po") as language key in the .desktop file.