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}},不过这个工作流程适用于任意助手的选择。

步骤 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

当你在第 8 步生成翻译时,Lingo.dev CLI 会自动创建 es/ 目录及其翻译文件。

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. 创建存储桶

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

    {
      "$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"

    不支持递归的全局模式(如:**/*.json)。

步骤 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