Handlebars

Локализуйте шаблоны Handlebars с помощью Lingo.dev CLI

Что такое Handlebars?

Handlebars — это популярный шаблонизатор, который даёт все возможности для эффективного создания семантических шаблонов. Он использует простой синтаксис и компилирует шаблоны в функции JavaScript, поэтому широко применяется для генерации HTML как на клиенте, так и на сервере.

Что такое Lingo.dev CLI?

Lingo.dev CLI — это бесплатный open-source CLI для перевода приложений и контента с помощью ИИ. Он создан, чтобы заменить традиционные системы управления переводами и легко интегрируется в существующие пайплайны.

Подробнее см. в разделе Обзор.

О данном гиде

В этом гайде рассказывается, как локализовать шаблоны Handlebars с помощью Lingo.dev CLI.

Вы узнаете, как:

  • Структурировать файлы переводов для проектов на Handlebars
  • Настроить пайплайн переводов
  • Генерировать переводы с помощью ИИ

Необходимые условия

Для работы с Lingo.dev CLI убедитесь, что установлен Node.js v18+:

❯ node -v
v22.17.0

Подход к локализации Handlebars

В шаблонах Handlebars лучше ссылаться на переводимый контент из JSON-файлов, а не вставлять текст напрямую. Такой подход даёт:

  • Чёткое разделение: структура шаблона и переводимый контент
  • Контроль версий: переводы хранятся в JSON-файлах
  • Без двусмысленности: явно указано, что подлежит переводу

Чтобы использовать переводы в шаблонах, понадобится вспомогательная функция для переводов. Часто используют такие варианты:

Простой кастомный helper:

{{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. Настройте исходную локаль

Исходная локаль — это оригинальный язык и регион, на которых был написан ваш контент. Чтобы настроить исходную локаль, укажите свойство locale.source в файле i18n.json:

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

Исходная локаль должна быть указана в виде языкового тега BCP 47.

Полный список кодов локалей, поддерживаемых Lingo.dev CLI, смотрите в разделе Поддерживаемые коды локалей.

Шаг 3. Настройте целевые локали

Целевые локали — это языки и регионы, на которые вы хотите перевести свой контент. Чтобы настроить целевые локали, укажите свойство locale.targets в файле i18n.json:

{
  "$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/ и переведённые файлы будут созданы автоматически Lingo.dev CLI при генерации переводов на шаге 8.

Пример 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 добавьте объект "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")

    Рекурсивные glob-шаблоны (например, **/*.json) не поддерживаются.

Шаг 6. Используйте переводы в шаблонах

Обращайтесь к ключам переводов в ваших Handlebars-шаблонах с помощью выбранного вами хелпера:

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>

То, как вы загружаете эти переводы и компилируете шаблоны, зависит от вашей сборки и выбранной библиотеки-хелпера.

Шаг 7. Настройте LLM

Lingo.dev CLI использует большие языковые модели (LLM) для перевода контента с помощью ИИ. Чтобы использовать одну из этих моделей, вам нужен 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