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 ? (
Loading sources...
+{t("sourceSelector.loadingSources")}
- 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")}