diff --git a/src/components/ControlPanel.tsx b/src/components/ControlPanel.tsx index 9bc13d35c..887d43ba7 100644 --- a/src/components/ControlPanel.tsx +++ b/src/components/ControlPanel.tsx @@ -614,7 +614,7 @@ export default function ControlPanel() { className="h-7 px-2.5 pl-1.5 gap-1" > - Back to notes + {t("controlPanel.backToNotes")} )} diff --git a/src/components/OnboardingFlow.tsx b/src/components/OnboardingFlow.tsx index 1c6523fc5..75fa0e4e5 100644 --- a/src/components/OnboardingFlow.tsx +++ b/src/components/OnboardingFlow.tsx @@ -47,9 +47,6 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { const { t } = useTranslation(); const { isSignedIn } = useAuth(); - // Max valid step index dynamically determined based on auth state - // Signed-in users: 3 steps (Welcome, Setup, Activation) - index 0-2 - // Non-signed-in users: 4 steps (Welcome, Setup, Permissions, Activation) - index 0-3 const getMaxStep = () => (isSignedIn ? 2 : 3); const [currentStep, setCurrentStep, removeCurrentStep] = useLocalStorage( @@ -350,11 +347,7 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { ); case 1: // Setup - Choose Mode & Configure (merged with permissions for signed-in users) - // Simplified path for signed-in users (cloud-first) with permissions if (isSignedIn && !skipAuth) { - const platform = permissionsHook.pasteToolsInfo?.platform; - const isMacOS = platform === "darwin"; - return (
@@ -589,11 +582,7 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { case 1: // For signed-in users: Setup step includes permissions if (isSignedIn && !skipAuth) { - return areRequiredPermissionsMet( - permissionsHook.micPermissionGranted, - permissionsHook.accessibilityPermissionGranted, - permissionsHook.pasteToolsInfo?.platform - ); + return areRequiredPermissionsMet(permissionsHook.micPermissionGranted); } // For non-signed-in users: Setup - check if configuration is complete @@ -622,11 +611,7 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { } // For non-signed-in users, this is permissions step - return areRequiredPermissionsMet( - permissionsHook.micPermissionGranted, - permissionsHook.accessibilityPermissionGranted, - permissionsHook.pasteToolsInfo?.platform - ); + return areRequiredPermissionsMet(permissionsHook.micPermissionGranted); } case 3: return hotkey.trim() !== ""; // Activation step for non-signed-in users diff --git a/src/components/PermissionsGate.tsx b/src/components/PermissionsGate.tsx index 818488e5a..e70f8f100 100644 --- a/src/components/PermissionsGate.tsx +++ b/src/components/PermissionsGate.tsx @@ -1,3 +1,4 @@ +import type React from "react"; import { useTranslation } from "react-i18next"; import { Shield, Check } from "lucide-react"; import { Card, CardContent } from "./ui/card"; @@ -22,11 +23,7 @@ export default function PermissionsGate({ onComplete }: PermissionsGateProps) { const requiredMet = permissions.pasteToolsInfo !== null && - areRequiredPermissionsMet( - permissions.micPermissionGranted, - permissions.accessibilityPermissionGranted, - permissions.pasteToolsInfo?.platform - ); + areRequiredPermissionsMet(permissions.micPermissionGranted); return (
{ - const formatError = getValidationMessage(hotkey, getPlatform()); - if (formatError) return formatError; - const platform = getPlatform(); - const normalized = normalizeHotkey(hotkey, platform); - if (meetingKey && normalizeHotkey(meetingKey, platform) === normalized) { - return t("hotkey.errors.slotConflict", { - slot: t("settingsPage.general.meetingHotkey.title"), - }); - } - if (agentKey && normalizeHotkey(agentKey, platform) === normalized) { - return t("hotkey.errors.slotConflict", { slot: t("agentMode.settings.hotkey") }); - } - return null; - }, + (hotkey: string) => + validateHotkeyForSlot( + hotkey, + { + "settingsPage.general.meetingHotkey.title": meetingKey, + "agentMode.settings.hotkey": agentKey, + }, + t + ), [meetingKey, agentKey, t] ); const validateMeetingHotkey = useCallback( - (hotkey: string) => { - const formatError = getValidationMessage(hotkey, getPlatform()); - if (formatError) return formatError; - const platform = getPlatform(); - const normalized = normalizeHotkey(hotkey, platform); - if (dictationKey && normalizeHotkey(dictationKey, platform) === normalized) { - return t("hotkey.errors.slotConflict", { slot: t("settingsPage.general.hotkey.title") }); - } - if (agentKey && normalizeHotkey(agentKey, platform) === normalized) { - return t("hotkey.errors.slotConflict", { slot: t("agentMode.settings.hotkey") }); - } - return null; - }, + (hotkey: string) => + validateHotkeyForSlot( + hotkey, + { + "settingsPage.general.hotkey.title": dictationKey, + "agentMode.settings.hotkey": agentKey, + }, + t + ), [dictationKey, agentKey, t] ); diff --git a/src/components/notes/NoteEditor.tsx b/src/components/notes/NoteEditor.tsx index 47adcc16f..85ca4bc52 100644 --- a/src/components/notes/NoteEditor.tsx +++ b/src/components/notes/NoteEditor.tsx @@ -17,6 +17,7 @@ import type { ActionProcessingState } from "../../hooks/useActionProcessing"; import ActionProcessingOverlay from "./ActionProcessingOverlay"; import DictationWidget from "./DictationWidget"; import { normalizeDbDate } from "../../utils/dateFormatting"; +import { parseTranscriptSegments } from "../../utils/parseTranscriptSegments"; function formatNoteDate(dateStr: string): string { const date = normalizeDbDate(dateStr); @@ -97,23 +98,7 @@ export default function NoteEditor({ const displaySegments = useMemo(() => { if (meetingSegments && meetingSegments.length > 0) return meetingSegments; - const raw = note.transcript || ""; - if (raw.startsWith("[")) { - try { - const parsed = JSON.parse(raw) as Array<{ - text: string; - source: "mic" | "system"; - timestamp?: number; - }>; - return parsed.map((s, i) => ({ - id: `stored-${i}`, - text: s.text, - source: s.source, - timestamp: s.timestamp, - })); - } catch {} - } - return []; + return parseTranscriptSegments(note.transcript || ""); }, [meetingSegments, note.transcript]); const hasChatSegments = displaySegments.length > 0; @@ -193,11 +178,6 @@ export default function NoteEditor({ document.execCommand("insertText", false, text); }, []); - const prevMeetingRecRef = useRef(false); - useEffect(() => { - prevMeetingRecRef.current = !!isMeetingRecording; - }, [isMeetingRecording]); - // Auto-switch to transcript view after recording stops and transcript is ready const prevRecordingRef = useRef(false); const pendingTranscriptSwitchRef = useRef(false); @@ -390,7 +370,9 @@ export default function NoteEditor({ isRecording={isRecording || !!isMeetingRecording} isProcessing={isProcessing} onStart={onStartRecording} - onStop={isMeetingRecording ? onStopMeetingRecording! : onStopRecording} + onStop={ + isMeetingRecording ? (onStopMeetingRecording ?? onStopRecording) : onStopRecording + } actionPicker={isMeetingRecording ? undefined : actionPicker} />
diff --git a/src/components/notes/PersonalNotesView.tsx b/src/components/notes/PersonalNotesView.tsx index aa1c65d6b..96d1ffc3e 100644 --- a/src/components/notes/PersonalNotesView.tsx +++ b/src/components/notes/PersonalNotesView.tsx @@ -22,6 +22,7 @@ import { useNoteDragAndDrop } from "../../hooks/useNoteDragAndDrop"; import { cn } from "../lib/utils"; import { MEETINGS_FOLDER_NAME } from "./shared"; import logger from "../../utils/logger"; +import { parseTranscriptSegments } from "../../utils/parseTranscriptSegments"; import { useNotes, useActiveNoteId, @@ -715,19 +716,15 @@ export default function PersonalNotesView({ let formattedTranscript = ""; let isMeetingNote = false; if (rawTranscript) { - try { - const segments = JSON.parse(rawTranscript) as Array<{ - text: string; - source: string; - }>; - if (Array.isArray(segments) && segments.length > 0 && segments[0].source) { - isMeetingNote = true; - formattedTranscript = segments - .map((s) => `${s.source === "mic" ? "You" : "Them"}: ${s.text}`) - .join("\n"); - } - } catch { - // Not JSON segments — use raw transcript as-is + const segments = parseTranscriptSegments(rawTranscript); + if (segments.length > 0) { + isMeetingNote = true; + formattedTranscript = segments + .map( + (s) => + `${s.source === "mic" ? t("notes.speaker.you") : t("notes.speaker.them")}: ${s.text}` + ) + .join("\n"); } if (!formattedTranscript) { formattedTranscript = rawTranscript; diff --git a/src/components/settings/AgentModeSettings.tsx b/src/components/settings/AgentModeSettings.tsx index 69f4486b1..794564c6a 100644 --- a/src/components/settings/AgentModeSettings.tsx +++ b/src/components/settings/AgentModeSettings.tsx @@ -6,8 +6,7 @@ import { HotkeyInput } from "../ui/HotkeyInput"; import { Toggle } from "../ui/toggle"; import { SettingsRow, SettingsPanel, SettingsPanelRow, SectionHeader } from "../ui/SettingsSection"; import ReasoningModelSelector from "../ReasoningModelSelector"; -import { getValidationMessage, normalizeHotkey } from "../../utils/hotkeyValidator"; -import { getPlatform } from "../../utils/platform"; +import { validateHotkeyForSlot } from "../../utils/hotkeyValidation"; export default function AgentModeSettings() { const { t } = useTranslation(); @@ -45,21 +44,15 @@ export default function AgentModeSettings() { const isCustomMode = cloudAgentMode === "byok"; const validateAgentHotkey = useCallback( - (hotkey: string) => { - const formatError = getValidationMessage(hotkey, getPlatform()); - if (formatError) return formatError; - const platform = getPlatform(); - const normalized = normalizeHotkey(hotkey, platform); - if (dictationKey && normalizeHotkey(dictationKey, platform) === normalized) { - return t("hotkey.errors.slotConflict", { slot: t("settingsPage.general.hotkey.title") }); - } - if (meetingKey && normalizeHotkey(meetingKey, platform) === normalized) { - return t("hotkey.errors.slotConflict", { - slot: t("settingsPage.general.meetingHotkey.title"), - }); - } - return null; - }, + (hotkey: string) => + validateHotkeyForSlot( + hotkey, + { + "settingsPage.general.hotkey.title": dictationKey, + "settingsPage.general.meetingHotkey.title": meetingKey, + }, + t + ), [dictationKey, meetingKey, t] ); diff --git a/src/components/ui/RichTextEditor.tsx b/src/components/ui/RichTextEditor.tsx index c366ec15c..158772a7c 100644 --- a/src/components/ui/RichTextEditor.tsx +++ b/src/components/ui/RichTextEditor.tsx @@ -10,8 +10,6 @@ import { cn } from "../lib/utils"; interface RichTextEditorProps { value: string; onChange?: (value: string) => void; - onSelect?: () => void; - onBlur?: () => void; placeholder?: string; className?: string; disabled?: boolean; @@ -21,8 +19,6 @@ interface RichTextEditorProps { export function RichTextEditor({ value, onChange, - onSelect, - onBlur, placeholder, className, disabled, @@ -59,12 +55,6 @@ export function RichTextEditor({ internalValueRef.current = md; onChange?.(md); }, - onSelectionUpdate: () => { - onSelect?.(); - }, - onBlur: () => { - onBlur?.(); - }, editorProps: { attributes: { class: "rich-text-editor-content", diff --git a/src/helpers/audioManager.js b/src/helpers/audioManager.js index 2b11eece5..2caa64657 100644 --- a/src/helpers/audioManager.js +++ b/src/helpers/audioManager.js @@ -815,78 +815,6 @@ registerProcessor("pcm-streaming-processor", PCMStreamingProcessor); return apiKey; } - async optimizeAudio(audioBlob) { - return new Promise((resolve) => { - const audioContext = new (window.AudioContext || window.webkitAudioContext)(); - const reader = new FileReader(); - - reader.onload = async () => { - try { - const arrayBuffer = reader.result; - const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); - - // Convert to 16kHz mono for smaller size and faster upload - const sampleRate = 16000; - const channels = 1; - const length = Math.floor(audioBuffer.duration * sampleRate); - const offlineContext = new OfflineAudioContext(channels, length, sampleRate); - - const source = offlineContext.createBufferSource(); - source.buffer = audioBuffer; - source.connect(offlineContext.destination); - source.start(); - - const renderedBuffer = await offlineContext.startRendering(); - const wavBlob = this.audioBufferToWav(renderedBuffer); - resolve(wavBlob); - } catch (error) { - // If optimization fails, use original - resolve(audioBlob); - } - }; - - reader.onerror = () => resolve(audioBlob); - reader.readAsArrayBuffer(audioBlob); - }); - } - - audioBufferToWav(buffer) { - const length = buffer.length; - const arrayBuffer = new ArrayBuffer(44 + length * 2); - const view = new DataView(arrayBuffer); - const sampleRate = buffer.sampleRate; - const channelData = buffer.getChannelData(0); - - const writeString = (offset, string) => { - for (let i = 0; i < string.length; i++) { - view.setUint8(offset + i, string.charCodeAt(i)); - } - }; - - writeString(0, "RIFF"); - view.setUint32(4, 36 + length * 2, true); - writeString(8, "WAVE"); - writeString(12, "fmt "); - view.setUint32(16, 16, true); - view.setUint16(20, 1, true); - view.setUint16(22, 1, true); - view.setUint32(24, sampleRate, true); - view.setUint32(28, sampleRate * 2, true); - view.setUint16(32, 2, true); - view.setUint16(34, 16, true); - writeString(36, "data"); - view.setUint32(40, length * 2, true); - - let offset = 44; - for (let i = 0; i < length; i++) { - const sample = Math.max(-1, Math.min(1, channelData[i])); - view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true); - offset += 2; - } - - return new Blob([arrayBuffer], { type: "audio/wav" }); - } - async processWithReasoningModel(text, model, agentName) { logger.logReasoning("CALLING_REASONING_SERVICE", { model, diff --git a/src/helpers/clipboard.js b/src/helpers/clipboard.js index 3a89672cd..0d3d0e51e 100644 --- a/src/helpers/clipboard.js +++ b/src/helpers/clipboard.js @@ -162,7 +162,9 @@ class ClipboardManager { } const possiblePaths = [ - path.join(process.resourcesPath, "bin", "nircmd.exe"), + ...(process.resourcesPath + ? [path.join(process.resourcesPath, "bin", "nircmd.exe")] + : []), path.join(__dirname, "..", "..", "resources", "bin", "nircmd.exe"), path.join(process.cwd(), "resources", "bin", "nircmd.exe"), ]; @@ -1151,8 +1153,6 @@ class ClipboardManager { // desync (X11 clipboard vs Wayland input) so portal is preferred. // On GNOME, Mutter doesn't reliably route uinput to native Wayland // windows (issue #292), so portal is tried first there too. - const preferUinput = false; - const tryUinputPaste = async () => { const args = ["--uinput"]; if (earlyIsTerminal) args.push("--terminal"); @@ -1166,19 +1166,6 @@ class ClipboardManager { restoreClipboard(); }; - if (preferUinput) { - try { - await tryUinputPaste(); - return "uinput"; - } catch (uinputError) { - debugLogger.warn( - "uinput paste failed on KDE, falling back to portal", - { error: uinputError?.message }, - "clipboard" - ); - } - } - if ((isGnome || isKde) && linuxFastPaste && !this.portalDenied) { const MAX_PORTAL_RETRIES = 3; for (let attempt = 0; attempt < MAX_PORTAL_RETRIES; attempt++) { @@ -1220,13 +1207,11 @@ class ClipboardManager { } } - if (!preferUinput) { - try { - await tryUinputPaste(); - return "uinput"; - } catch (uinputError) { - debugLogger.warn("uinput paste failed", { error: uinputError?.message }, "clipboard"); - } + try { + await tryUinputPaste(); + return "uinput"; + } catch (uinputError) { + debugLogger.warn("uinput paste failed", { error: uinputError?.message }, "clipboard"); } // XTest/XWayland fallback: works for XWayland apps on any Wayland compositor diff --git a/src/helpers/database.js b/src/helpers/database.js index 0931f99f1..a977c4002 100644 --- a/src/helpers/database.js +++ b/src/helpers/database.js @@ -1243,6 +1243,14 @@ class DatabaseManager { cleanup() { try { + if (this.db) { + try { + this.db.close(); + } catch (closeError) { + debugLogger.error("Error closing database", { error: closeError.message }, "database"); + } + this.db = null; + } const dbPath = path.join( app.getPath("userData"), process.env.NODE_ENV === "development" ? "transcriptions-dev.db" : "transcriptions.db" diff --git a/src/helpers/ipcHandlers.js b/src/helpers/ipcHandlers.js index 2a407652f..8d6c32a19 100644 --- a/src/helpers/ipcHandlers.js +++ b/src/helpers/ipcHandlers.js @@ -1,4 +1,4 @@ -const { ipcMain, app, shell, BrowserWindow } = require("electron"); +const { ipcMain, app, shell, BrowserWindow, systemPreferences } = require("electron"); const path = require("path"); const http = require("http"); const https = require("https"); @@ -108,6 +108,8 @@ class IPCHandlers { this.assemblyAiStreaming = null; this.deepgramStreaming = null; this._dictationStreaming = null; + this._meetingMicStreaming = null; + this._meetingSystemStreaming = null; this._autoLearnEnabled = true; // Default on, synced from renderer this._autoLearnDebounceTimer = null; this._autoLearnLatestData = null; @@ -802,7 +804,6 @@ class IPCHandlers { // Passes `true` to isTrustedAccessibilityClient to trigger the macOS system prompt ipcMain.handle("prompt-accessibility-permission", async () => { if (process.platform !== "darwin") return true; - const { systemPreferences } = require("electron"); return systemPreferences.isTrustedAccessibilityClient(true); }); @@ -2006,6 +2007,7 @@ class IPCHandlers { microphone: i18nMain.t("systemSettings.microphone"), sound: i18nMain.t("systemSettings.sound"), accessibility: i18nMain.t("systemSettings.accessibility"), + systemAudio: i18nMain.t("systemSettings.systemAudio"), }; return { success: false, @@ -2045,9 +2047,8 @@ class IPCHandlers { ipcMain.handle("request-microphone-access", async () => { if (process.platform !== "darwin") { - return { granted: true }; + return { granted: true, status: "granted" }; } - const { systemPreferences } = require("electron"); const granted = await systemPreferences.askForMediaAccess("microphone"); return { granted }; }); @@ -2056,7 +2057,6 @@ class IPCHandlers { if (process.platform !== "darwin") { return { granted: true, status: "granted" }; } - const { systemPreferences } = require("electron"); const status = systemPreferences.getMediaAccessStatus("microphone"); return { granted: status === "granted", status }; }); @@ -2067,7 +2067,6 @@ class IPCHandlers { } if (this.audioTapManager?.isSupported()) { - const { systemPreferences } = require("electron"); const status = systemPreferences.getMediaAccessStatus("screen"); return { granted: status === "granted", status, mode: "native" }; } @@ -2084,7 +2083,6 @@ class IPCHandlers { return { granted: false, status: "unsupported", mode: "unsupported" }; } - const { systemPreferences } = require("electron"); const status = systemPreferences.getMediaAccessStatus("screen"); if (status === "granted") { return { granted: true, status: "granted", mode: "native" }; @@ -2381,9 +2379,6 @@ class IPCHandlers { let meetingTranscriptionPrepareInProgress = false; let meetingTranscriptionPreparePromise = null; - this._meetingMicStreaming = null; - this._meetingSystemStreaming = null; - const attachMeetingStreamingHandlers = (streaming, win, source) => { const send = (channel, data) => { if (!win || win.isDestroyed()) { @@ -2484,19 +2479,16 @@ class IPCHandlers { language: options.language, preconfigured: options.mode !== "byok", }; - const pairs = hasNativeMeetingSystemAudio() - ? (await fetchRealtimeToken(event, options, { streams: 2 })).map((secret, index) => ({ - ref: index === 0 ? "_meetingMicStreaming" : "_meetingSystemStreaming", - secret, - source: index === 0 ? "mic" : "system", - })) - : [ - { - ref: "_meetingMicStreaming", - secret: await fetchRealtimeToken(event, options), - source: "mic", - }, - ]; + let pairs; + if (hasNativeMeetingSystemAudio()) { + const secrets = await fetchRealtimeToken(event, options, { streams: 2 }); + pairs = [ + { ref: "_meetingMicStreaming", secret: secrets[0], source: "mic" }, + { ref: "_meetingSystemStreaming", secret: secrets[1], source: "system" }, + ]; + } else { + pairs = [{ ref: "_meetingMicStreaming", secret: await fetchRealtimeToken(event, options), source: "mic" }]; + } for (const { ref, source } of pairs) { this[ref] = new OpenAIRealtimeStreaming(); @@ -3455,8 +3447,14 @@ class IPCHandlers { let hasXclip = false; let hasXsel = false; if (isKde) { - try { execFileSync("which", ["xclip"], { timeout: 1000 }); hasXclip = true; } catch {} - try { execFileSync("which", ["xsel"], { timeout: 1000 }); hasXsel = true; } catch {} + try { + execFileSync("which", ["xclip"], { timeout: 1000 }); + hasXclip = true; + } catch {} + try { + execFileSync("which", ["xsel"], { timeout: 1000 }); + hasXsel = true; + } catch {} } return { ...status, isKde, hasXclip, hasXsel }; }); diff --git a/src/helpers/meetingDetectionEngine.js b/src/helpers/meetingDetectionEngine.js index f983fe93a..e6aec1398 100644 --- a/src/helpers/meetingDetectionEngine.js +++ b/src/helpers/meetingDetectionEngine.js @@ -200,6 +200,9 @@ class MeetingDetectionEngine { detection.dismissed = true; } } + } catch (error) { + this._meetingModeActive = false; + debugLogger.error("Error handling notification response", { error: error?.message, detectionId, action }, "meeting"); } finally { this.windowManager.dismissMeetingNotification(); } diff --git a/src/helpers/openaiRealtimeStreaming.js b/src/helpers/openaiRealtimeStreaming.js index 85a46f47a..fc02fc59e 100644 --- a/src/helpers/openaiRealtimeStreaming.js +++ b/src/helpers/openaiRealtimeStreaming.js @@ -20,7 +20,6 @@ class OpenAIRealtimeStreaming { this.pendingResolve = null; this.pendingReject = null; this.connectionTimeout = null; - this.closeResolve = null; this.isDisconnecting = false; this.audioBytesSent = 0; this.model = "gpt-4o-mini-transcribe"; @@ -105,9 +104,6 @@ class OpenAIRealtimeStreaming { this.pendingReject = null; this.pendingResolve = null; } - if (this.closeResolve) { - this.closeResolve({ text: this.getFullTranscript() }); - } this.cleanup(); if (wasActive && !this.isDisconnecting) { this.onSessionEnd?.({ text: this.getFullTranscript() }); @@ -355,7 +351,6 @@ class OpenAIRealtimeStreaming { this.isConnected = false; this.isConnecting = false; - this.closeResolve = null; } } diff --git a/src/hooks/useAudioRecording.js b/src/hooks/useAudioRecording.js index ec349103c..7a703574c 100644 --- a/src/hooks/useAudioRecording.js +++ b/src/hooks/useAudioRecording.js @@ -231,14 +231,6 @@ export const useAudioRecording = (toast, options = {}) => { }; }, [toast, onToggle, performStartRecording, performStopRecording, t]); - const startRecording = async () => { - return performStartRecording(); - }; - - const stopRecording = async () => { - return performStopRecording(); - }; - const cancelRecording = async () => { if (audioManagerRef.current) { const state = audioManagerRef.current.getState(); @@ -262,9 +254,9 @@ export const useAudioRecording = (toast, options = {}) => { const toggleListening = async () => { if (!isRecording && !isProcessing) { - await startRecording(); + await performStartRecording(); } else if (isRecording) { - await stopRecording(); + await performStopRecording(); } }; @@ -274,8 +266,8 @@ export const useAudioRecording = (toast, options = {}) => { isStreaming, transcript, partialTranscript, - startRecording, - stopRecording, + startRecording: performStartRecording, + stopRecording: performStopRecording, cancelRecording, cancelProcessing, toggleListening, diff --git a/src/hooks/useMeetingTranscription.ts b/src/hooks/useMeetingTranscription.ts index dbe302932..36e7a0090 100644 --- a/src/hooks/useMeetingTranscription.ts +++ b/src/hooks/useMeetingTranscription.ts @@ -232,31 +232,21 @@ export function useMeetingTranscription(): UseMeetingTranscriptionReturn { const pendingCleanupRef = useRef | null>(null); const cleanup = useCallback(async () => { - const processors = [micProcessorRef]; - const sources = [micSourceRef]; - const streams = [micStreamRef]; - const contexts = [micContextRef]; - - for (const ref of processors) { - await flushAndDisconnectProcessor(ref.current); - ref.current = null; - } - for (const ref of sources) { - ref.current?.disconnect(); - ref.current = null; - } - for (const ref of streams) { - try { - ref.current?.getTracks().forEach((t) => t.stop()); - } catch {} - ref.current = null; - } - for (const ref of contexts) { - try { - await ref.current?.close(); - } catch {} - ref.current = null; - } + await flushAndDisconnectProcessor(micProcessorRef.current); + micProcessorRef.current = null; + + micSourceRef.current?.disconnect(); + micSourceRef.current = null; + + try { + micStreamRef.current?.getTracks().forEach((t) => t.stop()); + } catch {} + micStreamRef.current = null; + + try { + await micContextRef.current?.close(); + } catch {} + micContextRef.current = null; ipcCleanupsRef.current.forEach((fn) => fn()); ipcCleanupsRef.current = []; diff --git a/src/hooks/usePermissions.ts b/src/hooks/usePermissions.ts index 63dc86e1d..d1f18f38b 100644 --- a/src/hooks/usePermissions.ts +++ b/src/hooks/usePermissions.ts @@ -3,6 +3,7 @@ import type { TFunction } from "i18next"; import { useTranslation } from "react-i18next"; import type { PasteToolsResult } from "../types/electron"; import { useLocalStorage } from "./useLocalStorage"; +import logger from "../utils/logger"; export interface UsePermissionsReturn { // State @@ -150,7 +151,7 @@ export const usePermissions = ( showAlertDialog?.({ title: titles[settingType], description: result.error }); } } catch (error) { - console.error(`Failed to open ${settingType} settings:`, error); + logger.error(`Failed to open ${settingType} settings:`, error); showAlertDialog?.({ title: titles[settingType], description: unableToOpenDescriptions[settingType], @@ -215,7 +216,7 @@ export const usePermissions = ( setMicPermissionGranted(true); setMicPermissionError(null); } catch (err) { - console.error("Microphone permission denied:", err); + logger.error("Microphone permission denied:", err); const message = describeMicError(err, t); setMicPermissionError(message); if (showAlertDialog) { @@ -246,7 +247,7 @@ export const usePermissions = ( } return null; } catch (error) { - console.error("Failed to check paste tools:", error); + logger.error("Failed to check paste tools:", error); return null; } finally { setIsCheckingPasteTools(false); @@ -313,7 +314,7 @@ export const usePermissions = ( await window.electronAPI.pasteText(t("hooks.permissions.accessibilityTestText")); setAccessibilityPermissionGranted(true); } catch (err) { - console.error("Accessibility permission test failed:", err); + logger.error("Accessibility permission test failed:", err); if (showAlertDialog) { showAlertDialog({ title: t("hooks.permissions.titles.accessibilityNeeded"), diff --git a/src/hooks/useSystemAudioPermission.ts b/src/hooks/useSystemAudioPermission.ts index 05073f141..5bc77cac7 100644 --- a/src/hooks/useSystemAudioPermission.ts +++ b/src/hooks/useSystemAudioPermission.ts @@ -49,22 +49,29 @@ export function useSystemAudioPermission() { const request = useCallback(async (): Promise => { if (!isMacOS) return true; - const currentAccess = access ?? - (await window.electronAPI?.checkSystemAudioAccess?.()) ?? { - granted: false, - status: "unsupported" as const, - mode: "unsupported" as const, - }; + setIsChecking(true); + try { + const currentAccess = access ?? + (await window.electronAPI?.checkSystemAudioAccess?.()) ?? { + granted: false, + status: "unsupported" as const, + mode: "unsupported" as const, + }; + + if (currentAccess.mode !== "native") { + setAccess(currentAccess); + return false; + } - if (currentAccess.mode !== "native") { - setAccess(currentAccess); + const result = await window.electronAPI?.requestSystemAudioAccess?.(); + const nextAccess = result ?? currentAccess; + setAccess(nextAccess); + return nextAccess.granted; + } catch { return false; + } finally { + setIsChecking(false); } - - const result = await window.electronAPI?.requestSystemAudioAccess?.(); - const nextAccess = result ?? currentAccess; - setAccess(nextAccess); - return nextAccess.granted; }, [access, isMacOS]); const granted = access?.granted ?? false; diff --git a/src/index.css b/src/index.css index fcc7e2b89..64d5ca038 100644 --- a/src/index.css +++ b/src/index.css @@ -870,13 +870,6 @@ body:has(.agent-overlay-window) #root { background: oklch(0.5 0 0 / 0.3); } -@media (prefers-reduced-motion: reduce) { - .agent-overlay-window * { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - } -} - /* ── TipTap / ProseMirror Rich Text Editor ── */ .rich-text-editor-content { diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index c6baecf7f..332515e4b 100644 --- a/src/locales/de/translation.json +++ b/src/locales/de/translation.json @@ -78,7 +78,8 @@ "systemSettings": { "microphone": "Bitte öffnen Sie die Systemeinstellungen, um Mikrofonberechtigungen zu konfigurieren.", "sound": "Bitte öffnen Sie die Sound-Einstellungen des Systems (z. B. pavucontrol).", - "accessibility": "Bedienungshilfen-Einstellungen sind auf dieser Plattform nicht verfügbar." + "accessibility": "Bedienungshilfen-Einstellungen sind auf dieser Plattform nicht verfügbar.", + "systemAudio": "Systemaudio-Einstellungen sind auf dieser Plattform nicht verfügbar." }, "titleBar": { "quitTitle": "OpenWhispr beenden", @@ -245,7 +246,20 @@ "clearAllDescription": "Alle Transkriptionen und Audiodateien werden dauerhaft entfernt.", "clearAllSuccess": "Verlauf gelöscht", "clearAllErrorTitle": "Verlauf konnte nicht gelöscht werden", - "clearAllErrorDescription": "Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut." + "clearAllErrorDescription": "Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.", + "copyText": "Text kopieren", + "deleteItem": "Löschen", + "viewRawTranscript": "Rohtranskript anzeigen", + "showInFolder": "Im Finder anzeigen", + "showInFolderWindows": "Im Explorer anzeigen", + "showInFolderLinux": "Im Dateimanager anzeigen", + "retryTranscription": "Erneut transkribieren", + "retrying": "Wird erneut transkribiert…", + "retrySuccess": "Transkription aktualisiert", + "retryError": "Erneute Transkription fehlgeschlagen", + "audioNotFound": "Audiodatei nicht gefunden oder abgelaufen", + "noAiProcessing": "Auf diese Transkription wurde keine KI-Verarbeitung angewendet", + "rawTranscript": "Rohtranskript" }, "cloudMigration": { "title": "Willkommen bei OpenWhispr Pro", @@ -263,7 +277,8 @@ "bannerDescription": "Aktiviere die GPU-Beschleunigung für schnellere Leistung.", "enableButton": "GPU aktivieren", "dismissButton": "Nicht jetzt" - } + }, + "backToNotes": "Zurück zu Notizen" }, "reasoning": { "mode": { @@ -1145,7 +1160,12 @@ "autoHide": "Automatisch ausblenden bei Inaktivität", "autoHideDescription": "Symbol ausblenden, bis Sie mit dem Diktieren beginnen", "description": "Steuern Sie, wann das Diktat-Symbol auf Ihrem Bildschirm sichtbar ist", - "title": "Schwebendes Symbol" + "title": "Schwebendes Symbol", + "startPosition": "Startposition", + "startPositionDescription": "Wo das Sprachrekorder-Panel auf dem Bildschirm erscheint", + "bottomRight": "Unten rechts", + "center": "Unten Mitte", + "bottomLeft": "Unten links" }, "hotkey": { "activationMode": "Aktivierungsmodus", @@ -1236,8 +1256,12 @@ "step1Title": "Daemon starten", "step1Desc": "Starten Sie ydotoold und aktivieren Sie es für jeden Anmeldevorgang.", "step2Title": "Prüfen, ob er läuft" + }, + "xclip": { + "step1Title": "xclip installieren" } - } + }, + "xclipDesc": "Zwischenablage-Tool für KDE-Wayland-Einfügen (xclip oder xsel)" }, "updates": { "badges": { @@ -1327,7 +1351,17 @@ "usageAnalytics": "Nutzungsanalysen", "usageAnalyticsDescription": "Helfen Sie uns, OpenWhispr zu verbessern, indem Sie anonyme Leistungsdaten teilen. Wir senden niemals Transkriptionsinhalte – nur Timing- und Fehlerdaten.", "dataRetention": "Datenaufbewahrung", - "dataRetentionDescription": "Transkriptionen und Audio lokal im Verlauf speichern. Wenn deaktiviert, werden Transkriptionen eingefügt, aber nicht gespeichert." + "dataRetentionDescription": "Transkriptionen und Audio lokal im Verlauf speichern. Wenn deaktiviert, werden Transkriptionen eingefügt, aber nicht gespeichert.", + "audioRetention": "Audio-Aufbewahrung", + "audioRetentionDescription": "Audioaufnahmen lokal für erneute Transkription und Download speichern. Dateien werden nach Ablauf der Aufbewahrungsfrist automatisch gelöscht.", + "audioRetentionDisabled": "Deaktiviert", + "audioRetentionDays": "{{count}} Tage", + "audioStorageUsage": "Speicherverbrauch", + "audioStorageUsageDescription": "Auf diesem Gerät gespeicherte Audiodateien", + "audioStorageFiles": "{{count}} Dateien, {{size}}", + "audioStorageEmpty": "Keine Audiodateien gespeichert", + "clearAllAudio": "Alle Audiodateien löschen", + "clearAllAudioConfirm": "Alle gespeicherten Audiodateien löschen? Dies kann nicht rückgängig gemacht werden." }, "prompts": { "description": "Den einheitlichen System-Prompt anzeigen, anpassen und testen, der Textbereinigung und Anweisungserkennung steuert", @@ -1663,6 +1697,10 @@ }, "enhance": { "title": "Verbesserungsfehler" + }, + "speaker": { + "you": "Du", + "them": "Gegenüber" } }, "dictionary": { diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 0e4ad138a..f87322023 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -78,7 +78,8 @@ "systemSettings": { "microphone": "Please open your system settings to configure microphone permissions.", "sound": "Please open your system sound settings (for example, pavucontrol).", - "accessibility": "Accessibility settings are not available on this platform." + "accessibility": "Accessibility settings are not available on this platform.", + "systemAudio": "System audio settings are not available on this platform." }, "titleBar": { "quitTitle": "Quit OpenWhispr", @@ -276,7 +277,8 @@ "bannerDescription": "Enable GPU acceleration for faster performance.", "enableButton": "Enable GPU", "dismissButton": "Not now" - } + }, + "backToNotes": "Back to notes" }, "reasoning": { "mode": { @@ -568,9 +570,15 @@ "noMoreMeetings": "No more meetings today", "now": "Now", "join": "Join", - "untitledEvent": "Untitled event", + "untitledEvent": "Untitled Event", "systemAudioRequired": "System Audio permission is needed to capture other participants' audio.", - "openSettings": "Open System Settings" + "openSettings": "Open System Settings", + "signInRequired": "Sign in to use meeting recording", + "signInDescription": "Create an account or sign in to record and transcribe meetings.", + "connectCalendar": "Connect your calendar", + "connectCalendarDescription": "Link your Google Calendar to see upcoming meetings and automatically record notes.", + "notABot": "OpenWhispr records using your system audio — it never joins meetings as a bot.", + "connectCalendarButton": "Connect Calendar" }, "cleanupPrompt": "Cleanup", "developerSection": { @@ -1296,8 +1304,12 @@ "step1Title": "Start the daemon", "step1Desc": "Start ydotoold and enable it so it runs on every login.", "step2Title": "Verify it's running" + }, + "xclip": { + "step1Title": "Install xclip" } - } + }, + "xclipDesc": "Clipboard tool for KDE Wayland paste (xclip or xsel)" }, "updates": { "badges": { @@ -1771,6 +1783,10 @@ }, "enhance": { "title": "Enhancement error" + }, + "speaker": { + "you": "You", + "them": "Them" } }, "dictionary": { @@ -1933,23 +1949,6 @@ "description": "OpenWhispr captures audio directly from your system — it never joins your meetings as a participant. Just connect your calendar and take notes as normal." } }, - "upcoming": { - "title": "Upcoming Meetings", - "transcriptions": "Transcriptions", - "tomorrow": "Tomorrow", - "noMoreMeetings": "No more meetings today", - "now": "Now", - "join": "Join", - "untitledEvent": "Untitled Event", - "systemAudioRequired": "System Audio permission is needed to capture other participants' audio.", - "openSettings": "Open System Settings", - "signInRequired": "Sign in to use meeting recording", - "signInDescription": "Create an account or sign in to record and transcribe meetings.", - "connectCalendar": "Connect your calendar", - "connectCalendarDescription": "Link your Google Calendar to see upcoming meetings and automatically record notes.", - "notABot": "OpenWhispr records using your system audio — it never joins meetings as a bot.", - "connectCalendarButton": "Connect Calendar" - }, "notesView": { "searchPlaceholder": "Search notes…", "newNote": "New Note", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 623c90bdb..baeee075b 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -78,7 +78,8 @@ "systemSettings": { "microphone": "Abre la configuración del sistema para permisos de micrófono.", "sound": "Abre la configuración de sonido del sistema (por ejemplo, pavucontrol).", - "accessibility": "La configuración de accesibilidad no está disponible en esta plataforma." + "accessibility": "La configuración de accesibilidad no está disponible en esta plataforma.", + "systemAudio": "La configuración de audio del sistema no está disponible en esta plataforma." }, "titleBar": { "quitTitle": "Salir de OpenWhispr", @@ -245,7 +246,20 @@ "clearAllDescription": "Todas las transcripciones y archivos de audio se eliminarán permanentemente.", "clearAllSuccess": "Historial borrado", "clearAllErrorTitle": "No se pudo borrar el historial", - "clearAllErrorDescription": "Algo salió mal. Inténtalo de nuevo." + "clearAllErrorDescription": "Algo salió mal. Inténtalo de nuevo.", + "copyText": "Copiar texto", + "deleteItem": "Eliminar", + "viewRawTranscript": "Ver transcripción sin procesar", + "showInFolder": "Mostrar en Finder", + "showInFolderWindows": "Mostrar en Explorador", + "showInFolderLinux": "Mostrar en gestor de archivos", + "retryTranscription": "Volver a transcribir", + "retrying": "Transcribiendo de nuevo…", + "retrySuccess": "Transcripción actualizada", + "retryError": "Error al volver a transcribir", + "audioNotFound": "Archivo de audio no encontrado o expirado", + "noAiProcessing": "No se aplicó procesamiento de IA a esta transcripción", + "rawTranscript": "Transcripción sin procesar" }, "cloudMigration": { "title": "Bienvenido a OpenWhispr Pro", @@ -263,7 +277,8 @@ "bannerDescription": "Activa la aceleración GPU para un rendimiento más rápido.", "enableButton": "Activar GPU", "dismissButton": "Ahora no" - } + }, + "backToNotes": "Volver a notas" }, "reasoning": { "mode": { @@ -1145,7 +1160,12 @@ "autoHide": "Ocultar automáticamente cuando está inactivo", "autoHideDescription": "Mantén el ícono oculto hasta que comiences a dictar", "description": "Controla cuándo es visible el ícono de dictado en tu pantalla", - "title": "Ícono flotante" + "title": "Ícono flotante", + "startPosition": "Posición inicial", + "startPositionDescription": "Dónde aparece el panel de grabación de voz en la pantalla", + "bottomRight": "Inferior derecha", + "center": "Inferior centro", + "bottomLeft": "Inferior izquierda" }, "hotkey": { "activationMode": "Modo de activación", @@ -1236,8 +1256,12 @@ "step1Title": "Iniciar el daemon", "step1Desc": "Inicia ydotoold y habilítalo para que se ejecute en cada inicio de sesión.", "step2Title": "Verificar que está en ejecución" + }, + "xclip": { + "step1Title": "Instalar xclip" } - } + }, + "xclipDesc": "Herramienta de portapapeles para pegado en KDE Wayland (xclip o xsel)" }, "updates": { "badges": { @@ -1327,7 +1351,17 @@ "usageAnalytics": "Analíticas de uso", "usageAnalyticsDescription": "Ayúdanos a mejorar OpenWhispr compartiendo métricas de rendimiento anónimas. Nunca enviamos contenido de transcripciones, solo datos de tiempo y errores.", "dataRetention": "Retención de datos", - "dataRetentionDescription": "Almacenar transcripciones y audio localmente en tu historial. Cuando está desactivado, las transcripciones se pegan pero no se guardan." + "dataRetentionDescription": "Almacenar transcripciones y audio localmente en tu historial. Cuando está desactivado, las transcripciones se pegan pero no se guardan.", + "audioRetention": "Retención de audio", + "audioRetentionDescription": "Almacena grabaciones de audio localmente para volver a transcribir y descargar. Los archivos se eliminan automáticamente tras el periodo de retención.", + "audioRetentionDisabled": "Desactivado", + "audioRetentionDays": "{{count}} días", + "audioStorageUsage": "Uso de almacenamiento", + "audioStorageUsageDescription": "Archivos de audio almacenados en este dispositivo", + "audioStorageFiles": "{{count}} archivos, {{size}}", + "audioStorageEmpty": "No hay archivos de audio almacenados", + "clearAllAudio": "Eliminar todo el audio", + "clearAllAudioConfirm": "¿Eliminar todos los archivos de audio almacenados? Esta acción no se puede deshacer." }, "prompts": { "description": "Visualiza, personaliza y prueba el prompt de sistema unificado que impulsa la corrección de texto y la detección de instrucciones", @@ -1663,6 +1697,10 @@ }, "enhance": { "title": "Error de mejora" + }, + "speaker": { + "you": "Tú", + "them": "Ellos" } }, "dictionary": { diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index 681b159f4..de02ec63c 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -78,7 +78,8 @@ "systemSettings": { "microphone": "Veuillez ouvrir les paramètres système pour configurer les permissions du microphone.", "sound": "Veuillez ouvrir les paramètres son du système (par exemple, pavucontrol).", - "accessibility": "Les paramètres d'accessibilité ne sont pas disponibles sur cette plateforme." + "accessibility": "Les paramètres d'accessibilité ne sont pas disponibles sur cette plateforme.", + "systemAudio": "Les paramètres audio système ne sont pas disponibles sur cette plateforme." }, "titleBar": { "quitTitle": "Quitter OpenWhispr", @@ -245,7 +246,20 @@ "clearAllDescription": "Toutes les transcriptions et fichiers audio seront définitivement supprimés.", "clearAllSuccess": "Historique effacé", "clearAllErrorTitle": "Impossible d'effacer l'historique", - "clearAllErrorDescription": "Une erreur est survenue. Veuillez réessayer." + "clearAllErrorDescription": "Une erreur est survenue. Veuillez réessayer.", + "copyText": "Copier le texte", + "deleteItem": "Supprimer", + "viewRawTranscript": "Voir la transcription brute", + "showInFolder": "Afficher dans le Finder", + "showInFolderWindows": "Afficher dans l'Explorateur", + "showInFolderLinux": "Afficher dans le gestionnaire de fichiers", + "retryTranscription": "Retranscrire", + "retrying": "Retranscription en cours…", + "retrySuccess": "Transcription mise à jour", + "retryError": "Échec de la retranscription", + "audioNotFound": "Fichier audio introuvable ou expiré", + "noAiProcessing": "Aucun traitement IA n'a été appliqué à cette transcription", + "rawTranscript": "Transcription brute" }, "cloudMigration": { "title": "Bienvenue sur OpenWhispr Pro", @@ -263,7 +277,8 @@ "bannerDescription": "Activez l'accélération GPU pour de meilleures performances.", "enableButton": "Activer le GPU", "dismissButton": "Pas maintenant" - } + }, + "backToNotes": "Retour aux notes" }, "reasoning": { "mode": { @@ -1145,7 +1160,12 @@ "autoHide": "Masquer automatiquement au repos", "autoHideDescription": "Garder l'icône masquée jusqu'au début de la dictée", "description": "Contrôlez quand l'icône de dictée est visible à l'écran", - "title": "Icône flottante" + "title": "Icône flottante", + "startPosition": "Position de départ", + "startPositionDescription": "Emplacement du panneau d'enregistrement vocal à l'écran", + "bottomRight": "En bas à droite", + "center": "En bas au centre", + "bottomLeft": "En bas à gauche" }, "hotkey": { "activationMode": "Mode d'activation", @@ -1236,8 +1256,12 @@ "step1Title": "Démarrer le daemon", "step1Desc": "Démarrez ydotoold et activez-le pour qu'il se lance à chaque connexion.", "step2Title": "Vérifier qu'il est en cours d'exécution" + }, + "xclip": { + "step1Title": "Installer xclip" } - } + }, + "xclipDesc": "Outil de presse-papiers pour le collage KDE Wayland (xclip ou xsel)" }, "updates": { "badges": { @@ -1327,7 +1351,17 @@ "usageAnalytics": "Analyse d'utilisation", "usageAnalyticsDescription": "Aidez-nous à améliorer OpenWhispr en partageant des métriques de performance anonymes. Nous n'envoyons jamais le contenu de vos transcriptions — uniquement des données de performance et d'erreurs.", "dataRetention": "Conservation des données", - "dataRetentionDescription": "Stocker les transcriptions et l'audio localement dans votre historique. Lorsque désactivé, les transcriptions sont collées mais pas enregistrées." + "dataRetentionDescription": "Stocker les transcriptions et l'audio localement dans votre historique. Lorsque désactivé, les transcriptions sont collées mais pas enregistrées.", + "audioRetention": "Conservation de l'audio", + "audioRetentionDescription": "Stocke les enregistrements audio localement pour la retranscription et le téléchargement. Les fichiers sont automatiquement supprimés après la période de conservation.", + "audioRetentionDisabled": "Désactivé", + "audioRetentionDays": "{{count}} jours", + "audioStorageUsage": "Espace utilisé", + "audioStorageUsageDescription": "Fichiers audio stockés sur cet appareil", + "audioStorageFiles": "{{count}} fichiers, {{size}}", + "audioStorageEmpty": "Aucun fichier audio stocké", + "clearAllAudio": "Supprimer tous les fichiers audio", + "clearAllAudioConfirm": "Supprimer tous les fichiers audio stockés ? Cette action est irréversible." }, "prompts": { "description": "Affichez, personnalisez et testez le prompt système unifié qui gère la correction du texte et la détection des instructions", @@ -1701,6 +1735,10 @@ }, "enhance": { "title": "Erreur d'amélioration" + }, + "speaker": { + "you": "Vous", + "them": "Eux" } }, "dictionary": { diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index fcc21aec9..96f3e6b79 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -78,7 +78,8 @@ "systemSettings": { "microphone": "Apri le impostazioni di sistema per configurare i permessi del microfono.", "sound": "Apri le impostazioni audio di sistema (ad esempio, pavucontrol).", - "accessibility": "Le impostazioni di accessibilità non sono disponibili su questa piattaforma." + "accessibility": "Le impostazioni di accessibilità non sono disponibili su questa piattaforma.", + "systemAudio": "Le impostazioni audio di sistema non sono disponibili su questa piattaforma." }, "titleBar": { "quitTitle": "Esci da OpenWhispr", @@ -245,7 +246,20 @@ "clearAllDescription": "Tutte le trascrizioni e i file audio verranno rimossi definitivamente.", "clearAllSuccess": "Cronologia cancellata", "clearAllErrorTitle": "Impossibile cancellare la cronologia", - "clearAllErrorDescription": "Qualcosa è andato storto. Riprova." + "clearAllErrorDescription": "Qualcosa è andato storto. Riprova.", + "copyText": "Copia testo", + "deleteItem": "Elimina", + "viewRawTranscript": "Visualizza trascrizione grezza", + "showInFolder": "Mostra nel Finder", + "showInFolderWindows": "Mostra in Esplora file", + "showInFolderLinux": "Mostra nel gestore file", + "retryTranscription": "Ritrascrivi", + "retrying": "Ritrascrizione in corso…", + "retrySuccess": "Trascrizione aggiornata", + "retryError": "Ritrascrizione non riuscita", + "audioNotFound": "File audio non trovato o scaduto", + "noAiProcessing": "Nessuna elaborazione IA applicata a questa trascrizione", + "rawTranscript": "Trascrizione grezza" }, "cloudMigration": { "title": "Benvenuto in OpenWhispr Pro", @@ -263,7 +277,8 @@ "bannerDescription": "Attiva l'accelerazione GPU per prestazioni più veloci.", "enableButton": "Attiva GPU", "dismissButton": "Non ora" - } + }, + "backToNotes": "Torna alle note" }, "reasoning": { "mode": { @@ -1145,7 +1160,12 @@ "autoHide": "Nascondi automaticamente quando inattivo", "autoHideDescription": "Mantieni l'icona nascosta finché non inizi a dettare", "description": "Controlla quando l'icona di dettatura è visibile sullo schermo", - "title": "Icona flottante" + "title": "Icona flottante", + "startPosition": "Posizione iniziale", + "startPositionDescription": "Dove appare il pannello di registrazione vocale sullo schermo", + "bottomRight": "In basso a destra", + "center": "In basso al centro", + "bottomLeft": "In basso a sinistra" }, "hotkey": { "activationMode": "Modalità di attivazione", @@ -1236,8 +1256,12 @@ "step1Title": "Avvia il daemon", "step1Desc": "Avvia ydotoold e abilitalo per l'esecuzione a ogni accesso.", "step2Title": "Verifica che sia in esecuzione" + }, + "xclip": { + "step1Title": "Installa xclip" } - } + }, + "xclipDesc": "Strumento appunti per incollare in KDE Wayland (xclip o xsel)" }, "updates": { "badges": { @@ -1327,7 +1351,17 @@ "usageAnalytics": "Analisi dell'utilizzo", "usageAnalyticsDescription": "Aiutaci a migliorare OpenWhispr condividendo metriche di prestazione anonime. Non inviamo mai il contenuto delle trascrizioni, solo dati su tempi ed errori.", "dataRetention": "Conservazione dei dati", - "dataRetentionDescription": "Salva trascrizioni e audio localmente nella cronologia. Se disattivato, le trascrizioni vengono incollate ma non salvate." + "dataRetentionDescription": "Salva trascrizioni e audio localmente nella cronologia. Se disattivato, le trascrizioni vengono incollate ma non salvate.", + "audioRetention": "Conservazione audio", + "audioRetentionDescription": "Salva le registrazioni audio localmente per la ritrascrizione e il download. I file vengono eliminati automaticamente al termine del periodo di conservazione.", + "audioRetentionDisabled": "Disattivato", + "audioRetentionDays": "{{count}} giorni", + "audioStorageUsage": "Spazio utilizzato", + "audioStorageUsageDescription": "File audio salvati su questo dispositivo", + "audioStorageFiles": "{{count}} file, {{size}}", + "audioStorageEmpty": "Nessun file audio salvato", + "clearAllAudio": "Elimina tutti i file audio", + "clearAllAudioConfirm": "Eliminare tutti i file audio salvati? Questa azione è irreversibile." }, "prompts": { "description": "Visualizza, personalizza e testa il prompt di sistema unificato che gestisce la correzione del testo e il rilevamento delle istruzioni", @@ -1663,6 +1697,10 @@ }, "enhance": { "title": "Errore di miglioramento" + }, + "speaker": { + "you": "Tu", + "them": "Loro" } }, "dictionary": { diff --git a/src/locales/ja/translation.json b/src/locales/ja/translation.json index e6dabd1e2..abe86df3c 100644 --- a/src/locales/ja/translation.json +++ b/src/locales/ja/translation.json @@ -78,7 +78,8 @@ "systemSettings": { "microphone": "システム設定を開いてマイクの権限を設定してください。", "sound": "システムのサウンド設定を開いてください(例: pavucontrol)。", - "accessibility": "このプラットフォームではアクセシビリティ設定を利用できません。" + "accessibility": "このプラットフォームではアクセシビリティ設定を利用できません。", + "systemAudio": "システムオーディオの設定はこのプラットフォームでは利用できません。" }, "titleBar": { "quitTitle": "OpenWhispr を終了", @@ -245,7 +246,20 @@ "clearAllDescription": "すべての文字起こしと音声ファイルが完全に削除されます。", "clearAllSuccess": "履歴を削除しました", "clearAllErrorTitle": "履歴を削除できませんでした", - "clearAllErrorDescription": "問題が発生しました。もう一度お試しください。" + "clearAllErrorDescription": "問題が発生しました。もう一度お試しください。", + "copyText": "テキストをコピー", + "deleteItem": "削除", + "viewRawTranscript": "生の文字起こしを表示", + "showInFolder": "Finderで表示", + "showInFolderWindows": "エクスプローラーで表示", + "showInFolderLinux": "ファイルマネージャーで表示", + "retryTranscription": "再文字起こし", + "retrying": "再文字起こし中…", + "retrySuccess": "文字起こしが更新されました", + "retryError": "再文字起こしに失敗しました", + "audioNotFound": "音声ファイルが見つからないか有効期限切れです", + "noAiProcessing": "この文字起こしにはAI処理が適用されていません", + "rawTranscript": "生の文字起こし" }, "cloudMigration": { "title": "OpenWhispr Pro へようこそ", @@ -263,7 +277,8 @@ "bannerDescription": "GPU アクセラレーションを有効にしてパフォーマンスを向上させましょう。", "enableButton": "GPU を有効化", "dismissButton": "後で" - } + }, + "backToNotes": "ノートに戻る" }, "reasoning": { "mode": { @@ -1145,7 +1160,12 @@ "autoHide": "使用していないときに自動で非表示", "autoHideDescription": "ディクテーション開始までアイコンを非表示にする", "description": "画面上のディクテーションアイコンの表示を制御", - "title": "フローティングアイコン" + "title": "フローティングアイコン", + "startPosition": "開始位置", + "startPositionDescription": "音声レコーダーパネルが画面上に表示される位置", + "bottomRight": "右下", + "center": "下中央", + "bottomLeft": "左下" }, "hotkey": { "activationMode": "起動モード", @@ -1236,8 +1256,12 @@ "step1Title": "デーモンを起動", "step1Desc": "ydotoold を起動し、毎回のログイン時に実行されるよう有効化します。", "step2Title": "実行中か確認" + }, + "xclip": { + "step1Title": "xclipをインストール" } - } + }, + "xclipDesc": "KDE Waylandの貼り付け用クリップボードツール(xclipまたはxsel)" }, "updates": { "badges": { @@ -1327,7 +1351,17 @@ "usageAnalytics": "使用状況の分析", "usageAnalyticsDescription": "匿名のパフォーマンスデータを共有して OpenWhispr の改善にご協力ください。文字起こしの内容は送信されません。タイミングとエラーデータのみです。", "dataRetention": "データ保持", - "dataRetentionDescription": "文字起こしと音声を履歴にローカル保存します。無効にすると、文字起こしは貼り付けられますが保存されません。" + "dataRetentionDescription": "文字起こしと音声を履歴にローカル保存します。無効にすると、文字起こしは貼り付けられますが保存されません。", + "audioRetention": "音声の保持", + "audioRetentionDescription": "再文字起こしやダウンロードのために音声録音をローカルに保存します。ファイルは保持期間が過ぎると自動的に削除されます。", + "audioRetentionDisabled": "無効", + "audioRetentionDays": "{{count}}日", + "audioStorageUsage": "ストレージ使用量", + "audioStorageUsageDescription": "このデバイスに保存された音声ファイル", + "audioStorageFiles": "{{count}}ファイル、{{size}}", + "audioStorageEmpty": "保存された音声ファイルはありません", + "clearAllAudio": "すべての音声を削除", + "clearAllAudioConfirm": "保存されたすべての音声ファイルを削除しますか?この操作は元に戻せません。" }, "prompts": { "description": "テキスト補正と指示検出を行う統合システムプロンプトの表示、カスタマイズ、テスト", @@ -1591,6 +1625,10 @@ }, "enhance": { "title": "強化エラー" + }, + "speaker": { + "you": "あなた", + "them": "相手" } }, "dictionary": { diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json index 0a0778589..14bca2e7a 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -78,7 +78,8 @@ "systemSettings": { "microphone": "Abra as configurações do sistema para configurar as permissões de microfone.", "sound": "Abra as configurações de som do sistema (por exemplo, pavucontrol).", - "accessibility": "As configurações de acessibilidade não estão disponíveis nesta plataforma." + "accessibility": "As configurações de acessibilidade não estão disponíveis nesta plataforma.", + "systemAudio": "As configurações de áudio do sistema não estão disponíveis nesta plataforma." }, "windows": { "pttUnavailable": "O listener nativo de Push-to-Talk não está disponível" @@ -217,7 +218,20 @@ "clearAllDescription": "Todas as transcrições e arquivos de áudio serão removidos permanentemente.", "clearAllSuccess": "Histórico limpo", "clearAllErrorTitle": "Não foi possível limpar o histórico", - "clearAllErrorDescription": "Algo deu errado. Tente novamente." + "clearAllErrorDescription": "Algo deu errado. Tente novamente.", + "copyText": "Copiar texto", + "deleteItem": "Excluir", + "viewRawTranscript": "Ver transcrição bruta", + "showInFolder": "Mostrar no Finder", + "showInFolderWindows": "Mostrar no Explorador", + "showInFolderLinux": "Mostrar no gerenciador de arquivos", + "retryTranscription": "Retranscrever", + "retrying": "Retranscrevendo…", + "retrySuccess": "Transcrição atualizada", + "retryError": "Falha ao retranscrever", + "audioNotFound": "Arquivo de áudio não encontrado ou expirado", + "noAiProcessing": "Nenhum processamento de IA foi aplicado a esta transcrição", + "rawTranscript": "Transcrição bruta" }, "cloudMigration": { "title": "Boas-vindas ao OpenWhispr Pro", @@ -235,7 +249,8 @@ "bannerDescription": "Ative a aceleração GPU para um desempenho mais rápido.", "enableButton": "Ativar GPU", "dismissButton": "Agora não" - } + }, + "backToNotes": "Voltar para notas" }, "reasoning": { "mode": { @@ -1117,7 +1132,12 @@ "autoHide": "Ocultar automaticamente quando ocioso", "autoHideDescription": "Mantenha o ícone oculto até iniciar o ditado", "description": "Controle quando o ícone de ditado fica visível na tela", - "title": "Ícone flutuante" + "title": "Ícone flutuante", + "startPosition": "Posição inicial", + "startPositionDescription": "Onde o painel de gravação de voz aparece na tela", + "bottomRight": "Inferior direito", + "center": "Inferior central", + "bottomLeft": "Inferior esquerdo" }, "hotkey": { "activationMode": "Modo de ativação", @@ -1208,8 +1228,12 @@ "step1Title": "Iniciar o daemon", "step1Desc": "Inicie o ydotoold e ative-o para que seja executado em cada início de sessão.", "step2Title": "Verificar se está em execução" + }, + "xclip": { + "step1Title": "Instalar xclip" } - } + }, + "xclipDesc": "Ferramenta de área de transferência para colar no KDE Wayland (xclip ou xsel)" }, "updates": { "badges": { @@ -1299,7 +1323,17 @@ "usageAnalytics": "Análise de uso", "usageAnalyticsDescription": "Ajude-nos a melhorar o OpenWhispr compartilhando métricas anônimas de desempenho. Nunca enviamos conteúdo de transcrição — apenas dados de tempo e erros.", "dataRetention": "Retenção de dados", - "dataRetentionDescription": "Armazenar transcrições e áudio localmente no seu histórico. Quando desativado, as transcrições são coladas mas não salvas." + "dataRetentionDescription": "Armazenar transcrições e áudio localmente no seu histórico. Quando desativado, as transcrições são coladas mas não salvas.", + "audioRetention": "Retenção de áudio", + "audioRetentionDescription": "Armazena gravações de áudio localmente para retranscrição e download. Os arquivos são excluídos automaticamente após o período de retenção.", + "audioRetentionDisabled": "Desativado", + "audioRetentionDays": "{{count}} dias", + "audioStorageUsage": "Uso de armazenamento", + "audioStorageUsageDescription": "Arquivos de áudio armazenados neste dispositivo", + "audioStorageFiles": "{{count}} arquivos, {{size}}", + "audioStorageEmpty": "Nenhum arquivo de áudio armazenado", + "clearAllAudio": "Excluir todos os áudios", + "clearAllAudioConfirm": "Excluir todos os arquivos de áudio armazenados? Esta ação não pode ser desfeita." }, "prompts": { "description": "Visualize, personalize e teste o prompt de sistema unificado que alimenta a limpeza de texto e a detecção de instruções", @@ -1663,6 +1697,10 @@ }, "enhance": { "title": "Erro de aprimoramento" + }, + "speaker": { + "you": "Você", + "them": "Outro" } }, "dictionary": { diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 41787930f..167b1c81a 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -78,7 +78,8 @@ "systemSettings": { "microphone": "Откройте системные настройки для настройки разрешений микрофона.", "sound": "Откройте системные настройки звука (например, pavucontrol).", - "accessibility": "Настройки универсального доступа недоступны на этой платформе." + "accessibility": "Настройки универсального доступа недоступны на этой платформе.", + "systemAudio": "Настройки системного аудио недоступны на этой платформе." }, "titleBar": { "quitTitle": "Закрыть OpenWhispr", @@ -245,7 +246,20 @@ "clearAllDescription": "Все транскрипции и аудиофайлы будут безвозвратно удалены.", "clearAllSuccess": "История очищена", "clearAllErrorTitle": "Не удалось очистить историю", - "clearAllErrorDescription": "Что-то пошло не так. Попробуйте ещё раз." + "clearAllErrorDescription": "Что-то пошло не так. Попробуйте ещё раз.", + "copyText": "Копировать текст", + "deleteItem": "Удалить", + "viewRawTranscript": "Показать исходную расшифровку", + "showInFolder": "Показать в Finder", + "showInFolderWindows": "Показать в Проводнике", + "showInFolderLinux": "Показать в файловом менеджере", + "retryTranscription": "Повторить расшифровку", + "retrying": "Повторная расшифровка…", + "retrySuccess": "Расшифровка обновлена", + "retryError": "Не удалось повторить расшифровку", + "audioNotFound": "Аудиофайл не найден или истёк срок хранения", + "noAiProcessing": "К этой расшифровке не применялась обработка ИИ", + "rawTranscript": "Исходная расшифровка" }, "cloudMigration": { "title": "Добро пожаловать в OpenWhispr Pro", @@ -263,7 +277,8 @@ "bannerDescription": "Включите ускорение GPU для повышения производительности.", "enableButton": "Включить GPU", "dismissButton": "Не сейчас" - } + }, + "backToNotes": "Назад к заметкам" }, "reasoning": { "mode": { @@ -1145,7 +1160,12 @@ "autoHide": "Скрывать при бездействии", "autoHideDescription": "Значок будет скрыт, пока вы не начнёте диктовку", "description": "Управляйте видимостью значка диктовки на экране", - "title": "Плавающий значок" + "title": "Плавающий значок", + "startPosition": "Начальная позиция", + "startPositionDescription": "Где на экране появляется панель записи голоса", + "bottomRight": "Внизу справа", + "center": "Внизу по центру", + "bottomLeft": "Внизу слева" }, "hotkey": { "activationMode": "Режим активации", @@ -1236,8 +1256,12 @@ "step1Title": "Запустить демон", "step1Desc": "Запустите ydotoold и включите его автозапуск при каждом входе.", "step2Title": "Проверить, что он работает" + }, + "xclip": { + "step1Title": "Установить xclip" } - } + }, + "xclipDesc": "Инструмент буфера обмена для вставки в KDE Wayland (xclip или xsel)" }, "updates": { "badges": { @@ -1327,7 +1351,17 @@ "usageAnalytics": "Аналитика использования", "usageAnalyticsDescription": "Помогите нам улучшить OpenWhispr, делясь анонимными метриками производительности. Мы никогда не отправляем содержимое транскрипций — только данные о времени и ошибках.", "dataRetention": "Хранение данных", - "dataRetentionDescription": "Сохранять транскрипции и аудио локально в истории. При отключении транскрипции вставляются, но не сохраняются." + "dataRetentionDescription": "Сохранять транскрипции и аудио локально в истории. При отключении транскрипции вставляются, но не сохраняются.", + "audioRetention": "Хранение аудио", + "audioRetentionDescription": "Сохранять аудиозаписи локально для повторной расшифровки и скачивания. Файлы автоматически удаляются по истечении срока хранения.", + "audioRetentionDisabled": "Отключено", + "audioRetentionDays": "{{count}} дн.", + "audioStorageUsage": "Использование хранилища", + "audioStorageUsageDescription": "Аудиофайлы, сохранённые на этом устройстве", + "audioStorageFiles": "{{count}} файлов, {{size}}", + "audioStorageEmpty": "Аудиофайлы отсутствуют", + "clearAllAudio": "Удалить все аудиофайлы", + "clearAllAudioConfirm": "Удалить все сохранённые аудиофайлы? Это действие нельзя отменить." }, "prompts": { "description": "Просматривайте, настраивайте и тестируйте единый системный промпт, который управляет очисткой текста и обнаружением инструкций", @@ -1663,6 +1697,10 @@ }, "enhance": { "title": "Ошибка улучшения" + }, + "speaker": { + "you": "Вы", + "them": "Собеседник" } }, "dictionary": { diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index a022d35f0..0115def16 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -78,7 +78,8 @@ "systemSettings": { "microphone": "请打开系统设置以配置麦克风权限。", "sound": "请打开系统声音设置(例如 pavucontrol)。", - "accessibility": "此平台不支持辅助功能设置。" + "accessibility": "此平台不支持辅助功能设置。", + "systemAudio": "系统音频设置在此平台上不可用。" }, "titleBar": { "quitTitle": "退出 OpenWhispr", @@ -245,7 +246,20 @@ "clearAllDescription": "所有转录和音频文件将被永久删除。", "clearAllSuccess": "历史记录已清除", "clearAllErrorTitle": "无法清除历史记录", - "clearAllErrorDescription": "出了点问题,请重试。" + "clearAllErrorDescription": "出了点问题,请重试。", + "copyText": "复制文本", + "deleteItem": "删除", + "viewRawTranscript": "查看原始转录", + "showInFolder": "在访达中显示", + "showInFolderWindows": "在资源管理器中显示", + "showInFolderLinux": "在文件管理器中显示", + "retryTranscription": "重新转录", + "retrying": "正在重新转录…", + "retrySuccess": "转录已更新", + "retryError": "重新转录失败", + "audioNotFound": "音频文件未找到或已过期", + "noAiProcessing": "此转录未应用 AI 处理", + "rawTranscript": "原始转录" }, "cloudMigration": { "title": "欢迎使用 OpenWhispr Pro", @@ -263,7 +277,8 @@ "bannerDescription": "启用 GPU 加速以获得更快的性能。", "enableButton": "启用 GPU", "dismissButton": "暂不启用" - } + }, + "backToNotes": "返回笔记" }, "reasoning": { "mode": { @@ -1145,7 +1160,12 @@ "autoHide": "空闲时自动隐藏", "autoHideDescription": "在开始听写前保持图标隐藏", "description": "控制听写图标在屏幕上的显示", - "title": "悬浮图标" + "title": "悬浮图标", + "startPosition": "起始位置", + "startPositionDescription": "语音录制面板在屏幕上的显示位置", + "bottomRight": "右下角", + "center": "底部居中", + "bottomLeft": "左下角" }, "hotkey": { "activationMode": "激活模式", @@ -1236,8 +1256,12 @@ "step1Title": "启动守护进程", "step1Desc": "启动 ydotoold 并设置为每次登录时自动运行。", "step2Title": "验证是否正在运行" + }, + "xclip": { + "step1Title": "安装 xclip" } - } + }, + "xclipDesc": "KDE Wayland 粘贴用的剪贴板工具(xclip 或 xsel)" }, "updates": { "badges": { @@ -1327,7 +1351,17 @@ "usageAnalytics": "使用分析", "usageAnalyticsDescription": "通过分享匿名性能数据帮助我们改进 OpenWhispr。我们绝不发送转录内容——仅发送计时和错误数据。", "dataRetention": "数据保留", - "dataRetentionDescription": "将转录和音频本地保存到历史记录中。禁用后,转录内容会被粘贴但不会被保存。" + "dataRetentionDescription": "将转录和音频本地保存到历史记录中。禁用后,转录内容会被粘贴但不会被保存。", + "audioRetention": "音频保留", + "audioRetentionDescription": "在本地存储音频录制文件以便重新转录和下载。文件会在保留期过后自动删除。", + "audioRetentionDisabled": "已禁用", + "audioRetentionDays": "{{count}} 天", + "audioStorageUsage": "存储用量", + "audioStorageUsageDescription": "存储在此设备上的音频文件", + "audioStorageFiles": "{{count}} 个文件,{{size}}", + "audioStorageEmpty": "没有存储的音频文件", + "clearAllAudio": "清除所有音频", + "clearAllAudioConfirm": "删除所有已存储的音频文件?此操作无法撤销。" }, "prompts": { "description": "查看、自定义和测试用于文本整理和指令识别的统一系统提示词", @@ -1663,6 +1697,10 @@ }, "enhance": { "title": "增强错误" + }, + "speaker": { + "you": "你", + "them": "对方" } }, "dictionary": { diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json index 96253d625..0447dd8b8 100644 --- a/src/locales/zh-TW/translation.json +++ b/src/locales/zh-TW/translation.json @@ -78,7 +78,8 @@ "systemSettings": { "microphone": "請開啟系統設定來配置麥克風權限。", "sound": "請開啟系統音效設定(例如 pavucontrol)。", - "accessibility": "此平台不支援輔助使用設定。" + "accessibility": "此平台不支援輔助使用設定。", + "systemAudio": "系統音訊設定在此平台上不可用。" }, "titleBar": { "quitTitle": "結束 OpenWhispr", @@ -245,7 +246,20 @@ "clearAllDescription": "所有轉錄和音訊檔案將被永久刪除。", "clearAllSuccess": "歷史紀錄已清除", "clearAllErrorTitle": "無法清除歷史紀錄", - "clearAllErrorDescription": "發生錯誤,請重試。" + "clearAllErrorDescription": "發生錯誤,請重試。", + "copyText": "複製文字", + "deleteItem": "刪除", + "viewRawTranscript": "檢視原始轉錄", + "showInFolder": "在 Finder 中顯示", + "showInFolderWindows": "在檔案總管中顯示", + "showInFolderLinux": "在檔案管理員中顯示", + "retryTranscription": "重新轉錄", + "retrying": "正在重新轉錄…", + "retrySuccess": "轉錄已更新", + "retryError": "重新轉錄失敗", + "audioNotFound": "找不到音訊檔案或檔案已過期", + "noAiProcessing": "此轉錄未套用 AI 處理", + "rawTranscript": "原始轉錄" }, "cloudMigration": { "title": "歡迎使用 OpenWhispr Pro", @@ -263,7 +277,8 @@ "bannerDescription": "啟用 GPU 加速以獲得更快的效能。", "enableButton": "啟用 GPU", "dismissButton": "暫時不要" - } + }, + "backToNotes": "返回筆記" }, "reasoning": { "mode": { @@ -1145,7 +1160,12 @@ "autoHide": "閒置時自動隱藏", "autoHideDescription": "在開始語音輸入前保持圖示隱藏", "description": "控制語音輸入圖示在畫面上的顯示時機", - "title": "浮動圖示" + "title": "浮動圖示", + "startPosition": "起始位置", + "startPositionDescription": "語音錄製面板在螢幕上的顯示位置", + "bottomRight": "右下角", + "center": "底部置中", + "bottomLeft": "左下角" }, "hotkey": { "activationMode": "啟用模式", @@ -1236,8 +1256,12 @@ "step1Title": "啟動常駐程式", "step1Desc": "啟動 ydotoold 並設定為每次登入時自動執行。", "step2Title": "驗證是否正在執行" + }, + "xclip": { + "step1Title": "安裝 xclip" } - } + }, + "xclipDesc": "KDE Wayland 貼上用的剪貼簿工具(xclip 或 xsel)" }, "updates": { "badges": { @@ -1327,7 +1351,17 @@ "usageAnalytics": "使用分析", "usageAnalyticsDescription": "分享匿名的效能指標,協助我們改善 OpenWhispr。我們絕不會傳送轉錄內容——只有計時和錯誤資料。", "dataRetention": "資料保留", - "dataRetentionDescription": "將轉錄和音訊本地儲存至歷史記錄。停用後,轉錄內容會被貼上但不會被儲存。" + "dataRetentionDescription": "將轉錄和音訊本地儲存至歷史記錄。停用後,轉錄內容會被貼上但不會被儲存。", + "audioRetention": "音訊保留", + "audioRetentionDescription": "將音訊錄製檔案儲存在本機以便重新轉錄和下載。檔案會在保留期限過後自動刪除。", + "audioRetentionDisabled": "已停用", + "audioRetentionDays": "{{count}} 天", + "audioStorageUsage": "儲存空間用量", + "audioStorageUsageDescription": "儲存在此裝置上的音訊檔案", + "audioStorageFiles": "{{count}} 個檔案,{{size}}", + "audioStorageEmpty": "沒有已儲存的音訊檔案", + "clearAllAudio": "清除所有音訊", + "clearAllAudioConfirm": "刪除所有已儲存的音訊檔案?此操作無法復原。" }, "prompts": { "description": "檢視、自訂和測試用於文字整理和指令偵測的統一系統提示詞", @@ -1663,6 +1697,10 @@ }, "enhance": { "title": "增強錯誤" + }, + "speaker": { + "you": "你", + "them": "對方" } }, "dictionary": { diff --git a/src/main.jsx b/src/main.jsx index bbb900f9d..f73406cee 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -74,7 +74,6 @@ function isOAuthBrowserRedirect() { window.location.href = `${OAUTH_PROTOCOL}://auth/callback?neon_auth_session_verifier=${encodeURIComponent(verifier)}`; }, 2000); - // Show an ultra-premium branded message while waiting document.body.innerHTML = `