← Files BrainerceARCHIVED FILE

skills/brainerce-storefront-build/references/i18n.md

7.97 KB · Oct 3, 2026 · 06:23 UTC

↓ Download file

## 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