# polylang/trunk/src/frontend/frontend-nav-menu.php

Polylang, version trunk. 344 lines.

- Page: https://pluginprobe.com/plugins/polylang/trunk/code/src/frontend/frontend-nav-menu.php
- Raw: https://pluginprobe.com/plugins/polylang/trunk/raw/src/frontend/frontend-nav-menu.php
- Modified: 2026-09-14T16:30:38+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/polylang/trunk/code/src/frontend/frontend-nav-menu.php#L10-L20`.

```php
<?php
/**
 * @package Polylang
 */

use WP_Syntex\Polylang\Switcher\Switcher;
use WP_Syntex\Polylang\Switcher\Element\Nav;
use WP_Syntex\Polylang\Switcher\Fields\Menu as Fields;
use WP_Syntex\Polylang\Switcher\Settings\Menu as Settings;

/**
 * Manages custom menus translations as well as the language switcher menu item on frontend
 *
 * @since 1.2
 */
class PLL_Frontend_Nav_Menu extends PLL_Nav_Menu {
	/**
	 * Current language.
	 *
	 * @var PLL_Language|null|false
	 */
	public $curlang;

	/**
	 * @var PLL_Links|null
	 */
	private ?PLL_Links $links;

	/**
	 * Constructor
	 *
	 * @since 1.2
	 *
	 * @param PLL_Frontend|PLL_REST_Request $polylang The Polylang object.
	 */
	public function __construct( &$polylang ) {
		parent::__construct( $polylang );

		$this->curlang = &$polylang->curlang;
		$this->links   = &$polylang->links;

		// Split the language switcher menu item in several language menu items
		add_filter( 'wp_get_nav_menu_items', array( $this, 'wp_get_nav_menu_items' ), 20 ); // after the customizer menus
		add_filter( 'wp_nav_menu_objects', array( $this, 'wp_nav_menu_objects' ) );
		add_filter( 'nav_menu_link_attributes', array( $this, 'nav_menu_link_attributes' ), 10, 2 );

		// Filters menus by language
		add_filter( 'theme_mod_nav_menu_locations', array( $this, 'nav_menu_locations' ), 20 );
		add_filter( 'wp_nav_menu_args', array( $this, 'wp_nav_menu_args' ) );

		// The customizer
		if ( isset( $_POST['wp_customize'], $_POST['customized'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			add_filter( 'wp_nav_menu_args', array( $this, 'filter_args_before_customizer' ) );
			add_filter( 'wp_nav_menu_args', array( $this, 'filter_args_after_customizer' ), 2000 );
		}
	}

	/**
	 * Sorts menu items by menu order.
	 *
	 * @since 1.7.9
	 *
	 * @param stdClass $a The first object to compare.
	 * @param stdClass $b The second object to compare.
	 * @return int -1 or 1 if $a is considered to be respectively less than or greater than $b.
	 */
	protected function usort_menu_items( $a, $b ) {
		return ( $a->menu_order < $b->menu_order ) ? -1 : 1;
	}

	/**
	 * Splits the one language switcher menu item of backend in several menu items on frontend.
	 * Takes care to menu_order as it is used later in wp_nav_menu().
	 *
	 * @since 1.1.1
	 *
	 * @param stdClass[] $items Menu items.
	 * @return stdClass[] Modified menu items.
	 */
	public function wp_get_nav_menu_items( $items ) {
		if ( empty( $this->curlang ) || empty( $this->links ) ) {
			return $items;
		}

		if ( doing_action( 'customize_register' ) ) { // needed since WP 4.3, doing_action available since WP 3.9
			return $items;
		}

		// The customizer menus does not sort the items and we need them to be sorted before splitting the language switcher
		usort( $items, array( $this, 'usort_menu_items' ) );

		$new_items = array();
		$offset    = 0;

		foreach ( $items as $item ) {
			$options = get_post_meta( $item->ID, '_pll_menu_item', true );

			if ( empty( $options ) || ! is_array( $options ) ) {
				$item->menu_order += $offset;
				$new_items[]       = $item;
				continue;
			}

			$settings = new Settings( Fields::remove_legacy_settings( $options ) );
			$elements = ( new Switcher( $settings, $this->links ) )->get_elements();

			if ( empty( $elements ) ) {
				continue;
			}

			if ( 'dropdown' === $settings->layout ) {
				// Parent item for dropdown.
				$element = new Nav( $this->curlang, $settings, $this->links );

				$item->title       = $element->get_label();
				$item->attr_title  = '';
				$item->url         = $element->url;
				$item->classes     = array_merge( $item->classes, $element->item_classes, array( 'pll-parent-menu-item' ) );
				$item->menu_order += $offset;

				if ( $settings->show_flags ) {
					// Since it is added to the parent `<li>`, no need to add it to the child elements too.
					$item->classes[] = 'pll-aspect-ratio-' . str_replace( ':', '', $settings->flag_aspect_ratio );
				}

				$new_items[] = $item;
				++$offset;
			}

			$i = 0; // For incrementation of menu order only in case of dropdown.
			foreach ( $elements as $element ) {
				++$i;
				$lang_item = clone $item;

				$lang_item->ID         = "{$lang_item->ID}-{$element->slug}"; // A unique ID.
				$lang_item->title      = $element->get_label();
				$lang_item->attr_title = '';
				$lang_item->url        = $element->url;
				$lang_item->lang       = $element->locale; // Save this for use in nav_menu_link_attributes.
				$lang_item->classes    = $element->item_classes;

				if ( 'dropdown' === $settings->layout ) {
					$lang_item->menu_order       = $item->menu_order + $i;
					$lang_item->menu_item_parent = $item->db_id;
					$lang_item->db_id            = 0; // To avoid recursion.
				} else {
					$lang_item->classes     = array_diff( $lang_item->classes, array( 'lang-item-first' ) ); // Doesn't mean anything here.
					$lang_item->classes[]   = 'pll-aspect-ratio-' . str_replace( ':', '', $settings->flag_aspect_ratio );
					$lang_item->menu_order += $offset;
				}

				$new_items[] = $lang_item;
				++$offset;
			}
			--$offset;
		}

		return $new_items;
	}

	/**
	 * Returns the ancestors of a menu item.
	 *
	 * @since 1.1.1
	 *
	 * @param stdClass $item Menu item.
	 * @return int[] Ancestors ids.
	 */
	public function get_ancestors( $item ) {
		$ids = array();
		$ancestor_id = (int) $item->db_id;
		while ( ( $ancestor_id = get_post_meta( $ancestor_id, '_menu_item_menu_item_parent', true ) ) && ! in_array( $ancestor_id, $ids ) ) {
			$ids[] = $ancestor_id;
		}
		return $ids;
	}

	/**
	 * Removes current-menu and current-menu-ancestor classes to lang switcher when not on the home page.
	 *
	 * @since 1.1.1
	 *
	 * @param stdClass[] $items An array of menu items.
	 * @return stdClass[]
	 */
	public function wp_nav_menu_objects( $items ) {
		$k_ids = array();
		$r_ids = array();

		foreach ( $items as $item ) {
			if ( ! empty( $item->classes ) && is_array( $item->classes ) ) {
				if ( in_array( 'current-lang', $item->classes ) ) {
					$item->current = false;
					$item->classes = array_diff( $item->classes, array( 'current-menu-item' ) );
					$r_ids = array_merge( $r_ids, $this->get_ancestors( $item ) ); // Remove the classes for these ancestors.
				} elseif ( in_array( 'current-menu-item', $item->classes ) ) {
					$k_ids = array_merge( $k_ids, $this->get_ancestors( $item ) ); // Keep the classes for these ancestors.
				}
			}
		}

		$r_ids = array_diff( $r_ids, $k_ids );

		foreach ( $items as $item ) {
			if ( ! empty( $item->db_id ) && in_array( $item->db_id, $r_ids ) ) {
				$item->classes = array_diff( $item->classes, array( 'current-menu-ancestor', 'current-menu-parent', 'current_page_parent', 'current_page_ancestor' ) );
			}
		}

		return $items;
	}

	/**
	 * Adds hreflang attribute for the language switcher menu items.
	 * available since WP 3.6.
	 *
	 * @since 1.1
	 *
	 * @param string[] $atts HTML attributes applied to the menu item's `<a>` element.
	 * @param stdClass $item Menu item.
	 * @return string[] Modified attributes.
	 */
	public function nav_menu_link_attributes( $atts, $item ) {
		if ( isset( $item->lang ) ) {
			$atts['lang']     = esc_attr( $item->lang );
			$atts['hreflang'] = $atts['lang'];
		}
		return $atts;
	}

	/**
	 * Fills the theme nav menus locations with the right menu in the right language
	 * Needs to wait for the language to be defined
	 *
	 * @since 1.2
	 *
	 * @param array|bool $menus list of nav menus locations, false if menu locations have not been filled yet
	 * @return array|bool modified list of nav menus locations
	 */
	public function nav_menu_locations( $menus ) {
		if ( is_array( $menus ) && ! empty( $this->curlang ) ) {
			// First get multilingual menu locations from DB
			$theme = get_option( 'stylesheet' );

			foreach ( array_keys( $menus ) as $loc ) {
				$menus[ $loc ] = empty( $this->options['nav_menus'][ $theme ][ $loc ][ $this->curlang->slug ] ) ? 0 : $this->options['nav_menus'][ $theme ][ $loc ][ $this->curlang->slug ];
			}

			// Support for theme customizer
			if ( is_customize_preview() ) {
				global $wp_customize;
				foreach ( $wp_customize->unsanitized_post_values() as $key => $value ) {
					if ( false !== strpos( $key, 'nav_menu_locations[' ) ) {
						$loc = substr( trim( $key, ']' ), 19 );
						$infos = $this->explode_location( $loc );
						if ( $infos['lang'] === $this->curlang->slug ) {
							$menus[ $infos['location'] ] = (int) $value;
						} elseif ( $this->curlang->is_default ) {
							$menus[ $loc ] = (int) $value;
						}
					}
				}
			}
		}
		return $menus;
	}

	/**
	 * Attempts to translate the nav menu when it is hardcoded or when no location is defined in wp_nav_menu().
	 *
	 * @since 1.7.10
	 *
	 * @param array $args Array of `wp_nav_menu()` arguments.
	 * @return array
	 */
	public function wp_nav_menu_args( $args ) {
		$theme = get_option( 'stylesheet' );

		if ( empty( $this->curlang ) || empty( $this->options['nav_menus'][ $theme ] ) ) {
			return $args;
		}

		// Get the nav menu based on the requested menu
		$menu = wp_get_nav_menu_object( $args['menu'] );

		// Attempt to find a translation of this menu
		// This obviously does not work if the nav menu has no associated theme location
		if ( $menu ) {
			foreach ( $this->options['nav_menus'][ $theme ] as $menus ) {
				if ( in_array( $menu->term_id, $menus ) && ! empty( $menus[ $this->curlang->slug ] ) ) {
					$args['menu'] = $menus[ $this->curlang->slug ];
					return $args;
				}
			}
		}

		// Get the first menu that has items and and is in the current language if we still can't find a menu
		if ( ! $menu && ! $args['theme_location'] ) {
			$menus = wp_get_nav_menus();
			foreach ( $menus as $menu_maybe ) {
				if ( wp_get_nav_menu_items( $menu_maybe->term_id, array( 'update_post_term_cache' => false ) ) ) {
					foreach ( $this->options['nav_menus'][ $theme ] as $menus ) {
						if ( in_array( $menu_maybe->term_id, $menus ) && ! empty( $menus[ $this->curlang->slug ] ) ) {
							$args['menu'] = $menus[ $this->curlang->slug ];
							return $args;
						}
					}
				}
			}
		}

		return $args;
	}

	/**
	 * Filters the nav menu location before the customizer so that it matches the temporary location in the customizer
	 *
	 * @since 1.8
	 *
	 * @param array $args wp_nav_menu $args
	 * @return array modified $args
	 */
	public function filter_args_before_customizer( $args ) {
		if ( ! empty( $this->curlang ) ) {
			$args['theme_location'] = $this->combine_location( $args['theme_location'], $this->curlang );
		}
		return $args;
	}

	/**
	 * Filters the nav menu location after the customizer to get back the true nav menu location for the theme
	 *
	 * @since 1.8
	 *
	 * @param array $args wp_nav_menu $args
	 * @return array modified $args
	 */
	public function filter_args_after_customizer( $args ) {
		$infos = $this->explode_location( $args['theme_location'] );
		$args['theme_location'] = $infos['location'];
		return $args;
	}
}

```
