Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 locale
  • i18n.t('greeting', locale='pt-BR') -- walks the pt-BR chain: pt-PT -> pt -> en
  • i18n.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.json has only greeting
  • pt.json has greeting and farewell
  • en.json has 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:

  1. configure() -- use DEFAULT_FALLBACKS with "en" as final fallback.
  2. configure(overrides={...}) -- merge overrides on top of defaults.
  3. 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

  1. configure() saves a reference to the original i18n.t() function and replaces it with a chain-aware wrapper.
  2. When i18n.t("key", locale="pt-BR") is called, the wrapper first tries the requested locale directly.
  3. If the key is missing, it walks the fallback chain for that locale (e.g., pt-PT -> pt).
  4. If no chain locale has the key, it tries the configured default locale (e.g., "en").
  5. If the key is still not found, it falls through to the original i18n.t() behaviour (which produces a missing-key marker).
  6. reset() restores the original i18n.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

About

Smart locale fallback chains for python-i18n. pt-BR falls back to pt-PT, not English.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages