PluginProbe
Translate and Go multilingual – Automatic AI translation – wpLingua / 2.16.6
Translate and Go multilingual – Automatic AI translation – wpLingua v2.16.6
2.16.6 2.16.5 2.16.4 2.16.3 2.16.2 2.16.1 2.16.0 2.15.2 2.15.1 2.15.0 2.14.3 2.14.2 2.14.1 2.14.0 2.13.1 2.13.0 2.12.3 2.12.2 2.12.1 trunk 1.0.3 1.0.4 1.0.5 1.1.0 1.1.1 All 111 releases
wplingua / inc / dictionary.php

dictionary.php in Translate and Go multilingual – Automatic AI translation – wpLingua 2.16.6, at inc/dictionary.php

969 lines 26.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // If this file is called directly, abort.
4 if ( ! defined( 'WPINC' ) ) {
5 die;
6 }
7
8
9 /**
10 * Get dictionary entries
11 *
12 * @return array
13 */
14 function wplng_dictionary_get_entries() {
15
16 $entries_clear = array();
17 $entries_json = get_option( 'wplng_dictionary_entries' );
18 $entries = json_decode( $entries_json, true );
19
20 if ( empty( $entries ) || ! is_array( $entries ) ) {
21 return array();
22 }
23
24 foreach ( $entries as $entry ) {
25
26 /**
27 * Get and check the source
28 */
29
30 if ( ! isset( $entry['source'] )
31 || ! is_string( $entry['source'] )
32 || mb_strlen( $entry['source'] ) >= 256
33 ) {
34 continue;
35 }
36
37 $source_clear = wplng_text_esc( $entry['source'] );
38 $source_clear = str_replace( '', '', $source_clear );
39 $source_clear = str_replace( '', '', $source_clear );
40 $source_clear = preg_replace( '#\[wplng_dictionary.*\]#', '', $source_clear );
41 $source_clear = preg_replace( '#\[\/wplng_dictionary\]#', '', $source_clear );
42
43 /**
44 * Check if rule already exist
45 */
46
47 $already_in = false;
48 foreach ( $entries_clear as $entry_clear ) {
49 if ( $source_clear === $entry_clear['source'] ) {
50 $already_in = true;
51 break;
52 }
53 }
54
55 if ( $already_in ) {
56 continue;
57 }
58
59 /**
60 * Get and check the rules
61 */
62
63 if ( ! isset( $entry['rules'] )
64 || ! is_array( $entry['rules'] )
65 ) {
66 /**
67 * Create the clear entries
68 */
69
70 $entries_clear[] = array(
71 'source' => $source_clear,
72 );
73 continue;
74 }
75
76 $rules_clear = array();
77
78 foreach ( $entry['rules'] as $language_id => $rule ) {
79 if ( ! wplng_is_valid_language_id( $language_id )
80 || ! is_string( $rule )
81 || '' === trim( $rule )
82 || $rule === $source_clear
83 || mb_strlen( $rule ) >= 256
84 ) {
85 continue;
86 }
87
88 $rules_clear[ $language_id ] = wplng_text_esc( $rule );
89 }
90
91 /**
92 * Create the clear entries
93 */
94
95 if ( empty( $rules_clear ) ) {
96 $entries_clear[] = array(
97 'source' => $source_clear,
98 );
99 } else {
100 $entries_clear[] = array(
101 'source' => $source_clear,
102 'rules' => $rules_clear,
103 );
104 }
105 }
106
107 /**
108 * Sort dictionary entries by sources length
109 */
110
111 usort(
112 $entries_clear,
113 function ( $a, $b ) {
114 return mb_strlen( $b['source'] ) - mb_strlen( $a['source'] );
115 }
116 );
117
118 /**
119 * Apply wplng_dictionary_entries filter
120 */
121
122 $entries_clear = apply_filters(
123 'wplng_dictionary_entries',
124 $entries_clear
125 );
126
127 return $entries_clear;
128 }
129
130
131 /**
132 * Get dictionary entries in JSON format
133 *
134 * @return string JSON
135 */
136 function wplng_dictionary_get_entries_json() {
137 return wp_json_encode(
138 wplng_dictionary_get_entries(),
139 JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
140 );
141 }
142
143
144 /**
145 * Add dictionary tag on a texts list
146 *
147 * @param array $texts
148 * @param array $dictionary_entries
149 * @return array Texts tagged
150 */
151 function wplng_dictionary_add_tags( $texts, $language_target_id, $dictionary_entries = false ) {
152
153 if ( false === $dictionary_entries ) {
154 $dictionary_entries = wplng_dictionary_get_entries();
155 }
156
157 /**
158 * Preg quote sources texts
159 */
160
161 foreach ( $dictionary_entries as $entry_key => $entry ) {
162 $dictionary_entries[ $entry_key ]['source'] = preg_quote( $entry['source'], '#' );
163 }
164
165 foreach ( $texts as $text_key => $text ) {
166
167 /**
168 * Get used dictionary entry in current text
169 */
170
171 $entries_used = array();
172
173 foreach ( $dictionary_entries as $entry_key => $entry ) {
174
175 $has_rules = isset( $entry['rules'] )
176 && is_array( $entry['rules'] )
177 && ! empty( $entry['rules'] );
178
179 if ( $has_rules && empty( $entry['rules'][ $language_target_id ] ) ) {
180 continue;
181 }
182
183 $preg_match = array();
184
185 preg_match_all(
186 '#' . $entry['source'] . '#iu',
187 $text,
188 $preg_match
189 );
190
191 $preg_match = $preg_match[0];
192
193 foreach ( $preg_match as $key => $match ) {
194
195 $upper = 'none';
196 // Caseless scripts (CJK, digits…) must not trigger uppercase detection:
197 // mb_strtoupper('テスト') === 'テスト' is true, which would incorrectly
198 // mark every CJK match as 'all' and uppercase the replacement.
199 $is_caseless = mb_strtolower( $match ) === mb_strtoupper( $match );
200 if ( ! $is_caseless ) {
201 if ( $match === mb_strtoupper( $match ) ) {
202 // Check uppercase
203 $upper = 'all';
204 } elseif ( $match === mb_strtoupper( mb_substr( $match, 0, 1 ) ) . mb_substr( $match, 1 ) ) {
205 // Check capitalize
206 $upper = 'first';
207 }
208 }
209
210 $entry_current = $entry;
211 $entry_current['source'] = $match;
212 $entry_current['key'] = $entry_key;
213 $entry_current['upper'] = $upper;
214 $entries_used[] = $entry_current;
215 }
216 }
217
218 /**
219 * Remove dupicate in used entries
220 */
221
222 $entries_used = array_map( 'serialize', $entries_used );
223 $entries_used = array_unique( $entries_used );
224 $entries_used = array_map( 'unserialize', $entries_used );
225
226 /**
227 * Put tempory tag in current text
228 */
229
230 foreach ( $entries_used as $entry_key => $entry_used ) {
231 // CJK scripts (Japanese, Chinese, etc.) have no word separators,
232 // so word boundaries cannot be used. For all other scripts (including
233 // Greek), (*UCP) makes \b Unicode-aware so letters are treated as \w.
234 if ( preg_match( '#[\x{2E80}-\x{9FFF}\x{F900}-\x{FAFF}]#u', $entry_used['source'] ) ) {
235 $boundary_pattern = '#' . $entry_used['source'] . '#u';
236 } else {
237 $boundary_pattern = '#(*UCP)\b' . $entry_used['source'] . '\b#u';
238 }
239 $text = preg_replace(
240 $boundary_pattern,
241 '' . str_repeat( '', $entry_key + 1 ) . '',
242 $text
243 );
244 }
245
246 /**
247 * Repace tempory tag by final tag
248 */
249
250 foreach ( $entries_used as $entry_key => $entry ) {
251
252 $search = '' . str_repeat( '', $entry_key + 1 ) . '';
253
254 $replace = '[wplng_dictionary ';
255 $replace .= 'key="' . $entry['key'] . '" ';
256 $replace .= 'upper="' . $entry['upper'] . '"]';
257 $replace .= $entry['source'];
258 $replace .= '[/wplng_dictionary]';
259
260 $text = str_replace(
261 $search,
262 $replace,
263 $text
264 );
265
266 }
267
268 /**
269 * Set updated text in text array
270 */
271
272 $texts[ $text_key ] = $text;
273 }
274
275 return $texts;
276 }
277
278
279 /**
280 * Transform dictionary tags to translated text
281 *
282 * @param array $texts
283 * @param array $dictionary_entries
284 * @return array Texts untagged
285 */
286 function wplng_dictionary_replace_tags( $texts, $language_target_id, $dictionary_entries = false ) {
287
288 if ( false === $dictionary_entries ) {
289 $dictionary_entries = wplng_dictionary_get_entries();
290 }
291
292 foreach ( $texts as $text_key => $text ) {
293
294 // Ensure spaces around dictionary tags when adjacent to letters from scripts
295 // that use word spaces (Latin, Greek, Cyrillic, Arabic, Hebrew, Devanagari…).
296 // This fixes CJK-source → alphabetic-target replacements where the translation
297 // API may not add spaces around the preserved tag (e.g. Japanese → French).
298 // CJK scripts (U+2E80–U+9FFF, U+F900–U+FAFF, U+AC00–U+D7AF) are intentionally
299 // excluded: they don't use word spaces, so no space should be inserted there.
300 $text = preg_replace(
301 '/(?<=[A-Za-z\x{00C0}-\x{04FF}\x{0590}-\x{097F}\d])\[wplng_dictionary /u',
302 ' [wplng_dictionary ',
303 $text
304 );
305 $text = preg_replace(
306 '/\[\/wplng_dictionary\](?=[A-Za-z\x{00C0}-\x{04FF}\x{0590}-\x{097F}\d])/u',
307 '[/wplng_dictionary] ',
308 $text
309 );
310
311 foreach ( $dictionary_entries as $key => $entry ) {
312
313 // For ruls "Do not translate
314 $replacement = $entry['source'];
315
316 // For specific rule by language
317 if ( ! empty( $entry['rules'][ $language_target_id ] ) ) {
318 $replacement = $entry['rules'][ $language_target_id ];
319 }
320
321 // Replacement for text in uppercase
322 $text = preg_replace(
323 '#\[wplng_dictionary key="' . $key . '" upper="all"\].+\[\/wplng_dictionary\]#Uu',
324 mb_strtoupper( $replacement ),
325 $text
326 );
327
328 // Replacement for text when only the first letter is uppercase
329 $text = preg_replace(
330 '#\[wplng_dictionary key="' . $key . '" upper="first"\].+\[\/wplng_dictionary\]#Uu',
331 mb_strtoupper( mb_substr( $replacement, 0, 1 ) ) . mb_substr( $replacement, 1 ),
332 $text
333 );
334
335 // Replacement for other case
336 $text = preg_replace(
337 '#\[wplng_dictionary key="' . $key . '" upper="none"\].+\[\/wplng_dictionary\]#Uu',
338 $replacement,
339 $text
340 );
341
342 }
343
344 // Cleaning of any residual rules
345 $text = preg_replace(
346 '#\[wplng_dictionary key=".*" upper=".*"\](.+)\[\/wplng_dictionary\]#Uu',
347 '${1}',
348 $text
349 );
350
351 $texts[ $text_key ] = $text;
352 }
353
354 return $texts;
355 }
356
357
358 /**
359 * Fired when the 'wplng_dictionary_entries' option is saved.
360 *
361 * Compares the old and new dictionary entries to determine which source texts
362 * and target languages are affected by the change. Builds $update_todo, a list
363 * of items whose cached translations need to be regenerated.
364 *
365 * Each item in $update_todo has the shape:
366 * [ 'source' => string, 'impacted_languages' => 'all' | string[] ]
367 *
368 * 'impacted_languages' is the string 'all' when the entry has no per-language
369 * rules (i.e., it is a "never translate" rule that applies to every language).
370 *
371 * @param string $old_value The previous JSON value of the option.
372 * @param string $new_value The new JSON value of the option.
373 */
374 function wplng_on_dictionary_entries_updated( $old_value, $new_value ) {
375
376 // Entries are stored as a JSON string in the WordPress options table.
377 // Decode them; fall back to an empty array if the value is absent or invalid.
378 $old_entries = json_decode( $old_value, true );
379 $new_entries = json_decode( $new_value, true );
380
381 if ( ! is_array( $old_entries ) ) {
382 $old_entries = array();
383 }
384
385 if ( ! is_array( $new_entries ) ) {
386 $new_entries = array();
387 }
388
389 // Re-index entries by their source text for O(1) lookup in the loops below.
390 // The dictionary is stored as a plain list; keying by source avoids nested loops.
391 $old_by_source = array();
392 foreach ( $old_entries as $entry ) {
393 if ( isset( $entry['source'] ) ) {
394 $old_by_source[ $entry['source'] ] = $entry;
395 }
396 }
397
398 $new_by_source = array();
399 foreach ( $new_entries as $entry ) {
400 if ( isset( $entry['source'] ) ) {
401 $new_by_source[ $entry['source'] ] = $entry;
402 }
403 }
404
405 // Will hold the list of source/language pairs whose translations need refreshing.
406 $update_todo = array();
407
408 /**
409 * Detect removed entries.
410 *
411 * When a rule is deleted, cached translations built with that rule may be
412 * outdated (e.g., a forced translation that should revert to the default
413 * AI output). All languages covered by the old rule must be refreshed.
414 */
415
416 foreach ( $old_by_source as $source => $old_entry ) {
417
418 // Skip entries that still exist in the new value.
419 if ( isset( $new_by_source[ $source ] ) ) {
420 continue;
421 }
422
423 $impacted_languages = array();
424
425 if ( empty( $old_entry['rules'] ) ) {
426 // No per-language rules → this was a "never translate" rule affecting all languages.
427 $impacted_languages = 'all';
428 } else {
429 // Collect every language that had a rule for this source.
430 foreach ( $old_entry['rules'] as $language_id => $rule ) {
431 $impacted_languages[] = $language_id;
432 }
433 }
434
435 $update_todo[] = array(
436 'source' => $source,
437 'impacted_languages' => $impacted_languages,
438 );
439 }
440
441 /**
442 * Detect added and modified entries
443 */
444
445 foreach ( $new_by_source as $source => $new_entry ) {
446 if ( ! isset( $old_by_source[ $source ] ) ) {
447
448 /**
449 * Added.
450 *
451 * Existing translations were produced without this rule, so they may
452 * not yet reflect the new forced translation or exclusion.
453 */
454
455 $impacted_languages = array();
456
457 if ( empty( $new_entry['rules'] ) ) {
458 // No per-language rules → "never translate" rule affecting all languages.
459 $impacted_languages = 'all';
460 } else {
461 // Collect every language that has a rule for this new source.
462 foreach ( $new_entry['rules'] as $language_id => $rule ) {
463 $impacted_languages[] = $language_id;
464 }
465 }
466
467 $update_todo[] = array(
468 'source' => $source,
469 'impacted_languages' => $impacted_languages,
470 );
471
472 } elseif ( $new_entry != $old_by_source[ $source ] ) {
473
474 /**
475 * Modified.
476 *
477 * Loose comparison (!=) is intentional: strict (!==) would treat arrays
478 * with the same key/value pairs but different key order as unequal,
479 * causing false positives when WordPress re-encodes the JSON.
480 */
481
482 $old_entry = $old_by_source[ $source ];
483 $impacted_languages = array();
484
485 if ( empty( $new_entry['rules'] ) || empty( $old_entry['rules'] )
486 || ! is_array( $new_entry['rules'] ) || ! is_array( $old_entry['rules'] )
487 ) {
488 // One side has no per-language rules, meaning the entry was or became a
489 // "never translate" rule — all languages are affected by the change.
490 $impacted_languages = 'all';
491 } else {
492 // Find every language whose rule value changed, was added, or was removed.
493 // array_diff_assoc() catches rules that changed value or were newly added.
494 // array_diff_key() catches rules that existed in the old entry but were removed.
495 // Merging both and taking the keys gives the full list of impacted languages.
496 $impacted_languages = array_keys(
497 array_merge(
498 array_diff_assoc( $new_entry['rules'], $old_entry['rules'] ),
499 array_diff_key( $old_entry['rules'], $new_entry['rules'] ),
500 )
501 );
502 }
503
504 if ( ! empty( $impacted_languages ) ) {
505 $update_todo[] = array(
506 'source' => $source,
507 'impacted_languages' => $impacted_languages,
508 );
509 }
510 }
511 }
512
513 if ( empty( $update_todo ) ) {
514 return;
515 }
516
517 // Normalize: if impacted_languages lists every active target language,
518 // collapse it to 'all' to avoid unnecessary per-language filtering later.
519 $all_target_ids = wplng_get_languages_target_ids();
520 foreach ( $update_todo as &$item ) {
521 if ( is_array( $item['impacted_languages'] )
522 && ! array_diff( $all_target_ids, $item['impacted_languages'] )
523 ) {
524 $item['impacted_languages'] = 'all';
525 }
526 }
527 unset( $item );
528
529 /**
530 * Get all translations
531 */
532
533 $translations = wplng_get_translations();
534 $translations_to_update = array();
535
536 // Foreach translations
537 foreach ( $translations as $index_strings => $translations_chunk ) {
538 foreach ( $translations_chunk as $key => $translation ) {
539
540 // Foreach $update_todo
541 foreach ( $update_todo as $todo ) {
542 $source_todo = mb_strtolower( $todo['source'] );
543 $source_translation = mb_strtolower( $translation['source'] );
544
545 // Use a simple substring match for CJK sources (no word boundaries in CJK text).
546 // For all other scripts, require word boundaries to avoid false matches (e.g. "men" in "women").
547 $is_cjk = (bool) preg_match( '#[\x{2E80}-\x{9FFF}\x{F900}-\x{FAFF}]#u', $source_todo );
548 $pattern = $is_cjk
549 ? '#' . preg_quote( $source_todo, '#' ) . '#iu'
550 : '#(*UCP)\b' . preg_quote( $source_todo, '#' ) . '\b#iu';
551
552 if ( ! wplng_str_contains( $source_translation, $source_todo )
553 || ! preg_match( $pattern, $source_translation )
554 ) {
555 continue;
556 }
557
558 $impacted_languages = 'all';
559
560 $translation_review = $translation['review'] ?? array();
561
562 if ( $todo['impacted_languages'] !== 'all' ) {
563
564 $impacted_languages = array();
565
566 foreach ( $translation['translations'] as $language_id => $value ) {
567 if ( in_array( $language_id, $todo['impacted_languages'], true )
568 && ! in_array( $language_id, $translation_review, true )
569 ) {
570 $impacted_languages[] = $language_id;
571 }
572 }
573 } else {
574
575 // Even when the dictionary rule impacts every language, translations
576 // that were manually reviewed must still be protected from being
577 // flagged for automatic regeneration.
578 $impacted_languages = array();
579
580 foreach ( $translation['translations'] as $language_id => $value ) {
581 if ( ! in_array( $language_id, $translation_review, true ) ) {
582 $impacted_languages[] = $language_id;
583 }
584 }
585 }
586
587 // If impacted_languages covers every language that has been translated,
588 // collapse it to 'all' to avoid unnecessary per-language filtering later.
589 if ( is_array( $impacted_languages ) ) {
590 $temp_translations = $translation['translations'];
591 foreach ( $impacted_languages as $language_id ) {
592 unset( $temp_translations[ $language_id ] );
593 }
594 if ( empty( $temp_translations ) ) {
595 $impacted_languages = 'all';
596 }
597 }
598
599 $translations_to_update[] = array(
600 'source' => $translation['source'],
601 'post_id' => $translation['post_id'],
602 'impacted_languages' => $impacted_languages,
603 );
604
605 }
606 }
607 }
608
609 // Clear $translations_to_update when no loanguage impacted
610 foreach ( $translations_to_update as $key => $value ) {
611 if ( empty( $value['impacted_languages'] ) ) {
612 unset( $translations_to_update[ $key ] );
613 }
614 }
615
616 // Deduplicate entries by post_id: the same translation can match several
617 // dictionary rules, producing multiple entries for the same post. Merge
618 // their impacted_languages (union), collapsing to 'all' if any entry is 'all'.
619 $translations_to_update_by_post = array();
620
621 foreach ( $translations_to_update as $value ) {
622
623 $post_id = $value['post_id'];
624
625 if ( ! isset( $translations_to_update_by_post[ $post_id ] ) ) {
626 $translations_to_update_by_post[ $post_id ] = $value;
627 continue;
628 }
629
630 $existing = $translations_to_update_by_post[ $post_id ];
631
632 if ( 'all' === $existing['impacted_languages']
633 || 'all' === $value['impacted_languages']
634 ) {
635 $translations_to_update_by_post[ $post_id ]['impacted_languages'] = 'all';
636 } else {
637 $translations_to_update_by_post[ $post_id ]['impacted_languages'] = array_values(
638 array_unique(
639 array_merge( $existing['impacted_languages'], $value['impacted_languages'] )
640 )
641 );
642 }
643 }
644
645 $translations_to_update = array_values( $translations_to_update_by_post );
646
647 if ( ! empty( $translations_to_update ) ) {
648 // Store the list in a short-lived transient so it survives the redirect
649 // that options.php performs after saving. The page function reads and
650 // immediately deletes it so it is only displayed once.
651 set_transient( 'wplng_dictionary_updated_data', $translations_to_update, 30 );
652 }
653 }
654
655
656 /**
657 * AJAX handler: process one translation update triggered by a dictionary rule change.
658 *
659 * Receives post_id and impacted_languages from the JS queue.
660 * For now, echoes the received data back so the JS loop can be validated end-to-end.
661 *
662 * @return void
663 */
664 function wplng_ajax_dictionary_update_translations() {
665
666 // ------------------------------------------------------------------------
667 // Get and check basic data
668 // ------------------------------------------------------------------------
669
670 /**
671 * Check the nonce to prevent unauthorized requests
672 */
673
674 check_ajax_referer( 'wplng_dictionary_update_translations', 'nonce' );
675
676 /**
677 * Check the user's permissions
678 */
679
680 if ( ! current_user_can( 'manage_options' ) ) {
681 wp_send_json_error(
682 array(
683 'error' => true,
684 'message' => __( 'Error [1]: The current user is not authorized to edit translations', 'wplingua' ),
685 )
686 );
687 return;
688 }
689
690 /**
691 * Check the post ID parameter
692 */
693
694 if ( empty( $_POST['post_id'] ) ) {
695 wp_send_json_error(
696 array(
697 'error' => true,
698 'message' => __( 'Error [2]: Invalid post ID parameter', 'wplingua' ),
699 )
700 );
701 return;
702 }
703
704 $post_id = intval( $_POST['post_id'] );
705
706 /**
707 * Check "impacted_languages" parameter
708 */
709
710 if ( empty( $_POST['impacted_languages'] )
711 || ! is_string( $_POST['impacted_languages'] )
712 ) {
713 wp_send_json_error(
714 array(
715 'error' => true,
716 'message' => __( 'Error [3]: Invalid impacted languages parameter', 'wplingua' ),
717 )
718 );
719 return;
720 }
721
722 $impacted_languages = sanitize_text_field( wp_unslash( $_POST['impacted_languages'] ) );
723
724 // Keep the raw (pre-decode) string: it is what was used to build the
725 // "check" value server-side, and is needed below to verify it again.
726 $impacted_languages_raw = $impacted_languages;
727
728 if ( $impacted_languages !== 'all' ) {
729
730 // We suppose it's a JSON with the list of impacted languages IDs
731 $impacted_languages = json_decode( $impacted_languages, true );
732
733 if ( ! is_array( $impacted_languages )
734 || ! wplng_is_valid_language_ids( $impacted_languages )
735 ) {
736 wp_send_json_error(
737 array(
738 'error' => true,
739 'message' => __( 'Error [4]: Invalid impacted languages data', 'wplingua' ),
740 )
741 );
742 return;
743 }
744 }
745
746 /**
747 * Check the "check" parameter.
748 *
749 * It is an encrypted copy of "post_id-impacted_languages" generated when
750 * the HTML was rendered (see wplng_option_page_dictionary_update_translations_html()).
751 * Decrypting it and comparing it to the values actually received prevents
752 * the browser (or an attacker via devtools) from tampering with post_id
753 * or impacted_languages before the request is sent.
754 */
755
756 if ( empty( $_POST['check'] ) || ! is_string( $_POST['check'] ) ) {
757 wp_send_json_error(
758 array(
759 'error' => true,
760 'message' => __( 'Error [5]: Parameter "check" not found', 'wplingua' ),
761 )
762 );
763 return;
764 }
765
766 $check_received = wplng_encryption_decrypt( sanitize_text_field( wp_unslash( $_POST['check'] ) ) );
767 $check_expected = $post_id . '-' . $impacted_languages_raw;
768
769 if ( '' === $check_received || ! hash_equals( $check_expected, $check_received ) ) {
770 wp_send_json_error(
771 array(
772 'error' => true,
773 'message' => __( 'Error [6]: Invalid "check" parameter', 'wplingua' ),
774 )
775 );
776 return;
777 }
778
779 /**
780 * Get and check the translation post
781 */
782
783 $post_translation = get_post( $post_id, ARRAY_A );
784
785 if ( ! isset( $post_translation['post_type'] )
786 || $post_translation['post_type'] !== 'wplng_translation'
787 ) {
788 wp_send_json_error(
789 array(
790 'error' => true,
791 'message' => __( 'Error [7]: Invalid post ID or post type', 'wplingua' ),
792 )
793 );
794 return;
795 }
796
797 // ------------------------------------------------------------------------
798 // Apply the update to the relevant translation
799 // ------------------------------------------------------------------------
800
801 if ( $impacted_languages === 'all' ) {
802
803 // ------------------------------------------------------------------------
804 // Delete the post if all languages are impacted
805 // ------------------------------------------------------------------------
806
807 if ( empty( wp_delete_post( $post_id, true ) ) ) {
808 wp_send_json_error(
809 array(
810 'error' => true,
811 'message' => __( 'Error [8]: Failed to delete the translation', 'wplingua' ),
812 )
813 );
814 return;
815 }
816
817 wp_send_json_success(
818 array(
819 'post_id' => $post_id,
820 'impacted_languages' => $impacted_languages,
821 )
822 );
823 return;
824
825 } else {
826
827 // ------------------------------------------------------------------------
828 // Clear impacted translation in translation post
829 // ------------------------------------------------------------------------
830
831 /**
832 * Check post meta : All meta
833 */
834
835 $meta = get_post_meta( $post_id );
836
837 if ( ! isset( $meta['wplng_translation_translations'][0] )
838 || ! is_string( $meta['wplng_translation_translations'][0] )
839 ) {
840 wp_send_json_error(
841 array(
842 'error' => true,
843 'message' => __( 'Error [9]: Translation meta "translations" not found', 'wplingua' ),
844 )
845 );
846 return;
847 }
848
849 /**
850 * Check post meta : Translations meta
851 */
852
853 $translation_meta = json_decode( $meta['wplng_translation_translations'][0], true );
854
855 if ( empty( $translation_meta ) || ! is_array( $translation_meta ) ) {
856 wp_send_json_error(
857 array(
858 'error' => true,
859 'message' => __( 'Error [10]: Invalid translation meta', 'wplingua' ),
860 )
861 );
862 return;
863 }
864
865 /**
866 * Check post meta : Original language
867 */
868
869 if ( empty( $meta['wplng_translation_original_language_id'][0] ) ) {
870 wp_send_json_error(
871 array(
872 'error' => true,
873 'message' => __( 'Error [11]: Translation meta "original language" not found', 'wplingua' ),
874 )
875 );
876 return;
877 }
878
879 $original_language_id_meta = $meta['wplng_translation_original_language_id'][0];
880
881 if ( $original_language_id_meta !== wplng_get_language_website_id() ) {
882 wp_send_json_error(
883 array(
884 'error' => true,
885 'message' => __( 'Error [12]: The language of the translation is not the same as the language of the site\'s settings', 'wplingua' ),
886 )
887 );
888 return;
889 }
890
891 /**
892 * Generate the new translation meta
893 */
894
895 $translation_meta_new = array();
896
897 foreach ( $translation_meta as $key => $value ) {
898 if ( ! isset( $value['language_id'] )
899 || ! wplng_is_valid_language_id( $value['language_id'] )
900 || ! isset( $value['translation'] )
901 || ! is_string( $value['translation'] )
902 ) {
903 continue;
904 }
905
906 if ( ! in_array( $value['language_id'], $impacted_languages, true )
907 || ! isset( $value['status'] )
908 || $value['status'] !== 'generated'
909 ) {
910 $translation_meta_new[] = $value;
911 }
912 }
913
914 /**
915 * Update the translation meta
916 */
917
918 if ( empty( $translation_meta_new ) ) {
919
920 /**
921 * Delete the translation post if all translation is finaly impacted
922 */
923 if ( empty( wp_delete_post( $post_id, true ) ) ) {
924 wp_send_json_error(
925 array(
926 'error' => true,
927 'message' => __( 'Error [13]: Failed to delete the translation', 'wplingua' ),
928 )
929 );
930 return;
931 }
932 } else {
933
934 /**
935 * Update the translations post meta
936 */
937
938 $meta_return = update_post_meta(
939 $post_id,
940 'wplng_translation_translations',
941 wp_json_encode(
942 $translation_meta_new,
943 JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
944 )
945 );
946
947 if ( false === $meta_return ) {
948 wp_send_json_error(
949 array(
950 'error' => true,
951 'message' => __( 'Error [14]: Failed to update the translation meta', 'wplingua' ),
952 )
953 );
954 return;
955 }
956
957 // Clear cache explicitly.
958 wplng_clear_translations_cache();
959 }
960 }
961
962 wp_send_json_success(
963 array(
964 'post_id' => $post_id,
965 'impacted_languages' => $impacted_languages,
966 )
967 );
968 }
969