Übersetzbare Models in Django (Model Translation)

von David Danier | 0 | 3807 Aufrufe

Anzeige Hier werben

Django ist ein relativ junges, aber sehr bekanntes und umfangreiches Framework zur Erstellung von Webseiten. In unserer Einführung in Django wird das Framework näher vorgestellt.

Bisher stoßen Entwickler immer wieder an Probleme, wenn es darum geht eine Webseite vollständig zu Internationalisieren. Django bietet für statische Texte eine Vielzahl von Möglichkeiten innerhalb von Python, in Templates oder sogar im Javascript eine Übersetzbarkeit der Inhalte zu garantieren. Gerade auf Datenbank-Ebene gibt es aber kein offizielles Modul oder eine offizielle Richtlinie.

Derzeitiger Stand

Aufgrund der Komplexität des Themas sind dementsprechend in der Vergangenheit mehrere Projekte entstanden, die diesen Missstand lösen wollen. Um einige zu nennen:

Alle dieser Projekte nutzen ähnliche Ansätze, man kann hier zwischen zwei Grund-Ansätzen unterscheiden:

Übersetzung in einem eigenen Model mit ForeignKey zurück auf das Haupt-Models (BookTranslation -> Book)

Vereinfacht  
Python
1
2
3
4
5
6
7
class Book(models.Model):
    attr = models.SomeField(...)

class BookTranslation(models.Model):
    book = models.ForeignKey(Book, related_name='translation')
    language = models.CharField(max_length=5, choices=settings.LANGUAGES)
    trans = models.SomeField(...)

Möglichen Übersetzungen werden als eigene Datenbank-Felder angelegt in der Tabelle des Haupt-Models

Vereinfacht  
Python
1
2
3
4
5
class Book(models.Model):
    attr = models.SomeField(...)
    trans_de = models.SomeField(...) # wird natürlich automatisch erzeugt
    trans_en = models.SomeField(...)
    trans_XY = models.SomeField(...)

Probleme mit den Ansätzen

Beide Ansätze haben deutliche Schwächen. Nutzt man einen ForeignKey in der Übersetzungtabelle, so wird die Abfrage der Übersetzung schwierig und es wird meist für jedes Objekt eine eigene Datenbankabfrage nach der Übersetzung notwendig. Verfrachtet man alle Übersetzungen hingegen in eine Tabelle, so steigt der Speicherbedarf und auch Wartungsaufwand beim Hinzufügen neuer Sprachen massiv an.

Gleichzeitig können beide Methoden einen doch sehr häufigen Anwendungsfall gar nicht abdecken. Oftmals ist es notwendig, dass unterschiedliche Sprachversionen eben nicht die gleichen Felder nutzen und sich hier auch im Model unterscheiden müssten. Dieser Anspruch kann durch unterschiedlichen Umfang der jeweiligen Sprachversion, oder auch durch sprach-/landesspezifische Unterschiede auftreten, kann also auch organisatorischer Natur sein. Konkret fehlt also die Möglichkeit ein Feld in einer bestimmten Sprachversion hinzuzufügen, wegzulassen oder evtl. sogar in anderen Ausprägungen zu verwenden (Zahlenwert vs. String).

Hinzu kommt der Missstand, dass so nicht auf einfache Art entschieden werden kann, ob eine übersetzte Version des Objekts in einer konkreten Sprache überhaupt existiert. Eine Abfrage wie Book.objects.filter(translation__language='de') oder Book.objects.filter(trans_de__isnull=False) ist zwar möglich, muss aber an allen Stellen im Programm konsequent umgesetzt werden, ist also fehleranfällig. Gerade die erste Variante offenbart eine weitere gravierende Schwachstelle: Das Objekt BookTranslation wird nun zwar über einen JOIN in der SQL-Query mit einbezogen, muss aber später ein weiteres Mal für den Zugriff auf die entsprechende Attribute abgerufen werden.

Lösungsansatz - Wieso nicht genau umgekehrt?

Bisher wird immer versucht die Datenbank aus dem Gesichtspunkt Ein Objekt hat mehrere Übersetzungen zu modellieren. Dreht man den Spieß um, so erhält man die Aussage Jede Übersetzung hat einen Satz an Gemeinsamkeiten. Durch diese Sichtweise wird eine Internationalisierung auf Datenbankebene um einiges vereinfacht.

Allen bisherigen Lösungen gemein ist, dass die Übersetzung als eigenständiges Objekt und Teil des Gesamt-Objektes angesehen wird. Dreht man diese Ansatz um und sieht die Gemeinsamkeiten, also den nicht-übersetzbaren Teil des Objektes, also Teil des Gesamt-Objektes an ergeben sich ganz neue Möglichkeiten.

Ein einfaches Code-Beispiel:

 
Python
1
2
3
4
5
6
7
class BookCommon(models.Model):
    attr = models.SomeField(...)

class Book(models.Model):
    common = models.ForeignKey(BookCommon, related_name='translations')
    language = models.CharField(max_length=5, choices=settings.LANGUAGES)
    trans = models.SomeField(...)

Durch diesen Aufbau ergeben sich direkt einige Vorteile:

  • Das Book-Objekt selbst hat ein language-Attribut, die Abfrage einer Sprach-Version ist also sehr einfach.
  • Da der ForeignKey im Book-Objekt selbst steht, kann bei der Abfrage select_related() zum Einsatz kommen und somit die Datenbanklast reduziert werden.
  • Jede Sprachversion kann als eigenständiges Objekt angesehen werden, Formulare oder die Bearbeitung/Auflistung im Admin benötigen also keine Sonderlösungen. Gemeinsame Attribute können beispielsweise sehr einfach in einem Formular zusammengefasst werden, ohne Inline-Edits.
  • Die Tabellen werden nicht unnötig aufgeblasen.
  • Das Objekt mit dem später gearbeitet wird ist die übersetzte Version, Dinge wie get_absolute_url() zu verwenden ist also weniger fehleranfällig.
  • Der Zugriff auf den gemeinsamen Anteil ist stark vereinfacht, nun kann einfach book.common.attr verwendet werden.
  • Keine Nutzung von externen Modulen zur Vereinfachung einer eh schon hinkenden Lösung notwendig. ;-)

Aber da fehlt doch noch was? Das löst nicht alle genannten Probleme!

Besonders interessant wird dieser Lösungsansatz, wenn sich die unterschiedlichen Sprach-Versionen deutlich unterscheiden. Hier kann durch die normale Ableitung für jede Sprache ein eigenes Model erstellt werden, welches jeweils unterschiedliche Attribute verwenden kann.

 
Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class BookCommon(models.Model):
    attr = models.SomeField(...)

class DeBook(BookCommon):
    common = models.ForeignKey(BookCommon, related_name='translations')
    trans = models.SomeField(...)

class EnBook(BookCommon):
    common = models.ForeignKey(BookCommon, related_name='translations')
    other_trans = models.SomeField(...)

# Mögliche Vereinfachung für die Nutung
BookTranslations = {
    'de': DeBook,
    'en': EnBook,
}

Durch diese einfache, aber elegante Lösung können unterschiedliche Sprachen eigene Akzente setzen und die verschiedenen Sprach-Versionen können - je nach Anforderungen - weiter individualisiert werden. Gerade bei sich noch im Wachsen befindenden Webseiten können so Probleme, die durch zu starke Vereinheitlichung entstehen, schon im Vorfeld vermieden werden.

Gleichzeitig entfallen hier die letzten Stellen, an denen es notwendig ist über Optimierungen nachzudenken oder spezielle Abfragen zu nutzen. Der Aufruf von select_related() beispielsweise ist nicht mehr notwendig. Trotzdem sollte, falls möglich, eine Lösung ohne eigenständige Tabellen bevorzugt werden, da hier eine umgekehrte Abfrage (aka "Die Art wie das bestehende Lösungen machen") einfacher möglich ist.

Fazit

Übersetzbare Models sind in Django auch ohne größeren Aufwand oder die Notwendigkeit externe Applikationen zu verwenden möglich. Hierbei ergeben sich gleichzeitig sogar einige Vorteile, die die bestehenden Lösungen nicht bieten können. Eine genaue Planung der Datenbank bleibt aber natürlich auch in Zukunft notwendig.

Über den Autor: David Danier
David Danier arbeitet seit mehr als neun Jahren im Bereich Web Programmierung und ist unter anderem Geschäftsführer der Webagentur Team23 sowie Webmasterpro.de.
Profilseite betrachten