This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
pyi18n-plug
Lightweight, plugin-style internationalization for Python — JSON catalogs, CLDR plurals, RTL support, and a CLI. Zero hard dependencies.
Install
# Minimal — zero dependencies, works out of the box
pip install pyi18n-plug
# With optional CLDR formatting (plurals, numbers, currency, dates)
pip install "pyi18n-plug[babel]"
Requires Python 3.9+. No hard runtime dependencies.
Project tree
pyi18n-plug/
├── pyproject.toml
├── README.md
├── LICENSE
├── .gitignore
├── examples/
│ └── demo.py
├── src/
│ └── pyi18n/
│ ├── __init__.py
│ ├── py.typed
│ ├── core.py
│ ├── loader.py
│ ├── formatter.py
│ ├── exceptions.py
│ ├── cli.py
│ └── locales/
│ ├── en.json
│ ├── de.json
│ ├── fr.json
│ ├── it.json
│ ├── hi.json
│ ├── ur.json
│ └── zh.json
└── tests/
├── test_core.py
├── test_loader.py
├── test_formatter.py
├── test_plural.py
└── test_cli.py
Verify the install
After installing, run the built-in demo — this proves the package, its CLI entry point, and all seven bundled locale files are present and working:
pyi18n demo
Expected output (seven greetings, one per locale):
========================================================
pyi18n demo — built-in locales
========================================================
Available: de, en, fr, hi, it, ur, zh
[de]
t(greeting, name=World) -> Hallo, World!
...
[ur]
t(greeting, name=World) -> ہیلو، World!
...
A one-liner check:
python -c "
import sys; sys.path.insert(0, 'src')
from pyi18n import I18n
i = I18n()
for loc in i.available_locales():
i.set_locale(loc)
print(loc, '->', i.t('greeting', name='World'))
"
# de -> Hallo, World!
# en -> Hello, World!
# fr -> Bonjour, World !
# hi -> नमस्ते, World!
# it -> Ciao, World!
# ur -> ہیلو، World!
# zh -> 你好,World!
Quick start
from pyi18n import I18n
i18n = I18n() # English is active by default
print(i18n.t("greeting", name="World"))
# → Hello, World!
i18n.set_locale("hi") # switch to Hindi
print(i18n.t("greeting", name="दुनिया"))
# → नमस्ते, दुनिया!
i18n.set_locale("ur") # switch to Urdu (RTL)
print(i18n.t("greeting", name="دنیا"))
print(i18n.direction) # → 'rtl'
Built-in locales
Seven languages ship with the package — usable immediately after pip install:
| Code | Language | Script | Direction | Plural categories |
|---|---|---|---|---|
en |
English | Latin | LTR | one, other |
de |
German | Latin | LTR | one, other |
fr |
French | Latin | LTR | one, other |
it |
Italian | Latin | LTR | one, other |
hi |
Hindi | Devanagari | LTR | one, other |
ur |
Urdu | Arabic | RTL | one, other |
zh |
Chinese | Han | LTR | other |
Verify what's installed:
from pyi18n import I18n
print(I18n().available_locales())
# → ['de', 'en', 'fr', 'hi', 'it', 'ur', 'zh']
Adding your own languages
Three ways — pick whichever fits your workflow.
1. From an in-memory dictionary
i18n.register_locale("es", data={
"greeting": "Hola, {name}!",
"welcome": "Bienvenido a pyi18n-plug.",
"items": {"one": "{count} elemento", "other": "{count} elementos"},
})
i18n.set_locale("es")
print(i18n.t("greeting", name="Mundo")) # → Hola, Mundo!
2. From a JSON file
i18n.register_locale("pt", path="locales/pt.json")
3. From a whole folder
Point at a directory; every *.json file becomes a locale named after its
filename stem:
loaded = i18n.load_directory("./locales") # returns number of files loaded
print(f"Loaded {loaded} locales")
locales/
├── en.json ← English (fallback)
├── es.json ← Spanish
├── pt.json ← Portuguese
└── sw.json ← Swahili
Catalog format
Every catalog is a nested JSON object. Placeholders use Python's
str.format syntax. Plural entries are objects whose keys are CLDR
categories:
{
"greeting": "Hello, {name}!",
"welcome": "Welcome to pyi18n-plug.",
"errors": {
"not_found": "{name} was not found."
},
"items": {
"one": "{count} item",
"other": "{count} items"
}
}
Nested keys use dots — i18n.t("errors.not_found", name="User").
Plurals
Uses Babel's full CLDR rules when installed, with a safe fallback when not:
i18n.set_locale("en")
i18n.plural("items", 1) # → '1 item'
i18n.plural("items", 5) # → '5 items'
i18n.set_locale("fr")
i18n.plural("items", 0) # → '0 élément' (French CLDR: 0 → one)
i18n.plural("items", 2) # → '2 éléments'
i18n.set_locale("zh")
i18n.plural("items", 3) # → '3 个项目' (Chinese has only 'other')
Or let t() pick the form automatically:
i18n.t("items", count=7) # → '7 items'
RTL support
from pyi18n import is_rtl, RTL_LOCALES
is_rtl("ur") # → True
is_rtl("he") # → True
is_rtl("en") # → False
RTL_LOCALES # → frozenset({'ar', 'ur', 'he', 'fa', 'yi'})
i18n.set_locale("ur")
i18n.direction # → 'rtl'
In templates: <html lang="{{ i18n.locale }}" dir="{{ i18n.direction }}">.
CLI
After pip install pyi18n-plug, the pyi18n command is on your PATH:
# Print sample translations for every built-in locale
pyi18n demo
# List registered locales
pyi18n list
# Create a starter catalog — useful when adding a new language
pyi18n init es
# → wrote es.json
# Translate a key from the shell (great for debugging placeholders)
pyi18n translate greeting -l hi -p name=World
pyi18n translate items -l en -p count=3
# CI check: reports any keys in en.json missing from other catalogs
pyi18n validate ./locales
Exit codes are 0 (success) / 1 (failure), so validate drops into CI
without any wrapper.
Optional formatting (numbers, currency, dates)
Available with pip install "pyi18n-plug[babel]". Each method falls back to
str() if Babel is not installed, so calling them is always safe.
from datetime import datetime
i18n.set_locale("de")
i18n.number(1234.5) # → '1.234,5'
i18n.currency(99.99, "EUR") # → '99,99 €'
i18n.datetime(datetime.now()) # → '24.10.2026, 14:03:22'
i18n.set_locale("hi")
i18n.number(1234567) # → '12,34,567' (Indian grouping)
Fallback chain
Missing keys never raise — they fall through in this order:
current locale → fallback locale → default (en) → literal key string
i18n.register_locale("xx", {"greeting": "X {name}!"})
i18n.set_locale("xx")
i18n.t("greeting", name="A") # → 'X A!' (from xx)
i18n.t("welcome") # → 'Welcome...' (from en)
i18n.t("does.not.exist") # → 'does.not.exist' (literal key)
i18n.t("greeting") # → 'X {name}!' (missing placeholder preserved)
A half-translated catalog still renders. You can ship new languages incrementally without breaking production pages.
Framework integration
FastAPI
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from pyi18n import I18n
app = FastAPI()
templates = Jinja2Templates(directory="templates")
@app.get("/{locale}/", response_class=HTMLResponse)
async def home(request: Request, locale: str):
i18n = I18n()
try:
i18n.set_locale(locale)
except Exception:
i18n.set_locale("en")
html = templates.get_template("home.html").render(
request=request, i18n=i18n, locale=i18n.locale,
)
response = HTMLResponse(html)
response.headers["Content-Language"] = i18n.locale
return response
Flask
from flask import Flask, g, request
from pyi18n import I18n
app = Flask(__name__)
@app.before_request
def _i18n():
i18n = I18n()
try:
i18n.set_locale(request.accept_languages.best_match(i18n.available_locales()))
except Exception:
pass
g.i18n = i18n
@app.get("/")
def home():
return g.i18n.t("greeting", name="World")
Django
from django.http import HttpResponse
from pyi18n import I18n
I18N = I18n() # module-level, one engine per process
def home(request):
I18N.set_locale(request.LANGUAGE_CODE or "en")
return HttpResponse(I18N.t("greeting", name="World"))
API reference
| Call | Returns | Notes |
|---|---|---|
I18n(default_locale="en") |
I18n |
Loads built-in catalogs |
i18n.set_locale(code) |
None |
Raises LocaleNotFoundError if unknown |
i18n.t(key, **params) |
str |
Substitutes {placeholders} |
i18n.plural(key, count) |
str |
Picks CLDR form |
i18n.has(key) |
bool |
True if key resolves in the chain |
i18n.available_locales() |
list[str] |
Sorted |
i18n.locale / i18n.direction |
str |
'en' / 'ltr' |
i18n.register_locale(code, data=…) |
None |
In-memory |
i18n.register_locale(code, path=…) |
None |
From JSON file |
i18n.load_directory(path) |
int |
Number of files loaded |
i18n.unregister_locale(code) |
None |
Safe if missing |
i18n.number / currency / datetime |
str |
Optional Babel extras |
is_rtl(code) |
bool |
Locale-level helper |
Development
git clone https://github.com/ankitarora05/pyi18n-plug.git
cd pyi18n-plug
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest -q
Run the bundled demo:
python examples/demo.py
Add a new built-in locale:
cp src/pyi18n/locales/en.json src/pyi18n/locales/es.json
# translate values, keep keys identical
# register in SUPPORTED (src/pyi18n/core.py)
pytest -q
CI
Every push and pull request runs tests, validation, and build verification via
.github/workflows/workflow.yml. Releases
are published to PyPI automatically on GitHub Release via Trusted Publishing
(no stored tokens).
License
MIT — see LICENSE.
Contributing
Issues and PRs welcome at ankitarora05/pyi18n-plug.
- Add a test for any new behaviour (
pytest -qmust pass). - If you add a built-in locale, include every key that exists in
en.json. - Keep the import path stable (
pyi18n).
Metadata
Release files for pyi18n-plug 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 | Provenance | |
|---|---|---|---|---|
| pyi18n_plug-1.0.0.tar.gz | 17.8 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyi18n_plug-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 32.2 kB
Release files / pyi18n_plug-1.0.0.tar.gz
| Download URL | pyi18n_plug-1.0.0.tar.gz |
|---|---|
| Size | 17.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f31da4f88cd30faaeee470a8e8e0820970290e39407ff7066196124274a8ef71
|
|
BLAKE2b-256 checksum How to use checksums |
42e45307ff5c40c17ec148ca4e3f9b8b165e92ba40dc2b4c1de7f097c6b8f3be
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logRelease files / pyi18n_plug-1.0.0-py3-none-any.whl
| Download URL | pyi18n_plug-1.0.0-py3-none-any.whl |
|---|---|
| Size | 14.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
da8e20e7ae22c445ce452b965d6b9c2275e2b55671a826661717678bd9446f0c
|
|
BLAKE2b-256 checksum How to use checksums |
061880f0b84eb99d4232f66d03fba906e2651eb49a26049f0f1dbab82ca94875
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logSecurity for pyi18n-plug 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pyi18n_plug-1.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.