@sevenfold/setto-client 0.17.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/index.d.ts +1 -0
- package/dist/lib/language-routing.d.ts +59 -0
- package/dist/setto-client.js +86 -5
- package/dist/setto-client.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { SettoLanguages } from './LanguageSwitcher';
|
|
2
2
|
export { DEFAULT_LANGUAGE, languageName, pickInitialLanguage, siteLanguages, } from './lib/languages';
|
|
3
|
+
export { alternateLinks, languageFromPath, pathForLanguage, redirectForVisitor, type LanguageRoute, } from './lib/language-routing';
|
|
3
4
|
export { SettoProvider, useSetto } from './provider';
|
|
4
5
|
export { GuestEditProvider, useGuestEdit } from './guest-edit';
|
|
5
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;
|
package/dist/setto-client.js
CHANGED
|
@@ -72,6 +72,59 @@ function pickInitialLanguage(available, browserLanguages = typeof navigator ===
|
|
|
72
72
|
}
|
|
73
73
|
return fallback;
|
|
74
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
|
+
}
|
|
75
128
|
const INLINE_MARKER = "data-setto-languages-inline";
|
|
76
129
|
function SettoLanguages({ languages, inline = false }) {
|
|
77
130
|
const { i18n } = useTranslation();
|
|
@@ -109,8 +162,8 @@ function SettoLanguages({ languages, inline = false }) {
|
|
|
109
162
|
setOpen(false);
|
|
110
163
|
if (tag === current) return;
|
|
111
164
|
storeLanguage(tag);
|
|
112
|
-
|
|
113
|
-
|
|
165
|
+
const target = pathForLanguage(window.location.pathname, tag, languages);
|
|
166
|
+
window.location.assign(target + window.location.search + window.location.hash);
|
|
114
167
|
};
|
|
115
168
|
return /* @__PURE__ */ jsxs("div", { ref, style: inline ? inlineWrapStyle : floatingWrapStyle, children: [
|
|
116
169
|
/* @__PURE__ */ jsxs(
|
|
@@ -22006,10 +22059,34 @@ function SettoProvider({ config, children }) {
|
|
|
22006
22059
|
);
|
|
22007
22060
|
useEffect(() => {
|
|
22008
22061
|
if (!i18n || languages.length < 2) return;
|
|
22009
|
-
const
|
|
22010
|
-
|
|
22011
|
-
|
|
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;
|
|
22012
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]);
|
|
22013
22090
|
const storeRef = useRef(null);
|
|
22014
22091
|
const themeStoreRef = useRef(null);
|
|
22015
22092
|
const assetStoreRef = useRef(null);
|
|
@@ -24875,6 +24952,7 @@ export {
|
|
|
24875
24952
|
T,
|
|
24876
24953
|
adminRedirectUrl,
|
|
24877
24954
|
adminViewUrl,
|
|
24955
|
+
alternateLinks,
|
|
24878
24956
|
checkoutIntentUrl,
|
|
24879
24957
|
currentAdminView,
|
|
24880
24958
|
extractContentKeys,
|
|
@@ -24886,11 +24964,14 @@ export {
|
|
|
24886
24964
|
isAdminRoute,
|
|
24887
24965
|
isFormFieldType,
|
|
24888
24966
|
isLinkContext,
|
|
24967
|
+
languageFromPath,
|
|
24889
24968
|
languageName,
|
|
24890
24969
|
nodeChildren,
|
|
24891
24970
|
parseBlockTree,
|
|
24971
|
+
pathForLanguage,
|
|
24892
24972
|
pickInitialLanguage,
|
|
24893
24973
|
readLocaleFields,
|
|
24974
|
+
redirectForVisitor,
|
|
24894
24975
|
settoSubmissionUrl,
|
|
24895
24976
|
siteHomeUrl,
|
|
24896
24977
|
siteLanguages,
|