Skip to content

Commit

Permalink
REST API: Add locale suggestion endpoint & helpers for plugins and th…
Browse files Browse the repository at this point in the history
…emes (#609)

* REST API: Add locale suggestion endpoint & helpers for plugins and themes

* Fix spelling error

* Make the alternate suggestion string more human-friendly

* Switch to an abstract class & validation function

* Add type to `debug` param, use built-in validation & sanitization

* Remove the unnecessary param check
  • Loading branch information
ryelle authored May 15, 2024
1 parent f789d56 commit c299529
Show file tree
Hide file tree
Showing 6 changed files with 642 additions and 0 deletions.
3 changes: 3 additions & 0 deletions mu-plugins/helpers/helpers.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<?php

namespace WordPressdotorg\MU_Plugins\Helpers;

defined( 'WPINC' ) || die();

require_once __DIR__ . '/locale.php';

/**
* Join a string with a natural language conjunction at the end.
*
Expand Down
179 changes: 179 additions & 0 deletions mu-plugins/helpers/locale.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<?php
/**
* Set up some helper functions for fetching locale data.
*/

namespace WordPressdotorg\MU_Plugins\Helpers\Locale;

/**
* Get all locales with subdomain mapping.
*/
function get_all_locales_with_subdomain() {
global $wpdb;
return $wpdb->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 );
}
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace WordPressdotorg\MU_Plugins\REST_API;

/**
* Base_Locale_Banner_Controller
*/
abstract class Base_Locale_Banner_Controller extends \WP_REST_Controller {
/**
* Register the endpoint routes used across both themes and plugins.
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->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<slug>[^/]+)/',
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 '<div>' . $result . '</div>'; // phpcs:ignore
}

return null;
}
}
Loading

0 comments on commit c299529

Please sign in to comment.