Metadata-Version: 2.4
Name: python-i18n-locale-chain
Version: 1.0.0
Summary: Smart locale fallback chains for python-i18n -- because pt-BR users deserve pt-PT, not English
Author-email: "i18nagent.ai" <hello@i18nagent.ai>
License-Expression: MIT
Project-URL: Homepage, https://github.com/i18n-agent/python-i18n-locale-chain
Project-URL: Repository, https://github.com/i18n-agent/python-i18n-locale-chain
Project-URL: Documentation, https://github.com/i18n-agent/python-i18n-locale-chain#readme
Project-URL: Changelog, https://github.com/i18n-agent/python-i18n-locale-chain/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/i18n-agent/python-i18n-locale-chain/issues
Keywords: python-i18n,i18n,locale,fallback,translation,internationalization
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Internationalization
Classifier: Topic :: Software Development :: Localization
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: python-i18n>=0.3.1
Provides-Extra: test
Requires-Dist: pytest>=7.0; extra == "test"
Dynamic: license-file

# 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](https://github.com/danhper/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

```bash
pip install python-i18n-locale-chain
```

## Quick Start

### 1. Install the package

```bash
pip install python-i18n-locale-chain
```

### 2. Configure fallback chains

```python
from i18n_locale_chain import configure

configure()
```

### 3. Use `i18n.t()` as normal

```python
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/`](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

```bash
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.

```python
from i18n_locale_chain import configure

configure()
```

### Override specific chains

```python
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

```python
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

```python
from i18n_locale_chain import configure

# Use German as the final fallback instead of English
configure(default_locale="de")
```

### Restore original behaviour

```python
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](LICENSE) file.

Built by [i18nagent.ai](https://i18nagent.ai)
