Мультиязычный плагин для WordPress на React.js
СтатьиДанная статья — текст моего выступления на митапе WP Moscow 16.
В статье я хочу рассмотреть способ создания мультиязычного интерфейса на Javascript для плагина WordPress.
Для примера я написал достаточно простой плагин для генерации пользователей — задача в целом не очень сложная, но достаточная, чтобы рассмотреть такой кейс.
Необходимость генерации большого количества пользователей на WordPress нужна крайне редко. Большой задачи это не составляет — все функции есть, остается только подставить данные и запустить процесс.
Но хочется более изящного решения, чем просто файл с кодом, кочующий из проекта в проект — чтобы можно было удобно наблюдать за процессом и настраивать параметры создания пользователей. И главное — чтобы он не только подставлял данные заполнения профилей самостоятельно, но и поддерживал мультиязычность.
Когда мы используем только php, особой трудности в создании интерфейса мультиязычного плагина нет. Но при разработке приложения на JS возникает вопрос — как это сделать правильно? Зашивать все поддерживаемые языки в бандл — не вариант, поскольку если появится новый перевод, то плагин необходимо будет пересобрать. Плюс это увеличивает вес javascript-файла, что тоже не очень.
Давайте разберемся с этой задачей, и в дополнении рассмотрим вопрос о безопасности — научим плагин идентифицировать пользователей (несколько оригинальной схемой), а также скроем его роуты из общей схемы REST API.
Для начала давайте мельком взглянем на наше frontend-приложение — это достаточно простой интерфейс с минимальным количеством настроек: мы можем указать количество создаваемых пользователей и их роли (кроме администраторов — это запрещено по требованию безопасности).
При нажатии кнопки “Сгенерировать” начинается сам процесс:
Особой оригинальности от плагина пока нет, но главную задачу он выполняет — быстро наполняет сайт пользователями с заполненными профилями.
Наше React-приложение по своей сути является графическим интерфейсом пользователя, который передает команды через REST API. К нему давайте и перейдем.
Для начала перейдем к тому месту, где у нас находятся роуты нашего плагина — к хуку rest_api_init. Плагин и здесь достаточно минималистичен — всего два роута:
// Add routes for plugin
add_action( 'rest_api_init', function() {
// Get UI text
register_rest_route( 'usergen', 'start', $args2);
// Create user
register_rest_route( 'usergen', 'create', $args2);
});
Однако, если мы посмотрим на список всех роутов WordPress (достаточно перейти на oursite/wp-json), то мы увидим, что нашего пути нет. Это первая фишка в докладе — схема нашего плагина “засекречена”. Такой ловкий маневр мы совершили благодаря одному флагу: ‘show_in_index’ => false. По стандарту WordPress показывает все зарегистрированные роуты, но иной раз кое-что скрыть бывает очень полезным.
Двигаемся дальше, перейдем к первому endpont’у, который назван start. Если бы создавали плагин на голом php, то все текстовые элементы на установленном языке отрисовались с помощью стандартных функций типа __( ‘How many users do you need?’, ‘usergenerator’ ). Но нам нужен мультиязычный плагин, и такой, чтобы не нужно было всякий раз увеличивать бандл js. Поэтому для начала мы возвращаем нашему приложению массив текста для текущего языка:
function user_generator_get_ui( WP_REST_Request $request ) {
return [
'status' => 'success',
'data' => [
'controlTitle' => __( 'How many users do you need?', 'usergenerator' ),
'controlButton' => __( 'Generate', 'usergenerator' ),
'userslistTitle' => __( 'List of users', 'usergenerator' ),
'roleTitle' => __( 'User roles', 'usergenerator' ),
'roleEditor' => __( 'Editor', 'usergenerator' ),
'roleAuthor' => __( 'Author', 'usergenerator' ),
'roleContributor' => __( 'Contributor', 'usergenerator' ),
'roleSubscriber' => __( 'Subscriber', 'usergenerator' )
]
];
}
А уже клиент просто разбирает пришедший массив по местам:
Теперь, когда пользователь получил интерфейс на своем родном языке, он может сконфигурировать плагин — всего 2 параметра, но все его — и запустить процесс генерации.
Здесь в игру вступает второй endpoint — create. У него уже есть целый 1 параметр — роль пользователя — который мы кроме того, что принимаем, ещё и проверяем на валидность (чтобы не подсунули каких-нибудь ещё ролей). Для проверки используем замечательный параметр enum, а чтобы параметр был указан обязательно — устанавливаем required = true. Дополнительно проверяем, что приходит именно строка (но это уже не столь важно, хотя никогда не помешает указывать типы).
'role' => [
'default' => null,
'required' => true,
'type' => 'string',
'enum' => [
'subscriber',
'contributor',
'author',
'editor'
]
]
После всех проверок мы попадаем в функцию по созданию пользователя:
function user_generator_create_user( WP_REST_Request $request ) {
// Generate new user
$user = new USER_GENERATOR_User();
// Insert user
$user_id = wp_insert_user( [
'user_pass' => $user->user_pass,
'user_login' => $user->user_login,
'user_email' => $user->user_email,
'user_url' => $user->user_url,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'description' => $user->description,
'role' => $request['role']
] );
Здесь особой уличной магии нет, все самые интересные процессы происходят внутри класса USER_GENERATOR_User. Такое прикольное название обусловлено тем, что классы, созданные для нашего плагина, не должны конфликтовать с чем-либо ещё, а учитывая количество плагинов для WordPress такое вполне вероятно.
Класс по своей сути просто генерирует готовый объект с нужными для сохранения ключами:
- некоторые строки генерируются исходя из окружения (например, email и логин)
- личные имена и строчки в биографии получаются рандомно.
Например, вот такая функция возвращает нам случайную фамилию:
function user_generator_get_surname() {
$surnames = [
__( 'surname-1', 'usergenerator' ),
__( 'surname-2', 'usergenerator' ),
__( 'surname-3', 'usergenerator' ),
__( 'surname-4', 'usergenerator' ),
__( 'surname-5', 'usergenerator' ),
__( 'surname-6', 'usergenerator' ),
__( 'surname-7', 'usergenerator' ),
__( 'surname-8', 'usergenerator' ),
__( 'surname-9', 'usergenerator' ),
__( 'surname-10', 'usergenerator' ),
__( 'surname-11', 'usergenerator' ),
__( 'surname-12', 'usergenerator' ),
__( 'surname-13', 'usergenerator' ),
__( 'surname-14', 'usergenerator' ),
__( 'surname-15', 'usergenerator')
];
return user_generator_get_random( $surnames );
}
Да, совершенно верно, все имена, фамилии и факты из биографии генерируемых пользователей — все переводится на текущий язык. И вы уже заметили, что вместо ожидаемых строк с именами мы используем этакие плейсхолдеры.
Такая конструкция выбрана не случайно — вместо того, чтобы раз и навсегда “зашится” на универсальном наборе, мы можем предоставить возможность использования наиболее популярных имен и фамилий для каждого языка.
Но такой подход имеет несколько неочевидных подводных камней. Например, при использовании фамилий в некоторых языках в зависимости от пола пользователя они имеют склонения (и то не всегда). Плагин это предусматривает и может добавлять окончания:
$gender = $this->set_gender();
switch($gender) {
case 'man': {
$this->first_name = user_generator_get_male_name();
$this->last_name = trim( user_generator_get_surname() . __( 'male-declension-surname', 'usergenerator' ) );
break;
}
case 'female': {
$this->first_name = user_generator_get_female_name();
$this->last_name = trim( user_generator_get_surname() . __( 'female-declension-surname', 'usergenerator' ) );
break;
}
}
Здесь уже будет задачей для переводчика найти подходящие под плагин фамилии — чтобы они удовлетворяли одному правилу склонения. Отсюда же, кстати, есть и вторая проблема — такой подход обрекает плагин на вечные ~98% перевода, поскольку некоторые языки вообще не имеют склонений по фамилиям, а какие-то склоняют только в одном случае.
Мы оказались в самом конце нашего рассказа, осталось самое главное — безопасность. А поскольку безопасность прежде всего, нам нужно исключить возможность несанкционированного использования нашего API. Вариантов исполнения может быть достаточно много, я предлагаю рассмотреть не самый очевидный.
Если мы сейчас попробуем перейти к любому endpoint’у, то WordPress нам учтиво сообщит, что нам выполнять данное действие не разрешено. Эта логика у нас появляется благодаря параметру permission_callback, который мы указываем при создании роута.
В нашем случае за проверку доступа отвечает функция user_generator_permission_callback, которая проводит достаточно простую проверку: мы проверяем наличие ‘HTTP_X_API_KEY’, внутри которого лежит закодированный токен, который сравнивается с тем, что имеется у пользователя, который его запрашивает и ещё не успел “протухнуть”. Если проверки пройдены, то мы пропускаем запрос дальше.
function user_generator_permission_callback( WP_REST_Request $request ) {
// Check the availability of the token in HTTP_X_API_KEY
if( isset( $_SERVER['HTTP_X_API_KEY'] ) ) {
// Let's read the received token and find the saved token for this user
$received_token = json_decode( hex2bin( $_SERVER['HTTP_X_API_KEY'] ) );
$saved_token = json_decode( hex2bin( get_user_meta( $received_token->id, 'usergenerator_token', true ) ) );
// Keys and user ID must match, and this token must not expire
if( $saved_token->expires > time() && $received_token->id === $saved_token->id && $received_token->key === $saved_token->key ) {
return true;
}
}
// In other cases deny access
return false;
}
А откуда у нас взялся этот ключ? Мы его генерируем каждый раз при загрузке страницы с нашим плагином:
// Create token for current user
$current_user_token = bin2hex(
json_encode (
[
'id' => $current_user_id,
'key' => wp_generate_password( 32, true ),
'expires' => time() + 3600
]
)
);
// Save token
update_user_meta( $current_user_id, 'usergenerator_token', $current_user_token );
А затем просто подставляем в генерируемый html, откуда его без проблем заберет javascript:
// Render app
echo('<div class="wrap">
<h1>'. __( 'User Generator', 'usergenerator' ) .'</h1>
<div id="usergenerator" data-api="'. home_url( 'wp-json/usergen' ) .'" data-token="'. $current_user_token .'"></div>
</div>');
Здесь же мы также указываем ссылку до WordPress REST API, чтобы наше приложение точно знало, по какому адресу отправлять запрос.
Вот и все готово. Плагин можно скачать из официального репозитория, а его исходный код доступен на github.
Спасибо за внимание и до новых встреч.