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

472 lines 15.0 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 $default_lang = $this->settings['default_lang'];
250 $desired_lang = $this->current_lang;
251
252 // Special handling for the front page (homepage)
253 if (is_front_page() && $this->has_explicit_lang_preference) {
254 $front_page_id = get_option('page_on_front');
255 if ($front_page_id) {
256 $front_page_lang = get_post_meta($front_page_id, '_ai_lang', true) ?: $default_lang;
257
258 if ($desired_lang !== $front_page_lang) {
259 $group = get_post_meta($front_page_id, '_ai_translation_group', true);
260 if ($group) {
261 $translated = $this->get_translation_in_lang($group, 'page', $desired_lang);
262 if ($translated) {
263 wp_safe_redirect(get_permalink($translated->ID), 302);
264 exit;
265 }
266 }
267 }
268 }
269 return;
270 }
271
272 if (!is_singular()) {
273 return;
274 }
275
276 global $post;
277 if (!$post) {
278 return;
279 }
280
281 $post_lang = get_post_meta($post->ID, '_ai_lang', true) ?: $default_lang;
282
283 // If the user has no explicit language preference (no GET, no cookie),
284 // and lands directly on a page, we treat that page's language as their
285 // preference and simply store it in the cookie without redirecting.
286 if (!$this->has_explicit_lang_preference) {
287 setcookie('aibui_lang', $post_lang, time() + MONTH_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
288 return;
289 }
290
291 if ($desired_lang === $post_lang) {
292 return;
293 }
294
295 $group = get_post_meta($post->ID, '_ai_translation_group', true);
296 if (!$group) {
297 return;
298 }
299
300 $translated = $this->get_translation_in_lang($group, get_post_type($post), $desired_lang);
301 if ($translated) {
302 wp_safe_redirect(get_permalink($translated->ID), 302);
303 exit;
304 }
305 }
306
307 public function filter_front_queries($query)
308 {
309 if (is_admin() || !$query->is_main_query()) {
310 return;
311 }
312
313 $current_lang = $this->current_lang;
314 $default_lang = $this->settings['default_lang'];
315
316 if ($current_lang === $default_lang) {
317 return;
318 }
319
320 if ($query->is_singular()) {
321 return;
322 }
323
324 $meta_query = (array) $query->get('meta_query');
325 $meta_query[] = array(
326 'key' => '_ai_lang',
327 'value' => $current_lang,
328 'compare' => '='
329 );
330 $query->set('meta_query', $meta_query);
331 }
332
333 /** Hreflang output */
334 public function output_hreflang_tags()
335 {
336 if (!is_singular()) {
337 return;
338 }
339
340 global $post;
341 if (!$post) {
342 return;
343 }
344
345 $group = get_post_meta($post->ID, '_ai_translation_group', true);
346 if (!$group) {
347 return;
348 }
349
350 $translations = $this->get_group_posts($group, get_post_type($post));
351 if (empty($translations)) {
352 return;
353 }
354
355 $links = array();
356 foreach ($translations as $translation) {
357 $lang = get_post_meta($translation->ID, '_ai_lang', true);
358 if (!$lang) {
359 continue;
360 }
361 $links[$lang] = get_permalink($translation->ID);
362 }
363
364 if (empty($links)) {
365 return;
366 }
367
368 foreach ($links as $lang => $url) {
369 printf(
370 '<link rel="alternate" hreflang="%s" href="%s" />' . "\n",
371 esc_attr($lang),
372 esc_url($url)
373 );
374 }
375
376 if (isset($links[$this->settings['default_lang']])) {
377 printf(
378 '<link rel="alternate" hreflang="x-default" href="%s" />' . "\n",
379 esc_url($links[$this->settings['default_lang']])
380 );
381 }
382 }
383
384 /** Helpers */
385 public function get_translation_in_lang($group, $post_type, $lang)
386 {
387 $posts = $this->get_group_posts($group, $post_type);
388 foreach ($posts as $post) {
389 $post_lang = get_post_meta($post->ID, '_ai_lang', true);
390 if ($post_lang === $lang) {
391 return $post;
392 }
393 }
394 return null;
395 }
396
397 public function get_group_posts($group, $post_type)
398 {
399 $cache_key = 'aibui_group_' . md5($group . $post_type);
400 $cached = wp_cache_get($cache_key, 'aibui_translations');
401 if ($cached !== false) {
402 return $cached;
403 }
404
405 $posts = get_posts(array(
406 'post_type' => $post_type,
407 'posts_per_page' => -1,
408 'post_status' => array('publish', 'draft', 'pending', 'future'),
409 'meta_query' => array(
410 array(
411 'key' => '_ai_translation_group',
412 'value' => $group,
413 'compare' => '='
414 )
415 )
416 ));
417
418 wp_cache_set($cache_key, $posts, 'aibui_translations', MINUTE_IN_SECONDS);
419 return $posts;
420 }
421
422 public function maybe_swap_template_part($pre_render, $block)
423 {
424 if (!is_array($block) || ($block['blockName'] ?? '') !== 'core/template-part') {
425 return $pre_render;
426 }
427
428 $slug = $block['attrs']['slug'] ?? '';
429 if (!$slug) {
430 return $pre_render;
431 }
432
433 $theme = $block['attrs']['theme'] ?? wp_get_theme()->get_stylesheet();
434 $base_part = $this->get_template_part_post($slug, $theme);
435 if (!$base_part) {
436 return $pre_render;
437 }
438
439 $group = get_post_meta($base_part->ID, '_ai_translation_group', true);
440 if (!$group) {
441 return $pre_render;
442 }
443
444 $translated = $this->get_translation_in_lang($group, 'wp_template_part', $this->current_lang);
445 if (!$translated) {
446 return $pre_render;
447 }
448
449 return do_blocks($translated->post_content);
450 }
451
452 private function get_template_part_post($slug, $theme)
453 {
454 $parts = get_posts(array(
455 'post_type' => 'wp_template_part',
456 'name' => $slug,
457 'posts_per_page' => 1,
458 'post_status' => array('publish', 'draft', 'pending', 'future'),
459 'tax_query' => array(
460 array(
461 'taxonomy' => 'wp_theme',
462 'field' => 'name',
463 'terms' => $theme,
464 )
465 )
466 ));
467
468 return $parts ? $parts[0] : null;
469 }
470 }
471
472