Configurable locale fallback chains for Django's i18n system
Project description
django-locale-chain
Smart locale fallback chains for Django -- because pt-BR users deserve pt-PT, not English.
The Problem
Django's translation system falls back directly to the project's LANGUAGE_CODE when a regional locale variant is missing. There is no intermediate fallback. This is a long-standing limitation (Django Ticket #28636).
Example: A user's browser sends Accept-Language: pt-BR. Your Django project has pt-PT translations but no pt-BR locale. Django skips pt-PT entirely and shows English (or whatever your LANGUAGE_CODE is).
The same thing happens with es-MX -> es, fr-CA -> fr, de-AT -> de, and every other regional variant.
Your users see English when a perfectly good translation exists in a sibling locale.
The Solution
One middleware. Zero changes to your existing translation code.
django-locale-chain installs gettext fallback chains using Python's native gettext.GNUTranslations.add_fallback mechanism. Missing keys in the primary locale catalogue are resolved from fallback locales before reaching the project's default language. Your existing translation calls just work:
{% trans "key" %}in templatesgettext("key")and_("key")in Python codengettext()pluralization- All
django.utils.translationfunctions
Installation
pip install django-locale-chain
Quick Start
1. Add the middleware
Add LocaleChainMiddleware to your MIDDLEWARE setting after Django's LocaleMiddleware:
# settings.py
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django_locale_chain.middleware.LocaleChainMiddleware", # <-- add this
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
That's it. All 75 default fallback chains are active. A pt-BR user will now see pt-PT translations when pt-BR is not available.
2. (Optional) Configure custom chains via settings
# settings.py
LOCALE_FALLBACK_CHAINS = {
"pt-BR": ["pt-PT", "pt"],
"es-MX": ["es-419", "es"],
"fr-CA": ["fr"],
}
Your custom chains are merged with the defaults. Keys you specify replace the corresponding default chain.
3. (Optional) Configure programmatically
# In your AppConfig.ready() hook
from django_locale_chain import configure
class MyAppConfig(AppConfig):
name = "myapp"
def ready(self):
configure()
Configuration Modes
Default (zero config)
Just add the middleware. Uses all 75 built-in fallback chains covering Chinese, Portuguese, Spanish, French, German, Italian, Dutch, English, Arabic, Norwegian, and Malay regional variants.
Django settings
# settings.py
# Custom chains merged with defaults
LOCALE_FALLBACK_CHAINS = {
"pt-BR": ["pt-PT", "pt"],
"ja-JP": ["ja"],
}
Programmatic API
from django_locale_chain import configure, reset
# Zero-config -- all 75 default chains
configure()
# Override specific chains while keeping all defaults
configure(overrides={"pt-BR": ["pt"]})
# Full custom map, merged with defaults
configure(fallbacks={"ja-JP": ["ja"]})
# Full custom map, no defaults
configure(fallbacks={"pt-BR": ["pt-PT"]}, merge_defaults=False)
# Restore default behaviour
reset()
API Reference
configure(overrides=None, fallbacks=None, merge_defaults=True)
Activate locale fallback chains programmatically.
| Parameter | Type | Default | Description |
|---|---|---|---|
overrides |
dict | None |
None |
Additional or replacement chains merged on top of defaults. Ignored when fallbacks is provided. |
fallbacks |
dict | None |
None |
A complete fallback map. When given, overrides is ignored. |
merge_defaults |
bool |
True |
Whether to include the 75 built-in defaults as a base layer |
Returns the resolved dict[str, list[str]] fallback map.
reset()
Remove all custom configuration and restore default behaviour. Safe to call multiple times.
LocaleChainMiddleware
Django middleware class. Place after django.middleware.locale.LocaleMiddleware in MIDDLEWARE. Reads configuration from configure(), the LOCALE_FALLBACK_CHAINS setting, or the built-in defaults (in that priority order).
DEFAULT_FALLBACKS
A dict[str, list[str]] containing all 75 built-in fallback chains. Importable from django_locale_chain.fallback_map for inspection or as a base for custom maps.
merge_fallbacks(overrides=None, base=None, merge_defaults=True)
Merge two fallback maps, returning a new dict. Entries in overrides replace same-key entries in base. Neither input is mutated.
Django Settings Reference
| Setting | Type | Description |
|---|---|---|
LOCALE_FALLBACK_CHAINS |
dict[str, list[str]] |
Custom fallback chains, merged with built-in defaults |
Priority order (highest to lowest):
configure()call (programmatic API)LOCALE_FALLBACK_CHAINSDjango setting- Built-in defaults (zero-config)
Default Fallback Map
Chinese (Traditional)
| Locale | Fallback Chain |
|---|---|
| zh-Hant-HK | zh-Hant-TW -> zh-Hant -> (LANGUAGE_CODE) |
| zh-Hant-MO | zh-Hant-HK -> zh-Hant-TW -> zh-Hant -> (LANGUAGE_CODE) |
| zh-Hant-TW | zh-Hant -> (LANGUAGE_CODE) |
Chinese (Simplified)
| Locale | Fallback Chain |
|---|---|
| zh-Hans-SG | zh-Hans -> (LANGUAGE_CODE) |
| zh-Hans-MY | zh-Hans -> (LANGUAGE_CODE) |
Portuguese
| Locale | Fallback Chain |
|---|---|
| pt-BR | pt-PT -> pt -> (LANGUAGE_CODE) |
| pt-PT | pt -> (LANGUAGE_CODE) |
| pt-AO | pt-PT -> pt -> (LANGUAGE_CODE) |
| pt-MZ | pt-PT -> pt -> (LANGUAGE_CODE) |
Spanish
| Locale | Fallback Chain |
|---|---|
| es-419 | es -> (LANGUAGE_CODE) |
| es-MX | es-419 -> es -> (LANGUAGE_CODE) |
| es-AR | es-419 -> es -> (LANGUAGE_CODE) |
| es-CO | es-419 -> es -> (LANGUAGE_CODE) |
| es-CL | es-419 -> es -> (LANGUAGE_CODE) |
| es-PE | es-419 -> es -> (LANGUAGE_CODE) |
| es-VE | es-419 -> es -> (LANGUAGE_CODE) |
| es-EC | es-419 -> es -> (LANGUAGE_CODE) |
| es-GT | es-419 -> es -> (LANGUAGE_CODE) |
| es-CU | es-419 -> es -> (LANGUAGE_CODE) |
| es-BO | es-419 -> es -> (LANGUAGE_CODE) |
| es-DO | es-419 -> es -> (LANGUAGE_CODE) |
| es-HN | es-419 -> es -> (LANGUAGE_CODE) |
| es-PY | es-419 -> es -> (LANGUAGE_CODE) |
| es-SV | es-419 -> es -> (LANGUAGE_CODE) |
| es-NI | es-419 -> es -> (LANGUAGE_CODE) |
| es-CR | es-419 -> es -> (LANGUAGE_CODE) |
| es-PA | es-419 -> es -> (LANGUAGE_CODE) |
| es-UY | es-419 -> es -> (LANGUAGE_CODE) |
| es-PR | es-419 -> es -> (LANGUAGE_CODE) |
French
| Locale | Fallback Chain |
|---|---|
| fr-CA | fr -> (LANGUAGE_CODE) |
| fr-BE | fr -> (LANGUAGE_CODE) |
| fr-CH | fr -> (LANGUAGE_CODE) |
| fr-LU | fr -> (LANGUAGE_CODE) |
| fr-MC | fr -> (LANGUAGE_CODE) |
| fr-SN | fr -> (LANGUAGE_CODE) |
| fr-CI | fr -> (LANGUAGE_CODE) |
| fr-ML | fr -> (LANGUAGE_CODE) |
| fr-CM | fr -> (LANGUAGE_CODE) |
| fr-MG | fr -> (LANGUAGE_CODE) |
| fr-CD | fr -> (LANGUAGE_CODE) |
German
| Locale | Fallback Chain |
|---|---|
| de-AT | de -> (LANGUAGE_CODE) |
| de-CH | de -> (LANGUAGE_CODE) |
| de-LU | de -> (LANGUAGE_CODE) |
| de-LI | de -> (LANGUAGE_CODE) |
Italian
| Locale | Fallback Chain |
|---|---|
| it-CH | it -> (LANGUAGE_CODE) |
Dutch
| Locale | Fallback Chain |
|---|---|
| nl-BE | nl -> (LANGUAGE_CODE) |
English
| Locale | Fallback Chain |
|---|---|
| en-GB | en -> (LANGUAGE_CODE) |
| en-AU | en-GB -> en -> (LANGUAGE_CODE) |
| en-NZ | en-AU -> en-GB -> en -> (LANGUAGE_CODE) |
| en-IN | en-GB -> en -> (LANGUAGE_CODE) |
| en-CA | en -> (LANGUAGE_CODE) |
| en-ZA | en-GB -> en -> (LANGUAGE_CODE) |
| en-IE | en-GB -> en -> (LANGUAGE_CODE) |
| en-SG | en-GB -> en -> (LANGUAGE_CODE) |
Arabic
| Locale | Fallback Chain |
|---|---|
| ar-SA | ar -> (LANGUAGE_CODE) |
| ar-EG | ar -> (LANGUAGE_CODE) |
| ar-AE | ar -> (LANGUAGE_CODE) |
| ar-MA | ar -> (LANGUAGE_CODE) |
| ar-DZ | ar -> (LANGUAGE_CODE) |
| ar-IQ | ar -> (LANGUAGE_CODE) |
| ar-KW | ar -> (LANGUAGE_CODE) |
| ar-QA | ar -> (LANGUAGE_CODE) |
| ar-BH | ar -> (LANGUAGE_CODE) |
| ar-OM | ar -> (LANGUAGE_CODE) |
| ar-JO | ar -> (LANGUAGE_CODE) |
| ar-LB | ar -> (LANGUAGE_CODE) |
| ar-TN | ar -> (LANGUAGE_CODE) |
| ar-LY | ar -> (LANGUAGE_CODE) |
| ar-SD | ar -> (LANGUAGE_CODE) |
| ar-YE | ar -> (LANGUAGE_CODE) |
Norwegian
| Locale | Fallback Chain |
|---|---|
| nb | no -> (LANGUAGE_CODE) |
| nn | nb -> no -> (LANGUAGE_CODE) |
Malay
| Locale | Fallback Chain |
|---|---|
| ms-MY | ms -> (LANGUAGE_CODE) |
| ms-SG | ms -> (LANGUAGE_CODE) |
| ms-BN | ms -> (LANGUAGE_CODE) |
Example
A working Django project is included in the example/ directory:
cd example && pip install -r requirements.txt && python manage.py runserver 8111
Then test the fallback chain with:
curl -H "Accept-Language: pt-BR" http://localhost:8111/
See example/README.md for full details.
How It Works
- Django's
LocaleMiddlewareruns first and activates the user's preferred language viatranslation.activate(). LocaleChainMiddlewarereads the active language and looks up its fallback chain.- For each fallback locale in the chain, a
DjangoTranslationcatalogue is loaded and linked viagettext.GNUTranslations.add_fallback(). - The original terminal fallback (your
LANGUAGE_CODE) is preserved at the end of the chain. - Subsequent
gettext()/_()calls walk the chain automatically -- the fallback resolution is invisible to your application code. - Django caches translation catalogues per language per process, so the fallback wiring happens once per language and subsequent requests are virtually free.
FAQ
Is this production-ready?
Yes. The library uses Python's native gettext.GNUTranslations.add_fallback mechanism and Django's public DjangoTranslation API. No monkey-patching, no private API access beyond the standard _fallback attribute that is part of Python's gettext protocol.
Performance impact?
Negligible. Fallback catalogues are loaded once per language per process. After the initial wiring, gettext() calls walk the fallback chain with zero additional overhead from this library -- it is Python's built-in gettext resolution.
Does it work with .po and .mo files?
Yes. This library operates on Django's translation catalogues, which are compiled from .po files into .mo files. Any translation format that Django supports will work.
Can I use a non-English default locale?
Yes. The fallback chains are independent of your project's LANGUAGE_CODE. They only control which sibling locales are checked before the default language.
Can I deactivate it?
Yes. Call reset() to remove all custom configuration, or simply remove the middleware from your MIDDLEWARE setting.
Does it work with Django REST Framework? Yes. DRF uses Django's translation system under the hood, so fallback chains work automatically.
Minimum Django version? Django 4.2 (LTS). Also tested with Django 5.0 and 5.1.
Contributing
- Open issues for bugs or feature requests.
- PRs welcome, especially for adding new locale fallback chains.
- Run tests with:
pytest
License
MIT License - see LICENSE file.
Built by i18nagent.ai
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_locale_chain-1.0.0.tar.gz.
File metadata
- Download URL: django_locale_chain-1.0.0.tar.gz
- Upload date:
- Size: 20.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c4a4a108a676a282585c1696d28c5979789d47c0c2bcc14c929137ab33162477
|
|
| MD5 |
644d106cde16a22da78af3456874c7e4
|
|
| BLAKE2b-256 |
caa692b7283afa9069eda41a4d8cb9a66226cb7aaf828fc417fb2be5599e35c0
|
File details
Details for the file django_locale_chain-1.0.0-py3-none-any.whl.
File metadata
- Download URL: django_locale_chain-1.0.0-py3-none-any.whl
- Upload date:
- Size: 13.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66a28c8f897899ee1d5563d31ca4e707803ab8f38c7d9f745121a1b1cd5ef998
|
|
| MD5 |
d23bdd33bbbee24d4df5339408990063
|
|
| BLAKE2b-256 |
337b40f2add034bf5a81e9e284a9e44a419ee293af17c82ffca3bb68373bff64
|