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,644 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation Editor Enhancements for Intentional Blanks
|
|
3
|
+
*
|
|
4
|
+
* Adds "Do Not Translate" buttons to wagtail-localize's translation editor.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
(function() {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// Configuration (can be overridden by Django template)
|
|
11
|
+
const config = {
|
|
12
|
+
apiBaseUrl: window.INTENTIONAL_BLANKS_API_URL || '/intentional-blanks/',
|
|
13
|
+
labelText: "Mark 'Do Not Translate'",
|
|
14
|
+
marker: '__DO_NOT_TRANSLATE__',
|
|
15
|
+
backupSeparator: '|backup|',
|
|
16
|
+
cssClasses: {
|
|
17
|
+
container: 'do-not-translate',
|
|
18
|
+
checkboxContainer: 'do-not-translate-checkbox-container',
|
|
19
|
+
checkbox: 'do-not-translate-checkbox',
|
|
20
|
+
label: 'do-not-translate-label'
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Clean props data before React initializes.
|
|
26
|
+
* Replaces marker strings with actual values so React never sees the marker.
|
|
27
|
+
*/
|
|
28
|
+
function cleanPropsData() {
|
|
29
|
+
const editorContainer = document.querySelector('.js-translation-editor');
|
|
30
|
+
if (!editorContainer) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!editorContainer.dataset.props) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const props = JSON.parse(editorContainer.dataset.props);
|
|
40
|
+
|
|
41
|
+
if (!props.segments) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!props.initialStringTranslations) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Build a map: StringSegment ID → source value
|
|
50
|
+
// This matches how wagtail-localize links translations to segments
|
|
51
|
+
const segmentMap = new Map();
|
|
52
|
+
props.segments.forEach(segment => {
|
|
53
|
+
if (segment.type === 'string') {
|
|
54
|
+
// segment.id is the StringSegment ID (primary key)
|
|
55
|
+
segmentMap.set(segment.id, segment.source);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
let cleanedCount = 0;
|
|
60
|
+
props.initialStringTranslations.forEach((translation, index) => {
|
|
61
|
+
if (!translation.data) return;
|
|
62
|
+
|
|
63
|
+
const isMarker = translation.data === config.marker;
|
|
64
|
+
const hasBackup = translation.data.startsWith(config.marker + config.backupSeparator);
|
|
65
|
+
|
|
66
|
+
if (isMarker || hasBackup) {
|
|
67
|
+
|
|
68
|
+
if (hasBackup) {
|
|
69
|
+
// Extract original value from backup
|
|
70
|
+
const parts = translation.data.split(config.backupSeparator);
|
|
71
|
+
if (parts.length > 1) {
|
|
72
|
+
translation.data = parts[1];
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// No backup, use source value from segment
|
|
76
|
+
// Use segment_id (not string_id) to look up the source value
|
|
77
|
+
const sourceValue = segmentMap.get(translation.segment_id);
|
|
78
|
+
translation.data = sourceValue || '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Clear translation metadata to prevent "Translated manually on..." text
|
|
82
|
+
translation.last_translated_by = null;
|
|
83
|
+
translation.comment = '';
|
|
84
|
+
|
|
85
|
+
cleanedCount++;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
// Write cleaned data back
|
|
91
|
+
editorContainer.dataset.props = JSON.stringify(props);
|
|
92
|
+
|
|
93
|
+
return true;
|
|
94
|
+
|
|
95
|
+
} catch (e) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// CRITICAL: Clean props as early as possible, before React initializes
|
|
101
|
+
let propsAlreadyCleaned = false;
|
|
102
|
+
|
|
103
|
+
// Try immediately (synchronous)
|
|
104
|
+
if (cleanPropsData()) {
|
|
105
|
+
propsAlreadyCleaned = true;
|
|
106
|
+
} else {
|
|
107
|
+
// Container doesn't exist yet - watch for it to appear
|
|
108
|
+
|
|
109
|
+
const observer = new MutationObserver(() => {
|
|
110
|
+
const editorContainer = document.querySelector('.js-translation-editor');
|
|
111
|
+
if (editorContainer && editorContainer.dataset.props && !propsAlreadyCleaned) {
|
|
112
|
+
if (cleanPropsData()) {
|
|
113
|
+
propsAlreadyCleaned = true;
|
|
114
|
+
observer.disconnect();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Watch for container to be added to DOM
|
|
120
|
+
if (document.documentElement) {
|
|
121
|
+
observer.observe(document.documentElement, {
|
|
122
|
+
childList: true,
|
|
123
|
+
subtree: true
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Update props JSON after marking/unmarking a segment.
|
|
130
|
+
* Keeps React's data in sync with the backend.
|
|
131
|
+
*/
|
|
132
|
+
function updatePropsAfterToggle(segmentId, doNotTranslate, sourceValue, translatedValue) {
|
|
133
|
+
|
|
134
|
+
const editorContainer = document.querySelector('.js-translation-editor');
|
|
135
|
+
if (!editorContainer || !editorContainer.dataset.props) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const props = JSON.parse(editorContainer.dataset.props);
|
|
141
|
+
|
|
142
|
+
// Find the translation in initialStringTranslations by segment_id
|
|
143
|
+
// segmentId here is the StringSegment ID
|
|
144
|
+
const translation = props.initialStringTranslations?.find(t => t.segment_id === segmentId);
|
|
145
|
+
|
|
146
|
+
if (translation) {
|
|
147
|
+
if (doNotTranslate) {
|
|
148
|
+
// Marking - use source value and clear metadata
|
|
149
|
+
translation.data = sourceValue;
|
|
150
|
+
translation.last_translated_by = null;
|
|
151
|
+
translation.comment = '';
|
|
152
|
+
} else {
|
|
153
|
+
// Unmarking - use translated value or empty
|
|
154
|
+
translation.data = translatedValue || '';
|
|
155
|
+
// Metadata will be restored when user edits/saves the translation
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
editorContainer.dataset.props = JSON.stringify(props);
|
|
159
|
+
} else {
|
|
160
|
+
}
|
|
161
|
+
} catch (e) {
|
|
162
|
+
console.error('[updatePropsAfterToggle] Error updating props:', e);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Initialize everything in the correct order
|
|
168
|
+
*/
|
|
169
|
+
function init() {
|
|
170
|
+
function initAll() {
|
|
171
|
+
// Try to clean props if not already done
|
|
172
|
+
if (!propsAlreadyCleaned) {
|
|
173
|
+
if (cleanPropsData()) {
|
|
174
|
+
propsAlreadyCleaned = true;
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
}
|
|
178
|
+
// Initialize our UI enhancements
|
|
179
|
+
initializeButtons();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (document.readyState === 'loading') {
|
|
183
|
+
document.addEventListener('DOMContentLoaded', initAll);
|
|
184
|
+
} else {
|
|
185
|
+
// DOM already ready, run immediately
|
|
186
|
+
initAll();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Track if buttons have been initialized to prevent duplicates
|
|
191
|
+
let buttonsInitialized = false;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Add "Do Not Translate" checkboxes to all translation segments
|
|
195
|
+
*/
|
|
196
|
+
function initializeButtons() {
|
|
197
|
+
if (buttonsInitialized) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const editorContainer = document.querySelector('.js-translation-editor');
|
|
202
|
+
if (!editorContainer) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let segmentsData;
|
|
207
|
+
try {
|
|
208
|
+
const propsData = JSON.parse(editorContainer.dataset.props);
|
|
209
|
+
segmentsData = (propsData.segments || []).filter(seg => seg.type === 'string');
|
|
210
|
+
} catch (e) {
|
|
211
|
+
console.error('Failed to parse translation editor data:', e);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (segmentsData.length === 0) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Wait for React to render segments
|
|
220
|
+
const observer = new MutationObserver(() => {
|
|
221
|
+
const segmentElements = document.querySelectorAll('li.incomplete, li.complete');
|
|
222
|
+
|
|
223
|
+
if (segmentElements.length > 0 && segmentElements.length === segmentsData.length && !buttonsInitialized) {
|
|
224
|
+
observer.disconnect();
|
|
225
|
+
buttonsInitialized = true;
|
|
226
|
+
attachButtonsToSegments(segmentElements, segmentsData);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
231
|
+
|
|
232
|
+
// Fallback if React has already rendered
|
|
233
|
+
setTimeout(() => {
|
|
234
|
+
if (!buttonsInitialized) {
|
|
235
|
+
const segmentElements = document.querySelectorAll('li.incomplete, li.complete');
|
|
236
|
+
if (segmentElements.length === segmentsData.length) {
|
|
237
|
+
observer.disconnect();
|
|
238
|
+
buttonsInitialized = true;
|
|
239
|
+
attachButtonsToSegments(segmentElements, segmentsData);
|
|
240
|
+
} else {
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}, 1000);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Attach checkboxes to rendered segment elements
|
|
248
|
+
*/
|
|
249
|
+
function attachButtonsToSegments(segmentElements, segmentsData) {
|
|
250
|
+
const translationId = getTranslationId();
|
|
251
|
+
if (!translationId) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
segmentElements.forEach((container, index) => {
|
|
256
|
+
if (index >= segmentsData.length) return;
|
|
257
|
+
|
|
258
|
+
const segmentData = segmentsData[index];
|
|
259
|
+
|
|
260
|
+
// Use segment.id (StringSegment ID) - matches wagtail-localize's structure
|
|
261
|
+
// This is what the backend expects and what links to translations
|
|
262
|
+
const segmentId = segmentData.id;
|
|
263
|
+
|
|
264
|
+
if (!segmentId) return;
|
|
265
|
+
|
|
266
|
+
container.dataset.segmentId = segmentId;
|
|
267
|
+
|
|
268
|
+
const buttonContainer = container.querySelector('ul');
|
|
269
|
+
if (!buttonContainer) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const checkboxWrapper = createDoNotTranslateCheckbox(segmentId, translationId);
|
|
274
|
+
const listItem = document.createElement('li');
|
|
275
|
+
listItem.appendChild(checkboxWrapper);
|
|
276
|
+
|
|
277
|
+
// Always insert as the last child to maintain consistent position
|
|
278
|
+
// Mark it so we can identify it later
|
|
279
|
+
listItem.dataset.intentionalBlanksCheckbox = 'true';
|
|
280
|
+
buttonContainer.appendChild(listItem);
|
|
281
|
+
|
|
282
|
+
setupEditModeObserver(container, listItem);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
checkAllSegmentsStatus(translationId);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Create a checkbox with label for "Do Not Translate"
|
|
290
|
+
*/
|
|
291
|
+
function createDoNotTranslateCheckbox(segmentId, translationId) {
|
|
292
|
+
// Create container
|
|
293
|
+
const container = document.createElement('div');
|
|
294
|
+
container.className = config.cssClasses.checkboxContainer;
|
|
295
|
+
|
|
296
|
+
// Create checkbox
|
|
297
|
+
const checkbox = document.createElement('input');
|
|
298
|
+
checkbox.type = 'checkbox';
|
|
299
|
+
checkbox.id = `do-not-translate-${segmentId}`;
|
|
300
|
+
checkbox.className = config.cssClasses.checkbox;
|
|
301
|
+
checkbox.dataset.translationId = translationId;
|
|
302
|
+
|
|
303
|
+
// Create label
|
|
304
|
+
const label = document.createElement('label');
|
|
305
|
+
label.htmlFor = checkbox.id;
|
|
306
|
+
label.className = config.cssClasses.label;
|
|
307
|
+
label.textContent = config.labelText;
|
|
308
|
+
|
|
309
|
+
// Add change event listener
|
|
310
|
+
checkbox.addEventListener('change', function(e) {
|
|
311
|
+
toggleDoNotTranslate(checkbox, segmentId, translationId);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Append checkbox and label to container
|
|
315
|
+
container.appendChild(checkbox);
|
|
316
|
+
container.appendChild(label);
|
|
317
|
+
|
|
318
|
+
return container;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Toggle "do not translate" status for a segment
|
|
323
|
+
*/
|
|
324
|
+
function toggleDoNotTranslate(checkbox, segmentId, translationId) {
|
|
325
|
+
const container = checkbox.closest('[data-segment-id]');
|
|
326
|
+
|
|
327
|
+
if (!container) {
|
|
328
|
+
console.error('Container not found for segment', segmentId);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const doNotTranslate = checkbox.checked;
|
|
333
|
+
|
|
334
|
+
// Disable checkbox during request
|
|
335
|
+
checkbox.disabled = true;
|
|
336
|
+
|
|
337
|
+
const url = `${config.apiBaseUrl}translations/${translationId}/segment/${segmentId}/do-not-translate/`;
|
|
338
|
+
const formData = new FormData();
|
|
339
|
+
formData.append('do_not_translate', doNotTranslate);
|
|
340
|
+
formData.append('csrfmiddlewaretoken', getCsrfToken());
|
|
341
|
+
|
|
342
|
+
fetch(url, {
|
|
343
|
+
method: 'POST',
|
|
344
|
+
body: formData,
|
|
345
|
+
})
|
|
346
|
+
.then(response => response.json())
|
|
347
|
+
.then(data => {
|
|
348
|
+
if (data.success) {
|
|
349
|
+
// Update React's props data first
|
|
350
|
+
updatePropsAfterToggle(segmentId, data.do_not_translate, data.source_value, data.translated_value);
|
|
351
|
+
|
|
352
|
+
// Then update the UI
|
|
353
|
+
updateSegmentUI(container, checkbox, data.do_not_translate, data.source_value, data.translated_value);
|
|
354
|
+
showNotification('success', data.message);
|
|
355
|
+
} else {
|
|
356
|
+
// Revert checkbox state on error
|
|
357
|
+
checkbox.checked = !doNotTranslate;
|
|
358
|
+
showNotification('error', data.error || 'Failed to update segment');
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
.catch(error => {
|
|
362
|
+
console.error('Error toggling do not translate:', error);
|
|
363
|
+
// Revert checkbox state on error
|
|
364
|
+
checkbox.checked = !doNotTranslate;
|
|
365
|
+
showNotification('error', 'Network error occurred');
|
|
366
|
+
})
|
|
367
|
+
.finally(() => {
|
|
368
|
+
checkbox.disabled = false;
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Check the status of all segments with a single API call
|
|
374
|
+
*/
|
|
375
|
+
function checkAllSegmentsStatus(translationId) {
|
|
376
|
+
const url = `${config.apiBaseUrl}translations/${translationId}/status/?t=${Date.now()}`;
|
|
377
|
+
|
|
378
|
+
fetch(url, { cache: 'no-store' }) // Do not cache the API call
|
|
379
|
+
.then(response => response.json())
|
|
380
|
+
.then(data => {
|
|
381
|
+
if (data.success && data.segments) {
|
|
382
|
+
Object.keys(data.segments).forEach(segmentId => {
|
|
383
|
+
const segmentData = data.segments[segmentId];
|
|
384
|
+
const container = document.querySelector(`[data-segment-id="${segmentId}"]`);
|
|
385
|
+
|
|
386
|
+
if (container && segmentData.do_not_translate) {
|
|
387
|
+
const checkbox = container.querySelector(`.${config.cssClasses.checkbox}`);
|
|
388
|
+
if (checkbox) {
|
|
389
|
+
updateSegmentUI(container, checkbox, true, segmentData.source_text);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
.catch(error => console.error('Error checking segments status:', error));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Set up observer to show/hide checkbox based on edit mode
|
|
400
|
+
*/
|
|
401
|
+
function setupEditModeObserver(container, checkboxContainer) {
|
|
402
|
+
const observer = new MutationObserver(() => {
|
|
403
|
+
const textarea = container.querySelector('textarea');
|
|
404
|
+
const isEditMode = !!textarea;
|
|
405
|
+
|
|
406
|
+
// Hide checkbox in edit mode, show when not editing
|
|
407
|
+
if (checkboxContainer) {
|
|
408
|
+
checkboxContainer.style.display = isEditMode ? 'none' : '';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Ensure checkbox stays at the end of the button list
|
|
412
|
+
// React may re-render buttons and insert them before our checkbox
|
|
413
|
+
const buttonContainer = container.querySelector('ul');
|
|
414
|
+
if (buttonContainer && checkboxContainer && checkboxContainer.parentNode === buttonContainer) {
|
|
415
|
+
// Check if checkbox is already the last child
|
|
416
|
+
if (buttonContainer.lastElementChild !== checkboxContainer) {
|
|
417
|
+
// Move it to the end
|
|
418
|
+
buttonContainer.appendChild(checkboxContainer);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// If textarea appears and we have a restored value, apply it
|
|
423
|
+
if (textarea && container.dataset.restoredValue) {
|
|
424
|
+
textarea.value = container.dataset.restoredValue;
|
|
425
|
+
delete container.dataset.restoredValue;
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Observe for edit mode changes
|
|
430
|
+
observer.observe(container, {
|
|
431
|
+
childList: true,
|
|
432
|
+
subtree: true
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Store observer
|
|
436
|
+
container._editModeObserver = observer;
|
|
437
|
+
|
|
438
|
+
// Set initial state
|
|
439
|
+
const textarea = container.querySelector('textarea');
|
|
440
|
+
if (checkboxContainer && textarea) {
|
|
441
|
+
checkboxContainer.style.display = 'none';
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Hide/show the Edit button based on marked state
|
|
447
|
+
*/
|
|
448
|
+
function toggleEditButton(container, hide) {
|
|
449
|
+
// Find the "Translate" or "Edit" button - it's usually a button with specific text
|
|
450
|
+
const buttons = container.querySelectorAll('button');
|
|
451
|
+
buttons.forEach(button => {
|
|
452
|
+
const buttonText = button.textContent.trim();
|
|
453
|
+
if (buttonText === 'Translate' || buttonText === 'Edit') {
|
|
454
|
+
button.style.display = hide ? 'none' : '';
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Add "Using source value" badge after field name
|
|
461
|
+
*/
|
|
462
|
+
function addDoNotTranslateBadge(container) {
|
|
463
|
+
// Check if badge already exists
|
|
464
|
+
if (container.querySelector('.do-not-translate-badge')) return;
|
|
465
|
+
|
|
466
|
+
// Create badge element
|
|
467
|
+
const badge = document.createElement('span');
|
|
468
|
+
badge.className = 'do-not-translate-badge';
|
|
469
|
+
badge.textContent = 'Using source value';
|
|
470
|
+
|
|
471
|
+
// Try to find the field name (h4 element) and append badge to it
|
|
472
|
+
const fieldName = container.querySelector('h4');
|
|
473
|
+
if (fieldName) {
|
|
474
|
+
fieldName.appendChild(badge);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Fallback: if no h4, insert before the p element
|
|
479
|
+
const paragraph = container.querySelector('p');
|
|
480
|
+
if (paragraph) {
|
|
481
|
+
paragraph.parentNode.insertBefore(badge, paragraph);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Remove "Using source value" badge
|
|
487
|
+
*/
|
|
488
|
+
function removeDoNotTranslateBadge(container) {
|
|
489
|
+
const badge = container.querySelector('.do-not-translate-badge');
|
|
490
|
+
if (badge) {
|
|
491
|
+
badge.remove();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Update the UI to reflect segment status
|
|
497
|
+
*/
|
|
498
|
+
function updateSegmentUI(container, checkbox, doNotTranslate, sourceValue, translatedValue) {
|
|
499
|
+
if (!container) return;
|
|
500
|
+
|
|
501
|
+
// Find the editable field (textarea) or the display element (p tag in non-editable state)
|
|
502
|
+
const textarea = container.querySelector('textarea');
|
|
503
|
+
const displayP = container.querySelector('div.sc-iCoGMd p');
|
|
504
|
+
|
|
505
|
+
// Update checkbox state
|
|
506
|
+
checkbox.checked = doNotTranslate;
|
|
507
|
+
|
|
508
|
+
if (doNotTranslate) {
|
|
509
|
+
// Mark as do not translate - show source value
|
|
510
|
+
container.classList.add(config.cssClasses.container);
|
|
511
|
+
|
|
512
|
+
// Update wagtail-localize status classes
|
|
513
|
+
container.classList.remove('incomplete');
|
|
514
|
+
container.classList.add('complete');
|
|
515
|
+
|
|
516
|
+
// Update textarea if in editable state
|
|
517
|
+
if (textarea) {
|
|
518
|
+
textarea.value = sourceValue;
|
|
519
|
+
textarea.readOnly = true;
|
|
520
|
+
textarea.style.opacity = '0.7';
|
|
521
|
+
textarea.style.backgroundColor = '#f5f5f5';
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Update display paragraph if in non-editable state
|
|
525
|
+
if (displayP) {
|
|
526
|
+
displayP.textContent = sourceValue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Add badge after field name
|
|
530
|
+
addDoNotTranslateBadge(container);
|
|
531
|
+
|
|
532
|
+
// Hide the Edit button when marked as "Do Not Translate"
|
|
533
|
+
toggleEditButton(container, true);
|
|
534
|
+
} else {
|
|
535
|
+
// Unmark - restore translated value or clear to allow editing
|
|
536
|
+
container.classList.remove(config.cssClasses.container);
|
|
537
|
+
|
|
538
|
+
// Update wagtail-localize status classes based on whether there's a translation
|
|
539
|
+
if (translatedValue !== null && translatedValue !== undefined) {
|
|
540
|
+
container.classList.remove('incomplete');
|
|
541
|
+
container.classList.add('complete');
|
|
542
|
+
} else {
|
|
543
|
+
container.classList.remove('complete');
|
|
544
|
+
container.classList.add('incomplete');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Restore textarea if in editable state
|
|
548
|
+
if (textarea) {
|
|
549
|
+
if (translatedValue !== null && translatedValue !== undefined) {
|
|
550
|
+
textarea.value = translatedValue;
|
|
551
|
+
} else {
|
|
552
|
+
// No translation - clear the textarea
|
|
553
|
+
textarea.value = '';
|
|
554
|
+
}
|
|
555
|
+
textarea.readOnly = false;
|
|
556
|
+
textarea.style.opacity = '1';
|
|
557
|
+
textarea.style.backgroundColor = '';
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Restore display paragraph if in non-editable state
|
|
561
|
+
if (displayP) {
|
|
562
|
+
if (translatedValue !== null && translatedValue !== undefined) {
|
|
563
|
+
displayP.textContent = translatedValue;
|
|
564
|
+
// Store value for when user clicks Edit (React will create textarea with stale data)
|
|
565
|
+
container.dataset.restoredValue = translatedValue;
|
|
566
|
+
} else {
|
|
567
|
+
// No translation - clear the display and store empty string
|
|
568
|
+
displayP.textContent = '';
|
|
569
|
+
// Store empty string to override React's stale data
|
|
570
|
+
container.dataset.restoredValue = '';
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Remove badge
|
|
575
|
+
removeDoNotTranslateBadge(container);
|
|
576
|
+
|
|
577
|
+
// Show the Edit button when unmarked
|
|
578
|
+
toggleEditButton(container, false);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Get translation ID from page context
|
|
584
|
+
*/
|
|
585
|
+
function getTranslationId() {
|
|
586
|
+
// Try multiple methods to find translation ID
|
|
587
|
+
|
|
588
|
+
// 1. From URL
|
|
589
|
+
const match = window.location.pathname.match(/\/translations\/(\d+)\//);
|
|
590
|
+
if (match) return match[1];
|
|
591
|
+
|
|
592
|
+
// 2. From data attribute
|
|
593
|
+
const editor = document.querySelector('[data-translation-id]');
|
|
594
|
+
if (editor) return editor.dataset.translationId;
|
|
595
|
+
|
|
596
|
+
// 3. From global variable (set by Django template)
|
|
597
|
+
if (window.TRANSLATION_ID) return window.TRANSLATION_ID;
|
|
598
|
+
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Get CSRF token from page
|
|
604
|
+
*/
|
|
605
|
+
function getCsrfToken() {
|
|
606
|
+
const input = document.querySelector('[name=csrfmiddlewaretoken]');
|
|
607
|
+
if (input) return input.value;
|
|
608
|
+
|
|
609
|
+
const cookie = document.cookie.split('; ')
|
|
610
|
+
.find(row => row.startsWith('csrftoken='));
|
|
611
|
+
if (cookie) return cookie.split('=')[1];
|
|
612
|
+
|
|
613
|
+
return '';
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Show a notification message
|
|
618
|
+
*/
|
|
619
|
+
function showNotification(type, message) {
|
|
620
|
+
// Try to use Wagtail's notification system
|
|
621
|
+
if (window.wagtail && window.wagtail.messages) {
|
|
622
|
+
window.wagtail.messages.add({
|
|
623
|
+
type: type,
|
|
624
|
+
text: message
|
|
625
|
+
});
|
|
626
|
+
} else if (window.messages && window.messages.add) {
|
|
627
|
+
// Older Wagtail versions
|
|
628
|
+
window.messages.add(message, type);
|
|
629
|
+
} else {
|
|
630
|
+
// Fallback to console
|
|
631
|
+
console.log(`[${type.toUpperCase()}] ${message}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Initialize on load
|
|
636
|
+
init();
|
|
637
|
+
|
|
638
|
+
// Export for potential external use
|
|
639
|
+
window.IntentionalBlanks = {
|
|
640
|
+
init: init,
|
|
641
|
+
config: config
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
})();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{% extends "wagtail_localize/admin/edit_translation.html" %}
|
|
2
|
+
{% load static %}
|
|
3
|
+
|
|
4
|
+
{% block extra_css %}
|
|
5
|
+
{{ block.super }}
|
|
6
|
+
<link rel="stylesheet" href="{% static 'wagtail_localize_intentional_blanks/css/translation-editor.css' %}">
|
|
7
|
+
{% endblock %}
|
|
8
|
+
|
|
9
|
+
{% block extra_js %}
|
|
10
|
+
{{ block.super }}
|
|
11
|
+
<script>
|
|
12
|
+
// Pass configuration from Django to JavaScript
|
|
13
|
+
window.INTENTIONAL_BLANKS_API_URL = '/intentional-blanks/';
|
|
14
|
+
window.TRANSLATION_ID = {{ translation.id|default:"null" }};
|
|
15
|
+
</script>
|
|
16
|
+
<script src="{% static 'wagtail_localize_intentional_blanks/js/translation-editor.js' %}"></script>
|
|
17
|
+
{% endblock %}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Template tags module for wagtail-localize-intentional-blanks
|