PluginProbe
Linguise – AI Automatic Multilingual Translation / trunk
Linguise – AI Automatic Multilingual Translation vtrunk
2.2.64 2.2.63 2.2.62 2.2.61 2.2.60 2.2.59 2.2.58 2.2.57 2.2.56 2.2.55 2.2.54 2.2.53 2.2.52 2.2.51 2.2.50 2.2.49 2.2.47 2.2.48 2.2.46 2.2.45 2.2.44 2.2.43 2.2.42 1.9.7 1.9.8 All 255 releases
linguise / src / switcher.php

switcher.php in Linguise – AI Automatic Multilingual Translation trunk, at src/switcher.php

395 lines 15.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Linguise\WordPress;
4
5 use Linguise\WordPress\Helper;
6
7 defined('ABSPATH') || die('');
8
9 /**
10 * The main switcher handler for the frontend part.
11 */
12 class LinguiseSwitcher
13 {
14 /**
15 * The configuration for the switcher.
16 *
17 * @var array
18 */
19 private $config;
20
21 /**
22 * The host for translation, defaults to translate.linguise.com
23 *
24 * @var string
25 */
26 private $translate_host = 'https://translate.linguise.com';
27
28 /**
29 * Indicates if the switcher is loaded.
30 *
31 * @var boolean
32 */
33 private $is_loaded = false;
34
35 /**
36 * Initialize the switcher
37 *
38 * @param array $options The options to initialize the switcher with.
39 * @param string $base_url The base URL for the switcher.
40 * @param string|null $translate_host The host for translation, defaults to translate.linguise.com
41 *
42 * @return void
43 */
44 public function __construct($options, $base_url, $translate_host = \null)
45 {
46 $languages_names = Helper::getLanguagesInfos();
47
48 // Get from module parameters the enable languages
49 $languages_enabled_param = isset($options['enabled_languages']) ? $options['enabled_languages'] : array();
50 // Get the default language
51 $default_language = isset($options['default_language']) ? $options['default_language'] : 'en';
52 $language_name_display = isset($options['language_name_display']) ? $options['language_name_display'] : 'en';
53
54 // Generate language list with default language as first item
55 if ($language_name_display === 'en') {
56 $language_list = array($default_language => $languages_names->{$default_language}->name);
57 } else {
58 $language_list = array($default_language => $languages_names->{$default_language}->original_name);
59 }
60
61 foreach ($languages_enabled_param as $language) {
62 if ($language === $default_language) {
63 continue;
64 }
65
66 if (!isset($languages_names->{$language})) {
67 continue;
68 }
69
70 if ($language_name_display === 'en') {
71 $language_list[$language] = $languages_names->{$language}->name;
72 } else {
73 $language_list[$language] = $languages_names->{$language}->original_name;
74 }
75 }
76
77 if (preg_match('@(\/+)$@', parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), $matches) && !empty($matches[1])) {
78 $trailing_slashes = $matches[1];
79 } else {
80 $trailing_slashes = '';
81 }
82
83 linguiseSwitchMainSite();
84 $site_url = linguiseGetSite();
85 linguiseRestoreMultisite();
86
87 $base = rtrim(self::forceRelativeUrl($site_url), '/');
88 $config = array_merge(
89 [
90 'languages' => $language_list,
91 'base' => $base,
92 'base_url' => $base_url,
93 'original_path' => rtrim(substr(rtrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/'), strlen($base)), '/'),
94 'trailing_slashes' => $trailing_slashes,
95 ],
96 $options,
97 [
98 'default_language' => $default_language,
99 ]
100 );
101
102 // We do color mixing for shadow
103 $flag_shadow_color = $config['flag_shadow_color'] ?? '#000000';
104 $flag_hover_shadow_color = $config['flag_hover_shadow_color'] ?? '#000000';
105 $flag_shadow_color_alpha = $config['flag_shadow_color_alpha'] ?? 1.0;
106 $flag_hover_shadow_color_alpha = $config['flag_hover_shadow_color_alpha'] ?? 1.0;
107
108 $config['flag_shadow_color'] = Helper::mixColorAlpha($flag_shadow_color, $flag_shadow_color_alpha);
109 $config['flag_hover_shadow_color'] = Helper::mixColorAlpha($flag_hover_shadow_color, $flag_hover_shadow_color_alpha);
110
111 $this->config = $config;
112 if (!empty($translate_host)) {
113 // set new host
114 $this->translate_host = $translate_host;
115 }
116 }
117
118 /**
119 * Force the URL to be relative.
120 *
121 * @param string $url The URL to force to be relative.
122 *
123 * @return string|null The relative URL.
124 */
125 private static function forceRelativeUrl($url)
126 {
127 return preg_replace('/^(http)?s?:?\/\/[^\/]*(\/?.*)$/i', '$2', '' . $url);
128 }
129
130 /**
131 * Check if the switcher assets is loaded.
132 *
133 * @return boolean True if the switcher is loaded, false otherwise.
134 */
135 public function isAssetsLoaded()
136 {
137 return $this->is_loaded;
138 }
139
140 /**
141 * Load the scripts and styles for the switcher.
142 *
143 * @return void
144 */
145 public function loadScripts()
146 {
147 if ($this->is_loaded) {
148 return;
149 }
150
151 // do not load translate script for bricks edit page
152 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- View request, no action
153 $is_wp_bricks = array_key_exists('bricks', $_GET) ? $_GET['bricks'] : false;
154 if (!$is_wp_bricks && !empty($this->config['token'])) { // Only load if token is present
155 wp_enqueue_script('linguise_switcher', Helper::getScriptUrl('/assets/js/front.bundle.js'), array(), LINGUISE_VERSION);
156 wp_enqueue_style('linguise_switcher', Helper::getScriptUrl('/assets/css/front.bundle.css'), array(), LINGUISE_VERSION);
157
158 // make copy of $config
159 $config_local = json_decode(json_encode($this->config), true);
160
161 if ($this->config['dynamic_translations']['enabled'] === 1) {
162 $config_local['translate_host'] = $this->translate_host;
163 }
164
165 // Remove content we don't want to share
166 // FIXME: we should remove all config which is not actually used
167 unset($config_local['token']);
168 unset($config_local['expert_mode']);
169
170 wp_localize_script('linguise_switcher', 'linguise_configs', array('vars' => array('configs' => $config_local)));
171 $this->is_loaded = true;
172 }
173 }
174
175 /**
176 * Render the custom CSS for the switcher.
177 *
178 * @return string
179 */
180 public function makeCustomCSS()
181 {
182 $custom_css = '';
183 if ($this->config['flag_shape'] === 'rounded') {
184 $custom_css .= '
185 .linguise_switcher span.linguise_language_icon, #linguise_popup li span.linguise_flags {
186 width: ' . (int) $this->config['flag_width'] . 'px;
187 height: ' . (int) $this->config['flag_width'] . 'px;
188 }';
189 } else {
190 $custom_css .= '
191 .linguise_switcher span.linguise_language_icon, #linguise_popup li span.linguise_flags {
192 width: ' . (int) $this->config['flag_width'] . 'px;
193 height: ' . ((int) $this->config['flag_width'] * 2 / 3) . 'px;
194 }';
195 }
196
197 $custom_css .= '.lccaret svg {fill: '. esc_html($this->config['language_name_color']) .' !important}';
198 $custom_css .= '.linguise_lang_name {color: '. esc_html($this->config['language_name_color']) .' !important}';
199 $custom_css .= '.popup_linguise_lang_name {color: '. esc_html($this->config['popup_language_name_color'] ?? $this->config['language_name_color']) .' !important}';
200 $custom_css .= '.linguise_current_lang:hover .lccaret svg {fill: '. esc_html($this->config['language_name_hover_color']) .' !important}';
201 $custom_css .= '.linguise_lang_name:hover, .linguise_current_lang:hover .linguise_lang_name, .linguise-lang-item:hover .linguise_lang_name {color: '. esc_html($this->config['language_name_hover_color']) .' !important}';
202 $custom_css .= '.popup_linguise_lang_name:hover, .linguise-lang-item:hover .popup_linguise_lang_name {color: '. esc_html($this->config['popup_language_name_hover_color'] ?? $this->config['language_name_hover_color']) .' !important}';
203 $custom_css .= '.linguise_switcher span.linguise_language_icon, #linguise_popup li .linguise_flags {box-shadow: '. (int)$this->config['flag_shadow_h'] .'px '. (int)$this->config['flag_shadow_v'] .'px '. (int)$this->config['flag_shadow_blur'] .'px '. (int)$this->config['flag_shadow_spread'] .'px '. esc_html($this->config['flag_shadow_color']) .' !important}';
204 $custom_css .= '.linguise_switcher span.linguise_language_icon:hover, #linguise_popup li .linguise_flags:hover {box-shadow: '. (int)$this->config['flag_hover_shadow_h'] .'px '. (int)$this->config['flag_hover_shadow_v'] .'px '. (int)$this->config['flag_hover_shadow_blur'] .'px '. (int)$this->config['flag_hover_shadow_spread'] .'px '. esc_html($this->config['flag_hover_shadow_color']) .' !important}';
205
206 if ($this->config['flag_shape'] === 'rectangular') {
207 $custom_css .= '#linguise_popup.linguise_flag_rectangular ul li .linguise_flags, .linguise_switcher.linguise_flag_rectangular span.linguise_language_icon {border-radius: ' . (int) $this->config['flag_border_radius'] . 'px}';
208 }
209
210 if (!empty($this->config['custom_css'])) {
211 $custom_css .= esc_html($this->config['custom_css']);
212 }
213
214 return $custom_css;
215 }
216
217 /**
218 * Render the actual linguise switcher
219 *
220 * @param boolean $pin Should we pin this in-place or follow the position of the flag that is defined.
221 *
222 * @return string
223 */
224 public function renderShortcode($pin = \false)
225 {
226 $this->loadScripts();
227 $custom_css = $this->makeCustomCSS();
228
229 wp_add_inline_style('linguise_switcher', $custom_css);
230
231 if ($pin) {
232 return '<div class="linguise_switcher_root linguise_menu_root"></div>';
233 }
234
235 return '<div class="linguise_switcher_root"></div>';
236 }
237
238 /**
239 * Actual starting function for the switcher.
240 *
241 * @return void
242 */
243 public function start()
244 {
245 if (!empty($this->config['alternate_link'])) {
246 linguiseSwitchMainSite();
247 $site_url = linguiseGetSite();
248 linguiseRestoreMultisite();
249
250 $scheme = !empty($_SERVER['HTTPS']) ? 'https' : 'http';
251
252 $host = parse_url($site_url, PHP_URL_HOST);
253 $path = $this->config['original_path'];
254 $query = parse_url($site_url, PHP_URL_QUERY);
255 $alternates = $this->config['languages'];
256 $alternates['x-default'] = 'x-default';
257
258 $head_content = [];
259 global $wpdb;
260
261 $originalCharset = $wpdb->charset;
262 if ($wpdb->charset !== 'utf8mb4') {
263 $wpdb->set_charset($wpdb->__get('dbh'), 'utf8mb4');
264 }
265 foreach ($alternates as $language_code => $language_name) {
266 $url_translation = null;
267 if ($path) {
268 $db_query = $wpdb->prepare('SELECT * FROM ' . $wpdb->base_prefix . 'linguise_urls WHERE hash_source=%s AND language=%s', md5($path), $language_code);
269 $url_translation = $wpdb->get_row($db_query);
270 }
271
272 if (!is_wp_error($url_translation) && !empty($url_translation)) {
273 /**
274 * Hotfix for some of urls with non latin char become invalid (HTML entity–encoded Unicode characters)
275 * And causing some of SEO tool complaining, Semrush mark hreflang as broken.
276 * TODO: can be removed if core translate the URL properly
277 */
278 $encoded_translation = implode(
279 '/',
280 array_map('rawurlencode', explode('/', trim($url_translation->translation, '/')))
281 );
282
283 $url = $scheme . '://' . $host . $this->config['base'] . '/' . $encoded_translation . $this->config['trailing_slashes'] . $query;
284 } else {
285 $url = $scheme . '://' . $host . $this->config['base'] . (in_array($language_code, array($this->config['default_language'], 'x-default')) ? '' : '/' . $language_code) . $path . $this->config['trailing_slashes'] . $query;
286 }
287
288 $head_content[] = '<link rel="alternate" hreflang="' . $language_code . '" href="' . $url . '" />';
289 }
290 if ($originalCharset !== 'utf8mb4') {
291 $wpdb->set_charset($wpdb->__get('dbh'), $originalCharset);
292 }
293
294 if (!empty($head_content)) {
295 add_action('wp_head', function ($a) use ($head_content) {
296 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped already
297 echo implode("\n", $head_content);
298 });
299 }
300 }
301
302 add_filter('wp_get_nav_menu_items', [$this, 'hookNavMenuItems'], 20, 1);
303 add_action('wp_footer', [$this, 'hookFooter'], 10, 1);
304 add_action('wp_enqueue_scripts', [$this, 'loadScripts'], 10);
305 add_shortcode('linguise', [$this, 'hookShortcode']);
306 }
307
308 /**
309 * Simple code to render the shortcode with pinned position.
310 *
311 * @return string
312 */
313 public function hookShortcode()
314 {
315 return $this->renderShortcode(true);
316 }
317
318 /**
319 * Hook for footer to add the linguise switcher.
320 *
321 * @return void
322 */
323 public function hookFooter()
324 {
325 // Footer is an automatic flag switcher that will call the shortcode of linguise
326 if (!$this->config['token'] || $this->config['add_flag_automatically'] !== 1) {
327 return;
328 }
329
330 echo wp_kses(
331 $this->renderShortcode(),
332 [
333 'div' => [
334 'class' => true,
335 ],
336 ]
337 );
338 }
339
340 /**
341 * Hook for nav menu items to add the linguise switcher.
342 *
343 * @param array $items The menu items.
344 *
345 * @return array
346 */
347 public function hookNavMenuItems($items)
348 {
349 if (doing_action('customize_register')) {
350 return $items;
351 }
352
353 $is_found = false;
354 $new_items = [];
355 $offset = 0;
356
357 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- View request, no action
358 $current_language = (!empty($_GET['language']) && in_array($_GET['language'], array_keys($this->config['languages']))) ? $_GET['language'] : $this->config['default_language'];
359 foreach ($items as $item) {
360 $options = get_post_meta($item->ID, '_linguise_menu_item', true);
361 if ($options) {
362 // parent item for dropdown
363 $item->title = (!empty($this->config['enable_language_name'])) ? $this->config['languages'][$current_language] : '';
364 $item->attr_title = '';
365 $item->url = '#';
366 $item->classes = array('linguise_switcher_root linguise_menu_root linguise_parent_menu_item');
367
368 if ($this->config['flag_shape'] === 'rounded') {
369 $item->classes[] = 'linguise_flag_rounded';
370 } else {
371 $item->classes[] = 'linguise_flag_rectangular';
372 }
373
374 if ($this->config['flag_display_type'] === 'side_by_side') {
375 $item->classes[] = 'linguise_parent_menu_item_side_by_side';
376 }
377
378 $new_items[] = $item;
379 $is_found = true;
380 } else {
381 $item->menu_order += $offset;
382 $new_items[] = $item;
383 }
384 }
385
386 if ($is_found) {
387 $custom_css = $this->makeCustomCSS();
388 $this->loadScripts();
389 wp_add_inline_style('linguise_switcher', $custom_css);
390 }
391
392 return $new_items;
393 }
394 }
395