experience-ui-bundle-localize

MUST activate to localize / internationalize a uiBundles/*/src/ project (React or Angular): extract hardcoded user-facing strings into Custom Labels, wire a runtime i18n library over the Platform SDK backend, add labels for another language, or troubleshoot label rendering across locales. Triggers:

By forcedotcom · 2,057 installs

npx skills add forcedotcom/sf-skills --skill experience-ui-bundle-localize

Source repository · Upstream listing

Localize a UI Bundle Localize a UI Bundle: extract user facing strings into Salesforce Custom Labels, wire a runtime i18n library over the Platform SDK backend, and verify labels across locales. This file is the framework neutral workflow + guardrail spine . The framework specific detail — which i18n library, the translation call convention, the files scanned, the wiring shape, and the depth docs — lives in a per framework reference. The one paragraph mental model A UI Bundle can't use compile time label imports the way LWC does ( @salesforce/label/ resolves inside the platform's compiler, which your standalone bundle doesn't go through). Instead, your app fetches labels at runtime through the Salesforce GraphQL UI API and hands them to a standard i18n library to render. The Platform SDK provides the runtime plumbing: 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 your i18n library, and a manifest listing which labels your app uses — then author the labels themselves as Salesforce Custom Labels metadata. The exact library and call convention are framework specific; see your framework reference. 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 Configuring site languages or sfdc cms languageSettings experience ui bundle site generate skill Localizing an existing bundle Determine the framework (below), then the workflow Determine the framework. It is normally already decided by the calling context — passed down by the coordinator skill that invoked this one, or stated in the user's request. Use that. The frameworks this skill supports are exactly the reference folders under <SKILL DIR /references/ , each containing a localize.md (so react → <SKILL DIR /references/react/localize.md ). This is the single source of truth — adding a framework means adding a reference folder, nothing here changes. If the framework is known — open <SKILL DIR /references/<framework /localize.md and keep it alongside this spine. It supplies the library, the call convention, the files to scan, and the wiring code. If it is unknown (a standalone run where nobody said which) — run the deterministic detector on the app / uiBundle root before asking anyone: It combines an angular.json at/above the root, @angular/core / react in any non node modules package.json , and source file signatures, then prints one token and sets a matching exit code: react or angular (exit 0) → use that framework. Do not ask the user — the detection is deterministic. Open <SKILL DIR /references/<framework /localize.md . ambiguous (exit 2) → both frameworks are present. List <SKILL DIR /references/ and ask the user which one to localize. If they name a framework with no matching reference folder, it is not supported here — stop. unknown (exit 3) → no supported framework detected. Terminate the workflow. Do not guess and do not proceed: report that neither React nor Angular signals were found in the bundle, so localization cannot continue, and stop here. Throughout the steps below, <framework means the folder chosen here. The deterministic check scripts split by coupling: Framework neutral, shared in <SKILL DIR /scripts/ — detect framework.sh (the Step 0 detector above), check org api version.sh and detect bundle type.sh (pure org/metadata checks), and check manifest registered.sh (agnostic skeleton; it takes framework <framework to select the call site grammar). Framework specific, under <SKILL DIR /references/<framework / — check i18n wired.sh (its manifest into backend detection is i18n library shaped, so each framework ships its own). Preconditions: verify before editing Requirement Verify If missing 1 It's a uiBundles/ /src/ project (React or Angular) Project structure matches Not a UI Bundle → route to the correct skill 2 Platform SDK, UI Bundle, and build plugin siblings installed and aligned (≥11.49.3) package.json in the UI bundle dir Tell user to align and upgrade them; cannot proceed 3 You can identify where the app mounts Read the entry file (see the framework reference) 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 authenticated B2E or the request/context explicitly identifies a B2C site, not B2B Run the bundle type detection below and use the request/context for site product identity Explicit B2B → reject; site type not explicit → ask the user and stop until confirmed; B2C also requires precondition 6 6 For B2C only, an admin has enabled GraphQLApiOrgPrefForGuestUsers Ask the admin to confirm the org preference is already enabled Do not enable it; explain that guest GraphQL returns HTTP 403 without it and stop (dependency: W 23854208) 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: 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.) Bundle type detection (precondition 5). The bundle's type decides which localization branch applies. It is framework agnostic (pure Salesforce metadata). Pass the full path to the bundle dir; the script derives the metadata root from it, so the current directory does not matter: Act on the exit code contract: 0 → authenticated app (B2E or in core internal), use the B2E branch; 10 → bound public site app container candidate, meaning metadata proves site binding and guest access but not B2C versus B2B; 11 → bound non public/unsupported site, stop; 12 → both CustomApplication and one site binding exist, ask which runtime context is the localization target; 13 → multiple matching Experience site bindings, show the reported site names and ask which site/runtime context is the target; 2 → unbound/unknown, report the script output and stop rather than guessing. For exit 10 , route by explicit request/context: if it says B2C , confirm precondition 6 and use the B2C branch; if it says B2B , reject it; if product type is not explicit, ask the user whether the site is B2C or B2B and stop until confirmed. For exits 12 and 13 , require the user to choose the runtime context (and site for exit 13 ), then apply that branch's fallback and prerequisites. Never infer B2C from DigitalExperienceConfig , appSpace , appContainer , or AUTHENTICATED WITH PUBLIC ACCESS ENABLED ; the authentication value means guest access is enabled. For B2C, only an org admin may enable GraphQLApiOrgPrefForGuestUsers ; never provision or change it. Without it, guest label requests return HTTP 403. Track availability through W 23854208. 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 add i18n wiring or TODO markers to a B2B or unknown bundle. Workflow: the five steps Each step has a completion criterion and a confirm before continue pause. Framework specifics (file extensions, the translation call, the install, the init code) come from <SKILL DIR /references/<framework /localize.md . Step 1: Detect Goal: Scan the framework's component files for user facing hardcoded strings. (The framework reference names the file extensions to scan.) What to scan: String literals shown to users in markup: Welcome in a heading → 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 , id attributes) Text already wrapped in a translation call Console logs, error messages thrown to developers (not user facing) Class names, file paths, technical constants Action: 1. Scan the src/ directory for the framework's component files 2. Extract candidates, showing file path + line number for each 3. 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 literal with a translation call. Action for each string: 1. 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. 2. Add the label to force app/main/default/labels/CustomLabels.labels meta.xml : (Full XML structure: references/common/label xml.md ) 3. Replace the string in the component with your framework's translation call, and add any required import/injection. The exact call convention is in the framework reference. Completion criterion: Every confirmed string has both a CustomLabels entry and a translation call in its original location. Pause: "For each string I'll add a Custom Label and replace the literal with a translation call. 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 the i18n runtime knows to fetch it. Action: 1. 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. 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 translation call site against the manifest and treats a missing label manifest.ts (when call sites 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. Branch on the exit code: 0 , every key is registered (or there are no call sites 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 i18n wiring exists; scaffold it if the app has no i18n yet. The install command, the wiring code, and the B2E vs B2C f