Просмотр исходного кода

feat(www): locale specific urls (#12508)

Adam 2 месяцев назад
Родитель
Сommit
24cd84cda5
33 измененных файлов с 279 добавлено и 134 удалено
  1. 16 11
      packages/console/app/script/generate-sitemap.ts
  2. 2 0
      packages/console/app/src/app.tsx
  3. 3 3
      packages/console/app/src/component/footer.tsx
  4. 15 13
      packages/console/app/src/component/header.tsx
  5. 6 0
      packages/console/app/src/component/language-picker.tsx
  6. 5 3
      packages/console/app/src/component/legal.tsx
  7. 36 0
      packages/console/app/src/component/locale-links.tsx
  8. 4 0
      packages/console/app/src/context/language.tsx
  9. 34 0
      packages/console/app/src/lib/language.ts
  10. 12 1
      packages/console/app/src/middleware.ts
  11. 6 4
      packages/console/app/src/routes/[...404].tsx
  12. 8 7
      packages/console/app/src/routes/black.tsx
  13. 3 1
      packages/console/app/src/routes/black/index.tsx
  14. 3 1
      packages/console/app/src/routes/black/subscribe/[plan].tsx
  15. 3 3
      packages/console/app/src/routes/black/workspace.tsx
  16. 3 3
      packages/console/app/src/routes/brand/index.tsx
  17. 5 4
      packages/console/app/src/routes/changelog/index.tsx
  18. 2 2
      packages/console/app/src/routes/docs/[...path].ts
  19. 2 2
      packages/console/app/src/routes/docs/index.ts
  20. 24 16
      packages/console/app/src/routes/download/index.tsx
  21. 3 3
      packages/console/app/src/routes/enterprise/index.tsx
  22. 22 15
      packages/console/app/src/routes/index.tsx
  23. 8 6
      packages/console/app/src/routes/legal/privacy-policy/index.tsx
  24. 13 10
      packages/console/app/src/routes/legal/terms-of-service/index.tsx
  25. 2 2
      packages/console/app/src/routes/s/[id].ts
  26. 2 2
      packages/console/app/src/routes/t/[...path].tsx
  27. 8 6
      packages/console/app/src/routes/temp.tsx
  28. 3 1
      packages/console/app/src/routes/user-menu.tsx
  29. 3 1
      packages/console/app/src/routes/workspace.tsx
  30. 3 1
      packages/console/app/src/routes/workspace/[id]/members/member-section.tsx
  31. 4 2
      packages/console/app/src/routes/workspace/[id]/model-section.tsx
  32. 13 10
      packages/console/app/src/routes/zen/index.tsx
  33. 3 1
      packages/console/app/vite.config.ts

+ 16 - 11
packages/console/app/script/generate-sitemap.ts

@@ -3,6 +3,7 @@ import { readdir, writeFile } from "fs/promises"
 import { join, dirname } from "path"
 import { join, dirname } from "path"
 import { fileURLToPath } from "url"
 import { fileURLToPath } from "url"
 import { config } from "../src/config.js"
 import { config } from "../src/config.js"
+import { LOCALES, route } from "../src/lib/language.js"
 
 
 const __dirname = dirname(fileURLToPath(import.meta.url))
 const __dirname = dirname(fileURLToPath(import.meta.url))
 const BASE_URL = config.baseUrl
 const BASE_URL = config.baseUrl
@@ -27,12 +28,14 @@ async function getMainRoutes(): Promise<SitemapEntry[]> {
     { path: "/zen", priority: 0.8, changefreq: "weekly" },
     { path: "/zen", priority: 0.8, changefreq: "weekly" },
   ]
   ]
 
 
-  for (const route of staticRoutes) {
-    routes.push({
-      url: `${BASE_URL}${route.path}`,
-      priority: route.priority,
-      changefreq: route.changefreq,
-    })
+  for (const item of staticRoutes) {
+    for (const locale of LOCALES) {
+      routes.push({
+        url: `${BASE_URL}${route(locale, item.path)}`,
+        priority: item.priority,
+        changefreq: item.changefreq,
+      })
+    }
   }
   }
 
 
   return routes
   return routes
@@ -50,11 +53,13 @@ async function getDocsRoutes(): Promise<SitemapEntry[]> {
       const slug = file.replace(".mdx", "")
       const slug = file.replace(".mdx", "")
       const path = slug === "index" ? "/docs/" : `/docs/${slug}`
       const path = slug === "index" ? "/docs/" : `/docs/${slug}`
 
 
-      routes.push({
-        url: `${BASE_URL}${path}`,
-        priority: slug === "index" ? 0.9 : 0.7,
-        changefreq: "weekly",
-      })
+      for (const locale of LOCALES) {
+        routes.push({
+          url: `${BASE_URL}${route(locale, path)}`,
+          priority: slug === "index" ? 0.9 : 0.7,
+          changefreq: "weekly",
+        })
+      }
     }
     }
   } catch (error) {
   } catch (error) {
     console.error("Error reading docs directory:", error)
     console.error("Error reading docs directory:", error)

+ 2 - 0
packages/console/app/src/app.tsx

@@ -8,11 +8,13 @@ import "@ibm/plex/css/ibm-plex.css"
 import "./app.css"
 import "./app.css"
 import { LanguageProvider } from "~/context/language"
 import { LanguageProvider } from "~/context/language"
 import { I18nProvider } from "~/context/i18n"
 import { I18nProvider } from "~/context/i18n"
+import { strip } from "~/lib/language"
 
 
 export default function App() {
 export default function App() {
   return (
   return (
     <Router
     <Router
       explicitLinks={true}
       explicitLinks={true}
+      transformUrl={strip}
       root={(props) => (
       root={(props) => (
         <LanguageProvider>
         <LanguageProvider>
           <I18nProvider>
           <I18nProvider>

+ 3 - 3
packages/console/app/src/component/footer.tsx

@@ -26,13 +26,13 @@ export function Footer() {
         </a>
         </a>
       </div>
       </div>
       <div data-slot="cell">
       <div data-slot="cell">
-        <a href="/docs">{i18n.t("footer.docs")}</a>
+        <a href={language.route("/docs")}>{i18n.t("footer.docs")}</a>
       </div>
       </div>
       <div data-slot="cell">
       <div data-slot="cell">
-        <a href="/changelog">{i18n.t("footer.changelog")}</a>
+        <a href={language.route("/changelog")}>{i18n.t("footer.changelog")}</a>
       </div>
       </div>
       <div data-slot="cell">
       <div data-slot="cell">
-        <a href="/discord">{i18n.t("footer.discord")}</a>
+        <a href={language.route("/discord")}>{i18n.t("footer.discord")}</a>
       </div>
       </div>
       <div data-slot="cell">
       <div data-slot="cell">
         <a href={config.social.twitter}>{i18n.t("footer.x")}</a>
         <a href={config.social.twitter}>{i18n.t("footer.x")}</a>

+ 15 - 13
packages/console/app/src/component/header.tsx

@@ -20,6 +20,7 @@ import { github } from "~/lib/github"
 import { createEffect, onCleanup } from "solid-js"
 import { createEffect, onCleanup } from "solid-js"
 import { config } from "~/config"
 import { config } from "~/config"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
+import { useLanguage } from "~/context/language"
 import "./header-context-menu.css"
 import "./header-context-menu.css"
 
 
 const isDarkMode = () => window.matchMedia("(prefers-color-scheme: dark)").matches
 const isDarkMode = () => window.matchMedia("(prefers-color-scheme: dark)").matches
@@ -38,6 +39,7 @@ const fetchSvgContent = async (svgPath: string): Promise<string> => {
 export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
 export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
   const navigate = useNavigate()
   const navigate = useNavigate()
   const i18n = useI18n()
   const i18n = useI18n()
+  const language = useLanguage()
   const githubData = createAsync(() => github())
   const githubData = createAsync(() => github())
   const starCount = createMemo(() =>
   const starCount = createMemo(() =>
     githubData()?.stars
     githubData()?.stars
@@ -121,7 +123,7 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
   return (
   return (
     <section data-component="top">
     <section data-component="top">
       <div onContextMenu={handleLogoContextMenu}>
       <div onContextMenu={handleLogoContextMenu}>
-        <A href="/">
+        <A href={language.route("/")}>
           <img data-slot="logo light" src={logoLight} alt="OpenCode" width="189" height="34" />
           <img data-slot="logo light" src={logoLight} alt="OpenCode" width="189" height="34" />
           <img data-slot="logo dark" src={logoDark} alt="OpenCode" width="189" height="34" />
           <img data-slot="logo dark" src={logoDark} alt="OpenCode" width="189" height="34" />
         </A>
         </A>
@@ -142,7 +144,7 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
             <img data-slot="copy dark" src={copyWordmarkDark} alt="" />
             <img data-slot="copy dark" src={copyWordmarkDark} alt="" />
             {i18n.t("nav.context.copyWordmark")}
             {i18n.t("nav.context.copyWordmark")}
           </button>
           </button>
-          <button class="context-menu-item" onClick={() => navigate("/brand")}>
+          <button class="context-menu-item" onClick={() => navigate(language.route("/brand"))}>
             <img data-slot="copy light" src={copyBrandAssetsLight} alt="" />
             <img data-slot="copy light" src={copyBrandAssetsLight} alt="" />
             <img data-slot="copy dark" src={copyBrandAssetsDark} alt="" />
             <img data-slot="copy dark" src={copyBrandAssetsDark} alt="" />
             {i18n.t("nav.context.brandAssets")}
             {i18n.t("nav.context.brandAssets")}
@@ -157,24 +159,24 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
             </a>
             </a>
           </li>
           </li>
           <li>
           <li>
-            <a href="/docs">{i18n.t("nav.docs")}</a>
+            <a href={language.route("/docs")}>{i18n.t("nav.docs")}</a>
           </li>
           </li>
           <li>
           <li>
-            <A href="/enterprise">{i18n.t("nav.enterprise")}</A>
+            <A href={language.route("/enterprise")}>{i18n.t("nav.enterprise")}</A>
           </li>
           </li>
           <li>
           <li>
             <Switch>
             <Switch>
               <Match when={props.zen}>
               <Match when={props.zen}>
-                <a href="/auth">{i18n.t("nav.login")}</a>
+                <a href={language.route("/auth")}>{i18n.t("nav.login")}</a>
               </Match>
               </Match>
               <Match when={!props.zen}>
               <Match when={!props.zen}>
-                <A href="/zen">{i18n.t("nav.zen")}</A>
+                <A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
               </Match>
               </Match>
             </Switch>
             </Switch>
           </li>
           </li>
           <Show when={!props.hideGetStarted}>
           <Show when={!props.hideGetStarted}>
             <li>
             <li>
-              <A href="/download" data-slot="cta-button">
+              <A href={language.route("/download")} data-slot="cta-button">
                 <svg
                 <svg
                   width="18"
                   width="18"
                   height="18"
                   height="18"
@@ -245,7 +247,7 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
             <nav data-component="nav-mobile-menu-list">
             <nav data-component="nav-mobile-menu-list">
               <ul>
               <ul>
                 <li>
                 <li>
-                  <A href="/">{i18n.t("nav.home")}</A>
+                  <A href={language.route("/")}>{i18n.t("nav.home")}</A>
                 </li>
                 </li>
                 <li>
                 <li>
                   <a href={config.github.repoUrl} target="_blank" style="white-space: nowrap;">
                   <a href={config.github.repoUrl} target="_blank" style="white-space: nowrap;">
@@ -253,24 +255,24 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
                   </a>
                   </a>
                 </li>
                 </li>
                 <li>
                 <li>
-                  <a href="/docs">{i18n.t("nav.docs")}</a>
+                  <a href={language.route("/docs")}>{i18n.t("nav.docs")}</a>
                 </li>
                 </li>
                 <li>
                 <li>
-                  <A href="/enterprise">{i18n.t("nav.enterprise")}</A>
+                  <A href={language.route("/enterprise")}>{i18n.t("nav.enterprise")}</A>
                 </li>
                 </li>
                 <li>
                 <li>
                   <Switch>
                   <Switch>
                     <Match when={props.zen}>
                     <Match when={props.zen}>
-                      <a href="/auth">{i18n.t("nav.login")}</a>
+                      <a href={language.route("/auth")}>{i18n.t("nav.login")}</a>
                     </Match>
                     </Match>
                     <Match when={!props.zen}>
                     <Match when={!props.zen}>
-                      <A href="/zen">{i18n.t("nav.zen")}</A>
+                      <A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
                     </Match>
                     </Match>
                   </Switch>
                   </Switch>
                 </li>
                 </li>
                 <Show when={!props.hideGetStarted}>
                 <Show when={!props.hideGetStarted}>
                   <li>
                   <li>
-                    <A href="/download" data-slot="cta-button">
+                    <A href={language.route("/download")} data-slot="cta-button">
                       {i18n.t("nav.getStartedFree")}
                       {i18n.t("nav.getStartedFree")}
                     </A>
                     </A>
                   </li>
                   </li>

+ 6 - 0
packages/console/app/src/component/language-picker.tsx

@@ -1,10 +1,14 @@
 import { For, createSignal } from "solid-js"
 import { For, createSignal } from "solid-js"
+import { useLocation, useNavigate } from "@solidjs/router"
 import { Dropdown, DropdownItem } from "~/component/dropdown"
 import { Dropdown, DropdownItem } from "~/component/dropdown"
 import { useLanguage } from "~/context/language"
 import { useLanguage } from "~/context/language"
+import { route, strip } from "~/lib/language"
 import "./language-picker.css"
 import "./language-picker.css"
 
 
 export function LanguagePicker(props: { align?: "left" | "right" } = {}) {
 export function LanguagePicker(props: { align?: "left" | "right" } = {}) {
   const language = useLanguage()
   const language = useLanguage()
+  const navigate = useNavigate()
+  const location = useLocation()
   const [open, setOpen] = createSignal(false)
   const [open, setOpen] = createSignal(false)
 
 
   return (
   return (
@@ -21,6 +25,8 @@ export function LanguagePicker(props: { align?: "left" | "right" } = {}) {
               selected={locale === language.locale()}
               selected={locale === language.locale()}
               onClick={() => {
               onClick={() => {
                 language.setLocale(locale)
                 language.setLocale(locale)
+                const href = `${route(locale, strip(location.pathname))}${location.search}${location.hash}`
+                if (href !== `${location.pathname}${location.search}${location.hash}`) navigate(href)
                 setOpen(false)
                 setOpen(false)
               }}
               }}
             >
             >

+ 5 - 3
packages/console/app/src/component/legal.tsx

@@ -1,22 +1,24 @@
 import { A } from "@solidjs/router"
 import { A } from "@solidjs/router"
 import { LanguagePicker } from "~/component/language-picker"
 import { LanguagePicker } from "~/component/language-picker"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
+import { useLanguage } from "~/context/language"
 
 
 export function Legal() {
 export function Legal() {
   const i18n = useI18n()
   const i18n = useI18n()
+  const language = useLanguage()
   return (
   return (
     <div data-component="legal">
     <div data-component="legal">
       <span>
       <span>
         ©{new Date().getFullYear()} <a href="https://anoma.ly">Anomaly</a>
         ©{new Date().getFullYear()} <a href="https://anoma.ly">Anomaly</a>
       </span>
       </span>
       <span>
       <span>
-        <A href="/brand">{i18n.t("legal.brand")}</A>
+        <A href={language.route("/brand")}>{i18n.t("legal.brand")}</A>
       </span>
       </span>
       <span>
       <span>
-        <A href="/legal/privacy-policy">{i18n.t("legal.privacy")}</A>
+        <A href={language.route("/legal/privacy-policy")}>{i18n.t("legal.privacy")}</A>
       </span>
       </span>
       <span>
       <span>
-        <A href="/legal/terms-of-service">{i18n.t("legal.terms")}</A>
+        <A href={language.route("/legal/terms-of-service")}>{i18n.t("legal.terms")}</A>
       </span>
       </span>
       <span>
       <span>
         <LanguagePicker align="right" />
         <LanguagePicker align="right" />

+ 36 - 0
packages/console/app/src/component/locale-links.tsx

@@ -0,0 +1,36 @@
+import { Link } from "@solidjs/meta"
+import { For } from "solid-js"
+import { getRequestEvent } from "solid-js/web"
+import { config } from "~/config"
+import { useLanguage } from "~/context/language"
+import { LOCALES, route, tag } from "~/lib/language"
+
+function skip(path: string) {
+  const evt = getRequestEvent()
+  if (!evt) return false
+
+  const key = "__locale_links_seen"
+  const locals = evt.locals as Record<string, unknown>
+  const seen = locals[key] instanceof Set ? (locals[key] as Set<string>) : new Set<string>()
+  locals[key] = seen
+  if (seen.has(path)) return true
+  seen.add(path)
+  return false
+}
+
+export function LocaleLinks(props: { path: string }) {
+  const language = useLanguage()
+  if (skip(props.path)) return null
+
+  return (
+    <>
+      <Link rel="canonical" href={`${config.baseUrl}${route(language.locale(), props.path)}`} />
+      <For each={LOCALES}>
+        {(locale) => (
+          <Link rel="alternate" hreflang={tag(locale)} href={`${config.baseUrl}${route(locale, props.path)}`} />
+        )}
+      </For>
+      <Link rel="alternate" hreflang="x-default" href={`${config.baseUrl}${props.path}`} />
+    </>
+  )
+}

+ 4 - 0
packages/console/app/src/context/language.tsx

@@ -13,6 +13,7 @@ import {
   localeFromCookieHeader,
   localeFromCookieHeader,
   localeFromRequest,
   localeFromRequest,
   parseLocale,
   parseLocale,
+  route as localeRoute,
   tag as localeTag,
   tag as localeTag,
 } from "~/lib/language"
 } from "~/lib/language"
 
 
@@ -54,6 +55,9 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
       label: localeLabel,
       label: localeLabel,
       tag: localeTag,
       tag: localeTag,
       dir: localeDir,
       dir: localeDir,
+      route(pathname: string) {
+        return localeRoute(store.locale, pathname)
+      },
       setLocale(next: Locale) {
       setLocale(next: Locale) {
         setStore("locale", next)
         setStore("locale", next)
         if (typeof document !== "object") return
         if (typeof document !== "object") return

+ 34 - 0
packages/console/app/src/lib/language.ts

@@ -21,6 +21,12 @@ export const LOCALES = [
 export type Locale = (typeof LOCALES)[number]
 export type Locale = (typeof LOCALES)[number]
 
 
 export const LOCALE_COOKIE = "oc_locale" as const
 export const LOCALE_COOKIE = "oc_locale" as const
+export const LOCALE_HEADER = "x-opencode-locale" as const
+
+function fix(pathname: string) {
+  if (pathname.startsWith("/")) return pathname
+  return `/${pathname}`
+}
 
 
 const LABEL = {
 const LABEL = {
   en: "English",
   en: "English",
@@ -68,6 +74,28 @@ export function parseLocale(value: unknown): Locale | null {
   return null
   return null
 }
 }
 
 
+export function fromPathname(pathname: string) {
+  return parseLocale(fix(pathname).split("/")[1])
+}
+
+export function strip(pathname: string) {
+  const locale = fromPathname(pathname)
+  if (!locale) return fix(pathname)
+
+  const next = fix(pathname).slice(locale.length + 1)
+  if (!next) return "/"
+  if (next.startsWith("/")) return next
+  return `/${next}`
+}
+
+export function route(locale: Locale, pathname: string) {
+  const next = strip(pathname)
+  if (next.startsWith("/docs")) return next
+  if (locale === "en") return next
+  if (next === "/") return `/${locale}`
+  return `/${locale}${next}`
+}
+
 export function label(locale: Locale) {
 export function label(locale: Locale) {
   return LABEL[locale]
   return LABEL[locale]
 }
 }
@@ -160,6 +188,12 @@ export function localeFromCookieHeader(header: string | null) {
 }
 }
 
 
 export function localeFromRequest(request: Request) {
 export function localeFromRequest(request: Request) {
+  const fromHeader = parseLocale(request.headers.get(LOCALE_HEADER))
+  if (fromHeader) return fromHeader
+
+  const fromPath = fromPathname(new URL(request.url).pathname)
+  if (fromPath) return fromPath
+
   return (
   return (
     localeFromCookieHeader(request.headers.get("cookie")) ??
     localeFromCookieHeader(request.headers.get("cookie")) ??
     detectFromAcceptLanguage(request.headers.get("accept-language"))
     detectFromAcceptLanguage(request.headers.get("accept-language"))

+ 12 - 1
packages/console/app/src/middleware.ts

@@ -1,5 +1,16 @@
 import { createMiddleware } from "@solidjs/start/middleware"
 import { createMiddleware } from "@solidjs/start/middleware"
+import { LOCALE_HEADER, cookie, fromPathname, strip } from "~/lib/language"
 
 
 export default createMiddleware({
 export default createMiddleware({
-  onBeforeResponse() {},
+  onRequest(event) {
+    const url = new URL(event.request.url)
+    const locale = fromPathname(url.pathname)
+    if (!locale) return
+
+    event.request.headers.set(LOCALE_HEADER, locale)
+    event.response.headers.append("set-cookie", cookie(locale))
+
+    url.pathname = strip(url.pathname)
+    event.request = new Request(url, event.request)
+  },
 })
 })

+ 6 - 4
packages/console/app/src/routes/[...404].tsx

@@ -4,16 +4,18 @@ import { HttpStatusCode } from "@solidjs/start"
 import logoLight from "../asset/logo-ornate-light.svg"
 import logoLight from "../asset/logo-ornate-light.svg"
 import logoDark from "../asset/logo-ornate-dark.svg"
 import logoDark from "../asset/logo-ornate-dark.svg"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
+import { useLanguage } from "~/context/language"
 
 
 export default function NotFound() {
 export default function NotFound() {
   const i18n = useI18n()
   const i18n = useI18n()
+  const language = useLanguage()
   return (
   return (
     <main data-page="not-found">
     <main data-page="not-found">
       <Title>{i18n.t("notFound.title")}</Title>
       <Title>{i18n.t("notFound.title")}</Title>
       <HttpStatusCode code={404} />
       <HttpStatusCode code={404} />
       <div data-component="content">
       <div data-component="content">
         <section data-component="top">
         <section data-component="top">
-          <a href="/" data-slot="logo-link">
+          <a href={language.route("/")} data-slot="logo-link">
             <img data-slot="logo light" src={logoLight} alt="opencode logo light" />
             <img data-slot="logo light" src={logoLight} alt="opencode logo light" />
             <img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
             <img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
           </a>
           </a>
@@ -22,16 +24,16 @@ export default function NotFound() {
 
 
         <section data-component="actions">
         <section data-component="actions">
           <div data-slot="action">
           <div data-slot="action">
-            <a href="/">{i18n.t("notFound.home")}</a>
+            <a href={language.route("/")}>{i18n.t("notFound.home")}</a>
           </div>
           </div>
           <div data-slot="action">
           <div data-slot="action">
-            <a href="/docs">{i18n.t("notFound.docs")}</a>
+            <a href={language.route("/docs")}>{i18n.t("notFound.docs")}</a>
           </div>
           </div>
           <div data-slot="action">
           <div data-slot="action">
             <a href="https://github.com/anomalyco/opencode">{i18n.t("notFound.github")}</a>
             <a href="https://github.com/anomalyco/opencode">{i18n.t("notFound.github")}</a>
           </div>
           </div>
           <div data-slot="action">
           <div data-slot="action">
-            <a href="/discord">{i18n.t("notFound.discord")}</a>
+            <a href={language.route("/discord")}>{i18n.t("notFound.discord")}</a>
           </div>
           </div>
         </section>
         </section>
       </div>
       </div>

+ 8 - 7
packages/console/app/src/routes/black.tsx

@@ -1,5 +1,5 @@
 import { A, createAsync, RouteSectionProps } from "@solidjs/router"
 import { A, createAsync, RouteSectionProps } from "@solidjs/router"
-import { Title, Meta, Link } from "@solidjs/meta"
+import { Title, Meta } from "@solidjs/meta"
 import { createMemo, createSignal } from "solid-js"
 import { createMemo, createSignal } from "solid-js"
 import { github } from "~/lib/github"
 import { github } from "~/lib/github"
 import { config } from "~/config"
 import { config } from "~/config"
@@ -7,6 +7,7 @@ import { useLanguage } from "~/context/language"
 import { LanguagePicker } from "~/component/language-picker"
 import { LanguagePicker } from "~/component/language-picker"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
 import Spotlight, { defaultConfig, type SpotlightAnimationState } from "~/component/spotlight"
 import Spotlight, { defaultConfig, type SpotlightAnimationState } from "~/component/spotlight"
+import { LocaleLinks } from "~/component/locale-links"
 import "./black.css"
 import "./black.css"
 
 
 export default function BlackLayout(props: RouteSectionProps) {
 export default function BlackLayout(props: RouteSectionProps) {
@@ -70,9 +71,9 @@ export default function BlackLayout(props: RouteSectionProps) {
     <div data-page="black">
     <div data-page="black">
       <Title>{i18n.t("black.meta.title")}</Title>
       <Title>{i18n.t("black.meta.title")}</Title>
       <Meta name="description" content={i18n.t("black.meta.description")} />
       <Meta name="description" content={i18n.t("black.meta.description")} />
-      <Link rel="canonical" href={`${config.baseUrl}/black`} />
+      <LocaleLinks path="/black" />
       <Meta property="og:type" content="website" />
       <Meta property="og:type" content="website" />
-      <Meta property="og:url" content={`${config.baseUrl}/black`} />
+      <Meta property="og:url" content={`${config.baseUrl}${language.route("/black")}`} />
       <Meta property="og:title" content={i18n.t("black.meta.title")} />
       <Meta property="og:title" content={i18n.t("black.meta.title")} />
       <Meta property="og:description" content={i18n.t("black.meta.description")} />
       <Meta property="og:description" content={i18n.t("black.meta.description")} />
       <Meta property="og:image" content="/social-share-black.png" />
       <Meta property="og:image" content="/social-share-black.png" />
@@ -84,7 +85,7 @@ export default function BlackLayout(props: RouteSectionProps) {
       <Spotlight config={spotlightConfig} class="header-spotlight" onAnimationFrame={handleAnimationFrame} />
       <Spotlight config={spotlightConfig} class="header-spotlight" onAnimationFrame={handleAnimationFrame} />
 
 
       <header data-component="header">
       <header data-component="header">
-        <A href="/" data-component="header-logo">
+        <A href={language.route("/")} data-component="header-logo">
           <svg xmlns="http://www.w3.org/2000/svg" width="179" height="32" viewBox="0 0 179 32" fill="none">
           <svg xmlns="http://www.w3.org/2000/svg" width="179" height="32" viewBox="0 0 179 32" fill="none">
             <title>opencode</title>
             <title>opencode</title>
             <g clip-path="url(#clip0_3654_210259)">
             <g clip-path="url(#clip0_3654_210259)">
@@ -264,13 +265,13 @@ export default function BlackLayout(props: RouteSectionProps) {
           <a href={config.github.repoUrl} target="_blank">
           <a href={config.github.repoUrl} target="_blank">
             {i18n.t("nav.github")} <span data-slot="github-stars">[{starCount()}]</span>
             {i18n.t("nav.github")} <span data-slot="github-stars">[{starCount()}]</span>
           </a>
           </a>
-          <a href="/docs">{i18n.t("nav.docs")}</a>
+          <a href={language.route("/docs")}>{i18n.t("nav.docs")}</a>
           <LanguagePicker align="right" />
           <LanguagePicker align="right" />
           <span>
           <span>
-            <A href="/legal/privacy-policy">{i18n.t("legal.privacy")}</A>
+            <A href={language.route("/legal/privacy-policy")}>{i18n.t("legal.privacy")}</A>
           </span>
           </span>
           <span>
           <span>
-            <A href="/legal/terms-of-service">{i18n.t("legal.terms")}</A>
+            <A href={language.route("/legal/terms-of-service")}>{i18n.t("legal.terms")}</A>
           </span>
           </span>
         </div>
         </div>
         <span data-slot="anomaly-alt">
         <span data-slot="anomaly-alt">

+ 3 - 1
packages/console/app/src/routes/black/index.tsx

@@ -3,10 +3,12 @@ import { Title } from "@solidjs/meta"
 import { createMemo, createSignal, For, Match, onMount, Show, Switch } from "solid-js"
 import { createMemo, createSignal, For, Match, onMount, Show, Switch } from "solid-js"
 import { PlanIcon, plans } from "./common"
 import { PlanIcon, plans } from "./common"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
+import { useLanguage } from "~/context/language"
 
 
 export default function Black() {
 export default function Black() {
   const [params] = useSearchParams()
   const [params] = useSearchParams()
   const i18n = useI18n()
   const i18n = useI18n()
+  const language = useLanguage()
   const [selected, setSelected] = createSignal<string | null>((params.plan as string) || null)
   const [selected, setSelected] = createSignal<string | null>((params.plan as string) || null)
   const [mounted, setMounted] = createSignal(false)
   const [mounted, setMounted] = createSignal(false)
   const selectedPlan = createMemo(() => plans.find((p) => p.id === selected()))
   const selectedPlan = createMemo(() => plans.find((p) => p.id === selected()))
@@ -104,7 +106,7 @@ export default function Black() {
         </Switch>
         </Switch>
         <p data-slot="fine-print" style={{ "view-transition-name": "fine-print" }}>
         <p data-slot="fine-print" style={{ "view-transition-name": "fine-print" }}>
           {i18n.t("black.finePrint.beforeTerms")} ·{" "}
           {i18n.t("black.finePrint.beforeTerms")} ·{" "}
-          <A href="/legal/terms-of-service">{i18n.t("black.finePrint.terms")}</A>
+          <A href={language.route("/legal/terms-of-service")}>{i18n.t("black.finePrint.terms")}</A>
         </p>
         </p>
       </section>
       </section>
     </>
     </>

+ 3 - 1
packages/console/app/src/routes/black/subscribe/[plan].tsx

@@ -15,6 +15,7 @@ import { Modal } from "~/component/modal"
 import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js"
 import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js"
 import { Billing } from "@opencode-ai/console-core/billing.js"
 import { Billing } from "@opencode-ai/console-core/billing.js"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
+import { useLanguage } from "~/context/language"
 import { formError } from "~/lib/form-error"
 import { formError } from "~/lib/form-error"
 
 
 const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record<PlanID, (typeof plans)[number]>
 const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record<PlanID, (typeof plans)[number]>
@@ -267,6 +268,7 @@ function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data
 export default function BlackSubscribe() {
 export default function BlackSubscribe() {
   const params = useParams()
   const params = useParams()
   const i18n = useI18n()
   const i18n = useI18n()
+  const language = useLanguage()
   const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"]
   const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"]
   const plan = planData.id
   const plan = planData.id
 
 
@@ -467,7 +469,7 @@ export default function BlackSubscribe() {
         </Modal>
         </Modal>
         <p data-slot="fine-print">
         <p data-slot="fine-print">
           {i18n.t("black.finePrint.beforeTerms")} ·{" "}
           {i18n.t("black.finePrint.beforeTerms")} ·{" "}
-          <A href="/legal/terms-of-service">{i18n.t("black.finePrint.terms")}</A>
+          <A href={language.route("/legal/terms-of-service")}>{i18n.t("black.finePrint.terms")}</A>
         </p>
         </p>
       </section>
       </section>
     </>
     </>

+ 3 - 3
packages/console/app/src/routes/black/workspace.tsx

@@ -220,13 +220,13 @@ export default function BlackWorkspace() {
           <a href={config.github.repoUrl} target="_blank">
           <a href={config.github.repoUrl} target="_blank">
             {i18n.t("nav.github")} <span data-slot="github-stars">[{starCount()}]</span>
             {i18n.t("nav.github")} <span data-slot="github-stars">[{starCount()}]</span>
           </a>
           </a>
-          <a href="/docs">{i18n.t("nav.docs")}</a>
+          <a href={language.route("/docs")}>{i18n.t("nav.docs")}</a>
           <LanguagePicker align="right" />
           <LanguagePicker align="right" />
           <span>
           <span>
-            <A href="/legal/privacy-policy">{i18n.t("legal.privacy")}</A>
+            <A href={language.route("/legal/privacy-policy")}>{i18n.t("legal.privacy")}</A>
           </span>
           </span>
           <span>
           <span>
-            <A href="/legal/terms-of-service">{i18n.t("legal.terms")}</A>
+            <A href={language.route("/legal/terms-of-service")}>{i18n.t("legal.terms")}</A>
           </span>
           </span>
         </div>
         </div>
         <span data-slot="anomaly-alt">
         <span data-slot="anomaly-alt">

+ 3 - 3
packages/console/app/src/routes/brand/index.tsx

@@ -1,10 +1,10 @@
 import "./index.css"
 import "./index.css"
-import { Title, Meta, Link } from "@solidjs/meta"
+import { Title, Meta } from "@solidjs/meta"
 import { Header } from "~/component/header"
 import { Header } from "~/component/header"
-import { config } from "~/config"
 import { Footer } from "~/component/footer"
 import { Footer } from "~/component/footer"
 import { Legal } from "~/component/legal"
 import { Legal } from "~/component/legal"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
+import { LocaleLinks } from "~/component/locale-links"
 import previewLogoLight from "../../asset/brand/preview-opencode-logo-light.png"
 import previewLogoLight from "../../asset/brand/preview-opencode-logo-light.png"
 import previewLogoDark from "../../asset/brand/preview-opencode-logo-dark.png"
 import previewLogoDark from "../../asset/brand/preview-opencode-logo-dark.png"
 import previewWordmarkLight from "../../asset/brand/preview-opencode-wordmark-light.png"
 import previewWordmarkLight from "../../asset/brand/preview-opencode-wordmark-light.png"
@@ -56,7 +56,7 @@ export default function Brand() {
   return (
   return (
     <main data-page="enterprise">
     <main data-page="enterprise">
       <Title>{i18n.t("brand.title")}</Title>
       <Title>{i18n.t("brand.title")}</Title>
-      <Link rel="canonical" href={`${config.baseUrl}/brand`} />
+      <LocaleLinks path="/brand" />
       <Meta name="description" content={i18n.t("brand.meta.description")} />
       <Meta name="description" content={i18n.t("brand.meta.description")} />
       <div data-component="container">
       <div data-component="container">
         <Header />
         <Header />

+ 5 - 4
packages/console/app/src/routes/changelog/index.tsx

@@ -1,15 +1,15 @@
 import "./index.css"
 import "./index.css"
-import { Title, Meta, Link } from "@solidjs/meta"
+import { Title, Meta } from "@solidjs/meta"
 import { createAsync } from "@solidjs/router"
 import { createAsync } from "@solidjs/router"
 import { Header } from "~/component/header"
 import { Header } from "~/component/header"
 import { Footer } from "~/component/footer"
 import { Footer } from "~/component/footer"
 import { Legal } from "~/component/legal"
 import { Legal } from "~/component/legal"
-import { config } from "~/config"
 import { changelog } from "~/lib/changelog"
 import { changelog } from "~/lib/changelog"
 import type { HighlightGroup } from "~/lib/changelog"
 import type { HighlightGroup } from "~/lib/changelog"
 import { For, Show, createSignal } from "solid-js"
 import { For, Show, createSignal } from "solid-js"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
 import { useLanguage } from "~/context/language"
 import { useLanguage } from "~/context/language"
+import { LocaleLinks } from "~/component/locale-links"
 
 
 function formatDate(dateString: string, locale: string) {
 function formatDate(dateString: string, locale: string) {
   const date = new Date(dateString)
   const date = new Date(dateString)
@@ -107,7 +107,7 @@ export default function Changelog() {
   return (
   return (
     <main data-page="changelog">
     <main data-page="changelog">
       <Title>{i18n.t("changelog.title")}</Title>
       <Title>{i18n.t("changelog.title")}</Title>
-      <Link rel="canonical" href={`${config.baseUrl}/changelog`} />
+      <LocaleLinks path="/changelog" />
       <Meta name="description" content={i18n.t("changelog.meta.description")} />
       <Meta name="description" content={i18n.t("changelog.meta.description")} />
 
 
       <div data-component="container">
       <div data-component="container">
@@ -122,7 +122,8 @@ export default function Changelog() {
           <section data-component="releases">
           <section data-component="releases">
             <Show when={releases().length === 0}>
             <Show when={releases().length === 0}>
               <p>
               <p>
-                {i18n.t("changelog.empty")} <a href="/changelog.json">{i18n.t("changelog.viewJson")}</a>
+                {i18n.t("changelog.empty")}{" "}
+                <a href={language.route("/changelog.json")}>{i18n.t("changelog.viewJson")}</a>
               </p>
               </p>
             </Show>
             </Show>
             <For each={releases()}>
             <For each={releases()}>

+ 2 - 2
packages/console/app/src/routes/docs/[...path].ts

@@ -1,5 +1,5 @@
 import type { APIEvent } from "@solidjs/start/server"
 import type { APIEvent } from "@solidjs/start/server"
-import { localeFromCookieHeader, tag } from "~/lib/language"
+import { LOCALE_HEADER, localeFromCookieHeader, parseLocale, tag } from "~/lib/language"
 
 
 async function handler(evt: APIEvent) {
 async function handler(evt: APIEvent) {
   const req = evt.request.clone()
   const req = evt.request.clone()
@@ -7,7 +7,7 @@ async function handler(evt: APIEvent) {
   const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
   const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
 
 
   const headers = new Headers(req.headers)
   const headers = new Headers(req.headers)
-  const locale = localeFromCookieHeader(req.headers.get("cookie"))
+  const locale = parseLocale(req.headers.get(LOCALE_HEADER)) ?? localeFromCookieHeader(req.headers.get("cookie"))
   if (locale) headers.set("accept-language", tag(locale))
   if (locale) headers.set("accept-language", tag(locale))
 
 
   const response = await fetch(targetUrl, {
   const response = await fetch(targetUrl, {

+ 2 - 2
packages/console/app/src/routes/docs/index.ts

@@ -1,5 +1,5 @@
 import type { APIEvent } from "@solidjs/start/server"
 import type { APIEvent } from "@solidjs/start/server"
-import { localeFromCookieHeader, tag } from "~/lib/language"
+import { LOCALE_HEADER, localeFromCookieHeader, parseLocale, tag } from "~/lib/language"
 
 
 async function handler(evt: APIEvent) {
 async function handler(evt: APIEvent) {
   const req = evt.request.clone()
   const req = evt.request.clone()
@@ -7,7 +7,7 @@ async function handler(evt: APIEvent) {
   const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
   const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
 
 
   const headers = new Headers(req.headers)
   const headers = new Headers(req.headers)
-  const locale = localeFromCookieHeader(req.headers.get("cookie"))
+  const locale = parseLocale(req.headers.get(LOCALE_HEADER)) ?? localeFromCookieHeader(req.headers.get("cookie"))
   if (locale) headers.set("accept-language", tag(locale))
   if (locale) headers.set("accept-language", tag(locale))
 
 
   const response = await fetch(targetUrl, {
   const response = await fetch(targetUrl, {

+ 24 - 16
packages/console/app/src/routes/download/index.tsx

@@ -1,5 +1,5 @@
 import "./index.css"
 import "./index.css"
-import { Title, Meta, Link } from "@solidjs/meta"
+import { Title, Meta } from "@solidjs/meta"
 import { A, createAsync, query } from "@solidjs/router"
 import { A, createAsync, query } from "@solidjs/router"
 import { Header } from "~/component/header"
 import { Header } from "~/component/header"
 import { Footer } from "~/component/footer"
 import { Footer } from "~/component/footer"
@@ -11,6 +11,8 @@ import { config } from "~/config"
 import { createSignal, onMount, Show, JSX } from "solid-js"
 import { createSignal, onMount, Show, JSX } from "solid-js"
 import { DownloadPlatform } from "./types"
 import { DownloadPlatform } from "./types"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
+import { useLanguage } from "~/context/language"
+import { LocaleLinks } from "~/component/locale-links"
 
 
 type OS = "macOS" | "Windows" | "Linux" | null
 type OS = "macOS" | "Windows" | "Linux" | null
 
 
@@ -66,6 +68,7 @@ function CopyStatus() {
 
 
 export default function Download() {
 export default function Download() {
   const i18n = useI18n()
   const i18n = useI18n()
+  const language = useLanguage()
   const [detectedOS, setDetectedOS] = createSignal<OS>(null)
   const [detectedOS, setDetectedOS] = createSignal<OS>(null)
 
 
   onMount(() => {
   onMount(() => {
@@ -83,7 +86,7 @@ export default function Download() {
   return (
   return (
     <main data-page="download">
     <main data-page="download">
       <Title>{i18n.t("download.title")}</Title>
       <Title>{i18n.t("download.title")}</Title>
-      <Link rel="canonical" href={`${config.baseUrl}/download`} />
+      <LocaleLinks path="/download" />
       <Meta name="description" content={i18n.t("download.meta.description")} />
       <Meta name="description" content={i18n.t("download.meta.description")} />
       <div data-component="container">
       <div data-component="container">
         <Header hideGetStarted />
         <Header hideGetStarted />
@@ -97,7 +100,10 @@ export default function Download() {
               <h1>{i18n.t("download.hero.title")}</h1>
               <h1>{i18n.t("download.hero.title")}</h1>
               <p>{i18n.t("download.hero.subtitle")}</p>
               <p>{i18n.t("download.hero.subtitle")}</p>
               <Show when={detectedOS()}>
               <Show when={detectedOS()}>
-                <a href={getDownloadHref(getDownloadPlatform(detectedOS()))} data-component="download-button">
+                <a
+                  href={language.route(getDownloadHref(getDownloadPlatform(detectedOS())))}
+                  data-component="download-button"
+                >
                   <IconDownload />
                   <IconDownload />
                   {i18n.t("download.hero.button", { os: detectedOS()! })}
                   {i18n.t("download.hero.button", { os: detectedOS()! })}
                 </a>
                 </a>
@@ -169,7 +175,7 @@ export default function Download() {
                   </span>
                   </span>
                   <span>{i18n.t("download.platform.macosAppleSilicon")}</span>
                   <span>{i18n.t("download.platform.macosAppleSilicon")}</span>
                 </div>
                 </div>
-                <a href={getDownloadHref("darwin-aarch64-dmg")} data-component="action-button">
+                <a href={language.route(getDownloadHref("darwin-aarch64-dmg"))} data-component="action-button">
                   {i18n.t("download.action.download")}
                   {i18n.t("download.action.download")}
                 </a>
                 </a>
               </div>
               </div>
@@ -185,7 +191,7 @@ export default function Download() {
                   </span>
                   </span>
                   <span>{i18n.t("download.platform.macosIntel")}</span>
                   <span>{i18n.t("download.platform.macosIntel")}</span>
                 </div>
                 </div>
-                <a href={getDownloadHref("darwin-x64-dmg")} data-component="action-button">
+                <a href={language.route(getDownloadHref("darwin-x64-dmg"))} data-component="action-button">
                   {i18n.t("download.action.download")}
                   {i18n.t("download.action.download")}
                 </a>
                 </a>
               </div>
               </div>
@@ -208,7 +214,7 @@ export default function Download() {
                   </span>
                   </span>
                   <span>{i18n.t("download.platform.windowsX64")}</span>
                   <span>{i18n.t("download.platform.windowsX64")}</span>
                 </div>
                 </div>
-                <a href={getDownloadHref("windows-x64-nsis")} data-component="action-button">
+                <a href={language.route(getDownloadHref("windows-x64-nsis"))} data-component="action-button">
                   {i18n.t("download.action.download")}
                   {i18n.t("download.action.download")}
                 </a>
                 </a>
               </div>
               </div>
@@ -224,7 +230,7 @@ export default function Download() {
                   </span>
                   </span>
                   <span>{i18n.t("download.platform.linuxDeb")}</span>
                   <span>{i18n.t("download.platform.linuxDeb")}</span>
                 </div>
                 </div>
-                <a href={getDownloadHref("linux-x64-deb")} data-component="action-button">
+                <a href={language.route(getDownloadHref("linux-x64-deb"))} data-component="action-button">
                   {i18n.t("download.action.download")}
                   {i18n.t("download.action.download")}
                 </a>
                 </a>
               </div>
               </div>
@@ -240,7 +246,7 @@ export default function Download() {
                   </span>
                   </span>
                   <span>{i18n.t("download.platform.linuxRpm")}</span>
                   <span>{i18n.t("download.platform.linuxRpm")}</span>
                 </div>
                 </div>
-                <a href={getDownloadHref("linux-x64-rpm")} data-component="action-button">
+                <a href={language.route(getDownloadHref("linux-x64-rpm"))} data-component="action-button">
                   {i18n.t("download.action.download")}
                   {i18n.t("download.action.download")}
                 </a>
                 </a>
               </div>
               </div>
@@ -257,7 +263,7 @@ export default function Download() {
                   </span>
                   </span>
                   <span>Linux (.AppImage)</span>
                   <span>Linux (.AppImage)</span>
                 </div>
                 </div>
-                <a href={getDownloadHref("linux-x64-appimage")} data-component="action-button">
+                <a href={language.route(getDownloadHref("linux-x64-appimage"))} data-component="action-button">
                   Download
                   Download
                 </a>
                 </a>
               </div>*/}
               </div>*/}
@@ -422,36 +428,38 @@ export default function Download() {
             </li>
             </li>
             <li>
             <li>
               <Faq question={i18n.t("home.faq.q2")}>
               <Faq question={i18n.t("home.faq.q2")}>
-                {i18n.t("home.faq.a2.before")} <a href="/docs">{i18n.t("home.faq.a2.link")}</a>.
+                {i18n.t("home.faq.a2.before")} <a href={language.route("/docs")}>{i18n.t("home.faq.a2.link")}</a>.
               </Faq>
               </Faq>
             </li>
             </li>
             <li>
             <li>
               <Faq question={i18n.t("home.faq.q3")}>
               <Faq question={i18n.t("home.faq.q3")}>
                 {i18n.t("download.faq.a3.beforeLocal")}{" "}
                 {i18n.t("download.faq.a3.beforeLocal")}{" "}
-                <a href="/docs/providers/#lm-studio" target="_blank">
+                <a href={language.route("/docs/providers/#lm-studio")} target="_blank">
                   {i18n.t("download.faq.a3.localLink")}
                   {i18n.t("download.faq.a3.localLink")}
                 </a>{" "}
                 </a>{" "}
-                {i18n.t("download.faq.a3.afterLocal.beforeZen")} <A href="/zen">{i18n.t("nav.zen")}</A>
+                {i18n.t("download.faq.a3.afterLocal.beforeZen")}{" "}
+                <A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
                 {i18n.t("download.faq.a3.afterZen")}
                 {i18n.t("download.faq.a3.afterZen")}
               </Faq>
               </Faq>
             </li>
             </li>
             <li>
             <li>
               <Faq question={i18n.t("home.faq.q5")}>
               <Faq question={i18n.t("home.faq.q5")}>
-                {i18n.t("home.faq.a5.beforeDesktop")} <a href="/download">{i18n.t("home.faq.a5.desktop")}</a>{" "}
-                {i18n.t("home.faq.a5.and")} <a href="/docs/cli/#web">{i18n.t("home.faq.a5.web")}</a>!
+                {i18n.t("home.faq.a5.beforeDesktop")}{" "}
+                <a href={language.route("/download")}>{i18n.t("home.faq.a5.desktop")}</a> {i18n.t("home.faq.a5.and")}{" "}
+                <a href={language.route("/docs/cli/#web")}>{i18n.t("home.faq.a5.web")}</a>!
               </Faq>
               </Faq>
             </li>
             </li>
             <li>
             <li>
               <Faq question={i18n.t("home.faq.q6")}>
               <Faq question={i18n.t("home.faq.q6")}>
                 {i18n.t("download.faq.a5.p1")} {i18n.t("download.faq.a5.p2.beforeZen")}{" "}
                 {i18n.t("download.faq.a5.p1")} {i18n.t("download.faq.a5.p2.beforeZen")}{" "}
-                <A href="/zen">{i18n.t("nav.zen")}</A>
+                <A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
                 {i18n.t("download.faq.a5.p2.afterZen")}
                 {i18n.t("download.faq.a5.p2.afterZen")}
               </Faq>
               </Faq>
             </li>
             </li>
             <li>
             <li>
               <Faq question={i18n.t("home.faq.q7")}>
               <Faq question={i18n.t("home.faq.q7")}>
                 {i18n.t("download.faq.a6.p1")} {i18n.t("download.faq.a6.p2.beforeShare")}{" "}
                 {i18n.t("download.faq.a6.p1")} {i18n.t("download.faq.a6.p2.beforeShare")}{" "}
-                <a href="/docs/share/#privacy">{i18n.t("download.faq.a6.shareLink")}</a>.
+                <a href={language.route("/docs/share/#privacy")}>{i18n.t("download.faq.a6.shareLink")}</a>.
               </Faq>
               </Faq>
             </li>
             </li>
             <li>
             <li>

+ 3 - 3
packages/console/app/src/routes/enterprise/index.tsx

@@ -1,12 +1,12 @@
 import "./index.css"
 import "./index.css"
-import { Title, Meta, Link } from "@solidjs/meta"
+import { Title, Meta } from "@solidjs/meta"
 import { createSignal, Show } from "solid-js"
 import { createSignal, Show } from "solid-js"
-import { config } from "~/config"
 import { Header } from "~/component/header"
 import { Header } from "~/component/header"
 import { Footer } from "~/component/footer"
 import { Footer } from "~/component/footer"
 import { Legal } from "~/component/legal"
 import { Legal } from "~/component/legal"
 import { Faq } from "~/component/faq"
 import { Faq } from "~/component/faq"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
+import { LocaleLinks } from "~/component/locale-links"
 
 
 export default function Enterprise() {
 export default function Enterprise() {
   const i18n = useI18n()
   const i18n = useI18n()
@@ -57,7 +57,7 @@ export default function Enterprise() {
   return (
   return (
     <main data-page="enterprise">
     <main data-page="enterprise">
       <Title>{i18n.t("enterprise.title")}</Title>
       <Title>{i18n.t("enterprise.title")}</Title>
-      <Link rel="canonical" href={`${config.baseUrl}/enterprise`} />
+      <LocaleLinks path="/enterprise" />
       <Meta name="description" content={i18n.t("enterprise.meta.description")} />
       <Meta name="description" content={i18n.t("enterprise.meta.description")} />
       <div data-component="container">
       <div data-component="container">
         <Header />
         <Header />

+ 22 - 15
packages/console/app/src/routes/index.tsx

@@ -1,5 +1,5 @@
 import "./index.css"
 import "./index.css"
-import { Title, Meta, Link } from "@solidjs/meta"
+import { Title, Meta } from "@solidjs/meta"
 //import { HttpHeader } from "@solidjs/start"
 //import { HttpHeader } from "@solidjs/start"
 import video from "../asset/lander/opencode-min.mp4"
 import video from "../asset/lander/opencode-min.mp4"
 import videoPoster from "../asset/lander/opencode-poster.png"
 import videoPoster from "../asset/lander/opencode-poster.png"
@@ -15,6 +15,8 @@ import { github } from "~/lib/github"
 import { createMemo } from "solid-js"
 import { createMemo } from "solid-js"
 import { config } from "~/config"
 import { config } from "~/config"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
+import { useLanguage } from "~/context/language"
+import { LocaleLinks } from "~/component/locale-links"
 
 
 function CopyStatus() {
 function CopyStatus() {
   return (
   return (
@@ -27,6 +29,7 @@ function CopyStatus() {
 
 
 export default function Home() {
 export default function Home() {
   const i18n = useI18n()
   const i18n = useI18n()
+  const language = useLanguage()
   const githubData = createAsync(() => github())
   const githubData = createAsync(() => github())
   const release = createMemo(() => githubData()?.release)
   const release = createMemo(() => githubData()?.release)
 
 
@@ -46,7 +49,7 @@ export default function Home() {
     <main data-page="opencode">
     <main data-page="opencode">
       {/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
       {/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
       <Title>{i18n.t("home.title")}</Title>
       <Title>{i18n.t("home.title")}</Title>
-      <Link rel="canonical" href={config.baseUrl} />
+      <LocaleLinks path="/" />
       <Meta property="og:image" content="/social-share.png" />
       <Meta property="og:image" content="/social-share.png" />
       <Meta name="twitter:image" content="/social-share.png" />
       <Meta name="twitter:image" content="/social-share.png" />
       <div data-component="container">
       <div data-component="container">
@@ -61,10 +64,10 @@ export default function Home() {
                   {i18n.t("home.banner.text")}
                   {i18n.t("home.banner.text")}
                   <span data-slot="platforms"> {i18n.t("home.banner.platforms")}</span>.
                   <span data-slot="platforms"> {i18n.t("home.banner.platforms")}</span>.
                 </span>
                 </span>
-                <a href="/download" data-slot="link">
+                <a href={language.route("/download")} data-slot="link">
                   {i18n.t("home.banner.downloadNow")}
                   {i18n.t("home.banner.downloadNow")}
                 </a>
                 </a>
-                <a href="/download" data-slot="link-mobile">
+                <a href={language.route("/download")} data-slot="link-mobile">
                   {i18n.t("home.banner.downloadBetaNow")}
                   {i18n.t("home.banner.downloadBetaNow")}
                 </a>
                 </a>
               </div>
               </div>
@@ -217,7 +220,7 @@ export default function Home() {
                 </div>
                 </div>
               </li>
               </li>
             </ul>
             </ul>
-            <a href="/docs">
+            <a href={language.route("/docs")}>
               <span>{i18n.t("home.what.readDocs")} </span>
               <span>{i18n.t("home.what.readDocs")} </span>
               <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
               <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                 <path
                 <path
@@ -657,7 +660,7 @@ export default function Home() {
 
 
                 <p>
                 <p>
                   {i18n.t("home.privacy.body")} {i18n.t("home.privacy.learnMore")}{" "}
                   {i18n.t("home.privacy.body")} {i18n.t("home.privacy.learnMore")}{" "}
-                  <a href="/docs/enterprise/">{i18n.t("home.privacy.link")}</a>.
+                  <a href={language.route("/docs/enterprise/")}>{i18n.t("home.privacy.link")}</a>.
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
@@ -673,14 +676,15 @@ export default function Home() {
               </li>
               </li>
               <li>
               <li>
                 <Faq question={i18n.t("home.faq.q2")}>
                 <Faq question={i18n.t("home.faq.q2")}>
-                  {i18n.t("home.faq.a2.before")} <a href="/docs">{i18n.t("home.faq.a2.link")}</a>.
+                  {i18n.t("home.faq.a2.before")} <a href={language.route("/docs")}>{i18n.t("home.faq.a2.link")}</a>.
                 </Faq>
                 </Faq>
               </li>
               </li>
               <li>
               <li>
                 <Faq question={i18n.t("home.faq.q3")}>
                 <Faq question={i18n.t("home.faq.q3")}>
-                  {i18n.t("home.faq.a3.p1")} {i18n.t("home.faq.a3.p2.beforeZen")} <A href="/zen">{i18n.t("nav.zen")}</A>
+                  {i18n.t("home.faq.a3.p1")} {i18n.t("home.faq.a3.p2.beforeZen")}{" "}
+                  <A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
                   {i18n.t("home.faq.a3.p2.afterZen")} {i18n.t("home.faq.a3.p3")} {i18n.t("home.faq.a3.p4.beforeLocal")}{" "}
                   {i18n.t("home.faq.a3.p2.afterZen")} {i18n.t("home.faq.a3.p3")} {i18n.t("home.faq.a3.p4.beforeLocal")}{" "}
-                  <a href="/docs/providers/#lm-studio" target="_blank">
+                  <a href={language.route("/docs/providers/#lm-studio")} target="_blank">
                     {i18n.t("home.faq.a3.p4.localLink")}
                     {i18n.t("home.faq.a3.p4.localLink")}
                   </a>
                   </a>
                   .
                   .
@@ -688,13 +692,15 @@ export default function Home() {
               </li>
               </li>
               <li>
               <li>
                 <Faq question={i18n.t("home.faq.q4")}>
                 <Faq question={i18n.t("home.faq.q4")}>
-                  {i18n.t("home.faq.a4.p1")} <a href="/docs/providers/#directory">{i18n.t("common.learnMore")}</a>.
+                  {i18n.t("home.faq.a4.p1")}{" "}
+                  <a href={language.route("/docs/providers/#directory")}>{i18n.t("common.learnMore")}</a>.
                 </Faq>
                 </Faq>
               </li>
               </li>
               <li>
               <li>
                 <Faq question={i18n.t("home.faq.q5")}>
                 <Faq question={i18n.t("home.faq.q5")}>
-                  {i18n.t("home.faq.a5.beforeDesktop")} <a href="/download">{i18n.t("home.faq.a5.desktop")}</a>{" "}
-                  {i18n.t("home.faq.a5.and")} <a href="/docs/web">{i18n.t("home.faq.a5.web")}</a>!
+                  {i18n.t("home.faq.a5.beforeDesktop")}{" "}
+                  <a href={language.route("/download")}>{i18n.t("home.faq.a5.desktop")}</a> {i18n.t("home.faq.a5.and")}{" "}
+                  <a href={language.route("/docs/web")}>{i18n.t("home.faq.a5.web")}</a>!
                 </Faq>
                 </Faq>
               </li>
               </li>
               <li>
               <li>
@@ -703,8 +709,9 @@ export default function Home() {
               <li>
               <li>
                 <Faq question={i18n.t("home.faq.q7")}>
                 <Faq question={i18n.t("home.faq.q7")}>
                   {i18n.t("home.faq.a7.p1")} {i18n.t("home.faq.a7.p2.beforeModels")}{" "}
                   {i18n.t("home.faq.a7.p1")} {i18n.t("home.faq.a7.p2.beforeModels")}{" "}
-                  <a href="/docs/zen/#privacy">{i18n.t("home.faq.a7.p2.modelsLink")}</a> {i18n.t("home.faq.a7.p2.and")}{" "}
-                  <a href="/docs/share/#privacy">{i18n.t("home.faq.a7.p2.shareLink")}</a>.
+                  <a href={language.route("/docs/zen/#privacy")}>{i18n.t("home.faq.a7.p2.modelsLink")}</a>{" "}
+                  {i18n.t("home.faq.a7.p2.and")}{" "}
+                  <a href={language.route("/docs/share/#privacy")}>{i18n.t("home.faq.a7.p2.shareLink")}</a>.
                 </Faq>
                 </Faq>
               </li>
               </li>
               <li>
               <li>
@@ -808,7 +815,7 @@ export default function Home() {
                   </svg>
                   </svg>
                 </div>
                 </div>
               </div>
               </div>
-              <A href="/zen">
+              <A href={language.route("/zen")}>
                 <span>{i18n.t("home.zenCta.link")} </span>
                 <span>{i18n.t("home.zenCta.link")} </span>
                 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                   <path
                   <path

+ 8 - 6
packages/console/app/src/routes/legal/privacy-policy/index.tsx

@@ -1,16 +1,18 @@
 import "../../brand/index.css"
 import "../../brand/index.css"
 import "./index.css"
 import "./index.css"
-import { Title, Meta, Link } from "@solidjs/meta"
+import { Title, Meta } from "@solidjs/meta"
 import { Header } from "~/component/header"
 import { Header } from "~/component/header"
-import { config } from "~/config"
 import { Footer } from "~/component/footer"
 import { Footer } from "~/component/footer"
 import { Legal } from "~/component/legal"
 import { Legal } from "~/component/legal"
+import { LocaleLinks } from "~/component/locale-links"
+import { useLanguage } from "~/context/language"
 
 
 export default function PrivacyPolicy() {
 export default function PrivacyPolicy() {
+  const language = useLanguage()
   return (
   return (
     <main data-page="legal">
     <main data-page="legal">
       <Title>OpenCode | Privacy Policy</Title>
       <Title>OpenCode | Privacy Policy</Title>
-      <Link rel="canonical" href={`${config.baseUrl}/legal/privacy-policy`} />
+      <LocaleLinks path="/legal/privacy-policy" />
       <Meta name="description" content="OpenCode privacy policy" />
       <Meta name="description" content="OpenCode privacy policy" />
       <div data-component="container">
       <div data-component="container">
         <Header />
         <Header />
@@ -33,9 +35,9 @@ export default function PrivacyPolicy() {
 
 
               <p>
               <p>
                 Remember that your use of OpenCode is at all times subject to our Terms of Use,{" "}
                 Remember that your use of OpenCode is at all times subject to our Terms of Use,{" "}
-                <a href="/legal/terms-of-service">https://opencode.ai/legal/terms-of-service</a>, which incorporates
-                this Privacy Policy. Any terms we use in this Policy without defining them have the definitions given to
-                them in the Terms of Use.
+                <a href={language.route("/legal/terms-of-service")}>https://opencode.ai/legal/terms-of-service</a>,
+                which incorporates this Privacy Policy. Any terms we use in this Policy without defining them have the
+                definitions given to them in the Terms of Use.
               </p>
               </p>
 
 
               <p>You may print a copy of this Privacy Policy by clicking the print button in your browser.</p>
               <p>You may print a copy of this Privacy Policy by clicking the print button in your browser.</p>

+ 13 - 10
packages/console/app/src/routes/legal/terms-of-service/index.tsx

@@ -1,16 +1,18 @@
 import "../../brand/index.css"
 import "../../brand/index.css"
 import "./index.css"
 import "./index.css"
-import { Title, Meta, Link } from "@solidjs/meta"
+import { Title, Meta } from "@solidjs/meta"
 import { Header } from "~/component/header"
 import { Header } from "~/component/header"
-import { config } from "~/config"
 import { Footer } from "~/component/footer"
 import { Footer } from "~/component/footer"
 import { Legal } from "~/component/legal"
 import { Legal } from "~/component/legal"
+import { LocaleLinks } from "~/component/locale-links"
+import { useLanguage } from "~/context/language"
 
 
 export default function TermsOfService() {
 export default function TermsOfService() {
+  const language = useLanguage()
   return (
   return (
     <main data-page="legal">
     <main data-page="legal">
       <Title>OpenCode | Terms of Service</Title>
       <Title>OpenCode | Terms of Service</Title>
-      <Link rel="canonical" href={`${config.baseUrl}/legal/terms-of-service`} />
+      <LocaleLinks path="/legal/terms-of-service" />
       <Meta name="description" content="OpenCode terms of service" />
       <Meta name="description" content="OpenCode terms of service" />
       <div data-component="container">
       <div data-component="container">
         <Header />
         <Header />
@@ -36,7 +38,7 @@ export default function TermsOfService() {
                 <strong>ANOMALY INNOVATIONS, INC.</strong> ("OpenCode," "we" and "us"). Your use of the Services in any
                 <strong>ANOMALY INNOVATIONS, INC.</strong> ("OpenCode," "we" and "us"). Your use of the Services in any
                 way means that you agree to all of these Terms, and these Terms will remain in effect while you use the
                 way means that you agree to all of these Terms, and these Terms will remain in effect while you use the
                 Services. These Terms include the provisions in this document as well as those in the Privacy Policy{" "}
                 Services. These Terms include the provisions in this document as well as those in the Privacy Policy{" "}
-                <a href="/legal/privacy-policy">https://opencode.ai/legal/privacy-policy</a>.{" "}
+                <a href={language.route("/legal/privacy-policy")}>https://opencode.ai/legal/privacy-policy</a>.{" "}
                 <strong>
                 <strong>
                   Your use of or participation in certain Services may also be subject to additional policies, rules
                   Your use of or participation in certain Services may also be subject to additional policies, rules
                   and/or conditions ("Additional Terms"), which are incorporated herein by reference, and you understand
                   and/or conditions ("Additional Terms"), which are incorporated herein by reference, and you understand
@@ -259,9 +261,10 @@ export default function TermsOfService() {
               <h3>Paid Services</h3>
               <h3>Paid Services</h3>
               <p>
               <p>
                 Certain of our Services, including Zen, may be subject to payments now or in the future (the "Paid
                 Certain of our Services, including Zen, may be subject to payments now or in the future (the "Paid
-                Services"). Please see our Paid Services page <a href="/zen">https://opencode.ai/zen</a> for a
-                description of the current Paid Services. Please note that any payment terms presented to you in the
-                process of using or signing up for a Paid Service are deemed part of these Terms.
+                Services"). Please see our Paid Services page{" "}
+                <a href={language.route("/zen")}>https://opencode.ai/zen</a> for a description of the current Paid
+                Services. Please note that any payment terms presented to you in the process of using or signing up for
+                a Paid Service are deemed part of these Terms.
               </p>
               </p>
 
 
               <h3>Billing</h3>
               <h3>Billing</h3>
@@ -315,9 +318,9 @@ export default function TermsOfService() {
               <h2 id="what-if-i-want-to-stop">What if I want to stop using the Services?</h2>
               <h2 id="what-if-i-want-to-stop">What if I want to stop using the Services?</h2>
               <p>
               <p>
                 You're free to do that at any time; please refer to our Privacy Policy{" "}
                 You're free to do that at any time; please refer to our Privacy Policy{" "}
-                <a href="/legal/privacy-policy">https://opencode.ai/legal/privacy-policy</a>, as well as the licenses
-                above, to understand how we treat information you provide to us after you have stopped using our
-                Services.
+                <a href={language.route("/legal/privacy-policy")}>https://opencode.ai/legal/privacy-policy</a>, as well
+                as the licenses above, to understand how we treat information you provide to us after you have stopped
+                using our Services.
               </p>
               </p>
 
 
               <p>
               <p>

+ 2 - 2
packages/console/app/src/routes/s/[id].ts

@@ -1,5 +1,5 @@
 import type { APIEvent } from "@solidjs/start/server"
 import type { APIEvent } from "@solidjs/start/server"
-import { localeFromCookieHeader, tag } from "~/lib/language"
+import { LOCALE_HEADER, localeFromCookieHeader, parseLocale, tag } from "~/lib/language"
 
 
 async function handler(evt: APIEvent) {
 async function handler(evt: APIEvent) {
   const req = evt.request.clone()
   const req = evt.request.clone()
@@ -7,7 +7,7 @@ async function handler(evt: APIEvent) {
   const targetUrl = `https://docs.opencode.ai/docs${url.pathname}${url.search}`
   const targetUrl = `https://docs.opencode.ai/docs${url.pathname}${url.search}`
 
 
   const headers = new Headers(req.headers)
   const headers = new Headers(req.headers)
-  const locale = localeFromCookieHeader(req.headers.get("cookie"))
+  const locale = parseLocale(req.headers.get(LOCALE_HEADER)) ?? localeFromCookieHeader(req.headers.get("cookie"))
   if (locale) headers.set("accept-language", tag(locale))
   if (locale) headers.set("accept-language", tag(locale))
 
 
   const response = await fetch(targetUrl, {
   const response = await fetch(targetUrl, {

+ 2 - 2
packages/console/app/src/routes/t/[...path].tsx

@@ -1,5 +1,5 @@
 import type { APIEvent } from "@solidjs/start/server"
 import type { APIEvent } from "@solidjs/start/server"
-import { localeFromCookieHeader, tag } from "~/lib/language"
+import { LOCALE_HEADER, localeFromCookieHeader, parseLocale, tag } from "~/lib/language"
 
 
 async function handler(evt: APIEvent) {
 async function handler(evt: APIEvent) {
   const req = evt.request.clone()
   const req = evt.request.clone()
@@ -7,7 +7,7 @@ async function handler(evt: APIEvent) {
   const targetUrl = `https://enterprise.opencode.ai/${url.pathname}${url.search}`
   const targetUrl = `https://enterprise.opencode.ai/${url.pathname}${url.search}`
 
 
   const headers = new Headers(req.headers)
   const headers = new Headers(req.headers)
-  const locale = localeFromCookieHeader(req.headers.get("cookie"))
+  const locale = parseLocale(req.headers.get(LOCALE_HEADER)) ?? localeFromCookieHeader(req.headers.get("cookie"))
   if (locale) headers.set("accept-language", tag(locale))
   if (locale) headers.set("accept-language", tag(locale))
 
 
   const response = await fetch(targetUrl, {
   const response = await fetch(targetUrl, {

+ 8 - 6
packages/console/app/src/routes/temp.tsx

@@ -6,6 +6,7 @@ import logoDark from "../asset/logo-ornate-dark.svg"
 import IMG_SPLASH from "../asset/lander/screenshot-splash.png"
 import IMG_SPLASH from "../asset/lander/screenshot-splash.png"
 import { IconCopy, IconCheck } from "../component/icon"
 import { IconCopy, IconCheck } from "../component/icon"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
+import { useLanguage } from "~/context/language"
 
 
 function CopyStatus() {
 function CopyStatus() {
   return (
   return (
@@ -18,6 +19,7 @@ function CopyStatus() {
 
 
 export default function Home() {
 export default function Home() {
   const i18n = useI18n()
   const i18n = useI18n()
+  const language = useLanguage()
 
 
   onMount(() => {
   onMount(() => {
     const commands = document.querySelectorAll("[data-copy]")
     const commands = document.querySelectorAll("[data-copy]")
@@ -49,16 +51,16 @@ export default function Home() {
           <img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
           <img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
           <h1 data-slot="title">{i18n.t("temp.hero.title")}</h1>
           <h1 data-slot="title">{i18n.t("temp.hero.title")}</h1>
           <div data-slot="login">
           <div data-slot="login">
-            <a href="/auth">{i18n.t("temp.zen")}</a>
+            <a href={language.route("/auth")}>{i18n.t("temp.zen")}</a>
           </div>
           </div>
         </section>
         </section>
 
 
         <section data-component="cta">
         <section data-component="cta">
           <div data-slot="left">
           <div data-slot="left">
-            <a href="/docs">{i18n.t("temp.getStarted")}</a>
+            <a href={language.route("/docs")}>{i18n.t("temp.getStarted")}</a>
           </div>
           </div>
           <div data-slot="center">
           <div data-slot="center">
-            <a href="/auth">{i18n.t("temp.zen")}</a>
+            <a href={language.route("/auth")}>{i18n.t("temp.zen")}</a>
           </div>
           </div>
           <div data-slot="right">
           <div data-slot="right">
             <button data-copy data-slot="command">
             <button data-copy data-slot="command">
@@ -83,8 +85,8 @@ export default function Home() {
             </li>
             </li>
             <li>
             <li>
               <strong>{i18n.t("temp.zen")}</strong> {i18n.t("temp.feature.zen.beforeLink")}{" "}
               <strong>{i18n.t("temp.zen")}</strong> {i18n.t("temp.feature.zen.beforeLink")}{" "}
-              <a href="/docs/zen">{i18n.t("temp.feature.zen.link")}</a> {i18n.t("temp.feature.zen.afterLink")}{" "}
-              <label>{i18n.t("home.banner.badge")}</label>
+              <a href={language.route("/docs/zen")}>{i18n.t("temp.feature.zen.link")}</a>{" "}
+              {i18n.t("temp.feature.zen.afterLink")} <label>{i18n.t("home.banner.badge")}</label>
             </li>
             </li>
             <li>
             <li>
               <strong>{i18n.t("home.what.multiSession.title")}</strong> {i18n.t("home.what.multiSession.body")}
               <strong>{i18n.t("home.what.multiSession.title")}</strong> {i18n.t("home.what.multiSession.body")}
@@ -148,7 +150,7 @@ export default function Home() {
         <section data-component="screenshots">
         <section data-component="screenshots">
           <figure>
           <figure>
             <figcaption>{i18n.t("temp.screenshot.caption")}</figcaption>
             <figcaption>{i18n.t("temp.screenshot.caption")}</figcaption>
-            <a href="/docs/cli">
+            <a href={language.route("/docs/cli")}>
               <img src={IMG_SPLASH} alt={i18n.t("temp.screenshot.alt")} />
               <img src={IMG_SPLASH} alt={i18n.t("temp.screenshot.alt")} />
             </a>
             </a>
           </figure>
           </figure>

+ 3 - 1
packages/console/app/src/routes/user-menu.tsx

@@ -3,6 +3,7 @@ import { getRequestEvent } from "solid-js/web"
 import { useAuthSession } from "~/context/auth"
 import { useAuthSession } from "~/context/auth"
 import { Dropdown } from "~/component/dropdown"
 import { Dropdown } from "~/component/dropdown"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
+import { useLanguage } from "~/context/language"
 import "./user-menu.css"
 import "./user-menu.css"
 
 
 const logout = action(async () => {
 const logout = action(async () => {
@@ -22,10 +23,11 @@ const logout = action(async () => {
 
 
 export function UserMenu(props: { email: string | null | undefined }) {
 export function UserMenu(props: { email: string | null | undefined }) {
   const i18n = useI18n()
   const i18n = useI18n()
+  const language = useLanguage()
   return (
   return (
     <div data-component="user-menu">
     <div data-component="user-menu">
       <Dropdown trigger={props.email ?? ""} align="right">
       <Dropdown trigger={props.email ?? ""} align="right">
-        <a href="/auth/logout" data-slot="item">
+        <a href={language.route("/auth/logout")} data-slot="item">
           {i18n.t("user.logout")}
           {i18n.t("user.logout")}
         </a>
         </a>
       </Dropdown>
       </Dropdown>

+ 3 - 1
packages/console/app/src/routes/workspace.tsx

@@ -6,6 +6,7 @@ import { UserMenu } from "./user-menu"
 import { withActor } from "~/context/auth.withActor"
 import { withActor } from "~/context/auth.withActor"
 import { User } from "@opencode-ai/console-core/user.js"
 import { User } from "@opencode-ai/console-core/user.js"
 import { Actor } from "@opencode-ai/console-core/actor.js"
 import { Actor } from "@opencode-ai/console-core/actor.js"
+import { useLanguage } from "~/context/language"
 
 
 const getUserEmail = query(async (workspaceID: string) => {
 const getUserEmail = query(async (workspaceID: string) => {
   "use server"
   "use server"
@@ -18,12 +19,13 @@ const getUserEmail = query(async (workspaceID: string) => {
 
 
 export default function WorkspaceLayout(props: RouteSectionProps) {
 export default function WorkspaceLayout(props: RouteSectionProps) {
   const params = useParams()
   const params = useParams()
+  const language = useLanguage()
   const userEmail = createAsync(() => getUserEmail(params.id!))
   const userEmail = createAsync(() => getUserEmail(params.id!))
   return (
   return (
     <main data-page="workspace">
     <main data-page="workspace">
       <header data-component="workspace-header">
       <header data-component="workspace-header">
         <div data-slot="header-brand">
         <div data-slot="header-brand">
-          <A href="/" data-component="site-title">
+          <A href={language.route("/")} data-component="site-title">
             <IconWorkspaceLogo />
             <IconWorkspaceLogo />
           </A>
           </A>
           <WorkspacePicker />
           <WorkspacePicker />

+ 3 - 1
packages/console/app/src/routes/workspace/[id]/members/member-section.tsx

@@ -8,6 +8,7 @@ import { Actor } from "@opencode-ai/console-core/actor.js"
 import { User } from "@opencode-ai/console-core/user.js"
 import { User } from "@opencode-ai/console-core/user.js"
 import { RoleDropdown } from "./role-dropdown"
 import { RoleDropdown } from "./role-dropdown"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
+import { useLanguage } from "~/context/language"
 import { formError, localizeError } from "~/lib/form-error"
 import { formError, localizeError } from "~/lib/form-error"
 
 
 const listMembers = query(async (workspaceID: string) => {
 const listMembers = query(async (workspaceID: string) => {
@@ -218,6 +219,7 @@ function MemberRow(props: {
 export function MemberSection() {
 export function MemberSection() {
   const params = useParams()
   const params = useParams()
   const i18n = useI18n()
   const i18n = useI18n()
+  const language = useLanguage()
   const data = createAsync(() => listMembers(params.id!))
   const data = createAsync(() => listMembers(params.id!))
   const submission = useSubmission(inviteMember)
   const submission = useSubmission(inviteMember)
   const [store, setStore] = createStore({
   const [store, setStore] = createStore({
@@ -277,7 +279,7 @@ export function MemberSection() {
       </div>
       </div>
       <div data-slot="beta-notice">
       <div data-slot="beta-notice">
         {i18n.t("workspace.members.beta.beforeLink")}{" "}
         {i18n.t("workspace.members.beta.beforeLink")}{" "}
-        <a href="/docs/zen/#for-teams" target="_blank" rel="noopener noreferrer">
+        <a href={language.route("/docs/zen/#for-teams")} target="_blank" rel="noopener noreferrer">
           {i18n.t("common.learnMore")}
           {i18n.t("common.learnMore")}
         </a>
         </a>
         .
         .

+ 4 - 2
packages/console/app/src/routes/workspace/[id]/model-section.tsx

@@ -17,6 +17,7 @@ import {
   IconZai,
   IconZai,
 } from "~/component/icon"
 } from "~/component/icon"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
+import { useLanguage } from "~/context/language"
 import { formError } from "~/lib/form-error"
 import { formError } from "~/lib/form-error"
 
 
 const getModelLab = (modelId: string) => {
 const getModelLab = (modelId: string) => {
@@ -80,6 +81,7 @@ const updateModel = action(async (form: FormData) => {
 export function ModelSection() {
 export function ModelSection() {
   const params = useParams()
   const params = useParams()
   const i18n = useI18n()
   const i18n = useI18n()
+  const language = useLanguage()
   const modelsInfo = createAsync(() => getModelsInfo(params.id!))
   const modelsInfo = createAsync(() => getModelsInfo(params.id!))
   const userInfo = createAsync(() => querySessionInfo(params.id!))
   const userInfo = createAsync(() => querySessionInfo(params.id!))
 
 
@@ -96,8 +98,8 @@ export function ModelSection() {
       <div data-slot="section-title">
       <div data-slot="section-title">
         <h2>{i18n.t("workspace.models.title")}</h2>
         <h2>{i18n.t("workspace.models.title")}</h2>
         <p>
         <p>
-          {i18n.t("workspace.models.subtitle.beforeLink")} <a href="/docs/zen#pricing ">{i18n.t("common.learnMore")}</a>
-          .
+          {i18n.t("workspace.models.subtitle.beforeLink")}{" "}
+          <a href={language.route("/docs/zen#pricing")}>{i18n.t("common.learnMore")}</a>.
         </p>
         </p>
       </div>
       </div>
       <div data-slot="models-list">
       <div data-slot="models-list">

+ 13 - 10
packages/console/app/src/routes/zen/index.tsx

@@ -1,9 +1,8 @@
 import "./index.css"
 import "./index.css"
 import { createAsync, query, redirect } from "@solidjs/router"
 import { createAsync, query, redirect } from "@solidjs/router"
-import { Title, Meta, Link } from "@solidjs/meta"
+import { Title, Meta } from "@solidjs/meta"
 //import { HttpHeader } from "@solidjs/start"
 //import { HttpHeader } from "@solidjs/start"
 import zenLogoLight from "../../asset/zen-ornate-light.svg"
 import zenLogoLight from "../../asset/zen-ornate-light.svg"
-import { config } from "~/config"
 import zenLogoDark from "../../asset/zen-ornate-dark.svg"
 import zenLogoDark from "../../asset/zen-ornate-dark.svg"
 import compareVideo from "../../asset/lander/opencode-comparison-min.mp4"
 import compareVideo from "../../asset/lander/opencode-comparison-min.mp4"
 import compareVideoPoster from "../../asset/lander/opencode-comparison-poster.png"
 import compareVideoPoster from "../../asset/lander/opencode-comparison-poster.png"
@@ -20,6 +19,8 @@ import { Header } from "~/component/header"
 import { getLastSeenWorkspaceID } from "../workspace/common"
 import { getLastSeenWorkspaceID } from "../workspace/common"
 import { IconGemini, IconMiniMax, IconZai } from "~/component/icon"
 import { IconGemini, IconMiniMax, IconZai } from "~/component/icon"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
+import { useLanguage } from "~/context/language"
+import { LocaleLinks } from "~/component/locale-links"
 
 
 const checkLoggedIn = query(async () => {
 const checkLoggedIn = query(async () => {
   "use server"
   "use server"
@@ -30,11 +31,12 @@ const checkLoggedIn = query(async () => {
 export default function Home() {
 export default function Home() {
   const loggedin = createAsync(() => checkLoggedIn())
   const loggedin = createAsync(() => checkLoggedIn())
   const i18n = useI18n()
   const i18n = useI18n()
+  const language = useLanguage()
   return (
   return (
     <main data-page="zen">
     <main data-page="zen">
       {/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
       {/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
       <Title>{i18n.t("zen.title")}</Title>
       <Title>{i18n.t("zen.title")}</Title>
-      <Link rel="canonical" href={`${config.baseUrl}/zen`} />
+      <LocaleLinks path="/zen" />
       <Meta property="og:image" content="/social-share-zen.png" />
       <Meta property="og:image" content="/social-share-zen.png" />
       <Meta name="twitter:image" content="/social-share-zen.png" />
       <Meta name="twitter:image" content="/social-share-zen.png" />
       <Meta name="opencode:auth" content={loggedin() ? "true" : "false"} />
       <Meta name="opencode:auth" content={loggedin() ? "true" : "false"} />
@@ -120,7 +122,7 @@ export default function Home() {
                   </svg>
                   </svg>
                 </div>
                 </div>
               </div>
               </div>
-              <a href="/auth">
+              <a href={language.route("/auth")}>
                 <span>{i18n.t("zen.cta.start")}</span>
                 <span>{i18n.t("zen.cta.start")}</span>
                 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                   <path
                   <path
@@ -175,7 +177,7 @@ export default function Home() {
                 <span>[1]</span>
                 <span>[1]</span>
                 <div>
                 <div>
                   <strong>{i18n.t("zen.how.step1.title")}</strong> - {i18n.t("zen.how.step1.beforeLink")}{" "}
                   <strong>{i18n.t("zen.how.step1.title")}</strong> - {i18n.t("zen.how.step1.beforeLink")}{" "}
-                  <a href="/docs/zen/#how-it-works" title={i18n.t("zen.how.step1.link")}>
+                  <a href={language.route("/docs/zen/#how-it-works")} title={i18n.t("zen.how.step1.link")}>
                     {i18n.t("zen.how.step1.link")}
                     {i18n.t("zen.how.step1.link")}
                   </a>
                   </a>
                 </div>
                 </div>
@@ -184,7 +186,8 @@ export default function Home() {
                 <span>[2]</span>
                 <span>[2]</span>
                 <div>
                 <div>
                   <strong>{i18n.t("zen.how.step2.title")}</strong> -{" "}
                   <strong>{i18n.t("zen.how.step2.title")}</strong> -{" "}
-                  <a href="/docs/zen/#pricing">{i18n.t("zen.how.step2.link")}</a> {i18n.t("zen.how.step2.afterLink")}
+                  <a href={language.route("/docs/zen/#pricing")}>{i18n.t("zen.how.step2.link")}</a>{" "}
+                  {i18n.t("zen.how.step2.afterLink")}
                 </div>
                 </div>
               </li>
               </li>
               <li>
               <li>
@@ -203,7 +206,7 @@ export default function Home() {
                 <span>[*]</span>
                 <span>[*]</span>
                 <p>
                 <p>
                   {i18n.t("zen.privacy.beforeExceptions")}{" "}
                   {i18n.t("zen.privacy.beforeExceptions")}{" "}
-                  <a href="/docs/zen/#privacy">{i18n.t("zen.privacy.exceptionsLink")}</a>.
+                  <a href={language.route("/docs/zen/#privacy")}>{i18n.t("zen.privacy.exceptionsLink")}</a>.
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
@@ -299,15 +302,15 @@ export default function Home() {
               <li>
               <li>
                 <Faq question={i18n.t("zen.faq.q4")}>
                 <Faq question={i18n.t("zen.faq.q4")}>
                   {i18n.t("zen.faq.a4.p1.beforePricing")}{" "}
                   {i18n.t("zen.faq.a4.p1.beforePricing")}{" "}
-                  <a href="/docs/zen/#pricing">{i18n.t("zen.faq.a4.p1.pricingLink")}</a>{" "}
+                  <a href={language.route("/docs/zen/#pricing")}>{i18n.t("zen.faq.a4.p1.pricingLink")}</a>{" "}
                   {i18n.t("zen.faq.a4.p1.afterPricing")} {i18n.t("zen.faq.a4.p2.beforeAccount")}{" "}
                   {i18n.t("zen.faq.a4.p1.afterPricing")} {i18n.t("zen.faq.a4.p2.beforeAccount")}{" "}
-                  <a href="/auth">{i18n.t("zen.faq.a4.p2.accountLink")}</a>. {i18n.t("zen.faq.a4.p3")}
+                  <a href={language.route("/auth")}>{i18n.t("zen.faq.a4.p2.accountLink")}</a>. {i18n.t("zen.faq.a4.p3")}
                 </Faq>
                 </Faq>
               </li>
               </li>
               <li>
               <li>
                 <Faq question={i18n.t("zen.faq.q5")}>
                 <Faq question={i18n.t("zen.faq.q5")}>
                   {i18n.t("zen.faq.a5.beforeExceptions")}{" "}
                   {i18n.t("zen.faq.a5.beforeExceptions")}{" "}
-                  <a href="/docs/zen/#privacy">{i18n.t("zen.faq.a5.exceptionsLink")}</a>.
+                  <a href={language.route("/docs/zen/#privacy")}>{i18n.t("zen.faq.a5.exceptionsLink")}</a>.
                 </Faq>
                 </Faq>
               </li>
               </li>
               <li>
               <li>

+ 3 - 1
packages/console/app/vite.config.ts

@@ -4,7 +4,9 @@ import { nitro } from "nitro/vite"
 
 
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
-    solidStart() as PluginOption,
+    solidStart({
+      middleware: "./src/middleware.ts",
+    }) as PluginOption,
     nitro({
     nitro({
       compatibilityDate: "2024-09-19",
       compatibilityDate: "2024-09-19",
       preset: "cloudflare_module",
       preset: "cloudflare_module",