PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.5.4
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.5.4
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.5.4, at includes/Admin/WPMLSupport.php

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