← Files BrainerceARCHIVED FILE
skills/brainerce-storefront-build/references/i18n.md
7.97 KB · Oct 3, 2026 · 06:23 UTC
## Internationalization (i18n)
When a store has i18n enabled, the SDK sends an `Accept-Language` header on
every request after you call `client.setLocale(locale)` once. All content
comes back translated automatically — no per-call `{ locale }` params, no
client-side translation overlay.
### Detecting i18n support
```typescript
const store = await client.getStoreInfo();
if (store.i18n?.enabled) {
// Multi-locale store — wire the flow below.
// store.i18n.defaultLocale → e.g. 'he'
// store.i18n.supportedLocales → e.g. ['he', 'en']
}
```
### Routing: `[locale]` segment
Next 15 App Router, "as-needed" locale prefixes:
- `/` and `/products` → clean URLs for the store's default locale
- `/en/` and `/en/products` → secondary locales get a prefix
```
src/app/[locale]/
layout.tsx
page.tsx
products/page.tsx
products/[slug]/page.tsx
cart/page.tsx
checkout/page.tsx
```
A middleware.ts canonicalizes `/{defaultLocale}/X` → `/X` (308), and rewrites
clean URLs internally to `/{defaultLocale}/...` so the `[locale]` segment
always resolves. The middleware also sets an `x-locale` response header so
Server Components can read it via `headers()`.
### SDK setup: call `setLocale` once
`setLocale()` is a synchronous property setter. Call it once and every
subsequent SDK call sends the `Accept-Language` header automatically:
```typescript
import { BrainerceClient } from 'brainerce';
const client = new BrainerceClient({ salesChannelId: 'vc_...' });
client.setLocale('he'); // done — all calls are now in Hebrew
```
No per-call `{ locale }` params. No React effect race conditions.
### Server Components
Read the locale from route params or the `x-locale` header, then pass it to
`getServerClient()`:
```typescript
import { headers } from 'next/headers';
async function getRequestLocale(): Promise<string | undefined> {
return (await headers()).get('x-locale') ?? undefined;
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const locale = await getRequestLocale();
const client = getServerClient();
client.setLocale(locale);
const product = await client.getProductBySlug(slug);
// NEVER feed raw HTML into <meta name="description"> — product.description
// is often rich-text HTML. Strip tags first, then truncate on a word
// boundary. See buildMetaDescription() in lib/seo.ts.
return {
title: product.seoTitle || product.name,
description: product.seoDescription || buildMetaDescription(product.description) || product.name,
};
}
```
### Client Components
The StoreProvider already calls `client.setLocale(locale)` with the locale
from route params. Components just use the client normally — no per-call
locale needed:
```typescript
'use client';
import { useParams } from 'next/navigation';
function ProductsPage() {
const { locale } = useParams<{ locale?: string }>();
useEffect(() => {
(async () => {
const client = getClient();
// No { locale } param — StoreProvider already called setLocale
const [cats, brands, tags] = await Promise.all([
client.getCategories(),
client.getBrands(),
client.getTags(),
]);
// …setState
})();
}, [locale]); // re-fetch when the user switches language
const result = await client.getProducts({ page: 1, limit: 20 });
// All fields come back translated automatically.
}
```
### Translated fields (complete list)
Every SDK call returns these fields in the active language — no client-side
overlay needed:
**Product:**
- `name`, `description`, `slug`
- `seoTitle`, `seoDescription` (merchant can author SEO copy per locale)
- `categories[].name`
- `brands[].name`
- `tags[].name`
- `variants[].name`
- `variant.attributes` keys and values — `getVariantOptions(variant)` returns translated attribute names and option values automatically
- `productAttributeOptions[].attribute.name`, `attributeOption.name`
- `metafields[].value` (the free-text values customers submit)
- `metafields[].definition.name`, `metafields[].definition.description` (the merchant-authored custom-field labels — "Warranty Info" / "מידע אחריות")
- `modifierGroups[].name`, `modifierGroups[].description` (the group label shown on the PDP — e.g. "Toppings" / "תוספות")
- `modifierGroups[].modifiers[].name`, `modifierGroups[].modifiers[].description` (each individual modifier — e.g. "Olives" / "זיתים")
**Promotional surfaces:**
- `cart.bundles[].name`, `cart.bundles[].description` (the bundle's own merchant-typed label, e.g. "Lunch Combo" / "ארוחת צהריים")
- `cart.bundles[].offeredProducts[].name`, `.slug` (each product inside the bundle — fetched fresh from `Product.translations`)
- `checkout.bumps[].title`, `checkout.bumps[].description` (the bump headline shown at checkout — falls back to the translated product name when merchant didn't override)
- `checkout.bumps[].bumpProduct.name`, `.slug` (the underlying product)
- Discount-rule names/descriptions (when surfaced as banner text via `displayConfig`)
**Taxonomy (`getCategories`, `getBrands`, `getTags`):**
- `name` on each item
**Cart:**
- `items[].product.name`, `items[].variant.name`
**Bundles & Order Bumps:**
- `bundleProduct.name`, `bundleProduct.slug`, `bumpProduct.name`, `bumpProduct.slug`
- Variant names inside bundles/bumps
**Checkout:**
- Line items: `items[].product.name`, `items[].variant.name`
- Discount banners, nudges, badges
**Recommendations (`getProductBySlug` response, `getCartRecommendations`,
`getCartUpgradeSuggestions`):**
- `name`, `slug` on every suggested product
**Search suggestions:**
- `name` on products and categories
- Cross-locale matching: a query typed in the active language matches both
the base `name` column AND `translations[locale].name`.
**Order history:**
- `items[].product.name`, `items[].variant.name`
### Checkout locale persistence
The locale is persisted when a checkout is created. Every subsequent GET on
that checkout returns line items in the original language, regardless of the
client's current locale setting. This ensures a consistent checkout
experience even if the user switches languages mid-session.
### Rendering the translated fields
Because the backend already overlays everything, UI code is just:
```typescript
// Product detail — brand badge + tag chips + category pills are all localized.
<div className="flex flex-wrap gap-2">
{product.categories?.map(cat => (
<span key={cat.id} className="rounded bg-muted px-2 py-1 text-xs">{cat.name}</span>
))}
</div>
{product.brands?.length ? (
<div className="text-muted-foreground text-sm">
{t('by')} <span className="font-medium">{product.brands.map(b => b.name).join(', ')}</span>
</div>
) : null}
<h1>{product.name}</h1>
{product.tags?.length ? (
<div className="flex flex-wrap gap-1.5">
{product.tags.map(tag => (
<span key={tag.id} className="rounded-full border px-2.5 py-0.5 text-xs">#{tag.name}</span>
))}
</div>
) : null}
```
### RTL handling
Resolve direction with `client.getStoreDirection(locale)` — it returns
`'ltr' | 'rtl'` for any BCP-47 tag (Arabic, Hebrew, Persian, Urdu, Yiddish
today; future RTL locales added by the platform are picked up automatically).
Do NOT maintain your own RTL locale set.
```tsx
// app/[locale]/layout.tsx
const dir = client.getStoreDirection(locale);
return <html lang={locale} dir={dir}>...</html>;
```
For static rendering, `storeInfo.i18n.defaultDirection` carries the store's
default direction, and each entry in `storeInfo.i18n.supportedLocaleObjects`
includes its own `direction`.
Let flexbox reverse automatically once `<html dir="rtl">` is set. Do NOT add
`flex-row-reverse` on top — that's a double-swap. DO swap directional icons
(chevrons, arrows) manually. Use logical CSS properties (`ms-*`/`me-*`)
instead of `ml-*`/`mr-*`.
### Language switcher
Read `storeInfo.i18n.supportedLocales` and render a switcher in the header
that navigates to `/{otherLocale}/{currentPathWithoutLocale}`. The middleware
handles the canonical redirect when the user picks the default locale.SHA-256: ea2314f8d0fb3d8851fc16575c20ec70f634327279da4f45ec0bb3a47381517d