Handlebars

使用 Lingo.dev CLI 本地化 Handlebars 模板

什么是 Handlebars?

Handlebars 是一种流行的模板引擎,能够高效地帮助你构建语义化模板。它采用简洁的语法,并将模板编译为 JavaScript 函数,因此被广泛应用于客户端和服务端的 HTML 生成。

什么是 Lingo.dev CLI?

Lingo.dev CLI 是一款免费的开源命令行工具,利用 AI 实现应用和内容的翻译。它旨在替代传统的翻译管理软件,并可集成到现有的开发流水线中。

如需了解更多,请参见 概述

关于本指南

本指南介绍如何使用 Lingo.dev CLI 对 Handlebars 模板进行本地化。

你将学习如何:

  • 为 Handlebars 项目组织翻译文件
  • 配置翻译流水线
  • 使用 AI 生成翻译内容

前置条件

要使用 Lingo.dev CLI,请确保已安装 Node.js v18 及以上版本:

❯ node -v
v22.17.0

Handlebars 的本地化方法

Handlebars 模板应通过 JSON 文件引用可翻译内容,而不是直接写入硬编码文本。这种方式带来以下优势:

  • 结构清晰:模板结构与可翻译内容分离
  • 版本可控:翻译内容通过 JSON 文件进行版本管理
  • 无歧义:明确指定哪些内容可翻译

要在模板中访问翻译内容,需要使用翻译辅助函数。常见选项包括:

简单自定义辅助函数:

{{t "product.title"}}
{{t "greeting" name="John"}}

handlebars-i18n - 功能丰富,支持格式化:

{{__ "product.title"}}
{{_date releaseDate}}
{{_price amount "USD"}}

npm | GitHub

handlebars-i18next - 另一种 i18next 集成方案:

{{t "product.title"}}

npm | GitHub

本指南在示例中使用 {{t}},但该工作流适用于任何 helper 选择。

步骤 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. 创建源内容

创建包含可翻译内容的 JSON 文件。这些文件应按照包含源语言区域代码的目录结构进行组织:

project/
├── locales/
│   └── en/
│       ├── common.json
│       └── store.json
├── templates/
│   └── product.handlebars
└── i18n.json

es/ 目录和翻译文件将在第 8 步通过 Lingo.dev CLI 自动生成。

示例 JSON 文件

locales/en/common.json:

{
  "navigation": {
    "home": "Home",
    "products": "Products",
    "about": "About",
    "contact": "Contact"
  },
  "footer": {
    "copyright": "All rights reserved",
    "privacy": "Privacy Policy",
    "terms": "Terms of Service"
  }
}

locales/en/store.json:

{
  "product": {
    "title": "Wireless Headphones",
    "description": "Premium sound quality with active noise cancellation",
    "price": "Price",
    "inStock": "In Stock",
    "outOfStock": "Out of Stock"
  },
  "cart": {
    "add": "Add to Cart"
  },
  "actions": {
    "buyNow": "Buy Now"
  }
}

步骤 5. 创建 bucket

  1. i18n.json 文件中,向 buckets 对象添加一个 "json" 对象:

    {
      "$schema": "https://lingo.dev/schema/i18n.json",
      "version": "1.10",
      "locale": {
        "source": "en",
        "targets": ["es"]
      },
      "buckets": {
        "json": {}
      }
    }
    
  2. "json" 对象中,定义一个包含一个或多个 include 模式的数组:

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

    这些模式用于定义需要翻译的文件。

    模式本身:

    • 必须包含 [locale] 作为已配置语言区域的占位符
    • 可以指向文件路径(例如,"locales/[locale]/common.json"
    • 可以使用星号作为通配符(例如,"locales/[locale]/*.json"

    不支持递归 glob 模式(例如,---CODE-PLACEHOLDER-7e78587627149123e88b8ccb34fe8035---)。

步骤 6. 在模板中使用翻译

在 Handlebars 模板中,使用您选择的 helper 引用翻译键:

templates/product.handlebars:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>{{t "product.title"}}</title>
</head>
<body>
  <nav>
    <a href="/">{{t "navigation.home"}}</a>
    <a href="/products">{{t "navigation.products"}}</a>
    <a href="/about">{{t "navigation.about"}}</a>
    <a href="/contact">{{t "navigation.contact"}}</a>
  </nav>

  <main>
    <article>
      <h1>{{t "product.title"}}</h1>
      <p>{{t "product.description"}}</p>

      <div class="price">
        <span>{{t "product.price"}}:</span>
        <span>$299.99</span>
      </div>

      <div class="stock">
        {{#if inStock}}
          <span class="available">{{t "product.inStock"}}</span>
        {{else}}
          <span class="unavailable">{{t "product.outOfStock"}}</span>
        {{/if}}
      </div>

      <div class="actions">
        <button class="primary">{{t "cart.add"}}</button>
        <button class="secondary">{{t "actions.buyNow"}}</button>
      </div>
    </article>
  </main>

  <footer>
    <p>{{t "footer.copyright"}}</p>
    <a href="/privacy">{{t "footer.privacy"}}</a>
    <a href="/terms">{{t "footer.terms"}}</a>
  </footer>
</body>
</html>

如何加载这些翻译并编译模板,取决于您的构建设置和所选 helper 库。

步骤 7. 配置 LLM

Lingo.dev CLI 使用大型语言模型(LLM)通过 AI 翻译内容。要使用这些模型之一,您需要从受支持的服务商获取 API 密钥。

为尽快开始使用,我们推荐 Lingo.dev Engine

  1. 注册 Lingo.dev 账号

  2. 运行以下命令:

    npx lingo.dev@latest login
    

    这将打开您的默认浏览器,并要求您进行身份验证。

  3. 按照提示操作。

步骤 8. 生成翻译内容

在包含 i18n.json 文件的目录下,运行以下命令:

npx lingo.dev@latest run

该命令将:

  1. 读取 i18n.json 文件。
  2. 查找需要翻译的 JSON 文件。
  3. 提取文件中的可翻译内容。
  4. 使用已配置的 LLM 翻译提取的内容。
  5. 将翻译后的内容写回文件系统。

首次生成翻译时,会创建一个 i18n.lock 文件。该文件用于记录已翻译的内容,从而在后续运行时避免不必要的重复翻译。

示例

项目结构

handlebars-localization/
├── locales/
│   ├── en/
│   │   ├── common.json
│   │   └── store.json
│   └── es/
│       ├── common.json
│       └── store.json
├── templates/
│   └── product.handlebars
└── i18n.json

locales/en/common.json

{
  "navigation": {
    "home": "Home",
    "products": "Products",
    "about": "About",
    "contact": "Contact"
  },
  "footer": {
    "copyright": "All rights reserved",
    "privacy": "Privacy Policy",
    "terms": "Terms of Service"
  }
}

locales/en/store.json

{
  "product": {
    "title": "Wireless Headphones",
    "description": "Premium sound quality with active noise cancellation",
    "price": "Price",
    "inStock": "In Stock",
    "outOfStock": "Out of Stock"
  },
  "cart": {
    "add": "Add to Cart"
  },
  "actions": {
    "buyNow": "Buy Now"
  }
}

locales/es/common.json

{
  "navigation": {
    "home": "Inicio",
    "products": "Productos",
    "about": "Acerca de",
    "contact": "Contacto"
  },
  "footer": {
    "copyright": "Todos los derechos reservados",
    "privacy": "Política de privacidad",
    "terms": "Términos de servicio"
  }
}

locales/es/store.json

{
  "product": {
    "title": "Auriculares inalámbricos",
    "description": "Calidad de sonido premium con cancelación activa de ruido",
    "price": "Precio",
    "inStock": "En stock",
    "outOfStock": "Agotado"
  },
  "cart": {
    "add": "Añadir al carrito"
  },
  "actions": {
    "buyNow": "Comprar ahora"
  }
}

templates/product.handlebars

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>{{t "product.title"}}</title>
</head>
<body>
  <nav>
    <a href="/">{{t "navigation.home"}}</a>
    <a href="/products">{{t "navigation.products"}}</a>
    <a href="/about">{{t "navigation.about"}}</a>
    <a href="/contact">{{t "navigation.contact"}}</a>
  </nav>

  <main>
    <article>
      <h1>{{t "product.title"}}</h1>
      <p>{{t "product.description"}}</p>

      <div class="price">
        <span>{{t "product.price"}}:</span>
        <span>$299.99</span>
      </div>

      <div class="stock">
        {{#if inStock}}
          <span class="available">{{t "product.inStock"}}</span>
        {{else}}
          <span class="unavailable">{{t "product.outOfStock"}}</span>
        {{/if}}
      </div>

      <div class="actions">
        <button class="primary">{{t "cart.add"}}</button>
        <button class="secondary">{{t "actions.buyNow"}}</button>
      </div>
    </article>
  </main>

  <footer>
    <p>{{t "footer.copyright"}}</p>
    <a href="/privacy">{{t "footer.privacy"}}</a>
    <a href="/terms">{{t "footer.terms"}}</a>
  </footer>
</body>
</html>

i18n.json

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

i18n.lock

version: 1
checksums:
  8a4f2c9e1d6b3a7f5e8c2d1b9a3f6e4c:
    navigation.home: 7b2e4f9a1c8d3b6f5e2a9d1c8b4f7e3a
    navigation.products: 3f8e2a9d1c6b4f7e5a2c9d1b8f4e7a3b
    navigation.about: 9d1c8b4f7e3a2f9e1d6b3a7f5e8c2d1b
    navigation.contact: 4f7e3a2c9d1b8f6e5a3f2d9c1b7e4a8f
    footer.copyright: 2c9d1b8f4e7a3b6f5e2a9d1c8b4f7e3a
    footer.privacy: 8b4f7e3a2c9d1b6f5e2a9d1c8f4e7a3b
    footer.terms: 6f5e2a9d1c8b4f7e3a2c9d1b8f4e7a3b
  3b6f5e2a9d1c8b4f7e3a2c9d1b8f4e7a:
    product.title: 1c8b4f7e3a2c9d1b6f5e2a9d1c8f4e7a
    product.description: 7e3a2c9d1b6f5e2a9d1c8b4f7e3a2c9d
    product.price: 9d1b6f5e2a9d1c8b4f7e3a2c9d1b8f4e
    product.inStock: 4f7e3a2c9d1b6f5e2a9d1c8b4f7e3a2c
    product.outOfStock: 2c9d1b6f5e2a9d1c8b4f7e3a2c9d1b8f
    cart.add: 8b4f7e3a2c9d1b6f5e2a9d1c8b4f7e3a
    actions.buyNow: 7e3a2c9d1b6f5e2a9d1c8b4f7e3a2c9d