From f20fa23cd3a47de9a2cf5ae5980764c7d139408e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=AF=E9=87=91=E4=B9=89?= Date: Thu, 18 Dec 2025 09:07:23 +0800 Subject: [PATCH 1/5] feat(i18n): add Chinese (Simplified) localization support - Add complete i18n infrastructure with I18nProvider and useI18n hook - Add Chinese (Simplified) translations for all UI elements - Auto-detect browser language preference - Add language switcher in settings panel - Create bilingual README with language toggle - Add GitHub Actions workflow for automated releases Closes: i18n support --- .github/workflows/release.yml | 136 +++++++++++++ README.md | 6 +- README_zh-CN.md | 94 +++++++++ electron/i18n.ts | 61 ++++++ electron/ipc/handlers.ts | 29 +-- electron/main.ts | 9 +- package-lock.json | 6 +- package.json | 2 +- src/components/launch/LaunchWindow.tsx | 18 +- src/components/launch/SourceSelector.tsx | 14 +- src/components/ui/dialog.tsx | 42 ++-- .../video-editor/AnnotationOverlay.tsx | 8 +- .../video-editor/AnnotationSettingsPanel.tsx | 86 +++++---- src/components/video-editor/ExportDialog.tsx | 20 +- .../video-editor/KeyboardShortcutsHelp.tsx | 22 ++- .../video-editor/PlaybackControls.tsx | 4 +- src/components/video-editor/SettingsPanel.tsx | 96 ++++++---- src/components/video-editor/VideoEditor.tsx | 52 +++-- src/components/video-editor/VideoPlayback.tsx | 4 +- src/components/video-editor/timeline/Item.tsx | 10 +- .../video-editor/timeline/KeyframeMarkers.tsx | 4 +- .../video-editor/timeline/TimelineEditor.tsx | 36 ++-- src/hooks/useScreenRecorder.ts | 6 +- src/i18n/index.tsx | 114 +++++++++++ src/i18n/zh-CN.ts | 179 ++++++++++++++++++ src/main.tsx | 23 ++- 26 files changed, 876 insertions(+), 205 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 README_zh-CN.md create mode 100644 electron/i18n.ts create mode 100644 src/i18n/index.tsx create mode 100644 src/i18n/zh-CN.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..65a3cd4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,136 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + build-windows: + runs-on: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies + run: npm ci + + - name: Install app dependencies + run: npx electron-builder install-app-deps + + - name: Build Windows app + run: npm run build:win + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload Windows artifact + uses: actions/upload-artifact@v4 + with: + name: windows-build + path: | + release/*.exe + release/*.exe.blockmap + retention-days: 1 + + build-macos: + runs-on: macos-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: npm ci + + - name: Install app dependencies + run: npx electron-builder install-app-deps + + - name: Build macOS app + run: npm run build:mac + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload macOS artifact + uses: actions/upload-artifact@v4 + with: + name: macos-build + path: | + release/*.dmg + release/*.dmg.blockmap + release/*.zip + retention-days: 1 + + build-linux: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies + run: npm ci + + - name: Install app dependencies + run: npx electron-builder install-app-deps + + - name: Build Linux app + run: npm run build:linux + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload Linux artifact + uses: actions/upload-artifact@v4 + with: + name: linux-build + path: | + release/*.AppImage + release/*.deb + retention-days: 1 + + release: + needs: [build-windows, build-macos, build-linux] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Display structure of downloaded files + run: ls -R artifacts + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + files: | + artifacts/windows-build/* + artifacts/macos-build/* + artifacts/linux-build/* + draft: false + prerelease: false + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 1d754f7..3eca276 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,10 @@ #

OpenScreen

+

+ English | 简体中文 +

+

OpenScreen is your free, open-source alternative to Screen Studio (sort of).

@@ -46,7 +50,7 @@ OpenScreen is 100% free for personal and commercial use. Use it, modify it, dist ## Installation -Download the latest installer for your platform from the [GitHub Releases](https://github.com/siddharthvaddem/openscreen/releases) page. +Download the latest installer for your platform from the [GitHub Releases](https://github.com/fengjinyi98/openscreen/releases) page. ### macOS diff --git a/README_zh-CN.md b/README_zh-CN.md new file mode 100644 index 0000000..d8909b0 --- /dev/null +++ b/README_zh-CN.md @@ -0,0 +1,94 @@ +

+ OpenScreen Logo +
+
+ + Ask DeepWiki + +

+ +#

OpenScreen

+ +

+ English | 简体中文 +

+ +

OpenScreen 是 Screen Studio 的免费开源替代品。

+ +如果你不想为 Screen Studio 每月支付 29 美元,但需要一个简单的工具来制作精美的产品演示和教程视频,这款免费应用就是为你准备的。OpenScreen 并未涵盖 Screen Studio 的所有功能,但已经能够满足基本需求! + +Screen Studio 是一款出色的产品,这绝对不是一个 1:1 的克隆。OpenScreen 是一个更简洁的版本,为那些想要掌控一切且不想付费的用户提供基础功能。如果你需要所有高级功能,最好还是支持 Screen Studio(他们确实做得很棒)。但如果你只是想要一个免费(没有任何附加条件)且开源的工具,这个项目完全可以胜任! + +OpenScreen 对个人和商业用途都是 100% 免费的。你可以使用、修改、分发它。(如果你愿意的话,请给个 star 支持一下!😁) + +**⚠️ 免责声明:这是一个测试版本,可能存在一些 bug(但希望你能有良好的使用体验!)** + +

+

+ OpenScreen 应用预览 + OpenScreen 应用预览 2 + OpenScreen 应用预览 3 + OpenScreen 应用预览 4 + +

+

+ +## 核心功能 +- 录制整个屏幕或特定应用窗口 +- 添加手动缩放(可自定义缩放深度) +- 自由定制缩放的持续时间和位置 +- 裁剪视频录制以隐藏部分内容 +- 选择壁纸、纯色、渐变或自定义图片作为背景 +- 运动模糊效果,使平移和缩放更流畅 +- 添加标注(文字、箭头、图片) +- 剪辑片段 +- 以不同的宽高比和分辨率导出 + +## 安装 + +从 [GitHub Releases](https://github.com/fengjinyi98/openscreen/releases) 页面下载适合你平台的最新安装包。 + +### macOS + +如果你遇到 macOS Gatekeeper 阻止应用运行的问题(因为应用没有开发者证书),可以在安装后运行以下终端命令来绕过: + +```bash +xattr -rd com.apple.quarantine /Applications/Openscreen.app +``` + +运行此命令后,请前往 **系统偏好设置 > 安全性与隐私** 授予"屏幕录制"和"辅助功能"权限。授权完成后即可启动应用。 + +### Linux + +从 releases 页面下载 `.AppImage` 文件。赋予执行权限后运行: + +```bash +chmod +x Openscreen-Linux-*.AppImage +./Openscreen-Linux-*.AppImage +``` + +根据你的桌面环境,可能需要授予屏幕录制权限。 + +### Windows + +从 releases 页面下载 `.exe` 安装程序,双击运行即可安装。 + +## 技术栈 +- Electron +- React +- TypeScript +- Vite +- PixiJS +- dnd-timeline + +--- + +_如果有任何问题,请提交 issue 🙏_ + +## 贡献 + +欢迎贡献!如果你想帮忙或查看当前正在进行的工作,请查看 open issues 和 [项目路线图](https://github.com/users/siddharthvaddem/projects/3) 来了解项目的当前方向并找到贡献的方式。 + +## 许可证 + +本项目采用 [MIT 许可证](./LICENSE)。使用本软件即表示你同意作者不对因使用本软件而产生的任何问题、损害或索赔承担责任。 diff --git a/electron/i18n.ts b/electron/i18n.ts new file mode 100644 index 0000000..3d4ace3 --- /dev/null +++ b/electron/i18n.ts @@ -0,0 +1,61 @@ +import { app } from 'electron' + +export type Language = 'en' | 'zh-CN' + +type InterpolationValues = Record + +function interpolate(template: string, values?: InterpolationValues): string { + if (!values) return template + return template.replace(/{{\s*([a-zA-Z0-9_]+)\s*}}/g, (_match, key: string) => { + const value = values[key] + return value === undefined || value === null ? '' : String(value) + }) +} + +function normalizeLanguage(value: string | null | undefined): Language | null { + if (!value) return null + const lower = value.toLowerCase() + if (lower === 'zh' || lower === 'zh-cn' || lower.startsWith('zh-')) return 'zh-CN' + if (lower === 'en' || lower.startsWith('en-')) return 'en' + return null +} + +export function getAppLanguage(): Language { + try { + return normalizeLanguage(app.getLocale?.()) ?? 'en' + } catch { + return 'en' + } +} + +const zhCN: Record = { + 'Recording: {{source}}': '正在录制:{{source}}', + 'Stop Recording': '停止录制', + 'Open': '打开', + 'Quit': '退出', + + 'Save Exported Video': '保存导出的视频', + 'MP4 Video': 'MP4 视频', + 'Export cancelled': '已取消导出', + 'Video exported successfully': '视频导出成功', + 'Failed to save exported video': '保存导出的视频失败', + + 'Select Video File': '选择视频文件', + 'Video Files': '视频文件', + 'All Files': '所有文件', + 'Failed to open file picker': '打开文件选择器失败', + + 'Video stored successfully': '视频保存成功', + 'Failed to store video': '保存视频失败', + 'No recorded video found': '未找到录制视频', + 'Failed to get video path': '获取视频路径失败', + + 'Screen': '屏幕', +} + +export function tMain(key: string, values?: InterpolationValues): string { + const language = getAppLanguage() + const template = language === 'zh-CN' ? (zhCN[key] ?? key) : key + return interpolate(template, values) +} + diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 34c9886..eb1780d 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -3,6 +3,7 @@ import { ipcMain, desktopCapturer, BrowserWindow, shell, app, dialog } from 'ele import fs from 'node:fs/promises' import path from 'node:path' import { RECORDINGS_DIR } from '../main' +import { tMain } from '../i18n' let selectedSource: any = null @@ -64,13 +65,13 @@ export function registerIpcHandlers( return { success: true, path: videoPath, - message: 'Video stored successfully' + message: tMain('Video stored successfully') } } catch (error) { console.error('Failed to store video:', error) return { success: false, - message: 'Failed to store video', + message: tMain('Failed to store video'), error: String(error) } } @@ -84,7 +85,7 @@ export function registerIpcHandlers( const videoFiles = files.filter(file => file.endsWith('.webm')) if (videoFiles.length === 0) { - return { success: false, message: 'No recorded video found' } + return { success: false, message: tMain('No recorded video found') } } const latestVideo = videoFiles.sort().reverse()[0] @@ -93,12 +94,12 @@ export function registerIpcHandlers( return { success: true, path: videoPath } } catch (error) { console.error('Failed to get video path:', error) - return { success: false, message: 'Failed to get video path', error: String(error) } + return { success: false, message: tMain('Failed to get video path'), error: String(error) } } }) ipcMain.handle('set-recording-state', (_, recording: boolean) => { - const source = selectedSource || { name: 'Screen' } + const source = selectedSource || { name: tMain('Screen') } if (onRecordingStateChange) { onRecordingStateChange(recording, source.name) } @@ -131,10 +132,10 @@ export function registerIpcHandlers( ipcMain.handle('save-exported-video', async (_, videoData: ArrayBuffer, fileName: string) => { try { const result = await dialog.showSaveDialog({ - title: 'Save Exported Video', + title: tMain('Save Exported Video'), defaultPath: path.join(app.getPath('downloads'), fileName), filters: [ - { name: 'MP4 Video', extensions: ['mp4'] } + { name: tMain('MP4 Video'), extensions: ['mp4'] } ], properties: ['createDirectory', 'showOverwriteConfirmation'] }); @@ -143,7 +144,7 @@ export function registerIpcHandlers( return { success: false, cancelled: true, - message: 'Export cancelled' + message: tMain('Export cancelled') }; } await fs.writeFile(result.filePath, Buffer.from(videoData)); @@ -151,13 +152,13 @@ export function registerIpcHandlers( return { success: true, path: result.filePath, - message: 'Video exported successfully' + message: tMain('Video exported successfully') }; } catch (error) { console.error('Failed to save exported video:', error) return { success: false, - message: 'Failed to save exported video', + message: tMain('Failed to save exported video'), error: String(error) } } @@ -166,11 +167,11 @@ export function registerIpcHandlers( ipcMain.handle('open-video-file-picker', async () => { try { const result = await dialog.showOpenDialog({ - title: 'Select Video File', + title: tMain('Select Video File'), defaultPath: RECORDINGS_DIR, filters: [ - { name: 'Video Files', extensions: ['webm', 'mp4', 'mov', 'avi', 'mkv'] }, - { name: 'All Files', extensions: ['*'] } + { name: tMain('Video Files'), extensions: ['webm', 'mp4', 'mov', 'avi', 'mkv'] }, + { name: tMain('All Files'), extensions: ['*'] } ], properties: ['openFile'] }); @@ -187,7 +188,7 @@ export function registerIpcHandlers( console.error('Failed to open file picker:', error); return { success: false, - message: 'Failed to open file picker', + message: tMain('Failed to open file picker'), error: String(error) }; } diff --git a/electron/main.ts b/electron/main.ts index e40e08b..3415b7a 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -4,6 +4,7 @@ import path from 'node:path' import fs from 'node:fs/promises' import { createHudOverlayWindow, createEditorWindow, createSourceSelectorWindow } from './windows' import { registerIpcHandlers } from './ipc/handlers' +import { tMain } from './i18n' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -69,11 +70,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 ? tMain('Recording: {{source}}', { source: selectedSourceName }) : "OpenScreen"; const menuTemplate = recording ? [ { - label: "Stop Recording", + label: tMain("Stop Recording"), click: () => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send("stop-recording-from-tray"); @@ -83,7 +84,7 @@ function updateTrayMenu(recording: boolean = false) { ] : [ { - label: "Open", + label: tMain("Open"), click: () => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.isMinimized() && mainWindow.restore(); @@ -93,7 +94,7 @@ function updateTrayMenu(recording: boolean = false) { }, }, { - label: "Quit", + label: tMain("Quit"), click: () => { app.quit(); }, diff --git a/package-lock.json b/package-lock.json index 1322701..9590bd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openscreen", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openscreen", - "version": "1.0.1", + "version": "1.0.2", "dependencies": { "@fix-webm-duration/fix": "^1.0.1", "@pixi/filter-drop-shadow": "^5.2.0", @@ -51,7 +51,7 @@ "@typescript-eslint/parser": "^7.1.1", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.21", - "electron": "^30.0.1", + "electron": "^30.5.1", "electron-builder": "^24.13.3", "electron-icon-builder": "^2.0.1", "electron-rebuild": "^3.2.9", diff --git a/package.json b/package.json index 7e092b1..e159ab2 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@typescript-eslint/parser": "^7.1.1", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.21", - "electron": "^30.0.1", + "electron": "^30.5.1", "electron-builder": "^24.13.3", "electron-icon-builder": "^2.0.1", "electron-rebuild": "^3.2.9", diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index f82ae6c..def7712 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -9,8 +9,10 @@ import { RxDragHandleDots2 } from "react-icons/rx"; import { FaFolderMinus } from "react-icons/fa6"; import { FiMinus, FiX } from "react-icons/fi"; import { ContentClamp } from "../ui/content-clamp"; +import { useI18n } from "@/i18n"; export function LaunchWindow() { + const { t } = useI18n(); const { recording, toggleRecording } = useScreenRecorder(); const [recordingStart, setRecordingStart] = useState(null); const [elapsed, setElapsed] = useState(0); @@ -39,7 +41,7 @@ export function LaunchWindow() { const s = (seconds % 60).toString().padStart(2, '0'); return `${m}:${s}`; }; - const [selectedSource, setSelectedSource] = useState("Screen"); + const [selectedSource, setSelectedSource] = useState(""); const [hasSelectedSource, setHasSelectedSource] = useState(false); useEffect(() => { @@ -50,7 +52,7 @@ export function LaunchWindow() { setSelectedSource(source.name); setHasSelectedSource(true); } else { - setSelectedSource("Screen"); + setSelectedSource(""); setHasSelectedSource(false); } } @@ -117,7 +119,9 @@ export function LaunchWindow() { disabled={recording} > - {selectedSource} + + {hasSelectedSource ? selectedSource : t('Screen')} +
@@ -137,7 +141,7 @@ export function LaunchWindow() { ) : ( <> - Record + {t('Record')} )} @@ -154,7 +158,7 @@ export function LaunchWindow() { disabled={recording} > - Open + {t('Open')} {/* Separator before hide/close buttons */} @@ -163,7 +167,7 @@ export function LaunchWindow() { variant="link" size="icon" className={`ml-2 ${styles.electronNoDrag} hudOverlayButton`} - title="Hide HUD" + title={t('Hide HUD')} onClick={sendHudOverlayHide} > @@ -174,7 +178,7 @@ export function LaunchWindow() { variant="link" size="icon" className={`ml-1 ${styles.electronNoDrag} hudOverlayButton`} - title="Close App" + title={t('Close App')} onClick={sendHudOverlayClose} > diff --git a/src/components/launch/SourceSelector.tsx b/src/components/launch/SourceSelector.tsx index 61bc2eb..6718e40 100644 --- a/src/components/launch/SourceSelector.tsx +++ b/src/components/launch/SourceSelector.tsx @@ -4,6 +4,7 @@ import { MdCheck } from "react-icons/md"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { Card } from "../ui/card"; import styles from "./SourceSelector.module.css"; +import { useI18n } from "@/i18n"; interface DesktopSource { id: string; @@ -14,6 +15,7 @@ interface DesktopSource { } export function SourceSelector() { + const { t } = useI18n(); const [sources, setSources] = useState([]); const [selectedSource, setSelectedSource] = useState(null); const [loading, setLoading] = useState(true); @@ -61,7 +63,7 @@ export function SourceSelector() {
-

Loading sources...

+

{t('Loading sources...')}

); @@ -72,8 +74,8 @@ export function SourceSelector() {
- Screens - Windows + {t('Screens')} + {t('Windows')}
@@ -134,7 +136,7 @@ export function SourceSelector() { {source.appIcon && ( App icon )} @@ -150,8 +152,8 @@ export function SourceSelector() {
- - + +
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 9dbeaa0..3682e9b 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -3,6 +3,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog" import { X } from "lucide-react" import { cn } from "@/lib/utils" +import { useI18n } from "@/i18n" const Dialog = DialogPrimitive.Root @@ -30,25 +31,28 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - {children} - - - Close - - - -)) +>(({ className, children, ...props }, ref) => { + const { t } = useI18n() + return ( + + + + {children} + + + {t("Close")} + + + + ) +}) DialogContent.displayName = DialogPrimitive.Content.displayName const DialogHeader = ({ diff --git a/src/components/video-editor/AnnotationOverlay.tsx b/src/components/video-editor/AnnotationOverlay.tsx index 0331ca9..cd9b351 100644 --- a/src/components/video-editor/AnnotationOverlay.tsx +++ b/src/components/video-editor/AnnotationOverlay.tsx @@ -3,6 +3,7 @@ import { Rnd } from "react-rnd"; import type { AnnotationRegion } from "./types"; import { cn } from "@/lib/utils"; import { getArrowComponent } from "./ArrowSvgs"; +import { useI18n } from "@/i18n"; interface AnnotationOverlayProps { annotation: AnnotationRegion; @@ -27,6 +28,7 @@ export function AnnotationOverlay({ zIndex, isSelectedBoost, }: AnnotationOverlayProps) { + const { t } = useI18n(); const x = (annotation.position.x / 100) * containerWidth; const y = (annotation.position.y / 100) * containerHeight; const width = (annotation.size.width / 100) * containerWidth; @@ -84,7 +86,7 @@ export function AnnotationOverlay({ return ( Annotation @@ -92,7 +94,7 @@ export function AnnotationOverlay({ } return (
- No image + {t('No image')}
); @@ -100,7 +102,7 @@ export function AnnotationOverlay({ if (!annotation.figureData) { return (
- No arrow data + {t('No arrow data')}
); } diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index 2e2999a..35674f0 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -11,6 +11,7 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { Slider } from "@/components/ui/slider"; import { cn } from "@/lib/utils"; import { getArrowComponent } from "./ArrowSvgs"; +import { useI18n } from "@/i18n"; interface AnnotationSettingsPanelProps { annotation: AnnotationRegion; @@ -42,6 +43,7 @@ export function AnnotationSettingsPanel({ onFigureDataChange, onDelete, }: AnnotationSettingsPanelProps) { + const { t } = useI18n(); const fileInputRef = useRef(null); const colorPalette = [ '#FF0000', // Red @@ -73,8 +75,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('Invalid file type'), { + description: t('Please upload a JPG, PNG, GIF, or WebP image file.'), }); event.target.value = ''; return; @@ -86,13 +88,13 @@ export function AnnotationSettingsPanel({ const dataUrl = e.target?.result as string; if (dataUrl) { onContentChange(dataUrl); - toast.success('Image uploaded successfully!'); + toast.success(t('Image uploaded successfully!')); } }; reader.onerror = () => { - toast.error('Failed to upload image', { - description: 'There was an error reading the file.', + toast.error(t('Failed to upload image'), { + description: t('There was an error reading the file.'), }); }; @@ -104,9 +106,9 @@ export function AnnotationSettingsPanel({
- Annotation Settings + {t('Annotation Settings')} - Active + {t('Active')}
@@ -115,28 +117,28 @@ export function AnnotationSettingsPanel({ - Text + {t('Text')} - Image + {t('Image')} - Arrow + {t('Arrow')} {/* Text Content */}
- +