Мультиязычный плагин для WordPress на React.js

Мультиязычный плагин для 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.

Спасибо за внимание и до новых встреч.

Анатолий Куликов

Анатолий Куликов

Автор блога, веб-разработчик
  • at sign
  • vk logo
Комментариев нет

Добавить комментарий

Для отправки комментария вам необходимо авторизоваться.