python gettext: указать локаль в _()
Я ищу способ установить язык на лету при запросе перевода строки в gettext. Я объясню почему:
У меня есть многопоточный бот, который отвечает на запросы пользователей текстом на нескольких серверах, поэтому мне приходится отвечать на разных языках. Документация gettext гласит, что для изменения локали во время работы вы должны сделать следующее:
import gettext # first, import gettext
lang1 = gettext.translation('myapplication', languages=['en']) # Load every translations
lang2 = gettext.translation('myapplication', languages=['fr'])
lang3 = gettext.translation('myapplication', languages=['de'])
# start by using language1
lang1.install()
# ... time goes by, user selects language 2
lang2.install()
# ... more time goes by, user selects language 3
lang3.install()
Но это не относится к моему случаю, так как бот многопоточный:
Представьте, что 2 следующих фрагмента работают одновременно:
import time
import gettext
lang1 = gettext.translation('myapplication', languages=['fr'])
lang1.install()
message(_("Loading a dummy task")) # This should be in french, and it will
time.sleep(10)
message(_("Finished loading")) # This should be in french too, but it wont :'(
а также
import time
import gettext
lang = gettext.translation('myapplication', languages=['en'])
time.sleep(3) # Not requested on the same time
lang.install()
message(_("Loading a dummy task")) # This should be in english, and it will
time.sleep(10)
message(_("Finished loading")) # This should be in english too, and it will
Вы можете видеть, что сообщения иногда переводятся в неправильной локали. Но если бы я мог сделать что-то вроде _("string", lang="FR")
проблема исчезнет!
Я что-то пропустил, или я использую не тот модуль для выполнения задачи... Я использую python3
5 ответов
Хотя приведенные выше решения кажутся работающими, они не работают с обычными
_()
функция, которая является псевдонимом gettext(). Но я хотел сохранить эту функцию, потому что она используется для извлечения строк перевода из источника (см. Документацию или, например, этот блог).
Поскольку мой модуль работает в многопроцессорной и многопоточной среде, использование встроенного пространства имен приложения или глобального пространства имен модуля не будет работать, потому что
_()
будет общим ресурсом и будет зависеть от состояния гонки, если несколько потоков устанавливают разные переводы.
Итак, сначала я написал короткую вспомогательную функцию, которая возвращает закрытие перевода:
import gettext
def get_translator(lang: str = "en"):
trans = gettext.translation("foo", localedir="/path/to/locale", languages=(lang,))
return trans.gettext
А затем в функциях, использующих переведенные строки, я назначил это закрытие перевода для
_
, что делает его желаемой функцией
_()
в локальной области моей функции без загрязнения глобального общего пространства имен:
def some_function(...):
_ = get_translator() # Pass whatever language is needed.
log.info(_("A translated log message!"))
(Дополнительные точки брауни для упаковки
get_translator()
в мемоизирующий кеш, чтобы не создавать одни и те же замыкания слишком много раз.)
Вы можете просто создавать объекты перевода для каждого языка прямо из
.mo
файлы:
from babel.support import Translations
def gettext(msg, lang):
return get_translator(lang).gettext(msg)
def get_translator(lang):
with open(f"path_to_{lang}_mo_file", "rb") as fp:
return Translations(fp=fp, domain="name_of_your_domain")
И кеш dict для них тоже можно легко туда закинуть.
В следующем примере используется translation
напрямую, как показано в o11c, чтобы разрешить использование потоков:
import gettext
import threading
import time
def translation_function(quit_flag, language):
lang = gettext.translation('simple', localedir='locale', languages=[language])
while not quit_flag.is_set():
print(lang.gettext("Running translator"), ": %s" % language)
time.sleep(1.0)
if __name__ == '__main__':
thread_list = list()
quit_flag = threading.Event()
try:
for lang in ['en', 'fr', 'de']:
t = threading.Thread(target=translation_function, args=(quit_flag, lang,))
t.daemon = True
t.start()
thread_list.append(t)
while True:
time.sleep(1.0)
except KeyboardInterrupt:
quit_flag.set()
for t in thread_list:
t.join()
Выход:
Бегущий переводчик: en Traducteur en cours d'exécution: fr Лауфенден Юберсетцер: де Бегущий переводчик: en Traducteur en cours d'exécution: fr Лауфенден Юберсетцер: де
Я бы опубликовал этот ответ, если бы знал больше о gettext
, Я оставляю свой предыдущий ответ для людей, которые действительно хотят продолжать использовать _()
,
Я потратил немного времени, чтобы создать сценарий, который использует все доступные в системе локали и пытается напечатать в них известное сообщение. Обратите внимание, что "все локали" включают в себя простые изменения кодировки, которые в любом случае отменяются Python, и множество переводов не завершены, поэтому используйте запасной вариант.
Очевидно, вам также придется внести соответствующие изменения в ваше использование xgettext
(или эквивалент) для вас реальный код для определения функции перевода.
#!/usr/bin/env python3
import gettext
import os
def all_languages():
rv = []
for lang in os.listdir(gettext._default_localedir):
base = lang.split('_')[0].split('.')[0].split('@')[0]
if 2 <= len(base) <= 3 and all(c.islower() for c in base):
if base != 'all':
rv.append(lang)
rv.sort()
rv.append('C.UTF-8')
rv.append('C')
return rv
class Domain:
def __init__(self, domain):
self._domain = domain
self._translations = {}
def _get_translation(self, lang):
try:
return self._translations[lang]
except KeyError:
# The fact that `fallback=True` is not the default is a serious design flaw.
rv = self._translations[lang] = gettext.translation(self._domain, languages=[lang], fallback=True)
return rv
def get(self, lang, msg):
return self._get_translation(lang).gettext(msg)
def print_messages(domain, msg):
domain = Domain(domain)
for lang in all_languages():
print(lang, ':', domain.get(lang, msg))
def main():
print_messages('libc', 'No such file or directory')
if __name__ == '__main__':
main()
В следующем простом примере показано, как использовать отдельный процесс для каждого переводчика:
import gettext
import multiprocessing
import time
def translation_function(language):
try:
lang = gettext.translation('simple', localedir='locale', languages=[language])
lang.install()
while True:
print(_("Running translator"), ": %s" % language)
time.sleep(1.0)
except KeyboardInterrupt:
pass
if __name__ == '__main__':
thread_list = list()
try:
for lang in ['en', 'fr', 'de']:
t = multiprocessing.Process(target=translation_function, args=(lang,))
t.daemon = True
t.start()
thread_list.append(t)
while True:
time.sleep(1.0)
except KeyboardInterrupt:
for t in thread_list:
t.join()
Вывод выглядит так:
Бегущий переводчик: en Traducteur en cours d'exécution: fr Лауфенден Юберсетцер: де Бегущий переводчик: en Traducteur en cours d'exécution: fr Лауфенден Юберсетцер: де
Когда я попробовал это, используя темы, я получил только английский перевод. Вы можете создавать отдельные потоки в каждом процессе для обработки соединений. Вы, вероятно, не хотите создавать новый процесс для каждого соединения.