feat(www): locale specific urls (#12508)
This commit is contained in:
@@ -3,6 +3,7 @@ import { readdir, writeFile } from "fs/promises"
|
||||
import { join, dirname } from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { config } from "../src/config.js"
|
||||
import { LOCALES, route } from "../src/lib/language.js"
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const BASE_URL = config.baseUrl
|
||||
@@ -27,12 +28,14 @@ async function getMainRoutes(): Promise<SitemapEntry[]> {
|
||||
{ path: "/zen", priority: 0.8, changefreq: "weekly" },
|
||||
]
|
||||
|
||||
for (const route of staticRoutes) {
|
||||
routes.push({
|
||||
url: `${BASE_URL}${route.path}`,
|
||||
priority: route.priority,
|
||||
changefreq: route.changefreq,
|
||||
})
|
||||
for (const item of staticRoutes) {
|
||||
for (const locale of LOCALES) {
|
||||
routes.push({
|
||||
url: `${BASE_URL}${route(locale, item.path)}`,
|
||||
priority: item.priority,
|
||||
changefreq: item.changefreq,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return routes
|
||||
@@ -50,11 +53,13 @@ async function getDocsRoutes(): Promise<SitemapEntry[]> {
|
||||
const slug = file.replace(".mdx", "")
|
||||
const path = slug === "index" ? "/docs/" : `/docs/${slug}`
|
||||
|
||||
routes.push({
|
||||
url: `${BASE_URL}${path}`,
|
||||
priority: slug === "index" ? 0.9 : 0.7,
|
||||
changefreq: "weekly",
|
||||
})
|
||||
for (const locale of LOCALES) {
|
||||
routes.push({
|
||||
url: `${BASE_URL}${route(locale, path)}`,
|
||||
priority: slug === "index" ? 0.9 : 0.7,
|
||||
changefreq: "weekly",
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error reading docs directory:", error)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
36
packages/console/app/src/component/locale-links.tsx
Normal file
36
packages/console/app/src/component/locale-links.tsx
Normal file
@@ -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}`} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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()}>
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
.
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,7 +4,9 @@ import { nitro } from "nitro/vite"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
solidStart() as PluginOption,
|
||||
solidStart({
|
||||
middleware: "./src/middleware.ts",
|
||||
}) as PluginOption,
|
||||
nitro({
|
||||
compatibilityDate: "2024-09-19",
|
||||
preset: "cloudflare_module",
|
||||
|
||||
Reference in New Issue
Block a user