How do I set up CDN delivery?
The CDN delivers your translations globally with low latency. This guide covers the URL shape, the cache behaviour, and how the SDK fetches from it.
How CDN delivery works #
You publish translations
→ JSON files written to R2 origin storage
→ Edge cache purged for the affected paths
→ A user requests a translation file
→ The nearest edge serves it (X-Cache-Status: HIT)Translations are served as JSON from https://cdn.better-i18n.com. CDN delivery is on by default — your SDK already points at it, there is nothing to enable.
CDN URL format #
https://cdn.better-i18n.com/{org}/{project}/manifest.json
https://cdn.better-i18n.com/{org}/{project}/{locale}/{namespace}.json
https://cdn.better-i18n.com/{org}/{project}/{locale}/batch.jsonMost projects have a single file per locale, and it is called translations.json:
https://cdn.better-i18n.com/acme/dashboard/en/translations.json
https://cdn.better-i18n.com/acme/dashboard/tr/translations.jsonIf your project keeps its keys in namespaces, each namespace becomes its own file — en/common.json, en/dashboard.json — and batch.json returns several of them in one request, which is what the SDK uses to avoid a fetch per namespace.
The same paths work with the project UUID in place of {org}/{project}, which is what the SDK falls back to if a slug changes under it.
Verify a project by hand:
curl -i https://cdn.better-i18n.com/your-org/your-project/en/translations.jsonRenaming a project doesn't break live apps #
When a project slug changes, a redirect marker is written at the old path. Requests to the old CDN URL answer 301 to the new one for 30 days, so a deployed app keeps working until you ship the new slug. After that window the old path is gone — treat the 30 days as a migration budget, not a permanent alias.
Cache headers #
What the edge actually returns:
| Path | Cache-Control |
|---|---|
{locale}/{namespace}.json | public, max-age=3600 |
{locale}/batch.json | public, max-age=60, s-maxage=60 |
Every response carries X-Cache-Status: HIT or MISS.
There is no stale-while-revalidate on these responses, and that is deliberate rather than an omission: Cloudflare's Cache API does not hand back a stale entry once max-age has passed, so advertising SWR would describe a behaviour you would not get. Freshness comes from the purge on publish, not from a revalidation window.
SDK fetch behaviour #
In @better-i18n/next the two knobs are Next.js ISR revalidation windows, not an in-memory TTL:
| Option | Default (production) | Default (dev) |
|---|---|---|
messagesRevalidateSeconds | 5 | 0 |
manifestRevalidateSeconds | 3600 | 0 |
export const i18n = createI18n({
project: "acme/dashboard",
messagesRevalidateSeconds: 30,
});Zero in development is on purpose — you want an edit to show up on the next request while you are working, and you want caching in production.
Monitoring #
Per-project CDN metrics — request volume and cache hit rate — are on the project's overview page.
Platform-wide CDN status is at status.better-i18n.com, which you can subscribe to for outage notifications.
Serving from your own domain #
Not self-serve today. If a strict CSP means you cannot call cdn.better-i18n.com, talk to support rather than proxying it yourself — a proxy in front of the CDN usually ends up caching translations twice with two different lifetimes, which is worse than the problem it solves.
Better I18N