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