diff --git a/mu-plugins/helpers/helpers.php b/mu-plugins/helpers/helpers.php index 78e8d4fc7..e3d9e2563 100644 --- a/mu-plugins/helpers/helpers.php +++ b/mu-plugins/helpers/helpers.php @@ -1,8 +1,11 @@ get_results( + "SELECT locale, subdomain FROM wporg_locales WHERE locale NOT LIKE '%\_%\_%'", + OBJECT_K + ); +} + +/** + * Get all available locales with valid WordPress locale values. + * + * Not all locales have valid WordPress sites, this filters out those that + * don't exist. + */ +function get_all_valid_locales() { + $all_locales = get_all_locales_with_subdomain(); + // Retrieve all the WordPress locales. + $all_locales = wp_list_pluck( $all_locales, 'locale' ); + + return array_filter( + $all_locales, + function( $locale ) { + return \GP_Locales::by_field( 'wp_locale', $locale ); + } + ); +} + +/** + * Get locales matching the HTTP accept language header. + * + * @return array List of locales. + */ +function get_locale_from_header() { + $res = array(); + + $available_locales = get_all_valid_locales(); + if ( ! $available_locales ) { + return $res; + } + + if ( ! isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) { + return $res; + } + + $http_locales = get_http_locales( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ); // phpcs:ignore + + if ( is_array( $http_locales ) ) { + foreach ( $http_locales as $http_locale ) { + $lang = $http_locale; + $region = $http_locale; + if ( str_contains( $http_locale, '-' ) ) { + list( $lang, $region ) = explode( '-', $http_locale ); + } + + /* + * Discard English -- it's the default for all browsers, + * ergo not very reliable information + */ + if ( 'en' === $lang ) { + continue; + } + + // Region should be uppercase. + $region = strtoupper( $region ); + + $mapped = map_locale( $lang, $region, $available_locales ); + if ( $mapped ) { + $res[] = $mapped; + } + } + + $res = array_unique( $res ); + } + + return $res; +} + +/** + * Given a HTTP Accept-Language header $header + * returns all the locales in it. + * + * @param string $header HTTP acccept header. + * @return array Matched locales. + */ +function get_http_locales( $header ) { + $locale_part_re = '[a-z]{2,}'; + $locale_re = "($locale_part_re(\-$locale_part_re)?)"; + + if ( preg_match_all( "/$locale_re/i", $header, $matches ) ) { + return $matches[0]; + } else { + return []; + } +} + +/** + * Tries to map a lang/region pair to one of our locales. + * + * @param string $lang Lang part of the HTTP accept header. + * @param string $region Region part of the HTTP accept header. + * @param array $available_locales List of available locales. + * @return string|false Our locale matching $lang and $region, false otherwise. + */ +function map_locale( $lang, $region, $available_locales ) { + $uregion = strtoupper( $region ); + $ulang = strtoupper( $lang ); + $variants = array( + "$lang-$region", + "{$lang}_$region", + "$lang-$uregion", + "{$lang}_$uregion", + "{$lang}_$ulang", + $lang, + ); + + foreach ( $variants as $variant ) { + if ( in_array( $variant, $available_locales ) ) { + return $variant; + } + } + + foreach ( $available_locales as $locale ) { + list( $locale_lang, ) = preg_split( '/[_-]/', $locale ); + if ( $lang === $locale_lang ) { + return $locale; + } + } + + return false; +} + +/** + * Get the active language packs for a package. + * + * @param string $type Package type. One of "theme", "plugin". + * @param string $slug Slug of the requested item (e.g., `jetpack`, `twentynineteen`). + * + * @return array + */ +function get_translated_locales( $type, $slug ) { + global $wpdb; + + $language_packs = $wpdb->get_results( + $wpdb->prepare( + 'SELECT * + FROM language_packs + WHERE + type = %s AND + domain = %s AND + active = 1 + GROUP BY language', + $type, + $slug + ) + ); + + // Retrieve all the WordPress locales in which the theme is translated. + $translated_locales = wp_list_pluck( $language_packs, 'language' ); + + require_once GLOTPRESS_LOCALES_PATH; + + // Validate the list of locales can be found by `wp_locale`. + return array_filter( + $translated_locales, + function( $locale ) { + return \GP_Locales::by_field( 'wp_locale', $locale ); + } + ); +} diff --git a/mu-plugins/rest-api/endpoints/class-wporg-base-locale-banner-controller.php b/mu-plugins/rest-api/endpoints/class-wporg-base-locale-banner-controller.php new file mode 100644 index 000000000..f82c540b9 --- /dev/null +++ b/mu-plugins/rest-api/endpoints/class-wporg-base-locale-banner-controller.php @@ -0,0 +1,66 @@ +namespace, + '/' . $this->rest_base, + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_response' ), + 'args' => array( + 'debug' => array( + 'type' => 'boolean', + ), + ), + 'permission_callback' => '__return_true', + ) + ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[^/]+)/', + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_response_for_item' ), + 'args' => array( + 'debug' => array( + 'type' => 'boolean', + ), + 'slug' => array( + 'validate_callback' => array( $this, 'check_slug' ), + ), + ), + 'permission_callback' => '__return_true', + ) + ); + } + + /** + * Check if the given slug is a valid item. + * + * Must be defined in the child class. + */ + abstract public function check_slug( $param ); + + /** + * Send the response as plain text so it can be used as-is. + */ + public function send_plain_text( $result ) { + header( 'Content-Type: text/text' ); + if ( $result ) { + echo '
' . $result . '
'; // phpcs:ignore + } + + return null; + } +} diff --git a/mu-plugins/rest-api/endpoints/class-wporg-plugins-locale-banner-controller.php b/mu-plugins/rest-api/endpoints/class-wporg-plugins-locale-banner-controller.php new file mode 100644 index 000000000..a3503c453 --- /dev/null +++ b/mu-plugins/rest-api/endpoints/class-wporg-plugins-locale-banner-controller.php @@ -0,0 +1,191 @@ +namespace = 'wporg-plugins/v1'; + $this->rest_base = 'locale-banner'; + } + + /** + * Validate the plugin slug. + */ + public function check_slug( $param ) { + return Plugin_Directory::get_plugin_post( $param ); + } + + /** + * Get banner for general plugin directory + */ + public function get_response( $request ) { + if ( ! defined( 'GLOTPRESS_LOCALES_PATH' ) ) { + return; + } + + require_once GLOTPRESS_LOCALES_PATH; + + $locale_subdomain_assoc = get_all_locales_with_subdomain(); + $current_locale = get_locale(); + $current_gp_locale = \GP_Locales::by_field( 'wp_locale', $current_locale ); + + // Build a list of WordPress locales which we'll suggest to the user. + $suggest_locales = array_values( array_intersect( get_locale_from_header(), get_all_valid_locales() ) ); + + $suggestion_links = []; + foreach ( $suggest_locales as $locale ) { + $language = \GP_Locales::by_field( 'wp_locale', $locale )->native_name; + $suggestion_links[ $locale ] = sprintf( + '%s', + $locale_subdomain_assoc[ $locale ]->subdomain, + esc_url( get_site()->path ), + $language + ); + } + + $suggest_string = ''; + + unset( $suggestion_links[ $current_locale ] ); + + if ( ! empty( $suggestion_links ) ) { + $output_locale = key( $suggestion_links ); + switch_to_locale( $output_locale ); + $suggest_string = sprintf( + // translators: %s: List of links to plugin directory in other locales. + __( 'The plugin directory is also available in %s.', 'wporg' ), + wp_sprintf_l( '%l', $suggestion_links ) + ); + } + + // Return more information if this is a debug request. + if ( ! empty( $request['debug'] ) ) { + return new \WP_REST_Response( + array( + 'currentLocale' => $current_locale, + 'suggestions' => $suggest_locales, + 'message' => $suggest_string, + ) + ); + } + + // The result should be a raw text response. + add_filter( 'rest_pre_echo_response', array( $this, 'send_plain_text' ) ); + return new \WP_REST_Response( $suggest_string ); + } + + /** + * Get banner for single plugins. + */ + public function get_response_for_item( $request ) { + // This has already been validated by `validate_callback`. + $plugin_slug = $request['slug']; + + if ( ! defined( 'GLOTPRESS_LOCALES_PATH' ) ) { + return; + } + + require_once GLOTPRESS_LOCALES_PATH; + + $locale_subdomain_assoc = get_all_locales_with_subdomain(); + $current_locale = get_locale(); + $current_gp_locale = \GP_Locales::by_field( 'wp_locale', $current_locale ); + $translated_locales = get_translated_locales( 'plugin', $plugin_slug ); + + // Build a list of WordPress locales which we'll suggest to the user. + $suggest_locales = array_values( array_intersect( get_locale_from_header(), $translated_locales ) ); + + $suggestion_links = []; + foreach ( $suggest_locales as $locale ) { + $language = \GP_Locales::by_field( 'wp_locale', $locale )->native_name; + $suggestion_links[ $locale ] = sprintf( + '%s', + $locale_subdomain_assoc[ $locale ]->subdomain, + esc_url( get_site()->path . $plugin_slug . '/' ), + $language + ); + } + + $suggest_string = ''; + + unset( $suggestion_links[ $current_locale ] ); + + // If we're on a rosetta site, and the plugin is not translated, the message should ask for help. + if ( 'en_US' !== $current_locale && $current_gp_locale && ! in_array( $current_locale, $translated_locales ) ) { + $output_locale = $current_locale; + switch_to_locale( $output_locale ); + + if ( ! empty( $suggestion_links ) ) { + $suggest_string = sprintf( + // translators: %1$s: Locale name, %2$s: List of links to plugin in other locales. + __( 'This plugin is not translated into %1$s yet, but it is available in %2$s.', 'wporg' ), + $current_gp_locale->native_name, + wp_sprintf_l( '%l', $suggestion_links ) + ); + } else { + $suggest_string = sprintf( + // translators: %s: Locale name. + __( 'This plugin is not translated into %s yet.', 'wporg' ), + $current_gp_locale->native_name + ); + } + $suggest_string .= ' ' . sprintf( + '%2$s', + esc_url( 'https://translate.wordpress.org/projects/wp-plugins/' . $plugin_slug ), + __( 'Help translate it!', 'wporg' ) + ); + + } else if ( ! empty( $suggestion_links ) ) { + $output_locale = key( $suggestion_links ); + switch_to_locale( $output_locale ); + $suggest_string = sprintf( + // translators: %s: List of links to plugin in other locales. + __( 'This plugin is also available in %s.', 'wporg' ), + wp_sprintf_l( '%l', $suggestion_links ) + ); + $suggest_string .= ' ' . sprintf( + '%2$s', + esc_url( 'https://translate.wordpress.org/projects/wp-plugins/' . $plugin_slug ), + __( 'Help improve the translation!', 'wporg' ) + ); + + } else if ( ! empty( $locales_from_header ) ) { + $output_locale = reset( $locales_from_header ); + switch_to_locale( $output_locale ); + + $suggest_string = sprintf( + // translators: %s: Locale name. + __( 'This plugin is not translated into %s yet.', 'wporg' ), + \GP_Locales::by_field( 'wp_locale', $output_locale )->native_name + ); + $suggest_string .= ' ' . sprintf( + '%2$s', + esc_url( 'https://translate.wordpress.org/projects/wp-plugins/' . $plugin_slug ), + __( 'Help translate it!', 'wporg' ) + ); + } + + // Return more information if this is a debug request. + if ( ! empty( $request['debug'] ) ) { + return new \WP_REST_Response( + array( + 'currentLocale' => $current_locale, + 'suggestions' => $suggest_locales, + 'message' => $suggest_string, + ) + ); + } + + // The result should be a raw text response. + add_filter( 'rest_pre_echo_response', array( $this, 'send_plain_text' ) ); + return new \WP_REST_Response( $suggest_string ); + } +} diff --git a/mu-plugins/rest-api/endpoints/class-wporg-themes-locale-banner-controller.php b/mu-plugins/rest-api/endpoints/class-wporg-themes-locale-banner-controller.php new file mode 100644 index 000000000..3c7acefaa --- /dev/null +++ b/mu-plugins/rest-api/endpoints/class-wporg-themes-locale-banner-controller.php @@ -0,0 +1,190 @@ +namespace = 'wporg-themes/v1'; + $this->rest_base = 'locale-banner'; + } + + /** + * Validate the theme slug. + */ + public function check_slug( $param ) { + $theme = wporg_themes_theme_information( $param ); + return ! isset( $theme->error ); + } + + /** + * Get banner for general theme directory + */ + public function get_response( $request ) { + if ( ! defined( 'GLOTPRESS_LOCALES_PATH' ) ) { + return; + } + + require_once GLOTPRESS_LOCALES_PATH; + + $locale_subdomain_assoc = get_all_locales_with_subdomain(); + $current_locale = get_locale(); + $current_gp_locale = \GP_Locales::by_field( 'wp_locale', $current_locale ); + + // Build a list of WordPress locales which we'll suggest to the user. + $suggest_locales = array_values( array_intersect( get_locale_from_header(), get_all_valid_locales() ) ); + + $suggestion_links = []; + foreach ( $suggest_locales as $locale ) { + $language = \GP_Locales::by_field( 'wp_locale', $locale )->native_name; + $suggestion_links[ $locale ] = sprintf( + '%s', + $locale_subdomain_assoc[ $locale ]->subdomain, + esc_url( get_site()->path ), + $language + ); + } + + $suggest_string = ''; + + unset( $suggestion_links[ $current_locale ] ); + + if ( ! empty( $suggestion_links ) ) { + $output_locale = key( $suggestion_links ); + switch_to_locale( $output_locale ); + $suggest_string = sprintf( + // translators: %s: List of links to theme directory in other locales. + __( 'The theme directory is also available in %s.', 'wporg' ), + wp_sprintf_l( '%l', $suggestion_links ) + ); + } + + // Return more information if this is a debug request. + if ( ! empty( $request['debug'] ) ) { + return new \WP_REST_Response( + array( + 'currentLocale' => $current_locale, + 'suggestions' => $suggest_locales, + 'message' => $suggest_string, + ) + ); + } + + // The result should be a raw text response. + add_filter( 'rest_pre_echo_response', array( $this, 'send_plain_text' ) ); + return new \WP_REST_Response( $suggest_string ); + } + + /** + * Get banner for single themes. + */ + public function get_response_for_item( $request ) { + // This has already been validated by `validate_callback`. + $theme_slug = $request['slug']; + + if ( ! defined( 'GLOTPRESS_LOCALES_PATH' ) ) { + return; + } + + require_once GLOTPRESS_LOCALES_PATH; + + $locale_subdomain_assoc = get_all_locales_with_subdomain(); + $current_locale = get_locale(); + $current_gp_locale = \GP_Locales::by_field( 'wp_locale', $current_locale ); + $translated_locales = get_translated_locales( 'theme', $theme_slug ); + + // Build a list of WordPress locales which we'll suggest to the user. + $suggest_locales = array_values( array_intersect( get_locale_from_header(), $translated_locales ) ); + + $suggestion_links = []; + foreach ( $suggest_locales as $locale ) { + $language = \GP_Locales::by_field( 'wp_locale', $locale )->native_name; + $suggestion_links[ $locale ] = sprintf( + '%s', + $locale_subdomain_assoc[ $locale ]->subdomain, + esc_url( get_site()->path . $theme_slug . '/' ), + $language + ); + } + + $suggest_string = ''; + + unset( $suggestion_links[ $current_locale ] ); + + // If we're on a rosetta site, and the theme is not translated, the message should ask for help. + if ( 'en_US' !== $current_locale && $current_gp_locale && ! in_array( $current_locale, $translated_locales ) ) { + $output_locale = $current_locale; + switch_to_locale( $output_locale ); + if ( ! empty( $suggestion_links ) ) { + $suggest_string = sprintf( + // translators: %1$s: Locale name, %2$s: List of links to theme in other locales. + __( 'This theme is not translated into %1$s yet, but it is available in %2$s.', 'wporg' ), + $current_gp_locale->native_name, + wp_sprintf_l( '%l', $suggestion_links ) + ); + } else { + $suggest_string = sprintf( + // translators: %s: Locale name. + __( 'This theme is not translated into %s yet.', 'wporg' ), + $current_gp_locale->native_name + ); + } + $suggest_string .= ' ' . sprintf( + '%2$s', + esc_url( 'https://translate.wordpress.org/projects/wp-themes/' . $theme_slug ), + __( 'Help translate it!', 'wporg' ) + ); + + } else if ( ! empty( $suggestion_links ) ) { + $output_locale = key( $suggestion_links ); + switch_to_locale( $output_locale ); + $suggest_string = sprintf( + // translators: %s: List of links to theme in other locales. + __( 'This theme is also available in %s.', 'wporg' ), + wp_sprintf_l( '%l', $suggestion_links ) + ); + $suggest_string .= ' ' . sprintf( + '%2$s', + esc_url( 'https://translate.wordpress.org/projects/wp-themes/' . $theme_slug ), + __( 'Help improve the translation!', 'wporg' ) + ); + + } else if ( ! empty( $locales_from_header ) ) { + $output_locale = reset( $locales_from_header ); + switch_to_locale( $output_locale ); + + $suggest_string = sprintf( + // translators: %s: Locale name. + __( 'This theme is not translated into %s yet.', 'wporg' ), + \GP_Locales::by_field( 'wp_locale', $output_locale )->native_name + ); + $suggest_string .= ' ' . sprintf( + '%2$s', + esc_url( 'https://translate.wordpress.org/projects/wp-themes/' . $theme_slug ), + __( 'Help translate it!', 'wporg' ) + ); + } + + // Return more information if this is a debug request. + if ( ! empty( $request['debug'] ) ) { + return new \WP_REST_Response( + array( + 'currentLocale' => $current_locale, + 'suggestions' => $suggest_locales, + 'message' => $suggest_string, + ) + ); + } + + // The result should be a raw text response. + add_filter( 'rest_pre_echo_response', array( $this, 'send_plain_text' ) ); + return new \WP_REST_Response( $suggest_string ); + } +} diff --git a/mu-plugins/rest-api/index.php b/mu-plugins/rest-api/index.php index 58671fff1..d8be68955 100644 --- a/mu-plugins/rest-api/index.php +++ b/mu-plugins/rest-api/index.php @@ -20,6 +20,19 @@ function initialize_rest_endpoints() { $users_controller = new Users_Controller(); $users_controller->register_routes(); + + if ( defined( 'WPORG_THEME_DIRECTORY_BLOGID' ) && WPORG_THEME_DIRECTORY_BLOGID === get_current_blog_id() ) { + require_once __DIR__ . '/endpoints/class-wporg-base-locale-banner-controller.php'; + require_once __DIR__ . '/endpoints/class-wporg-themes-locale-banner-controller.php'; + $locale_banner_controller = new Themes_Locale_Banner_Controller(); + $locale_banner_controller->register_routes(); + } + if ( class_exists( 'WordPressdotorg\Plugin_Directory\Plugin_Directory' ) ) { + require_once __DIR__ . '/endpoints/class-wporg-base-locale-banner-controller.php'; + require_once __DIR__ . '/endpoints/class-wporg-plugins-locale-banner-controller.php'; + $locale_banner_controller = new Plugins_Locale_Banner_Controller(); + $locale_banner_controller->register_routes(); + } } /**