On a Centra fashion storefront, Klaviyo sends transactional mail and next-intl holds the copy dictionary. The market that paid the bills spoke Dutch. If a receipt arrived in English, the shopper did not file a ticket. They forwarded it to support and asked if they ordered from the wrong site.
Order confirmation in en, password reset in en, shipping update that says "Your order" to a profile whose Centra language is nl-NL. Klaviyo still sent. The metric still fired. The copy was just from the wrong room.
Klaviyo sends the string I gave it
I treated Klaviyo like it would pick Dutch because the profile had a Dutch email and a Dutch shipping address. Klaviyo sends whatever string I put on the event or in the template. It does not run next-intl. It does not read my Next.js request locale. A flow triggered off Placed Order will happily render the default template language.
The storefront already had next-intl wired for nl-NL. Product pages were fine. Transactional copy lived in a second pile: hardcoded English in the Klaviyo template, or a server helper that called getTranslations() without a locale and inherited the default.
That default was English. A webhook does not inherit the shopper's browser. A queue worker does not inherit the PDP request. I kept debugging the template language in Klaviyo while the event payload had already left in English.
Resolve Dutch before the event leaves
Transactional HTML that I still render on the server goes through next-intl with an explicit locale. Not "whatever the current request is." A Klaviyo event can be sent from a webhook, a queue, a Centra plugin callback. There is no incoming Accept-Language. If I skip nl-NL, I get the default.
const locale = profile.language === 'nl' ? 'nl-NL' : 'en';
const t = await getTranslations({ locale, namespace: 'Transactional' });
await klaviyo.events.create({
data: {
type: 'event',
attributes: {
metric: { data: { type: 'metric', attributes: { name: 'Placed Order' } } },
profile: { data: { type: 'profile', attributes: { email } } },
properties: {
subject: t('orderReceipt.subject'),
preview: t('orderReceipt.preview'),
},
},
},
});I map from the Centra profile language I already store. nl becomes nl-NL. Everything else in this helper falls back to en. The subject and preview then leave as properties on the event. Klaviyo can print those strings without guessing.
When Klaviyo owns the template, the template must branch on the profile language Centra already syncs (person|lookup:"Language"), or I send pre-rendered next-intl strings as event properties. I would rather send a Dutch subject into an English-looking Klaviyo canvas than hope the canvas knows the shopper. next-intl already knew nl-NL. I had not invited it to the email.
Locale comes from the profile
I pick the locale at the moment I build the event, from a field the commerce backend already trusts. Centra language on the profile. Shipping country is a bad stand-in: Belgium gets French, and the Netherlands can still get English.
This helper is a two-way map (nl to nl-NL, else en). A third storefront language needs another branch, and a Klaviyo-owned template still has to look up Language the same way. I pick one strategy per flow. Either I render the strings, or Klaviyo branches. Mixing both, plus a guess, is how English sneaks back in.
A metric that fired can still be a receipt in the wrong language. Dutch shoppers notice the first English sentence.
Happy coding! Sander