PluginProbe
Polylang / 3.5
Polylang v3.5
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.5, at include/translate-option.php

401 lines 12.4 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
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 $cache = $this->cache->get( $lang );
114 if ( false === $cache ) {
115 $cache = $this->translate_string_recursive( $value, $this->keys );
116 $this->cache->set( $lang, $cache );
117 }
118
119 return $cache;
120 }
121
122 /**
123 * Recursively translates the strings registered for an option.
124 *
125 * @since 1.0
126 *
127 * @param mixed $values Either a string to translate or a list of strings to translate.
128 * @param array|bool $key Array of option keys to translate.
129 * @return array|string Translated string(s)
130 */
131 protected function translate_string_recursive( $values, $key ) {
132 $children = is_array( $key ) ? $key : array();
133
134 if ( is_array( $values ) || is_object( $values ) ) {
135 if ( count( $children ) ) {
136 foreach ( $children as $name => $child ) {
137 if ( is_array( $values ) && isset( $values[ $name ] ) ) {
138 $values[ $name ] = $this->translate_string_recursive( $values[ $name ], $child );
139 continue;
140 }
141
142 if ( is_object( $values ) && isset( $values->$name ) ) {
143 $values->$name = $this->translate_string_recursive( $values->$name, $child );
144 continue;
145 }
146
147 $pattern = '#^' . str_replace( '*', '(?:.+)', $name ) . '$#';
148
149 foreach ( $values as $n => &$value ) {
150 // The first case could be handled by the next one, but we avoid calls to preg_match here.
151 if ( '*' === $name || ( false !== strpos( $name, '*' ) && preg_match( $pattern, $n ) ) ) {
152 $value = $this->translate_string_recursive( $value, $child );
153 }
154 }
155 }
156 } else {
157 // Parent key is a wildcard and no sub-key has been whitelisted.
158 foreach ( $values as &$value ) {
159 $value = $this->translate_string_recursive( $value, $key );
160 }
161 }
162 } else {
163 $values = pll__( $values );
164 }
165
166 return $values;
167 }
168
169 /**
170 * Recursively registers strings for an option.
171 *
172 * @since 1.0
173 * @since 2.7 Signature modified
174 *
175 * @param string $context The group in which the strings will be registered.
176 * @param string $option Option name.
177 * @param mixed $values Option value.
178 * @param array|bool $key Array of option keys to translate.
179 * @return void
180 */
181 protected function register_string_recursive( $context, $option, $values, $key ) {
182 if ( is_object( $values ) ) {
183 $values = (array) $values;
184 }
185
186 if ( is_array( $values ) ) {
187 $children = is_array( $key ) ? $key : array();
188
189 if ( count( $children ) ) {
190 foreach ( $children as $name => $child ) {
191 if ( isset( $values[ $name ] ) ) {
192 $this->register_string_recursive( $context, $name, $values[ $name ], $child );
193 continue;
194 }
195
196 $pattern = '#^' . str_replace( '*', '(?:.+)', $name ) . '$#';
197
198 foreach ( $values as $n => $value ) {
199 // The first case could be handled by the next one, but we avoid calls to preg_match here.
200 if ( '*' === $name || ( false !== strpos( $name, '*' ) && preg_match( $pattern, $n ) ) ) {
201 $this->register_string_recursive( $context, $n, $value, $child );
202 }
203 }
204 }
205 } else {
206 foreach ( $values as $n => $value ) {
207 // Parent key is a wildcard and no sub-key has been whitelisted.
208 $this->register_string_recursive( $context, $n, $value, $key );
209 }
210 }
211 } else {
212 PLL_Admin_Strings::register_string( $option, $values, $context, true );
213 }
214 }
215
216 /**
217 * Returns the raw value of an option (without this class' filter).
218 *
219 * A static property is used to make sure that the option is not filtered
220 * whatever the number of instances of this class filtering the option.
221 *
222 * @since 3.3
223 *
224 * @param string $option_name Option name.
225 * @return mixed
226 */
227 protected function get_raw_option( $option_name ) {
228 self::$raw = true;
229 $option_value = get_option( $option_name );
230 self::$raw = false;
231
232 return $option_value;
233 }
234
235 /**
236 * Filters an option before it is updated.
237 *
238 * This is the step 1 in the update process, in which we prevent the update of
239 * strings to their translations by filtering them out, and we store the updated strings
240 * for the next step.
241 *
242 * @since 2.9
243 *
244 * @param mixed $value The new, unserialized option value.
245 * @param mixed $old_value The old (filtered) option value.
246 * @param string $name Option name.
247 * @return mixed
248 */
249 public function pre_update_option( $value, $old_value, $name ) {
250 // Stores the unfiltered old option value before it is updated in DB.
251 $unfiltered_old_value = $this->get_raw_option( $name );
252
253 $languages = PLL()->model->get_languages_list();
254
255 if ( empty( $languages ) ) {
256 return $value;
257 }
258
259 // Load translations in all languages.
260 foreach ( $languages as $language ) {
261 $this->translations[ $language->slug ] = new PLL_MO();
262 $this->translations[ $language->slug ]->import_from_db( $language );
263 }
264
265 $lang = pll_current_language();
266 if ( empty( $lang ) ) {
267 $lang = pll_default_language();
268 }
269
270 if ( empty( $lang ) ) {
271 return $value; // Something's wrong.
272 }
273
274 // Filters out the strings which would be updated to their translations and stores the updated strings.
275 $value = $this->check_value_recursive( $unfiltered_old_value, $value, $this->keys, $this->translations[ $lang ] );
276
277 return $value;
278 }
279
280 /**
281 * Updates the string translations to keep the same translated value when updating the original option.
282 *
283 * This is the step 2 in the update process. Knowing all strings that have been updated,
284 * we remove the old strings from the strings translations and replace them by
285 * the new strings with the old translations.
286 *
287 * @since 2.9
288 *
289 * @return void
290 */
291 public function update_option() {
292 $curlang = pll_current_language();
293
294 if ( ! empty( $this->updated_strings ) ) {
295 foreach ( PLL()->model->get_languages_list() as $language ) {
296
297 $mo = &$this->translations[ $language->slug ];
298
299 foreach ( $this->updated_strings as $old_string => $string ) {
300 $translation = $mo->translate( $old_string );
301 if ( ( empty( $curlang ) && $translation === $old_string ) || $language->slug === $curlang ) {
302 $translation = $string;
303 }
304
305 // Add new entry with new string and old translation.
306 $mo->add_entry( $mo->make_entry( $string, $translation ) );
307 }
308
309 $mo->export_to_db( $language );
310 }
311 }
312
313 $this->cache->clean();
314 }
315
316 /**
317 * Recursively compares the updated strings to the translation of the old string.
318 *
319 * This is the heart of the update process. If an updated string is found to be
320 * the same as the translation of the old string, we restore the old string to
321 * prevent the update in {@see PLL_Translate_Option::pre_update_option()}, otherwise
322 * the updated string is stored in {@see PLL_Translate_Option::updated_strings} to be able to
323 * later assign the translations to the new value in {@see PLL_Translate_Option::update_option()}.
324 *
325 * @since 2.9
326 * @since 3.5 Added $mo parameter.
327 *
328 * @param mixed $old_values The old option value.
329 * @param mixed $values The new option value.
330 * @param array|bool $key Array of option keys to translate.
331 * @param PLL_MO $mo Translations used to compare the updated string to the translated old string.
332 * @return mixed
333 */
334 protected function check_value_recursive( $old_values, $values, $key, $mo ) {
335 $children = is_array( $key ) ? $key : array();
336
337 if ( is_array( $values ) || is_object( $values ) ) {
338 if ( count( $children ) ) {
339 foreach ( $children as $name => $child ) {
340 if ( is_array( $values ) && is_array( $old_values ) && isset( $old_values[ $name ], $values[ $name ] ) ) {
341 $values[ $name ] = $this->check_value_recursive( $old_values[ $name ], $values[ $name ], $child, $mo );
342 continue;
343 }
344
345 if ( is_object( $values ) && is_object( $old_values ) && isset( $old_values->$name, $values->$name ) ) {
346 $values->$name = $this->check_value_recursive( $old_values->$name, $values->$name, $child, $mo );
347 continue;
348 }
349
350 $pattern = '#^' . str_replace( '*', '(?:.+)', $name ) . '$#';
351
352 foreach ( $values as $n => $value ) {
353 // The first case could be handled by the next one, but we avoid calls to preg_match here.
354 if ( '*' === $name || ( false !== strpos( $name, '*' ) && preg_match( $pattern, $n ) ) ) {
355 if ( is_array( $values ) && is_array( $old_values ) && isset( $old_values[ $n ] ) ) {
356 $values[ $n ] = $this->check_value_recursive( $old_values[ $n ], $value, $child, $mo );
357 }
358
359 if ( is_object( $values ) && is_object( $old_values ) && isset( $old_values->$n ) ) {
360 $values->$n = $this->check_value_recursive( $old_values->$n, $value, $child, $mo );
361 }
362 }
363 }
364 }
365 } else {
366 // Parent key is a wildcard and no sub-key has been whitelisted.
367 foreach ( $values as $n => $value ) {
368 if ( is_array( $values ) && is_array( $old_values ) && isset( $old_values[ $n ] ) ) {
369 $values[ $n ] = $this->check_value_recursive( $old_values[ $n ], $value, $key, $mo );
370 }
371
372 if ( is_object( $values ) && is_object( $old_values ) && isset( $old_values->$n ) ) {
373 $values->$n = $this->check_value_recursive( $old_values->$n, $value, $key, $mo );
374 }
375 }
376 }
377 } elseif ( $old_values !== $values ) {
378 if ( $mo->translate( $old_values ) === $values ) {
379 $values = $old_values; // Prevents updating the value to its translation.
380 } else {
381 $this->updated_strings[ $old_values ] = $values; // Stores the updated strings.
382 }
383 }
384
385 return $values;
386 }
387
388 /**
389 * Sanitizes the option value.
390 *
391 * @since 2.9
392 *
393 * @param string $value The unsanitised value.
394 * @param string $name The name of the option.
395 * @return string Sanitized value.
396 */
397 public function sanitize_option( $value, $name ) {
398 return sanitize_option( $name, $value );
399 }
400 }
401