Как настроить библиотеку Babel для больших проектов i18n, чтобы включить псевдотрансляцию и итеративную локализацию

В Metamova мы много работаем над локализацией, и многое из того, что я делаю на работе на регулярной основе, - это обеспечение того, чтобы любая работа, которую выполняли переводчики, действительно заканчивалась в любой доступной для человека форме. К сожалению, иногда наши исходные файлы настолько странные, что я не могу ничего исправить на этапе разработки локализации. Особенно, если речь идет о продуктах, которые нуждаются в постоянной поддержке, например веб-сайтах и ​​приложениях. Обычно причина, по которой мы не можем помочь, заключается в том, что этап перед локализацией - интернационализация - был выполнен неправильно или вообще не был выполнен.

Вот мой небольшой проект, в котором я интернационализировал свой веб-сайт, как будто он будет переведен на дюжину языков.

Я создал веб-сайт своей компании с помощью Flask. Отчасти потому, что я верен Python, отчасти потому, что на данный момент я не изучаю PHP, но главным образом потому, что с Flask просто весело работать.

В любом случае, для перевода Flask использует библиотеку Babel. У меня много проблем с Babel, и я хотел бы переписать многие его части, но его хорошие части слишком хороши. В Babel есть большая коллекция файлов данных, которые содержат спецификации дат, валюты, шрифтов и т. Д. Для локалей 735. Это очень впечатляет и очень полезно.

Babel извлекает ваш переводимый контент в PO-файлы, в которых вы можете управлять своими переводами, а затем указывает Flask отображать строки в соответствии с тем, какой языковой стандарт возвращает браузер пользователя.

Чтобы Babel знал, какой контент можно переводить, вы должны добавить в свой проект файл конфигурации Babel. Файл конфигурации содержит несколько предопределенных фильтров, которые сообщают Babel, где искать человеческий текст в вашем шаблоне HTML. Как правило, вам нужно перейти к своим HTML-шаблонам и обернуть весь отображаемый текст следующим образом:

{{ _('Some text here') }}

Двойные фигурные скобки сообщают движку веб-шаблонов Jinja, что это не HTML, а какое-то исполняемое выражение. Подчеркивание и обычные фигурные скобки говорят Babel, что все в кавычках, которые следуют ниже, является текстом для перевода.

Вот безумная идея, которая значительно упростит вам процесс интернационализации: не помещайте настоящий человеческий текст в HTML-код.

Думаю об этом. Для огромного международного сайта это не имеет смысла. Если ваш продукт является веб-сайтом (то есть это интернет-магазин, а не просто веб-страница портфолио), и ваш продукт является международным, некоторые страницы на вашей платформе обязательно будут выглядеть по-разному для разных регионов. У вас также есть разные функции и рекламные акции для разных стран, И если ваш продукт продолжает расти и меняться, контент будет меняться так быстро, что нет смысла постоянно обновлять HTML-код или переписывать его с новым контентом. Вы действительно рискуете создать такой беспорядок, что даже не сможете отслеживать разные версии исходных строк.

Более того, ваш исходный язык может измениться. У вас могут быть разные исходные языки, в зависимости от региона, на который вы переводите, его культуры и рынка. Например, если вы уже перевели с английского на немецкий, возможно, было бы разумнее сделать немецкий в качестве исходного языка для вашего скандинавского расширения. В Babel не может быть нескольких исходных языков, если исходный язык уже присутствует в шаблоне HTML. Это противоречит логике замысла Вавилона. Итак, давайте немного подправим дизайн Babel. Вот как.

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

До:

<li class="nav-item">
    <a class="nav-link active" href="#" data-scroll-nav="0">
        {{ _('Home') }}
    </a>
</li>

После:

<li class="nav-item">
    <a class="nav-link active" href="#" data-scroll-nav="0">
        {{ _('nav.item_1') }}
    </a>
</li>

Теперь функция Babel будет рассматривать идентификатор как источник и экспортировать его в файл PO, где вы можете добавить соответствующую строку в качестве перевода:

#: app/templates/metamova.html:78
msgid "nav.item_1"
msgstr "Home"

Здесь я хотел бы упомянуть, что я вижу очень много проблем с форматом PO, и его дизайн не имеет для меня особого смысла. Например, по умолчанию он обрабатывает строку на естественном языке как уникальный идентификатор (худшая идея), довольно сложно добавить дополнительную информацию о строках в структурированном виде, логика управления версиями излишне сложна и заставила меня перезаписать существующие переводы с пустыми строками несколько раз, прежде чем я понял, как работать с этими странными файлами. Однако, несмотря на все его недостатки, PO - единственный формат в индустрии переводов, который, в отличие от XLIFF, предлагает некоторую совместимость. Вот почему я предполагаю, что Бабель его использует. Итак, давайте рассмотрим то, что, на мой взгляд, является самым крутым способом использовать все функциональные возможности Babel, делая ваш процесс перевода максимально эффективным.

Когда вы инициализируете новую локаль в Babel, вы получаете новый PO-файл. По нашей логике, наши идентификаторы - это наши исходные строки. Таким образом, у вас будут PO-файлы для каждой локали, все содержащие идентификаторы, ожидающие подключения к вашему переведенному контенту.

Используя встроенный PO-синтаксический анализатор Babel, теперь мы можем читать все PO-файлы и объединять их в одну элегантную структуру JSON, отображающую все переводы для одного и того же идентификатора:

"nav.item_1": {
    "en": "Home",
    "uk": "Головна",
}

Вот моя функция для объединения файлов PO в JSON:

def export_strings(source='en', target=None):
    source_str = StringIO(open(translations + '/' + source +      
        '/LC_MESSAGES/messages.po' , 'r', encoding='utf-8').read())
    source_catalog = read_po(source_str)
    for_tron = { message.id: {source: message.string}
                 for message in source_catalog if message.id }

    if not target:
        for locale in babel.list_translations():
            locale = locale.language
            if locale != source:
                target_str = StringIO(open(translations + '/' +
                locale + '/LC_MESSAGES/messages.po', 'r',
                encoding='utf-8').read())
                target_catalog = read_po(target_str)

                for message in target_catalog:
                    if message.id and message.id in for_tron.keys():
                        for_tron[message.id][locale]=message.string
    else:
        target_str = StringIO(open(translations + '/' + target +
          '/LC_MESSAGES/messages.po', 'r', encoding='utf-8').read())
        target_catalog = read_po(target_str)

        for message in target_catalog:
            if message.id and message.id in for_tron.keys():
                for_tron[message.id][target] = message.string

    with open(app_path + '/json_strings/strings.json', 'w',
     encoding='utf-8') as outfile:
        json.dump(for_tron, outfile, ensure_ascii=False)

Теперь, независимо от того, кто занимается вашей разработкой локализации, все, что им нужно, - это один файл JSON. Инженер извлечет любой язык, который вы выберете в качестве источника, затем вставит переводы в соответствующий языковой стандарт. Затем вы можете использовать обновленный JSON для импорта строк в ваши PO-файлы:

def import_strings(filename=None, source='en', target=None):
    if filename:
        from_tron = json.loads(open(filename, 'r', encoding='utf-8')
         .read())
    else:
        from_tron = json.loads(open(app_path +
        '/json_strings/strings.json', 'r', encoding='utf-8').read())

    template_str = StringIO(
     open('messages.pot', 'r', encoding='utf-8').read())

    if not target:
        for locale in babel.list_translations():
            locale = locale.language
            new_catalog = Catalog()
            for id in from_tron:
                if locale in from_tron[id].keys():
                    new_catalog.add(id, from_tron[id][locale])
            new_catalog.update(template)
            write_po(open(translations + '/' + locale +                  
             '/LC_MESSAGES/messages.po', 'wb'), new_catalog)

    else:
        new_catalog = Catalog()
        for id in from_tron:
            if target in from_tron[id].keys():
                new_catalog.add(id, from_tron[id][target])
        new_catalog.update(template)
        write_po(open(translations + '/' + target + 
          '/LC_MESSAGES/messages.po', 'wb'), new_catalog)

Если вы хотите выполнить свою разработку более удобным для Web 2.0 способом, со всеми вашими строками, экспортированными в этот JSON, вы можете создать сервер, отправляющий POST-запросы в любую онлайн-службу перевода контролируемым образом. Вы можете добавить атрибут «дата» и сохранить все предыдущие версии строк, принадлежащих этому конкретному идентификатору, для любого языка. Et voila, ваш файл JSON теперь также является памятью переводов!

"nav.item_1": {
    "en": "Home",
    "uk": [
        {string: "Головна", date: ""},
        {string: "Домашня сторінка", date: ""}
    ]
}

Псевдотрансляция с помощью Babel

Но мы еще не закончили! Прежде чем отправлять строки на перевод, вы должны проверить, весь ли текст на вашей веб-странице извлечен. Это называется псевдотрансляцией. Мы просто вставим кучу случайных символов там, где должен быть перевод, импортируем их обратно на вашу веб-страницу и просмотрим их в браузере, чтобы увидеть, нет ли нигде каких-либо мошеннических исходных строк, которые не были экспортированы для перевода, например это:

Чтобы добиться этого с помощью Babel, нам нужно немного подправить его исходный код. Мы заставим Babel отображать нужные нам строки, добавив новый поддельный языковой стандарт, называемый «псевдо», в его список всех возможных языков.

Прежде всего, перейдите в папку babel в папке Python lib, затем откройте папку locale-data. Чтобы Babel загрузил локаль, необходимо поместить соответствующий файл DAT в папку locale-data. Чтобы получить файл DAT для нашего поддельного псевдоязыка, мы можем скопировать файл en.dat (если вы хотите протестировать языки с письмом справа налево, например арабский, вы можете скопировать один из файлов для арабского языка. locales). Затем мы назовем скопированный файл pseudo.dat.

Теперь откройте файл core.py в каталоге babel, найдите список LOCALE_ALIASES и добавьте два новых поддельных языковых стандарта в конец списка: 'псевдо': ' псевдо 'и' идентификаторы ':' идентификаторы '. (Я также добавил языковой стандарт поддельных идентификаторов, чтобы отображать наши исходные идентификаторы.)

Инициализируйте псевдо и ids как новые языковые стандарты для перевода в файле конфигурации или как переменную среды на вашем сервере.

LANGUAGES = os.environ.get('LANGUAGES') or [
                                        'en', 'uk', 'ids', 'pseudo']

Используя простое регулярное выражение, вставьте кучу крестиков вместо перевода в PO-файл и запустите команду Babel, чтобы скомпилировать перевод.

Flask запрашивает языковой стандарт, указанный в вашем браузере, в зависимости от вашего местоположения или предпочтительного языка, который вы выбрали в браузере. Теперь, чтобы заставить Flask отображать определенный языковой стандарт для определенной страницы, вы можете использовать функцию Babel force_locale. Вот как выглядит мой маршрут псевдотрансляции во Flask:

@app.route('/pseudo')
def pseudo():
    with flask_babel.force_locale('pseudo'):
        return render_template('metamova.html')

Теперь я могу просто перейти на https://metamova.com/pseudo, чтобы в любой момент просмотреть псевдотрансляцию моего сайта.

Чтобы отобразить исходные идентификаторы, нам даже не нужно инициализировать перевод. Если нет перевода из Babel, Flask просто отобразит строки, которые есть в HTML. Таким образом, вы можете сделать тот же маршрут, но для идентификаторов:

@app.route('/ids')
def ids():
    with flask_babel.force_locale('ids'):
        return render_template('metamova.html')

Теперь я могу в любое время проверить, какой идентификатор и куда идет, просто набрав https://metamova.com/ids в моем браузере.

Это очень интересно, потому что с помощью магии javascript мы могли бы создать интерактивное представление перевода в контексте, которое вставляло бы любые типы переводчика в нужное место на основе идентификатора, чтобы переводчик мог видеть позицию строки, а также насколько коротким или длинным он должен быть прямо на странице.

Для человека, который раньше переводил веб-сайты, это было мечтой. Для украинского языка одна простая строка вроде «Зарегистрироваться» может быть переведена как инфинитив («Зареєструватись»), повелительное настроение призыва к действию («Зареєструйтеся»), описательное существительное («Реєстрація») или как-то совершенно другое, гораздо более короткая фраза (например, «Вхід»), все зависит от того, где фраза появляется в пользовательском интерфейсе.

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

И если вы понятия не имеете, что делаете со своими переводами, или ваш процесс самый лучший, свяжитесь с нами, перейдя на metamova.com!