Translation management models
These models are responsible for managing the translation of objects.
TranslatableObject
A TranslatableObject represents a set of instances of a translatable model that are all translations of each another.
In Wagtail, objects are considered translations of each other when they are
of the same content type and have the same translation_key
value.
Attributes:
Name | Type | Description |
---|---|---|
translation_key |
UUIDField |
The translation_key that value that is used by the instances. |
content_type |
ForeignKey to ContentType |
Link to the base Django content type representing the model that the
instances use. Note that this field refers to the model that has the |
get_instance(self, locale)
Returns a model instance for this object in the given locale.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
locale |
Locale | int |
Either a Locale object or an ID of a Locale. |
required |
Returns:
Type | Description |
---|---|
Model |
The model instance. |
Exceptions:
Type | Description |
---|---|
Model.DoesNotExist |
If there is not an instance of this object in the given locale. |
Source code in wagtail_localize/models.py
def get_instance(self, locale):
"""
Returns a model instance for this object in the given locale.
Args:
locale (Locale | int): Either a Locale object or an ID of a Locale.
Returns:
Model: The model instance.
Raises:
Model.DoesNotExist: If there is not an instance of this object in the given locale.
"""
return self.content_type.get_object_for_this_type(
translation_key=self.translation_key, locale_id=pk(locale)
)
get_instance_or_none(self, locale)
Returns a model instance for this object in the given locale.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
locale |
Locale | int |
Either a Locale object or an ID of a Locale. |
required |
Returns:
Type | Description |
---|---|
Model |
The model instance if one exists. None: If the model doesn't exist. |
Source code in wagtail_localize/models.py
def get_instance_or_none(self, locale):
"""
Returns a model instance for this object in the given locale.
Args:
locale (Locale | int): Either a Locale object or an ID of a Locale.
Returns:
Model: The model instance if one exists.
None: If the model doesn't exist.
"""
try:
return self.get_instance(locale)
except self.content_type.model_class().DoesNotExist:
pass
has_translation(self, locale)
Returns True if there is an instance of this object in the given Locale.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
locale |
Locale | int |
Either a Locale object or an ID of a Locale. |
required |
Returns:
Type | Description |
---|---|
bool |
True if there is an instance of this object in the given locale. |
Source code in wagtail_localize/models.py
def has_translation(self, locale):
"""
Returns True if there is an instance of this object in the given Locale.
Args:
locale (Locale | int): Either a Locale object or an ID of a Locale.
Returns:
bool: True if there is an instance of this object in the given locale.
"""
return self.content_type.get_all_objects_for_this_type(
translation_key=self.translation_key, locale_id=pk(locale)
).exists()
Translation
Manages the translation of an object into a locale.
An instance of this model is created whenever an object is submitted for translation into a new language.
They can be disabled at any time, and are deleted or disabled automatically if either the source or destination object is deleted.
If the translation of a page is disabled, the page editor of the translation would return to the normal Wagtail editor.
Attributes:
Name | Type | Description |
---|---|---|
uuid |
UUIDField |
A unique ID for this translation used for referencing it from external systems. |
source |
ForeignKey to TranslationSource |
The source that is being translated. |
target_locale |
ForeignKey to Locale |
The Locale that the source is being translated into. |
created_at |
DateTimeField |
The date/time the translation was started. |
translations_last_updated_at |
DateTimeField |
The date/time of when a translated string was last updated. |
destination_last_updated_at |
DateTimeField |
The date/time of when the destination object was last updated. |
enabled |
boolean |
Whether this translation is enabled or not. |
export_po(self)
Exports all translatable strings with any translations that have already been made.
Returns:
Type | Description |
---|---|
polib.POFile |
A POFile object containing the source translatable strings and any translations. |
Source code in wagtail_localize/models.py
def export_po(self):
"""
Exports all translatable strings with any translations that have already been made.
Returns:
polib.POFile: A POFile object containing the source translatable strings and any translations.
"""
# Get messages
messages = []
string_segments = (
StringSegment.objects.filter(source=self.source)
.order_by("order")
.select_related("context", "string")
.annotate_translation(self.target_locale, include_errors=True)
)
for string_segment in string_segments:
messages.append(
(
string_segment.string.data,
string_segment.context.path,
string_segment.translation,
)
)
# Build a PO file
po = polib.POFile(wrapwidth=200)
po.metadata = {
"POT-Creation-Date": str(timezone.now()),
"MIME-Version": "1.0",
"Content-Type": "text/plain; charset=utf-8",
"X-WagtailLocalize-TranslationID": str(self.uuid),
}
for text, context, translation in messages:
po.append(
polib.POEntry(
msgid=text,
msgctxt=context,
msgstr=translation or "",
)
)
# Add any obsolete segments that have translations for future reference
# We find this by looking for obsolete contexts and annotate the latest
# translation for each one. Contexts that were never translated are
# excluded
for translation in (
StringTranslation.objects.filter(
context__object_id=self.source.object_id, locale=self.target_locale
)
.exclude(
translation_of_id__in=StringSegment.objects.filter(
source=self.source
).values_list("string_id", flat=True)
)
.select_related("translation_of", "context")
.iterator()
):
po.append(
polib.POEntry(
msgid=translation.translation_of.data,
msgstr=translation.data or "",
msgctxt=translation.context.path,
obsolete=True,
)
)
return po
get_progress(self)
Gets the current translation progress.
Returns tuple[int, int]: A two-tuple of integers. First integer is the total number of string segments to be translated. The second integer is the number of string segments that have been translated so far.
Source code in wagtail_localize/models.py
def get_progress(self):
"""
Gets the current translation progress.
Returns
tuple[int, int]: A two-tuple of integers. First integer is the total number of string segments to be translated.
The second integer is the number of string segments that have been translated so far.
"""
# Get QuerySet of Segments that need to be translated
required_segments = StringSegment.objects.filter(source_id=self.source_id)
# Annotate each Segment with a flag that indicates whether the segment is translated
# into the locale
required_segments = required_segments.annotate(
is_translated=Exists(
StringTranslation.objects.filter(
translation_of_id=OuterRef("string_id"),
context_id=OuterRef("context_id"),
locale_id=self.target_locale_id,
has_error=False,
)
)
)
# Count the total number of segments and the number of translated segments
aggs = required_segments.annotate(
is_translated_i=Case(
When(is_translated=True, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
)
).aggregate(
total_segments=Count("pk"), translated_segments=Sum("is_translated_i")
)
return aggs["total_segments"], aggs["translated_segments"]
get_status_display(self)
Returns a string to describe the current status of this translation to a user.
Returns:
Type | Description |
---|---|
str |
The status of this translation |
Source code in wagtail_localize/models.py
def get_status_display(self):
"""
Returns a string to describe the current status of this translation to a user.
Returns:
str: The status of this translation
"""
total_segments, translated_segments = self.get_progress()
if total_segments == translated_segments:
return _("Up to date")
else:
return _("Waiting for translations")
get_target_instance(self)
Fetches the translated instance from the database.
Exceptions:
Type | Description |
---|---|
Model.DoesNotExist |
if the translation does not exist. |
Returns:
Type | Description |
---|---|
Model |
The translated instance. |
Source code in wagtail_localize/models.py
def get_target_instance(self):
"""
Fetches the translated instance from the database.
Raises:
Model.DoesNotExist: if the translation does not exist.
Returns:
Model: The translated instance.
"""
return self.source.get_translated_instance(self.target_locale)
get_target_instance_edit_url(self)
Returns the URL to edit the target instance.
Exceptions:
Type | Description |
---|---|
Model.DoesNotExist |
if the translation does not exist. |
Returns:
Type | Description |
---|---|
str |
The URL of the edit view of the target instance. |
Source code in wagtail_localize/models.py
def get_target_instance_edit_url(self):
"""
Returns the URL to edit the target instance.
Raises:
Model.DoesNotExist: if the translation does not exist.
Returns:
str: The URL of the edit view of the target instance.
"""
return get_edit_url(self.get_target_instance())
import_po(self, po, delete=False, user=None, translation_type='manual', tool_name='')
Imports all translatable strings with any translations that have already been made.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
po |
polib.POFile |
A POFile object containing the source translatable strings and any translations. |
required |
delete |
boolean |
Set to True to delete any translations that do not appear in the PO file. |
False |
user |
User |
The user who is performing this operation. Used for logging purposes. |
None |
translation_type |
'manual' or 'machine' |
Whether the translationw as performed by a human or machine. Defaults to 'manual'. |
'manual' |
tool_name |
string |
The name of the tool that was used to perform the translation. Defaults to ''. |
'' |
Returns:
Type | Description |
---|---|
list[POImportWarning] |
A list of POImportWarning objects representing any non-fatal issues that were encountered while importing the PO file. |
Source code in wagtail_localize/models.py
@transaction.atomic
def import_po(
self, po, delete=False, user=None, translation_type="manual", tool_name=""
):
"""
Imports all translatable strings with any translations that have already been made.
Args:
po (polib.POFile): A POFile object containing the source translatable strings and any translations.
delete (boolean, optional): Set to True to delete any translations that do not appear in the PO file.
user (User, optional): The user who is performing this operation. Used for logging purposes.
translation_type ('manual' or 'machine', optional): Whether the translationw as performed by a human or machine. Defaults to 'manual'.
tool_name (string, optional): The name of the tool that was used to perform the translation. Defaults to ''.
Returns:
list[POImportWarning]: A list of POImportWarning objects representing any non-fatal issues that were
encountered while importing the PO file.
"""
seen_translation_ids = set()
warnings = []
if "X-WagtailLocalize-TranslationID" in po.metadata and po.metadata[
"X-WagtailLocalize-TranslationID"
] != str(self.uuid):
return []
for index, entry in enumerate(po):
try:
string = String.objects.get(
locale_id=self.source.locale_id, data=entry.msgid
)
context = TranslationContext.objects.get(
object_id=self.source.object_id, path=entry.msgctxt
)
# Ignore blank strings
if not entry.msgstr:
continue
# Ignore if the string doesn't appear in this context, and if there is not an obsolete StringTranslation
if (
not StringSegment.objects.filter(
string=string, context=context
).exists()
and not StringTranslation.objects.filter(
translation_of=string, context=context
).exists()
):
warnings.append(
StringNotUsedInContext(index, entry.msgid, entry.msgctxt)
)
continue
string_translation, created = string.translations.get_or_create(
locale_id=self.target_locale_id,
context=context,
defaults={
"data": entry.msgstr,
"updated_at": timezone.now(),
"translation_type": translation_type,
"tool_name": tool_name,
"last_translated_by": user,
"has_error": False,
"field_error": "",
},
)
seen_translation_ids.add(string_translation.id)
if not created:
# Update the string_translation only if it has changed
if string_translation.data != entry.msgstr:
string_translation.data = entry.msgstr
string_translation.translation_type = translation_type
string_translation.tool_name = tool_name
string_translation.last_translated_by = user
string_translation.updated_at = timezone.now()
string_translation.has_error = False # reset the error flag.
string_translation.save()
except TranslationContext.DoesNotExist:
warnings.append(UnknownContext(index, entry.msgctxt))
except String.DoesNotExist:
warnings.append(UnknownString(index, entry.msgid))
# Delete any translations that weren't mentioned
if delete:
StringTranslation.objects.filter(
context__object_id=self.source.object_id, locale=self.target_locale
).exclude(id__in=seen_translation_ids).delete()
return warnings
save_target(self, user=None, publish=True)
Saves the target page/snippet using the current translations.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
user |
User |
The user that is performing this action. Used for logging purposes. |
None |
publish |
boolean |
Set this to False to save a draft of the translation. Pages only. |
True |
Exceptions:
Type | Description |
---|---|
SourceDeletedError |
if the source object has been deleted. |
CannotSaveDraftError |
if the |
MissingTranslationError |
if a translation is missing and |
MissingRelatedObjectError |
if a related object is not translated and |
Returns:
Type | Description |
---|---|
Model |
The translated instance. |
Source code in wagtail_localize/models.py
def save_target(self, user=None, publish=True):
"""
Saves the target page/snippet using the current translations.
Args:
user (User, optional): The user that is performing this action. Used for logging purposes.
publish (boolean, optional): Set this to False to save a draft of the translation. Pages only.
Raises:
SourceDeletedError: if the source object has been deleted.
CannotSaveDraftError: if the `publish` parameter was set to `False` when translating a non-page object.
MissingTranslationError: if a translation is missing and `fallback `is not `True`.
MissingRelatedObjectError: if a related object is not translated and `fallback `is not `True`.
Returns:
Model: The translated instance.
"""
self.source.create_or_update_translation(
self.target_locale,
user=user,
publish=publish,
fallback=True,
copy_parent_pages=True,
)
TranslationLog
Keeps Track of when translations are created/updated.
Attributes:
Name | Type | Description |
---|---|---|
source |
ForeignKey to TranslationSource |
The source that was used for translation. |
locale |
ForeignKey to Locale |
The Locale that the source was translated into. |
created_at |
DateTimeField |
The date/time the translation was done. |
page_revision |
ForeignKey to PageRevision |
If the translation was of a page, this links to the PageRevision that was created |
get_instance(self)
Gets the instance of the translated object, if it still exists.
Exceptions:
Type | Description |
---|---|
Model.DoesNotExist |
if the translated object no longer exists. |
Returns:
Type | Description |
---|---|
|
The translated object. |
Source code in wagtail_localize/models.py
def get_instance(self):
"""
Gets the instance of the translated object, if it still exists.
Raises:
Model.DoesNotExist: if the translated object no longer exists.
Returns:
The translated object.
"""
return self.source.object.get_instance(self.locale)
TranslationSource
Frozen source content that is to be translated.
This is like a page revision, except it can be created for any model and it's only created/updated when a user submits something for translation.
Attributes:
Name | Type | Description |
---|---|---|
object |
ForeignKey to TranslatableObject |
The object that this is a source for |
specific_content_type |
ForeignKey to ContentType |
The specific content type that this was extracted from.
Note that |
locale |
ForeignKey to Locale |
The Locale of the instance that this source content was extracted from. |
object_repr |
TextField |
A string representing the name of the source object. Used in the UI. |
content_json |
TextField with JSON contents |
The serialized source content. Note that this is serialzed in the same way that Wagtail serializes page revisions. |
created_at |
DateTimeField |
The date/time at which the content was first extracted from this source. |
last_updated_at |
DateTimeField |
The date/time at which the content was last extracted from this source. |
_get_segments_for_translation(self, locale, fallback=False)
private
Returns a list of segments that can be passed into "ingest_segments" to translate an object.
Source code in wagtail_localize/models.py
def _get_segments_for_translation(self, locale, fallback=False):
"""
Returns a list of segments that can be passed into "ingest_segments" to translate an object.
"""
string_segments = (
StringSegment.objects.filter(source=self)
.annotate_translation(locale)
.select_related("context", "string")
)
template_segments = (
TemplateSegment.objects.filter(source=self)
.select_related("template")
.select_related("context")
)
related_object_segments = (
RelatedObjectSegment.objects.filter(source=self)
.select_related("object")
.select_related("context")
)
overridable_segments = (
OverridableSegment.objects.filter(source=self)
.annotate_override_json(locale)
.filter(override_json__isnull=False)
.select_related("context")
)
segments = []
for string_segment in string_segments:
if string_segment.translation:
string = StringValue(string_segment.translation)
elif fallback:
string = StringValue(string_segment.string.data)
else:
raise MissingTranslationError(string_segment, locale)
segment_value = StringSegmentValue(
string_segment.context.path,
string,
attrs=json.loads(string_segment.attrs),
).with_order(string_segment.order)
segments.append(segment_value)
for template_segment in template_segments:
template = template_segment.template
segment_value = TemplateSegmentValue(
template_segment.context.path,
template.template_format,
template.template,
template.string_count,
order=template_segment.order,
)
segments.append(segment_value)
for related_object_segment in related_object_segments:
if related_object_segment.object.has_translation(locale):
segment_value = RelatedObjectSegmentValue(
related_object_segment.context.path,
related_object_segment.object.content_type,
related_object_segment.object.translation_key,
order=related_object_segment.order,
)
segments.append(segment_value)
elif fallback:
# Skip this segment, this will reuse what is already in the database
continue
else:
raise MissingRelatedObjectError(related_object_segment, locale)
for overridable_segment in overridable_segments:
segment_value = OverridableSegmentValue(
overridable_segment.context.path,
json.loads(overridable_segment.override_json),
order=overridable_segment.order,
)
segments.append(segment_value)
return segments
as_instance(self)
Builds an instance of the object with the content of this source.
Returns:
Type | Description |
---|---|
Model |
A model instance that has the content of this TranslationSource. |
Exceptions:
Type | Description |
---|---|
SourceDeletedError |
if the source instance has been deleted. |
Source code in wagtail_localize/models.py
def as_instance(self):
"""
Builds an instance of the object with the content of this source.
Returns:
Model: A model instance that has the content of this TranslationSource.
Raises:
SourceDeletedError: if the source instance has been deleted.
"""
try:
instance = self.get_source_instance()
except models.ObjectDoesNotExist:
raise SourceDeletedError
if isinstance(instance, Page):
# see https://github.com/wagtail/wagtail/pull/8024
content_json = json.loads(self.content_json)
return instance.with_content_json(content_json)
elif isinstance(instance, ClusterableModel):
new_instance = instance.__class__.from_json(self.content_json)
else:
new_instance = model_from_serializable_data(
instance.__class__, json.loads(self.content_json)
)
new_instance.pk = instance.pk
new_instance.locale = instance.locale
new_instance.translation_key = instance.translation_key
return new_instance
create_or_update_translation(self, locale, user=None, publish=True, copy_parent_pages=False, fallback=False)
Creates/updates a translation of the object into the specified locale based on the content of this source and the translated strings currently in translation memory.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
locale |
Locale |
The target locale to generate the translation for. |
required |
user |
User |
The user who is carrying out this operation. For logging purposes |
None |
publish |
boolean |
Set this to False to save a draft of the translation. Pages only. |
True |
copy_parent_pages |
boolean |
Set this to True to make copies of the parent pages if they are not yet translated. |
False |
fallback |
boolean |
Set this to True to fallback to source strings/related objects if they are not yet translated. By default, this will raise an error if anything is missing. |
False |
Exceptions:
Type | Description |
---|---|
SourceDeletedError |
if the source object has been deleted. |
CannotSaveDraftError |
if the |
MissingTranslationError |
if a translation is missing and |
MissingRelatedObjectError |
if a related object is not translated and |
Returns:
Type | Description |
---|---|
Model |
The translated instance. |
Source code in wagtail_localize/models.py
def create_or_update_translation(
self, locale, user=None, publish=True, copy_parent_pages=False, fallback=False
):
"""
Creates/updates a translation of the object into the specified locale
based on the content of this source and the translated strings
currently in translation memory.
Args:
locale (Locale): The target locale to generate the translation for.
user (User, optional): The user who is carrying out this operation. For logging purposes
publish (boolean, optional): Set this to False to save a draft of the translation. Pages only.
copy_parent_pages (boolean, optional): Set this to True to make copies of the parent pages if they are not
yet translated.
fallback (boolean, optional): Set this to True to fallback to source strings/related objects if they are
not yet translated. By default, this will raise an error if anything is missing.
Raises:
SourceDeletedError: if the source object has been deleted.
CannotSaveDraftError: if the `publish` parameter was set to `False` when translating a non-page object.
MissingTranslationError: if a translation is missing and `fallback `is not `True`.
MissingRelatedObjectError: if a related object is not translated and `fallback `is not `True`.
Returns:
Model: The translated instance.
"""
original = self.as_instance()
created = False
# Only pages can be saved as draft
if not publish and not isinstance(original, Page):
raise CannotSaveDraftError
try:
translation = self.get_translated_instance(locale)
except models.ObjectDoesNotExist:
if isinstance(original, Page):
translation = original.copy_for_translation(
locale, copy_parents=copy_parent_pages
)
else:
translation = original.copy_for_translation(locale)
created = True
copy_synchronised_fields(original, translation)
segments = self._get_segments_for_translation(locale, fallback=fallback)
try:
with transaction.atomic():
# Ingest all translated segments
ingest_segments(original, translation, self.locale, locale, segments)
if isinstance(translation, Page):
# If the page is an alias, convert it into a regular page
if translation.alias_of_id:
translation.alias_of_id = None
translation.save(update_fields=["alias_of_id"], clean=False)
# Create initial revision
revision = translation.save_revision(
user=user, changed=False, clean=False
)
# Log the alias conversion
PageLogEntry.objects.log_action(
instance=translation,
revision=revision,
action="wagtail.convert_alias",
user=user,
data={
"page": {
"id": translation.id,
"title": translation.get_admin_display_title(),
},
},
)
# Make sure the slug is valid
translation.slug = find_available_slug(
translation.get_parent(),
slugify(translation.slug),
ignore_page_id=translation.id,
)
translation.save()
# Create a new revision
page_revision = translation.save_revision(user=user)
self.sync_view_restrictions(original, translation)
if publish:
page_revision.publish()
else:
# Note: we don't need to run full_clean for Pages as Wagtail does that in Page.save()
translation.full_clean()
translation.save()
page_revision = None
except ValidationError as e:
# If the validation error's field matches the context of a translation,
# set that error message on that translation.
# TODO (someday): Add support for errors raised from streamfield
for field_name, errors in e.error_dict.items():
try:
context = TranslationContext.objects.get(
object=self.object, path=field_name
)
except TranslationContext.DoesNotExist:
# TODO (someday): How would we handle validation errors for non-translatable fields?
continue
# Check for string translation
try:
string_translation = StringTranslation.objects.get(
translation_of_id__in=StringSegment.objects.filter(
source=self
).values_list("string_id", flat=True),
context=context,
locale=locale,
)
string_translation.set_field_error(errors)
except StringTranslation.DoesNotExist:
pass
# Check for segment override
try:
segment_override = SegmentOverride.objects.get(
context=context,
locale=locale,
)
segment_override.set_field_error(errors)
except SegmentOverride.DoesNotExist:
pass
raise
# Log that the translation was made
TranslationLog.objects.create(
source=self, locale=locale, page_revision=page_revision
)
return translation, created
export_po(self)
Exports all translatable strings from this source.
Note that because there is no target locale, all msgstr
fields will be blank.
Returns:
Type | Description |
---|---|
polib.POFile |
A POFile object containing the source translatable strings. |
Source code in wagtail_localize/models.py
def export_po(self):
"""
Exports all translatable strings from this source.
Note that because there is no target locale, all `msgstr` fields will be blank.
Returns:
polib.POFile: A POFile object containing the source translatable strings.
"""
# Get messages
messages = []
for string_segment in (
StringSegment.objects.filter(source=self)
.order_by("order")
.select_related("context", "string")
):
messages.append((string_segment.string.data, string_segment.context.path))
# Build a PO file
po = polib.POFile(wrapwidth=200)
po.metadata = {
"POT-Creation-Date": str(timezone.now()),
"MIME-Version": "1.0",
"Content-Type": "text/plain; charset=utf-8",
}
for text, context in messages:
po.append(
polib.POEntry(
msgid=text,
msgctxt=context,
msgstr="",
)
)
return po
get_ephemeral_translated_instance(self, locale, fallback=False)
Returns an instance with the translations added which is not intended to be saved.
This is used for previewing pages with draft translations applied.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
locale |
Locale |
The target locale to generate the ephemeral translation for. |
required |
fallback |
boolean |
Set this to True to fallback to source strings/related objects if they are not yet translated. By default, this will raise an error if anything is missing. |
False |
Exceptions:
Type | Description |
---|---|
SourceDeletedError |
if the source object has been deleted. |
MissingTranslationError |
if a translation is missing and |
MissingRelatedObjectError |
if a related object is not translated and |
Returns:
Type | Description |
---|---|
Model |
The translated instance with unsaved changes. |
Source code in wagtail_localize/models.py
def get_ephemeral_translated_instance(self, locale, fallback=False):
"""
Returns an instance with the translations added which is not intended to be saved.
This is used for previewing pages with draft translations applied.
Args:
locale (Locale): The target locale to generate the ephemeral translation for.
fallback (boolean): Set this to True to fallback to source strings/related objects if they are not yet
translated. By default, this will raise an error if anything is missing.
Raises:
SourceDeletedError: if the source object has been deleted.
MissingTranslationError: if a translation is missing and `fallback `is not `True`.
MissingRelatedObjectError: if a related object is not translated and `fallback `is not `True`.
Returns:
Model: The translated instance with unsaved changes.
"""
original = self.as_instance()
translation = self.get_translated_instance(locale)
copy_synchronised_fields(original, translation)
segments = self._get_segments_for_translation(locale, fallback=fallback)
# Ingest all translated segments
ingest_segments(original, translation, self.locale, locale, segments)
return translation
get_or_create_from_instance(instance)
classmethod
Creates or gets a TranslationSource for the given instance.
This extracts the content from the given instance. Then stores it in a new TranslationSource instance if one doesn't already exist. If one does already exist, it returns the existing TranslationSource without changing it.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
instance |
Model that inherits TranslatableMixin |
A Translatable model instance to find a TranslationSource instance for. |
required |
Returns:
Type | Description |
---|---|
tuple[TranslationSource, boolean] |
A two-tuple, the first component is the TranslationSource object, and the second component is a boolean that is True if the TranslationSource was created. |
Source code in wagtail_localize/models.py
@classmethod
def get_or_create_from_instance(cls, instance):
"""
Creates or gets a TranslationSource for the given instance.
This extracts the content from the given instance. Then stores it in a new TranslationSource instance if one
doesn't already exist. If one does already exist, it returns the existing TranslationSource without changing
it.
Args:
instance (Model that inherits TranslatableMixin): A Translatable model instance to find a TranslationSource
instance for.
Returns:
tuple[TranslationSource, boolean]: A two-tuple, the first component is the TranslationSource object, and
the second component is a boolean that is True if the TranslationSource was created.
"""
# Make sure we're using the specific version of pages
if isinstance(instance, Page):
instance = instance.specific
object, created = TranslatableObject.objects.get_or_create_from_instance(
instance
)
try:
return (
TranslationSource.objects.get(
object_id=object.translation_key, locale_id=instance.locale_id
),
False,
)
except TranslationSource.DoesNotExist:
pass
if isinstance(instance, ClusterableModel):
content_json = instance.to_json()
else:
serializable_data = get_serializable_data_for_fields(instance)
content_json = json.dumps(serializable_data, cls=DjangoJSONEncoder)
source, created = cls.objects.update_or_create(
object=object,
locale=instance.locale,
# You can't update the content type of a source. So if this happens,
# it'll try and create a new source and crash (can't have more than
# one source per object/locale)
specific_content_type=ContentType.objects.get_for_model(instance.__class__),
defaults={
"locale": instance.locale,
"object_repr": str(instance)[:200],
"content_json": content_json,
"schema_version": get_schema_version(instance._meta.app_label) or "",
"last_updated_at": timezone.now(),
},
)
source.refresh_segments()
return source, created
get_source_instance(self)
This gets the live version of instance that the source data was extracted from.
This is different to source.object.get_instance(source.locale) as the instance
returned by this methid will have the same model that the content was extracted
from. The model returned by object.get_instance
might be more generic since
that model only records the model that the TranslatableMixin was applied to but
that model might have child models.
Returns:
Type | Description |
---|---|
Model |
The model instance that this TranslationSource was created from. |
Exceptions:
Type | Description |
---|---|
Model.DoesNotExist |
If the source instance has been deleted. |
Source code in wagtail_localize/models.py
def get_source_instance(self):
"""
This gets the live version of instance that the source data was extracted from.
This is different to source.object.get_instance(source.locale) as the instance
returned by this methid will have the same model that the content was extracted
from. The model returned by `object.get_instance` might be more generic since
that model only records the model that the TranslatableMixin was applied to but
that model might have child models.
Returns:
Model: The model instance that this TranslationSource was created from.
Raises:
Model.DoesNotExist: If the source instance has been deleted.
"""
return self.specific_content_type.get_object_for_this_type(
translation_key=self.object_id, locale_id=self.locale_id
)
get_source_instance_edit_url(self)
Returns the URL to edit the source instance.
Source code in wagtail_localize/models.py
def get_source_instance_edit_url(self):
"""
Returns the URL to edit the source instance.
"""
return get_edit_url(self.get_source_instance())
refresh_segments(self)
Updates the *Segment models to reflect the latest version of the source.
This is called by from_instance
so you don't usually need to call this manually.
Source code in wagtail_localize/models.py
@transaction.atomic
def refresh_segments(self):
"""
Updates the *Segment models to reflect the latest version of the source.
This is called by `from_instance` so you don't usually need to call this manually.
"""
seen_string_segment_ids = []
seen_template_segment_ids = []
seen_related_object_segment_ids = []
seen_overridable_segment_ids = []
instance = self.as_instance()
for segment in extract_segments(instance):
if isinstance(segment, TemplateSegmentValue):
segment_obj = TemplateSegment.from_value(self, segment)
seen_template_segment_ids.append(segment_obj.id)
elif isinstance(segment, RelatedObjectSegmentValue):
segment_obj = RelatedObjectSegment.from_value(self, segment)
seen_related_object_segment_ids.append(segment_obj.id)
elif isinstance(segment, OverridableSegmentValue):
segment_obj = OverridableSegment.from_value(self, segment)
seen_overridable_segment_ids.append(segment_obj.id)
else:
segment_obj = StringSegment.from_value(self, self.locale, segment)
seen_string_segment_ids.append(segment_obj.id)
# Make sure the segment's field_path is pre-populated
segment_obj.context.get_field_path(instance)
# Delete any segments that weren't mentioned
self.stringsegment_set.exclude(id__in=seen_string_segment_ids).delete()
self.templatesegment_set.exclude(id__in=seen_template_segment_ids).delete()
self.relatedobjectsegment_set.exclude(
id__in=seen_related_object_segment_ids
).delete()
self.overridablesegment_set.exclude(
id__in=seen_overridable_segment_ids
).delete()
schema_out_of_date(self)
Returns True if the app that contains the model this source was generated from has been updated since the source was last updated.
Source code in wagtail_localize/models.py
def schema_out_of_date(self):
"""
Returns True if the app that contains the model this source was generated from
has been updated since the source was last updated.
"""
if not self.schema_version:
return False
current_schema_version = get_schema_version(
self.specific_content_type.app_label
)
return self.schema_version != current_schema_version
sync_view_restrictions(self, original, translation_page)
Synchronizes view restriction object for the translated page
Parameters:
Name | Type | Description | Default |
---|---|---|---|
original |
Page|Snippet |
The original instance. |
required |
translation_page |
Page|Snippet |
The translated instance. |
required |
Source code in wagtail_localize/models.py
def sync_view_restrictions(self, original, translation_page):
"""
Synchronizes view restriction object for the translated page
Args:
original (Page|Snippet): The original instance.
translation_page (Page|Snippet): The translated instance.
"""
if not isinstance(original, Page) or not isinstance(translation_page, Page):
raise NoViewRestrictionsError
if original.view_restrictions.exists():
original_restriction = original.view_restrictions.first()
if not translation_page.view_restrictions.exists():
view_restriction, child_object_map = _copy(
original_restriction,
exclude_fields=["id"],
update_attrs={"page": translation_page},
)
view_restriction.save()
else:
# if both exist, sync them
translation_restriction = translation_page.view_restrictions.first()
should_save = False
if (
translation_restriction.restriction_type
!= original_restriction.restriction_type
):
translation_restriction.restriction_type = (
original_restriction.restriction_type
)
should_save = True
if translation_restriction.password != original_restriction.password:
translation_restriction.password = original_restriction.password
should_save = True
if list(
original_restriction.groups.values_list("pk", flat=True)
) != list(translation_restriction.groups.values_list("pk", flat=True)):
translation_restriction.groups.set(
original_restriction.groups.all()
)
if should_save:
translation_restriction.save()
elif translation_page.view_restrictions.exists():
# the original no longer has the restriction, so drop it
translation_page.view_restrictions.all().delete()
update_from_db(self)
Retrieves the source instance from the database and updates this TranslationSource with its current contents.
Exceptions:
Type | Description |
---|---|
Model.DoesNotExist |
If the source instance has been deleted. |
Source code in wagtail_localize/models.py
@transaction.atomic
def update_from_db(self):
"""
Retrieves the source instance from the database and updates this TranslationSource
with its current contents.
Raises:
Model.DoesNotExist: If the source instance has been deleted.
"""
instance = self.get_source_instance()
if isinstance(instance, ClusterableModel):
self.content_json = instance.to_json()
else:
serializable_data = get_serializable_data_for_fields(instance)
self.content_json = json.dumps(serializable_data, cls=DjangoJSONEncoder)
self.schema_version = get_schema_version(instance._meta.app_label) or ""
self.object_repr = str(instance)[:200]
self.last_updated_at = timezone.now()
self.save(
update_fields=[
"content_json",
"schema_version",
"object_repr",
"last_updated_at",
]
)
self.refresh_segments()
update_or_create_from_instance(instance)
classmethod
Creates or updates a TranslationSource for the given instance.
This extracts the content from the given instance. Then stores it in a new TranslationSource instance if one doesn't already exist. If one does already exist, it updates the existing TranslationSource.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
instance |
Model that inherits TranslatableMixin |
A Translatable model instance to extract source content from. |
required |
Returns:
Type | Description |
---|---|
tuple[TranslationSource, boolean] |
A two-tuple, the first component is the TranslationSource object, and the second component is a boolean that is True if the TranslationSource was created. |
Source code in wagtail_localize/models.py
@classmethod
def update_or_create_from_instance(cls, instance):
"""
Creates or updates a TranslationSource for the given instance.
This extracts the content from the given instance. Then stores it in a new TranslationSource instance if one
doesn't already exist. If one does already exist, it updates the existing TranslationSource.
Args:
instance (Model that inherits TranslatableMixin): A Translatable model instance to extract source content
from.
Returns:
tuple[TranslationSource, boolean]: A two-tuple, the first component is the TranslationSource object, and
the second component is a boolean that is True if the TranslationSource was created.
"""
# Make sure we're using the specific version of pages
if isinstance(instance, Page):
instance = instance.specific
object, created = TranslatableObject.objects.get_or_create_from_instance(
instance
)
if isinstance(instance, ClusterableModel):
content_json = instance.to_json()
else:
serializable_data = get_serializable_data_for_fields(instance)
content_json = json.dumps(serializable_data, cls=DjangoJSONEncoder)
# Check if the instance has changed since the previous version
source = TranslationSource.objects.filter(
object_id=object.translation_key, locale_id=instance.locale_id
).first()
# Check if the instance has changed at all since the previous version
if source:
if json.loads(content_json) == json.loads(source.content_json):
return source, False
source, created = cls.objects.update_or_create(
object=object,
locale=instance.locale,
# You can't update the content type of a source. So if this happens,
# it'll try and create a new source and crash (can't have more than
# one source per object/locale)
specific_content_type=ContentType.objects.get_for_model(instance.__class__),
defaults={
"locale": instance.locale,
"object_repr": str(instance)[:200],
"content_json": content_json,
"schema_version": get_schema_version(instance._meta.app_label) or "",
"last_updated_at": timezone.now(),
},
)
source.refresh_segments()
return source, created
update_target_view_restrictions(self, locale)
Creates a corresponding view restriction object for the translated page for the given locale
Parameters:
Name | Type | Description | Default |
---|---|---|---|
locale |
Locale |
The target locale |
required |
Source code in wagtail_localize/models.py
def update_target_view_restrictions(self, locale):
"""
Creates a corresponding view restriction object for the translated page for the given locale
Args:
locale (Locale): The target locale
"""
original = self.as_instance()
# Only update restrictions for pages
if not isinstance(original, Page):
return
try:
translation_page = self.get_translated_instance(locale)
except Page.DoesNotExist:
return
self.sync_view_restrictions(original, translation_page)