PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 All 200 releases
← All changes | includes/REST/Translations.php +93 -0 4.6.04.9.2 View file →
@@ -32,8 +32,101 @@
32 32
33 33 public function register() {
34 34 $this->get( 'term-translations', [ $this, 'get_term_translations' ] );
35 35 $this->post( 'term-language', [ $this, 'set_term_language' ] );
36 +
37 + add_filter( 'rest_request_before_callbacks', [ $this, 'switch_language_for_term_update' ], 10, 3 );
38 + }
39 +
40 + /**
41 + * WPML lets translated terms share one slug across languages, but it
42 + * deliberately skips its language filtering whenever get_term_by() is in the
43 + * call stack (WPML_Term_Clauses::filter). Core wp_update_term() uses exactly
44 + * that lookup for its duplicate-slug check, so updating a term whose slug has
45 + * an older other-language twin via the core REST controllers always fails
46 + * with duplicate_term_slug — even when the payload never touches the slug
47 + * (e.g. changing only the KB order from the React admin). While such a
48 + * request is being dispatched, restrict slug lookups on the edited taxonomy
49 + * to the edited term's own language: the term matches itself, and genuine
50 + * same-language collisions are still rejected.
51 + *
52 + * The term's language is read from its term_taxonomy_id directly because
53 + * get_term() under WPML adjusts the ID to the current display language's
54 + * translation, which would report the wrong language here.
55 + */
56 + public function switch_language_for_term_update( $response, $handler, $request ) {
57 + global $wpdb;
58 +
59 + if ( ! $request instanceof WP_REST_Request || ! Helper::is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
60 + return $response;
61 + }
62 +
63 + if ( ! in_array( $request->get_method(), [ 'POST', 'PUT', 'PATCH' ], true ) ) {
64 + return $response;
65 + }
66 +
67 + if ( ! preg_match( '#^/wp/v2/(doc_category|doc_tag|knowledge_base)/(\d+)$#', $request->get_route(), $matches ) ) {
68 + return $response;
69 + }
70 +
71 + $taxonomy = $matches[1];
72 + $ttid = $wpdb->get_var(
73 + $wpdb->prepare(
74 + "SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE term_id = %d AND taxonomy = %s",
75 + absint( $matches[2] ),
76 + $taxonomy
77 + )
78 + );
79 + if ( ! $ttid ) {
80 + return $response;
81 + }
82 +
83 + $term_lang = apply_filters( 'wpml_element_language_code', null, [
84 + 'element_id' => (int) $ttid,
85 + 'element_type' => $taxonomy,
86 + ] );
87 + if ( ! $term_lang ) {
88 + return $response;
89 + }
90 +
91 + $clauses_filter = function ( $clauses, $taxonomies, $args ) use ( $taxonomy, $term_lang ) {
92 + global $wpdb;
93 +
94 + // Only slug lookups on the edited taxonomy (the duplicate-slug check).
95 + if ( empty( $args['slug'] ) || [ $taxonomy ] !== (array) $taxonomies ) {
96 + return $clauses;
97 + }
98 +
99 + $clauses['join'] .= " LEFT JOIN {$wpdb->prefix}icl_translations bd_icl_t ON bd_icl_t.element_id = tt.term_taxonomy_id AND bd_icl_t.element_type = CONCAT( 'tax_', tt.taxonomy )";
100 + $clauses['where'] .= $wpdb->prepare( ' AND ( bd_icl_t.language_code = %s OR bd_icl_t.language_code IS NULL )', $term_lang );
101 +
102 + return $clauses;
103 + };
104 + add_filter( 'terms_clauses', $clauses_filter, 10, 3 );
105 +
106 + // WPML's save hooks stamp edited terms with the *current* language, so a
107 + // request arriving in another language context would silently reassign
108 + // the term's language. Align the context with the edited term.
109 + $prev_lang = apply_filters( 'wpml_current_language', null );
110 + if ( $term_lang !== $prev_lang ) {
111 + do_action( 'wpml_switch_language', $term_lang );
112 + }
113 +
114 + // Scope the workaround to this callback only: undo both the clause
115 + // filter and the language switch once the targeted update has run, so
116 + // nothing leaks into batch subrequests or response-side hooks.
117 + $cleanup = function ( $after_response ) use ( &$cleanup, $clauses_filter, $term_lang, $prev_lang ) {
118 + remove_filter( 'terms_clauses', $clauses_filter, 10 );
119 + if ( $term_lang !== $prev_lang ) {
120 + do_action( 'wpml_switch_language', $prev_lang );
121 + }
122 + remove_filter( 'rest_request_after_callbacks', $cleanup, 10 );
123 +
124 + return $after_response;
125 + };
126 + add_filter( 'rest_request_after_callbacks', $cleanup, 10 );
127 +
128 + return $response;
36 129 }
37 130
38 131 /**
39 132 * Translation group + "this is a translation of" candidates for a term (or for