Skip to content

Commit

Permalink
Default behaviour (#14)
Browse files Browse the repository at this point in the history
* Add examples to readme

* Easy onboarding, add app with default behaviour
  • Loading branch information
allcaps authored Aug 7, 2024
1 parent ae68ecb commit 89bfb5a
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 111 deletions.
186 changes: 131 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,20 @@ Use Wagtail Translate to machine translate your Wagtail contents.
- [Documentation](https://github.com/allcaps/wagtail-translate/blob/main/README.md)
- [Changelog](https://github.com/allcaps/wagtail-translate/blob/main/CHANGELOG.md)
- [Contributing](https://github.com/allcaps/wagtail-translate/blob/main/CONTRIBUTING.md)
- [Discussions](https://github.com/allcaps/wagtail-translate/discussions)
- [Issues](https://github.com/allcaps/wagtail-translate/issues)
- [Security](https://github.com/allcaps/wagtail-translate/security)

## Supported versions

- Python ...
- Django ...
- Wagtail ...
- Python 3.8 - 3.12
- Django 4.2 - 5.0
- Wagtail 5.2 - 6.0

## Setup i18n
## Internationalization

You need to configure your project for authoring content in multiple languages.
See Wagtail documentation on [internationalization](https://docs.wagtail.org/en/stable/advanced_topics/i18n.html).

First, set up your project following the official Wagtail i18n instructions:
https://docs.wagtail.org/en/stable/advanced_topics/i18n.html

### TL;DR

Expand All @@ -35,12 +36,13 @@ USE_L10N = True

LANGUAGE_CODE = 'en'
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [
('en', "English"),
(LANGUAGE_CODE, "English"),
('fr', "French"),
]

INSTALLED_APPS += [
"wagtail.locales",
"wagtail.contrib.simple_translation",
]

MIDDLEWARE += [
Expand Down Expand Up @@ -68,93 +70,167 @@ python -m pip install wagtail-translate
``` python
INSTALLED_APPS = [
"wagtail_translate",
"wagtail.contrib.simple_translation",
"wagtail_translate.default_behaviour",
...
]

WAGTAIL_TRANSLATE_TRANSLATOR = "wagtail_translate.translators.rot13.ROT13Translator"
```

In your `apps.py`:

``` python
In the Wagtail admin interface, go to the homepage, in the dot-dot-dot-menu, choose "Translate".

The contents should be translated! 🥳

This example uses the `ROT13Translator`. It shifts each letter by 13 places. Applying it twice will return the text to its original form. So ROT13 is good for testing and evaluation, but not for real-world use.

## Deepl

Wagtail Translation has a [DeepL](https://www.deepl.com/) translator.
Install and configure it as follows:

- `pip install deepl`
- Get a DeepL API key from https://www.deepl.com/pro#developer
- Add `WAGTAIL_TRANSLATE_DEEPL_KEY = "..."` to your settings.
- Change `WAGTAIL_TRANSLATE_TRANSLATOR` to `"wagtail_translate.translators.deepl.DeepLTranslator"`.

## Customizing Translation Logic

Multi-language projects often have specific requirements for the translation process. Wagtail Translate allows developers to tailor the translation process to their needs.

This section provides examples of how to customize the translation logic. These are illustrations and not mandatory for using this package.

### Customizing the translation process, the basics

In `settings.py`:
- Remove `"wagtail_translate.default_behaviour"` from `INSTALLED_APPS`.
- Remove `WAGTAIL_TRANSLATE_TRANSLATOR = "..."` from the settings.

In one of your project apps, in `apps.py`:

```python
from django.apps import AppConfig


class YourAppConfig(AppConfig):
...
name = "your_app"

def ready(self):
from . import signals # noqa
```
Create a `signals.py`:
Add `signals.py` to your app:

```python
from django.dispatch import receiver
from wagtail.models import Page, TranslatableMixin
from wagtail_translate.translators.rot13 import ROT13Translator as Translator
import django.dispatch
from wagtail.models import Page

copy_for_translation_done = django.dispatch.Signal()
from wagtail_translate.translators.deepl import DeepLTranslator

# Wagtail 6.2 introduces the `copy_for_translation_done` signal.
try:
from wagtail.signals import copy_for_translation_done
except ImportError:
from wagtail_translate.signals import copy_for_translation_done

@receiver(copy_for_translation_done)
def actual_translation(sender, source_obj, target_obj, **kwargs):
"""
Perform actual translation.
def handle_translation_done_signal(sender, source_obj, target_obj, **kwargs):
# Get the source and target language codes
source_language_code = source_obj.locale.language_code
target_language_code = target_obj.locale.language_code

Wagtail triggers the copy_for_translation_done signal,
and this signal handler translates the contents.
# Initialize the translator, and translate.
translator = DeepLTranslator(source_language_code, target_language_code)
translated_obj = translator.translate_obj(source_obj, target_obj)

The source_obj must be a subclass of TranslatableMixin.
# Differentiate between regular Django model and Wagtail Page.
if isinstance(translated_obj, Page):
translated_obj.save_revision()
else:
translated_obj.save()
```

Integrators are expected to define their own signal receiver.
This receiver allows easy customization of behaviors:
You now have ejected from the Wagtail Translate default behaviour and can customize the translation process.

- A custom Translator class can be specified.
- Pages can be saved as drafts (to be reviewed)
or published (directly visible to the public).
- Custom workflows can be triggered.
- Data can be post-processed.
- And more ...
"""
### Using a custom translation service

if not issubclass(target_obj.__class__, TranslatableMixin):
raise Exception(
"Object must be a subclass of TranslatableMixin. "
f"Got {type(target_obj)}."
)
You might want to connect to your preferred machine translation service, subclass BaseTranslator:

# Get the source and target language codes
```python
# your_app/translators.py

from wagtail_translate.translators.base import BaseTranslator

class CustomTranslator(BaseTranslator):
def translate(self, source_string: str) -> str:
"""
Translate, a function that does the actual translation.
Add a call to your preferred translation service.
You'd supply the following values to the translation service:
- source_string
- self.source_language_code
- self.target_language_code
"""
translation = ... # Your translation logic here
return translation
```
Import your `CustomTranslator` in `signals.py` and use it.

### Using different translation services for various languages

For example, DeepL doesn't support Icelandic. So, we want to use some `CustomIcelandicTranslator` for Icelandic translations and `DeeplTranslator` for other languages.

```python
...

@receiver(copy_for_translation_done)
def my_translation_done_receiver(sender, source_obj, target_obj, **kwargs):
source_language_code = source_obj.locale.language_code
target_language_code = target_obj.locale.language_code

# Initialize the translator, and translate.
translator = Translator(source_language_code, target_language_code)
if source_language_code == "is" or target_language_code == "is":
translator_class = CustomIcelandicTranslator
else:
translator_class = DeeplTranslator

translator = translator_class(source_language_code, target_language_code)
translated_obj = translator.translate_obj(source_obj, target_obj)

# Differentiate between regular Django model and Wagtail Page.
# - Page instances have `save_revision` and `publish` methods.
# - Regular Django model (aka Wagtail Snippet) need to be saved.
if isinstance(translated_obj, Page):
# Calling `publish` is optional,
# and will publish the translated page.
# Without, the page will be in draft mode.
translated_obj.save_revision().publish()
translated_obj.save_revision()
else:
translated_obj.save()
```

In Wagtail admin interface, go to the homepage `/admin/pages/2/`, in the dot-dot-dot-menu, choose "Translate".
### Direct publishing of translations

The contents should be translated.
Change `translated_obj.save_revision()` to `translated_obj.save_revision().publish()` to publish the translated page directly.

## Deepl
### Store the source locale on the translated object

Install and configure [DeepL](https://www.deepl.com/) translator:
Sometimes it is useful to content editors to know the source language of the translated object. This can be done by adding a `source_locale` field to the model:

```python
class MyTranslatedModel(models.Model):
source_locale = models.ForeignKey(Locale, blank=True, null=True, on_delete=models.SET_NULL)
...

panels = [
FieldPanel("source_locale", readonly=True),
]
```
Run `makemigrations` and `migrate` to add the field to the database.

Set the field in the signal receiver:

```python
@receiver(copy_for_translation_done)
def my_translation_done_receiver(sender, source_obj, target_obj, **kwargs):
...
translated_obj.source_locale = source_obj.locale
translated_obj.save()
```

- `pip install deepl`
- Get a DeepL API key from https://www.deepl.com/pro#developer
- Add `WAGTAIL_TRANSLATE_DEEPL_KEY` to your settings.
- In your `signals.py`: `from wagtail_translate.translators.deepl import DeeplTranslator as Translator`

## Contributing

Expand Down
Empty file.
8 changes: 8 additions & 0 deletions src/wagtail_translate/default_behaviour/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.apps import AppConfig


class TestAppAppConfig(AppConfig):
name = "wagtail_translate.default_behaviour"

def ready(self):
from . import signals # noqa
40 changes: 40 additions & 0 deletions src/wagtail_translate/default_behaviour/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import importlib

from django.conf import settings
from django.dispatch import receiver
from wagtail.models import Page


# Wagtail 6.2 introduces the `copy_for_translation_done` signal.
try:
from wagtail.signals import copy_for_translation_done
except ImportError:
from wagtail_translate.signals import copy_for_translation_done


def load_class(class_path):
parts = class_path.rsplit(".", 1)
module_path = parts[0]
class_name = parts[1]
module = importlib.import_module(module_path)
return getattr(module, class_name)


@receiver(copy_for_translation_done)
def handle_translation_done_signal(sender, source_obj, target_obj, **kwargs):
# Get the source and target language codes
source_language_code = source_obj.locale.language_code
target_language_code = target_obj.locale.language_code

# Initialize the translator, and translate.
loaded_class = load_class(settings.WAGTAIL_TRANSLATE_TRANSLATOR)
translator = loaded_class(source_language_code, target_language_code)
translated_obj = translator.translate_obj(source_obj, target_obj)

# Differentiate between regular Django model and Wagtail Page.
# - Page instances have `save_revision` and `publish` methods.
# - Regular Django model (aka Wagtail Snippet) has a `save` method.
if isinstance(translated_obj, Page):
translated_obj.save_revision()
else:
translated_obj.save()
4 changes: 0 additions & 4 deletions tests/testapp/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,3 @@ class TestAppAppConfig(AppConfig):
name = "tests.testapp"
verbose_name = "Test App"
default_auto_field = "django.db.models.BigAutoField"

def ready(self):
# We patch Wagtail to inject the copy_for_translation_done signal.
from . import signals # noqa
52 changes: 0 additions & 52 deletions tests/testapp/signals.py

This file was deleted.

3 changes: 3 additions & 0 deletions tests/testproject/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
INSTALLED_APPS = [
"tests.testapp",
"wagtail_translate",
"wagtail_translate.default_behaviour",
"wagtail.contrib.simple_translation",
"wagtail.locales",
"wagtail.contrib.search_promotions",
Expand Down Expand Up @@ -62,6 +63,8 @@
"django.contrib.sitemaps",
]

WAGTAIL_TRANSLATE_TRANSLATOR = "wagtail_translate.translators.rot13.ROT13Translator"

MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
Expand Down

0 comments on commit 89bfb5a

Please sign in to comment.