@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.
- package/dist/LanguageSwitcher.d.ts +40 -0
- package/dist/index.d.ts +3 -0
- package/dist/lib/language-routing.d.ts +59 -0
- package/dist/lib/languages.d.ts +43 -0
- package/dist/setto-client.js +332 -2
- package/dist/setto-client.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/package.json +1 -1
|
@@ -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;
|
package/dist/setto-client.js
CHANGED
|
@@ -1,9 +1,295 @@
|
|
|
1
|
-
import {
|
|
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,
|