mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-30 05:43:55 +00:00
wip(app): i18n
This commit is contained in:
parent
b13c269162
commit
6037e88ddf
@ -1,4 +1,5 @@
|
|||||||
import { Dialog as Kobalte } from "@kobalte/core/dialog"
|
import { Dialog as Kobalte } from "@kobalte/core/dialog"
|
||||||
|
import { useI18n } from "../context/i18n"
|
||||||
import { IconButton } from "./icon-button"
|
import { IconButton } from "./icon-button"
|
||||||
|
|
||||||
export interface ImagePreviewProps {
|
export interface ImagePreviewProps {
|
||||||
@ -7,6 +8,7 @@ export interface ImagePreviewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ImagePreview(props: ImagePreviewProps) {
|
export function ImagePreview(props: ImagePreviewProps) {
|
||||||
|
const i18n = useI18n()
|
||||||
return (
|
return (
|
||||||
<div data-component="image-preview">
|
<div data-component="image-preview">
|
||||||
<div data-slot="image-preview-container">
|
<div data-slot="image-preview-container">
|
||||||
@ -15,7 +17,7 @@ export function ImagePreview(props: ImagePreviewProps) {
|
|||||||
<Kobalte.CloseButton data-slot="image-preview-close" as={IconButton} icon="close" variant="ghost" />
|
<Kobalte.CloseButton data-slot="image-preview-close" as={IconButton} icon="close" variant="ghost" />
|
||||||
</div>
|
</div>
|
||||||
<div data-slot="image-preview-body">
|
<div data-slot="image-preview-body">
|
||||||
<img src={props.src} alt={props.alt ?? "Image preview"} data-slot="image-preview-image" />
|
<img src={props.src} alt={props.alt ?? i18n.t("ui.imagePreview.alt")} data-slot="image-preview-image" />
|
||||||
</div>
|
</div>
|
||||||
</Kobalte.Content>
|
</Kobalte.Content>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
|
import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
import { createEffect, createSignal, For, onCleanup, type JSX, on, Show } from "solid-js"
|
import { createEffect, createSignal, For, onCleanup, type JSX, on, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
import { useI18n } from "../context/i18n"
|
||||||
import { Icon, type IconProps } from "./icon"
|
import { Icon, type IconProps } from "./icon"
|
||||||
import { IconButton } from "./icon-button"
|
import { IconButton } from "./icon-button"
|
||||||
import { TextField } from "./text-field"
|
import { TextField } from "./text-field"
|
||||||
@ -30,6 +31,7 @@ export interface ListRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void }) {
|
export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void }) {
|
||||||
|
const i18n = useI18n()
|
||||||
const [scrollRef, setScrollRef] = createSignal<HTMLDivElement | undefined>(undefined)
|
const [scrollRef, setScrollRef] = createSignal<HTMLDivElement | undefined>(undefined)
|
||||||
const [internalFilter, setInternalFilter] = createSignal("")
|
const [internalFilter, setInternalFilter] = createSignal("")
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
@ -174,6 +176,25 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emptyMessage = () => {
|
||||||
|
if (grouped.loading) return props.loadingMessage ?? i18n.t("ui.list.loading")
|
||||||
|
if (props.emptyMessage) return props.emptyMessage
|
||||||
|
|
||||||
|
const query = filter()
|
||||||
|
if (!query) return i18n.t("ui.list.empty")
|
||||||
|
|
||||||
|
const suffix = i18n.t("ui.list.emptyWithFilter.suffix")
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span>{i18n.t("ui.list.emptyWithFilter.prefix")}</span>
|
||||||
|
<span data-slot="list-filter">"{query}"</span>
|
||||||
|
<Show when={suffix}>
|
||||||
|
<span>{suffix}</span>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component="list" classList={{ [props.class ?? ""]: !!props.class }}>
|
<div data-component="list" classList={{ [props.class ?? ""]: !!props.class }}>
|
||||||
<Show when={!!props.search}>
|
<Show when={!!props.search}>
|
||||||
@ -208,10 +229,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||||||
fallback={
|
fallback={
|
||||||
<div data-slot="list-empty-state">
|
<div data-slot="list-empty-state">
|
||||||
<div data-slot="list-message">
|
<div data-slot="list-message">
|
||||||
{grouped.loading ? props.loadingMessage ?? "Loading" : props.emptyMessage ?? "No results"}
|
{emptyMessage()}
|
||||||
<Show when={!props.emptyMessage && !props.loadingMessage && !!filter()}>
|
|
||||||
{" "}for <span data-slot="list-filter">"{filter()}"</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { UserMessage } from "@opencode-ai/sdk/v2"
|
|||||||
import { ComponentProps, For, Match, Show, splitProps, Switch } from "solid-js"
|
import { ComponentProps, For, Match, Show, splitProps, Switch } from "solid-js"
|
||||||
import { DiffChanges } from "./diff-changes"
|
import { DiffChanges } from "./diff-changes"
|
||||||
import { Tooltip } from "@kobalte/core/tooltip"
|
import { Tooltip } from "@kobalte/core/tooltip"
|
||||||
|
import { useI18n } from "../context/i18n"
|
||||||
|
|
||||||
export function MessageNav(
|
export function MessageNav(
|
||||||
props: ComponentProps<"ul"> & {
|
props: ComponentProps<"ul"> & {
|
||||||
@ -12,6 +13,7 @@ export function MessageNav(
|
|||||||
getLabel?: (message: UserMessage) => string | undefined
|
getLabel?: (message: UserMessage) => string | undefined
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
const i18n = useI18n()
|
||||||
const [local, others] = splitProps(props, ["messages", "current", "size", "onMessageSelect", "getLabel"])
|
const [local, others] = splitProps(props, ["messages", "current", "size", "onMessageSelect", "getLabel"])
|
||||||
|
|
||||||
const content = () => (
|
const content = () => (
|
||||||
@ -48,7 +50,10 @@ export function MessageNav(
|
|||||||
data-slot="message-nav-title-preview"
|
data-slot="message-nav-title-preview"
|
||||||
data-active={message.id === local.current?.id || undefined}
|
data-active={message.id === local.current?.id || undefined}
|
||||||
>
|
>
|
||||||
<Show when={local.getLabel?.(message) ?? message.summary?.title} fallback="New message">
|
<Show
|
||||||
|
when={local.getLabel?.(message) ?? message.summary?.title}
|
||||||
|
fallback={i18n.t("ui.messageNav.newMessage")}
|
||||||
|
>
|
||||||
{local.getLabel?.(message) ?? message.summary?.title}
|
{local.getLabel?.(message) ?? message.summary?.title}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import { useData } from "../context"
|
|||||||
import { useDiffComponent } from "../context/diff"
|
import { useDiffComponent } from "../context/diff"
|
||||||
import { useCodeComponent } from "../context/code"
|
import { useCodeComponent } from "../context/code"
|
||||||
import { useDialog } from "../context/dialog"
|
import { useDialog } from "../context/dialog"
|
||||||
|
import { useI18n } from "../context/i18n"
|
||||||
import { BasicTool } from "./basic-tool"
|
import { BasicTool } from "./basic-tool"
|
||||||
import { GenericTool } from "./basic-tool"
|
import { GenericTool } from "./basic-tool"
|
||||||
import { Button } from "./button"
|
import { Button } from "./button"
|
||||||
@ -67,13 +68,14 @@ function getDiagnostics(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DiagnosticsDisplay(props: { diagnostics: Diagnostic[] }): JSX.Element {
|
function DiagnosticsDisplay(props: { diagnostics: Diagnostic[] }): JSX.Element {
|
||||||
|
const i18n = useI18n()
|
||||||
return (
|
return (
|
||||||
<Show when={props.diagnostics.length > 0}>
|
<Show when={props.diagnostics.length > 0}>
|
||||||
<div data-component="diagnostics">
|
<div data-component="diagnostics">
|
||||||
<For each={props.diagnostics}>
|
<For each={props.diagnostics}>
|
||||||
{(diagnostic) => (
|
{(diagnostic) => (
|
||||||
<div data-slot="diagnostic">
|
<div data-slot="diagnostic">
|
||||||
<span data-slot="diagnostic-label">Error</span>
|
<span data-slot="diagnostic-label">{i18n.t("ui.messagePart.diagnostic.error")}</span>
|
||||||
<span data-slot="diagnostic-location">
|
<span data-slot="diagnostic-location">
|
||||||
[{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}]
|
[{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}]
|
||||||
</span>
|
</span>
|
||||||
@ -179,81 +181,84 @@ export type ToolInfo = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getToolInfo(tool: string, input: any = {}): ToolInfo {
|
export function getToolInfo(tool: string, input: any = {}): ToolInfo {
|
||||||
|
const i18n = useI18n()
|
||||||
switch (tool) {
|
switch (tool) {
|
||||||
case "read":
|
case "read":
|
||||||
return {
|
return {
|
||||||
icon: "glasses",
|
icon: "glasses",
|
||||||
title: "Read",
|
title: i18n.t("ui.tool.read"),
|
||||||
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
|
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
|
||||||
}
|
}
|
||||||
case "list":
|
case "list":
|
||||||
return {
|
return {
|
||||||
icon: "bullet-list",
|
icon: "bullet-list",
|
||||||
title: "List",
|
title: i18n.t("ui.tool.list"),
|
||||||
subtitle: input.path ? getFilename(input.path) : undefined,
|
subtitle: input.path ? getFilename(input.path) : undefined,
|
||||||
}
|
}
|
||||||
case "glob":
|
case "glob":
|
||||||
return {
|
return {
|
||||||
icon: "magnifying-glass-menu",
|
icon: "magnifying-glass-menu",
|
||||||
title: "Glob",
|
title: i18n.t("ui.tool.glob"),
|
||||||
subtitle: input.pattern,
|
subtitle: input.pattern,
|
||||||
}
|
}
|
||||||
case "grep":
|
case "grep":
|
||||||
return {
|
return {
|
||||||
icon: "magnifying-glass-menu",
|
icon: "magnifying-glass-menu",
|
||||||
title: "Grep",
|
title: i18n.t("ui.tool.grep"),
|
||||||
subtitle: input.pattern,
|
subtitle: input.pattern,
|
||||||
}
|
}
|
||||||
case "webfetch":
|
case "webfetch":
|
||||||
return {
|
return {
|
||||||
icon: "window-cursor",
|
icon: "window-cursor",
|
||||||
title: "Webfetch",
|
title: i18n.t("ui.tool.webfetch"),
|
||||||
subtitle: input.url,
|
subtitle: input.url,
|
||||||
}
|
}
|
||||||
case "task":
|
case "task":
|
||||||
return {
|
return {
|
||||||
icon: "task",
|
icon: "task",
|
||||||
title: `${input.subagent_type || "task"} Agent`,
|
title: i18n.t("ui.tool.agent", { type: input.subagent_type || "task" }),
|
||||||
subtitle: input.description,
|
subtitle: input.description,
|
||||||
}
|
}
|
||||||
case "bash":
|
case "bash":
|
||||||
return {
|
return {
|
||||||
icon: "console",
|
icon: "console",
|
||||||
title: "Shell",
|
title: i18n.t("ui.tool.shell"),
|
||||||
subtitle: input.description,
|
subtitle: input.description,
|
||||||
}
|
}
|
||||||
case "edit":
|
case "edit":
|
||||||
return {
|
return {
|
||||||
icon: "code-lines",
|
icon: "code-lines",
|
||||||
title: "Edit",
|
title: i18n.t("ui.messagePart.title.edit"),
|
||||||
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
|
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
|
||||||
}
|
}
|
||||||
case "write":
|
case "write":
|
||||||
return {
|
return {
|
||||||
icon: "code-lines",
|
icon: "code-lines",
|
||||||
title: "Write",
|
title: i18n.t("ui.messagePart.title.write"),
|
||||||
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
|
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
|
||||||
}
|
}
|
||||||
case "apply_patch":
|
case "apply_patch":
|
||||||
return {
|
return {
|
||||||
icon: "code-lines",
|
icon: "code-lines",
|
||||||
title: "Patch",
|
title: i18n.t("ui.tool.patch"),
|
||||||
subtitle: input.files?.length ? `${input.files.length} file${input.files.length > 1 ? "s" : ""}` : undefined,
|
subtitle: input.files?.length
|
||||||
|
? `${input.files.length} ${i18n.t(input.files.length > 1 ? "ui.common.file.other" : "ui.common.file.one")}`
|
||||||
|
: undefined,
|
||||||
}
|
}
|
||||||
case "todowrite":
|
case "todowrite":
|
||||||
return {
|
return {
|
||||||
icon: "checklist",
|
icon: "checklist",
|
||||||
title: "To-dos",
|
title: i18n.t("ui.tool.todos"),
|
||||||
}
|
}
|
||||||
case "todoread":
|
case "todoread":
|
||||||
return {
|
return {
|
||||||
icon: "checklist",
|
icon: "checklist",
|
||||||
title: "Read to-dos",
|
title: i18n.t("ui.tool.todos.read"),
|
||||||
}
|
}
|
||||||
case "question":
|
case "question":
|
||||||
return {
|
return {
|
||||||
icon: "bubble-5",
|
icon: "bubble-5",
|
||||||
title: "Questions",
|
title: i18n.t("ui.tool.questions"),
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
@ -297,6 +302,7 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage; part
|
|||||||
|
|
||||||
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
|
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
const i18n = useI18n()
|
||||||
const [copied, setCopied] = createSignal(false)
|
const [copied, setCopied] = createSignal(false)
|
||||||
const [expanded, setExpanded] = createSignal(false)
|
const [expanded, setExpanded] = createSignal(false)
|
||||||
const [canExpand, setCanExpand] = createSignal(false)
|
const [canExpand, setCanExpand] = createSignal(false)
|
||||||
@ -385,7 +391,11 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<img data-slot="user-message-attachment-image" src={file.url} alt={file.filename ?? "attachment"} />
|
<img
|
||||||
|
data-slot="user-message-attachment-image"
|
||||||
|
src={file.url}
|
||||||
|
alt={file.filename ?? i18n.t("ui.message.attachment.alt")}
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -398,7 +408,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
|||||||
<button
|
<button
|
||||||
data-slot="user-message-expand"
|
data-slot="user-message-expand"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={expanded() ? "Collapse message" : "Expand message"}
|
aria-label={expanded() ? i18n.t("ui.message.collapse") : i18n.t("ui.message.expand")}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
toggleExpanded()
|
toggleExpanded()
|
||||||
@ -407,7 +417,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
|||||||
<Icon name="chevron-down" size="small" />
|
<Icon name="chevron-down" size="small" />
|
||||||
</button>
|
</button>
|
||||||
<div data-slot="user-message-copy-wrapper">
|
<div data-slot="user-message-copy-wrapper">
|
||||||
<Tooltip value={copied() ? "Copied!" : "Copy"} placement="top" gutter={8}>
|
<Tooltip value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="top" gutter={8}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={copied() ? "check" : "copy"}
|
icon={copied() ? "check" : "copy"}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@ -529,6 +539,7 @@ export const ToolRegistry = {
|
|||||||
|
|
||||||
PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
|
const i18n = useI18n()
|
||||||
const part = props.part as ToolPart
|
const part = props.part as ToolPart
|
||||||
|
|
||||||
const permission = createMemo(() => {
|
const permission = createMemo(() => {
|
||||||
@ -639,13 +650,13 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
|||||||
<div data-component="permission-prompt">
|
<div data-component="permission-prompt">
|
||||||
<div data-slot="permission-actions">
|
<div data-slot="permission-actions">
|
||||||
<Button variant="ghost" size="small" onClick={() => respond("reject")}>
|
<Button variant="ghost" size="small" onClick={() => respond("reject")}>
|
||||||
Deny
|
{i18n.t("ui.permission.deny")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" size="small" onClick={() => respond("always")}>
|
<Button variant="secondary" size="small" onClick={() => respond("always")}>
|
||||||
Allow always
|
{i18n.t("ui.permission.allowAlways")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" size="small" onClick={() => respond("once")}>
|
<Button variant="primary" size="small" onClick={() => respond("once")}>
|
||||||
Allow once
|
{i18n.t("ui.permission.allowOnce")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -687,6 +698,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
|
|||||||
ToolRegistry.register({
|
ToolRegistry.register({
|
||||||
name: "read",
|
name: "read",
|
||||||
render(props) {
|
render(props) {
|
||||||
|
const i18n = useI18n()
|
||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
if (props.input.offset) args.push("offset=" + props.input.offset)
|
if (props.input.offset) args.push("offset=" + props.input.offset)
|
||||||
if (props.input.limit) args.push("limit=" + props.input.limit)
|
if (props.input.limit) args.push("limit=" + props.input.limit)
|
||||||
@ -695,7 +707,7 @@ ToolRegistry.register({
|
|||||||
{...props}
|
{...props}
|
||||||
icon="glasses"
|
icon="glasses"
|
||||||
trigger={{
|
trigger={{
|
||||||
title: "Read",
|
title: i18n.t("ui.tool.read"),
|
||||||
subtitle: props.input.filePath ? getFilename(props.input.filePath) : "",
|
subtitle: props.input.filePath ? getFilename(props.input.filePath) : "",
|
||||||
args,
|
args,
|
||||||
}}
|
}}
|
||||||
@ -707,11 +719,12 @@ ToolRegistry.register({
|
|||||||
ToolRegistry.register({
|
ToolRegistry.register({
|
||||||
name: "list",
|
name: "list",
|
||||||
render(props) {
|
render(props) {
|
||||||
|
const i18n = useI18n()
|
||||||
return (
|
return (
|
||||||
<BasicTool
|
<BasicTool
|
||||||
{...props}
|
{...props}
|
||||||
icon="bullet-list"
|
icon="bullet-list"
|
||||||
trigger={{ title: "List", subtitle: getDirectory(props.input.path || "/") }}
|
trigger={{ title: i18n.t("ui.tool.list"), subtitle: getDirectory(props.input.path || "/") }}
|
||||||
>
|
>
|
||||||
<Show when={props.output}>
|
<Show when={props.output}>
|
||||||
{(output) => (
|
{(output) => (
|
||||||
@ -728,12 +741,13 @@ ToolRegistry.register({
|
|||||||
ToolRegistry.register({
|
ToolRegistry.register({
|
||||||
name: "glob",
|
name: "glob",
|
||||||
render(props) {
|
render(props) {
|
||||||
|
const i18n = useI18n()
|
||||||
return (
|
return (
|
||||||
<BasicTool
|
<BasicTool
|
||||||
{...props}
|
{...props}
|
||||||
icon="magnifying-glass-menu"
|
icon="magnifying-glass-menu"
|
||||||
trigger={{
|
trigger={{
|
||||||
title: "Glob",
|
title: i18n.t("ui.tool.glob"),
|
||||||
subtitle: getDirectory(props.input.path || "/"),
|
subtitle: getDirectory(props.input.path || "/"),
|
||||||
args: props.input.pattern ? ["pattern=" + props.input.pattern] : [],
|
args: props.input.pattern ? ["pattern=" + props.input.pattern] : [],
|
||||||
}}
|
}}
|
||||||
@ -753,6 +767,7 @@ ToolRegistry.register({
|
|||||||
ToolRegistry.register({
|
ToolRegistry.register({
|
||||||
name: "grep",
|
name: "grep",
|
||||||
render(props) {
|
render(props) {
|
||||||
|
const i18n = useI18n()
|
||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
if (props.input.pattern) args.push("pattern=" + props.input.pattern)
|
if (props.input.pattern) args.push("pattern=" + props.input.pattern)
|
||||||
if (props.input.include) args.push("include=" + props.input.include)
|
if (props.input.include) args.push("include=" + props.input.include)
|
||||||
@ -761,7 +776,7 @@ ToolRegistry.register({
|
|||||||
{...props}
|
{...props}
|
||||||
icon="magnifying-glass-menu"
|
icon="magnifying-glass-menu"
|
||||||
trigger={{
|
trigger={{
|
||||||
title: "Grep",
|
title: i18n.t("ui.tool.grep"),
|
||||||
subtitle: getDirectory(props.input.path || "/"),
|
subtitle: getDirectory(props.input.path || "/"),
|
||||||
args,
|
args,
|
||||||
}}
|
}}
|
||||||
@ -781,12 +796,13 @@ ToolRegistry.register({
|
|||||||
ToolRegistry.register({
|
ToolRegistry.register({
|
||||||
name: "webfetch",
|
name: "webfetch",
|
||||||
render(props) {
|
render(props) {
|
||||||
|
const i18n = useI18n()
|
||||||
return (
|
return (
|
||||||
<BasicTool
|
<BasicTool
|
||||||
{...props}
|
{...props}
|
||||||
icon="window-cursor"
|
icon="window-cursor"
|
||||||
trigger={{
|
trigger={{
|
||||||
title: "Webfetch",
|
title: i18n.t("ui.tool.webfetch"),
|
||||||
subtitle: props.input.url || "",
|
subtitle: props.input.url || "",
|
||||||
args: props.input.format ? ["format=" + props.input.format] : [],
|
args: props.input.format ? ["format=" + props.input.format] : [],
|
||||||
action: (
|
action: (
|
||||||
@ -812,6 +828,7 @@ ToolRegistry.register({
|
|||||||
name: "task",
|
name: "task",
|
||||||
render(props) {
|
render(props) {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
|
const i18n = useI18n()
|
||||||
const summary = () =>
|
const summary = () =>
|
||||||
(props.metadata.summary ?? []) as { id: string; tool: string; state: { status: string; title?: string } }[]
|
(props.metadata.summary ?? []) as { id: string; tool: string; state: { status: string; title?: string } }[]
|
||||||
|
|
||||||
@ -899,7 +916,7 @@ ToolRegistry.register({
|
|||||||
icon="task"
|
icon="task"
|
||||||
defaultOpen={true}
|
defaultOpen={true}
|
||||||
trigger={{
|
trigger={{
|
||||||
title: `${props.input.subagent_type || props.tool} Agent`,
|
title: i18n.t("ui.tool.agent", { type: props.input.subagent_type || props.tool }),
|
||||||
titleClass: "capitalize",
|
titleClass: "capitalize",
|
||||||
subtitle: props.input.description,
|
subtitle: props.input.description,
|
||||||
}}
|
}}
|
||||||
@ -912,13 +929,13 @@ ToolRegistry.register({
|
|||||||
<div data-component="permission-prompt">
|
<div data-component="permission-prompt">
|
||||||
<div data-slot="permission-actions">
|
<div data-slot="permission-actions">
|
||||||
<Button variant="ghost" size="small" onClick={() => respond("reject")}>
|
<Button variant="ghost" size="small" onClick={() => respond("reject")}>
|
||||||
Deny
|
{i18n.t("ui.permission.deny")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" size="small" onClick={() => respond("always")}>
|
<Button variant="secondary" size="small" onClick={() => respond("always")}>
|
||||||
Allow always
|
{i18n.t("ui.permission.allowAlways")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" size="small" onClick={() => respond("once")}>
|
<Button variant="primary" size="small" onClick={() => respond("once")}>
|
||||||
Allow once
|
{i18n.t("ui.permission.allowOnce")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -929,7 +946,7 @@ ToolRegistry.register({
|
|||||||
icon="task"
|
icon="task"
|
||||||
defaultOpen={true}
|
defaultOpen={true}
|
||||||
trigger={{
|
trigger={{
|
||||||
title: `${props.input.subagent_type || props.tool} Agent`,
|
title: i18n.t("ui.tool.agent", { type: props.input.subagent_type || props.tool }),
|
||||||
titleClass: "capitalize",
|
titleClass: "capitalize",
|
||||||
subtitle: props.input.description,
|
subtitle: props.input.description,
|
||||||
}}
|
}}
|
||||||
@ -969,12 +986,13 @@ ToolRegistry.register({
|
|||||||
ToolRegistry.register({
|
ToolRegistry.register({
|
||||||
name: "bash",
|
name: "bash",
|
||||||
render(props) {
|
render(props) {
|
||||||
|
const i18n = useI18n()
|
||||||
return (
|
return (
|
||||||
<BasicTool
|
<BasicTool
|
||||||
{...props}
|
{...props}
|
||||||
icon="console"
|
icon="console"
|
||||||
trigger={{
|
trigger={{
|
||||||
title: "Shell",
|
title: i18n.t("ui.tool.shell"),
|
||||||
subtitle: props.input.description,
|
subtitle: props.input.description,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -991,6 +1009,7 @@ ToolRegistry.register({
|
|||||||
ToolRegistry.register({
|
ToolRegistry.register({
|
||||||
name: "edit",
|
name: "edit",
|
||||||
render(props) {
|
render(props) {
|
||||||
|
const i18n = useI18n()
|
||||||
const diffComponent = useDiffComponent()
|
const diffComponent = useDiffComponent()
|
||||||
const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath))
|
const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath))
|
||||||
const filename = () => getFilename(props.input.filePath ?? "")
|
const filename = () => getFilename(props.input.filePath ?? "")
|
||||||
@ -1001,7 +1020,9 @@ ToolRegistry.register({
|
|||||||
trigger={
|
trigger={
|
||||||
<div data-component="edit-trigger">
|
<div data-component="edit-trigger">
|
||||||
<div data-slot="message-part-title-area">
|
<div data-slot="message-part-title-area">
|
||||||
<div data-slot="message-part-title">Edit {filename()}</div>
|
<div data-slot="message-part-title">
|
||||||
|
{i18n.t("ui.messagePart.title.edit")} {filename()}
|
||||||
|
</div>
|
||||||
<Show when={props.input.filePath?.includes("/")}>
|
<Show when={props.input.filePath?.includes("/")}>
|
||||||
<div data-slot="message-part-path">
|
<div data-slot="message-part-path">
|
||||||
<span data-slot="message-part-directory">{getDirectory(props.input.filePath!)}</span>
|
<span data-slot="message-part-directory">{getDirectory(props.input.filePath!)}</span>
|
||||||
@ -1040,6 +1061,7 @@ ToolRegistry.register({
|
|||||||
ToolRegistry.register({
|
ToolRegistry.register({
|
||||||
name: "write",
|
name: "write",
|
||||||
render(props) {
|
render(props) {
|
||||||
|
const i18n = useI18n()
|
||||||
const codeComponent = useCodeComponent()
|
const codeComponent = useCodeComponent()
|
||||||
const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath))
|
const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath))
|
||||||
const filename = () => getFilename(props.input.filePath ?? "")
|
const filename = () => getFilename(props.input.filePath ?? "")
|
||||||
@ -1050,7 +1072,9 @@ ToolRegistry.register({
|
|||||||
trigger={
|
trigger={
|
||||||
<div data-component="write-trigger">
|
<div data-component="write-trigger">
|
||||||
<div data-slot="message-part-title-area">
|
<div data-slot="message-part-title-area">
|
||||||
<div data-slot="message-part-title">Write {filename()}</div>
|
<div data-slot="message-part-title">
|
||||||
|
{i18n.t("ui.messagePart.title.write")} {filename()}
|
||||||
|
</div>
|
||||||
<Show when={props.input.filePath?.includes("/")}>
|
<Show when={props.input.filePath?.includes("/")}>
|
||||||
<div data-slot="message-part-path">
|
<div data-slot="message-part-path">
|
||||||
<span data-slot="message-part-directory">{getDirectory(props.input.filePath!)}</span>
|
<span data-slot="message-part-directory">{getDirectory(props.input.filePath!)}</span>
|
||||||
@ -1095,13 +1119,14 @@ interface ApplyPatchFile {
|
|||||||
ToolRegistry.register({
|
ToolRegistry.register({
|
||||||
name: "apply_patch",
|
name: "apply_patch",
|
||||||
render(props) {
|
render(props) {
|
||||||
|
const i18n = useI18n()
|
||||||
const diffComponent = useDiffComponent()
|
const diffComponent = useDiffComponent()
|
||||||
const files = createMemo(() => (props.metadata.files ?? []) as ApplyPatchFile[])
|
const files = createMemo(() => (props.metadata.files ?? []) as ApplyPatchFile[])
|
||||||
|
|
||||||
const subtitle = createMemo(() => {
|
const subtitle = createMemo(() => {
|
||||||
const count = files().length
|
const count = files().length
|
||||||
if (count === 0) return ""
|
if (count === 0) return ""
|
||||||
return `${count} file${count > 1 ? "s" : ""}`
|
return `${count} ${i18n.t(count > 1 ? "ui.common.file.other" : "ui.common.file.one")}`
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -1109,7 +1134,7 @@ ToolRegistry.register({
|
|||||||
{...props}
|
{...props}
|
||||||
icon="code-lines"
|
icon="code-lines"
|
||||||
trigger={{
|
trigger={{
|
||||||
title: "Patch",
|
title: i18n.t("ui.tool.patch"),
|
||||||
subtitle: subtitle(),
|
subtitle: subtitle(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -1122,22 +1147,22 @@ ToolRegistry.register({
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Match when={file.type === "delete"}>
|
<Match when={file.type === "delete"}>
|
||||||
<span data-slot="apply-patch-file-action" data-type="delete">
|
<span data-slot="apply-patch-file-action" data-type="delete">
|
||||||
Deleted
|
{i18n.t("ui.patch.action.deleted")}
|
||||||
</span>
|
</span>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={file.type === "add"}>
|
<Match when={file.type === "add"}>
|
||||||
<span data-slot="apply-patch-file-action" data-type="add">
|
<span data-slot="apply-patch-file-action" data-type="add">
|
||||||
Created
|
{i18n.t("ui.patch.action.created")}
|
||||||
</span>
|
</span>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={file.type === "move"}>
|
<Match when={file.type === "move"}>
|
||||||
<span data-slot="apply-patch-file-action" data-type="move">
|
<span data-slot="apply-patch-file-action" data-type="move">
|
||||||
Moved
|
{i18n.t("ui.patch.action.moved")}
|
||||||
</span>
|
</span>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={file.type === "update"}>
|
<Match when={file.type === "update"}>
|
||||||
<span data-slot="apply-patch-file-action" data-type="update">
|
<span data-slot="apply-patch-file-action" data-type="update">
|
||||||
Patched
|
{i18n.t("ui.patch.action.patched")}
|
||||||
</span>
|
</span>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@ -1171,6 +1196,7 @@ ToolRegistry.register({
|
|||||||
ToolRegistry.register({
|
ToolRegistry.register({
|
||||||
name: "todowrite",
|
name: "todowrite",
|
||||||
render(props) {
|
render(props) {
|
||||||
|
const i18n = useI18n()
|
||||||
const todos = createMemo(() => {
|
const todos = createMemo(() => {
|
||||||
const meta = props.metadata?.todos
|
const meta = props.metadata?.todos
|
||||||
if (Array.isArray(meta)) return meta
|
if (Array.isArray(meta)) return meta
|
||||||
@ -1193,7 +1219,7 @@ ToolRegistry.register({
|
|||||||
defaultOpen
|
defaultOpen
|
||||||
icon="checklist"
|
icon="checklist"
|
||||||
trigger={{
|
trigger={{
|
||||||
title: "To-dos",
|
title: i18n.t("ui.tool.todos"),
|
||||||
subtitle: subtitle(),
|
subtitle: subtitle(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -1218,6 +1244,7 @@ ToolRegistry.register({
|
|||||||
ToolRegistry.register({
|
ToolRegistry.register({
|
||||||
name: "question",
|
name: "question",
|
||||||
render(props) {
|
render(props) {
|
||||||
|
const i18n = useI18n()
|
||||||
const questions = createMemo(() => (props.input.questions ?? []) as QuestionInfo[])
|
const questions = createMemo(() => (props.input.questions ?? []) as QuestionInfo[])
|
||||||
const answers = createMemo(() => (props.metadata.answers ?? []) as QuestionAnswer[])
|
const answers = createMemo(() => (props.metadata.answers ?? []) as QuestionAnswer[])
|
||||||
const completed = createMemo(() => answers().length > 0)
|
const completed = createMemo(() => answers().length > 0)
|
||||||
@ -1225,8 +1252,8 @@ ToolRegistry.register({
|
|||||||
const subtitle = createMemo(() => {
|
const subtitle = createMemo(() => {
|
||||||
const count = questions().length
|
const count = questions().length
|
||||||
if (count === 0) return ""
|
if (count === 0) return ""
|
||||||
if (completed()) return `${count} answered`
|
if (completed()) return i18n.t("ui.question.subtitle.answered", { count })
|
||||||
return `${count} question${count > 1 ? "s" : ""}`
|
return `${count} ${i18n.t(count > 1 ? "ui.common.question.other" : "ui.common.question.one")}`
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -1235,7 +1262,7 @@ ToolRegistry.register({
|
|||||||
defaultOpen={completed()}
|
defaultOpen={completed()}
|
||||||
icon="bubble-5"
|
icon="bubble-5"
|
||||||
trigger={{
|
trigger={{
|
||||||
title: "Questions",
|
title: i18n.t("ui.tool.questions"),
|
||||||
subtitle: subtitle(),
|
subtitle: subtitle(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -1247,7 +1274,7 @@ ToolRegistry.register({
|
|||||||
return (
|
return (
|
||||||
<div data-slot="question-answer-item">
|
<div data-slot="question-answer-item">
|
||||||
<div data-slot="question-text">{q.question}</div>
|
<div data-slot="question-text">{q.question}</div>
|
||||||
<div data-slot="answer-text">{answer().join(", ") || "(no answer)"}</div>
|
<div data-slot="answer-text">{answer().join(", ") || i18n.t("ui.question.answer.none")}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@ -1261,6 +1288,7 @@ ToolRegistry.register({
|
|||||||
|
|
||||||
function QuestionPrompt(props: { request: QuestionRequest }) {
|
function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
|
const i18n = useI18n()
|
||||||
const questions = createMemo(() => props.request.questions)
|
const questions = createMemo(() => props.request.questions)
|
||||||
const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
|
const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
|
||||||
|
|
||||||
@ -1387,7 +1415,7 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
|
|||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
<button data-slot="question-tab" data-active={confirm()} onClick={() => selectTab(questions().length)}>
|
<button data-slot="question-tab" data-active={confirm()} onClick={() => selectTab(questions().length)}>
|
||||||
Confirm
|
{i18n.t("ui.common.confirm")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
@ -1396,7 +1424,7 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
|
|||||||
<div data-slot="question-content">
|
<div data-slot="question-content">
|
||||||
<div data-slot="question-text">
|
<div data-slot="question-text">
|
||||||
{question()?.question}
|
{question()?.question}
|
||||||
{multi() ? " (select all that apply)" : ""}
|
{multi() ? " " + i18n.t("ui.question.multiHint") : ""}
|
||||||
</div>
|
</div>
|
||||||
<div data-slot="question-options">
|
<div data-slot="question-options">
|
||||||
<For each={options()}>
|
<For each={options()}>
|
||||||
@ -1420,7 +1448,7 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
|
|||||||
data-picked={customPicked()}
|
data-picked={customPicked()}
|
||||||
onClick={() => selectOption(options().length)}
|
onClick={() => selectOption(options().length)}
|
||||||
>
|
>
|
||||||
<span data-slot="option-label">Type your own answer</span>
|
<span data-slot="option-label">{i18n.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||||
<Show when={!store.editing && input()}>
|
<Show when={!store.editing && input()}>
|
||||||
<span data-slot="option-description">{input()}</span>
|
<span data-slot="option-description">{input()}</span>
|
||||||
</Show>
|
</Show>
|
||||||
@ -1434,7 +1462,7 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
|
|||||||
ref={(el) => setTimeout(() => el.focus(), 0)}
|
ref={(el) => setTimeout(() => el.focus(), 0)}
|
||||||
type="text"
|
type="text"
|
||||||
data-slot="custom-input"
|
data-slot="custom-input"
|
||||||
placeholder="Type your answer..."
|
placeholder={i18n.t("ui.question.custom.placeholder")}
|
||||||
value={input()}
|
value={input()}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
const inputs = [...store.custom]
|
const inputs = [...store.custom]
|
||||||
@ -1443,10 +1471,10 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" variant="primary" size="small">
|
<Button type="submit" variant="primary" size="small">
|
||||||
{multi() ? "Add" : "Submit"}
|
{multi() ? i18n.t("ui.common.add") : i18n.t("ui.common.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" variant="ghost" size="small" onClick={() => setStore("editing", false)}>
|
<Button type="button" variant="ghost" size="small" onClick={() => setStore("editing", false)}>
|
||||||
Cancel
|
{i18n.t("ui.common.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Show>
|
</Show>
|
||||||
@ -1456,7 +1484,7 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
|
|||||||
|
|
||||||
<Show when={confirm()}>
|
<Show when={confirm()}>
|
||||||
<div data-slot="question-review">
|
<div data-slot="question-review">
|
||||||
<div data-slot="review-title">Review your answers</div>
|
<div data-slot="review-title">{i18n.t("ui.messagePart.review.title")}</div>
|
||||||
<For each={questions()}>
|
<For each={questions()}>
|
||||||
{(q, index) => {
|
{(q, index) => {
|
||||||
const value = () => store.answers[index()]?.join(", ") ?? ""
|
const value = () => store.answers[index()]?.join(", ") ?? ""
|
||||||
@ -1465,7 +1493,7 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
|
|||||||
<div data-slot="review-item">
|
<div data-slot="review-item">
|
||||||
<span data-slot="review-label">{q.question}</span>
|
<span data-slot="review-label">{q.question}</span>
|
||||||
<span data-slot="review-value" data-answered={answered()}>
|
<span data-slot="review-value" data-answered={answered()}>
|
||||||
{answered() ? value() : "(not answered)"}
|
{answered() ? value() : i18n.t("ui.question.review.notAnswered")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -1476,12 +1504,12 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
|
|||||||
|
|
||||||
<div data-slot="question-actions">
|
<div data-slot="question-actions">
|
||||||
<Button variant="ghost" size="small" onClick={reject}>
|
<Button variant="ghost" size="small" onClick={reject}>
|
||||||
Dismiss
|
{i18n.t("ui.common.dismiss")}
|
||||||
</Button>
|
</Button>
|
||||||
<Show when={!single()}>
|
<Show when={!single()}>
|
||||||
<Show when={confirm()}>
|
<Show when={confirm()}>
|
||||||
<Button variant="primary" size="small" onClick={submit}>
|
<Button variant="primary" size="small" onClick={submit}>
|
||||||
Submit
|
{i18n.t("ui.common.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!confirm() && multi()}>
|
<Show when={!confirm() && multi()}>
|
||||||
@ -1491,7 +1519,7 @@ function QuestionPrompt(props: { request: QuestionRequest }) {
|
|||||||
onClick={() => selectTab(store.tab + 1)}
|
onClick={() => selectTab(store.tab + 1)}
|
||||||
disabled={(store.answers[store.tab]?.length ?? 0) === 0}
|
disabled={(store.answers[store.tab]?.length ?? 0) === 0}
|
||||||
>
|
>
|
||||||
Next
|
{i18n.t("ui.common.next")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { TextField as Kobalte } from "@kobalte/core/text-field"
|
import { TextField as Kobalte } from "@kobalte/core/text-field"
|
||||||
import { createSignal, Show, splitProps } from "solid-js"
|
import { createSignal, Show, splitProps } from "solid-js"
|
||||||
import type { ComponentProps } from "solid-js"
|
import type { ComponentProps } from "solid-js"
|
||||||
|
import { useI18n } from "../context/i18n"
|
||||||
import { IconButton } from "./icon-button"
|
import { IconButton } from "./icon-button"
|
||||||
import { Tooltip } from "./tooltip"
|
import { Tooltip } from "./tooltip"
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ export interface TextFieldProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TextField(props: TextFieldProps) {
|
export function TextField(props: TextFieldProps) {
|
||||||
|
const i18n = useI18n()
|
||||||
const [local, others] = splitProps(props, [
|
const [local, others] = splitProps(props, [
|
||||||
"name",
|
"name",
|
||||||
"defaultValue",
|
"defaultValue",
|
||||||
@ -90,7 +92,11 @@ export function TextField(props: TextFieldProps) {
|
|||||||
<Kobalte.TextArea {...others} autoResize data-slot="input-input" class={local.class} />
|
<Kobalte.TextArea {...others} autoResize data-slot="input-input" class={local.class} />
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={local.copyable}>
|
<Show when={local.copyable}>
|
||||||
<Tooltip value={copied() ? "Copied" : "Copy to clipboard"} placement="top" gutter={8}>
|
<Tooltip
|
||||||
|
value={copied() ? i18n.t("ui.textField.copied") : i18n.t("ui.textField.copyToClipboard")}
|
||||||
|
placement="top"
|
||||||
|
gutter={8}
|
||||||
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={copied() ? "check" : "copy"}
|
icon={copied() ? "check" : "copy"}
|
||||||
|
|||||||
@ -30,4 +30,61 @@ export const dict = {
|
|||||||
"ui.messagePart.title.write": "Write",
|
"ui.messagePart.title.write": "Write",
|
||||||
"ui.messagePart.option.typeOwnAnswer": "Type your own answer",
|
"ui.messagePart.option.typeOwnAnswer": "Type your own answer",
|
||||||
"ui.messagePart.review.title": "Review your answers",
|
"ui.messagePart.review.title": "Review your answers",
|
||||||
|
|
||||||
|
"ui.list.loading": "Loading",
|
||||||
|
"ui.list.empty": "No results",
|
||||||
|
"ui.list.emptyWithFilter.prefix": "No results for",
|
||||||
|
"ui.list.emptyWithFilter.suffix": "",
|
||||||
|
|
||||||
|
"ui.messageNav.newMessage": "New message",
|
||||||
|
|
||||||
|
"ui.textField.copyToClipboard": "Copy to clipboard",
|
||||||
|
"ui.textField.copied": "Copied",
|
||||||
|
|
||||||
|
"ui.imagePreview.alt": "Image preview",
|
||||||
|
|
||||||
|
"ui.tool.read": "Read",
|
||||||
|
"ui.tool.list": "List",
|
||||||
|
"ui.tool.glob": "Glob",
|
||||||
|
"ui.tool.grep": "Grep",
|
||||||
|
"ui.tool.webfetch": "Webfetch",
|
||||||
|
"ui.tool.shell": "Shell",
|
||||||
|
"ui.tool.patch": "Patch",
|
||||||
|
"ui.tool.todos": "To-dos",
|
||||||
|
"ui.tool.todos.read": "Read to-dos",
|
||||||
|
"ui.tool.questions": "Questions",
|
||||||
|
"ui.tool.agent": "{{type}} Agent",
|
||||||
|
|
||||||
|
"ui.common.file.one": "file",
|
||||||
|
"ui.common.file.other": "files",
|
||||||
|
"ui.common.question.one": "question",
|
||||||
|
"ui.common.question.other": "questions",
|
||||||
|
|
||||||
|
"ui.common.add": "Add",
|
||||||
|
"ui.common.cancel": "Cancel",
|
||||||
|
"ui.common.confirm": "Confirm",
|
||||||
|
"ui.common.dismiss": "Dismiss",
|
||||||
|
"ui.common.next": "Next",
|
||||||
|
"ui.common.submit": "Submit",
|
||||||
|
|
||||||
|
"ui.permission.deny": "Deny",
|
||||||
|
"ui.permission.allowAlways": "Allow always",
|
||||||
|
"ui.permission.allowOnce": "Allow once",
|
||||||
|
|
||||||
|
"ui.message.expand": "Expand message",
|
||||||
|
"ui.message.collapse": "Collapse message",
|
||||||
|
"ui.message.copy": "Copy",
|
||||||
|
"ui.message.copied": "Copied!",
|
||||||
|
"ui.message.attachment.alt": "attachment",
|
||||||
|
|
||||||
|
"ui.patch.action.deleted": "Deleted",
|
||||||
|
"ui.patch.action.created": "Created",
|
||||||
|
"ui.patch.action.moved": "Moved",
|
||||||
|
"ui.patch.action.patched": "Patched",
|
||||||
|
|
||||||
|
"ui.question.subtitle.answered": "{{count}} answered",
|
||||||
|
"ui.question.answer.none": "(no answer)",
|
||||||
|
"ui.question.review.notAnswered": "(not answered)",
|
||||||
|
"ui.question.multiHint": "(select all that apply)",
|
||||||
|
"ui.question.custom.placeholder": "Type your answer...",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,4 +34,61 @@ export const dict = {
|
|||||||
"ui.messagePart.title.write": "写入",
|
"ui.messagePart.title.write": "写入",
|
||||||
"ui.messagePart.option.typeOwnAnswer": "输入自己的答案",
|
"ui.messagePart.option.typeOwnAnswer": "输入自己的答案",
|
||||||
"ui.messagePart.review.title": "检查你的答案",
|
"ui.messagePart.review.title": "检查你的答案",
|
||||||
|
|
||||||
|
"ui.list.loading": "加载中",
|
||||||
|
"ui.list.empty": "无结果",
|
||||||
|
"ui.list.emptyWithFilter.prefix": "没有关于",
|
||||||
|
"ui.list.emptyWithFilter.suffix": "的结果",
|
||||||
|
|
||||||
|
"ui.messageNav.newMessage": "新消息",
|
||||||
|
|
||||||
|
"ui.textField.copyToClipboard": "复制到剪贴板",
|
||||||
|
"ui.textField.copied": "已复制",
|
||||||
|
|
||||||
|
"ui.imagePreview.alt": "图片预览",
|
||||||
|
|
||||||
|
"ui.tool.read": "读取",
|
||||||
|
"ui.tool.list": "列表",
|
||||||
|
"ui.tool.glob": "Glob",
|
||||||
|
"ui.tool.grep": "Grep",
|
||||||
|
"ui.tool.webfetch": "Webfetch",
|
||||||
|
"ui.tool.shell": "Shell",
|
||||||
|
"ui.tool.patch": "补丁",
|
||||||
|
"ui.tool.todos": "待办",
|
||||||
|
"ui.tool.todos.read": "读取待办",
|
||||||
|
"ui.tool.questions": "问题",
|
||||||
|
"ui.tool.agent": "{{type}} 智能体",
|
||||||
|
|
||||||
|
"ui.common.file.one": "个文件",
|
||||||
|
"ui.common.file.other": "个文件",
|
||||||
|
"ui.common.question.one": "个问题",
|
||||||
|
"ui.common.question.other": "个问题",
|
||||||
|
|
||||||
|
"ui.common.add": "添加",
|
||||||
|
"ui.common.cancel": "取消",
|
||||||
|
"ui.common.confirm": "确认",
|
||||||
|
"ui.common.dismiss": "忽略",
|
||||||
|
"ui.common.next": "下一步",
|
||||||
|
"ui.common.submit": "提交",
|
||||||
|
|
||||||
|
"ui.permission.deny": "拒绝",
|
||||||
|
"ui.permission.allowAlways": "始终允许",
|
||||||
|
"ui.permission.allowOnce": "允许一次",
|
||||||
|
|
||||||
|
"ui.message.expand": "展开消息",
|
||||||
|
"ui.message.collapse": "收起消息",
|
||||||
|
"ui.message.copy": "复制",
|
||||||
|
"ui.message.copied": "已复制",
|
||||||
|
"ui.message.attachment.alt": "附件",
|
||||||
|
|
||||||
|
"ui.patch.action.deleted": "已删除",
|
||||||
|
"ui.patch.action.created": "已创建",
|
||||||
|
"ui.patch.action.moved": "已移动",
|
||||||
|
"ui.patch.action.patched": "已应用补丁",
|
||||||
|
|
||||||
|
"ui.question.subtitle.answered": "{{count}} 已回答",
|
||||||
|
"ui.question.answer.none": "(无答案)",
|
||||||
|
"ui.question.review.notAnswered": "(未回答)",
|
||||||
|
"ui.question.multiHint": "(可多选)",
|
||||||
|
"ui.question.custom.placeholder": "输入你的答案...",
|
||||||
} satisfies Partial<Record<Keys, string>>
|
} satisfies Partial<Record<Keys, string>>
|
||||||
|
|||||||
@ -120,6 +120,22 @@ Examples (non-exhaustive):
|
|||||||
- `Type your own answer`
|
- `Type your own answer`
|
||||||
- `Review your answers`
|
- `Review your answers`
|
||||||
|
|
||||||
|
### 4) Additional Hardcoded Strings (Full Audit)
|
||||||
|
|
||||||
|
Found during a full `packages/ui/src/components` + `packages/ui/src/context` sweep:
|
||||||
|
|
||||||
|
- `packages/ui/src/components/list.tsx`
|
||||||
|
- `Loading`
|
||||||
|
- `No results`
|
||||||
|
- `No results for "{{filter}}"`
|
||||||
|
- `packages/ui/src/components/message-nav.tsx`
|
||||||
|
- `New message`
|
||||||
|
- `packages/ui/src/components/text-field.tsx`
|
||||||
|
- `Copied`
|
||||||
|
- `Copy to clipboard`
|
||||||
|
- `packages/ui/src/components/image-preview.tsx`
|
||||||
|
- `Image preview` (alt text)
|
||||||
|
|
||||||
## Prioritized Implementation Plan
|
## Prioritized Implementation Plan
|
||||||
|
|
||||||
1. Completed (2026-01-20): Add `@opencode-ai/ui` i18n context (`packages/ui/src/context/i18n.tsx`) + export it.
|
1. Completed (2026-01-20): Add `@opencode-ai/ui` i18n context (`packages/ui/src/context/i18n.tsx`) + export it.
|
||||||
@ -128,8 +144,8 @@ Examples (non-exhaustive):
|
|||||||
- `packages/app/src/app.tsx`
|
- `packages/app/src/app.tsx`
|
||||||
- `packages/enterprise/src/app.tsx`
|
- `packages/enterprise/src/app.tsx`
|
||||||
4. Completed (2026-01-20): Convert `packages/ui/src/components/session-review.tsx` and `packages/ui/src/components/session-turn.tsx` to use `useI18n().t(...)`.
|
4. Completed (2026-01-20): Convert `packages/ui/src/components/session-review.tsx` and `packages/ui/src/components/session-turn.tsx` to use `useI18n().t(...)`.
|
||||||
5. Convert `packages/ui/src/components/message-part.tsx`.
|
5. Completed (2026-01-20): Convert `packages/ui/src/components/message-part.tsx`.
|
||||||
6. Do a full `packages/ui/src/components` + `packages/ui/src/context` audit for additional hardcoded copy.
|
6. Completed (2026-01-20): Do a full `packages/ui/src/components` + `packages/ui/src/context` audit for additional hardcoded copy.
|
||||||
|
|
||||||
## Notes / Risks
|
## Notes / Risks
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user