feat(web): i18n (#12471)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
135
packages/console/app/src/component/language-picker.css
Normal file
135
packages/console/app/src/component/language-picker.css
Normal file
@@ -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);
|
||||
}
|
||||
34
packages/console/app/src/component/language-picker.tsx
Normal file
34
packages/console/app/src/component/language-picker.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user