Search code examples
c++cmakegettext

How to localize c++/cmake program using GNU gettext


Could you provide an example of GNU gettext usage to localize the following program


#include <iostream>
#include <string>

#include <libintl.h>

int main() {

  std::string name = "foo";

  std::cout << gettext(name) << "\n";

  return 0;
}

assuming the following project structure?

project/
  main.cpp
  CMakeLists.txt
  lang/
    en_US.po
    de_DE.po
    fr_FR.po
    ru_RU.po
    ...

Existing guides are almost explicitly in C and really obscure. Also it is really unclear how to generate .pot files with the given project structure

Edit

$ xgettext will generate empty file in this case. Workaround:

// in main()
std::string_view name = gettext("foo");
std::cout << name << "\n";

Solution

  • how to generate .pot files

    You write them by hand or generate with msginit. It's the same as .po, just a different name for documentation.

    Could you provide an example of GNU gettext usage to localize the following program

    The following:

    cat >./CMakeLists.txt <<EOF
    cmake_minimum_required(VERSION 3.11)
    project(trans)
    include(CTest)
    add_executable(main main.cpp)
    file(GLOB ffs "lang/*.po")
    set(TEXTDOMAIN myprogram)
    make_directory(${CMAKE_CURRENT_BINARY_DIR}/locale)
    foreach(ff IN LISTS ffs)
      get_filename_component(lang ${ff} NAME_WE)
      make_directory(${CMAKE_CURRENT_BINARY_DIR}/locale/${lang})
      make_directory(${CMAKE_CURRENT_BINARY_DIR}/locale/${lang}/LC_MESSAGES)
      add_custom_command(
        OUTPUT
          ${CMAKE_CURRENT_BINARY_DIR}/locale/${lang}/LC_MESSAGES/${TEXTDOMAIN}.mo
        DEPENDS
          ${ff}
        COMMAND msgfmt -o
          ${CMAKE_CURRENT_BINARY_DIR}/locale/${lang}/LC_MESSAGES/${TEXTDOMAIN}.mo
          ${ff}
      )
      add_custom_target(gen_${lang} ALL DEPENDS
        ${CMAKE_CURRENT_BINARY_DIR}/locale/${lang}/LC_MESSAGES/${TEXTDOMAIN}.mo
      )
      add_test(NAME ${lang} COMMAND main)
      set_property(TEST ${lang} APPEND PROPERTY ENVIRONMENT
        TEXTDOMAINDIR=${CMAKE_CURRENT_BINARY_DIR}/locale
        LANGUAGE=${lang}
      )
      set_tests_properties(${lang} PROPERTIES
        PASS_REGULAR_EXPRESSION "${lang} foo"
      )
    endforeach()
    EOF
    

    cat >./main.cpp <<EOF
    #include <iostream>
    #include <string>
    #include <libintl.h>
    #include <cstdlib>
    #include <clocale>
    int main() {
            setlocale(LC_ALL, "");
            const char *textdomainstr = "myprogram";
            const char *textdomaindir = getenv("TEXTDOMAINDIR");
            if (textdomaindir) {
                    bindtextdomain(textdomainstr, textdomaindir);
            }
            textdomain(textdomainstr);
            //
            std::string name = "foo";
            std::cout << gettext(name.c_str()) << "\n";
    }
    EOF
    

    cat >./lang/de_DE.po <<EOF
    msgid ""
    msgstr ""
    "Project-Id-Version: PACKAGE VERSION\n"
    "Last-Translator: Automatically generated\n"
    "Language-Team: none\n"
    "Language: de\n"
    "MIME-Version: 1.0\n"
    "Content-Type: text/plain; charset=UTF-8\n"
    "Content-Transfer-Encoding: 8bit\n"
    "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
    
    msgid "foo"
    msgstr "de_DE foo"
    
    EOF
    
    cat >./lang/en_US.po <<EOF
    msgid ""
    msgstr ""
    "Project-Id-Version: PACKAGE VERSION\n"
    "Last-Translator: Automatically generated\n"
    "Language-Team: none\n"
    "Language: en\n"
    "MIME-Version: 1.0\n"
    "Content-Type: text/plain; charset=UTF-8\n"
    "Content-Transfer-Encoding: 8bit\n"
    "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
    
    msgid "foo"
    msgstr "en_US foo"
    
    EOF
    
    cat >./lang/pl_PL.po <<EOF
    msgid ""
    msgstr ""
    "Project-Id-Version: PACKAGE VERSION\n"
    "Last-Translator: Automatically generated\n"
    "Language-Team: none\n"
    "Language: pl\n"
    "MIME-Version: 1.0\n"
    "Content-Type: text/plain; charset=UTF-8\n"
    "Content-Transfer-Encoding: 8bit\n"
    "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
    
    msgid "foo"
    msgstr "pl_PL foo"
    
    EOF
    

    Results in:

    + cmake -H. -B./_build --no-warn-unused-cli -DCMAKE_VERBOSE_MAKEFILE=1 -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -DCMAKE_C_FLAGS=-Wall -ggdb3 -Wno-unused-function -fsanitize=address -fsanitize=undefined -fsanitize=pointer-compare -fsanitize=pointer-subtract -DCMAKE_CXX_FLAGS=-Wall -ggdb3 -Wno-unused-function -fsanitize=address -fsanitize=undefined -fsanitize=pointer-compare -fsanitize=pointer-subtract -DCMAKE_RUNTIME_OUTPUT_DIRECTORY=bin -DCMAKE_LIBRARY_OUTPUT_DIRECTORY=lib -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY=lib -G Ninja
    Not searching for unused variables given on the command line.
    -- The C compiler identification is GNU 12.2.0
    -- The CXX compiler identification is GNU 12.2.0
    -- Detecting C compiler ABI info
    -- Detecting C compiler ABI info - done
    -- Check for working C compiler: /usr/bin/cc - skipped
    -- Detecting C compile features
    -- Detecting C compile features - done
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Check for working CXX compiler: /usr/bin/c++ - skipped
    -- Detecting CXX compile features
    -- Detecting CXX compile features - done
    -- Configuring done
    -- Generating done
    -- Build files have been written to: /dev/shm/.1000.home.tmp.dir/_build
    + cmake --build ./_build --parallel --verbose
    [1/5] cd /dev/shm/.1000.home.tmp.dir/_build && msgfmt -o /dev/shm/.1000.home.tmp.dir/_build/locale/de_DE/LC_MESSAGES/myprogram.mo /dev/shm/.1000.home.tmp.dir/lang/de_DE.po
    [2/5] cd /dev/shm/.1000.home.tmp.dir/_build && msgfmt -o /dev/shm/.1000.home.tmp.dir/_build/locale/en_US/LC_MESSAGES/myprogram.mo /dev/shm/.1000.home.tmp.dir/lang/en_US.po
    [3/5] cd /dev/shm/.1000.home.tmp.dir/_build && msgfmt -o /dev/shm/.1000.home.tmp.dir/_build/locale/pl_PL/LC_MESSAGES/myprogram.mo /dev/shm/.1000.home.tmp.dir/lang/pl_PL.po
    [4/5] /usr/bin/c++   -Wall -ggdb3 -Wno-unused-function -fsanitize=address -fsanitize=undefined -fsanitize=pointer-compare -fsanitize=pointer-subtract -MD -MT CMakeFiles/main.dir/main.cpp.o -MF CMakeFiles/main.dir/main.cpp.o.d -o CMakeFiles/main.dir/main.cpp.o -c /dev/shm/.1000.home.tmp.dir/main.cpp
    [5/5] : && /usr/bin/c++ -Wall -ggdb3 -Wno-unused-function -fsanitize=address -fsanitize=undefined -fsanitize=pointer-compare -fsanitize=pointer-subtract  CMakeFiles/main.dir/main.cpp.o -o bin/main   && :
    + cd ./_build && ctest -V 
    UpdateCTestConfiguration  from :/dev/shm/.1000.home.tmp.dir/_build/DartConfiguration.tcl
    Parse Config file:/dev/shm/.1000.home.tmp.dir/_build/DartConfiguration.tcl
    UpdateCTestConfiguration  from :/dev/shm/.1000.home.tmp.dir/_build/DartConfiguration.tcl
    Parse Config file:/dev/shm/.1000.home.tmp.dir/_build/DartConfiguration.tcl
    Test project /dev/shm/.1000.home.tmp.dir/_build
    Constructing a list of tests
    Done constructing a list of tests
    Updating test list for fixtures
    Added 0 tests to meet fixture requirements
    Checking test dependency graph...
    Checking test dependency graph end
    test 1
        Start 1: de_DE
    
    1: Test command: /dev/shm/.1000.home.tmp.dir/_build/bin/main
    1: Working Directory: /dev/shm/.1000.home.tmp.dir/_build
    1: Environment variables: 
    1:  TEXTDOMAINDIR=/dev/shm/.1000.home.tmp.dir/_build/locale
    1:  LANGUAGE=de_DE
    1: Test timeout computed to be: 1500
    1: de_DE foo
    1/3 Test #1: de_DE ............................   Passed    0.01 sec
    test 2
        Start 2: en_US
    
    2: Test command: /dev/shm/.1000.home.tmp.dir/_build/bin/main
    2: Working Directory: /dev/shm/.1000.home.tmp.dir/_build
    2: Environment variables: 
    2:  TEXTDOMAINDIR=/dev/shm/.1000.home.tmp.dir/_build/locale
    2:  LANGUAGE=en_US
    2: Test timeout computed to be: 1500
    2: en_US foo
    2/3 Test #2: en_US ............................   Passed    0.01 sec
    test 3
        Start 3: pl_PL
    
    3: Test command: /dev/shm/.1000.home.tmp.dir/_build/bin/main
    3: Working Directory: /dev/shm/.1000.home.tmp.dir/_build
    3: Environment variables: 
    3:  TEXTDOMAINDIR=/dev/shm/.1000.home.tmp.dir/_build/locale
    3:  LANGUAGE=pl_PL
    3: Test timeout computed to be: 1500
    3: pl_PL foo
    3/3 Test #3: pl_PL ............................   Passed    0.01 sec
    
    100% tests passed, 0 tests failed out of 3
    
    Total Test time (real) =   0.04 sec
    

    how to generate .pot files with the given project structure

    I always understood that .pot files are .po files. They are the same. You write them, or let msginit generate them from a template file, where this template file was also written by you, or generated by some script. I understood it's a hint in documentation like msgmerge that those are different files, like .pot has only one translation and msgmerge updates it in .po file.

    You have to generate .mo files in proper <lang>/LC_MESSAGES/<textdomain>.mo directory and name structure.