Skip to content

How to Add Translations to Your Node.js Application with Azbox: Complete Guide

Learn how to integrate translations into your Node.js application using the azbox-node package. This comprehensive guide covers API integration, translation management, caching, error handli

  • date icon

    January 20, 2025

  • 03 min read
How to Add Translations to Your Node.js Application with Azbox: Complete Guide

Azbox publishes a small Node.js client, azbox-node. This guide covers what it actually does, which is less than you might expect, and how to build a working translation layer on top of it.

What the client does, and what it does not

The published package exposes exactly one class with one method:

export declare class AzboxClient {
  constructor(options: { token: string; projectId: string; language: string; baseUrl?: string });
  getKeywords(options?: { afterUpdatedAt?: Date }): Promise<AzboxKeyword[]>;
}

That is the whole API surface. In particular:

  • It only reads. There is no method to push or upload strings. Keywords are created in the Azbox panel, either one by one with Add Keyword or by importing a file (ARB, JSON/i18next, .xcstrings, XML, XLSX, CSV, YAML or PHP). See the Azbox quickstart.
  • It fetches one language at a time. language is set on the client, not per call. For several languages, create one client per language.
  • There is no translate() helper. You get the full keyword list and do the lookup yourself. That is what most of this guide is about.

The credential is passed as token, not apiKey. Passing apiKey throws AzboxClient: 'token' is required.

Prerequisites

  • Node.js 18 or newer — the client uses the global fetch
  • An Azbox project, with its Project ID and API key
  • Some keywords already added in the panel

Step 1: Install

npm install azbox-node

Step 2: Fetch the keywords

import { AzboxClient } from "azbox-node";

const client = new AzboxClient({
  token: process.env.AZBOX_API_KEY,
  projectId: process.env.AZBOX_PROJECT_ID,
  language: "ES",
});

const keywords = await client.getKeywords();
// [{ id: "home.title", data: { translation: "Bienvenido", context, reference, ... } }]

Under the hood this is a single call to GET https://api.azbox.io/v1/projects/:projectId/keywords?token=…&language=….

Step 3: Turn the list into a lookup

getKeywords() returns an array. Calling it per string would mean one HTTP request per string, so fetch once at startup and build a map:

// i18n.ts
import { AzboxClient } from "azbox-node";

type Dictionary = Map<string, string>;
const dictionaries = new Map<string, Dictionary>();

export async function loadLanguage(language: string): Promise<Dictionary> {
  const client = new AzboxClient({
    token: process.env.AZBOX_API_KEY!,
    projectId: process.env.AZBOX_PROJECT_ID!,
    language,
  });

  const keywords = await client.getKeywords();
  const dict: Dictionary = new Map(
    keywords
      .filter((kw) => typeof kw.data.translation === "string")
      .map((kw) => [kw.id, kw.data.translation as string]),
  );

  dictionaries.set(language, dict);
  return dict;
}

export function t(language: string, key: string, params?: Record<string, string | number>) {
  const value = dictionaries.get(language)?.get(key);
  if (value === undefined) return key; // fall back to the key, never to an empty string
  if (!params) return value;
  return value.replace(/\{(\w+)\}/g, (m, p) => String(params[p] ?? m));
}

Interpolation is your responsibility: the API returns the string as stored, placeholders included.

Step 4: Load at startup, not per request

const LANGUAGES = ["EN", "ES", "FR"];

await Promise.all(LANGUAGES.map(loadLanguage));

app.listen(3000);

If a language fails to load, decide deliberately whether to start anyway with a fallback or to fail loudly. Starting with a half-empty dictionary and no log is how you end up shipping raw keys to users.

Step 5: Refresh only what changed

getKeywords() accepts afterUpdatedAt, which maps to the afterUpdatedAtStr query parameter. Use it to poll for changes without refetching everything:

const lastSync = new Map<string, Date>();

export async function refresh(language: string) {
  const since = lastSync.get(language);
  const client = new AzboxClient({
    token: process.env.AZBOX_API_KEY!,
    projectId: process.env.AZBOX_PROJECT_ID!,
    language,
  });

  const changed = await client.getKeywords(since ? { afterUpdatedAt: since } : {});
  const dict = dictionaries.get(language) ?? new Map();

  for (const kw of changed) {
    if (typeof kw.data.translation === "string") dict.set(kw.id, kw.data.translation);
  }

  dictionaries.set(language, dict);
  lastSync.set(language, new Date());
  return changed.length;
}

This is what makes over-the-air updates work on the server: correct a string in the panel, and the next refresh picks it up without a deploy.

Step 6: Express middleware

import express from "express";
import { t } from "./i18n";

const app = express();

app.use((req, res, next) => {
  const header = req.headers["accept-language"] ?? "";
  const lang = String(header).slice(0, 2).toUpperCase();
  req.language = ["EN", "ES", "FR"].includes(lang) ? lang : "EN";
  next();
});

app.get("/api/welcome", (req, res) => {
  res.json({ message: t(req.language, "home.title", { name: "Ada" }) });
});

Error handling

getKeywords() throws on a non-2xx response and on an unexpected payload. Treat a failed refresh as non-fatal — you already have the previous dictionary in memory:

setInterval(() => {
  refresh("ES").catch((err) => console.error("[i18n] refresh failed:", err.message));
}, 5 * 60 * 1000);

Where the strings come from

Worth repeating, because it is the part people expect to automate and cannot: keywords are created in the panel, not from code. The usual flow is to import your existing en.json (or ARB, or XML) once, translate in Azbox, and let your application pull the results with the client above.

If you need programmatic creation, the client will not do it — you would be calling the Azbox HTTP API directly.

Blog

Latest Posts

Discover our latest articles and updates.

Gaming Localization Services: The 2026 Guide to Taking Your Game Global
date icon

February 21, 2026

04 min read

Gaming Localization Services: The 2026 Guide to Taking Your Game Global

Going global with your game isn’t just about translating text—it’s about making players in every market feel like the ga

Read More
App Localization Software: The 2026 Guide to Choosing and Using the Right Tool
date icon

February 20, 2026

04 min read

App Localization Software: The 2026 Guide to Choosing and Using the Right Tool

If you're scaling your app beyond one language, app localization software is the lever that turns "we should go glob

Read More
Translation for SaaS: Complete Guide to Localizing Your Product
date icon

February 15, 2026

04 min read

Translation for SaaS: Complete Guide to Localizing Your Product

Translation for SaaS is no longer optional. If you want to grow beyond your home market, you need a clear strategy t

Read More
Call to action background

Start Global Growth Today

Upload your language files, get them translated, and keep every locale in sync as your product keeps changing.

Get Started - It's Free