refactor(i18n): replace prop drilling with translation context

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
david_bai
2026-03-27 14:04:28 +08:00
parent 57004b3a1f
commit cf529eed64
34 changed files with 188 additions and 291 deletions
+7 -19
View File
@@ -1,20 +1,16 @@
"use client";
import ClipboardApp from "@/components/ClipboardApp";
import { useI18n } from "@/components/providers/TranslationProvider";
import { cn } from "@/lib/utils";
import SystemDiagram from "@/components/web/SystemDiagram";
import FAQSection from "@/components/web/FAQSection";
import HowItWorks from "@/components/web/HowItWorks";
import YouTubePlayer from "@/components/common/YouTubePlayer";
import KeyFeatures from "@/components/web/KeyFeatures";
import type { Messages } from "@/types/messages";
import LazyLoadWrapper from "@/components/common/LazyLoadWrapper";
interface PageContentProps {
messages: Messages;
lang: string;
}
export default function HomeClient({ messages, lang }: PageContentProps) {
export default function HomeClient() {
const { messages, lang } = useI18n();
const youtube_videoId = lang === "zh" ? "I0RLCpcbUXs" : "ypt-po_R2Ds";
const bilibili_videoId = lang === "zh" ? "BV1knrjYZEfn" : "BV1yErjYFEV7";
return (
@@ -41,7 +37,7 @@ export default function HomeClient({ messages, lang }: PageContentProps) {
{/* How It Works Section */}
<section aria-label="How It Works">
<LazyLoadWrapper>
<HowItWorks messages={messages} />
<HowItWorks />
</LazyLoadWrapper>
</section>
{/* Demo Video Section */}
@@ -81,27 +77,19 @@ export default function HomeClient({ messages, lang }: PageContentProps) {
{/* System Architecture Section */}
<section aria-label="System Architecture">
<LazyLoadWrapper>
<SystemDiagram messages={messages} />
<SystemDiagram />
</LazyLoadWrapper>
</section>
{/* Key Features */}
<section aria-label="Key Features">
<LazyLoadWrapper>
<KeyFeatures
messages={messages}
isInToolPage
titleClassName="text-2xl md:text-3xl"
/>
<KeyFeatures isInToolPage titleClassName="text-2xl md:text-3xl" />
</LazyLoadWrapper>
</section>
{/* FAQ Section */}
<section aria-label="Frequently Asked Questions">
<LazyLoadWrapper>
<FAQSection
messages={messages}
isInToolPage
titleClassName="text-2xl md:text-3xl"
/>
<FAQSection isInToolPage titleClassName="text-2xl md:text-3xl" />
</LazyLoadWrapper>
</section>
</main>
+5 -6
View File
@@ -1,11 +1,10 @@
import type { Messages } from "@/types/messages";
"use client";
interface AboutContentProps {
messages: Messages;
lang: string;
}
import { useI18n } from "@/components/providers/TranslationProvider";
export default function AboutContent() {
const { messages, lang } = useI18n();
export default function AboutContent({ messages, lang }: AboutContentProps) {
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold text-center mb-6">
+2 -8
View File
@@ -31,12 +31,6 @@ export async function generateMetadata({
};
}
export default async function About({
params: { lang },
}: {
params: { lang: string };
}) {
const messages = await getDictionary(lang);
return <AboutContent messages={messages} lang={lang} />;
export default function About() {
return <AboutContent />;
}
+1 -1
View File
@@ -28,7 +28,7 @@ export default async function BlogPage({
{/* Articles List */}
<div className="space-y-12">
{posts.map((post) => (
<ArticleListItem key={post.slug} post={post} lang={lang} messages={messages} />
<ArticleListItem key={post.slug} post={post} />
))}
</div>
</main>
+7 -7
View File
@@ -57,13 +57,13 @@ export default async function TagPage({
</div>
{/* Articles List */}
<div className="space-y-12">
{posts.length > 0 ? (
posts.map((post) => (
<ArticleListItem key={post.slug} post={post} lang={lang} messages={messages} />
))
) : (
<p>{messages.text.blog.tagEmpty}</p>
<div className="space-y-12">
{posts.length > 0 ? (
posts.map((post) => (
<ArticleListItem key={post.slug} post={post} />
))
) : (
<p>{messages.text.blog.tagEmpty}</p>
)}
</div>
</main>
+1 -1
View File
@@ -58,7 +58,7 @@ export default async function FAQ({
return (
<>
<JsonLd id="faq-ld" data={faqLd} />
<FAQSection messages={messages} />
<FAQSection />
</>
);
}
+3 -8
View File
@@ -32,11 +32,6 @@ export async function generateMetadata({
};
}
export default async function Features({
params: { lang },
}: {
params: { lang: string };
}) {
const messages = await getDictionary(lang);
return <KeyFeatures messages={messages} />;
}
export default function Features() {
return <KeyFeatures />;
}
+5 -6
View File
@@ -1,11 +1,10 @@
import type { Messages } from "@/types/messages";
"use client";
interface HelpContentProps {
messages: Messages;
lang: string;
}
import { useI18n } from "@/components/providers/TranslationProvider";
export default function HelpContent() {
const { messages, lang } = useI18n();
export default function HelpContent({ messages, lang }: HelpContentProps) {
return (
<div className="container mx-auto py-12">
<h1 className="text-4xl font-bold mb-6">{messages.text.help.h1}</h1>
+2 -7
View File
@@ -30,11 +30,6 @@ export async function generateMetadata({
},
};
}
export default async function Help({
params: { lang },
}: {
params: { lang: string };
}) {
const messages = await getDictionary(lang);
return <HelpContent messages={messages} lang={lang} />;
export default function Help() {
return <HelpContent />;
}
+6 -3
View File
@@ -1,6 +1,7 @@
import "./globals.css";
import Header from "@/components/web/Header";
import Footer from "@/components/web/Footer";
import { TranslationProvider } from "@/components/providers/TranslationProvider";
import { ThemeProvider } from "@/components/web/theme-provider";
import { getDictionary } from "@/lib/dictionary";
import JsonLd from "@/components/seo/JsonLd";
@@ -47,9 +48,11 @@ export default async function RootLayout({
disableTransitionOnChange
storageKey="theme-preference"
>
<Header messages={messages} lang={lang} />
<div className="flex-1">{children}</div>
<Footer messages={messages} lang={lang} />
<TranslationProvider messages={messages} lang={lang}>
<Header />
<div className="flex-1">{children}</div>
<Footer />
</TranslationProvider>
</ThemeProvider>
</body>
</html>
+1 -1
View File
@@ -61,7 +61,7 @@ export default async function Home({
return (
<>
<JsonLd id="home-ld" data={webAppLd} />
<HomeClient messages={messages} lang={lang} />
<HomeClient />
</>
);
}
@@ -1,10 +1,10 @@
import type { Messages } from "@/types/messages";
"use client";
interface PageContentProps {
messages: Messages;
}
import { useMessages } from "@/components/providers/TranslationProvider";
export default function PrivacyContent() {
const messages = useMessages();
export default function PrivacyContent({ messages }: PageContentProps) {
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold text-center mb-6">
+2 -7
View File
@@ -30,11 +30,6 @@ export async function generateMetadata({
},
};
}
export default async function Privacy({
params: { lang },
}: {
params: { lang: string };
}) {
const messages = await getDictionary(lang);
return <PrivacyContent messages={messages} />;
export default function Privacy() {
return <PrivacyContent />;
}
+5 -5
View File
@@ -1,10 +1,10 @@
import type { Messages } from "@/types/messages";
"use client";
interface PageContentProps {
messages: Messages;
}
import { useMessages } from "@/components/providers/TranslationProvider";
export default function TermsContent() {
const messages = useMessages();
export default function TermsContent({ messages }: PageContentProps) {
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold text-center mb-6">
+2 -7
View File
@@ -30,11 +30,6 @@ export async function generateMetadata({
},
};
}
export default async function TermsOfUse({
params: { lang },
}: {
params: { lang: string };
}) {
const messages = await getDictionary(lang);
return <TermsContent messages={messages} />;
export default function TermsOfUse() {
return <TermsContent />;
}
+5 -16
View File
@@ -1,5 +1,6 @@
"use client";
import React, { useRef, useCallback, useEffect, useMemo } from "react";
import { useMessages } from "@/components/providers/TranslationProvider";
import { Button } from "@/components/ui/button";
import useRichTextToPlainText from "../hooks/useRichTextToPlainText";
import QRCodeComponent from "./ClipboardApp/ShareCard";
@@ -18,13 +19,14 @@ import { getCachedId } from "@/lib/roomIdCache";
import { useConnectionFeedback } from "@/hooks/useConnectionFeedback";
const ClipboardApp = () => {
const messages = useMessages();
const { shareMessage, retrieveMessage, putMessageInMs } =
useClipboardAppMessages();
const dragCounter = useRef(0);
const retrieveJoinRoomBtnRef = useRef<HTMLButtonElement>(null);
const { messages, isLoadingMessages } = usePageSetup({
usePageSetup({
setRetrieveRoomId: useFileTransferStore.getState().setRetrieveRoomIdInput,
setActiveTab: useFileTransferStore.getState().setActiveTab,
retrieveJoinRoomBtnRef,
@@ -178,20 +180,9 @@ const ClipboardApp = () => {
// Connection feedback observer (Hook)
useConnectionFeedback({ messages, putMessageInMs });
if (isLoadingMessages || !messages) {
return (
<div className="container mx-auto px-4 py-8 w-full md:max-w-4xl">
<div className="min-h-[1000px] w-full bg-gray-200/50 dark:bg-gray-800/50 rounded-lg animate-pulse">
{" "}
Loading Editor...{" "}
</div>
</div>
);
}
return (
<div className="w-full mx-auto px-1 sm:px-1 py-3 sm:py-8 md:max-w-4xl md:container">
<FullScreenDropZone isDragging={isDragging} messages={messages} />
<FullScreenDropZone isDragging={isDragging} />
<div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-2 mb-4">
<Button
variant={activeTab === "send" ? "default" : "outline"}
@@ -225,7 +216,6 @@ const ClipboardApp = () => {
<CardContent className="px-3 sm:px-6">
{activeTab === "send" ? (
<SendTabPanel
messages={messages}
updateShareContent={updateShareContent}
addFilesToSend={addFilesToSend}
removeFileToSend={removeFileToSend}
@@ -240,7 +230,6 @@ const ClipboardApp = () => {
/>
) : (
<RetrieveTabPanel
messages={messages}
putMessageInMs={putMessageInMs}
setRetrieveRoomIdInput={setRetrieveRoomIdInput}
joinRoom={joinRoom}
@@ -257,7 +246,7 @@ const ClipboardApp = () => {
)}
</CardContent>
</Card>
{activeTab === "send" && shareLink && messages && (
{activeTab === "send" && shareLink && (
<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">
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useMessages } from "@/components/providers/TranslationProvider";
import { Button } from "@/components/ui/button";
import Tooltip from "@/components/Tooltip";
import type { Messages } from "@/types/messages";
import { getCachedId, setCachedId } from "@/lib/roomIdCache";
/**
@@ -42,7 +42,6 @@ import { getCachedId, setCachedId } from "@/lib/roomIdCache";
*/
type Props = {
messages: Messages;
getInputValue: () => string;
setInputValue: (val: string) => void;
putMessageInMs: (
@@ -69,7 +68,6 @@ type Props = {
};
export default function CachedIdActionButton({
messages,
getInputValue,
setInputValue,
putMessageInMs,
@@ -82,6 +80,7 @@ export default function CachedIdActionButton({
onUseCached,
disabled = false,
}: Props) {
const messages = useMessages();
const [hasCachedId, setHasCachedId] = useState<boolean>(false);
const [showSaveOverride, setShowSaveOverride] = useState<boolean>(false);
const clickCountRef = useRef(0);
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef } from "react";
import { useMessages } from "@/components/providers/TranslationProvider";
import { Button } from "@/components/ui/button";
import { Download, Trash2 } from "lucide-react";
import { Tooltip } from "@/components/Tooltip";
@@ -7,9 +8,6 @@ import { formatFileSize, generateFileId } from "@/lib/fileUtils";
import { AutoPopupDialog } from "@/components/common/AutoPopupDialog";
import { FileMeta, CustomFile, Progress } from "@/types/webrtc";
import FileTransferButton from "./FileTransferButton";
import { getDictionary } from "@/lib/dictionary";
import { useLocale } from "@/hooks/useLocale";
import type { Messages } from "@/types/messages";
import { useFileTransferStore } from "@/stores/fileTransferStore";
import { supportsAutoDownload } from "@/lib/browserUtils";
import { postLogToBackend } from "@/app/config/api";
@@ -68,8 +66,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
saveType,
largeFileThreshold = 500 * 1024 * 1024, // 500MB default
}) => {
const locale = useLocale();
const [messages, setMessages] = useState<Messages | null>(null);
const messages = useMessages();
// Get the cleaning method of the store
const { clearSendProgress, clearReceiveProgress } = useFileTransferStore();
@@ -111,12 +108,6 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
}
};
useEffect(() => {
getDictionary(locale)
.then((dict) => setMessages(dict))
.catch((error) => console.error("Failed to load messages:", error));
}, [locale]);
useEffect(() => {
// Separate single files and folders
const tempSingleFiles: FileMeta[] = [];
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react";
import { useMessages } from "@/components/providers/TranslationProvider";
import { Button } from "@/components/ui/button";
import { Download } from "lucide-react";
import {
@@ -7,9 +8,6 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { getDictionary } from "@/lib/dictionary";
import { useLocale } from "@/hooks/useLocale";
import type { Messages } from "@/types/messages";
interface FileTransferButtonProps {
onRequest: () => void;
@@ -28,19 +26,13 @@ const FileTransferButton = ({
isSavedToDisk,
isPendingSave = false,
}: FileTransferButtonProps) => {
const locale = useLocale();
const [messages, setMessages] = useState<Messages | null>(null);
const messages = useMessages();
// Button status judgment - 待保存状态时按钮应该可点击
const isDisabled =
isCurrentFileTransferring ||
isSavedToDisk ||
(isOtherFileTransferring && !isPendingSave);
useEffect(() => {
getDictionary(locale)
.then((dict) => setMessages(dict))
.catch((error) => console.error("Failed to load messages:", error));
}, [locale]);
// Display different tooltips based on status
const getTooltipContent = () => {
if (isSavedToDisk)
@@ -87,9 +79,6 @@ const FileTransferButton = ({
};
const buttonStyles = getButtonStyles();
if (messages === null) {
return <div>Loading...</div>;
}
return (
<TooltipProvider delayDuration={100}>
<Tooltip>
@@ -5,6 +5,7 @@ import React, {
useRef,
useCallback,
} from "react";
import { useMessages } from "@/components/providers/TranslationProvider";
import { Input } from "@/components/ui/input";
import { Upload } from "lucide-react";
import { FileMeta, CustomFile } from "@/types/webrtc";
@@ -23,11 +24,6 @@ declare module "@/components/ui/input" {
}
}
import { getDictionary } from "@/lib/dictionary";
import { useLocale } from "@/hooks/useLocale";
import type { Messages } from "@/types/messages";
import { en } from "@/constants/messages/en"; // Import English dictionary as default
function formatFileChosen(
template: string,
fileNum: number,
@@ -45,28 +41,19 @@ interface FileUploadHandlerProps {
const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
onFilePicked,
}) => {
const locale = useLocale();
const [messages, setMessages] = useState<Messages>(en); // Use English dictionary as initial value
const messages = useMessages();
const folderInputRef = useRef<HTMLInputElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
// File selector -- message prompt
const [fileText, setFileText] = useState<string>(
en.text.fileUploadHandler.noFileChosenTip
messages.text.fileUploadHandler.noFileChosenTip
);
const [isModalOpen, setIsModalOpen] = useState(false);
useEffect(() => {
if (locale !== "en") {
// Only load other language packs if not English
getDictionary(locale)
.then((dict) => {
setMessages(dict);
setFileText(dict.text.fileUploadHandler.noFileChosenTip);
})
.catch((error) => console.error("Failed to load messages:", error));
}
}, [locale]);
setFileText(messages.text.fileUploadHandler.noFileChosenTip);
}, [messages.text.fileUploadHandler.noFileChosenTip]);
const handleFileChange = useCallback(
(newFiles: CustomFile[]) => {
@@ -154,9 +141,6 @@ const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
const handleSelectFolder = () => {
folderInputRef.current?.click();
};
if (messages === null) {
return <div>Loading...</div>;
}
return (
<>
<div
@@ -1,16 +1,14 @@
import React from "react";
import { useMessages } from "@/components/providers/TranslationProvider";
import { Upload } from "lucide-react";
import type { Messages } from "@/types/messages";
interface FullScreenDropZoneProps {
isDragging: boolean;
messages: Messages;
}
const FullScreenDropZone: React.FC<FullScreenDropZoneProps> = ({
isDragging,
messages,
}) => {
const FullScreenDropZone: React.FC<FullScreenDropZoneProps> = ({ isDragging }) => {
const messages = useMessages();
if (!isDragging) return null;
return (
@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useState } from "react";
import { useMessages } from "@/components/providers/TranslationProvider";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
@@ -7,13 +8,11 @@ import {
} from "@/components/common/clipboard_btn";
import CachedIdActionButton from "@/components/ClipboardApp/CachedIdActionButton";
import FileListDisplay from "@/components/ClipboardApp/FileListDisplay";
import type { Messages } from "@/types/messages";
import type { FileMeta } from "@/types/webrtc";
import { useFileTransferStore } from "@/stores/fileTransferStore";
interface RetrieveTabPanelProps {
messages: Messages;
putMessageInMs: (
message: string,
isShareEnd?: boolean,
@@ -36,7 +35,6 @@ interface RetrieveTabPanelProps {
}
export function RetrieveTabPanel({
messages,
putMessageInMs,
setRetrieveRoomIdInput,
joinRoom,
@@ -50,6 +48,7 @@ export function RetrieveTabPanel({
retrieveMessage,
handleLeaveRoom,
}: RetrieveTabPanelProps) {
const messages = useMessages();
// Get the status from the store
const {
retrieveRoomStatusText,
@@ -114,7 +113,6 @@ export function RetrieveTabPanel({
/>
{/* Save/Use Cached ID Button placed after Paste button */}
<CachedIdActionButton
messages={messages}
getInputValue={() => retrieveRoomIdInput}
setInputValue={setRetrieveRoomIdInput}
putMessageInMs={putMessageInMs}
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback } from "react";
import { useMessages } from "@/components/providers/TranslationProvider";
import dynamic from "next/dynamic";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
@@ -10,7 +11,6 @@ import {
import { FileUploadHandler } from "@/components/ClipboardApp/FileUploadHandler";
import FileListDisplay from "@/components/ClipboardApp/FileListDisplay";
import AnimatedButton from "@/components/ui/AnimatedButton";
import type { Messages } from "@/types/messages";
import type { CustomFile, FileMeta } from "@/types/webrtc";
import { useFileTransferStore } from "@/stores/fileTransferStore";
@@ -29,7 +29,6 @@ const RichTextEditor = dynamic(
);
interface SendTabPanelProps {
messages: Messages;
updateShareContent: (content: string) => void;
addFilesToSend: (files: CustomFile[]) => void;
removeFileToSend: (meta: FileMeta) => void;
@@ -48,7 +47,6 @@ interface SendTabPanelProps {
}
export function SendTabPanel({
messages,
updateShareContent,
addFilesToSend,
removeFileToSend,
@@ -61,6 +59,7 @@ export function SendTabPanel({
handleLeaveSenderRoom,
putMessageInMs,
}: SendTabPanelProps) {
const messages = useMessages();
// Get the status from the store
const {
shareContent,
@@ -192,7 +191,6 @@ export function SendTabPanel({
</Button>
{/* Save/Use Cached ID Button in between */}
<CachedIdActionButton
messages={messages}
getInputValue={() => inputFieldValue}
setInputValue={setInputFieldValue}
putMessageInMs={putMessageInMs}
+2 -15
View File
@@ -1,13 +1,10 @@
import React, { useRef, useState, useEffect } from "react";
import dynamic from "next/dynamic";
import { useMessages } from "@/components/providers/TranslationProvider";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Copy, Download, Check } from "lucide-react";
import { WriteClipboardButton } from "../common/clipboard_btn";
import { getDictionary } from "@/lib/dictionary";
import { useLocale } from "@/hooks/useLocale";
import type { Messages } from "@/types/messages";
interface ShareCardProps {
RoomID: string;
shareLink: string;
@@ -22,8 +19,7 @@ const QRCodeSVG = dynamic(
}
);
const ShareCard: React.FC<ShareCardProps> = ({ RoomID, shareLink }) => {
const locale = useLocale();
const [messages, setMessages] = useState<Messages | null>(null);
const messages = useMessages();
const qrRef = useRef<HTMLDivElement>(null);
const [isCopied, setIsCopied] = useState<boolean>(false);
@@ -86,12 +82,6 @@ const ShareCard: React.FC<ShareCardProps> = ({ RoomID, shareLink }) => {
downloadQRCode(); // Fallback to download on any error
}
};
useEffect(() => {
getDictionary(locale)
.then((dict) => setMessages(dict))
.catch((error) => console.error("Failed to load messages:", error));
}, [locale]);
const downloadQRCode = () => {
if (!qrRef.current) return;
@@ -116,9 +106,6 @@ const ShareCard: React.FC<ShareCardProps> = ({ RoomID, shareLink }) => {
};
img.src = "data:image/svg+xml;base64," + btoa(svgData);
};
if (messages === null) {
return <div>Loading...</div>;
}
return (
<div className="bg-primary/10 p-2 sm:p-4 rounded-lg border border-primary/20">
<p className="text-primary mb-3 sm:mb-4 text-sm sm:text-base">
+6 -4
View File
@@ -1,15 +1,17 @@
"use client";
import { useI18n } from "@/components/providers/TranslationProvider";
import Link from "next/link";
import Image from "next/image";
import { type BlogPost } from "@/lib/blog";
import { Messages } from "@/types/messages";
interface ArticleListItemProps {
post: BlogPost;
lang: string;
messages: Messages;
}
export function ArticleListItem({ post, lang, messages }: ArticleListItemProps) {
export function ArticleListItem({ post }: ArticleListItemProps) {
const { messages, lang } = useI18n();
return (
<article className="bg-card rounded-xl shadow-lg hover:shadow-xl transition-shadow overflow-hidden">
<div className="relative h-80 w-full">
@@ -0,0 +1,51 @@
"use client";
import { createContext, useContext } from "react";
import type { Messages } from "@/types/messages";
type TranslationContextValue = {
messages: Messages;
lang: string;
};
const TranslationContext = createContext<TranslationContextValue | null>(null);
interface TranslationProviderProps extends TranslationContextValue {
children: React.ReactNode;
}
export function TranslationProvider({
children,
messages,
lang,
}: TranslationProviderProps) {
return (
<TranslationContext.Provider value={{ messages, lang }}>
{children}
</TranslationContext.Provider>
);
}
function useTranslationContext() {
const context = useContext(TranslationContext);
if (!context) {
throw new Error(
"Translation hooks must be used within TranslationProvider"
);
}
return context;
}
export function useMessages() {
return useTranslationContext().messages;
}
export function useLang() {
return useTranslationContext().lang;
}
export function useI18n() {
return useTranslationContext();
}
+4 -3
View File
@@ -1,10 +1,12 @@
"use client";
import { useMessages } from "@/components/providers/TranslationProvider";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import type { Messages } from "@/types/messages";
interface FAQMessage {
[key: string]: string;
@@ -47,7 +49,6 @@ interface FAQSectionProps {
showTitle?: boolean; // Whether to display the title
titleClassName?: string; // Title style class
lang?: string;
messages: Messages;
}
// Control the level and style of the title through props, so it can be used on other pages as well as on a standalone page
export default function FAQSection({
@@ -55,8 +56,8 @@ export default function FAQSection({
className = "",
showTitle = true,
titleClassName = "",
messages,
}: FAQSectionProps) {
const messages = useMessages();
const faqs = generateFAQs(messages);
// Set default styles for different scenarios
+5 -6
View File
@@ -1,14 +1,13 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { Messages } from "@/types/messages";
import { useI18n } from "@/components/providers/TranslationProvider";
import { languageDisplayNames } from "@/constants/i18n-config";
interface FooterProps {
messages: Messages;
lang: string;
}
export function Footer() {
const { messages, lang } = useI18n();
export function Footer({ messages, lang }: FooterProps) {
return (
<footer className="bg-background border-t mt-auto">
<div className="container mx-auto px-4 py-6">
+3 -10
View File
@@ -1,5 +1,6 @@
"use client";
import { useState } from "react";
import { useI18n } from "@/components/providers/TranslationProvider";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
@@ -7,22 +8,14 @@ import { Button } from "@/components/ui/button";
import Image from "next/image";
import { Menu, X, Github } from "lucide-react";
import LanguageSwitcher from "@/components/LanguageSwitcher";
import { Messages } from "@/types/messages";
import ThemeToggle from "@/components/web/ThemeToggle";
/**
* Props interface for the Header component
*/
interface HeaderProps {
messages: Messages;
lang: string;
}
/**
* Header component providing navigation, language switching, and GitHub link
* Features responsive design with mobile menu support
*/
const Header = ({ messages, lang }: HeaderProps) => {
const Header = () => {
const { messages, lang } = useI18n();
const pathname = usePathname();
const [isOpen, setIsOpen] = useState(false);
+14 -12
View File
@@ -1,28 +1,28 @@
"use client";
import React from "react";
import { useMessages } from "@/components/providers/TranslationProvider";
import { Button } from "@/components/ui/button";
import Image from "next/image";
import type { Messages } from "@/types/messages";
interface PageContentProps {
messages: Messages;
}
export default function HowItWorks() {
const messages = useMessages();
export default function HowItWorks({ messages }: PageContentProps) {
const steps = [
{
number: 1,
title: messages!.text.HowItWorks.step1Title,
description: messages!.text.HowItWorks.step1Description,
title: messages.text.HowItWorks.step1Title,
description: messages.text.HowItWorks.step1Description,
},
{
number: 2,
title: messages!.text.HowItWorks.step2Title,
description: messages!.text.HowItWorks.step2Description,
title: messages.text.HowItWorks.step2Title,
description: messages.text.HowItWorks.step2Description,
},
{
number: 3,
title: messages!.text.HowItWorks.step3Title,
description: messages!.text.HowItWorks.step3Description,
title: messages.text.HowItWorks.step3Title,
description: messages.text.HowItWorks.step3Description,
},
];
@@ -33,7 +33,9 @@ export default function HowItWorks({ messages }: PageContentProps) {
<h2 className="text-3xl md:text-4xl font-bold mb-6">
{messages.text.HowItWorks.h2}
</h2>
<p className="text-muted-foreground mb-8">{messages.text.HowItWorks.h2Description}</p>
<p className="text-muted-foreground mb-8">
{messages.text.HowItWorks.h2Description}
</p>
<Button className="bg-gradient-to-r from-purple-500 to-blue-500 hover:from-purple-600 hover:to-blue-600 text-white rounded-full px-8 py-6 text-lg">
{messages.text.HowItWorks.tryNowLabel}
</Button>
+6 -4
View File
@@ -1,21 +1,23 @@
"use client";
import { useMessages } from "@/components/providers/TranslationProvider";
import Image from "next/image";
import type { Messages } from "@/types/messages";
interface KeyFeaturesProps {
isInToolPage?: boolean; // Whether it is in the tool page (e.g. homepage)
className?: string; // Custom style class
showTitle?: boolean; // Whether to display the title
titleClassName?: string; // Title style class
messages: Messages;
}
export default function KeyFeatures({
export default function KeyFeatures({
isInToolPage = false,
className = "",
showTitle = true,
titleClassName = "",
messages
}: KeyFeaturesProps) {
const messages = useMessages();
// Set container styles
const containerClasses = `container mx-auto px-4 py-8 ${className}`;
const defaultTitleClasses = "font-semibold mb-6";
+5 -5
View File
@@ -1,11 +1,11 @@
"use client";
import { useMessages } from "@/components/providers/TranslationProvider";
import Image from "next/image";
import type { Messages } from "@/types/messages";
interface PageContentProps {
messages: Messages;
}
export default function SystemDiagram() {
const messages = useMessages();
export default function SystemDiagram({ messages }: PageContentProps) {
return (
<section className="py-16 bg-background">
<div className="container mx-auto px-4">
+13 -37
View File
@@ -1,7 +1,5 @@
import { useState, useCallback, useEffect } from "react";
import { useLocale } from "@/hooks/useLocale";
import { getDictionary } from "@/lib/dictionary";
import type { Messages } from "@/types/messages";
import { useState, useCallback, useMemo } from "react";
import { useMessages } from "@/components/providers/TranslationProvider";
interface ClipboardMessages {
copiedSuccess?: string;
@@ -22,44 +20,22 @@ interface ClipboardActions {
}
export const useClipboardActions = (): ClipboardActions => {
const locale = useLocale();
const [messages, setMessages] = useState<Messages | null>(null);
const [isLoadingMessages, setIsLoadingMessages] = useState<boolean>(true);
const messages = useMessages();
const [isCopied, setIsCopied] = useState<boolean>(false);
const [isPasted, setIsPasted] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [clipboardMessages, setClipboardMessages] = useState<ClipboardMessages>(
{}
const clipboardMessages = useMemo<ClipboardMessages>(
() => ({
copiedSuccess: messages.text.clipboard_btn.copiedLabel,
pastedSuccess: messages.text.clipboard_btn.pastedLabel,
copyError: "Failed to copy.",
readError: "Failed to read clipboard.",
loading: "Loading...",
}),
[messages]
);
useEffect(() => {
setIsLoadingMessages(true);
getDictionary(locale)
.then((dict) => {
setMessages(dict);
setClipboardMessages({
copiedSuccess: dict.text.clipboard_btn.copiedLabel,
pastedSuccess: dict.text.clipboard_btn.pastedLabel,
copyError: "Failed to copy.",
readError: "Failed to read clipboard.",
loading: "Loading...",
});
setIsLoadingMessages(false);
})
.catch((err) => {
console.error("Failed to load messages for useClipboardActions:", err);
setError("Failed to load messages");
setClipboardMessages({
// Provide fallbacks even on error
copyError: "Failed to copy.",
readError: "Failed to read clipboard.",
loading: "Loading...",
});
setIsLoadingMessages(false);
});
}, [locale]);
const copyText = useCallback(
async (textToCopy: string) => {
setError(null);
@@ -158,7 +134,7 @@ export const useClipboardActions = (): ClipboardActions => {
readClipboard,
isCopied,
isPasted,
isLoadingMessages,
isLoadingMessages: false,
error,
clipboardMessages,
};
+1 -26
View File
@@ -1,8 +1,5 @@
import { useState, useEffect } from "react";
import { getDictionary } from "@/lib/dictionary";
import { useLocale } from "@/hooks/useLocale";
import { useEffect } from "react";
import { trackReferrer } from "@/lib/tracking";
import type { Messages } from "@/types/messages";
interface UsePageSetupProps {
setRetrieveRoomId: (roomId: string) => void;
@@ -15,27 +12,6 @@ export function usePageSetup({
setActiveTab,
retrieveJoinRoomBtnRef,
}: UsePageSetupProps) {
const locale = useLocale();
const [messages, setMessages] = useState<Messages | null>(null);
const [isLoadingMessages, setIsLoadingMessages] = useState(true);
// Load internationalization messages
useEffect(() => {
setIsLoadingMessages(true);
getDictionary(locale)
.then((dict) => {
setMessages(dict);
})
.catch((error) => {
console.error("Failed to load messages:", error);
// Optionally set some default/fallback messages or an error state
setMessages(null); // Or some error indicator
})
.finally(() => {
setIsLoadingMessages(false);
});
}, [locale]);
// Track referrer and handle URL 'roomId' parameter
useEffect(() => {
// Guard in SSR
@@ -56,5 +32,4 @@ export function usePageSetup({
}
}, [setRetrieveRoomId, setActiveTab, retrieveJoinRoomBtnRef]); // Dependencies are stable setters and a ref
return { messages, isLoadingMessages };
}