5.0 KiB
layout | title | date | tags | published | author | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
post | SQLAlchemy-i18n with application factory, dynamic locales, and Celery | 2017-03-14 00:25:52+01:00 |
|
true |
|
SQLAlchemy-i18n is an awesome project if you need to make some of your model fields translatable. However, it is only usable if your application class can be direectly instantiated; if you use application factories and you have the available locales in your config, you are on your own.
SQLAlchemy-i18n basics
According to the QuickStart section of the SQLAlchemy-i18n documentation, you have to initialize the module like this:
from flask_babel import get_locale
from sqlalchemy_i18n import make_translatable, translation_base, Translatable
import sqlalchemy_utils
make_translatable(options={'locales': ['fi', 'en']})
class Article(Translatable, Base):
__tablename__ = 'articles'
__translatable__ = {'locales': ['fi', 'en']} # Available locales
locale = 'en' # Default locale
id = sa.Column(sa.Integer, primary_key=True)
author = sa.Column(sa.Unicode(255))
class ArticleTranslation(translation_base(Article)):
__tablename__ = 'article_translations'
name = sa.Column(sa.Unicode(255))
content = sa.Column(sa.UnicodeText)
Add application factories to the mixture
In one of my current projects, I have the following config (excerpt):
from flask_babel import lazy_gettext as _
class Config:
AVAILABLE_LOCALES = {
'en': _('English')
'hu': _('Hungarian')
}
DEFAULT_LOCALE = 'en'
And the following application factory (again, excerpt):
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
from app import models
def create_app(config_name):
app = Flask(__name__)
db.init_app(app)
Unfortunately, adding the make_translatable
call to create_app
and
leaving the __translatable__
definition (assuming the options of the
make_translatable
are the default) is not working (its value is not a
default for models), so here is what I came up with:
from sqlalchemy_i18n import Translatable, translation_base
from app import db
class Article(Translatable, db.Model):
__tablename__ = 'articles'
__translatable__ = {
'locales': current_app.config['AVAILABLE_LOCALES'],
}
locale = current_app.config['DEFAULT_LOCALE']
id = db.Column(sa.Integer, primary_key=True)
author = sa.Column(sa.Unicode(255))
This brings up a problem: my models have to be initialized with an active app context. Here is a solution for that:
from config import config
# I have removed the models import from here
def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config[config_name])
make_translatable(options={
'locales': list(app.config['AVAILABLE_LOCALES'].keys()),
})
with app.app_context():
# Import models so they are registered with SQLAlchemy
from app import models
The only drawback I found is that I cannot import my models without creating the app first; however, given I need to instantiate the app to have a working database connection, it’s actually not a problem. Well, not until Celery comes into play.
--Chemical X-- Celery
Up to this point I haven’t used Celery for this app. When I set that up, due to some import order madness (which I could overcome, but doesn’t worth it), my models got imported before my Celery tasks and the Celery app, so my well-crafted solution failed miserably.
Fortunately, SQLAlchemy-i18n doesn’t tamper with metaclasses neither does
monkey patching on my model classes. I only have to extend Translatable
,
and the magic happens on its own. There are two properties Translatable
needs: __translatable__
, a dictionary which can, among other things,
define the available locales (languages) for the model, and locale
, which
sets the default locale. As there are no tricky metaclasses are involved, I
could easily set these properties during app initialization time:
from app import models
def create_app(config):
app = Flask(__name__)
# Iterate over everything in the models module
for _, member in models.__dict__.items():
# We look for subclasses of Translatable, but not Translatable
# itself
if isinstance(member, type) and \
issubclass(member, Translatable) and \
member != Translatable:
member.__translatable__['locales'] = list(app.config['AVAILABLE_LOCALES'].keys())
member.locale = app.config['DEFAULT_LOCALE']
Honestly, I’m not particularly happy with this solution. It works nice with the current version of SQLAlchemy-i18n, but as soon as they switch their implementation, I’m screwed (but that’s where testing comes in, right?)