Перевод CLI-приложения на Python: локализация click и typer с GNU gettext

от автора

1. Введение

Для регистрации ПО в реестре Минцифры России нужно соблюсти несколько условий, одно из них — наличие русского языка на сайте и в документах. И хотя требований к языку в самом программном обеспечении я не нашёл (может быть, пока), но задача по русификации интерфейса появилась.

Мы в «Тантор Лабс» развиваем корпоративную платформу баз данных Tantor XData, один из её компонентов — CLI (интерфейс командной строки) на Python с библиотекой Typer, которая, в свою очередь, написана поверх Click. Этот инструмент мы и попробуем русифицировать, а в идеале — научимся переводить приложение на разные языки, если потребуется.

Я вышел в интернет с этим вопросом.

Первое, про что мы узнаём — GNU gettext. Это набор инструментов и документаций, которые используют многие языки программирования для перевода программ. В Python он доступен с помощью модуля gettext. Можно начать с изучения документации, но, как сказано в классике, «всё уже украдено до нас». Наверняка кто‑то уже решал подобную задачу и, возможно, поделился своим опытом.

На хабре есть статья от 2009 года, ещё под второй Python (с тех пор на самом деле не поменялось почти ничего). Можно взять её за основу, но дополнить, а так же указать на важную, как мне кажется, деталь, которую почти все упускают. Эта тема в интернете распространена достаточно слабо. Потому я и решил написать статью, где постараюсь собрать все нюансы, с которыми столкнулся сам.

2. Настройка окружения

Перед тем как начать, предлагаю создать виртуальное окружение Python. Для этого можно использовать любой из инструментов, venvpoetrypdmuv. Мне было интересно попробовать uv от разработчиков ruff, так что в примере будет использован именно он.

Установим его по инструкции.

curl -LsSf https://astral.sh/uv/install.sh | sh

Из директории, где планируется создание проекта, инициализируем его. Далее установим необходимые библиотеки, в данном случае typer, а остальные зависимости подтянутся за ним.

uv init uv add typer

Во всех примерах ниже используется Python из виртуального окружения. Для его активации на Unix-системах используется команда source .venv/bin/activate.

3. Перевод модуля приложения

Начнём с малого и попробуем разобраться с gettext на примерах, что доступны в его документации. Возьмёмся за перевод отдельного модуля, создадим тестовый файл, импортируем gettext и используем gettext.translation для указания местонахождения переводов. Функция принимает несколько аргументов, сразу разберёмся с ними.

  • domain — уникальное имя приложения или модуля. Удобство тут в том, что файлы перевода для этого домена можно шарить между разными приложениями, в том числе на разных языках программирования.

  • localedir — путь к файлам локалей, структуру самой директории рассмотрим чуть позже.

  • languages — опциональный список или любой Iterable[str] объект, который содержит указания на языки и в том числе на подкаталоги с переводами в указанной ранее localedir. Если не указано, то поиск языка и переводов будет производиться по переменным окружения: LANGUAGELC_ALLLC_MESSAGESLANG. Именно в таком порядке приоритета, как это регламентировано в GNU gettext документации.

  • fallback — True/False. Если перевода для указанного языка не будет найдено, при False возвратится ошибка об отсутствии пути, иначе текст будет выведен как есть, без перевода.

Зная всё это, напишем немного кода. В мире gettext принято использовать переменную _ для указания функции, которой помечают строки, использующие перевод.

localize_module.py

import gettext   t = gettext.translation(domain='messages', localedir='locale', fallback=True)   _ = t.gettext      print(_('Would you kindly'))  # Would you kindly

Сейчас при запуске с fallback=True программа выведет строку как есть. Но, поместив строку в функцию _(), инструменты для парсинга исходного кода смогут найти эту и другие обёрнутые строки.

3.1. GNU gettext

Во многих Unix-системах локали для GNU-приложений лежат по пути /usr/share/locale/<LANG>, или /usr/local/share/locale/<LANG>, в том числе это позволяет разным программам использовать одинаковые файлы переводов, разделяя их на основе имени домена. Подробнее можно прочитать в мануале Locating Catalogs (GNU gettext utilities).

В Python по умолчанию gettext использует путь <sys.base_prefix>/share/locale/<LANG>, но в качестве тестового примера для добавления перевода на русский язык в корне проекта создадим директорию вместе с подкаталогами locale/ru/LC_MESSAGES.

mkdir -p locale/ru/LC_MESSAGES
locale └── ru     └── LC_MESSAGES

Теперь необходимо из нашего кода извлечь все сообщения, помеченные функцией _(), в файл шаблона с расширением .pot (Portable Object Template). Шаблон будет содержать необходимую структуру и исходные строки, его можно будет использовать позже для перевода на конкретные языки.

Существуют разные инструменты, позволяющие сделать это — xgettext в составе пакета gettext, который умеет работать с исходными кодами на многих языках программирования. Некоторые дистрибутивы Python включают в состав pygettext — он делает то же самое, но поддерживает только сам Python. 

Для некоторых, более сложных вещей нам потребуются дополнительные утилиты из пакета gettext. Так что рекомендую ознакомиться с его установкой для своей ОС, я же приведу примеры для Mac OS и Ubuntu.

На Mac OS с установленным менеджером пакетов Homebrew.

brew install gettext

На Ubuntu воспользуемся apt.

sudo apt update sudo apt install gettext

3.1.1. Симлинки для pygettext

Действия ниже необходимы, только если будет использован pygettext и pygettext -V или pygettext3 -V выводят ошибку.

В моём случае, на Mac OS pygettext находится по пути /Library/Frameworks/Python.framework/Versions/3.12/share/doc/python3.12/examples/Tools/i18n/pygettext.py, при этом вызвать его, просто написав в консоли pygettext, как это работает на Ubuntu, не удаётся. Не могу сказать, почему модель распространения отличается, но раз Барон Мюнхгаузен сам себя вытащил из болота, то и мы добавим симлинк на pygettext самостоятельно.

sudo ln -s ../../../Library/Frameworks/Python.framework/Versions/3.12/share/doc/python3.12/examples/Tools/i18n/pygettext.py /usr/local/bin/pygettext  pygettext -V

3.1.2. Создание .pot-шаблона

Используем pygettext, чтобы создать шаблон. Как правило, для названия шаблона используют формат <domain>.pot, в нашем случае domain — messages, что также является значением по умолчанию. Для наглядности всё равно укажем имя выходного файла.

pygettext -o locale/messages.pot localize_module.py

Вместо pygettext, можно использовать xgettext, предварительно установив пакет gettext. Интерфейс у них в целом схож, но, чтобы разобрать и второй инструмент, давайте сделаем шаблон messagesx.pot.

xgettext -o locale/messagesx.pot localize_module.py

Ниже приведены исходные тексты обоих шаблонов, в целом они идентичны, за некоторыми исключениями.

messages.pot

# SOME DESCRIPTIVE TITLE.   # Copyright (C) YEAR ORGANIZATION   # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.   #   msgid ""   msgstr ""   "Project-Id-Version: PACKAGE VERSION\n"   "POT-Creation-Date: 2024-09-07 10:23+0300\n"   "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"   "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"   "Language-Team: LANGUAGE <LL@li.org>\n"   "MIME-Version: 1.0\n"   "Content-Type: text/plain; charset=UTF-8\n"   "Content-Transfer-Encoding: 8bit\n"   "Generated-By: pygettext.py 1.5\n"         #: localize_module.py:5   msgid "Would you kindly"   msgstr ""

messagesx.pot

# SOME DESCRIPTIVE TITLE.   # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER   # This file is distributed under the same license as the PACKAGE package.   # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.   #   #, fuzzy   msgid ""   msgstr ""   "Project-Id-Version: PACKAGE VERSION\n"   "Report-Msgid-Bugs-To: \n"   "POT-Creation-Date: 2024-09-07 10:30+0300\n"   "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"   "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"   "Language-Team: LANGUAGE <LL@li.org>\n"   "Language: \n"   "MIME-Version: 1.0\n"   "Content-Type: text/plain; charset=CHARSET\n"   "Content-Transfer-Encoding: 8bit\n"      #: localize_module.py:5   msgid "Would you kindly"   msgstr ""

Оба инструмента успешно вытащили из исходного кода искомую строку и сформировали шаблон, из которого уже можно создавать конкретные переводы, файлы с расширением .po (Portable Object).

В процессе работы был обнаружен ещё один недостаток инструмента pygettext, а именно некорректный парсинг строк, если строка заканчивается висящей запятой.

3.1.3. Создание файла перевода .po

Приступим к созданию файла с переводом. Есть несколько способов сделать это.

  • Самый простой — копируем содержимое файла шаблона messages.pot в файл messages.po.

  • Используем msginit, всё так же из набора утилит gettext.

  • msgmerge, так же из пакета gettext.

С первым пунктом всё понятно, перекладываем содержимое из одного файла в другой и меняем расширение. Кроме того, стоит обратить внимание на строку с Content-Type, при использовании pygettext кодировка (charset) будет указана автоматически Content-Type: text/plain; charset=UTF-8, напротив xgettext её не указывает и при ручном редактировании следует её указать на месте CHARSET в строке Content-Type: text/plain; charset=CHARSET.

В нашем примере воспользуемся msginit. Помимо простого перекладывания содержимого шаблона, утилита позволяет указать, для какого языка и кем выполнен перевод, его кодировку. Так же она автоматически заполнит формулу для расчёта плюральных форм, то есть окончания для перечислений в зависимости от количества, 1 попугай, 2 попугая, 5 попугаев. Эту информацию можно найти на сайте консорциума Unicode CLDR, но msginit заполнит её самостоятельно.

msginit -i locale/messages.pot -o locale/ru/LC_MESSAGES/messages.po -l ru_RU.UTF-8 --no-translator

Разберёмся с опциями, -i — путь до входного шаблона, -o — путь выходного файла, -l — язык перевода и его кодировка в формате ll_CC.encoding, также флагом --no-translator отключаем необходимость ввода информации о переводчике, для тестового примера это не нужно и всегда можно заполнить руками уже после.

Для заполнения файла с переводом можно использовать текстовый редактор, либо более подходящий инструмент редактирования .po файлов. К примеру, poedit — кроссплатформенный редактор переводов, или воспользоваться одним из онлайн-редакторов.

Синтаксис файла довольно простой, в начале находится метаинформация о пакете, организации, переводчике и т. д. Дальше следуют строки, которые требуют перевод: исходная строка вводится ключевым словом msgid, а её перевод — msgstr. Полное описание формата .po можно посмотреть на странице PO Files (GNU gettext utilities). Более сложные строки, с перечислениями и форматированием str.format, рассмотрим далее.

messages.po

# Russian translations for PACKAGE package   # Английские переводы для пакета PACKAGE.   # Copyright (C) 2024 ORGANIZATION   # Automatically generated, 2024.   #   msgid ""   msgstr ""   "Project-Id-Version: PACKAGE VERSION\n"   "POT-Creation-Date: 2024-09-07 10:23+0300\n"   "PO-Revision-Date: 2024-09-07 10:23+0300\n"   "Last-Translator: Automatically generated\n"   "Language-Team: none\n"   "MIME-Version: 1.0\n"   "Content-Type: text/plain; charset=UTF-8\n"   "Content-Transfer-Encoding: 8bit\n"   "Generated-By: pygettext.py 1.5\n"   "Language: ru\n"   "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "   "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"      #: localize_module.py:5   msgid "Would you kindly"   msgstr "Будь любезен"

Обратите внимание, информация, после строки ‘#, fuzzy‘ будет выведена при использовании перевода для пустой строки. Тем не менее, некоторые из строк можно отбросить без последствий. Header Entry (GNU gettext utilities).

3.1.4. Компиляция перевода в .mo

Готовый перевод, полученный на предыдущем шаге, необходимо скомпилировать в файл с расширением .mo (Machine Object), который понимают программы, использующие gettext.

Для этого тоже можно использовать два инструмента, msgfmt из пакета gettext, или msgfmt.py из ...python3.12/examples/Tools/i18n/msgfmt.py.

msgfmt -o locale/ru/LC_MESSAGES/messages.mo locale/ru/LC_MESSAGES/messages.po

В результате должна получиться следующая структура каталога с файлами .pot.po.mo.

locale ├── messages.pot └── ru     └── LC_MESSAGES         ├── messages.mo         └── messages.po  3 directories, 3 files

3.1.5. Запускаем код

После выполнения предыдущих этапов пробуем запустить наш пример — если всё сделано верно, программа выводит уже не Would you kindly, а Будь любезен.

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

printenv | grep -E 'LANGUAGE|LC_ALL|LC_MESSAGES|LANG'

В моём случае, на Mac OS есть переменная LANG=ru_RU.UTF-8, если у вас указан не тот язык, для которого подготавливался перевод, либо, по какой-то причине, переменной вовсе нет, можно попробовать перед запуском выполнить export LANGUAGE=ru.

3.2. Общий процесс работы с gettext

Для первичного перевода приложения, условно, app.py, с помощью инструмента xgettext/pygettext получить шаблон .pot, из него, используя msginit — файл перевода .po. Перевести строки в .po файле и скомпилировать .mo с помощью msgfmt.

Процесс создания первичного перевода приложения app.py

Процесс создания первичного перевода приложения app.py

Если же в приложении app.py были добавлены строки, то есть произошло обновление, то можно использовать с флагом xgettext -j (pygettext не поддерживает такой режим работы), что означает не перезапись файла шаблона полностью, а его обновление и добавление новых строк. И вместо msginit следует уже использовать msgmerge, он сохранит готовые переводы строк в файле .po, но дополнит их новыми.

Процесс обновления перевода приложения app.py

Процесс обновления перевода приложения app.py

4. Перевод всего приложения

Здесь начинается самое интересное. При текущем подходе в каждый файл нужно импортировать gettext, устанавливать перевод, указывать путь до директории с переводами и так далее.

Если же мы повторим то, что написано в документации python gettext в разделе про локализацию приложения, то локализация click и typer с ходу не заработает, проверять мы это не будем, но дальше рассмотрим подробнее и всё станет понятно.
На мой взгляд, в документации и во многих статьях уделяют недостаточно внимания разнице между Class-based API и GNU gettext API, и что эти варианты локализации работают независимо друг от друга. На такую мысль меня в том числе подтолкнуло обсуждение в репозитории typer, где человек пишет, что смог успешно использовать gettext для перевода строк в своём приложении, но не в зависимостях.

4.1. Class-based API

Class-based API является рекомендуемым вариантом для приложений на python, он более гибкий и позволяет вызвать функцию install для добавления во встроенные объекты builtins специальной функции, что позволит помечать строки для перевода без необходимости что-то импортировать.

В качестве примера создадим файл localize_classbased.py в корне проекта, где мы ранее создавали директорию с переводом.

localize_classbased.py

import gettext   gettext.install(domain='messages', localedir='locale')      print(_('Would you kindly'))  # Будь любезен

IDE, скорее всего, на такой код будет ругаться, но при необходимости это можно отключить по инструкциям в интернете.

Этот код работает, потому что если мы посмотрим на детали реализации install, то увидим создание экземпляра класса NullTranslations, у которого вызывается метод install, что присваивает вызов метода self.gettext к объекту _ в builtins.

gettext.NullTranslations

def install(self, names=None):       import builtins       builtins.__dict__['_'] = self.gettext       if names is not None:           allowed = {'gettext', 'ngettext', 'npgettext', 'pgettext'}           for name in allowed & set(names):               builtins.__dict__[name] = getattr(self, name)

Таким образом, чтобы использовать тот же самый перевод в других частях приложения, достаточно обернуть строку в _('string') и так как эта функция доступна в builtins, всё ожидаемо сработает.

Это лишь одна из особенностей Class-based API. Ещё можно упомянуть наличие класса GNUTranslations, который позволяет читать некоторые специфичные .mo файлы, возможность смены языка приложения «налету», и другие возможности можно посмотреть в документации.

4.2. GNU gettext API

GNU gettext API называют классическим подходом, схожим с тем, как локализация работает в других GNU-пакетах. Перевод приложения выполняется глобально на основе переменных окружения пользователя.

localize_gnu.py

import gettext   # биндим для домена messages директорию `locale` gettext.bindtextdomain(domain='messages', localedir='locale')  # устанавливаем текущий домен gettext.textdomain('messages') # берём функцию `gettext.gettext` и присваиваем её к подчёркиванию _ = gettext.gettext      print(_('Would you kindly'))  # Будь любезен

Здесь реализация отличается и gettext оперирует глобальными переменными, куда сохраняет результаты вызовов этих функций.

gettext

# a mapping b/w domains and locale directories   _localedirs = {}   # current global domain, `messages' used for compatibility w/ GNU gettext   _current_domain = 'messages'         def textdomain(domain=None):       global _current_domain       if domain is not None:           _current_domain = domain       return _current_domain         def bindtextdomain(domain, localedir=None):       global _localedirs       if localedir is not None:           _localedirs[domain] = localedir       return _localedirs.get(domain, _default_localedir)   def dgettext(domain, message):       try:           t = translation(domain, _localedirs.get(domain, None))       except OSError:           return message       return t.gettext(message)     def gettext(message):       return dgettext(_current_domain, message)

Чтобы использовать перевод в других частях проекта, нужно импортировать функцию from gettext import gettext as _.

Важный момент, функции textdomain и bindtextdomain должны вызываться до импорта тех частей приложения, где используется gettext. В противном случае строки, обёрнутые в функции _/gettext, не будут переведены, ведь вызов выполнится при импорте и до установки домена приложения.

5. Перевод typer и click

Как было упомянуто ранее, если использовать Class-based API подход, то реализованная в typer и click локализация работать не будет. Понять это можно, если посмотреть на пулл реквесты, где добавлялась поддержка gettext. Авторы использовали именно GNU gettext API.

В click есть предложение от сообщества для перехода на Class-based API, если это произойдёт, то статья не устареет, но некоторые утверждения потеряют актуальность.

Создадим файл app.py с базовой реализацией CLI-приложения из примеров typer с двумя командами.

Первая — hello, печатает приветствие и вторая — goodbye, печатает прощание. К обоим добавим минимальную справку, и не забудем при этом применить функцию _() к этим строкам.

Аргумент add_completion указан здесь как False, так как этот функционал пока что не имеет поддержки gettext.

app.py

import typer   from gettext import gettext as _      app = typer.Typer(     help=_('Test CLI application.'),     add_completion=False, )    @app.command(help=_('Prints greeting'))   def hello(name: str):       print(_('Hello {name}').format(name=name))         @app.command(help=_('Prints goodbye message'))   def goodbye(name: str):       print(_('Bye {name}!').format(name=name))

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

В файле main.py добавим вызов функций для использования gettext.

main.py

import gettext    gettext.bindtextdomain(domain='messages', localedir='locale')  gettext.textdomain('messages') # важно выполнить настройку до импорта модулей, где gettext используется  from app import app      if __name__ == '__main__':       app()

Используем всё так же xgettext, чтобы извлечь строки для перевода из нашего файла app.py. Чтобы позже этот файл не перезаписался при объединении, назовём его typer_app.pot.

xgettext -o locale/typer_app.pot app.py

На шаге с настройкой окружения оно было создано в директории по умолчанию, то есть .venv. Значит, typer и click лежат по следующему пути: .venv/lib/python3.12/site-packages. У вас он может незначительно отличаться из-за используемой версии Python.

Используем утилиту find, чтобы передать все файлы с расширением .py на вход xgettext. Приведённые ниже команды работают в Unix операционных системах, вероятно, в Windows, есть аналогичный инструмент.

xgettext --sort-by-file -o locale/typer.pot $(find .venv/lib/python3.12/site-packages/typer -name "*.py")  xgettext --sort-by-file -o locale/click.pot $(find .venv/lib/python3.12/site-packages/click -name "*.py")

Вместо утилит find и xgettext можно воспользоваться pybabel extract, но в данной статье рассматривать его не будем.

В результате были созданы ещё два новых .pot шаблона, click.pot и typer.pot, следующей командой объединим несколько шаблонов в один messages.pot.

xgettext --sort-by-file -o locale/messages.pot locale/*.pot

Здесь мы могли бы использовать утилиту find, как в командах выше, но так как наши .pot шаблоны лежат в одной директории, можно обойтись паттерном.

При таком объединении дубликаты строк объединяются в одну, а их комментарии «склеиваются».

#: .venv/lib/python3.12/site-packages/click/core.py:1387   #: .venv/lib/python3.12/site-packages/typer/core.py:579   #: .venv/lib/python3.12/site-packages/typer/rich_utils.py:91   msgid "Options"   msgstr ""

Как и ранее, воспользуемся msginit для создания файла перевода .po.

msginit -i locale/messages.pot -o locale/ru/LC_MESSAGES/messages.po -l ru_RU.UTF-8 --no-translator

Теперь займёмся переводом этого файла. Там довольно много строк подтянулось из typer и click, но мы не будем сильно распыляться, посмотрим какие строки выводит вызов справки нашего приложения, их и переведём.

Здесь уже можно видеть, как происходит перевод строк с форматированием.

#: app.py:20   #, python-brace-format   msgid "Bye {name}!"   msgstr "До свидания, {name}! \nВозвращайся в свой сказочный лес"

messages.po, ненужные строки для тестового примера были удалены.

msgid ""   msgstr ""   "Project-Id-Version: PACKAGE VERSION\n"   "Report-Msgid-Bugs-To: \n"   "POT-Creation-Date: 2024-09-12 13:56+0300\n"   "PO-Revision-Date: 2024-09-12 13:56+0300\n"   "Last-Translator: Automatically generated\n"   "Language-Team: none\n"   "Language: ru\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%10==1 && n%100!=11 ? 0 : n%10>=2 && "   "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"      #: .venv/lib/python3.12/site-packages/click/core.py:1309   #: .venv/lib/python3.12/site-packages/click/decorators.py:559   msgid "Show this message and exit."   msgstr "Показать это сообщение и выйти."      #: .venv/lib/python3.12/site-packages/click/core.py:2821   #: .venv/lib/python3.12/site-packages/typer/core.py:363   #: .venv/lib/python3.12/site-packages/typer/core.py:544   #, python-brace-format   msgid "default: {default}"   msgstr "по умолчанию: {default}"      #: .venv/lib/python3.12/site-packages/click/core.py:2834   #: .venv/lib/python3.12/site-packages/typer/core.py:365   #: .venv/lib/python3.12/site-packages/typer/core.py:553   msgid "required"   msgstr "обязательный"     #: .venv/lib/python3.12/site-packages/typer/rich_utils.py:85   msgid "[default: {}]"   msgstr "[по умолчанию: {}]"      #: .venv/lib/python3.12/site-packages/typer/rich_utils.py:88   msgid "[required]"   msgstr "[обязательный]"      #: app.py:8   msgid "Test CLI application."   msgstr "Тестовое приложение для демонстрации gettext."      #: app.py:13   msgid "Prints greeting"   msgstr "Печатает приветствие"      #: app.py:15   #, python-brace-format   msgid "Hello {name}"   msgstr "Привет {name}"      #: app.py:18   msgid "Prints goodbye message"   msgstr "Печатает прощание"      #: app.py:20   #, python-brace-format   msgid "Bye {name}!"   msgstr "До свидания, {name}! \nВозвращайся в свой сказочный лес"  

Скомпилируем машинный объект .mo с помощью msgfmt, как делали это ранее.

msgfmt -o locale/ru/LC_MESSAGES/messages.mo locale/ru/LC_MESSAGES/messages.po

Для проверки успешности перевода, выполним пару команд, вызов справки python main.py --help и python main.py goodbye 'наш ласковый Миша'.

6. Формы множественного числа

Файл с переводом, который содержит строки с форматированием, мы уже рассмотрели. Теперь рассмотрим небольшой пример со строками для единственного и множественного чисел, как упоминалось ранее, 1 попугай, 3 попугая, 5 попугаев.

Чтобы не повторяться, здесь мы уже не будем рассматривать подробный вызов команд для подготовки переводов.

Подготовим код в файле plural.py, в нём будем использовать функцию ngettext импортированную из gettext, которая как раз учитывает плюральные формы. В качестве первого аргумента ей передаётся вариант для единственного числа, второй — для множественного и третий аргумент — n, число, для которого будет выбрана подходящая форма строки с переводом. Если перевод на конкретный язык не был подготовлен, то для n=1 будет использована первая строка, для остальных случаев — вторая.

plural.py

import gettext      gettext.bindtextdomain(domain='messages', localedir='locale')   gettext.textdomain('messages')      print(gettext.ngettext(       "Python growth - {n} parrot",       "Python growth - {n} parrots",       1,   ).format(n=1))      print(gettext.ngettext(       "Python growth - {n} parrot",       "Python growth - {n} parrots",       3,   ).format(n=3))      print(gettext.ngettext(       "Python growth - {n} parrot",       "Python growth - {n} parrots",       5,   ).format(n=5))

messages.po

msgid ""   msgstr ""   "Project-Id-Version: PACKAGE VERSION\n"   "Report-Msgid-Bugs-To: \n"   "POT-Creation-Date: 2024-09-12 16:35+0300\n"   "PO-Revision-Date: 2024-09-12 16:35+0300\n"   "Last-Translator: Automatically generated\n"   "Language-Team: none\n"   "Language: ru\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%10==1 && n%100!=11 ? 0 : n%10>=2 && "   "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"      #: localize_plural.py:7 localize_plural.py:13 localize_plural.py:19   #, python-brace-format   msgid "Python growth - {n} parrot"   msgid_plural "Python growth - {n} parrots"   msgstr[0] "Рост Удава - {n} попугай"     msgstr[1] "Рост Удава - {n} попугая"     msgstr[2] "Рост Удава - {n} попугаев"

В файле перевода .po, собранном из этого кода, видим три формы для msgstr, их количество и выбор определяется формулой в строке "Plural-Forms:".

  • nplurals=3 — количество форм для русского языка их 3.

  • n%10==1 && n%100!=11 ? 0 — если число n при делении на 10 даёт остаток 1 и при делении на 100 не даёт остатка 11, то выбирается форма с индексом 0, то есть первая.

  • n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 — если при делении на 10, остаток >= 2 и при этом <= 4 и остаток от деления на 100 < 10, или >= 20, в этом случае выбирается форма с индексом 1, то есть вторая.

  • Во всех остальных случаях берётся третья форма с индексом 2.

Функционал поддерживает только целые числа.

7. Эпилог

В результате мы узнали про GNU gettext и набор инструментов для локализации. Полученный опыт можно использовать не только с Python, но и с другими языками программирования, которые предоставляют соответствующую поддержку.

Научились переводить приложения на Python с использованием стандартной библиотеки gettext, узнали про Class-based API и GNU gettext API подходы локализации.
Применили полученный опыт для локализации кода, написанного с применением библиотек click и typer.

А также рассмотрели, как переводятся строки с форматированием и изменяются строки с учётом форм множественного числа.

Надеюсь, статья окажется полезной. Я в том числе постарался оставить ссылки для расширенного изучения инструментов, которые применяются для локализации (l10n) и интернационализации (i18n).


ссылка на оригинал статьи https://habr.com/ru/articles/861814/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *