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.
- wagtail_localize_intentional_blanks/__init__.py +29 -0
- wagtail_localize_intentional_blanks/apps.py +29 -0
- wagtail_localize_intentional_blanks/constants.py +44 -0
- wagtail_localize_intentional_blanks/patch.py +182 -0
- wagtail_localize_intentional_blanks/static/wagtail_localize_intentional_blanks/css/translation-editor.css +114 -0
- wagtail_localize_intentional_blanks/static/wagtail_localize_intentional_blanks/js/translation-editor.js +644 -0
- wagtail_localize_intentional_blanks/templates/wagtail_localize/admin/edit_translation.html +17 -0
- wagtail_localize_intentional_blanks/templatetags/__init__.py +1 -0
- wagtail_localize_intentional_blanks/templatetags/intentional_blanks.py +52 -0
- wagtail_localize_intentional_blanks/urls.py +27 -0
- wagtail_localize_intentional_blanks/utils.py +415 -0
- wagtail_localize_intentional_blanks/views.py +340 -0
- wagtail_localize_intentional_blanks/wagtail_hooks.py +8 -0
- wagtail_localize_intentional_blanks-0.1.0.dist-info/METADATA +244 -0
- wagtail_localize_intentional_blanks-0.1.0.dist-info/RECORD +18 -0
- wagtail_localize_intentional_blanks-0.1.0.dist-info/WHEEL +5 -0
- wagtail_localize_intentional_blanks-0.1.0.dist-info/licenses/LICENSE +23 -0
- wagtail_localize_intentional_blanks-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
}
|