
React UI Localization
OfficialFreeStreamline localization for React UI Bundles.
Free · Opens the source repo
What React UI Localization does
The React UI Localization skill provides a structured workflow for localizing React projects that utilize Salesforce UI Bundles. It enables developers to extract hardcoded user-facing strings from their code and convert them into Salesforce Custom Labels. This skill integrates with i18next, a widely-used internationalization library for React, to facilitate the rendering of these labels based on the user's language preferences. By following the provided steps, developers can ensure that their applications are accessible to a global audience, enhancing user experience across different locales.
The skill operates within authenticated UI Bundles, specifically designed for internal or B2E applications. It does not support B2C/B2B site apps, which are currently outside the scope of this localization capability. Developers can utilize the skill to troubleshoot label rendering issues, add new languages to existing bundles, and verify that all labels render correctly across different locales. The skill includes essential pre-checks to validate the project structure and API compatibility, ensuring that developers have the necessary environment to proceed with localization tasks.
Included in the skill are several scripts that assist in verifying the project setup, including checks for API version compatibility and bundle type detection. This ensures that developers can quickly identify potential issues before attempting to localize their applications. The skill also provides references to additional documentation, covering topics such as i18next setup, Custom Labels XML formatting, and common pitfalls to avoid during the localization process. Overall, this skill is an essential tool for developers looking to enhance their React applications with robust internationalization support.
When to use it
Use this skill when you need to localize an existing authenticated React UI Bundle and ensure proper string handling across multiple languages.
When not to use it
Avoid this skill for B2C/B2B site applications or when generating new bundles, as it does not support those scenarios.
What you can build with it
Localizing an Existing Application
When you have an existing authenticated React UI Bundle and need to extract and manage user-facing strings for localization.
Troubleshooting Label Issues
Use this skill to diagnose and fix issues related to label rendering across different locales in your application.
Adding New Languages
When expanding your application's reach, this skill helps you add new languages to your localized bundle efficiently.
How to install React UI Localization
View source1. Install with the skills CLI
npx skills add forcedotcom/sf-skills/experience-ui-bundle-localize --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by forcedotcomLocalize a React UI Bundle
Walk a developer through localizing a React UI Bundle: detect hardcoded user-facing strings, extract them into Salesforce Custom Labels, wire up i18next over the Platform SDK GraphQL backend, and verify labels render across locales.
This file is the workflow + guardrail spine. Depth lives in linked docs:
- references/i18n-setup.md: the two files you write: the i18next init and the label manifest
- references/label-xml.md: Custom Labels and translation metadata XML shapes; the
namespace:Keyrules - references/interpolation.md: positional
{0}/{1}placeholder interpolation in labels - references/verifying.md: serve URL, locale flip, and verifying labels render
- references/gotchas.md: the three silent-fail traps: unregistered manifest keys, API-version bake-in, stale label cache
The one-paragraph mental model
A React UI Bundle can't use @salesforce/label/* the way LWC does, those imports resolve at compile time inside the platform's compiler, which your standalone React bundle doesn't go through. Instead, your app fetches labels at runtime through the Salesforce GraphQL UI API and hands them to i18next (a standard React i18n library) to render. The Platform SDK provides the runtime plumbing for this, a detector that reads the user's language, a backend that fetches labels over GraphQL, and a context fetch. You write two thin files: a short init that wires the SDK pieces into i18next, and a manifest listing which labels your app uses. The rest is authoring the labels themselves as Salesforce Custom Labels metadata.
import { useTranslation } from "react-i18next";
function WelcomeBanner() {
const { t } = useTranslation("c"); // "c" = custom label namespace
return <h1>{t("Welcome_Text")}</h1>; // renders "Welcome" or "Bienvenido" per user's language
}
Step 0: Route the task
| The task is… | Go to |
|---|---|
| Bundle doesn't exist yet | experience-ui-bundle-frontend-generate skill |
| Deploying the app with its labels | experience-ui-bundle-deploy skill |
| Localizing an existing bundle | Workflow below |
Preconditions: verify before editing
| # | Requirement | Verify | If missing |
|---|---|---|---|
| 1 | It's a uiBundles/*/src/ React project | Project structure matches | Not a UI Bundle → route to the correct skill |
| 2 | @salesforce/platform-sdk installed (≥11.42.1) | package.json in the UI bundle dir | Tell user to install it; cannot proceed |
| 3 | You can identify where the app mounts | Read the entry file (usually src/index.tsx) | No clear mount point → ask user to point it out |
| 4 | Target org actually supports API v68.0+ (runtime label GraphQL for UI Bundles ships in Release 264) | Run the runtime org-release check below | Org's max API version is below v68.0 (Release 262 or older) → cannot proceed; retarget a Release 264+ org or upgrade the org |
| 5 | The bundle is an authenticated app (B2E, or an in-core internal app), not a public site | Run the authenticated-app detection below | Bundle is a site (B2C/B2B) app → localization is not yet supported for site bundles; stop and tell the user B2C support is planned for when B2C localization is ready |
Runtime org-release check (precondition 4). The platform.labels GraphQL path that resolves labels at runtime for UI Bundles ships in Salesforce Release 264 (API v68.0 or higher). A sourceApiVersion in sfdx-project.json records what you declared, not what the org supports, so a newer CLI pointed at an older org can pass a static file check and then fail at runtime. Query the org's actual maximum API version before wiring anything:
bash <skill-dir>/scripts/check-org-api-version.sh <org-alias-or-username>
Exit 0 → the org supports v68.0+, proceed. Exit 1 → the org is too old or unreachable; do not write i18n wiring or labels, report the version mismatch to the user and stop. (sf api request rest inside the script keeps authentication at the CLI transport layer, so no access token enters context.)
Authenticated-app detection (precondition 5). The bundle's type decides whether localization is supported, and it's decided by deterministic file and string checks. Pass the full path to the bundle dir; the script derives the metadata root from it, so the current directory does not matter:
bash <skill-dir>/scripts/detect-bundle-type.sh <path-to-uiBundles/<name>/ dir>
Act on the exit code: 0 → authenticated app (in-core internal or B2E), proceed; 1 → site (B2C/B2B), localization is not yet supported for site bundles, stop and tell the user (B2C support is planned for when B2C localization is ready); 2 → unbound or cannot auto-detect, ask the user to confirm the bundle is an authenticated app (B2E or in-core internal) and stop if they cannot.
If a precondition isn't met, stop: report the specific block to the user and record a plan item to return once it's resolved. Do not edit the bundle, in particular, never add i18n wiring or TODO markers to a site (B2C/B2B) bundle that precondition 5 gated off as unsupported.
Workflow: the five steps
Each step has a checkable completion criterion and a confirm-before-continue pause.
Step 1: Detect
Goal: Scan .tsx/.jsx files for user-facing hardcoded strings.
What to scan:
- String literals inside JSX tags:
<h1>Welcome</h1>→ candidate - String props shown to users:
placeholder="Enter name"→ candidate - User-facing accessible text:
aria-label,aria-describedby,alt→ candidate (a screen-reader user hears these, so they must localize too)
What to skip:
- Import statements
- Object keys / property names
data-*attributes (machine-readable)- Test IDs (
data-testid,idattributes) - Text already wrapped in
t()calls - Console logs, error messages thrown to developers (not user-facing)
- Class names, file paths, technical constants
Action:
- Scan the
src/directory for.tsxand.jsxfiles - Extract candidates, showing file path + line number for each
- Show the list to the developer
Completion criterion: Developer confirms the list (or edits it to remove false positives).
Pause: "I found N user-facing strings across M components. Here's the list: [show file:line + string]. Look right? [confirm / edit the list / skip some]"
Step 2: Extract
Goal: For each confirmed string, add a Custom Label and replace the JSX literal with a t() call.
Action for each string:
- Propose a key name, format:
<Context>_<Role>(e.g.,"Welcome"→Welcome_Text,"Save"→Save_Button,"Failed to save"→Save_Failed_Message). Follow naming: PascalCase words, underscores between parts, descriptive enough to be unique. - Add the label to
force-app/main/default/labels/CustomLabels.labels-meta.xml:
(Full XML structure: references/label-xml.md)<labels> <fullName>Welcome_Text</fullName> <language>en_US</language> <protected>false</protected> <shortDescription>Welcome banner heading</shortDescription> <value>Welcome</value> </labels> - Replace the string in the component with
{t("Key")}:// Before: <h1>Welcome</h1> // After: <h1>{t("Welcome_Text")}</h1> - Add the import if not present:
import { useTranslation } from "react-i18next";andconst { t } = useTranslation("c");at the top of the component function.
Completion criterion:
Every confirmed string has both a CustomLabels entry and a t() call in its original location.
Pause: "For each string I'll add a Custom Label and replace the JSX with t(). Here are the proposed keys: [show string → namespace:Key mapping]. Apply these edits? [y / review each]"
Step 3: Register
Goal: Add each key to the label manifest so i18next knows to fetch it.
Action:
- Add each key to the manifest array in
src/i18n/label-manifest.ts:
If the file doesn't exist yet, Step 4 scaffolds it; the completion check below reports its absence, so don't test for the file by hand.export const labelManifest = [ "c:Welcome_Text", "c:Save_Button", "c:Save_Failed_Message", ];
Completion criterion:
Run check-manifest-registered.sh from the UI bundle dir (it scans src/ relative to the current directory) and report any errors it returns. It owns the deterministic inspection: it cross-checks every t("Key") call site against the manifest and treats a missing label-manifest.ts (when t() calls exist) as a failure. A key that's called but not registered renders as its own literal name at runtime with no error, the silent-fail trap this guards.
cd <path-to-uiBundles/<name>/ dir> # scripts scan src/ relative to here
bash <skill-dir>/scripts/check-manifest-registered.sh
Branch on the exit code: 0, every key is registered (or there are no t() calls to gate), proceed. 1, the manifest is missing or the listed keys aren't in it; scaffold or add them (Step 4 scaffolds the file) and re-run. 64, usage error, the source dir doesn't exist (wrong cwd or bad argument); this is not a "keys missing" result, do not scaffold or register, fix the path and re-run.
Pause: "Added N entries to label-manifest.ts. check-manifest-registered.sh passed: [confirm]."
Step 4: Wire
Goal: Ensure the i18next init exists; scaffold it if the app has no i18n yet.
Check:
Run check-i18n-wired.sh from the UI bundle dir (it scans src/ relative to the current directory) and report what it returns. The script owns the whole deterministic inspection: it looks for an init file defining initI18n() and a boot-time call to it, and when those exist it also reports whether the label manifest is imported and actually passed into the backend config. Do not re-derive any of this by reading files yourself.
cd <path-to-uiBundles/<name>/ dir> # scripts scan src/ relative to here
bash <skill-dir>/scripts/check-i18n-wired.sh
Branch on the exit code (the printed message names the specific file/symbol for your report, but the decision is the code):
- Exit
0→ fully wired, the manifest is passed into the backend; go to "If i18n already exists" below and just add new keys. - Exit
1→ noinitI18n()exists; scaffold the whole setup via "If no i18n setup exists yet". - Exit
2→ the init already exists but isn't called at boot; do not re-scaffold or overwrite it. Add only the boot-timeinitI18n()call in the entry file (step 4 of "If no i18n setup exists yet"), then re-run. - Exit
3→ wired at boot but the script could not confirm the manifest is passed into the backend. It scans the wholesrctree, but this last check is a textual heuristic: the manifest may be wired through a variable, spread, or helper the script can't see, so treat exit 3 as "verify before editing," not "definitely broken." Open the file the message names and confirm the manifest really isn't inbackendOptions. Only if it genuinely dangles, do what the message names: if the manifest is imported but unused, pass it into the existingbackendOptionswithout clobbering it; if there's nobackendOptions/SalesforceBackendconfig at all, add that backend block to the existing init (see references/i18n-setup.md). Never re-scaffold the init file or duplicate wiring that already works. - Exit
64→ usage error: the source dir doesn't exist (wrong cwd or bad argument). This is not a "no init" result; do not scaffold. Fix the path (run from the UI bundle dir, or pass itssrcpath) and re-run.
If no i18n setup exists yet:
- Install dependencies (tell the user to run):
npm install i18next react-i18next i18next-chained-backend i18next-localstorage-backend - Create
src/i18n/index.tswith the init wiring (full code: references/i18n-setup.md) - Create
src/i18n/label-manifest.tswith an empty array (Step 3 will populate it) - Call
initI18n()once at boot in the entry file (before mounting the app):import { initI18n } from "./i18n"; initI18n().then(() => { // mount app });
If i18n already exists:
Act on the message check-i18n-wired.sh already printed (above): if it reports the manifest wired, just add new keys to it; if it reports a reconcile is needed, do exactly what its message names (import the manifest and/or pass it into the backend config) without clobbering existing wiring.
Completion criterion:
initI18n() exists and is called once at boot; the manifest is wired into the backend.
Pause: "i18next setup [exists / created]. initI18n() is called at boot: [confirm]."
Step 5: Verify
Goal: Guide the developer to verify labels render in a second language.
Action:
-
Activate a second language (if not already active), tell the user: "In your org, go to Setup → Translation Workbench → Translation Settings → add a language (e.g., Spanish)."
-
Author a translation, scaffold an empty translation file for the language:
<!-- force-app/main/default/translations/es.translation-meta.xml --> <Translations xmlns="http://soap.sforce.com/2006/04/metadata"> <customLabels> <label>Bienvenido</label> <name>Welcome_Text</name> </customLabels> </Translations>(Full structure: references/label-xml.md)
Tell the user to either:
- Edit the XML file by hand (for a small number of labels), or
- Use Translation Workbench (Setup → Translate → Custom Label → pick language → enter translations), then retrieve with
sf project retrieve start --metadata Translations:es.
-
Build and deploy, tell the user:
sf config set target-org=<alias> # API version bakes in; point at the deploy target first npm run build sf project deploy start --source-dir force-app --target-org <alias> -
Open the app at the
/lwr/application/ai/<namespace>-<bundleName>URL on thelightning.force.comdomain (redirects to the app host). -
Change the user's Language (not Locale), Setup → My Settings → Language & Time Zone → Language → pick the translated language → Save.
-
Reload the app, labels should flip to the translated language.
If it doesn't render: Check the three gotchas in references/gotchas.md:
- Unregistered manifest key (Step 3 missed a label)
- API-version mismatch (built against a different org)
- Stale localStorage cache (clear
i18next_res_*keys in DevTools)
Completion criterion: Labels render in ≥2 locales, or the blocking gotcha is identified.
Pause: "To verify: activate a second language in Translation Workbench, author a translation (I can scaffold the XML), build/deploy, and reload. Want me to scaffold the translation file for [language]? [y / I'll do it manually]"
Edge cases: handle gracefully
- Already-localized code: detect existing
t()usage / a populated manifest; offer to add to the setup rather than re-scaffold everything. - No strings found: report cleanly and stop; do not invent work.
- App has no i18n setup yet: Step 4 scaffolds the two files first before Step 3 can register anything.
- Partial setup (manifest exists but init missing, or vice-versa), reconcile what's present; never clobber existing wiring.
Guardrails: never regress these
- Never machine-translate into deployable metadata. Scaffold empty translation files and guide the developer to author translations (by hand or via Translation Workbench). Do not call any MT API and paste the result into
translation-meta.xml; unreviewed machine translations are a quality liability. - Never register a key that has no label. Manifest entry count must equal label count (Step 3 criterion). An unregistered key renders as its own literal name with no console warning. It's the most common localization bug.
- Never clobber existing i18n wiring. If Step 4 finds an existing
initI18n(), reconcile (add the manifest import if missing) rather than replace the whole file. - Every file must be customer-safe. No
webapps, core-only paths, or internal infrastructure references anywhere. Write as if for an external customer in an SFDX project.
Commands & layout
<project-root>/ ← SFDX project root
└── force-app/main/default/
├── labels/CustomLabels.labels-meta.xml ← English base labels
├── translations/<locale>.translation-meta.xml ← one per translated language
└── uiBundles/<your-bundle>/
├── package.json
└── src/
├── i18n/
│ ├── index.ts ← init wiring (you write this once)
│ └── label-manifest.ts ← list of labels to fetch (you maintain this)
└── components/ ← components call t()
| Command | Run from | Purpose |
|---|---|---|
npm install i18next react-i18next i18next-chained-backend i18next-localstorage-backend | UI bundle dir | Install i18n dependencies (Step 4) |
npm run build | UI bundle dir | Build the app (API version bakes in, set target-org first) |
sf project deploy start --source-dir force-app | Project root | Deploy the app + labels + translations |
sf project retrieve start --metadata Translations:<locale> | Project root | Pull translations authored in Translation Workbench |
Pre-flight checklist: completion criteria for the whole run
- Every confirmed string has both a
CustomLabelsentry and at()call -
label-manifest.tsentry count == label count (no unregistered keys) -
initI18n()present and called once at boot - Labels render in ≥2 locales (or the blocking gotcha is named)
- No hand-written machine translations landed in
*-meta.xml(only scaffold-and-guide)
Frequently asked questions about React UI Localization
Similar skills
Playwright Component Testing
Test React and Vue components in isolation with Playwright.
Fluent UI Blazor
Integrate Fluent UI components in Blazor applications effortlessly.
Build MCP App
Create interactive UI widgets for MCP servers.
Web Design Reviewer
Identify and fix design issues in websites efficiently.
Markstream Install
Seamlessly integrate Markstream for Markdown rendering.
GSAP & Framer Scroll Animation
Create advanced scroll animations effortlessly.
