Skip to main content

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") and gettext("key") in Python code
  • {{ _("key") }} in Jinja2 templates
  • ngettext() 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):

  1. configure() call (programmatic API)
  2. LOCALE_FALLBACK_CHAINS Flask config setting
  3. 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

  1. LocaleChainBabel wraps Flask-Babel's Babel extension and reads LOCALE_FALLBACK_CHAINS from your Flask config.
  2. The resolved fallback chains are merged with the 75 built-in defaults.
  3. 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.
  4. The fallback resolution uses Python's native gettext.GNUTranslations.add_fallback mechanism -- no monkey-patching.
  5. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

flask_babel_locale_chain-1.0.0.tar.gz (13.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

flask_babel_locale_chain-1.0.0-py3-none-any.whl (11.6 kB view details)

Uploaded Python 3

File details

Details for the file flask_babel_locale_chain-1.0.0.tar.gz.

File metadata

File hashes

Hashes for flask_babel_locale_chain-1.0.0.tar.gz
Algorithm Hash digest
SHA256 d1dcb9a768e7a556e55d578115d108d69a4f3b00117c0eba29f4993dbe3139bc
MD5 f3d6adf0a88fee34149d3aff3418955f
BLAKE2b-256 86bd5e546c99913a5421b76b79a0c5dcf7a8d86b2b3dbf1c14854108c25718c7

See more details on using hashes here.

File details

Details for the file flask_babel_locale_chain-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for flask_babel_locale_chain-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 98d0608499584654c580e13dbbaf3f94e8fe7a741c04bf0bb1c0915aad5dccaf
MD5 833b94ed979b45138fafff6e464bc3cf
BLAKE2b-256 caae019018dd3f5e662c350a297d35934ebaa63645b3bef04fd6e586ac538b05

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page