Skip to content
better-i18n.com

The symptom is specific and it points at one thing: your page content changed language, so routing and the locale cookie are working, but the t() strings did not — so the messages your components are reading were never refetched.

That is usually not a cache. It is which locale-switching mode you are in.

The two modes, and why it matters #

useSetLocale() behaves differently depending on whether BetterI18nProvider is above it in the tree:

What happens on switch
With BetterI18nProviderCookie is set, new messages are fetched on the client, the tree re-renders — instantly, no server round trip
Without it (standalone)Cookie is set, then router.refresh() or a navigation — the server produces the new strings

Both work. They fail differently, and the symptom in this article's title is what the second one looks like when the refresh does not actually re-render on the server.

Check first: is BetterI18nProvider wrapping the part of the tree whose strings are stale? If it is not, you are in standalone mode and every locale switch depends on the server producing a fresh render — see below.

If you are in standalone mode #

router.refresh() re-renders on the server, but it cannot get past a cached render. Anything that caches the rendered output will serve the old locale's HTML:

  • A statically rendered or ISR page
  • Your own CDN in front of Next.js
  • force-cache on a fetch in the render path

The fix is to make the page dynamic for the routes that show translated UI, or to bound its revalidation. If a page is cached for an hour, its strings are an hour old — Better i18n has no way to reach into it.

The fastest way to switch modes #

If instant switching is what you want, mount the provider:

TSX
// app/layout.tsx
import { BetterI18nProvider } from "@better-i18n/next/client";

export default async function RootLayout({ children }) {
  const locale = await getLocale();
  const messages = await getMessages();

  return (
    <BetterI18nProvider
      locale={locale}
      messages={messages}
      config={{ project: "acme/dashboard", defaultLocale: "en" }}
    >
      {children}
    </BetterI18nProvider>
  );
}

Now useSetLocale() fetches the new language's messages from the CDN in the browser and re-renders. No server, no cache in the way, and the cookie still persists the choice for the next request.

useSetLocale() writes two cookies: the locale, and a marker saying the visitor chose it. The middleware reads that marker so a deliberate choice is not overridden by Accept-Language on the next request.

If your switcher sets the locale cookie by hand instead of calling useSetLocale(), you get the classic version of this bug: the switch works, and one navigation later the browser's language wins and it silently switches back. Use the hook, or write both cookies.

When it really is stale translations #

If the language is right but the wording is old, that is a different problem — the strings are cached somewhere between the CDN and the screen:

Bash
curl -s https://cdn.better-i18n.com/your-org/your-project/manifest.json

Newer than your publish means your app is holding a copy. I published but my app still shows old translations works through the layers.

Checklist #

  1. Is BetterI18nProvider above the stale components?
  2. If not, is the page cached or statically rendered?
  3. Is your switcher calling useSetLocale(), or writing cookies itself?
  4. Does the CDN manifest actually have your latest publish?