PluginProbe
Polylang / 3.8.6
Polylang v3.8.6
3.8.9 3.8.8 3.8.7 3.8.6 3.8.5 3.8.4 3.8.3 2.7 2.7.0.1 2.7.1 2.7.2 2.7.3 2.7.4 2.8 2.8.1 2.8.2 2.8.3 2.8.4 2.9 2.9.1 2.9.2 3.0 3.0.1 3.0.2 3.0.3 All 233 releases
polylang / src / translate-option.php

translate-option.php in Polylang 3.8.6, at src/translate-option.php

439 lines 13.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Polylang
4 */
5
6 /**
7 * Registers and translates strings in an option.
8 * When a string is updated in an original option, the translations of the old string are assigned to the new original string.
9 *
10 * @since 2.9
11 */
12 class PLL_Translate_Option {
13
14 /**
15 * Array of option keys to translate.
16 *
17 * @var string[]
18 */
19 private $keys;
20
21 /**
22 * Sanitization callback.
23 *
24 * @var callable|null
25 */
26 private $sanitize_callback;
27
28 /**
29 * Hashes for registered strings for this option.
30 *
31 * @var string[]
32 */
33 private $hashes = array();
34
35 /**
36 * Used to prevent filtering when retrieving the raw value of the option.
37 *
38 * @var bool
39 */
40 private static $raw = false;
41
42 /**
43 * Array of updated strings.
44 *
45 * @var array
46 */
47 private $updated_strings = array();
48
49 /**
50 * @var PLL_MO[]
51 */
52 private $translations;
53
54 /**
55 * Cache for the translated values.
56 *
57 * @var PLL_Cache<array|string>
58 */
59 private $cache;
60
61 /**
62 * Constructor
63 *
64 * @since 2.9
65 *
66 * @param string $name Option name.
67 * @param array $keys Recursive array of option keys to translate in the form:
68 * @example array(
69 * 'option_key_to_translate_1' => 1,
70 * 'option_key_to_translate_2' => 1,
71 * 'my_group' => array(
72 * 'sub_key_to_translate_1' => 1,
73 * 'sub_key_to_translate_2' => 1,
74 * ),
75 * )
76 *
77 * Note: only keys are interpreted. Any scalar can be used as values.
78 * @param array $args {
79 * Optional. Array of arguments for registering the option.
80 *
81 * @type string $context The group in which the strings will be registered.
82 * @type callable $sanitize_callback A callback function that sanitizes the option's value.
83 * }
84 */
85 public function __construct( $name, $keys = array(), $args = array() ) {
86 $this->cache = new PLL_Cache();
87
88 // Registers the strings.
89 $context = $args['context'] ?? 'Polylang';
90 $this->register_string_recursive( $context, $name, get_option( $name ), $keys );
91
92 // Translates the strings.
93 $this->keys = $keys;
94 add_filter( 'option_' . $name, array( $this, 'translate' ) ); // Make sure to add this filter after options are registered.
95
96 // Filters updated values.
97 add_filter( 'pre_update_option_' . $name, array( $this, 'pre_update_option' ), 10, 3 );
98 add_action( 'update_option_' . $name, array( $this, 'update_option' ) );
99
100 // Sanitizes translated strings.
101 if ( ! empty( $args['sanitize_callback'] ) ) {
102 $this->sanitize_callback = $args['sanitize_callback'];
103 }
104 add_filter( 'pll_sanitize_string_translation', array( $this, 'sanitize_option' ), 10, 4 );
105 }
106
107 /**
108 * Translates the strings registered for an option.
109 *
110 * @since 1.0
111 *
112 * @param mixed $value Either a string to translate or a list of strings to translate.
113 * @return mixed Translated string(s).
114 */
115 public function translate( $value ) {
116 if ( self::$raw ) {
117 return $value;
118 }
119
120 if ( empty( $GLOBALS['l10n']['pll_string'] ) || ! $GLOBALS['l10n']['pll_string'] instanceof PLL_MO ) {
121 return $value;
122 }
123
124 $lang = $GLOBALS['l10n']['pll_string']->get_header( 'Language' );
125
126 if ( ! is_string( $lang ) || '' === $lang ) {
127 return $value;
128 }
129
130 $cache = $this->cache->get( $lang );
131 if ( false === $cache ) {
132 $cache = $this->translate_string_recursive( $value, $this->keys );
133 $this->cache->set( $lang, $cache );
134 }
135
136 return $cache;
137 }
138
139 /**
140 * Recursively translates the strings registered for an option.
141 *
142 * @since 1.0
143 *
144 * @param mixed $values Either a string to translate or a list of strings to translate.
145 * @param array|bool $key Array of option keys to translate.
146 * @return array|string Translated string(s).
147 */
148 protected function translate_string_recursive( $values, $key ) {
149 $children = is_array( $key ) ? $key : array();
150
151 if ( is_array( $values ) || is_object( $values ) ) {
152 /** @var array|Traversable $values */
153 if ( count( $children ) ) {
154 $matcher = new PLL_Format_Util();
155
156 foreach ( $children as $name => $child ) {
157 if ( is_array( $values ) && isset( $values[ $name ] ) ) {
158 $values[ $name ] = $this->translate_string_recursive( $values[ $name ], $child );
159 continue;
160 }
161
162 if ( is_object( $values ) && isset( $values->$name ) ) {
163 $values->$name = $this->translate_string_recursive( $values->$name, $child );
164 continue;
165 }
166
167 foreach ( $values as $n => &$value ) {
168 // The first case could be handled by the next one, but we avoid calls to preg_match here.
169 if ( $matcher->matches( $n, $name ) ) {
170 $value = $this->translate_string_recursive( $value, $child );
171 }
172 }
173 }
174 } else {
175 // Parent key is a wildcard and no sub-key has been whitelisted.
176 foreach ( $values as &$value ) {
177 $value = $this->translate_string_recursive( $value, $key );
178 }
179 }
180 } else {
181 $values = pll__( $values );
182 }
183
184 return $values;
185 }
186
187 /**
188 * Recursively registers strings for an option.
189 *
190 * @since 1.0
191 * @since 2.7 Signature modified
192 *
193 * @param string $context The group in which the strings will be registered.
194 * @param string $option Option name.
195 * @param mixed $values Option value.
196 * @param array|bool $key Array of option keys to translate.
197 * @return void
198 */
199 protected function register_string_recursive( $context, $option, $values, $key ) {
200 if ( is_object( $values ) ) {
201 $values = (array) $values;
202 }
203
204 if ( is_array( $values ) ) {
205 $children = is_array( $key ) ? $key : array();
206
207 if ( count( $children ) ) {
208 $matcher = new PLL_Format_Util();
209
210 foreach ( $children as $name => $child ) {
211 if ( isset( $values[ $name ] ) ) {
212 $this->register_string_recursive( $context, $name, $values[ $name ], $child );
213 continue;
214 }
215
216 if ( ! $matcher->is_format( $name ) ) {
217 continue;
218 }
219
220 foreach ( $values as $n => $value ) {
221 if ( $matcher->matches( $n, $name ) ) {
222 $this->register_string_recursive( $context, $n, $value, $child );
223 }
224 }
225 }
226 } else {
227 foreach ( $values as $n => $value ) {
228 // Parent key is a wildcard and no sub-key has been whitelisted.
229 $this->register_string_recursive( $context, $n, $value, $key );
230 }
231 }
232 } elseif ( is_scalar( $values ) ) {
233 $string = (string) $values;
234 $this->hashes[] = md5( "$string|$option|$context" );
235 PLL_Admin_Strings::register_string( $option, $string, $context, true );
236 }
237 }
238
239 /**
240 * Returns the raw value of an option (without this class' filter).
241 *
242 * A static property is used to make sure that the option is not filtered
243 * whatever the number of instances of this class filtering the option.
244 *
245 * @since 3.3
246 *
247 * @param string $option_name Option name.
248 * @return mixed
249 */
250 protected function get_raw_option( $option_name ) {
251 self::$raw = true;
252 $option_value = get_option( $option_name );
253 self::$raw = false;
254
255 return $option_value;
256 }
257
258 /**
259 * Filters an option before it is updated.
260 *
261 * This is the step 1 in the update process, in which we prevent the update of
262 * strings to their translations by filtering them out, and we store the updated strings
263 * for the next step.
264 *
265 * @since 2.9
266 *
267 * @param mixed $value The new, unserialized option value.
268 * @param mixed $old_value The old (filtered) option value.
269 * @param string $name Option name.
270 * @return mixed
271 */
272 public function pre_update_option( $value, $old_value, $name ) {
273 // Stores the unfiltered old option value before it is updated in DB.
274 $unfiltered_old_value = $this->get_raw_option( $name );
275
276 $languages = PLL()->model->get_languages_list();
277
278 if ( empty( $languages ) ) {
279 return $value;
280 }
281
282 // Load translations in all languages.
283 foreach ( $languages as $language ) {
284 $this->translations[ $language->slug ] = new PLL_MO();
285 $this->translations[ $language->slug ]->import_from_db( $language );
286 }
287
288 $lang = pll_current_language();
289 if ( empty( $lang ) ) {
290 $lang = pll_default_language();
291 }
292
293 if ( empty( $lang ) ) {
294 return $value; // Something's wrong.
295 }
296
297 // Filters out the strings which would be updated to their translations and stores the updated strings.
298 $value = $this->check_value_recursive( $unfiltered_old_value, $value, $this->keys, $this->translations[ $lang ] );
299
300 return $value;
301 }
302
303 /**
304 * Updates the string translations to keep the same translated value when updating the original option.
305 *
306 * This is the step 2 in the update process. Knowing all strings that have been updated,
307 * we remove the old strings from the strings translations and replace them by
308 * the new strings with the old translations.
309 *
310 * @since 2.9
311 *
312 * @return void
313 */
314 public function update_option() {
315 $curlang = pll_current_language();
316
317 if ( ! empty( $this->updated_strings ) ) {
318 foreach ( PLL()->model->get_languages_list() as $language ) {
319
320 $mo = &$this->translations[ $language->slug ];
321
322 foreach ( $this->updated_strings as $old_string => $string ) {
323 $translation = $mo->translate( $old_string );
324 if ( ( empty( $curlang ) && $translation === $old_string ) || $language->slug === $curlang ) {
325 $translation = $string;
326 }
327
328 // Add new entry with new string and old translation.
329 $mo->add_entry( $mo->make_entry( $string, $translation ) );
330 }
331
332 $mo->export_to_db( $language );
333 }
334 }
335
336 $this->cache->clean();
337 }
338
339 /**
340 * Recursively compares the updated strings to the translation of the old string.
341 *
342 * This is the heart of the update process. If an updated string is found to be
343 * the same as the translation of the old string, we restore the old string to
344 * prevent the update in {@see PLL_Translate_Option::pre_update_option()}, otherwise
345 * the updated string is stored in {@see PLL_Translate_Option::updated_strings} to be able to
346 * later assign the translations to the new value in {@see PLL_Translate_Option::update_option()}.
347 *
348 * @since 2.9
349 * @since 3.5 Added $mo parameter.
350 *
351 * @param mixed $old_values The old option value.
352 * @param mixed $values The new option value.
353 * @param array|bool $key Array of option keys to translate.
354 * @param PLL_MO $mo Translations used to compare the updated string to the translated old string.
355 * @return mixed
356 */
357 protected function check_value_recursive( $old_values, $values, $key, $mo ) {
358 $children = is_array( $key ) ? $key : array();
359
360 if ( is_array( $values ) || is_object( $values ) ) {
361 /** @var array|Traversable $values */
362 if ( count( $children ) ) {
363 $matcher = new PLL_Format_Util();
364
365 foreach ( $children as $name => $child ) {
366 if ( is_array( $values ) && is_array( $old_values ) && isset( $old_values[ $name ], $values[ $name ] ) ) {
367 $values[ $name ] = $this->check_value_recursive( $old_values[ $name ], $values[ $name ], $child, $mo );
368 continue;
369 }
370
371 if ( is_object( $values ) && is_object( $old_values ) && isset( $old_values->$name, $values->$name ) ) {
372 $values->$name = $this->check_value_recursive( $old_values->$name, $values->$name, $child, $mo );
373 continue;
374 }
375
376 foreach ( $values as $n => $value ) {
377 // The first case could be handled by the next one, but we avoid calls to preg_match here.
378 if ( $matcher->matches( $n, $name ) ) {
379 if ( is_array( $values ) && is_array( $old_values ) && isset( $old_values[ $n ] ) ) {
380 $values[ $n ] = $this->check_value_recursive( $old_values[ $n ], $value, $child, $mo );
381 }
382
383 if ( is_object( $values ) && is_object( $old_values ) && isset( $old_values->$n ) ) {
384 $values->$n = $this->check_value_recursive( $old_values->$n, $value, $child, $mo );
385 }
386 }
387 }
388 }
389 } else {
390 // Parent key is a wildcard and no sub-key has been whitelisted.
391 foreach ( $values as $n => $value ) {
392 if ( is_array( $values ) && is_array( $old_values ) && isset( $old_values[ $n ] ) ) {
393 $values[ $n ] = $this->check_value_recursive( $old_values[ $n ], $value, $key, $mo );
394 }
395
396 if ( is_object( $values ) && is_object( $old_values ) && isset( $old_values->$n ) ) {
397 $values->$n = $this->check_value_recursive( $old_values->$n, $value, $key, $mo );
398 }
399 }
400 }
401 } elseif ( $old_values !== $values ) {
402 if ( $mo->translate( $old_values ) === $values ) {
403 $values = $old_values; // Prevents updating the value to its translation.
404 } else {
405 $this->updated_strings[ $old_values ] = $values; // Stores the updated strings.
406 }
407 }
408
409 return $values;
410 }
411
412 /**
413 * Sanitizes the string translation.
414 *
415 * @since 2.9
416 * @since 3.7 Add $context and $original parameters.
417 *
418 * @param string $value The unsanitised string translation value.
419 * @param string $name The name registered for the string.
420 * @param string $context The context registered for the string.
421 * @param string $original The original string to translate.
422 * @return string Sanitized value.
423 */
424 public function sanitize_option( $value, $name, $context, $original ) {
425 if ( ! in_array( md5( "$original|$name|$context" ), $this->hashes, true ) ) {
426 return $value;
427 }
428
429 if ( is_callable( $this->sanitize_callback ) ) {
430 return call_user_func( $this->sanitize_callback, $value, $name, $context, $original );
431 }
432
433 /** @var string $sanitized_value */
434 $sanitized_value = sanitize_option( $name, $value );
435
436 return $sanitized_value;
437 }
438 }
439