Better I18NBetter I18N
Server SDK

Express & Fastify

Node.js HTTP adapter for Express, Fastify, and Koa

This adapter is Node.js-specific. It bridges Node.js IncomingHttpHeaders to Web Standards Headers. If you're using Cloudflare Workers, Deno native (Deno.serve()), or Bun's HTTP server, skip this adapter — their request.headers is already a Web Standards Headers object and works directly with i18n.detectLocaleFromHeaders(request.headers).

@better-i18n/server/node provides two utilities for Node.js HTTP servers:

  • betterI18nMiddleware(i18n) — drop-in Express/Connect middleware
  • fromNodeHeaders(nodeHeaders) — converts IncomingHttpHeaders to Web Standards Headers for manual use with Fastify, Koa, or raw Node.js

Express

Create the i18n singleton

src/i18n.ts
import { createServerI18n } from "@better-i18n/server";

export const i18n = createServerI18n({
  project: "my-org/api",
  defaultLocale: "en",
});

Register the middleware

src/app.ts
import express from "express";
import { betterI18nMiddleware } from "@better-i18n/server/node"; 
import { i18n } from "./i18n";

const app = express();

app.use(betterI18nMiddleware(i18n)); 

app.get("/users/:id", async (req, res) => {
  const user = await db.users.findById(req.params.id);

  if (!user) {
    return res.status(404).json({ error: req.t("errors.notFound") }); 
  }

  res.json({ user, locale: req.locale }); 
});

export default app;

Add TypeScript types

betterI18nMiddleware injects req.locale and req.t at runtime, but TypeScript doesn't know about them. Add a declaration file to augment the Express Request type:

src/types/express.d.ts
import type { Translator } from "@better-i18n/server";

declare global {
  namespace Express {
    interface Request {
      locale: string; 
      t: Translator; 
    }
  }
}

This follows the same pattern as @types/passport and other Express middleware packages — augmenting Express.Request in a global declaration file is the idiomatic TypeScript approach.

Fastify

Fastify doesn't use Express middleware, but fromNodeHeaders converts Fastify's req.headers (IncomingHttpHeaders) to a Web Standards Headers object that detectLocaleFromHeaders accepts:

src/plugins/i18n.ts
import fp from "fastify-plugin";
import { fromNodeHeaders } from "@better-i18n/server/node"; 
import { i18n } from "../i18n";

export default fp(async (fastify) => {
  fastify.decorateRequest("locale", "");
  fastify.decorateRequest("t", null);

  fastify.addHook("onRequest", async (request) => {
    const headers = fromNodeHeaders(request.headers); 
    const locale = await i18n.detectLocaleFromHeaders(headers); 
    const t = await i18n.getTranslator(locale); 

    request.locale = locale;
    request.t = t;
  });
});

Augment Fastify's request type:

src/types/fastify.d.ts
import type { Translator } from "@better-i18n/server";

declare module "fastify" {
  interface FastifyRequest {
    locale: string;
    t: Translator;
  }
}

On this page