Agent Skill · Salesforce Commerce Cloud

sfnext-i18n

Implement internationalization in Storefront Next using i18next with useTranslation for components and getTranslation for server-side code. Use when adding translations, configuring locales, handling pluralization, using the Zod schema factory pattern, or managing extension translations. Covers namespaces, interpolation, and language switching.

Provider: Salesforce Commerce Cloud Path in repo: skills/storefront-next/skills/sfnext-i18n/SKILL.md

Skill body

Internationalization (i18n) Skill

This skill covers internationalization in Storefront Next using i18next with a dual-instance architecture (server + client).

Overview

Translation File Structure

Translations are organized as namespace files per locale, compiled into a TypeScript index:

src/locales/en-US/
├── index.ts              # Merges all namespace files + extensions
├── translations.json     # Default namespace (top-level keys become namespaces)
└── product.json          # "product" namespace (separate file)

src/locales/en-GB/
├── index.ts
├── translations.json
└── product.json

The index.ts imports all namespace files and extension translations:

import translations from '@/locales/en-US/translations.json';
import product from '@/locales/en-US/product.json';
import extensionTranslations from '@/extensions/locales/en-US/';

const allTranslations = { ...translations, product, ...extensionTranslations };
export default allTranslations satisfies ResourceLanguage;

Namespace structure in translations.json

Each top-level key is a namespace:

{
    "header": {
        "search": "Search",
        "account": "Account"
    },
    "footer": {
        "copyright": "© {{year}} Company"
    }
}

Separate namespace file (product.json)

{
    "title": "Product Details",
    "addToCart": "Add to Cart",
    "greeting": "Hello, {{name}}!",
    "itemCount_one": "{{count}} item",
    "itemCount_other": "{{count}} items"
}

Usage in Components

import { useTranslation } from 'react-i18next';

export function ProductCard() {
    const { t } = useTranslation('product');

    return (
        <div>
            <h1>{t('title')}</h1>
            <button>{t('addToCart')}</button>
            <p>{t('greeting', { name: 'John' })}</p>
            <p>{t('itemCount', { count: 5 })}</p>
        </div>
    );
}

Critical: Always pass a namespace to useTranslation():

// WRONG — missing namespace
const { t } = useTranslation();
t('title');  // Will not find the key

// CORRECT — with namespace
const { t } = useTranslation('product');
t('title');  // Works

Usage in Server Code

import { getTranslation } from '@/lib/i18next';

// In loaders/actions (pass context)
export function loader(args: LoaderFunctionArgs) {
    const { t } = getTranslation(args.context);
    return { title: t('product:title') };
}

// Client-side utilities (no context)
const { t } = getTranslation();
const message = t('product:addToCart');

Validation Schemas — Factory Pattern

Critical: Use a factory function for Zod schemas with translated messages to avoid race conditions:

// WRONG — Module-level schema (race condition: t() may not be initialized)
export const schema = z.object({
    email: z.string().email(t('validation:emailInvalid'))
});

// CORRECT — Factory function
import type { TFunction } from 'i18next';

export const createSchema = (t: TFunction) => {
    return z.object({
        email: z.string().email(t('validation:emailInvalid'))
    });
};

// Usage in component
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';

function MyForm() {
    const { t } = useTranslation();
    const schema = useMemo(() => createSchema(t), [t]);

    const form = useForm({ resolver: zodResolver(schema) });
}

Language Switching

import LocaleSwitcher from '@/components/locale-switcher';

export function Footer() {
    return <footer><LocaleSwitcher /></footer>;
}

Extension Translations

Extensions use extPascalCase namespace auto-derived from the extension directory name:

src/extensions/my-extension/locales/
├── en-US/translations.json
└── it-IT/translations.json
const { t } = useTranslation('extMyExtension');
t('welcome');

Common Pitfalls

Pitfall Problem Solution
Missing namespace Keys not found Always pass namespace: useTranslation('product')
Module-level t() in schemas Race condition on initialization Use factory pattern: createSchema(t)
Forgetting context on server Translations not found Use getTranslation(args.context) in loaders
Duplicate keys across namespaces Wrong translation shown Prefix with namespace: t('product:title')