Skip to content

Translation memory models

These models are responsible for storing individual source strings and their translations.

graph TD
A[String] --> B[wagtail.Locale]
C[StringTranslation] --> B
C --> A
C --> D[TranslationContext]
D --> E[TranslatableObject]
F[Template]

style B stroke-dasharray: 5 5
style E stroke-dasharray: 5 5

CannotSaveDraftError

Bases: Exception

Raised when a save draft was request on a non-page model.

Source code in wagtail_localize/models.py
245
246
247
248
class CannotSaveDraftError(Exception):
    """
    Raised when a save draft was request on a non-page model.
    """

LocaleSynchronization

Bases: Model

Stores the "Sync from" setting that users can add to Locales.

This tells Wagtail Localize to synchronise the contents of the 'sync_from' Locale to the 'locale' Locale.

Attributes:

Name Type Description
locale ForeignKey to Locale

The destination Locale of the synchronisation

sync_from ForeignKey to Locale

The source Locale of the synchronisation

Source code in wagtail_localize/models.py
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
@register_locale_component(
    heading=gettext_lazy("Synchronise content from another locale"),
    help_text=gettext_lazy(
        "Choose a locale to synchronise content from. "
        "Any existing and future content authored in the selected locale will "
        "be automatically copied to this one."
    ),
)
class LocaleSynchronization(models.Model):
    """
    Stores the "Sync from" setting that users can add to Locales.

    This tells Wagtail Localize to synchronise the contents of the 'sync_from' Locale to the 'locale' Locale.

    Attributes:
        locale (ForeignKey to Locale): The destination Locale of the synchronisation
        sync_from (ForeignKey to Locale): The source Locale of the synchronisation
    """

    locale = models.OneToOneField(
        "wagtailcore.Locale", on_delete=models.CASCADE, related_name="+"
    )
    sync_from = models.ForeignKey(
        "wagtailcore.Locale", on_delete=models.CASCADE, related_name="+"
    )

    base_form_class = LocaleSynchronizationModelForm

    def __str__(self):
        return f"LocaleSynchronization: {self.locale_id}, {self.sync_from_id}"

    def sync_trees(self, *, page_index=None):
        from .synctree import synchronize_tree

        background.enqueue(
            synchronize_tree,
            args=[self.sync_from, self.locale],
            kwargs={"page_index": page_index},
        )

NoViewRestrictionsError

Bases: Exception

Raised when trying to sync view restrictions for non-Page objects

Source code in wagtail_localize/models.py
251
252
253
254
class NoViewRestrictionsError(Exception):
    """
    Raised when trying to sync view restrictions for non-Page objects
    """

OverridableSegment

Bases: BaseSegment

Represents an overridable segment that was extracted from a TranslationSource.

Attributes:

Name Type Description
data_json TextField with JSON content

The value of the overridable segment as it is in the source.

source ForeignKey to TranslationSource

The source content that the string was extracted from.

context ForeignKey to TranslationContext

The context, which contains the position of the string in the source content.

order PositiveIntegerField

The index that this segment appears on the page.

Source code in wagtail_localize/models.py
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
class OverridableSegment(BaseSegment):
    """
    Represents an overridable segment that was extracted from a TranslationSource.

    Attributes:
        data_json (TextField with JSON content): The value of the overridable segment as it is in the source.
        source (ForeignKey to TranslationSource): The source content that the string was extracted from.
        context (ForeignKey to TranslationContext): The context, which contains the position of the string in the source content.
        order (PositiveIntegerField): The index that this segment appears on the page.
    """

    data_json = models.TextField()

    objects = OverridableSegmentQuerySet.as_manager()

    def __str__(self):
        return f"OverridableSegment({self.pk}) from TranslationSource({self.source_id})"

    @property
    def data(self):
        """
        Returns the decoded JSON data that's stored in .data_json
        """
        return json.loads(self.data_json)

    @classmethod
    def from_value(cls, source, value):
        context, context_created = TranslationContext.objects.get_or_create(
            object_id=source.object_id,
            path=value.path,
        )

        segment, created = cls.objects.get_or_create(
            source=source,
            context=context,
            order=value.order,
            data_json=json.dumps(value.data, cls=DjangoJSONEncoder),
        )

        return segment

data property

Returns the decoded JSON data that's stored in .data_json

OverridableSegmentQuerySet

Bases: QuerySet

Source code in wagtail_localize/models.py
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
class OverridableSegmentQuerySet(models.QuerySet):
    def annotate_override_json(self, locale, include_errors=False):
        """
        Adds an 'override_json' field to the segments containing the
        JSON-formatted data for segments that have been overriden.

        By default, this would exclude any overrides that have
        an error. To include these, set `include_errors` to True.
        """
        overrides = SegmentOverride.objects.filter(
            locale_id=pk(locale),
            context_id=OuterRef("context_id"),
        )

        if not include_errors:
            overrides = overrides.exclude(has_error=True)

        return self.annotate(override_json=Subquery(overrides.values("data_json")))

    def get_overrides(self, locale):
        """
        Returns a queryset of SegmentOverrides that override any of the
        segments in this queryset.
        """
        return SegmentOverride.objects.filter(
            id__in=self.annotate(
                override_id=Subquery(
                    SegmentOverride.objects.filter(
                        locale_id=pk(locale),
                        context_id=OuterRef("context_id"),
                    ).values("id")
                )
            ).values_list("override_id", flat=True)
        )

annotate_override_json(locale, include_errors=False)

Adds an 'override_json' field to the segments containing the JSON-formatted data for segments that have been overriden.

By default, this would exclude any overrides that have an error. To include these, set include_errors to True.

Source code in wagtail_localize/models.py
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
def annotate_override_json(self, locale, include_errors=False):
    """
    Adds an 'override_json' field to the segments containing the
    JSON-formatted data for segments that have been overriden.

    By default, this would exclude any overrides that have
    an error. To include these, set `include_errors` to True.
    """
    overrides = SegmentOverride.objects.filter(
        locale_id=pk(locale),
        context_id=OuterRef("context_id"),
    )

    if not include_errors:
        overrides = overrides.exclude(has_error=True)

    return self.annotate(override_json=Subquery(overrides.values("data_json")))

get_overrides(locale)

Returns a queryset of SegmentOverrides that override any of the segments in this queryset.

Source code in wagtail_localize/models.py
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
def get_overrides(self, locale):
    """
    Returns a queryset of SegmentOverrides that override any of the
    segments in this queryset.
    """
    return SegmentOverride.objects.filter(
        id__in=self.annotate(
            override_id=Subquery(
                SegmentOverride.objects.filter(
                    locale_id=pk(locale),
                    context_id=OuterRef("context_id"),
                ).values("id")
            )
        ).values_list("override_id", flat=True)
    )

POImportWarning

Base class for warnings that are yielded by Translation.import_po.

Source code in wagtail_localize/models.py
992
993
994
995
class POImportWarning:
    """
    Base class for warnings that are yielded by Translation.import_po.
    """

RelatedObjectSegment

Bases: BaseSegment

Represents a related object segment that was extracted from a TranslationSource.

Attributes:

Name Type Description
object ForeignKey to TranslatableObject

The TranslatableObject instance that represents the related object.

source ForeignKey to TranslationSource

The source content that the string was extracted from.

context ForeignKey to TranslationContext

The context, which contains the position of the string in the source content.

order PositiveIntegerField

The index that this segment appears on the page.

Source code in wagtail_localize/models.py
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
class RelatedObjectSegment(BaseSegment):
    """
    Represents a related object segment that was extracted from a TranslationSource.

    Attributes:
        object (ForeignKey to TranslatableObject): The TranslatableObject instance that represents the related object.
        source (ForeignKey to TranslationSource): The source content that the string was extracted from.
        context (ForeignKey to TranslationContext): The context, which contains the position of the string in the source content.
        order (PositiveIntegerField): The index that this segment appears on the page.
    """

    object = models.ForeignKey(
        TranslatableObject, on_delete=models.CASCADE, related_name="references"
    )

    def __str__(self):
        return (
            f"RelatedObjectSegment({self.pk}) from TranslatableObject({self.object_id})"
        )

    def get_source_instance(self):
        return self.object.get_instance_or_none(self.source.locale)

    @classmethod
    def from_value(cls, source, value):
        context, context_created = TranslationContext.objects.get_or_create(
            object_id=source.object_id,
            path=value.path,
        )

        segment, created = cls.objects.get_or_create(
            source=source,
            context=context,
            order=value.order,
            object=TranslatableObject.objects.get_or_create(
                content_type=value.content_type,
                translation_key=value.translation_key,
            )[0],
        )

        return segment

SegmentOverride

Bases: Model

Stores the overridden value of an OverridableSegment.

Some segments are not translatable, but can be optionally overridden in translations. For example, images.

If an overridable segment is overridden by a user for a locale, the value to override the segment with is stored in this model.

Attributes:

Name Type Description
locale ForeignKey to Locale

The Locale to override.

context ForeignKey to TranslationContext

The context to override. With the Locale, this tells us specifically which object/content path to override.

last_translated_by User

The user who last updated this override.

created_at DateTimeField

The date/time when the override was first created.

updated_at DateTimeField

The date/time when the override was last updated.

data_json TextField with JSON contents

The value to override the field with.

has_error BooleanField

Set to True if the value of this overtride has an error. We store overrides with errors in case they were edited from an external system. This allows us to display the error in Wagtail.

field_error TextField

if there was a database-level validation error while saving the translated object, that error is tored here.

Source code in wagtail_localize/models.py
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
class SegmentOverride(models.Model):
    """
    Stores the overridden value of an OverridableSegment.

    Some segments are not translatable, but can be optionally overridden in translations. For example, images.

    If an overridable segment is overridden by a user for a locale, the value to override the segment with is stored
    in this model.

    Attributes:
        locale (ForeignKey to Locale): The Locale to override.
        context (ForeignKey to TranslationContext): The context to override. With the Locale, this tells us specifically
            which object/content path to override.
        last_translated_by (User): The user who last updated this override.
        created_at (DateTimeField): The date/time when the override was first created.
        updated_at (DateTimeField): The date/time when the override was last updated.
        data_json (TextField with JSON contents): The value to override the field with.
        has_error (BooleanField): Set to True if the value of this overtride has an error. We store overrides with
            errors in case they were edited from an external system. This allows us to display the error in Wagtail.
        field_error (TextField): if there was a database-level validation error while saving the translated object, that
            error is tored here.
    """

    locale = models.ForeignKey(
        "wagtailcore.Locale", on_delete=models.CASCADE, related_name="overrides"
    )
    # FIXME: This should be a required field
    context = models.ForeignKey(
        TranslationContext,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="overrides",
    )
    last_translated_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    data_json = models.TextField()
    has_error = models.BooleanField(default=False)

    field_error = models.TextField(blank=True)

    def __str__(self):
        return f"SegmentOverride: {self.locale_id}, {self.context_id}"

    @property
    def data(self):
        return json.loads(self.data_json)

    def set_field_error(self, error):
        """
        Returns a string containing any validation errors on the saved value.

        Returns:
            str: The validation error if there is one.
            None: If there isn't an error.
        """
        self.has_error = True
        # TODO (someday): We currently only support one error at a time
        self.field_error = error[0].messages[0]
        self.save(update_fields=["has_error", "field_error"])

    def get_error(self):
        """
        Returns a string containing any validation errors on the saved value.

        Returns:
            str: The validation error if there is one.
            None: If there isn't an error.
        """
        # Check if a database error was raised when we last attempted to publish
        if self.has_error and self.context is not None and self.field_error:
            return self.field_error

get_error()

Returns a string containing any validation errors on the saved value.

Returns:

Name Type Description
str

The validation error if there is one.

None

If there isn't an error.

Source code in wagtail_localize/models.py
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
def get_error(self):
    """
    Returns a string containing any validation errors on the saved value.

    Returns:
        str: The validation error if there is one.
        None: If there isn't an error.
    """
    # Check if a database error was raised when we last attempted to publish
    if self.has_error and self.context is not None and self.field_error:
        return self.field_error

set_field_error(error)

Returns a string containing any validation errors on the saved value.

Returns:

Name Type Description
str

The validation error if there is one.

None

If there isn't an error.

Source code in wagtail_localize/models.py
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
def set_field_error(self, error):
    """
    Returns a string containing any validation errors on the saved value.

    Returns:
        str: The validation error if there is one.
        None: If there isn't an error.
    """
    self.has_error = True
    # TODO (someday): We currently only support one error at a time
    self.field_error = error[0].messages[0]
    self.save(update_fields=["has_error", "field_error"])

String

Bases: Model

Represents a unique string of translatable text.

Attributes:

Name Type Description
locale ForeignKey to Locale

The locale of the string.

data TextField

The string.

data_hash UUIDField

A hash of the string, for more efficient indexing of long strings.

Source code in wagtail_localize/models.py
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
class String(models.Model):
    """
    Represents a unique string of translatable text.

    Attributes:
        locale (ForeignKey to Locale): The locale of the string.
        data (TextField): The string.
        data_hash (UUIDField): A hash of the string, for more efficient indexing of long strings.
    """

    UUID_NAMESPACE = uuid.UUID("59ed7d1c-7eb5-45fa-9c8b-7a7057ed56d7")

    locale = models.ForeignKey(
        "wagtailcore.Locale", on_delete=models.CASCADE, related_name="source_strings"
    )

    data_hash = models.UUIDField()
    data = models.TextField()

    class Meta:
        unique_together = [("locale", "data_hash")]

    def __str__(self):
        return f"String: {self.locale_id}, {self.data_hash}"

    def save(self, *args, **kwargs):
        if self.data and self.data_hash is None:
            self.data_hash = self._get_data_hash(self.data)

        return super().save(*args, **kwargs)

    @classmethod
    def _get_data_hash(cls, data):
        """
        Generates a UUID from the given string.

        Args:
            data (string): The string to generate a hash of.

        Returns:
            UUID: The UUID hash.
        """
        return uuid.uuid5(cls.UUID_NAMESPACE, data)

    @classmethod
    def from_value(cls, locale, stringvalue):
        """
        Gets or creates a String instance from a StringValue object.

        Args:
            locale (ForeignKey to Locale) The locale of the string.
            stringvalue (StringValue): The value of the string.

        Returns:
            String: The String instance that corresponds with the given stringvalue and locale.
        """
        string, created = cls.objects.get_or_create(
            locale_id=pk(locale),
            data_hash=cls._get_data_hash(stringvalue.data),
            defaults={"data": stringvalue.data},
        )

        return string

    def as_value(self):
        """
        Creates a StringValue object from the contents of this string.

        Returns:
            StringValue: A StringValue instance with the content of this String.
        """
        return StringValue(self.data)

as_value()

Creates a StringValue object from the contents of this string.

Returns:

Name Type Description
StringValue

A StringValue instance with the content of this String.

Source code in wagtail_localize/models.py
1472
1473
1474
1475
1476
1477
1478
1479
def as_value(self):
    """
    Creates a StringValue object from the contents of this string.

    Returns:
        StringValue: A StringValue instance with the content of this String.
    """
    return StringValue(self.data)

from_value(locale, stringvalue) classmethod

Gets or creates a String instance from a StringValue object.

Parameters:

Name Type Description Default
stringvalue StringValue

The value of the string.

required

Returns:

Name Type Description
String

The String instance that corresponds with the given stringvalue and locale.

Source code in wagtail_localize/models.py
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
@classmethod
def from_value(cls, locale, stringvalue):
    """
    Gets or creates a String instance from a StringValue object.

    Args:
        locale (ForeignKey to Locale) The locale of the string.
        stringvalue (StringValue): The value of the string.

    Returns:
        String: The String instance that corresponds with the given stringvalue and locale.
    """
    string, created = cls.objects.get_or_create(
        locale_id=pk(locale),
        data_hash=cls._get_data_hash(stringvalue.data),
        defaults={"data": stringvalue.data},
    )

    return string

StringSegment

Bases: BaseSegment

Represents a translatable string that was extracted from a TranslationSource.

Attributes:

Name Type Description
string ForeignKey to String

The string that was extracted.

attrs TextField with JSON contents

The HTML attributes that were extracted from the string.

When we extract the segment, we replace HTML attributes with id attributes and the attributes that were removed are stored in this field. When the translated strings come back, we replace the id attributes with the original HTML attributes.

For example, for this segment:

<a href="https://www.example.com">Link to example.com</a>

We will remove the href tag from it and replace it with an id:

<a id="a1">Link to example.com</a>

And then this field will be populated with the following JSON:

{
    "a#a1": {
        "href": "https://www.example.com"
    }
}
source ForiegnKey[TranslationSource]

The source content that the string was extracted from.

context ForeignKey to TranslationContext

The context, which contains the position of the string in the source content.

order PositiveIntegerField

The index that this segment appears on the page.

Source code in wagtail_localize/models.py
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
class StringSegment(BaseSegment):
    """
    Represents a translatable string that was extracted from a TranslationSource.

    Attributes:
        string (ForeignKey to String): The string that was extracted.
        attrs (TextField with JSON contents): The HTML attributes that were extracted from the string.

            When we extract the segment, we replace HTML attributes with ``id`` attributes and the attributes that were
            removed are stored in this field. When the translated strings come back, we replace the ``id`` attributes
            with the original HTML attributes.

            For example, for this segment:

            ``<a href="https://www.example.com">Link to example.com</a>``

            We will remove the ``href`` tag from it and replace it with an ``id``:

            ``<a id="a1">Link to example.com</a>``

            And then this field will be populated with the following JSON:

            ``` json
            {
                "a#a1": {
                    "href": "https://www.example.com"
                }
            }
            ```

        source (ForiegnKey[TranslationSource]): The source content that the string was extracted from.
        context (ForeignKey to TranslationContext): The context, which contains the position of the string in the source content.
        order (PositiveIntegerField): The index that this segment appears on the page.
    """

    string = models.ForeignKey(
        String, on_delete=models.CASCADE, related_name="segments"
    )
    attrs = models.TextField(blank=True)

    objects = StringSegmentQuerySet.as_manager()

    def __str__(self):
        return f"StringSegment from String({self.string_id})"

    @classmethod
    def from_value(cls, source, language, value):
        """
        Gets or creates a TemplateSegment instance from a TemplateValue object.

        Args:
            source (TranslationSource): The source the template value was extracted from.
            template_value (TemplateValue): The value of the template.

        Returns:
            TemplateSegment: The TemplateSegment instance that corresponds with the given template_value and source.
        """
        string = String.from_value(language, value.string)
        context, context_created = TranslationContext.objects.get_or_create(
            object_id=source.object_id,
            path=value.path,
        )

        segment, created = cls.objects.get_or_create(
            source=source,
            context=context,
            order=value.order,
            string=string,
            attrs=json.dumps(value.attrs, cls=DjangoJSONEncoder),
        )

        return segment

from_value(source, language, value) classmethod

Gets or creates a TemplateSegment instance from a TemplateValue object.

Parameters:

Name Type Description Default
source TranslationSource

The source the template value was extracted from.

required
template_value TemplateValue

The value of the template.

required

Returns:

Name Type Description
TemplateSegment

The TemplateSegment instance that corresponds with the given template_value and source.

Source code in wagtail_localize/models.py
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
@classmethod
def from_value(cls, source, language, value):
    """
    Gets or creates a TemplateSegment instance from a TemplateValue object.

    Args:
        source (TranslationSource): The source the template value was extracted from.
        template_value (TemplateValue): The value of the template.

    Returns:
        TemplateSegment: The TemplateSegment instance that corresponds with the given template_value and source.
    """
    string = String.from_value(language, value.string)
    context, context_created = TranslationContext.objects.get_or_create(
        object_id=source.object_id,
        path=value.path,
    )

    segment, created = cls.objects.get_or_create(
        source=source,
        context=context,
        order=value.order,
        string=string,
        attrs=json.dumps(value.attrs, cls=DjangoJSONEncoder),
    )

    return segment

StringSegmentQuerySet

Bases: QuerySet

Source code in wagtail_localize/models.py
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
class StringSegmentQuerySet(models.QuerySet):
    def annotate_translation(self, locale, include_errors=False):
        """
        Adds a 'translation' field to the segments containing the
        text content of the segment translated into the specified
        locale.

        By default, this would exclude any translations that have
        an error. To include these, set `include_errors` to True.
        """
        translations = StringTranslation.objects.filter(
            translation_of_id=OuterRef("string_id"),
            locale_id=pk(locale),
            context_id=OuterRef("context_id"),
        )

        if not include_errors:
            translations = translations.exclude(has_error=True)

        return self.annotate(translation=Subquery(translations.values("data")))

    def get_translations(self, locale):
        """
        Returns a queryset of StringTranslations that match any of the
        strings in this queryset.
        """
        return StringTranslation.objects.filter(
            id__in=self.annotate(
                translation_id=Subquery(
                    StringTranslation.objects.filter(
                        translation_of_id=OuterRef("string_id"),
                        locale_id=pk(locale),
                        context_id=OuterRef("context_id"),
                    ).values("id")
                )
            ).values_list("translation_id", flat=True)
        )

annotate_translation(locale, include_errors=False)

Adds a 'translation' field to the segments containing the text content of the segment translated into the specified locale.

By default, this would exclude any translations that have an error. To include these, set include_errors to True.

Source code in wagtail_localize/models.py
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
def annotate_translation(self, locale, include_errors=False):
    """
    Adds a 'translation' field to the segments containing the
    text content of the segment translated into the specified
    locale.

    By default, this would exclude any translations that have
    an error. To include these, set `include_errors` to True.
    """
    translations = StringTranslation.objects.filter(
        translation_of_id=OuterRef("string_id"),
        locale_id=pk(locale),
        context_id=OuterRef("context_id"),
    )

    if not include_errors:
        translations = translations.exclude(has_error=True)

    return self.annotate(translation=Subquery(translations.values("data")))

get_translations(locale)

Returns a queryset of StringTranslations that match any of the strings in this queryset.

Source code in wagtail_localize/models.py
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
def get_translations(self, locale):
    """
    Returns a queryset of StringTranslations that match any of the
    strings in this queryset.
    """
    return StringTranslation.objects.filter(
        id__in=self.annotate(
            translation_id=Subquery(
                StringTranslation.objects.filter(
                    translation_of_id=OuterRef("string_id"),
                    locale_id=pk(locale),
                    context_id=OuterRef("context_id"),
                ).values("id")
            )
        ).values_list("translation_id", flat=True)
    )

StringTranslation

Bases: Model

Represents a translation of a string.

Attributes:

Name Type Description
translation_of ForeignKey to String

The String that this is a translation of.

locale ForeignKey to Locale

The Locale of this translation.

context ForeignKey to TranslationContext

The context that this translation was made in. This allows different fields/pages to have different translations of the same source string.

data TextField

The translation.

translation_type CharField with choices 'manual' or 'machine'

Whether the translationw as performed by a human or machine.

tool_name CharField

The name of the tool that was used to make this translation.

last_translated_by ForeignKey to User

The user who last updated this translation.

created_at DateTimeField

The date/time that this translation was first created.

updated_at DateFimeField

The date/time that this translation was last updated.

has_error BooleanField

Set to True if the value of this translation has an error. We store translations with errors in case they were edited from an external system. This allows us to display the error in Wagtail.

field_error TextField

If there was a database-level validation error while saving the translated object, that error is tored here. Note that this only makes sense if the context is not null.

Source code in wagtail_localize/models.py
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
class StringTranslation(models.Model):
    """
    Represents a translation of a string.

    Attributes:
        translation_of (ForeignKey to String): The String that this is a translation of.
        locale (ForeignKey to Locale): The Locale of this translation.
        context (ForeignKey to TranslationContext): The context that this translation was made in. This allows different
            fields/pages to have different translations of the same source string.
        data (TextField): The translation.
        translation_type (CharField with choices 'manual' or 'machine'): Whether the translationw as performed by a human or machine.
        tool_name (CharField): The name of the tool that was used to make this translation.
        last_translated_by (ForeignKey to User): The user who last updated this translation.
        created_at (DateTimeField): The date/time that this translation was first created.
        updated_at (DateFimeField): The date/time that this translation was last updated.
        has_error (BooleanField): Set to True if the value of this translation has an error. We store translations with
            errors in case they were edited from an external system. This allows us to display the error in Wagtail.
        field_error (TextField): If there was a database-level validation error while saving the translated object, that
            error is tored here. Note that this only makes sense if the context is not null.
    """

    TRANSLATION_TYPE_MANUAL = "manual"
    TRANSLATION_TYPE_MACHINE = "machine"
    TRANSLATION_TYPE_CHOICES = [
        (TRANSLATION_TYPE_MANUAL, gettext_lazy("Manual")),
        (TRANSLATION_TYPE_MACHINE, gettext_lazy("Machine")),
    ]

    translation_of = models.ForeignKey(
        String, on_delete=models.CASCADE, related_name="translations"
    )
    locale = models.ForeignKey(
        "wagtailcore.Locale",
        on_delete=models.CASCADE,
        related_name="string_translations",
    )
    context = models.ForeignKey(
        TranslationContext,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="translations",
    )
    data = models.TextField()
    translation_type = models.CharField(max_length=20, choices=TRANSLATION_TYPE_CHOICES)
    tool_name = models.CharField(max_length=255, blank=True)
    last_translated_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    has_error = models.BooleanField(default=False)
    field_error = models.TextField(blank=True)

    class Meta:
        unique_together = [("locale", "translation_of", "context")]

    def __str__(self):
        return f"StringTranslation: {self.translation_of_id}, {self.locale_id}, {self.context_id}, {self.translation_type}"

    def save(self, *args, **kwargs):
        update_fields = kwargs.get("update_fields")
        super().save(*args, **kwargs)

        # Set has_error if the string is invalid.
        # Since we allow translations to be made by external tools, we need to allow invalid
        # HTML in the database so that it can be fixed in Wagtail. However, we do want to know
        # if any strings are invalid so we don't use them on a page.
        updating_data = update_fields is None or "data" in update_fields
        if updating_data and not self.has_error:
            try:
                StringValue.from_translated_html(self.data)
                validate_translation_links(self.translation_of.data, self.data)
            except ValueError:
                self.has_error = True
                self.save(update_fields=["has_error"])

    @classmethod
    def from_text(cls, translation_of, locale, context, data):
        """
        Gets or creates a StringTranslation instance from the given parameters.

        Args:
            translation_of (ForeignKey to String): The String that this is a translation of.
            locale (ForeignKey to Locale): The Locale of this translation.
            context (ForeignKey to TranslationContext): The context that this translation was made in. This allows different
                fields/pages to have different translations of the same source string.
            data (TextField): The translation.

        Returns:
            String: The String instance that corresponds with the given stringvalue and locale.
        """
        segment, created = cls.objects.get_or_create(
            translation_of=translation_of,
            locale_id=pk(locale),
            context_id=pk(context),
            defaults={"data": data},
        )

        return segment

    def set_field_error(self, error):
        """
        This sets the `has_error`/`field_error` fields to the value of the given ValidationError instance.

        Note: If the given ValidationError contains multiple errors, only the first one is stored.

        Note: This updates the database instance as well.

        Args:
            error (ValidationError): The validation error to store.
        """
        self.has_error = True
        # TODO (someday): We currently only support one error at a time
        self.field_error = error[0].messages[0]
        self.save(update_fields=["has_error", "field_error"])

    def get_error(self):
        """
        Returns a string containing any validation errors on the saved value.

        Returns:
            str: The validation error if there is one.
            None: If there isn't an error.
        """
        if not self.has_error:
            return

        # Check for HTML validation errors
        try:
            StringValue.from_translated_html(self.data)
            validate_translation_links(self.translation_of.data, self.data)
        except ValueError as e:
            return e.args[0]

        # Check if a database error was raised when we last attempted to publish
        if self.context is not None and self.field_error:
            return self.field_error

    def get_comment(self):
        """
        Returns a comment to display to the user containing info on how and when the string was translated.

        Returns:
            str: A comment to display to the user.
        """
        if self.tool_name:
            return _("Translated with {tool_name} on {date}").format(
                tool_name=self.tool_name, date=self.updated_at.strftime(DATE_FORMAT)
            )

        elif self.translation_type == self.TRANSLATION_TYPE_MANUAL:
            return _("Translated manually on {date}").format(
                date=self.updated_at.strftime(DATE_FORMAT)
            )

        elif self.translation_type == self.TRANSLATION_TYPE_MACHINE:
            return _("Machine translated on {date}").format(
                date=self.updated_at.strftime(DATE_FORMAT)
            )

from_text(translation_of, locale, context, data) classmethod

Gets or creates a StringTranslation instance from the given parameters.

Parameters:

Name Type Description Default
translation_of ForeignKey to String

The String that this is a translation of.

required
locale ForeignKey to Locale

The Locale of this translation.

required
context ForeignKey to TranslationContext

The context that this translation was made in. This allows different fields/pages to have different translations of the same source string.

required
data TextField

The translation.

required

Returns:

Name Type Description
String

The String instance that corresponds with the given stringvalue and locale.

Source code in wagtail_localize/models.py
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
@classmethod
def from_text(cls, translation_of, locale, context, data):
    """
    Gets or creates a StringTranslation instance from the given parameters.

    Args:
        translation_of (ForeignKey to String): The String that this is a translation of.
        locale (ForeignKey to Locale): The Locale of this translation.
        context (ForeignKey to TranslationContext): The context that this translation was made in. This allows different
            fields/pages to have different translations of the same source string.
        data (TextField): The translation.

    Returns:
        String: The String instance that corresponds with the given stringvalue and locale.
    """
    segment, created = cls.objects.get_or_create(
        translation_of=translation_of,
        locale_id=pk(locale),
        context_id=pk(context),
        defaults={"data": data},
    )

    return segment

get_comment()

Returns a comment to display to the user containing info on how and when the string was translated.

Returns:

Name Type Description
str

A comment to display to the user.

Source code in wagtail_localize/models.py
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
def get_comment(self):
    """
    Returns a comment to display to the user containing info on how and when the string was translated.

    Returns:
        str: A comment to display to the user.
    """
    if self.tool_name:
        return _("Translated with {tool_name} on {date}").format(
            tool_name=self.tool_name, date=self.updated_at.strftime(DATE_FORMAT)
        )

    elif self.translation_type == self.TRANSLATION_TYPE_MANUAL:
        return _("Translated manually on {date}").format(
            date=self.updated_at.strftime(DATE_FORMAT)
        )

    elif self.translation_type == self.TRANSLATION_TYPE_MACHINE:
        return _("Machine translated on {date}").format(
            date=self.updated_at.strftime(DATE_FORMAT)
        )

get_error()

Returns a string containing any validation errors on the saved value.

Returns:

Name Type Description
str

The validation error if there is one.

None

If there isn't an error.

Source code in wagtail_localize/models.py
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
def get_error(self):
    """
    Returns a string containing any validation errors on the saved value.

    Returns:
        str: The validation error if there is one.
        None: If there isn't an error.
    """
    if not self.has_error:
        return

    # Check for HTML validation errors
    try:
        StringValue.from_translated_html(self.data)
        validate_translation_links(self.translation_of.data, self.data)
    except ValueError as e:
        return e.args[0]

    # Check if a database error was raised when we last attempted to publish
    if self.context is not None and self.field_error:
        return self.field_error

set_field_error(error)

This sets the has_error/field_error fields to the value of the given ValidationError instance.

Note: If the given ValidationError contains multiple errors, only the first one is stored.

Note: This updates the database instance as well.

Parameters:

Name Type Description Default
error ValidationError

The validation error to store.

required
Source code in wagtail_localize/models.py
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
def set_field_error(self, error):
    """
    This sets the `has_error`/`field_error` fields to the value of the given ValidationError instance.

    Note: If the given ValidationError contains multiple errors, only the first one is stored.

    Note: This updates the database instance as well.

    Args:
        error (ValidationError): The validation error to store.
    """
    self.has_error = True
    # TODO (someday): We currently only support one error at a time
    self.field_error = error[0].messages[0]
    self.save(update_fields=["has_error", "field_error"])

Template

Bases: Model

A Template stores the structure of a RichTextField or RichTextBlock.

When a RichTextField/RichTextBlock is converted into segments, all translatable segments are stripped out of the block and stored as String instances. The remaining HTML is saved as a Template and is used for recombining the translated strings back into a rich text value.

Attributes:

Name Type Description
template TextField

The template value

uuid UUIDField

A hash of the template contents for efficient indexing.

template_format CharField

The format of the template (currently, only 'html' is supported).

string_count PositiveIntegerField

The number of translatable stirngs that were extracted from the template.

Source code in wagtail_localize/models.py
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
class Template(models.Model):
    """
    A Template stores the structure of a RichTextField or RichTextBlock.

    When a RichTextField/RichTextBlock is converted into segments, all translatable segments are stripped out of the
    block and stored as String instances. The remaining HTML is saved as a Template and is used for recombining the
    translated strings back into a rich text value.

    Attributes:
        template (TextField): The template value
        uuid (UUIDField): A hash of the template contents for efficient indexing.
        template_format (CharField): The format of the template (currently, only 'html' is supported).
        string_count (PositiveIntegerField): The number of translatable stirngs that were extracted from the template.
    """

    BASE_UUID_NAMESPACE = uuid.UUID("4599eabc-3f8e-41a9-be61-95417d26a8cd")

    uuid = models.UUIDField(unique=True)
    template = models.TextField()
    template_format = models.CharField(max_length=100)
    string_count = models.PositiveIntegerField()

    def __str__(self):
        return f"Template: {self.uuid}, {self.template_format}, {self.string_count}"

    @classmethod
    def from_value(cls, template_value):
        """
        Gets or creates a Template instance from a TemplateValue object.

        Args:
            template_value (TemplateValue): The value of the template.

        Returns:
            Template: The Template instance that corresponds with the given template_value.
        """
        uuid_namespace = uuid.uuid5(cls.BASE_UUID_NAMESPACE, template_value.format)

        template, created = cls.objects.get_or_create(
            uuid=uuid.uuid5(uuid_namespace, template_value.template),
            defaults={
                "template": template_value.template,
                "template_format": template_value.format,
                "string_count": template_value.string_count,
            },
        )

        return template

from_value(template_value) classmethod

Gets or creates a Template instance from a TemplateValue object.

Parameters:

Name Type Description Default
template_value TemplateValue

The value of the template.

required

Returns:

Name Type Description
Template

The Template instance that corresponds with the given template_value.

Source code in wagtail_localize/models.py
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
@classmethod
def from_value(cls, template_value):
    """
    Gets or creates a Template instance from a TemplateValue object.

    Args:
        template_value (TemplateValue): The value of the template.

    Returns:
        Template: The Template instance that corresponds with the given template_value.
    """
    uuid_namespace = uuid.uuid5(cls.BASE_UUID_NAMESPACE, template_value.format)

    template, created = cls.objects.get_or_create(
        uuid=uuid.uuid5(uuid_namespace, template_value.template),
        defaults={
            "template": template_value.template,
            "template_format": template_value.format,
            "string_count": template_value.string_count,
        },
    )

    return template

TemplateSegment

Bases: BaseSegment

Represents a template segment that was extracted from a TranslationSource.

Attributes:

Name Type Description
template ForeignKey to Template

The template that was extracted.

source ForeignKey[TranslationSource]

The source content that the string was extracted from.

context ForeignKey to TranslationContext

The context, which contains the position of the string in the source content.

order PositiveIntegerField

The index that this segment appears on the page.

Source code in wagtail_localize/models.py
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
class TemplateSegment(BaseSegment):
    """
    Represents a template segment that was extracted from a TranslationSource.

    Attributes:
        template (ForeignKey to Template): The template that was extracted.
        source (ForeignKey[TranslationSource]): The source content that the string was extracted from.
        context (ForeignKey to TranslationContext): The context, which contains the position of the string in the source content.
        order (PositiveIntegerField): The index that this segment appears on the page.
    """

    template = models.ForeignKey(
        Template, on_delete=models.CASCADE, related_name="segments"
    )

    def __str__(self):
        return f"TemplateSegment({self.pk}) from Template({self.template_id})"

    @classmethod
    def from_value(cls, source, value):
        """
        Gets or creates a TemplateSegment instance from a TemplateValue object.

        Args:
            source (TranslationSource): The source the template value was extracted from.
            template_value (TemplateValue): The value of the template.

        Returns:
            TemplateSegment: The TemplateSegment instance that corresponds with the given template_value and source.
        """
        template = Template.from_value(value)
        context, context_created = TranslationContext.objects.get_or_create(
            object_id=source.object_id,
            path=value.path,
        )

        segment, created = cls.objects.get_or_create(
            source=source,
            context=context,
            order=value.order,
            template=template,
        )

        return segment

from_value(source, value) classmethod

Gets or creates a TemplateSegment instance from a TemplateValue object.

Parameters:

Name Type Description Default
source TranslationSource

The source the template value was extracted from.

required
template_value TemplateValue

The value of the template.

required

Returns:

Name Type Description
TemplateSegment

The TemplateSegment instance that corresponds with the given template_value and source.

Source code in wagtail_localize/models.py
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
@classmethod
def from_value(cls, source, value):
    """
    Gets or creates a TemplateSegment instance from a TemplateValue object.

    Args:
        source (TranslationSource): The source the template value was extracted from.
        template_value (TemplateValue): The value of the template.

    Returns:
        TemplateSegment: The TemplateSegment instance that corresponds with the given template_value and source.
    """
    template = Template.from_value(value)
    context, context_created = TranslationContext.objects.get_or_create(
        object_id=source.object_id,
        path=value.path,
    )

    segment, created = cls.objects.get_or_create(
        source=source,
        context=context,
        order=value.order,
        template=template,
    )

    return segment

TranslatableObject

Bases: Model

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.

Source code in wagtail_localize/models.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
class TranslatableObject(models.Model):
    """
    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:
        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.
    """

    translation_key = models.UUIDField(primary_key=True)
    content_type = models.ForeignKey(
        ContentType, on_delete=models.CASCADE, related_name="+"
    )

    objects = TranslatableObjectManager()

    class Meta:
        unique_together = [("content_type", "translation_key")]

    def __str__(self):
        return f"TranslatableObject: {self.translation_key}, {self.content_type_id}"

    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()

    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)
        )

    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

get_instance(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:

Name Type Description
Model

The model instance.

Raises:

Type Description
DoesNotExist

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

Source code in wagtail_localize/models.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
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(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:

Name Type Description
Model

The model instance if one exists.

None

If the model doesn't exist.

Source code in wagtail_localize/models.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
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(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:

Name Type Description
bool

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

Source code in wagtail_localize/models.py
193
194
195
196
197
198
199
200
201
202
203
204
205
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

Bases: Model

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.

Source code in wagtail_localize/models.py
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
class Translation(models.Model):
    """
    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:
        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.
    """

    uuid = models.UUIDField(unique=True, default=uuid.uuid4)

    source = models.ForeignKey(
        TranslationSource, on_delete=models.CASCADE, related_name="translations"
    )
    target_locale = models.ForeignKey(
        "wagtailcore.Locale",
        on_delete=models.CASCADE,
        related_name="translations",
    )

    created_at = models.DateTimeField(auto_now_add=True)
    translations_last_updated_at = models.DateTimeField(null=True)
    destination_last_updated_at = models.DateTimeField(null=True)
    enabled = models.BooleanField(default=True)

    class Meta:
        unique_together = [
            ("source", "target_locale"),
        ]

    def __str__(self):
        return f"Translation: {self.uuid}, {self.source_id}, {self.target_locale_id}, (enabled: {self.enabled})"

    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)

    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())

    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=Coalesce(Sum("is_translated_i"), 0),
        )

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

    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")

    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

    @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:
                # Filter by hash instead to avoid case sensitivity issues
                # https://github.com/wagtail/wagtail-localize/issues/758
                string = String.objects.get(
                    locale_id=self.source.locale_id,
                    data_hash=String._get_data_hash(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 and string_translation.data != entry.msgstr:
                    # Update the string_translation only if it has changed
                    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

    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,
        )

export_po()

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
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
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()

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
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
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=Coalesce(Sum("is_translated_i"), 0),
    )

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

get_status_display()

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

Returns:

Name Type Description
str

The status of this translation

Source code in wagtail_localize/models.py
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
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()

Fetches the translated instance from the database.

Raises:

Type Description
DoesNotExist

if the translation does not exist.

Returns:

Name Type Description
Model

The translated instance.

Source code in wagtail_localize/models.py
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
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()

Returns the URL to edit the target instance.

Raises:

Type Description
DoesNotExist

if the translation does not exist.

Returns:

Name Type Description
str

The URL of the edit view of the target instance.

Source code in wagtail_localize/models.py
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
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(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 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
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
@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:
            # Filter by hash instead to avoid case sensitivity issues
            # https://github.com/wagtail/wagtail-localize/issues/758
            string = String.objects.get(
                locale_id=self.source.locale_id,
                data_hash=String._get_data_hash(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 and string_translation.data != entry.msgstr:
                # Update the string_translation only if it has changed
                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(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

Raises:

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:

Name Type Description
Model

The translated instance.

Source code in wagtail_localize/models.py
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
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,
    )

TranslationContext

Bases: Model

Represents a context that a string may be translated in.

Strings can be translated differently in different contexts. A context is a combination of an object and content path.

Attributes:

Name Type Description
object ForeignKey to TranslatableObject

The object.

path TextField

The content path.

field_path TextField

the field path.

path_id UUIDField

A hash of the path for efficient indexing of long content paths.

Source code in wagtail_localize/models.py
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
class TranslationContext(models.Model):
    """
    Represents a context that a string may be translated in.

    Strings can be translated differently in different contexts. A context is a combination of an object and content
    path.

    Attributes:
        object (ForeignKey to TranslatableObject): The object.
        path (TextField): The content path.
        field_path (TextField): the field path.
        path_id (UUIDField): A hash of the path for efficient indexing of long content paths.
    """

    object = models.ForeignKey(
        TranslatableObject, on_delete=models.CASCADE, related_name="+"
    )
    path_id = models.UUIDField()
    path = models.TextField()
    field_path = models.TextField()

    class Meta:
        unique_together = [
            ("object", "path_id"),
        ]

    def __str__(self):
        return f"TranslationContext: {self.object_id}, {self.path_id}, {self.path}, {self.field_path}"

    def save(self, *args, **kwargs):
        if self.path and self.path_id is None:
            self.path_id = self._get_path_id(self.path)

        return super().save(*args, **kwargs)

    @classmethod
    def _get_path_id(cls, path):
        """
        Generates a UUID from the given content path.

        Args:
            path (string): The content path to generate a hash of.

        Returns:
            UUID: The UUID hash.
        """
        return uuid.uuid5(uuid.UUID("fcab004a-2b50-11ea-978f-2e728ce88125"), path)

    def get_field_path(self, instance):
        """
        Gets the field path for this context

        Field path's were introduced in version 1.0, any contexts that were created before that release won't have one.
        """
        if not self.field_path:

            def get_field_path_from_field(instance, path_components):
                field_name = path_components[0]
                field = instance._meta.get_field(field_name)

                if isinstance(field, StreamField):

                    def get_field_path_from_streamfield_block(value, path_components):
                        if isinstance(value, blocks.StructValue):
                            blocks_by_id = dict(value)
                        else:
                            if isinstance(value, ListValue):
                                blocks_by_id = {
                                    block.id: block for block in value.bound_blocks
                                }
                            else:
                                blocks_by_id = {block.id: block for block in value}

                        block_id = path_components[0]
                        block = blocks_by_id[block_id]

                        if isinstance(value, blocks.StructValue):
                            block_type = block_id
                            block_def = value.block.child_blocks[block_type]
                            block_value = block
                        else:
                            if isinstance(value, ListValue):
                                block_type = "item"
                                block_def = value.list_block.child_block
                            else:
                                block_type = block.block_type
                                block_def = value.stream_block.child_blocks[block_type]
                            block_value = block.value

                        if apps.is_installed("wagtail.images"):
                            from wagtail.images.blocks import ImageBlock

                            if isinstance(block_def, ImageBlock):
                                # the path components are ["the_image_block_field_name", "alt_text"]
                                # so there is no need for further processing as this will return
                                # ["image_block", "alt_text"]
                                return [block_type] + path_components[1:]

                        if isinstance(
                            block_def,
                            blocks.StructBlock | blocks.StreamBlock | blocks.ListBlock,
                        ):
                            return [block_type] + get_field_path_from_streamfield_block(
                                block_value, path_components[1:]
                            )
                        else:
                            return [block_type]

                    return [field_name] + get_field_path_from_streamfield_block(
                        field.value_from_object(instance), path_components[1:]
                    )

                elif (
                    isinstance(field, models.ManyToOneRel)
                    and isinstance(field.remote_field, ParentalKey)
                    and issubclass(field.related_model, TranslatableMixin)
                ):
                    manager = getattr(instance, field_name)
                    child_instance = manager.get(translation_key=path_components[1])
                    return [field_name] + get_field_path_from_field(
                        child_instance, path_components[2:]
                    )

                else:
                    return [field_name]

            self.field_path = ".".join(
                get_field_path_from_field(instance, self.path.split("."))
            )
            self.save(update_fields=["field_path"])

        return self.field_path

get_field_path(instance)

Gets the field path for this context

Field path's were introduced in version 1.0, any contexts that were created before that release won't have one.

Source code in wagtail_localize/models.py
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
def get_field_path(self, instance):
    """
    Gets the field path for this context

    Field path's were introduced in version 1.0, any contexts that were created before that release won't have one.
    """
    if not self.field_path:

        def get_field_path_from_field(instance, path_components):
            field_name = path_components[0]
            field = instance._meta.get_field(field_name)

            if isinstance(field, StreamField):

                def get_field_path_from_streamfield_block(value, path_components):
                    if isinstance(value, blocks.StructValue):
                        blocks_by_id = dict(value)
                    else:
                        if isinstance(value, ListValue):
                            blocks_by_id = {
                                block.id: block for block in value.bound_blocks
                            }
                        else:
                            blocks_by_id = {block.id: block for block in value}

                    block_id = path_components[0]
                    block = blocks_by_id[block_id]

                    if isinstance(value, blocks.StructValue):
                        block_type = block_id
                        block_def = value.block.child_blocks[block_type]
                        block_value = block
                    else:
                        if isinstance(value, ListValue):
                            block_type = "item"
                            block_def = value.list_block.child_block
                        else:
                            block_type = block.block_type
                            block_def = value.stream_block.child_blocks[block_type]
                        block_value = block.value

                    if apps.is_installed("wagtail.images"):
                        from wagtail.images.blocks import ImageBlock

                        if isinstance(block_def, ImageBlock):
                            # the path components are ["the_image_block_field_name", "alt_text"]
                            # so there is no need for further processing as this will return
                            # ["image_block", "alt_text"]
                            return [block_type] + path_components[1:]

                    if isinstance(
                        block_def,
                        blocks.StructBlock | blocks.StreamBlock | blocks.ListBlock,
                    ):
                        return [block_type] + get_field_path_from_streamfield_block(
                            block_value, path_components[1:]
                        )
                    else:
                        return [block_type]

                return [field_name] + get_field_path_from_streamfield_block(
                    field.value_from_object(instance), path_components[1:]
                )

            elif (
                isinstance(field, models.ManyToOneRel)
                and isinstance(field.remote_field, ParentalKey)
                and issubclass(field.related_model, TranslatableMixin)
            ):
                manager = getattr(instance, field_name)
                child_instance = manager.get(translation_key=path_components[1])
                return [field_name] + get_field_path_from_field(
                    child_instance, path_components[2:]
                )

            else:
                return [field_name]

        self.field_path = ".".join(
            get_field_path_from_field(instance, self.path.split("."))
        )
        self.save(update_fields=["field_path"])

    return self.field_path

TranslationLog

Bases: Model

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.

revision ForeignKey to Revision

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

Source code in wagtail_localize/models.py
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
class TranslationLog(models.Model):
    """
    Keeps Track of when translations are created/updated.

    Attributes:
        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.
        revision (ForeignKey to Revision): If the translation was of a page, this links to the Revision that was created
    """

    source = models.ForeignKey(
        TranslationSource, on_delete=models.CASCADE, related_name="translation_logs"
    )
    locale = models.ForeignKey(
        "wagtailcore.Locale",
        on_delete=models.CASCADE,
        related_name="translation_logs",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    revision = models.ForeignKey(
        "wagtailcore.Revision",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )

    def __str__(self):
        return (
            f"TranslationLog: {self.source_id}, {self.locale_id}, {self.revision_id} "
        )

    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)

get_instance()

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

Raises:

Type Description
DoesNotExist

if the translated object no longer exists.

Returns:

Type Description

The translated object.

Source code in wagtail_localize/models.py
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
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

Bases: Model

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.

Source code in wagtail_localize/models.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
class TranslationSource(models.Model):
    """
    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:
        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.
    """

    object = models.ForeignKey(
        TranslatableObject, on_delete=models.CASCADE, related_name="sources"
    )
    specific_content_type = models.ForeignKey(
        ContentType, on_delete=models.CASCADE, related_name="+"
    )
    locale = models.ForeignKey("wagtailcore.Locale", on_delete=models.CASCADE)
    object_repr = models.TextField(max_length=200)
    content_json = models.TextField()
    # The name of the last migration to be applied to the app that contains the specific_content_type model
    # This is used to provide a warning to a user when they are editing a translation that was submitted with
    # an older schema
    # Can be blank if the app has no migrations or the TranslationSource was submitted with an old version
    # of Wagtail Localize
    schema_version = models.CharField(max_length=255, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    last_updated_at = models.DateTimeField()

    objects = TranslationSourceQuerySet.as_manager()

    class Meta:
        unique_together = [
            ("object", "locale"),
        ]

    def __str__(self):
        return f"TranslationSource: {self.object_id}, {self.specific_content_type_id}, {self.locale}"

    @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),
                "last_updated_at": timezone.now(),
            },
        )
        source.refresh_segments()
        return source, created

    @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 and 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),
                "last_updated_at": timezone.now(),
            },
        )
        source.refresh_segments()
        return source, created

    @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)
        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()

    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
        )

    def get_source_instance_edit_url(self):
        """
        Returns the URL to edit the source instance.
        """
        return get_edit_url(self.get_source_instance())

    def get_translated_instance(self, locale):
        return self.specific_content_type.get_object_for_this_type(
            translation_key=self.object_id, locale_id=pk(locale)
        )

    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 as err:
            raise SourceDeletedError from err

        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

    @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()

    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

    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

    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-DraftStateMixin 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 models with DraftStateMixin can be saved as a draft
        if not publish and not isinstance(original, DraftStateMixin):
            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
                    new_revision = translation.save_revision(user=user)

                    self.sync_view_restrictions(original, translation)

                    if publish:
                        transaction.on_commit(new_revision.publish)

                elif isinstance(translation, DraftStateMixin):
                    # We copied another instance which may be live, so we make sure this one matches the desired state
                    translation.live = publish
                    translation.save()

                    # Create a new revision of the Snippet
                    new_revision = translation.save_revision(user=user)

                    if publish:
                        transaction.on_commit(new_revision.publish)

                elif isinstance(translation, RevisionMixin):
                    translation.save()

                    # Create a new revision of the Snippet
                    # - Models which are not subclasses of DraftStateMixin are not publishable
                    new_revision = translation.save_revision(user=user)

                else:
                    # Note: we don't need to run full_clean for DraftStateMixin objects or Pages as Wagtail does that in RevisionMixin.save_revision()
                    translation.full_clean()

                    translation.save()
                    new_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, revision=new_revision)

        return translation, created

    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

    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

    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()

    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)

as_instance()

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

Returns:

Name Type Description
Model

A model instance that has the content of this TranslationSource.

Raises:

Type Description
SourceDeletedError

if the source instance has been deleted.

Source code in wagtail_localize/models.py
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
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 as err:
        raise SourceDeletedError from err

    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(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

Raises:

Type Description
SourceDeletedError

if the source object has been deleted.

CannotSaveDraftError

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

MissingTranslationError

if a translation is missing and fallbackis not True.

MissingRelatedObjectError

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

Returns:

Name Type Description
Model

The translated instance.

Source code in wagtail_localize/models.py
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
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-DraftStateMixin 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 models with DraftStateMixin can be saved as a draft
    if not publish and not isinstance(original, DraftStateMixin):
        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
                new_revision = translation.save_revision(user=user)

                self.sync_view_restrictions(original, translation)

                if publish:
                    transaction.on_commit(new_revision.publish)

            elif isinstance(translation, DraftStateMixin):
                # We copied another instance which may be live, so we make sure this one matches the desired state
                translation.live = publish
                translation.save()

                # Create a new revision of the Snippet
                new_revision = translation.save_revision(user=user)

                if publish:
                    transaction.on_commit(new_revision.publish)

            elif isinstance(translation, RevisionMixin):
                translation.save()

                # Create a new revision of the Snippet
                # - Models which are not subclasses of DraftStateMixin are not publishable
                new_revision = translation.save_revision(user=user)

            else:
                # Note: we don't need to run full_clean for DraftStateMixin objects or Pages as Wagtail does that in RevisionMixin.save_revision()
                translation.full_clean()

                translation.save()
                new_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, revision=new_revision)

    return translation, created

export_po()

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
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
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(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

Raises:

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:

Name Type Description
Model

The translated instance with unsaved changes.

Source code in wagtail_localize/models.py
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
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
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
@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),
            "last_updated_at": timezone.now(),
        },
    )
    source.refresh_segments()
    return source, created

get_source_instance()

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:

Name Type Description
Model

The model instance that this TranslationSource was created from.

Raises:

Type Description
DoesNotExist

If the source instance has been deleted.

Source code in wagtail_localize/models.py
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
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()

Returns the URL to edit the source instance.

Source code in wagtail_localize/models.py
503
504
505
506
507
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()

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
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
@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()

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
909
910
911
912
913
914
915
916
917
918
919
920
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(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
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
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()

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

Raises:

Type Description
DoesNotExist

If the source instance has been deleted.

Source code in wagtail_localize/models.py
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
@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)
    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
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
@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 and 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),
            "last_updated_at": timezone.now(),
        },
    )
    source.refresh_segments()
    return source, created

update_target_view_restrictions(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
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
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)

cleanup_translation_on_delete(instance, **kwargs)

When either the source or destination object is deleted, remove the corresponding translation data.

When all translations of the object are removed, delete all remaining metadata.

Source code in wagtail_localize/models.py
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
def cleanup_translation_on_delete(instance, **kwargs):
    """
    When either the source or destination object is deleted, remove the corresponding translation data.

    When all translations of the object are removed, delete all remaining metadata.
    """
    translation_key = instance.translation_key
    locale_id = instance.locale_id

    StringTranslation.objects.filter(
        context__object_id=translation_key, locale=locale_id
    ).delete()
    SegmentOverride.objects.filter(
        context__object__translation_key=translation_key, locale=locale_id
    ).delete()
    Translation.objects.filter(source__object_id=translation_key).filter(
        # Remove the translations where this object was the source
        Q(source__locale_id=locale_id)
        # Remove the translations where this object was the destination
        | Q(target_locale_id=locale_id)
    ).delete()

    # There are no more translations for this key, so do the full cleanup.
    if not Translation.objects.filter(source__object_id=translation_key).exists():
        # Must be done separately because of `on_delete=models.Protect`
        for model in [
            OverridableSegment,
            RelatedObjectSegment,
            StringSegment,
            TemplateSegment,
        ]:
            model.objects.filter(context__object_id=translation_key).delete()

        for model in [SegmentOverride, StringTranslation]:
            model.objects.filter(
                context__object__translation_key=translation_key
            ).delete()

        # This will cascade to TranslationSource, TranslationLog, TranslationContext as well as any extracted segments.
        TranslatableObject.objects.filter(translation_key=translation_key).delete()

disable_translation_on_delete(instance, **kwargs)

When either a source or destination object is deleted, disable the translation record.

Source code in wagtail_localize/models.py
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
def disable_translation_on_delete(instance, **kwargs):
    """
    When either a source or destination object is deleted, disable the translation record.
    """
    Translation.objects.filter(
        source__object_id=instance.translation_key, enabled=True
    ).filter(
        # Disable translations where this object was the source
        Q(source__locale_id=instance.locale_id)
        # Disable translations where this object was the destination
        | Q(target_locale_id=instance.locale_id)
    ).update(enabled=False)

get_edit_url(instance)

Returns the URL of the given instance.

As there's no standard way to get this information from Wagtail, this only works with Pages and snippets at the moment.

Parameters:

Name Type Description Default
instance Model

A model instance to find the edit URL of.

required

Returns:

Name Type Description
str

The URL of the edit page of the given instance.

Source code in wagtail_localize/models.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def get_edit_url(instance):
    """
    Returns the URL of the given instance.

    As there's no standard way to get this information from Wagtail,
    this only works with Pages and snippets at the moment.

    Args:
        instance (Model): A model instance to find the edit URL of.

    Returns:
        str: The URL of the edit page of the given instance.
    """
    if isinstance(instance, Page):
        return reverse("wagtailadmin_pages:edit", args=[instance.id])

    elif instance._meta.model in get_snippet_models():
        return reverse(
            f"wagtailsnippets_{instance._meta.app_label}_{instance._meta.model_name}:edit",
            args=[quote(instance.pk)],
        )

    elif "wagtail_localize.modeladmin" in settings.INSTALLED_APPS:
        return reverse(
            f"{instance._meta.app_label}_{instance._meta.model_name}_modeladmin_edit",
            args=[quote(instance.pk)],
        )

get_schema_version(app_label: str) -> str

Returns the name of the last applied migration for the given app label.

Source code in wagtail_localize/models.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def get_schema_version(app_label: str) -> str:
    """
    Returns the name of the last applied migration for the given app label.
    """
    try:
        if migration := (
            MigrationRecorder.Migration.objects.filter(app=app_label)
            .order_by("applied")
            .last()
        ):
            return migration.name
    except OperationalError:
        ...

    return ""

pk(obj)

A helper that gets the primary key of a model instance if one is passed in. If not, this returns the parameter itself.

This allows functions to have parameters that accept either a primary key or model instance. For example:

def get_translations(target_locale):
    return Translation.objects.filter(target_locale=pk(target_locale))


# Both of these would be valid calls
get_translations(Locale.objects.get(id=1))
get_translations(1)

Parameters:

Name Type Description Default
obj Model | any

A model instance or primary key value.

required

Returns:

Name Type Description
any

The primary key of the model instance, or value of obj parameter.

Source code in wagtail_localize/models.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def pk(obj):
    """
    A helper that gets the primary key of a model instance if one is passed in.
    If not, this returns the parameter itself.

    This allows functions to have parameters that accept either a primary key
    or model instance. For example:

    ``` python
    def get_translations(target_locale):
        return Translation.objects.filter(target_locale=pk(target_locale))


    # Both of these would be valid calls
    get_translations(Locale.objects.get(id=1))
    get_translations(1)
    ```

    Args:
        obj (Model | any): A model instance or primary key value.

    Returns:
        any: The primary key of the model instance, or value of `obj` parameter.
    """
    if isinstance(obj, models.Model):
        return obj.pk
    else:
        return obj