PluginProbe
Translate and Go multilingual – Automatic AI translation – wpLingua / 2.16.7
Translate and Go multilingual – Automatic AI translation – wpLingua v2.16.7
2.16.7 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 All 112 releases
← All changes | inc/dictionary.php +663 -18 2.13.02.16.7 View file →
@@ -28,9 +28,9 @@
28 28 */
29 29
30 30 if ( ! isset( $entry['source'] )
31 31 || ! is_string( $entry['source'] )
32 - || strlen( $entry['source'] ) >= 256
32 + || mb_strlen( $entry['source'] ) >= 256
33 33 ) {
34 34 continue;
35 35 }
36 36
@@ -79,9 +79,9 @@
79 79 if ( ! wplng_is_valid_language_id( $language_id )
80 80 || ! is_string( $rule )
81 81 || '' === trim( $rule )
82 82 || $rule === $source_clear
83 - || strlen( $rule ) >= 256
83 + || mb_strlen( $rule ) >= 256
84 84 ) {
85 85 continue;
86 86 }
87 87
@@ -110,9 +110,9 @@
110 110
111 111 usort(
112 112 $entries_clear,
113 113 function ( $a, $b ) {
114 - return strlen( $b['source'] ) - strlen( $a['source'] );
114 + return mb_strlen( $b['source'] ) - mb_strlen( $a['source'] );
115 115 }
116 116 );
117 117
118 118 /**
@@ -158,9 +158,9 @@
158 158 * Preg quote sources texts
159 159 */
160 160
161 161 foreach ( $dictionary_entries as $entry_key => $entry ) {
162 - $dictionary_entries[ $entry_key ]['source'] = preg_quote( $entry['source'] );
162 + $dictionary_entries[ $entry_key ]['source'] = preg_quote( $entry['source'], '#' );
163 163 }
164 164
165 165 foreach ( $texts as $text_key => $text ) {
166 166
@@ -182,9 +182,9 @@
182 182
183 183 $preg_match = array();
184 184
185 185 preg_match_all(
186 - '#' . $entry['source'] . '#i',
186 + '#' . $entry['source'] . '#iu',
187 187 $text,
188 188 $preg_match
189 189 );
190 190
@@ -192,14 +192,20 @@
192 192
193 193 foreach ( $preg_match as $key => $match ) {
194 194
195 195 $upper = 'none';
196 - if ( $match === strtoupper( $match ) ) {
197 - // Check uppercase
198 - $upper = 'all';
199 - } elseif ( $match === ucfirst( $match ) ) {
200 - // Check capitalize
201 - $upper = 'first';
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 + }
202 208 }
203 209
204 210 $entry_current = $entry;
205 211 $entry_current['source'] = $match;
@@ -221,10 +227,18 @@
221 227 * Put tempory tag in current text
222 228 */
223 229
224 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 + }
225 239 $text = preg_replace(
226 - '#\b' . $entry_used['source'] . '\b#',
240 + $boundary_pattern,
227 241 '⊕' . str_repeat( '⊖', $entry_key + 1 ) . '⊕',
228 242 $text
229 243 );
230 244 }
@@ -275,8 +289,26 @@
275 289 $dictionary_entries = wplng_dictionary_get_entries();
276 290 }
277 291
278 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 +
279 311 foreach ( $dictionary_entries as $key => $entry ) {
280 312
281 313 // For ruls "Do not translate
282 314 $replacement = $entry['source'];
@@ -287,23 +319,23 @@
287 319 }
288 320
289 321 // Replacement for text in uppercase
290 322 $text = preg_replace(
291 - '#\[wplng_dictionary key="' . $key . '" upper="all"\].+\[\/wplng_dictionary\]#U',
292 - strtoupper( $replacement ),
323 + '#\[wplng_dictionary key="' . $key . '" upper="all"\].+\[\/wplng_dictionary\]#Uu',
324 + mb_strtoupper( $replacement ),
293 325 $text
294 326 );
295 327
296 328 // Replacement for text when only the first letter is uppercase
297 329 $text = preg_replace(
298 - '#\[wplng_dictionary key="' . $key . '" upper="first"\].+\[\/wplng_dictionary\]#U',
299 - ucfirst( $replacement ),
330 + '#\[wplng_dictionary key="' . $key . '" upper="first"\].+\[\/wplng_dictionary\]#Uu',
331 + mb_strtoupper( mb_substr( $replacement, 0, 1 ) ) . mb_substr( $replacement, 1 ),
300 332 $text
301 333 );
302 334
303 335 // Replacement for other case
304 336 $text = preg_replace(
305 - '#\[wplng_dictionary key="' . $key . '" upper="none"\].+\[\/wplng_dictionary\]#U',
337 + '#\[wplng_dictionary key="' . $key . '" upper="none"\].+\[\/wplng_dictionary\]#Uu',
306 338 $replacement,
307 339 $text
308 340 );
309 341
@@ -310,9 +342,9 @@
310 342 }
311 343
312 344 // Cleaning of any residual rules
313 345 $text = preg_replace(
314 - '#\[wplng_dictionary key=".*" upper=".*"\](.+)\[\/wplng_dictionary\]#U',
346 + '#\[wplng_dictionary key=".*" upper=".*"\](.+)\[\/wplng_dictionary\]#Uu',
315 347 '${1}',
316 348 $text
317 349 );
318 350
@@ -319,5 +351,618 @@
319 351 $texts[ $text_key ] = $text;
320 352 }
321 353
322 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 + );
323 968 }