Skip to content

Internationalization

The gallery ships translations for Spanish (es), Basque/Euskara (eu), and French (fr). English is the source language — strings live untranslated in the templates and the .po msgid is the English text.

How it works at runtime

  1. LocaleMiddleware (in MIDDLEWARE, positioned after SessionMiddleware and before CommonMiddleware) inspects the user's Accept-Language header.
  2. Django picks the best match from LANGUAGES (defined in the consumer's settings).
  3. Strings wrapped in {% trans %} / {% blocktranslate %} are looked up in the .mo for that language.
  4. Misses fall back to the LANGUAGE_CODE default.

The library's translations live under src/django_cotton_gallery/locale/<lang>/LC_MESSAGES/django.{po,mo}. Django auto-discovers them because django_cotton_gallery is in the consumer's INSTALLED_APPS. No LOCALE_PATHS setting required.

Adding a new translatable string

When you add a {% trans "X" %} or {% blocktranslate %}...{% endblocktranslate %} block to a template, the .po files don't know about it yet. Three options to add the translation:

Option A — Makefile targets (canonical)

The repo ships Makefile targets that wrap Django's makemessages / compilemessages and require system gettext (apt install gettext on Debian/Ubuntu, brew install gettext on macOS):

make messages          # extract new msgids into the .po files (es / eu / fr)
# Edit the .po files: replace the empty msgstr "" with the translation.
make compile-messages  # compile .po → .mo

Under the hood these cd into src/django_cotton_gallery/ (the directory that contains locale/, so only our package catalogs are scanned) and run python -m django makemessages / compilemessages for es / eu / fr. If you need to drive it by hand:

cd src/django_cotton_gallery
uv run --project ../.. python -m django makemessages -l es -l eu -l fr --extension=html,py,txt
uv run --project ../.. python -m django compilemessages -l es -l eu -l fr

Note: use python -m django, not django-admin — the latter is not on PATH in the uv environment.

Option B — polib (optional fallback, no system gettext)

If you can't install system gettext, use the polib Python library instead (already in [dev]) to edit and compile the catalogs directly:

import polib
from pathlib import Path

NEW_STRINGS = {
    "Click me": {
        "es": "Hacé clic",
        "eu": "Egin klik",
        "fr": "Cliquez ici",
    },
}

base = Path("src/django_cotton_gallery/locale")
for lang in ["es", "eu", "fr"]:
    po_path = base / lang / "LC_MESSAGES" / "django.po"
    mo_path = base / lang / "LC_MESSAGES" / "django.mo"
    po = polib.pofile(str(po_path))
    for msgid, langs in NEW_STRINGS.items():
        if po.find(msgid) is None:
            po.append(polib.POEntry(msgid=msgid, msgstr=langs[lang]))
    po.save(str(po_path))
    po.save_as_mofile(str(mo_path))

Option C — edit the .po directly + recompile

Manually edit src/django_cotton_gallery/locale/<lang>/LC_MESSAGES/django.po to add the entry, then recompile:

import polib
po = polib.pofile("src/django_cotton_gallery/locale/es/LC_MESSAGES/django.po")
po.save_as_mofile("src/django_cotton_gallery/locale/es/LC_MESSAGES/django.mo")

Verifying translations applied

After updating .mo files, restart the dev server and hit the page with the right Accept-Language:

curl -s -H "Accept-Language: es" http://localhost:8000/django-cotton-gallery/get-started/ | grep -o 'Empezar\|De cero a tu primer componente'

Or open the page in a browser, change the system language preference, and refresh.

For a visual check, capture screenshots in each language with headless Chrome:

google-chrome --headless=new --disable-gpu --hide-scrollbars --window-size=1600,1100 \
  --virtual-time-budget=8000 --lang=eu --accept-lang=eu \
  --screenshot=/tmp/gallery-eu.png http://localhost:8000/django-cotton-gallery/

Adding a new language

  1. Create the directory:
mkdir -p src/django_cotton_gallery/locale/<code>/LC_MESSAGES
  1. Copy an existing .po as a starting point:
cp src/django_cotton_gallery/locale/es/LC_MESSAGES/django.po \
   src/django_cotton_gallery/locale/<code>/LC_MESSAGES/django.po
  1. Update the header of the new .po: Language: <code>\n and Content-Language: <code>.

  2. Translate every msgstr in the file — all UI strings.

  3. Compile:

import polib
po = polib.pofile("src/django_cotton_gallery/locale/<code>/LC_MESSAGES/django.po")
po.save_as_mofile("src/django_cotton_gallery/locale/<code>/LC_MESSAGES/django.mo")
  1. Tell consumers about it by adding the language to the LANGUAGES setting in their project. Until they add it, Django won't expose it in set_language even though the .mo is shipped.

  2. Document the new language in the README and on the docs site.

Common gotchas

  • Translation doesn't apply in the browser → check that LocaleMiddleware is in MIDDLEWARE. Without it, Django never reads Accept-Language.
  • Translation is in the .po but the page still shows English → the .mo is stale. Recompile.
  • makemessages errors with "no .po files found" → run it from src/django_cotton_gallery/ (the directory that contains locale/), not the repo root. The make messages target already cds there for you.
  • Plural forms render as singular → check that Plural-Forms is set in the .po header (e.g. nplurals=2; plural=(n != 1); for languages like es/en/fr).
  • Euskera translations need review — the maintainer doesn't speak Basque fluently. Native-speaker contributions welcome.