Using cmake to update gettext files

CMake is a tool to manage building of source code. GNU gettext utilities offers a set of conventions about how programs should be written to support message catalogues and how they can be organised. Together they keep the maintenance together in a way that's OS agnostic.

Resources

The workflow process is straightforward. After manually creating a language file:

  • Create or update a message.pot template file from source code (pot-update);
  • Merge template with existing translation (po-merge);
  • Compile the human translations po files into machine readable mo files (po-compile);
  • Repeat as necessary.

After creating a cmake CMakeLists.txt for a project, add the following code to create pot-update, po-merge and po-compile, those three utilities can be run using the normal make process to create an initial template, merge that template into language po files, and then to use those po files to create compiled language mo files for inclusion in your project.

Set up the i18n discovery and build arguments with find_package(Intl), then find_package(Gettext) to set up gettext.

The three utilities xgettext, msgmerge and msgfmt need to be found using find_program, then add them to the generated make files.

For clarity the three executables are added in an if() block. First adding a make target with add_custom_target() then adding the commands to the target with add_custom_command(). These will run every time they are called with "make pot-update" "make po-merge" and "make po-compile".

The file structure looks like the following:

project/
project/build
project/src/CMakeLists.txt
project/src/locale/project.pot
project/src/locale/en_GB/project.po
project/src/locale/en_GB/project.mo
project/src/locale/fr_FR/project.po
project/src/locale/fr_FR/project.mo
project/src/Project.cpp
project/README.md

# Setting up Internationalisation (i18n)
find_package (Intl REQUIRED)
if (Intl_FOUND)
    message(STATUS "Internationalization (i18n) found:")
    message(STATUS " INTL_INCLUDE_DIRS: ${Intl_INCLUDE_DIRS}")
    message(STATUS " INTL_LIBRARIES: ${Intl_LIBRARIES}")
    message(STATUS " Version: ${Intl_VERSION}")
    include_directories(${Intl_INCLUDE_DIRS})
    link_directories(${Intl_LIBRARY_DIRS})
else ()
    message(STATUS "Internationalization (i18n) Not found!")
endif ()

find_package(Gettext REQUIRED)
if (Gettext_FOUND)
    message(STATUS "Gettext found:")
    message(STATUS " Version: ${GETTEXT_VERSION_STRING}")
else ()
    message(STATUS "Gettext Not found!")
endif ()

find_program(GETTEXT_XGETTEXT_EXECUTABLE xgettext)
find_program(GETTEXT_MSGMERGE_EXECUTABLE msgmerge)
find_program(GETTEXT_MSGFMT_EXECUTABLE msgfmt)

if (GETTEXT_XGETTEXT_EXECUTABLE)

    message(DEBUG " xgettext: ${GETTEXT_XGETTEXT_EXECUTABLE}")
    file(GLOB_RECURSE CPP_FILES RELATIVE ${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/*.cpp)
    add_custom_target(
        pot-update
        COMMENT "pot-update: Done."
        DEPENDS ${CMAKE_SOURCE_DIR}/locale/${OUTPUT_NAME}.pot
    )
    add_custom_command(
        TARGET pot-update
        PRE_BUILD
        COMMAND
            ${GETTEXT_XGETTEXT_EXECUTABLE}
            --from-code=utf-8
            --c++
            --force-po
            --output=${CMAKE_SOURCE_DIR}/locale/${OUTPUT_NAME}.pot
            --keyword=_
            --width=80
            ${CPP_FILES}
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        COMMENT "pot-update: Pot file generated: ${CMAKE_SOURCE_DIR}/locale/${OUTPUT_NAME}.pot"
    )

endif (GETTEXT_XGETTEXT_EXECUTABLE)

if (GETTEXT_MSGMERGE_EXECUTABLE)

    message(DEBUG " msgmerge: ${GETTEXT_MSGMERGE_EXECUTABLE}")

    add_custom_target(
        pot-merge
        COMMENT "pot-merge: Done."
        DEPENDS ${CMAKE_SOURCE_DIR}/locale/${OUTPUT_NAME}.pot
    )

    file(GLOB PO_FILES ${CMAKE_SOURCE_DIR}/locale/*/${OUTPUT_NAME}.po)
    message(TRACE " PO_FILES: ${PO_FILES}")

    foreach(PO_FILE IN ITEMS ${PO_FILES})
        message(STATUS " Adding msgmerge for: ${PO_FILE}")
        add_custom_command(
            TARGET pot-merge
            PRE_BUILD
            COMMAND
                ${GETTEXT_MSGMERGE_EXECUTABLE} ${PO_FILE}
                ${CMAKE_SOURCE_DIR}/locale/${OUTPUT_NAME}.pot
            COMMENT "pot-merge: ${PO_FILE}"
        )
    endforeach()

endif (GETTEXT_MSGMERGE_EXECUTABLE)

if (GETTEXT_MSGFMT_EXECUTABLE)

    message(DEBUG " msgmerge: ${GETTEXT_MSGFMT_EXECUTABLE}")
    file(GLOB PO_LANGS LIST_DIRECTORIES true ${CMAKE_SOURCE_DIR}/locale/*)
    message(TRACE " PO_LANGS: ${PO_LANGS}")

    add_custom_target(
        po-compile
        COMMENT "po-compile: Done."
    )

    foreach(PO_LANG IN ITEMS ${PO_LANGS})
        if(IS_DIRECTORY ${PO_LANG})
        message(STATUS " Adding msgfmt for: ${PO_LANG}")
        add_custom_command(
            TARGET po-compile
            PRE_BUILD
            COMMAND
                ${GETTEXT_MSGFMT_EXECUTABLE}
                --output-file=${OUTPUT_NAME}.mo
                ${OUTPUT_NAME}.po
            WORKING_DIRECTORY ${PO_LANG}
            COMMENT "po-compile: ${PO_LANG}"
        )
        endif()
    endforeach()

endif (GETTEXT_MSGFMT_EXECUTABLE)