Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

python gettext: specify locale in _()

I am looking fo a way to set the language on the fly when requesting a translation for a string in gettext. I'll explain why :

I have a multithreaded bot that respond to users by text on multiple servers, thus needing to reply in different languages. The documentation of gettext states that, to change locale while running, you should do the following :

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()

But, this does not apply in my case, as the bot is multithreaded :

Imagine the 2 following snippets are running at the same time :

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 :'(

and

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

You can see that messages sometimes are translated in the wrong locale. But, if I could do something like _("string", lang="FR"), the problem would disappear !

Have I missed something, or I'm using the wrong module to do the task... I'm using python3

like image 661
WayToDoor Avatar asked Dec 24 '22 04:12

WayToDoor


2 Answers

While the above solutions seem to work, they don’t play well with the conventional _() function that aliases gettext(). But I wanted to keep that function, because it’s used to extract translation strings from the source (see docs or e.g. this blog).

Because my module runs in a multi-process and multi-threaded environment, using the application’s built-in namespace or a module’s global namespace wouldn’t work because _() would be a shared resource and subject to race conditions if multiple threads install different translations.

So, first I wrote a short helper function that returns a translation closure:

import gettext

def get_translator(lang: str = "en"):
    trans = gettext.translation("foo", localedir="/path/to/locale", languages=(lang,))
    return trans.gettext

And then, in functions that use translated strings I assigned that translation closure to the _, thus making it the desired function _() in the local scope of my function without polluting a global shared namespace:

def some_function(...):
    _ = get_translator()  # Pass whatever language is needed.

    log.info(_("A translated log message!"))

(Extra brownie points for wrapping the get_translator() function into a memoizing cache to avoid creating the same closures too many times.)

like image 173
Jens Avatar answered Dec 26 '22 20:12

Jens


You can just create translation objects for each language directly from .mo files:

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")

And a dict cache for them can be easily thrown in there too.

like image 29
Lucia Avatar answered Dec 26 '22 19:12

Lucia