@salesforce/ui-bundle-template-app-react-template-b2x 11.55.0 → 11.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/uiBundles/reactexternalapp/package.json +4 -4
  3. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/language-switcher/LanguageSwitcher.tsx +116 -0
  4. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/language-switcher/__examples__/language-switcher-example.tsx +36 -0
  5. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/language-switcher/__examples__/vite-config-site-example.ts +44 -0
  6. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/language-switcher/buildLanguageUrl.ts +158 -0
  7. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/language-switcher/index.ts +28 -0
  8. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/language-switcher/languages.ts +46 -0
  9. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/README.md +235 -0
  10. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/api/apiUtils.ts +56 -0
  11. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/api/orgApiVersionService.ts +26 -0
  12. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/api/searchableContentTypesService.ts +116 -0
  13. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/channelResolver.ts +40 -0
  14. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/cmsQueryFragment.ts +97 -0
  15. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/contentTypeSessionCache.ts +134 -0
  16. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/contentTypeUtils.ts +38 -0
  17. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/hooks/useSearchableContentTypes.ts +128 -0
  18. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/index.ts +54 -0
  19. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/parseResponse.ts +65 -0
  20. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/searchChannel.ts +15 -0
  21. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/types.ts +66 -0
  22. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/registry.ts +38 -0
  23. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/sobject/index.ts +19 -0
  24. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/sobject/parseResponse.ts +56 -0
  25. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/sobject/queryFragment.ts +141 -0
  26. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/types.ts +101 -0
  27. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/api/searchService.ts +110 -45
  28. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/GlobalSearchBox.tsx +98 -0
  29. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/MergedSearchResults.tsx +34 -17
  30. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/Search.tsx +31 -13
  31. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/SearchResults.tsx +17 -11
  32. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/SourceSection.tsx +9 -4
  33. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/controls/ScopeSelector.tsx +50 -6
  34. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/results/CmsResultRow.tsx +101 -0
  35. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/results/resolveResultRenderer.ts +49 -0
  36. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/config.json +7 -2
  37. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/constants.ts +8 -0
  38. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/hooks/useSearch.ts +313 -50
  39. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/index.ts +26 -1
  40. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/queryBuilder.ts +62 -118
  41. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/types.ts +74 -5
  42. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/index.ts +5 -3
  43. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/AccountObjectDetailPage.tsx +4 -2
  44. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/Home.tsx +3 -42
  45. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/routes.tsx +12 -5
  46. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/types/globals.d.ts +23 -0
  47. package/dist/force-app/main/default/uiBundles/reactexternalapp/tsconfig.tsbuildinfo +1 -1
  48. package/dist/package-lock.json +2 -2
  49. package/dist/package.json +1 -1
  50. package/package.json +4 -2
  51. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/AccountSearch.tsx +0 -25
package/dist/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [11.56.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.55.1...v11.56.0) (2026-08-12)
7
+
8
+ **Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
9
+
10
+
11
+
12
+
13
+
14
+ ## [11.55.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.55.0...v11.55.1) (2026-08-11)
15
+
16
+ **Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
17
+
18
+
19
+
20
+
21
+
6
22
  ## [11.55.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.54.1...v11.55.0) (2026-08-11)
7
23
 
8
24
  **Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
@@ -18,8 +18,8 @@
18
18
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
19
19
  },
20
20
  "dependencies": {
21
- "@salesforce/platform-sdk": "^11.55.0",
22
- "@salesforce/ui-bundle": "^11.55.0",
21
+ "@salesforce/platform-sdk": "^11.56.0",
22
+ "@salesforce/ui-bundle": "^11.56.0",
23
23
  "@tailwindcss/vite": "^4.1.17",
24
24
  "class-variance-authority": "^0.7.1",
25
25
  "clsx": "^2.1.1",
@@ -46,8 +46,8 @@
46
46
  "@graphql-eslint/eslint-plugin": "^4.1.0",
47
47
  "@graphql-tools/utils": "^11.0.0",
48
48
  "@playwright/test": "^1.49.0",
49
- "@salesforce/graphiti": "^11.55.0",
50
- "@salesforce/vite-plugin-ui-bundle": "^11.55.0",
49
+ "@salesforce/graphiti": "^11.56.0",
50
+ "@salesforce/vite-plugin-ui-bundle": "^11.56.0",
51
51
  "@testing-library/jest-dom": "^6.6.3",
52
52
  "@testing-library/react": "^16.1.0",
53
53
  "@testing-library/user-event": "^14.5.2",
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ /**
8
+ * Drop-in language switcher for B2X (Experience) apps.
9
+ *
10
+ * Renders a labeled `<select>` of the site's supported {@link LANGUAGES}.
11
+ * Choosing a language rewrites the current URL so the chosen code becomes the
12
+ * first path segment after `basePath` (see {@link buildLanguageUrl}) and reloads
13
+ * the page via `window.location.assign`. A full reload lets the platform serve
14
+ * the correctly localized content — this component owns only the URL rewrite.
15
+ */
16
+
17
+ import { buildLanguageUrl, getLanguageFromUrl, urlSegmentToCode } from "./buildLanguageUrl";
18
+ import { DEFAULT_LANGUAGE, LANGUAGE_CODES, LANGUAGES } from "./languages";
19
+
20
+ /**
21
+ * The subset of `SFDC_ENV` this component reads. We type it structurally here
22
+ * (rather than relying on the ambient `SfdcEnv`) so the feature type-checks in
23
+ * isolation: its ambient augmentation intentionally declares only `language?`,
24
+ * while `basePath` is provided by the platform types once composed into an app.
25
+ */
26
+ interface SfdcEnvSubset {
27
+ basePath?: string;
28
+ /** Page locale in hyphenated form, e.g. "en-US" (the platform's injected form). */
29
+ language?: string;
30
+ }
31
+
32
+ /** Read `SFDC_ENV` off the global scope without assuming it exists. */
33
+ function getSfdcEnv(): SfdcEnvSubset | undefined {
34
+ return (globalThis as { SFDC_ENV?: SfdcEnvSubset }).SFDC_ENV;
35
+ }
36
+
37
+ /**
38
+ * Resolve the active language, in precedence order:
39
+ *
40
+ * 1. `SFDC_ENV.language` — authoritative when the platform provides it. The
41
+ * platform injects this in hyphenated form (e.g. `"en-US"`), so it is
42
+ * remapped back to a locale code (`en_US`) before matching, mirroring how the
43
+ * URL segment is handled.
44
+ * 2. The leading URL path segment after `basePath`, when it is a supported
45
+ * language code — reflects the page the user is actually viewing (e.g. a
46
+ * direct hit on `/shop/fr/catalog`) even before the runtime populates
47
+ * `SFDC_ENV.language`.
48
+ * 3. {@link DEFAULT_LANGUAGE}.
49
+ *
50
+ * Only codes in {@link LANGUAGES} are honored; anything else falls through.
51
+ */
52
+ export function getCurrentLanguage(): string {
53
+ const envLanguage = getSfdcEnv()?.language;
54
+ if (envLanguage) {
55
+ const envCode = urlSegmentToCode(envLanguage);
56
+ if (LANGUAGE_CODES.has(envCode)) {
57
+ return envCode;
58
+ }
59
+ }
60
+
61
+ const basePath = getSfdcEnv()?.basePath ?? "";
62
+ const urlLanguage = getLanguageFromUrl(window.location.href, basePath, LANGUAGE_CODES);
63
+ return urlLanguage ?? DEFAULT_LANGUAGE;
64
+ }
65
+
66
+ export interface LanguageSwitcherProps {
67
+ /** Accessible label for the control. Defaults to "Language". */
68
+ label?: string;
69
+ /** Extra classes merged onto the `<select>`. */
70
+ className?: string;
71
+ }
72
+
73
+ export function LanguageSwitcher({ label = "Language", className }: LanguageSwitcherProps) {
74
+ // Render nothing when there is no real choice to make. A single-option
75
+ // dropdown is an inert control: it can't change anything, yet it still takes
76
+ // a focus stop and is announced as a "1 of 1" menu, implying options that
77
+ // don't exist. Omitting it entirely keeps the accessibility tree honest.
78
+ if (LANGUAGES.length < 2) {
79
+ return null;
80
+ }
81
+
82
+ const current = getCurrentLanguage();
83
+
84
+ const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
85
+ const next = event.target.value;
86
+ // No-op when the selection already matches the active language.
87
+ if (next === current) return;
88
+
89
+ const basePath = getSfdcEnv()?.basePath ?? "";
90
+ const nextUrl = buildLanguageUrl(
91
+ window.location.href,
92
+ basePath,
93
+ next,
94
+ LANGUAGE_CODES,
95
+ DEFAULT_LANGUAGE,
96
+ );
97
+ window.location.assign(nextUrl);
98
+ };
99
+
100
+ return (
101
+ <select
102
+ aria-label={label}
103
+ value={current}
104
+ onChange={handleChange}
105
+ className={
106
+ className ?? "rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground"
107
+ }
108
+ >
109
+ {LANGUAGES.map((language) => (
110
+ <option key={language.code} value={language.code}>
111
+ {language.label}
112
+ </option>
113
+ ))}
114
+ </select>
115
+ );
116
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ /**
8
+ * Example: mounting <LanguageSwitcher /> in an app layout/header.
9
+ *
10
+ * This file is illustrative — it is copied into the app as an example, not wired
11
+ * into routing. To adopt the switcher, render <LanguageSwitcher /> wherever your
12
+ * app's chrome lives (typically the nav/header in `appLayout.tsx`). It manages
13
+ * its own state from the URL + SFDC_ENV, so no props are required.
14
+ */
15
+
16
+ import { Outlet } from "react-router";
17
+ import { LanguageSwitcher } from "../index";
18
+
19
+ export default function AppLayoutWithLanguageSwitcher() {
20
+ return (
21
+ <>
22
+ <nav className="bg-white border-b border-gray-200">
23
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
24
+ <div className="flex justify-between items-center h-16">
25
+ <span className="text-xl font-semibold text-gray-900">React App</span>
26
+ <div className="flex items-center gap-2">
27
+ {/* Drop the switcher anywhere in your header/chrome. */}
28
+ <LanguageSwitcher />
29
+ </div>
30
+ </div>
31
+ </div>
32
+ </nav>
33
+ <Outlet />
34
+ </>
35
+ );
36
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ /**
8
+ * Example: switching your app's `vite.config.ts` to the SITE entry of
9
+ * `@salesforce/vite-plugin-ui-bundle` so language switching works in local dev.
10
+ *
11
+ * This file is illustrative — it is copied into the app as an example, not used
12
+ * directly. Apply the two changes below to your real `vite.config.ts`.
13
+ *
14
+ * WHY: On a deployed Experience site the platform injects `SFDC_ENV.language`
15
+ * and folds the active language into `SFDC_ENV.basePath` (e.g. `/shop/fr`),
16
+ * recomputed from the URL on every load. The generic local Vite dev server does
17
+ * neither — `SFDC_ENV.language` is absent and `basePath` is always `/` — so the
18
+ * switcher can't route a non-default language locally (the reload 404s and the
19
+ * labels never flip). The SITE entry mirrors production for local dev only.
20
+ *
21
+ * HOW: change the import from the generic plugin to the `/site` subpath, and
22
+ * pass the feature's supported `LANGUAGES` (the single source of truth). The
23
+ * plugin needs the language CODES, so map the `{ code, label }` list to codes.
24
+ * The FIRST entry is the default, served un-prefixed at `/` — matching
25
+ * `DEFAULT_LANGUAGE` in `languages.ts`.
26
+ *
27
+ * This has NO effect on a deployed build; non-site bundles keep the generic
28
+ * `@salesforce/vite-plugin-ui-bundle` import unchanged.
29
+ */
30
+
31
+ // 1. Replace the generic plugin import:
32
+ // import salesforce from "@salesforce/vite-plugin-ui-bundle";
33
+ // with the site entry + the feature's language list:
34
+ import siteUiBundlePlugin from "@salesforce/vite-plugin-ui-bundle/site";
35
+ import { LANGUAGES } from "../languages";
36
+
37
+ // 2. In your `defineConfig({ plugins: [...] })`, replace the `salesforce()`
38
+ // call with `siteUiBundlePlugin(...)`, passing the language codes:
39
+ export const examplePlugins = [
40
+ // tailwindcss(),
41
+ // react(),
42
+ siteUiBundlePlugin({ languages: LANGUAGES.map((l) => l.code) }),
43
+ // ...codegen
44
+ ];
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ /** Strip leading and trailing slashes from a path fragment. */
8
+ function trimSlashes(value: string): string {
9
+ return value.replace(/^\/+/, "").replace(/\/+$/, "");
10
+ }
11
+
12
+ /**
13
+ * Convert a Salesforce locale code to the form used in the URL path.
14
+ *
15
+ * Locale codes use underscores (e.g. `en_US`), but URL path segments use hyphens
16
+ * (e.g. `en-US`). This maps a code to its URL segment.
17
+ */
18
+ function codeToUrlSegment(code: string): string {
19
+ return code.replace(/_/g, "-");
20
+ }
21
+
22
+ /**
23
+ * Convert a hyphenated language token back to a Salesforce locale code — the
24
+ * inverse of {@link codeToUrlSegment} (e.g. `en-US` → `en_US`). Locale codes
25
+ * never contain hyphens, so this remapping is unambiguous. Used both for URL
26
+ * path segments and for the hyphenated `SFDC_ENV.language` value the platform
27
+ * injects (e.g. `"en-US"`), which share the same hyphen convention.
28
+ */
29
+ export function urlSegmentToCode(segment: string): string {
30
+ return segment.replace(/-/g, "_");
31
+ }
32
+
33
+ /**
34
+ * Recover the true, language-free site root from `basePath`.
35
+ *
36
+ * When a non-default language is active, the platform folds the language segment
37
+ * into `SFDC_ENV.basePath` itself — `/shop` becomes `/shop/fr` — rather than
38
+ * leaving `basePath` as the stable site root with the language living in the path
39
+ * after it. If we took `basePath` at face value, the language segment would be
40
+ * treated as part of the base and never seen as a leading language segment, so
41
+ * switching to the default (which should remove the segment) and switching
42
+ * between non-default languages (which should replace it) would both fail.
43
+ *
44
+ * Strip a single trailing segment when it is a known language (remapped from its
45
+ * URL form back to a locale code), yielding the language-free site root so the
46
+ * rest of this module can treat the leading path segment after it as the language.
47
+ */
48
+ function normalizeBasePath(basePath: string, knownCodes: ReadonlySet<string>): string {
49
+ const segments = trimSlashes(basePath).split("/").filter(Boolean);
50
+ const last = segments[segments.length - 1];
51
+ if (last && knownCodes.has(urlSegmentToCode(last))) {
52
+ segments.pop();
53
+ }
54
+ return segments.join("/");
55
+ }
56
+
57
+ /**
58
+ * Return the path segments that live AFTER `basePath` in `pathname`.
59
+ * Both are compared with leading/trailing slashes normalized.
60
+ */
61
+ function segmentsAfterBase(pathname: string, basePath: string): string[] {
62
+ const base = trimSlashes(basePath);
63
+ const trimmedPath = trimSlashes(pathname);
64
+
65
+ let rest = trimmedPath;
66
+ if (base && (trimmedPath === base || trimmedPath.startsWith(`${base}/`))) {
67
+ rest = trimmedPath.slice(base.length);
68
+ }
69
+ return trimSlashes(rest).split("/").filter(Boolean);
70
+ }
71
+
72
+ /**
73
+ * Read the language encoded in a URL: the first path segment after `basePath`,
74
+ * remapped from its URL form (hyphens) back to a locale code (underscores) and
75
+ * returned only when it is one of `knownCodes`. Returns `undefined` when the
76
+ * leading segment is absent or is not a known language code.
77
+ *
78
+ * @param currentUrl Absolute URL to inspect (e.g. `window.location.href`).
79
+ * @param basePath Site root path (e.g. `SFDC_ENV.basePath`); may be `""`.
80
+ * @param knownCodes Language codes considered valid leading segments.
81
+ * @returns The matched locale code (e.g. `en_US`), or `undefined`.
82
+ */
83
+ export function getLanguageFromUrl(
84
+ currentUrl: string,
85
+ basePath: string,
86
+ knownCodes: ReadonlySet<string>,
87
+ ): string | undefined {
88
+ const { pathname } = new URL(currentUrl);
89
+ const siteRoot = normalizeBasePath(basePath, knownCodes);
90
+ const [first] = segmentsAfterBase(pathname, siteRoot);
91
+ if (!first) {
92
+ return undefined;
93
+ }
94
+ const code = urlSegmentToCode(first);
95
+ return knownCodes.has(code) ? code : undefined;
96
+ }
97
+
98
+ /**
99
+ * Rewrite `currentUrl` so `language` is reflected in the first path segment after
100
+ * `basePath`. The rest of the path, the query string, and the hash are preserved.
101
+ *
102
+ * The leading segment after `basePath` is examined and remapped from its URL form
103
+ * (hyphens) back to a locale code before being compared against `knownCodes`:
104
+ *
105
+ * - **Default language** (`language === defaultLanguage`): the default is the
106
+ * implicit, un-prefixed language, so NO segment is written. An existing
107
+ * known-language leading segment is REMOVED (`/shop/fr/page` → `/shop/page`);
108
+ * if there is none, the path is left unchanged.
109
+ * - **Non-default language**: an existing known-language leading segment is
110
+ * REPLACED — so switching never stacks segments (`/en-US/fr/page`) — otherwise
111
+ * the language is INSERTED as a new first segment. The written segment uses the
112
+ * URL form of the code (underscores → hyphens, e.g. `en_US` → `en-US`).
113
+ *
114
+ * When a non-default language is active the platform folds that language into
115
+ * `basePath` itself (`/shop` → `/shop/fr`); a trailing known-language segment is
116
+ * stripped from `basePath` first (see {@link normalizeBasePath}) so the leading
117
+ * segment after the true site root is always what gets replaced/removed/inserted.
118
+ *
119
+ * `basePath` is matched with trailing slashes normalized, and the result never
120
+ * contains double slashes.
121
+ *
122
+ * @param currentUrl Absolute URL to rewrite (e.g. `window.location.href`).
123
+ * @param basePath Site root path (e.g. `SFDC_ENV.basePath`); may be `""`.
124
+ * @param language Locale code to reflect after `basePath` (e.g. `en_US`).
125
+ * @param knownCodes Language codes eligible for the leading-segment check.
126
+ * @param defaultLanguage Locale code that is served without a URL segment.
127
+ * @returns The rewritten absolute URL as a string.
128
+ */
129
+ export function buildLanguageUrl(
130
+ currentUrl: string,
131
+ basePath: string,
132
+ language: string,
133
+ knownCodes: ReadonlySet<string>,
134
+ defaultLanguage: string,
135
+ ): string {
136
+ const url = new URL(currentUrl);
137
+
138
+ const base = normalizeBasePath(basePath, knownCodes);
139
+ const segments = segmentsAfterBase(url.pathname, base);
140
+ const leadingIsLanguage = segments.length > 0 && knownCodes.has(urlSegmentToCode(segments[0]));
141
+
142
+ if (language === defaultLanguage) {
143
+ // The default language is served un-prefixed. Drop an existing language
144
+ // segment so switching back to default removes it, rather than writing one.
145
+ if (leadingIsLanguage) {
146
+ segments.shift();
147
+ }
148
+ } else if (leadingIsLanguage) {
149
+ segments[0] = codeToUrlSegment(language);
150
+ } else {
151
+ segments.unshift(codeToUrlSegment(language));
152
+ }
153
+
154
+ const rebuiltPath = [base, ...segments].filter(Boolean).join("/");
155
+ url.pathname = `/${rebuiltPath}`;
156
+
157
+ return url.toString();
158
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ /**
8
+ * Public API for the language switcher feature.
9
+ *
10
+ * Most apps only need:
11
+ * ```tsx
12
+ * import { LanguageSwitcher } from ".../features/language-switcher";
13
+ * <LanguageSwitcher />
14
+ * ```
15
+ *
16
+ * Lower-level pieces are exported for custom UIs and testing:
17
+ * - {@link getCurrentLanguage} to read the active language
18
+ * - {@link buildLanguageUrl} to compute the rewritten URL yourself
19
+ * - {@link LANGUAGES} / {@link DEFAULT_LANGUAGE} for the supported set
20
+ */
21
+
22
+ export { LanguageSwitcher, getCurrentLanguage } from "./LanguageSwitcher";
23
+ export type { LanguageSwitcherProps } from "./LanguageSwitcher";
24
+
25
+ export { buildLanguageUrl, getLanguageFromUrl } from "./buildLanguageUrl";
26
+
27
+ export { LANGUAGES, DEFAULT_LANGUAGE, LANGUAGE_CODES } from "./languages";
28
+ export type { LanguageOption } from "./languages";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ /** A single language option offered by the switcher. */
8
+ export interface LanguageOption {
9
+ /** Salesforce locale code, e.g. "en_US". Used as the URL path segment. */
10
+ code: string;
11
+ /** Human-readable label shown in the dropdown, e.g. "English". */
12
+ label: string;
13
+ }
14
+
15
+ /**
16
+ * The languages this app offers in the switcher.
17
+ *
18
+ * IMPORTANT — keep this list in sync BY HAND with the site's language settings:
19
+ * digitalExperiences/site/<siteName>/sfdc_cms__languageSettings/content.json
20
+ *
21
+ * There is no runtime API to read `sfdc_cms__languageSettings` on a published
22
+ * site, so the supported languages are baked into the bundle at authoring time.
23
+ * When the site's content.json changes, update this array (and
24
+ * {@link DEFAULT_LANGUAGE}) to match.
25
+ *
26
+ * `code` values are Salesforce locale codes (underscores). They are converted to
27
+ * hyphenated form for use as the first URL path segment after `basePath` (e.g.
28
+ * `en_US` → `/en-US/...`, `fr` → `/fr/...`). Display order here is the order shown
29
+ * in the dropdown.
30
+ */
31
+ export const LANGUAGES: readonly LanguageOption[] = [
32
+ { code: "en_US", label: "English" },
33
+ { code: "fr", label: "Français" },
34
+ { code: "es", label: "Español" },
35
+ { code: "de", label: "Deutsch" },
36
+ { code: "ja", label: "日本語" },
37
+ ];
38
+
39
+ /**
40
+ * Language used when `SFDC_ENV.language` is missing or is not one of
41
+ * {@link LANGUAGES}. Should match the site's default language in content.json.
42
+ */
43
+ export const DEFAULT_LANGUAGE = "en_US";
44
+
45
+ /** Set of known language codes, for fast membership checks. */
46
+ export const LANGUAGE_CODES: ReadonlySet<string> = new Set(LANGUAGES.map((l) => l.code));