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.
languageis 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, notapiKey. PassingapiKeythrowsAzboxClient: '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.