fix(webui): complete i18n audit

This commit is contained in:
chengyongru
2026-08-03 17:53:33 +08:00
committed by chengyongru
parent df11fd92a6
commit 2b63715282
22 changed files with 1535 additions and 994 deletions
+3 -1
View File
@@ -31,7 +31,9 @@ export function AttachmentTile({ attachment, className, inline = false, variant
target="_blank"
rel="noreferrer noopener"
className="block bg-muted/20"
aria-label={attachment.name ? `Open ${attachment.name}` : t("lightbox.open", { defaultValue: "Open image" })}
aria-label={attachment.name
? t("message.openAttachment", { name: attachment.name })
: t("lightbox.open", { defaultValue: "Open image" })}
>
<img
src={attachment.url}
+5 -2
View File
@@ -1,4 +1,5 @@
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import {
INLINE_TOKEN_HIGHLIGHT_COLOR,
@@ -140,6 +141,7 @@ export function CliAppMentionToken({
variant: "composer" | "message";
isHero?: boolean;
}) {
const { t } = useTranslation();
const color = app.brand_color || INLINE_TOKEN_HIGHLIGHT_COLOR;
const mentionName = label.startsWith("@") ? label.slice(1) : label;
const logoUrls = useMemo(() => logoFallbackUrls(app.logo_url), [app.logo_url]);
@@ -150,7 +152,7 @@ export function CliAppMentionToken({
return (
<InlineTokenHighlight
testId={`${testIdPrefix}-cli-mention-${app.name}`}
title={`CLI app: ${app.display_name || app.name}`}
title={t("thread.composer.mentions.cliTitle", { name: app.display_name || app.name })}
color={color}
>
<span
@@ -195,6 +197,7 @@ export function McpPresetMentionToken({
variant: "composer" | "message";
isHero?: boolean;
}) {
const { t } = useTranslation();
const color = preset.brand_color || INLINE_TOKEN_HIGHLIGHT_COLOR;
const mentionName = label.startsWith("@") ? label.slice(1) : label;
const logoUrls = useMemo(() => logoFallbackUrls(preset.logo_url), [preset.logo_url]);
@@ -205,7 +208,7 @@ export function McpPresetMentionToken({
return (
<InlineTokenHighlight
testId={`${testIdPrefix}-mcp-mention-${preset.name}`}
title={`MCP server: ${preset.display_name || preset.name}`}
title={t("thread.composer.mentions.mcpTitle", { name: preset.display_name || preset.name })}
color={color}
>
<span
@@ -411,6 +411,7 @@ function inlineLinkPreviewFromChildren(children: ReactNode): InlineLinkPreview |
}
function InlineLinkPreviewRow({ link }: { link: InlineLinkPreview }) {
const { t } = useTranslation();
const { favicon, onFaviconError, onFaviconLoad } = useFaviconFallback(link.host);
const label = link.prefix
? `${link.prefix}${link.title}`
@@ -421,7 +422,7 @@ function InlineLinkPreviewRow({ link }: { link: InlineLinkPreview }) {
href={link.href}
target="_blank"
rel="noreferrer noopener"
aria-label={`Open link: ${label}`}
aria-label={t("message.openLink", { label })}
className={cn(
"not-prose inline-flex max-w-full items-center gap-2 align-baseline",
"text-blue-500 no-underline underline-offset-2 hover:underline dark:text-blue-300",
+3 -1
View File
@@ -1,4 +1,5 @@
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import {
CliAppMentionToken,
@@ -69,6 +70,7 @@ export function UserMessageText({
cliApps: CliAppInfo[];
mcpPresets: McpPresetInfo[];
}) {
const { t } = useTranslation();
const segments = splitUserMessageSegments(text, cliApps, mcpPresets);
return (
<>
@@ -80,7 +82,7 @@ export function UserMessageText({
<InlineTokenHighlight
key={`skill-${segment.name}-${index}`}
testId={`message-skill-reference-${segment.name.toLowerCase()}`}
title={`Skill: ${segment.name}`}
title={t("message.skill", { name: segment.name })}
color={INLINE_TOKEN_HIGHLIGHT_COLOR}
className="font-medium"
>
@@ -7678,7 +7678,7 @@ function McpAppsCatalogRow({
onClick={() => setSetupOpen(false)}
className="h-7 rounded-full px-2.5 text-[11.5px] font-semibold text-muted-foreground"
>
{tx("actions.cancel", "Cancel")}
{tx("settings.actions.cancel", "Cancel")}
</Button>
</div>
<div className="mt-3 grid gap-2">
@@ -304,10 +304,13 @@ export function ChannelValidationDetails({ validation }: { validation: ChannelVa
}
export function ChannelValidationChecks({ validation }: { validation: ChannelValidationPayload }) {
const { t } = useTranslation();
if (!validation.checks.length) return null;
return (
<div className="border-t border-border/60 px-4 py-4">
<div className="mb-2 text-[12px] font-semibold text-foreground">Connection checks</div>
<div className="mb-2 text-[12px] font-semibold text-foreground">
{t("settings.channels.connectionChecks")}
</div>
<div className="space-y-2">
{validation.checks.slice(0, 6).map((check) => (
<div key={check.id} className="flex gap-2 text-[12px] leading-5">
@@ -326,7 +329,7 @@ export function ChannelValidationChecks({ validation }: { validation: ChannelVal
rel="noreferrer"
className="inline-flex items-center gap-1 text-foreground underline decoration-border underline-offset-4"
>
Open
{t("settings.channels.open")}
<ExternalLink className="h-3 w-3" aria-hidden />
</a>
) : null}
+4 -2
View File
@@ -1,4 +1,5 @@
import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
import type { UIMessage } from "@/lib/types";
@@ -49,6 +50,7 @@ export function PromptRail({
onJumpToPrompt,
scrollRef,
}: PromptRailProps) {
const { t } = useTranslation();
const railRef = useRef<HTMLDivElement>(null);
const measuredPromptsRef = useRef<MeasuredPrompt[]>([]);
const promptAnchors = useMemo(() => userPromptAnchors(messages), [messages]);
@@ -142,7 +144,7 @@ export function PromptRail({
return (
<div
ref={railRef}
aria-label="User prompt navigation"
aria-label={t("thread.promptNavigator.railAria")}
className={cn(
"thread-prompt-rail group pointer-events-auto absolute top-3 z-20 w-9 opacity-100",
"transition-opacity duration-200",
@@ -159,7 +161,7 @@ export function PromptRail({
<button
key={marker.ids.join("|")}
type="button"
aria-label={`Jump to prompt: ${marker.label}`}
aria-label={t("thread.promptNavigator.jumpTo", { label: marker.label })}
onClick={() => onJumpToPrompt(marker.ids[marker.ids.length - 1])}
onBlur={() => setFocusedMarkerIndex(null)}
onFocus={() => setFocusedMarkerIndex(index)}
+27 -23
View File
@@ -1,6 +1,7 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
@@ -30,29 +31,32 @@ interface DialogContentProps
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
DialogContentProps
>(({ className, children, showCloseButton = true, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<DialogPrimitive.Content
ref={ref}
className={cn(
"grid w-full max-w-lg origin-center gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg",
className,
)}
{...props}
>
{children}
{showCloseButton ? (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
) : null}
</DialogPrimitive.Content>
</div>
</DialogPortal>
));
>(({ className, children, showCloseButton = true, ...props }, ref) => {
const { t } = useTranslation();
return (
<DialogPortal>
<DialogOverlay />
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<DialogPrimitive.Content
ref={ref}
className={cn(
"grid w-full max-w-lg origin-center gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg",
className,
)}
{...props}
>
{children}
{showCloseButton ? (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">{t("common.close")}</span>
</DialogPrimitive.Close>
) : null}
</DialogPrimitive.Content>
</div>
</DialogPortal>
);
});
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
+34 -30
View File
@@ -2,6 +2,7 @@ import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cva, type VariantProps } from "class-variance-authority";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
@@ -74,36 +75,39 @@ const SheetContent = React.forwardRef<
...props
},
ref,
) => (
<SheetPortal>
<SheetOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
sheetVariants({ side }),
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"duration-300",
className,
)}
{...props}
>
{children}
{showCloseButton ? (
<DialogPrimitive.Close
className={cn(
"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity",
"hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
"disabled:pointer-events-none",
closeButtonClassName,
)}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
) : null}
</DialogPrimitive.Content>
</SheetPortal>
));
) => {
const { t } = useTranslation();
return (
<SheetPortal>
<SheetOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
sheetVariants({ side }),
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"duration-300",
className,
)}
{...props}
>
{children}
{showCloseButton ? (
<DialogPrimitive.Close
className={cn(
"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity",
"hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
"disabled:pointer-events-none",
closeButtonClassName,
)}
>
<X className="h-4 w-4" />
<span className="sr-only">{t("common.close")}</span>
</DialogPrimitive.Close>
) : null}
</DialogPrimitive.Content>
</SheetPortal>
);
});
SheetContent.displayName = DialogPrimitive.Content.displayName;
const SheetTitle = React.forwardRef<