qwen-code/packages/web-shell/client/components/messages/UserMessage.tsx
Shaojin Wen 8296ce9e54
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
fix(web-shell): i18n for ~43 hardcoded English strings across 15 files (#6516)
* fix(web-shell): i18n for ~43 hardcoded English strings across 15 files

Multi-round audit replaced hardcoded UI strings with t() calls and
added en/zh-CN i18n keys for:

- Session timeline labels, aria-labels, and kind labels (MessageList)
- SubAgent tab labels, tools count, pending/running status
- Auth protocol option labels, placeholders, API key masking
- Voice dictation UI states (VoiceButton)
- Copy tooltip, Runtime badge, N/A, Server/Agent fallbacks
- Mermaid code block labels, image alt text, model switch summary

* fix(web-shell): i18n for missed models placeholder in AuthMessage

* fix(web-shell): invalidate timeline cache on locale switch

Store the t function reference in sessionTimelineCache alongside the
message signature. When the locale changes, t gets a new reference,
triggering cache invalidation and re-generating entries with the new
language labels.
2026-07-08 08:46:37 +00:00

124 lines
3.5 KiB
TypeScript

import {
memo,
useMemo,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { isSafeImageSrc } from './Markdown';
import { useWebShellCustomization } from '../../customization';
import { useI18n } from '../../i18n';
import flashStyles from '../MessageLocateFlash.module.css';
import styles from './UserMessage.module.css';
interface UserMessageImage {
data: string;
mimeType: string;
}
interface UserMessageProps {
content: string;
images?: UserMessageImage[];
isLocateFlashing?: boolean;
}
export const UserMessage = memo(function UserMessage({
content,
images,
isLocateFlashing = false,
}: UserMessageProps) {
const { t } = useI18n();
const { renderUserMessageContent } = useWebShellCustomization();
const contentRef = useRef<HTMLDivElement>(null);
const [expanded, setExpanded] = useState(false);
const [heightOverflowing, setHeightOverflowing] = useState(false);
const renderedContent = useMemo(
() => renderUserMessageContent?.({ content, images }) ?? content,
[content, images, renderUserMessageContent],
);
const measureOverflow = useCallback(() => {
const el = contentRef.current;
if (!el) return;
setHeightOverflowing(el.scrollHeight > 400);
}, []);
useLayoutEffect(() => {
setExpanded(false);
measureOverflow();
}, [content, images?.length, measureOverflow]);
useEffect(() => {
const el = contentRef.current;
if (!el || typeof ResizeObserver === 'undefined') return;
const observer = new ResizeObserver(measureOverflow);
observer.observe(el);
return () => observer.disconnect();
}, [measureOverflow]);
return (
<div className={styles.chatMessageRow}>
<div
className={`${styles.chatBubble}${
isLocateFlashing ? ` ${flashStyles.flash}` : ''
}`}
>
<div
ref={contentRef}
className={`${styles.chatContent} ${
heightOverflowing && !expanded ? styles.chatContentCollapsed : ''
}`}
>
{images && images.length > 0 && (
<div className={styles.chatImages}>
{images.map((img, index) => {
const src = img.data.startsWith('data:')
? img.data
: `data:${img.mimeType};base64,${img.data}`;
if (!isSafeImageSrc(src)) return null;
return (
<img
key={index}
src={src}
alt={t('user.uploadedImage', { index: index + 1 })}
className={styles.chatImageThumb}
onLoad={measureOverflow}
/>
);
})}
</div>
)}
{renderedContent}
</div>
{heightOverflowing && (
<button
type="button"
className={styles.toggleButton}
onClick={() => setExpanded((value) => !value)}
>
<span>
{expanded ? t('userMessage.showLess') : t('userMessage.showMore')}
</span>
<svg
className={`${styles.toggleIcon} ${
expanded ? styles.toggleIconExpanded : ''
}`}
viewBox="0 0 16 16"
aria-hidden="true"
>
<path
d="m4 6 4 4 4-4"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
)}
</div>
</div>
);
});