Metadata-Version: 2.4
Name: collective.translators
Version: 1.0.0a2
Summary: Pluggable external translation utilities for Plone
Author-email: Mauro Amico <mauro.amico@gmail.com>
License-Expression: GPL-2.0-only
Project-URL: PyPI, https://pypi.org/project/collective.translators
Project-URL: Source, https://github.com/collective/collective.translators
Project-URL: Issues, https://github.com/collective/collective.translators/issues
Project-URL: Changelog, https://github.com/collective/collective.translators/blob/main/CHANGES.md
Keywords: Python,Plone,CMS
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Plone
Classifier: Framework :: Plone :: 6.0
Classifier: Framework :: Plone :: Addon
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
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
Requires-Python: <3.15,>=3.10
Description-Content-Type: text/markdown
License-File: LICENSE.GPL
License-File: LICENSE.md
Requires-Dist: Products.CMFCore
Requires-Dist: Products.CMFPlone
Requires-Dist: Zope
Requires-Dist: plone.api
Requires-Dist: plone.app.multilingual
Requires-Dist: plone.app.registry
Requires-Dist: plone.base
Requires-Dist: plone.browserlayer
Requires-Dist: plone.restapi
Requires-Dist: requests
Provides-Extra: test
Requires-Dist: plone.app.contenttypes; extra == "test"
Requires-Dist: plone.app.testing; extra == "test"
Requires-Dist: plone.restapi[test]; extra == "test"
Requires-Dist: plone.testing; extra == "test"
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: pytest-plone; extra == "test"
Provides-Extra: release
Requires-Dist: zest.pocompile; extra == "release"
Requires-Dist: zest.releaser[recommended]; extra == "release"
Requires-Dist: zestreleaser.towncrier; extra == "release"
Provides-Extra: aws
Requires-Dist: boto3; extra == "aws"
Provides-Extra: chatgpt
Requires-Dist: openai; extra == "chatgpt"
Provides-Extra: deepseek
Requires-Dist: openai; extra == "deepseek"
Provides-Extra: deepl
Requires-Dist: deepl; extra == "deepl"
Provides-Extra: ollama
Requires-Dist: ollama; extra == "ollama"
Provides-Extra: services
Requires-Dist: collective.translators[aws,chatgpt,deepl,deepseek,ollama]; extra == "services"
Dynamic: license-file

# collective.translators

This package extends [plone.app.multilingual](https://github.com/plone/plone.app.multilingual) with pluggable external translation utilities for automatic content translation in Plone.
It integrates several translation providers, so you can configure DeepL, AWS Translate, LibreTranslate, DeepSeek, Ollama, or ChatGPT and use them to translate your content.

This add-on requires [plone.app.multilingual PR #468](https://github.com/plone/plone.app.multilingual/pull/468).
No released version of `plone.app.multilingual` provides the `IExternalTranslationService` interface yet.

## Translation services

Each service registers a named utility that provides `IExternalTranslationService`.
The utilities share the same interface for translating content and for reporting the languages they support, so you can switch between providers without changing anything else.

Configure each service from its own control panel.

Google Translate is not part of this package.
`plone.app.multilingual` already ships it, and you configure it with the Google API key in the *Languages* site setup.

| Service | Utility name | Factory |
| --- | --- | --- |
| AWS Translate | `aws_translate` | `AWSTranslatorFactory` |
| ChatGPT | `chatgpt_translate` | `ChatGPTFactory` |
| DeepL | `deepl_translate` | `DeeplTranslatorFactory` |
| DeepSeek | `deepseek` | `DeepSeekFactory` |
| LibreTranslate | `libretranslate_translate` | `LibreTranslateTranslatorFactory` |
| Ollama | `ollama` | `OllamaFactory` |

### AWS Translate

Uses Amazon AWS Translate.
Reads the credentials and the region from the Plone registry.
Falls back to language autodetection when the source language is unknown.

### ChatGPT

Uses the OpenAI API.
Reads the credentials from the Plone registry.

### DeepL

Uses the DeepL API, on either the Free or the Pro endpoint.
Reads the API key from the Plone registry.
Detects the source language on request, and translates both text and HTML.

### DeepSeek

Uses DeepSeek, a translation API backed by a large language model.
Reads the API key from the Plone registry.
Translates through chat completions.

### LibreTranslate

Uses an [open source LibreTranslate server](https://libretranslate.com/).
You set the server URL and the API key.
Detects the source language on request, and translates both text and HTML.

### Ollama

Uses an Ollama server, so the models run on your own hardware.
You set the server URL and the model.
Use this service for private or offline translation.

## Installation

Add `collective.translators` to the dependencies of your project.

LibreTranslate works out of the box, because it only needs HTTP requests.
The other services need a client library, which you pull in through an extra:

```text
collective.translators[deepl]
collective.translators[deepl,aws]
```

### Add-ons control panel

Every translation service ships its own install and uninstall profile, and appears as a separate entry in the add-ons control panel.

| Add-on | Extra it needs |
| --- | --- |
| Collective Translators: AWS Translate | `collective.translators[aws]` |
| Collective Translators: ChatGPT | `collective.translators[chatgpt]` |
| Collective Translators: DeepL | `collective.translators[deepl]` |
| Collective Translators: DeepSeek | `collective.translators[deepseek]` |
| Collective Translators: LibreTranslate | none |
| Collective Translators: Ollama | `collective.translators[ollama]` |

An entry appears only when the library it needs is importable.
A control panel therefore never points at a service that your site cannot use.

Installing any service also installs the shared `collective.translators:default` profile as a dependency.
That profile holds no configuration of its own, so it stays out of the list while it has nothing to offer.
It appears as `Collective Translators: shared base` only while an upgrade of the add-on is waiting to run, because the control panel reaches the upgrade steps of a product only through a profile it lists.

### Upgrade from 1.0.0a2 or older

Older versions installed every service from a single profile.
To migrate a site, open `/prefs_install_products_form` and run the upgrade that Collective Translators offers there.
The same step is also available from `/portal_setup/manage_upgrades`, under the `collective.translators:default` profile.

The upgrade keeps the settings of the services whose library you installed, API keys included.
It removes the leftover registry records and configlets of the other services.
Install those services again from the add-ons control panel once you add their library.

## Add a new service

Contribute a new translation service in either of two ways:

- Open a pull request against this package, following the structure below.
- Publish a separate Plone add-on that provides an external translation utility with the same interface and the same registration pattern.

Read the code of an existing service, such as DeepL or LibreTranslate, for a concrete example of every file below.

### 1. Implement and register the utility

Your utility class must implement `IExternalTranslationService` from `plone.app.multilingual.interfaces`, with at least these methods:

- `is_available()` returns `True` when the service is enabled and ready.
- `available_languages()` returns the supported language codes, or the supported source and target pairs.
- `translate_content(content, source_language, target_language)` translates the content and returns the translated text.

Register a module level instance of the class in `mytool/configure.zcml`:

```xml
<utility
    provides="plone.app.multilingual.interfaces.IExternalTranslationService"
    name="your_tool_name"
    component=".utility.YourTranslator"
    />
```

### 2. Include the package

Add the include to `src/collective/translators/configure.zcml`.
Guard it with a `zcml:condition` when your service needs a client library:

```xml
<include
    package=".mytool"
    zcml:condition="installed yourlibrary"
    />
```

### 3. Add a control panel

This step is optional.
Skip it when your service needs no configuration.

Write the registry schema and the control panel form in `mytool/controlpanel.py`, then register the browser page and the `plone.restapi` adapter in `mytool/configure.zcml`.
Give the adapter a `configlet_id` that matches the `action_id` of the configlet, otherwise `plone.restapi` leaves your panel out of `@controlpanels`.

Declare a browser layer that extends `collective.translators.interfaces.IBrowserLayer` in `mytool/interfaces.py`, and bind the browser page to that layer.

### 4. Ship an install and an uninstall profile

Register both profiles in `mytool/profiles.zcml`, and include that file from `mytool/configure.zcml`.
The profiles then exist only when the ZCML of your service loads, which is what hides the add-on when the library is missing.

```xml
<genericsetup:registerProfile
    name="default"
    title="Collective Translators: My Tool"
    provides="Products.GenericSetup.interfaces.EXTENSION"
    directory="profiles/default"
    />
```

Put `metadata.xml`, `browserlayer.xml`, `registry.xml`, and `controlpanel.xml` in `mytool/profiles/default/`.
Make `metadata.xml` depend on `profile-collective.translators:default`.

Put `browserlayer.xml`, `registry.xml`, and `controlpanel.xml` in `mytool/profiles/uninstall/`, each one with `remove="true"`.

Add the name of your service to `collective.translators.setuphandlers.SERVICES`, so that its uninstall profile stays hidden from the add-ons control panel.

### 5. Test the service

Restart your site.
Install your add-on from the add-ons control panel.
Open the control panel of your service, enter your API key or your settings, and translate a page.

## Contribute

- [Issue tracker](https://github.com/collective/collective.translators/issues)
- [Source code](https://github.com/collective/collective.translators/)

## License

GPL version 2.

# Changelog

<!--
   You should *NOT* be adding new change log entries to this file.
   You should create a file in the news directory instead.
   For helpful instructions, please see:
   https://github.com/plone/plone.releaser/blob/master/ADD-A-NEWS-ITEM.rst
-->

<!-- towncrier release notes start -->

## 1.0.0a2 (2026-10-01)


### Breaking changes

- Remove the Google Translate service, with its profile and control panel.
  The 1002 upgrade step cleans its registry records and configlet from existing sites. 
- Split the single GenericSetup profile into one install and one uninstall profile per translation service. Each service is now a separate entry in the add-ons control panel, shown only when the library it needs is importable. The shared `collective.translators:default` profile is hidden and installed as a dependency, and it no longer registers a browser layer of its own: every service registers a layer extending `collective.translators.interfaces.IBrowserLayer`. Existing sites are migrated by the upgrade step to profile version 1001, which keeps the settings of the available services and removes the leftovers of the others. [mamico] 


### Bug fixes

- Align `configlet_id` with the `action_id` of the configlet for AWS Translate, ChatGPT, DeepL, DeepSeek, LibreTranslate and Ollama, so the panels are listed by the `@controlpanels` endpoint of `plone.restapi`. [mamico] 
- Register the missing control panel configlet for Ollama. [mamico] 
- Show the `collective.translators:default` profile in the add-ons control panel while it has upgrade steps to run, and hide it the rest of the time. The control panel drops a hidden profile before it builds the entry of its product, so a permanently hidden base profile kept its upgrade steps out of `/prefs_install_products_form`, and those steps are the migration path of the sites installed with the single profile of 1.0.0a2 and older. Keying the decision on pending upgrades rather than on one version number keeps later migrations reachable as well. The uninstall profiles stay hidden at all times. [mamico] 


### Internal

- Move package metadata from `setup.py` to `pyproject.toml` @plone 
- Require `Manage portal` for every control panel view. DeepSeek, Google Translate and LibreTranslate used to require `plone.app.controlpanel.Language`, while their configlet already required `Manage portal`. [mamico] 


### Tests

- Run the test suite with pytest instead of zope-testrunner, and declare `pytest`, `pytest-cov` and `pytest-plone` in the `test` extra. Add coverage of the per service profiles and of the upgrade step. [mamico] 
- Run the tests in CI against the `plone.app.multilingual` branch that provides `IExternalTranslationService`, checked out by mxdev from `tox -e init`, and move the constraints to the Plone 6.2 set the test matrix already names. Without it the ZCML of the add-on cannot be loaded at all against a released `plone.app.multilingual`. [mamico] 

## 1.0.0a1 (2026-08-24)


### Bug fixes:

- Fixed the issue related to not yet installed package. [mamico] 

## 1.0.0a0 (2026-08-17)


### Internal:

- Update configuration files @plone 

## 100.0.0 (2025-05-11)

No significant changes.


## 1.0.0 (2025-05-11)

No significant changes.
