Twig

AI-перевод Twig-шаблонов с помощью Lingo.dev CLI

Что такое Twig?

Twig — это гибкий, быстрый и безопасный шаблонизатор для PHP. Он широко используется в приложениях на Symfony и других PHP-фреймворках для разделения логики отображения и логики приложения. Twig использует чистый синтаксис, благодаря которому шаблоны легко читать и писать.

Что такое Lingo.dev CLI?

Lingo.dev CLI — это бесплатный open-source CLI для перевода приложений и контента с помощью ИИ. Он создан, чтобы заменить традиционные системы управления переводами и легко интегрируется в существующие пайплайны.

Чтобы узнать больше, смотрите Обзор.

О данном руководстве

В этом гайде рассказывается, как переводить Twig-шаблоны с помощью Lingo.dev CLI.

Вы узнаете, как:

  • Создать проект с нуля
  • Настроить пайплайн для перевода
  • Генерировать переводы с помощью ИИ

Необходимые условия

Для работы с Lingo.dev CLI убедитесь, что установлен Node.js v18+:

❯ node -v
v22.17.0

Шаг 1. Создайте проект

В директории вашего проекта создайте файл i18n.json:

{
  "$schema": "https://lingo.dev/schema/i18n.json",
  "version": "1.10",
  "locale": {
    "source": "en",
    "targets": ["es"]
  },
  "buckets": {}
}

Этот файл определяет поведение пайплайна перевода: какие языки использовать и где на файловой системе находится локализуемый контент.

Подробнее о доступных свойствах смотрите в i18n.json.

Шаг 2. Настройте исходную локаль

Исходная локаль — это оригинальный язык и регион, на которых был написан ваш контент. Чтобы настроить исходную локаль, укажите свойство locale.source в файле i18n.json:

{
  "$schema": "https://lingo.dev/schema/i18n.json",
  "version": "1.10",
  "locale": {
    "source": "en",
    "targets": ["es"]
  },
  "buckets": {}
}

Исходная локаль должна быть указана в формате BCP 47 language tag.

Полный список поддерживаемых кодов локалей в Lingo.dev CLI смотрите в разделе Поддерживаемые коды локалей.

Шаг 3. Настройка целевых локалей

Целевые локали — это языки и регионы, на которые вы хотите перевести свой контент. Чтобы настроить целевые локали, укажите свойство locale.targets в файле i18n.json:

{
  "$schema": "https://lingo.dev/schema/i18n.json",
  "version": "1.10",
  "locale": {
    "source": "en",
    "targets": ["es"]
  },
  "buckets": {}
}

Шаг 4. Создайте исходный контент

Если вы ещё не сделали этого, создайте один или несколько Twig-шаблонов, которые содержат переводимый контент. Эти файлы должны находиться по пути, в котором где-то встречается исходная локаль (например, в названии папки как en/ или в имени файла как template_en.html.twig).

В Twig-шаблонах к переводимому контенту относятся:

  • Текст внутри блочных элементов: h1-h6, p, div, li, blockquote, article, section и др.
  • Текст внутри строчных элементов: a, strong, em, span, code и др.
  • Значения атрибутов, включая:
    • атрибут content у тегов meta
    • атрибуты alt и title у тегов img
    • атрибуты placeholder, title и aria-label у тегов input, textarea, button, a, abbr и link

Как обрабатывается синтаксис Twig:

Синтаксис Twig полностью сохраняется при переводе:

  • Twig-переменные и выражения ({{ user.name }}, {{ product.price|number_format }}) остаются без изменений внутри переводимого текста
  • Twig-структуры управления ({% if %}, {% for %}, {% set %}) сохраняются и не переводятся
  • Twig-комментарии ({# internal note #}) сохраняются и не переводятся
  • Twig-фильтры ({{ "now"|date('Y') }}) продолжают работать в переведённых шаблонах

Как обрабатывается встроенный HTML:

Когда текст содержит встроенные HTML-элементы (например, <strong>, <span>, <em>, <a> и др.), весь текстовый блок переводится как единое целое. Это сохраняет контекст для лучшего качества перевода и поддерживает встроенное форматирование.

Например:

<p>Hello <strong>{{ user.name }}</strong>, welcome back!</p>

Весь абзац "Hello <strong>{{ user.name }}</strong>, welcome back!" переводится как единый блок, при этом сохраняются и теги <strong>, и переменная Twig. Это гарантирует, что AI-переводчик видит весь контекст, а также сохраняет и инлайн-форматирование, и синтаксис Twig в переводе.

Автоматический атрибут lang:

Атрибут lang у элемента <html> автоматически обновляется в соответствии с целевой локалью:

<!-- Source (en/template.html.twig) -->
<html>
<head>
    <title>Welcome</title>
</head>

<!-- Target (es/template.html.twig) -->
<html lang="es">
<head>
    <title>Bienvenido</title>
</head>

Непереводимый контент:

Содержимое внутри тегов <script> и <style> не переводится.

Шаг 5. Создайте bucket

  1. В файле i18n.json добавьте объект "twig" в объект buckets:

    {
      "$schema": "https://lingo.dev/schema/i18n.json",
      "version": "1.10",
      "locale": {
        "source": "en",
        "targets": ["es"]
      },
      "buckets": {
        "twig": {}
      }
    }
    
  2. В объекте "twig" определите массив из одного или нескольких паттернов include:

    {
      "$schema": "https://lingo.dev/schema/i18n.json",
      "version": "1.10",
      "locale": {
        "source": "en",
        "targets": ["es"]
      },
      "buckets": {
        "twig": {
          "include": ["./[locale]/*.twig"]
        }
      }
    }
    

    Эти паттерны определяют, какие файлы переводить.

    Сами паттерны:

    • должны содержать [locale] как плейсхолдер для выбранной локали
    • могут указывать на пути к файлам (например, "[locale]/template.html.twig")
    • могут использовать звёздочки как подстановочные знаки (например, "[locale]/*.twig")

    Рекурсивные glob-паттерны (например, **/*.twig) не поддерживаются.

Шаг 6. Настройте LLM

Lingo.dev CLI использует большие языковые модели (LLM) для AI-перевода контента. Чтобы использовать одну из этих моделей, вам нужен API-ключ от поддерживаемого провайдера.

Чтобы начать как можно быстрее, рекомендуем использовать Lingo.dev Engine:

  1. Зарегистрируйтесь в Lingo.dev.

  2. Выполните следующую команду:

    npx lingo.dev@latest login
    

    Откроется ваш браузер по умолчанию и появится запрос на аутентификацию.

  3. Следуйте подсказкам.

Шаг 7. Сгенерируйте переводы

В каталоге, где находится файл i18n.json, выполните следующую команду:

npx lingo.dev@latest run

Эта команда:

  1. Считывает файл i18n.json.
  2. Находит файлы, которые нужно перевести.
  3. Извлекает переводимый контент из файлов.
  4. Использует настроенную LLM для перевода извлечённого контента.
  5. Записывает переведённый контент обратно в файловую систему.

При первом создании переводов создаётся файл i18n.lock. Этот файл отслеживает, какой контент уже переведён, чтобы избежать лишних повторных переводов при следующих запусках.

Пример

en/example.html.twig

{% set user = app.user %}
<!DOCTYPE html>
<html>
<head>
    <meta name="description" content="Welcome to our application">
    <title>Welcome</title>
</head>
<body>
    <header>
        <nav>
            <a href="/" title="Go to homepage">Home</a>
            <a href="/about" title="Learn more about us">About</a>
            <a href="/contact" title="Get in touch">Contact</a>
        </nav>
    </header>

    <main>
        <section class="hero">
            <h1>Welcome to Our Platform</h1>
            <p>Hello <strong>{{ user.name }}</strong>, we're glad to have you here!</p>
            <p>Start exploring our features and discover what makes us <em>unique</em>.</p>
        </section>

        {% if user.isPremium %}
        <section class="premium-benefits">
            <h2>Premium Benefits</h2>
            <ul>
                <li>Unlimited access to all features</li>
                <li>Priority customer support</li>
                <li>Advanced analytics and reporting</li>
            </ul>
        </section>
        {% endif %}

        <section class="getting-started">
            <h2>Getting Started</h2>
            <p>Follow these simple steps to begin your journey:</p>
            <ol>
                <li>Complete your profile</li>
                <li>Explore the dashboard</li>
                <li>Invite your team members</li>
            </ol>

            <form action="/profile/update" method="post">
                <label for="bio">Tell us about yourself:</label>
                <textarea id="bio" name="bio" placeholder="Enter your bio here" aria-label="User biography"></textarea>

                <label for="email">Email address:</label>
                <input type="email" id="email" name="email" placeholder="[email protected]" aria-label="Email address">

                <button type="submit" title="Save your profile changes">Save Profile</button>
            </form>
        </section>

        {# This section is for internal notes and won't be displayed #}
        {% if app.debug %}
        <section class="debug-info">
            <h3>Debug Information</h3>
            <p>User ID: {{ user.id }}</p>
            <p>Last login: {{ user.lastLogin|date('Y-m-d H:i:s') }}</p>
        </section>
        {% endif %}
    </main>

    <footer>
        <p>Need help? <a href="/support" title="Visit our support center">Contact Support</a></p>
        <p>&copy; {{ "now"|date('Y') }} Our Company. All rights reserved.</p>
    </footer>
</body>
</html>

es/example.html.twig

{% set user = app.user %}
<!DOCTYPE html>
<html lang="es">
<head>
    <meta name="description" content="Bienvenido a nuestra aplicación">
    <title>Bienvenido</title>
</head>
<body>
    <header>
        <nav><a href="/" title="Ir a la página de inicio">Inicio</a>
            <a href="/about" title="Conoce más sobre nosotros">Acerca de</a>
            <a href="/contact" title="Ponte en contacto">Contacto</a></nav>
    </header>

    <main>
        <section class="hero">
            <h1>Bienvenido a nuestra plataforma</h1>
            <p>Hola <strong>{{ user.name }}</strong>, nos alegra tenerte aquí!</p>
            <p>Comienza a explorar nuestras funciones y descubre lo que nos hace <em>únicos</em>.</p>
        </section>

        {% if user.isPremium %}
        <section class="premium-benefits">
            <h2>Beneficios premium</h2>
            <ul>
                <li>Acceso ilimitado a todas las funciones</li>
                <li>Soporte prioritario al cliente</li>
                <li>Análisis y reportes avanzados</li>
            </ul>
        </section>
        {% endif %}

        <section class="getting-started">
            <h2>Primeros pasos</h2>
            <p>Sigue estos sencillos pasos para comenzar tu experiencia:</p>
            <ol>
                <li>Completa tu perfil</li>
                <li>Explora el panel de control</li>
                <li>Invita a los miembros de tu equipo</li>
            </ol>

            <form action="/profile/update" method="post"><label for="bio">Cuéntanos sobre ti:</label>
                <textarea id="bio" name="bio" placeholder="Ingresa tu biografía aquí" aria-label="Biografía del usuario"></textarea>

                <label for="email">Dirección de correo electrónico:</label>
                <input type="email" id="email" name="email" placeholder="[email protected]" aria-label="Dirección de correo electrónico">

                <button type="submit" title="Guardar los cambios de tu perfil">Guardar perfil</button></form>
        </section>

        {# This section is for internal notes and won't be displayed #}
        {% if app.debug %}
        <section class="debug-info">
            <h3>Información de depuración</h3>
            <p>ID de usuario: {{ user.id }}</p>
            <p>Último inicio de sesión: {{ user.lastLogin|date('Y-m-d H:i:s') }}</p>
        </section>
        {% endif %}
    </main>

    <footer>
        <p>¿Necesitas ayuda? <a href="/support" title="Visita nuestro centro de soporte">Contacta con soporte</a></p>
        <p>© {{ "now"|date('Y') }} Nuestra empresa. Todos los derechos reservados.</p>
    </footer>
</body>
</html>

i18n.json

{
  "$schema": "https://lingo.dev/schema/i18n.json",
  "version": "1.10",
  "locale": {
    "source": "en",
    "targets": ["es"]
  },
  "buckets": {
    "twig": {
      "include": ["./[locale]/*.twig"]
    }
  }
}

i18n.lock

version: 1
checksums:
  2d3d028d905803e471ca9f97a4969d5e:
    head/0#content: 1308168cca4fa5d8d7a0cf24e55e93fc
    head/1: 3180ad6b8de344b781637750259e0f53
    body/0/0: 9de5fe40cbf5f851a6d2270f01fe0739
    body/1/0/0: c59070fe496d5e4bd0066295b63a9056
    body/1/0/1: 12d74865332bf1988d51e84ba67aae09
    body/1/0/2: 58f0e438e665c77eedc440c5a8529b1a
    body/1/1/0: 119e3aa396d12a5a1aa7058e0983f9b9
    body/1/1/1/0: 60f9a22f4200bb4620a6ff7a1797ec30
    body/1/1/1/1: 03846a81f16f5e4a11acfd9445ad497d
    body/1/1/1/2: 15aae9d70ff1fb682f7d86baca81dcc0
    body/1/2/0: fbd403146395526d68ac68d142a50e21
    body/1/2/1: da8dc7fe06175d8b805f7f565bfe2788
    body/1/2/2/0: 061e1acc1b9ebad9de09fd5626e813c7
    body/1/2/2/1: 67f022a3f9e278d065a063b5e29dd932
    body/1/2/2/2: 7e23f048179f6661050edaa796528fe0
    body/1/2/3: 635f7e9a4afc00de34f975914afbb8b8
    body/1/3/0: 7a7892379e31868abba9865d20be2b72
    body/1/3/1: 8740df822561d74d51bb30e4b39d6193
    body/1/3/2: 0429f12258fabbde3abaca3dd9986178
    body/2/0: d32e57e4a5a65f3bee8b63dcb2bfa8e7
    body/2/1: 7e10a8ab9cc4e6d603b3cdc48849688f