afv-library/skills/experience-ui-bundle-localize/references/i18n-setup.md

170 lines
6.0 KiB
Markdown

# i18n Setup: the two files you write
You write two files to set up i18n in a React UI Bundle. The Platform SDK provides the runtime plumbing (detector, backend, context fetch); you just wire it into i18next.
---
## File 1: `src/i18n/index.ts` (the init wiring)
This is the only "glue" you write. It connects the SDK's i18n pieces to i18next.
```typescript
import { createDataSDK } from "@salesforce/platform-sdk";
import {
createSalesforceDetector,
fetchI18nContext,
SalesforceBackend,
} from "@salesforce/platform-sdk/i18n";
import i18next from "i18next";
import ChainedBackend from "i18next-chained-backend";
import LocalStorageBackend from "i18next-localstorage-backend";
import { initReactI18next } from "react-i18next";
import { labelManifest } from "./label-manifest";
export async function initI18n() {
const dataSDK = await createDataSDK();
const ctx = await fetchI18nContext(dataSDK);
// Tell the browser the user's language + text direction (RTL support).
document.documentElement.dir = ctx.dir;
document.documentElement.lang = ctx.lang;
await i18next
.use(ChainedBackend)
.use(createSalesforceDetector(dataSDK))
.use(initReactI18next)
.init({
fallbackLng: "en", // untranslated keys fall back to the English base value
defaultNS: "c", // "c" = your org's custom-label namespace
backend: {
backends: [LocalStorageBackend, SalesforceBackend],
backendOptions: [
{ expirationTime: 86400000 }, // cache labels in localStorage for a day
{ dataSDK, labelManifest }, // fetch the rest over GraphQL
],
},
interpolation: {
// escapeValue: false is correct for React: React already escapes JSX
// output. Do NOT feed interpolated label output into
// dangerouslySetInnerHTML; that bypasses React's escaping and, with this
// setting, is an XSS vector when a label interpolates user-controlled
// text. Render labels as normal JSX (`{t(...)}`).
escapeValue: false,
prefix: "{", // Salesforce labels interpolate with {0}, {1}, …
suffix: "}",
},
});
}
```
**Call it once at boot**, before mounting your app:
```typescript
// src/index.tsx
import { initI18n } from "./i18n";
initI18n().then(() => {
// mount app here
root.render(<App />);
});
```
---
## File 2: `src/i18n/label-manifest.ts` (the list of labels your app uses)
This tells i18next which labels to fetch at boot.
```typescript
export const labelManifest = [
"c:Welcome_Text",
"c:Save_Button",
"c:Save_Failed_Message",
// one entry per label, "namespace:Key"
];
```
**Format:** `"<namespace>:<Key>"`
- `c` = custom labels in your org
- `Key` = the `<fullName>` from your `CustomLabels.labels-meta.xml`
The manifest is how i18next knows what to fetch. An **unregistered key fails silently**: it renders as its own literal name (e.g., `"Welcome_Text"` instead of "Welcome") with no console warning. Always keep the manifest in sync with your `t()` calls.
---
## Dependencies
Install these first (Step 4 of the main workflow):
```bash
npm install i18next react-i18next i18next-chained-backend i18next-localstorage-backend
```
Use `@salesforce/platform-sdk` **≥11.42.1**. The `@salesforce/platform-sdk/i18n` subpath (`SalesforceBackend`, `createSalesforceDetector`, `fetchI18nContext`) has existed since 11.4.1, and 11.7.0 added `reloadI18nContext` for refreshing the cached label context. 11.42.1 is the validated floor because it carries the fix that batches a namespace into 100-name-or-fewer queries. Without it, a manifest of more than 100 labels in one namespace hits the `uiapi.platform.labels` limit and the whole namespace read fails. Keep the SDK's siblings (`@salesforce/vite-plugin-ui-bundle`, `@salesforce/ui-bundle`) on the same version.
Known-good companion versions:
- `i18next` **^24.2.2**
- `react-i18next` **^15.5.1**
- `i18next-chained-backend` **^4.6.2**
- `i18next-localstorage-backend` **^4.2.0**
---
## What you DON'T write
The Platform SDK ships with:
- `createSalesforceDetector`: reads the user's language from the org
- `SalesforceBackend`: fetches labels over GraphQL
- `fetchI18nContext`: gets language, locale, text direction, currency
If you see an older example that vendors `salesforce-detector.ts` or `salesforce-backend.ts` into `src/`, it predates the SDK's i18n export. You no longer copy those files in; just import from `@salesforce/platform-sdk/i18n`.
---
## How it works at boot
1. Bundle loads, `initI18n()` runs
2. `createDataSDK()` initializes the SDK
3. `fetchI18nContext()` queries the org for the user's language/locale/direction
4. `SalesforceBackend` reads the manifest and issues a GraphQL query per namespace:
```graphql
query LoadLabels {
uiapi {
platform {
labels(namespace: "c", names: ["Welcome_Text", "Save_Button", ...]) {
name
value
resolvedLocale
}
}
}
}
```
5. Platform returns labels at the user's resolved locale
6. i18next caches them (in memory + localStorage)
7. React mounts; components call `t()`; lookups hit the cache
The two backends chain: `LocalStorageBackend` serves cached labels (24-hour expiry), and `SalesforceBackend` fetches misses over GraphQL. This makes subsequent loads fast.
---
## Namespace note
`defaultNS: "c"` means components can call `t("Welcome_Text")` instead of `t("c:Welcome_Text")`; the namespace is implicit. If you're loading labels from multiple namespaces (e.g., framework-shipped labels like `LightningDatatable`), you'd specify the namespace in the `useTranslation` hook:
```typescript
const { t } = useTranslation("c"); // custom labels
const { t: tFw } = useTranslation("LightningDatatable"); // framework labels
```
For most bundles, a single `"c"` namespace is all you need.
---
## Related
- [label-xml.md](label-xml.md): the Custom Labels metadata XML shape
- [interpolation.md](interpolation.md): how `{0}/{1}` placeholders work
- [verifying.md](verifying.md): the serve/verify flow
- [gotchas.md](gotchas.md): silent-fail traps to avoid