继续多语言补充

This commit is contained in:
xintaofei
2026-03-07 14:22:18 +08:00
parent 47189318e5
commit 89c91ac1eb
12 changed files with 398 additions and 117 deletions

View File

@@ -3,6 +3,7 @@ import type { BundledLanguage } from "shiki"
import type { AdaptedContentPart } from "@/lib/adapters/ai-elements-adapter"
import type { MessageRole } from "@/lib/types"
import { normalizeToolName } from "@/lib/tool-call-normalization"
import { useTranslations } from "next-intl"
import {
countUnifiedDiffLineChanges,
estimateChangedLineStats,
@@ -1881,6 +1882,7 @@ const ToolCallPart = memo(function ToolCallPart({
}: {
part: Extract<AdaptedContentPart, { type: "tool-call" }>
}) {
const t = useTranslations("Folder.chat.contentParts")
const [manualOpen, setManualOpen] = useState(false)
const normalizedToolName = useMemo(
() => normalizeToolName(part.toolName),
@@ -2046,7 +2048,7 @@ const ToolCallPart = memo(function ToolCallPart({
/>
{liveOutputTruncated && (
<div className="text-[11px] text-muted-foreground">
Showing tail output while streaming for performance.
{t("showingTailOutput")}
</div>
)}
</div>
@@ -2068,9 +2070,14 @@ const ToolResultPart = memo(function ToolResultPart({
}: {
part: Extract<AdaptedContentPart, { type: "tool-result" }>
}) {
const t = useTranslations("Folder.chat.contentParts")
return (
<Tool>
<ToolHeader type="dynamic-tool" state={part.state} toolName="Result" />
<ToolHeader
type="dynamic-tool"
state={part.state}
toolName={t("result")}
/>
<ToolContent>
<ToolOutput output={part.output} errorText={part.errorText} />
</ToolContent>

View File

@@ -1,6 +1,7 @@
"use client"
import { useEffect, useMemo, useState } from "react"
import { useLocale, useTranslations } from "next-intl"
import type { LiveMessage } from "@/contexts/acp-connections-context"
import { inferLiveToolName } from "@/lib/tool-call-normalization"
import {
@@ -9,11 +10,6 @@ import {
} from "@/lib/line-change-stats"
import { FilePenLine, Timer, Wrench } from "lucide-react"
function formatElapsed(ms: number): string {
if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`
return `${(ms / 1_000).toFixed(1)}s`
}
interface LiveTurnStatsProps {
message: LiveMessage
}
@@ -27,14 +23,9 @@ interface LiveEditStats extends LineChangeStats {
files: number
}
const COMPACT_NUMBER = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
})
function formatCompactInt(n: number): string {
function formatCompactInt(n: number, formatter: Intl.NumberFormat): string {
if (n < 1000) return String(n)
return COMPACT_NUMBER.format(n).toUpperCase()
return formatter.format(n)
}
function asObject(value: unknown): Record<string, unknown> | null {
@@ -267,8 +258,18 @@ function extractLiveEditStats(message: LiveMessage): LiveEditStats {
}
export function LiveTurnStats({ message }: LiveTurnStatsProps) {
const locale = useLocale()
const t = useTranslations("Folder.chat.liveTurnStats")
const [elapsed, setElapsed] = useState(() => Date.now() - message.startedAt)
const editStats = useMemo(() => extractLiveEditStats(message), [message])
const compactNumberFormatter = useMemo(
() =>
new Intl.NumberFormat(locale, {
notation: "compact",
maximumFractionDigits: 1,
}),
[locale]
)
useEffect(() => {
const timer = setInterval(() => {
@@ -295,26 +296,32 @@ export function LiveTurnStats({ message }: LiveTurnStatsProps) {
isThinking = true
}
const elapsedLabel =
elapsed >= 60_000
? t("elapsedMinutes", { value: (elapsed / 60_000).toFixed(1) })
: t("elapsedSeconds", { value: (elapsed / 1_000).toFixed(1) })
return (
<div className="flex h-8 shrink-0 items-center justify-center gap-3 px-4 text-xs leading-none text-muted-foreground">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-primary animate-pulse shrink-0" />
{isThinking && message.content.length <= 1 ? (
<span>Thinking...</span>
<span>{t("thinking")}</span>
) : (
<span>Streaming</span>
<span>{t("streaming")}</span>
)}
<span className="text-border leading-none">|</span>
<span className="inline-flex items-center gap-1 leading-none">
<Timer className="h-3 w-3 shrink-0" />
{formatElapsed(elapsed)}
{elapsedLabel}
</span>
{editStats.files > 0 && (
<>
<span className="text-border leading-none">|</span>
<span className="inline-flex items-center gap-1 leading-none">
<FilePenLine className="h-3 w-3 shrink-0" />
{editStats.files}F +{formatCompactInt(editStats.additions)}/-
{formatCompactInt(editStats.deletions)}
{editStats.files}F +
{formatCompactInt(editStats.additions, compactNumberFormatter)}/-
{formatCompactInt(editStats.deletions, compactNumberFormatter)}
</span>
</>
)}
@@ -323,7 +330,7 @@ export function LiveTurnStats({ message }: LiveTurnStatsProps) {
<span className="text-border leading-none">|</span>
<span className="inline-flex items-center gap-1 leading-none">
<Wrench className="h-3 w-3 shrink-0" />
{toolCallCount} tool {toolCallCount === 1 ? "use" : "uses"}
{t("toolUseCount", { count: toolCallCount })}
</span>
</>
)}

View File

@@ -25,6 +25,7 @@ import {
} from "@/components/ai-elements/message-thread"
import { Message, MessageContent } from "@/components/ai-elements/message"
import { Loader2 } from "lucide-react"
import { useTranslations } from "next-intl"
import {
buildPlanKey,
extractLatestPlanEntriesFromMessages,
@@ -46,7 +47,10 @@ interface ResolvedMessageGroup extends MessageGroup {
resources: UserResourceDisplay[]
}
function fallbackExtractUserResources(group: MessageGroup): {
function fallbackExtractUserResources(
group: MessageGroup,
attachedResourcesText: string
): {
parts: AdaptedContentPart[]
resources: UserResourceDisplay[]
} {
@@ -87,14 +91,17 @@ function fallbackExtractUserResources(group: MessageGroup): {
}
if (parsedParts.length === 0 && dedupedResources.length > 0) {
parsedParts.push({ type: "text", text: "Attached resources" })
parsedParts.push({ type: "text", text: attachedResourcesText })
}
return { parts: parsedParts, resources: dedupedResources }
}
function resolveMessageGroup(group: MessageGroup): ResolvedMessageGroup {
const resolved = fallbackExtractUserResources(group)
function resolveMessageGroup(
group: MessageGroup,
attachedResourcesText: string
): ResolvedMessageGroup {
const resolved = fallbackExtractUserResources(group, attachedResourcesText)
return {
...group,
parts: resolved.parts,
@@ -161,6 +168,7 @@ export function MessageListView({
onPendingClear,
isActive = true,
}: MessageListViewProps) {
const t = useTranslations("Folder.chat.messageList")
const { detail, loading, error, refetch } = useDbMessageDetail(conversationId)
const turnCount = detail?.turns.length ?? 0
@@ -225,12 +233,16 @@ export function MessageListView({
[pendingMessages]
)
const resolvedGroups = useMemo(
() => groups.map(resolveMessageGroup),
[groups]
() =>
groups.map((group) => resolveMessageGroup(group, t("attachedResources"))),
[groups, t]
)
const resolvedPendingGroups = useMemo(
() => pendingGroups.map(resolveMessageGroup),
[pendingGroups]
() =>
pendingGroups.map((group) =>
resolveMessageGroup(group, t("attachedResources"))
),
[pendingGroups, t]
)
const showLiveMessage = Boolean(
@@ -245,7 +257,7 @@ export function MessageListView({
<div className="flex h-full items-center justify-center">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
<span>Loading...</span>
<span>{t("loading")}</span>
</div>
</div>
)
@@ -255,7 +267,9 @@ export function MessageListView({
return (
<div className="p-6">
<div className="text-center py-12">
<p className="text-destructive text-sm">Error: {error}</p>
<p className="text-destructive text-sm">
{t("error", { message: error })}
</p>
</div>
</div>
)
@@ -276,7 +290,7 @@ export function MessageListView({
!showLiveMessage ? (
<div className="text-center py-12">
<p className="text-muted-foreground text-sm">
No messages in this conversation.
{t("emptyConversation")}
</p>
</div>
) : (