The site now exists in German (at the root, with /impressum and /datenschutz) and English (/en). A request without a language follows the visitor's choice from the language switch, then the browser language, then German; the old English legal addresses move to /en for good. All visible texts are data in src/content/de.ts and en.ts, so components hold none of their own. The header carries a DE/EN switch whose bit curtain covers the page while it is swapped and reopens at the same reading position. Title, description, canonical, hreflang, Open Graph, the share image, the sitemap and the structured data are per language. The privacy notice now describes the cookie that remembers the chosen language. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
299 lines
8.9 KiB
TypeScript
299 lines
8.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
languageAlternates,
|
|
localeOfPath,
|
|
pathFor,
|
|
routeRequest,
|
|
sitemapEntries,
|
|
switchPath,
|
|
} from "./routes";
|
|
|
|
describe("pathFor", () => {
|
|
it("serves German at the root, with German names for the legal pages", () => {
|
|
expect(pathFor("home", "de")).toBe("/");
|
|
expect(pathFor("imprint", "de")).toBe("/impressum");
|
|
expect(pathFor("privacy", "de")).toBe("/datenschutz");
|
|
});
|
|
|
|
it("serves English under /en", () => {
|
|
expect(pathFor("home", "en")).toBe("/en");
|
|
expect(pathFor("imprint", "en")).toBe("/en/imprint");
|
|
expect(pathFor("privacy", "en")).toBe("/en/privacy");
|
|
});
|
|
});
|
|
|
|
describe("languageAlternates", () => {
|
|
it("names both languages of a page and German as the fallback", () => {
|
|
expect(languageAlternates("home")).toEqual({
|
|
de: "/",
|
|
en: "/en",
|
|
"x-default": "/",
|
|
});
|
|
expect(languageAlternates("privacy")).toEqual({
|
|
de: "/datenschutz",
|
|
en: "/en/privacy",
|
|
"x-default": "/datenschutz",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("localeOfPath", () => {
|
|
it.each([
|
|
["/", "de"],
|
|
["/impressum", "de"],
|
|
["/en", "en"],
|
|
["/en/", "en"],
|
|
["/en/imprint", "en"],
|
|
["/en/anything/else", "en"],
|
|
// Only a whole first segment counts.
|
|
["/energy", "de"],
|
|
["/dev/en", "de"],
|
|
["", "de"],
|
|
] as const)("%s is %s", (pathname, locale) => {
|
|
expect(localeOfPath(pathname)).toBe(locale);
|
|
});
|
|
});
|
|
|
|
describe("switchPath", () => {
|
|
it("leads to the same page in the other language", () => {
|
|
expect(switchPath("/", "en")).toBe("/en");
|
|
expect(switchPath("/en", "de")).toBe("/");
|
|
expect(switchPath("/impressum", "en")).toBe("/en/imprint");
|
|
expect(switchPath("/en/imprint", "de")).toBe("/impressum");
|
|
expect(switchPath("/datenschutz", "en")).toBe("/en/privacy");
|
|
expect(switchPath("/en/privacy", "de")).toBe("/datenschutz");
|
|
});
|
|
|
|
it("stays on the page when the language is already the requested one", () => {
|
|
expect(switchPath("/en/imprint", "en")).toBe("/en/imprint");
|
|
expect(switchPath("/datenschutz", "de")).toBe("/datenschutz");
|
|
});
|
|
|
|
it("ignores a trailing slash", () => {
|
|
expect(switchPath("/en/", "de")).toBe("/");
|
|
expect(switchPath("/impressum/", "en")).toBe("/en/imprint");
|
|
});
|
|
|
|
it("leads to the homepage from an address that is not a page", () => {
|
|
expect(switchPath("/nothing-here", "en")).toBe("/en");
|
|
expect(switchPath("/en/nothing-here", "de")).toBe("/");
|
|
// A German page name under /en is not an English page.
|
|
expect(switchPath("/en/impressum", "de")).toBe("/");
|
|
});
|
|
});
|
|
|
|
const german = { cookie: undefined, acceptLanguage: "de-DE,de;q=0.9,en;q=0.8" };
|
|
const english = { cookie: undefined, acceptLanguage: "en-US,en;q=0.9" };
|
|
const unknown = { cookie: undefined, acceptLanguage: undefined };
|
|
|
|
describe("routeRequest", () => {
|
|
describe("addresses without a language", () => {
|
|
it.each([
|
|
["/", "/de"],
|
|
["/impressum", "/de/imprint"],
|
|
["/datenschutz", "/de/privacy"],
|
|
])("shows %s in German to a German browser", (pathname, to) => {
|
|
expect(routeRequest({ pathname, ...german })).toEqual({
|
|
kind: "rewrite",
|
|
to,
|
|
negotiated: true,
|
|
});
|
|
});
|
|
|
|
it("shows German when nothing is known about the visitor, as for a search engine", () => {
|
|
expect(routeRequest({ pathname: "/", ...unknown })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/de",
|
|
negotiated: true,
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
["/", "/en"],
|
|
["/impressum", "/en/imprint"],
|
|
["/datenschutz", "/en/privacy"],
|
|
])(
|
|
"sends an English browser from %s to %s, not permanently",
|
|
(pathname, to) => {
|
|
expect(routeRequest({ pathname, ...english })).toEqual({
|
|
kind: "redirect",
|
|
to,
|
|
permanent: false,
|
|
negotiated: true,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("leaves an English browser on German once German was chosen", () => {
|
|
expect(routeRequest({ pathname: "/", ...english, cookie: "de" })).toEqual(
|
|
{ kind: "rewrite", to: "/de", negotiated: true },
|
|
);
|
|
});
|
|
|
|
it("sends a German browser to English once English was chosen", () => {
|
|
expect(routeRequest({ pathname: "/", ...german, cookie: "en" })).toEqual({
|
|
kind: "redirect",
|
|
to: "/en",
|
|
permanent: false,
|
|
negotiated: true,
|
|
});
|
|
});
|
|
|
|
it("treats a trailing slash like the page itself", () => {
|
|
expect(routeRequest({ pathname: "/impressum/", ...english })).toEqual({
|
|
kind: "redirect",
|
|
to: "/en/imprint",
|
|
permanent: false,
|
|
negotiated: true,
|
|
});
|
|
expect(routeRequest({ pathname: "/impressum/", ...german })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/de/imprint",
|
|
negotiated: true,
|
|
});
|
|
expect(routeRequest({ pathname: "/impressum//", ...german })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/de/imprint",
|
|
negotiated: true,
|
|
});
|
|
});
|
|
|
|
// The legal pages used to live here in English; search engines know those addresses.
|
|
it.each([
|
|
["/imprint", "/en/imprint"],
|
|
["/privacy", "/en/privacy"],
|
|
["/privacy/", "/en/privacy"],
|
|
])(
|
|
"moves the English page name %s to %s for good, whatever the browser says",
|
|
(pathname, to) => {
|
|
for (const visitor of [german, english, unknown]) {
|
|
expect(routeRequest({ pathname, ...visitor })).toEqual({
|
|
kind: "redirect",
|
|
to,
|
|
permanent: true,
|
|
negotiated: false,
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
it("shows the German error page for an unknown address, whatever the browser says", () => {
|
|
expect(routeRequest({ pathname: "/nothing-here", ...english })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/de/nothing-here",
|
|
negotiated: false,
|
|
});
|
|
expect(routeRequest({ pathname: "/a/b", ...german })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/de/a/b",
|
|
negotiated: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("English addresses", () => {
|
|
it.each([
|
|
["/en", "/en"],
|
|
["/en/", "/en"],
|
|
["/en/imprint", "/en/imprint"],
|
|
["/en/privacy", "/en/privacy"],
|
|
])("shows %s in English to everyone", (pathname, to) => {
|
|
for (const visitor of [german, english, { ...english, cookie: "de" }]) {
|
|
expect(routeRequest({ pathname, ...visitor })).toEqual({
|
|
kind: "rewrite",
|
|
to,
|
|
negotiated: false,
|
|
});
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
["/en/impressum", "/en/imprint"],
|
|
["/en/datenschutz", "/en/privacy"],
|
|
["/en/datenschutz/", "/en/privacy"],
|
|
])(
|
|
"moves the German page name %s to the English one for good",
|
|
(pathname, to) => {
|
|
expect(routeRequest({ pathname, ...german })).toEqual({
|
|
kind: "redirect",
|
|
to,
|
|
permanent: true,
|
|
negotiated: false,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("shows the English error page for an unknown English address", () => {
|
|
expect(routeRequest({ pathname: "/en/nothing-here", ...german })).toEqual(
|
|
{ kind: "rewrite", to: "/en/nothing-here", negotiated: false },
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("addresses with the internal German prefix", () => {
|
|
it.each([
|
|
["/de", "/"],
|
|
["/de/", "/"],
|
|
["/de/impressum", "/impressum"],
|
|
["/de/datenschutz", "/datenschutz"],
|
|
["/de/imprint", "/impressum"],
|
|
["/de/privacy", "/datenschutz"],
|
|
["/de/nothing-here", "/nothing-here"],
|
|
])(
|
|
"moves %s to %s for good, so that each page has one address",
|
|
(pathname, to) => {
|
|
expect(routeRequest({ pathname, ...english })).toEqual({
|
|
kind: "redirect",
|
|
to,
|
|
permanent: true,
|
|
negotiated: false,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("only treats a whole segment as a prefix", () => {
|
|
expect(routeRequest({ pathname: "/design", ...german })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/de/design",
|
|
negotiated: false,
|
|
});
|
|
expect(routeRequest({ pathname: "/energy", ...german })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/de/energy",
|
|
negotiated: false,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("sitemapEntries", () => {
|
|
it("lists every language of the given pages, each with all its alternates", () => {
|
|
const languages = {
|
|
de: "https://example.com/",
|
|
en: "https://example.com/en",
|
|
"x-default": "https://example.com/",
|
|
};
|
|
expect(sitemapEntries("https://example.com", ["home"])).toEqual([
|
|
{ url: "https://example.com/", alternates: { languages } },
|
|
{ url: "https://example.com/en", alternates: { languages } },
|
|
]);
|
|
});
|
|
|
|
it("keeps the order of the pages", () => {
|
|
expect(
|
|
sitemapEntries("https://example.com", ["privacy", "home"]).map(
|
|
(entry) => entry.url,
|
|
),
|
|
).toEqual([
|
|
"https://example.com/datenschutz",
|
|
"https://example.com/en/privacy",
|
|
"https://example.com/",
|
|
"https://example.com/en",
|
|
]);
|
|
});
|
|
|
|
it("is empty without pages", () => {
|
|
expect(sitemapEntries("https://example.com", [])).toEqual([]);
|
|
});
|
|
});
|