diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 8e8eaf4327..de2a1d6670 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -13,6 +13,7 @@ import "unfonts.css"; import "./styles/theme.css"; import { CapErrorBoundary } from "./components/CapErrorBoundary"; +import { I18nProvider } from "./i18n"; import { generalSettingsStore } from "./store"; import { initAnonymousUser } from "./utils/analytics"; import { type AppTheme, commands } from "./utils/tauri"; @@ -89,9 +90,11 @@ const queryClient = new QueryClient({ export default function App() { return ( - - - + + + + + ); } diff --git a/apps/desktop/src/i18n.tsx b/apps/desktop/src/i18n.tsx new file mode 100644 index 0000000000..b65417bb95 --- /dev/null +++ b/apps/desktop/src/i18n.tsx @@ -0,0 +1,370 @@ +import { + createContext, + createSignal, + onCleanup, + onMount, + type ParentProps, + useContext, +} from "solid-js"; +import { uiSettingsStore } from "./store"; + +export type Locale = "en" | "zh-CN"; + +const zhCN: Record = { + General: "通用", + Shortcuts: "快捷键", + Recordings: "录制", + Screenshots: "截图", + Integrations: "集成", + License: "许可证", + Experimental: "实验功能", + Feedback: "反馈", + Changelog: "更新日志", + "View previous versions": "查看历史版本", + "Sign Out": "退出登录", + "Sign In": "登录", + "General Settings": "通用设置", + Appearance: "外观", + System: "跟随系统", + Light: "浅色", + Dark: "深色", + "Select theme: {theme}": "选择主题:{theme}", + "Preview of {theme} theme": "{theme}主题预览", + App: "应用", + "Always show dock icon": "始终显示程序坞图标", + "Show Cap in the dock even when there are no windows available to close.": "即使没有可关闭窗口,也在程序坞中显示 Cap。", + "Enable system notifications": "启用系统通知", + "Show system notifications for events like copying to clipboard, saving files, and more. You may need to manually allow Cap access via your system's notification settings.": "显示复制到剪贴板、保存文件等事件的系统通知。你可能需要在系统通知设置中手动允许 Cap 的通知权限。", + Recording: "录制", + "Instant mode max resolution": "极速模式最大分辨率", + "Choose the maximum resolution for Instant Mode recordings.": "选择极速模式录制的最大分辨率。", + "Recording countdown": "录制倒计时", + "Countdown before recording starts": "录制开始前倒计时", + Off: "关闭", + "3 seconds": "3 秒", + "5 seconds": "5 秒", + "10 seconds": "10 秒", + "Main window recording start behaviour": "主窗口录制开始行为", + "The main window recording start behaviour": "主窗口在开始录制时的行为", + Close: "关闭", + Minimise: "最小化", + "Studio recording finish behaviour": "工作室录制结束行为", + "The studio recording finish behaviour": "工作室录制结束时的行为", + "Open editor": "打开编辑器", + "Show in overlay": "在悬浮层中显示", + "After deleting recording behaviour": "删除录制后的行为", + "Should Cap reopen after deleting an in progress recording?": "删除进行中的录制后是否重新打开 Cap?", + "Do Nothing": "不做任何操作", + "Reopen Recording Window": "重新打开录制窗口", + "Delete instant mode recordings after upload": "上传后删除极速模式录制", + "After finishing an instant recording, should Cap will delete it from your device?": "极速录制完成并上传后是否从本地删除?", + "Crash-recoverable recording": "可崩溃恢复的录制", + "Records in fragmented segments that can be recovered if the app crashes or your system loses power. May have slightly higher storage usage during recording.": "录制为分段文件,可在应用崩溃或断电后恢复。录制过程中可能略微增加存储占用。", + "Max capture framerate": "最大采集帧率", + "Maximum framerate for screen capture. Higher values may cause instability on some systems.": "屏幕采集的最大帧率。较高的数值可能导致部分系统不稳定。", + "60 FPS (Recommended)": "60 FPS(推荐)", + "Higher framerates may cause frame drops or increased CPU usage on some systems.": "更高的帧率可能导致掉帧或更高的 CPU 占用。", + "Cap Pro Settings": "Cap Pro 设置", + "Automatically open shareable links": "自动打开分享链接", + "Whether Cap should automatically open instant recordings in your browser": "Cap 是否应自动在浏览器中打开极速录制", + "Default Project Name": "默认项目名称", + "Choose the template to use as the default project and file name.": "选择默认项目和文件名的模板。", + Reset: "重置", + Save: "保存", + "How to customize?": "如何自定义?", + "Use placeholders in your template that will be automatically filled in.": "使用占位符,系统会自动填充。", + "Recording Mode": "录制模式", + '{recording_mode}': "{recording_mode}", + "Studio": "工作室", + Instant: "极速", + Screenshot: "截图", + "Target": "目标", + "Display": "屏幕", + Window: "窗口", + Area: "区域", + "Date & Time": "日期与时间", + "Custom Formats": "自定义格式", + "Click to copy": "点击复制", + "The name of the monitor or the title of the app depending on the recording mode.": "根据录制模式显示显示器名称或应用标题。", + "You can also use a custom format for time. The placeholders are case-sensitive. For 24-hour time, use {moment} or use lower cased hh for 12-hour format.": "你也可以自定义时间格式。占位符区分大小写。24 小时制使用 {moment};12 小时制使用小写的 hh。", + "Excluded Windows": "排除的窗口", + "Choose which windows Cap hides from your recordings.": "选择录制时要隐藏的窗口。", + "Note:": "提示:", + "Only Cap related windows can be excluded on Windows due to technical limitations.": "由于技术限制,Windows 上只能排除与 Cap 相关的窗口。", + "Reset to Default": "恢复默认", + Add: "添加", + "No windows are currently excluded.": "当前没有排除任何窗口。", + "Remove excluded window": "移除排除的窗口", + Unknown: "未知", + "Self host": "自托管", + "Cap Server URL": "Cap 服务器地址", + "This setting should only be changed if you are self hosting your own instance of Cap Web.": "仅在你自托管 Cap Web 时修改此设置。", + Update: "更新", + Language: "语言", + "Choose the language for the app.": "选择应用语言。", + English: "英语", + "Simplified Chinese": "简体中文", + "Settings": "设置", + Back: "返回", + Import: "导入", + "Show all": "全部", + "Manage your recordings and perform actions.": "管理你的录制并执行相关操作。", + "No recordings found": "未找到录制内容", + "Recording thumbnail": "录制缩略图", + "Recording in progress": "录制中", + "Recording failed": "录制失败", + "Open link": "打开链接", + Edit: "编辑", + "The recording failed so this file may have issues in the editor! If your having issues recovering the file please reach out to support!": "录制失败,文件在编辑器中可能存在问题!如果恢复文件时遇到问题,请联系支持。", + "Recording is potentially corrupted": "录制可能已损坏", + Reupload: "重新上传", + "Open recording bundle": "打开录制文件夹", + Delete: "删除", + "Are you sure you want to delete this recording?": "确定要删除这条录制吗?", + "Load more": "加载更多", + "No": "没有", + "No matching": "没有匹配的", + recordings: "录制", + "instant recordings": "极速录制", + "studio recordings": "工作室录制", + "Search recordings": "搜索录制", + "Manage your screenshots and perform actions.": "管理你的截图并执行相关操作。", + "No screenshots found": "未找到截图", + "Search screenshots": "搜索截图", + screenshots: "截图", + "Screenshot thumbnail": "截图缩略图", + "Open folder": "打开文件夹", + "Open in editor": "在编辑器中打开", + "Copy image": "复制图片", + "Are you sure you want to delete this screenshot?": "确定要删除这张截图吗?", + "Start studio recording": "开始工作室录制", + "Start instant recording": "开始极速录制", + "Restart recording": "重新开始录制", + "Stop recording": "停止录制", + "Pause/resume recording": "暂停/继续录制", + "Cycle recording mode": "切换录制模式", + "Open recording picker": "打开录制选择器", + "Record display": "录制屏幕", + "Record window": "录制窗口", + "Record area": "录制区域", + "Configure system-wide keyboard shortcuts to control Cap": "配置全局快捷键以控制 Cap", + "Set hotkeys...": "设置快捷键…", + None: "无", + "Experimental Features": "实验功能", + "These features are still in development and may not work as expected.": "这些功能仍在开发中,可能无法按预期工作。", + "Recording Features": "录制功能", + "Custom cursor capture in Studio Mode": "工作室模式自定义光标捕获", + "Studio Mode recordings will capture cursor state separately for customisation (size, smoothing) in the editor. Currently experimental as cursor events may not be captured accurately.": "工作室模式会单独记录光标状态,以便在编辑器中自定义(大小、平滑度)。目前仍为实验功能,可能无法准确捕获光标事件。", + "Native camera preview": "原生摄像头预览", + "Show the camera preview using a native GPU surface instead of rendering it within the webview. This is not functional on certain Windows systems so your mileage may vary.": "使用原生 GPU 表面显示摄像头预览,而不是在 WebView 内渲染。在某些 Windows 系统上可能不可用。", + "Auto zoom on clicks": "点击自动缩放", + "Automatically generate zoom segments around mouse clicks during Studio Mode recordings. This helps highlight important interactions in your recordings.": "在工作室模式录制时自动生成点击位置的缩放片段,用于突出重要交互。", + "Logs uploaded successfully": "日志上传成功", + "Failed to upload logs": "日志上传失败", + "Send Feedback": "提交反馈", + "Help us improve Cap by submitting feedback or reporting bugs. We'll get right on it.": "提交反馈或报告问题,帮助我们改进 Cap。我们会尽快处理。", + "Tell us what you think about Cap...": "告诉我们你对 Cap 的看法…", + "Thank you for your feedback!": "感谢你的反馈!", + "Submitting...": "正在提交…", + "Submit Feedback": "提交反馈", + "Join the Community": "加入社区", + "Have questions, want to share ideas, or just hang out? Join the Cap Discord community.": "有问题、想分享想法,或只是想聊聊?加入 Cap Discord 社区。", + "Join Discord": "加入 Discord", + "Debug Information": "调试信息", + "Upload your logs to help us diagnose issues with Cap. No personal information is included.": "上传日志以帮助我们诊断 Cap 的问题,不包含个人信息。", + "Uploading...": "正在上传…", + "Upload Logs": "上传日志", + "System Information": "系统信息", + "Loading system information...": "正在加载系统信息…", + "Operating System": "操作系统", + "Capture Support": "采集支持", + "Screen Capture": "屏幕采集", + Supported: "支持", + "Not Supported": "不支持", + "Available Encoders": "可用编码器", + New: "新", + "Version {version}": "版本 {version}", + "S3 Config": "S3 配置", + "Connect your own S3 bucket for complete control over your data storage. All new shareable link uploads will be automatically uploaded to your configured S3 bucket, ensuring you maintain complete ownership and control over your content. Perfect for organizations requiring data sovereignty and custom storage policies.": "连接你自己的 S3 存储桶,以完全掌控数据存储。所有新的可分享链接上传将自动上传到你配置的 S3 存储桶,确保你对内容拥有完整的所有权和控制权。适合需要数据主权和自定义存储策略的组织。", + "Configure integrations to extend Cap's functionality and connect with third-party services.": "配置集成以扩展 Cap 功能并连接第三方服务。", + "Upgrade to Pro": "升级到 Pro", + Configure: "配置", + "S3 configuration saved successfully": "S3 配置已保存", + "S3 configuration deleted successfully": "S3 配置已删除", + "S3 connection test failed. Check your config and network connection.": "S3 连接测试失败,请检查配置和网络连接。", + "Connection test timed out after 5 seconds. Please check your endpoint URL and network connection.": "连接测试在 5 秒后超时,请检查 Endpoint 地址和网络连接。", + "S3 configuration test successful! Connection is working.": "S3 配置测试成功,连接正常。", + "It should take under 10 minutes to set up and connect your storage bucket to Cap. View the": "设置并连接你的存储桶到 Cap 通常不超过 10 分钟。查看", + "Storage Config Guide": "存储配置指南", + "to get started.": "开始使用。", + "Storage Provider": "存储服务商", + "Other S3-Compatible": "其他兼容 S3 的服务", + "Access Key ID": "Access Key ID", + "Secret Access Key": "Secret Access Key", + Endpoint: "Endpoint", + "Bucket Name": "Bucket 名称", + Region: "区域", + "Removing...": "正在移除…", + "Remove Config": "移除配置", + "Testing...": "正在测试…", + "Test Connection": "测试连接", + "Saving...": "正在保存…", + "Cap Pro License": "Cap Pro 许可证", + "Your account is upgraded to": "你的账户已升级为", + "and already includes a commercial license.": "并已包含商业许可证。", + "Commercial License": "商业许可证", + "License Key": "许可证密钥", + Expires: "到期", + "Deactivate License": "停用许可证", + "Have a license key?": "已有许可证密钥?", + "License key": "许可证密钥", + "Activating...": "正在激活…", + "Activate License": "激活许可证", + "For commercial use": "用于商业用途", + "billed annually": "按年计费", + "one-time payment": "一次性付款", + "Switch to": "切换为", + lifetime: "终身", + yearly: "年度", + "Loading...": "加载中…", + "Purchase License": "购买许可证", + "Commercial Use of Cap Recorder + Editor": "Cap 录制器和编辑器的商业使用", + "Community Support": "社区支持", + "Local-only features": "仅本地功能", + "Perpetual license option": "永久许可证选项", + "No Camera": "无摄像头", + "Show camera preview": "显示摄像头预览", + "No Microphone": "无麦克风", + "System audio capture requires macOS 13.0 or later": "系统音频采集需要 macOS 13.0 或更高版本", + "Record System Audio": "录制系统音频", + "No System Audio": "不录制系统音频", + On: "开", + "Recording Modes": "录制模式", + "Share instantly with a link. Your recording uploads as you record, so you can share it immediately when you're done.": "录制时即时上传并生成链接,完成后即可立即分享。", + "Record locally in the highest quality for editing later. Perfect for creating polished content with effects and transitions.": "本地高质量录制,便于后期编辑,适合制作带特效和转场的精致内容。", + "Capture and annotate screenshots instantly. Great for quick captures, bug reports, and visual communication.": "即时截取并标注截图,适合快速捕捉、问题报告与视觉沟通。", + "Studio Mode": "工作室模式", + "Instant Mode": "极速模式", + "Screenshot copied to clipboard": "截图已复制到剪贴板", + "Failed to copy screenshot": "截图复制失败", + "Screenshot saved": "截图已保存", + "Failed to save screenshot": "截图保存失败", + "{type} preview for {label}": "{label} 的{type}预览", + "{label} icon": "{label} 图标", + "Upload failed": "上传失败", + "Copy to clipboard": "复制到剪贴板", + "Save as...": "另存为…", + "Retry upload": "重试上传", + "Request Permission": "请求权限", + "Go Back": "返回", + "Unable to check for updates.": "无法检查更新。", + "Please download the latest version manually from cap.so/download. Your data will not be lost.": "请手动从 cap.so/download 下载最新版本。你的数据不会丢失。", + "If this issue persists, please contact support.": "如果问题仍然存在,请联系支持。", + "No update available": "没有可用更新", + "Failed to download or install the update.": "下载或安装更新失败。", + "Update has been installed. Restart Cap to finish updating.": "更新已安装,请重启 Cap 完成更新。", + "Restart Now": "立即重启", + "Installing Update": "正在安装更新", + "No recordings yet": "还没有录制内容", + "Your screen recordings will appear here. Start recording to get started!": "你的屏幕录制会显示在这里。开始录制即可。", + "View All Recordings": "查看所有录制", + "No screenshots yet": "还没有截图", + "Your screenshots will appear here. Take a screenshot to get started!": "你的截图会显示在这里。先截图试试吧。", + "View All Screenshots": "查看所有截图", + "No displays found": "未找到屏幕", + "No windows found": "未找到窗口", + "Search displays": "搜索屏幕", + "Search windows": "搜索窗口", + "No matching displays": "没有匹配的屏幕", + "No matching windows": "没有匹配的窗口", + "No matching recordings": "没有匹配的录制", + "No matching screenshots": "没有匹配的截图", + "Import Error": "导入错误", + "Failed to import video: {error}": "导入视频失败:{error}", + "Video Files": "视频文件", + "Update Error": "更新错误", + "Unable to check for updates. Please download the latest version manually from cap.so/download. Your data will not be lost.\n\nIf this issue persists, please contact support.": "无法检查更新。请手动从 cap.so/download 下载最新版本。你的数据不会丢失。\n\n如果问题仍然存在,请联系支持。", + "Version {version} of Cap is available, would you like to install it?": "发现 Cap 新版本 {version},是否安装?", + "Update Cap": "更新 Cap", + Ignore: "忽略", + "Are you sure you want to change the server URL to '{origin}'? You will need to sign in again.": "确定要把服务器地址改为“{origin}”吗?你需要重新登录。", + Commercial: "商业", + Pro: "专业", + Personal: "个人", + "Signing In...": "正在登录…", + "Cancel Sign In": "取消登录", + "Failed to load recordings": "加载录制失败", + "Failed to load screenshots": "加载截图失败", + "Unable to load displays. Try using the Display button.": "无法加载屏幕,请尝试使用“屏幕”按钮。", + "Unable to load windows. Try using the Window button.": "无法加载窗口,请尝试使用“窗口”按钮。", + "Choose display": "选择屏幕", + "Choose window": "选择窗口", +}; + +const messages: Record> = { + en: {}, + "zh-CN": zhCN, +}; + +const isLocale = (value: unknown): value is Locale => + value === "en" || value === "zh-CN"; + +type I18nContextValue = { + locale: () => Locale; + setLocale: (next: Locale) => void; + t: (key: string, vars?: Record) => string; +}; + +const I18nContext = createContext(); + +const applyVars = ( + text: string, + vars?: Record, +) => { + if (!vars) return text; + return text.replace(/\{(\w+)\}/g, (_, key) => + key in vars ? String(vars[key]) : "", + ); +}; + +export function I18nProvider(props: ParentProps) { + const [locale, setLocaleSignal] = createSignal("en"); + + onMount(() => { + uiSettingsStore.get().then((data) => { + if (isLocale(data?.language)) setLocaleSignal(data.language); + }); + const cleanup = uiSettingsStore.listen((data) => { + if (isLocale(data?.language)) setLocaleSignal(data.language); + }); + onCleanup(() => cleanup.then((c) => c())); + }); + + const t = (key: string, vars?: Record) => { + const current = locale(); + const translated = messages[current]?.[key] ?? key; + return applyVars(translated, vars); + }; + + const setLocale = (next: Locale) => { + setLocaleSignal(next); + void uiSettingsStore.set({ language: next }); + }; + + return ( + + {props.children} + + ); +} + +export function useI18n() { + const ctx = useContext(I18nContext); + if (!ctx) throw new Error("I18nContext not found"); + return ctx; +} + +export const localeOptions = [ + { value: "en", label: "English" }, + { value: "zh-CN", label: "Simplified Chinese" }, +] as const satisfies { value: Locale; label: string }[]; diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx index 8ffb7d02e8..73e6db1c6e 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx @@ -6,6 +6,7 @@ import { createEffect, createSignal, } from "solid-js"; +import { useI18n } from "~/i18n"; import { trackEvent } from "~/utils/analytics"; import { createCurrentRecordingQuery } from "~/utils/queries"; import { @@ -48,6 +49,7 @@ export function CameraSelectBase(props: { iconClass: string; permissions?: OSPermissionsCheck; }) { + const { t } = useI18n(); const currentRecording = createCurrentRecordingQuery(); const requestPermission = useRequestPermission(); const [cameraWindowOpen, setCameraWindowOpen] = createSignal(false); @@ -116,7 +118,7 @@ export function CameraSelectBase(props: { Promise.all([ CheckMenuItem.new({ - text: NO_CAMERA, + text: t(NO_CAMERA), checked: props.value === null, action: () => onChange(null), }), @@ -138,7 +140,7 @@ export function CameraSelectBase(props: { >

- {props.value?.display_name ?? NO_CAMERA} + {props.value?.display_name ?? t(NO_CAMERA)}

{showHiddenIndicator() && ( @@ -147,7 +149,7 @@ export function CameraSelectBase(props: { onClick={openCameraWindow} onPointerDown={(e) => e.stopPropagation()} class="flex items-center justify-center px-2 py-1 rounded-full bg-gray-6 text-gray-11 hover:bg-gray-7 transition-colors" - title="Show camera preview" + title={t("Show camera preview")} > diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/ChangeLogButton.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/ChangeLogButton.tsx index 132b271d45..8d508885f3 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/ChangeLogButton.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/ChangeLogButton.tsx @@ -4,11 +4,13 @@ import { getCurrentWindow } from "@tauri-apps/api/window"; import { createEffect, createResource } from "solid-js"; import { createStore } from "solid-js/store"; import Tooltip from "~/components/Tooltip"; +import { useI18n } from "~/i18n"; import { commands } from "~/utils/tauri"; import { apiClient } from "~/utils/web-api"; import IconLucideBell from "~icons/lucide/bell"; const ChangelogButton = () => { + const { t } = useI18n(); const [changelogState, setChangelogState] = makePersisted( createStore({ hasUpdate: false, @@ -64,7 +66,7 @@ const ChangelogButton = () => { }); return ( - +
- Recording Modes + + {t("Recording Modes")} +
@@ -105,10 +109,10 @@ export default function ModeInfoPanel(props: ModeInfoPanelProps) { isSelected() ? "text-blue-11" : "text-gray-12", )} > - {option.title} + {t(option.titleKey)}

- {option.description} + {t(option.descriptionKey)}

diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/SystemAudio.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/SystemAudio.tsx index 33488a0a2e..adcebe4ec5 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/SystemAudio.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/SystemAudio.tsx @@ -1,7 +1,7 @@ import { createQuery } from "@tanstack/solid-query"; import type { Component, ComponentProps, JSX } from "solid-js"; import { Dynamic } from "solid-js/web"; - +import { useI18n } from "~/i18n"; import { createCurrentRecordingQuery, isSystemAudioSupported, @@ -31,6 +31,7 @@ export function SystemAudioToggleRoot( icon: JSX.Element; }, ) { + const { t } = useI18n(); const { rawOptions, setOptions } = useRecordingOptions(); const currentRecording = createCurrentRecordingQuery(); const systemAudioSupported = createQuery(() => isSystemAudioSupported); @@ -39,7 +40,7 @@ export function SystemAudioToggleRoot( !!currentRecording.data || systemAudioSupported.data === false; const tooltipMessage = () => { if (systemAudioSupported.data === false) { - return "System audio capture requires macOS 13.0 or later"; + return t("System audio capture requires macOS 13.0 or later"); } return undefined; }; @@ -58,14 +59,14 @@ export function SystemAudioToggleRoot( {props.icon}

{rawOptions.captureSystemAudio - ? "Record System Audio" - : "No System Audio"} + ? t("Record System Audio") + : t("No System Audio")}

- {rawOptions.captureSystemAudio ? "On" : "Off"} + {rawOptions.captureSystemAudio ? t("On") : t("Off")} ); diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/TargetCard.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/TargetCard.tsx index aa662ec9ac..b81afeac3a 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/TargetCard.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/TargetCard.tsx @@ -8,6 +8,7 @@ import type { ComponentProps } from "solid-js"; import { createMemo, createSignal, Show, splitProps } from "solid-js"; import toast from "solid-toast"; import Tooltip from "~/components/Tooltip"; +import { useI18n } from "~/i18n"; import { type CaptureDisplayWithThumbnail, type CaptureWindowWithThumbnail, @@ -82,6 +83,7 @@ export default function TargetCard(props: TargetCardProps) { "disabled", "highlightQuery", ]); + const { t } = useI18n(); const [imageExists, setImageExists] = createSignal(true); const recordingProps = () => { @@ -136,7 +138,7 @@ export default function TargetCard(props: TargetCardProps) { if (target) return target.owner_name; const recording = recordingTarget(); if (recording) { - return recording.mode === "studio" ? "Studio Mode" : "Instant Mode"; + return recording.mode === "studio" ? t("Studio Mode") : t("Instant Mode"); } return undefined; }); @@ -222,10 +224,10 @@ export default function TargetCard(props: TargetCardProps) { if (!screenshot) return; try { await commands.copyScreenshotToClipboard(screenshot.path); - toast.success("Screenshot copied to clipboard"); + toast.success(t("Screenshot copied to clipboard")); } catch (error) { console.error("Failed to copy screenshot:", error); - toast.error("Failed to copy screenshot"); + toast.error(t("Failed to copy screenshot")); } }; @@ -245,10 +247,10 @@ export default function TargetCard(props: TargetCardProps) { }); if (!path) return; await commands.copyFileToPath(screenshot.path, path); - toast.success("Screenshot saved"); + toast.success(t("Screenshot saved")); } catch (error) { console.error("Failed to save screenshot:", error); - toast.error("Failed to save screenshot"); + toast.error(t("Failed to save screenshot")); } }; @@ -279,7 +281,8 @@ export default function TargetCard(props: TargetCardProps) { e.stopPropagation(); const recording = recordingTarget(); if (!recording) return; - if (!(await ask("Are you sure you want to delete this recording?"))) return; + if (!(await ask(t("Are you sure you want to delete this recording?")))) + return; await remove(recording.path, { recursive: true }); recordingProps()?.onRefetch?.(); }; @@ -331,9 +334,10 @@ export default function TargetCard(props: TargetCardProps) { {(src) => ( {`${ {`${label()} @@ -360,7 +364,7 @@ export default function TargetCard(props: TargetCardProps) {
- {recordingFailed() ? "Recording failed" : "Upload failed"} + {recordingFailed() ? t("Recording failed") : t("Upload failed")}
@@ -386,7 +390,7 @@ export default function TargetCard(props: TargetCardProps) {
- +
- +
- +
- +
- +
- +
- +
void; label: string }) { } export default function TargetMenuGrid(props: TargetMenuGridProps) { + const { t } = useI18n(); const items = createMemo(() => props.targets ?? []); const skeletonItems = createMemo(() => Array.from({ length: props.skeletonCount ?? DEFAULT_SKELETON_COUNT }), @@ -178,11 +180,13 @@ export default function TargetMenuGrid(props: TargetMenuGridProps) { return ( } - title="No recordings yet" - description="Your screen recordings will appear here. Start recording to get started!" + title={t("No recordings yet")} + description={t( + "Your screen recordings will appear here. Start recording to get started!", + )} action={ onViewAll - ? { label: "View All Recordings", onClick: onViewAll } + ? { label: t("View All Recordings"), onClick: onViewAll } : undefined } /> @@ -193,11 +197,13 @@ export default function TargetMenuGrid(props: TargetMenuGridProps) { return ( } - title="No screenshots yet" - description="Your screenshots will appear here. Take a screenshot to get started!" + title={t("No screenshots yet")} + description={t( + "Your screenshots will appear here. Take a screenshot to get started!", + )} action={ onViewAll - ? { label: "View All Screenshots", onClick: onViewAll } + ? { label: t("View All Screenshots"), onClick: onViewAll } : undefined } /> @@ -208,8 +214,8 @@ export default function TargetMenuGrid(props: TargetMenuGridProps) {
{props.emptyMessage ?? (props.variant === "display" - ? "No displays found" - : "No windows found")} + ? t("No displays found") + : t("No windows found"))}
); }; @@ -377,7 +383,7 @@ export default function TargetMenuGrid(props: TargetMenuGridProps) { {(onViewAll) => ( )} @@ -422,7 +428,7 @@ export default function TargetMenuGrid(props: TargetMenuGridProps) { {(onViewAll) => ( )} diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/TargetSelectInfoPill.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/TargetSelectInfoPill.tsx index 2f05e6b66d..820dd6e477 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/TargetSelectInfoPill.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/TargetSelectInfoPill.tsx @@ -1,5 +1,6 @@ import type { Component, ComponentProps } from "solid-js"; import { Dynamic } from "solid-js/web"; +import { useI18n } from "~/i18n"; export default function TargetSelectInfoPill(props: { value: T | null; @@ -10,6 +11,7 @@ export default function TargetSelectInfoPill(props: { ComponentProps<"button"> & { variant: "blue" | "red" } >; }) { + const { t } = useI18n(); return ( (props: { }} > {!props.permissionGranted - ? "Request Permission" + ? t("Request Permission") : props.value !== null - ? "On" - : "Off"} + ? t("On") + : t("Off")} ); } diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx index c3496b4c99..0fe6cff494 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx @@ -33,6 +33,7 @@ import { Transition } from "solid-transition-group"; import Mode from "~/components/Mode"; import { RecoveryToast } from "~/components/RecoveryToast"; import Tooltip from "~/components/Tooltip"; +import { useI18n } from "~/i18n"; import { Input } from "~/routes/editor/ui"; import { authStore } from "~/store"; import { createSignInMutation } from "~/utils/auth"; @@ -174,31 +175,32 @@ type SharedTargetMenuProps = { }; function TargetMenuPanel(props: TargetMenuPanelProps & SharedTargetMenuProps) { + const { t } = useI18n(); const [search, setSearch] = createSignal(""); const trimmedSearch = createMemo(() => search().trim()); const normalizedQuery = createMemo(() => trimmedSearch().toLowerCase()); const placeholder = props.variant === "display" - ? "Search displays" + ? t("Search displays") : props.variant === "window" - ? "Search windows" + ? t("Search windows") : props.variant === "recording" - ? "Search recordings" - : "Search screenshots"; + ? t("Search recordings") + : t("Search screenshots"); const noResultsMessage = props.variant === "display" - ? "No matching displays" + ? t("No matching displays") : props.variant === "window" - ? "No matching windows" + ? t("No matching windows") : props.variant === "recording" - ? "No matching recordings" - : "No matching screenshots"; + ? t("No matching recordings") + : t("No matching screenshots"); const handleImport = async () => { const result = await dialog.open({ filters: [ { - name: "Video Files", + name: t("Video Files"), extensions: ["mp4", "mov", "avi", "mkv", "webm", "wmv", "m4v", "flv"], }, ], @@ -213,8 +215,10 @@ function TargetMenuPanel(props: TargetMenuPanelProps & SharedTargetMenuProps) { } catch (e) { console.error("Failed to import video:", e); await dialog.message( - `Failed to import video: ${e instanceof Error ? e.message : String(e)}`, - { title: "Import Error", kind: "error" }, + t("Failed to import video: {error}", { + error: e instanceof Error ? e.message : String(e), + }), + { title: t("Import Error"), kind: "error" }, ); } } @@ -287,7 +291,7 @@ function TargetMenuPanel(props: TargetMenuPanelProps & SharedTargetMenuProps) { focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-9 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-1" > - Back + {t("Back")}
@@ -319,7 +323,7 @@ function TargetMenuPanel(props: TargetMenuPanelProps & SharedTargetMenuProps) { onClick={handleImport} > - Import + {t("Import")}
@@ -392,7 +396,9 @@ export default function () { } let hasChecked = false; -function createUpdateCheck() { +function createUpdateCheck( + t: (key: string, vars?: Record) => string, +) { if (import.meta.env.DEV) return; const navigate = useNavigate(); @@ -410,8 +416,10 @@ function createUpdateCheck() { } catch (e) { console.error("Failed to check for updates:", e); await dialog.message( - "Unable to check for updates. Please download the latest version manually from cap.so/download. Your data will not be lost.\n\nIf this issue persists, please contact support.", - { title: "Update Error", kind: "error" }, + t( + "Unable to check for updates. Please download the latest version manually from cap.so/download. Your data will not be lost.\n\nIf this issue persists, please contact support.", + ), + { title: t("Update Error"), kind: "error" }, ); return; } @@ -421,8 +429,15 @@ function createUpdateCheck() { let shouldUpdate: boolean | undefined; try { shouldUpdate = await dialog.confirm( - `Version ${update.version} of Cap is available, would you like to install it?`, - { title: "Update Cap", okLabel: "Update", cancelLabel: "Ignore" }, + t( + "Version {version} of Cap is available, would you like to install it?", + { version: update.version }, + ), + { + title: t("Update Cap"), + okLabel: t("Update"), + cancelLabel: t("Ignore"), + }, ); } catch (e) { console.error("Failed to show update dialog:", e); @@ -435,6 +450,7 @@ function createUpdateCheck() { } function Page() { + const { t } = useI18n(); const { rawOptions, setOptions } = useRecordingOptions(); const currentRecording = createCurrentRecordingQuery(); const isRecording = () => !!currentRecording.data; @@ -640,12 +656,12 @@ function Page() { const displayErrorMessage = () => { if (!displayTargets.error) return undefined; - return "Unable to load displays. Try using the Display button."; + return t("Unable to load displays. Try using the Display button."); }; const windowErrorMessage = () => { if (!windowTargets.error) return undefined; - return "Unable to load windows. Try using the Window button."; + return t("Unable to load windows. Try using the Window button."); }; const selectDisplayTarget = async (target: CaptureDisplayWithThumbnail) => { @@ -693,7 +709,7 @@ function Page() { setModeInfoMenuOpen(false); }); - createUpdateCheck(); + createUpdateCheck(t); onMount(async () => { const { __CAP__ } = window as typeof window & { @@ -975,7 +991,7 @@ function Page() { Component={IconMdiMonitor} disabled={isRecording()} onClick={() => toggleTargetMode("display")} - name="Display" + name={t("Display")} class="flex-1 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0" />
toggleTargetMode("window")} - name="Window" + name={t("Window")} class="flex-1 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0" />
toggleTargetMode("area")} - name="Area" + name={t("Area")} />
@@ -1086,7 +1102,7 @@ function Page() { data-tauri-drag-region >
- Settings}> + {t("Settings")}}> - Screenshots}> + {t("Screenshots")}}> - Recordings}> + {t("Recordings")}}>
@@ -1258,7 +1274,9 @@ function Page() { targets={recordingsData()} isLoading={recordings.isPending} errorMessage={ - recordings.error ? "Failed to load recordings" : undefined + recordings.error + ? t("Failed to load recordings") + : undefined } onSelect={async (recording) => { if (recording.mode === "studio") { @@ -1308,7 +1326,9 @@ function Page() { targets={screenshotsData()} isLoading={screenshots.isPending} errorMessage={ - screenshots.error ? "Failed to load screenshots" : undefined + screenshots.error + ? t("Failed to load screenshots") + : undefined } onSelect={async (screenshot) => { await commands.showWindow({ diff --git a/apps/desktop/src/routes/(window-chrome)/settings.tsx b/apps/desktop/src/routes/(window-chrome)/settings.tsx index 718124dd05..317aeda7a8 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings.tsx @@ -8,12 +8,14 @@ import { createResource, For, onMount, Show, Suspense } from "solid-js"; import { CapErrorBoundary } from "~/components/CapErrorBoundary"; import { SignInButton } from "~/components/SignInButton"; +import { useI18n } from "~/i18n"; import { authStore } from "~/store"; import { trackEvent } from "~/utils/analytics"; const WINDOW_SIZE = { width: 700, height: 540 } as const; export default function Settings(props: RouteSectionProps) { + const { t } = useI18n(); const auth = authStore.createQuery(); const [version] = createResource(() => getVersion()); @@ -40,47 +42,47 @@ export default function Settings(props: RouteSectionProps) { each={[ { href: "general", - name: "General", + name: t("General"), icon: IconCapSettings, }, { href: "hotkeys", - name: "Shortcuts", + name: t("Shortcuts"), icon: IconCapHotkeys, }, { href: "recordings", - name: "Recordings", + name: t("Recordings"), icon: IconLucideSquarePlay, }, { href: "screenshots", - name: "Screenshots", + name: t("Screenshots"), icon: IconLucideImage, }, { href: "integrations", - name: "Integrations", + name: t("Integrations"), icon: IconLucideUnplug, }, { href: "license", - name: "License", + name: t("License"), icon: IconLucideGift, }, { href: "experimental", - name: "Experimental", + name: t("Experimental"), icon: IconCapSettings, }, { href: "feedback", - name: "Feedback", + name: t("Feedback"), icon: IconLucideMessageSquarePlus, }, { href: "changelog", - name: "Changelog", + name: t("Changelog"), icon: IconLucideBell, }, ].filter(Boolean)} @@ -109,7 +111,7 @@ export default function Settings(props: RouteSectionProps) { class="text-gray-11 hover:text-gray-12 underline transition-colors" onClick={() => shell.open("https://cap.so/download/versions")} > - View previous versions + {t("View previous versions")}
)} @@ -120,10 +122,10 @@ export default function Settings(props: RouteSectionProps) { variant={auth.data ? "gray" : "dark"} class="w-full" > - Sign Out + {t("Sign Out")} ) : ( - Sign In + {t("Sign In")} )}
diff --git a/apps/desktop/src/routes/(window-chrome)/settings/changelog.tsx b/apps/desktop/src/routes/(window-chrome)/settings/changelog.tsx index 958ae70ef3..3057c9aaa3 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/changelog.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/changelog.tsx @@ -4,9 +4,11 @@ import { ErrorBoundary, For, onMount, Show, Suspense } from "solid-js"; import { SolidMarkdown } from "solid-markdown"; import { AbsoluteInsetLoader } from "~/components/Loader"; +import { useI18n } from "~/i18n"; import { apiClient } from "~/utils/web-api"; export default function Page() { + const { t } = useI18n(); console.log("[Changelog] Component mounted"); const changelog = createQuery(() => { @@ -71,7 +73,7 @@ export default function Page() {
- New + {t("New")}
@@ -80,7 +82,7 @@ export default function Page() { {entry.title}
- Version {entry.version} -{" "} + {t("Version {version}", { version: entry.version })} -{" "} {new Date(entry.publishedAt).toLocaleDateString()}
( props.initialStore ?? { uploadIndividualFiles: false, @@ -44,19 +46,22 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) {

- Experimental Features + {t("Experimental Features")}

- These features are still in development and may not work as - expected. + {t( + "These features are still in development and may not work as expected.", + )}

-

Recording Features

+

{t("Recording Features")}

handleChange("custom_cursor_capture2", value) @@ -64,8 +69,10 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { /> {type() !== "windows" && ( handleChange("enableNativeCameraPreview", value) @@ -73,8 +80,10 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { /> )} { handleChange("autoZoomOnClicks", value); diff --git a/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx b/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx index dde7ede637..e3061883ae 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx @@ -5,6 +5,7 @@ import { type as ostype } from "@tauri-apps/plugin-os"; import { createResource, createSignal, For, Show } from "solid-js"; import toast from "solid-toast"; +import { useI18n } from "~/i18n"; import { commands, type SystemDiagnostics } from "~/utils/tauri"; import { apiClient, protectedHeaders } from "~/utils/web-api"; @@ -28,6 +29,7 @@ async function fetchDiagnostics(): Promise { } export default function FeedbackTab() { + const { t } = useI18n(); const [feedback, setFeedback] = createSignal(""); const [uploadingLogs, setUploadingLogs] = createSignal(false); const [diagnostics] = createResource(fetchDiagnostics); @@ -39,9 +41,9 @@ export default function FeedbackTab() { setUploadingLogs(true); try { await commands.uploadLogs(); - toast.success("Logs uploaded successfully"); + toast.success(t("Logs uploaded successfully")); } catch (error) { - toast.error("Failed to upload logs"); + toast.error(t("Failed to upload logs")); console.error("Failed to upload logs:", error); } finally { setUploadingLogs(false); @@ -53,10 +55,13 @@ export default function FeedbackTab() {
-

Send Feedback

+

+ {t("Send Feedback")} +

- Help us improve Cap by submitting feedback or reporting bugs. - We'll get right on it. + {t( + "Help us improve Cap by submitting feedback or reporting bugs. We'll get right on it.", + )}

setFeedback(e.currentTarget.value)} - placeholder="Tell us what you think about Cap..." + placeholder={t("Tell us what you think about Cap...")} required minLength={10} class="p-2 w-full h-32 text-[13px] rounded-md border transition-colors duration-200 resize-none bg-gray-2 placeholder:text-gray-10 border-gray-3 text-primary focus:outline-none focus:ring-1 focus:ring-gray-8 hover:border-gray-6" @@ -85,7 +90,9 @@ export default function FeedbackTab() { )} {submission.result?.success && ( -

Thank you for your feedback!

+

+ {t("Thank you for your feedback!")} +

)}

- Join the Community + {t("Join the Community")}

- Have questions, want to share ideas, or just hang out? Join the - Cap Discord community. + {t( + "Have questions, want to share ideas, or just hang out? Join the Cap Discord community.", + )}

- Debug Information + {t("Debug Information")}

- Upload your logs to help us diagnose issues with Cap. No personal - information is included. + {t( + "Upload your logs to help us diagnose issues with Cap. No personal information is included.", + )}

- System Information + {t("System Information")}

- Loading system information... + {t("Loading system information...")}

} > @@ -167,7 +176,7 @@ export default function FeedbackTab() { {(ver) => (

- Operating System + {t("Operating System")}

{ver().displayName} @@ -177,7 +186,9 @@ export default function FeedbackTab() {

-

Capture Support

+

+ {t("Capture Support")} +

- Screen Capture:{" "} - {captureSupported ? "Supported" : "Not Supported"} + {t("Screen Capture")}:{" "} + {captureSupported + ? t("Supported") + : t("Not Supported")}
@@ -195,7 +208,7 @@ export default function FeedbackTab() { 0}>

- Available Encoders + {t("Available Encoders")}

diff --git a/apps/desktop/src/routes/(window-chrome)/settings/general.tsx b/apps/desktop/src/routes/(window-chrome)/settings/general.tsx index 4ba8d8c8a5..3717c1e9f6 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/general.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/general.tsx @@ -24,6 +24,7 @@ import { createStore, reconcile } from "solid-js/store"; import themePreviewAuto from "~/assets/theme-previews/auto.jpg"; import themePreviewDark from "~/assets/theme-previews/dark.jpg"; import themePreviewLight from "~/assets/theme-previews/light.jpg"; +import { type Locale, localeOptions, useI18n } from "~/i18n"; import { Input } from "~/routes/editor/ui"; import { authStore, generalSettingsStore } from "~/store"; import { @@ -41,8 +42,8 @@ import IconLucidePlus from "~icons/lucide/plus"; import IconLucideX from "~icons/lucide/x"; import { SettingItem, ToggleSettingItem } from "./Setting"; -const getExclusionPrimaryLabel = (entry: WindowExclusion) => - entry.ownerName ?? entry.windowTitle ?? entry.bundleIdentifier ?? "Unknown"; +const getExclusionPrimaryLabel = (entry: WindowExclusion, fallback: string) => + entry.ownerName ?? entry.windowTitle ?? entry.bundleIdentifier ?? fallback; const getExclusionSecondaryLabel = (entry: WindowExclusion) => { if (entry.ownerName && entry.windowTitle) { @@ -128,18 +129,19 @@ function AppearanceSection(props: { currentTheme: AppTheme; onThemeChange: (theme: AppTheme) => void; }) { + const { t } = useI18n(); const options = [ { id: "system", - name: "System", + name: t("System"), }, { id: "light", - name: "Light", + name: t("Light"), }, { id: "dark", - name: "Dark", + name: t("Dark"), }, ] satisfies { id: AppTheme; name: string }[]; @@ -152,14 +154,16 @@ function AppearanceSection(props: { return (
-

General Settings

+

+ {t("General Settings")} +

e.preventDefault()} >
-

Appearance

+

{t("Appearance")}

{(theme) => ( @@ -179,7 +183,9 @@ function AppearanceSection(props: { props.currentTheme !== theme.id, }, )} - aria-label={`Select theme: ${theme.name}`} + aria-label={t("Select theme: {theme}", { + theme: theme.name, + })} >
@@ -188,7 +194,9 @@ function AppearanceSection(props: { class="animate-in fade-in duration-300" draggable={false} src={preview} - alt={`Preview of ${theme.name} theme`} + alt={t("Preview of {theme} theme", { + theme: theme.name, + })} /> )} @@ -213,6 +221,7 @@ function AppearanceSection(props: { } function Inner(props: { initialStore: GeneralSettingsStore | null }) { + const { t, locale, setLocale } = useI18n(); const [settings, setSettings] = createStore( deriveInitialSettings(props.initialStore), ); @@ -245,6 +254,11 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { const ostype: OsType = type(); const excludedWindows = createMemo(() => settings.excludedWindows ?? []); + const languageOptions = () => + localeOptions.map((option) => ({ + text: t(option.label), + value: option.value, + })); const matchesExclusion = ( exclusion: WindowExclusion, @@ -351,7 +365,8 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { | MainWindowRecordingStartBehaviour | PostStudioRecordingBehaviour | PostDeletionBehaviour - | number, + | number + | Locale, >(props: { label: string; description: string; @@ -403,18 +418,31 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { generalSettingsStore.set({ theme: newTheme }); }} /> + + setLocale(value)} + options={languageOptions()} + /> + {ostype === "macos" && ( - + handleChange("hideDockIcon", !v)} /> { if (value) { @@ -445,10 +473,12 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { )} - + handleChange("instantModeMaxResolution", value) @@ -459,98 +489,113 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { }))} /> handleChange("recordingCountdown", value)} options={[ - { text: "Off", value: 0 }, - { text: "3 seconds", value: 3 }, - { text: "5 seconds", value: 5 }, - { text: "10 seconds", value: 10 }, + { text: t("Off"), value: 0 }, + { text: t("3 seconds"), value: 3 }, + { text: t("5 seconds"), value: 5 }, + { text: t("10 seconds"), value: 10 }, ]} /> handleChange("mainWindowRecordingStartBehaviour", value) } options={[ - { text: "Close", value: "close" }, - { text: "Minimise", value: "minimise" }, + { text: t("Close"), value: "close" }, + { text: t("Minimise"), value: "minimise" }, ]} /> handleChange("postStudioRecordingBehaviour", value) } options={[ - { text: "Open editor", value: "openEditor" }, + { text: t("Open editor"), value: "openEditor" }, { - text: "Show in overlay", + text: t("Show in overlay"), value: "showOverlay", }, ]} /> handleChange("postDeletionBehaviour", value)} options={[ - { text: "Do Nothing", value: "doNothing" }, + { text: t("Do Nothing"), value: "doNothing" }, { - text: "Reopen Recording Window", + text: t("Reopen Recording Window"), value: "reopenRecordingWindow", }, ]} /> handleChange("deleteInstantRecordingsAfterUpload", v) } /> handleChange("crashRecoveryRecording", value)} />
handleChange("maxFps", value)} options={MAX_FPS_OPTIONS.map((option) => ({ - text: option.label, + text: + option.value === 60 + ? t("60 FPS (Recommended)") + : option.label, value: option.value, }))} /> {(settings.maxFps ?? 60) > 60 && (

- ⚠️ Higher framerates may cause frame drops or increased CPU usage - on some systems. + ⚠️{" "} + {t( + "Higher framerates may cause frame drops or increased CPU usage on some systems.", + )}

)}
handleChange("disableAutoOpenLinks", !v)} /> @@ -582,7 +627,10 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { if ( !(await confirm( - `Are you sure you want to change the server URL to '${origin}'? You will need to sign in again.`, + t( + "Are you sure you want to change the server URL to '{origin}'? You will need to sign in again.", + { origin }, + ), )) ) return; @@ -616,15 +664,18 @@ function ServerURLSetting(props: { value: string; onChange: (v: string) => void; }) { + const { t } = useI18n(); const [value, setValue] = createWritableMemo(() => props.value); return (
-

Self host

+

{t("Self host")}

props.onChange(value())} > - Update + {t("Update")}
@@ -652,6 +703,7 @@ function DefaultProjectNameCard(props: { value: string | null; onChange: (name: string | null) => Promise; }) { + const { t } = useI18n(); const MOMENT_EXAMPLE_TEMPLATE = "{moment:DDDD, MMMM D, YYYY h:mm A}"; const macos = type() === "macos"; const today = new Date(); @@ -715,7 +767,7 @@ function DefaultProjectNameCard(props: { return (
@@ -787,41 +841,44 @@ function DefaultProjectNameCard(props: { -

How to customize?

+

{t("How to customize?")}

- Use placeholders in your template that will be automatically - filled in. + {t( + "Use placeholders in your template that will be automatically filled in.", + )}

-

Recording Mode

+

{t("Recording Mode")}

- {"{recording_mode}"} → "Studio", "Instant", - or "Screenshot" + {"{recording_mode}"} → {t("Studio")},{" "} + {t("Instant")}, {t("Screenshot")}

- {"{mode}"} → "studio", "instant", or + {"{mode}"} → "studio", "instant",{" "} "screenshot"

-

Target

+

{t("Target")}

- {"{target_kind}"} → "Display", "Window", or - "Area" + {"{target_kind}"} → {t("Display")},{" "} + {t("Window")}, {t("Area")}

- {"{target_name}"} → The name of the monitor - or the title of the app depending on the recording mode. + {"{target_name}"} →{" "} + {t( + "The name of the monitor or the title of the app depending on the recording mode.", + )}

-

Date & Time

+

{t("Date & Time")}

{"{date}"} → {dateString}

@@ -832,12 +889,12 @@ function DefaultProjectNameCard(props: {
-

Custom Formats

+

{t("Custom Formats")}

- You can also use a custom format for time. The placeholders are - case-sensitive. For 24-hour time, use{" "} - {"{moment:HH:mm}"} or use lower cased{" "} - hh for 12-hour format. + {t( + "You can also use a custom format for time. The placeholders are case-sensitive. For 24-hour time, use {moment} or use lower cased hh for 12-hour format.", + { moment: "{moment:HH:mm}" }, + )}

{MOMENT_EXAMPLE_TEMPLATE} →{" "} @@ -861,6 +918,7 @@ function ExcludedWindowsCard(props: { isLoading: boolean; isWindows: boolean; }) { + const { t } = useI18n(); const hasExclusions = () => props.excludedWindows.length > 0; const canAdd = () => !props.isLoading; @@ -921,15 +979,16 @@ function ExcludedWindowsCard(props: {

-

Excluded Windows

+

{t("Excluded Windows")}

- Choose which windows Cap hides from your recordings. + {t("Choose which windows Cap hides from your recordings.")}

- Note: Only Cap - related windows can be excluded on Windows due to technical - limitations. + {t("Note:")}{" "} + {t( + "Only Cap related windows can be excluded on Windows due to technical limitations.", + )}

@@ -943,7 +1002,7 @@ function ExcludedWindowsCard(props: { void props.onReset(); }} > - Reset to Default + {t("Reset to Default")}
@@ -962,7 +1021,7 @@ function ExcludedWindowsCard(props: { when={hasExclusions()} fallback={

- No windows are currently excluded. + {t("No windows are currently excluded.")}

} > @@ -972,7 +1031,7 @@ function ExcludedWindowsCard(props: {
- {getExclusionPrimaryLabel(entry)} + {getExclusionPrimaryLabel(entry, t("Unknown"))} {(label) => ( @@ -986,7 +1045,7 @@ function ExcludedWindowsCard(props: { type="button" class="flex items-center justify-center rounded-full bg-gray-4/70 text-gray-11 transition-colors hover:bg-gray-5 hover:text-gray-12 size-6" onClick={() => void props.onRemove(index())} - aria-label="Remove excluded window" + aria-label={t("Remove excluded window")} > diff --git a/apps/desktop/src/routes/(window-chrome)/settings/hotkeys.tsx b/apps/desktop/src/routes/(window-chrome)/settings/hotkeys.tsx index 6c99b069d4..c9a1a8f8dd 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/hotkeys.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/hotkeys.tsx @@ -11,8 +11,8 @@ import { Switch, } from "solid-js"; import { createStore } from "solid-js/store"; +import { useI18n } from "~/i18n"; import { hotkeysStore } from "~/store"; - import { commands, type Hotkey, @@ -45,6 +45,7 @@ export default function () { const MODIFIER_KEYS = new Set(["Meta", "Shift", "Control", "Alt"]); function Inner(props: { initialStore: HotkeysStore | null }) { + const { t } = useI18n(); const [hotkeys, setHotkeys] = createStore<{ [K in HotkeyAction]?: Hotkey; }>(props.initialStore?.hotkeys ?? {}); @@ -92,9 +93,9 @@ function Inner(props: { initialStore: HotkeysStore | null }) { return (
-

Shortcuts

+

{t("Shortcuts")}

- Configure system-wide keyboard shortcuts to control Cap + {t("Configure system-wide keyboard shortcuts to control Cap")}

@@ -112,7 +113,9 @@ function Inner(props: { initialStore: HotkeysStore | null }) { return ( <>
-

{ACTION_TEXT[item()]}

+

+ {t(ACTION_TEXT[item()] ?? "")} +

@@ -120,7 +123,7 @@ function Inner(props: { initialStore: HotkeysStore | null }) { when={hotkeys[item()]} fallback={

- Set hotkeys... + {t("Set hotkeys...")}

} > @@ -183,7 +186,7 @@ function Inner(props: { initialStore: HotkeysStore | null }) { class="flex items-center text-[11px] uppercase transition-colors hover:bg-gray-6 hover:border-gray-7 cursor-pointer py-3 px-2.5 h-5 bg-gray-4 border border-gray-5 rounded-lg text-gray-11 hover:text-gray-12" > - None + {t("None")}

} > diff --git a/apps/desktop/src/routes/(window-chrome)/settings/integrations/index.tsx b/apps/desktop/src/routes/(window-chrome)/settings/integrations/index.tsx index 491e2752bd..326b6e8a86 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/integrations/index.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/integrations/index.tsx @@ -4,10 +4,12 @@ import { For, onMount } from "solid-js"; import IconLucideDatabase from "~icons/lucide/database"; import "@total-typescript/ts-reset/filter-boolean"; +import { useI18n } from "~/i18n"; import { authStore } from "~/store"; import { commands } from "~/utils/tauri"; export default function AppsTab() { + const { t } = useI18n(); const navigate = useNavigate(); const auth = authStore.createQuery(); @@ -19,9 +21,10 @@ export default function AppsTab() { const apps = [ { - name: "S3 Config", - description: + name: t("S3 Config"), + description: t( "Connect your own S3 bucket for complete control over your data storage. All new shareable link uploads will be automatically uploaded to your configured S3 bucket, ensuring you maintain complete ownership and control over your content. Perfect for organizations requiring data sovereignty and custom storage policies.", + ), icon: IconLucideDatabase, url: "/settings/integrations/s3-config", pro: true, @@ -43,10 +46,11 @@ export default function AppsTab() { return (
-

Integrations

+

{t("Integrations")}

- Configure integrations to extend Cap's functionality and connect with - third-party services. + {t( + "Configure integrations to extend Cap's functionality and connect with third-party services.", + )}

@@ -62,7 +66,7 @@ export default function AppsTab() { variant="primary" onClick={() => handleAppClick(app)} > - {app.pro && !isPro() ? "Upgrade to Pro" : "Configure"} + {app.pro && !isPro() ? t("Upgrade to Pro") : t("Configure")}

{app.description}

diff --git a/apps/desktop/src/routes/(window-chrome)/settings/integrations/s3-config.tsx b/apps/desktop/src/routes/(window-chrome)/settings/integrations/s3-config.tsx index 55cfa69afc..f3e6970b78 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/integrations/s3-config.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/integrations/s3-config.tsx @@ -3,6 +3,7 @@ import { createEventBus } from "@solid-primitives/event-bus"; import { createWritableMemo } from "@solid-primitives/memo"; import { useMutation } from "@tanstack/solid-query"; import { createResource, Suspense } from "solid-js"; +import { useI18n } from "~/i18n"; import { Input } from "~/routes/editor/ui"; import { commands } from "~/utils/tauri"; import { apiClient, protectedHeaders } from "~/utils/web-api"; @@ -26,6 +27,7 @@ const DEFAULT_CONFIG = { }; export default function S3ConfigPage() { + const { t } = useI18n(); const [_s3Config, { refetch }] = createResource(async () => { const response = await apiClient.desktop.getS3Config({ headers: await protectedHeaders(), @@ -50,7 +52,9 @@ export default function S3ConfigPage() { }, onSuccess: async () => { await refetch(); - await commands.globalMessageDialog("S3 configuration saved successfully"); + await commands.globalMessageDialog( + t("S3 configuration saved successfully"), + ); }, })); @@ -67,7 +71,7 @@ export default function S3ConfigPage() { onSuccess: async () => { await refetch(); await commands.globalMessageDialog( - "S3 configuration deleted successfully", + t("S3 configuration deleted successfully"), ); }, })); @@ -88,7 +92,9 @@ export default function S3ConfigPage() { if (response.status !== 200) throw new Error( - `S3 connection test failed. Check your config and network connection.`, + t( + "S3 connection test failed. Check your config and network connection.", + ), ); return response; @@ -98,7 +104,9 @@ export default function S3ConfigPage() { if (error instanceof Error) { if (error.name === "AbortError") throw new Error( - "Connection test timed out after 5 seconds. Please check your endpoint URL and network connection.", + t( + "Connection test timed out after 5 seconds. Please check your endpoint URL and network connection.", + ), ); } @@ -107,7 +115,7 @@ export default function S3ConfigPage() { }, onSuccess: async () => { await commands.globalMessageDialog( - "S3 configuration test successful! Connection is working.", + t("S3 configuration test successful! Connection is working."), ); }, })); @@ -168,23 +176,24 @@ export default function S3ConfigPage() {

- It should take under 10 minutes to set up and connect your - storage bucket to Cap. View the{" "} + {t( + "It should take under 10 minutes to set up and connect your storage bucket to Cap. View the", + )}{" "} - Storage Config Guide + {t("Storage Config Guide")} {" "} - to get started. + {t("to get started.")}

{renderInput( - "Access Key ID", + t("Access Key ID"), "accessKeyId", "PL31OADSQNK", "password", )} {renderInput( - "Secret Access Key", + t("Secret Access Key"), "secretAccessKey", "PL31OADSQNK", "password", )} {renderInput( - "Endpoint", + t("Endpoint"), "endpoint", "https://s3.amazonaws.com", )} - {renderInput("Bucket Name", "bucketName", "my-bucket")} - {renderInput("Region", "region", "us-east-1")} + {renderInput(t("Bucket Name"), "bucketName", "my-bucket")} + {renderInput(t("Region"), "region", "us-east-1")}
); })()} @@ -261,11 +272,11 @@ export default function S3ConfigPage() { variant="destructive" onClick={() => deleteConfig.mutate()} > - {deleteConfig.isPending ? "Removing..." : "Remove Config"} + {deleteConfig.isPending ? t("Removing...") : t("Remove Config")} )}
diff --git a/apps/desktop/src/routes/(window-chrome)/settings/license.tsx b/apps/desktop/src/routes/(window-chrome)/settings/license.tsx index 6709433f14..84bbcd76c8 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/license.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/license.tsx @@ -10,6 +10,7 @@ import { Suspense, Switch, } from "solid-js"; +import { useI18n } from "~/i18n"; import { generalSettingsStore } from "~/store"; import { createLicenseQuery } from "~/utils/queries"; import { createRive } from "~/utils/rive"; @@ -19,6 +20,7 @@ import PricingRive from "../../../assets/rive/pricing.riv"; import { Input } from "../../editor/ui"; export default function Page() { + const { t } = useI18n(); const license = createLicenseQuery(); const queryClient = useQueryClient(); @@ -30,13 +32,13 @@ export default function Page() {

- Cap Pro License + {t("Cap Pro License")}

- Your account is upgraded to{" "} - Cap Pro and - already includes a commercial license. + {t("Your account is upgraded to")}{" "} + Cap Pro{" "} + {t("and already includes a commercial license.")}

@@ -48,12 +50,12 @@ export default function Page() {

- Commercial License + {t("Commercial License")}

 										{license().licenseKey}
@@ -62,7 +64,7 @@ export default function Page() {
 								
 									{(expiry) => (
 										
- +

{new Date(expiry()).toLocaleDateString()}

@@ -80,7 +82,7 @@ export default function Page() { queryClient.refetchQueries({ queryKey: ["bruh"] }); }} > - Deactivate License + {t("Deactivate License")}
@@ -108,6 +110,7 @@ function LicenseKeyActivate(props: { {(generalSettings) => { const [licenseKey, setLicenseKey] = createSignal(""); + const { t } = useI18n(); const activateLicenseKey = createMutation(() => ({ mutationFn: async (vars: { licenseKey: string }) => { @@ -137,9 +140,11 @@ function LicenseKeyActivate(props: { return (
-

Have a license key?

+

+ {t("Have a license key?")} +

setLicenseKey(e.currentTarget.value)} class="w-full bg-gray-3 border-gray-4" @@ -155,8 +160,8 @@ function LicenseKeyActivate(props: { } > {activateLicenseKey.isPending - ? "Activating..." - : "Activate License"} + ? t("Activating...") + : t("Activate License")}
@@ -175,6 +180,7 @@ function LicenseKeyActivate(props: { type CommercialLicenseType = "yearly" | "lifetime"; function CommercialLicensePurchase() { + const { t } = useI18n(); const queryClient = useQueryClient(); const [_type, _setType] = createSignal("yearly"); @@ -231,10 +237,10 @@ function CommercialLicensePurchase() {

- Commercial License + {t("Commercial License")}

- For commercial use + {t("For commercial use")}

@@ -243,7 +249,9 @@ function CommercialLicensePurchase() { .00 /

- {isCommercialAnnual() ? "billed annually" : "one-time payment"} + {isCommercialAnnual() + ? t("billed annually") + : t("one-time payment")}

- Switch to {isCommercialAnnual() ? "lifetime" : "yearly"}:{" "} + {t("Switch to")}{" "} + {isCommercialAnnual() ? t("lifetime") : t("yearly")}:{" "} {isCommercialAnnual() ? "$58" : "$29"} @@ -265,8 +274,8 @@ function CommercialLicensePurchase() { size="lg" > {openCommercialCheckout.isPending - ? "Loading..." - : "Purchase License"} + ? t("Loading...") + : t("Purchase License")}

@@ -274,10 +283,10 @@ function CommercialLicensePurchase() {
    {[ - "Commercial Use of Cap Recorder + Editor", - "Community Support", - "Local-only features", - "Perpetual license option", + t("Commercial Use of Cap Recorder + Editor"), + t("Community Support"), + t("Local-only features"), + t("Perpetual license option"), ].map((feature) => (
  • diff --git a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx index f1813799c2..b9019fa6c1 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx @@ -23,6 +23,7 @@ import { } from "solid-js"; import { createStore, produce } from "solid-js/store"; import CapTooltip from "~/components/Tooltip"; +import { useI18n } from "~/i18n"; import { Input } from "~/routes/editor/ui"; import { trackEvent } from "~/utils/analytics"; import { createTauriEventListener } from "~/utils/createEventListener"; @@ -44,19 +45,19 @@ type Recording = { const Tabs = [ { id: "all", - label: "Show all", + labelKey: "Show all", }, { id: "instant", icon: , - label: "Instant", + labelKey: "Instant", }, { id: "studio", icon: , - label: "Studio", + labelKey: "Studio", }, -] satisfies { id: string; label: string; icon?: JSX.Element }[]; +] satisfies { id: string; labelKey: string; icon?: JSX.Element }[]; const PAGE_SIZE = 20; @@ -98,6 +99,7 @@ const recordingsQuery = queryOptions({ }); export default function Recordings() { + const { t } = useI18n(); const [activeTab, setActiveTab] = createSignal<(typeof Tabs)[number]["id"]>( Tabs[0].id, ); @@ -157,8 +159,12 @@ export default function Recordings() { const emptyMessage = createMemo(() => { const tabLabel = - activeTab() === "all" ? "recordings" : `${activeTab()} recordings`; - const prefix = trimmedSearch() ? "No matching" : "No"; + activeTab() === "all" + ? t("recordings") + : activeTab() === "instant" + ? t("instant recordings") + : t("studio recordings"); + const prefix = trimmedSearch() ? t("No matching") : t("No"); return `${prefix} ${tabLabel}`; }); @@ -187,16 +193,16 @@ export default function Recordings() { return (
    -

    Recordings

    +

    {t("Recordings")}

    - Manage your recordings and perform actions. + {t("Manage your recordings and perform actions.")}

    0} fallback={

    - No recordings found + {t("No recordings found")}

    } > @@ -214,7 +220,7 @@ export default function Recordings() { onClick={() => setActiveTab(tab.id)} > {tab.icon && tab.icon} -

    {tab.label}

    +

    {t(tab.labelKey)}

    )} @@ -232,12 +238,12 @@ export default function Recordings() { setSearch(""); } }} - placeholder="Search recordings" + placeholder={t("Search recordings")} autoCapitalize="off" autocorrect="off" autocomplete="off" spellcheck={false} - aria-label="Search recordings" + aria-label={t("Search recordings")} />
@@ -281,7 +287,7 @@ export default function Recordings() { ) } > - Load more + {t("Load more")}
@@ -299,10 +305,14 @@ function RecordingItem(props: { onCopyVideoToClipboard: () => void; uploadProgress: number | undefined; }) { + const { t } = useI18n(); const [imageExists, setImageExists] = createSignal(true); const mode = () => props.recording.meta.mode; - const firstLetterUpperCase = () => - mode().charAt(0).toUpperCase() + mode().slice(1); + const modeLabel = () => { + if (mode() === "instant") return t("Instant"); + if (mode() === "studio") return t("Studio"); + return mode(); + }; const queryClient = useQueryClient(); const studioCompleteCheck = () => @@ -329,7 +339,7 @@ function RecordingItem(props: { > Recording thumbnail )} -

{firstLetterUpperCase()}

+

{modeLabel()}

@@ -360,7 +370,7 @@ function RecordingItem(props: { )} > -

Recording in progress

+

{t("Recording in progress")}

@@ -380,7 +390,7 @@ function RecordingItem(props: { )} > -

Recording failed

+

{t("Recording failed")}

@@ -401,7 +411,7 @@ function RecordingItem(props: { {(sharing) => ( shell.open(sharing().link)} > @@ -409,14 +419,16 @@ function RecordingItem(props: { )} { if ( props.recording.meta.status.status === "Failed" && !(await confirm( - "The recording failed so this file may have issues in the editor! If your having issues recovering the file please reach out to support!", + t( + "The recording failed so this file may have issues in the editor! If your having issues recovering the file please reach out to support!", + ), { - title: "Recording is potentially corrupted", + title: t("Recording is potentially corrupted"), kind: "warning", }, )) @@ -447,7 +459,7 @@ function RecordingItem(props: { when={props.uploadProgress || reupload.isPending} fallback={ reupload.mutate()} > @@ -464,7 +476,7 @@ function RecordingItem(props: { {(sharing) => ( shell.open(sharing().link)} > @@ -476,15 +488,17 @@ function RecordingItem(props: { }} revealItemInDir(`${props.recording.path}/`)} > { - if (!(await ask("Are you sure you want to delete this recording?"))) + if ( + !(await ask(t("Are you sure you want to delete this recording?"))) + ) return; await remove(props.recording.path, { recursive: true }); diff --git a/apps/desktop/src/routes/(window-chrome)/settings/screenshots.tsx b/apps/desktop/src/routes/(window-chrome)/settings/screenshots.tsx index 4d191bd7ad..345cae5f87 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/screenshots.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/screenshots.tsx @@ -16,6 +16,7 @@ import { type ParentProps, Show, } from "solid-js"; +import { useI18n } from "~/i18n"; import { Input } from "~/routes/editor/ui"; import { trackEvent } from "~/utils/analytics"; import { createTauriEventListener } from "~/utils/createEventListener"; @@ -44,6 +45,7 @@ const screenshotsQuery = queryOptions({ }); export default function Screenshots() { + const { t } = useI18n(); const [search, setSearch] = createSignal(""); const trimmedSearch = createMemo(() => search().trim()); const normalizedSearch = createMemo(() => trimmedSearch().toLowerCase()); @@ -80,8 +82,8 @@ export default function Screenshots() { ); const emptyMessage = createMemo(() => { - const prefix = trimmedSearch() ? "No matching" : "No"; - return `${prefix} screenshots`; + const prefix = trimmedSearch() ? t("No matching") : t("No"); + return `${prefix} ${t("screenshots")}`; }); const handleScreenshotClick = (screenshot: Screenshot) => { @@ -116,16 +118,16 @@ export default function Screenshots() { return (
-

Screenshots

+

{t("Screenshots")}

- Manage your screenshots and perform actions. + {t("Manage your screenshots and perform actions.")}

0} fallback={

- No screenshots found + {t("No screenshots found")}

} > @@ -143,12 +145,12 @@ export default function Screenshots() { setSearch(""); } }} - placeholder="Search screenshots" + placeholder={t("Search screenshots")} autoCapitalize="off" autocorrect="off" autocomplete="off" spellcheck={false} - aria-label="Search screenshots" + aria-label={t("Search screenshots")} />
@@ -185,7 +187,7 @@ export default function Screenshots() { ) } > - Load more + {t("Load more")}
@@ -202,6 +204,7 @@ function ScreenshotItem(props: { onOpenFolder: () => void; onCopyImageToClipboard: () => void; }) { + const { t } = useI18n(); const [imageExists, setImageExists] = createSignal(true); const queryClient = useQueryClient(); @@ -217,7 +220,7 @@ function ScreenshotItem(props: { > Screenshot thumbnail setImageExists(false)} /> @@ -228,31 +231,33 @@ function ScreenshotItem(props: {
{ if ( - !(await ask("Are you sure you want to delete this screenshot?")) + !(await ask( + t("Are you sure you want to delete this screenshot?"), + )) ) return; // screenshot.path is the png file. Parent is the .cap folder. diff --git a/apps/desktop/src/routes/(window-chrome)/update.tsx b/apps/desktop/src/routes/(window-chrome)/update.tsx index 41cee01e50..14761048f6 100644 --- a/apps/desktop/src/routes/(window-chrome)/update.tsx +++ b/apps/desktop/src/routes/(window-chrome)/update.tsx @@ -4,8 +4,10 @@ import { getCurrentWindow, UserAttentionType } from "@tauri-apps/api/window"; import { relaunch } from "@tauri-apps/plugin-process"; import { check, type Update } from "@tauri-apps/plugin-updater"; import { createResource, createSignal, Match, Show, Switch } from "solid-js"; +import { useI18n } from "~/i18n"; export default function () { + const { t } = useI18n(); const navigate = useNavigate(); const [updateError, setUpdateError] = createSignal(null); @@ -16,7 +18,7 @@ export default function () { return update; } catch (e) { console.error("Failed to check for updates:", e); - setUpdateError("Unable to check for updates."); + setUpdateError(t("Unable to check for updates.")); return; } }); @@ -27,20 +29,23 @@ export default function () {

{updateError()}

- Please download the latest version manually from cap.so/download. - Your data will not be lost. + {t( + "Please download the latest version manually from cap.so/download. Your data will not be lost.", + )}

- If this issue persists, please contact support. + {t("If this issue persists, please contact support.")}

- +
No update available + + {t("No update available")} + ) } keyed @@ -85,7 +90,7 @@ export default function () { .catch((e) => { console.error("Failed to download/install update:", e); setUpdateError( - "Failed to download or install the update.", + t("Failed to download or install the update."), ); }); }), @@ -101,9 +106,13 @@ export default function () {

- Update has been installed. Restart Cap to finish updating. + {t( + "Update has been installed. Restart Cap to finish updating.", + )}

- +
( <>

- Installing Update + {t("Installing Update")}

diff --git a/apps/desktop/src/store.ts b/apps/desktop/src/store.ts index 2fd16a965c..b906eca7e1 100644 --- a/apps/desktop/src/store.ts +++ b/apps/desktop/src/store.ts @@ -10,6 +10,10 @@ import type { RecordingSettingsStore, } from "~/utils/tauri"; +type UiSettingsStore = { + language?: "en" | "zh-CN"; +}; + let _store: Promise | undefined; const store = () => { if (!_store) { @@ -62,3 +66,4 @@ export const generalSettingsStore = declareStore("general_settings"); export const recordingSettingsStore = declareStore("recording_settings"); +export const uiSettingsStore = declareStore("ui_settings");