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-h6、p、div、li、blockquote、article、sectionなど - インライン要素内のテキストコンテンツ:
a、strong、em、span、codeなど - 属性値(以下を含む):
metaタグのcontent属性imgタグのaltおよびtitle属性input、textarea、button、a、abbr、linkタグのplaceholder、title、aria-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. バケットを作成する
-
i18n.jsonファイルで、bucketsオブジェクトに"twig"オブジェクトを追加します:{ "$schema": "https://lingo.dev/schema/i18n.json", "version": "1.10", "locale": { "source": "en", "targets": ["es"] }, "buckets": { "twig": {} } } -
"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の使用をお勧めします:
-
次のコマンドを実行します:
npx lingo.dev@latest loginこれによりデフォルトのブラウザが開き、認証を求められます。
-
プロンプトに従います。
ステップ7. 翻訳を生成する
i18n.jsonファイルを含むディレクトリで、次のコマンドを実行します:
npx lingo.dev@latest run
このコマンドは:
i18n.jsonファイルを読み込みます。- 翻訳が必要なファイルを検索します。
- ファイルから翻訳可能なコンテンツを抽出します。
- 設定されたLLMを使用して抽出されたコンテンツを翻訳します。
- 翻訳されたコンテンツをファイルシステムに書き戻します。
翻訳が初めて生成されるとき、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>© {{ "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