0
0
mirror of https://github.com/wagtail/wagtail.git synced 2024-11-29 09:33:54 +01:00

Add contrib simple translation (#6528)

This commit is contained in:
Coen van der Kamp 2021-04-14 09:56:00 +02:00 committed by GitHub
parent 4f8ef843d0
commit b1b69360a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 899 additions and 7 deletions

View File

@ -21,10 +21,9 @@ This document describes how to configure Wagtail for authoring content in
multiple languages.
.. note::
Wagtail provides the infrastructure for creating and serving content in multiple languages,
but does not itself provide an admin interface for managing translations of the same content
across different languages. For this, the `wagtail-localize <https://github.com/wagtail/wagtail-localize>`_
app must be installed separately.
Wagtail provides the infrastructure for creating and serving content in multiple languages.
There are two options for managing translations across different languages in the admin interface:
:ref:`wagtail.contrib.simple_translation<simple_translation>` or the more advanced `wagtail-localize <https://github.com/wagtail/wagtail-localize>`_ (third-party package).
This document only covers the internationalisation of content managed by Wagtail.
For information on how to translate static content in template files, JavaScript
@ -611,9 +610,22 @@ data migration).
Translation workflow
--------------------
As mentioned at the beginning, Wagtail does not supply any built-in user interface
or external integration that provides a translation workflow. This has been left
for third-party packages to solve.
As mentioned at the beginning, Wagtail does supply ``wagtail.contrib.simple_translation``.
The simple_translation module provides a user interface that allows users to copy pages and translatable snippets into another language.
- Copies are created in the source language (not translated)
- Copies of pages are in draft status
Content editors need to translate the content and publish the pages.
To enable add ``"wagtail.contrib.simple_translation"`` to ``INSTALLED_APPS``
and run ``python manage.py migrate`` to create the ``submit_translation`` permissions.
In the Wagtail admin, go to settings and give some users or groups the "Can submit translations" permission.
.. note::
Simple Translation is optional. It can be switched out by third-party packages. Like the more advanced `wagtail-localize <https://github.com/wagtail/wagtail-localize>`_.
Wagtail Localize
^^^^^^^^^^^^^^^^

View File

@ -15,6 +15,7 @@ Wagtail ships with a variety of extra optional modules.
modeladmin/index
postgres_search
searchpromotions
simple_translation
table_block
redirects
legacy_richtext
@ -63,6 +64,12 @@ A module allowing for more customisable representation and management of custom
A module for managing "Promoted Search Results"
:doc:`simple_translation`
-------------------------
A module for copying translatables (pages and snippets) to another language.
:doc:`table_block`
-----------------------

View File

@ -0,0 +1,31 @@
.. _simple_translation:
Simple translation
==================
The simple_translation module provides a user interface that allows users to copy pages and translatable snippets into another language.
- Copies are created in the source language (not translated)
- Copies of pages are in draft status
Content editors need to translate the content and publish the pages.
.. note::
Simple Translation is optional. It can be switched out by third-party packages. Like the more advanced `wagtail-localize <https://github.com/wagtail/wagtail-localize>`_.
Basic configuration
~~~~~~~~~~~~~~~~~~~
Add ``"wagtail.contrib.simple_translation"`` to INSTALLED_APPS in your settings file:
.. code-block:: python
INSTALLED_APPS = [
...
"wagtail.contrib.simple_translation",
]
Run ``python manage.py migrate`` to create the necessary permissions.
In the Wagtail admin, go to settings and give some users or groups the "Can submit translations" permission.

View File

@ -0,0 +1,3 @@
default_app_config = (
"wagtail.contrib.simple_translation.apps.SimpleTranslationAppConfig"
)

View File

@ -0,0 +1,8 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class SimpleTranslationAppConfig(AppConfig):
name = "wagtail.contrib.simple_translation"
label = "simple_translation"
verbose_name = _("Wagtail simple translation")

View File

@ -0,0 +1,48 @@
from django import forms
from django.utils.translation import gettext_lazy, ngettext
from wagtail.core.models import Locale, Page
class SubmitTranslationForm(forms.Form):
# Note: We don't actually use select_all in Python, it is just the
# easiest way to add the widget to the form. It's controlled in JS.
select_all = forms.BooleanField(label=gettext_lazy("Select all"), required=False)
locales = forms.ModelMultipleChoiceField(
label=gettext_lazy("Locales"),
queryset=Locale.objects.none(),
widget=forms.CheckboxSelectMultiple,
)
include_subtree = forms.BooleanField(
required=False, help_text=gettext_lazy("All child pages will be created.")
)
def __init__(self, instance, *args, **kwargs):
super().__init__(*args, **kwargs)
hide_include_subtree = True
if isinstance(instance, Page):
descendant_count = instance.get_descendants().count()
if descendant_count > 0:
hide_include_subtree = False
self.fields["include_subtree"].label = ngettext(
"Include subtree ({} page)",
"Include subtree ({} pages)",
descendant_count,
).format(descendant_count)
if hide_include_subtree:
self.fields["include_subtree"].widget = forms.HiddenInput()
self.fields["locales"].queryset = Locale.objects.exclude(
id__in=instance.get_translations(inclusive=True).values_list(
"locale_id", flat=True
)
)
# Using len() instead of count() here as we're going to evaluate this queryset
# anyway and it gets cached so it'll only have one query in the end.
if len(self.fields["locales"].queryset) < 2:
self.fields["select_all"].widget = forms.HiddenInput()

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.16 on 2021-04-12 20:22
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='SimpleTranslation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
options={
'permissions': [('submit_translation', 'Can submit translations')],
'default_permissions': [],
},
),
]

View File

@ -0,0 +1,16 @@
from django.db.models import Model
class SimpleTranslation(Model):
"""
SimpleTranslation, dummy model to create the `submit_translation` permission.
We need this model to be concrete or the following management commands will misbehave:
- `remove_stale_contenttypes`, will drop the perm
- `dump_data`, will complain about the missing table
"""
class Meta:
default_permissions = []
permissions = [
('submit_translation', "Can submit translations"),
]

View File

@ -0,0 +1,48 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n wagtailadmin_tags %}
{% block titletag %}{{ view.get_title }} {{ view.get_subtitle }}{% endblock %}
{% block content %}
{% include "wagtailadmin/shared/header.html" with title=view.get_title subtitle=view.get_subtitle icon="doc-empty-inverse" %}
<div class="nice-padding">
<form method="POST">
{% csrf_token %}
{% if next_url %}
<input type="hidden" name="next" value="{{ next_url }}">
{% endif %}
{% for field in form.hidden_fields %}{{ field }}{% endfor %}
<ul class="fields">
{% block visible_fields %}
{% for field in form.visible_fields %}
{% include "wagtailadmin/shared/field_as_li.html" %}
{% endfor %}
{% endblock %}
<li>
<input type="submit" value="{% trans 'Submit' %}" class="button" />
</li>
</ul>
</form>
</div>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
var selectAll = document.querySelector('[name="select_all"]');
var locales = document.querySelectorAll('[name="locales"]');
selectAll.addEventListener('change', function() {
for (var i = 0; i < locales.length; i++) {
locales[i].checked = selectAll.checked;
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,86 @@
from django.forms import CheckboxInput, HiddenInput
from django.test import TestCase, override_settings
from wagtail.contrib.simple_translation.forms import SubmitTranslationForm
from wagtail.core.models import Locale, Page
from wagtail.tests.i18n.models import TestPage
from wagtail.tests.utils import WagtailTestUtils
@override_settings(
LANGUAGES=[
("en", "English"),
("fr", "French"),
("de", "German"),
],
WAGTAIL_CONTENT_LANGUAGES=[
("en", "English"),
("fr", "French"),
("de", "German"),
],
)
class TestSubmitPageTranslation(WagtailTestUtils, TestCase):
def setUp(self):
self.en_locale = Locale.objects.first()
self.fr_locale = Locale.objects.create(language_code="fr")
self.de_locale = Locale.objects.create(language_code="de")
self.en_homepage = Page.objects.get(depth=2)
self.fr_homepage = self.en_homepage.copy_for_translation(self.fr_locale)
self.de_homepage = self.en_homepage.copy_for_translation(self.de_locale)
self.en_blog_index = TestPage(title="Blog", slug="blog")
self.en_homepage.add_child(instance=self.en_blog_index)
self.en_blog_post = TestPage(title="Blog post", slug="blog-post")
self.en_blog_index.add_child(instance=self.en_blog_post)
def test_include_subtree(self):
form = SubmitTranslationForm(instance=self.en_blog_post)
self.assertIsInstance(form.fields["include_subtree"].widget, HiddenInput)
form = SubmitTranslationForm(instance=self.en_blog_index)
self.assertIsInstance(form.fields["include_subtree"].widget, CheckboxInput)
self.assertEqual(
form.fields["include_subtree"].label, "Include subtree (1 page)"
)
form = SubmitTranslationForm(instance=self.en_homepage)
self.assertEqual(
form.fields["include_subtree"].label, "Include subtree (2 pages)"
)
def test_locales_queryset(self):
# Homepage is translated to all locales.
form = SubmitTranslationForm(instance=self.en_homepage)
self.assertEqual(
list(
form.fields["locales"].queryset.values_list("language_code", flat=True)
),
[],
)
# Blog index can be translated to `de` and `fr`.
form = SubmitTranslationForm(instance=self.en_blog_index)
self.assertEqual(
list(
form.fields["locales"].queryset.values_list("language_code", flat=True)
),
["de", "fr"],
)
# Blog post can be translated to `de` and `fr`.
form = SubmitTranslationForm(instance=self.en_blog_post)
self.assertEqual(
list(
form.fields["locales"].queryset.values_list("language_code", flat=True)
),
["de", "fr"],
)
def test_select_all(self):
form = SubmitTranslationForm(instance=self.en_homepage)
# Homepage is translated to all locales.
self.assertIsInstance(form.fields["select_all"].widget, HiddenInput)
form = SubmitTranslationForm(instance=self.en_blog_index)
# Blog post can be translated to `de` and `fr`.
self.assertIsInstance(form.fields["select_all"].widget, CheckboxInput)

View File

@ -0,0 +1,18 @@
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from wagtail.tests.utils import TestCase
class TestMigrations(TestCase):
def test_content_type_exists(self):
self.assertTrue(
ContentType.objects.filter(
app_label="simple_translation", model="simpletranslation"
).exists()
)
def test_permission_exists(self):
self.assertTrue(
Permission.objects.filter(codename="submit_translation").exists()
)

View File

@ -0,0 +1,265 @@
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.http import Http404
from django.test import RequestFactory, override_settings
from django.urls import reverse
from django.utils.translation import gettext_lazy
from wagtail.contrib.simple_translation.forms import SubmitTranslationForm
from wagtail.contrib.simple_translation.views import (
SubmitPageTranslationView, SubmitSnippetTranslationView, SubmitTranslationView)
from wagtail.core.models import Locale, Page, ParentNotTranslatedError
from wagtail.tests.i18n.models import TestPage
from wagtail.tests.snippets.models import TranslatableSnippet
from wagtail.tests.utils import TestCase, WagtailTestUtils
@override_settings(
LANGUAGES=[
("en", "English"),
("fr", "French"),
("de", "German"),
],
WAGTAIL_CONTENT_LANGUAGES=[
("en", "English"),
("fr", "French"),
("de", "German"),
],
)
class TestSubmitTranslationView(WagtailTestUtils, TestCase):
def setUp(self):
self.en_locale = Locale.objects.first()
self.fr_locale = Locale.objects.create(language_code="fr")
self.de_locale = Locale.objects.create(language_code="de")
self.en_homepage = Page.objects.get(depth=2)
self.factory = RequestFactory()
def test_template_name(self):
self.assertEqual(
SubmitTranslationView.template_name,
"simple_translation/admin/submit_translation.html",
)
def test_title(self):
self.assertEqual(SubmitTranslationView().title, gettext_lazy("Translate"))
self.assertEqual(SubmitTranslationView().get_title(), gettext_lazy("Translate"))
def test_subtitle(self):
view = SubmitTranslationView()
view.object = self.en_homepage
self.assertEqual(view.get_subtitle(), str(self.en_homepage))
def test_get_form(self):
view = SubmitTranslationView()
view.request = self.factory.get("/path/does/not/matter/")
view.object = self.en_homepage
form = view.get_form()
self.assertIsInstance(form, SubmitTranslationForm)
def test_get_success_url(self):
with self.assertRaises(NotImplementedError):
view = SubmitTranslationView()
view.object = self.en_homepage
view.get_success_url()
def test_get_context_data(self, **kwargs):
view = SubmitTranslationView()
view.request = self.factory.get("/path/does/not/matter/")
view.object = self.en_homepage
context = view.get_context_data()
self.assertTrue("form" in context.keys())
self.assertIsInstance(context["form"], SubmitTranslationForm)
def test_dispatch_as_anon(self):
url = reverse(
"simple_translation:submit_page_translation", args=(self.en_homepage.id,)
)
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f"/admin/login/?next={url}")
def test_dispatch_as_moderator(self):
url = reverse(
"simple_translation:submit_page_translation", args=(self.en_homepage.id,)
)
user = self.login()
group = Group.objects.get(name="Moderators")
user.groups.add(group)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_dispatch_as_user_with_perm(self):
url = reverse(
"simple_translation:submit_page_translation", args=(self.en_homepage.id,)
)
user = self.login()
permission = Permission.objects.get(codename="submit_translation")
user.user_permissions.add(permission)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
@override_settings(
LANGUAGES=[
("en", "English"),
("fr", "French"),
("de", "German"),
],
WAGTAIL_CONTENT_LANGUAGES=[
("en", "English"),
("fr", "French"),
("de", "German"),
],
)
class TestSubmitPageTranslationView(WagtailTestUtils, TestCase):
def setUp(self):
self.en_locale = Locale.objects.first()
self.fr_locale = Locale.objects.create(language_code="fr")
self.de_locale = Locale.objects.create(language_code="de")
self.en_homepage = Page.objects.get(depth=2)
self.fr_homepage = self.en_homepage.copy_for_translation(self.fr_locale)
self.de_homepage = self.en_homepage.copy_for_translation(self.de_locale)
self.en_blog_index = TestPage(title="Blog", slug="blog")
self.en_homepage.add_child(instance=self.en_blog_index)
self.en_blog_post = TestPage(title="Blog post", slug="blog-post")
self.en_blog_index.add_child(instance=self.en_blog_post)
def test_title(self):
self.assertEqual(SubmitPageTranslationView.title, "Translate page")
def test_get_subtitle(self):
view = SubmitPageTranslationView()
view.object = self.en_homepage
self.assertEqual(view.get_subtitle(), "Welcome to your new Wagtail site!")
def test_submit_page_translation_view_test_get(self):
url = reverse(
"simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
)
self.login()
response = self.client.get(url)
assert isinstance(response.context["form"], SubmitTranslationForm)
def test_submit_page_translation_view_test_post_invalid(self):
url = reverse(
"simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
)
self.login()
response = self.client.post(url, {})
assert response.status_code == 200
assert response.context["form"].errors == {
"locales": ["This field is required."]
}
def test_submit_page_translation_view_test_post_single_locale(self):
url = reverse(
"simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
)
de = Locale.objects.get(language_code="de").id
data = {"locales": [de], "include_subtree": True}
self.login()
response = self.client.post(url, data)
assert response.status_code == 302
assert response.url == f"/admin/pages/{self.en_blog_index.get_parent().id}/"
response = self.client.get(response.url) # follow the redirect
assert [msg.message for msg in response.context["messages"]] == [
"The page 'Blog' was successfully created in German"
]
def test_submit_page_translation_view_test_post_multiple_locales(self):
# Needs an extra page to hit recursive function
en_blog_post_sub = Page(title="Blog post sub", slug="blog-post-sub")
self.en_blog_post.add_child(instance=en_blog_post_sub)
url = reverse(
"simple_translation:submit_page_translation", args=(self.en_blog_post.id,)
)
de = Locale.objects.get(language_code="de").id
fr = Locale.objects.get(language_code="fr").id
data = {"locales": [de, fr], "include_subtree": True}
self.login()
with self.assertRaisesMessage(ParentNotTranslatedError, ""):
self.client.post(url, data)
url = reverse(
"simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
)
response = self.client.post(url, data)
assert response.status_code == 302
assert response.url == f"/admin/pages/{self.en_blog_index.get_parent().id}/"
response = self.client.get(response.url) # follow the redirect
assert [msg.message for msg in response.context["messages"]] == [
"The page 'Blog' was successfully created in 2 locales"
]
@override_settings(
LANGUAGES=[
("en", "English"),
("fr", "French"),
("de", "German"),
],
WAGTAIL_CONTENT_LANGUAGES=[
("en", "English"),
("fr", "French"),
("de", "German"),
],
)
class TestSubmitSnippetTranslationView(WagtailTestUtils, TestCase):
def setUp(self):
self.en_locale = Locale.objects.first()
self.fr_locale = Locale.objects.create(language_code="fr")
self.en_snippet = TranslatableSnippet(text="Hello world", locale=self.en_locale)
self.en_snippet.save()
def test_get_title(self):
view = SubmitSnippetTranslationView()
view.object = self.en_snippet
self.assertEqual(view.get_title(), "Translate translatable snippet")
def test_get_object(self):
view = SubmitSnippetTranslationView()
view.object = self.en_snippet
view.kwargs = {
"app_label": "some_app",
"model_name": "some_model",
"pk": 1,
}
with self.assertRaises(Http404):
view.get_object()
content_type = ContentType.objects.get_for_model(self.en_snippet)
view.kwargs = {
"app_label": content_type.app_label,
"model_name": content_type.model,
"pk": str(self.en_snippet.pk),
}
self.assertEqual(view.get_object(), self.en_snippet)
def test_get_success_url(self):
view = SubmitSnippetTranslationView()
view.object = self.en_snippet
view.kwargs = {
"app_label": "some_app",
"model_name": "some_model",
"pk": 99,
}
self.assertEqual(
view.get_success_url(), "/admin/snippets/some_app/some_model/99/"
)
def test_get_success_message(self):
view = SubmitSnippetTranslationView()
view.object = self.en_snippet
self.assertEqual(
view.get_success_message(self.fr_locale),
f"Successfully created French for translatable snippet 'TranslatableSnippet object ({self.en_snippet.id})'",
)

View File

@ -0,0 +1,102 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.test import TestCase
from django.urls import reverse
from wagtail.admin import widgets as wagtailadmin_widgets
from wagtail.contrib.simple_translation.wagtail_hooks import (
page_listing_more_buttons, register_submit_translation_permission)
from wagtail.core.models import Locale, Page
from wagtail.tests.i18n.models import TestPage
from wagtail.tests.utils import WagtailTestUtils
class Utils(WagtailTestUtils, TestCase):
def setUp(self):
self.en_locale = Locale.objects.first()
self.fr_locale = Locale.objects.create(language_code="fr")
self.de_locale = Locale.objects.create(language_code="de")
self.en_homepage = Page.objects.get(depth=2)
self.fr_homepage = self.en_homepage.copy_for_translation(self.fr_locale)
self.de_homepage = self.en_homepage.copy_for_translation(self.de_locale)
self.en_blog_index = TestPage(title="Blog", slug="blog")
self.en_homepage.add_child(instance=self.en_blog_index)
self.en_blog_post = TestPage(title="Blog post", slug="blog-post")
self.en_blog_index.add_child(instance=self.en_blog_post)
class TestWagtailHooksURLs(TestCase):
def test_register_admin_urls_page(self):
self.assertEqual(
reverse("simple_translation:submit_page_translation", args=(1,)),
"/admin/translation/submit/page/1/",
)
def test_register_admin_urls_snippet(self):
app_label = "foo"
model_name = "bar"
pk = 1
self.assertEqual(
reverse(
"simple_translation:submit_snippet_translation",
args=(app_label, model_name, pk),
),
"/admin/translation/submit/snippet/foo/bar/1/",
)
class TestWagtailHooksPermission(Utils):
def test_register_submit_translation_permission(self):
assert list(
register_submit_translation_permission().values_list("id", flat=True)
) == [
Permission.objects.get(
content_type__app_label="simple_translation",
codename="submit_translation",
).id
]
class TestWagtailHooksButtons(Utils):
class PagePerms:
def __init__(self, user):
self.user = user
def test_page_listing_more_buttons(self):
# Root, no button
root_page = self.en_blog_index.get_root()
if get_user_model().USERNAME_FIELD == 'email':
user = get_user_model().objects.create_user(email="jos@example.com")
else:
user = get_user_model().objects.create_user(username="jos")
assert list(page_listing_more_buttons(root_page, self.PagePerms(user))) == []
# No permissions, no button
home_page = self.en_homepage
assert list(page_listing_more_buttons(root_page, self.PagePerms(user))) == []
# Homepage is translated to all languages, no button
perm = Permission.objects.get(codename="submit_translation")
if get_user_model().USERNAME_FIELD == 'email':
user = get_user_model().objects.create_user(email="henk@example.com")
else:
user = get_user_model().objects.create_user(username="henk")
# New user, to prevent permission cache.
user.user_permissions.add(perm)
group = Group.objects.get(name="Editors")
user.groups.add(group)
page_perms = self.PagePerms(user)
assert list(page_listing_more_buttons(home_page, page_perms)) == []
# Page does not have translations yet... button!
blog_page = self.en_blog_post
assert isinstance(
list(page_listing_more_buttons(blog_page, page_perms))[0],
wagtailadmin_widgets.Button,
)

View File

@ -0,0 +1,145 @@
from django.contrib import messages
from django.contrib.admin.utils import unquote
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
from django.views.generic import TemplateView
from django.views.generic.detail import SingleObjectMixin
from wagtail.core.models import Page, TranslatableMixin
from wagtail.snippets.views.snippets import get_snippet_model_from_url_params
from .forms import SubmitTranslationForm
class SubmitTranslationView(SingleObjectMixin, TemplateView):
template_name = "simple_translation/admin/submit_translation.html"
title = gettext_lazy("Translate")
def get_title(self):
return self.title
def get_subtitle(self):
return str(self.object)
def get_form(self):
if self.request.method == "POST":
return SubmitTranslationForm(self.object, self.request.POST)
return SubmitTranslationForm(self.object)
def get_success_url(self):
raise NotImplementedError
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(
{
"form": self.get_form(),
}
)
return context
def post(self, request, **kwargs): # pragma: no mccabe
form = self.get_form()
if form.is_valid():
with transaction.atomic():
for locale in form.cleaned_data["locales"]:
if isinstance(self.object, Page):
self.object.copy_for_translation(locale)
if form.cleaned_data["include_subtree"]:
def _walk(current_page):
for child_page in current_page.get_children():
child_page.copy_for_translation(locale)
if child_page.numchild:
_walk(child_page)
_walk(self.object)
else:
self.object.copy_for_translation(
locale
).save() # pragma: no cover
if len(form.cleaned_data["locales"]) == 1:
locales = form.cleaned_data["locales"][0].get_display_name()
else:
# Note: always plural
locales = _("{} locales").format(len(form.cleaned_data["locales"]))
messages.success(self.request, self.get_success_message(locales))
return redirect(self.get_success_url())
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def dispatch(self, request, *args, **kwargs):
if not request.user.has_perms(["simple_translation.submit_translation"]):
raise PermissionDenied
self.object = self.get_object()
return super().dispatch(request, *args, **kwargs)
class SubmitPageTranslationView(SubmitTranslationView):
title = gettext_lazy("Translate page")
def get_subtitle(self):
return self.object.get_admin_display_title()
def get_object(self):
page = get_object_or_404(Page, id=self.kwargs["page_id"]).specific
# Can't translate the root page
if page.is_root():
raise Http404
return page
def get_success_url(self):
return reverse("wagtailadmin_explore", args=[self.get_object().get_parent().id])
def get_success_message(self, locales):
return _(
"The page '{page_title}' was successfully created in {locales}"
).format(page_title=self.object.get_admin_display_title(), locales=locales)
class SubmitSnippetTranslationView(SubmitTranslationView):
def get_title(self):
return _("Translate {model_name}").format(
model_name=self.object._meta.verbose_name
)
def get_object(self):
model = get_snippet_model_from_url_params(
self.kwargs["app_label"], self.kwargs["model_name"]
)
if not issubclass(model, TranslatableMixin):
raise Http404
return get_object_or_404(model, pk=unquote(self.kwargs["pk"]))
def get_success_url(self):
return reverse(
"wagtailsnippets:edit",
args=[
self.kwargs["app_label"],
self.kwargs["model_name"],
self.kwargs["pk"],
],
)
def get_success_message(self, locales):
return _("Successfully created {locales} for {model_name} '{object}'").format(
model_name=self.object._meta.verbose_name,
object=str(self.object),
locales=locales,
)

View File

@ -0,0 +1,78 @@
from django.contrib.admin.utils import quote
from django.contrib.auth.models import Permission
from django.urls import include, path, reverse
from django.utils.translation import gettext as _
from wagtail.admin import widgets as wagtailadmin_widgets
from wagtail.core import hooks
from wagtail.core.models import Locale, TranslatableMixin
from wagtail.snippets.widgets import SnippetListingButton
from .views import SubmitPageTranslationView, SubmitSnippetTranslationView
@hooks.register("register_admin_urls")
def register_admin_urls():
urls = [
path(
"submit/page/<int:page_id>/",
SubmitPageTranslationView.as_view(),
name="submit_page_translation",
),
path(
"submit/snippet/<slug:app_label>/<slug:model_name>/<str:pk>/",
SubmitSnippetTranslationView.as_view(),
name="submit_snippet_translation",
),
]
return [
path(
"translation/",
include(
(urls, "simple_translation"),
namespace="simple_translation",
),
)
]
@hooks.register("register_permissions")
def register_submit_translation_permission():
return Permission.objects.filter(content_type__app_label="simple_translation", codename="submit_translation")
@hooks.register("register_page_listing_more_buttons")
def page_listing_more_buttons(page, page_perms, is_parent=False, next_url=None):
if page_perms.user.has_perm("simple_translation.submit_translation") and not page.is_root():
# If there's at least one locale that we haven't translated into yet, show "Translate this page" button
has_locale_to_translate_to = Locale.objects.exclude(
id__in=page.get_translations(inclusive=True).values_list("locale_id", flat=True)
).exists()
if has_locale_to_translate_to:
url = reverse("simple_translation:submit_page_translation", args=[page.id])
yield wagtailadmin_widgets.Button(_("Translate"), url, priority=60)
@hooks.register("register_snippet_listing_buttons")
def register_snippet_listing_buttons(snippet, user, next_url=None):
model = type(snippet)
if issubclass(model, TranslatableMixin) and user.has_perm("simple_translation.submit_translation"):
# If there's at least one locale that we haven't translated into yet, show "Translate" button
has_locale_to_translate_to = Locale.objects.exclude(
id__in=snippet.get_translations(inclusive=True).values_list("locale_id", flat=True)
).exists()
if has_locale_to_translate_to:
url = reverse(
"simple_translation:submit_snippet_translation",
args=[model._meta.app_label, model._meta.model_name, quote(snippet.pk)],
)
yield SnippetListingButton(
_("Translate"),
url,
attrs={"aria-label": _("Translate '%(title)s'") % {"title": str(snippet)}},
priority=100,
)

View File

@ -121,6 +121,7 @@ INSTALLED_APPS = [
'wagtail.tests.search',
'wagtail.tests.modeladmintest',
'wagtail.tests.i18n',
'wagtail.contrib.simple_translation',
'wagtail.contrib.styleguide',
'wagtail.contrib.routable_page',
'wagtail.contrib.frontend_cache',