158 lines
5.0 KiB
Markdown
158 lines
5.0 KiB
Markdown
|
---
|
|||
|
layout: post
|
|||
|
title: "SQLAlchemy-i18n with application factory, dynamic locales, and Celery"
|
|||
|
date: 2017-03-14 00:25:52+01:00
|
|||
|
tags: [development, python, flask]
|
|||
|
published: true
|
|||
|
author:
|
|||
|
name: Gergely Polonkai
|
|||
|
email: gergely@polonkai.eu
|
|||
|
---
|
|||
|
|
|||
|
[SQLAlchemy-i18n](https://sqlalchemy-i18n.readthedocs.io/) 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](http://flask.pocoo.org/docs/0.12/patterns/appfactories/) 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:
|
|||
|
|
|||
|
```python
|
|||
|
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):
|
|||
|
|
|||
|
```python
|
|||
|
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):
|
|||
|
|
|||
|
```python
|
|||
|
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:
|
|||
|
|
|||
|
```python
|
|||
|
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?)
|