Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<lang>/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)
Expand Down
71 changes: 71 additions & 0 deletions escalated/locale_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""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/<lang>/LC_MESSAGES/django.po
escalated_locale/locale/<lang>/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

# 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() -> str | None:
"""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
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 @ git+https://github.com/escalated-dev/escalated-locale.git@v0.1.1#subdirectory=packages/pypi",
]

[project.optional-dependencies]
Expand Down
7 changes: 7 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
44 changes: 44 additions & 0 deletions tests/unit/test_locale_paths.py
Original file line number Diff line number Diff line change
@@ -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 importlib

real_import_module = importlib.import_module

def fake_import_module(name, *args, **kwargs):
if name == "escalated_locale":
raise ImportError("simulated missing package")
return real_import_module(name, *args, **kwargs)

monkeypatch.setattr(locale_paths, "import_module", fake_import_module)
assert locale_paths.get_central_locale_path() is None