diff options
| author | Adam <[email protected]> | 2026-02-06 11:30:40 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-06 11:30:40 -0600 |
| commit | 24cd84cda5522e4607e8e3fb3626f289d7e348f4 (patch) | |
| tree | 81dec57e565c24255e5e6ad2dfac58ea0e770018 /packages/console/app/src | |
| parent | 8069197329d2d1b958d8e7f63daaf9662a97027d (diff) | |
| download | opencode-24cd84cda5522e4607e8e3fb3626f289d7e348f4.tar.gz opencode-24cd84cda5522e4607e8e3fb3626f289d7e348f4.zip | |
feat(www): locale specific urls (#12508)
Diffstat (limited to 'packages/console/app/src')
31 files changed, 260 insertions, 122 deletions
diff --git a/packages/console/app/src/app.tsx b/packages/console/app/src/app.tsx index 3d16a64ab..3eb70606a 100644 --- a/packages/console/app/src/app.tsx +++ b/packages/console/app/src/app.tsx @@ -8,11 +8,13 @@ import "@ibm/plex/css/ibm-plex.css" import "./app.css" import { LanguageProvider } from "~/context/language" import { I18nProvider } from "~/context/i18n" +import { strip } from "~/lib/language" export default function App() { return ( <Router explicitLinks={true} + transformUrl={strip} root={(props) => ( <LanguageProvider> <I18nProvider> diff --git a/packages/console/app/src/component/footer.tsx b/packages/console/app/src/component/footer.tsx index 45dae87ec..d81bf3247 100644 --- a/packages/console/app/src/component/footer.tsx +++ b/packages/console/app/src/component/footer.tsx @@ -26,13 +26,13 @@ export function Footer() { </a> </div> <div data-slot="cell"> - <a href="/docs">{i18n.t("footer.docs")}</a> + <a href={language.route("/docs")}>{i18n.t("footer.docs")}</a> </div> <div data-slot="cell"> - <a href="/changelog">{i18n.t("footer.changelog")}</a> + <a href={language.route("/changelog")}>{i18n.t("footer.changelog")}</a> </div> <div data-slot="cell"> - <a href="/discord">{i18n.t("footer.discord")}</a> + <a href={language.route("/discord")}>{i18n.t("footer.discord")}</a> </div> <div data-slot="cell"> <a href={config.social.twitter}>{i18n.t("footer.x")}</a> diff --git a/packages/console/app/src/component/header.tsx b/packages/console/app/src/component/header.tsx index 3eca8b88c..50f1b73d3 100644 --- a/packages/console/app/src/component/header.tsx +++ b/packages/console/app/src/component/header.tsx @@ -20,6 +20,7 @@ import { github } from "~/lib/github" import { createEffect, onCleanup } from "solid-js" import { config } from "~/config" import { useI18n } from "~/context/i18n" +import { useLanguage } from "~/context/language" import "./header-context-menu.css" 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 }) { const navigate = useNavigate() const i18n = useI18n() + const language = useLanguage() const githubData = createAsync(() => github()) const starCount = createMemo(() => githubData()?.stars @@ -121,7 +123,7 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { return ( <section data-component="top"> <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 dark" src={logoDark} alt="OpenCode" width="189" height="34" /> </A> @@ -142,7 +144,7 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { <img data-slot="copy dark" src={copyWordmarkDark} alt="" /> {i18n.t("nav.context.copyWordmark")} </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 dark" src={copyBrandAssetsDark} alt="" /> {i18n.t("nav.context.brandAssets")} @@ -157,24 +159,24 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { </a> </li> <li> - <a href="/docs">{i18n.t("nav.docs")}</a> + <a href={language.route("/docs")}>{i18n.t("nav.docs")}</a> </li> <li> - <A href="/enterprise">{i18n.t("nav.enterprise")}</A> + <A href={language.route("/enterprise")}>{i18n.t("nav.enterprise")}</A> </li> <li> <Switch> <Match when={props.zen}> - <a href="/auth">{i18n.t("nav.login")}</a> + <a href={language.route("/auth")}>{i18n.t("nav.login")}</a> </Match> <Match when={!props.zen}> - <A href="/zen">{i18n.t("nav.zen")}</A> + <A href={language.route("/zen")}>{i18n.t("nav.zen")}</A> </Match> </Switch> </li> <Show when={!props.hideGetStarted}> <li> - <A href="/download" data-slot="cta-button"> + <A href={language.route("/download")} data-slot="cta-button"> <svg width="18" height="18" @@ -245,7 +247,7 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { <nav data-component="nav-mobile-menu-list"> <ul> <li> - <A href="/">{i18n.t("nav.home")}</A> + <A href={language.route("/")}>{i18n.t("nav.home")}</A> </li> <li> <a href={config.github.repoUrl} target="_blank" style="white-space: nowrap;"> @@ -253,24 +255,24 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { </a> </li> <li> - <a href="/docs">{i18n.t("nav.docs")}</a> + <a href={language.route("/docs")}>{i18n.t("nav.docs")}</a> </li> <li> - <A href="/enterprise">{i18n.t("nav.enterprise")}</A> + <A href={language.route("/enterprise")}>{i18n.t("nav.enterprise")}</A> </li> <li> <Switch> <Match when={props.zen}> - <a href="/auth">{i18n.t("nav.login")}</a> + <a href={language.route("/auth")}>{i18n.t("nav.login")}</a> </Match> <Match when={!props.zen}> - <A href="/zen">{i18n.t("nav.zen")}</A> + <A href={language.route("/zen")}>{i18n.t("nav.zen")}</A> </Match> </Switch> </li> <Show when={!props.hideGetStarted}> <li> - <A href="/download" data-slot="cta-button"> + <A href={language.route("/download")} data-slot="cta-button"> {i18n.t("nav.getStartedFree")} </A> </li> diff --git a/packages/console/app/src/component/language-picker.tsx b/packages/console/app/src/component/language-picker.tsx index b30471d01..f42fd8065 100644 --- a/packages/console/app/src/component/language-picker.tsx +++ b/packages/console/app/src/component/language-picker.tsx @@ -1,10 +1,14 @@ import { For, createSignal } from "solid-js" +import { useLocation, useNavigate } from "@solidjs/router" import { Dropdown, DropdownItem } from "~/component/dropdown" import { useLanguage } from "~/context/language" +import { route, strip } from "~/lib/language" import "./language-picker.css" export function LanguagePicker(props: { align?: "left" | "right" } = {}) { const language = useLanguage() + const navigate = useNavigate() + const location = useLocation() const [open, setOpen] = createSignal(false) return ( @@ -21,6 +25,8 @@ export function LanguagePicker(props: { align?: "left" | "right" } = {}) { selected={locale === language.locale()} onClick={() => { 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) }} > diff --git a/packages/console/app/src/component/legal.tsx b/packages/console/app/src/component/legal.tsx index 3cc3627a7..39c534bf2 100644 --- a/packages/console/app/src/component/legal.tsx +++ b/packages/console/app/src/component/legal.tsx @@ -1,22 +1,24 @@ import { A } from "@solidjs/router" import { LanguagePicker } from "~/component/language-picker" import { useI18n } from "~/context/i18n" +import { useLanguage } from "~/context/language" export function Legal() { const i18n = useI18n() + const language = useLanguage() return ( <div data-component="legal"> <span> ©{new Date().getFullYear()} <a href="https://anoma.ly">Anomaly</a> </span> <span> - <A href="/brand">{i18n.t("legal.brand")}</A> + <A href={language.route("/brand")}>{i18n.t("legal.brand")}</A> </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> - <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> <LanguagePicker align="right" /> diff --git a/packages/console/app/src/component/locale-links.tsx b/packages/console/app/src/component/locale-links.tsx new file mode 100644 index 000000000..f773bb885 --- /dev/null +++ b/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}`} /> + </> + ) +} diff --git a/packages/console/app/src/context/language.tsx b/packages/console/app/src/context/language.tsx index 7e3e5286c..2999242f0 100644 --- a/packages/console/app/src/context/language.tsx +++ b/packages/console/app/src/context/language.tsx @@ -13,6 +13,7 @@ import { localeFromCookieHeader, localeFromRequest, parseLocale, + route as localeRoute, tag as localeTag, } from "~/lib/language" @@ -54,6 +55,9 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont label: localeLabel, tag: localeTag, dir: localeDir, + route(pathname: string) { + return localeRoute(store.locale, pathname) + }, setLocale(next: Locale) { setStore("locale", next) if (typeof document !== "object") return diff --git a/packages/console/app/src/lib/language.ts b/packages/console/app/src/lib/language.ts index e1e62d8fd..7506857ce 100644 --- a/packages/console/app/src/lib/language.ts +++ b/packages/console/app/src/lib/language.ts @@ -21,6 +21,12 @@ export const LOCALES = [ export type Locale = (typeof LOCALES)[number] 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 = { en: "English", @@ -68,6 +74,28 @@ export function parseLocale(value: unknown): Locale | 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) { return LABEL[locale] } @@ -160,6 +188,12 @@ export function localeFromCookieHeader(header: string | null) { } 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 ( localeFromCookieHeader(request.headers.get("cookie")) ?? detectFromAcceptLanguage(request.headers.get("accept-language")) diff --git a/packages/console/app/src/middleware.ts b/packages/console/app/src/middleware.ts index 620fc25aa..6667c4447 100644 --- a/packages/console/app/src/middleware.ts +++ b/packages/console/app/src/middleware.ts @@ -1,5 +1,16 @@ import { createMiddleware } from "@solidjs/start/middleware" +import { LOCALE_HEADER, cookie, fromPathname, strip } from "~/lib/language" 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) + }, }) diff --git a/packages/console/app/src/routes/[...404].tsx b/packages/console/app/src/routes/[...404].tsx index 7e7111ef6..414491f9d 100644 --- a/packages/console/app/src/routes/[...404].tsx +++ b/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 logoDark from "../asset/logo-ornate-dark.svg" import { useI18n } from "~/context/i18n" +import { useLanguage } from "~/context/language" export default function NotFound() { const i18n = useI18n() + const language = useLanguage() return ( <main data-page="not-found"> <Title>{i18n.t("notFound.title")}</Title> <HttpStatusCode code={404} /> <div data-component="content"> <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 dark" src={logoDark} alt="opencode logo dark" /> </a> @@ -22,16 +24,16 @@ export default function NotFound() { <section data-component="actions"> <div data-slot="action"> - <a href="/">{i18n.t("notFound.home")}</a> + <a href={language.route("/")}>{i18n.t("notFound.home")}</a> </div> <div data-slot="action"> - <a href="/docs">{i18n.t("notFound.docs")}</a> + <a href={language.route("/docs")}>{i18n.t("notFound.docs")}</a> </div> <div data-slot="action"> <a href="https://github.com/anomalyco/opencode">{i18n.t("notFound.github")}</a> </div> <div data-slot="action"> - <a href="/discord">{i18n.t("notFound.discord")}</a> + <a href={language.route("/discord")}>{i18n.t("notFound.discord")}</a> </div> </section> </div> diff --git a/packages/console/app/src/routes/black.tsx b/packages/console/app/src/routes/black.tsx index 012b392e2..243ae687d 100644 --- a/packages/console/app/src/routes/black.tsx +++ b/packages/console/app/src/routes/black.tsx @@ -1,5 +1,5 @@ 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 { github } from "~/lib/github" import { config } from "~/config" @@ -7,6 +7,7 @@ import { useLanguage } from "~/context/language" import { LanguagePicker } from "~/component/language-picker" import { useI18n } from "~/context/i18n" import Spotlight, { defaultConfig, type SpotlightAnimationState } from "~/component/spotlight" +import { LocaleLinks } from "~/component/locale-links" import "./black.css" export default function BlackLayout(props: RouteSectionProps) { @@ -70,9 +71,9 @@ export default function BlackLayout(props: RouteSectionProps) { <div data-page="black"> <Title>{i18n.t("black.meta.title")}</Title> <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: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:description" content={i18n.t("black.meta.description")} /> <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} /> <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"> <title>opencode</title> <g clip-path="url(#clip0_3654_210259)"> @@ -264,13 +265,13 @@ export default function BlackLayout(props: RouteSectionProps) { <a href={config.github.repoUrl} target="_blank"> {i18n.t("nav.github")} <span data-slot="github-stars">[{starCount()}]</span> </a> - <a href="/docs">{i18n.t("nav.docs")}</a> + <a href={language.route("/docs")}>{i18n.t("nav.docs")}</a> <LanguagePicker align="right" /> <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> - <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> </div> <span data-slot="anomaly-alt"> diff --git a/packages/console/app/src/routes/black/index.tsx b/packages/console/app/src/routes/black/index.tsx index ad7a115d0..72b196f57 100644 --- a/packages/console/app/src/routes/black/index.tsx +++ b/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 { PlanIcon, plans } from "./common" import { useI18n } from "~/context/i18n" +import { useLanguage } from "~/context/language" export default function Black() { const [params] = useSearchParams() const i18n = useI18n() + const language = useLanguage() const [selected, setSelected] = createSignal<string | null>((params.plan as string) || null) const [mounted, setMounted] = createSignal(false) const selectedPlan = createMemo(() => plans.find((p) => p.id === selected())) @@ -104,7 +106,7 @@ export default function Black() { </Switch> <p data-slot="fine-print" style={{ "view-transition-name": "fine-print" }}> {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> </section> </> diff --git a/packages/console/app/src/routes/black/subscribe/[plan].tsx b/packages/console/app/src/routes/black/subscribe/[plan].tsx index 347be7419..644d87d9b 100644 --- a/packages/console/app/src/routes/black/subscribe/[plan].tsx +++ b/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 { Billing } from "@opencode-ai/console-core/billing.js" import { useI18n } from "~/context/i18n" +import { useLanguage } from "~/context/language" import { formError } from "~/lib/form-error" 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() { const params = useParams() const i18n = useI18n() + const language = useLanguage() const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"] const plan = planData.id @@ -467,7 +469,7 @@ export default function BlackSubscribe() { </Modal> <p data-slot="fine-print"> {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> </section> </> diff --git a/packages/console/app/src/routes/black/workspace.tsx b/packages/console/app/src/routes/black/workspace.tsx index 03950a243..106e8a23e 100644 --- a/packages/console/app/src/routes/black/workspace.tsx +++ b/packages/console/app/src/routes/black/workspace.tsx @@ -220,13 +220,13 @@ export default function BlackWorkspace() { <a href={config.github.repoUrl} target="_blank"> {i18n.t("nav.github")} <span data-slot="github-stars">[{starCount()}]</span> </a> - <a href="/docs">{i18n.t("nav.docs")}</a> + <a href={language.route("/docs")}>{i18n.t("nav.docs")}</a> <LanguagePicker align="right" /> <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> - <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> </div> <span data-slot="anomaly-alt"> diff --git a/packages/console/app/src/routes/brand/index.tsx b/packages/console/app/src/routes/brand/index.tsx index a1bc6c54c..eda3c8472 100644 --- a/packages/console/app/src/routes/brand/index.tsx +++ b/packages/console/app/src/routes/brand/index.tsx @@ -1,10 +1,10 @@ import "./index.css" -import { Title, Meta, Link } from "@solidjs/meta" +import { Title, Meta } from "@solidjs/meta" import { Header } from "~/component/header" -import { config } from "~/config" import { Footer } from "~/component/footer" import { Legal } from "~/component/legal" import { useI18n } from "~/context/i18n" +import { LocaleLinks } from "~/component/locale-links" import previewLogoLight from "../../asset/brand/preview-opencode-logo-light.png" import previewLogoDark from "../../asset/brand/preview-opencode-logo-dark.png" import previewWordmarkLight from "../../asset/brand/preview-opencode-wordmark-light.png" @@ -56,7 +56,7 @@ export default function Brand() { return ( <main data-page="enterprise"> <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")} /> <div data-component="container"> <Header /> diff --git a/packages/console/app/src/routes/changelog/index.tsx b/packages/console/app/src/routes/changelog/index.tsx index 595ae9d1b..54f037479 100644 --- a/packages/console/app/src/routes/changelog/index.tsx +++ b/packages/console/app/src/routes/changelog/index.tsx @@ -1,15 +1,15 @@ import "./index.css" -import { Title, Meta, Link } from "@solidjs/meta" +import { Title, Meta } from "@solidjs/meta" import { createAsync } from "@solidjs/router" import { Header } from "~/component/header" import { Footer } from "~/component/footer" import { Legal } from "~/component/legal" -import { config } from "~/config" import { changelog } from "~/lib/changelog" import type { HighlightGroup } from "~/lib/changelog" import { For, Show, createSignal } from "solid-js" import { useI18n } from "~/context/i18n" import { useLanguage } from "~/context/language" +import { LocaleLinks } from "~/component/locale-links" function formatDate(dateString: string, locale: string) { const date = new Date(dateString) @@ -107,7 +107,7 @@ export default function Changelog() { return ( <main data-page="changelog"> <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")} /> <div data-component="container"> @@ -122,7 +122,8 @@ export default function Changelog() { <section data-component="releases"> <Show when={releases().length === 0}> <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> </Show> <For each={releases()}> diff --git a/packages/console/app/src/routes/docs/[...path].ts b/packages/console/app/src/routes/docs/[...path].ts index 5493c31a6..0711b5ce0 100644 --- a/packages/console/app/src/routes/docs/[...path].ts +++ b/packages/console/app/src/routes/docs/[...path].ts @@ -1,5 +1,5 @@ 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) { 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 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)) const response = await fetch(targetUrl, { diff --git a/packages/console/app/src/routes/docs/index.ts b/packages/console/app/src/routes/docs/index.ts index 5493c31a6..0711b5ce0 100644 --- a/packages/console/app/src/routes/docs/index.ts +++ b/packages/console/app/src/routes/docs/index.ts @@ -1,5 +1,5 @@ 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) { 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 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)) const response = await fetch(targetUrl, { diff --git a/packages/console/app/src/routes/download/index.tsx b/packages/console/app/src/routes/download/index.tsx index 0c89e4abd..b5dbbd39a 100644 --- a/packages/console/app/src/routes/download/index.tsx +++ b/packages/console/app/src/routes/download/index.tsx @@ -1,5 +1,5 @@ import "./index.css" -import { Title, Meta, Link } from "@solidjs/meta" +import { Title, Meta } from "@solidjs/meta" import { A, createAsync, query } from "@solidjs/router" import { Header } from "~/component/header" import { Footer } from "~/component/footer" @@ -11,6 +11,8 @@ import { config } from "~/config" import { createSignal, onMount, Show, JSX } from "solid-js" import { DownloadPlatform } from "./types" import { useI18n } from "~/context/i18n" +import { useLanguage } from "~/context/language" +import { LocaleLinks } from "~/component/locale-links" type OS = "macOS" | "Windows" | "Linux" | null @@ -66,6 +68,7 @@ function CopyStatus() { export default function Download() { const i18n = useI18n() + const language = useLanguage() const [detectedOS, setDetectedOS] = createSignal<OS>(null) onMount(() => { @@ -83,7 +86,7 @@ export default function Download() { return ( <main data-page="download"> <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")} /> <div data-component="container"> <Header hideGetStarted /> @@ -97,7 +100,10 @@ export default function Download() { <h1>{i18n.t("download.hero.title")}</h1> <p>{i18n.t("download.hero.subtitle")}</p> <Show when={detectedOS()}> - <a href={getDownloadHref(getDownloadPlatform(detectedOS()))} data-component="download-button"> + <a + href={language.route(getDownloadHref(getDownloadPlatform(detectedOS())))} + data-component="download-button" + > <IconDownload /> {i18n.t("download.hero.button", { os: detectedOS()! })} </a> @@ -169,7 +175,7 @@ export default function Download() { </span> <span>{i18n.t("download.platform.macosAppleSilicon")}</span> </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")} </a> </div> @@ -185,7 +191,7 @@ export default function Download() { </span> <span>{i18n.t("download.platform.macosIntel")}</span> </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")} </a> </div> @@ -208,7 +214,7 @@ export default function Download() { </span> <span>{i18n.t("download.platform.windowsX64")}</span> </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")} </a> </div> @@ -224,7 +230,7 @@ export default function Download() { </span> <span>{i18n.t("download.platform.linuxDeb")}</span> </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")} </a> </div> @@ -240,7 +246,7 @@ export default function Download() { </span> <span>{i18n.t("download.platform.linuxRpm")}</span> </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")} </a> </div> @@ -257,7 +263,7 @@ export default function Download() { </span> <span>Linux (.AppImage)</span> </div> - <a href={getDownloadHref("linux-x64-appimage")} data-component="action-button"> + <a href={language.route(getDownloadHref("linux-x64-appimage"))} data-component="action-button"> Download </a> </div>*/} @@ -422,36 +428,38 @@ export default function Download() { </li> <li> <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> </li> <li> <Faq question={i18n.t("home.faq.q3")}> {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")} </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")} </Faq> </li> <li> <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> </li> <li> <Faq question={i18n.t("home.faq.q6")}> {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")} </Faq> </li> <li> <Faq question={i18n.t("home.faq.q7")}> {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> </li> <li> diff --git a/packages/console/app/src/routes/enterprise/index.tsx b/packages/console/app/src/routes/enterprise/index.tsx index c9f487490..ee323ff82 100644 --- a/packages/console/app/src/routes/enterprise/index.tsx +++ b/packages/console/app/src/routes/enterprise/index.tsx @@ -1,12 +1,12 @@ import "./index.css" -import { Title, Meta, Link } from "@solidjs/meta" +import { Title, Meta } from "@solidjs/meta" import { createSignal, Show } from "solid-js" -import { config } from "~/config" import { Header } from "~/component/header" import { Footer } from "~/component/footer" import { Legal } from "~/component/legal" import { Faq } from "~/component/faq" import { useI18n } from "~/context/i18n" +import { LocaleLinks } from "~/component/locale-links" export default function Enterprise() { const i18n = useI18n() @@ -57,7 +57,7 @@ export default function Enterprise() { return ( <main data-page="enterprise"> <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")} /> <div data-component="container"> <Header /> diff --git a/packages/console/app/src/routes/index.tsx b/packages/console/app/src/routes/index.tsx index a56b2ac62..e47134d2b 100644 --- a/packages/console/app/src/routes/index.tsx +++ b/packages/console/app/src/routes/index.tsx @@ -1,5 +1,5 @@ import "./index.css" -import { Title, Meta, Link } from "@solidjs/meta" +import { Title, Meta } from "@solidjs/meta" //import { HttpHeader } from "@solidjs/start" import video from "../asset/lander/opencode-min.mp4" import videoPoster from "../asset/lander/opencode-poster.png" @@ -15,6 +15,8 @@ import { github } from "~/lib/github" import { createMemo } from "solid-js" import { config } from "~/config" import { useI18n } from "~/context/i18n" +import { useLanguage } from "~/context/language" +import { LocaleLinks } from "~/component/locale-links" function CopyStatus() { return ( @@ -27,6 +29,7 @@ function CopyStatus() { export default function Home() { const i18n = useI18n() + const language = useLanguage() const githubData = createAsync(() => github()) const release = createMemo(() => githubData()?.release) @@ -46,7 +49,7 @@ export default function Home() { <main data-page="opencode"> {/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/} <Title>{i18n.t("home.title")}</Title> - <Link rel="canonical" href={config.baseUrl} /> + <LocaleLinks path="/" /> <Meta property="og:image" content="/social-share.png" /> <Meta name="twitter:image" content="/social-share.png" /> <div data-component="container"> @@ -61,10 +64,10 @@ export default function Home() { {i18n.t("home.banner.text")} <span data-slot="platforms"> {i18n.t("home.banner.platforms")}</span>. </span> - <a href="/download" data-slot="link"> + <a href={language.route("/download")} data-slot="link"> {i18n.t("home.banner.downloadNow")} </a> - <a href="/download" data-slot="link-mobile"> + <a href={language.route("/download")} data-slot="link-mobile"> {i18n.t("home.banner.downloadBetaNow")} </a> </div> @@ -217,7 +220,7 @@ export default function Home() { </div> </li> </ul> - <a href="/docs"> + <a href={language.route("/docs")}> <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"> <path @@ -657,7 +660,7 @@ export default function Home() { <p> {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> </div> </div> @@ -673,14 +676,15 @@ export default function Home() { </li> <li> <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> </li> <li> <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")}{" "} - <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")} </a> . @@ -688,13 +692,15 @@ export default function Home() { </li> <li> <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> </li> <li> <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> </li> <li> @@ -703,8 +709,9 @@ export default function Home() { <li> <Faq question={i18n.t("home.faq.q7")}> {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> </li> <li> @@ -808,7 +815,7 @@ export default function Home() { </svg> </div> </div> - <A href="/zen"> + <A href={language.route("/zen")}> <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"> <path diff --git a/packages/console/app/src/routes/legal/privacy-policy/index.tsx b/packages/console/app/src/routes/legal/privacy-policy/index.tsx index 8b30ba14e..b1b210455 100644 --- a/packages/console/app/src/routes/legal/privacy-policy/index.tsx +++ b/packages/console/app/src/routes/legal/privacy-policy/index.tsx @@ -1,16 +1,18 @@ import "../../brand/index.css" import "./index.css" -import { Title, Meta, Link } from "@solidjs/meta" +import { Title, Meta } from "@solidjs/meta" import { Header } from "~/component/header" -import { config } from "~/config" import { Footer } from "~/component/footer" import { Legal } from "~/component/legal" +import { LocaleLinks } from "~/component/locale-links" +import { useLanguage } from "~/context/language" export default function PrivacyPolicy() { + const language = useLanguage() return ( <main data-page="legal"> <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" /> <div data-component="container"> <Header /> @@ -33,9 +35,9 @@ export default function PrivacyPolicy() { <p> 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>You may print a copy of this Privacy Policy by clicking the print button in your browser.</p> diff --git a/packages/console/app/src/routes/legal/terms-of-service/index.tsx b/packages/console/app/src/routes/legal/terms-of-service/index.tsx index f0d7be61c..f770aa7a0 100644 --- a/packages/console/app/src/routes/legal/terms-of-service/index.tsx +++ b/packages/console/app/src/routes/legal/terms-of-service/index.tsx @@ -1,16 +1,18 @@ import "../../brand/index.css" import "./index.css" -import { Title, Meta, Link } from "@solidjs/meta" +import { Title, Meta } from "@solidjs/meta" import { Header } from "~/component/header" -import { config } from "~/config" import { Footer } from "~/component/footer" import { Legal } from "~/component/legal" +import { LocaleLinks } from "~/component/locale-links" +import { useLanguage } from "~/context/language" export default function TermsOfService() { + const language = useLanguage() return ( <main data-page="legal"> <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" /> <div data-component="container"> <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 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{" "} - <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> 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 @@ -259,9 +261,10 @@ export default function TermsOfService() { <h3>Paid Services</h3> <p> 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> <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> <p> 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> diff --git a/packages/console/app/src/routes/s/[id].ts b/packages/console/app/src/routes/s/[id].ts index 3cdc486a4..628a75b2e 100644 --- a/packages/console/app/src/routes/s/[id].ts +++ b/packages/console/app/src/routes/s/[id].ts @@ -1,5 +1,5 @@ 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) { 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 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)) const response = await fetch(targetUrl, { diff --git a/packages/console/app/src/routes/t/[...path].tsx b/packages/console/app/src/routes/t/[...path].tsx index 5cb412f78..3f65c6822 100644 --- a/packages/console/app/src/routes/t/[...path].tsx +++ b/packages/console/app/src/routes/t/[...path].tsx @@ -1,5 +1,5 @@ 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) { 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 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)) const response = await fetch(targetUrl, { diff --git a/packages/console/app/src/routes/temp.tsx b/packages/console/app/src/routes/temp.tsx index d6d890479..0a2447f44 100644 --- a/packages/console/app/src/routes/temp.tsx +++ b/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 { IconCopy, IconCheck } from "../component/icon" import { useI18n } from "~/context/i18n" +import { useLanguage } from "~/context/language" function CopyStatus() { return ( @@ -18,6 +19,7 @@ function CopyStatus() { export default function Home() { const i18n = useI18n() + const language = useLanguage() onMount(() => { 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" /> <h1 data-slot="title">{i18n.t("temp.hero.title")}</h1> <div data-slot="login"> - <a href="/auth">{i18n.t("temp.zen")}</a> + <a href={language.route("/auth")}>{i18n.t("temp.zen")}</a> </div> </section> <section data-component="cta"> <div data-slot="left"> - <a href="/docs">{i18n.t("temp.getStarted")}</a> + <a href={language.route("/docs")}>{i18n.t("temp.getStarted")}</a> </div> <div data-slot="center"> - <a href="/auth">{i18n.t("temp.zen")}</a> + <a href={language.route("/auth")}>{i18n.t("temp.zen")}</a> </div> <div data-slot="right"> <button data-copy data-slot="command"> @@ -83,8 +85,8 @@ export default function Home() { </li> <li> <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> <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"> <figure> <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")} /> </a> </figure> diff --git a/packages/console/app/src/routes/user-menu.tsx b/packages/console/app/src/routes/user-menu.tsx index f089aad81..fa1c1f60b 100644 --- a/packages/console/app/src/routes/user-menu.tsx +++ b/packages/console/app/src/routes/user-menu.tsx @@ -3,6 +3,7 @@ import { getRequestEvent } from "solid-js/web" import { useAuthSession } from "~/context/auth" import { Dropdown } from "~/component/dropdown" import { useI18n } from "~/context/i18n" +import { useLanguage } from "~/context/language" import "./user-menu.css" const logout = action(async () => { @@ -22,10 +23,11 @@ const logout = action(async () => { export function UserMenu(props: { email: string | null | undefined }) { const i18n = useI18n() + const language = useLanguage() return ( <div data-component="user-menu"> <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")} </a> </Dropdown> diff --git a/packages/console/app/src/routes/workspace.tsx b/packages/console/app/src/routes/workspace.tsx index d7117a7d2..10e503ac5 100644 --- a/packages/console/app/src/routes/workspace.tsx +++ b/packages/console/app/src/routes/workspace.tsx @@ -6,6 +6,7 @@ import { UserMenu } from "./user-menu" import { withActor } from "~/context/auth.withActor" import { User } from "@opencode-ai/console-core/user.js" import { Actor } from "@opencode-ai/console-core/actor.js" +import { useLanguage } from "~/context/language" const getUserEmail = query(async (workspaceID: string) => { "use server" @@ -18,12 +19,13 @@ const getUserEmail = query(async (workspaceID: string) => { export default function WorkspaceLayout(props: RouteSectionProps) { const params = useParams() + const language = useLanguage() const userEmail = createAsync(() => getUserEmail(params.id!)) return ( <main data-page="workspace"> <header data-component="workspace-header"> <div data-slot="header-brand"> - <A href="/" data-component="site-title"> + <A href={language.route("/")} data-component="site-title"> <IconWorkspaceLogo /> </A> <WorkspacePicker /> diff --git a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx index dce23a496..5a440632f 100644 --- a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx +++ b/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 { RoleDropdown } from "./role-dropdown" import { useI18n } from "~/context/i18n" +import { useLanguage } from "~/context/language" import { formError, localizeError } from "~/lib/form-error" const listMembers = query(async (workspaceID: string) => { @@ -218,6 +219,7 @@ function MemberRow(props: { export function MemberSection() { const params = useParams() const i18n = useI18n() + const language = useLanguage() const data = createAsync(() => listMembers(params.id!)) const submission = useSubmission(inviteMember) const [store, setStore] = createStore({ @@ -277,7 +279,7 @@ export function MemberSection() { </div> <div data-slot="beta-notice"> {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")} </a> . diff --git a/packages/console/app/src/routes/workspace/[id]/model-section.tsx b/packages/console/app/src/routes/workspace/[id]/model-section.tsx index d67c72b56..97f95278a 100644 --- a/packages/console/app/src/routes/workspace/[id]/model-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/model-section.tsx @@ -17,6 +17,7 @@ import { IconZai, } from "~/component/icon" import { useI18n } from "~/context/i18n" +import { useLanguage } from "~/context/language" import { formError } from "~/lib/form-error" const getModelLab = (modelId: string) => { @@ -80,6 +81,7 @@ const updateModel = action(async (form: FormData) => { export function ModelSection() { const params = useParams() const i18n = useI18n() + const language = useLanguage() const modelsInfo = createAsync(() => getModelsInfo(params.id!)) const userInfo = createAsync(() => querySessionInfo(params.id!)) @@ -96,8 +98,8 @@ export function ModelSection() { <div data-slot="section-title"> <h2>{i18n.t("workspace.models.title")}</h2> <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> </div> <div data-slot="models-list"> diff --git a/packages/console/app/src/routes/zen/index.tsx b/packages/console/app/src/routes/zen/index.tsx index 50a818e0d..d32c1bbaa 100644 --- a/packages/console/app/src/routes/zen/index.tsx +++ b/packages/console/app/src/routes/zen/index.tsx @@ -1,9 +1,8 @@ import "./index.css" 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 zenLogoLight from "../../asset/zen-ornate-light.svg" -import { config } from "~/config" import zenLogoDark from "../../asset/zen-ornate-dark.svg" import compareVideo from "../../asset/lander/opencode-comparison-min.mp4" import compareVideoPoster from "../../asset/lander/opencode-comparison-poster.png" @@ -20,6 +19,8 @@ import { Header } from "~/component/header" import { getLastSeenWorkspaceID } from "../workspace/common" import { IconGemini, IconMiniMax, IconZai } from "~/component/icon" import { useI18n } from "~/context/i18n" +import { useLanguage } from "~/context/language" +import { LocaleLinks } from "~/component/locale-links" const checkLoggedIn = query(async () => { "use server" @@ -30,11 +31,12 @@ const checkLoggedIn = query(async () => { export default function Home() { const loggedin = createAsync(() => checkLoggedIn()) const i18n = useI18n() + const language = useLanguage() return ( <main data-page="zen"> {/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/} <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 name="twitter:image" content="/social-share-zen.png" /> <Meta name="opencode:auth" content={loggedin() ? "true" : "false"} /> @@ -120,7 +122,7 @@ export default function Home() { </svg> </div> </div> - <a href="/auth"> + <a href={language.route("/auth")}> <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"> <path @@ -175,7 +177,7 @@ export default function Home() { <span>[1]</span> <div> <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")} </a> </div> @@ -184,7 +186,8 @@ export default function Home() { <span>[2]</span> <div> <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> </li> <li> @@ -203,7 +206,7 @@ export default function Home() { <span>[*]</span> <p> {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> </div> </div> @@ -299,15 +302,15 @@ export default function Home() { <li> <Faq question={i18n.t("zen.faq.q4")}> {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")}{" "} - <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> </li> <li> <Faq question={i18n.t("zen.faq.q5")}> {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> </li> <li> |
