@@ -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 = `