@salesforce/ui-bundle-template-app-react-template-b2x 11.55.1 → 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.
- package/dist/CHANGELOG.md +8 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/package.json +4 -4
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/language-switcher/LanguageSwitcher.tsx +116 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/language-switcher/__examples__/language-switcher-example.tsx +36 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/language-switcher/__examples__/vite-config-site-example.ts +44 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/language-switcher/buildLanguageUrl.ts +158 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/language-switcher/index.ts +28 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/language-switcher/languages.ts +46 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/README.md +235 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/api/apiUtils.ts +56 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/api/orgApiVersionService.ts +26 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/api/searchableContentTypesService.ts +116 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/channelResolver.ts +40 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/cmsQueryFragment.ts +97 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/contentTypeSessionCache.ts +134 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/contentTypeUtils.ts +38 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/hooks/useSearchableContentTypes.ts +128 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/index.ts +54 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/parseResponse.ts +65 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/searchChannel.ts +15 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/cms/types.ts +66 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/registry.ts +38 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/sobject/index.ts +19 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/sobject/parseResponse.ts +56 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/sobject/queryFragment.ts +141 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/adapters/types.ts +101 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/api/searchService.ts +110 -45
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/GlobalSearchBox.tsx +98 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/MergedSearchResults.tsx +34 -17
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/Search.tsx +31 -13
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/SearchResults.tsx +17 -11
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/SourceSection.tsx +9 -4
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/controls/ScopeSelector.tsx +50 -6
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/results/CmsResultRow.tsx +101 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/results/resolveResultRenderer.ts +49 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/config.json +7 -2
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/constants.ts +8 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/hooks/useSearch.ts +313 -50
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/index.ts +26 -1
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/queryBuilder.ts +62 -118
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/types.ts +74 -5
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/index.ts +5 -3
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/AccountObjectDetailPage.tsx +4 -2
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/Home.tsx +3 -42
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/routes.tsx +12 -5
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/types/globals.d.ts +23 -0
- package/dist/force-app/main/default/uiBundles/reactexternalapp/tsconfig.tsbuildinfo +1 -1
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/package.json +4 -2
- package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/AccountSearch.tsx +0 -25
package/dist/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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
|
+
|
|
6
14
|
## [11.55.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.55.0...v11.55.1) (2026-08-11)
|
|
7
15
|
|
|
8
16
|
**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.
|
|
22
|
-
"@salesforce/ui-bundle": "^11.
|
|
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.
|
|
50
|
-
"@salesforce/vite-plugin-ui-bundle": "^11.
|
|
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));
|