Handlebars

Localize Handlebars templates with Lingo.dev CLI

What is Handlebars?

Handlebars is a popular templating engine that provides the power necessary to let you build semantic templates effectively. It uses a clean syntax and compiles templates into JavaScript functions, making it widely used for generating HTML in both client-side and server-side applications.

What is Lingo.dev CLI?

Lingo.dev CLI is a free, open-source CLI for translating apps and content with AI. It's designed to replace traditional translation management software while integrating with existing pipelines.

To learn more, see Overview.

About this guide

This guide explains how to localize Handlebars templates using Lingo.dev CLI.

You'll learn how to:

  • Structure translation files for Handlebars projects
  • Configure a translation pipeline
  • Generate translations with AI

Prerequisites

To use Lingo.dev CLI, ensure that Node.js v18+ is installed:

❯ node -v
v22.17.0

The Handlebars localization approach

Handlebars templates should reference translatable content from JSON files rather than containing hard-coded text. This approach provides:

  • Clear separation: Template structure vs translatable content
  • Version control: Translations tracked in JSON files
  • No ambiguity: Explicitly define what's translatable

To access translations in templates, you'll need a translation helper function. Common options include:

Simple custom helper:

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

handlebars-i18n - Feature-rich with formatting:

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

npm | GitHub

handlebars-i18next - Alternative i18next integration:

{{t "product.title"}}

npm | GitHub

This guide uses {{t}} in examples, but the workflow applies to any helper choice.

Step 1. Set up a project

In your project's directory, create an i18n.json file:

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

This file defines the behavior of the translation pipeline, including what languages to translate between and where the localizable content exists on the file system.

To learn more about the available properties, see i18n.json.

Step 2. Configure the source locale

The source locale is the original language and region that your content was written in. To configure the source locale, set the locale.source property in the i18n.json file:

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

The source locale must be provided as a BCP 47 language tag.

For the complete list of the locale codes that Lingo.dev CLI supports, see Supported locale codes.

Step 3. Configure the target locales

The target locales are the languages and regions you want to translate your content into. To configure the target locales, set the locale.targets property in the i18n.json file:

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

Step 4. Create the source content

Create JSON files containing your translatable content. These files should be organized in a directory structure that includes the source locale code:

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

The es/ directory and translated files will be created automatically by Lingo.dev CLI when you generate translations in Step 8.

Example JSON files

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"
  }
}

Step 5. Create a bucket

  1. In the i18n.json file, add a "json" object to the buckets object:

    {
      "$schema": "https://lingo.dev/schema/i18n.json",
      "version": "1.10",
      "locale": {
        "source": "en",
        "targets": ["es"]
      },
      "buckets": {
        "json": {}
      }
    }
    
  2. In the "json" object, define an array of one or more include patterns:

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

    These patterns define which files to translate.

    The patterns themselves:

    • must contain [locale] as a placeholder for the configured locale
    • can point to file paths (e.g., "locales/[locale]/common.json")
    • can use asterisks as wildcard placeholders (e.g., "locales/[locale]/*.json")

    Recursive glob patterns (e.g., **/*.json) are not supported.

Step 6. Use translations in templates

Reference translation keys in your Handlebars templates using your chosen 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>

How you load these translations and compile your templates depends on your build setup and chosen helper library.

Step 7. Configure an LLM

Lingo.dev CLI uses large language models (LLMs) to translate content with AI. To use one of these models, you need an API key from a supported provider.

To get up and running as quickly as possible, we recommend using Lingo.dev Engine:

  1. Sign up for a Lingo.dev account.

  2. Run the following command:

    npx lingo.dev@latest login
    

    This will open your default browser and ask you to authenticate.

  3. Follow the prompts.

Step 8. Generate the translations

In the directory that contains the i18n.json file, run the following command:

npx lingo.dev@latest run

This command:

  1. Reads the i18n.json file.
  2. Finds the JSON files that need to be translated.
  3. Extracts the translatable content from the files.
  4. Uses the configured LLM to translate the extracted content.
  5. Writes the translated content back to the file system.

The first time translations are generated, an i18n.lock file is created. This file keeps track of what content has been translated, preventing unnecessary retranslations on subsequent runs.

Example

Project structure

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