Усі статті
SymfonyБази даних 24 жовтня 2026 · 2 хв читання

Переклади контенту як у WordPress, але на Symfony

У WordPress з WPML і в Joomla кожен переклад — окремий запис зі своєю адресою, SEO й датою. Мені потрібна була та сама модель для портфоліо на Symfony, без сторонніх бандлів.

Задача

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

При цьому сайт має знати, що «Переклади контенту» і «Content translations» — одна стаття. Інакше перемикач мови не знатиме, куди вести, а Google не отримає hreflang.

Чому не JSON-поля

Популярний варіант — зберігати всі мови в одному рядку: title_uk, title_en або JSON-поле. Це зручно для назв товарів, але для довгого контенту погано:

  • Не можна опублікувати українську версію, поки англійська ще чернетка.
  • Слаг, SEO й дата публікації в кожної мови свої — полів стає вдвічі більше.
  • Додати третю мову означає міграцію всіх таблиць.

Схема даних

Кожна мова — окремий запис. Записи однієї статті повʼязані через таблицю translation_group. Пара «група + мова» унікальна, тож двох англійських версій однієї статті не буде.

Група перекладів #12 зі спільними обкладинкою, тегами й порядком і двома записами: post · uk за адресою /uk/blog/pereklady-kontentu і post · en за адресою /en/blog/content-translations, обидва опубліковані
Група перекладів і два її записи
src/Entity/Post.phpPHP
#[ORM\Entity(repositoryClass: PostRepository::class)]
#[ORM\UniqueConstraint(columns: ['translation_group_id', 'locale'])]
class Post
{
    #[ORM\ManyToOne(inversedBy: 'posts')]
    #[ORM\JoinColumn(nullable: false)]
    private TranslationGroup $translationGroup;

    #[ORM\Column(length: 5)]
    private string $locale;

    #[ORM\Column(length: 160)]
    private string $title;

    #[ORM\Column(length: 160)]
    private string $slug;
}

Як знайти переклад

Перемикач мови й теги hreflang беруть інші записи тієї ж групи. Це один запит без жодних JOIN по мовних таблицях:

src/Repository/PostRepository.phpPHP
public function findTranslation(Post $post, string $locale): ?Post
{
    return $this->findOneBy([
        'translationGroup' => $post->getTranslationGroup(),
        'locale' => $locale,
        'status' => PostStatus::Published,
    ]);
}

Що спільне для всіх мов

Частина даних не залежить від мови: обкладинка, теги, порядок на сайті, звʼязок з клієнтом. Їх я зберігаю на групі, а не на записі. Змінив тег в українській версії — він змінився і в англійській.

Самі теги теж мають назву для кожної мови, але одну адресу: /uk/blog?tag=symfony і /en/blog?tag=symfony показують ті самі статті у своїй мові.

Що вийшло

  • Кожна мова публікується, коли готова, і має власні слаг та SEO.
  • Третя мова — це запис у налаштуваннях, а не міграція.
  • У списку адмінки одразу видно, яких перекладів бракує.

Та сама схема працює для проєктів, послуг і досвіду. Якщо робите багатомовний сайт на Symfony чи Laravel і хочете так само, напишіть — розкажу деталі.

Теми: SymfonyБази даних
Віталій
Написати

Потрібен розробник під ваш проєкт?

Розкажіть про задачу — відповім, як її зробити і скільки часу це займе.

Написати мені sliusardev@gmail.com

Написати мені

Розкажіть коротко про задачу — відповім на email, зазвичай протягом дня.