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.
Three I18n Patterns
Section titled “Three I18n Patterns”Pattern A: JSON Field
Section titled “Pattern A: JSON Field”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.
Pattern B: T9N Table
Section titled “Pattern B: T9N Table”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.
Pattern C: One Record Per Language
Section titled “Pattern C: One Record Per Language”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.
Comparison
Section titled “Comparison”| Pattern | Query per language | Records per item | Fits |
|---|---|---|---|
| JSON field | no | one | a short text, loaded whole |
| T9N table | yes | one row per language | several translated fields |
| One record per language | yes | one record per language | content written separately per language |
Pick the simplest one that fits.
The Fallback Chain
Section titled “The Fallback Chain”- The requested language.
- The default language of the channel, from the module’s own Channel model.
- 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.
Channels and Languages
Section titled “Channels and Languages”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.
Adding Language Support to a Module
Section titled “Adding Language Support to a Module”- Text that varies by language. Pick a storage pattern above.
- The fallback. Give the module’s Channel a
default_languageand resolve in the order of the chain. - 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.