PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.7.2
AI Builder – Generate pages, blocks, images & translate with AI v2.7.2
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 2.3.11 All 121 releases
ai-builder / includes / class-translation-manager.php

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

604 lines 20.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 // Handle homepage redirect early, before WP decides what to show
38 add_action('template_redirect', array($this, 'maybe_redirect_homepage'), 1);
39 add_action('template_redirect', array($this, 'maybe_redirect_to_translation'), 10);
40 add_action('pre_get_posts', array($this, 'filter_front_queries'));
41 add_action('wp_head', array($this, 'output_hreflang_tags'), 5);
42 add_filter('language_attributes', array($this, 'filter_language_attributes'), 10, 2);
43 add_filter('pre_render_block', array($this, 'maybe_swap_template_part'), 10, 2);
44 }
45 }
46
47 public function get_current_language()
48 {
49 return $this->current_lang;
50 }
51
52 public function filter_language_attributes($output, $doctype)
53 {
54 if ($doctype !== 'html') {
55 return $output;
56 }
57
58 $lang = $this->current_lang;
59 $dir = in_array($lang, array('ar', 'he', 'fa'), true) ? 'rtl' : 'ltr';
60
61 return sprintf('lang="%s" dir="%s"', esc_attr($lang), esc_attr($dir));
62 }
63
64 public function get_default_language()
65 {
66 return $this->settings['default_lang'];
67 }
68
69 public function is_switcher_enabled()
70 {
71 return !empty($this->settings['enable_switcher']);
72 }
73
74 public function get_available_languages()
75 {
76 return $this->settings['available_langs'];
77 }
78
79 public function get_settings()
80 {
81 return $this->settings;
82 }
83
84 /**
85 * In the admin, make sure "View" links include the post language as ?ai_lang=...
86 * so that designers land on the matching front-end version and are not redirected.
87 */
88 public function add_lang_query_to_admin_view_link($permalink, $post, $leavename = null)
89 {
90 if (!is_admin() || empty($post)) {
91 return $permalink;
92 }
93
94 // $post may be an object or an ID depending on the filter (post_link/page_link/post_type_link)
95 if (!is_object($post)) {
96 $post = get_post($post);
97 }
98 if (!$post) {
99 return $permalink;
100 }
101
102 // Only append ai_lang if this post actually belongs to a translation group
103 // and has at least one other translation. Otherwise, keep the URL clean.
104 $group = get_post_meta($post->ID, '_ai_translation_group', true);
105 if (!$group) {
106 return $permalink;
107 }
108
109 $translations = $this->get_group_posts($group, get_post_type($post));
110 if (empty($translations) || count($translations) <= 1) {
111 // No real translations available for this post
112 return $permalink;
113 }
114
115 $default_lang = $this->settings['default_lang'];
116 $lang = get_post_meta($post->ID, '_ai_lang', true) ?: $default_lang;
117
118 // Avoid duplicating the parameter if it's already there.
119 $permalink = remove_query_arg('ai_lang', $permalink);
120 return add_query_arg('ai_lang', $lang, $permalink);
121 }
122
123 private function determine_current_language()
124 {
125 $available = $this->settings['available_langs'];
126 $default = $this->settings['default_lang'];
127 $lang = $default;
128
129 if (isset($_GET['ai_lang'])) {
130 $candidate = sanitize_text_field(wp_unslash($_GET['ai_lang']));
131 if (in_array($candidate, $available, true)) {
132 $lang = $candidate;
133 $this->has_explicit_lang_preference = true;
134 setcookie('aibui_lang', $lang, time() + MONTH_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
135 }
136 } elseif (isset($_COOKIE['aibui_lang'])) {
137 $candidate = sanitize_text_field(wp_unslash($_COOKIE['aibui_lang']));
138 if (in_array($candidate, $available, true)) {
139 $lang = $candidate;
140 $this->has_explicit_lang_preference = true;
141 }
142 }
143
144 return $lang;
145 }
146
147 /** Admin columns & filters */
148 public function add_language_column($columns)
149 {
150 $columns['aibui_translations'] = __('Translations', 'ai-builder');
151 return $columns;
152 }
153
154 public function render_language_column($column, $post_id)
155 {
156 if ($column !== 'aibui_translations') {
157 return;
158 }
159
160 $default = $this->settings['default_lang'];
161 $current_lang = get_post_meta($post_id, '_ai_lang', true) ?: $default;
162 $current_label = AIBUI_Translation_Handler::SUPPORTED_LANGUAGES[$current_lang] ?? strtoupper($current_lang);
163
164 $items = array(
165 sprintf('<span class="aibui-current-lang">%s</span>', esc_html($current_label))
166 );
167
168 $group = get_post_meta($post_id, '_ai_translation_group', true);
169 if ($group) {
170 $translations = $this->get_group_posts($group, get_post_type($post_id));
171 foreach ($translations as $translation) {
172 if ((int) $translation->ID === (int) $post_id) {
173 continue;
174 }
175 $lang = get_post_meta($translation->ID, '_ai_lang', true) ?: $default;
176 $label = AIBUI_Translation_Handler::SUPPORTED_LANGUAGES[$lang] ?? strtoupper($lang);
177 $items[] = sprintf(
178 '<a class="aibui-lang-link" href="%s">%s</a>',
179 esc_url(get_edit_post_link($translation->ID)),
180 esc_html($label)
181 );
182 }
183 }
184
185 echo implode(' | ', $items);
186 }
187
188 public function print_admin_styles()
189 {
190 $screen = get_current_screen();
191 if (!$screen || !in_array($screen->id, array('edit-page', 'edit-post', 'edit-wp_template_part'), true)) {
192 return;
193 }
194
195 ?>
196 <style>
197 .column-aibui_translations {
198 width: 10%;
199 }
200 .aibui-current-lang {
201 font-weight: 600;
202 color: #007cba;
203 }
204 .aibui-lang-link {
205 text-decoration: none;
206 }
207 </style>
208 <?php
209 }
210
211 public function add_language_filter($post_type)
212 {
213 if (!in_array($post_type, array('post', 'page', 'wp_template_part'), true)) {
214 return;
215 }
216
217 $selected = isset($_GET['aibui_lang']) ? sanitize_text_field(wp_unslash($_GET['aibui_lang'])) : '';
218 ?>
219 <select name="aibui_lang" id="aibui_lang">
220 <option value=""><?php esc_html_e('All languages', 'ai-builder'); ?></option>
221 <?php foreach (AIBUI_Translation_Handler::SUPPORTED_LANGUAGES as $code => $label) : ?>
222 <option value="<?php echo esc_attr($code); ?>" <?php selected($selected, $code); ?>>
223 <?php echo esc_html($label); ?>
224 </option>
225 <?php endforeach; ?>
226 </select>
227 <?php
228 }
229
230 public function filter_admin_by_language($query)
231 {
232 if (!is_admin() || !$query->is_main_query()) {
233 return;
234 }
235
236 global $pagenow;
237 if (!in_array($pagenow, array('edit.php'), true)) {
238 return;
239 }
240
241 $lang = isset($_GET['aibui_lang']) ? sanitize_text_field(wp_unslash($_GET['aibui_lang'])) : '';
242 if ($lang === '') {
243 return;
244 }
245
246 $meta_query = (array) $query->get('meta_query');
247 $meta_query[] = array(
248 'key' => '_ai_lang',
249 'value' => $lang,
250 'compare' => '='
251 );
252 $query->set('meta_query', $meta_query);
253 }
254
255 /** Front language handling */
256 public function register_query_var($vars)
257 {
258 $vars[] = 'ai_lang';
259 return $vars;
260 }
261
262 /**
263 * Handle homepage translation redirect
264 * This runs early to catch the homepage case before WP shows the wrong content
265 */
266 public function maybe_redirect_homepage()
267 {
268 // Only process if we have an explicit language preference
269 if (!$this->has_explicit_lang_preference) {
270 return;
271 }
272
273 // Check if we're on the site root (homepage)
274 $request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
275 $parsed = parse_url($request_uri);
276 $path = isset($parsed['path']) ? trim($parsed['path'], '/') : '';
277
278 // If path is empty or just the WordPress subdirectory, we're on homepage
279 $home_path = trim(parse_url(home_url(), PHP_URL_PATH) ?: '', '/');
280 $is_homepage = ($path === $home_path || $path === '');
281
282 if (!$is_homepage) {
283 return;
284 }
285
286 $default_lang = $this->settings['default_lang'];
287 $desired_lang = $this->current_lang;
288
289 // Get the front page ID from WordPress settings
290 $show_on_front = get_option('show_on_front');
291 $front_page_id = get_option('page_on_front');
292
293 // If WordPress is set to show a static page as homepage
294 if ($show_on_front === 'page' && $front_page_id) {
295 $front_page_lang = get_post_meta($front_page_id, '_ai_lang', true) ?: $default_lang;
296
297 // If desired language matches the front page language, no redirect needed
298 // Just ensure we stay on the homepage (the pre_get_posts hook will handle forcing the page)
299 if ($desired_lang === $front_page_lang) {
300 return;
301 }
302
303 // Find the translation of the front page
304 $group = get_post_meta($front_page_id, '_ai_translation_group', true);
305 if ($group) {
306 $translated = $this->get_translation_in_lang($group, 'page', $desired_lang);
307 if ($translated) {
308 // Redirect to the translated page
309 wp_safe_redirect(get_permalink($translated->ID), 302);
310 exit;
311 }
312 }
313 }
314
315 // If show_on_front is 'posts' (blog mode), or no static page is set,
316 // look for a page marked as homepage translation for the desired language
317 // This handles the case where the site uses blog mode but has translated "landing" pages
318 // But skip this for default language (let WordPress show the blog/homepage normally)
319 if ($desired_lang !== $default_lang) {
320 $homepage_translation = $this->find_homepage_for_language($desired_lang);
321 if ($homepage_translation) {
322 wp_safe_redirect(get_permalink($homepage_translation->ID), 302);
323 exit;
324 }
325 }
326 }
327
328 /**
329 * Find a page that serves as homepage for a specific language
330 * Looks for pages with _ai_lang matching and _ai_is_homepage = 1
331 * Or pages that are translations of the default homepage
332 */
333 private function find_homepage_for_language($lang)
334 {
335 $default_lang = $this->settings['default_lang'];
336
337 // If looking for default language, don't redirect (stay on blog/homepage)
338 if ($lang === $default_lang) {
339 return null;
340 }
341
342 // First, try to find a page explicitly marked as homepage for this language
343 $query = new WP_Query(array(
344 'post_type' => 'page',
345 'post_status' => 'publish',
346 'posts_per_page' => 1,
347 'meta_query' => array(
348 'relation' => 'AND',
349 array(
350 'key' => '_ai_lang',
351 'value' => $lang,
352 'compare' => '='
353 ),
354 array(
355 'key' => '_ai_is_homepage',
356 'value' => '1',
357 'compare' => '='
358 )
359 )
360 ));
361
362 if ($query->have_posts()) {
363 return $query->posts[0];
364 }
365
366 // Fallback: find a page that is a translation of the page_on_front (if any)
367 $front_page_id = get_option('page_on_front');
368 if ($front_page_id) {
369 $group = get_post_meta($front_page_id, '_ai_translation_group', true);
370 if ($group) {
371 return $this->get_translation_in_lang($group, 'page', $lang);
372 }
373 }
374
375 return null;
376 }
377
378 public function maybe_redirect_to_translation()
379 {
380 if (!is_singular()) {
381 return;
382 }
383
384 global $post;
385 if (!$post) {
386 return;
387 }
388
389 $default_lang = $this->settings['default_lang'];
390 $post_lang = get_post_meta($post->ID, '_ai_lang', true) ?: $default_lang;
391
392 // If the user has no explicit language preference (no GET, no cookie),
393 // and lands directly on a page, we treat that page's language as their
394 // preference and simply store it in the cookie without redirecting.
395 if (!$this->has_explicit_lang_preference) {
396 setcookie('aibui_lang', $post_lang, time() + MONTH_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
397 return;
398 }
399
400 $desired_lang = $this->current_lang;
401
402 if ($desired_lang === $post_lang) {
403 return;
404 }
405
406 $group = get_post_meta($post->ID, '_ai_translation_group', true);
407 if (!$group) {
408 return;
409 }
410
411 $translated = $this->get_translation_in_lang($group, get_post_type($post), $desired_lang);
412 if ($translated) {
413 wp_safe_redirect(get_permalink($translated->ID), 302);
414 exit;
415 }
416 }
417
418 public function filter_front_queries($query)
419 {
420 if (is_admin() || !$query->is_main_query()) {
421 return;
422 }
423
424 // Force homepage detection when on root with ?ai_lang= parameter
425 $request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
426 $parsed = parse_url($request_uri);
427 $path = isset($parsed['path']) ? trim($parsed['path'], '/') : '';
428 $home_path = trim(parse_url(home_url(), PHP_URL_PATH) ?: '', '/');
429 $is_homepage = ($path === $home_path || $path === '');
430
431 if ($is_homepage && isset($_GET['ai_lang'])) {
432 $show_on_front = get_option('show_on_front');
433 $front_page_id = get_option('page_on_front');
434
435 // If WordPress is set to show a static page as homepage
436 if ($show_on_front === 'page' && $front_page_id) {
437 // Force the query to show the front page
438 $query->set('page_id', $front_page_id);
439 $query->is_home = false;
440 $query->is_page = true;
441 $query->is_singular = true;
442 }
443 }
444
445 $current_lang = $this->current_lang;
446 $default_lang = $this->settings['default_lang'];
447
448 if ($current_lang === $default_lang) {
449 return;
450 }
451
452 if ($query->is_singular()) {
453 return;
454 }
455
456 $meta_query = (array) $query->get('meta_query');
457 $meta_query[] = array(
458 'key' => '_ai_lang',
459 'value' => $current_lang,
460 'compare' => '='
461 );
462 $query->set('meta_query', $meta_query);
463 }
464
465 /** Hreflang output */
466 public function output_hreflang_tags()
467 {
468 if (!is_singular()) {
469 return;
470 }
471
472 global $post;
473 if (!$post) {
474 return;
475 }
476
477 $group = get_post_meta($post->ID, '_ai_translation_group', true);
478 if (!$group) {
479 return;
480 }
481
482 $translations = $this->get_group_posts($group, get_post_type($post));
483 if (empty($translations)) {
484 return;
485 }
486
487 $links = array();
488 foreach ($translations as $translation) {
489 $lang = get_post_meta($translation->ID, '_ai_lang', true);
490 if (!$lang) {
491 continue;
492 }
493 $links[$lang] = get_permalink($translation->ID);
494 }
495
496 if (empty($links)) {
497 return;
498 }
499
500 foreach ($links as $lang => $url) {
501 printf(
502 '<link rel="alternate" hreflang="%s" href="%s" />' . "\n",
503 esc_attr($lang),
504 esc_url($url)
505 );
506 }
507
508 if (isset($links[$this->settings['default_lang']])) {
509 printf(
510 '<link rel="alternate" hreflang="x-default" href="%s" />' . "\n",
511 esc_url($links[$this->settings['default_lang']])
512 );
513 }
514 }
515
516 /** Helpers */
517 public function get_translation_in_lang($group, $post_type, $lang)
518 {
519 $posts = $this->get_group_posts($group, $post_type);
520 foreach ($posts as $post) {
521 $post_lang = get_post_meta($post->ID, '_ai_lang', true);
522 if ($post_lang === $lang) {
523 return $post;
524 }
525 }
526 return null;
527 }
528
529 public function get_group_posts($group, $post_type)
530 {
531 $cache_key = 'aibui_group_' . md5($group . $post_type);
532 $cached = wp_cache_get($cache_key, 'aibui_translations');
533 if ($cached !== false) {
534 return $cached;
535 }
536
537 $posts = get_posts(array(
538 'post_type' => $post_type,
539 'posts_per_page' => -1,
540 'post_status' => array('publish', 'draft', 'pending', 'future'),
541 'meta_query' => array(
542 array(
543 'key' => '_ai_translation_group',
544 'value' => $group,
545 'compare' => '='
546 )
547 )
548 ));
549
550 wp_cache_set($cache_key, $posts, 'aibui_translations', MINUTE_IN_SECONDS);
551 return $posts;
552 }
553
554 public function maybe_swap_template_part($pre_render, $block)
555 {
556 if (!is_array($block) || ($block['blockName'] ?? '') !== 'core/template-part') {
557 return $pre_render;
558 }
559
560 $slug = $block['attrs']['slug'] ?? '';
561 if (!$slug) {
562 return $pre_render;
563 }
564
565 $theme = $block['attrs']['theme'] ?? wp_get_theme()->get_stylesheet();
566 $base_part = $this->get_template_part_post($slug, $theme);
567 if (!$base_part) {
568 return $pre_render;
569 }
570
571 $group = get_post_meta($base_part->ID, '_ai_translation_group', true);
572 if (!$group) {
573 return $pre_render;
574 }
575
576 $translated = $this->get_translation_in_lang($group, 'wp_template_part', $this->current_lang);
577 if (!$translated) {
578 return $pre_render;
579 }
580
581 return do_blocks($translated->post_content);
582 }
583
584 private function get_template_part_post($slug, $theme)
585 {
586 $parts = get_posts(array(
587 'post_type' => 'wp_template_part',
588 'name' => $slug,
589 'posts_per_page' => 1,
590 'post_status' => array('publish', 'draft', 'pending', 'future'),
591 'tax_query' => array(
592 array(
593 'taxonomy' => 'wp_theme',
594 'field' => 'name',
595 'terms' => $theme,
596 )
597 )
598 ));
599
600 return $parts ? $parts[0] : null;
601 }
602 }
603
604