Smart locale fallback chains for python-i18n -- because pt-BR users deserve pt-PT, not English
Project description
python-i18n-locale-chain
Smart locale fallback chains for python-i18n -- because pt-BR users deserve pt-PT, not English.
The Problem
The python-i18n library supports a single fallback locale. When a translation key is missing in the active locale, it jumps directly to the fallback (typically "en"). There is no intermediate fallback.
Example: Your app sets locale = "pt-BR". You have pt-PT translations but no pt-BR locale files. python-i18n skips pt-PT entirely and shows the English fallback.
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 function call. Zero changes to your existing translation code.
python-i18n-locale-chain wraps i18n.t() with a chain-aware resolver that walks a sequence of fallback locales before reaching the default language. Your existing translation calls just work:
i18n.t('greeting')-- looks up the active locale, then its fallback chain, then the default localei18n.t('greeting', locale='pt-BR')-- walks the pt-BR chain: pt-PT -> pt -> eni18n.t('items', count=5)-- kwargs are passed through to the resolved translation
Installation
pip install python-i18n-locale-chain
Quick Start
1. Install the package
pip install python-i18n-locale-chain
2. Configure fallback chains
from i18n_locale_chain import configure
configure()
3. Use i18n.t() as normal
import i18n
i18n.set("file_format", "json")
i18n.load_path.append("translations/")
# Chain fallback is automatic -- pt-BR falls back to pt-PT, then pt, then en
result = i18n.t("greeting", locale="pt-BR")
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.
Example
A runnable example is included in the example/ directory. It demonstrates fallback resolution for pt-BR with three translation files of decreasing coverage:
pt-BR.jsonhas onlygreetingpt.jsonhasgreetingandfarewellen.jsonhas all three keys
pip install -e .
cd example
python main.py
Output:
greeting: Oi # resolved from pt-BR (direct match)
farewell: Adeus # fell back to pt (pt-BR -> pt-PT -> pt)
welcome: Welcome to LocaleChain # fell back to en (pt-BR -> pt-PT -> pt -> en)
Configuration Modes
Default (zero config)
Just call configure(). Uses all 75 built-in fallback chains covering Chinese, Portuguese, Spanish, French, German, Italian, Dutch, English, Arabic, Norwegian, and Malay regional variants.
from i18n_locale_chain import configure
configure()
Override specific chains
from i18n_locale_chain import configure
# Merge overrides on top of defaults
configure(overrides={
"pt-BR": ["pt"], # Skip pt-PT, go straight to pt
"ja-JP": ["ja"], # Add a new chain
})
Full custom map
from i18n_locale_chain import configure
# 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)
Custom default locale
from i18n_locale_chain import configure
# Use German as the final fallback instead of English
configure(default_locale="de")
Restore original behaviour
from i18n_locale_chain import reset
# Remove chain-aware wrapper and restore original i18n.t()
reset()
API Reference
configure(overrides=None, fallbacks=None, merge_defaults=True, default_locale="en")
Activate chain-aware translation lookup.
| Parameter | Type | Default | Description |
|---|---|---|---|
overrides |
dict | None |
None |
Additional or replacement chains merged on top of defaults |
fallbacks |
dict | None |
None |
A complete fallback map |
merge_defaults |
bool |
True |
Whether to include defaults when fallbacks is supplied |
default_locale |
str |
"en" |
The locale to try as a last resort |
Calling conventions:
configure()-- use DEFAULT_FALLBACKS with"en"as final fallback.configure(overrides={...})-- merge overrides on top of defaults.configure(fallbacks={...}, merge_defaults=False)-- use only the supplied fallbacks, ignoring defaults entirely.
reset()
Restore the original i18n.t and clear chain state. Safe to call multiple times.
DEFAULT_FALLBACKS
A dict[str, list[str]] containing all 75 built-in fallback chains. Importable for inspection or as a base for custom maps.
merge_fallbacks(base, overrides)
Merge two fallback maps, returning a new dict. Entries in overrides replace same-key entries in base. Neither input is mutated.
Default Fallback Map
Chinese (Traditional)
| Locale | Fallback Chain |
|---|---|
| zh-Hant-HK | zh-Hant-TW -> zh-Hant -> (default) |
| zh-Hant-MO | zh-Hant-HK -> zh-Hant-TW -> zh-Hant -> (default) |
| zh-Hant-TW | zh-Hant -> (default) |
Chinese (Simplified)
| Locale | Fallback Chain |
|---|---|
| zh-Hans-SG | zh-Hans -> (default) |
| zh-Hans-MY | zh-Hans -> (default) |
Portuguese
| Locale | Fallback Chain |
|---|---|
| pt-BR | pt-PT -> pt -> (default) |
| pt-PT | pt -> (default) |
| pt-AO | pt-PT -> pt -> (default) |
| pt-MZ | pt-PT -> pt -> (default) |
Spanish
| Locale | Fallback Chain |
|---|---|
| es-419 | es -> (default) |
| es-MX | es-419 -> es -> (default) |
| es-AR | es-419 -> es -> (default) |
| es-CO | es-419 -> es -> (default) |
| es-CL | es-419 -> es -> (default) |
| es-PE | es-419 -> es -> (default) |
| es-VE | es-419 -> es -> (default) |
| es-EC | es-419 -> es -> (default) |
| es-GT | es-419 -> es -> (default) |
| es-CU | es-419 -> es -> (default) |
| es-BO | es-419 -> es -> (default) |
| es-DO | es-419 -> es -> (default) |
| es-HN | es-419 -> es -> (default) |
| es-PY | es-419 -> es -> (default) |
| es-SV | es-419 -> es -> (default) |
| es-NI | es-419 -> es -> (default) |
| es-CR | es-419 -> es -> (default) |
| es-PA | es-419 -> es -> (default) |
| es-UY | es-419 -> es -> (default) |
| es-PR | es-419 -> es -> (default) |
French
| Locale | Fallback Chain |
|---|---|
| fr-CA | fr -> (default) |
| fr-BE | fr -> (default) |
| fr-CH | fr -> (default) |
| fr-LU | fr -> (default) |
| fr-MC | fr -> (default) |
| fr-SN | fr -> (default) |
| fr-CI | fr -> (default) |
| fr-ML | fr -> (default) |
| fr-CM | fr -> (default) |
| fr-MG | fr -> (default) |
| fr-CD | fr -> (default) |
German
| Locale | Fallback Chain |
|---|---|
| de-AT | de -> (default) |
| de-CH | de -> (default) |
| de-LU | de -> (default) |
| de-LI | de -> (default) |
Italian
| Locale | Fallback Chain |
|---|---|
| it-CH | it -> (default) |
Dutch
| Locale | Fallback Chain |
|---|---|
| nl-BE | nl -> (default) |
English
| Locale | Fallback Chain |
|---|---|
| en-GB | en -> (default) |
| en-AU | en-GB -> en -> (default) |
| en-NZ | en-AU -> en-GB -> en -> (default) |
| en-IN | en-GB -> en -> (default) |
| en-CA | en -> (default) |
| en-ZA | en-GB -> en -> (default) |
| en-IE | en-GB -> en -> (default) |
| en-SG | en-GB -> en -> (default) |
Arabic
| Locale | Fallback Chain |
|---|---|
| ar-SA | ar -> (default) |
| ar-EG | ar -> (default) |
| ar-AE | ar -> (default) |
| ar-MA | ar -> (default) |
| ar-DZ | ar -> (default) |
| ar-IQ | ar -> (default) |
| ar-KW | ar -> (default) |
| ar-QA | ar -> (default) |
| ar-BH | ar -> (default) |
| ar-OM | ar -> (default) |
| ar-JO | ar -> (default) |
| ar-LB | ar -> (default) |
| ar-TN | ar -> (default) |
| ar-LY | ar -> (default) |
| ar-SD | ar -> (default) |
| ar-YE | ar -> (default) |
Norwegian
| Locale | Fallback Chain |
|---|---|
| nb | no -> (default) |
| nn | nb -> no -> (default) |
Malay
| Locale | Fallback Chain |
|---|---|
| ms-MY | ms -> (default) |
| ms-SG | ms -> (default) |
| ms-BN | ms -> (default) |
How It Works
configure()saves a reference to the originali18n.t()function and replaces it with a chain-aware wrapper.- When
i18n.t("key", locale="pt-BR")is called, the wrapper first tries the requested locale directly. - If the key is missing, it walks the fallback chain for that locale (e.g.,
pt-PT->pt). - If no chain locale has the key, it tries the configured default locale (e.g.,
"en"). - If the key is still not found, it falls through to the original
i18n.t()behaviour (which produces a missing-key marker). reset()restores the originali18n.t()and clears all chain state.
FAQ
Is this production-ready?
Yes. The library wraps i18n.t() with a thin chain-aware resolver. It uses python-i18n's public translations.has() API to check key existence and delegates all actual translation to the original i18n.t().
Performance impact?
Minimal. For each call, the wrapper checks translations.has() for the requested locale and then for each fallback in the chain. These are dictionary lookups into python-i18n's in-memory translation store. No file I/O or parsing happens during lookup.
Does it work with YAML and JSON translation files? Yes. This library operates on python-i18n's in-memory translation store, which is populated from YAML, JSON, or programmatically added translations. Any format that python-i18n supports will work.
Can I use a non-English default locale?
Yes. Pass default_locale="de" (or any locale) to configure(). The default locale is only used as a last resort when neither the requested locale nor any fallback in its chain has the key.
Can I deactivate it?
Yes. Call reset() to restore the original i18n.t() and remove all chain configuration.
Does it work with python-i18n's built-in fallback?
Yes. The chain-aware wrapper temporarily disables python-i18n's own fallback setting during each lookup to prevent conflicts, then restores it. This ensures the chain order is respected without interfering with python-i18n's internals.
Minimum Python version? Python 3.8. No additional dependencies beyond python-i18n itself.
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 python_i18n_locale_chain-1.0.0.tar.gz.
File metadata
- Download URL: python_i18n_locale_chain-1.0.0.tar.gz
- Upload date:
- Size: 14.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1598348b8898ff376600220daa7cca1315b91e7940b2b3c17753683d8759c750
|
|
| MD5 |
497b112c952d895464e3f2b0bce7c622
|
|
| BLAKE2b-256 |
a6f132db28f8d1fb26b731a3335a82adcd308f86e86d320b2543064b0efe5ef0
|
File details
Details for the file python_i18n_locale_chain-1.0.0-py3-none-any.whl.
File metadata
- Download URL: python_i18n_locale_chain-1.0.0-py3-none-any.whl
- Upload date:
- Size: 9.0 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 |
bbacf1510294fda7f3860d4b96ef318707a9c7efeb6a4c49557e40dac37ca4bb
|
|
| MD5 |
9408f9ca7580727abd23dc8ab4c9d5e7
|
|
| BLAKE2b-256 |
b9d3a4d1f0e0179835da58cd648672d2c096ad9349b7727af243befa1638f5f1
|