Metadata-Version: 2.5
Name: django-admin-translated-urls
Version: 0.1.0
Summary: The Django admin's URLs in the active language: its apps', models' and pages' names, translated.
Project-URL: Homepage, https://github.com/rodolvbg/django-admin-translated-urls
Project-URL: Issues, https://github.com/rodolvbg/django-admin-translated-urls/issues
Project-URL: Repository, https://github.com/rodolvbg/django-admin-translated-urls
Author-email: Rodolfo Valentín Becerra García <rodolvbg@gmail.com>
Maintainer-email: Rodolfo Valentín Becerra García <rodolvbg@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: admin,django,i18n,translation,urls
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Framework :: Django :: 6.1
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Internationalization
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: django>=4.2
Provides-Extra: dev
Requires-Dist: coverage[toml]>=7.4; extra == 'dev'
Requires-Dist: django-stubs[compatible-mypy]>=5; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pre-commit>=4; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest-django>=4.8; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: tox-uv>=1; extra == 'dev'
Requires-Dist: tox>=4; extra == 'dev'
Description-Content-Type: text/markdown

# django-admin-translated-urls

[![Build status](https://github.com/rodolvbg/django-admin-translated-urls/actions/workflows/pytest.yml/badge.svg)](https://github.com/rodolvbg/django-admin-translated-urls/actions/workflows/pytest.yml)
[![PyPI version](https://img.shields.io/pypi/v/django-admin-translated-urls.svg)](https://pypi.org/project/django-admin-translated-urls/)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-admin-translated-urls)](https://pypi.org/project/django-admin-translated-urls/)
[![PyPI - Django Version](https://img.shields.io/pypi/djversions/django-admin-translated-urls)](https://pypi.org/project/django-admin-translated-urls/)
[![Downloads](https://static.pepy.tech/personalized-badge/django-admin-translated-urls?period=month&units=international_system&left_color=black&right_color=blue&left_text=Downloads/month)](https://pepy.tech/project/django-admin-translated-urls)

The Django admin's URLs in the active language: its apps', models' and
pages' names, translated.

| | Django's | Translated |
|---|---|---|
| English | `/admin/scheduling/appointment/3/change/` | `/admin/scheduling/appointments/3/change/` |
| Spanish | `/es/admin/scheduling/appointment/3/change/` | `/es/admin/agenda/citas/3/modificar/` |

- An app's path: its `verbose_name`; a model's: its `verbose_name_plural`;
  slugified, in ASCII when it can be (`Categorías`: `categorias`).
- Its pages' words (`add`, `change`, `delete`, `history`, and a user's
  `password`): translated by the package (Spanish, French, Italian,
  German and Portuguese so far), and yours (`url_segments`).
- The URL names stay Django's: `reverse("admin:app_model_change", …)`,
  templates and other packages work as always, in the active language.
- Django's paths redirect to the translated ones: links saved before still
  work.
- Paths fixed by hand, so that renaming a model doesn't change its URLs.
- A system check for two apps, or two models, with the same path in some
  language.

Only Django: any `AdminSite` (Django's, Unfold's, your own).

## Install

```bash
pip install django-admin-translated-urls
```

```python
INSTALLED_APPS = [
    ...,
    "django_admin_translated_urls",  # Its translations of the pages' words.
]
```

## Usage

Your admin site, with its mixin:

```python
# myproject/admin.py
from django.contrib import admin
from django_admin_translated_urls import TranslatedURLsMixin


class ProjectAdminSite(TranslatedURLsMixin, admin.AdminSite):
    pass


admin_site = ProjectAdminSite(name="admin")
```

Or `TranslatedURLsAdminSite`, as the default site (`admin.site`, and
`@admin.register`):

```python
# myproject/apps.py
from django.contrib.admin.apps import AdminConfig


class ProjectAdminConfig(AdminConfig):
    default_site = "django_admin_translated_urls.TranslatedURLsAdminSite"
```

```python
INSTALLED_APPS = ["myproject.apps.ProjectAdminConfig", ...]  # For "django.contrib.admin".
```

Its URLs in each language with `i18n_patterns`:

```python
from django.conf.urls.i18n import i18n_patterns
from django.urls import path

urlpatterns = i18n_patterns(
    path("admin/", admin_site.urls),
    prefix_default_language=False,  # /admin/ in LANGUAGE_CODE's, /en/admin/…
)
```

Without `i18n_patterns`, they're in `LANGUAGE_CODE`'s: an admin in one
language that isn't English.

Your models' and apps' names need their translations, as for the rest of
the admin (`gettext_lazy`, `makemessages`).

### Fixing a path

By default, a path follows its name: renaming a model's
`verbose_name_plural` changes its URLs (the old ones redirect only if
they're Django's). To keep them, fix them, translatable:

```python
from django.utils.translation import gettext_lazy as _


@admin.register(Appointment, site=admin_site)
class AppointmentAdmin(admin.ModelAdmin):
    url_slug = _("appointments")


class SchedulingConfig(AppConfig):
    name = "scheduling"
    verbose_name = _("Diary")
    url_slug = _("scheduling")
```

An app's, from the site too: `app_url_slugs = {"scheduling": _("agenda")}`.

### Your own pages

A model admin's own pages (`get_urls()`) keep their paths, unless their
words are in its `url_segments`:

```python
from django.utils.translation import pgettext_lazy


class AppointmentAdmin(admin.ModelAdmin):
    url_segments = {"calendar": pgettext_lazy("admin URL", "calendar")}

    def get_urls(self):
        return [
            path("calendar/", self.admin_site.admin_view(self.calendar_view), name="…"),
            *super().get_urls(),
        ]
```

`calendar/`: `/es/admin/agenda/citas/calendario/`. A word is a whole
segment of a `path()`'s route (not a regular expression's); the site's
`url_segments` are every model's (`URL_SEGMENTS` and yours, to add one).

### Options

| Of the site | |
|---|---|
| `url_segments` | Every model's pages' words, translated (`URL_SEGMENTS`). |
| `app_url_slugs` | An app's path, by its label. |
| `redirect_untranslated_urls` | Django's paths redirect to theirs (`True`). |
| `get_app_url_slug(app_label)`, `get_model_url_slug(model, model_admin)`, `get_url_segments(model_admin)` | To choose them otherwise. |

| Of a model admin | |
|---|---|
| `url_slug` | Its path, instead of its `verbose_name_plural`'s. |
| `url_segments` | Its own pages' words, translated. |

| Of an app | |
|---|---|
| `AppConfig.url_slug` | Its path, instead of its `verbose_name`'s. |

### Checks

`manage.py check`, in each language of `LANGUAGES`:

- `admin_translated_urls.E001`: two apps with the same path.
- `admin_translated_urls.E002`: an app's path is the site's own
  (`login/`, `logout/`…).
- `admin_translated_urls.E003`: two models of an app with the same path.
- `admin_translated_urls.E004`: two words of a model's pages with the same
  path.

## Compatibility

Django 4.2 to 6.1, Python 3.10 to 3.14.

## License

MIT
