Configurable locale fallback chains for Flask-Babel
Project description
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
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 flask_babel_locale_chain-1.0.0.tar.gz.
File metadata
- Download URL: flask_babel_locale_chain-1.0.0.tar.gz
- Upload date:
- Size: 13.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d1dcb9a768e7a556e55d578115d108d69a4f3b00117c0eba29f4993dbe3139bc
|
|
| MD5 |
f3d6adf0a88fee34149d3aff3418955f
|
|
| BLAKE2b-256 |
86bd5e546c99913a5421b76b79a0c5dcf7a8d86b2b3dbf1c14854108c25718c7
|
File details
Details for the file flask_babel_locale_chain-1.0.0-py3-none-any.whl.
File metadata
- Download URL: flask_babel_locale_chain-1.0.0-py3-none-any.whl
- Upload date:
- Size: 11.6 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 |
98d0608499584654c580e13dbbaf3f94e8fe7a741c04bf0bb1c0915aad5dccaf
|
|
| MD5 |
833b94ed979b45138fafff6e464bc3cf
|
|
| BLAKE2b-256 |
caae019018dd3f5e662c350a297d35934ebaa63645b3bef04fd6e586ac538b05
|