PluginProbe
Polylang / 3.8
Polylang v3.8
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 / Model / Languages.php

Languages.php in Polylang 3.8, at src/Model/Languages.php

1,388 lines 45.9 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 namespace WP_Syntex\Polylang\Model;
7
8 use PLL_Cache;
9 use PLL_Language;
10 use PLL_Language_Factory;
11 use PLL_Translatable_Objects;
12 use WP_Error;
13 use WP_Term;
14 use WP_Syntex\Polylang\Options\Options;
15
16 defined( 'ABSPATH' ) || exit;
17
18 /**
19 * Model for the languages.
20 *
21 * @since 3.7
22 */
23 class Languages {
24 public const INNER_LOCALE_PATTERN = '[a-z]{2,3}(?:_[A-Z]{2})?(?:_[a-z0-9]+)?';
25 public const INNER_SLUG_PATTERN = '[a-z][a-z0-9_-]*';
26
27 public const LOCALE_PATTERN = '^' . self::INNER_LOCALE_PATTERN . '$';
28 public const SLUG_PATTERN = '^' . self::INNER_SLUG_PATTERN . '$';
29
30 public const TRANSIENT_NAME = 'pll_languages_list';
31 private const CACHE_KEY = 'languages';
32
33 /**
34 * Polylang's options.
35 *
36 * @var Options
37 */
38 private $options;
39
40 /**
41 * Translatable objects registry.
42 *
43 * @var PLL_Translatable_Objects
44 */
45 private $translatable_objects;
46
47 /**
48 * Internal non persistent cache object.
49 *
50 * @var PLL_Cache<mixed>
51 */
52 private $cache;
53
54 /**
55 * Flag set to true during the language objects creation.
56 *
57 * @var bool
58 */
59 private $is_creating_list = false;
60
61 /**
62 * Tells if {@see WP_Syntex\Polylang\Model\Languages::get_list()} can be used.
63 *
64 * @var bool
65 */
66 private $languages_ready = false;
67
68 /**
69 * List of automatic language proxies.
70 *
71 * @var Languages_Proxy_Interface[]
72 *
73 * @phpstan-var array<non-falsy-string, Languages_Proxy_Interface>
74 */
75 private $automatic_proxies = array();
76
77 /**
78 * List of language proxies.
79 *
80 * @var Languages_Proxy_Interface[]
81 *
82 * @phpstan-var array<non-falsy-string, Languages_Proxy_Interface>
83 */
84 private $proxies = array();
85
86 /**
87 * Constructor.
88 *
89 * @since 3.7
90 *
91 * @param Options $options Polylang's options.
92 * @param PLL_Translatable_Objects $translatable_objects Translatable objects registry.
93 * @param PLL_Cache $cache Internal non persistent cache object.
94 *
95 * @phpstan-param PLL_Cache<mixed> $cache
96 */
97 public function __construct( Options $options, PLL_Translatable_Objects $translatable_objects, PLL_Cache $cache ) {
98 $this->options = $options;
99 $this->translatable_objects = $translatable_objects;
100 $this->cache = $cache;
101 }
102
103 /**
104 * Returns the language by its term_id, tl_term_id, slug or locale.
105 *
106 * @since 0.1
107 * @since 3.4 Allow to get a language by `term_taxonomy_id`.
108 * @since 3.7 Moved from `PLL_Model::get_language()` to `WP_Syntex\Polylang\Model\Languages::get()`.
109 *
110 * @param mixed $value `term_id`, `term_taxonomy_id`, `slug`, `locale`, or `w3c` of the queried language.
111 * `term_id` and `term_taxonomy_id` can be fetched for any language taxonomy.
112 * /!\ For the `term_taxonomy_id`, prefix the ID by `tt:` (ex: `"tt:{$tt_id}"`),
113 * this is to prevent confusion between `term_id` and `term_taxonomy_id`.
114 * @return PLL_Language|false Language object, false if no language found.
115 *
116 * @phpstan-param PLL_Language|WP_Term|int|string $value
117 */
118 public function get( $value ) {
119 if ( $value instanceof PLL_Language ) {
120 return $value;
121 }
122
123 // Cast WP_Term to PLL_Language.
124 if ( $value instanceof WP_Term ) {
125 return $this->get( $value->term_id );
126 }
127
128 $return = $this->cache->get( 'language:' . $value );
129
130 if ( $return instanceof PLL_Language ) {
131 return $return;
132 }
133
134 foreach ( $this->get_list() as $lang ) {
135 foreach ( $lang->get_tax_props() as $props ) {
136 $this->cache->set( 'language:' . $props['term_id'], $lang );
137 $this->cache->set( 'language:tt:' . $props['term_taxonomy_id'], $lang );
138 }
139 $this->cache->set( 'language:' . $lang->slug, $lang );
140 $this->cache->set( 'language:' . $lang->locale, $lang );
141 $this->cache->set( 'language:' . $lang->w3c, $lang );
142 }
143
144 /** @var PLL_Language|false */
145 return $this->cache->get( 'language:' . $value );
146 }
147
148 /**
149 * Adds a new language and creates a default category for this language.
150 *
151 * @since 1.2
152 * @since 3.7 Moved from `PLL_Admin_Model::add_language()` to `WP_Syntex\Polylang\Model\Languages::add()`.
153 * @since 3.8 Returns the new object language.
154 *
155 * @param array $args {
156 * Arguments used to create the language.
157 *
158 * @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will
159 * not be loaded...
160 * @type string $name Optional. Language name (used only for display). Default to the language name from {@see src/settings/languages.php}.
161 * @type string $slug Optional. Language code (ideally 2-letters ISO 639-1 language code). Default to the language code from {@see src/settings/languages.php}.
162 * @type bool $rtl Optional. True if rtl language, false otherwise. Default is false.
163 * @type bool $is_rtl Optional. True if rtl language, false otherwise. Will be converted to rtl. Default is false.
164 * @type int $term_group Optional. Language order when displayed. Default is 0.
165 * @type string $flag Optional. Country code, {@see src/settings/flags.php}.
166 * @type string $flag_code Optional. Country code, {@see src/settings/flags.php}. Will be converted to flag.
167 * @type bool $no_default_cat Optional. If set, no default category will be created for this language. Default is false.
168 * }
169 * @return PLL_Language|WP_Error The object language on success, a `WP_Error` otherwise.
170 *
171 * @phpstan-param array{
172 * name?: string,
173 * slug?: string,
174 * locale?: string,
175 * rtl?: bool,
176 * is_rtl?: bool,
177 * term_group?: int|numeric-string,
178 * flag?: string,
179 * flag_code?: string,
180 * no_default_cat?: bool
181 * } $args
182 */
183 public function add( $args ) {
184 $args['rtl'] = $args['rtl'] ?? $args['is_rtl'] ?? null;
185 $args['flag'] = $args['flag'] ?? $args['flag_code'] ?? null;
186 $args['term_group'] = $args['term_group'] ?? 0;
187
188 if ( ! empty( $args['locale'] ) && ( ! isset( $args['name'] ) || ! isset( $args['slug'] ) ) ) {
189 $languages = include POLYLANG_DIR . '/src/settings/languages.php';
190 if ( ! empty( $languages[ $args['locale'] ] ) ) {
191 $found = $languages[ $args['locale'] ];
192 $args['name'] = $args['name'] ?? $found['name'];
193 $args['slug'] = $args['slug'] ?? $found['code'];
194 $args['rtl'] = $args['rtl'] ?? 'rtl' === $found['dir'];
195 $args['flag'] = $args['flag'] ?? $found['flag'];
196 }
197 }
198
199 $errors = $this->validate_lang( $args );
200 if ( $errors->has_errors() ) {
201 return $errors;
202 }
203
204 /**
205 * @phpstan-var array{
206 * name: non-empty-string,
207 * slug: non-empty-string,
208 * locale: non-empty-string,
209 * rtl: bool,
210 * term_group: int|numeric-string,
211 * flag?: non-empty-string,
212 * no_default_cat?: bool
213 * } $args
214 */
215 // First the language taxonomy.
216 $result = wp_insert_term(
217 $args['name'],
218 'language',
219 array(
220 'slug' => $args['slug'],
221 'description' => $this->build_metas( $args ),
222 )
223 );
224 if ( is_wp_error( $result ) ) {
225 return new WP_Error( 'pll_add_language', __( 'Could not add the language.', 'polylang' ) );
226 }
227
228 $id = (int) $result['term_id'];
229
230 $result = wp_update_term( $id, 'language', array( 'term_group' => (int) $args['term_group'] ) ); // Can't set the term group directly in `wp_insert_term()`.
231 if ( is_wp_error( $result ) ) {
232 return new WP_Error( 'pll_add_language', __( 'Could not set the language order.', 'polylang' ) );
233 }
234
235 // The other language taxonomies.
236 $errors = $this->update_secondary_language_terms( $args['slug'], $args['name'] );
237 if ( $errors->has_errors() ) {
238 return $errors;
239 }
240
241 if ( empty( $this->options['default_lang'] ) ) {
242 // If this is the first language created, set it as default language
243 $this->options['default_lang'] = $args['slug'];
244 }
245
246 // Refresh languages.
247 $this->clean_cache();
248 $new_language = $this->get( $id );
249
250 if ( ! $new_language ) {
251 return new WP_Error( 'pll_add_language', __( 'Could not add the language.', 'polylang' ) );
252 }
253
254 flush_rewrite_rules(); // Refresh rewrite rules.
255
256 /**
257 * Fires after a language is added.
258 *
259 * @since 1.9
260 *
261 * @param array $args {
262 * Arguments used to create the language.
263 *
264 * @type string $name Language name (used only for display).
265 * @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
266 * @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will
267 * not be loaded...
268 * @type bool $rtl True if rtl language, false otherwise.
269 * @type int $term_group Language order when displayed.
270 * @type string $flag Optional. Country code, {@see src/settings/flags.php}.
271 * @type bool $no_default_cat Optional. If set, no default category will be created for this language.
272 * }
273 */
274 do_action( 'pll_add_language', $args );
275
276 return $new_language;
277 }
278
279 /**
280 * Updates language properties.
281 *
282 * @since 1.2
283 * @since 3.7 Moved from `PLL_Admin_Model::update_language()` to `WP_Syntex\Polylang\Model\Languages::update()`.
284 * @since 3.8 Returns the updated object language.
285 *
286 * @param array $args {
287 * Arguments used to modify the language.
288 *
289 * @type int $lang_id ID of the language to modify.
290 * @type string $name Optional. Language name (used only for display).
291 * @type string $slug Optional. Language code (ideally 2-letters ISO 639-1 language code).
292 * @type string $locale Optional. WordPress locale. If something wrong is used for the locale, the .mo files will
293 * not be loaded...
294 * @type bool $rtl Optional. True if rtl language, false otherwise.
295 * @type bool $is_rtl Optional. True if rtl language, false otherwise. Will be converted to rtl.
296 * @type int $term_group Optional. Language order when displayed.
297 * @type string $flag Optional, country code, {@see src/settings/flags.php}.
298 * @type string $flag_code Optional. Country code, {@see src/settings/flags.php}. Will be converted to flag.
299 * }
300 * @return PLL_Language|WP_Error The updated object language on success, a `WP_Error` otherwise.
301 *
302 * @phpstan-param array{
303 * lang_id: int|numeric-string,
304 * name?: string,
305 * slug?: string,
306 * locale?: string,
307 * rtl?: bool,
308 * is_rtl?: bool,
309 * term_group?: int|numeric-string,
310 * flag?: string,
311 * flag_code?: string
312 * } $args
313 */
314 public function update( $args ) {
315 $id = (int) $args['lang_id'];
316 $lang = $this->get( $id );
317
318 if ( empty( $lang ) ) {
319 return new WP_Error( 'pll_invalid_language_id', __( 'The language does not seem to exist.', 'polylang' ) );
320 }
321
322 $args['locale'] = $args['locale'] ?? $lang->locale;
323 $args['name'] = $args['name'] ?? $lang->name;
324 $args['slug'] = $args['slug'] ?? $lang->slug;
325 $args['rtl'] = $args['rtl'] ?? $args['is_rtl'] ?? $lang->is_rtl;
326 $args['flag'] = $args['flag'] ?? $args['flag_code'] ?? $lang->flag_code;
327 $args['term_group'] = $args['term_group'] ?? $lang->term_group;
328
329 $errors = $this->validate_lang( $args, $lang );
330 if ( $errors->has_errors() ) {
331 return $errors;
332 }
333
334 /**
335 * @phpstan-var array{
336 * lang_id: int|numeric-string,
337 * name: non-empty-string,
338 * slug: non-empty-string,
339 * locale: non-empty-string,
340 * rtl: bool,
341 * term_group: int|numeric-string,
342 * flag?: non-empty-string
343 * } $args
344 */
345 // Update links to this language in posts and terms in case the slug has been modified.
346 $slug = $args['slug'];
347 $old_slug = $lang->slug;
348
349 // Update the language itself.
350 $errors = $this->update_secondary_language_terms( $args['slug'], $args['name'], $lang );
351 if ( $errors->has_errors() ) {
352 return $errors;
353 }
354
355 $result = wp_update_term(
356 $lang->get_tax_prop( 'language', 'term_id' ),
357 'language',
358 array(
359 'slug' => $slug,
360 'name' => $args['name'],
361 'description' => $this->build_metas( $args ),
362 'term_group' => (int) $args['term_group'],
363 )
364 );
365 if ( is_wp_error( $result ) ) {
366 return new WP_Error( 'pll_update_language', __( 'Could not update the language.', 'polylang' ) );
367 }
368
369 if ( $old_slug !== $slug ) {
370 // Update the language slug in translations.
371 $errors = $this->update_translations( $old_slug, $slug );
372 if ( $errors->has_errors() ) {
373 return $errors;
374 }
375
376 // Update language option in widgets.
377 foreach ( $GLOBALS['wp_registered_widgets'] as $widget ) {
378 if ( ! empty( $widget['callback'][0] ) && ! empty( $widget['params'][0]['number'] ) ) {
379 $obj = $widget['callback'][0];
380 $number = $widget['params'][0]['number'];
381 if ( is_object( $obj ) && method_exists( $obj, 'get_settings' ) && method_exists( $obj, 'save_settings' ) ) {
382 $settings = $obj->get_settings();
383 if ( isset( $settings[ $number ]['pll_lang'] ) && $settings[ $number ]['pll_lang'] == $old_slug ) {
384 $settings[ $number ]['pll_lang'] = $slug;
385 $obj->save_settings( $settings );
386 }
387 }
388 }
389 }
390
391 // Update menus locations in options.
392 $nav_menus = $this->options->get( 'nav_menus' );
393
394 if ( ! empty( $nav_menus ) ) {
395 foreach ( $nav_menus as $theme => $locations ) {
396 foreach ( array_keys( $locations ) as $location ) {
397 if ( isset( $nav_menus[ $theme ][ $location ][ $old_slug ] ) ) {
398 $nav_menus[ $theme ][ $location ][ $slug ] = $nav_menus[ $theme ][ $location ][ $old_slug ];
399 unset( $nav_menus[ $theme ][ $location ][ $old_slug ] );
400 }
401 }
402 }
403
404 $this->options->set( 'nav_menus', $nav_menus );
405 }
406
407 /*
408 * Update domains in options.
409 * This must happen after the term is saved (see `Options\Business\Domains::sanitize()`).
410 */
411 $domains = $this->options->get( 'domains' );
412
413 if ( isset( $domains[ $old_slug ] ) ) {
414 $domains[ $slug ] = $domains[ $old_slug ];
415 unset( $domains[ $old_slug ] );
416 $this->options->set( 'domains', $domains );
417 }
418
419 /*
420 * Update the default language option if necessary.
421 * This must happen after the term is saved (see `Options\Business\Default_Lang::sanitize()`).
422 */
423 if ( $lang->is_default ) {
424 $this->options->set( 'default_lang', $slug );
425 }
426 }
427
428 // Refresh languages.
429 $this->clean_cache();
430 $updated_language = $this->get( $id );
431
432 if ( ! $updated_language ) {
433 return new WP_Error( 'pll_update_language', __( 'Could not update the language.', 'polylang' ) );
434 }
435
436 // Refresh rewrite rules.
437 flush_rewrite_rules();
438
439 /**
440 * Fires after a language is updated.
441 *
442 * @since 1.9
443 * @since 3.2 Added $lang parameter.
444 *
445 * @param array $args {
446 * Arguments used to modify the language. @see WP_Syntex\Polylang\Model\Languages::update().
447 *
448 * @type string $name Language name (used only for display).
449 * @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
450 * @type string $locale WordPress locale.
451 * @type bool $rtl True if rtl language, false otherwise.
452 * @type int $term_group Language order when displayed.
453 * @type string $no_default_cat Optional, if set, no default category has been created for this language.
454 * @type string $flag Optional, country code, @see src/settings/flags.php.
455 * }
456 * @param PLL_Language $lang Previous value of the language being edited.
457 */
458 do_action( 'pll_update_language', $args, $lang );
459
460 return $updated_language;
461 }
462
463 /**
464 * Deletes a language.
465 *
466 * @since 1.2
467 * @since 3.7 Moved from `PLL_Admin_Model::delete_language()` to `WP_Syntex\Polylang\Model\Languages::delete()`.
468 *
469 * @param int $lang_id Language term_id.
470 * @return bool
471 */
472 public function delete( $lang_id ): bool {
473 $lang = $this->get( (int) $lang_id );
474
475 if ( empty( $lang ) ) {
476 return false;
477 }
478
479 // Oops! We are deleting the default language...
480 // Need to do this before losing the information for default category translations.
481 if ( $lang->is_default ) {
482 $slugs = $this->get_list( array( 'fields' => 'slug' ) );
483 $slugs = array_diff( $slugs, array( $lang->slug ) );
484
485 if ( ! empty( $slugs ) ) {
486 $this->update_default( reset( $slugs ) ); // Arbitrary choice...
487 } else {
488 unset( $this->options['default_lang'] );
489 }
490 }
491
492 // Delete the translations.
493 $this->update_translations( $lang->slug );
494
495 // Delete language option in widgets.
496 foreach ( $GLOBALS['wp_registered_widgets'] as $widget ) {
497 if ( ! empty( $widget['callback'][0] ) && ! empty( $widget['params'][0]['number'] ) ) {
498 $obj = $widget['callback'][0];
499 $number = $widget['params'][0]['number'];
500 if ( is_object( $obj ) && method_exists( $obj, 'get_settings' ) && method_exists( $obj, 'save_settings' ) ) {
501 $settings = $obj->get_settings();
502 if ( isset( $settings[ $number ]['pll_lang'] ) && $settings[ $number ]['pll_lang'] == $lang->slug ) {
503 unset( $settings[ $number ]['pll_lang'] );
504 $obj->save_settings( $settings );
505 }
506 }
507 }
508 }
509
510 // Delete menus locations.
511 $nav_menus = $this->options->get( 'nav_menus' );
512
513 if ( ! empty( $nav_menus ) ) {
514 foreach ( $nav_menus as $theme => $locations ) {
515 foreach ( array_keys( $locations ) as $location ) {
516 unset( $nav_menus[ $theme ][ $location ][ $lang->slug ] );
517 }
518 }
519
520 $this->options->set( 'nav_menus', $nav_menus );
521 }
522
523 // Delete users options.
524 delete_metadata( 'user', 0, 'pll_filter_content', '', true );
525 delete_metadata( 'user', 0, "description_{$lang->slug}", '', true );
526
527 // Delete domain.
528 $domains = $this->options->get( 'domains' );
529 unset( $domains[ $lang->slug ] );
530 $this->options->set( 'domains', $domains );
531
532 /*
533 * Delete the language itself.
534 *
535 * Reverses the language taxonomies order is required to make sure 'language' is deleted in last.
536 *
537 * The initial order with the 'language' taxonomy at the beginning of 'PLL_Language::term_props' property
538 * is done by {@see PLL_Model::filter_terms_orderby()}
539 */
540 foreach ( array_reverse( $lang->get_tax_props( 'term_id' ) ) as $taxonomy_name => $term_id ) {
541 wp_delete_term( $term_id, $taxonomy_name );
542 }
543
544 // Refresh languages.
545 $this->clean_cache();
546 $this->get_list();
547
548 flush_rewrite_rules(); // refresh rewrite rules
549 return true;
550 }
551
552 /**
553 * Checks if there are languages or not.
554 *
555 * @since 3.3
556 * @since 3.7 Moved from `PLL_Model::has_languages()` to `WP_Syntex\Polylang\Model\Languages::has()`.
557 *
558 * @return bool True if there are, false otherwise.
559 */
560 public function has(): bool {
561 if ( ! empty( $this->cache->get( self::CACHE_KEY ) ) ) {
562 return true;
563 }
564
565 if ( ! empty( get_transient( self::TRANSIENT_NAME ) ) ) {
566 return true;
567 }
568
569 return ! empty( $this->get_terms() );
570 }
571
572 /**
573 * Returns the list of available languages.
574 * - Stores the list in a db transient (except flags), unless `PLL_CACHE_LANGUAGES` is set to false.
575 * - Caches the list (with flags) in a `PLL_Cache` object.
576 *
577 * @since 0.1
578 * @since 3.7 Moved from `PLL_Model::get_languages_list()` to `WP_Syntex\Polylang\Model\Languages::get_list()`.
579 *
580 * @param array $args {
581 * @type string $fields Returns only that field if set; {@see PLL_Language} for a list of fields.
582 * @type bool $hide_empty Hides languages with no posts if set to `true` (defaults to `false`). Deprecated, use the `hide_empty` filter instead.
583 * @type bool $hide_default Hides default language from the list (default to `false`). Deprecated, use the `hide_default` filter instead.
584 * }
585 * @return array List of PLL_Language objects or PLL_Language object properties.
586 */
587 public function get_list( $args = array() ): array {
588 if ( ! $this->are_ready() ) {
589 _doing_it_wrong(
590 __METHOD__ . '()',
591 "It must not be called before the hook 'pll_pre_init'.",
592 '3.4'
593 );
594 }
595
596 if ( ! empty( $args['hide_empty'] ) ) {
597 _deprecated_argument(
598 __METHOD__,
599 '3.8',
600 sprintf(
601 /* translators: 1: argument name, 2: PHP code snippet. */
602 esc_html__( 'The argument %1$s has been replaced by %2$s.', 'polylang' ),
603 '<code>hide_empty</code>',
604 '<code>PLL()->model->languages->filter( \'hide_empty\' )->get_list()</code>'
605 )
606 );
607 }
608
609 if ( ! empty( $args['hide_default'] ) ) {
610 _deprecated_argument(
611 __METHOD__,
612 '3.8',
613 sprintf(
614 /* translators: 1: argument name, 2: PHP code snippet. */
615 esc_html__( 'The argument %1$s has been replaced by %2$s.', 'polylang' ),
616 '<code>hide_default</code>',
617 '<code>PLL()->model->languages->filter( \'hide_default\' )->get_list()</code>'
618 )
619 );
620 }
621
622 $languages = $this->cache->get( self::CACHE_KEY );
623
624 if ( ! is_array( $languages ) ) {
625 // Bail out early if languages are currently created to avoid an infinite loop.
626 if ( $this->is_creating_list ) {
627 return array();
628 }
629
630 $this->is_creating_list = true;
631
632 if ( ! pll_get_constant( 'PLL_CACHE_LANGUAGES', true ) ) {
633 // Create the languages from taxonomies.
634 $languages = $this->get_from_taxonomies();
635 } else {
636 $languages = get_transient( self::TRANSIENT_NAME );
637
638 if ( empty( $languages ) || ! is_array( $languages ) || empty( reset( $languages )['term_props'] ) ) { // Test `term_props` in case we got a transient older than 3.4.
639 // Create the languages from taxonomies.
640 $languages = $this->get_from_taxonomies();
641 } else {
642 // Create the languages directly from arrays stored in the transient.
643 $languages = array_map(
644 array( new PLL_Language_Factory( $this->options ), 'get' ),
645 $languages
646 );
647
648 // Remove potential empty language.
649 $languages = array_filter( $languages );
650
651 // Re-index.
652 $languages = array_values( $languages );
653 }
654 }
655
656 /**
657 * Filters the list of languages *after* it is stored in the persistent cache.
658 * /!\ This filter is fired *before* the $polylang object is available.
659 *
660 * @since 1.8
661 * @since 3.4 Deprecated. If you used this hook to filter URLs, you may hook `'site_url'` instead.
662 * @deprecated
663 *
664 * @param PLL_Language[] $languages The list of language objects.
665 */
666 $languages = apply_filters_deprecated( 'pll_after_languages_cache', array( $languages ), '3.4' );
667
668 foreach ( $this->automatic_proxies as $proxy ) {
669 $languages = $proxy->filter( $languages );
670 }
671
672 if ( $this->are_ready() ) {
673 $this->cache->set( self::CACHE_KEY, $languages );
674 }
675
676 $this->is_creating_list = false;
677 }
678
679 // Backward compatibility with Polylang < 3.8.
680 $languages = array_filter(
681 $languages,
682 function ( $lang ) use ( $args ) {
683 $keep_empty = empty( $args['hide_empty'] ) || $lang->get_tax_prop( 'language', 'count' );
684 $keep_default = empty( $args['hide_default'] ) || ! $lang->is_default;
685 return $keep_empty && $keep_default;
686 }
687 );
688
689 $languages = array_values( $languages ); // Re-index.
690 return $this->maybe_convert_list( $languages, (array) $args );
691 }
692
693 /**
694 * Tells if {@see WP_Syntex\Polylang\Model\Languages::get_list()} can be used.
695 *
696 * @since 3.4
697 * @since 3.7 Moved from `PLL_Model::are_languages_ready()` to `WP_Syntex\Polylang\Model\Languages::are_ready()`.
698 *
699 * @return bool
700 */
701 public function are_ready(): bool {
702 return $this->languages_ready;
703 }
704
705 /**
706 * Sets the internal property `$languages_ready` to `true`, telling that {@see WP_Syntex\Polylang\Model\Languages::get_list()} can be used.
707 *
708 * @since 3.4
709 * @since 3.7 Moved from `PLL_Model::set_languages_ready()` to `WP_Syntex\Polylang\Model\Languages::set_ready()`.
710 *
711 * @return void
712 */
713 public function set_ready(): void {
714 $this->languages_ready = true;
715 }
716
717 /**
718 * Returns the default language.
719 *
720 * @since 3.4
721 * @since 3.7 Moved from `PLL_Model::get_default_language()` to `WP_Syntex\Polylang\Model\Languages::get_default()`.
722 *
723 * @return PLL_Language|false Default language object, `false` if no language found.
724 */
725 public function get_default() {
726 if ( empty( $this->options['default_lang'] ) ) {
727 return false;
728 }
729
730 return $this->get( $this->options['default_lang'] );
731 }
732
733 /**
734 * Updates the default language.
735 * Takes care to update default category, nav menu locations, and flushes cache and rewrite rules.
736 *
737 * @since 1.8
738 * @since 3.7 Moved from `PLL_Admin_Model::update_default_lang()` to `WP_Syntex\Polylang\Model\Languages::update_default()`.
739 * Returns a `WP_Error` object.
740 *
741 * @param string $slug New language slug.
742 * @return WP_Error A `WP_Error` object containing possible errors during slug validation/sanitization.
743 */
744 public function update_default( $slug ): WP_Error {
745 $prev_default_lang = $this->options->get( 'default_lang' );
746
747 if ( $prev_default_lang === $slug ) {
748 return new WP_Error();
749 }
750
751 $errors = $this->options->set( 'default_lang', $slug );
752
753 if ( $errors->has_errors() ) {
754 return $errors;
755 }
756
757 // The nav menus stored in theme locations should be in the default language.
758 $theme = get_stylesheet();
759 if ( ! empty( $this->options['nav_menus'][ $theme ] ) ) {
760 $menus = array();
761
762 foreach ( $this->options['nav_menus'][ $theme ] as $key => $loc ) {
763 $menus[ $key ] = empty( $loc[ $slug ] ) ? 0 : $loc[ $slug ];
764 }
765 set_theme_mod( 'nav_menu_locations', $menus );
766 }
767
768 /**
769 * Fires when a default language is updated.
770 *
771 * @since 3.1
772 * @since 3.7 The previous default language's slug is passed as 2nd param.
773 * The default language is updated before this hook is fired.
774 *
775 * @param string $slug New default language's slug.
776 * @param string $prev_default_lang Previous default language's slug.
777 */
778 do_action( 'pll_update_default_lang', $slug, $prev_default_lang );
779
780 // Update options.
781
782 $this->clean_cache();
783 flush_rewrite_rules();
784
785 return new WP_Error();
786 }
787
788 /**
789 * Maybe adds the missing language terms for 3rd party language taxonomies.
790 *
791 * @since 3.4
792 * @since 3.7 Moved from `PLL_Model::maybe_create_language_terms()` to `WP_Syntex\Polylang\Model\Languages::maybe_create_terms()`.
793 *
794 * @return void
795 */
796 public function maybe_create_terms(): void {
797 $registered_taxonomies = array_diff(
798 $this->translatable_objects->get_taxonomy_names( array( 'language' ) ),
799 // Exclude the post and term language taxonomies from the list.
800 array(
801 $this->translatable_objects->get( 'post' )->get_tax_language(),
802 $this->translatable_objects->get( 'term' )->get_tax_language(),
803 )
804 );
805
806 if ( empty( $registered_taxonomies ) ) {
807 // No 3rd party language taxonomies.
808 return;
809 }
810
811 // We have at least one 3rd party language taxonomy.
812 $known_taxonomies = get_option( 'pll_language_taxonomies', array() );
813 $known_taxonomies = is_array( $known_taxonomies ) ? $known_taxonomies : array();
814 $new_taxonomies = array_diff( $registered_taxonomies, $known_taxonomies );
815
816 if ( empty( $new_taxonomies ) ) {
817 // No new 3rd party language taxonomies.
818 return;
819 }
820
821 // We have at least one unknown 3rd party language taxonomy.
822 foreach ( $this->get_list() as $language ) {
823 $this->update_secondary_language_terms( $language->slug, $language->name, $language, $new_taxonomies );
824 }
825
826 // Clear the cache, so the new `term_id` and `term_taxonomy_id` appear in the languages list.
827 $this->clean_cache();
828
829 // Keep the previous values, so this is triggered only once per taxonomy.
830 update_option( 'pll_language_taxonomies', array_merge( $known_taxonomies, $new_taxonomies ) );
831 }
832
833 /**
834 * Cleans language cache.
835 *
836 * @since 3.7
837 *
838 * @return void
839 */
840 public function clean_cache(): void {
841 delete_transient( self::TRANSIENT_NAME );
842 $this->clean_local_cache();
843 }
844
845 /**
846 * Cleans local language cache.
847 *
848 * @since 3.8
849 *
850 * @return void
851 */
852 public function clean_local_cache(): void {
853 $this->cache->clean();
854 }
855
856 /**
857 * Deletes the transient from the options table since WordPress does not do it when using object cache.
858 *
859 * @since 3.8
860 *
861 * @return void
862 */
863 public function delete_transient_from_options_table(): void {
864 if ( wp_using_ext_object_cache() || wp_installing() ) {
865 delete_option( '_transient_' . self::TRANSIENT_NAME );
866 }
867 }
868
869 /**
870 * Applies arguments that change the type of the elements of the given list of languages.
871 *
872 * @since 3.8
873 *
874 * @param PLL_Language[] $languages The list of language objects.
875 * @param array $args {
876 * @type string $fields Optional. Returns only that field if set; {@see PLL_Language} for a list of fields.
877 * }
878 * @return array List of `PLL_Language` objects or `PLL_Language` object properties.
879 */
880 public function maybe_convert_list( array $languages, array $args ): array {
881 if ( ! empty( $args['fields'] ) ) {
882 return wp_list_pluck( $languages, $args['fields'] );
883 }
884 return $languages;
885 }
886
887 /**
888 * Registers languages proxies.
889 *
890 * @since 3.8
891 *
892 * @param Languages_Proxy_Interface $proxy Proxy instance.
893 * @param string $mode Optional. Tell how the proxy must be applied. Possible values are:
894 * - `callable`: the proxy must be called manually with `filter()`.
895 * - `automatic`: the proxy is meant to be always called, automatically.
896 * Default is `callable`.
897 * @return self
898 *
899 * @phpstan-param 'callable'|'automatic' $mode
900 */
901 public function register_proxy( Languages_Proxy_Interface $proxy, string $mode = 'callable' ): self {
902 if ( 'automatic' === $mode ) {
903 $this->automatic_proxies[ $proxy->key() ] = $proxy;
904 } else {
905 $this->proxies[ $proxy->key() ] = $proxy;
906 }
907 return $this;
908 }
909
910 /**
911 * Stacks a proxy that will filter the list of languages.
912 *
913 * @since 3.8
914 *
915 * @param string $key Proxy's key.
916 * @return Languages_Proxies
917 */
918 public function filter( string $key ): Languages_Proxies {
919 return new Languages_Proxies( $this, $this->proxies, $key );
920 }
921
922 /**
923 * Builds the language metas into an array and serializes it, to be stored in the term description.
924 *
925 * @since 3.4
926 * @since 3.7 Moved from `PLL_Admin_Model::build_language_metas()` to `WP_Syntex\Polylang\Model\Languages::build_metas()`.
927 *
928 * @param array $args {
929 * Arguments used to build the language metas.
930 *
931 * @type string $name Language name (used only for display).
932 * @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
933 * @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will not
934 * be loaded...
935 * @type bool $rtl True if rtl language, false otherwise.
936 * @type int $term_group Language order when displayed.
937 * @type int $lang_id Optional, ID of the language to modify. An empty value means the language is being
938 * created.
939 * @type string $flag Optional, country code, {@see src/settings/flags.php}.
940 * }
941 * @return string The serialized description array updated.
942 *
943 * @phpstan-param array{
944 * name: non-empty-string,
945 * slug: non-empty-string,
946 * locale: non-empty-string,
947 * rtl: bool,
948 * term_group: int|numeric-string,
949 * lang_id?: int|numeric-string,
950 * flag?: non-empty-string
951 * } $args
952 */
953 protected function build_metas( array $args ): string {
954 if ( ! empty( $args['lang_id'] ) ) {
955 $language_term = get_term( (int) $args['lang_id'] );
956
957 if ( $language_term instanceof WP_Term ) {
958 $old_data = maybe_unserialize( $language_term->description );
959 }
960 }
961
962 if ( empty( $old_data ) || ! is_array( $old_data ) ) {
963 $old_data = array();
964 }
965
966 $new_data = array(
967 'locale' => $args['locale'],
968 'rtl' => ! empty( $args['rtl'] ),
969 'flag_code' => empty( $args['flag'] ) ? '' : $args['flag'],
970 );
971
972 /**
973 * Allow to add data to store for a language.
974 * `$locale`, `$rtl`, and `$flag_code` cannot be overwritten.
975 *
976 * @since 3.4
977 *
978 * @param mixed[] $add_data Data to add.
979 * @param mixed[] $args {
980 * Arguments used to create the language.
981 *
982 * @type string $name Language name (used only for display).
983 * @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
984 * @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will
985 * not be loaded...
986 * @type bool $rtl True if rtl language, false otherwise.
987 * @type int $term_group Language order when displayed.
988 * @type int $lang_id Optional, ID of the language to modify. An empty value means the language is
989 * being created.
990 * @type string $flag Optional, country code, {@see src/settings/flags.php}.
991 * }
992 * @param mixed[] $new_data New data.
993 * @param mixed[] $old_data {
994 * Original data. Contains at least the following:
995 *
996 * @type string $locale WordPress locale.
997 * @type bool $rtl True if rtl language, false otherwise.
998 * @type string $flag_code Country code.
999 * }
1000 */
1001 $add_data = apply_filters( 'pll_language_metas', array(), $args, $new_data, $old_data );
1002 // Don't allow to overwrite `$locale`, `$rtl`, and `$flag_code`.
1003 $new_data = array_merge( $old_data, $add_data, $new_data );
1004
1005 /** @var non-empty-string $serialized maybe_serialize() cannot return anything else than a string when fed by an array. */
1006 $serialized = maybe_serialize( $new_data );
1007 return $serialized;
1008 }
1009
1010 /**
1011 * Validates data entered when creating or updating a language.
1012 *
1013 * @since 0.4
1014 * @since 3.7 Moved from `PLL_Admin_Model::validate_lang()` to `WP_Syntex\Polylang\Model\Languages::validate_lang()`.
1015 *
1016 * @param array $args Parameters of {@see WP_Syntex\Polylang\Model\Languages::add() or @see WP_Syntex\Polylang\Model\Languages::update()}.
1017 * @param PLL_Language|null $lang Optional the language currently updated, the language is created if not set.
1018 * @return WP_Error
1019 *
1020 * @phpstan-param array{
1021 * locale?: string,
1022 * slug?: string,
1023 * name?: string,
1024 * flag?: string
1025 * } $args
1026 */
1027 protected function validate_lang( $args, ?PLL_Language $lang = null ): WP_Error {
1028 $errors = new WP_Error();
1029
1030 // Validate locale with the same pattern as WP 4.3. See #28303.
1031 if ( empty( $args['locale'] ) || ! preg_match( '#' . self::LOCALE_PATTERN . '#', $args['locale'], $matches ) ) {
1032 $errors->add( 'pll_invalid_locale', __( 'Enter a valid WordPress locale', 'polylang' ) );
1033 }
1034
1035 // Validate slug characters.
1036 if ( empty( $args['slug'] ) || ! preg_match( '#' . self::SLUG_PATTERN . '#', $args['slug'] ) ) {
1037 $errors->add( 'pll_invalid_slug', __( 'The language code contains invalid characters', 'polylang' ) );
1038 }
1039
1040 // Validate slug is unique.
1041 foreach ( $this->get_list() as $language ) {
1042 if ( ! empty( $args['slug'] ) && $language->slug === $args['slug'] && ( null === $lang || $lang->term_id !== $language->term_id ) ) {
1043 $errors->add( 'pll_non_unique_slug', __( 'The language code must be unique', 'polylang' ) );
1044 }
1045 }
1046
1047 // Validate name.
1048 // No need to sanitize it as `wp_insert_term()` will do it for us.
1049 if ( empty( $args['name'] ) ) {
1050 $errors->add( 'pll_invalid_name', __( 'The language must have a name', 'polylang' ) );
1051 }
1052
1053 // Validate flag.
1054 if ( ! empty( $args['flag'] ) && ! is_readable( POLYLANG_DIR . '/flags/' . $args['flag'] . '.png' ) ) {
1055 $flag = PLL_Language::get_flag_information( $args['flag'] );
1056
1057 if ( ! empty( $flag['url'] ) ) {
1058 $response = function_exists( 'vip_safe_wp_remote_get' ) ? vip_safe_wp_remote_get( sanitize_url( $flag['url'] ) ) : wp_remote_get( sanitize_url( $flag['url'] ) );
1059 }
1060
1061 if ( empty( $response ) || is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
1062 $errors->add( 'pll_invalid_flag', __( 'The flag does not exist', 'polylang' ) );
1063 }
1064 }
1065
1066 return $errors;
1067 }
1068
1069 /**
1070 * Updates the translations when a language slug has been modified in settings or deletes them when a language is removed.
1071 *
1072 * @since 0.5
1073 * @since 3.7 Moved from `PLL_Admin_Model::update_translations()` to `WP_Syntex\Polylang\Model\Languages::update_translations()`.
1074 * Visibility changed from public to protected.
1075 *
1076 * @global $wpdb wpdb global instance.
1077 *
1078 * @param string $old_slug The old language slug.
1079 * @param string $new_slug Optional, the new language slug, if not set it means that the language has been deleted.
1080 * @return WP_Error
1081 *
1082 * @phpstan-param non-empty-string $old_slug
1083 */
1084 protected function update_translations( $old_slug, $new_slug = '' ): WP_Error {
1085 global $wpdb;
1086
1087 $term_ids = array();
1088 $dr = array();
1089 $dt = array();
1090 $ut = array();
1091 $errors = new WP_Error();
1092
1093 $taxonomies = $this->translatable_objects->get_taxonomy_names( array( 'translations' ) );
1094 $terms = get_terms( array( 'taxonomy' => $taxonomies ) );
1095
1096 if ( is_array( $terms ) ) {
1097 foreach ( $terms as $term ) {
1098 $term_ids[ $term->taxonomy ][] = $term->term_id;
1099 $tr = maybe_unserialize( $term->description );
1100 $tr = is_array( $tr ) ? $tr : array();
1101
1102 /**
1103 * Filters the unserialized translation group description before it is
1104 * updated when a language is deleted or a language slug is changed.
1105 *
1106 * @since 3.2
1107 *
1108 * @param (int|string[])[] $tr {
1109 * List of translations with lang codes as array keys and IDs as array values.
1110 * Also in this array:
1111 *
1112 * @type string[] $sync List of synchronized translations with lang codes as array keys and array values.
1113 * }
1114 * @param string $old_slug The old language slug.
1115 * @param string $new_slug The new language slug.
1116 * @param WP_Term $term The term containing the post or term translation group.
1117 */
1118 $tr = apply_filters( 'update_translation_group', $tr, $old_slug, $new_slug, $term );
1119
1120 if ( ! empty( $tr[ $old_slug ] ) ) {
1121 if ( $new_slug ) {
1122 $tr[ $new_slug ] = $tr[ $old_slug ]; // Suppress this for delete.
1123 } else {
1124 $dr['id'][] = (int) $tr[ $old_slug ];
1125 $dr['tt'][] = (int) $term->term_taxonomy_id;
1126 }
1127 unset( $tr[ $old_slug ] );
1128
1129 if ( empty( $tr ) || 1 == count( $tr ) ) {
1130 $dt['t'][] = (int) $term->term_id;
1131 $dt['tt'][] = (int) $term->term_taxonomy_id;
1132 } else {
1133 $ut['case'][] = array( $term->term_id, maybe_serialize( $tr ) );
1134 $ut['in'][] = (int) $term->term_id;
1135 }
1136 }
1137 }
1138 }
1139
1140 // Delete relationships.
1141 if ( ! empty( $dr ) ) {
1142 $result = $wpdb->query(
1143 $wpdb->prepare(
1144 sprintf(
1145 "DELETE FROM {$wpdb->term_relationships} WHERE object_id IN (%s) AND term_taxonomy_id IN (%s)",
1146 implode( ',', array_fill( 0, count( $dr['id'] ), '%d' ) ),
1147 implode( ',', array_fill( 0, count( $dr['tt'] ), '%d' ) )
1148 ),
1149 array_merge( $dr['id'], $dr['tt'] )
1150 )
1151 );
1152 if ( false === $result ) {
1153 $errors->add( 'pll_delete_relationships', __( 'Could not delete relationships.', 'polylang' ) );
1154 }
1155 }
1156
1157 // Delete terms.
1158 if ( ! empty( $dt ) ) {
1159 $result = $wpdb->query(
1160 $wpdb->prepare(
1161 sprintf(
1162 "DELETE FROM {$wpdb->terms} WHERE term_id IN (%s)",
1163 implode( ',', array_fill( 0, count( $dt['t'] ), '%d' ) )
1164 ),
1165 $dt['t']
1166 )
1167 );
1168 if ( false === $result ) {
1169 $errors->add( 'pll_delete_terms', __( 'Could not delete translation groups.', 'polylang' ) );
1170 }
1171
1172 $result = $wpdb->query(
1173 $wpdb->prepare(
1174 sprintf(
1175 "DELETE FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id IN (%s)",
1176 implode( ',', array_fill( 0, count( $dt['tt'] ), '%d' ) )
1177 ),
1178 $dt['tt']
1179 )
1180 );
1181 if ( false === $result ) {
1182 $errors->add( 'pll_delete_term_taxonomy', __( 'Could not delete translation groups.', 'polylang' ) );
1183 }
1184 }
1185
1186 // Update terms.
1187 if ( ! empty( $ut ) ) {
1188 $result = $wpdb->query(
1189 $wpdb->prepare(
1190 sprintf(
1191 "UPDATE {$wpdb->term_taxonomy} SET description = ( CASE term_id %s END ) WHERE term_id IN (%s)",
1192 implode( ' ', array_fill( 0, count( $ut['case'] ), 'WHEN %d THEN %s' ) ),
1193 implode( ',', array_fill( 0, count( $ut['in'] ), '%d' ) )
1194 ),
1195 array_merge( array_merge( ...$ut['case'] ), $ut['in'] )
1196 )
1197 );
1198 if ( false === $result ) {
1199 $errors->add( 'pll_update_term_taxonomy', __( 'Could not update translation groups.', 'polylang' ) );
1200 }
1201 }
1202
1203 if ( ! empty( $term_ids ) ) {
1204 foreach ( $term_ids as $taxonomy => $ids ) {
1205 clean_term_cache( $ids, $taxonomy );
1206 }
1207 }
1208
1209 return $errors;
1210 }
1211
1212 /**
1213 * Updates or adds new terms for a secondary language taxonomy (aka not 'language').
1214 *
1215 * @since 3.4
1216 * @since 3.7 Moved from `PLL_Model::update_secondary_language_terms()` to `WP_Syntex\Polylang\Model\Languages::update_secondary_language_terms()`.
1217 *
1218 * @param string $slug Language term slug (with or without the `pll_` prefix).
1219 * @param string $name Language name (label).
1220 * @param PLL_Language|null $language Optional. A language object. Required to update the existing terms.
1221 * @param string[] $taxonomies Optional. List of language taxonomies to deal with. An empty value means
1222 * all of them. Defaults to all taxonomies.
1223 * @return WP_Error
1224 *
1225 * @phpstan-param non-empty-string $slug
1226 * @phpstan-param non-empty-string $name
1227 * @phpstan-param array<non-empty-string> $taxonomies
1228 */
1229 protected function update_secondary_language_terms( $slug, $name, ?PLL_Language $language = null, array $taxonomies = array() ): WP_Error {
1230 $slug = 0 === strpos( $slug, 'pll_' ) ? $slug : "pll_$slug";
1231 $errors = new WP_Error();
1232
1233 foreach ( $this->translatable_objects->get_secondary_translatable_objects() as $object ) {
1234 if ( ! empty( $taxonomies ) && ! in_array( $object->get_tax_language(), $taxonomies, true ) ) {
1235 // Not in the list.
1236 continue;
1237 }
1238
1239 if ( ! empty( $language ) ) {
1240 $term_id = $language->get_tax_prop( $object->get_tax_language(), 'term_id' );
1241 } else {
1242 $term_id = 0;
1243 }
1244
1245 if ( empty( $term_id ) ) {
1246 // Attempt to repair the language if a term has been deleted by a database cleaning tool.
1247 $result = wp_insert_term( $name, $object->get_tax_language(), array( 'slug' => $slug ) );
1248 if ( is_wp_error( $result ) ) {
1249 $errors->add(
1250 'pll_add_secondary_language_terms',
1251 /* translators: %s is a taxonomy name */
1252 sprintf( __( 'Could not add secondary language term for taxonomy %s.', 'polylang' ), $object->get_tax_language() )
1253 );
1254 }
1255 continue;
1256 }
1257
1258 /** @var PLL_Language $language */
1259 if ( "pll_{$language->slug}" !== $slug || $language->name !== $name ) {
1260 // Something has changed.
1261 $result = wp_update_term( $term_id, $object->get_tax_language(), array( 'slug' => $slug, 'name' => $name ) );
1262 if ( is_wp_error( $result ) ) {
1263 $errors->add(
1264 'pll_update_secondary_language_terms',
1265 /* translators: %s is a taxonomy name */
1266 sprintf( __( 'Could not update secondary language term for taxonomy %s.', 'polylang' ), $object->get_tax_language() )
1267 );
1268 }
1269 }
1270 }
1271
1272 return $errors;
1273 }
1274
1275 /**
1276 * Returns the list of available languages, based on the language taxonomy terms.
1277 * Stores the list in a db transient and in a `PLL_Cache` object.
1278 *
1279 * @since 3.4
1280 * @since 3.7 Moved from `PLL_Model::get_languages_from_taxonomies()` to `WP_Syntex\Polylang\Model\Languages::get_from_taxonomies()`.
1281 *
1282 * @return PLL_Language[] An array of `PLL_Language` objects, array keys are the type.
1283 *
1284 * @phpstan-return list<PLL_Language>
1285 */
1286 protected function get_from_taxonomies(): array {
1287 $terms_by_slug = array();
1288
1289 foreach ( $this->get_terms() as $term ) {
1290 // Except for language taxonomy term slugs, remove 'pll_' prefix from the other language taxonomy term slugs.
1291 $key = 'language' === $term->taxonomy ? $term->slug : substr( $term->slug, 4 );
1292 $terms_by_slug[ $key ][ $term->taxonomy ] = $term;
1293 }
1294
1295 /**
1296 * @var (
1297 * array{
1298 * string: array{
1299 * language: WP_Term,
1300 * }&array<non-empty-string, WP_Term>
1301 * }
1302 * ) $terms_by_slug
1303 */
1304 $languages = array_filter(
1305 array_map(
1306 array( new PLL_Language_Factory( $this->options ), 'get_from_terms' ),
1307 array_values( $terms_by_slug )
1308 )
1309 );
1310
1311 /**
1312 * Filters the list of languages *before* it is stored in the persistent cache.
1313 * /!\ This filter is fired *before* the $polylang object is available.
1314 *
1315 * @since 1.7.5
1316 * @since 3.4 Deprecated.
1317 * @deprecated
1318 *
1319 * @param PLL_Language[] $languages The list of language objects.
1320 * @param Language $model `Language` object.
1321 */
1322 $languages = apply_filters_deprecated( 'pll_languages_list', array( $languages, $this ), '3.4', 'pll_additional_language_data' );
1323
1324 if ( ! $this->are_ready() ) {
1325 // Do not cache an incomplete list.
1326 /** @var list<PLL_Language> $languages */
1327 return $languages;
1328 }
1329
1330 /*
1331 * Don't store directly objects as it badly break with some hosts ( GoDaddy ) due to race conditions when using object cache.
1332 * Thanks to captin411 for catching this!
1333 *
1334 * @see https://wordpress.org/support/topic/fatal-error-pll_model_languages_list?replies=8#post-6782255
1335 */
1336 $languages_data = array_map(
1337 function ( $language ) {
1338 return $language->to_array( 'db' );
1339 },
1340 $languages
1341 );
1342
1343 set_transient( self::TRANSIENT_NAME, $languages_data );
1344
1345 /** @var list<PLL_Language> $languages */
1346 return $languages;
1347 }
1348
1349 /**
1350 * Returns the list of existing language terms.
1351 * - Returns all terms, that are or not assigned to posts.
1352 * - Terms are ordered by `term_group` and `term_id` (see `WP_Syntex\Polylang\Model\Languages::filter_terms_orderby()`).
1353 *
1354 * @since 3.2.3
1355 * @since 3.7 Moved from `PLL_Model::get_language_terms()` to `WP_Syntex\Polylang\Model\Languages::get_terms()`.
1356 *
1357 * @return WP_Term[]
1358 */
1359 protected function get_terms(): array {
1360 $terms = get_terms(
1361 array(
1362 'taxonomy' => $this->translatable_objects->get_taxonomy_names( array( 'language' ) ),
1363 'hide_empty' => false,
1364 )
1365 );
1366
1367 if ( empty( $terms ) || is_wp_error( $terms ) ) {
1368 return array();
1369 }
1370
1371 $callback = static function ( $a, $b ) {
1372 if ( $a->taxonomy !== $b->taxonomy ) {
1373 // Sort terms by 'language' taxonomy first.
1374 return 'language' === $a->taxonomy ? -1 : 1;
1375 }
1376 if ( $a->term_group !== $b->term_group ) {
1377 // Then by term_group.
1378 return $a->term_group < $b->term_group ? -1 : 1;
1379 }
1380 // Then by term_id.
1381 return $a->term_id < $b->term_id ? -1 : 1;
1382 };
1383
1384 usort( $terms, $callback );
1385 return $terms;
1386 }
1387 }
1388