Разбор конфликтов слияния msgcat в “хорошо выглядящие” ошибки консоли с использованием bash и различных инструментов командной строки.

Вопрос или проблема

Меня попросил @terdon опубликовать этот последующий пост к более специфической проблеме, с которой я столкнулся и решил здесь.

Очевидно, мой выбор инструментов оказался не самым удачным, поэтому я опишу случай использования в деталях и спрошу:
Какой набор CLI инструментов предоставляет наиболее краткое, но в то же время поддерживаемое решение для вывода конфликтов слияния msgcat в журнал IDE и CI инструментов, так чтобы разработчики, не знакомые с gettext или, скажем, обычным git, имели возможность на них реагировать?

… Довольно многословно, не так ли? Как часто бывает в “созревших” программных проектах, это вовсе не простая задача, даже если она действительно должна быть.

Проблема: мне нужно разрабатывать собственное обнаружение конфликтов и ведение журнала, потому что…

Ранее я справлялся с этой проблемой, передавая --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 и инструментов командной строки для автоматизации поиска и логирования конфликтов. Рассмотрим улучшенный подход, который можно внедрить для достижения этой цели:

  1. Создание сценария для слияния и проверки файлов .po. Основная логика заключается в использовании grep и awk для сканирования файла на наличие маркеров конфликта после выполнения msgcat. Таким образом, можно извлечь необходимые строки и обработать их для дальнейшего логирования.

  2. Обработка метаданных и конфликтующих строк. Чтобы избежать ложных срабатываний на метаданные, необходимо учитывать специфическую структуру .po файлов. Это требует сканирования файлов после первой пустой строки (где заканчиваются метаданные) и акцентирования внимания на реальных текстовых конфликтах.

  3. Пользовательский вывод ошибок. Вывод сообщений об ошибках необходимо организовать так, чтобы они были понятны разработчикам. Логирование должно включать: номер строки, файл источника и дополнительную информацию о каталоге, чтобы обеспечивать контекст для исправления.

Ниже пример улучшенного скрипта, который реализует вышеописанные принципы:

#!/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.

Оцените материал
Добавить комментарий

Капча загружается...