500K characters included every month. Add payment details to start.
Next.js Developer Guide

Best Translation API
for Next.js

Compare Google Translate, DeepL, and Langbly for the App Router. Route Handlers, Server Components, build-time locale files, caching, and pricing.

Payment details required. No charge within 500K characters/month.

Pricing comparison

Real pricing based on monthly character volume. All prices in USD.

VolumeGoogle TranslateDeepLLangblyYou Save
500K chars/mo$0Check live plan$0
Both included/evaluation
1M chars/mo$10Check live plan$2.50
75% vs Google
5M chars/mo$90Check live plan$22.50
75% vs Google
25M chars/mo$490Check live plan$122.50
75% vs Google
100M chars/mo$1,990Above Growth limit$497.50
75% vs Google

Feature comparison

FeatureGoogle TranslateDeepLLangbly
npm package@google-cloud/translatedeepl-nodelangbly
Auth methodService account JSONAPI key stringAPI key string
Works with plain fetch()REST only, no SDKREST only, no SDK
Official SDK runtimeNode runtimeNode runtimeNode runtime
Route Handler friendly
Server Component friendlyNode runtime onlyNode runtime only
Secrets stay server-side
Build-time locale generationCustom scriptCustom scriptCustom script
Drop-in for existing v2 code
HTML format support
TypeScript types@types includedBuilt-in typesWritten in TypeScript
Auto-retry (429/5xx)ManualManualBuilt-in exponential backoff
Language coverage100+ languages30+ languages100+ languages
Published usage rate$20/1M after 500KCheck live Growth plan$5/1M after 500K
Included/evaluation usage500K/moDeveloper: 1M total500K/mo
Context-awareLimited

Keep the API key server-side: a Route Handler

Rule one for translation in Next.js: the API key never leaves the server. Anything under app/ that is marked "use client" ships to the browser, and so does every NEXT_PUBLIC_ variable. A Route Handler gives you a small, typed proxy that your client components can call safely.

Langbly speaks the Google Translate v2 request and response format, so a single fetch is all you need:

// app/api/translate/route.ts
import { NextResponse } from "next/server";

type TranslateBody = {
  q: string;
  target: string;
  source?: string;
};

export async function POST(request: Request) {
  const { q, target, source } = (await request.json()) as TranslateBody;

  if (!q || !target) {
    return NextResponse.json(
      { error: "Both q and target are required" },
      { status: 400 }
    );
  }

  const response = await fetch(
    "https://api.langbly.com/language/translate/v2",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: "Bearer " + process.env.LANGBLY_API_KEY,
      },
      body: JSON.stringify({ q, target, source, format: "text" }),
    }
  );

  if (!response.ok) {
    return NextResponse.json(
      { error: "Translation failed" },
      { status: response.status }
    );
  }

  const json = await response.json();
  const [translation] = json.data.translations;

  return NextResponse.json({
    text: translation.translatedText,
    detectedSource: translation.detectedSourceLanguage,
  });
}

The response body comes back as {"data":{"translations":[{"translatedText":"Hallo wereld","detectedSourceLanguage":"en"}]}}, the exact shape the Google v2 API returns. If you already have a Next.js app wired to Google Translate, you swap the base URL and the auth header and the rest of your code keeps working.

Prefer a typed client over raw fetch? The official SDK is on npm as langbly, written in TypeScript, with auto-retry and typed error classes.

// lib/langbly.ts
import { Langbly } from "langbly";

export const langbly = new Langbly({
  apiKey: process.env.LANGBLY_API_KEY!,
});

Google's client is the awkward one here. It authenticates with a service account JSON file, which means either a file on disk your deploy target may not have, or a base64 environment variable you decode at boot. DeepL and Langbly both take a plain key string, which is one line in your hosting dashboard.

Build time vs request time: where should translation run?

Most Next.js projects have two kinds of text, and they want different treatment.

Static UI copy (nav labels, buttons, marketing sections) does not change per request. Translate it once during the build, write JSON locale files, and let next-intl or your own dictionary read them. Zero API calls at runtime, zero latency, and the cost is a one-off.

// scripts/build-locales.ts
// Run with: tsx scripts/build-locales.ts
import { writeFile } from "node:fs/promises";
import { Langbly } from "langbly";
import en from "../messages/en.json";

const client = new Langbly({ apiKey: process.env.LANGBLY_API_KEY! });
const locales = ["nl", "de", "fr", "es", "ja"];
const source = en as Record<string, string>;

for (const locale of locales) {
  const entries = Object.entries(source);
  const translated = await Promise.all(
    entries.map(async ([key, value]) => {
      const result = await client.translate(value, { target: locale });
      return [key, result.translatedText] as const;
    })
  );

  await writeFile(
    `./messages/${locale}.json`,
    JSON.stringify(Object.fromEntries(translated), null, 2)
  );
}

Dynamic content (user posts, product descriptions, support replies) has to be translated at request time. Do it inside a Server Component so the key stays on the server and the browser gets finished HTML, with no loading spinner and no client bundle growth.

// app/[locale]/posts/[id]/page.tsx
import { translate } from "@/lib/translate";
import { getPost } from "@/lib/posts";

export default async function PostPage({
  params,
}: {
  params: Promise<{ locale: string; id: string }>;
}) {
  const { locale, id } = await params;
  const post = await getPost(id);

  const [title, body] = await Promise.all([
    translate(post.title, locale),
    translate(post.body, locale, "html"),
  ]);

  return (
    <article>
      <h1>{title}</h1>
      <div dangerouslySetInnerHTML={{ __html: body }} />
    </article>
  );
}

Note the format option in that second call. Setting it to "html" keeps your tags and attributes intact instead of translating the markup itself, which matters for rich text out of a CMS.

For i18n routing, the App Router pattern is a [locale] segment plus middleware that redirects a bare path to the visitor's preferred locale. Middleware runs on every request, so keep it to a header check and a redirect. Never call a translation API from middleware.

Cache aggressively, or pay twice for the same sentence

This is the mistake that turns a $5 bill into a $200 bill. Translation output is deterministic enough to cache, and the same product title will be requested thousands of times. There is no reason to pay for it more than once.

One catch specific to Next.js: the Data Cache only covers GET fetches. Translation calls are POSTs, so next: { revalidate } on the fetch does nothing. Wrap the call in unstable_cache instead and key it on the text plus the target locale.

// lib/translate.ts
import { unstable_cache } from "next/cache";

const ONE_MONTH = 60 * 60 * 24 * 30;

async function fetchTranslation(
  text: string,
  target: string,
  format: "text" | "html"
) {
  const response = await fetch(
    "https://api.langbly.com/language/translate/v2",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: "Bearer " + process.env.LANGBLY_API_KEY,
      },
      body: JSON.stringify({ q: text, target, format }),
      cache: "no-store",
    }
  );

  if (!response.ok) {
    throw new Error("Translation failed: " + response.status);
  }

  const json = await response.json();
  return json.data.translations[0].translatedText as string;
}

export function translate(
  text: string,
  target: string,
  format: "text" | "html" = "text"
) {
  return unstable_cache(
    () => fetchTranslation(text, target, format),
    ["translate", target, format, text],
    { revalidate: ONE_MONTH, tags: ["translations"] }
  )();
}

Three habits worth building on top of this:

  • Skip the call entirely when the source locale and the target locale match. It sounds obvious and it is missed constantly.
  • Batch related strings into one request rather than firing one request per string. Fewer round trips, less rate-limit pressure.
  • Store translations you intend to keep in your own database, and use the API only for cache misses. Then call revalidateTag("translations") when the source content changes.

Frequently asked questions

What is the best translation API for Next.js?

Langbly fits the App Router particularly well: it is a single JSON endpoint you can call with plain fetch from a Route Handler or a Server Component, it uses a simple API key instead of a service account file, and it costs $5 per million characters. DeepL is a good option if you only need European languages. Google Translate has broad coverage but the heaviest setup.

How do I add i18n translation to a Next.js App Router app?

Add a [locale] dynamic segment under app/, use middleware to redirect bare paths to the visitor preferred locale, and read static copy from JSON message files. Generate those files at build time with a script that calls a translation API, and translate dynamic content inside Server Components at request time. Keep the API key in a server-only environment variable, never in a NEXT_PUBLIC_ one.

Should I translate in a Server Component or a Route Handler?

Use a Server Component when the page itself renders the translated text: the key stays on the server and the browser receives finished HTML with no extra round trip. Use a Route Handler when a client component needs to trigger a translation after the page loads, such as a "translate this comment" button. Both keep the key server-side, which client-side fetching does not.

How do I avoid paying to translate the same string twice in Next.js?

Cache on the text plus the target locale. Next.js only caches GET fetches in its Data Cache, and translation calls are POSTs, so wrap the call in unstable_cache with a long revalidate window and a translations tag. Persist translations you plan to reuse in your own database, and translate static UI copy once at build time into JSON locale files instead of on every request.

Can I switch a Next.js app from Google Translate to Langbly without rewriting code?

Yes. Langbly is a drop-in replacement for the Google Translate v2 API. Point your requests at https://api.langbly.com/language/translate/v2, send an Authorization: Bearer header with your key, and keep the same JSON body with q and target. The response comes back in the identical shape, so your parsing code and any i18n library built on the v2 format keeps working unchanged.

Ready to try Langbly?

500K characters included every month. Migrate in minutes.

Same API as Google Translate v2. Just change the URL.