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,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Views for handling AJAX requests from the translation editor.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from django.contrib.auth.decorators import login_required
|
|
8
|
+
from django.core.exceptions import PermissionDenied
|
|
9
|
+
from django.db.models import Q
|
|
10
|
+
from django.http import JsonResponse
|
|
11
|
+
from django.views.decorators.cache import never_cache
|
|
12
|
+
from django.views.decorators.http import require_POST
|
|
13
|
+
from wagtail_localize.models import (
|
|
14
|
+
StringSegment,
|
|
15
|
+
StringTranslation,
|
|
16
|
+
Translation,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from .constants import get_setting
|
|
20
|
+
from .utils import (
|
|
21
|
+
get_backup_separator,
|
|
22
|
+
is_do_not_translate,
|
|
23
|
+
mark_segment_do_not_translate,
|
|
24
|
+
unmark_segment_do_not_translate,
|
|
25
|
+
validate_configuration,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_permission(user):
|
|
32
|
+
"""
|
|
33
|
+
Check if user has permission to mark segments as "do not translate".
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
user: Django User instance
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
PermissionDenied if user doesn't have permission
|
|
40
|
+
"""
|
|
41
|
+
required_permission = get_setting("REQUIRED_PERMISSION")
|
|
42
|
+
|
|
43
|
+
if required_permission is None:
|
|
44
|
+
# No specific permission required
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
if not user.has_perm(required_permission):
|
|
48
|
+
raise PermissionDenied(
|
|
49
|
+
f"User does not have required permission: {required_permission}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@login_required
|
|
56
|
+
@require_POST
|
|
57
|
+
def mark_segment_do_not_translate_view(request, translation_id, segment_id):
|
|
58
|
+
"""
|
|
59
|
+
Mark a translation segment as "do not translate".
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
translation_id: The Translation ID
|
|
63
|
+
segment_id: The StringSegment ID (not String ID)
|
|
64
|
+
|
|
65
|
+
POST params:
|
|
66
|
+
do_not_translate: bool - True to mark as do not translate, False to unmark
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
JSON response with success status and source value
|
|
70
|
+
|
|
71
|
+
Example AJAX call:
|
|
72
|
+
fetch('/intentional-blanks/translations/123/segment/456/do-not-translate/', {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: {'X-CSRFToken': csrfToken},
|
|
75
|
+
body: new FormData({do_not_translate: 'true'})
|
|
76
|
+
})
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
# Check permissions
|
|
80
|
+
check_permission(request.user)
|
|
81
|
+
|
|
82
|
+
# Get objects
|
|
83
|
+
translation = Translation.objects.get(id=translation_id)
|
|
84
|
+
|
|
85
|
+
# segment_id is the StringSegment ID (matches wagtail-localize's segment.id)
|
|
86
|
+
logger.info(
|
|
87
|
+
f"Marking segment as do not translate: translation_id={translation_id}, segment_id={segment_id}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
segment = StringSegment.objects.get(id=segment_id, source=translation.source)
|
|
91
|
+
string = segment.string
|
|
92
|
+
|
|
93
|
+
if not string:
|
|
94
|
+
logger.error(f"StringSegment {segment_id} has no associated String")
|
|
95
|
+
return JsonResponse(
|
|
96
|
+
{
|
|
97
|
+
"success": False,
|
|
98
|
+
"error": f"Segment {segment_id} has no associated String. The translation may be corrupted.",
|
|
99
|
+
},
|
|
100
|
+
status=400,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Get action - only accept explicit 'true' or 'false'
|
|
104
|
+
do_not_translate_param = request.POST.get("do_not_translate", "").lower()
|
|
105
|
+
if do_not_translate_param not in ("true", "false"):
|
|
106
|
+
return JsonResponse(
|
|
107
|
+
{
|
|
108
|
+
"success": False,
|
|
109
|
+
"error": 'Invalid do_not_translate parameter. Must be "true" or "false".',
|
|
110
|
+
},
|
|
111
|
+
status=400,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
do_not_translate = do_not_translate_param == "true"
|
|
115
|
+
|
|
116
|
+
if do_not_translate:
|
|
117
|
+
mark_segment_do_not_translate(translation, segment, user=request.user)
|
|
118
|
+
message = "Segment marked as do not translate"
|
|
119
|
+
else:
|
|
120
|
+
unmark_segment_do_not_translate(translation, segment)
|
|
121
|
+
message = "Segment unmarked, ready for manual translation"
|
|
122
|
+
|
|
123
|
+
# Get the source text to display in UI
|
|
124
|
+
source_text = segment.string.data if segment.string else ""
|
|
125
|
+
|
|
126
|
+
# Get the translated value (if any) for unmarking
|
|
127
|
+
translated_value = None
|
|
128
|
+
if not do_not_translate:
|
|
129
|
+
try:
|
|
130
|
+
existing_translation = StringTranslation.objects.get(
|
|
131
|
+
translation_of=segment.string,
|
|
132
|
+
locale=translation.target_locale,
|
|
133
|
+
context=segment.context,
|
|
134
|
+
)
|
|
135
|
+
validate_configuration()
|
|
136
|
+
marker = get_setting("MARKER")
|
|
137
|
+
backup_separator = get_backup_separator()
|
|
138
|
+
# Make sure it's not the marker or encoded marker format
|
|
139
|
+
if (
|
|
140
|
+
existing_translation.data != marker
|
|
141
|
+
and not existing_translation.data.startswith(
|
|
142
|
+
marker + backup_separator
|
|
143
|
+
)
|
|
144
|
+
):
|
|
145
|
+
translated_value = existing_translation.data
|
|
146
|
+
except StringTranslation.DoesNotExist:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
return JsonResponse(
|
|
150
|
+
{
|
|
151
|
+
"success": True,
|
|
152
|
+
"source_value": source_text,
|
|
153
|
+
"translated_value": translated_value,
|
|
154
|
+
"do_not_translate": do_not_translate,
|
|
155
|
+
"message": message,
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
except Translation.DoesNotExist:
|
|
160
|
+
return JsonResponse(
|
|
161
|
+
{"success": False, "error": "Translation not found"}, status=404
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
except StringSegment.DoesNotExist:
|
|
165
|
+
logger.error(
|
|
166
|
+
f"StringSegment {segment_id} does not exist for translation {translation_id}"
|
|
167
|
+
)
|
|
168
|
+
return JsonResponse(
|
|
169
|
+
{
|
|
170
|
+
"success": False,
|
|
171
|
+
"error": f"Segment {segment_id} not found. The translation may need to be re-synced.",
|
|
172
|
+
},
|
|
173
|
+
status=404,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
except PermissionDenied as e:
|
|
177
|
+
return JsonResponse({"success": False, "error": str(e)}, status=403)
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
# Log the error
|
|
181
|
+
logger.exception("Error in mark_segment_do_not_translate_view")
|
|
182
|
+
|
|
183
|
+
return JsonResponse({"success": False, "error": str(e)}, status=400)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@login_required
|
|
187
|
+
def get_segment_status(request, translation_id, segment_id):
|
|
188
|
+
"""
|
|
189
|
+
Get the current status of a segment (marked as do not translate or not).
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
translation_id: The Translation ID
|
|
193
|
+
segment_id: The StringSegment ID (not String ID)
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
JSON response with status info
|
|
197
|
+
|
|
198
|
+
Example:
|
|
199
|
+
GET /intentional-blanks/translations/123/segment/456/status/
|
|
200
|
+
"""
|
|
201
|
+
try:
|
|
202
|
+
check_permission(request.user)
|
|
203
|
+
|
|
204
|
+
translation = Translation.objects.get(id=translation_id)
|
|
205
|
+
|
|
206
|
+
# segment_id is the StringSegment ID (matches wagtail-localize's segment.id)
|
|
207
|
+
segment = StringSegment.objects.get(id=segment_id, source=translation.source)
|
|
208
|
+
string = segment.string
|
|
209
|
+
|
|
210
|
+
if not string:
|
|
211
|
+
return JsonResponse(
|
|
212
|
+
{
|
|
213
|
+
"success": False,
|
|
214
|
+
"error": f"Segment {segment_id} has no associated String.",
|
|
215
|
+
},
|
|
216
|
+
status=400,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
string_translation = StringTranslation.objects.get(
|
|
221
|
+
translation_of=segment.string, # translation_of expects a String, not StringSegment
|
|
222
|
+
locale=translation.target_locale,
|
|
223
|
+
)
|
|
224
|
+
do_not_translate = is_do_not_translate(string_translation)
|
|
225
|
+
translated_text = string_translation.data if not do_not_translate else None
|
|
226
|
+
except StringTranslation.DoesNotExist:
|
|
227
|
+
do_not_translate = False
|
|
228
|
+
translated_text = None
|
|
229
|
+
|
|
230
|
+
source_text = segment.string.data if segment.string else ""
|
|
231
|
+
|
|
232
|
+
return JsonResponse(
|
|
233
|
+
{
|
|
234
|
+
"success": True,
|
|
235
|
+
"do_not_translate": do_not_translate,
|
|
236
|
+
"source_text": source_text,
|
|
237
|
+
"translated_text": translated_text,
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
except Translation.DoesNotExist:
|
|
242
|
+
return JsonResponse(
|
|
243
|
+
{"success": False, "error": "Translation not found"}, status=404
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
except StringSegment.DoesNotExist:
|
|
247
|
+
logger.error(
|
|
248
|
+
f"StringSegment {segment_id} does not exist for translation {translation_id}"
|
|
249
|
+
)
|
|
250
|
+
return JsonResponse(
|
|
251
|
+
{
|
|
252
|
+
"success": False,
|
|
253
|
+
"error": f"Segment {segment_id} not found. The translation may need to be re-synced.",
|
|
254
|
+
},
|
|
255
|
+
status=404,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
except PermissionDenied as e:
|
|
259
|
+
return JsonResponse({"success": False, "error": str(e)}, status=403)
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
return JsonResponse({"success": False, "error": str(e)}, status=400)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@login_required
|
|
266
|
+
@never_cache
|
|
267
|
+
def get_translation_status(request, translation_id):
|
|
268
|
+
"""
|
|
269
|
+
Get the status of all segments for a translation in one request.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
translation_id: The Translation ID
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
JSON response with a mapping of StringSegment IDs to their status
|
|
276
|
+
|
|
277
|
+
Example:
|
|
278
|
+
GET /intentional-blanks/translations/123/status/
|
|
279
|
+
Response: {
|
|
280
|
+
"success": true,
|
|
281
|
+
"segments": {
|
|
282
|
+
"456": {"do_not_translate": true, "source_text": "Hello"},
|
|
283
|
+
"457": {"do_not_translate": false, "source_text": "World"}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
(Keys are StringSegment IDs, not String IDs)
|
|
287
|
+
"""
|
|
288
|
+
try:
|
|
289
|
+
check_permission(request.user)
|
|
290
|
+
|
|
291
|
+
translation = Translation.objects.get(id=translation_id)
|
|
292
|
+
|
|
293
|
+
# Get all string translations for this translation source that are marked as "do not translate"
|
|
294
|
+
validate_configuration()
|
|
295
|
+
marker = get_setting("MARKER")
|
|
296
|
+
backup_separator = get_backup_separator()
|
|
297
|
+
|
|
298
|
+
# Get all StringSegments for this source
|
|
299
|
+
all_segments = StringSegment.objects.filter(
|
|
300
|
+
source=translation.source
|
|
301
|
+
).select_related("string")
|
|
302
|
+
string_ids = list(all_segments.values_list("string_id", flat=True))
|
|
303
|
+
|
|
304
|
+
# Get marked translations
|
|
305
|
+
marked_translations = (
|
|
306
|
+
StringTranslation.objects.filter(
|
|
307
|
+
locale=translation.target_locale, translation_of_id__in=string_ids
|
|
308
|
+
)
|
|
309
|
+
.filter(Q(data=marker) | Q(data__startswith=marker + backup_separator))
|
|
310
|
+
.select_related("translation_of")
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Build a map: String ID -> StringSegment ID
|
|
314
|
+
string_to_segment_map = {seg.string_id: seg.id for seg in all_segments}
|
|
315
|
+
|
|
316
|
+
# Build a mapping of StringSegment ID -> status
|
|
317
|
+
segments = {}
|
|
318
|
+
for st in marked_translations:
|
|
319
|
+
string_id = st.translation_of.id
|
|
320
|
+
segment_id = string_to_segment_map.get(string_id)
|
|
321
|
+
|
|
322
|
+
if segment_id:
|
|
323
|
+
segments[str(segment_id)] = {
|
|
324
|
+
"do_not_translate": True,
|
|
325
|
+
"source_text": st.translation_of.data,
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return JsonResponse({"success": True, "segments": segments})
|
|
329
|
+
|
|
330
|
+
except Translation.DoesNotExist:
|
|
331
|
+
return JsonResponse(
|
|
332
|
+
{"success": False, "error": "Translation not found"}, status=404
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
except PermissionDenied as e:
|
|
336
|
+
return JsonResponse({"success": False, "error": str(e)}, status=403)
|
|
337
|
+
|
|
338
|
+
except Exception as e:
|
|
339
|
+
logger.exception("Error in get_translation_status")
|
|
340
|
+
return JsonResponse({"success": False, "error": str(e)}, status=400)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wagtail hooks for intentional blanks integration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Placeholder for future Wagtail hooks
|
|
6
|
+
# The intentional blanks functionality is now implemented via a monkey-patch
|
|
7
|
+
# to wagtail-localize's TranslationSource._get_segments_for_translation()
|
|
8
|
+
# See patch.py for the implementation.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wagtail-localize-intentional-blanks
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Allow translators to mark wagtail-localize segments as 'do not translate'
|
|
5
|
+
Author-email: "Lincoln Loop, LLC" <info@lincolnloop.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lincolnloop/wagtail-localize-intentional-blanks
|
|
8
|
+
Project-URL: Documentation, https://github.com/lincolnloop/wagtail-localize-intentional-blanks
|
|
9
|
+
Project-URL: Repository, https://github.com/lincolnloop/wagtail-localize-intentional-blanks
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/lincolnloop/wagtail-localize-intentional-blanks/issues
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: Django
|
|
13
|
+
Classifier: Framework :: Django :: 4.2
|
|
14
|
+
Classifier: Framework :: Django :: 5.0
|
|
15
|
+
Classifier: Framework :: Wagtail
|
|
16
|
+
Classifier: Framework :: Wagtail :: 5
|
|
17
|
+
Classifier: Framework :: Wagtail :: 6
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Topic :: Software Development :: Internationalization
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: Django>=4.2
|
|
31
|
+
Requires-Dist: wagtail>=5.2
|
|
32
|
+
Requires-Dist: wagtail-localize>=1.8
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-django>=4.5; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
37
|
+
Requires-Dist: selenium>=4.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pillow>=10.0; extra == "dev"
|
|
39
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
40
|
+
Requires-Dist: flake8>=6.0; extra == "dev"
|
|
41
|
+
Requires-Dist: isort>=5.12; extra == "dev"
|
|
42
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
43
|
+
Provides-Extra: docs
|
|
44
|
+
Requires-Dist: mkdocs>=1.5; extra == "docs"
|
|
45
|
+
Requires-Dist: mkdocs-material>=9.0; extra == "docs"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
# wagtail-localize-intentional-blanks
|
|
49
|
+
|
|
50
|
+
A Wagtail library that extends [wagtail-localize](https://github.com/wagtail/wagtail-localize) to allow translators to mark translation segments as "do not translate". These segments count as translated (contributing to progress) but fall back to the source page's value when rendered.
|
|
51
|
+
|
|
52
|
+
<img width="985" height="236" alt="url_intentionally_blank" src="https://github.com/user-attachments/assets/310c5ad0-780c-43a6-a73f-1783c1d5f30e" />
|
|
53
|
+
<img width="1088" height="230" alt="number_intentionally_blank" src="https://github.com/user-attachments/assets/e9be46c7-86cd-43b7-a0d7-1747714c4bf9" />
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
- ✅ **Translator Control**: Translators decide which fields to not translate
|
|
59
|
+
- ✅ **Per-Locale Flexibility**: One language can translate a value while another language can use the source value
|
|
60
|
+
- ✅ **Progress Tracking**: "Do not translate" segments count as translated by `wagtail-localize`
|
|
61
|
+
- ✅ **No Template Changes**: Works transparently with existing templates
|
|
62
|
+
- ✅ **Drop-in Integration**: Minimal code changes required
|
|
63
|
+
- ✅ **UI Included**: Adds "Do Not Translate" buttons to translation editor
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install wagtail-localize-intentional-blanks
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
### 1. Add to INSTALLED_APPS
|
|
74
|
+
|
|
75
|
+
**Important:** `wagtail_localize_intentional_blanks` must come **before** `wagtail_localize` in `INSTALLED_APPS` for template overrides to work.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# settings.py
|
|
79
|
+
|
|
80
|
+
INSTALLED_APPS = [
|
|
81
|
+
# ... other apps
|
|
82
|
+
'wagtail_localize_intentional_blanks', # Must be BEFORE wagtail_localize
|
|
83
|
+
'wagtail_localize',
|
|
84
|
+
'wagtail_localize.locales',
|
|
85
|
+
# ... other apps
|
|
86
|
+
]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 2. Include URLs
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# urls.py
|
|
93
|
+
|
|
94
|
+
from django.urls import path, include
|
|
95
|
+
|
|
96
|
+
urlpatterns = [
|
|
97
|
+
# ... other patterns
|
|
98
|
+
path(
|
|
99
|
+
'intentional-blanks/', include('wagtail_localize_intentional_blanks.urls')
|
|
100
|
+
),
|
|
101
|
+
]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
That's it! The "Do Not Translate" button will now appear in the translation editor for all translatable fields. No code changes to your blocks or models required.
|
|
105
|
+
|
|
106
|
+
## How It Works
|
|
107
|
+
|
|
108
|
+
This library works by:
|
|
109
|
+
|
|
110
|
+
1. **Adding UI controls** - JavaScript adds "Do Not Translate" checkboxes to the translation editor
|
|
111
|
+
2. **Storing markers** - When checked, a marker string (`__DO_NOT_TRANSLATE__`) is stored in the translation
|
|
112
|
+
3. **Automatic replacement** - When rendering pages, a patch intercepts segment retrieval and replaces markers with source values
|
|
113
|
+
4. **Progress tracking** - Marked segments count as "translated" for progress calculation
|
|
114
|
+
|
|
115
|
+
**Key benefit:** No code changes to your blocks or models. The library handles everything automatically through patching wagtail-localize's internal methods.
|
|
116
|
+
|
|
117
|
+
## Usage
|
|
118
|
+
|
|
119
|
+
### In the Translation Editor
|
|
120
|
+
|
|
121
|
+
1. Open a page translation in wagtail-localize's editor
|
|
122
|
+
2. For each segment, you'll see a "Mark 'Do Not Translate'" checkbox
|
|
123
|
+
3. Check it to mark that segment as do not translate
|
|
124
|
+
4. The segment counts as translated (shows green)
|
|
125
|
+
5. When the page renders, it automatically shows the source value for that field
|
|
126
|
+
6. If the value in the original page changes, and the translated pages are synced, the segment still counts as translated (shows green) and when the page renders, it shows the updated source value for that field
|
|
127
|
+
|
|
128
|
+
### Common Use Cases
|
|
129
|
+
|
|
130
|
+
- **Brand names and trademarks** - Keep consistent across locales
|
|
131
|
+
- **Product codes and SKUs** - No translation needed
|
|
132
|
+
- **URLs** - Pages may contain the translations for different languages, but a URL is the same for all languages
|
|
133
|
+
- **IDs** - Not language-specific identifiers
|
|
134
|
+
|
|
135
|
+
### Configuration
|
|
136
|
+
|
|
137
|
+
You can customize behavior in your Django settings:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
# settings.py
|
|
141
|
+
|
|
142
|
+
# Enable/disable the feature globally
|
|
143
|
+
WAGTAIL_LOCALIZE_INTENTIONAL_BLANKS_ENABLED = True
|
|
144
|
+
|
|
145
|
+
# Custom marker (advanced)
|
|
146
|
+
WAGTAIL_LOCALIZE_INTENTIONAL_BLANKS_MARKER = "__DO_NOT_TRANSLATE__"
|
|
147
|
+
|
|
148
|
+
# Require specific permission (default: None = any translator)
|
|
149
|
+
WAGTAIL_LOCALIZE_INTENTIONAL_BLANKS_REQUIRED_PERMISSION = 'cms.can_mark_do_not_translate'
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Advanced Usage
|
|
153
|
+
|
|
154
|
+
### Programmatic API
|
|
155
|
+
|
|
156
|
+
You can programmatically mark segments as "do not translate" using the provided utilities:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from wagtail_localize_intentional_blanks.utils import (
|
|
160
|
+
mark_segment_do_not_translate,
|
|
161
|
+
unmark_segment_do_not_translate,
|
|
162
|
+
get_source_fallback_stats,
|
|
163
|
+
)
|
|
164
|
+
from wagtail_localize.models import Translation, StringSegment
|
|
165
|
+
|
|
166
|
+
# Mark a segment
|
|
167
|
+
translation = Translation.objects.get(id=123)
|
|
168
|
+
segment = StringSegment.objects.get(id=456)
|
|
169
|
+
mark_segment_do_not_translate(translation, segment, user=request.user)
|
|
170
|
+
|
|
171
|
+
# Unmark a segment
|
|
172
|
+
unmark_segment_do_not_translate(translation, segment)
|
|
173
|
+
|
|
174
|
+
# Get statistics
|
|
175
|
+
stats = get_source_fallback_stats(translation)
|
|
176
|
+
print(f"{stats['do_not_translate']} segments marked as do not translate")
|
|
177
|
+
print(f"{stats['manually_translated']} segments manually translated")
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Requirements
|
|
181
|
+
|
|
182
|
+
- Python 3.10+
|
|
183
|
+
- Django 4.2+
|
|
184
|
+
- Wagtail 5.2+
|
|
185
|
+
- wagtail-localize 1.8+
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
190
|
+
|
|
191
|
+
## Contributing
|
|
192
|
+
|
|
193
|
+
Contributions are welcome!
|
|
194
|
+
|
|
195
|
+
## Development
|
|
196
|
+
|
|
197
|
+
### Setting Up for Development
|
|
198
|
+
|
|
199
|
+
1. Clone the repository:
|
|
200
|
+
```bash
|
|
201
|
+
git clone https://github.com/lincolnloop/wagtail-localize-intentional-blanks.git
|
|
202
|
+
cd wagtail-localize-intentional-blanks
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
2. Install the package with development dependencies:
|
|
206
|
+
```bash
|
|
207
|
+
pip install -e ".[dev]"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
This installs the package in editable mode along with testing tools (pytest, black, flake8, etc.).
|
|
211
|
+
|
|
212
|
+
### Running Tests
|
|
213
|
+
|
|
214
|
+
Run the test suite with pytest:
|
|
215
|
+
```bash
|
|
216
|
+
pytest
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Run tests with coverage:
|
|
220
|
+
```bash
|
|
221
|
+
pytest --cov=wagtail_localize_intentional_blanks
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Run specific test files:
|
|
225
|
+
```bash
|
|
226
|
+
pytest tests/test_utils.py
|
|
227
|
+
pytest tests/test_views.py
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Code Quality
|
|
231
|
+
|
|
232
|
+
Check code with ruff:
|
|
233
|
+
```bash
|
|
234
|
+
ruff check .
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Format with ruff:
|
|
238
|
+
```bash
|
|
239
|
+
ruff format .
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Credits
|
|
243
|
+
|
|
244
|
+
Created by [Lincoln Loop, LLC](https://lincolnloop.com) for the Wagtail community.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
wagtail_localize_intentional_blanks/__init__.py,sha256=T5-7FLF993ygWsaNKweK7utCRqxJVLwgB2Czi4Lj8_0,857
|
|
2
|
+
wagtail_localize_intentional_blanks/apps.py,sha256=wTgWJ_1whFkExZmUhBEnHj17sbgqwiHVRA2TPzcv1PI,897
|
|
3
|
+
wagtail_localize_intentional_blanks/constants.py,sha256=yhrGeuoe9wmHpqdwztaYQ-7ctz8RsWx64myQU73iKg4,1352
|
|
4
|
+
wagtail_localize_intentional_blanks/patch.py,sha256=e-SpjXvFtyZEOhhj67RvWYvrYgFqbAZTkPOUaHL6VAA,6590
|
|
5
|
+
wagtail_localize_intentional_blanks/urls.py,sha256=KvZ-Jp0ci2qsV6Gc8yajkDYNBlZkCDkqSi4l80bIFN0,685
|
|
6
|
+
wagtail_localize_intentional_blanks/utils.py,sha256=tRCQA3pIRiRFJKRfKcSvQ2K4DLVNahtnPYxnyw3FjGY,14423
|
|
7
|
+
wagtail_localize_intentional_blanks/views.py,sha256=ffvXdve9vl4NwVsmoXj3RBy2W4XOV0IvYVFpaAfqnnI,11244
|
|
8
|
+
wagtail_localize_intentional_blanks/wagtail_hooks.py,sha256=Bk9bpdsquvO_ccbF1bTjpK8q6Yo975fcN_SrrEY1mO0,288
|
|
9
|
+
wagtail_localize_intentional_blanks/static/wagtail_localize_intentional_blanks/css/translation-editor.css,sha256=Iw_3iA35_8EreFaMvFya852RBlUQLiPDPN_8PElxl7k,2363
|
|
10
|
+
wagtail_localize_intentional_blanks/static/wagtail_localize_intentional_blanks/js/translation-editor.js,sha256=JE1g4wDxkWfbc6_FPFI-f2ZgEoOjqrgik4t6dmAxj9s,22753
|
|
11
|
+
wagtail_localize_intentional_blanks/templates/wagtail_localize/admin/edit_translation.html,sha256=ePGwLgWV8__Y3W0LxXAbDv2XCN49l1ZQ__aSkxMbck4,635
|
|
12
|
+
wagtail_localize_intentional_blanks/templatetags/__init__.py,sha256=7wPp_kk5jsll7wxkgUqWBBfHOPDP1tc_D2Cx7gJyH1o,63
|
|
13
|
+
wagtail_localize_intentional_blanks/templatetags/intentional_blanks.py,sha256=66yrni7vY-XPLteVMRcO_aS-Fp0eHuPWe0y3lxV3Bq0,1390
|
|
14
|
+
wagtail_localize_intentional_blanks-0.1.0.dist-info/licenses/LICENSE,sha256=b99OiFRYnxpXDbpBxCVdf-9VJ4htEBKvbky52Jhq08g,1138
|
|
15
|
+
wagtail_localize_intentional_blanks-0.1.0.dist-info/METADATA,sha256=gdwODZ1FJeqRQpz47VAd48otMJDVxe7FpPY0l48rBiQ,7933
|
|
16
|
+
wagtail_localize_intentional_blanks-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
+
wagtail_localize_intentional_blanks-0.1.0.dist-info/top_level.txt,sha256=H6IKrSKJO-H7vBnwcudF-yPhvlz8A_DD6XcKfioV8dQ,36
|
|
18
|
+
wagtail_localize_intentional_blanks-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
All original code is provided under the MIT License:
|
|
2
|
+
|
|
3
|
+
The MIT License (MIT)
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2025 Lincoln Loop, LLC
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in
|
|
15
|
+
all copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wagtail_localize_intentional_blanks
|