🌍 Easy AI18n
Easy AI18n is a modern Python3 internationalization library that supports AI translation, multi-user scenarios, and the complete string formatting syntax, making project globalization more elegant and natural.
✨ Key Features:
- 🚀 Simple and Easy: Internationalize your project with just a few lines of code
- ✨ Elegant Syntax: Blends naturally into your existing code
- 🤖 AI Translation: Supports translation with large language models (LLM) for high-quality results
- 📝 Formatting Compatible: Full support for all Python string formatting syntax
- 🌐 Dynamic Multi-language: Supports dynamic language selection at runtime
🔍 Comparison with Other i18n Tools
| Other i18n Tools | EasyAI18n |
|---|---|
Requires manually maintaining key and i18n files, high development cost |
Automatically extracts translatable content, no manual file maintenance |
Only supports part of the formatting syntax |
Full support for all formatting syntax |
No runtime language switching, not suitable for multi-user scenarios |
Supports the default locale and multi-language switching, ideal for multi-user environments |
⚡ Quick Start
📦 Installation
# Install runtime dependencies (without the extra dependencies needed to build locale files)
uv add easy-ai18n
# Add the translation builder dependency to dev dependencies
uv add --dev "easy-ai18n[builder]"
🧪 Simple Example
/i18n.py
from easy_ai18n import EasyAI18n
i18n = EasyAI18n("en")
_ = i18n.i18n()
if __name__ == "__main__":
i18n.build(to_locales=["ja"])
/main.py
from i18n import _
print(_("Hello, world!")['ja'])
📘 Usage Guide
🔎 Language Selector
The language selector is the core mechanism Easy AI18n uses to retrieve multi-language results: a single translation produces all languages at once, and you pick from them as needed. It works in two directions:
- Pre locale selector
_['ja']("text"): locks the language first, then translates the text — suitable for scopes with a fixed language (e.g. all text within a user session) - Post locale selector
_("text")['ja']/_("text")('ja'): translates first to obtain aLocaleContent, then picks the language from it — suitable for retrieving different languages on a per-call basis
The result of translation is LocaleContent _("text"): it exists as a string in the default locale, carrying
translations for all languages internally, ready for on-demand retrieval later.
When no language is specified, the default locale text is returned (by default this is the source language
source_locale; the source language has no translation files, so the original text is returned directly; this can be
overridden with default_locale). The selector is not limited to language codes: a custom selector can accept any
object (e.g. Telegram's Message) and resolve the user's language from it, enabling dynamic switching in multi-user
scenarios.
All the forms and their types are as follows:
_ = i18n.i18n() # I18n: translation function
_t: PreLocaleSelector = _['ja'] # Pre locale selector: locks the language (a str language code when unspecified)
_t("text") # Pre locale selector call
_['ja']("text") # Equivalent form (without binding a variable)
content: LocaleContent = _("text") # Multi-language object: a string in the default locale, containing all languages
content['ja'] # Post locale selector (index to get a language)
content('ja') # Post locale selector (call to get a language, equivalent to indexing)
_("text")['ja'] # Equivalent form (without binding a variable)
Example:
from easy_ai18n import EasyAI18n
i18n = EasyAI18n("en", func_names=['_', '_t'])
_ = i18n.i18n()
_t = _['ja'] # Pre locale selector: locks Japanese
d = {
1: _('apple'), # Default locale (source language en) -> original text
2: _('banana'),
3: _t('orange'), # Pre locale selector -> Japanese
}
print(d[1]['zh-hans']) # Post locale selector: get Simplified Chinese output: 苹果
print(d[2]) # Default locale output output: banana
print(d[3]) # Pre locale selector output output: みかん
Overriding the default locale:
_ = i18n.i18n(default_locale="ja")
print(_('apple')) # Default locale is ja, outputs Japanese directly
⚙️ Build Options
build() supports controlling the extraction scope and concurrency behavior:
i18n.build(
to_locales=["ja", "ru"],
project_root="./",
# Scan root directory (defaults to the current working directory; include/exclude are resolved relative to it)
include=["src/**"], # Only extract matching files/directories (glob supported)
exclude=["tests/**", "build/**"], # Exclude files or directories (.venv/.git/.idea excluded by default)
concurrent_locales=False,
# Translate locales concurrently (default True; recommended to disable for free/rate-limited APIs)
max_retries=3, # Extra retry attempts after a single locale fails (default 2)
)
Custom locale directory: generated in ./i18n by default; change it via locales_dir:
i18n = EasyAI18n("en", locales_dir="./locales")
📝 String Formatting and Variable Interpolation
Full support for f-string variable interpolation and all Python formatting syntax
from easy_ai18n import EasyAI18n
i18n = EasyAI18n("en")
_ = i18n.i18n()
name = "Alice"
count = 3
print(_(f"Hello, {name}!")['zh-hans']) # output: 你好, Alice!
print(_(f"Count: {count:02d}")['zh-hans']) # output: 数量: 03
print(_(f"Value: {count!r}")['zh-hans']) # output: 值: 3
# Multiple arguments are joined automatically (separated by a space by default)
print(_("Hello", "world")['zh-hans']) # output: 你好 世界
# sep= custom separator (can also be set globally at construction: EasyAI18n("en", sep="-"))
print(_("Hello", "world", sep="-")['zh-hans']) # output: 你好-世界
🛠️ Custom Translation Function Names
from easy_ai18n import EasyAI18n
i18n = EasyAI18n(
"en",
func_names=["_t", "_"] # Custom translation function names
)
_t = i18n.i18n()
_ = _t
print(_t("Hello, world!"))
print(_("Hello, world!"))
🤖 Translating with AI
Default translator: when translator is not passed, build() uses the free GoogleTranslator by default, no
configuration required:
i18n.build(to_locales=["ja"]) # Default GoogleTranslator
Two LLM translation modes:
- Bulk
LLMBulkTranslatortranslates multiple texts at once - Per-item
LLMItemTranslatortranslates one text at a time
from easy_ai18n import EasyAI18n
from easy_ai18n.translators import LLMBulkTranslator
translator = LLMBulkTranslator(api_key="...", base_url="...", model="gpt-5-mini")
i18n = EasyAI18n("en")
i18n.build(to_locales=["ru", "ja", "zh-hant"], translator=translator)
_ = i18n.i18n()
print(_("Hello, world!")['zh-hant'])
Custom Agent: both LLM translators support passing any pydantic-ai Agent, so you can connect local models or other providers:
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
from easy_ai18n.translators import LLMItemTranslator
agent = Agent(
model=OpenAIChatModel(
"qwen2.5:7b", # Local Ollama model
provider=OpenAIProvider(api_key="ollama", base_url="http://localhost:11434/v1"),
),
output_type=str, # Change to list[TranslatorResult] when using LLMBulkTranslator
)
translator = LLMItemTranslator(agent=agent)
🛠️ Custom Translators
Just subclass BaseTranslator and implement translate_chunk:
from easy_ai18n import EasyAI18n, TextMap
from easy_ai18n.translators import BaseTranslator
class MyTranslator(BaseTranslator):
async def translate_chunk(self, *, texts: TextMap, target_lang: str, source_lang: str) -> TextMap:
# Implement the translation logic
...
i18n = EasyAI18n("zh-hans")
i18n.build(
to_locales=["ja"],
translator=MyTranslator(),
)
👥 Multi-user Language Scenarios (e.g. Telegram Bot)
Implement dynamic language selection in multi-user environments via a custom language selector:
/i18n.py:
from pyrogram.types import Message
from easy_ai18n import EasyAI18n, PostLocaleSelector
class MyPostLocaleSelector(PostLocaleSelector[Message]):
def __getitem__(self, locale: Message | str) -> str:
if isinstance(locale, str):
return super().__getitem__(locale)
return super().__getitem__(locale.from_user.language_code)
i18n = EasyAI18n("en")
t_ = i18n.i18n(post_locale_selector=MyPostLocaleSelector)
if __name__ == "__main__":
i18n.build(to_locales=["en", "ru"])
/bot.py:
from pyrogram import Client
from pyrogram.types import Message
from i18n import t_
bot = Client("my_bot")
@bot.on_message()
async def start(_, msg: Message):
await msg.reply(t_[msg]("Hello, world!"))
if __name__ == "__main__":
bot.run()
📂 Example Projects
Real-world projects using Easy AI18n
🗂️ Project Structure
easy_ai18n
├── __init__.py # EasyAI18n entry + public API
├── i18n.py # Translation runtime (I18n, Pre/PostLocaleSelector, LocaleContent)
├── translators.py # Translators (ABC + GoogleTranslator + LLM*Translator)
├── errors.py # Exception classes
├── py.typed # PEP 561 type marker
├── _builder.py # Builder: extract, translate, generate YAML files
├── _parser.py # AST parser
├── _progress.py # Progress display (rich progress bar / falls back to line reports outside a terminal)
├── _loader.py # Loader: load locale files
└── _types.py # Text/TextId/TextMap type definitions
📜 License
This project is open-sourced under the MIT License
If this project helps you, feel free to give it a ⭐ Star!
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 easy_ai18n-1.2.0.tar.gz.
File metadata
- Download URL: easy_ai18n-1.2.0.tar.gz
- Upload date:
- Size: 35.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9bcb73566d242f7888dca4ec118753cc9db5c8ef86283e40ddd6411e961cced5
|
|
| MD5 |
1c17ffe1ca261eb296179a674094996a
|
|
| BLAKE2b-256 |
fd8a07a97458540cd544d6359d76b36e3dabe11d35e0661b6a1a4c3ca4cf665d
|
Provenance
The following attestation bundles were made for easy_ai18n-1.2.0.tar.gz:
Publisher:
python-publish.yml on z-mio/easy-ai18n
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
easy_ai18n-1.2.0.tar.gz -
Subject digest:
9bcb73566d242f7888dca4ec118753cc9db5c8ef86283e40ddd6411e961cced5 - Sigstore transparency entry: 2495154715
- Sigstore integration time:
-
Permalink:
z-mio/easy-ai18n@ac4b7c5cc88e52067a00b35090aaa79affdd380e -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/z-mio
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@ac4b7c5cc88e52067a00b35090aaa79affdd380e -
Trigger Event:
release
-
Statement type:
File details
Details for the file easy_ai18n-1.2.0-py3-none-any.whl.
File metadata
- Download URL: easy_ai18n-1.2.0-py3-none-any.whl
- Upload date:
- Size: 31.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
54dbddc9d84f1c90a03c3b266a9127ec15a7c607869d79ddb1033628d48a1070
|
|
| MD5 |
33c57e7e1e9a6c1b16010be77db49d2c
|
|
| BLAKE2b-256 |
cb756329e11a3e809e13b5c044230dbdb8b045e296feb5d20800a89cba50d4d2
|
Provenance
The following attestation bundles were made for easy_ai18n-1.2.0-py3-none-any.whl:
Publisher:
python-publish.yml on z-mio/easy-ai18n
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
easy_ai18n-1.2.0-py3-none-any.whl -
Subject digest:
54dbddc9d84f1c90a03c3b266a9127ec15a7c607869d79ddb1033628d48a1070 - Sigstore transparency entry: 2495154719
- Sigstore integration time:
-
Permalink:
z-mio/easy-ai18n@ac4b7c5cc88e52067a00b35090aaa79affdd380e -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/z-mio
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@ac4b7c5cc88e52067a00b35090aaa79affdd380e -
Trigger Event:
release
-
Statement type: