diff --git a/wagtail_localize/models.py b/wagtail_localize/models.py index fa3fb40f..231b42a8 100644 --- a/wagtail_localize/models.py +++ b/wagtail_localize/models.py @@ -1242,6 +1242,9 @@ class RelatedObjectSegment(BaseSegment): TranslatableObject, on_delete=models.CASCADE, related_name="references" ) + 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( diff --git a/wagtail_localize/static_src/editor/components/TranslationEditor/index.tsx b/wagtail_localize/static_src/editor/components/TranslationEditor/index.tsx index 8062e184..71a90c6f 100644 --- a/wagtail_localize/static_src/editor/components/TranslationEditor/index.tsx +++ b/wagtail_localize/static_src/editor/components/TranslationEditor/index.tsx @@ -77,7 +77,31 @@ export interface SynchronisedValueSegment extends SegmentCommon { editUrl: string; } -export type Segment = StringSegment | SynchronisedValueSegment; +export interface RelatedObjectSegment extends SegmentCommon { + type: 'related_object'; + source: { + title: string; + isLive: boolean; + liveUrl?: string; + editUrl?: string; + createTranslationRequestUrl?: string; + } | null; + dest: { + title: string; + isLive: boolean; + liveUrl?: string; + editUrl?: string; + } | null; + translationProgress: { + totalSegments: number; + translatedSegments: number; + } | null; // Null if translated without wagtail-localize +} + +export type Segment = + | StringSegment + | SynchronisedValueSegment + | RelatedObjectSegment; export interface StringTranslationAPI { string_id: number; diff --git a/wagtail_localize/static_src/editor/components/TranslationEditor/segments.tsx b/wagtail_localize/static_src/editor/components/TranslationEditor/segments.tsx index 296a6638..4a5bfc1a 100644 --- a/wagtail_localize/static_src/editor/components/TranslationEditor/segments.tsx +++ b/wagtail_localize/static_src/editor/components/TranslationEditor/segments.tsx @@ -18,7 +18,8 @@ import { StringTranslationAPI, SegmentOverride, SegmentOverrideAPI, - Locale + Locale, + RelatedObjectSegment } from '.'; import { EditorState, @@ -718,6 +719,92 @@ const EditorSynchronisedValueSegment: FunctionComponent< ); }; +interface EditorRelatedObjectSegmentProps { + segment: RelatedObjectSegment; +} + +const EditorRelatedObjectSegment: FunctionComponent< + EditorRelatedObjectSegmentProps +> = ({ segment }) => { + const openEditUrl = () => { + if (segment.dest) { + window.open(segment.dest.editUrl); + } + }; + + const openCreateTranslationRequestUrl = () => { + if (!!segment.source && segment.source.createTranslationRequestUrl) { + window.open(segment.source.createTranslationRequestUrl); + } + }; + + let message = <>; + + if (segment.dest) { + if (segment.translationProgress !== null) { + // Translated with Wagtail localize. Show progress + message = ( + <> + {segment.translationProgress.translatedSegments} /{' '} + {segment.translationProgress.totalSegments}{' '} + {gettext('segments translated')} + {segment.translationProgress.translatedSegments == + segment.translationProgress.totalSegments && ( + + )} + + ); + } else { + // Segment translated without Wagtail localize. Just show a tick + message = ; + } + } else { + // Not translated + message = ( + <> + {gettext('Not translated')}{' '} + + + ); + } + + return ( +
  • + {segment.location.subField && ( + + {segment.location.subField} + + )} + +

    + {segment.source + ? segment.source.title + : gettext('[DELETED]')} +

    +
    + +
  • {message}
  • +
  • + {segment.dest && segment.dest.editUrl && ( + + {gettext('Edit')} + + )} + {!segment.dest && + !!segment.source && + segment.source.createTranslationRequestUrl && ( + + {gettext('Translate')} + + )} +
  • + + + ); +}; + interface EditorSegmentListProps extends EditorProps, EditorState { dispatch: React.Dispatch; csrfToken: string; @@ -777,6 +864,9 @@ const EditorSegmentList: FunctionComponent = ({ /> ); } + case 'related_object': { + return ; + } } }); diff --git a/wagtail_localize/tests/test_edit_translation.py b/wagtail_localize/tests/test_edit_translation.py index 57f83f00..afcbfa9a 100644 --- a/wagtail_localize/tests/test_edit_translation.py +++ b/wagtail_localize/tests/test_edit_translation.py @@ -187,6 +187,34 @@ def test_edit_page_translation(self): self.assertEqual(props['segments'][9]['location'], {'tab': 'content', 'field': 'Text block', 'blockId': str(STREAM_BLOCK_ID), 'fieldHelpText': '', 'subField': None, 'widget': None}) # TODO: Examples that use fieldHelpText and subField + # Check related object + related_object_segment = props['segments'][10] + self.assertEqual(related_object_segment['type'], 'related_object') + self.assertEqual(related_object_segment['contentPath'], 'test_snippet') + self.assertEqual(related_object_segment['location'], {'tab': 'content', 'field': 'Test snippet', 'blockId': None, 'fieldHelpText': '', 'subField': None, 'widget': None}) + self.assertEqual(related_object_segment['source']['title'], str(self.snippet)) + self.assertEqual(related_object_segment['dest']['title'], str(self.fr_snippet)) + self.assertEqual(related_object_segment['translationProgress'], {'totalSegments': 1, 'translatedSegments': 0}) + + def test_manually_translated_related_object(self): + # Related objects don't have to be translated by Wagtail localize so test with the snippet's translation record deleted + self.snippet_translation.delete() + + response = self.client.get(reverse('wagtailadmin_pages:edit', args=[self.fr_page.id])) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtail_localize/admin/edit_translation.html') + + props = json.loads(response.context['props']) + + # Check related object + related_object_segment = props['segments'][10] + self.assertEqual(related_object_segment['type'], 'related_object') + self.assertEqual(related_object_segment['contentPath'], 'test_snippet') + self.assertEqual(related_object_segment['location'], {'tab': 'content', 'field': 'Test snippet', 'blockId': None, 'fieldHelpText': '', 'subField': None, 'widget': None}) + self.assertEqual(related_object_segment['source']['title'], str(self.snippet)) + self.assertEqual(related_object_segment['dest']['title'], str(self.fr_snippet)) + self.assertIsNone(related_object_segment['translationProgress']) + def test_override_types(self): # Similar to above but adds some more overridable things to test with self.page.test_synchronized_image = Image.objects.create( diff --git a/wagtail_localize/views/edit_translation.py b/wagtail_localize/views/edit_translation.py index 8086230e..c9808481 100644 --- a/wagtail_localize/views/edit_translation.py +++ b/wagtail_localize/views/edit_translation.py @@ -355,6 +355,7 @@ def edit_translation(request, translation, instance): overridable_segments = translation.source.overridablesegment_set.all().order_by('order') segment_overrides = overridable_segments.get_overrides(translation.target_locale) + related_object_segments = translation.source.relatedobjectsegment_set.all().order_by('order') tab_helper = TabHelper(source_instance) @@ -407,7 +408,75 @@ def edit_translation(request, translation, instance): for segment in overridable_segments ] - segments = string_segment_data + syncronised_value_segment_data + def get_source_object_info(segment): + instance = segment.get_source_instance() + + if isinstance(instance, Page): + return { + 'title': str(instance), + 'isLive': instance.live, + 'liveUrl': instance.full_url, + 'editUrl': reverse('wagtailadmin_pages:edit', args=[instance.id]), + 'createTranslationRequestUrl': reverse('wagtail_localize:submit_page_translation', args=[instance.id]), + } + + else: + return { + 'title': str(instance), + 'isLive': True, + 'editUrl': reverse('wagtailsnippets:edit', args=[instance._meta.app_label, instance._meta.model_name, quote(instance.id)]), + 'createTranslationRequestUrl': reverse('wagtail_localize:submit_snippet_translation', args=[instance._meta.app_label, instance._meta.model_name, quote(instance.id)]), + } + + def get_dest_object_info(segment): + instance = segment.object.get_instance_or_none(translation.target_locale) + if not instance: + return + + if isinstance(instance, Page): + return { + 'title': str(instance), + 'isLive': instance.live, + 'liveUrl': instance.full_url, + 'editUrl': reverse('wagtailadmin_pages:edit', args=[instance.id]), + } + + else: + return { + 'title': str(instance), + 'isLive': True, + 'editUrl': reverse('wagtailsnippets:edit', args=[instance._meta.app_label, instance._meta.model_name, quote(instance.id)]), + } + + def get_translation_progress(segment, locale): + try: + translation = Translation.objects.get(source__object_id=segment.object_id, target_locale=locale, enabled=True) + + except Translation.DoesNotExist: + return None + + total_segments, translated_segments = translation.get_progress() + + return { + 'totalSegments': total_segments, + 'translatedSegments': translated_segments, + } + + related_object_segment_data = [ + { + 'type': 'related_object', + 'id': segment.id, + 'contentPath': segment.context.path, + 'location': get_segment_location_info(source_instance, tab_helper, segment.context.path), + 'order': segment.order, + 'source': get_source_object_info(segment), + 'dest': get_dest_object_info(segment), + 'translationProgress': get_translation_progress(segment, translation.target_locale), + } + for segment in related_object_segments + ] + + segments = string_segment_data + syncronised_value_segment_data + related_object_segment_data segments.sort(key=lambda segment: segment['order']) return render(request, 'wagtail_localize/admin/edit_translation.html', {