Skip to content

Commit c299529

Browse files
authored
REST API: Add locale suggestion endpoint & helpers for plugins and themes (#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
1 parent f789d56 commit c299529

File tree

6 files changed

+642
-0
lines changed

6 files changed

+642
-0
lines changed

mu-plugins/helpers/helpers.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
<?php
22

33
namespace WordPressdotorg\MU_Plugins\Helpers;
4+
45
defined( 'WPINC' ) || die();
56

7+
require_once __DIR__ . '/locale.php';
8+
69
/**
710
* Join a string with a natural language conjunction at the end.
811
*

mu-plugins/helpers/locale.php

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
/**
3+
* Set up some helper functions for fetching locale data.
4+
*/
5+
6+
namespace WordPressdotorg\MU_Plugins\Helpers\Locale;
7+
8+
/**
9+
* Get all locales with subdomain mapping.
10+
*/
11+
function get_all_locales_with_subdomain() {
12+
global $wpdb;
13+
return $wpdb->get_results(
14+
"SELECT locale, subdomain FROM wporg_locales WHERE locale NOT LIKE '%\_%\_%'",
15+
OBJECT_K
16+
);
17+
}
18+
19+
/**
20+
* Get all available locales with valid WordPress locale values.
21+
*
22+
* Not all locales have valid WordPress sites, this filters out those that
23+
* don't exist.
24+
*/
25+
function get_all_valid_locales() {
26+
$all_locales = get_all_locales_with_subdomain();
27+
// Retrieve all the WordPress locales.
28+
$all_locales = wp_list_pluck( $all_locales, 'locale' );
29+
30+
return array_filter(
31+
$all_locales,
32+
function( $locale ) {
33+
return \GP_Locales::by_field( 'wp_locale', $locale );
34+
}
35+
);
36+
}
37+
38+
/**
39+
* Get locales matching the HTTP accept language header.
40+
*
41+
* @return array List of locales.
42+
*/
43+
function get_locale_from_header() {
44+
$res = array();
45+
46+
$available_locales = get_all_valid_locales();
47+
if ( ! $available_locales ) {
48+
return $res;
49+
}
50+
51+
if ( ! isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) {
52+
return $res;
53+
}
54+
55+
$http_locales = get_http_locales( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ); // phpcs:ignore
56+
57+
if ( is_array( $http_locales ) ) {
58+
foreach ( $http_locales as $http_locale ) {
59+
$lang = $http_locale;
60+
$region = $http_locale;
61+
if ( str_contains( $http_locale, '-' ) ) {
62+
list( $lang, $region ) = explode( '-', $http_locale );
63+
}
64+
65+
/*
66+
* Discard English -- it's the default for all browsers,
67+
* ergo not very reliable information
68+
*/
69+
if ( 'en' === $lang ) {
70+
continue;
71+
}
72+
73+
// Region should be uppercase.
74+
$region = strtoupper( $region );
75+
76+
$mapped = map_locale( $lang, $region, $available_locales );
77+
if ( $mapped ) {
78+
$res[] = $mapped;
79+
}
80+
}
81+
82+
$res = array_unique( $res );
83+
}
84+
85+
return $res;
86+
}
87+
88+
/**
89+
* Given a HTTP Accept-Language header $header
90+
* returns all the locales in it.
91+
*
92+
* @param string $header HTTP acccept header.
93+
* @return array Matched locales.
94+
*/
95+
function get_http_locales( $header ) {
96+
$locale_part_re = '[a-z]{2,}';
97+
$locale_re = "($locale_part_re(\-$locale_part_re)?)";
98+
99+
if ( preg_match_all( "/$locale_re/i", $header, $matches ) ) {
100+
return $matches[0];
101+
} else {
102+
return [];
103+
}
104+
}
105+
106+
/**
107+
* Tries to map a lang/region pair to one of our locales.
108+
*
109+
* @param string $lang Lang part of the HTTP accept header.
110+
* @param string $region Region part of the HTTP accept header.
111+
* @param array $available_locales List of available locales.
112+
* @return string|false Our locale matching $lang and $region, false otherwise.
113+
*/
114+
function map_locale( $lang, $region, $available_locales ) {
115+
$uregion = strtoupper( $region );
116+
$ulang = strtoupper( $lang );
117+
$variants = array(
118+
"$lang-$region",
119+
"{$lang}_$region",
120+
"$lang-$uregion",
121+
"{$lang}_$uregion",
122+
"{$lang}_$ulang",
123+
$lang,
124+
);
125+
126+
foreach ( $variants as $variant ) {
127+
if ( in_array( $variant, $available_locales ) ) {
128+
return $variant;
129+
}
130+
}
131+
132+
foreach ( $available_locales as $locale ) {
133+
list( $locale_lang, ) = preg_split( '/[_-]/', $locale );
134+
if ( $lang === $locale_lang ) {
135+
return $locale;
136+
}
137+
}
138+
139+
return false;
140+
}
141+
142+
/**
143+
* Get the active language packs for a package.
144+
*
145+
* @param string $type Package type. One of "theme", "plugin".
146+
* @param string $slug Slug of the requested item (e.g., `jetpack`, `twentynineteen`).
147+
*
148+
* @return array
149+
*/
150+
function get_translated_locales( $type, $slug ) {
151+
global $wpdb;
152+
153+
$language_packs = $wpdb->get_results(
154+
$wpdb->prepare(
155+
'SELECT *
156+
FROM language_packs
157+
WHERE
158+
type = %s AND
159+
domain = %s AND
160+
active = 1
161+
GROUP BY language',
162+
$type,
163+
$slug
164+
)
165+
);
166+
167+
// Retrieve all the WordPress locales in which the theme is translated.
168+
$translated_locales = wp_list_pluck( $language_packs, 'language' );
169+
170+
require_once GLOTPRESS_LOCALES_PATH;
171+
172+
// Validate the list of locales can be found by `wp_locale`.
173+
return array_filter(
174+
$translated_locales,
175+
function( $locale ) {
176+
return \GP_Locales::by_field( 'wp_locale', $locale );
177+
}
178+
);
179+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace WordPressdotorg\MU_Plugins\REST_API;
4+
5+
/**
6+
* Base_Locale_Banner_Controller
7+
*/
8+
abstract class Base_Locale_Banner_Controller extends \WP_REST_Controller {
9+
/**
10+
* Register the endpoint routes used across both themes and plugins.
11+
*
12+
* @see register_rest_route()
13+
*/
14+
public function register_routes() {
15+
register_rest_route(
16+
$this->namespace,
17+
'/' . $this->rest_base,
18+
array(
19+
'methods' => \WP_REST_Server::READABLE,
20+
'callback' => array( $this, 'get_response' ),
21+
'args' => array(
22+
'debug' => array(
23+
'type' => 'boolean',
24+
),
25+
),
26+
'permission_callback' => '__return_true',
27+
)
28+
);
29+
register_rest_route(
30+
$this->namespace,
31+
'/' . $this->rest_base . '/(?P<slug>[^/]+)/',
32+
array(
33+
'methods' => \WP_REST_Server::READABLE,
34+
'callback' => array( $this, 'get_response_for_item' ),
35+
'args' => array(
36+
'debug' => array(
37+
'type' => 'boolean',
38+
),
39+
'slug' => array(
40+
'validate_callback' => array( $this, 'check_slug' ),
41+
),
42+
),
43+
'permission_callback' => '__return_true',
44+
)
45+
);
46+
}
47+
48+
/**
49+
* Check if the given slug is a valid item.
50+
*
51+
* Must be defined in the child class.
52+
*/
53+
abstract public function check_slug( $param );
54+
55+
/**
56+
* Send the response as plain text so it can be used as-is.
57+
*/
58+
public function send_plain_text( $result ) {
59+
header( 'Content-Type: text/text' );
60+
if ( $result ) {
61+
echo '<div>' . $result . '</div>'; // phpcs:ignore
62+
}
63+
64+
return null;
65+
}
66+
}

0 commit comments

Comments
 (0)