I published but my app still shows old translations
Give it a minute first. The CDN caches translations for 60 seconds, and publishing purges what changed — so most reports of this turn out to be someone checking within twenty seconds.
If it is still old after a minute, one command tells you which side to look at.
Which side is stale #
curl -s https://cdn.better-i18n.com/your-org/your-project/manifest.jsonCompare files[<lang>].lastModified with the time you published.
Older than your publish → the publish did not reach the CDN. Go to "The publish did not land".
Newer → the CDN has your new strings and your app is holding an old copy. Go to "Your app is holding a copy".
The manifest is the right thing to check because it is the same file every SDK uses to decide whether to refetch. If it is current and your app disagrees, the disagreement is downstream.
The publish did not land #
Nothing was pending. better-i18n publish:status lists what would go live. Empty means your edit was never staged for publishing — check you saved it, and that you are in the project you think you are.
The language is a draft. Draft languages are excluded from publishing on purpose. The Languages page shows the status.
Wrong project. Publishing from acme/dashboard while your app reads acme/website produces exactly this symptom, with no error anywhere. The identifier in the dashboard and the one in your config have to match character for character.
Your app is holding a copy #
Work down the list — each layer has its own clock.
The SDK's own refresh interval. In production the Next.js SDK refetches messages every 5 seconds by default and the manifest every hour; in development both are 0, meaning every request. Raising messagesRevalidateSeconds for fewer requests also lengthens this window:
export default createI18n({
project: "your-org/your-project",
messagesRevalidateSeconds: 5,
});Rendered output, not translations. A statically rendered or ISR page contains the strings from when it was built. New translations cannot change an HTML file that is already generated — the page has to be regenerated. export const revalidate = 30 bounds it; a rebuild ends it now.
Make publishing trigger the rebuild. Instead of guessing at intervals, have Better i18n tell your app when to revalidate:
// app/api/i18n/revalidate/route.ts
import { createRevalidateHandler } from "@better-i18n/next";
export const POST = createRevalidateHandler({
secret: process.env.BETTER_I18N_WEBHOOK_SECRET!,
revalidatePaths: ["/"],
});Point a webhook at it and a publish revalidates the pages you name, signature-verified. This is the only arrangement where "published" and "visible" are the same moment. See How do I set up webhooks?.
A CDN or proxy in front of your app. Cloudflare, Fastly, Vercel's edge cache — they cache your rendered pages, and none of them know you published anything. Purge there too, or use the webhook above.
The browser. Hard refresh (Cmd+Shift+R / Ctrl+Shift+R) before concluding anything, and check in a private window. A service worker will happily serve last week's bundle.
Narrowing it in one pass #
# Is the CDN current?
curl -s https://cdn.better-i18n.com/your-org/your-project/manifest.json
# Is the string actually there?
curl -s https://cdn.better-i18n.com/your-org/your-project/en/translations.json | grep -i "your string"
# Is your app's response cached, and by whom?
curl -I https://your-app.com/the-pageIf the CDN has the string and your page does not, the answer is in the third command's headers — age, x-vercel-cache, cf-cache-status, or an etag that has not moved.
Related #
- My translations aren't showing: what do I check? — when they never appeared at all
- Locale switching updates content but not UI translations (SSR)
- How do I set up webhooks?
Better I18N