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단계. 소스 콘텐츠 생성하기
아직 생성하지 않았다면, 번역할 콘텐츠가 포함된 하나 이상의 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> 등)가 포함되어 있는 경우, 전체 텍스트 블록이 하나의 완전한 단위로 번역됩니다. 이를 통해 더 나은 번역 품질을 위한 컨텍스트를 보존하고 인라인 서식을 유지합니다.
예시:
<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"객체에서 하나 이상의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 사용을 권장합니다:
-
Lingo.dev 계정에 가입합니다.
-
다음 명령을 실행합니다:
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