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/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 +++++++++++++-------- 8 files changed, 177 insertions(+), 82 deletions(-) (limited to 'packages/web/src/components/share') 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 ( <>