PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.2.2
AI Builder – Generate pages, blocks, images & translate with AI v2.2.2
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.2.2, at includes/class-translation-manager.php

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