wagtail-localize-intentional-blanks 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ """
2
+ wagtail-localize-intentional-blanks
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+
5
+ A Wagtail library that extends wagtail-localize to allow translators to mark
6
+ translation segments as "do not translate". These segments count as not needing
7
+ to be translated, and fall back to the source page's value when rendered.
8
+
9
+ :copyright: (c) 2025 by Lincoln Loop, LLC
10
+ :license: MIT, see LICENSE for more details.
11
+ """
12
+
13
+ __version__ = "0.1.0"
14
+ __author__ = "Lincoln Loop, LLC"
15
+ __license__ = "MIT"
16
+
17
+ # Public API
18
+ from .constants import DO_NOT_TRANSLATE_MARKER
19
+
20
+ # Utility functions can be imported from their modules if needed:
21
+ # from wagtail_localize_intentional_blanks.utils import mark_segment_do_not_translate
22
+
23
+ __all__ = [
24
+ "DO_NOT_TRANSLATE_MARKER",
25
+ ]
26
+
27
+
28
+ # Default Django app config
29
+ default_app_config = "wagtail_localize_intentional_blanks.apps.IntentionalBlanksConfig"
@@ -0,0 +1,29 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class IntentionalBlanksConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "wagtail_localize_intentional_blanks"
7
+ verbose_name = "Wagtail Localize Intentional Blanks"
8
+
9
+ def ready(self):
10
+ """
11
+ Run when Django starts.
12
+
13
+ Register signal handlers, check dependencies, etc.
14
+ """
15
+ # Check that wagtail-localize is installed
16
+ try:
17
+ import wagtail_localize # noqa: F401
18
+ except ImportError:
19
+ raise ImportError(
20
+ "wagtail-localize must be installed to use wagtail-localize-intentional-blanks. Install it with: pip install wagtail-localize"
21
+ )
22
+
23
+ # Apply monkey-patch to wagtail-localize
24
+ from .patch import apply_patch
25
+
26
+ apply_patch()
27
+
28
+ # Import wagtail hooks
29
+ from . import wagtail_hooks # noqa
@@ -0,0 +1,44 @@
1
+ """
2
+ Constants used throughout the library.
3
+ """
4
+
5
+ # The marker value stored in StringTranslation.data to indicate "do not translate"
6
+ DO_NOT_TRANSLATE_MARKER = "__DO_NOT_TRANSLATE__"
7
+
8
+ # The separator used when encoding backup values in the marker
9
+ # Format: MARKER + BACKUP_SEPARATOR + original_value
10
+ # Example: "__DO_NOT_TRANSLATE__|backup|original_value"
11
+ BACKUP_SEPARATOR = "|backup|"
12
+
13
+ # Settings keys (can be overridden in Django settings)
14
+ SETTINGS_PREFIX = "WAGTAIL_LOCALIZE_INTENTIONAL_BLANKS"
15
+
16
+ # Default configuration
17
+ DEFAULTS = {
18
+ # Whether to enable the feature globally
19
+ "ENABLED": True,
20
+ # Custom marker (if you want to use a different value)
21
+ "MARKER": DO_NOT_TRANSLATE_MARKER,
22
+ # Custom backup separator (if you want to use a different value)
23
+ "BACKUP_SEPARATOR": BACKUP_SEPARATOR,
24
+ # Permission required to mark segments as "do not translate"
25
+ # None = any translator, or specify a permission string
26
+ "REQUIRED_PERMISSION": None,
27
+ }
28
+
29
+
30
+ def get_setting(key, default=None):
31
+ """
32
+ Get a setting value from Django settings.
33
+
34
+ Args:
35
+ key: Setting key (without prefix)
36
+ default: Default value if not found
37
+
38
+ Returns:
39
+ The setting value
40
+ """
41
+ from django.conf import settings
42
+
43
+ full_key = f"{SETTINGS_PREFIX}_{key}"
44
+ return getattr(settings, full_key, DEFAULTS.get(key, default))
@@ -0,0 +1,182 @@
1
+ """
2
+ Monkey-patch for wagtail-localize to support intentional blanks.
3
+
4
+ This patches TranslationSource._get_segments_for_translation() to check for
5
+ the __DO_NOT_TRANSLATE__ marker and use source values instead of translations
6
+ when the marker is present.
7
+ """
8
+
9
+ import json
10
+ import logging
11
+
12
+ from wagtail_localize.models import TranslationSource
13
+ from wagtail_localize.segments import (
14
+ OverridableSegmentValue,
15
+ RelatedObjectSegmentValue,
16
+ StringSegmentValue,
17
+ TemplateSegmentValue,
18
+ )
19
+ from wagtail_localize.strings import StringValue
20
+
21
+ from .constants import get_setting
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Store the original method so we can call it if needed
26
+ _original_get_segments_for_translation = TranslationSource._get_segments_for_translation
27
+
28
+
29
+ def _get_segments_for_translation_with_intentional_blanks(self, locale, fallback=False):
30
+ """
31
+ Enhanced version of _get_segments_for_translation that handles intentional blanks.
32
+
33
+ When a StringTranslation contains the __DO_NOT_TRANSLATE__ marker, this method
34
+ uses the source value instead, allowing translators to mark segments as
35
+ "do not translate" while still completing the translation.
36
+
37
+ This works correctly for all field types including multi-segment RichTextField.
38
+ """
39
+ if not get_setting("ENABLED"):
40
+ # Feature disabled, use original implementation
41
+ return _original_get_segments_for_translation(self, locale, fallback)
42
+
43
+ marker = get_setting("MARKER")
44
+ backup_separator = get_setting("BACKUP_SEPARATOR")
45
+
46
+ # Import here to avoid circular imports
47
+ from wagtail_localize.models import MissingTranslationError, StringSegment
48
+
49
+ string_segments = (
50
+ StringSegment.objects.filter(source=self)
51
+ .annotate_translation(locale)
52
+ .select_related("context", "string")
53
+ )
54
+
55
+ segments = []
56
+
57
+ for string_segment in string_segments:
58
+ if string_segment.translation:
59
+ # Check if this translation is marked as "do not translate"
60
+ translation_data = string_segment.translation
61
+
62
+ # Check for marker (exact match or with encoded backup)
63
+ if translation_data == marker or translation_data.startswith(
64
+ marker + backup_separator
65
+ ):
66
+ # Use source value instead of translation
67
+ logger.debug(
68
+ f"Intentional blank detected for segment {string_segment.string_id} in locale {locale}, using source value"
69
+ )
70
+ string = StringValue(string_segment.string.data)
71
+ else:
72
+ # Use the translated value
73
+ string = StringValue(translation_data)
74
+ elif fallback:
75
+ string = StringValue(string_segment.string.data)
76
+ else:
77
+ raise MissingTranslationError(string_segment, locale)
78
+
79
+ segment_value = StringSegmentValue(
80
+ string_segment.context.path,
81
+ string,
82
+ attrs=json.loads(string_segment.attrs),
83
+ ).with_order(string_segment.order)
84
+
85
+ segments.append(segment_value)
86
+
87
+ # Handle template segments (templates are locale-independent)
88
+ template_segments = self.templatesegment_set.all().select_related("template")
89
+ for template_segment in template_segments:
90
+ segment_value = TemplateSegmentValue(
91
+ template_segment.context.path,
92
+ template_segment.template.template_format,
93
+ template_segment.template.template,
94
+ template_segment.template.string_count,
95
+ ).with_order(template_segment.order)
96
+
97
+ segments.append(segment_value)
98
+
99
+ # Handle related object segments
100
+ related_object_segments = self.relatedobjectsegment_set.all().select_related(
101
+ "object"
102
+ )
103
+ for related_object_segment in related_object_segments:
104
+ try:
105
+ instance = related_object_segment.object.get_instance(locale)
106
+ except related_object_segment.object.content_type.model_class().DoesNotExist:
107
+ if fallback:
108
+ instance = related_object_segment.object.get_instance(self.locale)
109
+ else:
110
+ raise
111
+
112
+ segment_value = RelatedObjectSegmentValue(
113
+ related_object_segment.context.path,
114
+ related_object_segment.object.content_type,
115
+ instance.pk,
116
+ ).with_order(related_object_segment.order)
117
+
118
+ segments.append(segment_value)
119
+
120
+ # Handle overridable segments (no locale filter needed - they're source-specific)
121
+ overridable_segments = self.overridablesegment_set.all().select_related("context")
122
+ for overridable_segment in overridable_segments:
123
+ segment_value = OverridableSegmentValue(
124
+ overridable_segment.context.path,
125
+ overridable_segment.data_json, # Use data_json field
126
+ ).with_order(overridable_segment.order)
127
+
128
+ segments.append(segment_value)
129
+
130
+ return segments
131
+
132
+
133
+ def apply_patch():
134
+ """
135
+ Apply the monkey-patch to TranslationSource.
136
+
137
+ This should be called from the app's ready() method.
138
+ """
139
+ TranslationSource._get_segments_for_translation = (
140
+ _get_segments_for_translation_with_intentional_blanks
141
+ )
142
+
143
+ # Also patch update_from_db to migrate markers after syncing translated pages.
144
+ # If a user 1. marks a field as 'Do Not Translate', then 2. updates the
145
+ # source field value, then 3. clicks 'Sync translated pages', we want to
146
+ # make sure that the field remains marked as 'Do Not Translate'.
147
+ _patch_update_from_db()
148
+
149
+
150
+ # Store the original update_from_db method
151
+ _original_update_from_db = TranslationSource.update_from_db
152
+
153
+
154
+ def _update_from_db_with_marker_migration(self):
155
+ """
156
+ Enhanced version of update_from_db that migrates markers after updating.
157
+
158
+ This ensures that when source content changes and is synced, any 'Do Not Translate'
159
+ markers are automatically migrated to the new Strings.
160
+ """
161
+ # Call the original method to perform the update
162
+ result = _original_update_from_db(self)
163
+
164
+ if not get_setting("ENABLED"):
165
+ return result
166
+
167
+ # After updating, migrate any orphaned markers for all target locales
168
+ from wagtail_localize.models import Translation
169
+
170
+ from .utils import migrate_do_not_translate_markers
171
+
172
+ translations = Translation.objects.filter(source=self)
173
+
174
+ for translation in translations:
175
+ migrate_do_not_translate_markers(self, translation.target_locale)
176
+
177
+ return result
178
+
179
+
180
+ def _patch_update_from_db():
181
+ """Patch the update_from_db method to migrate markers after sync."""
182
+ TranslationSource.update_from_db = _update_from_db_with_marker_migration
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Styles for Intentional Blanks UI
3
+ */
4
+
5
+ /* Checkbox container */
6
+ .do-not-translate-checkbox-container {
7
+ display: inline-flex;
8
+ align-items: center;
9
+ gap: 8px;
10
+ padding: 4px 8px;
11
+ border-radius: 3px;
12
+ transition: background-color 0.2s;
13
+ vertical-align: middle;
14
+ }
15
+
16
+ .do-not-translate-checkbox-container:hover {
17
+ background-color: #f5f5f5;
18
+ }
19
+
20
+ /* Checkbox styling */
21
+ .do-not-translate-checkbox {
22
+ width: 16px;
23
+ height: 16px;
24
+ cursor: pointer;
25
+ margin: 0;
26
+ accent-color: #007d7e;
27
+ }
28
+
29
+ .do-not-translate-checkbox:disabled {
30
+ opacity: 0.5;
31
+ cursor: not-allowed;
32
+ }
33
+
34
+ /* Label styling */
35
+ .do-not-translate-label {
36
+ font-size: 13px;
37
+ color: #333;
38
+ cursor: pointer;
39
+ margin: 0;
40
+ user-select: none;
41
+ white-space: nowrap;
42
+ }
43
+
44
+ .do-not-translate-checkbox:disabled + .do-not-translate-label {
45
+ opacity: 0.5;
46
+ cursor: not-allowed;
47
+ }
48
+
49
+ /* Segment marked as do not translate */
50
+ .do-not-translate {
51
+ background-color: #e8f5e9;
52
+ border-left: 4px solid #4caf50;
53
+ padding-left: 12px;
54
+ position: relative;
55
+ transition: background-color 0.3s ease, border-left 0.3s ease;
56
+ }
57
+
58
+ /* Badge that appears after field name */
59
+ .do-not-translate-badge {
60
+ display: inline-block;
61
+ background: #4caf50;
62
+ color: white;
63
+ padding: 2px 8px;
64
+ border-radius: 3px;
65
+ font-size: 11px;
66
+ font-weight: bold;
67
+ margin-left: 8px;
68
+ text-transform: uppercase;
69
+ animation: fadeIn 0.3s ease-in;
70
+ vertical-align: middle;
71
+ }
72
+
73
+ @keyframes fadeIn {
74
+ from {
75
+ opacity: 0;
76
+ transform: translateY(-5px);
77
+ }
78
+ to {
79
+ opacity: 1;
80
+ transform: translateY(0);
81
+ }
82
+ }
83
+
84
+ /* Input field when marked as do not translate */
85
+ .do-not-translate input[readonly],
86
+ .do-not-translate textarea[readonly] {
87
+ background-color: #f5f5f5;
88
+ cursor: not-allowed;
89
+ transition: background-color 0.3s ease, opacity 0.3s ease;
90
+ }
91
+
92
+ /* Smooth transitions for input fields */
93
+ input[type="text"],
94
+ textarea {
95
+ transition: background-color 0.3s ease, opacity 0.3s ease;
96
+ }
97
+
98
+ /* Responsive adjustments */
99
+ @media (max-width: 768px) {
100
+ .do-not-translate-checkbox-container {
101
+ padding: 2px 4px;
102
+ gap: 6px;
103
+ }
104
+
105
+ .do-not-translate-label {
106
+ font-size: 12px;
107
+ }
108
+
109
+ .do-not-translate::before {
110
+ position: static;
111
+ display: block;
112
+ margin-bottom: 8px;
113
+ }
114
+ }