Metadata-Version: 2.4
Name: dj-i18n-translate
Version: 0.3.1
Summary: Small Django model translation helpers with admin integration.
Author: Kitcat
License-Expression: MIT
Keywords: django,i18n,translation,admin
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django<7.0,>=5.2
Provides-Extra: drf
Requires-Dist: djangorestframework>=3.15; extra == "drf"
Provides-Extra: ninja
Requires-Dist: django-ninja>=1.0; extra == "ninja"
Provides-Extra: google
Requires-Dist: google-cloud-translate>=3.0; extra == "google"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-django; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: tox; extra == "dev"
Requires-Dist: djangorestframework>=3.15; extra == "dev"
Requires-Dist: django-ninja>=1.0; extra == "dev"
Requires-Dist: google-cloud-translate>=3.0; extra == "dev"
Dynamic: license-file

# dj-i18n-translate

[![PyPI version](https://img.shields.io/pypi/v/dj-i18n-translate.svg)](https://pypi.org/project/dj-i18n-translate/)
[![Python versions](https://img.shields.io/pypi/pyversions/dj-i18n-translate.svg)](https://pypi.org/project/dj-i18n-translate/)
[![Django versions](https://img.shields.io/badge/django-5.2%20%7C%206.0-0C4B33.svg)](https://www.djangoproject.com/)
[![License](https://img.shields.io/pypi/l/dj-i18n-translate.svg)](LICENSE)

Small Django model translation helpers for Django 5.2+ and Django 6.x.

Use this package when your source model keeps the default language fields, and
each translated language should live in a separate related row.

## Contents

- [Install](#install)
- [Basic setup](#basic-setup)
- [Create and update translations](#create-and-update-translations)
- [Read translations](#read-translations)
- [Django admin](#django-admin)
- [Automatic translation](#automatic-translation)
- [DRF integration](#drf-integration)
- [Django Ninja integration](#django-ninja-integration)
- [Settings](#settings)
- [Example project](#example-project)

## Install

```bash
pip install dj-i18n-translate
```

Optional integrations:

```bash
pip install "dj-i18n-translate[drf]"
pip install "dj-i18n-translate[ninja]"
pip install "dj-i18n-translate[google]"
```

Install combined extras when needed:

```bash
pip install "dj-i18n-translate[drf,ninja,google]"
```

Add the package to `INSTALLED_APPS`:

```python
INSTALLED_APPS = [
    # ...
    "dj_i18n_translate",
    "catalog",
]
```

## Basic setup

Start with a normal Django model. Keep the default language on the source model.
Create the translation model at module scope so Django migrations can discover
it.

```python
# catalog/models.py
from django.db import models
from dj_i18n_translate.factory import create_translation_model
from dj_i18n_translate.models import TranslatableMixin


class Category(TranslatableMixin, models.Model):
    name = models.CharField(max_length=255)
    description = models.TextField(blank=True)

    def __str__(self):
        return self.name


CategoryTranslation = create_translation_model(
    Category,
    fields=["name", "description"],
)
```

Run migrations normally:

```bash
python manage.py makemigrations
python manage.py migrate
```

The generated model contains:

- `i18n_source`: foreign key to the source model.
- `i18n_language`: indexed language code.
- one nullable copy of each translated field.
- a unique constraint for `i18n_source` and `i18n_language`.

Only concrete non-relation fields can be translated.

### Custom related name

By default, translations are available through `category.translations`. Override
that when the name conflicts with your model.

```python
CategoryTranslation = create_translation_model(
    Category,
    fields=["name", "description"],
    related_name="localized",
)
```

Usage:

```python
category.localized.create(
    i18n_language="vi",
    name="Chu de",
    description="Noi dung tieng Viet",
)
```

### Custom table name

By default, Django generates the translation table name from the app label and
translation model name. Override it with `db_table` when you need a fixed table
name.

```python
CategoryTranslation = create_translation_model(
    Category,
    fields=["name", "description"],
    db_table="catalog_category_i18n",
)
```

### Custom manager

`TranslatableMixin` already provides a manager with `with_translations()`. If
your model needs a custom manager, build it from `TranslatableQuerySet`.

```python
from django.db import models
from dj_i18n_translate.models import TranslatableMixin, TranslatableQuerySet


class CategoryManager(models.Manager.from_queryset(TranslatableQuerySet)):
    def published(self):
        return self.filter(is_published=True)


class Category(TranslatableMixin, models.Model):
    objects = CategoryManager()
    name = models.CharField(max_length=255)
    is_published = models.BooleanField(default=True)
```

Usage:

```python
categories = Category.objects.published().with_translations("vi")
```

### Decorator shortcut

Use the decorator only when you do not need a named `CategoryTranslation`
variable in the module.

```python
from django.db import models
from dj_i18n_translate.decorators import translatable
from dj_i18n_translate.models import TranslatableMixin


@translatable(fields=["title"])
class Article(TranslatableMixin, models.Model):
    title = models.CharField(max_length=255)
```

## Create and update translations

Create translations through the generated model:

```python
category = Category.objects.create(
    name="Keyboard themes",
    description="Visual resources for keyboard apps.",
)

CategoryTranslation.objects.create(
    i18n_source=category,
    i18n_language="vi",
    name="Chu de ban phim",
    description="Tai nguyen hinh anh cho ung dung ban phim.",
)
```

For imports or sync jobs, use `update_or_create()`:

```python
CategoryTranslation.objects.update_or_create(
    i18n_source=category,
    i18n_language="fr",
    defaults={
        "name": "Themes clavier",
        "description": "Ressources visuelles pour les apps clavier.",
    },
)
```

## Read translations

### Single object

```python
category.get_translated_field("name", "vi")
category.get_translated_field("description", "vi")
```

If no translation exists, `get_translated_field()` returns the source field
value by default.

```python
category.get_translated_field("name", "fr")  # falls back to category.name
category.get_translated_field("name", "fr", fallback=False)  # returns None
```

Use `translate()` when you need the translation row itself:

```python
translation = category.translate("vi")
if translation:
    print(translation.name)
```

### Lists and views

Prefetch translations before rendering lists.

```python
# catalog/views.py
from django.shortcuts import render
from .models import Category


def category_list(request):
    language = request.GET.get("lang", "en")
    categories = Category.objects.with_translations(language).order_by("id")
    rows = [
        {
            "name": category.get_translated_field("name", language),
            "description": category.get_translated_field("description", language),
        }
        for category in categories
    ]

    return render(
        request,
        "catalog/category_list.html",
        {"rows": rows, "language": language},
    )
```

Template usage:

```django
{% for row in rows %}
  <h2>{{ row.name }}</h2>
  <p>{{ row.description }}</p>
{% endfor %}
```

If the language is resolved later, prefetch all translations:

```python
categories = Category.objects.with_translations().order_by("id")
```

## Django admin

### Manual translation inline

Use `TranslatableAdminMixin` when editors should type translations manually.
This does not call any machine translator.

Setup:

```python
# catalog/admin.py
from django.contrib import admin
from dj_i18n_translate.admin import TranslatableAdminMixin
from .models import Category


@admin.register(Category)
class CategoryAdmin(TranslatableAdminMixin, admin.ModelAdmin):
    list_display = ["id", "name"]
```

Usage:

1. Open a `Category` in Django admin.
2. Add one inline row per target language.
3. Save the source object.

### Inline layout options

```python
@admin.register(Category)
class CategoryAdmin(TranslatableAdminMixin, admin.ModelAdmin):
    translation_inline_stacked = True
    translation_inline_collapse = False
```

## Automatic translation

Use `AutoTranslateAdminMixin` when selected admin rows should be translated by a
configured translator.

### Custom translator setup

Create a translator class:

```python
# catalog/translators.py
from dj_i18n_translate.translators import BaseTranslator


class ExampleTranslator(BaseTranslator):
    def translate(self, text, source_language, target_language):
        return f"{text} ({target_language})"

    def get_supported_languages(self):
        return ["en", "vi", "fr"]
```

Configure it:

```python
# settings.py
DJ_I18N_TRANSLATE_DEFAULT_LANGUAGE = "en"
DJ_I18N_TRANSLATE_SUPPORTED_LANGUAGES = ["en", "vi", "fr"]
DJ_I18N_TRANSLATE_TRANSLATOR = "catalog.translators.ExampleTranslator"
```

Register the admin:

```python
from django.contrib import admin
from dj_i18n_translate.admin import AutoTranslateAdminMixin
from .models import Category


@admin.register(Category)
class CategoryAdmin(AutoTranslateAdminMixin, admin.ModelAdmin):
    list_display = ["id", "name"]
```

Usage:

1. Select rows in the changelist.
2. Run the `Translate selected objects` action.
3. Translation rows are created or updated for configured target languages.

`DJ_I18N_TRANSLATE_DEFAULT_LANGUAGE` is skipped as a target language.

To exercise the translation flow without calling the configured translator:

```python
DJ_I18N_TRANSLATE_DRY_RUN = True
```

The translator setting is still required. Each non-empty value is saved with
` (translated)` appended, using the configured supported languages.

### Batch translator

Implement `translate_batch()` for fewer provider calls.

```python
class ExampleBatchTranslator(BaseTranslator):
    def translate_batch(self, texts, source_language, target_language):
        return [f"{text} ({target_language})" for text in texts]

    def get_supported_languages(self):
        return ["en", "vi", "fr"]
```

`BaseTranslator` bridges `translate()` and `translate_batch()`, so a translator
can implement either one.

### Google Translate setup

Install the extra:

```bash
pip install "dj-i18n-translate[google]"
```

Configure the built-in adapter:

```python
DJ_I18N_TRANSLATE_TRANSLATOR = "dj_i18n_translate.translators.GoogleTranslator"
DJ_I18N_TRANSLATE_GOOGLE_PROJECT_ID = "my-gcp-project"
DJ_I18N_TRANSLATE_GOOGLE_LOCATION = "global"
DJ_I18N_TRANSLATE_GOOGLE_CREDENTIALS_FILE = "/path/to/service-account.json"
```

If `DJ_I18N_TRANSLATE_GOOGLE_CREDENTIALS_FILE` is not set, Google Application
Default Credentials are used.

## DRF integration

Install the extra:

```bash
pip install "dj-i18n-translate[drf]"
```

Add DRF to `INSTALLED_APPS` if your project does not already have it:

```python
INSTALLED_APPS = [
    # ...
    "rest_framework",
    "dj_i18n_translate",
]
```

### Serializer setup

```python
# catalog/api.py
from rest_framework import serializers
from dj_i18n_translate.drf import TranslatableSerializerMixin
from .models import Category


class CategorySerializer(TranslatableSerializerMixin, serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = ["id", "name", "description"]
```

### List API usage

```python
from rest_framework.generics import ListAPIView


class CategoryListAPIView(ListAPIView):
    serializer_class = CategorySerializer

    def get_queryset(self):
        return Category.objects.with_translations().order_by("id")
```

Request translated output:

```http
GET /api/categories?lang=vi
Accept-Language: vi
```

The query parameter wins over the header.

### ViewSet usage

```python
from rest_framework import viewsets
from dj_i18n_translate.drf import TranslatableViewSetMixin


class CategoryViewSet(
    TranslatableViewSetMixin,
    viewsets.ReadOnlyModelViewSet,
):
    queryset = Category.objects.order_by("id")
    serializer_class = CategorySerializer
```

## Django Ninja integration

Install the extra:

```bash
pip install "dj-i18n-translate[ninja]"
```

### Schema setup

```python
# catalog/api.py
from ninja import NinjaAPI
from dj_i18n_translate.ninja import create_translatable_schema
from dj_i18n_translate.ninja import prefetch_translations
from .models import Category


api = NinjaAPI()

CategorySchema = create_translatable_schema(
    Category,
    name="CategorySchema",
    fields=["id", "name", "description"],
)
```

### Endpoint usage

```python
@api.get("/categories", response=list[CategorySchema])
def categories(request):
    queryset = Category.objects.order_by("id")
    return prefetch_translations(queryset)
```

Request translated output:

```http
GET /api/categories?lang=vi
Accept-Language: vi
```

`create_translatable_schema()` keeps the normal Ninja model schema behavior and
adds resolvers only for selected translated fields.

## Settings

All settings are optional.

```python
DJ_I18N_TRANSLATE_DEFAULT_LANGUAGE = "en"
DJ_I18N_TRANSLATE_SUPPORTED_LANGUAGES = ["en", "vi", "fr"]
DJ_I18N_TRANSLATE_RELATED_NAME = "translations"
DJ_I18N_TRANSLATE_TRANSLATOR = "catalog.translators.ExampleTranslator"
DJ_I18N_TRANSLATE_DRY_RUN = False
DJ_I18N_TRANSLATE_LANGUAGE_QUERY_PARAM = "lang"
DJ_I18N_TRANSLATE_LANGUAGE_HEADER = "Accept-Language"
DJ_I18N_TRANSLATE_LANGUAGE_ALIASES = {"in": "id"}
DJ_I18N_TRANSLATE_GOOGLE_PROJECT_ID = "my-gcp-project"
DJ_I18N_TRANSLATE_GOOGLE_LOCATION = "global"
DJ_I18N_TRANSLATE_GOOGLE_CREDENTIALS_FILE = "/path/to/service-account.json"
```

| Setting | Default | Used for |
| --- | --- | --- |
| `DEFAULT_LANGUAGE` | `"en"` | Source language and admin auto-translate skip target. |
| `SUPPORTED_LANGUAGES` | `[]` | Target languages for auto-translate. |
| `RELATED_NAME` | `"translations"` | Default source-to-translation related name. |
| `TRANSLATOR` | `None` | Import path for automatic translation. |
| `DRY_RUN` | `False` | Append ` (translated)` without calling the configured translator. |
| `LANGUAGE_QUERY_PARAM` | `"lang"` | Request query parameter checked first. |
| `LANGUAGE_HEADER` | `"Accept-Language"` | Request header checked after the query parameter. |
| `LANGUAGE_ALIASES` | `{"in": "id"}` | Target language code aliases for translator providers. |
| `GOOGLE_PROJECT_ID` | `None` | Google Cloud project for Google Translate. |
| `GOOGLE_LOCATION` | `"global"` | Google Translate location. |
| `GOOGLE_CREDENTIALS_FILE` | `None` | Optional Google service-account file. |

## Example project

Runnable examples and snapshots live in [example/README.md](example/README.md).
