Как локализовать программу C++/cmake с помощью GNU gettext

Не могли бы вы привести пример использования GNU gettext для локализации следующей программы


#include <iostream>
#include <string>

#include <libintl.h>

int main() {

  std::string name = "foo";

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

  return 0;
}

при следующей структуре проекта?

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

Существующие руководства почти полностью написаны на C и очень неясны. Также действительно непонятно, как генерировать файлы .pot с заданной структурой проекта.

Редактировать

В этом случае $ xgettext сгенерирует пустой файл. Обходной путь:

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

структура каталогов важна. Это en_US/LC_MESSAGES. gettext(name)?? name is a std::string`...

KamilCuk 22.10.2022 07:25
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
85
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

как генерировать файлы .pot

Вы пишете их вручную или генерируете с помощью msginit. Это то же самое, что и .po, просто другое название документации.

Не могли бы вы привести пример использования GNU gettext для локализации следующей программы

Следующий:

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

Результат:

+ 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

как создать файлы .pot с заданной структурой проекта

Я всегда понимал, что .pot файлы — это .po файлы. Они одинаковые. Вы их пишете, или позволяете msginit сгенерировать их из файла шаблона, где этот файл шаблона также был написан вами, или сгенерирован каким-то скриптом. Я понял, что это намек в документации типа msgmerge, что это разные файлы, типа .pot имеет только один перевод и msgmerge обновляет его в .po файле.

Вы должны сгенерировать .mo файлы в правильной <lang>/LC_MESSAGES/<textdomain>.mo директории и структуре имен.

На первый взгляд все в порядке, но этот файл CMake не создает исходный файл /lang/main.pot (или аналогичный); также я борюсь с использованием xgettext, так как он генерирует файл без каких-либо строк для перевода

yukkute 22.10.2022 23:18

Я не понимаю вашего комментария. At a glance it seems fine что на первый взгляд кажется неправильным? CMake file does not generate /lang/main.pot (or similar) initial file ? С чего бы это? Что такое «исходный файл main.pot»? also i'm struggling with xgettext usage, as it generates a file without any strings for translation Конечно, xgettext для gettext("A CONSTANT STRING"), а не для разбора кода C++.

KamilCuk 22.10.2022 23:23

Руководство GNU: «При запуске нового перевода переводчик создает файл с именем LANG.po как копию файла шаблона package.pot с изменениями в начальных комментариях (в начале файла) и в записи заголовка ( первая запись, ближе к началу файла)". Я предполагаю, что CMake может создать этот файл .pot

yukkute 22.10.2022 23:26

Да, вы, как сопровождающий программы, предоставляете файл шаблона другим переводчикам для выполнения переводов. Вы пишете файл вручную или генерируете, например, с помощью xgettext. Это тот же файл, только msgstr "" пустой материал для заполнения. Как github.com/util-linux/util-linux/blob/master/po/util-linux.p‌​ot . Выложенный вами код, как вы заметили, не совместим с xgettext. Измените код на gettext("foo") или напишите свой собственный генератор, или сделайте вручную.

KamilCuk 22.10.2022 23:29

Это то, с чем я борюсь, потому что xgettext у меня не работает, поскольку он генерирует пустой файл шаблона.

yukkute 22.10.2022 23:30

? Да потому что xgettext не для gettext(some code), а для gettext("some constant string"). Это простой грубый парсер регулярных выражений. Действительно, gettext() в любом случае следует всегда использовать с постоянными строками, но в вашем коде было std::string.

KamilCuk 22.10.2022 23:31

Другие вопросы по теме