Реалізація мультимовності (i18n) сайту

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація мультимовності (i18n) сайту
Складна
від 1 тижня до 3 місяців
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація мультимовної підтримки сайту (i18n)

Мультимовність — це не «просто приробити перекладач». Це архітектурне рішення, яке впливає на структуру URL, зберігання контенту в БД, SEO, кеширування та розгортання. Зробити правильно з першого разу важливіше, ніж зробити швидко.

Архітектурні варіанти URL

Три підходи до організації URL мультимовного сайту:

Стратегія Приклади Коли використовувати
Поддомен ru.example.com, en.example.com Різні сервери/CDN на регіон
Шлях example.com/ru/, example.com/en/ Один сервер, більшість випадків
Окремий домен example.ru, example.com Різні юридичні особи або бренди

Шлях (/ru/, /en/) — найпоширеніший та найпростіший в реалізації.

Серверна частина: Laravel + Astrotomic Translatable

composer require astrotomic/laravel-translatable
// database/migrations/..._create_product_translations_table.php
Schema::create('product_translations', function (Blueprint $table) {
    $table->id();
    $table->foreignId('product_id')->constrained()->cascadeOnDelete();
    $table->string('locale', 10)->index();
    $table->string('title');
    $table->text('description')->nullable();
    $table->string('slug')->nullable();
    $table->unique(['product_id', 'locale']);
    $table->timestamps();
});
// app/Models/Product.php
use Astrotomic\Translatable\Contracts\Translatable as TranslatableContract;
use Astrotomic\Translatable\Translatable;

class Product extends Model implements TranslatableContract
{
    use Translatable;

    public array $translatedAttributes = ['title', 'description', 'slug'];

    protected $fillable = ['price', 'sku', 'is_active'];
}
// Створення з перекладами
Product::create([
    'price'    => 1990,
    'sku'      => 'PROD-001',
    'ru'       => ['title' => 'Умные часы', 'slug' => 'umnye-chasy'],
    'en'       => ['title' => 'Smart Watch', 'slug' => 'smart-watch'],
    'de'       => ['title' => 'Smartwatch', 'slug' => 'smartwatch'],
]);

// Читання за поточною локаллю
app()->setLocale('en');
$product->title; // "Smart Watch"

app()->setLocale('ru');
$product->title; // "Умные часы"

Маршрутизація з префіксами локалей

// routes/web.php
Route::prefix('{locale}')
    ->where(['locale' => 'ru|en|de|fr|uk'])
    ->middleware('setLocale')
    ->group(function () {
        Route::get('/', [HomeController::class, 'index'])->name('home');
        Route::get('/catalog', [CatalogController::class, 'index'])->name('catalog');
        Route::get('/catalog/{slug}', [ProductController::class, 'show'])->name('product');
    });

// Редирект з / на /ru/ або визначену мову
Route::get('/', function () {
    return redirect()->route('home', ['locale' => app()->getLocale()]);
});
// app/Http/Middleware/SetLocale.php
public function handle(Request $request, Closure $next): mixed
{
    $locale = $request->route('locale') ?? session('locale', 'ru');

    if (!in_array($locale, ['ru', 'en', 'de', 'fr', 'uk'])) {
        abort(404);
    }

    App::setLocale($locale);
    session(['locale' => $locale]);

    return $next($request);
}

Фронтенд: i18next

npm install i18next react-i18next i18next-http-backend i18next-browser-languagedetector
// i18n/config.ts
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import HttpBackend from 'i18next-http-backend'
import LanguageDetector from 'i18next-browser-languagedetector'

i18n
  .use(HttpBackend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: 'ru',
    supportedLngs: ['ru', 'en', 'de', 'fr', 'uk'],
    ns: ['common', 'catalog', 'checkout'],
    defaultNS: 'common',
    backend: {
      loadPath: '/locales/{{lng}}/{{ns}}.json',
    },
    detection: {
      order: ['path', 'cookie', 'localStorage', 'navigator'],
      lookupFromPathIndex: 0,
    },
    interpolation: {
      escapeValue: false, // React вже екранує
    },
  })

export default i18n
public/locales/
  ru/
    common.json
    catalog.json
    checkout.json
  en/
    common.json
    catalog.json
    checkout.json
// public/locales/ru/common.json
{
  "nav": {
    "catalog": "Каталог",
    "cart": "Корзина ({{count}})",
    "account": "Личный кабинет"
  },
  "actions": {
    "addToCart": "Добавить в корзину",
    "buyNow": "Купить сейчас"
  },
  "product": {
    "count_one": "{{count}} товар",
    "count_few": "{{count}} товара",
    "count_many": "{{count}} товаров",
    "count_other": "{{count}} товаров"
  }
}
// Використання в компонентах
import { useTranslation } from 'react-i18next'

function CatalogHeader({ count }: { count: number }) {
  const { t, i18n } = useTranslation('common')

  return (
    <header>
      <h1>{t('nav.catalog')}</h1>
      <span>{t('product.count', { count })}</span>
      <span>Мова: {i18n.language}</span>
    </header>
  )
}

SEO: hreflang, canonical, sitemap

// Генерація hreflang для всіх сторінок
function hreflangTags(string $routeName, array $params = []): string
{
    $locales = ['ru', 'en', 'de', 'fr', 'uk'];
    $tags = '';

    foreach ($locales as $locale) {
        $url = route($routeName, array_merge($params, ['locale' => $locale]));
        $tags .= "<link rel=\"alternate\" hreflang=\"{$locale}\" href=\"{$url}\" />\n";
    }

    $defaultUrl = route($routeName, array_merge($params, ['locale' => 'ru']));
    $tags .= "<link rel=\"alternate\" hreflang=\"x-default\" href=\"{$defaultUrl}\" />\n";

    return $tags;
}
<!-- sitemap.xml з мовними альтернативами -->
<url>
  <loc>https://example.com/ru/catalog/smart-watch</loc>
  <xhtml:link rel="alternate" hreflang="ru" href="https://example.com/ru/catalog/smart-watch"/>
  <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/catalog/smart-watch"/>
  <xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/catalog/smart-watch"/>
  <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/ru/catalog/smart-watch"/>
</url>

Переклад контенту: workflow

Ручний переклад через Google Translate API для першого наповнення:

// app/Services/TranslationService.php
use Google\Cloud\Translate\V2\TranslateClient;

class TranslationService
{
    private TranslateClient $client;

    public function __construct()
    {
        $this->client = new TranslateClient([
            'key' => config('services.google_translate.key'),
        ]);
    }

    public function translateBatch(array $texts, string $targetLang, string $sourceLang = 'ru'): array
    {
        $results = $this->client->translateBatch($texts, [
            'source' => $sourceLang,
            'target' => $targetLang,
            'format' => 'html', // зберігає HTML-теги
        ]);

        return array_column($results, 'text');
    }
}
// artisan команда для масового перекладу
php artisan translate:products --from=ru --to=en,de,fr,uk --batch=50

Кеширування перекладів

Кешуємо файли перекладів у Redis, щоб не читати JSON з диска при кожному запиті:

// AppServiceProvider
public function boot(): void
{
    if (app()->isProduction()) {
        $this->app->singleton('translator', function ($app) {
            $loader = new CachedTranslationLoader(
                $app['translation.loader'],
                $app['cache.store'],
                ttl: 3600
            );
            return new Translator($loader, $app['config']['app.locale']);
        });
    }
}

Числівники для всіх мов

// Універсальний плюралізатор через Intl.PluralRules
function createPluralizer(locale: string, forms: Record<string, string>) {
  const rules = new Intl.PluralRules(locale)
  return (n: number) => `${n} ${forms[rules.select(n)] ?? forms.other}`
}

const pluralizers = {
  ru: createPluralizer('ru', { one: 'товар', few: 'товара', many: 'товаров', other: 'товаров' }),
  en: createPluralizer('en', { one: 'item', other: 'items' }),
  de: createPluralizer('de', { one: 'Artikel', other: 'Artikel' }),
  uk: createPluralizer('uk', { one: 'товар', few: 'товари', many: 'товарів', other: 'товарів' }),
}

Терміни

Налаштування i18n-інфраструктури (маршрути, middleware, Translatable, i18next) без перекладу контенту — 3–4 дні. Переклад інтерфейсу на 4–5 мов з автоматичною першою трансляцією та ручною вичиткою — ще 3–5 днів залежно від обсягу рядків. Повний запуск з SEO-конфігурацією та sitemap — 1–1.5 тижня.