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/component | |
| parent | 8069197329d2d1b958d8e7f63daaf9662a97027d (diff) | |
| download | opencode-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.tsx | 6 | ||||
| -rw-r--r-- | packages/console/app/src/component/header.tsx | 28 | ||||
| -rw-r--r-- | packages/console/app/src/component/language-picker.tsx | 6 | ||||
| -rw-r--r-- | packages/console/app/src/component/legal.tsx | 8 | ||||
| -rw-r--r-- | packages/console/app/src/component/locale-links.tsx | 36 |
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}`} /> + </> + ) +} |
