Skip to content

Translatable Fields

This module provides the classes used to configure translatable fields and a couple of related utility functions

BaseTranslatableField

is_editable(self, obj)

Returns True if the field is editable on the given object

Source code in wagtail_localize/fields.py
def is_editable(self, obj):
    """
    Returns True if the field is editable on the given object
    """
    return True

is_overridable(self, obj)

Returns True if the value of this field can be overridden. This is only applicable to fields that are synchronized

Source code in wagtail_localize/fields.py
def is_overridable(self, obj):
    """
    Returns True if the value of this field can be overridden. This is only
    applicable to fields that are synchronized
    """
    return self.is_synchronized(obj)

is_synchronized(self, obj)

Returns True if the value of this field on the given object should be copied when translations are created/updated

Source code in wagtail_localize/fields.py
def is_synchronized(self, obj):
    """
    Returns True if the value of this field on the given object should be
    copied when translations are created/updated
    """
    return False

is_translated(self, obj)

Returns True if the value of this field on the given object should be extracted and submitted for translation

Source code in wagtail_localize/fields.py
def is_translated(self, obj):
    """
    Returns True if the value of this field on the given object should be
    extracted and submitted for translation
    """
    return False

SynchronizedField

A field that should always be kept in sync with the original page.

is_overridable(self, obj)

Returns True if the value of this field can be overridden. This is only applicable to fields that are synchronized

Source code in wagtail_localize/fields.py
def is_overridable(self, obj):
    return self.is_synchronized(obj) and self.overridable

is_synchronized(self, obj)

Returns True if the value of this field on the given object should be copied when translations are created/updated

Source code in wagtail_localize/fields.py
def is_synchronized(self, obj):
    return self.is_editable(obj)

TranslatableField

A field that should be translated whenever the original page changes

is_synchronized(self, obj)

Returns True if the value of this field on the given object should be copied when translations are created/updated

Source code in wagtail_localize/fields.py
def is_synchronized(self, obj):
    field = self.get_field(obj.__class__)

    # Child relations should all be synchronised before translation
    if isinstance(field, (models.ManyToOneRel)) and isinstance(field.remote_field, ParentalKey):
        return True

    # Streamfields need to be re-synchronised before translation so the structure and non-translatable content is copied over
    return isinstance(field, StreamField)

is_translated(self, obj)

Returns True if the value of this field on the given object should be extracted and submitted for translation

Source code in wagtail_localize/fields.py
def is_translated(self, obj):
    return True

copy_synchronised_fields(source, target)

Copies data in synchronised fields from the source instance to the target instance.

Note: Both instances must have the same model class

Parameters:

Name Type Description Default
source Model

The source instance to copy data from.

required
target Model

The target instance to copy data to.

required
Source code in wagtail_localize/fields.py
def copy_synchronised_fields(source, target):
    """
    Copies data in synchronised fields from the source instance to the target instance.

    Note: Both instances must have the same model class

    Arguments:
        source (Model): The source instance to copy data from.
        target (Model): The target instance to copy data to.
    """
    for translatable_field in get_translatable_fields(source.__class__):
        if translatable_field.is_synchronized(source):
            field = translatable_field.get_field(target.__class__)

            if isinstance(field, (models.ManyToOneRel)) and isinstance(field.remote_field, ParentalKey):
                # Use modelcluster's copy_child_relation for child relations

                if issubclass(field.related_model, TranslatableMixin):
                    # Get a list of the primary keys for the existing child objects
                    existing_pks_by_translation_key = {
                        child_object.translation_key: child_object.pk
                        for child_object in getattr(target, field.name).all()
                    }

                    # Copy the new child objects across (note this replaces existing ones)
                    child_object_map = source.copy_child_relation(field.name, target)

                    # Update locale of each child object and recycle PK
                    for (child_relation, source_pk), child_objects in child_object_map.items():
                        if source_pk is None:
                            # This is a list of the child objects that were never saved
                            for child_object in child_objects:
                                child_object.pk = existing_pks_by_translation_key.get(child_object.translation_key)
                                child_object.locale = target.locale
                        else:
                            child_object = child_objects

                            child_object.pk = existing_pks_by_translation_key.get(child_object.translation_key)
                            child_object.locale = target.locale

                else:
                    source.copy_child_relation(field.name, target)

            else:
                # For all other fields, just set the attribute
                setattr(
                    target, field.attname, getattr(source, field.attname)
                )

get_translatable_fields(model)

Derives a list of translatable fields from the given model class.

Parameters:

Name Type Description Default
model Model class

The model class to derive translatable fields from.

required

Returns:

Type Description
list[TranslatableField or SynchronizedField]

A list of TranslatableField and SynchronizedFields that were derived from the model.

Source code in wagtail_localize/fields.py
def get_translatable_fields(model):
    """
    Derives a list of translatable fields from the given model class.

    Arguments:
        model (Model class): The model class to derive translatable fields from.

    Returns:
        list[TranslatableField or SynchronizedField]: A list of TranslatableField and SynchronizedFields that were
            derived from the model.

    """
    # Note: If you update this, please update "docs/concept/translatable-fields-autogen.md"
    if hasattr(model, 'translatable_fields'):
        return model.translatable_fields

    translatable_fields = []

    for field in model._meta.get_fields():
        # Ignore automatically generated IDs
        if isinstance(field, models.AutoField):
            continue

        # Ignore non-editable fields
        if not field.editable:
            continue

        # Ignore many to many fields (not supported yet)
        # TODO: Add support for these
        if isinstance(field, models.ManyToManyField):
            continue

        # Ignore fields defined by MP_Node mixin
        if issubclass(model, MP_Node) and field.name in ['path', 'depth', 'numchild']:
            continue

        # Ignore some editable fields defined on Page
        if issubclass(model, Page) and field.name in ['go_live_at', 'expire_at', 'first_published_at', 'content_type', 'owner']:
            continue

        # URL, Email and choices fields are an exception to the rule below.
        # Text fields are translatable, but these are synchronised.
        if isinstance(field, (models.URLField, models.EmailField)) or isinstance(field, models.CharField) and field.choices:
            translatable_fields.append(SynchronizedField(field.name))

        # Translatable text fields should be translatable
        elif isinstance(field, (StreamField, RichTextField, models.TextField, models.CharField)):
            translatable_fields.append(TranslatableField(field.name))

        # Foreign keys to translatable models should be translated. Others should be synchronised
        elif isinstance(field, models.ForeignKey):
            # Ignore if this is a link to a parent model
            if isinstance(field, ParentalKey):
                continue

            # Ignore parent links
            if isinstance(field, models.OneToOneField) and field.remote_field.parent_link:
                continue

            # All FKs to translatable models should be translatable.
            # With the exception of pages that are special because we can localize them at runtime easily.
            # TODO: Perhaps we need a special type for pages where it links to the translation if availabe,
            # but falls back to the source if it isn't translated yet?
            # Note: This exact same decision was made for page chooser blocks in segments/extract.py
            if issubclass(field.related_model, TranslatableMixin) and not issubclass(field.related_model, Page):
                translatable_fields.append(TranslatableField(field.name))
            else:
                translatable_fields.append(SynchronizedField(field.name))

        # Fields that support extracting segments are translatable
        elif hasattr(field, "get_translatable_segments"):
            translatable_fields.append(TranslatableField(field.name))

        else:
            # Everything else is synchronised
            translatable_fields.append(SynchronizedField(field.name))

    # Add child relations for clusterable models
    if issubclass(model, ClusterableModel):
        for child_relation in get_all_child_relations(model):
            # Ignore comments
            if issubclass(model, Page) and child_relation.name == COMMENTS_RELATION_NAME:
                continue

            if issubclass(child_relation.related_model, TranslatableMixin):
                translatable_fields.append(TranslatableField(child_relation.name))
            else:
                translatable_fields.append(SynchronizedField(child_relation.name))

    # Combine with any overrides defined on the model
    override_translatable_fields = getattr(model, 'override_translatable_fields', [])

    if override_translatable_fields:
        override_translatable_fields = {
            field.field_name: field
            for field in override_translatable_fields
        }

        combined_translatable_fields = []
        for field in translatable_fields:
            if field.field_name in override_translatable_fields:
                combined_translatable_fields.append(override_translatable_fields.pop(field.field_name))
            else:
                combined_translatable_fields.append(field)

        if override_translatable_fields:
            combined_translatable_fields.extend(override_translatable_fields.values())

        return combined_translatable_fields

    else:
        return translatable_fields