PluginProbe
Polylang / 3.6.7
Polylang v3.6.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
polylang / include / translate-option.php

translate-option.php in Polylang 3.6.7, at include/translate-option.php

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