flask-babel-locale-chain
Smart locale fallback chains for Flask-Babel -- because pt-BR users deserve pt-PT, not English.
The Problem
Flask-Babel's translation system falls back directly to the app's BABEL_DEFAULT_LOCALE when a regional locale variant is missing. There is no intermediate fallback.
Example: A user's browser sends Accept-Language: pt-BR. Your Flask app has pt-PT translations but no pt-BR locale. Flask-Babel skips pt-PT entirely and shows English (or whatever your BABEL_DEFAULT_LOCALE 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 extension. Zero changes to your existing translation code.
flask-babel-locale-chain wraps Flask-Babel with fallback chain support using Python's native gettext.GNUTranslations.add_fallback mechanism. Missing keys in the primary locale catalogue are resolved from fallback locales before reaching the app's default language. Your existing translation calls just work:
_("key")andgettext("key")in Python code{{ _("key") }}in Jinja2 templatesngettext()pluralization- All Flask-Babel translation functions
Installation
pip install flask-babel-locale-chain
Quick Start
1. Install the package
pip install flask-babel-locale-chain
2. Initialize the extension
from flask import Flask
from flask_locale_chain import LocaleChainBabel
app = Flask(__name__)
lcb = LocaleChainBabel(app)
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.
3. (Optional) Add custom chains via Flask config
# config.py or in your app factory
app.config["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.
4. Use _() / gettext() as normal
from flask_babel import gettext as _
@app.route("/")
def index():
return _("greeting") # Falls back through the chain automatically
App Factory Pattern
flask-babel-locale-chain supports Flask's application factory pattern with init_app():
from flask import Flask
from flask_locale_chain import LocaleChainBabel
lcb = LocaleChainBabel()
def create_app():
app = Flask(__name__)
app.config["BABEL_DEFAULT_LOCALE"] = "en"
app.config["LOCALE_FALLBACK_CHAINS"] = {
"pt-BR": ["pt-PT", "pt"],
}
lcb.init_app(app)
return app
You can also pass an existing Babel instance if you need to configure Babel separately:
from flask_babel import Babel
from flask_locale_chain import LocaleChainBabel
babel = Babel()
lcb = LocaleChainBabel()
def create_app():
app = Flask(__name__)
babel.init_app(app)
lcb.init_app(app, babel=babel)
return app
Configuration Modes
Default (zero config)
Just create the extension. Uses all 75 built-in fallback chains covering Chinese, Portuguese, Spanish, French, German, Italian, Dutch, English, Arabic, Norwegian, and Malay regional variants.
lcb = LocaleChainBabel(app)
Flask config
# Custom chains merged with defaults
app.config["LOCALE_FALLBACK_CHAINS"] = {
"pt-BR": ["pt-PT", "pt"],
"ja-JP": ["ja"],
}
Programmatic API
from flask_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
LocaleChainBabel(app=None, babel=None)
Flask extension class.
| Parameter | Type | Default | Description |
|---|---|---|---|
app |
Flask | None |
None |
Flask application. If provided, init_app() is called immediately. |
babel |
Babel | None |
None |
Existing Babel instance. A new one is created if not provided. |
init_app(app, babel=None)
Initialize the extension with a Flask application. Reads LOCALE_FALLBACK_CHAINS from app.config and merges with built-in defaults.
get_chain(locale) -> list[str]
Return the fallback chain for a given locale code. Returns an empty list if no chain is configured.
configure(overrides=None, fallbacks=None, merge_defaults=True, default_locale="en")
Activate chain-aware translation lookup (programmatic API).
| 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 project's default locale |
reset()
Remove all custom configuration and restore default behaviour. 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(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.
Flask Config 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_CHAINSFlask config setting- Built-in defaults (zero-config)
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) |
Example
A complete working example is included in the example/ directory. It demonstrates locale fallback chains with Portuguese (pt-BR -> pt-PT -> pt) and Chinese (zh-Hant-HK -> zh-Hant-TW -> zh-Hant) variants.
cd example && pip install -r requirements.txt && python app.py
Then visit http://localhost:5000/?lang=pt-BR to see the fallback in action.
How It Works
LocaleChainBabelwraps Flask-Babel'sBabelextension and readsLOCALE_FALLBACK_CHAINSfrom your Flask config.- The resolved fallback chains are merged with the 75 built-in defaults.
- When a translation key is missing in the active locale, the chain is walked in order -- each fallback locale's catalogue is checked before reaching the app's default locale.
- The fallback resolution uses Python's native
gettext.GNUTranslations.add_fallbackmechanism -- no monkey-patching. - Your existing
_()/gettext()/ngettext()calls work unchanged.
FAQ
Is this production-ready?
Yes. The library uses Python's native gettext.GNUTranslations.add_fallback mechanism and Flask-Babel's public API. No monkey-patching, no private API access.
Performance impact?
Negligible. Fallback catalogues are loaded once per locale. 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 Flask-Babel's translation catalogues, which are compiled from .po files into .mo files. Any translation format that Flask-Babel supports will work.
Can I use a non-English default locale?
Yes. The fallback chains are independent of your app's BABEL_DEFAULT_LOCALE. 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 LocaleChainBabel extension from your app.
Does it work with Flask blueprints? Yes. Flask-Babel's translation system is app-wide, so fallback chains apply to all blueprints registered on the app.
Does it work with the app factory pattern?
Yes. Use lcb = LocaleChainBabel() and then lcb.init_app(app) in your factory function.
Minimum Python version? Python 3.8. Works with Flask 2.0+ and Flask-Babel 3.0+.
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
Metadata
Release files for flask-babel-locale-chain 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| flask_babel_locale_chain-1.0.0.tar.gz | 13.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| flask_babel_locale_chain-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 24.7 kB
Release files / flask_babel_locale_chain-1.0.0.tar.gz
| Download URL | flask_babel_locale_chain-1.0.0.tar.gz |
|---|---|
| Size | 13.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d1dcb9a768e7a556e55d578115d108d69a4f3b00117c0eba29f4993dbe3139bc
|
|
BLAKE2b-256 checksum How to use checksums |
86bd5e546c99913a5421b76b79a0c5dcf7a8d86b2b3dbf1c14854108c25718c7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Release files / flask_babel_locale_chain-1.0.0-py3-none-any.whl
| Download URL | flask_babel_locale_chain-1.0.0-py3-none-any.whl |
|---|---|
| Size | 11.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
98d0608499584654c580e13dbbaf3f94e8fe7a741c04bf0bb1c0915aad5dccaf
|
|
BLAKE2b-256 checksum How to use checksums |
caae019018dd3f5e662c350a297d35934ebaa63645b3bef04fd6e586ac538b05
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|