summaryrefslogtreecommitdiffhomepage
path: root/packages/console/app/src/component
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-06 11:30:40 -0600
committerGitHub <[email protected]>2026-02-06 11:30:40 -0600
commit24cd84cda5522e4607e8e3fb3626f289d7e348f4 (patch)
tree81dec57e565c24255e5e6ad2dfac58ea0e770018 /packages/console/app/src/component
parent8069197329d2d1b958d8e7f63daaf9662a97027d (diff)
downloadopencode-24cd84cda5522e4607e8e3fb3626f289d7e348f4.tar.gz
opencode-24cd84cda5522e4607e8e3fb3626f289d7e348f4.zip
feat(www): locale specific urls (#12508)
Diffstat (limited to 'packages/console/app/src/component')
-rw-r--r--packages/console/app/src/component/footer.tsx6
-rw-r--r--packages/console/app/src/component/header.tsx28
-rw-r--r--packages/console/app/src/component/language-picker.tsx6
-rw-r--r--packages/console/app/src/component/legal.tsx8
-rw-r--r--packages/console/app/src/component/locale-links.tsx36
5 files changed, 65 insertions, 19 deletions
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}`} />
+ </>
+ )
+}