Skip to content

Translation management models

These models are responsible for managing the translation of objects.

graph TD A[TranslationSource] --> B[TranslatableObject] C[Translation] --> A D[TranslationLog] --> A C --> E[wagtail.Locale] A --> E D --> E A --> F[django.ContentType] B --> F style E stroke-dasharray: 5 5 style F stroke-dasharray: 5 5

TranslatableObject

A TranslatableObject represents a set of instances of a translatable model that are all translations of each another.

In Wagtail, objects are considered translations of each other when they are of the same content type and have the same translation_key value.

Attributes:

Name Type Description
translation_key UUIDField

The translation_key that value that is used by the instances.

content_type ForeignKey to ContentType

Link to the base Django content type representing the model that the instances use. Note that this field refers to the model that has the locale and translation_key fields and not the specific type.

get_instance(self, locale)

Returns a model instance for this object in the given locale.

Parameters:

Name Type Description Default
locale Locale | int

Either a Locale object or an ID of a Locale.

required

Returns:

Type Description
Model

The model instance.

Exceptions:

Type Description
Model.DoesNotExist

If there is not an instance of this object in the given locale.

Source code in wagtail_localize/models.py
def get_instance(self, locale):
    """
    Returns a model instance for this object in the given locale.

    Args:
        locale (Locale | int): Either a Locale object or an ID of a Locale.

    Returns:
        Model: The model instance.

    Raises:
        Model.DoesNotExist: If there is not an instance of this object in the given locale.
    """
    return self.content_type.get_object_for_this_type(
        translation_key=self.translation_key, locale_id=pk(locale)
    )

get_instance_or_none(self, locale)

Returns a model instance for this object in the given locale.

Parameters:

Name Type Description Default
locale Locale | int

Either a Locale object or an ID of a Locale.

required

Returns:

Type Description
Model

The model instance if one exists. None: If the model doesn't exist.

Source code in wagtail_localize/models.py
def get_instance_or_none(self, locale):
    """
    Returns a model instance for this object in the given locale.

    Args:
        locale (Locale | int): Either a Locale object or an ID of a Locale.

    Returns:
        Model: The model instance if one exists.
        None: If the model doesn't exist.
    """
    try:
        return self.get_instance(locale)
    except self.content_type.model_class().DoesNotExist:
        pass

has_translation(self, locale)

Returns True if there is an instance of this object in the given Locale.

Parameters:

Name Type Description Default
locale Locale | int

Either a Locale object or an ID of a Locale.

required

Returns:

Type Description
bool

True if there is an instance of this object in the given locale.

Source code in wagtail_localize/models.py
def has_translation(self, locale):
    """
    Returns True if there is an instance of this object in the given Locale.

    Args:
        locale (Locale | int): Either a Locale object or an ID of a Locale.

    Returns:
        bool: True if there is an instance of this object in the given locale.
    """
    return self.content_type.get_all_objects_for_this_type(
        translation_key=self.translation_key, locale_id=pk(locale)
    ).exists()

Translation

Manages the translation of an object into a locale.

An instance of this model is created whenever an object is submitted for translation into a new language.

They can be disabled at any time, and are deleted or disabled automatically if either the source or destination object is deleted.

If the translation of a page is disabled, the page editor of the translation would return to the normal Wagtail editor.

Attributes:

Name Type Description
uuid UUIDField

A unique ID for this translation used for referencing it from external systems.

source ForeignKey to TranslationSource

The source that is being translated.

target_locale ForeignKey to Locale

The Locale that the source is being translated into.

created_at DateTimeField

The date/time the translation was started.

translations_last_updated_at DateTimeField

The date/time of when a translated string was last updated.

destination_last_updated_at DateTimeField

The date/time of when the destination object was last updated.

enabled boolean

Whether this translation is enabled or not.

export_po(self)

Exports all translatable strings with any translations that have already been made.

Returns:

Type Description
polib.POFile

A POFile object containing the source translatable strings and any translations.

Source code in wagtail_localize/models.py
def export_po(self):
    """
    Exports all translatable strings with any translations that have already been made.

    Returns:
        polib.POFile: A POFile object containing the source translatable strings and any translations.
    """
    # Get messages
    messages = []

    string_segments = (
        StringSegment.objects.filter(source=self.source)
        .order_by("order")
        .select_related("context", "string")
        .annotate_translation(self.target_locale, include_errors=True)
    )

    for string_segment in string_segments:
        messages.append(
            (
                string_segment.string.data,
                string_segment.context.path,
                string_segment.translation,
            )
        )

    # Build a PO file
    po = polib.POFile(wrapwidth=200)
    po.metadata = {
        "POT-Creation-Date": str(timezone.now()),
        "MIME-Version": "1.0",
        "Content-Type": "text/plain; charset=utf-8",
        "X-WagtailLocalize-TranslationID": str(self.uuid),
    }

    for text, context, translation in messages:
        po.append(
            polib.POEntry(
                msgid=text,
                msgctxt=context,
                msgstr=translation or "",
            )
        )

    # Add any obsolete segments that have translations for future reference
    # We find this by looking for obsolete contexts and annotate the latest
    # translation for each one. Contexts that were never translated are
    # excluded
    for translation in (
        StringTranslation.objects.filter(
            context__object_id=self.source.object_id, locale=self.target_locale
        )
        .exclude(
            translation_of_id__in=StringSegment.objects.filter(
                source=self.source
            ).values_list("string_id", flat=True)
        )
        .select_related("translation_of", "context")
        .iterator()
    ):
        po.append(
            polib.POEntry(
                msgid=translation.translation_of.data,
                msgstr=translation.data or "",
                msgctxt=translation.context.path,
                obsolete=True,
            )
        )

    return po

get_progress(self)

Gets the current translation progress.

Returns tuple[int, int]: A two-tuple of integers. First integer is the total number of string segments to be translated. The second integer is the number of string segments that have been translated so far.

Source code in wagtail_localize/models.py
def get_progress(self):
    """
    Gets the current translation progress.

    Returns
        tuple[int, int]: A two-tuple of integers. First integer is the total number of string segments to be translated.
            The second integer is the number of string segments that have been translated so far.
    """
    # Get QuerySet of Segments that need to be translated
    required_segments = StringSegment.objects.filter(source_id=self.source_id)

    # Annotate each Segment with a flag that indicates whether the segment is translated
    # into the locale
    required_segments = required_segments.annotate(
        is_translated=Exists(
            StringTranslation.objects.filter(
                translation_of_id=OuterRef("string_id"),
                context_id=OuterRef("context_id"),
                locale_id=self.target_locale_id,
                has_error=False,
            )
        )
    )

    # Count the total number of segments and the number of translated segments
    aggs = required_segments.annotate(
        is_translated_i=Case(
            When(is_translated=True, then=Value(1)),
            default=Value(0),
            output_field=IntegerField(),
        )
    ).aggregate(
        total_segments=Count("pk"), translated_segments=Sum("is_translated_i")
    )

    return aggs["total_segments"], aggs["translated_segments"]

get_status_display(self)

Returns a string to describe the current status of this translation to a user.

Returns:

Type Description
str

The status of this translation

Source code in wagtail_localize/models.py
def get_status_display(self):
    """
    Returns a string to describe the current status of this translation to a user.

    Returns:
        str: The status of this translation
    """
    total_segments, translated_segments = self.get_progress()
    if total_segments == translated_segments:
        return _("Up to date")
    else:
        return _("Waiting for translations")

get_target_instance(self)

Fetches the translated instance from the database.

Exceptions:

Type Description
Model.DoesNotExist

if the translation does not exist.

Returns:

Type Description
Model

The translated instance.

Source code in wagtail_localize/models.py
def get_target_instance(self):
    """
    Fetches the translated instance from the database.

    Raises:
        Model.DoesNotExist: if the translation does not exist.

    Returns:
        Model: The translated instance.
    """
    return self.source.get_translated_instance(self.target_locale)

get_target_instance_edit_url(self)

Returns the URL to edit the target instance.

Exceptions:

Type Description
Model.DoesNotExist

if the translation does not exist.

Returns:

Type Description
str

The URL of the edit view of the target instance.

Source code in wagtail_localize/models.py
def get_target_instance_edit_url(self):
    """
    Returns the URL to edit the target instance.

    Raises:
        Model.DoesNotExist: if the translation does not exist.

    Returns:
        str: The URL of the edit view of the target instance.
    """
    return get_edit_url(self.get_target_instance())

import_po(self, po, delete=False, user=None, translation_type='manual', tool_name='')

Imports all translatable strings with any translations that have already been made.

Parameters:

Name Type Description Default
po polib.POFile

A POFile object containing the source translatable strings and any translations.

required
delete boolean

Set to True to delete any translations that do not appear in the PO file.

False
user User

The user who is performing this operation. Used for logging purposes.

None
translation_type 'manual' or 'machine'

Whether the translationw as performed by a human or machine. Defaults to 'manual'.

'manual'
tool_name string

The name of the tool that was used to perform the translation. Defaults to ''.

''

Returns:

Type Description
list[POImportWarning]

A list of POImportWarning objects representing any non-fatal issues that were encountered while importing the PO file.

Source code in wagtail_localize/models.py
@transaction.atomic
def import_po(
    self, po, delete=False, user=None, translation_type="manual", tool_name=""
):
    """
    Imports all translatable strings with any translations that have already been made.

    Args:
        po (polib.POFile): A POFile object containing the source translatable strings and any translations.
        delete (boolean, optional): Set to True to delete any translations that do not appear in the PO file.
        user (User, optional): The user who is performing this operation. Used for logging purposes.
        translation_type ('manual' or 'machine', optional): Whether the translationw as performed by a human or machine. Defaults to 'manual'.
        tool_name (string, optional): The name of the tool that was used to perform the translation. Defaults to ''.

    Returns:
        list[POImportWarning]: A list of POImportWarning objects representing any non-fatal issues that were
        encountered while importing the PO file.
    """
    seen_translation_ids = set()
    warnings = []

    if "X-WagtailLocalize-TranslationID" in po.metadata and po.metadata[
        "X-WagtailLocalize-TranslationID"
    ] != str(self.uuid):
        return []

    for index, entry in enumerate(po):
        try:
            string = String.objects.get(
                locale_id=self.source.locale_id, data=entry.msgid
            )
            context = TranslationContext.objects.get(
                object_id=self.source.object_id, path=entry.msgctxt
            )

            # Ignore blank strings
            if not entry.msgstr:
                continue

            # Ignore if the string doesn't appear in this context, and if there is not an obsolete StringTranslation
            if (
                not StringSegment.objects.filter(
                    string=string, context=context
                ).exists()
                and not StringTranslation.objects.filter(
                    translation_of=string, context=context
                ).exists()
            ):
                warnings.append(
                    StringNotUsedInContext(index, entry.msgid, entry.msgctxt)
                )
                continue

            string_translation, created = string.translations.get_or_create(
                locale_id=self.target_locale_id,
                context=context,
                defaults={
                    "data": entry.msgstr,
                    "updated_at": timezone.now(),
                    "translation_type": translation_type,
                    "tool_name": tool_name,
                    "last_translated_by": user,
                    "has_error": False,
                    "field_error": "",
                },
            )

            seen_translation_ids.add(string_translation.id)

            if not created:
                # Update the string_translation only if it has changed
                if string_translation.data != entry.msgstr:
                    string_translation.data = entry.msgstr
                    string_translation.translation_type = translation_type
                    string_translation.tool_name = tool_name
                    string_translation.last_translated_by = user
                    string_translation.updated_at = timezone.now()
                    string_translation.has_error = False  # reset the error flag.
                    string_translation.save()

        except TranslationContext.DoesNotExist:
            warnings.append(UnknownContext(index, entry.msgctxt))

        except String.DoesNotExist:
            warnings.append(UnknownString(index, entry.msgid))

    # Delete any translations that weren't mentioned
    if delete:
        StringTranslation.objects.filter(
            context__object_id=self.source.object_id, locale=self.target_locale
        ).exclude(id__in=seen_translation_ids).delete()

    return warnings

save_target(self, user=None, publish=True)

Saves the target page/snippet using the current translations.

Parameters:

Name Type Description Default
user User

The user that is performing this action. Used for logging purposes.

None
publish boolean

Set this to False to save a draft of the translation. Pages only.

True

Exceptions:

Type Description
SourceDeletedError

if the source object has been deleted.

CannotSaveDraftError

if the publish parameter was set to False when translating a non-page object.

MissingTranslationError

if a translation is missing and fallbackis not True.

MissingRelatedObjectError

if a related object is not translated and fallbackis not True.

Returns:

Type Description
Model

The translated instance.

Source code in wagtail_localize/models.py
def save_target(self, user=None, publish=True):
    """
    Saves the target page/snippet using the current translations.

    Args:
        user (User, optional): The user that is performing this action. Used for logging purposes.
        publish (boolean, optional): Set this to False to save a draft of the translation. Pages only.

    Raises:
        SourceDeletedError: if the source object has been deleted.
        CannotSaveDraftError: if the `publish` parameter was set to `False` when translating a non-page object.
        MissingTranslationError: if a translation is missing and `fallback `is not `True`.
        MissingRelatedObjectError: if a related object is not translated and `fallback `is not `True`.

    Returns:
        Model: The translated instance.
    """
    self.source.create_or_update_translation(
        self.target_locale,
        user=user,
        publish=publish,
        fallback=True,
        copy_parent_pages=True,
    )

TranslationLog

Keeps Track of when translations are created/updated.

Attributes:

Name Type Description
source ForeignKey to TranslationSource

The source that was used for translation.

locale ForeignKey to Locale

The Locale that the source was translated into.

created_at DateTimeField

The date/time the translation was done.

page_revision ForeignKey to PageRevision

If the translation was of a page, this links to the PageRevision that was created

get_instance(self)

Gets the instance of the translated object, if it still exists.

Exceptions:

Type Description
Model.DoesNotExist

if the translated object no longer exists.

Returns:

Type Description

The translated object.

Source code in wagtail_localize/models.py
def get_instance(self):
    """
    Gets the instance of the translated object, if it still exists.

    Raises:
        Model.DoesNotExist: if the translated object no longer exists.

    Returns:
        The translated object.
    """
    return self.source.object.get_instance(self.locale)

TranslationSource

Frozen source content that is to be translated.

This is like a page revision, except it can be created for any model and it's only created/updated when a user submits something for translation.

Attributes:

Name Type Description
object ForeignKey to TranslatableObject

The object that this is a source for

specific_content_type ForeignKey to ContentType

The specific content type that this was extracted from. Note that TranslatableObject.content_type doesn't store the most specific content type, but this does.

locale ForeignKey to Locale

The Locale of the instance that this source content was extracted from.

object_repr TextField

A string representing the name of the source object. Used in the UI.

content_json TextField with JSON contents

The serialized source content. Note that this is serialzed in the same way that Wagtail serializes page revisions.

created_at DateTimeField

The date/time at which the content was first extracted from this source.

last_updated_at DateTimeField

The date/time at which the content was last extracted from this source.

_get_segments_for_translation(self, locale, fallback=False) private

Returns a list of segments that can be passed into "ingest_segments" to translate an object.

Source code in wagtail_localize/models.py
def _get_segments_for_translation(self, locale, fallback=False):
    """
    Returns a list of segments that can be passed into "ingest_segments" to translate an object.
    """
    string_segments = (
        StringSegment.objects.filter(source=self)
        .annotate_translation(locale)
        .select_related("context", "string")
    )

    template_segments = (
        TemplateSegment.objects.filter(source=self)
        .select_related("template")
        .select_related("context")
    )

    related_object_segments = (
        RelatedObjectSegment.objects.filter(source=self)
        .select_related("object")
        .select_related("context")
    )

    overridable_segments = (
        OverridableSegment.objects.filter(source=self)
        .annotate_override_json(locale)
        .filter(override_json__isnull=False)
        .select_related("context")
    )

    segments = []

    for string_segment in string_segments:
        if string_segment.translation:
            string = StringValue(string_segment.translation)
        elif fallback:
            string = StringValue(string_segment.string.data)
        else:
            raise MissingTranslationError(string_segment, locale)

        segment_value = StringSegmentValue(
            string_segment.context.path,
            string,
            attrs=json.loads(string_segment.attrs),
        ).with_order(string_segment.order)

        segments.append(segment_value)

    for template_segment in template_segments:
        template = template_segment.template
        segment_value = TemplateSegmentValue(
            template_segment.context.path,
            template.template_format,
            template.template,
            template.string_count,
            order=template_segment.order,
        )
        segments.append(segment_value)

    for related_object_segment in related_object_segments:
        if related_object_segment.object.has_translation(locale):
            segment_value = RelatedObjectSegmentValue(
                related_object_segment.context.path,
                related_object_segment.object.content_type,
                related_object_segment.object.translation_key,
                order=related_object_segment.order,
            )
            segments.append(segment_value)

        elif fallback:
            # Skip this segment, this will reuse what is already in the database
            continue
        else:
            raise MissingRelatedObjectError(related_object_segment, locale)

    for overridable_segment in overridable_segments:
        segment_value = OverridableSegmentValue(
            overridable_segment.context.path,
            json.loads(overridable_segment.override_json),
            order=overridable_segment.order,
        )
        segments.append(segment_value)

    return segments

as_instance(self)

Builds an instance of the object with the content of this source.

Returns:

Type Description
Model

A model instance that has the content of this TranslationSource.

Exceptions:

Type Description
SourceDeletedError

if the source instance has been deleted.

Source code in wagtail_localize/models.py
def as_instance(self):
    """
    Builds an instance of the object with the content of this source.

    Returns:
        Model: A model instance that has the content of this TranslationSource.

    Raises:
        SourceDeletedError: if the source instance has been deleted.
    """
    try:
        instance = self.get_source_instance()
    except models.ObjectDoesNotExist:
        raise SourceDeletedError

    if isinstance(instance, Page):
        # see https://github.com/wagtail/wagtail/pull/8024
        content_json = json.loads(self.content_json)
        return instance.with_content_json(content_json)

    elif isinstance(instance, ClusterableModel):
        new_instance = instance.__class__.from_json(self.content_json)

    else:
        new_instance = model_from_serializable_data(
            instance.__class__, json.loads(self.content_json)
        )

    new_instance.pk = instance.pk
    new_instance.locale = instance.locale
    new_instance.translation_key = instance.translation_key

    return new_instance

create_or_update_translation(self, locale, user=None, publish=True, copy_parent_pages=False, fallback=False)

Creates/updates a translation of the object into the specified locale based on the content of this source and the translated strings currently in translation memory.

Parameters:

Name Type Description Default
locale Locale

The target locale to generate the translation for.

required
user User

The user who is carrying out this operation. For logging purposes

None
publish boolean

Set this to False to save a draft of the translation. Pages only.

True
copy_parent_pages boolean

Set this to True to make copies of the parent pages if they are not yet translated.

False
fallback boolean

Set this to True to fallback to source strings/related objects if they are not yet translated. By default, this will raise an error if anything is missing.

False

Exceptions:

Type Description
SourceDeletedError

if the source object has been deleted.

CannotSaveDraftError

if the publish parameter was set to False when translating a non-page object.

MissingTranslationError

if a translation is missing and fallbackis not True.

MissingRelatedObjectError

if a related object is not translated and fallbackis not True.

Returns:

Type Description
Model

The translated instance.

Source code in wagtail_localize/models.py
def create_or_update_translation(
    self, locale, user=None, publish=True, copy_parent_pages=False, fallback=False
):
    """
    Creates/updates a translation of the object into the specified locale
    based on the content of this source and the translated strings
    currently in translation memory.

    Args:
        locale (Locale): The target locale to generate the translation for.
        user (User, optional): The user who is carrying out this operation. For logging purposes
        publish (boolean, optional): Set this to False to save a draft of the translation. Pages only.
        copy_parent_pages (boolean, optional): Set this to True to make copies of the parent pages if they are not
            yet translated.
        fallback (boolean, optional): Set this to True to fallback to source strings/related objects if they are
            not yet translated. By default, this will raise an error if anything is missing.

    Raises:
        SourceDeletedError: if the source object has been deleted.
        CannotSaveDraftError: if the `publish` parameter was set to `False` when translating a non-page object.
        MissingTranslationError: if a translation is missing and `fallback `is not `True`.
        MissingRelatedObjectError: if a related object is not translated and `fallback `is not `True`.

    Returns:
        Model: The translated instance.
    """
    original = self.as_instance()
    created = False

    # Only pages can be saved as draft
    if not publish and not isinstance(original, Page):
        raise CannotSaveDraftError

    try:
        translation = self.get_translated_instance(locale)
    except models.ObjectDoesNotExist:
        if isinstance(original, Page):
            translation = original.copy_for_translation(
                locale, copy_parents=copy_parent_pages
            )
        else:
            translation = original.copy_for_translation(locale)

        created = True

    copy_synchronised_fields(original, translation)

    segments = self._get_segments_for_translation(locale, fallback=fallback)

    try:
        with transaction.atomic():
            # Ingest all translated segments
            ingest_segments(original, translation, self.locale, locale, segments)

            if isinstance(translation, Page):
                # If the page is an alias, convert it into a regular page
                if translation.alias_of_id:
                    translation.alias_of_id = None
                    translation.save(update_fields=["alias_of_id"], clean=False)

                    # Create initial revision
                    revision = translation.save_revision(
                        user=user, changed=False, clean=False
                    )

                    # Log the alias conversion
                    PageLogEntry.objects.log_action(
                        instance=translation,
                        revision=revision,
                        action="wagtail.convert_alias",
                        user=user,
                        data={
                            "page": {
                                "id": translation.id,
                                "title": translation.get_admin_display_title(),
                            },
                        },
                    )

                # Make sure the slug is valid
                translation.slug = find_available_slug(
                    translation.get_parent(),
                    slugify(translation.slug),
                    ignore_page_id=translation.id,
                )
                translation.save()

                # Create a new revision
                page_revision = translation.save_revision(user=user)

                self.sync_view_restrictions(original, translation)

                if publish:
                    page_revision.publish()

            else:
                # Note: we don't need to run full_clean for Pages as Wagtail does that in Page.save()
                translation.full_clean()

                translation.save()
                page_revision = None

    except ValidationError as e:
        # If the validation error's field matches the context of a translation,
        # set that error message on that translation.
        # TODO (someday): Add support for errors raised from streamfield
        for field_name, errors in e.error_dict.items():
            try:
                context = TranslationContext.objects.get(
                    object=self.object, path=field_name
                )

            except TranslationContext.DoesNotExist:
                # TODO (someday): How would we handle validation errors for non-translatable fields?
                continue

            # Check for string translation
            try:
                string_translation = StringTranslation.objects.get(
                    translation_of_id__in=StringSegment.objects.filter(
                        source=self
                    ).values_list("string_id", flat=True),
                    context=context,
                    locale=locale,
                )

                string_translation.set_field_error(errors)

            except StringTranslation.DoesNotExist:
                pass

            # Check for segment override
            try:
                segment_override = SegmentOverride.objects.get(
                    context=context,
                    locale=locale,
                )

                segment_override.set_field_error(errors)

            except SegmentOverride.DoesNotExist:
                pass

        raise

    # Log that the translation was made
    TranslationLog.objects.create(
        source=self, locale=locale, page_revision=page_revision
    )

    return translation, created

export_po(self)

Exports all translatable strings from this source.

Note that because there is no target locale, all msgstr fields will be blank.

Returns:

Type Description
polib.POFile

A POFile object containing the source translatable strings.

Source code in wagtail_localize/models.py
def export_po(self):
    """
    Exports all translatable strings from this source.

    Note that because there is no target locale, all `msgstr` fields will be blank.

    Returns:
        polib.POFile: A POFile object containing the source translatable strings.
    """
    # Get messages
    messages = []

    for string_segment in (
        StringSegment.objects.filter(source=self)
        .order_by("order")
        .select_related("context", "string")
    ):
        messages.append((string_segment.string.data, string_segment.context.path))

    # Build a PO file
    po = polib.POFile(wrapwidth=200)
    po.metadata = {
        "POT-Creation-Date": str(timezone.now()),
        "MIME-Version": "1.0",
        "Content-Type": "text/plain; charset=utf-8",
    }

    for text, context in messages:
        po.append(
            polib.POEntry(
                msgid=text,
                msgctxt=context,
                msgstr="",
            )
        )

    return po

get_ephemeral_translated_instance(self, locale, fallback=False)

Returns an instance with the translations added which is not intended to be saved.

This is used for previewing pages with draft translations applied.

Parameters:

Name Type Description Default
locale Locale

The target locale to generate the ephemeral translation for.

required
fallback boolean

Set this to True to fallback to source strings/related objects if they are not yet translated. By default, this will raise an error if anything is missing.

False

Exceptions:

Type Description
SourceDeletedError

if the source object has been deleted.

MissingTranslationError

if a translation is missing and fallbackis not True.

MissingRelatedObjectError

if a related object is not translated and fallbackis not True.

Returns:

Type Description
Model

The translated instance with unsaved changes.

Source code in wagtail_localize/models.py
def get_ephemeral_translated_instance(self, locale, fallback=False):
    """
    Returns an instance with the translations added which is not intended to be saved.

    This is used for previewing pages with draft translations applied.

    Args:
        locale (Locale): The target locale to generate the ephemeral translation for.
        fallback (boolean): Set this to True to fallback to source strings/related objects if they are not yet
            translated. By default, this will raise an error if anything is missing.

    Raises:
        SourceDeletedError: if the source object has been deleted.
        MissingTranslationError: if a translation is missing and `fallback `is not `True`.
        MissingRelatedObjectError: if a related object is not translated and `fallback `is not `True`.

    Returns:
        Model: The translated instance with unsaved changes.
    """
    original = self.as_instance()
    translation = self.get_translated_instance(locale)

    copy_synchronised_fields(original, translation)

    segments = self._get_segments_for_translation(locale, fallback=fallback)

    # Ingest all translated segments
    ingest_segments(original, translation, self.locale, locale, segments)

    return translation

get_or_create_from_instance(instance) classmethod

Creates or gets a TranslationSource for the given instance.

This extracts the content from the given instance. Then stores it in a new TranslationSource instance if one doesn't already exist. If one does already exist, it returns the existing TranslationSource without changing it.

Parameters:

Name Type Description Default
instance Model that inherits TranslatableMixin

A Translatable model instance to find a TranslationSource instance for.

required

Returns:

Type Description
tuple[TranslationSource, boolean]

A two-tuple, the first component is the TranslationSource object, and the second component is a boolean that is True if the TranslationSource was created.

Source code in wagtail_localize/models.py
@classmethod
def get_or_create_from_instance(cls, instance):
    """
    Creates or gets a TranslationSource for the given instance.

    This extracts the content from the given instance. Then stores it in a new TranslationSource instance if one
    doesn't already exist. If one does already exist, it returns the existing TranslationSource without changing
    it.

    Args:
        instance (Model that inherits TranslatableMixin): A Translatable model instance to find a TranslationSource
            instance for.

    Returns:
        tuple[TranslationSource, boolean]: A two-tuple, the first component is the TranslationSource object, and
            the second component is a boolean that is True if the TranslationSource was created.
    """
    # Make sure we're using the specific version of pages
    if isinstance(instance, Page):
        instance = instance.specific

    object, created = TranslatableObject.objects.get_or_create_from_instance(
        instance
    )

    try:
        return (
            TranslationSource.objects.get(
                object_id=object.translation_key, locale_id=instance.locale_id
            ),
            False,
        )
    except TranslationSource.DoesNotExist:
        pass

    if isinstance(instance, ClusterableModel):
        content_json = instance.to_json()
    else:
        serializable_data = get_serializable_data_for_fields(instance)
        content_json = json.dumps(serializable_data, cls=DjangoJSONEncoder)

    source, created = cls.objects.update_or_create(
        object=object,
        locale=instance.locale,
        # You can't update the content type of a source. So if this happens,
        # it'll try and create a new source and crash (can't have more than
        # one source per object/locale)
        specific_content_type=ContentType.objects.get_for_model(instance.__class__),
        defaults={
            "locale": instance.locale,
            "object_repr": str(instance)[:200],
            "content_json": content_json,
            "schema_version": get_schema_version(instance._meta.app_label) or "",
            "last_updated_at": timezone.now(),
        },
    )
    source.refresh_segments()
    return source, created

get_source_instance(self)

This gets the live version of instance that the source data was extracted from.

This is different to source.object.get_instance(source.locale) as the instance returned by this methid will have the same model that the content was extracted from. The model returned by object.get_instance might be more generic since that model only records the model that the TranslatableMixin was applied to but that model might have child models.

Returns:

Type Description
Model

The model instance that this TranslationSource was created from.

Exceptions:

Type Description
Model.DoesNotExist

If the source instance has been deleted.

Source code in wagtail_localize/models.py
def get_source_instance(self):
    """
    This gets the live version of instance that the source data was extracted from.

    This is different to source.object.get_instance(source.locale) as the instance
    returned by this methid will have the same model that the content was extracted
    from. The model returned by `object.get_instance` might be more generic since
    that model only records the model that the TranslatableMixin was applied to but
    that model might have child models.

    Returns:
        Model: The model instance that this TranslationSource was created from.

    Raises:
        Model.DoesNotExist: If the source instance has been deleted.
    """
    return self.specific_content_type.get_object_for_this_type(
        translation_key=self.object_id, locale_id=self.locale_id
    )

get_source_instance_edit_url(self)

Returns the URL to edit the source instance.

Source code in wagtail_localize/models.py
def get_source_instance_edit_url(self):
    """
    Returns the URL to edit the source instance.
    """
    return get_edit_url(self.get_source_instance())

refresh_segments(self)

Updates the *Segment models to reflect the latest version of the source.

This is called by from_instance so you don't usually need to call this manually.

Source code in wagtail_localize/models.py
@transaction.atomic
def refresh_segments(self):
    """
    Updates the *Segment models to reflect the latest version of the source.

    This is called by `from_instance` so you don't usually need to call this manually.
    """
    seen_string_segment_ids = []
    seen_template_segment_ids = []
    seen_related_object_segment_ids = []
    seen_overridable_segment_ids = []

    instance = self.as_instance()
    for segment in extract_segments(instance):
        if isinstance(segment, TemplateSegmentValue):
            segment_obj = TemplateSegment.from_value(self, segment)
            seen_template_segment_ids.append(segment_obj.id)
        elif isinstance(segment, RelatedObjectSegmentValue):
            segment_obj = RelatedObjectSegment.from_value(self, segment)
            seen_related_object_segment_ids.append(segment_obj.id)
        elif isinstance(segment, OverridableSegmentValue):
            segment_obj = OverridableSegment.from_value(self, segment)
            seen_overridable_segment_ids.append(segment_obj.id)
        else:
            segment_obj = StringSegment.from_value(self, self.locale, segment)
            seen_string_segment_ids.append(segment_obj.id)

        # Make sure the segment's field_path is pre-populated
        segment_obj.context.get_field_path(instance)

    # Delete any segments that weren't mentioned
    self.stringsegment_set.exclude(id__in=seen_string_segment_ids).delete()
    self.templatesegment_set.exclude(id__in=seen_template_segment_ids).delete()
    self.relatedobjectsegment_set.exclude(
        id__in=seen_related_object_segment_ids
    ).delete()
    self.overridablesegment_set.exclude(
        id__in=seen_overridable_segment_ids
    ).delete()

schema_out_of_date(self)

Returns True if the app that contains the model this source was generated from has been updated since the source was last updated.

Source code in wagtail_localize/models.py
def schema_out_of_date(self):
    """
    Returns True if the app that contains the model this source was generated from
    has been updated since the source was last updated.
    """
    if not self.schema_version:
        return False

    current_schema_version = get_schema_version(
        self.specific_content_type.app_label
    )
    return self.schema_version != current_schema_version

sync_view_restrictions(self, original, translation_page)

Synchronizes view restriction object for the translated page

Parameters:

Name Type Description Default
original Page|Snippet

The original instance.

required
translation_page Page|Snippet

The translated instance.

required
Source code in wagtail_localize/models.py
def sync_view_restrictions(self, original, translation_page):
    """
    Synchronizes view restriction object for the translated page

    Args:
        original (Page|Snippet): The original instance.
        translation_page (Page|Snippet): The translated instance.
    """
    if not isinstance(original, Page) or not isinstance(translation_page, Page):
        raise NoViewRestrictionsError

    if original.view_restrictions.exists():
        original_restriction = original.view_restrictions.first()
        if not translation_page.view_restrictions.exists():
            view_restriction, child_object_map = _copy(
                original_restriction,
                exclude_fields=["id"],
                update_attrs={"page": translation_page},
            )
            view_restriction.save()
        else:
            # if both exist, sync them
            translation_restriction = translation_page.view_restrictions.first()
            should_save = False
            if (
                translation_restriction.restriction_type
                != original_restriction.restriction_type
            ):
                translation_restriction.restriction_type = (
                    original_restriction.restriction_type
                )
                should_save = True
            if translation_restriction.password != original_restriction.password:
                translation_restriction.password = original_restriction.password
                should_save = True
            if list(
                original_restriction.groups.values_list("pk", flat=True)
            ) != list(translation_restriction.groups.values_list("pk", flat=True)):
                translation_restriction.groups.set(
                    original_restriction.groups.all()
                )

            if should_save:
                translation_restriction.save()

    elif translation_page.view_restrictions.exists():
        # the original no longer has the restriction, so drop it
        translation_page.view_restrictions.all().delete()

update_from_db(self)

Retrieves the source instance from the database and updates this TranslationSource with its current contents.

Exceptions:

Type Description
Model.DoesNotExist

If the source instance has been deleted.

Source code in wagtail_localize/models.py
@transaction.atomic
def update_from_db(self):
    """
    Retrieves the source instance from the database and updates this TranslationSource
    with its current contents.

    Raises:
        Model.DoesNotExist: If the source instance has been deleted.
    """
    instance = self.get_source_instance()

    if isinstance(instance, ClusterableModel):
        self.content_json = instance.to_json()
    else:
        serializable_data = get_serializable_data_for_fields(instance)
        self.content_json = json.dumps(serializable_data, cls=DjangoJSONEncoder)

    self.schema_version = get_schema_version(instance._meta.app_label) or ""
    self.object_repr = str(instance)[:200]
    self.last_updated_at = timezone.now()

    self.save(
        update_fields=[
            "content_json",
            "schema_version",
            "object_repr",
            "last_updated_at",
        ]
    )
    self.refresh_segments()

update_or_create_from_instance(instance) classmethod

Creates or updates a TranslationSource for the given instance.

This extracts the content from the given instance. Then stores it in a new TranslationSource instance if one doesn't already exist. If one does already exist, it updates the existing TranslationSource.

Parameters:

Name Type Description Default
instance Model that inherits TranslatableMixin

A Translatable model instance to extract source content from.

required

Returns:

Type Description
tuple[TranslationSource, boolean]

A two-tuple, the first component is the TranslationSource object, and the second component is a boolean that is True if the TranslationSource was created.

Source code in wagtail_localize/models.py
@classmethod
def update_or_create_from_instance(cls, instance):
    """
    Creates or updates a TranslationSource for the given instance.

    This extracts the content from the given instance. Then stores it in a new TranslationSource instance if one
    doesn't already exist. If one does already exist, it updates the existing TranslationSource.

    Args:
        instance (Model that inherits TranslatableMixin): A Translatable model instance to extract source content
            from.

    Returns:
        tuple[TranslationSource, boolean]: A two-tuple, the first component is the TranslationSource object, and
            the second component is a boolean that is True if the TranslationSource was created.
    """
    # Make sure we're using the specific version of pages
    if isinstance(instance, Page):
        instance = instance.specific

    object, created = TranslatableObject.objects.get_or_create_from_instance(
        instance
    )

    if isinstance(instance, ClusterableModel):
        content_json = instance.to_json()
    else:
        serializable_data = get_serializable_data_for_fields(instance)
        content_json = json.dumps(serializable_data, cls=DjangoJSONEncoder)

    # Check if the instance has changed since the previous version
    source = TranslationSource.objects.filter(
        object_id=object.translation_key, locale_id=instance.locale_id
    ).first()

    # Check if the instance has changed at all since the previous version
    if source:
        if json.loads(content_json) == json.loads(source.content_json):
            return source, False

    source, created = cls.objects.update_or_create(
        object=object,
        locale=instance.locale,
        # You can't update the content type of a source. So if this happens,
        # it'll try and create a new source and crash (can't have more than
        # one source per object/locale)
        specific_content_type=ContentType.objects.get_for_model(instance.__class__),
        defaults={
            "locale": instance.locale,
            "object_repr": str(instance)[:200],
            "content_json": content_json,
            "schema_version": get_schema_version(instance._meta.app_label) or "",
            "last_updated_at": timezone.now(),
        },
    )
    source.refresh_segments()
    return source, created

update_target_view_restrictions(self, locale)

Creates a corresponding view restriction object for the translated page for the given locale

Parameters:

Name Type Description Default
locale Locale

The target locale

required
Source code in wagtail_localize/models.py
def update_target_view_restrictions(self, locale):
    """
    Creates a corresponding view restriction object for the translated page for the given locale

    Args:
        locale (Locale): The target locale
    """
    original = self.as_instance()

    # Only update restrictions for pages
    if not isinstance(original, Page):
        return

    try:
        translation_page = self.get_translated_instance(locale)
    except Page.DoesNotExist:
        return

    self.sync_view_restrictions(original, translation_page)