Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions packages/console/app/script/generate-sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions packages/console/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
6 changes: 3 additions & 3 deletions packages/console/app/src/component/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
28 changes: 15 additions & 13 deletions packages/console/app/src/component/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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>
Expand All @@ -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")}
Expand All @@ -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"
Expand Down Expand Up @@ -245,32 +247,32 @@ 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;">
{i18n.t("nav.github")} <span>[{starCount()}]</span>
</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>
Expand Down
6 changes: 6 additions & 0 deletions packages/console/app/src/component/language-picker.tsx
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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)
}}
>
Expand Down
8 changes: 5 additions & 3 deletions packages/console/app/src/component/legal.tsx
Original file line number Diff line number Diff line change
@@ -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" />
Expand Down
36 changes: 36 additions & 0 deletions packages/console/app/src/component/locale-links.tsx
Original file line number Diff line number Diff line change
@@ -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}`} />
</>
)
}
4 changes: 4 additions & 0 deletions packages/console/app/src/context/language.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
localeFromCookieHeader,
localeFromRequest,
parseLocale,
route as localeRoute,
tag as localeTag,
} from "~/lib/language"

Expand Down Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions packages/console/app/src/lib/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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]
}
Expand Down Expand Up @@ -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"))
Expand Down
13 changes: 12 additions & 1 deletion packages/console/app/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -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)
},
})
Loading
Loading