PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.8.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.8.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 3.5.2 All 199 releases
betterdocs / includes / Admin / WPMLSupport.php

WPMLSupport.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.8.2, at includes/Admin/WPMLSupport.php

205 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace WPDeveloper\BetterDocs\Admin;
3
4 use WP_Post;
5
6 if ( ! defined( 'ABSPATH' ) ) {
7 exit;
8 }
9
10 // This class integrates with WPML's documented hook API (wpml_post_language_details,
11 // wpml_element_trid, wpml_get_element_translations, etc.). Those hook names are owned
12 // by WPML and must be used verbatim, so the plugin-prefix rule does not apply here.
13 // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
14
15 /**
16 * Helpers for round-tripping WPML language data through BetterDocs
17 * export / import. WPML stores per-post language data in `icl_translations`,
18 * which the regular WXR / CSV export doesn't touch — so without this layer
19 * imported translations come in as plain posts and lose their translation
20 * group on the target site.
21 *
22 * Transport meta keys (synthetic — never persisted to the DB):
23 * - META_LANG: language code of this post (e.g. "en", "de").
24 * - META_SOURCE_SLUG: slug of the source post when this row is a translation.
25 * Empty when this row IS the source/original.
26 */
27 class WPMLSupport {
28 const META_LANG = '_betterdocs_wpml_lang';
29 const META_SOURCE_SLUG = '_betterdocs_wpml_source_slug';
30
31 public static function is_active(): bool {
32 return defined( 'ICL_SITEPRESS_VERSION' );
33 }
34
35 /**
36 * Read WPML language details for one post. Returns null when WPML is off,
37 * the post type isn't translatable, or no language details exist.
38 *
39 * @return array{language_code:string, source_slug:string}|null
40 */
41 public static function get_post_language_meta( int $post_id ): ?array {
42 if ( ! self::is_active() || $post_id <= 0 ) {
43 return null;
44 }
45
46 $post_type = get_post_type( $post_id );
47 if ( ! $post_type ) {
48 return null;
49 }
50
51 $details = apply_filters( 'wpml_post_language_details', null, $post_id );
52 if ( ! is_array( $details ) && ! is_object( $details ) ) {
53 return null;
54 }
55 $details = (array) $details;
56
57 $language_code = isset( $details['language_code'] ) ? (string) $details['language_code'] : '';
58 if ( $language_code === '' ) {
59 return null;
60 }
61
62 // The `wpml_post_language_details` filter only exposes language_code /
63 // locale / display_name / etc. — it has NO source_language_code key, so
64 // we can't learn the source language from it. Resolve the whole
65 // translation group instead: `wpml_get_element_translations` returns one
66 // row per language, each carrying its own `source_language_code`
67 // (null/empty for the original). Find this post's own row to read its
68 // source language, then pull the slug of the row in that language.
69 $source_slug = '';
70 $element_type = 'post_' . $post_type;
71 $trid = apply_filters( 'wpml_element_trid', null, $post_id, $element_type );
72 if ( $trid ) {
73 $translations = apply_filters( 'wpml_get_element_translations', null, $trid, $element_type );
74 if ( is_array( $translations ) ) {
75 $source_lang = '';
76 foreach ( $translations as $t ) {
77 if ( isset( $t->element_id ) && (int) $t->element_id === $post_id ) {
78 $source_lang = isset( $t->source_language_code ) ? (string) $t->source_language_code : '';
79 break;
80 }
81 }
82
83 if ( $source_lang !== '' ) {
84 foreach ( $translations as $t ) {
85 $t_lang = isset( $t->language_code ) ? (string) $t->language_code : '';
86 $t_id = isset( $t->element_id ) ? (int) $t->element_id : 0;
87 if ( $t_lang === $source_lang && $t_id > 0 ) {
88 $src = get_post( $t_id );
89 if ( $src instanceof WP_Post ) {
90 $source_slug = $src->post_name;
91 }
92 break;
93 }
94 }
95 }
96 }
97 }
98
99 return [
100 'language_code' => $language_code,
101 'source_slug' => $source_slug,
102 ];
103 }
104
105 /**
106 * Assign WPML language to a freshly-imported post.
107 *
108 * When $source_slug is given, looks up the local source post by slug,
109 * fetches its trid, and registers this post as a translation in that
110 * group. When $source_slug is empty or the source isn't found locally,
111 * WPML creates a fresh trid (the post becomes a new source).
112 */
113 public static function assign_post_language( int $post_id, string $language_code, string $source_slug = '' ): bool {
114 if ( ! self::is_active() || $post_id <= 0 || $language_code === '' ) {
115 return false;
116 }
117
118 global $sitepress;
119 if ( ! is_object( $sitepress ) || ! method_exists( $sitepress, 'set_element_language_details' ) ) {
120 return false;
121 }
122
123 $post_type = get_post_type( $post_id );
124 if ( ! $post_type ) {
125 return false;
126 }
127 $element_type = 'post_' . $post_type;
128
129 $trid = null;
130 $source_lang = null;
131 if ( $source_slug !== '' ) {
132 $source_post = self::find_post_by_slug( $source_slug, $post_type );
133 if ( $source_post instanceof WP_Post ) {
134 $trid = apply_filters( 'wpml_element_trid', null, $source_post->ID, $element_type );
135 if ( ! $trid ) {
136 $trid = null;
137 }
138
139 $src_details = apply_filters( 'wpml_post_language_details', null, $source_post->ID );
140 if ( is_array( $src_details ) || is_object( $src_details ) ) {
141 $src_details = (array) $src_details;
142 if ( ! empty( $src_details['language_code'] ) ) {
143 $source_lang = (string) $src_details['language_code'];
144 }
145 }
146 }
147 }
148
149 $sitepress->set_element_language_details( $post_id, $element_type, $trid, $language_code, $source_lang );
150 return true;
151 }
152
153 /**
154 * Expand a post ID list to include all WPML translations of each post.
155 *
156 * Raw SQL queries (and `get_posts` calls without `suppress_filters`) only
157 * return the active-language row, so translations would otherwise be
158 * silently dropped from the export. No-op when WPML is inactive.
159 *
160 * @param int[] $post_ids
161 * @return int[]
162 */
163 public static function expand_with_translations( array $post_ids ): array {
164 if ( empty( $post_ids ) || ! self::is_active() ) {
165 return $post_ids;
166 }
167
168 $expanded = $post_ids;
169 foreach ( $post_ids as $post_id ) {
170 $post_type = get_post_type( $post_id );
171 if ( ! $post_type ) {
172 continue;
173 }
174 $element_type = 'post_' . $post_type;
175 $trid = apply_filters( 'wpml_element_trid', null, (int) $post_id, $element_type );
176 if ( ! $trid ) {
177 continue;
178 }
179 $translations = apply_filters( 'wpml_get_element_translations', null, $trid, $element_type );
180 if ( ! is_array( $translations ) ) {
181 continue;
182 }
183 foreach ( $translations as $translation ) {
184 if ( ! empty( $translation->element_id ) ) {
185 $expanded[] = (int) $translation->element_id;
186 }
187 }
188 }
189
190 return array_values( array_unique( array_map( 'intval', $expanded ) ) );
191 }
192
193 private static function find_post_by_slug( string $slug, string $post_type ): ?WP_Post {
194 $posts = get_posts( [
195 'name' => $slug,
196 'post_type' => $post_type,
197 'post_status' => 'any',
198 'posts_per_page' => 1,
199 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- intentional: bypass WPML's own query filters to resolve the untranslated source post and avoid recursion in the WPML support layer.
200 'suppress_filters' => true,
201 ] );
202 return ! empty( $posts ) ? $posts[0] : null;
203 }
204 }
205