Skip to content

Languages

A storefront request names a channel and a language, and every module on its path answers in that language or falls back. Modules store translations in one of three ways and resolve them in the same order. The shared language list and the _t9n naming: Concepts.

Every translation of a field sits in one JSON column, keyed by language code.

class AgreementVersion(BaseModel): # django_agreements
summary_t9n = models.JSONField(
default=dict, help_text='Checkbox label per language: {"en": "I accept...", "pl": "Akceptuję..."}'
)

Used by: Agreements for the checkbox label above, PIM (Feature.name_t9n), Checkout (name_t9n and description_t9n on a payment method) and ContentDB for blog authors, among others.

One read returns every language. The price is querying: finding the records whose Polish text contains a word means scanning JSON. It fits a short text that is always loaded whole, such as a consent label.

A separate table holds one row per record and language.

class DeliveryPointT9N(BaseModel): # django_deliverypoints
point = models.ForeignKey(
"django_deliverypoints.DeliveryPoint", on_delete=models.CASCADE, related_name="translations"
)
language = models.ForeignKey("django_regional.Language", on_delete=models.CASCADE, related_name="+")
name = models.CharField(max_length=200, blank=True, default="")
hint = models.TextField(blank=True, default="")
opening_hours = models.CharField(max_length=255, blank=True, default="")
class Meta:
constraints = [models.UniqueConstraint(fields=["point", "language"], name="unique_point_language")]

Used by: DeliveryPoints, FAQ (FaqGroupT9N, FaqItemT9N) and Matrix (ProductT9N).

Each translated field is a column, so it can be filtered and indexed per language. It costs a join. It fits several translated fields on one record.

The whole record is in one language, and each language has its own record.

class Draft(models.Model): # django_contentdb
channels = models.ManyToManyField("ContentChannel", related_name="drafts", blank=True)
language = models.ForeignKey("Language", on_delete=models.CASCADE, default=1)

Used by: ContentDB, for pages and posts. Language here is ContentDB’s own model, a copy of the shared list that sync_contentdb_languages fills. ContentChannel.default_language points at the same model.

Each language version is written by itself: the Polish page is a document, not a translation of the English one. It fits editorial content where the versions differ in more than words.

PatternQuery per languageRecords per itemFits
JSON fieldnoonea short text, loaded whole
T9N tableyesone row per languageseveral translated fields
One record per languageyesone record per languagecontent written separately per language

Pick the simplest one that fits.

  1. The requested language.
  2. The default language of the channel, from the module’s own Channel model.
  3. What the module has without a translation. With a T9N table that is the base record. With a JSON field there is no base record: Agreements returns the first language the field holds.

DeliveryPoints implements it in django_deliverypoints/services/point_service.py:

def resolve_translation(
point: DeliveryPoint, channel: DeliveryPointChannel | None = None, language_iso2: str | None = None
) -> dict:
base = {"name": point.name, "hint": point.hint, "opening_hours": point.opening_hours}
if not language_iso2:
return base
# Try requested language
t9n = DeliveryPointT9N.objects.filter(point=point, language__iso2__iexact=language_iso2).first()
if t9n:
return _t9n_to_dict(t9n, base)
# Try channel default language
if channel and channel.default_language:
t9n = DeliveryPointT9N.objects.filter(point=point, language=channel.default_language).first()
if t9n:
return _t9n_to_dict(t9n, base)
return base

_t9n_to_dict fills every empty translated field from the base record. The base record always has values, so a point with no translations and a channel with no default language still returns a name.

A module’s Channel model names the default language the chain falls back to, and for most modules the set of languages the channel supports. Which Channel model carries which language field, and how module channels are copied from PIM: Channels.

  1. Text that varies by language. Pick a storage pattern above.
  2. The fallback. Give the module’s Channel a default_language and resolve in the order of the chain.
  3. The base record. Keep usable values on it, so the last step of the chain always has something to return.

Who may call the API, as staff or with a token: Access and API Tokens.