Metadata-Version: 2.4
Name: djangocms_deepl_translations
Version: 0.1.0
Summary: 
License: GNU GPLv3
License-File: AUTHORS.md
License-File: LICENSE
Author: Kapt dev team
Author-email: dev@kapt.mobi
Requires-Python: >=3.10,<4
Classifier: Development Status :: 5 - Production/Stable
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 CMS :: 5.0
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: License :: Other/Proprietary License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
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
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Requires-Dist: deepl
Requires-Dist: django-cms
Requires-Dist: djangocms-transfer
Requires-Dist: djangocms-versioning
Project-URL: Repository, https://gitlab.com/kapt/open-source/djangocms-deepl-translations
Description-Content-Type: text/markdown

# djangocms-deepl-translations

Allows to translate Django CMS 5+ content using the DeepL API.

![Pipeline badge](https://gitlab.com/kapt/open-source/djangocms-deepl-translations/badges/main/pipeline.svg)
![Coverage badge](https://gitlab.com/kapt/open-source/djangocms-deepl-translations/badges/main/coverage.svg)
![Release badge](https://gitlab.com/kapt/open-source/djangocms-deepl-translations/-/badges/release.svg)

## Installation

1. Install the module from [PyPI](https://pypi.org/project/djangocms-deepl-translations/):
    ```
    python3 -m pip install djangocms-deepl-translations
    ```

    Or, if you're using poetry :
    ```
    poetry add djangocms-deepl-translations
    ```

2. Add it in your `INSTALLED_APPS`:
    ```
      "djangocms_deepl_translations",
      "djangocms_transfer",
    ```

    **Important — Order when using contribs:** If you use optional contribs (e.g. `djangocms_deepl_translations.contrib.djangocms_stories`), list them **before** `djangocms_deepl_translations`. The contrib’s `ready()` must run before the main app’s admin registers its URLs, so that the contrib can register its “translate to” view in the registry. Otherwise the admin will not expose the contrib’s translation URL.

    Example with the Stories contrib:
    ```
      "djangocms_deepl_translations.contrib.djangocms_stories",
      "djangocms_deepl_translations",
      "djangocms_transfer",
    ```

3. Run the migrations:
    ```
    python manage.py migrate
    ```

## Configuration

### Required settings

- `DJANGOCMS_DEEPL_TRANSLATIONS_API_KEY`: set in your Django settings.

### Optional settings

- `DJANGOCMS_DEEPL_TRANSLATIONS_API_LANGUAGE_MAPPING`: a dict mapping your project’s language codes to DeepL API language codes (e.g. `"en"` → `"EN-GB"`, `"fr"` → `"FR"`). If unset, the default mapping is used (`fr` → `"FR"`, `en` → `"EN-GB"`, `es` → `"ES"`, `de` → `"DE"`, `it` → `"IT"`, `pt` → `"PT-PT"`, `nl` → `"NL"`).

- `DJANGOCMS_DEEPL_TRANSLATIONS_WARN_UNCONFIGURED_PLUGINS`: when `True` (default), Django's system check warns when installed CMS plugin types have no translation config. Set to `False` to disable these warnings.

- `DJANGOCMS_DEEPL_TRANSLATIONS_BULK_TRANSLATE_DEFAULT_USER_ID`: optional integer user id used by `bulk_translate_pages` (and related bulk commands) when `--user` is not passed; falls back to the first superuser if unset.


#### Plugin translation config: `DJANGOCMS_DEEPL_TRANSLATIONS_PLUGIN_CONFIG`

This setting defines, for each CMS plugin type, which model fields are sent to the DeepL API and how embedded plugins (e.g. links inside text) are handled. Entries **override or extend** the built-in defaults (TextPlugin from [djangocms-text](https://github.com/django-cms/djangocms-text), LinkPlugin from [djangocms-link](https://github.com/django-cms/djangocms-link)).

Each key is a plugin type name (the class `__name__`, e.g. `"TextPlugin"`, `"LinkPlugin"`). Each value is a dict with the following optional parameters:

| Parameter | Description |
|-----------|-------------|
| **`translatable_fields`** | Tuple of field paths whose values are sent to the DeepL API. Each item is a field name (e.g. `"name"`) or a **nested path** using `/`: e.g. `"config/accordion_item_header"`|
| **`no_translation`** | Boolean. If `True`, this plugin type is not translated (no API call). Use for structural or non-text plugins. Cannot be combined with `translatable_fields`. |

**Example** (override default, add a custom plugin):

```python
DJANGOCMS_DEEPL_TRANSLATIONS_PLUGIN_CONFIG = {
    # Override TextPlugin
    "TextPlugin": {
        "translatable_fields": ("body",),
    },
    # Add config for a plugin that has no translatable content
    "StructuralPlugin": {
        "no_translation": True,
    },
    # Add config for a custom plugin with two translatable fields
    "MyCustomPlugin": {
        "translatable_fields": ("title", "description"),
    },
    # Nested path: value at plugin_data["data"]["config"]["accordion_item_header"]
    "AccordionItemPlugin": {
        "translatable_fields": ("config/accordion_item_header",),
    },
}
```

When `DJANGOCMS_DEEPL_TRANSLATIONS_WARN_UNCONFIGURED_PLUGINS` is `True` (default), Django's system check (e.g. `manage.py check`) warns with a single message listing all **installed** plugin types that have no entry in this config (or in the built-in defaults), so you can add one and avoid incomplete translations. The message also mentions the `suggest_deepl_plugin_config` command to generate a base config.

##### Management command: `suggest_deepl_plugin_config`

The command `suggest_deepl_plugin_config` lists all installed CMS plugin types that do not yet have a translation config (neither in the built-in defaults nor in `DJANGOCMS_DEEPL_TRANSLATIONS_PLUGIN_CONFIG`). It prints on stdout a ready-to-paste Python snippet that adds an entry for each of them with `no_translation: True`, so that the system check no longer warns and translation is explicitly disabled until you configure them.

**When to use it:** When you first set up the module or after installing new CMS plugins, to quickly get a full config and then replace `no_translation: True` with real options (e.g. `translatable_fields`) only for the plugins you want to translate.

**Usage:**

```bash
python manage.py suggest_deepl_plugin_config
```

**Example output** (when some plugins are unconfigured):

```python
# Add this to your settings to disable translation for plugins not yet configured.
# Then replace no_translation=True with translatable_fields (etc.) as needed.
DJANGOCMS_DEEPL_TRANSLATIONS_PLUGIN_CONFIG = {
    "PlaceholderPlugin": {
        "no_translation": True,
    },
    "URLFieldPlugin": {
        "no_translation": True,
    },
}
```

Copy the printed block into your settings file. If every installed plugin already has a config, the command prints a success message and no snippet.

### Management command: bulk_translate_pages

Bulk-translate **published** CMS pages from a source language to a target language using the same pipeline as the admin “Translate to” action (`PageTranslator` + DeepL API). Only pages that have a **published** `PageContent` in the source language (via **djangocms-versioning**) are processed.

**Two-phase run (default, without `--create-only`):**

1. **Phase 1** — For each page: create and publish target-language page content with **empty placeholders** (titles/slugs/meta still translated). This makes target pages exist so links in content can resolve correctly on the next pass.
2. **Phase 2** — Full translation: export placeholders, translate plugins, fill target, publish.

**Typical workflow:** Run the full command once (both phases). If you maintain many pages and need targets to exist before a second pass, you can run with `--create-only` first, then run again without it to execute phase 2 only on the pages that completed phase 1.

**Usage:**

```bash
python manage.py bulk_translate_pages --source-language fr --target-language en
```

| Option | Description |
|--------|-------------|
| `--source-language` | Required. Django/CMS language code (e.g. `fr`, `en`). |
| `--target-language` | Required. Target language code. |
| `--page-ids` | Optional. One or more page primary keys. If set, only those pages are processed (each must still be published in the source language). Example: `--page-ids 12 34 56`. |
| `--create-only` | Only phase 1: create/publish empty target contents; no placeholder translation. |
| `--fail-silently` | On error, log to stderr and continue. **Default:** stop and re-raise on first error (full traceback). |
| `--user` | User ID used for versioning publish actions. Default: `DJANGOCMS_DEEPL_TRANSLATIONS_BULK_TRANSLATE_DEFAULT_USER_ID` if set, otherwise the first superuser. |

See also [Optional settings](#optional-settings) for `DJANGOCMS_DEEPL_TRANSLATIONS_BULK_TRANSLATE_DEFAULT_USER_ID`. The command name is `bulk_translate_pages` (`python manage.py bulk_translate_pages`).

## Optional contribs

Optional integrations (e.g. for translating articles from [djangocms-stories](https://pypi.org/project/djangocms-stories/)) live under `djangocms_deepl_translations.contrib.*`. Add the contrib app to `INSTALLED_APPS` **before** `djangocms_deepl_translations` (see [Installation](#installation) above), so that the contrib’s `ready()` runs first and registers its “translate to” view in the admin.

| Contrib | App name | Purpose |
|--------|----------|--------|
| Stories (posts/articles) | `djangocms_deepl_translations.contrib.djangocms_stories` | DeepL translation for djangocms-stories posts; toolbar “Translate to” on post detail views. |

When the Stories contrib is installed, you can bulk-translate posts (published source `PostContent` only) with:

```bash
python manage.py bulk_translate_stories_posts --source-language fr --target-language en
```

Options mirror [bulk_translate_pages](#management-command-bulk_translate_pages) above: `--post-ids`, `--create-only`, `--fail-silently`, `--user` (with post primary keys instead of page ids).

## Extracting / updating translations (`i18n_tools`, Poe, `makemessages`, `translate_messages`, `compilemessages`)

### Prerequisites

1. **Install project dependencies** with Poetry from the repository root (see [Installation](#installation) for runtime deps):

   ```bash
   poetry install
   ```

2. **Install [Poethepoet](https://poethepoet.natn.io/)** so the `poe` tasks are available (it is listed under the `dev` dependency group):

   ```bash
   poetry install --with dev
   ```

3. **Optional — auto-translation of `.po` files** uses [django-deep-translator](https://pypi.org/project/django-deep-translator/) via the `translate_messages` management command. Install the `i18n_tools` optional group:

   ```bash
   poetry install --with i18n_tools
   ```

   You can combine groups, e.g. `poetry install --with dev,i18n_tools`.

### The `i18n_tools` package

The [`i18n_tools/`](i18n_tools/) directory holds a small Django entrypoint (`i18n_tools/manage.py`) and settings (`i18n_tools/settings.py`) used only to run:

- **`makemessages`** — extract/update translatable strings into `.po` files under `djangocms_deepl_translations/locale/` and contrib `locale/` trees.
- **`translate_messages`** — fill empty/fuzzy entries in those `.po` files using machine translation (requires `--with i18n_tools`).
- **`compilemessages`** — compile `.po` to `.mo` for use at runtime.

These commands are wired as **Poe** tasks in [`pyproject.toml`](pyproject.toml) (`[tool.poe.tasks.*]`).

### Poe tasks

After `poetry install --with dev` (and `--with i18n_tools` when using `translate_messages`), run tasks as `poe <task>` or, from the repo root:

```bash
poetry makemessages --language fr
poetry translate_messages -l fr
poetry compilemessages
```

| Task | Purpose |
|------|--------|
| `poetry makemessages` | Regenerate `.po` for one locale (`-l` / `--language`, default `fr`). Run for each language you maintain, e.g. `poetry makemessages --language de` and `poetry makemessages --language fr`. |
| `poetry translate_messages` | Auto-translate untranslated/fuzzy strings in `.po` for the **target** locale (`-l`) from the **source** locale (`-s` / `--source-language`, default `en`). |
| `poe compilemessages` | Compile `.po` → `.mo` for one locale (`-l`, default `en`). Run per locale or invoke `poetry run python3 i18n_tools/manage.py compilemessages` with no `-l` to compile every catalog Django finds. |

Example: after `makemessages`, refresh **both**French and German catalogs from English:

Make messages for French and German:
```bash
# Generate .po files for French and German
poetry makemessages -l fr
poetry makemessages -l de
```

Fill .po using English as source (default -s is en):
```bash
# Fill .po using English as source (default -s is en)
poetry translate_messages -l fr
poetry translate_messages -l de
```

Then compile both locales for runtime:
```bash
poe compilemessages
```

**Note:** Machine translation depends on django-deep-translator’s backend configuration (e.g. API keys if applicable). Untranslated strings only are updated when using `-u` (already set in the Poe task).

## Contributing

Feel free to send feedbacks on the module using it's [Home page](https://gitlab.com/kapt/open-source/djangocms-deepl-translations).
See [CONTRIBUTING.md](CONTRIBUTING.md) for contributions.

## Tests

Tests are located in the `tests` directory.

Tests are written with [pytest](https://docs.pytest.org/en/).
They are based on a [testapp models](tests/testapp/models.py) :

To run the tests, you must launch the command [./run_tests.sh](run_tests.sh)

A coverage report can be generated using the command [./run_coverage.sh](run_coverage.sh)

### Cross-version matrix ([tox](https://tox.wiki/))

CI runs **[tox](https://tox.wiki/)** (see [`tox.ini`](tox.ini)) against the Python, Django, and django CMS versions declared as **PyPI classifiers** in [`pyproject.toml`](pyproject.toml): django CMS **5.0.x** with Django **4.2 / 5.0 / 5.1 / 5.2 / 6.0**, and Python **3.10–3.14** (Django **6.x** envs use Python **≥ 3.12** only). Interpreters missing on the machine are skipped (`skip_missing_interpreters`).

Local examples:

```bash
pip install "tox>=4.21"
tox run -e py312-django52-cms50    # one environment
# Full matrix: use run-parallel (not `run -p`) — tox 4 puts `-p` on the parallel subcommand.
tox run-parallel                   # parallel envs; default -p = auto (all logical CPUs)
tox p                              # short alias for run-parallel
tox run-parallel -p 4              # at most 4 envs at once (less RAM / less load)
tox run -e coverage                # coverage + `coverage.xml` (default `python3`, Django 5.2, CMS 5)
```

**Parallelism:** **`tox run`** executes each environment **sequentially**. For several envs at once, use **`tox run-parallel`** (or **`tox p`**); `-p auto` / `-p N` applies to that subcommand, not to `tox run`. Optional **`pytest -n auto`** (pytest-xdist) inside one env: `tox run -e py312-django52-cms50 -- tests -n auto` (Django DB settings may need tweaks); prefer **`run-parallel`** for the matrix first.

#### pyenv

[tox](https://tox.wiki/) does **not** read `~/.pyenv/versions` by itself: it looks for **`python3.10`**, **`python3.11`**, … on your **`PATH`**. That usually means the **pyenv shims** (`~/.pyenv/shims`), after init.

1. **Load pyenv in the shell** you use to run tox (bash example):
   ```bash
   export PYENV_ROOT="${PYENV_ROOT:-$HOME/.pyenv}"
   export PATH="$PYENV_ROOT/bin:$PATH"
   eval "$(pyenv init - bash)"   # zsh: eval "$(pyenv init - zsh)"
   ```
2. **Install** the runtimes you need, e.g. `pyenv install 3.10.13` (repeat for 3.11, 3.12, …).
3. **Expose every version to shims in this repo** (often required when tox is installed via **pipx**, which otherwise sees a minimal `PATH`):
   ```bash
   cd /path/to/djangocms-deepl-translations
   pyenv local 3.14 3.12 3.11 3.10   # use your exact `pyenv versions` names
   ```
   This creates/updates **`.python-version`** so `python3.10`, `python3.11`, … resolve correctly.
4. **Check** before tox: `command -v python3.10 && python3.10 -V`.

The deprecated **tox-pyenv** plugin targeted tox 3; with tox 4, **`pyenv local` + shims on `PATH`** is the usual approach ([reference](https://stackoverflow.com/questions/76836871)).

If your **IDE terminal** does not load `~/.bashrc` / `~/.zshrc`, pyenv may never be on `PATH` — run the `eval "$(pyenv init …)"` block in that terminal or use a login shell.


## License

This project is licensed under the GNU GPLv3 License - see the [LICENSE](LICENSE) file for details.

