Вопрос или проблема
Меня попросил @terdon опубликовать этот последующий пост к более специфической проблеме, с которой я столкнулся и решил здесь.
Очевидно, мой выбор инструментов оказался не самым удачным, поэтому я опишу случай использования в деталях и спрошу:
Какой набор CLI инструментов предоставляет наиболее краткое, но в то же время поддерживаемое решение для вывода конфликтов слияния msgcat в журнал IDE и CI инструментов, так чтобы разработчики, не знакомые с gettext или, скажем, обычным git, имели возможность на них реагировать?
… Довольно многословно, не так ли? Как часто бывает в “созревших” программных проектах, это вовсе не простая задача, даже если она действительно должна быть.
Проблема: мне нужно разрабатывать собственное обнаружение конфликтов и ведение журнала, потому что…
- “Никто” не кажется осведомлённым о GettextResourceManager, поэтому в моём текущем проекте мы используем
Mono.Unix.Catalog
, который поддерживает загрузку только одного каталога на приложение. Этот проект также имеет много старых компонентов glade-2, которые сами не поддерживают несколько каталогов (нет способа указать им, в каком каталоге искать, они всегда обращаются к текущему связываемому).
Так что в этом проекте все .po файлы всех зависимостей объединяются вместе с помощью msgcat в один большой каталог. - msgcat, с учётом версии gettext 0.21 для Debian 12, не предоставляет никаких средств для выражения конфликтов слияния через код завершения или вывод stdout/stderr. Конфликты записываются в результирующий .po файл и выглядят как конфликты слияния git (или svn конфликты слияния). Загрузка .mo файла, сгенерированного из такого конфликтного .po файла, приводит к тому, что либо конфликтные метки попадают в GUI (с
--use-fuzzy
), либо перевод полностью отсутствует, но без сообщения об ошибке. - msguniq якобы является родным инструментом gettext для выполнения проверки на дубликаты – но он работает только с одним .po файлом. Вызов его на выводе от msgcat (с присутствующими конфликтующими строками), однако, не даёт никакого вывода, поэтому я не имею понятия, как им следует пользоваться:
msguniq --repeated <merged .po file with conflicts>
- msgcomm может находить msgids, используемые в нескольких .po файлах (приятно…), но совсем не учитывает, конфликтуют они или нет (… – но бесполезно):
msgcomm --more-than=1 file1.po file2.po [...]
Ранее я справлялся с этой проблемой, передавая --use-first
в msgcat, но это имеет значительный недостаток, поскольку является полностью молчаливым (по дизайну): Необходимость устранить дублирование msgid, используемого в разных источниках, вполне вероятна (так как естественные языки неточны), но мы можем заметить, что это произошло, только в ручном тесте – или в производстве. Более того, поскольку это может происходить для разных сообщений на разных языках, один человек полностью неспособен тестировать это.
Поэтому вместо --use-first
я хочу действительно выдать ошибку, когда происходит конфликт слияния, так чтобы у разработчика была реалистичная возможность проверить и исправить (либо копированием одного текста в другой, либо удалением дубликатов msgid).
Для этого возникает несколько ограничений:
- Указатель конфликтов msgcat имеет форму
"#-#-#-#-#
имя исходного файла
(
Project-Id-Version)
если установлено в исходном файле
#-#-#-#-#\n"
например,"#-#-#-#-# pt.po (My Library Catalogue) #-#-#-#-#\n"
или"#-#-#-#-# pt.po #-#-#-#-#\n"
для исходного файла ‘pt.po’ без или с пустым Project-Id-Version. - Запись метаданных
msgid ""
всегда конфликтует, если в любом исходном файле установлено значимое значение Project-Id-Version. (В более общем плане, если различаются какие-либо поля метаданных; семейство расширенийX-Poedit-*
обеспечивает другие вероятные кандидаты.)
Так что мы хотим пропустить запись метаданных и только обращаться за помощью в случае наличия других конфликтов: отчет о них не полезен. - .po файлы должны быть названы в соответствии с переводом, который они предоставляют, например,
de.po
,pt.po
.
(Довольно раздражающая конвенция в этом проекте, не ошибка gettext и т. д.) - gettext не является основным (например, реализации Mono и glade, которые вызвали этот беспорядок), поэтому сообщение об ошибке должно быть понятным для разработчиков, которые никогда о нём не слышали, например, быть похожим на ошибку компилятора и иначе как можно более чистым.
(Это немного субъективно, но#-#-#-#-#
выглядит не чисто – он очень “визуально загружен”, особенно посреди длинного журнала с многими другими выходами инструментов.) - bash и многие из номинально общих CLI инструментов также не являются основными на практике, поэтому мы должны как-то одновременно сохранять скрипт кратким и некриминальным… Я с радостью признаю, что я сам не достаточно знаком с Linux, чтобы добиться обеих целей одновременно.
(Например, “все знают” grep, но sed уже довольно архаичен для многих людей, а awk – любой awk – рискует вызвать рефлексивный технический паралич. … Замечание: это может не быть так для вас, но это, безусловно, мой опыт с всеми и каждым людьми, с которыми я сотрудничал, включая будь-то длительных натуралов Linux. Да, это действительно так!!!)
Вот два минимальных тестовых файла, которые при объединении вызовут конфликт:
- library/pt.po
msgid ""
msgstr "Project-Id-Version: My Library Catalogue\n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Done"
msgstr "Feito"
msgid "Does not conflict."
msgstr ""
- application/pt.po
msgid ""
msgstr "Project-Id-Version: My Application Catalogue\n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Done"
msgstr "Pronto"
msgid "Does not conflict."
msgstr ""
Для этих файлов моё текущее решение выводит:
$ bash postbuild.sh
Found localisation: application/pt.po
Found localisation: library/pt.po
Creating ./pt/LC_MESSAGES/Domain.mo
Merge conflicts found in './pt/LC_MESSAGES/Domain.po':
22 from library 'My Application Catalogue'
24 from library 'My Library Catalogue'
Последние три строки являются отчётом о конфликте. У него, безусловно, есть недостатки:
- нет исходного файла
- нет визуального разделителя между конфликтами
но я считал мой скрипт уже слишком перегруженным, чтобы попытаться сделать больше.
Моё текущее решение:
На каждое приложение существует один файл postbuild.sh, который собирает соответствующие .po файлы в массив SOURCES
, а затем вызывает скрипт слияния:
#!/bin/bash
SOURCES=(application/pt.po library/pt.po)
source compile_mo_files.sh; compile_mo_files SOURCES . true
Я вызываю их в PostBuildEvent
MSBuild, так что все приложения создают свою локализацию при построении.
Скрипт слияния compile_mo_files.sh:
#!/bin/bash
# рекурсивно ищет в переданных SOURCE_DIRECTORIES файлы .po, выводит каждый найденный файл,
# msgcat их все вместе и msgfmt генерирует единственный файл 'Domain.mo' из них
compile_mo_files()
{
# параметры https://mywiki.wooledge.org/BashFAQ/048#The_problem_with_bash.27s_name_references
if [[ $1 != SOURCE_DIRECTORIES ]]; then
local -n SOURCE_DIRECTORIES=$1 # массив директорий, содержащих .po файлы
fi
if [[ $2 != OUTPUT_DIRECTORY ]]; then
local OUTPUT_DIRECTORY=$2 # путь, в котором будут созданы отдельные папки языков
fi
if [ -z "${OUTPUT_DIRECTORY}" ]; then
OUTPUT_DIRECTORY='.'
fi
if [[ $3 != FAIL_ON_GETTEXT_ISSUES ]]; then
# если установить в любое истинное значение,
# - вызывать msgcat так, чтобы дублированные msgid создавали конфликты слияния,
# - проверять предупреждения msgcat
# и прекращать, если какие-либо найдены после
local FAIL_ON_GETTEXT_ISSUES=$3
fi
# собрать .po файлы
shopt -s lastpipe
declare -A POS
for SOURCE in "${SOURCE_DIRECTORIES[@]}"; do
PREFIX=$(dirname "$(dirname "$SOURCE")")/
find "$SOURCE" -name "*.po" |
{
while read -r PO; do
LOCALISATION=$OUTPUT_DIRECTORY/$(basename "$PO" .po)/LC_MESSAGES
MO=$LOCALISATION/Domain.mo
echo Found localisation: "${PO#"$PREFIX"}"
mkdir -p "$LOCALISATION"
POS["$MO"]="${POS[$MO]}"" $PO"
done
}
done
# объединить .po файлы, сгенерировать .mo файлы
if [ ! "$FAIL_ON_GETTEXT_ISSUES" ]; then
# Обходной путь: Правильное решение заключалось бы в создании одного .mo на .po и их загрузке под разными доменами,
# но Mono.Unix.Catalog не поддерживает опрос нескольких доменов.
# Но при слиянии возникает множество "fuzzy", например для msgid "".
# FIXME:
# Реализовать или найти собственный/правильный обёртку для intl, переключиться на один .mo на каждый язык на проект!
# https://www.gnu.org/software/gettext/manual/html_node/C_0023.html#C_0023-1 вероятно, было бы лучшим.
# Взял на себя смелость добавить в 'builddeps/gettext/gettext-runtime/intl-csharp', но glade не нравится,
# поэтому мы, вероятно, застряли с этим обходным путем для проектов с пред-Avalonia GUI.
# Помещая свои эксперименты в 'proper-intl' ветки для справки.
# -Zsar 2024-10-17
MSGCAT_WORKAROUND='--use-first'
fi
EXIT_STATUS=0
for MO in "${!POS[@]}"; do
echo Creating "$MO"
PO=${MO/.mo/.po}
# FIXME: вывод msgcat потерян!
# К сожалению, msgcat практически никогда не выдаёт код состояния, отличный от нуля, поэтому мы вынуждены делать это вручную. -Zsar 2025-01-06
WARNINGS=$({ msgcat ${MSGCAT_WORKAROUND:+"$MSGCAT_WORKAROUND"} --no-wrap -o "$PO" ${POS[$MO]} 1>/dev/null; } 2>&1)
msgfmt --use-fuzzy -o "$MO" "$PO" # --use-fuzzy чтобы мы могли предварительно перевести pt и т. д. с DeepL и всё ещё отмечать это как "пожалуйста, проверьте" для внешних переводчиков
# проверка предупреждений msgcat
if [ "$WARNINGS" ]; then
printf 'предупреждения msgcat должны быть исправлены, чтобы избежать неожиданных результатов:\n%s\n' "$WARNINGS"
EXIT_STATUS=2 # продолжить выводить _все_ проблемы в журнал; 3 хуже чем 2, так что не страшно перезаписывать позже -Zsar 2025-01-06
fi
# проверка на конфликты слияния
if [ "$FAIL_ON_GETTEXT_ISSUES" ]; then
# Замечание: awk (или более общие POSIX расширенные регулярные выражения) _не поддерживают_ неконфликтные группы
# и будут творить ерунду, если вы их используете! см. https://stackoverflow.com/a/57059535
# Поэтому мы выводим содержимое второй группы вместо создания первой группы неконфликтной.
# -Zsar 2025-01-06
CONFLICT_MARKER='#-#-#-#-#\s+\S+\s+(\(([^()]+)\)\s+)?#-#-#-#-#'
METADATA_LINE_NUMBER=$(sed -n '/^$/{=;q}' "$PO")
CONFLICTS=$(gawk "NR > $METADATA_LINE_NUMBER && /$CONFLICT_MARKER/ && match(\$0, /$CONFLICT_MARKER/, library_name) { print NR, \"from library \047\"library_name[2]\"\047\" }" "$PO")
if [ "$CONFLICTS" ]; then
printf "Найдены конфликты слияния в '%s':\n%s\n" "$PO" "$CONFLICTS"
EXIT_STATUS=3
fi
fi
done
shopt -u lastpipe
exit $EXIT_STATUS
}
Чтоб вы знали: проект, возможно, избавится от glade-2 и, следовательно, от необходимости объединять эти .po файлы, но я не собираюсь этого видеть. Однако это причина, по которой я не стремлюсь, например, бросать вызов конвенции именования .po файлов – это в будущем просто утратит актуальность.
(Например, с момента публикации оригинального вопроса, мы по крайней мере перешли с Debian 10 на 12, но существует так много всего, чего ещё предстоит сделать, что это, вероятно, займёт годы. Следующим шагом будет переход от Mono-совместимого .NET 4.7.2.)
Ответ или решение
В свете задачи, представленной выше, проблема заключается в необходимости интеграции и обработки конфликтов, возникающих при слиянии файлов перевода .po, используя инструменты командной строки в Unix-системе. Рассмотрим, как можно структурировать процесс обнаружения и логирования конфликтов с помощью Bash и других CLI инструментов, чтобы обеспечить ясность и доступность решаемого вопроса для разработчиков, незнакомых с gettext или git.
Теория
При работе с несколькими источниками переводов в системе, использующей Mono.Unix.Catalog, возникает необходимость объединения всех файлов .po в единый каталог. msgcat — это утилита, предназначенная для таких целей, но она имеет ограничения: она не сигнализирует о конфликтах слияния через коды выхода или вывод в стандартный поток ошибок. Конфликты фиксируются в результирующем .po файле в виде маркеров конфликта, аналогичных маркерам в git, что требует ручной проверки и исправления.
Ключевая задача состоит в создании автоматизированного процесса, который бы идентифицировал такие конфликты и информировал о них в консоли в понятном и структурированном виде. Это позволит своевременно исправлять ошибки переводов, минимизируя их появление в продуктивной среде.
Пример
Рассмотрим пример: имеются два файла перевода, которые необходимо слить. Оба файла содержат различающиеся переводы для фразы "Done". После слияния с помощью msgcat файл объединенного .po сохраняет оба варианта, сопровождая их маркерами конфликта:
msgid "Done"
#-#-#-#-# library/pt.po (My Library Catalogue) #-#-#-#-#
msgstr "Feito"
#-#-#-#-# application/pt.po (My Application Catalogue) #-#-#-#-#
msgstr "Pronto"
Использование msguniq или msgcomm в данной ситуации оказывается неэффективным из-за ограничений их функционала, поскольку они либо работают только с одиночными файлами, либо не учитывают наличие конфликтов.
Применение
Чтобы эффективно решать проблему, можно использовать комбинацию Bash и инструментов командной строки для автоматизации поиска и логирования конфликтов. Рассмотрим улучшенный подход, который можно внедрить для достижения этой цели:
-
Создание сценария для слияния и проверки файлов .po. Основная логика заключается в использовании grep и awk для сканирования файла на наличие маркеров конфликта после выполнения msgcat. Таким образом, можно извлечь необходимые строки и обработать их для дальнейшего логирования.
-
Обработка метаданных и конфликтующих строк. Чтобы избежать ложных срабатываний на метаданные, необходимо учитывать специфическую структуру .po файлов. Это требует сканирования файлов после первой пустой строки (где заканчиваются метаданные) и акцентирования внимания на реальных текстовых конфликтах.
-
Пользовательский вывод ошибок. Вывод сообщений об ошибках необходимо организовать так, чтобы они были понятны разработчикам. Логирование должно включать: номер строки, файл источника и дополнительную информацию о каталоге, чтобы обеспечивать контекст для исправления.
Ниже пример улучшенного скрипта, который реализует вышеописанные принципы:
#!/bin/bash
# Функция для идентификации и логирования конфликтов в .po файле
log_conflicts() {
local po_file=$1
local conflicts
# Исключаем метаданные, проверяя только строки после первой пустой строки
local metadata_line_number=$(sed -n '/^$/{=;q}' "$po_file")
# Используем awk для поиска конфликтных маркеров
conflicts=$(awk -v metadata_line="$metadata_line_number" '
NR > metadata_line && /#-#-#-#-#/ {
if (match($0, /#-#-#-#-#\s+\S+(\s+\([^()]+\))?\s+#-#-#-#-#/, groups)) {
library = (groups[2] ? groups[2] : "Unknown")
print "Строка " NR ": конфликт из " library
}
}' "$po_file")
if [ "$conflicts" ]; then
printf "Обнаружены конфликты в '%s':\n%s\n" "$po_file" "$conflicts"
return 1
fi
return 0
}
# Основная функция для слияния и обработки .po файлов
merge_po_files() {
local sources=("$@")
local merged_po="merged_output.po" # Файл, в который будет производиться слияние
# Используем msgcat для объединения файлов
msgcat "${sources[@]}" -o "$merged_po"
# Проверяем на наличие конфликтов
if ! log_conflicts "$merged_po"; then
echo "Конфликты были обнаружены в процессе слияния. Пожалуйста, исправьте их вручную."
else
echo "Слияние выполнено успешно, конфликтов не обнаружено."
fi
}
# Пример использования
merge_po_files library/pt.po application/pt.po
Этот скрипт демонстрирует, как можно эффективно использовать инструменты командной строки для обнаружения и логирования конфликтов в ясной и доступной форме. Такой подход значительно облегчает выявление проблем и расположение сообщений о них в консоли, что упрощает процесс поддержки и коррекции для разработчиков, незнакомых с внутренними механизмами gettext.