Flask-Babel nebrat (alespoň ne pro překlady)

Zajímavý šok mě čekal, když jsem se rozhodl použít internacionalizační knihovnu Flask-Babel jako pomocníka pro vývoj webové aplikace v pythonu pod frameworkem Flask.

Knihovna slouží jako jakési "lepidlo" mezi Flaskem a Babelem, která poskytuje funkce pro lokalizaci a překlady řetězců z MO katalogů. Ve světě pythonu je velice oblíbená a často používaná pro internacionalizaci GUI a web aplikací.

V čem tedy tkví problém? Představme si, že máme jednoduchou webovou aplikaci následujících řádků:

from flask import Flask, session from flaskext.babel import Babel, gettext app = Flask(__name__) # Dulezite je nastavit app.root_path adresar, flask-babel hleda .mo soubory v: # app.root_path+'/translations/JAZYK/LC_MESSAGES/messages.mo' babel = Babel(app) @babel.localeselector def get_locale(): return session['locale'] if 'locale' in session else 'cs' @babel.timezoneselector def get_tz(): return session['locale_tz'] if 'locale_tz' in session else 'Europe/Prague' # ... Nejake funkce na UI, vcetne nastaveni locale @app.route('/') def page_index(): return gettext('Hello, world!')

Aplikace dělá to, co dělat má. Zobrazuje "Hello, world!" v jazyce, který vrací localeselector.

Pokud se podíváte na aplikaci přes profiler, s hrůzou zjístíte, že největší čas (u mě 17ms) sežere funkce _parse() z modulu gettext.py. To je opravdu hodně. Koukáte na to a nechápete, co se děje... Vrhnete se na hledání problému, které je tak otravné, že ani silné kafe vedle na stolku vám nezabrání v usínání.

V čem tedy spočívá problém? Podívejme se na úsek kódu knihovny Flask-Babel, mluví sám za sebe:

from flask import _request_ctx_stack # ... def get_translations(): ctx = _request_ctx_stack.top if ctx is None: return None translations = getattr(ctx, 'babel_translations', None) # <==== COZE?!? if translations is None: dirname = os.path.join(ctx.app.root_path, 'translations') translations = support.Translations.load(dirname, [get_locale()]) ctx.babel_translations = translations return translations def gettext(string, **variables): t = get_translations() if t is None: return string % variables return t.ugettext(string) % variables

Ano, správně. Flask-Babel nahrává katalog řetězců (babel.support.Translations) při KAŽDÉM novém requestu. Jediná "cache", kterou vytváří, je již načtený objekt Translations ve stávajícím requestu, takže když voláte několikrát za sebou gettext(), k načtení katalogu už opakovaně nedochází. Nicméně, jakmile se request dokončí, uvolní se ze zásobníku kontext a můžeme vesele znovunačítat dále.

V zásadě je několik možností, jak z toho ven:

  1. Koupit kvalitní hardware a dělat weby s nízkou návštěvností.
  2. Obcházet funkce překladu knihovny Flask-Babel. Využívat jen formátovací funkce (datum, čísla).
  3. Vykašlat se na lepidlo, které špatně lepí a používat ke všemu přímo Babel + cache Translations objektů.
  4. DOPLNĚNO: Přejít na Flask-BabelEx, ale pozor na nekompatibilitu.

Třetí možnost vyžaduje dopsání kódu, který by mohl například vypadat takto:

from babel.support import Translations _translations = {} def get_translation(lang): if not lang in _translations: _translations[lang] = Translations.load(app.root_path+'/translations', [lang]) return _translations[lang] def gettext(string, **variables): lang = session['lang'] if 'lang' in session else 'cs' if lang == 'en': return string % variables return get_translation(lang).ugettext(string) % variables app.jinja_env.add_extension('jinja2.ext.i18n') app.jinja_env.install_gettext_callables( gettext, lambda s,p,n: 'ngettext not implemented yet :)', newstyle=True )

U ngettext přeji štěstí při řešení plurálových forem (možná by Vám mohla napovědět fce Translations.plural(num)), kdybyste se rozhodli cachovat i samotné překlady :)