@sevenfold/setto-client 0.16.0 → 0.18.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.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The control a visitor uses to read the site in their own language.
3
+ *
4
+ * Mounted by `SettoProvider` rather than placed by the agent, and only when
5
+ * the site actually speaks more than one language — so it costs a Norwegian
6
+ * plumber's site nothing, and a site that gains a second language grows the
7
+ * control without anybody editing a header.
8
+ *
9
+ * That decision is worth stating because it is a trade. Auto-mounting means
10
+ * the switcher cannot sit inside a design it knows nothing about, so it sits
11
+ * in a corner instead: quiet, small, and out of the way of whatever the agent
12
+ * built. A site that wants it in its own header can render `<SettoLanguages>`
13
+ * anywhere and pass `inline`, and the floating one stands down.
14
+ *
15
+ * Styling is inline for the same reason the editing chrome's is: this renders
16
+ * inside a page whose stylesheet Setto did not write, and most of these sites
17
+ * ship Tailwind's Preflight, which would otherwise strip a bare `<button>`
18
+ * back to nothing.
19
+ */
20
+ export interface SettoLanguagesProps {
21
+ /** Every language the site speaks, primary first. */
22
+ languages: readonly string[];
23
+ /**
24
+ * Rendered in the flow of the page rather than fixed in a corner.
25
+ *
26
+ * Also tells the auto-mounted copy to stand down, so a site that places its
27
+ * own switcher does not end up with two.
28
+ */
29
+ inline?: boolean;
30
+ }
31
+ export declare function SettoLanguages({ languages, inline }: SettoLanguagesProps): import("react/jsx-runtime").JSX.Element | null;
32
+ /**
33
+ * The floating copy, mounted by the provider.
34
+ *
35
+ * Renders nothing when the site places its own, and nothing when there is only
36
+ * one language to be in.
37
+ */
38
+ export declare function AutoLanguageSwitcher({ languages }: {
39
+ languages: readonly string[];
40
+ }): import("react/jsx-runtime").JSX.Element | null;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ export { SettoLanguages } from './LanguageSwitcher';
2
+ export { DEFAULT_LANGUAGE, languageName, pickInitialLanguage, siteLanguages, } from './lib/languages';
3
+ export { alternateLinks, languageFromPath, pathForLanguage, redirectForVisitor, type LanguageRoute, } from './lib/language-routing';
1
4
  export { SettoProvider, useSetto } from './provider';
2
5
  export { GuestEditProvider, useGuestEdit } from './guest-edit';
3
6
  export { SettoChrome, useSettoChromeSlot } from './chrome';
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The language lives in the URL.
3
+ *
4
+ * It did not, for one release: the choice was kept per browser in
5
+ * `localStorage`, which is invisible to a search engine and unshareable
6
+ * between people. A Norwegian plumber who wants to be found in English cannot
7
+ * be, if the English version has no address. So the primary language stays at
8
+ * the root — `/`, `/priser` — and every other language takes a prefix:
9
+ * `/en`, `/en/priser`.
10
+ *
11
+ * Primary at the root rather than `/no/…` deliberately: every Setto site alive
12
+ * today has its Norwegian pages at the root, they are linked to and indexed
13
+ * there, and moving them would trade a real thing for a tidy one.
14
+ *
15
+ * The routes themselves never learn about this. `BrowserRouter` takes the
16
+ * prefix as its `basename`, so a site's own `/priser` keeps matching whether
17
+ * the visitor is at `/priser` or `/en/priser` — which matters because those
18
+ * routes are written per site by the build agent, and a scheme that required
19
+ * it to think about languages is a scheme that would be got wrong.
20
+ */
21
+ export interface LanguageRoute {
22
+ /** The language this URL is in. */
23
+ language: string;
24
+ /** `''` for the primary language, `/en` otherwise. Feed to `basename`. */
25
+ basename: string;
26
+ /** The path with the prefix removed, always starting with `/`. */
27
+ rest: string;
28
+ }
29
+ /**
30
+ * Read the language out of a path.
31
+ *
32
+ * Only a prefix that is a language this site actually speaks counts — a site
33
+ * with a page at `/no` and no Norwegian in its list keeps that page.
34
+ */
35
+ export declare function languageFromPath(pathname: string, languages: readonly string[], primary?: string): LanguageRoute;
36
+ /** The same page, in another language. */
37
+ export declare function pathForLanguage(pathname: string, target: string, languages: readonly string[], primary?: string): string;
38
+ /**
39
+ * Every address this page has, for `hreflang`.
40
+ *
41
+ * Without these a search engine sees two unrelated pages that happen to say
42
+ * similar things, and picks one. `x-default` points at the primary, which is
43
+ * what a reader with no stated preference should land on.
44
+ */
45
+ export declare function alternateLinks(origin: string, pathname: string, languages: readonly string[], primary?: string): Array<{
46
+ hreflang: string;
47
+ href: string;
48
+ }>;
49
+ /**
50
+ * Where a first-time visitor should be sent, or null to leave them alone.
51
+ *
52
+ * Only ever from the unprefixed root towards a prefix, only when the browser
53
+ * asks for a language this site speaks and the visitor has expressed no
54
+ * preference of their own here before. One direction only: a redirect that can
55
+ * send `/en` back to `/` is a redirect that can loop, and somebody who has
56
+ * clicked «Norsk» on an English page must not be bounced back to English on
57
+ * their next click.
58
+ */
59
+ export declare function redirectForVisitor(pathname: string, languages: readonly string[], browserLanguages: readonly string[], stored: string | null, primary?: string): string | null;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Which languages a site speaks, and which one a visitor is reading.
3
+ *
4
+ * Every Setto site has been Norwegian-only, not because the machinery could
5
+ * not hold more — `I18nStore` has taken a language argument since the
6
+ * beginning and the publish path already serialises one bundle per language —
7
+ * but because nothing ever told a site it had a second one, and nothing gave a
8
+ * visitor a way to ask for it.
9
+ *
10
+ * The choice is deliberately per browser and not per URL. A path prefix
11
+ * (`/en/…`) is the better answer for search engines and the wrong answer for
12
+ * this pipeline today: every site is a single-page app whose routes the agent
13
+ * writes, and quietly doubling every route is not a thing to do to thirteen
14
+ * live sites in one release. Recorded here rather than hidden: when these
15
+ * sites want to be found in two languages, this is the decision to revisit.
16
+ */
17
+ /** Norwegian is the floor: every site has it, and it is what the agent writes. */
18
+ export declare const DEFAULT_LANGUAGE = "no";
19
+ /** «Norsk», or the tag itself when we have no name for it. */
20
+ export declare function languageName(tag: string): string;
21
+ /**
22
+ * The languages this site actually speaks, primary first, no duplicates.
23
+ *
24
+ * A configured list that forgets to include the primary language is a site
25
+ * whose own copy is unreachable, so the primary is inserted rather than
26
+ * trusted.
27
+ */
28
+ export declare function siteLanguages(configured: readonly string[] | undefined, primary?: string): string[];
29
+ /** What this browser last chose here, if anything. */
30
+ export declare function readStoredLanguage(): string | null;
31
+ export declare function storeLanguage(tag: string): void;
32
+ /**
33
+ * The language to open in.
34
+ *
35
+ * Their own previous choice first — it is the only signal that is definitely
36
+ * about this site. Then what the browser asks for, matched on the base tag so
37
+ * `en-GB` finds `en`. Then the site's primary, which is always present.
38
+ *
39
+ * Note what is *not* here: no geolocation, and no guessing from a domain. A
40
+ * Norwegian abroad is still Norwegian, and a site that decides it knows better
41
+ * than `Accept-Language` is a site people fight with.
42
+ */
43
+ export declare function pickInitialLanguage(available: readonly string[], browserLanguages?: readonly string[], stored?: string | null): string;
@@ -1,9 +1,295 @@
1
- import { jsx, jsxs, Fragment } from "react/jsx-runtime";
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { useState, useRef, useEffect, useMemo, createContext, useContext, useCallback, useLayoutEffect, useSyncExternalStore, forwardRef, Fragment as Fragment$1, useId, useInsertionEffect } from "react";
4
4
  import { useTranslation } from "react-i18next";
5
5
  import * as ReactDOM from "react-dom";
6
6
  import { createPortal } from "react-dom";
7
+ const STORAGE_KEY$1 = "setto.lang";
8
+ const DEFAULT_LANGUAGE = "no";
9
+ const ENDONYMS = {
10
+ no: "Norsk",
11
+ nb: "Norsk",
12
+ nn: "Nynorsk",
13
+ en: "English",
14
+ sv: "Svenska",
15
+ da: "Dansk",
16
+ de: "Deutsch",
17
+ fr: "Français",
18
+ es: "Español",
19
+ it: "Italiano",
20
+ nl: "Nederlands",
21
+ pl: "Polski",
22
+ pt: "Português",
23
+ fi: "Suomi",
24
+ is: "Íslenska",
25
+ lt: "Lietuvių",
26
+ uk: "Українська",
27
+ ru: "Русский",
28
+ ar: "العربية",
29
+ tr: "Türkçe",
30
+ so: "Soomaali",
31
+ ti: "ትግርኛ",
32
+ th: "ไทย",
33
+ vi: "Tiếng Việt",
34
+ zh: "中文",
35
+ ja: "日本語"
36
+ };
37
+ function languageName(tag) {
38
+ const base = tag.toLowerCase().split("-")[0] ?? tag;
39
+ return ENDONYMS[base] ?? tag.toUpperCase();
40
+ }
41
+ function siteLanguages(configured, primary = DEFAULT_LANGUAGE) {
42
+ const out = [primary];
43
+ for (const tag of configured ?? []) {
44
+ if (tag && !out.includes(tag)) out.push(tag);
45
+ }
46
+ return out;
47
+ }
48
+ function readStoredLanguage() {
49
+ try {
50
+ return window.localStorage.getItem(STORAGE_KEY$1);
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ function storeLanguage(tag) {
56
+ try {
57
+ window.localStorage.setItem(STORAGE_KEY$1, tag);
58
+ } catch {
59
+ }
60
+ }
61
+ function pickInitialLanguage(available, browserLanguages = typeof navigator === "undefined" ? [] : navigator.languages ?? [navigator.language], stored = null) {
62
+ const fallback = available[0] ?? DEFAULT_LANGUAGE;
63
+ if (stored && available.includes(stored)) return stored;
64
+ for (const wanted of browserLanguages) {
65
+ const base = wanted.toLowerCase().split("-")[0];
66
+ if (!base) continue;
67
+ const hit = available.find((tag) => tag.toLowerCase().split("-")[0] === base);
68
+ if (hit) return hit;
69
+ if ((base === "nb" || base === "nn" || base === "no") && available.includes("no")) {
70
+ return "no";
71
+ }
72
+ }
73
+ return fallback;
74
+ }
75
+ function normalise(pathname) {
76
+ const path = pathname.startsWith("/") ? pathname : `/${pathname}`;
77
+ return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
78
+ }
79
+ function languageFromPath(pathname, languages, primary = languages[0] ?? DEFAULT_LANGUAGE) {
80
+ const path = normalise(pathname);
81
+ const first = path.split("/")[1] ?? "";
82
+ const match = languages.find(
83
+ (tag) => tag !== primary && tag.toLowerCase() === first.toLowerCase()
84
+ );
85
+ if (!match) return { language: primary, basename: "", rest: path };
86
+ const rest = path.slice(first.length + 1);
87
+ return { language: match, basename: `/${match}`, rest: rest === "" ? "/" : rest };
88
+ }
89
+ function pathForLanguage(pathname, target, languages, primary = languages[0] ?? DEFAULT_LANGUAGE) {
90
+ const { rest } = languageFromPath(pathname, languages, primary);
91
+ if (target === primary) return rest;
92
+ return rest === "/" ? `/${target}` : `/${target}${rest}`;
93
+ }
94
+ function alternateLinks(origin, pathname, languages, primary = languages[0] ?? DEFAULT_LANGUAGE) {
95
+ if (languages.length < 2) return [];
96
+ const links = languages.map((tag) => ({
97
+ hreflang: tag,
98
+ href: origin + pathForLanguage(pathname, tag, languages, primary)
99
+ }));
100
+ links.push({
101
+ hreflang: "x-default",
102
+ href: origin + pathForLanguage(pathname, primary, languages, primary)
103
+ });
104
+ return links;
105
+ }
106
+ function isReservedPath(pathname) {
107
+ return pathname === "/setto" || pathname.startsWith("/setto/");
108
+ }
109
+ function redirectForVisitor(pathname, languages, browserLanguages, stored, primary = languages[0] ?? DEFAULT_LANGUAGE) {
110
+ if (languages.length < 2) return null;
111
+ if (isReservedPath(pathname)) return null;
112
+ const current = languageFromPath(pathname, languages, primary);
113
+ if (current.basename !== "") return null;
114
+ if (stored) {
115
+ return stored !== primary && languages.includes(stored) ? pathForLanguage(pathname, stored, languages, primary) : null;
116
+ }
117
+ for (const wanted of browserLanguages) {
118
+ const base = wanted.toLowerCase().split("-")[0];
119
+ if (!base) continue;
120
+ if (base === primary.toLowerCase().split("-")[0]) return null;
121
+ const norwegian = ["no", "nb", "nn"];
122
+ if (norwegian.includes(base) && norwegian.includes(primary.toLowerCase())) return null;
123
+ const hit = languages.find((tag) => tag.toLowerCase().split("-")[0] === base);
124
+ if (hit && hit !== primary) return pathForLanguage(pathname, hit, languages, primary);
125
+ }
126
+ return null;
127
+ }
128
+ const INLINE_MARKER = "data-setto-languages-inline";
129
+ function SettoLanguages({ languages, inline = false }) {
130
+ const { i18n } = useTranslation();
131
+ const [open, setOpen] = useState(false);
132
+ const ref = useRef(null);
133
+ useEffect(() => {
134
+ if (!inline) return;
135
+ const root = document.documentElement;
136
+ const count = Number(root.getAttribute(INLINE_MARKER) ?? "0") + 1;
137
+ root.setAttribute(INLINE_MARKER, String(count));
138
+ return () => {
139
+ const left = Number(root.getAttribute(INLINE_MARKER) ?? "1") - 1;
140
+ if (left > 0) root.setAttribute(INLINE_MARKER, String(left));
141
+ else root.removeAttribute(INLINE_MARKER);
142
+ };
143
+ }, [inline]);
144
+ useEffect(() => {
145
+ if (!open) return;
146
+ const onDown = (e) => {
147
+ if (!ref.current?.contains(e.target)) setOpen(false);
148
+ };
149
+ const onKey = (e) => {
150
+ if (e.key === "Escape") setOpen(false);
151
+ };
152
+ document.addEventListener("mousedown", onDown);
153
+ document.addEventListener("keydown", onKey);
154
+ return () => {
155
+ document.removeEventListener("mousedown", onDown);
156
+ document.removeEventListener("keydown", onKey);
157
+ };
158
+ }, [open]);
159
+ if (languages.length < 2) return null;
160
+ const current = i18n.language;
161
+ const choose = (tag) => {
162
+ setOpen(false);
163
+ if (tag === current) return;
164
+ storeLanguage(tag);
165
+ const target = pathForLanguage(window.location.pathname, tag, languages);
166
+ window.location.assign(target + window.location.search + window.location.hash);
167
+ };
168
+ return /* @__PURE__ */ jsxs("div", { ref, style: inline ? inlineWrapStyle : floatingWrapStyle, children: [
169
+ /* @__PURE__ */ jsxs(
170
+ "button",
171
+ {
172
+ type: "button",
173
+ onClick: () => setOpen((o) => !o),
174
+ "aria-expanded": open,
175
+ "aria-haspopup": "listbox",
176
+ "aria-label": `Språk: ${languageName(current)}`,
177
+ style: triggerStyle,
178
+ children: [
179
+ /* @__PURE__ */ jsx(GlobeIcon, {}),
180
+ languageName(current)
181
+ ]
182
+ }
183
+ ),
184
+ open ? /* @__PURE__ */ jsx("ul", { role: "listbox", style: inline ? inlineListStyle : floatingListStyle, children: languages.map((tag) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
185
+ "button",
186
+ {
187
+ type: "button",
188
+ role: "option",
189
+ "aria-selected": tag === current,
190
+ onClick: () => choose(tag),
191
+ style: {
192
+ ...optionStyle,
193
+ fontWeight: tag === current ? 600 : 400
194
+ },
195
+ children: languageName(tag)
196
+ }
197
+ ) }, tag)) }) : null
198
+ ] });
199
+ }
200
+ function AutoLanguageSwitcher({ languages }) {
201
+ const [inlineElsewhere, setInlineElsewhere] = useState(false);
202
+ useEffect(() => {
203
+ const check = () => setInlineElsewhere(document.documentElement.hasAttribute(INLINE_MARKER));
204
+ check();
205
+ const observer = new MutationObserver(check);
206
+ observer.observe(document.documentElement, {
207
+ attributes: true,
208
+ attributeFilter: [INLINE_MARKER]
209
+ });
210
+ return () => observer.disconnect();
211
+ }, []);
212
+ if (inlineElsewhere || languages.length < 2) return null;
213
+ return /* @__PURE__ */ jsx(SettoLanguages, { languages });
214
+ }
215
+ function GlobeIcon() {
216
+ return /* @__PURE__ */ jsxs(
217
+ "svg",
218
+ {
219
+ width: "15",
220
+ height: "15",
221
+ viewBox: "0 0 24 24",
222
+ fill: "none",
223
+ stroke: "currentColor",
224
+ strokeWidth: "1.8",
225
+ strokeLinecap: "round",
226
+ strokeLinejoin: "round",
227
+ "aria-hidden": "true",
228
+ children: [
229
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
230
+ /* @__PURE__ */ jsx("path", { d: "M3 12h18M12 3c2.4 2.6 3.6 5.6 3.6 9s-1.2 6.4-3.6 9c-2.4-2.6-3.6-5.6-3.6-9S9.6 5.6 12 3z" })
231
+ ]
232
+ }
233
+ );
234
+ }
235
+ const floatingWrapStyle = {
236
+ position: "fixed",
237
+ left: "max(16px, env(safe-area-inset-left))",
238
+ bottom: "max(16px, env(safe-area-inset-bottom))",
239
+ zIndex: 2147482e3,
240
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
241
+ };
242
+ const inlineWrapStyle = {
243
+ position: "relative",
244
+ display: "inline-block",
245
+ fontFamily: "inherit"
246
+ };
247
+ const triggerStyle = {
248
+ display: "inline-flex",
249
+ alignItems: "center",
250
+ gap: 7,
251
+ minHeight: 40,
252
+ padding: "8px 14px",
253
+ borderRadius: 999,
254
+ border: "1px solid rgba(127, 127, 127, 0.32)",
255
+ // Translucent over whatever the site put behind it, and legible either way.
256
+ background: "rgba(255, 255, 255, 0.82)",
257
+ backdropFilter: "blur(8px)",
258
+ WebkitBackdropFilter: "blur(8px)",
259
+ color: "#1a1a1a",
260
+ fontSize: 13.5,
261
+ fontWeight: 500,
262
+ lineHeight: 1,
263
+ cursor: "pointer",
264
+ boxShadow: "0 4px 14px rgba(0, 0, 0, 0.12)"
265
+ };
266
+ const listBase = {
267
+ position: "absolute",
268
+ margin: 0,
269
+ padding: 4,
270
+ listStyle: "none",
271
+ minWidth: 150,
272
+ borderRadius: 12,
273
+ border: "1px solid rgba(127, 127, 127, 0.24)",
274
+ background: "#ffffff",
275
+ boxShadow: "0 12px 32px rgba(0, 0, 0, 0.18)",
276
+ zIndex: 1
277
+ };
278
+ const floatingListStyle = { ...listBase, left: 0, bottom: "calc(100% + 8px)" };
279
+ const inlineListStyle = { ...listBase, left: 0, top: "calc(100% + 8px)" };
280
+ const optionStyle = {
281
+ display: "block",
282
+ width: "100%",
283
+ minHeight: 40,
284
+ padding: "9px 12px",
285
+ border: 0,
286
+ borderRadius: 8,
287
+ background: "transparent",
288
+ color: "#1a1a1a",
289
+ fontSize: 13.5,
290
+ textAlign: "left",
291
+ cursor: "pointer"
292
+ };
7
293
  function __rest(s, e) {
8
294
  var t = {};
9
295
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
@@ -21767,6 +22053,40 @@ function SettoProvider({ config, children }) {
21767
22053
  }, []);
21768
22054
  const access = useSiteAccess(supabase, session, config.siteId);
21769
22055
  const { i18n } = useTranslation();
22056
+ const languages = useMemo(
22057
+ () => siteLanguages(config.languages, config.languages?.[0]),
22058
+ [config.languages]
22059
+ );
22060
+ useEffect(() => {
22061
+ if (!i18n || languages.length < 2) return;
22062
+ const redirect = redirectForVisitor(
22063
+ window.location.pathname,
22064
+ languages,
22065
+ navigator.languages ?? [navigator.language],
22066
+ readStoredLanguage()
22067
+ );
22068
+ if (redirect && redirect !== window.location.pathname) {
22069
+ window.location.replace(redirect + window.location.search + window.location.hash);
22070
+ return;
22071
+ }
22072
+ const { language } = languageFromPath(window.location.pathname, languages);
22073
+ if (language !== i18n.language) void i18n.changeLanguage(language);
22074
+ document.documentElement.lang = language;
22075
+ }, [i18n, languages]);
22076
+ useEffect(() => {
22077
+ if (languages.length < 2) return;
22078
+ const links = alternateLinks(window.location.origin, window.location.pathname, languages);
22079
+ const added = links.map(({ hreflang, href }) => {
22080
+ const el = document.createElement("link");
22081
+ el.rel = "alternate";
22082
+ el.hreflang = hreflang;
22083
+ el.href = href;
22084
+ el.dataset.settoAlternate = "";
22085
+ document.head.appendChild(el);
22086
+ return el;
22087
+ });
22088
+ return () => added.forEach((el) => el.remove());
22089
+ }, [languages]);
21770
22090
  const storeRef = useRef(null);
21771
22091
  const themeStoreRef = useRef(null);
21772
22092
  const assetStoreRef = useRef(null);
@@ -21809,7 +22129,8 @@ function SettoProvider({ config, children }) {
21809
22129
  const editShellActive = editMode && !!value.store;
21810
22130
  return /* @__PURE__ */ jsx(SettoContext.Provider, { value, children: /* @__PURE__ */ jsxs(Fragment, { children: [
21811
22131
  /* @__PURE__ */ jsx(EditModeShell, { active: editShellActive, themeStore: value.themeStore, children }),
21812
- /* @__PURE__ */ jsx(SettoChrome, {})
22132
+ /* @__PURE__ */ jsx(SettoChrome, {}),
22133
+ /* @__PURE__ */ jsx(AutoLanguageSwitcher, { languages })
21813
22134
  ] }) });
21814
22135
  }
21815
22136
  function useSetto() {
@@ -24603,6 +24924,7 @@ export {
24603
24924
  BLOCK_REGISTRY,
24604
24925
  BLOCK_TYPES,
24605
24926
  CHECKOUT_INTENT,
24927
+ DEFAULT_LANGUAGE,
24606
24928
  EMPTY_I18N_STORE_SNAPSHOT,
24607
24929
  FAB_INSET,
24608
24930
  FAB_SIZE,
@@ -24621,6 +24943,7 @@ export {
24621
24943
  SettoForm,
24622
24944
  SettoIcon,
24623
24945
  SettoImage,
24946
+ SettoLanguages,
24624
24947
  SettoMark,
24625
24948
  SettoProvider,
24626
24949
  SettoRepeater,
@@ -24629,6 +24952,7 @@ export {
24629
24952
  T,
24630
24953
  adminRedirectUrl,
24631
24954
  adminViewUrl,
24955
+ alternateLinks,
24632
24956
  checkoutIntentUrl,
24633
24957
  currentAdminView,
24634
24958
  extractContentKeys,
@@ -24640,11 +24964,17 @@ export {
24640
24964
  isAdminRoute,
24641
24965
  isFormFieldType,
24642
24966
  isLinkContext,
24967
+ languageFromPath,
24968
+ languageName,
24643
24969
  nodeChildren,
24644
24970
  parseBlockTree,
24971
+ pathForLanguage,
24972
+ pickInitialLanguage,
24645
24973
  readLocaleFields,
24974
+ redirectForVisitor,
24646
24975
  settoSubmissionUrl,
24647
24976
  siteHomeUrl,
24977
+ siteLanguages,
24648
24978
  useCoarsePointer,
24649
24979
  useGuestEdit,
24650
24980
  useSectionTheme,