From dc53086c1e73d43d3a28fc4cdf161e83d09b1877 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:34:35 -0600 Subject: wip(docs): i18n (#12681) --- packages/web/src/components/Footer.astro | 125 ++++++ packages/web/src/components/Head.astro | 16 +- packages/web/src/components/Header.astro | 228 +++++------ packages/web/src/components/Lander.astro | 62 +-- packages/web/src/components/Share.tsx | 437 +++++++++++---------- packages/web/src/components/SiteTitle.astro | 2 +- packages/web/src/components/share/common.tsx | 71 +++- packages/web/src/components/share/content-bash.tsx | 5 +- packages/web/src/components/share/content-code.tsx | 2 - .../web/src/components/share/content-error.tsx | 5 +- .../web/src/components/share/content-markdown.tsx | 7 +- packages/web/src/components/share/content-text.tsx | 5 +- packages/web/src/components/share/copy-button.tsx | 10 +- packages/web/src/components/share/part.tsx | 154 +++++--- 14 files changed, 694 insertions(+), 435 deletions(-) create mode 100644 packages/web/src/components/Footer.astro (limited to 'packages/web/src/components') diff --git a/packages/web/src/components/Footer.astro b/packages/web/src/components/Footer.astro new file mode 100644 index 000000000..20d65ee9d --- /dev/null +++ b/packages/web/src/components/Footer.astro @@ -0,0 +1,125 @@ +--- +import config from "virtual:starlight/user-config" +import LanguageSelect from "@astrojs/starlight/components/LanguageSelect.astro" +import { Icon } from "@astrojs/starlight/components" + +const { lang, editUrl, lastUpdated, entry } = Astro.locals.starlightRoute +const template = entry.data.template +const issueLink = Astro.locals.t("app.footer.issueLink", "Found a bug? Open an issue") +const discordLink = Astro.locals.t("app.footer.discordLink", "Join our Discord community") + +const github = config.social?.find((item) => item.icon === "github") +const discord = config.social?.find((item) => item.icon === "discord") +--- + +{ + template === "doc" && ( + + ) +} + + diff --git a/packages/web/src/components/Head.astro b/packages/web/src/components/Head.astro index 0eaf115b5..4b35d26db 100644 --- a/packages/web/src/components/Head.astro +++ b/packages/web/src/components/Head.astro @@ -1,10 +1,9 @@ --- import { Base64 } from "js-base64"; -import type { Props } from '@astrojs/starlight/props' import Default from '@astrojs/starlight/components/Head.astro' import config from '../../config.mjs' -const base = import.meta.env.BASE_URL.slice(1) +const base = import.meta.env.BASE_URL.replace(/^\//, "").replace(/\/$/, "") const slug = Astro.url.pathname.replace(/^\//, "").replace(/\/$/, ""); const { @@ -12,7 +11,12 @@ const { data: { title , description }, }, } = Astro.locals.starlightRoute; -const isDocs = slug.startsWith("docs") +const isDocs = base === "" ? true : slug === base || slug.startsWith(`${base}/`) +const t = Astro.locals.t as (key: string) => string +const titleSuffix = t("app.head.titleSuffix") +const shareSlug = base === "" ? "s" : `${base}/s` +const isShare = slug === shareSlug || slug.startsWith(`${shareSlug}/`) +const isHome = slug === "" || slug === base let encodedTitle = ''; let ogImage = `${config.url}/social-share.png`; @@ -38,13 +42,13 @@ if (isDocs) { } --- -{ slug === "" && ( -{title} | AI coding agent built for the terminal +{ isHome && ( +{title} | {titleSuffix} )} -{ (!slug.startsWith(`${base}/s`)) && ( +{ !isShare && ( )} diff --git a/packages/web/src/components/Header.astro b/packages/web/src/components/Header.astro index 396200a3e..bb13c9117 100644 --- a/packages/web/src/components/Header.astro +++ b/packages/web/src/components/Header.astro @@ -1,128 +1,136 @@ --- -import config from '../../config.mjs'; -import astroConfig from 'virtual:starlight/user-config'; -import { Icon } from '@astrojs/starlight/components'; -import { HeaderLinks } from 'toolbeam-docs-theme/components'; -import Default from 'toolbeam-docs-theme/overrides/Header.astro'; -import SocialIcons from 'virtual:starlight/components/SocialIcons'; -import SiteTitle from '@astrojs/starlight/components/SiteTitle.astro'; +import config from "../../config.mjs" +import astroConfig from "virtual:starlight/user-config" +import { getRelativeLocaleUrl } from "astro:i18n" +import { Icon } from "@astrojs/starlight/components" +import Default from "toolbeam-docs-theme/overrides/Header.astro" +import SiteTitle from "@astrojs/starlight/components/SiteTitle.astro" -const path = Astro.url.pathname; - -const links = astroConfig.social || []; -const headerLinks = config.headerLinks; +const path = Astro.url.pathname +const locale = Astro.currentLocale || "root" +const route = Astro.locals.starlightRoute +const t = Astro.locals.t as (key: string) => string +const links = astroConfig.social || [] +const headerLinks = config.headerLinks +const sharePath = /\/s(\/|$)/.test(path) +function href(url: string) { + if (url === "/" || url === "/docs" || url === "/docs/") { + return getRelativeLocaleUrl(locale, "") + } + return url +} --- -{ path.startsWith("/s") -?
-
- -
-
- { - headerLinks?.map(({ name, url }) => ( - {name} - )) - } -
-
- { - links.length > 0 && ( - - ) - } -
-
- : -} +{sharePath ? ( +
+
+ +
+
+ {headerLinks?.map(({ name, url }) => ( + {t(name)} + ))} +
+
+ {links.length > 0 && ( + + )} +
+
+) : ( + +)} + diff --git a/packages/web/src/components/Lander.astro b/packages/web/src/components/Lander.astro index 2bfe0a102..9713cfff6 100644 --- a/packages/web/src/components/Lander.astro +++ b/packages/web/src/components/Lander.astro @@ -1,7 +1,7 @@ --- import { Image } from 'astro:assets'; +import { getRelativeLocaleUrl } from 'astro:i18n'; import config from "virtual:starlight/user-config"; -import type { Props } from '@astrojs/starlight/props'; import CopyIcon from "../assets/lander/copy.svg"; import CheckIcon from "../assets/lander/check.svg"; @@ -19,8 +19,14 @@ const imageAttrs = { alt: image?.alt || '', }; -const github = config.social.filter(s => s.icon === 'github')[0]; -const discord = config.social.filter(s => s.icon === 'discord')[0]; +const github = (config.social || []).filter(s => s.icon === 'github')[0]; +const discord = (config.social || []).filter(s => s.icon === 'discord')[0]; +const locale = Astro.currentLocale || 'root'; +const t = Astro.locals.t as (key: string) => string; +const docsHref = getRelativeLocaleUrl(locale, "") +const docsCliHref = getRelativeLocaleUrl(locale, "cli") +const docsIdeHref = getRelativeLocaleUrl(locale, "ide") +const docsGithubHref = getRelativeLocaleUrl(locale, "github") const command = "curl -fsSL" const protocol = "https://" @@ -44,19 +50,21 @@ if (image) {
-

The AI coding agent built for the terminal.

+

{t('app.lander.hero.title')}

-
- - - - - - + + + + + ) } @@ -502,6 +513,8 @@ export function fromV1(v1: Message.Info): MessageWithParts { id: v1.id, sessionID: v1.metadata.sessionID, role: "assistant", + parentID: "", + agent: "build", time: { created: v1.metadata.time.created, completed: v1.metadata.time.completed, @@ -521,7 +534,6 @@ export function fromV1(v1: Message.Info): MessageWithParts { modelID: v1.metadata.assistant!.modelID, providerID: v1.metadata.assistant!.providerID, mode: "build", - system: v1.metadata.assistant!.system, error: v1.metadata.error, parts: v1.parts.flatMap((part, index): MessageV2.Part[] => { const base = { @@ -557,6 +569,8 @@ export function fromV1(v1: Message.Info): MessageWithParts { if (part.toolInvocation.state === "partial-call") { return { status: "pending", + input: {}, + raw: "", } } @@ -596,6 +610,11 @@ export function fromV1(v1: Message.Info): MessageWithParts { id: v1.id, sessionID: v1.metadata.sessionID, role: "user", + agent: "user", + model: { + providerID: "", + modelID: "", + }, time: { created: v1.metadata.time.created, }, diff --git a/packages/web/src/components/SiteTitle.astro b/packages/web/src/components/SiteTitle.astro index 28a30cb23..6f1405cb1 100644 --- a/packages/web/src/components/SiteTitle.astro +++ b/packages/web/src/components/SiteTitle.astro @@ -4,7 +4,7 @@ import config from 'virtual:starlight/user-config'; const { siteTitle, siteTitleHref } = Astro.locals.starlightRoute; --- - + { config.logo && logos.dark && ( <> diff --git a/packages/web/src/components/share/common.tsx b/packages/web/src/components/share/common.tsx index cab2dbdb0..7ca4daa6a 100644 --- a/packages/web/src/components/share/common.tsx +++ b/packages/web/src/components/share/common.tsx @@ -1,16 +1,55 @@ -import { createSignal, onCleanup, splitProps } from "solid-js" +import { createContext, createSignal, onCleanup, splitProps, useContext } from "solid-js" import type { JSX } from "solid-js/jsx-runtime" import { IconCheckCircle, IconHashtag } from "../icons" +export type ShareMessages = { locale: string } & Record + +const shareContext = createContext() + +export function ShareI18nProvider(props: { messages: ShareMessages; children: JSX.Element }) { + return {props.children} +} + +export function useShareMessages() { + const value = useContext(shareContext) + if (value) { + return value + } + throw new Error("ShareI18nProvider is required") +} + +export function normalizeLocale(locale: string) { + return locale === "root" ? "en" : locale +} + +export function formatNumber(value: number, locale: string) { + return new Intl.NumberFormat(normalizeLocale(locale)).format(value) +} + +export function formatCurrency(value: number, locale: string) { + return new Intl.NumberFormat(normalizeLocale(locale), { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(value) +} + +export function formatCount(value: number, locale: string, singular: string, plural: string) { + const unit = value === 1 ? singular : plural + return `${formatNumber(value, locale)} ${unit}` +} + interface AnchorProps extends JSX.HTMLAttributes { id: string } export function AnchorIcon(props: AnchorProps) { const [local, rest] = splitProps(props, ["id", "children"]) const [copied, setCopied] = createSignal(false) + const messages = useShareMessages() return ( -
+
{ @@ -32,7 +71,7 @@ export function AnchorIcon(props: AnchorProps) { - Copied! + {messages.copied}
) } @@ -59,19 +98,33 @@ export function createOverflow() { } } -export function formatDuration(ms: number): string { +export function formatDuration(ms: number, locale: string): string { + const normalized = normalizeLocale(locale) const ONE_SECOND = 1000 const ONE_MINUTE = 60 * ONE_SECOND if (ms >= ONE_MINUTE) { - const minutes = Math.floor(ms / ONE_MINUTE) - return minutes === 1 ? `1min` : `${minutes}mins` + return new Intl.NumberFormat(normalized, { + style: "unit", + unit: "minute", + unitDisplay: "narrow", + maximumFractionDigits: 0, + }).format(Math.floor(ms / ONE_MINUTE)) } if (ms >= ONE_SECOND) { - const seconds = Math.floor(ms / ONE_SECOND) - return `${seconds}s` + return new Intl.NumberFormat(normalized, { + style: "unit", + unit: "second", + unitDisplay: "narrow", + maximumFractionDigits: 0, + }).format(Math.floor(ms / ONE_SECOND)) } - return `${ms}ms` + return new Intl.NumberFormat(normalized, { + style: "unit", + unit: "millisecond", + unitDisplay: "narrow", + maximumFractionDigits: 0, + }).format(ms) } diff --git a/packages/web/src/components/share/content-bash.tsx b/packages/web/src/components/share/content-bash.tsx index 5ccd95c0b..f8130ecc6 100644 --- a/packages/web/src/components/share/content-bash.tsx +++ b/packages/web/src/components/share/content-bash.tsx @@ -1,6 +1,6 @@ import style from "./content-bash.module.css" import { createResource, createSignal } from "solid-js" -import { createOverflow } from "./common" +import { createOverflow, useShareMessages } from "./common" import { codeToHtml } from "shiki" interface Props { @@ -11,6 +11,7 @@ interface Props { } export function ContentBash(props: Props) { + const messages = useShareMessages() const [commandHtml] = createResource( () => props.command, async (command) => { @@ -59,7 +60,7 @@ export function ContentBash(props: Props) { data-slot="expand-button" onClick={() => setExpanded((e) => !e)} > - {expanded() ? "Show less" : "Show more"} + {expanded() ? messages.show_less : messages.show_more} )}
diff --git a/packages/web/src/components/share/content-code.tsx b/packages/web/src/components/share/content-code.tsx index 2f383b8be..297828602 100644 --- a/packages/web/src/components/share/content-code.tsx +++ b/packages/web/src/components/share/content-code.tsx @@ -1,6 +1,5 @@ import { codeToHtml, bundledLanguages } from "shiki" import { createResource, Suspense } from "solid-js" -import { transformerNotationDiff } from "@shikijs/transformers" import style from "./content-code.module.css" interface Props { @@ -20,7 +19,6 @@ export function ContentCode(props: Props) { light: "github-light", dark: "github-dark", }, - transformers: [transformerNotationDiff()], })) as string }, ) diff --git a/packages/web/src/components/share/content-error.tsx b/packages/web/src/components/share/content-error.tsx index 1e8cbeaad..0f8588997 100644 --- a/packages/web/src/components/share/content-error.tsx +++ b/packages/web/src/components/share/content-error.tsx @@ -1,6 +1,6 @@ import style from "./content-error.module.css" import { type JSX, createSignal } from "solid-js" -import { createOverflow } from "./common" +import { createOverflow, useShareMessages } from "./common" interface Props extends JSX.HTMLAttributes { expand?: boolean @@ -8,6 +8,7 @@ interface Props extends JSX.HTMLAttributes { export function ContentError(props: Props) { const [expanded, setExpanded] = createSignal(false) const overflow = createOverflow() + const messages = useShareMessages() return (
@@ -16,7 +17,7 @@ export function ContentError(props: Props) {
{((!props.expand && overflow.status) || expanded()) && ( )} diff --git a/packages/web/src/components/share/content-markdown.tsx b/packages/web/src/components/share/content-markdown.tsx index b9b1d5dcb..10a06bf5e 100644 --- a/packages/web/src/components/share/content-markdown.tsx +++ b/packages/web/src/components/share/content-markdown.tsx @@ -1,10 +1,9 @@ import { marked } from "marked" import { codeToHtml } from "shiki" import markedShiki from "marked-shiki" -import { createOverflow } from "./common" +import { createOverflow, useShareMessages } from "./common" import { CopyButton } from "./copy-button" import { createResource, createSignal } from "solid-js" -import { transformerNotationDiff } from "@shikijs/transformers" import style from "./content-markdown.module.css" const markedWithShiki = marked.use( @@ -24,7 +23,6 @@ const markedWithShiki = marked.use( light: "github-light", dark: "github-dark", }, - transformers: [transformerNotationDiff()], }) }, }), @@ -44,6 +42,7 @@ export function ContentMarkdown(props: Props) { ) const [expanded, setExpanded] = createSignal(false) const overflow = createOverflow() + const messages = useShareMessages() return (
setExpanded((e) => !e)} > - {expanded() ? "Show less" : "Show more"} + {expanded() ? messages.show_less : messages.show_more} )} diff --git a/packages/web/src/components/share/content-text.tsx b/packages/web/src/components/share/content-text.tsx index 5db12a537..b549b74a4 100644 --- a/packages/web/src/components/share/content-text.tsx +++ b/packages/web/src/components/share/content-text.tsx @@ -1,6 +1,6 @@ import style from "./content-text.module.css" import { createSignal } from "solid-js" -import { createOverflow } from "./common" +import { createOverflow, useShareMessages } from "./common" import { CopyButton } from "./copy-button" interface Props { @@ -11,6 +11,7 @@ interface Props { export function ContentText(props: Props) { const [expanded, setExpanded] = createSignal(false) const overflow = createOverflow() + const messages = useShareMessages() return (
setExpanded((e) => !e)} > - {expanded() ? "Show less" : "Show more"} + {expanded() ? messages.show_less : messages.show_more} )} diff --git a/packages/web/src/components/share/copy-button.tsx b/packages/web/src/components/share/copy-button.tsx index 892d5553f..6c5097a99 100644 --- a/packages/web/src/components/share/copy-button.tsx +++ b/packages/web/src/components/share/copy-button.tsx @@ -1,5 +1,6 @@ import { createSignal } from "solid-js" import { IconClipboard, IconCheckCircle } from "../icons" +import { useShareMessages } from "./common" import styles from "./copy-button.module.css" interface CopyButtonProps { @@ -8,6 +9,7 @@ interface CopyButtonProps { export function CopyButton(props: CopyButtonProps) { const [copied, setCopied] = createSignal(false) + const messages = useShareMessages() function handleCopyClick() { if (props.text) { @@ -20,7 +22,13 @@ export function CopyButton(props: CopyButtonProps) { return (
-
diff --git a/packages/web/src/components/share/part.tsx b/packages/web/src/components/share/part.tsx index f7a6a9304..45bd97fe3 100644 --- a/packages/web/src/components/share/part.tsx +++ b/packages/web/src/components/share/part.tsx @@ -25,7 +25,7 @@ import { ContentDiff } from "./content-diff" import { ContentText } from "./content-text" import { ContentBash } from "./content-bash" import { ContentError } from "./content-error" -import { formatDuration } from "../share/common" +import { formatCount, formatDuration, formatNumber, normalizeLocale, useShareMessages } from "../share/common" import { ContentMarkdown } from "./content-markdown" import type { MessageV2 } from "opencode/session/message-v2" import type { Diagnostic } from "vscode-languageserver-types" @@ -44,6 +44,7 @@ export interface PartProps { export function Part(props: PartProps) { const [copied, setCopied] = createSignal(false) const id = createMemo(() => props.message.id + "-" + props.index) + const messages = useShareMessages() return (
- @@ -143,11 +144,13 @@ export function Part(props: PartProps) {
{props.last && props.message.role === "assistant" && props.message.time.completed && (
- {DateTime.fromMillis(props.message.time.completed).toLocaleString(DateTime.DATETIME_MED)} + {DateTime.fromMillis(props.message.time.completed) + .setLocale(normalizeLocale(messages.locale)) + .toLocaleString(DateTime.DATETIME_MED)}
)}
@@ -155,13 +158,13 @@ export function Part(props: PartProps) { {props.message.role === "assistant" && props.part.type === "reasoning" && (
- Thinking + {messages.thinking}
- +
- +
@@ -170,13 +173,7 @@ export function Part(props: PartProps) { )} {props.message.role === "user" && props.part.type === "file" && (
-
Attachment
-
{props.part.filename}
-
- )} - {props.message.role === "user" && props.part.type === "file" && ( -
-
Attachment
+
{messages.attachment}
{props.part.filename}
)} @@ -188,7 +185,7 @@ export function Part(props: PartProps) { )} {props.part.type === "tool" && props.part.state.status === "error" && (
- {formatErrorString(props.part.state.error)} + {formatErrorString(props.part.state.error, messages.error)}
)} @@ -343,43 +340,45 @@ function getShikiLang(filename: string) { return type ? (overrides[type] ?? type) : "plaintext" } -function getDiagnostics(diagnosticsByFile: Record, currentFile: string): JSX.Element[] { +function getDiagnostics( + diagnosticsByFile: Record, + currentFile: string, + label: string, +): JSX.Element[] { const result: JSX.Element[] = [] if (diagnosticsByFile === undefined || diagnosticsByFile[currentFile] === undefined) return result - for (const diags of Object.values(diagnosticsByFile)) { - for (const d of diags) { - if (d.severity !== 1) continue - - const line = d.range.start.line + 1 - const column = d.range.start.character + 1 - - result.push( -
-          
-            Error
-          
-          
-            [{line}:{column}]
-          
-          {d.message}
-        
, - ) - } + for (const d of diagnosticsByFile[currentFile]) { + if (d.severity !== 1) continue + + const line = d.range.start.line + 1 + const column = d.range.start.character + 1 + + result.push( +
+        
+          {label}
+        
+        
+          [{line}:{column}]
+        
+        {d.message}
+      
, + ) } return result } -function formatErrorString(error: string): JSX.Element { +function formatErrorString(error: string, label: string): JSX.Element { const errorMarker = "Error: " const startsWithError = error.startsWith(errorMarker) return startsWithError ? (
       
-        Error
+        {label}
       
       {error.slice(errorMarker.length)}
     
@@ -391,6 +390,7 @@ function formatErrorString(error: string): JSX.Element { } export function TodoWriteTool(props: ToolProps) { + const messages = useShareMessages() const priority: Record = { in_progress: 0, pending: 1, @@ -406,9 +406,9 @@ export function TodoWriteTool(props: ToolProps) { <>
- - Creating plan - Completing plan + + {messages.creating_plan} + {messages.completing_plan}
@@ -429,6 +429,8 @@ export function TodoWriteTool(props: ToolProps) { } export function GrepTool(props: ToolProps) { + const messages = useShareMessages() + return ( <>
@@ -439,7 +441,12 @@ export function GrepTool(props: ToolProps) { 0}> @@ -482,6 +489,8 @@ export function ListTool(props: ToolProps) { } export function WebFetchTool(props: ToolProps) { + const messages = useShareMessages() + return ( <>
@@ -491,7 +500,7 @@ export function WebFetchTool(props: ToolProps) {
- {formatErrorString(props.state.output)} + {formatErrorString(props.state.output, messages.error)} @@ -505,6 +514,7 @@ export function WebFetchTool(props: ToolProps) { } export function ReadTool(props: ToolProps) { + const messages = useShareMessages() const filePath = createMemo(() => stripWorkingDirectory(props.state.input?.filePath, props.message.path.cwd)) return ( @@ -518,10 +528,10 @@ export function ReadTool(props: ToolProps) {
- {formatErrorString(props.state.output)} + {formatErrorString(props.state.output, messages.error)} - + @@ -537,8 +547,11 @@ export function ReadTool(props: ToolProps) { } export function WriteTool(props: ToolProps) { + const messages = useShareMessages() const filePath = createMemo(() => stripWorkingDirectory(props.state.input?.filePath, props.message.path.cwd)) - const diagnostics = createMemo(() => getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath)) + const diagnostics = createMemo(() => + getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath, messages.error), + ) return ( <> @@ -554,10 +567,10 @@ export function WriteTool(props: ToolProps) {
- {formatErrorString(props.state.output)} + {formatErrorString(props.state.output, messages.error)} - + @@ -568,8 +581,11 @@ export function WriteTool(props: ToolProps) { } export function EditTool(props: ToolProps) { + const messages = useShareMessages() const filePath = createMemo(() => stripWorkingDirectory(props.state.input.filePath, props.message.path.cwd)) - const diagnostics = createMemo(() => getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath)) + const diagnostics = createMemo(() => + getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath, messages.error), + ) return ( <> @@ -582,7 +598,7 @@ export function EditTool(props: ToolProps) {
- {formatErrorString(props.state.metadata?.message || "")} + {formatErrorString(props.state.metadata?.message || "", messages.error)}
@@ -609,6 +625,8 @@ export function BashTool(props: ToolProps) { } export function GlobTool(props: ToolProps) { + const messages = useShareMessages() + return ( <>
@@ -619,7 +637,12 @@ export function GlobTool(props: ToolProps) { 0}>
@@ -639,11 +662,12 @@ interface ResultsButtonProps extends ParentProps { } function ResultsButton(props: ResultsButtonProps) { const [show, setShow] = createSignal(false) + const messages = useShareMessages() return ( <>