Twig

Lingo.dev CLIを使用したTwigテンプレートのAI翻訳

Twigとは?

Twigは、PHPのための柔軟で高速かつ安全なテンプレートエンジンです。Symfonyアプリケーションやその他のPHPフレームワークで広く使用されており、プレゼンテーションロジックをアプリケーションロジックから分離します。Twigは、テンプレートを読みやすく書きやすくするクリーンな構文を使用しています。

Lingo.dev CLIとは?

Lingo.dev CLIは、AIを使用してアプリやコンテンツを翻訳するための無料のオープンソースCLIです。従来の翻訳管理ソフトウェアを置き換えるように設計されており、既存のパイプラインと統合できます。

詳細については、概要を参照してください。

このガイドについて

このガイドでは、Lingo.dev CLIを使用してTwigテンプレートを翻訳する方法を説明します。

以下の内容を学習します:

  • ゼロからプロジェクトを作成する
  • 翻訳パイプラインを設定する
  • AIで翻訳を生成する

前提条件

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. ソースロケールを設定する

_ソースロケール_は、コンテンツが書かれた元の言語と地域です。ソースロケールを設定するには、i18n.jsonファイルでlocale.sourceプロパティを設定します:

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

ソースロケールは、BCP 47言語タグとして指定する必要があります。

Lingo.dev CLIがサポートするロケールコードの完全なリストについては、サポートされているロケールコードを参照してください。

ステップ3. ターゲットロケールの設定

_ターゲットロケール_は、コンテンツを翻訳したい言語と地域です。ターゲットロケールを設定するには、i18n.jsonファイルのlocale.targetsプロパティを設定します。

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

ステップ4. ソースコンテンツの作成

まだ作成していない場合は、翻訳するコンテンツを含む1つ以上のTwigテンプレートファイルを作成します。これらのファイルは、パスのどこかにソースロケールを含むパスに配置する必要があります(例:en/のようなディレクトリ名、またはtemplate_en.html.twigのようなファイル名の一部として)。

Twigテンプレートの場合、翻訳可能なコンテンツには以下が含まれます。

  • ブロック要素内のテキストコンテンツ:h1-h6pdivliblockquotearticlesectionなど
  • インライン要素内のテキストコンテンツ:astrongemspancodeなど
  • 属性値(以下を含む):
    • metaタグのcontent属性
    • imgタグのaltおよびtitle属性
    • inputtextareabuttonaabbrlinkタグのplaceholdertitlearia-label属性

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>など)が含まれている場合、テキストブロック全体が1つの完全な単位として翻訳されます。これにより、より高品質な翻訳のためのコンテキストが保持され、インライン書式が維持されます。

例:

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

段落全体"Hello <strong>{{ user.name }}</strong>, welcome back!"は単一のブロックとして翻訳され、<strong>タグとTwig変数の両方がそのまま保持されます。これにより、AI翻訳エンジンが完全なコンテキストを持ち、インライン書式とTwig構文の両方が翻訳で保持されます。

自動lang属性:

<html>要素のlang属性は、ターゲットロケールに合わせて自動的に更新されます:

<!-- 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. バケットを作成する

  1. i18n.jsonファイルで、bucketsオブジェクトに"twig"オブジェクトを追加します:

    {
      "$schema": "https://lingo.dev/schema/i18n.json",
      "version": "1.10",
      "locale": {
        "source": "en",
        "targets": ["es"]
      },
      "buckets": {
        "twig": {}
      }
    }
    
  2. "twig"オブジェクトで、1つ以上の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