Руководство: как правильно настроить hreflang-теги для мультиязычного сайта на Cotonti
Типовые решения и инструкция разработчикам: динамическая генерация rel="alternate" hreflang и x-default с поддержкой IETF‑стандарта для многоязычных страниц в Cotonti (плагин i18n) на примере сайта для ОАЭ
Оглавление
- 1. Проблема: недостаточность стандартного функционала i18n и важность правильной языковой разметки
- 2. Ключевые понятия: внутренние коды плагина и международный стандарт IETF (RFC 5646)
- 3. Конфигурация плагина i18n и особенности многоязычного сайта в ОАЭ
- 4. Механика формирования URL с языковым префиксом (связка .htaccess и i18n)
- 5. Разработанное решение: интеграция улучшенной логики в i18n.header.tags.php
- 6. Детальный разбор кода: построчное объяснение каждого блока
- 7. Настройка шаблона header.tpl
- 8. Проверка и отладка: ожидаемый результат в исходном коде страницы
- 9. Типичные ошибки и способы их устранения
- 10. Расширение решения на другие модули Cotonti
- 11. Заключение и лучшие практики
- 12. Приложения: SQL‑запросы, карта IETF‑тегов для ОАЭ, правила .htaccess
1. Проблема: недостаточность стандартного функционала i18n и важность правильной языковой разметки
1.1 Актуальность задачи для многоязычного сайта в одной стране
Современные поисковые системы (Google, Яндекс, Bing) активно используют атрибут hreflang и значение lang тега <html> для определения языковой и региональной принадлежности страницы. Для сайта, обслуживающего аудиторию в пределах одной страны, но на нескольких языках (например, Объединённые Арабские Эмираты с арабским, английским, русским, украинским, французским и другими), корректная разметка критически важна. Она позволяет:
- Исключить дублирование контента между языковыми версиями.
- Показать пользователю в поисковой выдаче версию на его родном языке, даже если он физически находится в ОАЭ.
- Чётко обозначить поисковикам, что все языковые версии относятся к одному региону (ОАЭ) и не конкурируют с аналогичными версиями для других стран (например, русскоязычный сайт для России).
Стандартный плагин i18n для Cotonti отлично справляется с хранением переводов и переключением языков, но не генерирует теги <link rel="alternate" hreflang="..."> и не устанавливает полный IETF‑атрибут lang. Вместо этого он оперирует короткими внутренними кодами (например, ua, en, ru), что не соответствует требованиям HTML5 и SEO.
1.2 Типичные проблемы при ручном добавлении разметки
Попытки вручную прописать статические теги в шаблоне header.tpl приводят к ряду проблем:
- Негибкость – при добавлении нового языка или изменении алиаса страницы требуется правка шаблона.
- Ошибки в URL – легко забыть обновить ссылку, что ведёт к битым альтернативным версиям.
- Некорректный
x-default– не учитывается настройкаomitmain, в результате страница по умолчанию может указывать не на ту версию. - Отсутствие проверки существования перевода – теги генерируются для всех языков, даже если страница не переведена, что вводит поисковики в заблуждение.
1.3 Требования к автоматическому решению
Необходимо динамическое решение, которое:
- Генерирует теги
<link rel="alternate" hreflang="xx-XX" href="URL">только для языков, на которые страница действительно переведена. - Формирует корректный тег
x-default, указывающий на основную языковую версию с учётом настройкиomitmain. - Устанавливает атрибут
langтега<html>в полном IETF‑формате (язык-РЕГИОН), напримерuk-AE,en-AE,ru-AE. - Учитывает настройки ЧПУ (
rewrite) и генерирует абсолютные URL с доменом. - Не требует ручного вмешательства при добавлении/удалении языков или переводов.
- Работает на страницах модуля
page(с возможностью расширения).
2. Ключевые понятия: внутренние коды плагина и международный стандарт IETF (RFC 5646)
2.1 Внутренние коды i18n
Плагин i18n использует в настройке locales собственные короткие идентификаторы, например:
ua|Українська
en|English
ru|Русский
pl|PolskiЭти коды (ua, en, ru, pl) применяются:
- в параметре URL
?l=uaили префиксе/ua/; - в cookie
i18n_locale; - в поле
ipage_localeтаблицы переводовcot_i18n_pages.
Они удобны для внутренней логики, но не являются стандартными языковыми тегами для HTML и поисковиков.
2.2 Стандарт IETF BCP 47 (RFC 5646)
Согласно стандарту, языковой тег должен иметь формат язык-РЕГИОН, где:
- язык – двухбуквенный код ISO 639-1 (например,
uk– украинский,ru– русский,en– английский); - РЕГИОН – двухбуквенный код страны ISO 3166-1 alpha-2 (например,
AE– Объединённые Арабские Эмираты,UA– Украина,US– США).
Примеры корректных IETF‑тегов для сайта, ориентированного на ОАЭ:
uk-AE– украинский язык, аудитория в ОАЭ;ru-AE– русский язык, аудитория в ОАЭ;en-AE– английский язык, аудитория в ОАЭ;fr-AE– французский язык, аудитория в ОАЭ.
Почему важно указывать регион AE для всех языков? Поисковые системы учитывают региональную привязку. Если для русскоязычной версии указать ru-RU, Google может решить, что страница предназначена для России, и не показывать её пользователям, находящимся в ОАЭ. Тег ru-AE явно сообщает: «это русскоязычный контент, актуальный для аудитории в ОАЭ». Аналогично, en-AE предпочтительнее, чем en-US или en-GB, если сайт не ориентирован исключительно на экспатов из конкретной страны.
2.3 Необходимость преобразования внутренних кодов в IETF
В нашем решении вводится массив-карта $i18n_ietf_map, который связывает короткий внутренний код плагина (например, 'ua') с полным IETF‑тегом ('uk-AE'). Это позволяет:
- корректно установить
lang="uk-AE"для украинской версии; - сгенерировать
hreflang="uk-AE"для альтернативных ссылок.
При этом внутренняя логика плагина продолжает использовать короткие коды – они остаются в URL и в базе данных. Такое разделение обеспечивает совместимость с существующей структурой и полное соответствие современным стандартам.
3. Конфигурация плагина i18n и особенности многоязычного сайта в ОАЭ
Рассмотрим фактические настройки плагина, используемые в проекте (из таблицы cot_config):
('plug', 'i18n', '', '02', 'locales', 0, 'ua|Українська\r\nen|English\r\nru|Русский\r\npl|Polski', ...);
('plug', 'i18n', '', '03', 'omitmain', 3, '1', '1', ...);
('plug', 'i18n', '', '04', 'rewrite', 3, '1', '0', ...);
('plug', 'i18n', '', '05', 'cookie', 3, '0', '0', ...);Пояснение параметров:
locales– список поддерживаемых языков с внутренними кодами (ua,en,ru,pl). Внимание: в нашем проекте для украинского используется кодua, а не стандартныйuk. Это важно для правильной настройки карты преобразования.omitmain– значение1означает, что для основного языка (в$cfg['defaultlang'] = 'ua') языковой префикс в URL не добавляется. Украинская версия будет доступна по адресу/e-vehicles/article, а альтернативные –/en/e-vehicles/article.rewrite–1включает ЧПУ, и плагин формирует URL с префиксом языка (например,/en/page).cookie–0– выбор языка не сохраняется в cookie (можно изменить при необходимости).
В файле config.php проекта должны быть определены:
$cfg['defaultlang'] = 'ua';
$cfg['mainurl'] = 'https://cotonti.local';Такая конфигурация идеально подходит для сайта, ориентированного на ОАЭ, где основной язык (украинский) используется без префикса, а дополнительные языки добавляются через префикс.
4. Механика формирования URL с языковым префиксом (связка .htaccess и i18n)
4.1 Правила в .htaccess
В корневом .htaccess содержится правило, которое преобразует ЧПУ‑префиксы в GET‑параметр l:
# Language selector
RewriteRule ^(en|ru|de|pl|ua)/(.*) $2?l=$1 [QSA,NC,NE,DPI]Это правило работает так: при запросе /en/e-vehicles/article сервер внутренне перенаправляет на /e-vehicles/article?l=en. Далее Cotonti, получив параметр l, активирует соответствующую локаль и загружает перевод страницы.
Важно: список языков в правиле должен совпадать с внутренними кодами из настройки locales (ua, en, ru, pl). В примере выше также присутствует de – его можно убрать или оставить, если немецкий язык добавлен.
4.2 Функция cot_url() и параметр force_no_lang
Функция cot_url() – стандартный инструмент Cotonti для генерации ЧПУ‑ссылок. Её сигнатура:
cot_url($area, $params, $anchor = '', $escape_html = false, $force_no_lang = false)Параметр $force_no_lang играет ключевую роль. Если он установлен в true, функция никогда не добавит языковой префикс, даже если в массиве $params присутствует элемент 'l' и настройка rewrite включена. Это даёт нам полный контроль над формированием URL для x-default при omitmain = 1.
4.3 Влияние omitmain на URL основного языка
- omitmain = 1: для основного языка префикс не добавляется. Украинская версия:
/e-vehicles/article. Альтернативные:/en/e-vehicles/article. - omitmain = 0: даже для основного языка добавляется префикс. Украинская версия:
/ua/e-vehicles/article.
Наше решение автоматически учитывает эту настройку при генерации x-default и тегов альтернативных языков.
5. Разработанное решение: интеграция улучшенной логики в i18n.header.tags.php
Мы расширяем стандартный файл плагина plugins/i18n/i18n.header.tags.php, добавляя три ключевых блока:
- Карта преобразования внутренних кодов в IETF‑теги (
$i18n_ietf_map). - Установка переменной
HTML_LANGдля использования в шаблоне<html lang="{HTML_LANG}">. - Генерация тегов
hreflangдля переведённых страниц модуляpageс учётом всех настроек.
Ниже представлен итоговый код с подробными комментариями. Оригинальная логика переключателя языков сохранена, исправлен только баг с формированием URL в админ‑панели.
5.1 Полный код файла i18n.header.tags.php
<?php
/* ====================
[BEGIN_COT_EXT]
Hooks=header.tags
Tags=header.tpl:{I18N_LANG_ROW_URL},{I18N_LANG_ROW_CODE},{I18N_LANG_ROW_TITLE},{I18N_LANG_ROW_CLASS},{I18N_LANG_ROW_SELECTED}
[END_COT_EXT]
==================== */
/**
* Renders language selector
* Отрисовка переключателя языков и генерация тегов hreflang.
* Фиксы и дополнительные плюшки от https://github.com/webitproff
* @package I18n
* @copyright (c) Cotonti Team
* @license https://github.com/Cotonti/Cotonti/blob/master/License.txt
*/
defined('COT_CODE') or die('Wrong URL');
// ------------------------------------------------------------------------------
// IETF (Internet Engineering Task Force) – организация, разрабатывающая стандарты
// интернета, в том числе формат языковых тегов вида "язык-РЕГИОН" (RFC 5646).
// Карта преобразования коротких внутренних кодов плагина i18n в полные IETF-теги.
// Для сайта, ориентированного на ОАЭ, регион указывается как AE.
// ------------------------------------------------------------------------------
$i18n_ietf_map = [
'ua' => 'uk-AE', // Украинский язык для ОАЭ (основной)
'ru' => 'ru-AE', // Русский язык для ОАЭ
'en' => 'en-AE', // Английский язык для ОАЭ
'pl' => 'pl-AE' // Польский язык для ОАЭ
];
// ========== 1. ПОСТРОЕНИЕ ПЕРЕКЛЮЧАТЕЛЯ ЯЗЫКОВ (с исправлением для админки) ==========
if (count($i18n_locales) > 0) {
foreach ($i18n_locales as $lc => $lc_title) {
$lc_class = ($lc == $i18n_locale) ? 'selected' : '';
$lc_selected = ($lc == $i18n_locale) ? 'selected="selected"' : '';
$i18n_urlparams = $_GET;
if ($cfg['plugin']['i18n']['omitmain'] && $lc == $i18n_fallback && (!$cfg['plugin']['i18n']['cookie'] || !cot_import('i18n_locale', 'COOKIE', 'ALP'))) {
unset($i18n_urlparams['l']);
} else {
$i18n_urlparams['l'] = $lc;
}
if (defined('COT_PLUG')) {
$i18n_ext = 'plug';
} else {
$i18n_ext = $env['ext'];
unset($i18n_urlparams['e']);
}
if (isset($i18n_urlparams['rwr'])) {
unset($i18n_urlparams['rwr']);
}
// Исправление для админ-панели: URL должен содержать /admin/ после языкового префикса
if (defined('COT_ADMIN') && COT_ADMIN) {
$url = cot_url('admin', $i18n_urlparams, '', false, true);
} else {
$url = cot_url($i18n_ext, $i18n_urlparams, '', false, true);
}
$t->assign([
'I18N_LANG_ROW_URL' => $url,
'I18N_LANG_ROW_CODE' => $lc,
'I18N_LANG_ROW_FLAG' => $lc === 'en' ? 'gb' : $lc,
'I18N_LANG_ROW_TITLE' => htmlspecialchars($lc_title),
'I18N_LANG_ROW_CLASS' => $lc_class,
'I18N_LANG_ROW_SELECTED' => $lc_selected,
]);
$t->parse('HEADER.I18N_LANG.I18N_LANG_ROW');
}
$t->parse('HEADER.I18N_LANG');
}
// ========== 2. УСТАНОВКА ПОЛНОГО IETF-ТЕГА ДЛЯ АТРИБУТА lang ==========
$current_short_locale = $i18n_locale ?: Cot::$cfg['defaultlang'];
$html_lang = isset($i18n_ietf_map[$current_short_locale])
? $i18n_ietf_map[$current_short_locale]
: $current_short_locale;
$t->assign('HTML_LANG', $html_lang);
// ========== 3. ГЕНЕРАЦИЯ ТЕГОВ hreflang ДЛЯ ПЕРЕВЕДЁННЫХ СТРАНИЦ ==========
if ($env['ext'] == 'page' && isset($id) && $id > 0) {
$translated_locales = cot_i18n_list_page_locales($id);
if (!empty($translated_locales)) {
$page_data = Cot::$db->query("SELECT page_cat, page_alias FROM " . Cot::$db->pages . " WHERE page_id = ?", array($id))->fetch();
if ($page_data) {
$url_params = empty($page_data['page_alias'])
? array('c' => $page_data['page_cat'], 'id' => $id)
: array('c' => $page_data['page_cat'], 'al' => $page_data['page_alias']);
$alternate_tags = '';
$main_url = rtrim(Cot::$cfg['mainurl'], '/');
// Теги для переведённых языков (кроме основного)
foreach ($translated_locales as $lc) {
if ($lc == Cot::$cfg['defaultlang']) continue;
$params = $url_params;
$params['l'] = $lc;
$url = $main_url . '/' . cot_url('page', $params, '', false, false);
$hreflang = isset($i18n_ietf_map[$lc]) ? $i18n_ietf_map[$lc] : $lc;
$alternate_tags .= '' . "\n";
}
// Тег x-default
$default_params = $url_params;
if ((int)Cot::$cfg['plugin']['i18n']['omitmain'] === 1) {
unset($default_params['l']);
$default_url = $main_url . '/' . cot_url('page', $default_params, '', false, true);
} else {
$default_params['l'] = Cot::$cfg['defaultlang'];
$default_url = $main_url . '/' . cot_url('page', $default_params, '', false, false);
}
$alternate_tags .= '' . "\n";
$t->assign('ALTERNATE_TAGS', $alternate_tags);
$t->parse('HEADER.ALTERNATE_TAGS');
}
}
}Этот код полностью заменяет оригинальный файл i18n.header.tags.php.
5.2 Что нужно добавить в шаблон header.tpl
В файле темы header.tpl необходимо использовать переменные {HTML_LANG} и {ALTERNATE_TAGS}. Фрагмент шаблона:
<!-- BEGIN: HEADER -->
<!DOCTYPE html>
<!-- IF {HTML_LANG} -->
<html lang="{HTML_LANG}" data-bs-theme="light">
<!-- ELSE -->
<html lang="{PHP.usr.lang}" data-bs-theme="light">
<!-- ENDIF -->
<head>
...
<!-- IF {ALTERNATE_TAGS} -->
{ALTERNATE_TAGS}
<!-- ENDIF -->
...
</head>Важно: переменная {ALTERNATE_TAGS} должна располагаться внутри <head> до закрывающего тега.
6. Детальный разбор кода: построчное объяснение каждого блока
6.1 Карта преобразования $i18n_ietf_map
Массив связывает внутренние коды плагина ('ua', 'ru', 'en', 'pl') с полными IETF‑тегами для ОАЭ. Если в будущем добавится новый язык (например, французский с кодом 'fr'), достаточно дополнить карту записью 'fr' => 'fr-AE'.
6.2 Генерация переключателя языков (блок 1)
Этот код повторяет оригинальную логику, за исключением исправления для админ‑панели. Раньше при переключении языка в админке URL формировался некорректно (например, /en/page?m=page вместо /en/admin/page). Теперь при обнаружении константы COT_ADMIN вызывается cot_url('admin', ...), что гарантирует правильный путь.
6.3 Установка HTML_LANG (блок 2)
$current_short_locale = $i18n_locale ?: Cot::$cfg['defaultlang'];
$html_lang = isset($i18n_ietf_map[$current_short_locale]) ? $i18n_ietf_map[$current_short_locale] : $current_short_locale;
$t->assign('HTML_LANG', $html_lang);Определяется текущий язык (из переменной плагина $i18n_locale или из конфига). Затем через карту он преобразуется в полный IETF‑тег. Если для языка нет записи в карте, используется оригинальный короткий код (на случай временного отсутствия). Результат передаётся в шаблон.
6.4 Генерация тегов hreflang (блок 3)
Проверка контекста
if ($env['ext'] == 'page' && isset($id) && $id > 0) {Код выполняется только на страницах модуля page, когда известен идентификатор.
Получение списка переведённых языков
$translated_locales = cot_i18n_list_page_locales($id);Функция плагина возвращает массив внутренних кодов языков, для которых существует запись в cot_i18n_pages. Например, ['en', 'ru', 'pl']. Основной язык (ua) в этот массив обычно не входит, если для него нет явного перевода.
Получение данных страницы
$page_data = Cot::$db->query("SELECT page_cat, page_alias FROM " . Cot::$db->pages . " WHERE page_id = ?", array($id))->fetch();Из таблицы pages извлекаются категория и алиас, необходимые для построения ЧПУ.
Формирование базовых параметров URL
$url_params = empty($page_data['page_alias'])
? array('c' => $page_data['page_cat'], 'id' => $id)
: array('c' => $page_data['page_cat'], 'al' => $page_data['page_alias']);Если у страницы есть алиас, используем его (al), иначе – идентификатор (id).
Цикл по переведённым языкам
foreach ($translated_locales as $lc) {
if ($lc == Cot::$cfg['defaultlang']) continue;
$params = $url_params;
$params['l'] = $lc;
$url = $main_url . '/' . cot_url('page', $params, '', false, false);
$hreflang = isset($i18n_ietf_map[$lc]) ? $i18n_ietf_map[$lc] : $lc;
$alternate_tags .= '' . "\n";
}- Пропускаем основной язык – он будет представлен через
x-default. - Добавляем параметр
lс внутренним кодом языка. - Вызываем
cot_url()сforce_no_lang = false, чтобы получить относительный путь с языковым префиксом (еслиrewriteвключён). - Присоединяем основной URL сайта, получая абсолютный адрес.
- Преобразуем внутренний код в IETF‑тег через карту и формируем тег
<link>.
Генерация x-default
$default_params = $url_params;
if ((int)Cot::$cfg['plugin']['i18n']['omitmain'] === 1) {
unset($default_params['l']);
$default_url = $main_url . '/' . cot_url('page', $default_params, '', false, true);
} else {
$default_params['l'] = Cot::$cfg['defaultlang'];
$default_url = $main_url . '/' . cot_url('page', $default_params, '', false, false);
}- Если
omitmain = 1, удаляем параметрlи вызываемcot_url()сforce_no_lang = true. Это гарантирует URL без языкового префикса. - Если
omitmain = 0, добавляем параметрlсо значением основного языка и вызываемcot_url()сforce_no_lang = false– получим URL с префиксом (например,/ua/...).
Передача в шаблон
$t->assign('ALTERNATE_TAGS', $alternate_tags);
$t->parse('HEADER.ALTERNATE_TAGS');Собранная строка с тегами помещается в переменную шаблона и парсится (даже если блок не объявлен, это безопасно).
7. Настройка шаблона header.tpl
Полный пример актуального шаблона header.tpl из темы Index36:
<!-- BEGIN: HEADER -->
<!DOCTYPE html>
<!-- IF {HTML_LANG} -->
<html lang="{HTML_LANG}" data-bs-theme="light">
<!-- ELSE -->
<html lang="{PHP.usr.lang}" data-bs-theme="light">
<!-- ENDIF -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{HEADER_TITLE}</title>
<!-- IF {HEADER_META_DESCRIPTION} -->
<meta name="description" content="{HEADER_META_DESCRIPTION}" />
<!-- ENDIF -->
<!-- IF {HEADER_BASEHREF} -->
{HEADER_BASEHREF}
<!-- ENDIF -->
<!-- IF {HEADER_CANONICAL_URL} -->
<link rel="canonical" href="{HEADER_CANONICAL_URL}" />
<!-- ENDIF -->
<!-- IF {ALTERNATE_TAGS} -->
{ALTERNATE_TAGS}
<!-- ENDIF -->
<link rel="shortcut icon" href="favicon.ico" />
<link rel="icon" href="{PHP.cfg.themes_dir}/{PHP.theme}/img/icon.webp" type="image/svg+xml">
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
<!-- IF {PHP.out.meta} -->
{PHP.out.meta}
<!-- ENDIF -->
<script>
(function () {
const storedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const defaultTheme = storedTheme || (prefersDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-bs-theme', defaultTheme);
})();
</script>
{HEADER_HEAD}
</head>Здесь:
{HTML_LANG}подставляет значениеuk-AE,en-AEи т.д.{ALTERNATE_TAGS}выводит сгенерированные<link>теги.
8. Проверка и отладка: ожидаемый результат в исходном коде страницы
8.1 Украинская (основная) версия
URL: https://cotonti.local/catname/page-alias-123
Исходный код (фрагмент):
<!DOCTYPE html>
<html lang="uk-AE" data-bs-theme="light">
<head>
...
<link rel="canonical" href="https://cotonti.local/catname/page-alias-123" />
<link rel="alternate" hreflang="en-AE" href="https://cotonti.local/en/catname/page-alias-123">
<link rel="alternate" hreflang="pl-AE" href="https://cotonti.local/pl/catname/page-alias-123">
<link rel="alternate" hreflang="ru-AE" href="https://cotonti.local/ru/catname/page-alias-123">
<link rel="alternate" hreflang="x-default" href="https://cotonti.local/catname/page-alias-123">
...
</head>Анализ:
lang="uk-AE"– полный IETF‑тег для украинского языка в ОАЭ.- hreflang содержат коды
en-AE,pl-AE,ru-AE(если есть переводы). x-defaultуказывает на украинскую версию без префикса (благодаряomitmain=1).
8.2 Русская версия
URL: https://cotonti.local/ru/catname/page-alias-123
<!DOCTYPE html>
<html lang="ru-AE" data-bs-theme="light">
<head>
...
<link rel="canonical" href="https://cotonti.local/ru/catname/page-alias-123" />
<link rel="alternate" hreflang="en-AE" href="https://cotonti.local/en/catname/page-alias-123">
<link rel="alternate" hreflang="pl-AE" href="https://cotonti.local/pl/catname/page-alias-123">
<link rel="alternate" hreflang="ru-AE" href="https://cotonti.local/ru/catname/page-alias-123">
<link rel="alternate" hreflang="x-default" href="https://cotonti.local/catname/page-alias-123">
...
</head>Обратите внимание: x-default по‑прежнему ведёт на URL без префикса – украинскую версию. Это правильное поведение, так как основная версия является страницей по умолчанию.
8.3 Ситуация с omitmain = 0
Если бы параметр omitmain был равен 0, то x-default выглядел бы так:
<link rel="alternate" hreflang="x-default" href="https://cotonti.local/ua/catname/page-alias-123">А основной язык также мог бы иметь собственный тег hreflang (если убрать условие пропуска в цикле).
9. Типичные ошибки и способы их устранения
9.1 Теги hreflang не выводятся на странице
- Причина: страница не относится к модулю
page, либо переменная$idне определена. - Решение: проверьте, что URL действительно ведёт на страницу модуля
page. Для других модулей расширьте условие (см. раздел 10).
9.2 x-default содержит префикс языка при omitmain=1
- Причина: неверно передан параметр
force_no_langили в массиве$default_paramsостался элементl. - Решение: убедитесь, что для
omitmain=1используетсяunset($default_params['l'])и вызовcot_url(..., true).
9.3 В тегах hreflang отображаются короткие коды вместо IETF
- Причина: отсутствует запись для языка в массиве
$i18n_ietf_map. - Решение: добавьте соответствующую пару в карту, например
'fr' => 'fr-AE'.
9.4 Атрибут lang содержит короткий код
- Причина: переменная
{HTML_LANG}не передана в шаблон или текущий язык не найден в карте. - Решение: проверьте наличие блока 2 в коде и правильность карты.
9.5 Ссылки в hreflang относительные (без домена)
- Причина:
Cot::$cfg['mainurl']не задан или содержит слеш в конце, который был удалён функциейrtrim, но при склейке возникла ошибка. - Решение: в
config.phpпропишите$cfg['mainurl'] = 'https://cotonti.local';(без завершающего слеша).
9.6 Дублирование тега для основного языка
- Причина: в цикле не пропущен основной язык.
- Рекомендация: оставьте условие
if ($lc == Cot::$cfg['defaultlang']) continue;. Дублирование hreflang для одной и той же версии может запутать поисковики.
9.7 Проблемы с кешированием
Если используется статический кеш страниц, изменения могут проявиться не сразу. Очистите кеш Cotonti или временно отключите кеширование для проверки.
10. Расширение решения на другие модули Cotonti
Представленный код ориентирован на модуль page. Если ваш сайт использует мультиязычность в других модулях (например, market для товаров), адаптация потребует нескольких шагов.
10.1 Пример для модуля market
Предположим, у вас есть модуль market с товарами, и переводы хранятся в таблице cot_i18n_market. Алгоритм расширения:
- В условии
if ($env['ext'] == 'page' && isset($id) && $id > 0)замените наif (in_array($env['ext'], ['page', 'market']) && isset($id) && $id > 0). - Получите ID товара (может быть в
$idили$al). - Создайте функцию, аналогичную
cot_i18n_list_page_locales(), которая возвращает массив переведённых локалей для товара. - Сформируйте
$url_paramsв соответствии с правилами маршрутизации модуляmarket(например,array('c' => $cat, 'id' => $id)). - Используйте ту же логику генерации тегов.
Принцип остаётся неизменным: получить список языков перевода, для каждого сформировать URL с параметром l, преобразовать короткий код в IETF и добавить тег.
11. Заключение и лучшие практики
Мы разработали и внедрили полностью автоматическое решение для генерации корректной языковой разметки в Cotonti с плагином i18n. Оно:
- Динамически формирует теги
hreflangиx-defaultтолько для существующих переводов. - Устанавливает атрибут
langв соответствии со стандартом IETF BCP 47. - Учитывает настройки
omitmainиrewrite, гарантируя правильные абсолютные URL. - Поддерживает специфику многоязычного сайта в пределах одной страны (ОАЭ) благодаря региональной привязке
-AE. - Не требует ручного вмешательства при изменении списка языков или добавлении переводов.
- Включает исправление для корректной работы переключателя языков в админ‑панели.
Рекомендации по дальнейшему обслуживанию:
- При обновлении плагина i18n файл
i18n.header.tags.phpможет быть перезаписан. Рекомендуется создать резервную копию или вынести кастомную логику в отдельный мини‑плагин, подключающийся к хукуheader.tagsпосле i18n. - Регулярно проверяйте актуальность карты
$i18n_ietf_mapпри добавлении новых языков. - Используйте инструменты Google Search Console для проверки корректности разметки hreflang.
Данное руководство основано на реальном внедрении в проект cotonti.local (демонстрационный домен). Код распространяется под лицензией MIT, вы можете свободно использовать и модифицировать его.
12. Приложения
12.1 SQL‑запрос для проверки переводов конкретной страницы
SELECT ipage_locale FROM cot_i18n_pages WHERE ipage_id = 123;Замените 123 на ID вашей страницы. Результат покажет внутренние коды языков, на которые страница переведена.
12.2 Полная карта IETF‑тегов для ОАЭ (пример с часто используемыми языками)
| Внутренний код (i18n) | IETF‑тег | Описание |
| ua | uk-AE | Украинский (ОАЭ) |
| ru | ru-AE | Русский (ОАЭ) |
| en | en-AE | Английский (ОАЭ) |
| fr | fr-AE | Французский (ОАЭ) |
| ar | ar-AE | Арабский (ОАЭ) |
| hi | hi-AE | Хинди (ОАЭ) |
| ur | ur-AE | Урду (ОАЭ) |
12.3 Правила .htaccess для языковых префиксов (адаптировано под ОАЭ)
# Language selector (перечислите все используемые внутренние коды)
RewriteRule ^(en|ru|pl|ua|fr|ar)/(.*) $2?l=$1 [QSA,NC,NE,DPI]Убедитесь, что список совпадает с кодами в настройке locales плагина i18n.
12.4 Полезные ссылки
- Google: Управление многоязычными и многорегиональными сайтами
- RFC 5646 — Tags for Identifying Languages
- ISO 3166-1 alpha-2 country codes
- Cotonti CMF GitHub
- Тема Index36 (используемый шаблон)
Статья подготовлена на основе реального опыта внедрения и отладки. Автор выражает благодарность сообществу Cotonti (кроме отдельных динозавров-педиков) и разработчикам плагина i18n. При возникновении вопросов обращайтесь в соответствующие разделы форума поддержки.
Версия документа: 2.0
Дата: 22 апреля 2026 г.
Автор: Практикующий разработчик (webitproff)
Целевая CMS: Cotonti Siena (v0.9.26 и выше)
Задействованный плагин: i18n (Content Internationalization)
Особенности: полная поддержка региональной привязки (ОАЭ), стандарт IETF BCP 47, автоматическая установка атрибута lang и генерация hreflang
Важные изменения – современный формат языковых тегов требует использования дефиса (-)
В HTML правильно писать <html lang="en-GB"> (с дефисом).
Почему?
Стандарт BCP 47 / RFC 5646 – современный формат языковых тегов требует использования дефиса (-) в качестве разделителя между языком и регионом.
en – язык (английский)
GB – регион (Великобритания)
Подчёркивание (en_GB) – устаревший синтаксис (из POSIX, Java, некоторых старых систем). В HTML, XML, HTTP-заголовке Accept-Language используется только дефис.
Валидация и SEO – браузеры и поисковые системы корректно парсят только теги с дефисами. Например, Google и Яндекс при анализе lang ожидают en-GB, а en_GB может быть проигнорирован или воспринят как неизвестный.
Пример
<!DOCTYPE html>
<html lang="en-GB">
...Если нужно указать регион без дефиса – неверно.
Дефис – единственный допустимый разделитель по спецификации W3C.
Комментарии (0)
Автор контента
Оффлайн
Sodium Carbonate
Последняя авторизация: 11.07.2026 19:44
- Страница размещена: 22.04.2026 06:52
- Последнее обновление: 28.04.2026 06:40