Veap bundles an intl system with server-side locale detection, message loading from core, plugins, templates and the application, and typed translation helpers for both server and client.
Configuration#
Intl options live in veap.config.ts under intl:
import type { VeapConfig } from "@veap/core/core";
const config: VeapConfig = {
intl: {
cookie: "VEAP_LOCALE", // cookie storing the chosen locale
default: "en", // fallback locale
locales: ["en", "pl"], // supported locales
timeZone: "Europe/Warsaw", // default for date/relative formatting
},
};Environment variables override config values: VEAPCONFIG_INTL_COOKIE, VEAPCONFIG_INTL_DEFAULT, VEAPCONFIG_INTL_TIMEZONE (plus NEXT_PUBLIC_* variants and TZ for the time zone). Defaults: cookie VEAP_LOCALE, locale en, list ["en"], time zone UTC.
Server usage#
I18nProvider from @veap/core/intl/server is an async Server Component: it detects the locale (cookie first, then Accept-Language negotiation, then default), loads messages and renders the client provider:
// app/layout.tsx (generated for you)
import { I18nProvider } from "@veap/core/intl/server";
<I18nProvider>{children}</I18nProvider>;For translations outside the React tree (Server Actions, route handlers, mailables):
import { getTranslation } from "@veap/core/intl/server";
const { t, date, relativeTime, locale } = await getTranslation();
t("Welcome back, {name}", { name: user.name });getTranslation(localeOverride?, timeZoneOverride?) follows the same detection chain; the mail translator used by mailables is wired to it.
Client usage#
Inside the provider tree, the client entry gives hooks:
"use client";
import {
useTranslation,
useLocale,
useSupportedLocales,
} from "@veap/core/intl";
export function Greeting() {
const { t, date, relativeTime } = useTranslation();
const locale = useLocale();
return (
<p>
{t("Hello!")}
{date(new Date())}
{relativeTime(someDate)} // "3 minutes ago"
</p>
);
}Hooks throw if rendered outside I18nProvider. useTranslation returns the translator created from the provider's messages, locale and time zone.
Message sources and precedence#
IntlService.getMessages(locale) deep-merges, in order (later wins):
- Messages registered programmatically (
registerMessages(locale, dict)). - Core dictionaries (built-in
en,pl). - Every registered plugin's
locales[locale]loader. - Templates' locale loaders (via the same
localesrecord onITemplate). - Application files:
locales/<locale>.jsonin the project root (plus any extra directories passed togetMessages).
A missing key renders the key itself (or the key with {param} interpolation when values are passed), so development never blocks on missing translations.
Message format#
Values use intl-messageformat (ICU):
{
"cart": {
"items": "{count, plural, =0 {Your cart is empty} one {# item} other {# items}}",
"since": "Member since {joined, date, short}"
}
}t("cart.items", { count: 3, joined: someDate }) formats with the active locale.
Locale switching#
Write the configured cookie (VEAP_LOCALE by default) with a supported locale value; detection picks it up on the next request. The panel plugin ships a language switcher doing exactly this (useLocale/useSupportedLocales on the client, cookie write server-side).
Typing keys#
TranslationKeys derives nested dot-paths from an IntlMessages global interface; augment it from your application to get key completion:
declare global {
interface IntlMessages {
home: { hero: string };
}
}Date and relative time#
The translator exposes date(date, options?) (Intl.DateTimeFormat with the configured time zone) and relativeTime(date, { now?, numeric?, style? }) (Intl.RelativeTimeFormat with sensible unit thresholds). Both accept Date, ISO strings or timestamps.