Мультиязычность в PHP 2012: KG + RU + EN без i18n фреймворков
В 2012 году правительственный портал, образовательная платформа, или корпоративный сайт в Кыргызстане обязаны были работать на трёх языках: кыргызском (ky), русском (ru) и английском (en). Larevel 4 только вышел, Symfony 2 был молодым. Большинство проектов строилось на CodeIgniter или чистом PHP.
Встроенной поддержки i18n в CodeIgniter не хватало для наших требований. Symfony Translation Component существовал, но использовать только его компонент отдельно было непривычно. Мы построили свою систему — простую, предсказуемую, с SEO-friendly URL.
URL-структура
Первое решение: как язык попадает в URL?
Вариант А: yoursite.kg/ky/about, yoursite.kg/ru/about
Вариант Б: ky.yoursite.kg, ru.yoursite.kg
Вариант В: yoursite.kg/about?lang=ky
Вариант В — плохой: параметр не индексируется отдельно в поисковиках. Вариант Б — хорош для SEO, но усложняет инфраструктуру (три домена, три SSL-сертификата в 2012 году стоили денег). Выбрали Вариант А.
// routes.php — определение языка из первого сегмента URL
class Router {
private array $supportedLangs = ['ky', 'ru', 'en'];
private string $defaultLang = 'ky'; // Кыргызский — основной
public function parse(string $uri): array {
$segments = explode('/', trim($uri, '/'));
if (!empty($segments[0]) && in_array($segments[0], $this->supportedLangs)) {
$lang = array_shift($segments);
$path = implode('/', $segments);
} else {
// Нет языка в URL — редирект на кыргызскую версию
$lang = $this->defaultLang;
$path = $uri;
header('Location: /' . $lang . $uri, true, 301);
exit;
}
return ['lang' => $lang, 'path' => $path];
}
}
Языковые файлы: простой формат с fallback
// languages/ky.php — кыргызский (основной)
return [
'site.title' => 'Кыргызстан Порталы',
'nav.home' => 'Башкы бет',
'nav.about' => 'Биз жөнүндө',
'nav.services' => 'Кызматтар',
'nav.contact' => 'Байланыш',
'footer.rights' => 'Бардык укуктар корголгон',
'error.not_found' => 'Бет табылган жок',
'form.submit' => 'Жөнөтүү',
'form.required' => 'Милдеттүү талаа',
];
// languages/ru.php — русский (все ключи переведены)
return [
'site.title' => 'Портал Кыргызстана',
'nav.home' => 'Главная',
'nav.about' => 'О нас',
// ... и т.д.
];
// languages/en.php — английский (может быть неполным — используем fallback)
return [
'site.title' => 'Kyrgyzstan Portal',
'nav.home' => 'Home',
// nav.contact ещё не переведён — будет использоваться fallback
];
// Класс перевода с fallback-цепочкой: en → ru → ky
class Translator {
private array $translations = [];
private array $fallbackChain;
public function __construct(string $lang) {
// Fallback: если ключ не найден в en, берём ru, если нет в ru — ky
$this->fallbackChain = match($lang) {
'en' => ['en', 'ru', 'ky'],
'ru' => ['ru', 'ky'],
default => ['ky'],
};
foreach (array_unique($this->fallbackChain) as $l) {
$file = __DIR__ . '/languages/' . $l . '.php';
if (file_exists($file)) {
$this->translations[$l] = require $file;
}
}
}
public function t(string $key, array $params = []): string {
foreach ($this->fallbackChain as $lang) {
if (isset($this->translations[$lang][$key])) {
$text = $this->translations[$lang][$key];
// Подстановка параметров: ":name" → значение
foreach ($params as $k => $v) {
$text = str_replace(':' . $k, $v, $text);
}
return $text;
}
}
// Ключ не найден нигде — вернуть сам ключ (видно разработчику)
return '{' . $key . '}';
}
}
// Глобальный хелпер
function __t(string $key, array $params = []): string {
return app()->translator->t($key, $params);
}
Мультиязычный контент в базе данных
Для контента (статьи, страницы, категории) — отдельные колонки per язык:
-- Простая схема: колонки для каждого языка
CREATE TABLE pages (
id INT AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(200) NOT NULL, -- URL-идентификатор (одинаковый для всех языков)
-- Контент на каждом языке
title_ky VARCHAR(500),
title_ru VARCHAR(500),
title_en VARCHAR(500),
body_ky MEDIUMTEXT,
body_ru MEDIUMTEXT,
body_en MEDIUMTEXT,
-- SEO-мета для каждого языка
meta_desc_ky VARCHAR(300),
meta_desc_ru VARCHAR(300),
meta_desc_en VARCHAR(300),
is_published TINYINT(1) DEFAULT 0,
created_at DATETIME
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
// Получение контента с fallback
class Page {
public function getTitle(string $lang): string {
// Приоритет: запрошенный язык → русский → кыргызский
$field = 'title_' . $lang;
if (!empty($this->$field)) return $this->$field;
if (!empty($this->title_ru)) return $this->title_ru;
return $this->title_ky ?? '';
}
public function getBody(string $lang): string {
$field = 'body_' . $lang;
if (!empty($this->$field)) return $this->$field;
if (!empty($this->body_ru)) return $this->body_ru;
return $this->body_ky ?? '';
}
}
SEO: hreflang для поисковиков
Чтобы Google понимал, что три URL — это одна страница на разных языках:
// В шаблоне <head>: hreflang для всех языков
$currentSlug = $page->slug; // Например: 'about-us'
?>
<link rel="alternate" hreflang="ky" href="https://yoursite.kg/ky/<?= $currentSlug ?>">
<link rel="alternate" hreflang="ru" href="https://yoursite.kg/ru/<?= $currentSlug ?>">
<link rel="alternate" hreflang="en" href="https://yoursite.kg/en/<?= $currentSlug ?>">
<!-- x-default: язык по умолчанию для пользователей без подходящего -->
<link rel="alternate" hreflang="x-default" href="https://yoursite.kg/ky/<?= $currentSlug ?>">
<?php
Языковой переключатель
// Компонент переключателя языков
function languageSwitcher(string $currentLang, string $currentSlug): string {
$langs = ['ky' => 'Кыргызча', 'ru' => 'Русский', 'en' => 'English'];
$output = '<div class="lang-switcher">';
foreach ($langs as $code => $label) {
$url = '/' . $code . '/' . $currentSlug;
$active = $code === $currentLang ? ' class="active"' : '';
$output .= "<a href=\"{$url}\"{$active}>{$label}</a> ";
}
return $output . '</div>';
}
Что изменилось с тех пор
Symfony Translation Component, Laravel Localization, next-intl — сегодня зрелые, документированные решения. gettext + PO-файлы — стандарт для многих CMS.
Но архитектурные решения, которые мы принимали в 2012, остаются актуальными: fallback-цепочка языков, hreflang для SEO, разделение UI-строк и контента. Инструменты изменились. Задачи, которые они решают — нет.
Для сайтов, обслуживающих Кыргызстан в 2025 году, трёхязычность (KY + RU + EN) по-прежнему стандарт для государственных и образовательных проектов. Принципы построения такой системы — те же самые.