@salesforce/ui-bundle-template-feature-react-language-switcher
v11.71.3
Published
Language switcher for B2X UI Bundles — rewrites the URL language segment and reloads
Maintainers
Keywords
Readme
feature-react-language-switcher
A drop-in language switcher for B2X (public / Experience) React UI Bundles.
Selecting a language rewrites the current URL so the chosen language becomes the
first path segment after basePath, then reloads the page at that URL. A full
reload lets the platform serve the correctly localized content — this feature owns
only the URL rewrite. It does not load translations, manage i18n state, or persist
a preference.
B2X only. Do not add this feature to B2E (internal /
CustomApplication) apps. Those serve logged-in users whose language comes from their profile, and their URLs have no language segment.
Usage
Render <LanguageSwitcher /> anywhere in your app chrome (typically the header in
appLayout.tsx). It reads everything it needs from the URL and SFDC_ENV, so no
props are required:
import { LanguageSwitcher } from ".../features/language-switcher";
<header>
{/* ... */}
<LanguageSwitcher />
</header>;Optional props:
label— accessible label for the<select>(default"Language").className— replaces the default Tailwind classes on the<select>.
Single-language sites render nothing. When fewer than two languages are configured in
languages.ts, the component returnsnull— a one-option dropdown can't change anything, so showing it would add an inert focus stop announced as a "1 of 1" menu. The switcher appears only when there is a real choice to make.
How the URL is rewritten
Given basePath from SFDC_ENV.basePath, the leading path segment after it is
handled by the chosen language:
DEFAULT_LANGUAGEis the implicit, un-prefixed language, so no segment is written. An existing language segment is removed (/shop/fr/page→/shop/page); a path with no language segment is left unchanged.- Any other language is replaced into an existing language segment (no
/en-US/fr/pagestacking), or inserted when there is none.
The rest of the path, the query string, and the hash are preserved.
Locale codes use underscores (en_US), but the URL path segment uses hyphens
(en-US). The switcher writes the hyphenated form and remaps it back to the
underscore code when reading the current language from the URL.
The table below assumes DEFAULT_LANGUAGE = en_US:
| Before (basePath = /shop) | Pick | After |
| --------------------------- | ------- | --------------------------- |
| /shop/catalog?q=x#top | fr | /shop/fr/catalog?q=x#top |
| /shop/de-DE/catalog | fr | /shop/fr/catalog |
| /shop | fr | /shop/fr |
| /shop/fr/catalog?q=x#top | en_US | /shop/catalog?q=x#top |
| /shop/catalog | en_US | /shop/catalog (unchanged) |
Selecting the language that is already active is a no-op (no reload).
The current language
Resolved in precedence order by getCurrentLanguage():
globalThis.SFDC_ENV.language— authoritative when the platform provides it. The platform injects this in hyphenated form (e.g."en-US"), so it is remapped back to a locale code (en_US) before matching, exactly like the URL segment.- The leading URL path segment after
basePath, remapped from hyphens back to a locale code, when it is a supported language — so a direct hit on/shop/en-US/catalogresolves toen_USeven before the runtime populatesSFDC_ENV.language. DEFAULT_LANGUAGE— when neither of the above yields a supported code.
Only codes present in LANGUAGES are honored; anything else falls through to the
next step.
SFDC_ENVdoes not declarelanguagein the platform types yet.
Text direction (RTL) — drive it from getCurrentLanguage(), not the session locale
This feature does not touch <html dir> (it owns only the URL rewrite). If your app
loads translations via i18next and supports a right-to-left language (Arabic,
Hebrew, Farsi, Urdu, …), the i18n init — not this feature — sets direction, and
there is one trap worth calling out.
The Platform SDK's fetchI18nContext() returns a dir for the session
locale. In B2E that is also the display locale, so ctx.dir is correct. But on a
B2X site the session locale is the guest user's fixed profile language, which
ignores the language this switcher picks — so ctx.dir will not flip to rtl
when the user selects an RTL language, even though <html lang> did. lang and
dir end up disagreeing.
Direction is a property of the language itself, so derive it from the resolved
display language — the same value this feature exposes via getCurrentLanguage() —
using i18next.dir() (which carries a built-in RTL list). In your i18n init:
import i18next from "i18next";
import { getCurrentLanguage } from ".../features/language-switcher";
// Follows the switcher-selected language on a site; in B2E (no switcher / URL
// segment) getCurrentLanguage() resolves to the session locale, so this is a
// no-op there. Falls back to the SDK's session lang only when nothing resolves.
const resolvedLang = getCurrentLanguage() || ctx.lang;
document.documentElement.dir = i18next.dir(resolvedLang); // "rtl" for ar/he/fa/…
document.documentElement.lang = resolvedLang.replace(/_/g, "-"); // BCP-47 formSetting
diris not the same as an RTL layout.dir="rtl"flips text flow, alignment, and caret behavior, but the layout only mirrors (margins, padding, float/flex sides, directional icons) if your CSS uses logical properties (ms-/me-/ps-/pe-/start-/end-in Tailwind) rather than physical ones (ml-/mr-/left-/right-). Converting an app built with physical properties is a separate effort this feature does not perform. This feature ships an ambient augmentation (src/types/globals.d.ts) adding the optionallanguagefield. The runtime populates it in dash form (e.g."language":"en-US") — the platform converts the underscore locale code before injecting it.
Local development
On a deployed Experience site the platform injects SFDC_ENV.language and folds
the active language into SFDC_ENV.basePath (e.g. /shop/fr), recomputing both
from the URL on every load. The generic local Vite dev server does neither —
SFDC_ENV.language is absent and basePath is always / — so the switcher
can't route a non-default language locally (the reload 404s and labels never
flip; only the default language works).
This feature integrates with the site entry of
@salesforce/vite-plugin-ui-bundle. Switch your vite.config.ts to import
@salesforce/vite-plugin-ui-bundle/site and pass the supported LANGUAGES
(a shipped __examples__/vite-config-site-example.ts shows this):
import siteUiBundlePlugin from "@salesforce/vite-plugin-ui-bundle/site";
import { LANGUAGES } from "./src/features/language-switcher/languages";
export default defineConfig({
// The plugin needs the language codes; the first entry is the default.
plugins: [siteUiBundlePlugin({ languages: LANGUAGES.map((l) => l.code) })],
});The site plugin then, for local dev only, treats a leading URL segment that
matches one of LANGUAGES as the active language — injecting SFDC_ENV.language
and a matching basePath, and serving the language-prefixed path (SPA fallback).
This mirrors production, so the switcher changes language live via URL + reload —
no server restart.
Then npm run dev and use the switcher normally: picking French navigates to
/fr, Chinese to /zh-CN, and the default language drops the segment. This has
no effect on a deployed build, and non-site bundles keep using the generic
@salesforce/vite-plugin-ui-bundle import unchanged.
Keeping the language list in sync
The supported languages are hardcoded in
languages.ts
(LANGUAGES + DEFAULT_LANGUAGE). There is no runtime API to read
sfdc_cms__languageSettings on a published site, so the list must be kept in sync
by hand with the site's:
digitalExperiences/site/<siteName>/sfdc_cms__languageSettings/content.jsonWhen the site's supported languages change, update LANGUAGES (and
DEFAULT_LANGUAGE) to match.
Unsupported languages are the app's responsibility. The switcher only ever writes codes from
LANGUAGES, but nothing stops a URL from carrying a language segment that is not configured insfdc_cms__languageSettings— a hand-typed or stale link like/shop/it-IT/catalog, for example. The switcher does not honor it (an unrecognized segment is not treated as a language, sogetCurrentLanguage()falls through toDEFAULT_LANGUAGE), but it also does not strip or rewrite it — the segment stays in the URL and is served by the platform and your React app like any other path. It is up to the app to handle such a URL gracefully (e.g. redirect to a supported language or render a friendly not-found state). If left unhandled, an unsupported language segment can surface as a 404 / error page.
Public API
| Export | Description |
| -------------------- | ------------------------------------------------------------------- |
| LanguageSwitcher | Drop-in <select> component. |
| getCurrentLanguage | Resolves the active language from SFDC_ENV with default fallback. |
| buildLanguageUrl | Pure helper: inserts/replaces the leading language path segment. |
| LANGUAGES | Typed list of supported languages ({ code, label }). |
| DEFAULT_LANGUAGE | Default language code. |
| LANGUAGE_CODES | Set of known codes for membership checks. |
