Library / SDK
django-parler/django-parler avatar
django-parler/django-parler

django-parler: translated fields on a Django model without rewriting your ORM

Easily translate "cheese omelet" into "omelette au fromage".

738 stars162 forksPythonApache-2.0

At a glance

What is it?
django-parler stores translations in a separate table with one row per language and exposes them as ordinary attributes, with a queryset method for filtering on translated fields and a fallback language so a missing translation is not a blank page. The costs are the ones the README admits: queries need special method names, and the admin listing still runs one query per object.
Who is it for?
Adopt django-parler if you have a Django model with content that varies by language and you want the translated fields to look like ordinary attributes on the model, since the whole design is built around not touching your own query code. Keep away if you need full text search across translations or a translation workflow with per-field versioning, because the model is one row per language per object with no indexing story documented.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 15 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Translations in a second table, not columns on the model

django-parler is a set of Django model translations, and the tagline is simple Django model translations without nasty hacks. The mechanism is a separate table.

You mark fields as translatable with the TranslatedFields wrapper, and the package creates a companion model holding the translated values with one row per language:

python
    from django.db import models
    from parler.models import TranslatableModel, TranslatedFields

    class MyModel(TranslatableModel):
        translations = TranslatedFields(
            title = models.CharField(_("Title"), max_length=200)
        )

        def __unicode__(self):
            return self.title

The key property is that the translated fields are used like regular fields. An instance gives you object.title and it resolves to the current language, and the README's example is a model whose title reads as cheese omelet in English and omelette du fromage in French.

That is the design decision, and it is what separates django-parler from the alternative of putting a column per language on the model. A separate table means you are not adding a column every time you add a language, and you are not fighting the ORM with per-language field names. The cost is that the translation rows are a related model, so the join has to happen somewhere, and the package does it in the attribute access.

The feature list is worth reading for the compatibility claims, because they are the reason to pick this over rolling your own. There is nice admin integration and automatic fallback to the default language. The separate table is compatible with django-hvad, and the package plays nice with django-polymorphic, django-mptt and such, with no ORM query hacks, easy combination with custom Manager or QuerySet classes, and easy construction of the translations model manually when needed.

The project is Apache-2.0 licensed, the default branch is master, the last push was on 2026-10-07, and the single release listed is v2.4 from 2026-05-14.

Reading and writing in the current language

The language handling has three layers, and it is worth knowing which one you need.

The first is per object. get_current_language() returns the language the object is currently in, and set_current_language() switches it. The README's example shows the whole cycle, where setting the language and then assigning to the field creates the translation on demand, and saving persists it:

python
    >>> object = MyModel.objects.all()[0]
    >>> object.get_current_language()
    'en'
    >>> object.title
    u'cheese omelet'
    >>> object.set_current_language('fr')       # Only switches
    >>> object.title = "omelette du fromage"    # Translation is created on demand.
    >>> object.save()

The comment on the set_current_language line says only switches, and that is the important word. Switching the language does not copy anything, it changes which translation row the attribute access resolves to, and the translation row is created when you first write to a field in that language.

The second is per queryset. The queryset can be instructed to return objects in a specific language with the language method, and by default the current Django language is used:

python
    >>> objects = MyModel.objects.language('fr').all()
    >>> objects[0].title
    u'omelette du fromage'

The third is temporary, with a context manager for switching the language for the duration of a block, and a function for reading a single field in a given language without changing anything else:

python
    from parler.utils.context import switch_language

    with switch_language(model, 'fr'):
        print model.title

The single field getter is model.safe_translation_getter('title', language_code='fr'), which is the one to use when you want a value in a specific language and do not want to disturb the object's current language.

Two queryset methods, and why you cannot filter the way you normally would

Filtering on a translated field is where django-parler stops being transparent, and the README is explicit that the ORM restrictions are the reason.

There are two methods. The first is translated, which queries the translated fields:

python
    MyObject.objects.translated(title='cheese omelet')

The second is active_translations, which returns objects in the current language and possibly the fallback language. The active languages are the current language, plus the fallback language when hide_untranslated is False in the PARLER_LANGUAGES setting.

The name is chosen to warn you. Because the translations live in a related table, a filter on a translated field spans a multi-valued relationship, and Django's ORM does not support that without the restrictions the README cites. The consequence is stated in the special notes: queries for translated fields should be performed in a single translated or active_translations call. So you cannot mix a translated filter with an ordinary filter in the same queryset chain in the ordinary way.

There is a second gotcha in the same list. The active_translations method typically needs a distinct call to avoid duplicate results of the same object, because a join across a multi-valued relation produces a row per matching translation.

There is also an admin note. Using ModelAdmin.prepopulated_fields does not work, but you can use get_prepopulated_fields() as a workaround. That is a small thing to know before you spend an afternoon on a pre-populated slug field that never fills in.

The TODO section at the end of the README is frank about a known inefficiency. The list code currently performs one query per object, and this needs to be reduced. For a model with many rows, that is the difference between a list view that loads quickly and one that does not.

Configuring the languages and the fallback

The settings are where the behaviour of a missing translation is decided, and the admin tab configuration is optional.

Installation is a pip line and one app to add:

bash
    pip install django-parler
python
    INSTALLED_APPS += (
        'parler',
    )

The optional admin tab configuration uses PARLER_LANGUAGES, which is a dictionary keyed by site, with a default entry carrying the fallback behaviour:

python
    PARLER_LANGUAGES = {
        None: (
            {'code': 'en',},
            {'code': 'en-us',},
            {'code': 'it',},
            {'code': 'nl',},
        ),
        'default': {
            'fallback': 'en',             # defaults to PARLER_DEFAULT_LANGUAGE_CODE
            'hide_untranslated': False,   # the default; let .active_translations() return fallbacks too.
        }
    }

You replace None with the SITE_ID when you run a multi-site project with the sites framework, and each SITE_ID can be added as an additional entry in the dictionary.

The fallback behaviour is the interesting pair of options. The fallback is the language a missing translation falls back to, and by default the fallback language is the same as the LANGUAGE_CODE setting, and it can be changed with PARLER_DEFAULT_LANGUAGE_CODE. The hide_untranslated flag, False by default, controls whether active_translations returns fallbacks too.

The project also asks that you make sure your project is configured for multiple languages, and it notes that it might be useful to limit the LANGUAGES setting, with an example that lists en, en-us, it, nl, fr and es, and it uses gettext_lazy for the language names so they are translatable themselves.

The admin integration and the rest of the package

The admin integration is listed first in the features, and it is the part most Django developers will notice first, because a TranslatableModel gets per-language tabs in the admin rather than duplicated rows.

The advanced features list names the rest of the surface. You can create the TranslatedFieldsModel manually, which is the escape hatch when you need to construct translation rows in code rather than through the attribute interface. There are form classes for inline support, so a translatable model can be edited inline on a parent object. There are view classes for switching languages, creating and updating translatable objects. There are template tags for language switching buttons, so a template can offer a language picker without you writing the URL logic. And there are ORM methods to handle the translated fields, plus admin inlines support.

That list is a good description of the package's philosophy. It does not try to be a translation workflow tool with drafts, per-field review or versioning. It is a data layer that makes a translatable model behave like a model, plus the glue that lets the admin and the templates work with it.

The repository layout reflects that. There is a parler/ package, an example/ directory with an article app, an example app, a manage.py and a theme1 directory, a docs/ directory, a CHANGES.rst, an AUTHORS file, a runtests.py, a tox.ini, a setup.py and a pyproject.toml.

The build configuration is modern in one file and older in the other. The pyproject uses setuptools with a Django 5.2 requirement for the build, isort with a black profile and a line length of 99, and black with the same line length. The setup.py still carries the classic patterns, including the part where building an sdist calls Django's compilemessages command so the django.mo file exists, and it reads the version out of parler/__init__.py with a regex.

Where django-parler is the wrong choice

The first wrong case is a project that needs per-field translation workflow. django-parler stores one row per language with the whole set of translated fields for that language. There is no per-field versioning, no draft state and no review status, because the model has no concept of one. If your translators work on one field at a time across dozens of languages, you need a different layer.

The second is a project that needs to search inside translated content. The translated fields live in a related table, and the documented query interface is exact matching through translated and active_translations. A full text search across translations, or a filter that combines a translated field with several ordinary filters, is not what this package does.

The third is a large dataset with a list view. The TODO is explicit that the list code performs one query per object and needs to be reduced. A model with thousands of rows and several languages turns the admin list into thousands of queries, and that is a real problem for an editor, not just a slow one.

The fourth is a project that already has a translation approach you are happy with. If you have written your own translated model, or you use a package with a different storage model, migrating is work with no gain.

The alternative worth weighing is storing a JSON field per language on the model, which is simple, keeps everything in one row and lets you query with JSON operators on some backends. You give up the ability to filter on a translated field with a normal equality lookup, you lose the per-language indexes, and you end up writing the fallback logic yourself. django-parler takes that work off you and puts the join cost in your lap instead, which is the right trade for a content site and the wrong one for a high-traffic filtered list.

Editorial conclusion

Adopt django-parler if you have a Django model with content that varies by language and you want the translated fields to look like ordinary attributes on the model, since the whole design is built around not touching your own query code. Keep away if you need full text search across translations or a translation workflow with per-field versioning, because the model is one row per language per object with no indexing story documented. Before you migrate an existing app, read the special notes in the README, because queries for translated fields must go through a single translated or active_translations call, and note that version 2.4 shipped on 2026-05-14 with the last push on 2026-10-07 and no newer release since.

Frequently asked questions

How do I install django-parler and define a translatable model?

Run pip install django-parler, add parler to INSTALLED_APPS, and make the model inherit from TranslatableModel with a TranslatedFields wrapper declaring the translatable fields. The package then stores the translated values in a separate model with one row per language.

How do I filter a Django queryset on a translated field?

Use the translated method for an exact match on the current language, or active_translations to include the fallback language when hide_untranslated is False. Because of ORM restrictions on multi-valued relationships, a query for translated fields should be performed in a single translated or active_translations call, and active_translations typically needs a distinct call to avoid duplicate rows.

How does django-parler handle a field with no translation yet?

It falls back automatically to the default language. The fallback is the same as the LANGUAGE_CODE setting by default and can be changed with PARLER_DEFAULT_LANGUAGE_CODE, and the hide_untranslated option in PARLER_LANGUAGES controls whether active_translations returns fallbacks as well.

What is the known performance problem in django-parler?

The TODO in the README states that the list code currently performs one query per object and that this needs to be reduced. For models with many rows that turns a list view into a large number of queries.

Official sources

  1. django-parler/django-parler on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/django-parler-django-parler.svg)](https://hysenlabs.com/projects/django-parler-django-parler)

Was this useful?