Здравствуйте! В этом уроке я расскажу, как реализовать мультиязычную поддержку на вашем сайте с использованием возможностей OctoberCMS. Прошу заметить, что это не готовый плагин, а руководство по его созданию.
Зачем еще один мультиязычный плагин?
Поддержка мультиязычности – наиважнейшая задача, если вы планируете выводить бизнес за пределы РФ и СНГ. К сожалению, на рынке в данный момент есть лишь один плагин Translate от официального разработчика, решающий эту задачу. Однако я не уверен, что он может подойди всем, как это было в моем случае.
Функционал
Плагин поддерживает неограниченное количество языков. В этом плане расширяемость безупречная.
Язык устанавливается клиенту в зависимости от URL, в котором он находится на данный момент. А также поддерживает язык по умолчанию. Например:
https://domain.com/
– язык по умолчанию
https://domain.com/en
– устанавливается английский язык
Плагин умеет автоматически устанавливать клиенту язык в зависимости от языка, что он использует по умолчанию в браузере. Современные браузеры передают это серверу в HTTP заголовке HTTP_ACCEPT_LANGUAGE
. Новых пользователей плагин определяет по куки файлу lang
: если его нет, значит, пользователь заходит на сайт впервые, и необходимо определить для него язык автоматически.
Плагин не конфликтует с поисковыми роботами. Система автоматического определения языка срабатывает только в том случае, когда есть заголовок HTTP_ACCEPT_LANGUAGE
, который боты не передают. Если этот заголовок не передан, устанавливается язык из префикса URI.
Преимущества
- Простота как в использовании, так и в реализации
- Возможность автоматической установки языка для нового клиента
- Работа без использования базы данных
- Хорошая расширяемость
- Это ваш собственный плагин, все-таки!
Недостатки
- Не может работать с масками. Однако это легко решается обновлением кода 😅
Подготовка
Перейдем, наконец, к разработке нашего плагина. Для начала создадим плагин через терминал с помощью встроенной в CMS команды
php artisan create:plugin EasyLang.Lang
Создадим также файлы init.php
и routes.php
в корне плагина, – они нам понадобится в дальнейшем.
Создадим следующие необходимые каталоги в корне плагина:
- lang (директория с локализацией)
- config (конфиг-файл плагина)
- classes (для файлов с классами)
И, наконец, создадим компонент Lang следующей командой:
php artisan create:component EasyLang.Lang Lang
Добавление конфигураций
Создайте файл /config/locales.php
со следующем содержимым:
<?php return [
'ru' => [
'lang' => 'Русский',
'icon' => 'ru.svg',
'dialect' => 'RU',
'iso' => 'ru',
],
'en' => [
'lang' => 'English',
'icon' => 'gb.svg',
'dialect' => 'US',
'iso' => 'en'
]
];
Здесь необходимо перечислить все поддерживаемые вами языки. Вы можете удалить/добавить некоторые параметры в массив – они доступны во фронтенде. Например, я использую ключ lang
для вывода имени языка при выборе, а также icon
для вставки соответствующего флага страны рядом. Вы можете скачать флаги стран из любого источника. Из коммерческих соображений я не публикую их здесь, поскольку мой пак платный 😃
Настройка файла routes.php
Этот файл взят из официального плагина Translate
<?php
$locales = config('easylang.lang::locales');
if (!$locales) {
return;
}
foreach ($locales as $locale => $data) {
Route::group(['prefix' => $locale, 'middleware' => 'web'], function () {
Route::any('{slug?}', 'Cms\Classes\CmsController@run')->where('slug', '(.*)?');
});
Route::any($locale, 'Cms\Classes\CmsController@run')->middleware('web');
/*
* Ensure Url::action() retains the localized URL
* by re-registering the route after the CMS.
*/
Event::listen('cms.route', function () use ($locale) {
Route::group(['prefix' => $locale, 'middleware' => 'web'], function () {
Route::any('{slug?}', 'Cms\Classes\CmsController@run')->where('slug', '(.*)?');
});
});
}
Здесь происходит создание роутов для каждого поддерживаемого вами языка.
Настройка файла init.php
<?php
if (!function_exists('default_locale')) {
function default_locale() {
return config('app.locale', config('app.fallback_locale'));
}
}
/**
* Создаем глобальную функцию lang(),
* которая будет возвращать переведенную строку
*/
if (!function_exists('lang')) {
function lang($code) {
return Lang::get($code);
}
}
/**
* Создаем глобальную функцию locale_url(),
* которая используется для смены языка сайта
*/
if (!function_exists('locale_url')) {
function locale_url($locale = 'ru', $url) {
$defaultLocale = default_locale(); // Язык сайта по умолчанию
$currentPrefix = request()->route()->getPrefix() ?: $defaultLocale;
if ($locale == $defaultLocale && $url == '') {
return str_replace("/{$currentPrefix}", '', url()->current());
}
$path = str_replace("/{$currentPrefix}", '/', request()->path());
$uri = '/' . $locale . ($path == '/' ? '' : "/{$path}");
return $uri;
}
}
Регистрация плагина, twig функций и компонента
<?php namespace EasyLang\Lang;
use System\Classes\PluginBase;
class Plugin extends PluginBase
{
public function pluginDetails()
{
return [
'name' => 'EasyLangLang',
'description' => 'EasyLang lang support.',
'author' => 'EasyLang'
];
}
public function registerMarkupTags()
{
return [
'functions' => [
'lang' => function ($code) {
return lang("easylang.lang::lang.{$code}");
},
'locale_url' => function ($locale = 'ru', $url = '') {
return locale_url($locale, $url);
}
]
];
}
public function registerComponents()
{
return [
'EasyLang\Lang\Components\Lang' => 'Lang'
];
}
}
Добавление файлов локализации
Добавьте файлы /lang/{ru|en}/lang.php
. В этих файлах хранятся локализованные строчки вашего контента. Настройте их содержимое согласно официальной документации OctoberCMS.
Реализация компонента Lang и логики плагина
В ранее созданном компоненте Lang
добавьте следующий код (его описание представлено в комментариях):
<?php namespace EasyLang\Lang\Components;
use Cookie;
use Lang as LangFacade;
use Cms\Classes\ComponentBase;
use EasyLang\Lang\Classes\LangDetect;
class Lang extends ComponentBase
{
protected $lang;
protected $locales;
public function componentDetails()
{
return [
'name' => 'Lang',
'description' => 'Lang your website.'
];
}
public function onRun()
{
$cookieLang = Cookie::get('lang');
$this->locales = $this->page['locales'] = config('easylang.lang::locales');
$defaultLocale = default_locale();
$urlLang = request()->route()->getPrefix() ?: $defaultLocale;
$detectedLang = (new LangDetect)->getBestMatch($urlLang, array_keys($this->locales));
/**
* Проверяем, установлена ли кука у клиента
*/
if ($cookieLang) {
/**
* Если установлена кука, но URL локализации не соответствует её значению,
* значит, пользователь хочет изменить язык.
* Обновляем ему существующую куку.
*/
if ($urlLang != $cookieLang)
$this->setIncomingUserLangCookie($urlLang);
} else {
/**
* Если кука не установлена, устанавливаем её.
* Значение куки определяется динамически благодаря
* библиотеке для определения языка клиента.
*/
$this->setIncomingUserLangCookie($detectedLang);
/**
* Если кука не установлена, и URL локализации не соответствует
* определенному библиотекой языку, делаем принудительную
* переадресацию на правильный URL.
*/
if ($urlLang != $detectedLang)
return $this->redirectLangPrefix($detectedLang);
}
/**
* Устанавливаем язык по умолчанию в систему.
*/
LangFacade::setLocale($urlLang);
$currentLang = LangFacade::getLocale();
$this->page['current_lang'] = $this->locales[$currentLang];
$this->page['current_lang_iso'] = $currentLang;
}
protected function setIncomingUserLangCookie($lang)
{
return Cookie::queue('lang', $lang, 1440);
}
protected function redirectLangPrefix($lang, $uri = '')
{
return redirect(locale_url($lang, $uri));
}
}
Библиотека для определения языка браузера
В интернете я нашел интересную статью о том, как правильно определить язык браузера клиента. Ссылка на статью: habr.
Однако библиотека слегка отличается от оригинала: было изменено возвращаемое значение.
<?php namespace EasyLang\Lang\Classes;
/**
* Lang_detect Class
*
* Language detection library for CodeIgniter.
*
* @author La2ha
* @version 1.0
* @link http://la2ha.ru/dev-seo-diy/web/lang_detect
*/
class LangDetect
{
var $language = null;
public function __construct()
{
if ($list = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? strtolower($_SERVER['HTTP_ACCEPT_LANGUAGE']) : null) {
if (preg_match_all('/([a-z]{1,8}(?:-[a-z]{1,8})?)(?:;q=([0-9.]+))?/', $list, $list)) {
$this->language = array_combine($list[1], $list[2]);
foreach ($this->language as $n => $v)
$this->language[$n] = $v ? $v : 1;
arsort($this->language, SORT_NUMERIC);
}
} else $this->language = array();
}
public function getBestMatch($default, $langs)
{
$languages=array();
foreach ($langs as $lang => $alias) {
if (is_array($alias)) {
foreach ($alias as $alias_lang) {
$languages[strtolower($alias_lang)] = strtolower($lang);
}
}else $languages[strtolower($alias)]=strtolower($lang);
}
foreach ($this->language as $l => $v) {
$s = strtok($l, '-'); // убираем то что идет после тире в языках вида "en-us, ru-ru"
if (isset($languages[$s]))
return $s;
}
return $default;
}
}
Фронтенд
Добавьте в шаблон созданный компонент [Lang]
. Он запустит всю логику и будет действовать в рамках тех страниц что его видят.
Следующий фрагмент позволит выбирать между языками на вашей странице:
{% if locales %}
<div>
<a href="#" style="display: flex; gap: 7px; font-weight: bold">
<img src="{{ ('assets/images/flags/' ~ current_lang.icon) | theme }}" style="width: 8px;">
{{ current_lang.lang }}
</a>
{% for iso, locale in locales if iso != current_lang_iso %}
<a href="{{ locale_url(iso) }}" style="display: flex; gap: 7px;">
<img src="{{ ('assets/images/flags/' ~ locale.icon) | theme }}" style="width: 8px;">
{{ locale.lang }}
</a>
{% endfor %}
</div>
{% endif %}
Доступ к локализованному контенту
Для того чтобы вывести на странице локализованную строку, необходимо использовать следующую конструкцию: {{ lang('my.first.localized.phrase') }}
Оптимизация SEO для поисковиков
Вы также можете легко произвести оптимизацию для поисковых ботов, рассказав им о том, что ваш сайт поддерживает и другие языки.
Добавление HTML конструкций в шаблон
Добавьте/измените следующие HTML конструкции в вашем шаблоне:
<html lang="{{ current_lang_iso }}">
<head>
<link rel="canonical" href="{{ url().current() }}">
{% for iso, locale in locales if iso != current_lang_iso %}
<link rel="alternate" href="{{ locale_url(iso, '') }}" hreflang="{{ iso }}">
{% endfor %}
...
</head>
...
</html>
Файлы Sitemap и их динамическая генерация
Следующим необходимым шагом для оптимизации SEO является передача файлов Sitemap.
Файлы Sitemap представляют из себя карту вашего сайта в виде XML. С помощью этих файлов поисковикам можно рассказать, обо всех страницах, которые есть на вашем сайте.
Я написал скрипт, который генерирует файл sitemap.xml
в корне вашего сайта автоматически раз в сутки по cron’у. Вы же можете делать это как угодно. Лично я рекомендую именно такой вариант работы.
<?php namespace EasyLang\Sitemap\Schedule;
use Cms\Classes\{Theme, Page};
class Sitemap
{
protected $locales = [];
protected $pages = [];
protected $defaultLocale;
protected $outputFilePath;
public function __construct()
{
$this->locales = config('easylang.lang::locales');
$this->pages = Page::listInTheme(Theme::getEditTheme(), true);
$this->defaultLocale = default_locale();
$this->outputFilePath = base_path('sitemap.xml');
}
public function generate()
{
$sitemap = '<?xml version="1.0" encoding="UTF-8"?>';
$sitemap .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">';
foreach ($this->pages as $page) {
foreach ($this->locales as $locale => $localeData) {
$pageUrl = url(($locale == $this->defaultLocale ? '' : "/{$locale}") . $page->url);
$sitemap .= '<url>';
$sitemap .= '<loc>' . $pageUrl . '</loc>';
foreach (array_except($this->locales, [$locale]) as $alternateLocale => $alternateLocaleData) {
$alternateUrl = url(($alternateLocale == $this->defaultLocale ? '' : "/{$alternateLocale}") . $page->url);
$sitemap .= '<xhtml:link rel="alternate" hreflang="'.$alternateLocale.'" href="'.$alternateUrl.'" />';
}
$sitemap .= '</url>';
}
}
$sitemap .= '</urlset>';
return file_put_contents($this->outputFilePath, $sitemap);
}
}
Код выше генерирует динамически файл sitemap.xml
на основе страниц, созданных вами в административной панели. Конечно, он не многофункционален, не способен работать со страницами, использующими slug
параметры и не имеет черный список.
В конечном счете этот файл будет доступен для поисковых ботов по адресу https://domain.com/sitemap.xml
.
Примерно так будет выглядеть ваш sitemap.xml
после обработки скрипта:
<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://domain.com</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://domain.com/en" />
</url>
<url>
<loc>https://domain.com/en</loc>
<xhtml:link rel="alternate" hreflang="ru" href="https://domain.com" />
</url>
</urlset>