From bdacf3f76bc3d7ce0d188f4da64a031f94e5c92e Mon Sep 17 00:00:00 2001 From: Matt Gros <3311227+mpge@users.noreply.github.com> Date: Fri, 1 May 2026 22:10:04 -0400 Subject: [PATCH 1/5] feat(i18n): consume central translations from escalated-locale Add the central `escalated-locale` PyPI package as a dependency and expose `escalated.locale_paths.get_locale_paths()` so host projects (and our test settings) can layer the plugin-local override on top of the central catalogue with the override winning. Assumes the central package ships gettext artifacts at `escalated_locale/locale//LC_MESSAGES/django.{po,mo}`. --- escalated/locale_paths.py | 72 +++++++++++++++++++++++++++++++++++++++ pyproject.toml | 6 ++++ tests/settings.py | 7 ++++ 3 files changed, 85 insertions(+) create mode 100644 escalated/locale_paths.py diff --git a/escalated/locale_paths.py b/escalated/locale_paths.py new file mode 100644 index 0000000..6fddb15 --- /dev/null +++ b/escalated/locale_paths.py @@ -0,0 +1,72 @@ +"""Helpers for wiring the central `escalated-locale` package into Django. + +The canonical translation source for the entire Escalated portfolio is +the `escalated-locale` PyPI package. It is expected to ship both the +canonical JSON catalogues and pre-compiled gettext artifacts at:: + + escalated_locale/locale//LC_MESSAGES/django.po + escalated_locale/locale//LC_MESSAGES/django.mo + +This module exposes those paths so host projects can compose Django's +`LOCALE_PATHS` with the plugin-local override winning over the central +package, e.g.:: + + # settings.py + from escalated.locale_paths import get_locale_paths + + LOCALE_PATHS = get_locale_paths() + +`LOCALE_PATHS` is searched in order, so the plugin-local override comes +first and the central package provides the baseline. +""" + +from __future__ import annotations + +import os +from importlib import import_module +from typing import List, Optional + +# Plugin-local override directory shipped with this package. Translators +# can drop overrides in here to win over the central catalogues. +PLUGIN_LOCALE_DIR: str = os.path.join(os.path.dirname(__file__), "locale") + + +def get_central_locale_path() -> Optional[str]: + """Return the absolute path to `escalated_locale/locale`, or None. + + Resolves the path lazily so a missing/unpublished `escalated-locale` + package does not break import-time settings loading. Host projects + that need translations should ensure the package is installed. + """ + try: + pkg = import_module("escalated_locale") + except ImportError: + return None + + pkg_dir = os.path.dirname(getattr(pkg, "__file__", "") or "") + if not pkg_dir: + return None + + candidate = os.path.join(pkg_dir, "locale") + return candidate if os.path.isdir(candidate) else None + + +def get_locale_paths(*extra: str) -> List[str]: + """Compose a Django-compatible `LOCALE_PATHS` tuple. + + Order (highest priority first): + 1. Any caller-supplied `extra` paths + 2. The plugin-local override at `escalated/locale/` + 3. The central `escalated_locale/locale/` directory (if installed) + + The plugin-local directory is always included so existing overrides + keep working even if the central package is not yet installed. + """ + paths: List[str] = [p for p in extra if p] + paths.append(PLUGIN_LOCALE_DIR) + + central = get_central_locale_path() + if central: + paths.append(central) + + return paths diff --git a/pyproject.toml b/pyproject.toml index b533f66..95647f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,12 @@ dependencies = [ "django>=4.2", "inertia-django>=1.0", "requests>=2.28", + # Central translations distributed via PyPI. Ships canonical JSON plus + # pre-compiled gettext .po/.mo files at + # `escalated_locale/locale/{lang}/LC_MESSAGES/django.{po,mo}`, which we + # surface via `escalated.locale.get_central_locale_path()` so host + # projects can wire it into Django's LOCALE_PATHS. + "escalated-locale ~= 0.1", ] [project.optional-dependencies] diff --git a/tests/settings.py b/tests/settings.py index 76f7bed..d92bded 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,6 +1,13 @@ +from escalated.locale_paths import get_locale_paths + DEBUG = True USE_TZ = True +# Compose LOCALE_PATHS with the plugin-local override first (winning +# priority) and the central `escalated-locale` package second. This +# mirrors the wiring host projects are documented to use in the README. +LOCALE_PATHS = get_locale_paths() + DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", From d8ad0d8db4303d4946259a56027114b6dabc697e Mon Sep 17 00:00:00 2001 From: Matt Gros <3311227+mpge@users.noreply.github.com> Date: Fri, 1 May 2026 22:10:26 -0400 Subject: [PATCH 2/5] docs(i18n): document central locale wiring and override layering Document `get_locale_paths()` for host projects in README and add an Unreleased CHANGELOG entry covering the central locale consumption. --- CHANGELOG.md | 4 ++++ README.md | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd1f144..bf55158 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] ### Added +- Central translations sourced from the `escalated-locale` PyPI package + via `escalated.locale_paths.get_locale_paths()`; the plugin-local + `escalated/locale/` directory remains as the override layer that wins + over the central catalogue - Missing admin views for automations, articles, and side-conversations - Inertia UI optional with `UI_ENABLED` setting - Plugin system with service layer and admin views diff --git a/README.md b/README.md index a7fe88a..c9da8d7 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,40 @@ Your layout component must accept a `#header` slot and a default slot. Escalated See the [`@escalated-dev/escalated` README](https://github.com/escalated-dev/escalated) for full theming documentation and CSS custom properties. +## Translations (i18n) + +Escalated for Django consumes translations from the central +[`escalated-locale`](https://github.com/escalated-dev/escalated-locale) +PyPI package. The package ships canonical JSON catalogues alongside +pre-compiled gettext artifacts at +`escalated_locale/locale//LC_MESSAGES/django.{po,mo}`, which +plug directly into Django's translation system via `LOCALE_PATHS`. + +The package is installed automatically as a dependency of +`escalated-django`. Wire it into your project's `settings.py`: + +```python +# settings.py +from escalated.locale_paths import get_locale_paths + +LOCALE_PATHS = get_locale_paths( + # Optional: your project's own override directory (highest priority) + BASE_DIR / "locale", +) +``` + +`LOCALE_PATHS` is searched in order, so the layering becomes: + +1. Your project's `locale/` (if you passed one to `get_locale_paths`) +2. The plugin-local `escalated/locale/` override directory +3. The central `escalated_locale/locale/` baseline + +Translation contributions should be sent to +[`escalated-locale`](https://github.com/escalated-dev/escalated-locale). +The plugin-local `escalated/locale/` directory is reserved for +Django-specific overrides that shouldn't ship to other framework +plugins. + ## Hosting Modes ### Self-Hosted (default) From 7d90918d9fdc98e49c5ee95c871ce6377fc07ae4 Mon Sep 17 00:00:00 2001 From: Matt Gros <3311227+mpge@users.noreply.github.com> Date: Fri, 1 May 2026 22:10:47 -0400 Subject: [PATCH 3/5] test(i18n): cover locale_paths helper composition Verify the plugin-local override is always present, caller extras take priority, and `get_central_locale_path()` degrades gracefully when the `escalated-locale` package is not installed. --- tests/unit/test_locale_paths.py | 44 +++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/unit/test_locale_paths.py diff --git a/tests/unit/test_locale_paths.py b/tests/unit/test_locale_paths.py new file mode 100644 index 0000000..caa644f --- /dev/null +++ b/tests/unit/test_locale_paths.py @@ -0,0 +1,44 @@ +"""Unit tests for `escalated.locale_paths`. + +These tests intentionally do not require the `escalated-locale` package +to be installed — `get_central_locale_path()` returns None when it is +absent, and `get_locale_paths()` should still surface the plugin-local +override directory. +""" + +import os + +from escalated import locale_paths + + +def test_plugin_locale_dir_exists(): + """The plugin-local override directory must always exist on disk.""" + assert os.path.isdir(locale_paths.PLUGIN_LOCALE_DIR) + + +def test_get_locale_paths_includes_plugin_local_dir(): + paths = locale_paths.get_locale_paths() + assert locale_paths.PLUGIN_LOCALE_DIR in paths + + +def test_get_locale_paths_extras_take_priority(): + """Caller-supplied paths must come before the plugin-local override.""" + extra = "/tmp/example-host-locale" + paths = locale_paths.get_locale_paths(extra) + assert paths[0] == extra + assert paths.index(extra) < paths.index(locale_paths.PLUGIN_LOCALE_DIR) + + +def test_get_central_locale_path_handles_missing_package(monkeypatch): + """If `escalated_locale` cannot be imported, return None gracefully.""" + import builtins + + real_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name == "escalated_locale": + raise ImportError("simulated missing package") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", fake_import) + assert locale_paths.get_central_locale_path() is None From 21005130f187ce79bc3604b5a9e046740e19426b Mon Sep 17 00:00:00 2001 From: Matt Gros <3311227+mpge@users.noreply.github.com> Date: Sat, 2 May 2026 15:36:42 -0400 Subject: [PATCH 4/5] build: install escalated-locale from git source (until PyPI publish) pip resolves escalated-locale from github.com/escalated-dev/escalated-locale via PEP 440 direct git URL (v0.1.1 tag, subdirectory=packages/pypi). Once published to PyPI this constraint can revert to "escalated-locale ~= 0.1". Also patch the missing-package test in tests/unit/test_locale_paths.py to intercept import_module on the loader module instead of builtins.__import__, since the central package is now installed via this dependency and the prior monkeypatch never reached the import call site. --- pyproject.toml | 2 +- tests/unit/test_locale_paths.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 95647f0..aa18e4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ dependencies = [ # `escalated_locale/locale/{lang}/LC_MESSAGES/django.{po,mo}`, which we # surface via `escalated.locale.get_central_locale_path()` so host # projects can wire it into Django's LOCALE_PATHS. - "escalated-locale ~= 0.1", + "escalated-locale @ git+https://github.com/escalated-dev/escalated-locale.git@v0.1.1#subdirectory=packages/pypi", ] [project.optional-dependencies] diff --git a/tests/unit/test_locale_paths.py b/tests/unit/test_locale_paths.py index caa644f..dc0c3ec 100644 --- a/tests/unit/test_locale_paths.py +++ b/tests/unit/test_locale_paths.py @@ -31,14 +31,14 @@ def test_get_locale_paths_extras_take_priority(): def test_get_central_locale_path_handles_missing_package(monkeypatch): """If `escalated_locale` cannot be imported, return None gracefully.""" - import builtins + import importlib - real_import = builtins.__import__ + real_import_module = importlib.import_module - def fake_import(name, *args, **kwargs): + def fake_import_module(name, *args, **kwargs): if name == "escalated_locale": raise ImportError("simulated missing package") - return real_import(name, *args, **kwargs) + return real_import_module(name, *args, **kwargs) - monkeypatch.setattr(builtins, "__import__", fake_import) + monkeypatch.setattr(locale_paths, "import_module", fake_import_module) assert locale_paths.get_central_locale_path() is None From 5c913763d4d13095c41169ca801f7f60abdba057 Mon Sep 17 00:00:00 2001 From: Matt Gros <3311227+mpge@users.noreply.github.com> Date: Sat, 2 May 2026 15:38:32 -0400 Subject: [PATCH 5/5] style(locale_paths): modernize type annotations to satisfy ruff UP006/UP045 Drop `typing.List`/`typing.Optional` in favor of PEP 585 / PEP 604 syntax (`list[str]`, `str | None`). Ruff's `UP` rules flag the legacy spellings and were failing CI for this branch. No behavior change. --- escalated/locale_paths.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/escalated/locale_paths.py b/escalated/locale_paths.py index 6fddb15..9610712 100644 --- a/escalated/locale_paths.py +++ b/escalated/locale_paths.py @@ -24,14 +24,13 @@ import os from importlib import import_module -from typing import List, Optional # Plugin-local override directory shipped with this package. Translators # can drop overrides in here to win over the central catalogues. PLUGIN_LOCALE_DIR: str = os.path.join(os.path.dirname(__file__), "locale") -def get_central_locale_path() -> Optional[str]: +def get_central_locale_path() -> str | None: """Return the absolute path to `escalated_locale/locale`, or None. Resolves the path lazily so a missing/unpublished `escalated-locale` @@ -51,7 +50,7 @@ def get_central_locale_path() -> Optional[str]: return candidate if os.path.isdir(candidate) else None -def get_locale_paths(*extra: str) -> List[str]: +def get_locale_paths(*extra: str) -> list[str]: """Compose a Django-compatible `LOCALE_PATHS` tuple. Order (highest priority first): @@ -62,7 +61,7 @@ def get_locale_paths(*extra: str) -> List[str]: The plugin-local directory is always included so existing overrides keep working even if the central package is not yet installed. """ - paths: List[str] = [p for p in extra if p] + paths: list[str] = [p for p in extra if p] paths.append(PLUGIN_LOCALE_DIR) central = get_central_locale_path()