diff --git a/packages/console/app/src/context/language.tsx b/packages/console/app/src/context/language.tsx index 310b56596a6..7f581326cca 100644 --- a/packages/console/app/src/context/language.tsx +++ b/packages/console/app/src/context/language.tsx @@ -11,7 +11,7 @@ import { dir as localeDir, label as localeLabel, localeFromCookieHeader, - localeFromRequest, + pageLocale, parseLocale, route as localeRoute, tag as localeTag, @@ -19,7 +19,7 @@ import { function initial() { const evt = getRequestEvent() - if (evt) return localeFromRequest(evt.request) + if (evt) return pageLocale(evt.request) if (typeof document === "object") { const fromDom = parseLocale(document.documentElement.dataset.locale) diff --git a/packages/console/app/src/entry-server.tsx b/packages/console/app/src/entry-server.tsx index 619a1925d55..44252ff18f0 100644 --- a/packages/console/app/src/entry-server.tsx +++ b/packages/console/app/src/entry-server.tsx @@ -1,7 +1,7 @@ // @refresh reload import { createHandler, StartServer } from "@solidjs/start/server" import { getRequestEvent } from "solid-js/web" -import { dir, localeFromRequest, tag } from "~/lib/language" +import { dir, pageLocale, tag } from "~/lib/language" const criticalCSS = `[data-component="top"]{min-height:80px;display:flex;align-items:center}` @@ -10,7 +10,7 @@ export default createHandler( { const evt = getRequestEvent() - const locale = evt ? localeFromRequest(evt.request) : "en" + const locale = evt ? pageLocale(evt.request) : "en" return ( diff --git a/packages/console/app/src/lib/language.ts b/packages/console/app/src/lib/language.ts index 2fc83ec43af..b8fe086702d 100644 --- a/packages/console/app/src/lib/language.ts +++ b/packages/console/app/src/lib/language.ts @@ -325,6 +325,17 @@ export function localeFromRequest(request: Request) { ) } +// Pages take their language from the URL alone, so one URL serves the same language to people, crawlers and caches. +export function pageLocale(request: Request) { + const pathname = new URL(request.url).pathname + return ( + parseLocale(request.headers.get(LOCALE_HEADER)) ?? + fromPathname(pathname) ?? + fromDocsPathname(pathname) ?? + ("en" satisfies Locale) + ) +} + export function cookie(locale: Locale) { return `${LOCALE_COOKIE}=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax` } diff --git a/packages/console/app/src/routes/docs/[...path].ts b/packages/console/app/src/routes/docs/[...path].ts index baf24ba115d..9f76441d022 100644 --- a/packages/console/app/src/routes/docs/[...path].ts +++ b/packages/console/app/src/routes/docs/[...path].ts @@ -1,6 +1,6 @@ import type { APIEvent } from "@solidjs/start/server" import { Resource } from "@opencode/console-resource" -import { cookie, docs, localeFromRequest, tag } from "~/lib/language" +import { docs, pageLocale, tag } from "~/lib/language" async function handler(evt: APIEvent) { const req = evt.request.clone() @@ -10,7 +10,7 @@ async function handler(evt: APIEvent) { return Response.redirect(new URL(moved[1] === "go" ? "/v2/docs/console/go/" : "/v2/docs/console/models/", url), 302) } - const locale = localeFromRequest(req) + const locale = pageLocale(req) const host = Resource.App.stage === "production" ? "docs.opencode.ai" : "docs.dev.opencode.ai" const targetUrl = `https://${host}${docs(locale, url.pathname)}${url.search}` @@ -22,9 +22,7 @@ async function handler(evt: APIEvent) { headers, body: req.body, }) - const next = new Response(response.body, response) - next.headers.append("set-cookie", cookie(locale)) - return next + return new Response(response.body, response) } export const GET = handler diff --git a/packages/console/app/src/routes/docs/index.ts b/packages/console/app/src/routes/docs/index.ts index 517108b0650..cbeac6a354e 100644 --- a/packages/console/app/src/routes/docs/index.ts +++ b/packages/console/app/src/routes/docs/index.ts @@ -1,11 +1,11 @@ import type { APIEvent } from "@solidjs/start/server" import { Resource } from "@opencode/console-resource" -import { cookie, docs, localeFromRequest, tag } from "~/lib/language" +import { docs, pageLocale, tag } from "~/lib/language" async function handler(evt: APIEvent) { const req = evt.request.clone() const url = new URL(req.url) - const locale = localeFromRequest(req) + const locale = pageLocale(req) const host = Resource.App.stage === "production" ? "docs.opencode.ai" : "docs.dev.opencode.ai" const targetUrl = `https://${host}${docs(locale, url.pathname)}${url.search}` @@ -17,9 +17,7 @@ async function handler(evt: APIEvent) { headers, body: req.body, }) - const next = new Response(response.body, response) - next.headers.append("set-cookie", cookie(locale)) - return next + return new Response(response.body, response) } export const GET = handler diff --git a/packages/console/app/test/language.test.ts b/packages/console/app/test/language.test.ts index 100361ba601..0dc469bf983 100644 --- a/packages/console/app/test/language.test.ts +++ b/packages/console/app/test/language.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { route } from "../src/lib/language" +import { LOCALE_HEADER, pageLocale, route } from "../src/lib/language" describe("language.route", () => { test("keeps the changelog unprefixed for every locale", () => { @@ -11,3 +11,19 @@ describe("language.route", () => { expect(route("de", "/download")).toBe("/de/download") }) }) + +describe("pageLocale", () => { + const request = (path: string, headers: Record = {}) => + new Request(`https://opencode.ai${path}`, { headers }) + const german = { "accept-language": "de-DE,de;q=0.9", cookie: "oc_locale=de" } + + test("ignores the browser language and cookie on unprefixed pages", () => { + expect(pageLocale(request("/", german))).toBe("en") + expect(pageLocale(request("/docs/agents/", german))).toBe("en") + }) + + test("uses the locale in the URL", () => { + expect(pageLocale(request("/", { [LOCALE_HEADER]: "de" }))).toBe("de") + expect(pageLocale(request("/docs/de/agents/"))).toBe("de") + }) +})