PluginProbe
Polylang / 3.7.7
Polylang v3.7.7
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
← All changes | include/translate-option.php +155 -50 3.03.7.7 View file →
@@ -13,13 +13,34 @@
13 13
14 14 /**
15 15 * Array of option keys to translate.
16 16 *
17 - * @var array
17 + * @var string[]
18 18 */
19 19 private $keys;
20 20
21 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 + /**
22 43 * Array of updated strings.
23 44 *
24 45 * @var array
25 46 */
@@ -25,8 +46,20 @@
25 46 */
26 47 private $updated_strings = array();
27 48
28 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 + /**
29 62 * Constructor
30 63 *
31 64 * @since 2.9
32 65 *
@@ -44,15 +77,17 @@
44 77 * Note: only keys are interpreted. Any scalar can be used as values.
45 78 * @param array $args {
46 79 * Optional. Array of arguments for registering the option.
47 80 *
48 - * @type string $context The group in which the strings will be registered.
49 - * @type string $sanitize_callback A callback function that sanitizes the option's value.
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.
50 83 * }
51 84 */
52 85 public function __construct( $name, $keys = array(), $args = array() ) {
86 + $this->cache = new PLL_Cache();
87 +
53 88 // Registers the strings.
54 - $context = isset( $args['context'] ) ? $args['context'] : 'Polylang';
89 + $context = $args['context'] ?? 'Polylang';
55 90 $this->register_string_recursive( $context, $name, get_option( $name ), $keys );
56 91
57 92 // Translates the strings.
58 93 $this->keys = $keys;
@@ -62,13 +97,12 @@
62 97 add_filter( 'pre_update_option_' . $name, array( $this, 'pre_update_option' ), 10, 3 );
63 98 add_action( 'update_option_' . $name, array( $this, 'update_option' ) );
64 99
65 100 // Sanitizes translated strings.
66 - if ( empty( $args['sanitize_callback'] ) ) {
67 - add_filter( 'pll_sanitize_string_translation', array( $this, 'sanitize_option' ), 10, 2 );
68 - } else {
69 - add_filter( 'pll_sanitize_string_translation', $args['sanitize_callback'], 10, 3 );
101 + if ( ! empty( $args['sanitize_callback'] ) ) {
102 + $this->sanitize_callback = $args['sanitize_callback'];
70 103 }
104 + add_filter( 'pll_sanitize_string_translation', array( $this, 'sanitize_option' ), 10, 4 );
71 105 }
72 106
73 107 /**
74 108 * Translates the strings registered for an option.
@@ -78,9 +112,29 @@
78 112 * @param mixed $value Either a string to translate or a list of strings to translate.
79 113 * @return mixed Translated string(s).
80 114 */
81 115 public function translate( $value ) {
82 - return $this->translate_string_recursive( $value, $this->keys );
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;
83 137 }
84 138
85 139 /**
86 140 * Recursively translates the strings registered for an option.
@@ -88,15 +142,18 @@
88 142 * @since 1.0
89 143 *
90 144 * @param mixed $values Either a string to translate or a list of strings to translate.
91 145 * @param array|bool $key Array of option keys to translate.
92 - * @return array|string Translated string(s)
146 + * @return array|string Translated string(s).
93 147 */
94 148 protected function translate_string_recursive( $values, $key ) {
95 149 $children = is_array( $key ) ? $key : array();
96 150
97 151 if ( is_array( $values ) || is_object( $values ) ) {
152 + /** @var array|Traversable $values */
98 153 if ( count( $children ) ) {
154 + $matcher = new PLL_Format_Util();
155 +
99 156 foreach ( $children as $name => $child ) {
100 157 if ( is_array( $values ) && isset( $values[ $name ] ) ) {
101 158 $values[ $name ] = $this->translate_string_recursive( $values[ $name ], $child );
102 159 continue;
@@ -106,13 +163,11 @@
106 163 $values->$name = $this->translate_string_recursive( $values->$name, $child );
107 164 continue;
108 165 }
109 166
110 - $pattern = '#^' . str_replace( '*', '(?:.+)', $name ) . '$#';
111 -
112 167 foreach ( $values as $n => &$value ) {
113 168 // The first case could be handled by the next one, but we avoid calls to preg_match here.
114 - if ( '*' === $name || ( false !== strpos( $name, '*' ) && preg_match( $pattern, $n ) ) ) {
169 + if ( $matcher->matches( $n, $name ) ) {
115 170 $value = $this->translate_string_recursive( $value, $child );
116 171 }
117 172 }
118 173 }
@@ -149,8 +204,10 @@
149 204 if ( is_array( $values ) ) {
150 205 $children = is_array( $key ) ? $key : array();
151 206
152 207 if ( count( $children ) ) {
208 + $matcher = new PLL_Format_Util();
209 +
153 210 foreach ( $children as $name => $child ) {
154 211 if ( isset( $values[ $name ] ) ) {
155 212 $this->register_string_recursive( $context, $name, $values[ $name ], $child );
156 213 continue;
@@ -155,13 +212,14 @@
155 212 $this->register_string_recursive( $context, $name, $values[ $name ], $child );
156 213 continue;
157 214 }
158 215
159 - $pattern = '#^' . str_replace( '*', '(?:.+)', $name ) . '$#';
216 + if ( ! $matcher->is_format( $name ) ) {
217 + continue;
218 + }
160 219
161 220 foreach ( $values as $n => $value ) {
162 - // The first case could be handled by the next one, but we avoid calls to preg_match here.
163 - if ( '*' === $name || ( false !== strpos( $name, '*' ) && preg_match( $pattern, $n ) ) ) {
221 + if ( $matcher->matches( $n, $name ) ) {
164 222 $this->register_string_recursive( $context, $n, $value, $child );
165 223 }
166 224 }
167 225 }
@@ -170,14 +228,35 @@
170 228 // Parent key is a wildcard and no sub-key has been whitelisted.
171 229 $this->register_string_recursive( $context, $n, $value, $key );
172 230 }
173 231 }
174 - } else {
175 - PLL_Admin_Strings::register_string( $option, $values, $context, true );
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 );
176 236 }
177 237 }
178 238
179 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 + /**
180 259 * Filters an option before it is updated.
181 260 *
182 261 * This is the step 1 in the update process, in which we prevent the update of
183 262 * strings to their translations by filtering them out, and we store the updated strings
@@ -191,21 +270,33 @@
191 270 * @return mixed
192 271 */
193 272 public function pre_update_option( $value, $old_value, $name ) {
194 273 // Stores the unfiltered old option value before it is updated in DB.
195 - remove_filter( 'option_' . $name, array( $this, 'translate' ), 10, 2 );
196 - $unfiltered_old_value = get_option( $name );
197 - add_filter( 'option_' . $name, array( $this, 'translate' ), 20, 2 );
274 + $unfiltered_old_value = $this->get_raw_option( $name );
198 275
199 - // Load strings translations according to the admin language filter
200 - $locale = pll_current_language( 'locale' );
201 - if ( empty( $locale ) ) {
202 - $locale = pll_default_language( 'locale' );
276 + $languages = PLL()->model->get_languages_list();
277 +
278 + if ( empty( $languages ) ) {
279 + return $value;
203 280 }
204 - PLL()->load_strings_translations( $locale );
205 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 +
206 297 // Filters out the strings which would be updated to their translations and stores the updated strings.
207 - $value = $this->check_value_recursive( $unfiltered_old_value, $value, $this->keys );
298 + $value = $this->check_value_recursive( $unfiltered_old_value, $value, $this->keys, $this->translations[ $lang ] );
208 299
209 300 return $value;
210 301 }
211 302
@@ -223,22 +314,19 @@
223 314 public function update_option() {
224 315 $curlang = pll_current_language();
225 316
226 317 if ( ! empty( $this->updated_strings ) ) {
227 - foreach ( pll_languages_list() as $lang ) {
318 + foreach ( PLL()->model->get_languages_list() as $language ) {
228 319
229 - $language = PLL()->model->get_language( $lang );
230 - $mo = new PLL_MO();
231 - $mo->import_from_db( $language );
320 + $mo = &$this->translations[ $language->slug ];
232 321
233 322 foreach ( $this->updated_strings as $old_string => $string ) {
234 323 $translation = $mo->translate( $old_string );
235 - if ( ( empty( $curlang ) && $translation === $old_string ) || $lang === $curlang ) {
324 + if ( ( empty( $curlang ) && $translation === $old_string ) || $language->slug === $curlang ) {
236 325 $translation = $string;
237 326 }
238 327
239 - // Removes the old entry and replace it by the new one, with the same translation.
240 - $mo->delete_entry( $old_string );
328 + // Add new entry with new string and old translation.
241 329 $mo->add_entry( $mo->make_entry( $string, $translation ) );
242 330 }
243 331
244 332 $mo->export_to_db( $language );
@@ -243,8 +331,10 @@
243 331
244 332 $mo->export_to_db( $language );
245 333 }
246 334 }
335 +
336 + $this->cache->clean();
247 337 }
248 338
249 339 /**
250 340 * Recursively compares the updated strings to the translation of the old string.
@@ -255,41 +345,44 @@
255 345 * the updated string is stored in {@see PLL_Translate_Option::updated_strings} to be able to
256 346 * later assign the translations to the new value in {@see PLL_Translate_Option::update_option()}.
257 347 *
258 348 * @since 2.9
349 + * @since 3.5 Added $mo parameter.
259 350 *
260 351 * @param mixed $old_values The old option value.
261 - * @param mixed $values The new option value..
352 + * @param mixed $values The new option value.
262 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.
263 355 * @return mixed
264 356 */
265 - protected function check_value_recursive( $old_values, $values, $key ) {
357 + protected function check_value_recursive( $old_values, $values, $key, $mo ) {
266 358 $children = is_array( $key ) ? $key : array();
267 359
268 360 if ( is_array( $values ) || is_object( $values ) ) {
361 + /** @var array|Traversable $values */
269 362 if ( count( $children ) ) {
363 + $matcher = new PLL_Format_Util();
364 +
270 365 foreach ( $children as $name => $child ) {
271 366 if ( is_array( $values ) && is_array( $old_values ) && isset( $old_values[ $name ], $values[ $name ] ) ) {
272 - $values[ $name ] = $this->check_value_recursive( $old_values[ $name ], $values[ $name ], $child );
367 + $values[ $name ] = $this->check_value_recursive( $old_values[ $name ], $values[ $name ], $child, $mo );
273 368 continue;
274 369 }
275 370
276 371 if ( is_object( $values ) && is_object( $old_values ) && isset( $old_values->$name, $values->$name ) ) {
277 - $values->$name = $this->check_value_recursive( $old_values->$name, $values->$name, $child );
372 + $values->$name = $this->check_value_recursive( $old_values->$name, $values->$name, $child, $mo );
278 373 continue;
279 374 }
280 375
281 - $pattern = '#^' . str_replace( '*', '(?:.+)', $name ) . '$#';
282 -
283 376 foreach ( $values as $n => $value ) {
284 377 // The first case could be handled by the next one, but we avoid calls to preg_match here.
285 - if ( '*' === $name || ( false !== strpos( $name, '*' ) && preg_match( $pattern, $n ) ) ) {
378 + if ( $matcher->matches( $n, $name ) ) {
286 379 if ( is_array( $values ) && is_array( $old_values ) && isset( $old_values[ $n ] ) ) {
287 - $values[ $n ] = $this->check_value_recursive( $old_values[ $n ], $value, $child );
380 + $values[ $n ] = $this->check_value_recursive( $old_values[ $n ], $value, $child, $mo );
288 381 }
289 382
290 383 if ( is_object( $values ) && is_object( $old_values ) && isset( $old_values->$n ) ) {
291 - $values->$n = $this->check_value_recursive( $old_values->$n, $value, $child );
384 + $values->$n = $this->check_value_recursive( $old_values->$n, $value, $child, $mo );
292 385 }
293 386 }
294 387 }
295 388 }
@@ -296,18 +389,18 @@
296 389 } else {
297 390 // Parent key is a wildcard and no sub-key has been whitelisted.
298 391 foreach ( $values as $n => $value ) {
299 392 if ( is_array( $values ) && is_array( $old_values ) && isset( $old_values[ $n ] ) ) {
300 - $values[ $n ] = $this->check_value_recursive( $old_values[ $n ], $value, $key );
393 + $values[ $n ] = $this->check_value_recursive( $old_values[ $n ], $value, $key, $mo );
301 394 }
302 395
303 396 if ( is_object( $values ) && is_object( $old_values ) && isset( $old_values->$n ) ) {
304 - $values->$n = $this->check_value_recursive( $old_values->$n, $value, $key );
397 + $values->$n = $this->check_value_recursive( $old_values->$n, $value, $key, $mo );
305 398 }
306 399 }
307 400 }
308 401 } elseif ( $old_values !== $values ) {
309 - if ( pll__( $old_values ) === $values ) {
402 + if ( $mo->translate( $old_values ) === $values ) {
310 403 $values = $old_values; // Prevents updating the value to its translation.
311 404 } else {
312 405 $this->updated_strings[ $old_values ] = $values; // Stores the updated strings.
313 406 }
@@ -316,16 +409,28 @@
316 409 return $values;
317 410 }
318 411
319 412 /**
320 - * Sanitizes the option value.
413 + * Sanitizes the string translation.
321 414 *
322 415 * @since 2.9
416 + * @since 3.7 Add $context and $original parameters.
323 417 *
324 - * @param string $value The unsanitised value.
325 - * @param string $name The name of the option.
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.
326 422 * @return string Sanitized value.
327 423 */
328 - public function sanitize_option( $value, $name ) {
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 */
329 434 return sanitize_option( $name, $value );
330 435 }
331 436 }