diff --git a/electron/main.ts b/electron/main.ts index d26dbcc1..e33d934e 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -117,57 +117,57 @@ function setupApplicationMenu() { template.push( { - label: "File", + label: "文件", submenu: [ { - label: "Load Project…", + label: "加载项目…", accelerator: "CmdOrCtrl+O", click: () => sendEditorMenuAction("menu-load-project"), }, { - label: "Save Project…", + label: "保存项目…", accelerator: "CmdOrCtrl+S", click: () => sendEditorMenuAction("menu-save-project"), }, { - label: "Save Project As…", + label: "另存项目…", accelerator: "CmdOrCtrl+Shift+S", click: () => sendEditorMenuAction("menu-save-project-as"), }, - ...(isMac ? [] : [{ type: "separator" as const }, { role: "quit" as const }]), + ...(isMac ? [] : [{ type: "separator" as const }, { role: "quit" as const, label: "退出" }]), ], }, { - label: "Edit", + label: "编辑", submenu: [ - { role: "undo" }, - { role: "redo" }, + { role: "undo", label: "撤销" }, + { role: "redo", label: "重做" }, { type: "separator" }, - { role: "cut" }, - { role: "copy" }, - { role: "paste" }, - { role: "selectAll" }, + { role: "cut", label: "剪切" }, + { role: "copy", label: "复制" }, + { role: "paste", label: "粘贴" }, + { role: "selectAll", label: "全选" }, ], }, { - label: "View", + label: "视图", submenu: [ - { role: "reload" }, - { role: "forceReload" }, - { role: "toggleDevTools" }, + { role: "reload", label: "重新加载" }, + { role: "forceReload", label: "强制重新加载" }, + { role: "toggleDevTools", label: "开发者工具" }, { type: "separator" }, - { role: "resetZoom" }, - { role: "zoomIn" }, - { role: "zoomOut" }, + { role: "resetZoom", label: "重置缩放" }, + { role: "zoomIn", label: "放大" }, + { role: "zoomOut", label: "缩小" }, { type: "separator" }, - { role: "togglefullscreen" }, + { role: "togglefullscreen", label: "全屏" }, ], }, { - label: "Window", + label: "窗口", submenu: isMac - ? [{ role: "minimize" }, { role: "zoom" }, { type: "separator" }, { role: "front" }] - : [{ role: "minimize" }, { role: "close" }], + ? [{ role: "minimize", label: "最小化" }, { role: "zoom" }, { type: "separator" }, { role: "front" }] + : [{ role: "minimize", label: "最小化" }, { role: "close", label: "关闭" }], }, ); @@ -192,11 +192,11 @@ function getTrayIcon(filename: string) { function updateTrayMenu(recording: boolean = false) { if (!tray) return; const trayIcon = recording ? recordingTrayIcon : defaultTrayIcon; - const trayToolTip = recording ? `Recording: ${selectedSourceName}` : "OpenScreen"; + const trayToolTip = recording ? `正在录制: ${selectedSourceName}` : "OpenScreen"; const menuTemplate = recording ? [ { - label: "Stop Recording", + label: "停止录制", click: () => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send("stop-recording-from-tray"); @@ -206,7 +206,7 @@ function updateTrayMenu(recording: boolean = false) { ] : [ { - label: "Open", + label: "打开", click: () => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.isMinimized() && mainWindow.restore(); @@ -216,7 +216,7 @@ function updateTrayMenu(recording: boolean = false) { }, }, { - label: "Quit", + label: "退出", click: () => { app.quit(); }, @@ -251,12 +251,12 @@ function createEditorWindowWrapper() { const choice = dialog.showMessageBoxSync(mainWindow!, { type: "warning", - buttons: ["Save & Close", "Discard & Close", "Cancel"], + buttons: ["保存并关闭", "放弃并关闭", "取消"], defaultId: 0, cancelId: 2, - title: "Unsaved Changes", - message: "You have unsaved changes.", - detail: "Do you want to save your project before closing?", + title: "未保存的更改", + message: "您有未保存的更改。", + detail: "关闭前是否要保存项目?", }); if (choice === 0) { diff --git a/package-lock.json b/package-lock.json index 865c801d..0f18ade2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openscreen", - "version": "1.1.3", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openscreen", - "version": "1.1.3", + "version": "1.2.0", "dependencies": { "@fix-webm-duration/fix": "^1.0.1", "@pixi/filter-drop-shadow": "^5.2.0", @@ -31,6 +31,7 @@ "fix-webm-duration": "^1.0.6", "gif.js": "^0.2.0", "gsap": "^3.13.0", + "i18next": "^25.8.18", "lucide-react": "^0.545.0", "mediabunny": "^1.25.1", "motion": "^12.23.24", @@ -38,6 +39,7 @@ "pixi.js": "^8.14.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^16.5.8", "react-icons": "^5.5.0", "react-resizable-panels": "^3.0.6", "react-rnd": "^10.5.2", @@ -339,9 +341,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -8214,6 +8216,15 @@ "dev": true, "license": "ISC" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -8306,6 +8317,37 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/i18next": { + "version": "25.8.18", + "resolved": "https://registry.npmmirror.com/i18next/-/i18next-25.8.18.tgz", + "integrity": "sha512-lzY5X83BiL5AP77+9DydbrqkQHFN9hUzWGjqjLpPcp5ZOzuu1aSoKaU3xbBLSjWx9dAzW431y+d+aogxOZaKRA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/icon-gen": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/icon-gen/-/icon-gen-2.1.0.tgz", @@ -11302,6 +11344,33 @@ "node": ">=6" } }, + "node_modules/react-i18next": { + "version": "16.5.8", + "resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-16.5.8.tgz", + "integrity": "sha512-2ABeHHlakxVY+LSirD+OiERxFL6+zip0PaHo979bgwzeHg27Sqc82xxXWIrSFmfWX0ZkrvXMHwhsi/NGUf5VQg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-icons": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", @@ -13216,7 +13285,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -13374,6 +13443,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", @@ -14163,6 +14241,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json index a3f4e2af..b71259e6 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "fix-webm-duration": "^1.0.6", "gif.js": "^0.2.0", "gsap": "^3.13.0", + "i18next": "^25.8.18", "lucide-react": "^0.545.0", "mediabunny": "^1.25.1", "motion": "^12.23.24", @@ -48,6 +49,7 @@ "pixi.js": "^8.14.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^16.5.8", "react-icons": "^5.5.0", "react-resizable-panels": "^3.0.6", "react-rnd": "^10.5.2", diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index f565d1e1..144bcf91 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { BsRecordCircle } from "react-icons/bs"; import { FaRegStopCircle } from "react-icons/fa"; import { FaFolderOpen } from "react-icons/fa6"; @@ -12,6 +13,7 @@ import { AudioLevelMeter } from "../ui/audio-level-meter"; import styles from "./LaunchWindow.module.css"; export function LaunchWindow() { + const { t } = useTranslation(); const { recording, toggleRecording, @@ -65,7 +67,7 @@ export function LaunchWindow() { const s = (seconds % 60).toString().padStart(2, "0"); return `${m}:${s}`; }; - const [selectedSource, setSelectedSource] = useState("Screen"); + const [selectedSource, setSelectedSource] = useState(t("launch.screen")); const [hasSelectedSource, setHasSelectedSource] = useState(false); useEffect(() => { @@ -76,7 +78,7 @@ export function LaunchWindow() { setSelectedSource(source.name); setHasSelectedSource(true); } else { - setSelectedSource("Screen"); + setSelectedSource(t("launch.screen")); setHasSelectedSource(false); } } @@ -192,7 +194,7 @@ export function LaunchWindow() { className={`${styles.hudIconBtn} ${systemAudioEnabled ? styles.hudIconActive : ""}`} onClick={() => !recording && setSystemAudioEnabled(!systemAudioEnabled)} disabled={recording} - title={systemAudioEnabled ? "Disable system audio" : "Enable system audio"} + title={systemAudioEnabled ? t("launch.disableSystemAudio") : t("launch.enableSystemAudio")} > {systemAudioEnabled ? ( @@ -204,7 +206,7 @@ export function LaunchWindow() { className={`${styles.hudIconBtn} ${microphoneEnabled ? styles.hudIconActive : ""}`} onClick={toggleMicrophone} disabled={recording} - title={microphoneEnabled ? "Disable microphone" : "Enable microphone"} + title={microphoneEnabled ? t("launch.disableMicrophone") : t("launch.enableMicrophone")} > {microphoneEnabled ? ( @@ -241,7 +243,7 @@ export function LaunchWindow() { className={`${styles.hudIconBtn} ${styles.electronNoDrag}`} onClick={openVideoFile} disabled={recording} - title="Open video file" + title={t("launch.openVideoFile")} > @@ -251,17 +253,17 @@ export function LaunchWindow() { className={`${styles.hudIconBtn} ${styles.electronNoDrag}`} onClick={openProjectFile} disabled={recording} - title="Open project" + title={t("launch.openProject")} > {/* Window controls */}
- -
diff --git a/src/components/launch/SourceSelector.tsx b/src/components/launch/SourceSelector.tsx index 43feeafa..357ab23d 100644 --- a/src/components/launch/SourceSelector.tsx +++ b/src/components/launch/SourceSelector.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { MdCheck } from "react-icons/md"; import { Button } from "../ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; @@ -13,6 +14,7 @@ interface DesktopSource { } export function SourceSelector() { + const { t } = useTranslation(); const [sources, setSources] = useState([]); const [selectedSource, setSelectedSource] = useState(null); const [loading, setLoading] = useState(true); @@ -63,7 +65,7 @@ export function SourceSelector() { >
-

Loading sources...

+

{t("sourceSelector.loadingSources")}

); @@ -110,13 +112,13 @@ export function SourceSelector() { value="screens" className="data-[state=active]:bg-white/15 data-[state=active]:text-white text-zinc-400 rounded-full text-xs py-1 transition-all" > - Screens + {t("sourceSelector.screens")} - Windows + {t("sourceSelector.windows")}
@@ -143,14 +145,14 @@ export function SourceSelector() { onClick={() => window.close()} className="px-5 py-1 text-xs text-zinc-400 hover:text-white hover:bg-white/5 rounded-full" > - Cancel + {t("sourceSelector.cancel")}
diff --git a/src/components/video-editor/AddCustomFontDialog.tsx b/src/components/video-editor/AddCustomFontDialog.tsx index b1a321fb..490c4a9c 100644 --- a/src/components/video-editor/AddCustomFontDialog.tsx +++ b/src/components/video-editor/AddCustomFontDialog.tsx @@ -1,5 +1,6 @@ import { Plus } from "lucide-react"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { @@ -25,6 +26,7 @@ interface AddCustomFontDialogProps { } export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) { + const { t } = useTranslation(); const [open, setOpen] = useState(false); const [importUrl, setImportUrl] = useState(""); const [fontName, setFontName] = useState(""); @@ -45,17 +47,17 @@ export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) { const handleAdd = async () => { // Validate inputs if (!importUrl.trim()) { - toast.error("Please enter a Google Fonts import URL"); + toast.error(t("addFont.enterImportUrl")); return; } if (!isValidGoogleFontsUrl(importUrl)) { - toast.error("Please enter a valid Google Fonts URL"); + toast.error(t("addFont.invalidUrl")); return; } if (!fontName.trim()) { - toast.error("Please enter a font name"); + toast.error(t("addFont.enterFontName")); return; } @@ -65,7 +67,7 @@ export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) { // Extract font family from URL const fontFamily = parseFontFamilyFromImport(importUrl); if (!fontFamily) { - toast.error("Could not extract font family from URL"); + toast.error(t("addFont.cantExtractFamily")); setLoading(false); return; } @@ -86,7 +88,7 @@ export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) { onFontAdded(newFont); } - toast.success(`Font "${fontName}" added successfully`); + toast.success(t("addFont.fontAdded", { name: fontName })); // Reset and close setImportUrl(""); @@ -95,10 +97,10 @@ export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) { } catch (error) { console.error("Failed to add custom font:", error); const errorMessage = error instanceof Error ? error.message : "Failed to load font"; - toast.error("Failed to add font", { + toast.error(t("addFont.failedToAddFont"), { description: errorMessage.includes("timeout") - ? "Font took too long to load. Please check the URL and try again." - : "The font could not be loaded. Please verify the Google Fonts URL is correct.", + ? t("addFont.fontTimeout") + : t("addFont.fontLoadError"), }); } finally { setLoading(false); @@ -114,21 +116,21 @@ export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) { className="w-full bg-white/5 border-white/10 text-slate-200 hover:bg-white/10 h-9 text-xs" > - Add Google Font + {t("addFont.addGoogleFont")} - Add Google Font + {t("addFont.addGoogleFont")} - Add a custom font from Google Fonts to use in your annotations. + {t("addFont.addGoogleFontDesc")}

- Get this from Google Fonts: Select a font → Click "Get font" → Copy the @import URL + {t("addFont.googleFontsUrlHint")}

- This is how the font will appear in the font selector + {t("addFont.displayNameHint")}

@@ -164,14 +166,14 @@ export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) { onClick={() => setOpen(false)} className="bg-white/5 border-white/10 text-slate-200 hover:bg-white/10" > - Cancel + {t("addFont.cancel")}
diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index 94f31edb..c7f2a453 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -1,4 +1,5 @@ import Block from "@uiw/react-color-block"; +import { useTranslation } from "react-i18next"; import { AlignCenter, AlignLeft, @@ -63,6 +64,7 @@ export function AnnotationSettingsPanel({ onFigureDataChange, onDelete, }: AnnotationSettingsPanelProps) { + const { t } = useTranslation(); const fileInputRef = useRef(null); const [customFonts, setCustomFonts] = useState([]); @@ -99,8 +101,8 @@ export function AnnotationSettingsPanel({ // Validate file type const validTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"]; if (!validTypes.includes(file.type)) { - toast.error("Invalid file type", { - description: "Please upload a JPG, PNG, GIF, or WebP image file.", + toast.error(t("settingsToast.invalidFileType"), { + description: t("settingsToast.pleaseUploadImage"), }); event.target.value = ""; return; @@ -112,13 +114,13 @@ export function AnnotationSettingsPanel({ const dataUrl = e.target?.result as string; if (dataUrl) { onContentChange(dataUrl); - toast.success("Image uploaded successfully!"); + toast.success(t("settingsToast.imageUploadedSuccess")); } }; reader.onerror = () => { - toast.error("Failed to upload image", { - description: "There was an error reading the file.", + toast.error(t("settingsToast.failedToUploadImage"), { + description: t("settingsToast.errorReadingFile"), }); }; @@ -130,9 +132,9 @@ export function AnnotationSettingsPanel({
- Annotation Settings + {t("annotation.settings")} - Active + {t("annotation.active")}
@@ -148,14 +150,14 @@ export function AnnotationSettingsPanel({ className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-slate-400 py-2 rounded-lg transition-all gap-2" > - Text + {t("annotation.text")} - Image + {t("annotation.image")} - Arrow + {t("annotation.arrow")} {/* Text Content */}
- +