Compare commits
1 Commits
master
...
sqla-i18n-
Author | SHA1 | Date |
---|---|---|
|
aca0cc49be | 4 years ago |
1 changed files with 157 additions and 0 deletions
@ -0,0 +1,157 @@
|
||||
--- |
||||
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?) |
Loading…
Reference in new issue