PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.1.4
AI Builder – Generate pages, blocks, images & translate with AI v2.1.4
2.7.10 2.7.9 2.7.8 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.3.10 All 122 releases
ai-builder / includes / class-translation-manager.php

class-translation-manager.php in AI Builder – Generate pages, blocks, images & translate with AI 2.1.4, at includes/class-translation-manager.php

452 lines 14.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 class AIBUI_Translation_Manager
8 {
9 private $settings;
10 private $current_lang;
11 private $has_explicit_lang_preference = false;
12
13 public function __construct()
14 {
15 $this->settings = AIBUI_Translation_Settings::get_settings();
16 $this->current_lang = $this->determine_current_language();
17
18 if (is_admin()) {
19 add_filter('manage_edit-post_columns', array($this, 'add_language_column'));
20 add_filter('manage_edit-page_columns', array($this, 'add_language_column'));
21 add_filter('manage_edit-wp_template_part_columns', array($this, 'add_language_column'));
22
23 add_action('manage_post_posts_custom_column', array($this, 'render_language_column'), 10, 2);
24 add_action('manage_page_posts_custom_column', array($this, 'render_language_column'), 10, 2);
25 add_action('manage_wp_template_part_posts_custom_column', array($this, 'render_language_column'), 10, 2);
26
27 add_action('restrict_manage_posts', array($this, 'add_language_filter'));
28 add_action('pre_get_posts', array($this, 'filter_admin_by_language'));
29 add_action('admin_head', array($this, 'print_admin_styles'));
30
31 // Ensure "View" links in admin (list + editor) include the correct ai_lang
32 add_filter('post_type_link', array($this, 'add_lang_query_to_admin_view_link'), 10, 2);
33 add_filter('post_link', array($this, 'add_lang_query_to_admin_view_link'), 10, 3);
34 add_filter('page_link', array($this, 'add_lang_query_to_admin_view_link'), 10, 3);
35 } else {
36 add_filter('query_vars', array($this, 'register_query_var'));
37 add_action('template_redirect', array($this, 'maybe_redirect_to_translation'));
38 add_action('pre_get_posts', array($this, 'filter_front_queries'));
39 add_action('wp_head', array($this, 'output_hreflang_tags'), 5);
40 add_filter('language_attributes', array($this, 'filter_language_attributes'), 10, 2);
41 add_filter('pre_render_block', array($this, 'maybe_swap_template_part'), 10, 2);
42 }
43 }
44
45 public function get_current_language()
46 {
47 return $this->current_lang;
48 }
49
50 public function filter_language_attributes($output, $doctype)
51 {
52 if ($doctype !== 'html') {
53 return $output;
54 }
55
56 $lang = $this->current_lang;
57 $dir = in_array($lang, array('ar', 'he', 'fa'), true) ? 'rtl' : 'ltr';
58
59 return sprintf('lang="%s" dir="%s"', esc_attr($lang), esc_attr($dir));
60 }
61
62 public function get_default_language()
63 {
64 return $this->settings['default_lang'];
65 }
66
67 public function is_switcher_enabled()
68 {
69 return !empty($this->settings['enable_switcher']);
70 }
71
72 public function get_available_languages()
73 {
74 return $this->settings['available_langs'];
75 }
76
77 public function get_settings()
78 {
79 return $this->settings;
80 }
81
82 /**
83 * In the admin, make sure "View" links include the post language as ?ai_lang=...
84 * so that designers land on the matching front-end version and are not redirected.
85 */
86 public function add_lang_query_to_admin_view_link($permalink, $post, $leavename = null)
87 {
88 if (!is_admin() || empty($post)) {
89 return $permalink;
90 }
91
92 // $post may be an object or an ID depending on the filter (post_link/page_link/post_type_link)
93 if (!is_object($post)) {
94 $post = get_post($post);
95 }
96 if (!$post) {
97 return $permalink;
98 }
99
100 $default_lang = $this->settings['default_lang'];
101 $lang = get_post_meta($post->ID, '_ai_lang', true) ?: $default_lang;
102
103 // Avoid duplicating the parameter if it's already there.
104 $permalink = remove_query_arg('ai_lang', $permalink);
105 return add_query_arg('ai_lang', $lang, $permalink);
106 }
107
108 private function determine_current_language()
109 {
110 $available = $this->settings['available_langs'];
111 $default = $this->settings['default_lang'];
112 $lang = $default;
113
114 if (isset($_GET['ai_lang'])) {
115 $candidate = sanitize_text_field(wp_unslash($_GET['ai_lang']));
116 if (in_array($candidate, $available, true)) {
117 $lang = $candidate;
118 $this->has_explicit_lang_preference = true;
119 setcookie('aibui_lang', $lang, time() + MONTH_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
120 }
121 } elseif (isset($_COOKIE['aibui_lang'])) {
122 $candidate = sanitize_text_field(wp_unslash($_COOKIE['aibui_lang']));
123 if (in_array($candidate, $available, true)) {
124 $lang = $candidate;
125 $this->has_explicit_lang_preference = true;
126 }
127 }
128
129 return $lang;
130 }
131
132 /** Admin columns & filters */
133 public function add_language_column($columns)
134 {
135 $columns['aibui_translations'] = __('Translations', 'ai-builder');
136 return $columns;
137 }
138
139 public function render_language_column($column, $post_id)
140 {
141 if ($column !== 'aibui_translations') {
142 return;
143 }
144
145 $default = $this->settings['default_lang'];
146 $current_lang = get_post_meta($post_id, '_ai_lang', true) ?: $default;
147 $current_label = AIBUI_Translation_Handler::SUPPORTED_LANGUAGES[$current_lang] ?? strtoupper($current_lang);
148
149 $items = array(
150 sprintf('<span class="aibui-current-lang">%s</span>', esc_html($current_label))
151 );
152
153 $group = get_post_meta($post_id, '_ai_translation_group', true);
154 if ($group) {
155 $translations = $this->get_group_posts($group, get_post_type($post_id));
156 foreach ($translations as $translation) {
157 if ((int) $translation->ID === (int) $post_id) {
158 continue;
159 }
160 $lang = get_post_meta($translation->ID, '_ai_lang', true) ?: $default;
161 $label = AIBUI_Translation_Handler::SUPPORTED_LANGUAGES[$lang] ?? strtoupper($lang);
162 $items[] = sprintf(
163 '<a class="aibui-lang-link" href="%s">%s</a>',
164 esc_url(get_edit_post_link($translation->ID)),
165 esc_html($label)
166 );
167 }
168 }
169
170 echo implode(' | ', $items);
171 }
172
173 public function print_admin_styles()
174 {
175 $screen = get_current_screen();
176 if (!$screen || !in_array($screen->id, array('edit-page', 'edit-post', 'edit-wp_template_part'), true)) {
177 return;
178 }
179
180 ?>
181 <style>
182 .column-aibui_translations {
183 width: 10%;
184 }
185 .aibui-current-lang {
186 font-weight: 600;
187 color: #007cba;
188 }
189 .aibui-lang-link {
190 text-decoration: none;
191 }
192 </style>
193 <?php
194 }
195
196 public function add_language_filter($post_type)
197 {
198 if (!in_array($post_type, array('post', 'page', 'wp_template_part'), true)) {
199 return;
200 }
201
202 $selected = isset($_GET['aibui_lang']) ? sanitize_text_field(wp_unslash($_GET['aibui_lang'])) : '';
203 ?>
204 <select name="aibui_lang" id="aibui_lang">
205 <option value=""><?php esc_html_e('All languages', 'ai-builder'); ?></option>
206 <?php foreach (AIBUI_Translation_Handler::SUPPORTED_LANGUAGES as $code => $label) : ?>
207 <option value="<?php echo esc_attr($code); ?>" <?php selected($selected, $code); ?>>
208 <?php echo esc_html($label); ?>
209 </option>
210 <?php endforeach; ?>
211 </select>
212 <?php
213 }
214
215 public function filter_admin_by_language($query)
216 {
217 if (!is_admin() || !$query->is_main_query()) {
218 return;
219 }
220
221 global $pagenow;
222 if (!in_array($pagenow, array('edit.php'), true)) {
223 return;
224 }
225
226 $lang = isset($_GET['aibui_lang']) ? sanitize_text_field(wp_unslash($_GET['aibui_lang'])) : '';
227 if ($lang === '') {
228 return;
229 }
230
231 $meta_query = (array) $query->get('meta_query');
232 $meta_query[] = array(
233 'key' => '_ai_lang',
234 'value' => $lang,
235 'compare' => '='
236 );
237 $query->set('meta_query', $meta_query);
238 }
239
240 /** Front language handling */
241 public function register_query_var($vars)
242 {
243 $vars[] = 'ai_lang';
244 return $vars;
245 }
246
247 public function maybe_redirect_to_translation()
248 {
249 if (!is_singular()) {
250 return;
251 }
252
253 global $post;
254 if (!$post) {
255 return;
256 }
257
258 $default_lang = $this->settings['default_lang'];
259 $post_lang = get_post_meta($post->ID, '_ai_lang', true) ?: $default_lang;
260
261 // If the user has no explicit language preference (no GET, no cookie),
262 // and lands directly on a page, we treat that page's language as their
263 // preference and simply store it in the cookie without redirecting.
264 if (!$this->has_explicit_lang_preference) {
265 setcookie('aibui_lang', $post_lang, time() + MONTH_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
266 return;
267 }
268
269 $desired_lang = $this->current_lang;
270
271 if ($desired_lang === $post_lang) {
272 return;
273 }
274
275 $group = get_post_meta($post->ID, '_ai_translation_group', true);
276 if (!$group) {
277 return;
278 }
279
280 $translated = $this->get_translation_in_lang($group, get_post_type($post), $desired_lang);
281 if ($translated) {
282 wp_safe_redirect(get_permalink($translated->ID), 302);
283 exit;
284 }
285 }
286
287 public function filter_front_queries($query)
288 {
289 if (is_admin() || !$query->is_main_query()) {
290 return;
291 }
292
293 $current_lang = $this->current_lang;
294 $default_lang = $this->settings['default_lang'];
295
296 if ($current_lang === $default_lang) {
297 return;
298 }
299
300 if ($query->is_singular()) {
301 return;
302 }
303
304 $meta_query = (array) $query->get('meta_query');
305 $meta_query[] = array(
306 'key' => '_ai_lang',
307 'value' => $current_lang,
308 'compare' => '='
309 );
310 $query->set('meta_query', $meta_query);
311 }
312
313 /** Hreflang output */
314 public function output_hreflang_tags()
315 {
316 if (!is_singular()) {
317 return;
318 }
319
320 global $post;
321 if (!$post) {
322 return;
323 }
324
325 $group = get_post_meta($post->ID, '_ai_translation_group', true);
326 if (!$group) {
327 return;
328 }
329
330 $translations = $this->get_group_posts($group, get_post_type($post));
331 if (empty($translations)) {
332 return;
333 }
334
335 $links = array();
336 foreach ($translations as $translation) {
337 $lang = get_post_meta($translation->ID, '_ai_lang', true);
338 if (!$lang) {
339 continue;
340 }
341 $links[$lang] = get_permalink($translation->ID);
342 }
343
344 if (empty($links)) {
345 return;
346 }
347
348 foreach ($links as $lang => $url) {
349 printf(
350 '<link rel="alternate" hreflang="%s" href="%s" />' . "\n",
351 esc_attr($lang),
352 esc_url($url)
353 );
354 }
355
356 if (isset($links[$this->settings['default_lang']])) {
357 printf(
358 '<link rel="alternate" hreflang="x-default" href="%s" />' . "\n",
359 esc_url($links[$this->settings['default_lang']])
360 );
361 }
362 }
363
364 /** Helpers */
365 public function get_translation_in_lang($group, $post_type, $lang)
366 {
367 $posts = $this->get_group_posts($group, $post_type);
368 foreach ($posts as $post) {
369 $post_lang = get_post_meta($post->ID, '_ai_lang', true);
370 if ($post_lang === $lang) {
371 return $post;
372 }
373 }
374 return null;
375 }
376
377 public function get_group_posts($group, $post_type)
378 {
379 $cache_key = 'aibui_group_' . md5($group . $post_type);
380 $cached = wp_cache_get($cache_key, 'aibui_translations');
381 if ($cached !== false) {
382 return $cached;
383 }
384
385 $posts = get_posts(array(
386 'post_type' => $post_type,
387 'posts_per_page' => -1,
388 'post_status' => array('publish', 'draft', 'pending', 'future'),
389 'meta_query' => array(
390 array(
391 'key' => '_ai_translation_group',
392 'value' => $group,
393 'compare' => '='
394 )
395 )
396 ));
397
398 wp_cache_set($cache_key, $posts, 'aibui_translations', MINUTE_IN_SECONDS);
399 return $posts;
400 }
401
402 public function maybe_swap_template_part($pre_render, $block)
403 {
404 if (!is_array($block) || ($block['blockName'] ?? '') !== 'core/template-part') {
405 return $pre_render;
406 }
407
408 $slug = $block['attrs']['slug'] ?? '';
409 if (!$slug) {
410 return $pre_render;
411 }
412
413 $theme = $block['attrs']['theme'] ?? wp_get_theme()->get_stylesheet();
414 $base_part = $this->get_template_part_post($slug, $theme);
415 if (!$base_part) {
416 return $pre_render;
417 }
418
419 $group = get_post_meta($base_part->ID, '_ai_translation_group', true);
420 if (!$group) {
421 return $pre_render;
422 }
423
424 $translated = $this->get_translation_in_lang($group, 'wp_template_part', $this->current_lang);
425 if (!$translated) {
426 return $pre_render;
427 }
428
429 return do_blocks($translated->post_content);
430 }
431
432 private function get_template_part_post($slug, $theme)
433 {
434 $parts = get_posts(array(
435 'post_type' => 'wp_template_part',
436 'name' => $slug,
437 'posts_per_page' => 1,
438 'post_status' => array('publish', 'draft', 'pending', 'future'),
439 'tax_query' => array(
440 array(
441 'taxonomy' => 'wp_theme',
442 'field' => 'name',
443 'terms' => $theme,
444 )
445 )
446 ));
447
448 return $parts ? $parts[0] : null;
449 }
450 }
451
452