diff options
| author | Adam <[email protected]> | 2026-02-06 08:54:51 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-06 08:54:51 -0600 |
| commit | 812597bb8b101896a8988493d37261ff851ae502 (patch) | |
| tree | 3b713c13f49c65665e6a7296ed483a3f6f8befbf /packages/console/app/src/component | |
| parent | 0ec5f6608bdfea5be62dbbdc4c04a61de6d3e67c (diff) | |
| download | opencode-812597bb8b101896a8988493d37261ff851ae502.tar.gz opencode-812597bb8b101896a8988493d37261ff851ae502.zip | |
feat(web): i18n (#12471)
Diffstat (limited to 'packages/console/app/src/component')
| -rw-r--r-- | packages/console/app/src/component/email-signup.tsx | 14 | ||||
| -rw-r--r-- | packages/console/app/src/component/footer.tsx | 16 | ||||
| -rw-r--r-- | packages/console/app/src/component/header.tsx | 66 | ||||
| -rw-r--r-- | packages/console/app/src/component/language-picker.css | 135 | ||||
| -rw-r--r-- | packages/console/app/src/component/language-picker.tsx | 34 | ||||
| -rw-r--r-- | packages/console/app/src/component/legal.tsx | 12 |
6 files changed, 233 insertions, 44 deletions
diff --git a/packages/console/app/src/component/email-signup.tsx b/packages/console/app/src/component/email-signup.tsx index 65f81b5fc..bd33e9200 100644 --- a/packages/console/app/src/component/email-signup.tsx +++ b/packages/console/app/src/component/email-signup.tsx @@ -2,6 +2,7 @@ import { action, useSubmission } from "@solidjs/router" import dock from "../asset/lander/dock.png" import { Resource } from "@opencode-ai/console-resource" import { Show } from "solid-js" +import { useI18n } from "~/context/i18n" const emailSignup = action(async (formData: FormData) => { "use server" @@ -23,22 +24,21 @@ const emailSignup = action(async (formData: FormData) => { export function EmailSignup() { const submission = useSubmission(emailSignup) + const i18n = useI18n() return ( <section data-component="email"> <div data-slot="section-title"> - <h3>Be the first to know when we release new products</h3> - <p>Join the waitlist for early access.</p> + <h3>{i18n.t("email.title")}</h3> + <p>{i18n.t("email.subtitle")}</p> </div> <form data-slot="form" action={emailSignup} method="post"> - <input type="email" name="email" placeholder="Email address" required /> + <input type="email" name="email" placeholder={i18n.t("email.placeholder")} required /> <button type="submit" disabled={submission.pending}> - Subscribe + {i18n.t("email.subscribe")} </button> </form> <Show when={submission.result}> - <div style="color: #03B000; margin-top: 24px;"> - Almost done, check your inbox and confirm your email address - </div> + <div style="color: #03B000; margin-top: 24px;">{i18n.t("email.success")}</div> </Show> <Show when={submission.error}> <div style="color: #FF408F; margin-top: 24px;">{submission.error}</div> diff --git a/packages/console/app/src/component/footer.tsx b/packages/console/app/src/component/footer.tsx index 27f8ddd65..45dae87ec 100644 --- a/packages/console/app/src/component/footer.tsx +++ b/packages/console/app/src/component/footer.tsx @@ -2,12 +2,16 @@ import { createAsync } from "@solidjs/router" import { createMemo } from "solid-js" import { github } from "~/lib/github" import { config } from "~/config" +import { useLanguage } from "~/context/language" +import { useI18n } from "~/context/i18n" export function Footer() { + const language = useLanguage() + const i18n = useI18n() const githubData = createAsync(() => github()) const starCount = createMemo(() => githubData()?.stars - ? new Intl.NumberFormat("en-US", { + ? new Intl.NumberFormat(language.tag(language.locale()), { notation: "compact", compactDisplay: "short", }).format(githubData()!.stars!) @@ -18,20 +22,20 @@ export function Footer() { <footer data-component="footer"> <div data-slot="cell"> <a href={config.github.repoUrl} target="_blank"> - GitHub <span>[{starCount()}]</span> + {i18n.t("footer.github")} <span>[{starCount()}]</span> </a> </div> <div data-slot="cell"> - <a href="/docs">Docs</a> + <a href="/docs">{i18n.t("footer.docs")}</a> </div> <div data-slot="cell"> - <a href="/changelog">Changelog</a> + <a href="/changelog">{i18n.t("footer.changelog")}</a> </div> <div data-slot="cell"> - <a href="/discord">Discord</a> + <a href="/discord">{i18n.t("footer.discord")}</a> </div> <div data-slot="cell"> - <a href={config.social.twitter}>X</a> + <a href={config.social.twitter}>{i18n.t("footer.x")}</a> </div> </footer> ) diff --git a/packages/console/app/src/component/header.tsx b/packages/console/app/src/component/header.tsx index 72e9d0418..3eca8b88c 100644 --- a/packages/console/app/src/component/header.tsx +++ b/packages/console/app/src/component/header.tsx @@ -19,6 +19,7 @@ import { createStore } from "solid-js/store" import { github } from "~/lib/github" import { createEffect, onCleanup } from "solid-js" import { config } from "~/config" +import { useI18n } from "~/context/i18n" import "./header-context-menu.css" const isDarkMode = () => window.matchMedia("(prefers-color-scheme: dark)").matches @@ -36,12 +37,14 @@ const fetchSvgContent = async (svgPath: string): Promise<string> => { export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { const navigate = useNavigate() + const i18n = useI18n() const githubData = createAsync(() => github()) const starCount = createMemo(() => githubData()?.stars ? new Intl.NumberFormat("en-US", { notation: "compact", compactDisplay: "short", + maximumFractionDigits: 0, }).format(githubData()?.stars!) : config.github.starsFormatted.compact, ) @@ -119,8 +122,8 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { <section data-component="top"> <div onContextMenu={handleLogoContextMenu}> <A href="/"> - <img data-slot="logo light" src={logoLight} alt="opencode logo light" width="189" height="34" /> - <img data-slot="logo dark" src={logoDark} alt="opencode logo dark" 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" /> </A> </div> @@ -130,49 +133,56 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { style={`left: ${store.contextMenuPosition.x}px; top: ${store.contextMenuPosition.y}px;`} > <button class="context-menu-item" onClick={copyLogoToClipboard}> - <img data-slot="copy light" src={copyLogoLight} alt="Logo" /> - <img data-slot="copy dark" src={copyLogoDark} alt="Logo" /> - Copy logo as SVG + <img data-slot="copy light" src={copyLogoLight} alt="" /> + <img data-slot="copy dark" src={copyLogoDark} alt="" /> + {i18n.t("nav.context.copyLogo")} </button> <button class="context-menu-item" onClick={copyWordmarkToClipboard}> - <img data-slot="copy light" src={copyWordmarkLight} alt="Wordmark" /> - <img data-slot="copy dark" src={copyWordmarkDark} alt="Wordmark" /> - Copy wordmark as SVG + <img data-slot="copy light" src={copyWordmarkLight} alt="" /> + <img data-slot="copy dark" src={copyWordmarkDark} alt="" /> + {i18n.t("nav.context.copyWordmark")} </button> <button class="context-menu-item" onClick={() => navigate("/brand")}> - <img data-slot="copy light" src={copyBrandAssetsLight} alt="Brand Assets" /> - <img data-slot="copy dark" src={copyBrandAssetsDark} alt="Brand Assets" /> - Brand assets + <img data-slot="copy light" src={copyBrandAssetsLight} alt="" /> + <img data-slot="copy dark" src={copyBrandAssetsDark} alt="" /> + {i18n.t("nav.context.brandAssets")} </button> </div> </Show> <nav data-component="nav-desktop"> <ul> <li> - <a href={config.github.repoUrl} target="_blank"> - GitHub <span>[{starCount()}]</span> + <a href={config.github.repoUrl} target="_blank" style="white-space: nowrap;"> + {i18n.t("nav.github")} <span>[{starCount()}]</span> </a> </li> <li> - <a href="/docs">Docs</a> + <a href="/docs">{i18n.t("nav.docs")}</a> </li> <li> - <A href="/enterprise">Enterprise</A> + <A href="/enterprise">{i18n.t("nav.enterprise")}</A> </li> <li> <Switch> <Match when={props.zen}> - <a href="/auth">Login</a> + <a href="/auth">{i18n.t("nav.login")}</a> </Match> <Match when={!props.zen}> - <A href="/zen">Zen</A> + <A href="/zen">{i18n.t("nav.zen")}</A> </Match> </Switch> </li> <Show when={!props.hideGetStarted}> <li> <A href="/download" data-slot="cta-button"> - <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> + <svg + width="18" + height="18" + viewBox="0 0 18 18" + fill="none" + xmlns="http://www.w3.org/2000/svg" + style="flex-shrink: 0;" + > <path d="M12.1875 9.75L9.00001 12.9375L5.8125 9.75M9.00001 2.0625L9 12.375M14.4375 15.9375H3.5625" stroke="currentColor" @@ -180,7 +190,7 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { stroke-linecap="square" /> </svg> - Free + {i18n.t("nav.free")} </A> </li> </Show> @@ -195,7 +205,7 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { class="nav-toggle" onClick={() => setStore("mobileMenuOpen", !store.mobileMenuOpen)} > - <span class="sr-only">Open menu</span> + <span class="sr-only">{i18n.t("nav.openMenu")}</span> <Switch> <Match when={store.mobileMenuOpen}> <svg @@ -235,33 +245,33 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { <nav data-component="nav-mobile-menu-list"> <ul> <li> - <A href="/">Home</A> + <A href="/">{i18n.t("nav.home")}</A> </li> <li> - <a href={config.github.repoUrl} target="_blank"> - GitHub <span>[{starCount()}]</span> + <a href={config.github.repoUrl} target="_blank" style="white-space: nowrap;"> + {i18n.t("nav.github")} <span>[{starCount()}]</span> </a> </li> <li> - <a href="/docs">Docs</a> + <a href="/docs">{i18n.t("nav.docs")}</a> </li> <li> - <A href="/enterprise">Enterprise</A> + <A href="/enterprise">{i18n.t("nav.enterprise")}</A> </li> <li> <Switch> <Match when={props.zen}> - <a href="/auth">Login</a> + <a href="/auth">{i18n.t("nav.login")}</a> </Match> <Match when={!props.zen}> - <A href="/zen">Zen</A> + <A href="/zen">{i18n.t("nav.zen")}</A> </Match> </Switch> </li> <Show when={!props.hideGetStarted}> <li> <A href="/download" data-slot="cta-button"> - Get started for free + {i18n.t("nav.getStartedFree")} </A> </li> </Show> diff --git a/packages/console/app/src/component/language-picker.css b/packages/console/app/src/component/language-picker.css new file mode 100644 index 000000000..8ef2306d7 --- /dev/null +++ b/packages/console/app/src/component/language-picker.css @@ -0,0 +1,135 @@ +[data-component="language-picker"] { + width: auto; +} + +[data-component="footer"] [data-component="language-picker"] { + width: 100%; +} + +[data-component="footer"] [data-component="language-picker"] [data-component="dropdown"] { + width: 100%; +} + +/* Standard site footer (grid of cells) */ +[data-component="footer"] [data-slot="cell"] [data-component="language-picker"] { + height: 100%; +} + +[data-component="footer"] [data-slot="cell"] [data-component="language-picker"] [data-slot="trigger"] { + width: 100%; + padding: 2rem 0; + border-radius: 0; + justify-content: center; + gap: var(--space-2); + color: inherit; + font: inherit; +} + +[data-component="footer"] [data-slot="cell"] [data-component="language-picker"] [data-slot="trigger"] span { + flex: 0 0 auto; + text-align: center; + font-weight: inherit; +} + +[data-component="footer"] [data-slot="cell"] [data-component="language-picker"] [data-slot="trigger"]:hover { + background: var(--color-background-weak); + text-decoration: underline; + text-underline-offset: var(--space-1); + text-decoration-thickness: 1px; +} + +/* Footer dropdown should open upward */ +[data-component="footer"] [data-component="language-picker"] [data-slot="dropdown"] { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: var(--space-2); + max-height: min(60vh, 420px); + overflow: auto; +} + +[data-component="legal"] { + flex-wrap: wrap; + row-gap: var(--space-2); +} + +[data-component="legal"] [data-component="language-picker"] { + width: auto; +} + +[data-component="legal"] [data-component="language-picker"] [data-slot="trigger"] { + padding: 0; + border-radius: 0; + background: transparent; + font: inherit; + color: var(--color-text-weak); + white-space: nowrap; +} + +[data-component="legal"] [data-component="language-picker"] [data-slot="trigger"] span { + font-weight: inherit; +} + +[data-component="legal"] [data-component="language-picker"] [data-slot="trigger"]:hover { + background: transparent; + color: var(--color-text); + text-decoration: underline; + text-underline-offset: var(--space-1); + text-decoration-thickness: 1px; +} + +[data-component="legal"] [data-component="language-picker"] [data-slot="dropdown"] { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: var(--space-2); + max-height: min(60vh, 420px); + overflow: auto; +} + +/* Black pages footer */ +[data-page="black"] [data-component="language-picker"] { + width: auto; +} + +[data-page="black"] [data-component="language-picker"] [data-component="dropdown"] { + width: auto; +} + +[data-page="black"] [data-component="language-picker"] [data-slot="trigger"] { + padding: 0; + border-radius: 0; + background: transparent; + font: inherit; + color: rgba(255, 255, 255, 0.39); +} + +[data-page="black"] [data-component="language-picker"] [data-slot="trigger"]:hover { + background: transparent; + text-decoration: underline; + text-underline-offset: 4px; +} + +[data-page="black"] [data-component="language-picker"] [data-slot="dropdown"] { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: 12px; + background-color: #0e0e10; + border-color: rgba(255, 255, 255, 0.14); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6); + max-height: min(60vh, 420px); + overflow: auto; +} + +[data-page="black"] [data-component="language-picker"] [data-slot="item"] { + color: rgba(255, 255, 255, 0.86); +} + +[data-page="black"] [data-component="language-picker"] [data-slot="item"]:hover { + background-color: rgba(255, 255, 255, 0.06); +} + +[data-page="black"] [data-component="language-picker"] [data-slot="item"][data-selected="true"] { + background-color: rgba(255, 255, 255, 0.1); +} diff --git a/packages/console/app/src/component/language-picker.tsx b/packages/console/app/src/component/language-picker.tsx new file mode 100644 index 000000000..b30471d01 --- /dev/null +++ b/packages/console/app/src/component/language-picker.tsx @@ -0,0 +1,34 @@ +import { For, createSignal } from "solid-js" +import { Dropdown, DropdownItem } from "~/component/dropdown" +import { useLanguage } from "~/context/language" +import "./language-picker.css" + +export function LanguagePicker(props: { align?: "left" | "right" } = {}) { + const language = useLanguage() + const [open, setOpen] = createSignal(false) + + return ( + <div data-component="language-picker"> + <Dropdown + trigger={language.label(language.locale())} + align={props.align ?? "left"} + open={open()} + onOpenChange={setOpen} + > + <For each={language.locales}> + {(locale) => ( + <DropdownItem + selected={locale === language.locale()} + onClick={() => { + language.setLocale(locale) + setOpen(false) + }} + > + {language.label(locale)} + </DropdownItem> + )} + </For> + </Dropdown> + </div> + ) +} diff --git a/packages/console/app/src/component/legal.tsx b/packages/console/app/src/component/legal.tsx index e971a31e1..3cc3627a7 100644 --- a/packages/console/app/src/component/legal.tsx +++ b/packages/console/app/src/component/legal.tsx @@ -1,19 +1,25 @@ import { A } from "@solidjs/router" +import { LanguagePicker } from "~/component/language-picker" +import { useI18n } from "~/context/i18n" export function Legal() { + const i18n = useI18n() return ( <div data-component="legal"> <span> ©{new Date().getFullYear()} <a href="https://anoma.ly">Anomaly</a> </span> <span> - <A href="/brand">Brand</A> + <A href="/brand">{i18n.t("legal.brand")}</A> </span> <span> - <A href="/legal/privacy-policy">Privacy</A> + <A href="/legal/privacy-policy">{i18n.t("legal.privacy")}</A> </span> <span> - <A href="/legal/terms-of-service">Terms</A> + <A href="/legal/terms-of-service">{i18n.t("legal.terms")}</A> + </span> + <span> + <LanguagePicker align="right" /> </span> </div> ) |
