Генераторы статических сайтов (static site generators, SSGs), такие, как Zola (Rust), Hugo (Golang), Jekyll (Ruby), Pelican (Python), Gatsby (JS) и прочие, активно набирают для создания личных технологических блогов и небольших веб-сайтов. Большинство популярных SSG поддерживают локализацию строк интерфейса, а также дают возможность размещения страниц на нескольких зыках side-by-side. Однако, эффективное поддержание актуальности перевода самого контента на разных языках является весьма непростой задачей, особенно когда контент статей и разделов на сайте обновляется со временем.

В данной статье мы предложим интересный подход, который позволяет не только перевести статический Markdown-контент на все языки мира, но и поддерживать актуальность переводов при изменении исходных текстов. Данный туториал будет на примере Zola, но предлагаемый подход с минимальными изменениями адаптируется практически к любым популярным статическим генераторам.

TL;DR: Markdown (en) -> gettext -> Weblate -> Markdown (ru, de, fr, ...)

Введение

Обычной практикой в большинстве SSGs является добавление локализованных копий контента рядом друг с другом с помощью файлов .<lang-code>.md. Например, чтобы перевести index.md на французский и немецкий языки, вам нужно создать файлы index.fr.md и index.de.md в том же каталоге, что и index.md. На примере Zola:

content/_index.md    <!-- Оригинальный текст на английском
content/_index.de.md <!-- Перевод на немецкий
content/_index.fr.md <!-- Перевод на французский

После этого fr и de локали включаются где-нибудь в файле настроек и контент появляется на сайте. На примере Zola (`config.toml`):

[languages.de]         <!-- Включение немецкого
[languages.de.translations]
summary = "Mein Blog"  <!-- Переводы строк интерфейса

[languages.fr]         <!-- Включение французского
[languages.fr.translations]
summary = "Mon blog"   <!-- Переводы строк интерфейса

Обратите внимание, что перевод различных элементов навигации и строк интерфейса и перевод самого контента - это разные вещи. Скорее всего, все служебные строки уже и так переведены до вас и вам надо будет лишь поменять только название вашего блога или веб-сайта в файле конфигурации или отдельных файлах локализации. Сам же контент размещается отдельно в .<lang-code>.md. Поддержка переводов самого контента и есть основной challenge.

Наивный подход

Первое, что приходит в голову, это взять оригинальный контент в _index.md и скормить его как есть в Google Translate (а, еще лучше, в Deepl), для получения _index.fr.md, _index.de.md. В целом рабочий вариант, то есть нюансы.

Проблема №1. YAML Front Matter

Файлы Markdown, используемые SSGs, обычно имеют так называемый блок YAML Front Matter (YFM), который содержит дополнительные метаданные. Некоторые ключи из этого блока, такие как title и description, должны быть переведены, а некоторые другие, такие как preview_image, переводить как раз таки не нужно:

---
title: Donate to support the development of Organic Maps
description: Your money pays for all project-related expenses and motivates us to improve Organic Maps.
weight: 10
extra:
  menu_title: Donate
  preview_image: donate/donate.png
---

Organic Maps app is _free for everyone_ thanks to your **[donations][donate]**:

- No ads
- No trackers
- No registration
- No push notifications
- Open source

Google Translate выдает примерно следующее:

---
title: Пожертвуйте, чтобы поддержать разработку органических карт
описание: Ваши деньги покрывают все расходы, связанные с проектом, и мотивируют нас улучшать органические карты.
вес: 10
дополнительный:
   menu_title: Пожертвовать
   preview_image: пожертвовать/пожертвовать.png
---

Приложение Organic Maps _бесплатно для всех_ благодаря вашим **[пожертвованиям][пожертвованиям]**:

- Без рекламы
- Нет трекеров
- Нет регистрации
- Нет push-уведомлений
- Открытый источник

Проблема №2. Ручные правки и синхронизация контента

Как можно видеть на примере текста выше, несмотря на весь хайп с развитием AI и прочих ChatGPT, машинные переводы всё еще оставляют желать лучшего. Можно использовать переведенный текст в качестве основы, но всё равно потребуется определенное количество ручных правок и proof-reading прежде чем это можно будет показывать людям.

Теперь представим, что исходный текст тоже поменялся. Что будем делать? Искать поменявшиеся куски текста вручную и пытаться подменить соответствующие блоки в переведенном тексте? В целом это рабочий вариант если языков не много и они относительно понятные. Где-то примерно на добавлении арабского, фарси и иврита энтузиазм начинает угасать. Теперь давайте представим, что у вас 30+ таких языков. Никаких рук не хватит вручную отслеживать где что поменялось.

Решение

Пообщавшись с различными open-source проектами и немного подумав, мы пришли к следующей схеме:

  1. Токенизируем оригинальный Markdown контент в какой-нибудь стандартный формат i18n файлов, используемых для перевода строк приложений (gettext, xliff, JSON localization files, и д.р.);

  2. Подключаем инструмент для управления переводами в i18n файлах, коих много на рынке (Weblate, Transiflex, POEditor, и д.р.);

  3. Генерируем локализованные Markdown файлы путём замены оригинальных строк на локализованные строки из i18n файлов.

Звучит немного cryptic, и, так оно и есть. Но давайте разберемся с деталями.

Перевод статических веб-сайтов при помощи po4a и Weblate
Перевод статических веб-сайтов при помощи po4a и Weblate

Markdown в i18n файлы

Проблема (и преимущество) Markdown в том, что это просто текст. Предположим, что у вас есть несколько файлов на разных языках:

  • index.mdанглийский

  • index.de.md немецкий

  • index.de.md французский

Как будем отслеживать изменение английского текста чтобы изменять текст на немецком или французском? Если тексты имеют разную структуру (параграфы, разделы и т.п.), то по факту это просто будет тройная работа. Наверное, стоит немного пожертвовать гибкостью, но сделать структуру текстов одинаковой, чтобы можно было бы легко находить измененные параграфы на немецком или французском после изменения английского (до тех пор, пока мы не устанем на арабском).

Здесь мы предлагаем следующее решение. Берется Markdown текст и разбивается на отдельные параграфы или предложения (по желанию), которые уже в свою очередь парсятся в файлы локализаций (gettext, xliff, JSON localization files, и д.р.). Вместо одного сплошного потока текста в Markdown имеет уже поток параграфов.

Исходный_index.md:

Organic Maps app is _free for everyone_ thanks to your **[donations][donate]**:

- No ads
- No trackers
- No registration

После разбивки:

1: Organic Maps app is _free for everyone_ thanks to your **[donations][stripe]**:
2: No ads
3: No trackers
4: No registration

Здесь мы не будем изобретать какой-то свой доморощенный формат. Существует множество известных форматов для хранения строк локализации — файлы GNU gettext, XLIFF, JSON i18next и другие. Прелесть стандартных форматов в том, что они поддерживаются всеми известными инструментами и профессионалами перевода. В данном туториале мы будем использовать текстовый формат gettext, широко распространенный в мире open-source:

# Комментарий
msgid оригинальная строка или её id
msgstr перевод

Например, для содержимого Markdown выше этот файл будет выглядеть так:

#. type: Plain text
#: content/donate/index.md
#, markdown-text
msgid "Organic Maps app is _free for everyone_ thanks to your **[donations][stripe]**:"
msgstr ""

#. type: Bullet: '- '
#: content/_index.md content/donate/index.md
#, markdown-text
msgid "No ads"
msgstr ""

#. type: Bullet: '- '
#: content/_index.md
#, markdown-text
msgid "No tracking"
msgstr ""

#. type: Bullet: '- '
#: content/_index.md
#, markdown-text
msgid "No data collection"
msgstr ""

Сам формат, конечно, выглядит, конечно, так себе. Хорошая новость в том, что вам не придется править это руками. Формат gettext более чем стандартный и поддерживаемый десятками различных инструментов. Самое главное, что любые переводчики, которые хоть когда-либо работали с какими-нибудь переводами программного обеспечения, вполне себе точно знают что делать с этим форматом.

Вторая хорошая новость, что задача конвертации различного полуструктурированного текста как Markdown в gettext уже давно решена в мире opensource. Поприветствуем инструмент под названием po4a (gettext .po for anything):

po4a (PO для всего) упрощает перевод документации и ее обслуживание. Он извлекает переводимый материал из исходного документа и помещает его в файл PO, адаптированный к процессу перевода. Как только этот файл PO обновляется переводчиком, po4a повторно вводит перевод в структуру исходного документа для создания переведенного документа.

Поддерживаемые форматы:

  • asciidoc: AsciiDoc format

  • man: Good old manual page format

  • pod: Perl Online Documentation (POD) format

  • xml: generic XML documents

    • docbook: DocBook XML

    • xhtml: XHTML documents

    • dia: uncompressed Dia diagrams

    • guide: Gentoo Linux's XML documentation format

    • wml: WML documents

  • ...

Десятки сотен различных man pages, tutorials и прочей документации в мире open-source переводится через данный инструмент с использованием методики, которую мы предлагаем в этой статье. А именно, можно взять неструктурированный текст, будь то Markdown или man page, разбить его на параграфы, сохранить параграфы в виде GNU text format (.po файлы), а дальше уже работать с переводами отдельных строк с использованием распространенных инструментов.

po4a настраивается через конфигурационный файл.po4a.cfg:

[po4a_langs] id pt-BR es nl eu cs ca bn zh-Hans hu mr pl hi uk de
[po4a_paths] po/content.pot $lang:po/content.$lang.po

[options] opt:"--verbose" opt:"--addendum-charset=UTF-8" opt:"--localized-charset=UTF-8" opt:"--master-charset=UTF-8" opt:"--master-language=en_US" opt:"--porefs=file" opt:"--msgmerge-opt='--no-wrap'" opt:"--wrap-po=newlines"

[po4a_alias:markdown] text opt:"--option markdown" opt:"--option keyvalue" opt:"--option yfm_keys=title,description,menu_title" opt:"--addendum-charset=UTF-8" opt:"--localized-charset=UTF-8" opt:"--master-charset=UTF-8" opt:"--keep=80"

[type: markdown] content/_index.md $lang:content/_index.$lang.md
[type: markdown] content/donate/index.md $lang:content/donate/index.$lang.md
[type: markdown] content/news/_index.md $lang:content/news/_index.$lang.md
[type: markdown] content/privacy/index.md $lang:content/privacy/index.$lang.md
[type: markdown] content/support-us/index.md $lang:content/support-us/index.$lang.md
[type: markdown] content/terms/index.md $lang:content/terms/index.$lang.md
  • [po4a_langs] список языков для переводов;

  • [po4a_paths] путь для сохранения исходных строк (content.pot) и пути для сохранения .po файлов локализации для каждого языка;

  • [options] определяет опции как работать с .po файлами, просто оставьте как мы предлагаем;

  • [po4a_alias:markdown] задает различные опции парсинга Markdown:

    • opt:"--option yfm_keys=title,description,menu_title" говорит po4a о необходимости перевода title, description, menu_title из блока YAML Front Matters;

  • [type: markdown] задает отображение между исходными и генерируемыеми Markdown файлами

    • content/_index.md - исходный файл

    • $lang:content/_index.$lang.md - переведенный файл для языка $lang

После настройки и запуска этой тулзы получаем пачку .po файлов со строками из Markdown.

  • content.pot - исходный текст

  • content.de.po - немецкий

  • content.fr.po - французский

Изначально самих переводов, конечно же, нет:

#. type: Bullet: '- '
#: content/_index.md content/donate/index.md
#, markdown-text
msgid "No ads"
msgstr ""

#. type: Bullet: '- '
#: content/_index.md
#, markdown-text
msgid "No tracking"
msgstr ""

#. type: Bullet: '- '
#: content/_index.md
#, markdown-text
msgid "No data collection"
msgstr ""

Далее разберемся что же делать с этими непонятными .po файлами и как они помогут нам помочь.

i18n файлы в переводы

Вы находитесь здесь:

Markdown -> i18n файлы -> [Переводы]

Формат .po файлов определенно не людей. Но опять же, вы не будете править его руками. Существует множество инструментов для работы с файлами i18n: Weblate, Transifex, POEditor и другие.

В этом руководстве мы будем использовать Weblate (https://weblate.org/), поскольку это один из лучших инструментов на рынке. И он open-source. Weblate удобен для человека и понятен профессионалам в области переводов. Есть много полезных функций:

  • Глоссарии. Можно определить часто используемую терминологию для использования во всех частях проекта (не только на сайте). Одни и те же вещи должны называться одними и теми же словами везде.

  • Память переводов. Можно использовать и обновлять базу переводов экономить время и деньги.

  • Совместная работа. Переводчики могут работать на переводами совместно и делать review переводов коллег.

  • Машинные переводы. Можно подключить Google Translate, Deepl, Microsoft Translator и другие сервисы для машинного перевода. Причем переведенные автоматически строки можно добавлять как suggestions или помечать как "needs edit" для уточнения людьми.

  • Языковая поддержка. Поддержка более 500 языков из коробки. Можно добавить и другие.

  • Контроль версий. Weblate работает непосредственно с git репозиторием. Данные никуда не пропадут и у вас будет понятный и удобный инструмент контроля версий.

  • Удобный UI. Сделано всё максимально понятно для людей, работающих с переводами. Можно работать на декстопе, телефоне, планшете.

Список активных языков для переводов
Список активных языков для переводов
Статистика по строкам
Статистика по строкам
Работа со строками
Работа со строками

Помимо очень даже конкурентноспособного набора возможностей, мы также имели следущие рассуждения при выборе Weblate:

  1. Open-source. Мы также работаем над open-source проектом и предпочитаем использовать в своей работе open-source инструменты. Нет никакого желания быть залоченным на очередной проприетарный сервис, который сегодня работает, а завтра поднимет цены в 1000 раз, добавит тонну ненужных свистелок или же просто уйдет в закат по причине закончившихся инвестиций. Это наш субъективный bias, который можно с нами не разделять.

  2. Сообщество. На hosted.weblate.org обитает огромное сообщество энтузиастов, которые помогают с переводами для различных open-source проектов. Люди из сообщества open-source предпочитают помогает open-source. Мы получаем десятки contributions каждую неделю. Наличие такого сообщества - это то, что было важно для нас. Но это никак не мешает заплатить денег любым профессиональным переводчикам чтобы они перевели файлы в указанном сервисе.

  3. Отличная поддержка. Ребята из Weblate реально зарабатывают деньги на своем проект и живут этим. Можно сколько угодно говорить о стоимости opensource, но мы, как коллеги по opensource цеху, заплатили $0. При этом на все наши запросы нам отвечают максимум в течение одного рабочего дня. Причем зачастую в формате "проблема была, пофикшено в это PR, можете проверять". Рекорд составил 1 час от момента запроса до момента появляется нужной фичи на сайте. Даже платная поддержка от Microsoft и AWS за сотни тысяч долларов в год не работает так быстро и эффективно.

В данном туториале мы будем использовать managed service https://hosted.weblate.org/. Для open-source проектов hosted.weblate.org предоставляется бесплатно. Остальным придется заплатить или поднять и поддерживать такой сервис самостоятельно. Всё честно. Мы категорически рекомендуем Weblate для перевода строк в ваших приложениях и сервисах. Просто не жадничайте и заплатите весьма символические деньги за поддержку сервиса чтобы ребята могли развиваться.

Далее мы будем интегрировать Weblate с git репозиторием. Скорее всего, у вас уже есть репозиторий для вашего статического сайта. Надо добавить туда сгенерированные на предыдущем шаге .po файлы. После этого настроить доступ к репозиторию в Weblate.

Добавление репозитория с GitHub
Добавление репозитория с GitHub
  • Component name: любое название, например, "My Website"

  • URL slug: "my-blog"

  • Version control system: GitHub Pull Requests.

  • Source code repository: GitHub Repository

Weblate просканирует репозиторий на .po файлы и настроет проект автоматически.

Выбираем gettext PO files и директорию, где вы разместили .po файлы
Выбираем gettext PO files и директорию, где вы разместили .po файлы

Остальные настройки в целом можно оставить как есть.

  • File format: gettext PO files

  • File mask: po/content.*.po - маска для поиска .po файлов

  • Language filter: ^[^.]+$

  • Source language: English

  • Monolingual base language file - оставить пустым

  • Template for new translations: po/content.pot

  • Language code style: this defines the format of language codes in [po4a_langs].

После синхронизации репозитория добавляем языки:

Включим несколько add-ons для упрощения работы (Managed -> Addons):

  • Automatic translations Это дополнение можно использовать для автоматического перевода строк в файлах .po с помощью Google Translate, Deepl, Microsoft Translation или других инструментов. Сгенерированные переводы могут отображаться как «Suggestions» или как добавленные с флагом «Needs Edit», что означает, что строка требует проверки человеком.

  • Squash Git commits. Это действительно полезное дополнение для тех, кто предпочитает порядок в истории git.

  • Customize gettext output. Этот addon нужен для интеграции с po4a чтобы Weblate не делал line wrapping в .po файлах. Выбирайте "Long lines wrapping": "No line wrapping". Иначе каждый раз файлы будут переформатироваться.

После интеграции репозитория с Weblate можно делать переводы непосредственно в сервисе. Обычно мы делаем machine translations с флагом "needs edit", после чего строки через какое-то время вычитываются сообществом. В теории вы можете включить хоть все 500 языков, если сможете это перевеварить.

Переводы в i18n файлы

Вы находитесь здесь:

Markdown -> i18n файлы -> Переводы -> [i18n файлы]

С настройками выше Weblate будет создавать (или обновлять существующие) Pull Requests в репозиторий на GitHub каждые 24 часа. Можно не дожидаться 24 часов и нажать кнопку "Push" в разделе "Manage".

Статус синхронизации с git репозиторием.
Статус синхронизации с git репозиторием.

i18n файлы в Markdown

Вы находитесь здесь:

Markdown -> i18n файлы -> Переводы -> i18n файлы -> Переведенный Markdown

Остался буквально последний шаг. Надо из .po файлов переводов сгенерировать переведенные Markdown файлы. Как это сделать кажется не очевидным, но есть простое решение. Достаточно взять оригинальные Markdown файлы и дальше условно простым replace поменять английские строки на переведенные строки из .po файлов. Можно делать неточный (fuzzy) поиск, чтобы повысить процент совпадений.

Идея кажется банальной, но она работает. Структура документов сохраняется (это и плюс и минус). Строки без перевода остаются на английском (это тоже и плюс и минус). Можно настроить порог перевода, после которого будет включаться новый перевод сайта. Наверное, не стоит включать новый язык, если степень перевода строк меньше 60-80%.

Описанный выше подход также реализует po4a, которую мы уже настроили ранее. В конфигурационном файле выше мы задавали mapping между исходными файлами и генерируемыми файлами:

[type: markdown] content/_index.md $lang:content/_index.$lang.md
[type: markdown] content/donate/index.md $lang:content/donate/index.$lang.md
..

Также в конфигурационном файле задается опция opt:"--keep=80", которая как раз задает необходимый процент переводов (80%) для генерации Markdown. Если процент ниже, Markdown просто не генерируется и на сайте не будет страницы с таким переводом. Выбор языков делается в каждом генераторе по разному. В Zola мы настроили редирект на английскую версию при отсутствии локализованной.

Запуск po4a с обновленными .po файлами выглядит примерно так:

content/_index.uk.md is 100% translated (69 strings).
content/donate/index.uk.md is 100% translated (39 strings).
content/news/_index.uk.md is 100% translated (3 strings).
content/privacy/index.uk.md is 100% translated (9 strings).
content/support-us/index.uk.md is 100% translated (19 strings).
content/terms/index.uk.md is 100% translated (13 strings).
Discard content/_index.zh-Hans.md (45 of 69 strings; only 65.21% translated; need 80%).
Discard content/donate/index.zh-Hans.md (15 of 39 strings; only 38.46% translated; need 80%).
Discard content/news/_index.zh-Hans.md (1 of 3 strings; only 33.33% translated; need 80%).
Discard content/privacy/index.zh-Hans.md (1 of 9 strings; only 11.11% translated; need 80%).
Discard content/support-us/index.zh-Hans.md (5 of 19 strings; only 26.31% translated; need 80%).
Discard content/terms/index.zh-Hans.md (9 of 13 strings; only 69.23% translated; need 80%).

В случае Zola также необходимо включать новые языки в файле конфигурации config.toml.

Автоматизация

После последнего шага цикл замкнулся:

Markdown -> i18n файлы -> Переводы -> i18n файлы -> Переведенный Markdown.

Мы были слишком ленивые и написали скрипт, который всё делает автоматически. Скрипт, в том числе, добавляет строки интерфейсов из config.toml в .po файлы для перевода на Weblate. В других SSGs, например в Hugo, данные строки хранятся в отдельных .json файлах, которые можно просто добавить в Weblate без дополнительных телодвижениях. В Zola пока не дошли до этого уровня развития цивилизации.

Вы можете сэкономить время и взять все готовые скрипты из нашего публичного репозитория:

https://github.com/organicmaps/organicmaps.github.io/tree/master/tools

Ограничения

Обратите внимание, что ручная правка текста в предложенном workflow возможна только для исходных Markdown файлов. Переведенные файлы всегда обновляются из исходных файлов. Структура переведенных файлов всегда соответствует исходным файлам. Это значит, что параграфы всегда будут в том же порядке и на тех же местах. Но текст можно писать разный, не обязательно предложение-в-предложение и слово-в-слово.

В целом это те разумные trade off, на которые мы сознательно пошли. Больше гибкости - больше работы при переводе. Если вам надо переводить 2-3 языка, может вам и не нужна такая сложная система . Можно хоть каждый перевод поддерживать вручную с произвольной структурой. Нам надо было 30+ языков. Подумайте, как быстро вы закончитесь поддерживать переводов десяток страниц на 30+ языков?

Результаты

До внедрения данного подхода у нас были переводы на несколько языков (русский, немецкий, французский, итальянский, турецкий). Оригинальный контент менялся достаточно часто, что привело к полной рассинхронизации структуры в переводах. Где-то было написано одно, где-то совсем другое, всё это было весьма трудно поддерживать. Не говоря уже о том, что люди, которые делают переводы, не обязательно программисты и совсем не обязаны знать как использовать git чтобы править какие-то там Markdown. Что еще и почти невозможно делать с мобилки где-нибудь в метро.

Мы провели пилотное тестирование предложенного подхода и получили весьма позитивный feedback от сообщества. Практически все основные контрибьюторы переводов пожелали перейти на Weblate. Мы оставили несколько понятных нам языков в режиме сырого Markdown для удобства экспериментирования с wording и текстами. Остальное было переведено на po4a + Weblate. Уже за несколько месяцев после список языков на веб-сайта увеличился существенно:

Català Čeština Deutsch Español Euskara Français हिंदी Magyar Bahasa Indonesia Italiano मराठी Dutch Polski Português (Brazil) Русский Svenska Türkçe Українська 英语

Сейчас мы можем легко добавлять новые языки на сайт. Достаточно кликнуть кнопку "Add Translations" и дальше постепенно переводить параграф за параграфом, пока уровень переводов не достигнет необходимого threshold 80%. После этого появляются Markdown файлы и кнопка с новым языком на веб-сайте. Можно использовать созданную систему для переводов инструкций пользователей и новостей. Поддержка данной системы не отнимает особо много времени за счет практически полной автоматизации. В целом можно констатировать успех.

Комментарии (0)