Saturday, October 29, 2022

[SOLVED] How to localize c++/cmake program using GNU gettext

Issue

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.



Answered By - KamilCuk
Answer Checked By - Marilyn (WPSolving Volunteer)