refactor(i18n): convert remaining useMessages to useTranslations

- FAQSection: useTranslations with dynamic keys via type assertion
- ClipboardApp: useTranslations for JSX, keep useMessages for hooks
- SendTabPanel: useTranslations for html and roomStatus namespaces
- RetrieveTabPanel: useTranslations for html, roomStatus, and ClipboardApp
- FileListDisplay: useTranslations for FileListDisplay namespace
- FileUploadHandler: useTranslations for fileUploadHandler namespace

Only ClipboardApp.tsx retains useMessages for hooks requiring full messages object.
This commit is contained in:
david_bai
2026-03-27 15:09:15 +08:00
parent 131d1e12f5
commit 29897bea87
6 changed files with 85 additions and 125 deletions
+9 -6
View File
@@ -1,6 +1,6 @@
"use client";
import React, { useRef, useCallback, useEffect, useMemo } from "react";
import { useMessages } from "next-intl";
import { useMessages, useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import useRichTextToPlainText from "../hooks/useRichTextToPlainText";
import QRCodeComponent from "./ClipboardApp/ShareCard";
@@ -19,7 +19,10 @@ import { getCachedId } from "@/lib/roomIdCache";
import { useConnectionFeedback } from "@/hooks/useConnectionFeedback";
const ClipboardApp = () => {
// Keep useMessages for hooks that need the full message object
const messages = useMessages();
// Use useTranslations for static keys in JSX
const tHtml = useTranslations("text.ClipboardApp.html");
const { shareMessage, retrieveMessage, putMessageInMs } =
useClipboardAppMessages();
@@ -192,7 +195,7 @@ const ClipboardApp = () => {
id="send-tab"
aria-selected={activeTab === "send"}
>
{messages.text.ClipboardApp.html.senderTab}
{tHtml("senderTab")}
</Button>
<Button
variant={activeTab === "retrieve" ? "default" : "outline"}
@@ -202,15 +205,15 @@ const ClipboardApp = () => {
id="retrieve-tab"
aria-selected={activeTab === "retrieve"}
>
{messages.text.ClipboardApp.html.retrieveTab}
{tHtml("retrieveTab")}
</Button>
</div>
<Card className="border-4 sm:border-8 shadow-md">
<CardHeader className="px-3 sm:px-6 py-3 sm:py-6">
<CardTitle className="text-lg sm:text-xl">
{activeTab === "send"
? messages.text.ClipboardApp.html.shareTitleLabel
: messages.text.ClipboardApp.html.retrieveTitleLabel}
? tHtml("shareTitleLabel")
: tHtml("retrieveTitleLabel")}
</CardTitle>
</CardHeader>
<CardContent className="px-3 sm:px-6">
@@ -250,7 +253,7 @@ const ClipboardApp = () => {
<Card className="border-2 sm:border-4 shadow-md mt-2 sm:mt-4">
<CardHeader className="pb-3 sm:pb-6">
<CardTitle className="text-base sm:text-lg">
{messages.text.ClipboardApp.html.retrieveMethodTitle}
{tHtml("retrieveMethodTitle")}
</CardTitle>
</CardHeader>
<CardContent className="pt-0 px-3 sm:px-6">
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from "react";
import { useMessages } from "next-intl";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { Download, Trash2 } from "lucide-react";
import { Tooltip } from "@/components/Tooltip";
@@ -66,7 +66,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
saveType,
largeFileThreshold = 500 * 1024 * 1024, // 500MB default
}) => {
const messages = useMessages();
const t = useTranslations("text.FileListDisplay");
// Get the cleaning method of the store
const { clearSendProgress, clearReceiveProgress } = useFileTransferStore();
@@ -300,25 +300,20 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
// Get download count
const downloadCount = downloadCounts[item.fileId] || 0;
if (messages === null) {
return <div>Loading...</div>;
}
return (
<div className="flex flex-col sm:flex-row sm:items-center gap-2 min-w-0 flex-shrink-0">
{progress && progress.progress < 1 ? ( //Show progress or completed
<div className="w-full sm:w-auto">
<TransferProgress
message={
mode === "sender"
? messages.text.FileListDisplay.sendingLabel
: messages.text.FileListDisplay.receivingLabel
mode === "sender" ? t("sendingLabel") : t("receivingLabel")
}
progress={progress}
/>
</div>
) : showCompletion ? (
<span className="text-sm text-green-500 whitespace-nowrap">
{messages.text.FileListDisplay.finishedLabel}
{t("finishedLabel")}
</span>
) : null}
@@ -342,7 +337,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
{/* display download Num*/}
{mode === "sender" && (
<span className="text-xs sm:text-sm whitespace-nowrap">
{messages.text.FileListDisplay.downloadCountLabel}: {downloadCount}
{t("downloadCountLabel")}: {downloadCount}
</span>
)}
{mode === "sender" && onDelete && (
@@ -360,9 +355,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
className="text-xs sm:text-sm px-2 sm:px-3"
>
<Trash2 className="h-3 w-3 sm:h-4 sm:w-4 sm:mr-2" />
<span className="hidden sm:inline">
{messages.text.FileListDisplay.deleteLabel}
</span>
<span className="hidden sm:inline">{t("deleteLabel")}</span>
</Button>
)}
</div>
@@ -375,7 +368,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
const formatSize = formatFileSize(item.size);
const tooltipContent = isFolder
? `${formatFolderTips(
messages!.text.FileListDisplay.folderSummaryTemplate,
t("folderSummaryTemplate"),
item.name,
item.fileCount || 0,
formatSize
@@ -398,7 +391,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
<span className="text-xs sm:text-sm text-muted-foreground">
{isFolder
? `${formatFolderDis(
messages!.text.FileListDisplay.folderInlineTemplate,
t("folderInlineTemplate"),
item.fileCount || 0,
formatSize
)}`
@@ -412,9 +405,6 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
</div>
);
};
if (messages === null) {
return <div>Loading...</div>;
}
return (
<>
{(singleFiles.length > 0 || folders.length > 0) && (
@@ -424,17 +414,13 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
<div className="mb-2">
<AutoPopupDialog
storageKey="Choose-location-popup-shown"
title={messages.text.FileListDisplay.popupDialogTitle}
description={
messages.text.FileListDisplay.popupDialogDescription
}
title={t("popupDialogTitle")}
description={t("popupDialogDescription")}
condition={() => needPickLocation}
/>
{/* Regular reminder to select the save directory */}
<div className="flex items-center">
<p className="text-red-500 mb-2">
{messages.text.FileListDisplay.chooseSavePathTip}
</p>
<p className="text-red-500 mb-2">{t("chooseSavePathTip")}</p>
{onLocationPick && (
<Button
onClick={async () => {
@@ -445,7 +431,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
size="sm"
className="mr-2 text-red-500"
>
{messages.text.FileListDisplay.chooseSavePathLabel}
{t("chooseSavePathLabel")}
</Button>
)}
</div>
@@ -5,7 +5,7 @@ import React, {
useRef,
useCallback,
} from "react";
import { useMessages } from "next-intl";
import { useTranslations } from "next-intl";
import { Input } from "@/components/ui/input";
import { Upload } from "lucide-react";
import { FileMeta, CustomFile } from "@/types/webrtc";
@@ -41,19 +41,17 @@ interface FileUploadHandlerProps {
const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
onFilePicked,
}) => {
const messages = useMessages();
const t = useTranslations("text.fileUploadHandler");
const folderInputRef = useRef<HTMLInputElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
// File selector -- message prompt
const [fileText, setFileText] = useState<string>(
messages.text.fileUploadHandler.noFileChosenTip
);
const [fileText, setFileText] = useState<string>(t("noFileChosenTip"));
const [isModalOpen, setIsModalOpen] = useState(false);
useEffect(() => {
setFileText(messages.text.fileUploadHandler.noFileChosenTip);
}, [messages.text.fileUploadHandler.noFileChosenTip]);
setFileText(t("noFileChosenTip"));
}, [t]);
const handleFileChange = useCallback(
(newFiles: CustomFile[]) => {
@@ -64,16 +62,13 @@ const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
const folderNum = newFiles.filter((file) => file.folderName).length;
const choose_dis = formatFileChosen(
messages!.text.fileUploadHandler.fileChosenTemplate,
t("fileChosenTemplate"),
fileNum,
folderNum
);
setFileText(choose_dis);
setTimeout(
() => setFileText(messages!.text.fileUploadHandler.noFileChosenTip),
2000
);
setTimeout(() => setFileText(t("noFileChosenTip")), 2000);
// Reset the file input
if (fileInputRef.current) {
fileInputRef.current.value = "";
@@ -82,7 +77,7 @@ const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
folderInputRef.current.value = "";
}
},
[messages, onFilePicked]
[t, onFilePicked]
);
// Click to upload file processing
@@ -148,7 +143,7 @@ const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
onClick={handleZoneClick}
>
<p className="text-sm text-muted-foreground mb-4">
{messages.text.fileUploadHandler.chooseFileTip}
{t("chooseFileTip")}
</p>
<Upload className="h-12 w-12 mx-auto mb-4 text-primary" />
<p className="text-sm text-muted-foreground">{fileText}</p>
@@ -177,10 +172,10 @@ const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="text-xl font-semibold">
{messages.text.fileUploadHandler.chosenDiagTitle}
{t("chosenDiagTitle")}
</DialogTitle>
<DialogDescription className="mt-2 text-muted-foreground">
{messages.text.fileUploadHandler.chosenDiagDescription}
{t("chosenDiagDescription")}
</DialogDescription>
</DialogHeader>
<div className="flex justify-center gap-4 mt-6">
@@ -188,13 +183,13 @@ const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
onClick={handleSelectFile}
className="px-4 py-2 rounded transition-colors bg-primary text-primary-foreground hover:bg-primary/90"
>
{messages.text.fileUploadHandler.selectFileLabel}
{t("selectFileLabel")}
</button>
<button
onClick={handleSelectFolder}
className="px-4 py-2 rounded transition-colors bg-secondary text-secondary-foreground hover:bg-secondary/80"
>
{messages.text.fileUploadHandler.selectFolderLabel}
{t("selectFolderLabel")}
</button>
</div>
</DialogContent>
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from "react";
import { useMessages } from "next-intl";
import { useTranslations } from "next-intl";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
@@ -48,7 +48,9 @@ export function RetrieveTabPanel({
retrieveMessage,
handleLeaveRoom,
}: RetrieveTabPanelProps) {
const messages = useMessages();
const tHtml = useTranslations("text.ClipboardApp.html");
const tRoomStatus = useTranslations("text.ClipboardApp.roomStatus");
const t = useTranslations("text.ClipboardApp");
// Get the status from the store
const {
retrieveRoomStatusText,
@@ -61,25 +63,24 @@ export function RetrieveTabPanel({
} = useFileTransferStore();
const onLocationPick = useCallback(async (): Promise<boolean> => {
if (!messages) return false; // Should not happen if panel is rendered
if (!window.showDirectoryPicker) {
putMessageInMs(messages.text.ClipboardApp.pickSaveUnsupported, false);
putMessageInMs(t("pickSaveUnsupported"), false);
return false;
}
if (!window.confirm(messages.text.ClipboardApp.pickSaveMsg)) return false;
if (!window.confirm(t("pickSaveMsg"))) return false;
try {
const directoryHandle = await window.showDirectoryPicker();
await setReceiverDirectoryHandle(directoryHandle);
putMessageInMs(messages.text.ClipboardApp.pickSaveSuccess, false);
putMessageInMs(t("pickSaveSuccess"), false);
return true;
} catch (err: any) {
if (err.name !== "AbortError") {
console.error("Failed to set up folder receive:", err);
putMessageInMs(messages.text.ClipboardApp.pickSaveError, false);
putMessageInMs(t("pickSaveError"), false);
}
return false;
}
}, [messages, putMessageInMs, setReceiverDirectoryHandle]);
}, [t, putMessageInMs, setReceiverDirectoryHandle]);
const handleFileRequestFromPanel = useCallback(
(meta: FileMeta) => {
@@ -100,15 +101,15 @@ export function RetrieveTabPanel({
<div className="mb-3 text-sm text-muted-foreground">
{retrieveRoomStatusText ||
(isReceiverInRoom
? messages.text.ClipboardApp.roomStatus.connectedLabel
: messages.text.ClipboardApp.roomStatus.receiverEmptyMessage)}
? tRoomStatus("connectedLabel")
: tRoomStatus("receiverEmptyMessage"))}
</div>
<div className="space-y-3 mb-4">
{/* Room ID input section */}
<div className="space-y-2">
<div className="flex flex-col sm:flex-row gap-2">
<ReadClipboardButton
title={messages.text.ClipboardApp.html.readClipboardLabel}
title={tHtml("readClipboardLabel")}
onRead={setRetrieveRoomIdInput}
/>
{/* Save/Use Cached ID Button placed after Paste button */}
@@ -122,9 +123,7 @@ export function RetrieveTabPanel({
aria-label="Retrieve Room ID"
value={retrieveRoomIdInput}
onChange={(e) => setRetrieveRoomIdInput(e.target.value)}
placeholder={
messages.text.ClipboardApp.html.retrieveRoomIdPlaceholder
}
placeholder={tHtml("retrieveRoomIdPlaceholder")}
className="flex-1 min-w-0"
/>
</div>
@@ -138,7 +137,7 @@ export function RetrieveTabPanel({
ref={retrieveJoinRoomBtnRef}
disabled={isReceiverInRoom || !retrieveRoomIdInput.trim()}
>
{messages.text.ClipboardApp.html.joinRoomLabel}
{tHtml("joinRoomLabel")}
</Button>
<Button
variant={isAnyFileTransferring ? "destructive" : "outline"}
@@ -147,8 +146,8 @@ export function RetrieveTabPanel({
className="w-full sm:w-auto px-4 order-2"
>
{isAnyFileTransferring
? messages.text.ClipboardApp.roomStatus.leaveRoomLabel + " ⚠️"
: messages.text.ClipboardApp.roomStatus.leaveRoomLabel}
? tRoomStatus("leaveRoomLabel") + " ⚠️"
: tRoomStatus("leaveRoomLabel")}
</Button>
</div>
</div>
@@ -159,7 +158,7 @@ export function RetrieveTabPanel({
</div>
<div className="flex justify-start">
<WriteClipboardButton
title={messages.text.ClipboardApp.html.copyLabel}
title={tHtml("copyLabel")}
textToCopy={richTextToPlainText(retrievedContent)}
/>
</div>
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from "react";
import { useMessages } from "next-intl";
import { useTranslations } from "next-intl";
import dynamic from "next/dynamic";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
@@ -59,7 +59,8 @@ export function SendTabPanel({
handleLeaveSenderRoom,
putMessageInMs,
}: SendTabPanelProps) {
const messages = useMessages();
const tHtml = useTranslations("text.ClipboardApp.html");
const tRoomStatus = useTranslations("text.ClipboardApp.roomStatus");
// Get the status from the store
const {
shareContent,
@@ -138,17 +139,17 @@ export function SendTabPanel({
<div className="mb-3 text-sm text-muted-foreground">
{shareRoomStatusText ||
(isSenderInRoom
? messages.text.ClipboardApp.roomStatus.onlyOneMessage
: messages.text.ClipboardApp.roomStatus.senderEmptyMessage)}
? tRoomStatus("onlyOneMessage")
: tRoomStatus("senderEmptyMessage"))}
</div>
<RichTextEditor value={shareContent} onChange={updateShareContent} />
<div className="flex flex-col sm:flex-row gap-2 my-3">
<ReadClipboardButton
title={messages.text.ClipboardApp.html.pasteLabel}
title={tHtml("pasteLabel")}
onRead={updateShareContent}
/>
<WriteClipboardButton
title={messages.text.ClipboardApp.html.copyLabel}
title={tHtml("copyLabel")}
textToCopy={richTextToPlainText(shareContent)}
/>
</div>
@@ -166,7 +167,7 @@ export function SendTabPanel({
{/* Room ID input section */}
<div className="space-y-2">
<p className="text-sm text-muted-foreground">
{messages.text.ClipboardApp.html.inputRoomIdTip}
{tHtml("inputRoomIdTip")}
</p>
<div className="flex flex-col sm:flex-row gap-2">
<Input
@@ -175,9 +176,7 @@ export function SendTabPanel({
onChange={handleInputChange}
onPaste={handlePaste}
className="flex-1 min-w-0"
placeholder={
messages.text.ClipboardApp.html.retrieveRoomIdPlaceholder
}
placeholder={tHtml("retrieveRoomIdPlaceholder")}
/>
<Button
variant="outline"
@@ -186,8 +185,8 @@ export function SendTabPanel({
disabled={isSenderInRoom}
>
{isSimpleIdMode
? messages.text.ClipboardApp.html.generateRandomIdTip
: messages.text.ClipboardApp.html.generateSimpleIdTip}
? tHtml("generateRandomIdTip")
: tHtml("generateSimpleIdTip")}
</Button>
{/* Save/Use Cached ID Button in between */}
<CachedIdActionButton
@@ -206,7 +205,7 @@ export function SendTabPanel({
onClick={() => joinRoom(true, inputFieldValue.trim())}
disabled={isSenderInRoom || !inputFieldValue.trim()}
>
{messages.text.ClipboardApp.html.joinRoomLabel}
{tHtml("joinRoomLabel")}
</Button>
</div>
</div>
@@ -216,9 +215,7 @@ export function SendTabPanel({
<AnimatedButton
className="flex-1 order-1"
onClick={generateShareLinkAndBroadcast}
loadingText={
messages.text.ClipboardApp.html.syncSendingLoadingLabel
}
loadingText={tHtml("syncSendingLoadingLabel")}
disabled={
!isSenderInRoom ||
(sendFiles.length === 0 && shareContent.trim() === "") ||
@@ -226,7 +223,7 @@ export function SendTabPanel({
isAnyFileTransferring
}
>
{messages.text.ClipboardApp.html.syncSendingLabel}
{tHtml("syncSendingLabel")}
</AnimatedButton>
<Button
variant={isAnyFileTransferring ? "destructive" : "outline"}
@@ -235,8 +232,8 @@ export function SendTabPanel({
className="w-full sm:w-auto px-4 order-2"
>
{isAnyFileTransferring
? messages.text.ClipboardApp.roomStatus.leaveRoomLabel + " ⚠️"
: messages.text.ClipboardApp.roomStatus.leaveRoomLabel}
? tRoomStatus("leaveRoomLabel") + " ⚠️"
: tRoomStatus("leaveRoomLabel")}
</Button>
</div>
</div>
+18 -38
View File
@@ -1,6 +1,6 @@
"use client";
import { useMessages } from "next-intl";
import { useTranslations } from "next-intl";
import {
Accordion,
AccordionContent,
@@ -8,40 +8,13 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
interface FAQMessage {
[key: string]: string;
}
interface FAQ {
question: string;
answer: string;
}
const generateFAQs = (messages: { text: { faqs: FAQMessage } }): FAQ[] => {
const faqs: FAQ[] = [];
const faqsData = messages.text.faqs;
// Get the total number of questions (by finding keys starting with question_)
const questionKeys = Object.keys(faqsData).filter((key) =>
key.startsWith("question_")
);
// Automatically generate FAQ array based on the number of questions
questionKeys.forEach((qKey) => {
const index = qKey.split("_")[1]; // Get the numeric index
const aKey = `answer_${index}`;
if (faqsData[aKey]) {
// Ensure the corresponding answer exists
faqs.push({
question: faqsData[qKey],
answer: faqsData[aKey],
});
}
});
return faqs;
};
// Static FAQ count based on messages structure (indices 0-13)
const FAQ_COUNT = 14;
interface FAQSectionProps {
isInToolPage?: boolean; // Whether it is in the tool page (e.g. homepage)
@@ -57,8 +30,19 @@ export default function FAQSection({
showTitle = true,
titleClassName = "",
}: FAQSectionProps) {
const messages = useMessages();
const faqs = generateFAQs(messages);
const t = useTranslations("text.faqs");
// Generate FAQs using useTranslations with indexed keys
// We use type assertion since next-intl doesn't support dynamic keys in type system
const faqs: FAQ[] = [];
for (let i = 0; i < FAQ_COUNT; i++) {
const question = t(`question_${i}` as never);
const answer = t(`answer_${i}` as never);
// Only add if both question and answer exist (not fallback keys)
if (question && answer && !question.startsWith("question_")) {
faqs.push({ question, answer });
}
}
// Set default styles for different scenarios
const containerClasses = `container mx-auto px-4 py-8 ${className}`;
@@ -69,13 +53,9 @@ export default function FAQSection({
<div className={containerClasses}>
{showTitle &&
(isInToolPage ? (
<h2 className={`text-3xl ${titleClasses}`}>
{messages.text.faqs.faqLabel}
</h2>
<h2 className={`text-3xl ${titleClasses}`}>{t("faqLabel")}</h2>
) : (
<h1 className={`text-4xl ${titleClasses}`}>
{messages.text.faqs.faqLabel}
</h1>
<h1 className={`text-4xl ${titleClasses}`}>{t("faqLabel")}</h1>
))}
<Accordion type="single" collapsible className="w-full">
{faqs.map((faq, index) => (