django-parlor

Django model translations with even less nasty hacks.
git clone https://git.ce9e.org/django-parlor.git

commit
fa2cbdfe24adc6b5f4e5e42c042663904ff25d96
parent
6ea79efa7b3a6b8f6a0f1d71693356ca012e71ca
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2025-08-06 13:49
init

Diffstat

A .github/workflows/main.yml 25 +++++++++++++++++++++++++
A README.md 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A parlor/__init__.py 0
A parlor/admin.py 15 +++++++++++++++
A parlor/models.py 45 +++++++++++++++++++++++++++++++++++++++++++++
A pyproject.toml 43 +++++++++++++++++++++++++++++++++++++++++++

6 files changed, 208 insertions, 0 deletions


diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml

@@ -0,0 +1,25 @@
   -1     1 on: [push]
   -1     2 jobs:
   -1     3   lint:
   -1     4     runs-on: ubuntu-latest
   -1     5     steps:
   -1     6     - uses: actions/checkout@v4
   -1     7     - uses: actions/setup-python@v5
   -1     8     - run: pip install ruff
   -1     9     - name: linters
   -1    10       run: |
   -1    11         ruff check parlor
   -1    12   publish:
   -1    13     needs: [lint]
   -1    14     if: startsWith(github.ref, 'refs/tags')
   -1    15     runs-on: ubuntu-latest
   -1    16     permissions:
   -1    17       id-token: write
   -1    18     steps:
   -1    19     - uses: actions/checkout@v4
   -1    20     - uses: actions/setup-python@v5
   -1    21     - run: pip install build
   -1    22     - name: build
   -1    23       run: python3 -m build
   -1    24     - name: publish
   -1    25       uses: pypa/gh-action-pypi-publish@release/v1

diff --git a/README.md b/README.md

@@ -0,0 +1,80 @@
   -1     1 # django-parlor
   -1     2 
   -1     3 Django model translations with even less nasty hacks.
   -1     4 
   -1     5 ## Installation
   -1     6 
   -1     7 ```
   -1     8 pip install django-parlor
   -1     9 ```
   -1    10 
   -1    11 ## Usage
   -1    12 
   -1    13 ```python
   -1    14 from django.contrib import admin
   -1    15 from django.db import models
   -1    16 
   -1    17 from parlor.admin import TranslatableAdmin
   -1    18 from parlor.models import TranslatableModel
   -1    19 
   -1    20 
   -1    21 class MyModel(TranslatableModel):
   -1    22     ...
   -1    23 
   -1    24 
   -1    25 class MyModelTranslation(model.Model):
   -1    26     parent = MyModel.get_parent_field()
   -1    27     language_code = MyModel.get_lang_field()
   -1    28     ...
   -1    29 
   -1    30     class Meta:
   -1    31         unique_together = [('parent', 'language_code')]
   -1    32 
   -1    33 
   -1    34 admin.site.register(MyModel, TranslatableAdmin)
   -1    35 ```
   -1    36 
   -1    37 ## Status
   -1    38 
   -1    39 Right now this is more of a proof-of-concept. If people are interested in this
   -1    40 library it could certainly be expanded.
   -1    41 
   -1    42 ## Relation to django-parler
   -1    43 
   -1    44 ### Standing on the Shoulders of Giants
   -1    45 
   -1    46 There are two popular libraries for model translation in Django:
   -1    47 [django-modeltranslation](https://github.com/deschler/django-modeltranslation)
   -1    48 and [django-parler](https://github.com/django-parler/django-parler). The former
   -1    49 adds additional columns to the same table while the latter adds a new table for
   -1    50 each translatable model. Both of these libraries use a lot of magic which makes
   -1    51 them easy to use, but also leads to some hard-to-debug edge cases.
   -1    52 
   -1    53 There is also
   -1    54 [django-translated-fields](https://github.com/matthiask/django-translated-fields),
   -1    55 which uses the same basic approach as django-modeltranslation, but with much
   -1    56 less magic (and much less lines of code).
   -1    57 
   -1    58 Here I try to do something similar for django-parler, which has been
   -1    59 unmaintained for some time now.
   -1    60 
   -1    61 ### Migration
   -1    62 
   -1    63 If you want to migrate from django-parler to django-parlor, you need to replace
   -1    64 all instances of `parler.TranslatedFields` by explicit translation models as
   -1    65 described above. Note that the `parent` field is called `master` in
   -1    66 django-parler.
   -1    67 
   -1    68 To keep using the same data, you will also need to set `Meta.db_table` to the
   -1    69 name generated by django-parler (typically `myapp_mymodel_translation`).
   -1    70 
   -1    71 Finally you should run `manage.py makemigrations`. The generated migrations
   -1    72 will only contain minor changes though.
   -1    73 
   -1    74 ### Conceptual differences
   -1    75 
   -1    76 -   With django-parlor, the translation model must be created explicitly.
   -1    77 -   In the admin UI, django-parlor uses a stock inline instead of a custom
   -1    78     tabbed interface.
   -1    79 -   django-parlor only provides basic attribute access while django-parler
   -1    80     provides a lot more features.

diff --git a/parlor/__init__.py b/parlor/__init__.py

diff --git a/parlor/admin.py b/parlor/admin.py

@@ -0,0 +1,15 @@
   -1     1 from django.contrib import admin
   -1     2 
   -1     3 
   -1     4 class TranslatableAdmin(admin.ModelAdmin):
   -1     5     translation_inline_class = admin.TabularInline
   -1     6 
   -1     7     def get_inlines(self, request, obj):
   -1     8         class TranslationInline(self.translation_inline_class):
   -1     9             model = obj.translations.model
   -1    10             min_num = 2
   -1    11             extra = 0
   -1    12         return [
   -1    13             TranslationInline,
   -1    14             *super().get_inlines(request, obj),
   -1    15         ]

diff --git a/parlor/models.py b/parlor/models.py

@@ -0,0 +1,45 @@
   -1     1 from django.core.exceptions import ObjectDoesNotExist
   -1     2 from django.db import models
   -1     3 from django.utils.functional import cached_property
   -1     4 from django.utils.translation import get_language
   -1     5 
   -1     6 
   -1     7 class TranslationFallback:
   -1     8     def __getattr__(self, key):
   -1     9         return 'not translated'
   -1    10 
   -1    11 
   -1    12 class TranslatableModel(models.Model):
   -1    13     class Meta:
   -1    14         abstract = True
   -1    15 
   -1    16     @cached_property
   -1    17     def translation(self):
   -1    18         lang = get_language()
   -1    19         if self.pk:
   -1    20             try:
   -1    21                 return self.translations.get(language_code=lang)
   -1    22             except ObjectDoesNotExist:
   -1    23                 pass
   -1    24         return TranslationFallback()
   -1    25 
   -1    26     def __getattr__(self, key):
   -1    27         fields = self.translations.model._meta.get_fields()
   -1    28         if key in (f.attname for f in fields):
   -1    29             return getattr(self.translation, key)
   -1    30         else:
   -1    31             raise AttributeError
   -1    32 
   -1    33     @classmethod
   -1    34     def lang_field(cls):
   -1    35         return models.CharField('Language', max_length=15, db_index=True)
   -1    36 
   -1    37     @classmethod
   -1    38     def parent_field(cls):
   -1    39         return models.ForeignKey(
   -1    40             cls,
   -1    41             on_delete=models.CASCADE,
   -1    42             editable=False,
   -1    43             null=True,
   -1    44             related_name='translations',
   -1    45         )

diff --git a/pyproject.toml b/pyproject.toml

@@ -0,0 +1,43 @@
   -1     1 [build-system]
   -1     2 requires = ["setuptools"]
   -1     3 build-backend = "setuptools.build_meta"
   -1     4 
   -1     5 [project]
   -1     6 name = "django-parlor"
   -1     7 version = "0.0.1"
   -1     8 description = "Django model translations with even less nasty hacks."
   -1     9 readme = "README.md"
   -1    10 license = {text = "MIT"}
   -1    11 keywords = ["django", "translation"]
   -1    12 authors = [
   -1    13     {name = "Tobias Bengfort", email = "tobias.bengfort@posteo.de"},
   -1    14 ]
   -1    15 classifiers = [
   -1    16     "Development Status :: 4 - Beta",
   -1    17     "Environment :: Web Environment",
   -1    18     "Framework :: Django",
   -1    19     "Intended Audience :: Developers",
   -1    20     "License :: OSI Approved :: MIT License",
   -1    21     "Operating System :: OS Independent",
   -1    22     "Programming Language :: Python",
   -1    23     "Programming Language :: Python :: 3",
   -1    24 ]
   -1    25 dependencies = [
   -1    26     "django>=3.2",
   -1    27 ]
   -1    28 
   -1    29 [project.urls]
   -1    30 Homepage = "https://github.com/xi/django-parlor"
   -1    31 
   -1    32 [tool.setuptools.packages.find]
   -1    33 include = ["parlor*"]
   -1    34 
   -1    35 [tool.ruff.lint]
   -1    36 select = ["E", "F", "W", "C9", "I", "Q", "UP", "RUF"]
   -1    37 ignore = ["RUF012"]
   -1    38 
   -1    39 [tool.ruff.lint.flake8-quotes]
   -1    40 inline-quotes = "single"
   -1    41 
   -1    42 [tool.ruff.lint.isort]
   -1    43 force-single-line = true