Skip to content

Commit e8777fb

Browse files
committed
i18n: redesign core with registry-based, domain-aware dynamic lookups
- Add registry managing providers per domain with priorities and language chain - Install dynamic builtins (_/__/ngettext) resolving caller domain; reactive to set_locale - Add finalize_i18n for app setup; modernize install_global_translation/install_module_translation (compat) - Preserve active_translation as view of default domain; update set_locale behavior - Scan all providers in get_available_translations - Pin Babel to >=2.15,<3.0 for LazyProxy(enable_cache) - Add end-to-end tests using msgfmt and update unit tests - Update README with new design, APIs, and migration notes
1 parent 73baf9b commit e8777fb

File tree

6 files changed

+867
-498
lines changed

6 files changed

+867
-498
lines changed

README.md

Lines changed: 119 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,119 @@
1-
# i18n_core: Internationalization Utilities
2-
3-
`i18n_core` provides a set of utilities to simplify the process of internationalization (i18n) and localization (l10n) for Python applications. It builds upon the standard `locale` module and the powerful `babel` library to offer a streamlined way to manage translations.
4-
5-
## Features
6-
7-
* **Global & Module Translations:** Easily load translations for your main application and individual component libraries or modules.
8-
* **System Locale Detection:** Automatically detects the user's system locale on Windows, macOS, and Linux.
9-
* **Babel Integration:** Leverages `babel` for handling `.mo` translation files and locale data.
10-
* **WxPython Support:** Includes helpers specifically for integrating translations with WxPython applications (`i18n_core.gui`).
11-
* **Frozen Environment Support:** Patches `babel` to correctly locate locale data when running in frozen environments (e.g., PyInstaller).
12-
* **Context Manager:** Provides a `reset_locale` context manager to temporarily change the locale.
13-
14-
## Installation
15-
16-
```bash
17-
pip install i18n_core
18-
```
19-
*(Assuming the package is available on PyPI. If it's local, adjust accordingly)*
20-
21-
## Dependencies
22-
23-
* [Babel](https://babel.pocoo.org/)
24-
* [platform_utils](https://github.com/your_org/platform_utils) *(Assuming this is another internal or specific library)*
25-
26-
## Usage
27-
28-
### Initializing Global Translation
29-
30-
Typically, in your application's entry point, you'll install the global translation. This sets up the primary language and translation domain for your app.
31-
32-
```python
33-
import i18n_core
34-
import os
35-
36-
# Assuming your locale files are in a 'locale' subdirectory
37-
locale_dir = os.path.join(os.path.dirname(__file__), 'locale')
38-
app_domain = 'my_app' # Corresponds to my_app.mo files
39-
40-
# Detect system locale or specify one, e.g., 'es_ES'
41-
# Loads translations from locale_dir/<locale_id>/LC_MESSAGES/my_app.mo
42-
current_locale = i18n_core.install_global_translation(
43-
domain=app_domain,
44-
locale_path=locale_dir
45-
# locale_id='fr_FR' # Optionally force a locale
46-
)
47-
48-
print(f"Application locale set to: {current_locale}")
49-
50-
# Now you can use the standard gettext functions globally
51-
print(_("Hello, world!"))
52-
```
53-
54-
### Loading Module Translations
55-
56-
If you have separate libraries or modules with their own translations, you can merge them into the active global translation.
57-
58-
```python
59-
import i18n_core
60-
import my_module
61-
import os
62-
63-
# Assuming my_module has its own 'locale' subdirectory
64-
module_locale_dir = os.path.join(os.path.dirname(my_module.__file__), 'locale')
65-
module_domain = 'my_module' # Corresponds to my_module.mo
66-
67-
i18n_core.install_module_translation(
68-
domain=module_domain,
69-
locale_path=module_locale_dir,
70-
module=my_module # Or provide module name as string 'my_module'
71-
# Uses the currently active global locale by default
72-
)
73-
74-
# Strings from my_module's domain are now also available via _()
75-
print(_("Module-specific string"))
76-
```
77-
78-
### WxPython Integration
79-
80-
The `i18n_core.gui` module helps initialize WxPython's internal translation mechanisms.
81-
82-
```python
83-
import wx
84-
import i18n_core
85-
import i18n_core.gui
86-
import os
87-
88-
app = wx.App()
89-
90-
# After installing global translation with i18n_core.install_global_translation...
91-
locale_dir = i18n_core.application_locale_path # Get path used by global install
92-
current_locale = i18n_core.CURRENT_LOCALE
93-
94-
# Initialize wx.Locale
95-
wx_locale = i18n_core.gui.set_wx_locale(locale_dir, current_locale)
96-
97-
if not wx_locale:
98-
print("Failed to set Wx locale.")
99-
100-
# ... proceed with your WxPython application setup ...
101-
102-
app.MainLoop()
103-
```
104-
105-
## Locale Management
106-
107-
* `get_system_locale()`: Detects the OS locale.
108-
* `set_locale(locale_id)`: Sets the locale for the current process/thread.
109-
* `get_available_locales(domain, locale_path)`: Lists available `babel.Locale` objects based on found translation files.
110-
* `reset_locale()`: A context manager to temporarily change the locale and restore it afterwards.
111-
112-
```python
113-
from i18n_core import reset_locale, set_locale
114-
115-
print(f"Current locale: {i18n_core.CURRENT_LOCALE}")
116-
117-
with reset_locale():
118-
set_locale('fr_FR')
119-
print(f"Locale inside context: {i18n_core.CURRENT_LOCALE}")
120-
# Perform operations requiring French locale
121-
122-
print(f"Locale after context: {i18n_core.CURRENT_LOCALE}")
123-
```
124-
125-
## Contributing
126-
127-
Please refer to CONTRIBUTING.md for details. *(Optional: Create this file if needed)*
128-
129-
## License
130-
131-
This project is licensed under the terms of the LICENSE file.
1+
# i18n_core: Internationalization Utilities
2+
3+
`i18n_core` is a lightweight, order-agnostic i18n layer on top of Babel.
4+
It lets apps and libraries register translations at any time, and all lookups
5+
react to locale changes without re-registration.
6+
7+
## Features
8+
9+
- Dynamic lookups: `_`, `__`, `ngettext` reflect the current locale.
10+
- Order-agnostic: libraries can register before or after the app; no merge-by-call-order.
11+
- Domain-aware: no cross-domain bleed; use per-domain catalogs with explicit precedence.
12+
- System locale detection and Windows LCID support.
13+
- WxPython helpers: `i18n_core.gui.set_wx_locale`.
14+
- Frozen env support (works with embedded data paths).
15+
16+
## Installation
17+
18+
```bash
19+
pip install i18n_core
20+
```
21+
22+
## Dependencies
23+
24+
- Babel (>= 2.15, < 3.0)
25+
- platform_utils
26+
27+
## Quickstart (App)
28+
29+
Use `finalize_i18n` once at startup to set the default domain and locale.
30+
31+
```python
32+
import os
33+
from i18n_core import finalize_i18n, get_system_locale, _
34+
35+
locale_dir = os.path.join(os.path.dirname(__file__), "locale")
36+
finalize_i18n(
37+
locale_id=get_system_locale(),
38+
app_domain="my_app", # matches your .mo domain
39+
app_locale_path=locale_dir, # <locale>/<LCID>/LC_MESSAGES/my_app.mo
40+
)
41+
42+
print(_("Hello, world!"))
43+
```
44+
45+
Change locale anytime — all lookups update automatically:
46+
47+
```python
48+
from i18n_core import set_locale
49+
50+
set_locale("de_DE")
51+
```
52+
53+
## Libraries
54+
55+
Two options — both are safe and idempotent:
56+
57+
- Rely on inference (zero code): just `from i18n_core import _` and call it.
58+
The top-level package name becomes your domain, and `<pkg>/locale` is used.
59+
60+
- Explicit registration (recommended for clarity/perf):
61+
62+
```python
63+
import sys
64+
import i18n_core
65+
66+
i18n_core.install_module_translation(
67+
domain="my_lib", # your .mo domain
68+
module=sys.modules[__name__],
69+
# locale_path=<path> # optional; defaults to <pkg>/locale
70+
)
71+
72+
print(_("A library string"))
73+
```
74+
75+
## WxPython Integration
76+
77+
```python
78+
import wx
79+
import i18n_core.gui
80+
81+
app = wx.App()
82+
wx_locale = i18n_core.gui.set_wx_locale(locale_path, locale_id)
83+
```
84+
85+
## APIs
86+
87+
- `finalize_i18n(locale_id=None, languages=None, app_domain=None, app_locale_path=None, install_into_builtins=True, priority=100) -> str`
88+
- Configure default app domain/locale; install builtins wrappers.
89+
- `install_module_translation(domain=None, locale_id=None, locale_path=None, module=None, priority=50) -> None`
90+
- Register a provider for a domain and install wrappers into a specific module.
91+
- `install_global_translation(domain, locale_id=None, locale_path=None) -> str`
92+
- Back-compat shim: registers default domain and sets locale.
93+
- `set_locale(locale_id: str) -> str`
94+
- Normalize and apply process/Windows locale; updates i18n registry.
95+
- `get_available_translations(domain, locale_path=None) -> Iterable[str]`
96+
- List available locales for a domain across registered paths.
97+
- `get_available_locales(domain, locale_path=None) -> Iterable[babel.core.Locale]`
98+
- `reset_locale()` context manager: temporarily adjust process locale.
99+
100+
## Precedence & Domains
101+
102+
- Precedence is explicit: higher `priority` overrides lower within a domain
103+
(default: app=100, libraries=50). Ties resolve by first-registration order.
104+
- Lookups are domain-aware; module wrappers use their bound domain, and builtins
105+
use the default domain (or fall back to caller inference).
106+
107+
## Backward Compatibility Notes
108+
109+
- The legacy global-merge behavior is replaced by per-domain composites.
110+
`active_translation` now reflects the default domain, not a cross-domain merge.
111+
- `install_global_translation` and `install_module_translation` still exist but
112+
are now order-agnostic and idempotent.
113+
- Builtins `_`, `__`, `ngettext` are installed at import-time.
114+
If you previously used `try: __; except NameError: install_global_translation(...)`,
115+
switch to `finalize_i18n` in the app entry point.
116+
117+
## License
118+
119+
This project is licensed under the terms of the LICENSE file.

0 commit comments

Comments
 (0)