diff --git a/app/host/_components/ShareOptions.tsx b/app/host/_components/ShareOptions.tsx index b5460c2..18e6912 100644 --- a/app/host/_components/ShareOptions.tsx +++ b/app/host/_components/ShareOptions.tsx @@ -3,19 +3,21 @@ import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; import { Copy, Link as LinkIcon } from "lucide-react"; +import { useTranslations } from "next-intl"; interface ShareOptionsProps { roomId: string; } export function ShareOptions({ roomId }: ShareOptionsProps) { + const t = useTranslations("ShareOptions"); const { toast } = useToast(); function copyRoomId() { navigator.clipboard.writeText(roomId); toast({ - title: "Room code copied!", - description: "Share this code with others to let them join your room." + title: t("code-copied"), + description: t("code-copied-desc") }); } @@ -23,8 +25,8 @@ export function ShareOptions({ roomId }: ShareOptionsProps) { const shareableUrl = `${window.location.origin}/join?room=${roomId}`; navigator.clipboard.writeText(shareableUrl); toast({ - title: "Shareable link copied!", - description: "Share this link with others to let them join your room directly." + title: t("link-copied"), + description: t("link-copied-desc") }); } @@ -32,13 +34,13 @@ export function ShareOptions({ roomId }: ShareOptionsProps) {
- Room Code + {t("room-code")}
- {roomId || "Generating room code..."} + {roomId || t("generating-code")}
@@ -52,13 +54,13 @@ export function ShareOptions({ roomId }: ShareOptionsProps) {
- Shareable Link + {t("shareable-link")}
- {roomId ? `${window.location.origin}/join?room=${roomId}` : "Generating link..."} + {roomId ? `${window.location.origin}/join?room=${roomId}` : t("generating-link")}
); diff --git a/app/host/page.tsx b/app/host/page.tsx index 6deeb20..994f93f 100644 --- a/app/host/page.tsx +++ b/app/host/page.tsx @@ -5,6 +5,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { ToastAction } from "@/components/ui/toast"; import { useToast } from "@/hooks/use-toast"; import { ArrowLeft, Monitor, Users } from "lucide-react"; +import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; import Peer from "peerjs"; @@ -12,6 +13,8 @@ import { useEffect, useState } from "react"; import { ShareOptions } from "./_components/ShareOptions"; export default function HostPage() { + const tc = useTranslations("Common"); + const t = useTranslations("HostPage"); const [roomId, setRoomId] = useState(""); const [peer, setPeer] = useState(null); const [activeStream, setActiveStream] = useState(null); @@ -50,12 +53,12 @@ export default function HostPage() { if (!activeStream) { if (connections.length > 0) { toast({ - title: "New viewer connected", - description: "Click to start sharing your screen.", + title: t("new-viewer"), + description: t("new-viewer-desc"), duration: Infinity, action: ( { try { const stream = await navigator.mediaDevices.getDisplayMedia({ @@ -66,13 +69,13 @@ export default function HostPage() { } catch (err) { console.error("Screen sharing error:", err); toast({ - title: "Screen sharing error", - description: "Failed to start screen sharing. Please try again.", + title: t("share-error"), + description: t("share-error-desc"), variant: "destructive" }); } }}> - Start Sharing + {t("start-sharing")} ) }); @@ -104,8 +107,8 @@ export default function HostPage() { setRoomId(""); toast({ - title: "Session ended", - description: "Your screen sharing session has been terminated." + title: t("session-ended"), + description: t("session-ended-desc") }); router.push("/"); @@ -117,7 +120,7 @@ export default function HostPage() { @@ -125,9 +128,9 @@ export default function HostPage() { - Your Screen Sharing Room + {t("title")} - Share your room code or link with others to let them view your screen. To share audio as well, ensure you're using Chrome or Edge, and select the option to share a tab. + {t("description")} @@ -135,7 +138,7 @@ export default function HostPage() {
- Current Viewers + {t("current-viewers")}
{connections.length}
@@ -143,7 +146,7 @@ export default function HostPage() { {activeStream && (
)} diff --git a/app/join/page.tsx b/app/join/page.tsx index 7a5527b..b653660 100644 --- a/app/join/page.tsx +++ b/app/join/page.tsx @@ -5,11 +5,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Input } from "@/components/ui/input"; import { useToast } from "@/hooks/use-toast"; import { ArrowLeft, Users } from "lucide-react"; +import { useTranslations } from "next-intl"; import Link from "next/link"; import Peer from "peerjs"; import { useEffect, useRef, useState } from "react"; export default function JoinPage() { + const tc = useTranslations("Common"); + const t = useTranslations("JoinPage"); const [roomId, setRoomId] = useState(""); const [isConnecting, setIsConnecting] = useState(false); const [activeStream, setActiveStream] = useState(null); @@ -42,8 +45,8 @@ export default function JoinPage() { function joinRoom(roomIdToJoin: string = roomId) { if (!roomIdToJoin.trim()) { toast({ - title: "Room code required", - description: "Please enter a valid room code.", + title: t("code-required"), + description: t("code-required-desc"), variant: "destructive" }); return; @@ -59,8 +62,8 @@ export default function JoinPage() { connection.on("open", () => { toast({ - title: "Connected!", - description: "Waiting for host to share their screen..." + title: t("connected"), + description: t("connected-desc") }); }); @@ -76,8 +79,8 @@ export default function JoinPage() { setRoomId(""); setActiveStream(null); toast({ - title: "Disconnected", - description: "The session has been ended.", + title: t("disconnected"), + description: t("disconnected-desc"), variant: "destructive" }); }); @@ -87,8 +90,8 @@ export default function JoinPage() { console.error("Peer error:", err); setIsConnecting(false); toast({ - title: "Connection failed", - description: "Could not connect to the room. Please check the room code and try again.", + title: t("connection-failed"), + description: t("connection-failed-desc"), variant: "destructive" }); }); @@ -100,7 +103,7 @@ export default function JoinPage() { @@ -108,16 +111,16 @@ export default function JoinPage() { - Join a Room + {t("title")} - Enter the room code to join and view the shared screen + {t("description")} {!activeStream ? (
- setRoomId(e.target.value)} disabled={isConnecting} /> + setRoomId(e.target.value)} disabled={isConnecting} />
) : ( diff --git a/app/layout.tsx b/app/layout.tsx index 249dc07..e2009c1 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,34 +2,53 @@ import { Clarity } from "@/components/Clarity"; import { Toaster } from "@/components/ui/toaster"; import type { Metadata } from "next"; +import { NextIntlClientProvider } from "next-intl"; +import { getLocale, getMessages, getTranslations } from "next-intl/server"; import { Inter } from "next/font/google"; import Link from "next/link"; import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); -export const metadata = { - title: "Screen Share - Share Your Screen Instantly", - description: "Share your screen instantly with anyone using a simple room code. No downloads or sign-ups required.", - keywords: "screen sharing, webrtc, online screen share, browser screen sharing, free screen sharing" -} satisfies Metadata; +type Props = { + params: Promise<{ locale: string }>; +}; + +export async function generateMetadata({ params }: Props): Promise { + const t = await getTranslations({ locale: (await params).locale, namespace: "Common" }); + + return { + title: t("title"), + description: t("description"), + keywords: t("keywords") + }; +} + +export default async function RootLayout({ children }: { children: React.ReactNode }) { + const locale = await getLocale(); + // Providing all messages to the client + // side is the easiest way to get started + const messages = await getMessages(); + const t = await getTranslations({ locale, namespace: "Common" }); -export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - +
- {children} + {children}
- Built by{" "} - - Hin - - . The source code is available on{" "} - - Github - - . + {t.rich("footer", { + author: (chunks) => ( + + {chunks} + + ), + link: (chunks) => ( + + {chunks} + + ) + })}
diff --git a/app/page.tsx b/app/page.tsx index c701774..f4314d8 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,15 +1,17 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Monitor, Users } from "lucide-react"; +import { useTranslations } from "next-intl"; import Link from "next/link"; export default function Home() { + const t = useTranslations("Home"); return (
-

Share Your Screen Instantly

-

Create a room, share the code, and start presenting to your audience in seconds.

+

{t("header")}

+

{t("description")}

@@ -17,13 +19,13 @@ export default function Home() { - Start Sharing + {t("start-title")} - Create a room and share your screen with others + {t("start-desc")} - + @@ -32,14 +34,14 @@ export default function Home() { - Join a Room + {t("join-title")} - Enter a room code to view someone's screen + {t("join-desc")} diff --git a/i18n/messages/en.json b/i18n/messages/en.json new file mode 100644 index 0000000..7ffa347 --- /dev/null +++ b/i18n/messages/en.json @@ -0,0 +1,59 @@ +{ + "Common": { + "title": "Screen Share - Share Your Screen Instantly", + "description": "Share your screen instantly with anyone using a simple room code. No downloads or sign-ups required.", + "keywords": "screen sharing, webrtc, online screen share, browser screen sharing, free screen sharing", + "footer": "Built by Hin. The source code is available on Github.", + "back-to-home": "Back to Home" + }, + "Home": { + "header": "Share Your Screen Instantly", + "description": "Create a room, share the code, and start presenting to your audience in seconds.", + "start-title": "Start Sharing", + "start-desc": "Create a room and share your screen with others", + "create-room-btn": "Create Room", + "join-title": "Join a Room", + "join-desc": "Enter a room code to view someone's screen", + "join-room-btn": "Join Room" + }, + "ShareOptions": { + "code-copied": "Room code copied!", + "code-copied-desc": "Share this code with others to let them join your room.", + "link-copied": "Shareable link copied!", + "link-copied-desc": "Share this link with others to let them join your room directly.", + "room-code": "Room Code", + "copy-code-btn": "Copy Code", + "generating-code": "Generating room code...", + "shareable-link": "Shareable Link", + "copy-link-btn": "Copy Link", + "generating-link": "Generating link..." + }, + "HostPage": { + "title": "Your Screen Sharing Room", + "description": "Share your room code or link with others to let them view your screen. To share audio as well, ensure you're using Chrome or Edge, and select the option to share a tab.", + "current-viewers": "Current Viewers", + "stop-sharing": "Stop sharing", + "new-viewer": "New viewer connected", + "new-viewer-desc": "Click to start sharing your screen.", + "share-error": "Screen sharing error", + "share-error-desc": "Failed to start screen sharing. Please try again.", + "start-sharing": "Start Sharing", + "session-ended": "Session ended", + "session-ended-desc": "Your screen sharing session has been terminated." + }, + "JoinPage": { + "title": "Join a Room", + "description": "Enter the room code to join and view the shared screen", + "enter-code": "Enter room code", + "join-room": "Join Room", + "connecting": "Connecting...", + "code-required": "Room code required", + "code-required-desc": "Please enter a valid room code.", + "connected": "Connected!", + "connected-desc": "Waiting for host to share their screen...", + "disconnected": "Disconnected", + "disconnected-desc": "The session has been ended.", + "connection-failed": "Connection failed", + "connection-failed-desc": "Could not connect to the room. Please check the room code and try again." + } +} diff --git a/i18n/messages/zh.json b/i18n/messages/zh.json new file mode 100644 index 0000000..e797e9d --- /dev/null +++ b/i18n/messages/zh.json @@ -0,0 +1,59 @@ +{ + "Common": { + "title": "屏幕共享 - 立即共享您的屏幕", + "description": "使用简单的房间代码立即与任何人共享您的屏幕。无需下载或注册。", + "keywords": "屏幕共享, webrtc, 在线屏幕共享, 浏览器屏幕共享, 免费屏幕共享", + "footer": "由Hin构建。源代码可在Github上获得。", + "back-to-home": "返回首页" + }, + "Home": { + "header": "立即共享您的屏幕", + "description": "创建一个房间,分享代码,几秒钟内开始向您的观众展示。", + "start-title": "开始共享", + "start-desc": "创建一个房间并与其他人共享您的屏幕", + "create-room-btn": "创建房间", + "join-title": "加入房间", + "join-desc": "输入房间代码以查看某人的屏幕", + "join-room-btn": "加入房间" + }, + "ShareOptions": { + "code-copied": "房间代码已复制!", + "code-copied-desc": "与他人分享此代码,让他们加入您的房间。", + "link-copied": "可分享链接已复制!", + "link-copied-desc": "与他人分享此链接,让他们直接加入您的房间。", + "room-code": "房间代码", + "copy-code-btn": "复制代码", + "generating-code": "正在生成房间代码...", + "shareable-link": "可分享链接", + "copy-link-btn": "复制链接", + "generating-link": "正在生成链接..." + }, + "HostPage": { + "title": "您的屏幕共享房间", + "description": "与他人分享您的房间代码或链接,让他们查看您的屏幕。要共享音频,请确保您使用的是Chrome或Edge,并选择共享标签页的选项。", + "current-viewers": "当前观众", + "stop-sharing": "停止共享", + "new-viewer": "新观众已连接", + "new-viewer-desc": "点击开始共享您的屏幕。", + "share-error": "屏幕共享错误", + "share-error-desc": "无法启动屏幕共享。请再试一次。", + "start-sharing": "开始共享", + "session-ended": "会话已结束", + "session-ended-desc": "您的屏幕共享会话已终止。" + }, + "JoinPage": { + "title": "加入房间", + "description": "输入房间代码以加入并查看共享屏幕", + "enter-code": "输入房间代码", + "join-room": "加入房间", + "connecting": "连接中...", + "code-required": "需要房间代码", + "code-required-desc": "请输入有效的房间代码。", + "connected": "已连接!", + "connected-desc": "等待主持人共享他们的屏幕...", + "disconnected": "已断开连接", + "disconnected-desc": "会议已结束。", + "connection-failed": "连接失败", + "connection-failed-desc": "无法连接到房间。请检查房间代码并重试。" + } +} diff --git a/i18n/request.ts b/i18n/request.ts new file mode 100644 index 0000000..f58043d --- /dev/null +++ b/i18n/request.ts @@ -0,0 +1,22 @@ +import { getRequestConfig } from "next-intl/server"; +import { headers } from "next/headers"; + +const LOCALES = ["en", "zh"]; + +export default getRequestConfig(async () => { + // Provide a static locale, fetch a user setting, + // read locale from `cookies()`, `headers()`, etc. + // const locale = LOCALES.includes(navigator.language) ? navigator.language : "en"; + // const locale = (await cookies()).get('locale')?.value || "en"; + const locale = + (await headers()) + .get("accept-language") + ?.split(",") + .map((l) => l.replace(/;q=[\.\d]+/, "")) + .find((lang) => LOCALES.includes(lang)) || "en"; + + return { + locale, + messages: (await import(`./messages/${locale}.json`)).default + }; +}); diff --git a/next.config.ts b/next.config.ts index 518254b..1b806ca 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,11 +1,13 @@ import type { NextConfig } from "next"; +import createNextIntlPlugin from "next-intl/plugin"; + +const withNextIntl = createNextIntlPlugin(); const nextConfig: NextConfig = { - output: "export", eslint: { ignoreDuringBuilds: true }, images: { unoptimized: true } }; -export default nextConfig; +export default withNextIntl(nextConfig); diff --git a/package-lock.json b/package-lock.json index 8e5cd46..788b0b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "clsx": "^2.1.1", "lucide-react": "^0.462.0", "next": "15.0.3", + "next-intl": "^3.26.0", "peerjs": "^1.5.2", "react": "19.0.0-rc-66855b96-20241106", "react-dom": "19.0.0-rc-66855b96-20241106", @@ -184,6 +185,56 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz", + "integrity": "sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.3", + "@formatjs/intl-localematcher": "0.5.8", + "tslib": "2" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.3.tgz", + "integrity": "sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.4.tgz", + "integrity": "sha512-Tbvp5a9IWuxUcpWNIW6GlMQYEc4rwNHR259uUFoKWNN1jM9obf9Ul0e+7r7MvFOBNcN+13K7NuKCKqQiAn1QEg==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.2.4", + "@formatjs/icu-skeleton-parser": "1.8.8", + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.8.tgz", + "integrity": "sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.2.4", + "tslib": "2" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz", + "integrity": "sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -3789,6 +3840,18 @@ "node": ">= 0.4" } }, + "node_modules/intl-messageformat": { + "version": "10.7.7", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.7.tgz", + "integrity": "sha512-F134jIoeYMro/3I0h08D0Yt4N9o9pjddU/4IIxMMURqbAtI2wu70X8hvG1V48W49zXHXv3RKSF/po+0fDfsGjA==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.2.4", + "@formatjs/fast-memoize": "2.2.3", + "@formatjs/icu-messageformat-parser": "2.9.4", + "tslib": "2" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -4835,6 +4898,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next": { "version": "15.0.3", "resolved": "https://registry.npmjs.org/next/-/next-15.0.3.tgz", @@ -4889,6 +4961,27 @@ } } }, + "node_modules/next-intl": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.26.0.tgz", + "integrity": "sha512-gkamnHIANQzeW8xpTGRxd0xiOCztQhY8GDp79fgdlw0GioqrjTEfSWLhHkgaAtvHRbuh/ByJdwiEY5eNK9bUSQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.5.4", + "negotiator": "^1.0.0", + "use-intl": "^3.26.0" + }, + "peerDependencies": { + "next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -6699,6 +6792,19 @@ "punycode": "^2.1.0" } }, + "node_modules/use-intl": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.26.0.tgz", + "integrity": "sha512-HGXmpjGlbEv1uFZPfm557LK8p/hv0pKF9UwnrJeHUTxQx6bUGzMgpmPRLCVY3zkr7hfjy4LPwQJfk4Fhnn+dIg==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^2.2.0", + "intl-messageformat": "^10.5.14" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index f4aec4e..c644ae4 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "clsx": "^2.1.1", "lucide-react": "^0.462.0", "next": "15.0.3", + "next-intl": "^3.26.0", "peerjs": "^1.5.2", "react": "19.0.0-rc-66855b96-20241106", "react-dom": "19.0.0-rc-66855b96-20241106",