Taxonomies::LANGUAGE, 'hide_empty' => true, ) ); return ! is_wp_error( $count ) && (int) $count > 0; } /** * Replaces the language list from catalog locales. * * @param string $default_locale Default language locale. * @param string[] $locales Other languages' locales, in order. * @return true|WP_Error */ public static function set_languages( $default_locale, array $locales ) { $current = Registry::all(); $by_locale = array(); foreach ( $current as $language ) { $by_locale[ $language['locale'] ] = $language; } $locales = array_values( array_unique( array_merge( array( (string) $default_locale ), array_map( 'strval', $locales ) ) ) ); $list = array(); $taken = array(); foreach ( $locales as $locale ) { if ( isset( $by_locale[ $locale ] ) ) { $language = $by_locale[ $locale ]; } else { $known = Catalog::get( $locale ); if ( ! $known ) { return new WP_Error( 'akoml_unknown_locale', __( 'Pick languages from the list.', 'ako-multilang' ), array( 'status' => 400 ) ); } $code = Catalog::code_for( $locale, array_merge( $taken, array_keys( $current ) ) ); if ( isset( $current[ $code ] ) ) { return new WP_Error( 'akoml_duplicate_language', __( 'Each language can only be added once.', 'ako-multilang' ), array( 'status' => 400 ) ); } $language = array_merge( $known, array( 'code' => $code ) ); } $taken[] = $language['code']; $list[] = $language; } $default_code = $list[0]['code']; if ( '' !== Registry::default_code() && Registry::default_code() !== $default_code && self::has_tagged_posts() ) { return new WP_Error( 'akoml_default_locked', __( 'Your pages already have languages, so you can’t change the default here. Pages without a language would move to the new default.', 'ako-multilang' ), array( 'status' => 409 ) ); } foreach ( array_keys( $current ) as $code ) { if ( ! in_array( $code, $taken, true ) ) { Links::detach_language( $code ); } } return Registry::save( $list, $default_code ); } /** * Writing systems that point to one language, checked in order: the * first match wins. Letters only Urdu, Farsi or Arabic use come first; * other Arabic-script titles are settled in suggested_languages(). */ const SCRIPTS = array( 'ur' => '/[\x{0679}\x{0688}\x{0691}\x{06BA}\x{06BE}\x{06D2}]/u', 'fa_IR' => '/[\x{067E}\x{0686}\x{0698}\x{06AF}\x{06A9}\x{06CC}]/u', 'ar' => '/[\x{0629}\x{0643}\x{0649}\x{064A}]/u', 'arabic' => '/[\x{0600}-\x{06FF}]/u', 'he_IL' => '/[\x{0590}-\x{05FF}]/u', 'el' => '/[\x{0370}-\x{03FF}]/u', 'th' => '/[\x{0E00}-\x{0E7F}]/u', 'hi_IN' => '/[\x{0900}-\x{097F}]/u', 'ja' => '/[\x{3040}-\x{30FF}]/u', 'ko_KR' => '/[\x{AC00}-\x{D7AF}]/u', 'zh_CN' => '/[\x{4E00}-\x{9FFF}]/u', ); /** * Site locales written in Arabic script. */ const ARABIC_SCRIPT = array( 'ar', 'fa_IR', 'fa_AF', 'ur', 'ckb', 'ps', 'ug_CN', 'azb', 'skr', 'haz' ); /** * How many recent titles to read when looking for languages. */ const SCAN_LIMIT = 300; /** * Languages the site seems to use besides its own, strongest first. * * - "content": titles written in another script (two or more pages). * - "folder": a top-level page named after a language with pages under it, like /fa/. * - "installed": a WordPress language pack installed on the site. * * Content and folder matches are likely enough to pre-select; installed * packs are only offered. * * @return array */ public static function suggested_languages() { $site = get_locale(); $found = array(); $add = function ( $locale, $reason, $count ) use ( &$found, $site ) { if ( $locale === $site || ! Catalog::get( $locale ) || isset( $found[ $locale ] ) ) { return; } $found[ $locale ] = array( 'locale' => $locale, 'reason' => $reason, 'count' => (int) $count, ); }; $posts = get_posts( array( 'post_type' => Taxonomies::post_types(), 'post_status' => array( 'publish', 'draft', 'future', 'private' ), 'numberposts' => self::SCAN_LIMIT, 'orderby' => 'ID', 'order' => 'DESC', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); $counts = array(); foreach ( $posts as $post ) { foreach ( self::SCRIPTS as $locale => $pattern ) { if ( preg_match( $pattern, $post->post_title ) ) { $counts[ $locale ] = isset( $counts[ $locale ] ) ? $counts[ $locale ] + 1 : 1; break; } } } // Titles in Arabic script without a telling letter ("خانه") go with the // Arabic-script language seen most; Farsi when none is clear. if ( isset( $counts['arabic'] ) ) { if ( ! in_array( $site, self::ARABIC_SCRIPT, true ) ) { $ar = isset( $counts['ar'] ) ? $counts['ar'] : 0; $fa = isset( $counts['fa_IR'] ) ? $counts['fa_IR'] : 0; $ur = isset( $counts['ur'] ) ? $counts['ur'] : 0; $guess = ( $ar > $fa && $ar >= $ur ) ? 'ar' : ( ( $ur > $fa ) ? 'ur' : 'fa_IR' ); $counts[ $guess ] = ( isset( $counts[ $guess ] ) ? $counts[ $guess ] : 0 ) + $counts['arabic']; } unset( $counts['arabic'] ); } arsort( $counts ); foreach ( $counts as $locale => $count ) { if ( $count > 1 ) { $add( $locale, 'content', $count ); } } $parents = array(); foreach ( $posts as $post ) { if ( 'page' === $post->post_type && $post->post_parent ) { $parents[ $post->post_parent ] = isset( $parents[ $post->post_parent ] ) ? $parents[ $post->post_parent ] + 1 : 1; } } foreach ( $parents as $parent_id => $children ) { $parent = get_post( $parent_id ); if ( $parent && 0 === (int) $parent->post_parent && preg_match( '/^[a-z]{2,3}$/', $parent->post_name ) ) { $locale = self::locale_for_code( $parent->post_name ); if ( $locale ) { $add( $locale, 'folder', $children ); } } } foreach ( get_available_languages() as $locale ) { $add( $locale, 'installed', 0 ); } return array_values( $found ); } /** * The catalog locale for a short code: "de" → "de_DE", "fa" → "fa_IR". * * @param string $code Two- or three-letter code. * @return string Locale, or '' when the catalog has none. */ public static function locale_for_code( $code ) { $code = strtolower( (string) $code ); $match = ''; foreach ( array_keys( Catalog::LOCALES ) as $locale ) { $parts = explode( '_', $locale ); if ( strtolower( $parts[0] ) !== $code ) { continue; } if ( $locale === $code || $locale === $code . '_' . strtoupper( $code ) ) { return $locale; } if ( '' === $match && 2 === count( $parts ) ) { $match = $locale; } } return $match; } /** * The menu the switcher can join, or null: a classic menu location, or * the header's Navigation block on block themes. * * @return array{type:string,location:string,name:string,menu:int,menuName:string}|null */ public static function menu_location() { if ( wp_is_block_theme() ) { return self::block_navigation(); } $registered = get_registered_nav_menus(); $assigned = get_nav_menu_locations(); if ( ! $registered || ! $assigned ) { return null; } // Known main-menu slugs first, then other locations, then footer, // mobile, social and top-bar menus last. $order = array_unique( array_merge( array_intersect( self::PREFERRED_LOCATIONS, array_keys( $registered ) ), array_keys( $registered ) ) ); $main = array(); $secondary = array(); foreach ( $order as $location ) { if ( preg_match( self::SECONDARY_LOCATION, $location . ' ' . $registered[ $location ] ) ) { $secondary[] = $location; } else { $main[] = $location; } } foreach ( array_merge( $main, $secondary ) as $location ) { if ( empty( $assigned[ $location ] ) ) { continue; } $menu = wp_get_nav_menu_object( (int) $assigned[ $location ] ); if ( $menu ) { return array( 'type' => 'classic', 'location' => (string) $location, 'name' => (string) $registered[ $location ], 'menu' => (int) $menu->term_id, 'menuName' => (string) $menu->name, ); } } return null; } /** * The header's Navigation block on a block theme. `menu` is the * wp_navigation post it shows, or 0 when it shows WordPress's fallback * (a menu is then created when the switcher is added). * * @return array{type:string,location:string,name:string,menu:int,menuName:string}|null */ private static function block_navigation() { $part = get_block_template( get_stylesheet() . '//header', 'wp_template_part' ); if ( ! $part || ! is_string( $part->content ) ) { return null; } // Themes often keep the header in a pattern (Twenty Twenty-Five does). $blocks = resolve_pattern_blocks( parse_blocks( $part->content ) ); $navigation = self::find_block( $blocks, 'core/navigation' ); if ( ! $navigation ) { return null; } $menu = 0; if ( ! empty( $navigation['attrs']['ref'] ) ) { $menu = (int) $navigation['attrs']['ref']; } else { // Without a ref, the block shows the most recent published menu. $latest = get_posts( array( 'post_type' => 'wp_navigation', 'post_status' => 'publish', 'numberposts' => 1, 'orderby' => 'date', 'order' => 'DESC', 'fields' => 'ids', ) ); $menu = $latest ? (int) $latest[0] : 0; } $post = $menu ? get_post( $menu ) : null; if ( $menu && ( ! $post || 'wp_navigation' !== $post->post_type ) ) { return null; } return array( 'type' => 'block', 'location' => 'header', 'name' => __( 'Header menu', 'ako-multilang' ), 'menu' => $menu, 'menuName' => $post ? (string) $post->post_title : __( 'Header', 'ako-multilang' ), ); } /** * First block of a type in a parsed tree. * * @param array $blocks Parsed blocks. * @param string $name Block name. * @return array|null */ private static function find_block( array $blocks, $name ) { foreach ( $blocks as $block ) { if ( isset( $block['blockName'] ) && $name === $block['blockName'] ) { return $block; } if ( ! empty( $block['innerBlocks'] ) ) { $found = self::find_block( $block['innerBlocks'], $name ); if ( $found ) { return $found; } } } return null; } /** * Adds the switcher block to the end of a block theme's header menu. * * @param array $location From block_navigation(). * @param string $id Switcher ID. * @return true|WP_Error */ private static function add_to_block_navigation( array $location, $id ) { $menu = (int) $location['menu']; if ( ! $menu ) { // Turn WordPress's fallback (the page list) into a saved menu first. if ( ! class_exists( 'WP_Navigation_Fallback' ) ) { return new WP_Error( 'akoml_no_menu', __( 'We couldn’t find a menu in your theme to add the switcher to.', 'ako-multilang' ), array( 'status' => 400 ) ); } $fallback = \WP_Navigation_Fallback::get_fallback(); if ( ! $fallback instanceof \WP_Post ) { return new WP_Error( 'akoml_no_menu', __( 'We couldn’t find a menu in your theme to add the switcher to.', 'ako-multilang' ), array( 'status' => 400 ) ); } $menu = (int) $fallback->ID; } if ( ! current_user_can( 'edit_post', $menu ) ) { return new WP_Error( 'akoml_menu_forbidden', __( 'You can’t edit menus on this site. Pick another placement.', 'ako-multilang' ), array( 'status' => 403 ) ); } $post = get_post( $menu ); if ( false !== strpos( (string) $post->post_content, '