angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Umstellung auf eine mehrsprachige Datenbank

Wir fügen eine Tabelle mit Übersetzungen für jede Tabelle hinzu, die Felder enthält, die in mehreren Sprachen angezeigt werden müssen.

25 August 2019
post main image
unsplash.com/@kevnbhagat

Als ich dieses Projekt, diese Website, begann, dachte ich daran, dass sie mehrere Sprachen unterstützen musste. Aber natürlich habe ich mich wegen all der anderen Dinge, die ich lernen, hinzufügen und implementieren musste, nicht wirklich mit Datenbankdesign beschäftigt. Nach einem bestimmten Moment konnte ich mehrere Sprachen anzeigen und auswählen, aber das war für die Texte im Code und in den Vorlagen und nicht für die Datenbankinhalte wie Blogbeiträge und Seiten.

Um die Inhalte mehrsprachig zu gestalten, habe ich recherchiert und festgestellt, dass es mehrere Möglichkeiten gibt, dies zu tun, wobei jeder seine Vor- und Nachteile hat. Wenn Sie neu in diesem Bereich sind, empfehle ich Ihnen, sich die folgenden Links anzusehen. Ich habe entschieden, dass es nur einen richtigen Weg gibt, dies zu tun, und zwar durch Hinzufügen einer "Übersetzungstabelle" für jede Tabelle, die Felder enthält, die in mehreren Sprachen verfügbar sein müssen, oder, wenn Sie möchten, Hinzufügen eines "Übersetzungsobjekts" für jedes Objekt, das Attribute enthält, die in mehreren Sprachen verfügbar sein müssen. Ich weiß, dass ich jetzt mit komplexeren Abfragen konfrontiert werde, wie z.B. Rückgriff auf die Standardsprache und Verwendung von Zählen, Seitenumbruch, Begrenzung.

Standardsprache, ausgewählte Sprache und Sprachrückfall

Language Fallback ist der Mechanismus, bei dem wir die Inhaltselemente in der Standardsprache anzeigen, wenn sie für die ausgewählte Sprache nicht verfügbar sind. Der Rückfall in die Sprache ist eine Entscheidung, die Sie treffen müssen.

Ohne Sprachrückfall zeigen Sie nur die Inhaltselemente in der gewählten Sprache an, wenn nicht vorhanden, dann zu schade (403). Das ist relativ einfach. Wenn die ausgewählte Sprache Deutsch ist, zeigen Sie nur die deutschen Elemente an, wenn die ausgewählte Sprache Englisch ist, zeigen Sie nur die englischen Elemente.

Beim Sprachrückfall zeigen Sie die Inhaltselemente in der ausgewählten Sprache an, wenn sie nicht verfügbar sind, zeigen Sie sie dann in der Standardsprache an. Die Standardsprache ist die Sprache, die immer vorhanden sein muss. Für diese Website habe ich mich entschieden, einen Sprach-Fallback zu implementieren und möchte in Zukunft bestimmte Fallback-Items für eine ausgewählte Sprache überhaupt nicht mehr anzeigen können.

Hinzufügen der Sprachen zur Datenbank

Für alle mehrsprachigen Inhaltselemente muss eine zusätzliche Tabelle hinzugefügt werden. Ich habe bereits Sprachen in der Konfiguration definiert, aber jetzt eine Sprachtabelle erstellt:

class Language(Base):

    __tablename__ = 'language'

    id = Column(Integer, primary_key=True)

    # enable/disable language
    is_active = Column(Boolean, default=False, index=True)

    # name in your own language
    name = Column(String(100), server_default='', index=True)

    # short name in your own language
    short_name = Column(String(100), server_default='', index=True)

    # name in native language
    native_name = Column(String(100), server_default='', index=True)

    # short name in native language
    native_short_name = Column(String(100), server_default='', index=True)

    # language_region_code: en_US, de_DE, ...
    language_region_code = Column(String(16), server_default='', index=True)

    # lang_code: actual code shown to visitor, en, es, de (but also can be en_US, es_ES, etc.)
    language_code = Column(String(16), server_default='', index=True)

Dies ermöglicht es auch, eine Sprache in Zukunft zu aktivieren und zu deaktivieren. Dann habe ich die Kategorie der Inhaltselemente verwendet, um zu sehen, wie das funktioniert, ich habe gerade die Übersetzungstabelle für Kategorien hinzugefügt, die fast eine Kopie der Kategorietabelle ist:

class ContentItemCategory(Base):

    __tablename__ = 'content_item_category'

    id = Column(Integer, primary_key=True)

    name = Column(String(50), server_default='')
    description = Column(String(200), server_default='')

    # one-to-many relationship with translation
    # we only use this relationship to append translations
    content_item_category_translations = relationship(
        'ContentItemCategoryTranslation', 
        backref='content_item_category',
        lazy='dynamic')


class ContentItemCategoryTranslation(Base):

    __tablename__ = 'content_item_category_translation'

    id = Column(Integer, primary_key=True)

    # name, slug, description in specified language
    name = Column(String(100), server_default='')
    slug = Column(String(100), server_default='')
    description = Column(String(200), server_default='')

    # one-to-many relationship with language
    language_id = Column(Integer, ForeignKey('language.id'))

    # one-to-many relationship with content_item_category
    content_item_category_id = Column(Integer, ForeignKey('content_item_category.id'))

Nach der Implementierung der Änderungen und der Möglichkeit, Kategorien und Übersetzungen zu erstellen und zu bearbeiten, war es Zeit für einige Tests. Mein Hauptanliegen war es, wie man die Daten aus diesem neuen Setup erhält, einschließlich der Implementierung von Fallback in eine Standardsprache.

Der Wechsel zu den Übersetzungen war nicht viel Arbeit, da ich die ursprüngliche Version der Übersetzungstabellen fast identisch mit den Tabellen hielt. Für die Übersetzungstabellen habe ich das language_id, set to the default language und content_item_id, set to the id field hinzugefügt.

INSERT INTO content_item_translation (id, ..., language_id, content_item_id) select id, ..., 1, id from content_item;

Das SQL Erhalten der Kategorien sieht nicht zu kompliziert aus. Die Übersetzungstabelle wird in zwei Tabellen abgelegt, die erste, tr_selected, die zur Abfrage von Kategorien für die ausgewählte Sprache verwendet wird, und die zweite, tr_default, die Kategorien für die Standardsprache abfragt. Unten wählen wir die Felder name und slug, language_id=3 bedeutet Deutsch und language_id=1 bedeutet Englisch.
Wenn die Übersetzung für die ausgewählte Sprache nicht gefunden wird, wird die Standardsprache verwendet.

Wichtig: Beachten Sie, dass das Titelfeld im Modell die leere Zeichenkette als Standardwert hat. Die untenstehende Abfrage funktioniert also nur, wenn kein Sprachdatensatz angegeben wurde.

SELECT
    category.id,
    category.name,
    IFNULL(tr_selected.name, tr_default.name) name,
    IFNULL(tr_selected.slug, tr_default.slug) slug
  FROM content_item_category category
  LEFT OUTER JOIN content_item_category_translation tr_selected
    ON category.id = tr_selected.content_item_category_id AND tr_selected.language_id = 3
  LEFT OUTER JOIN content_item_category_translation tr_default
    ON category.id = tr_default.content_item_category_id AND tr_default.language_id = 1
  WHERE category.id = 3;

Um dies zu erreichen, verwenden SQLAlchemy wir zuerst den Alias der Übersetzungstabelle. Als nächstes verwenden wir das func.coalesce Ersetzen des IFNULL, und der Rest sieht ähnlich wie die SQL Abfrage unterschiedlich aus:

tr_selected, tr_default = aliased(ContentItemCategoryTranslation), aliased(ContentItemCategoryTranslation)
r = db.query(
  func.coalesce(tr_selected.name, tr_default.name),
  func.coalesce(tr_selected.slug, tr_default.slug)).\
    select_from(ContentItemCategory).\
    outerjoin(tr_selected, and_((ContentItemCategory.id == tr_selected.content_item_category_id), (tr_selected.language_id == 3))  ).\
    outerjoin(tr_default, and_((ContentItemCategory.id == tr_default.content_item_category_id), (tr_default.language_id == 1))  ).all()

Sollen wir Attribute (Felder) oder Objekte (Datensätze) auswählen? Datensätze der Übersetzungstabelle sind nur vorhanden, wenn eine Übersetzung hinzugefügt wurde. Es erscheint logisch, Objekte anstelle von Attributen auszuwählen, aber dies ist mit SQLAlchemynicht möglich, siehe auch unten.

Wir möchten, dass die Blog-Einträge nach Erstellungsdatum ausgewählt werden. Auch weil wir die zusammengeführten Teilergebnisse filtern, indem wir veröffentlicht = 1 verwenden, erhalten wir viele NULL Werte für Titel und Slug. Der Grund dafür ist natürlich, dass das Ergebnis alle Datensätze der Tabelle content_item zurückgibt. Wir können diese NULL Datensätze entfernen, indem wir eine HAVING Anweisung hinzufügen, die das Ende hinzufügt. Jetzt ist das Ergebnis korrekt.
Aber es gibt einen Haken. Im Moment ist die Anzahl der Datensätze in der Tabelle content_item 230. Die Anzahl der veröffentlichten Blog-Einträge beträgt 12. Das bedeutet, dass das HAVING Teil über 200 Datensätze filtern muss, 95%!

Der WHERE Teil wurde hinzugefügt, um die Felder content_item_type, published und content_item_parent_id zu filtern. Dies geschieht, um die Anzahl der Datensätze für das HAVING Teil zu reduzieren. Ich habe auch eine ORDER BY Klausel hinzugefügt, um die neuesten Blog-Posts zuerst anzuzeigen.

SELECT
    DISTINCT
    content_item.id,
    content_item.title,
    IFNULL(tr_selected.created_on, tr_default.created_on) created_on,
    IFNULL(tr_selected.title, tr_default.title) tr_title,
    IFNULL(tr_selected.slug, tr_default.slug) slug
  FROM content_item_translation tr_where, content_item
  LEFT OUTER JOIN content_item_translation tr_selected
    ON (
      content_item.id = tr_selected.content_item_id 
      AND tr_selected.published = 1 
      AND tr_selected.content_item_parent_id = 0 
      AND tr_selected.language_id = 3)
  LEFT OUTER JOIN content_item_translation tr_default
    ON (
      content_item.id = tr_default.content_item_id
      AND tr_default.published = 1 
      AND tr_default.content_item_parent_id = 0 
      AND tr_default.language_id = 1)
  WHERE
    content_item.id = tr_where.content_item_id
    AND content_item.content_item_type = 1
    AND tr_where.published = 1
    AND tr_default.content_item_parent_id = 0
  HAVING 
    tr_title IS NOT NULL
  ORDER BY created_on desc;

Die obige Abfrage basiert auf einem Beispiel aus dem Internet. Ich bin mir immer noch nicht sicher über das HAVING Teil und musste auch hinzufügen DISTINCT , um doppelte Zeilen zu entfernen. Wie auch immer, das SQLAlchemy Abfrageäquivalent der obigen SQL Abfrage ist:

tr_selected, tr_default, tr_where = aliased(ContentItemTranslation), aliased(ContentItemTranslation), aliased(ContentItemTranslation)
result_tuples = db.query(
    ContentItem.id, 
    ContentItem.title, 
    func.coalesce(tr_selected.created_on, tr_default.created_on).label('tr_created_on'),
    func.coalesce(tr_selected.title, tr_default.title).label('tr_title'),
    func.coalesce(tr_selected.slug, tr_default.slug).label('tr_slug')).\
    select_from(tr_where, ContentItem).\
    outerjoin(tr_selected, and_(
            (ContentItem.id == tr_selected.content_item_id),
            (tr_selected.published == 1),
            (tr_selected.content_item_parent_id == 0),
            (tr_selected.language_id == 3))).\
    outerjoin(tr_default, and_(
            (ContentItem.id == tr_default.content_item_id), 
            (tr_default.published == 1),
            (tr_default.content_item_parent_id == 0),
            (tr_default.language_id == 1))).\
    filter(and_(
        (ContentItem.id == tr_where.content_item_id),
        (ContentItem.content_item_type == 1),
        (tr_where.published == 1),
        (tr_where.content_item_parent_id == 0))).\
    having(literal_column('tr_title').isnot(None)).\
    distinct().\
    order_by(desc('tr_created_on')).all()

Ich musste das Internet durchsuchen, warum HAVING ich mit, MariaDB aber nicht mit SQLAlchemyihm arbeitete, siehe auch die untenstehenden Links. Wir brauchen hier die literal_column, das scheint nur bei MySQL / MariaDB (?) zu funktionieren. Ich kann das nicht bestätigen, ich verwende MariaDBnur .

Nehmen wir an, dass bei der Veröffentlichung einer Sprachübersetzung alles in Ordnung ist, d.h. alle Felder wurden mit einem Wert versehen. In diesem Fall müssen wir nur auf die Existenz eines Übersetzungsprotokolls prüfen. Glücklicherweise macht die obige Anfrage dies für uns. Wenn das Übersetzungsmodell ein Titelfeld mit dem Standardwert ''' (leere Zeichenkette) definiert, antwortet die Abfrage mit NULL für den Titel, wenn der Übersetzungssatz nicht existiert. Genau das, was wir wollen.

Die obige Abfrage gibt Attribute zurück, nicht Objekte. Ich kenne keine Möglichkeit, Objekte zurückzugeben, da die folgenden Änderungen fehlschlagen, da sie nur literale Werte zurückgeben können:

    func.coalesce(tr_selected, tr_default)

    case([(ContentItem.title == None, tr_default)],
        else_ = tr_selected),

Im Moment kenne ich keinen anderen Weg, als alle Attribute zur query(?) hinzuzufügen.

Eine weitere Möglichkeit, die Sprachrückführung zu implementieren.

Ich liebe einfache Select-Abfragen und die obige Outer-Join-Abfrage ist komplex, also lassen Sie uns versuchen, eine andere Methode zu finden, dies zu tun. Ich möchte, dass vollständige Objekte zurückgegeben werden, eine Liste von (content_item, content_item_translation) Tupeln, zur einfachen Verarbeitung in der Vorlage. Angenommen, die ausgewählte Sprache ist Deutsch und die Standardsprache ist Englisch. Unsere veröffentlichten Inhaltselemente können wie folgt aussehen:

+-----------------+-----------------------------+-----------------------------+
| content_item.id | content_item_translation.id | content_item_translation.id | 
|                 |            EN               |            DE               |
+-----------------+-----------------------------+-----------------------------+
|      7          |                             |                             |
|      6          |            6                |                             |
|      5          |            4                |            5                |
|      4          |                             |                             |
|      3          |            2                |            3                |
|      2          |            1                |                             |
|      1          |                             |                             |
+-----------------+-----------------------------+-----------------------------+

Nehmen wir an, dies sind Blog-Posts. Dann haben wir 4 Blog-Einträge auf Englisch (language_id=1) und 2 auf Deutsch (language_id=3). Gehen Sie auch davon aus, dass der Sprachfall immer durchgeführt wird und die Standardsprachelemente immer vorhanden sind. Dann ist die Gesamtzahl der Blogbeiträge nichts weiter als die Anzahl der Blogbeiträge für die Standardsprache zu zählen. Wir müssen sowohl die Tabellen content_item als auch content_item_translation verwenden.

# select EN titles
SELECT 
  ci.id as ci_id, ci_tr.id as ci_tr_id, ci_tr.title as ci_tr_title
  FROM content_item ci, content_item_translation ci_tr
  WHERE
    ci.content_item_type = 1
    AND ci_tr.content_item_id = ci.id
    AND ci_tr.language_id = 1
    AND ci_tr.published = 1
    AND ci_tr.content_item_parent_id = 0;

# count EN titles
SELECT 
  count( distinct ci_tr.id ) 
  FROM content_item ci, content_item_translation ci_tr
  WHERE
    ci.content_item_type = 1
    AND ci_tr.content_item_id = ci.id
    AND ci_tr.language_id = 1
    AND ci_tr.published = 1
    AND ci_tr.content_item_parent_id = 0;

# get content_item.id list for DE
SELECT 
  distinct ci.id
  FROM content_item ci, content_item_translation ci_tr
  WHERE
    ci.content_item_type = 1
    AND ci_tr.content_item_id = ci.id
    AND ci_tr.language_id = 3
    AND ci_tr.published = 1
    AND ci_tr.content_item_parent_id = 0;

# get content_item.id list for EN without DE
SELECT
  distinct ci.id
  FROM content_item ci, content_item_translation ci_tr
  WHERE
    ci.content_item_type = 1
    AND (ci_tr.content_item_id = ci.id
    AND ci_tr.language_id = 1
    AND ci_tr.published = 1
    AND ci_tr.content_item_parent_id = 0)
    AND ci.id NOT IN (
      SELECT
        distinct cis.id
        FROM content_item cis, content_item_translation cis_tr
        WHERE 
          cis.content_item_type = 1
          AND (cis_tr.content_item_id = cis.id
          AND cis_tr.language_id = 3
          AND cis_tr.published = 1
          AND cis_tr.content_item_parent_id = 0)
    );

Daraus folgt, dass, wenn die gewählte Sprache Deutsch ist, ein Rückfall erfolgen muss, wenn die content_item.id englische, aber keine deutschen Übersetzungen hat. Mit Hilfe einer Vereinigung können wir die Artikel für Deutsch und Englisch beziehen, zusammenführen und bestellen. Wir können UNION_ALL hier verwenden, weil wir wissen, dass die deutschen und englischen Elemente getrennt sind. Am Ende haben wir eine ORDER BY Sortierung der Ergebnisse vorgenommen.

# get german items
SELECT ci.id as ci_id, 
        ci_tr.id as ci_tr_id, 
        ci_tr.created_on as ci_tr_created_on, 
        ci_tr.title as ci_tr_title
  FROM content_item ci, content_item_translation ci_tr
  WHERE
    ci.content_item_type = 1
    AND ci_tr.content_item_id = ci.id
    AND ci_tr.language_id = 3
    AND ci_tr.published = 1
    AND ci_tr.content_item_parent_id = 0

UNION ALL

# add english items
SELECT ci.id as ci_id, 
        ci_tr.id as ci_tr_id, 
        ci_tr.created_on as ci_tr_created_on, 
        ci_tr.title as ci_tr_title
  FROM content_item ci, content_item_translation ci_tr
  WHERE
    ci.content_item_type = 1
    AND ci_tr.content_item_id = ci.id
  AND ci.id IN (
    SELECT
      distinct cid.id
      FROM content_item cid, content_item_translation cid_tr
      WHERE
      cid.content_item_type = 1
      AND (cid_tr.content_item_id = cid.id
      AND cid_tr.language_id = 1
      AND cid_tr.published = 1
      AND cid_tr.content_item_parent_id = 0)
      AND cid.id NOT IN (
        SELECT
        distinct cis.id
        FROM content_item cis, content_item_translation cis_tr
        WHERE 
          cis.content_item_type = 1
          AND (cis_tr.content_item_id = cis.id
          AND cis_tr.language_id = 3
          AND cis_tr.published = 1
          AND cis_tr.content_item_parent_id = 0)
      )
  )

ORDER BY ci_tr_created_on DESC;

Wir können auch Seitenzahlen hinzufügen, indem wir der Abfrage LIMIT 2 OFFSET 1 hinzufügen. Das funktioniert. Ist es besser als die Outer-Join-Abfrage? Einige Tests haben gezeigt, dass es mindestens zweimal langsamer ist. Aber es ist einfach, was auch ein großes Plus ist. Und es ist nun möglich, Objekte aus der SQLAlchemy Query anstelle von Attributen zu erhalten. Genau das, was ich wollte, deshalb bin ich zum "Objekt" gegangen. Um die Performance zu verbessern, können wir die Query-Ergebnisse zwischenspeichern, sie ändern sich nicht oft. Aber das kann später geschehen. Die entsprechende SQLAlchemy Abfrage lautet:

    # content_item_type is a constant, 1 = blog post
    # default language is english (id = 1)
    # language_id is the id of the selected language
    ci, ci_tr = aliased(ContentItem), aliased(ContentItemTranslation)
    s1 = db.query(ci, ci_tr).\
            filter(and_(\
                (ci.content_item_type == content_item_type),
                (ci_tr.content_item_id == ci.id),
                (ci_tr.language_id == language.id),
                (ci_tr.published == 1),
                (ci_tr.content_item_parent_id == 0),
                ))

    cisub, cisub_tr = aliased(ContentItem), aliased(ContentItemTranslation)
    s2_subquery = db.query(cisub.id).\
            filter(and_(\
                (cisub.content_item_type == content_item_type),
                (cisub_tr.content_item_id == cisub.id),
                (cisub_tr.language_id == language.id),
                (cisub_tr.published == 1),
                (cisub_tr.content_item_parent_id == 0)))
    
    s2 = db.query(ci, ci_tr).\
            filter(and_(\
                (ci.content_item_type == content_item_type),
                (ci_tr.content_item_id == ci.id),
                (ci_tr.language_id == 1),
                (ci_tr.published == 1),
                (ci_tr.content_item_parent_id == 0),
                (ci.id.notin_( s2_subquery ))))

    q = s1.union(s2).order_by(desc(ci.created_on))

    content_item_content_item_translation_tuples = q.all()

Ich bin glücklich, weil die Unionsabfrage Tupel mit Objekten zurückgibt, die ohne Manipulation an die Jinja Vorlage übergeben werden können. Und um die Blog-Posts für die Seitennavigation zu erhalten, fügen wir der Abfrage einfach die Funktionen Offset und Limit hinzu. Das Erhalten der Gesamtzahl der Blog-Einträge ist einfach das Erhalten der Anzahl der Blog-Einträge für die Standardsprache.

Auch hier gibt es etwas Verwirrendes. Sie können durch die created_on des Inhaltselements oder die created on der content item Übersetzung created_on bestellen. Wenn du den letzteren verwendest, können deine Blog-Posts durcheinander geraten. Also im Moment bestelle ich von der created_on des blog_post, nicht von den übersetzten Versionen.

Um den Beweis zu haben, dass es funktioniert, habe ich zwei deutsche und einen spanischen Blog-Post hinzugefügt. Im Moment bin ich noch dabei zu konvertieren, zum Zeitpunkt des Schreibens dieses Beitrags habe ich nur den Titel, den Untertitel und die Metadaten übersetzt. Überprüfen Sie Deutsch und Spanisch, um Änderungen zu sehen.

Zusammenfassung

Dieser Beitrag beschreibt eine Möglichkeit, mehrsprachige Unterstützung für Inhaltselemente wie Blogbeiträge und Seiten hinzuzufügen. Die hinzugefügte Übersetzungstabelle für jede Tabelle, die Übersetzungen benötigt, macht die Sache komplizierter. Ich bin ein wenig besorgt über den Speicherverbrauch (und die Leistung), wenn die Tabelle der Inhaltselemente wächst. Aber wir können das Zwischenspeichern von Abfrageergebnissen (z.B. 1-5 Minuten) verwenden, da sich diese Werte nicht oft ändern.

Bei der Erstellung von Abfragen beginne ich oft mit der Verwendung SQL, und transformiere diese dann später in SQLAlchemy, aber ich bin nicht die Einzige.

In einem der (sehr) nächsten Beiträge kann ich ein Diagramm der beteiligten Modelle zeigen. Wenn Sie frühere Beiträge gelesen haben, wird dies nicht überraschen. Das Inhaltselement steht in einer Eins-zu-Viele-Beziehung zu seinem Übersetzungsmodell Content-Item-Übersetzung. Das Inhalts-Item-Modell steht in vielfältigen Beziehungen zu den Inhalts-Item-Kategorie- und Inhalts-Item-Tag-Modellen, die beide auch Übersetzungstabellen haben. Und das Content-Element-Kategoriemodell steht in einer Beziehung von einem zu mehreren mit seinem Übersetzungsmodell Content-Element-Kategorie-Übersetzung. Etc.

Links / Impressum

Aliasing field names in SQLAlchemy model or underlying SQL table
https://stackoverflow.com/questions/37420135/aliasing-field-names-in-sqlalchemy-model-or-underlying-sql-table?rq=1

How to Design a Localization-Ready System
https://www.vertabelo.com/blog/technical-articles/data-modeling-for-multiple-languages-how-to-design-a-localization-ready-system

Multi language database, with default fallback
https://stackoverflow.com/questions/26765175/multi-language-database-with-default-fallback

Multilanguage Database Design in MySQL
https://www.apphp.com/tutorials/index.php?page=multilanguage-database-design-in-mysql

python sqlalchemy label usage
https://stackoverflow.com/questions/15555920/python-sqlalchemy-label-usage

Schema for a multilanguage database
https://stackoverflow.com/questions/316780/schema-for-a-multilanguage-database

sqlalchemy IS NOT NULL select
https://stackoverflow.com/questions/21784851/sqlalchemy-is-not-null-select/37196866

SQLAlchemy reference label in Having clause
https://stackoverflow.com/questions/51793704/sqlalchemy-reference-label-in-having-clause

Storing multilingual records in the MySQL database
https://php.vrana.cz/storing-multilingual-records-in-the-mysql-database.php

Using Language Identifiers (RFC 3066)
http://www.i18nguy.com/unicode/language-identifiers.html

What's the best database structure to keep multilingual data? [duplicate]
https://stackoverflow.com/questions/2227985/whats-the-best-database-structure-to-keep-multilingual-data

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.