diff --git a/src/components/ai-edition/LeftPanel.copyMessage.test.tsx b/src/components/ai-edition/LeftPanel.copyMessage.test.tsx index be2ab4e01..3c39fdfae 100644 --- a/src/components/ai-edition/LeftPanel.copyMessage.test.tsx +++ b/src/components/ai-edition/LeftPanel.copyMessage.test.tsx @@ -154,25 +154,31 @@ async function renderPanelWithAssistantMessage() { useProjectStore.setState({ projectId: "project-1" }); const { EditorDialogsProvider } = await import("@/contexts/EditorDialogsContext"); const { ChatStripPanel } = await import("./LeftPanel"); + const { TooltipProvider } = await import("@/components/ui/tooltip"); const view = render( - - - , + + + + + , ); // Flush the session load so the assistant bubble (and its copy button) is on screen. await waitFor(() => { - expect(view.getAllByTitle("chat.copyMessage").length).toBeGreaterThan(0); + expect(view.getAllByRole("button", { name: "chat.copyMessage" }).length).toBeGreaterThan(0); }); return view; } +const copyButtonsOf = (view: ReturnType) => + view.getAllByRole("button", { name: "chat.copyMessage" }); + describe("ChatStripPanel Copy message (issue #738)", () => { it("copies the full message through the preload bridge and reports success, never touching the denied navigator clipboard", async () => { stubClipboard(copyToClipboard); const view = await renderPanelWithAssistantMessage(); // Two copy buttons (user + assistant); the assistant message is the last one rendered. - const copyButtons = view.getAllByTitle("chat.copyMessage"); + const copyButtons = copyButtonsOf(view); // user + assistant + the empty trailing assistant turn. expect(copyButtons).toHaveLength(3); await act(async () => { @@ -193,7 +199,7 @@ describe("ChatStripPanel Copy message (issue #738)", () => { stubClipboard(undefined); const view = await renderPanelWithAssistantMessage(); - const copyButtons = view.getAllByTitle("chat.copyMessage"); + const copyButtons = copyButtonsOf(view); // The fallback inherits Electron's denial in a bare context: the click // lands on the navigator path and reports the failure toast. await act(async () => { @@ -229,7 +235,7 @@ describe("ChatStripPanel Copy message (issue #738)", () => { // copy button sits last. The handler must not branch on content — an // empty string crosses to the bridge exactly like any other content and // still reports success. - const copyButtons = view.getAllByTitle("chat.copyMessage"); + const copyButtons = copyButtonsOf(view); await act(async () => { fireEvent.click(copyButtons[copyButtons.length - 1]); await Promise.resolve(); diff --git a/src/components/ai-edition/LeftPanel.providerRefresh.test.tsx b/src/components/ai-edition/LeftPanel.providerRefresh.test.tsx index f14a8d73c..0501e5093 100644 --- a/src/components/ai-edition/LeftPanel.providerRefresh.test.tsx +++ b/src/components/ai-edition/LeftPanel.providerRefresh.test.tsx @@ -45,6 +45,7 @@ vi.mock("@/contexts/I18nContext", () => ({ useScopedT: () => (key: string) => key, })); +import { TooltipProvider } from "@/components/ui/tooltip"; import { EditorDialogsProvider, useEditorDialogActions } from "@/contexts/EditorDialogsContext"; import { ChatStripPanel } from "./LeftPanel"; @@ -80,10 +81,12 @@ afterEach(() => { describe("ChatStripPanel, against the lifted provider dialog", () => { it("re-reads the LLM snapshot when the dialog closes, and not when it opens", async () => { render( - - - - , + + + + + + , ); // Mount: the dialog is closed, so the same effect that watches for a close seeds the // composer's view of the provider config. diff --git a/src/components/ai-edition/LeftPanel.tooltips.test.tsx b/src/components/ai-edition/LeftPanel.tooltips.test.tsx new file mode 100644 index 000000000..9796e9b64 --- /dev/null +++ b/src/components/ai-edition/LeftPanel.tooltips.test.tsx @@ -0,0 +1,220 @@ +// @vitest-environment jsdom +// The chat panel's icon-only controls name themselves in the shared tooltip, not in a native +// `title`. Rewind and Compact say what they do, Send shows Enter as a chip, and a button that is +// greyed for now (Send with no provider, Compact with no conversation) still opens its tooltip: +// that is where "Set up a provider" is said, and a natively disabled button takes no pointer +// events, so it uses `aria-disabled`. + +import "@testing-library/jest-dom"; +import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const snapshot = vi.hoisted(() => ({ + value: { + config: null as { provider: string; model: string } | null, + connectedProviders: [] as string[], + availableProviders: [] as unknown[], + credentialSummary: [] as unknown[], + }, +})); +const sessions = vi.hoisted(() => ({ list: [] as unknown[] })); +const chatCompact = vi.hoisted(() => vi.fn()); + +vi.mock("@/native/client", () => ({ + nativeBridgeClient: { + aiEdition: { + llmGetSnapshot: () => Promise.resolve(snapshot.value), + chatListSessions: () => Promise.resolve(sessions.list), + chatSelectSession: () => + Promise.resolve({ + id: "session-1", + projectId: "project-1", + title: "Conversation 1", + createdAt: "2026-09-23T00:00:00Z", + updatedAt: "2026-09-23T00:00:00Z", + messages: [ + { + id: "m1", + role: "user", + content: "Cut the silences", + checkpointId: "cp1", + createdAt: "2026-09-23T00:00:01Z", + }, + { + id: "m2", + role: "assistant", + content: "Done", + createdAt: "2026-09-23T00:00:02Z", + }, + ], + }), + chatBudget: () => Promise.resolve(null), + chatCompact, + llmListProviderModels: () => Promise.resolve({ models: [] }), + }, + }, +})); + +vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn(), info: vi.fn() } })); + +// An echoing translator: the assertions read against keys, not prose that drifts with copy edits. +vi.mock("@/contexts/I18nContext", () => ({ + useI18n: () => ({ locale: "en", setLocale: vi.fn() }), + useScopedT: () => (key: string) => key, +})); + +import { TooltipProvider } from "@/components/ui/tooltip"; +import { EditorDialogsProvider } from "@/contexts/EditorDialogsContext"; +import { useProjectStore } from "@/lib/ai-edition/store/projectStore"; +import { ChatStripPanel } from "./LeftPanel"; + +class StubResizeObserver { + observe = vi.fn(); + unobserve = vi.fn(); + disconnect = vi.fn(); +} + +const oneSession = [ + { id: "session-1", title: "Conversation 1", messageCount: 2, createdAt: "2026-09-23T00:00:00Z" }, +]; + +beforeEach(() => { + vi.stubGlobal("ResizeObserver", StubResizeObserver); + // jsdom implements no scrolling, and the transcript pins itself to the bottom on every render. + Element.prototype.scrollTo = vi.fn(); + (window as unknown as { electronAPI?: unknown }).electronAPI = { + onAiEditionChatEvent: () => vi.fn(), + }; + useProjectStore.setState({ projectId: "project-1" }); + snapshot.value = { + config: null, + connectedProviders: [], + availableProviders: [], + credentialSummary: [], + }; + sessions.list = []; + chatCompact.mockClear(); +}); + +afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); + (window as unknown as { electronAPI?: unknown }).electronAPI = undefined; +}); + +function renderPanel() { + return render( + + + + + , + ); +} + +/** Opens a control's tooltip the way the keyboard does (focus opens it at once) and returns what + * it says and whether it shows a shortcut chip. */ +async function tooltipOn(control: HTMLElement) { + act(() => control.focus()); + const text = (await screen.findByRole("tooltip")).textContent; + const chip = document.querySelector('[data-slot="tooltip-content"] kbd')?.textContent ?? null; + act(() => control.blur()); + await waitFor(() => expect(screen.queryByRole("tooltip")).toBeNull()); + return { text, chip }; +} + +describe("ChatStripPanel tooltips", () => { + it("names the header buttons in the shared tooltip, with no native title", async () => { + sessions.list = oneSession; + renderPanel(); + await screen.findByRole("button", { name: "chat.deleteConversation" }); + + for (const name of [ + "chat.compactContext", + "chat.aiSettings", + "chat.history", + "chat.newConversation", + "chat.renameConversation", + "chat.deleteConversation", + ]) { + const button = screen.getByRole("button", { name }); + expect(button).not.toHaveAttribute("title"); + expect((await tooltipOn(button)).text).toBe(name); + } + }); + + // Plain text has no interactive element to hold a tooltip, so its hint stays native. + it("keeps the native title only on plain text", async () => { + sessions.list = oneSession; + renderPanel(); + expect(await screen.findByTitle("chat.clickToRename")).toBeInTheDocument(); + }); + + it("says what Rewind does, not only what the button is called", async () => { + sessions.list = oneSession; + renderPanel(); + const rewind = await screen.findByRole("button", { name: "chat.rewindToMessage" }); + expect(rewind).not.toHaveAttribute("title"); + expect((await tooltipOn(rewind)).text).toBe("chat.rewindTip"); + }); + + it("names Copy in the shared tooltip too", async () => { + sessions.list = oneSession; + renderPanel(); + const [copy] = await screen.findAllByRole("button", { name: "chat.copyMessage" }); + expect(copy).not.toHaveAttribute("title"); + expect((await tooltipOn(copy)).text).toBe("chat.copyMessage"); + }); + + // Nothing to talk to yet: Send is greyed, but it is where the reason is said. + it("says why Send is off when no provider is set up, and shows no chip then", async () => { + renderPanel(); + const send = await screen.findByRole("button", { name: "chat.send" }); + await waitFor(() => expect(send).toHaveAttribute("aria-disabled", "true")); + expect(send).not.toBeDisabled(); + expect(send).not.toHaveAttribute("title"); + const tip = await tooltipOn(send); + expect(tip.text).toBe("chat.composerDisabledNoProvider"); + expect(tip.chip).toBeNull(); + }); + + it("shows Enter as a chip on Send once it can send, never inside the string", async () => { + snapshot.value = { + config: { provider: "openai", model: "gpt-x" }, + connectedProviders: ["openai"], + availableProviders: [], + credentialSummary: [], + }; + renderPanel(); + fireEvent.change(await screen.findByPlaceholderText("chat.composerPlaceholder"), { + target: { value: "Cut the silences" }, + }); + const send = screen.getByRole("button", { name: "chat.send" }); + await waitFor(() => expect(send).not.toHaveAttribute("aria-disabled")); + const tip = await tooltipOn(send); + expect(tip.text).toBe("chat.sendEnter"); + expect(tip.chip).toBe("Enter"); + }); + + // While the title is being edited the button is greyed, but it stays hoverable and does nothing. + it("keeps Rename tooltip-able while the title is being edited", async () => { + sessions.list = oneSession; + renderPanel(); + const rename = await screen.findByRole("button", { name: "chat.renameConversation" }); + fireEvent.click(rename); + await screen.findByDisplayValue("Conversation 1"); + expect(rename).toHaveAttribute("aria-disabled", "true"); + expect(rename).not.toBeDisabled(); + expect((await tooltipOn(rename)).text).toBe("chat.renameConversation"); + }); + + it("keeps Compact tooltip-able while there is no conversation, and does nothing", async () => { + renderPanel(); + const compact = await screen.findByRole("button", { name: "chat.compactContext" }); + expect(compact).toHaveAttribute("aria-disabled", "true"); + expect(compact).not.toBeDisabled(); + expect((await tooltipOn(compact)).text).toBe("chat.compactContext"); + fireEvent.click(compact); + expect(chatCompact).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/ai-edition/LeftPanel.tsx b/src/components/ai-edition/LeftPanel.tsx index 81c62c306..82e809268 100644 --- a/src/components/ai-edition/LeftPanel.tsx +++ b/src/components/ai-edition/LeftPanel.tsx @@ -2,6 +2,7 @@ import { ArrowLeft, Check, Loader2, X } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { toast } from "sonner"; +import { Tooltip } from "@/components/ui/tooltip"; import { useEditorDialogActions, useEditorDialogSection } from "@/contexts/EditorDialogsContext"; import { useScopedT } from "@/contexts/I18nContext"; import { @@ -471,6 +472,7 @@ export function ChatStripPanel() { const [connectedProviders, setConnectedProviders] = useState(null); // unknown ≠ none; see chatAvailability.ts. const canChat = canSendChat(llmConfig, connectedProviders); + const sendDisabled = busy || !input.trim() || !canChat; const [modelPopoverOpen, setModelPopoverOpen] = useState(false); const modelButtonRef = useRef(null); const [modelPopoverRect, setModelPopoverRect] = useState<{ @@ -1006,92 +1008,95 @@ export function ChatStripPanel() { {t("chat.contextPercent", { percent: Math.min(100, Math.round(budget.ratio * 100)) })} - - - + + + - + + + + + + + + + + + + + @@ -1160,61 +1165,66 @@ export function ChatStripPanel() { t("chat.untitledConversation")} )} - - + + + + + + + + + + + + ) : null} @@ -1293,21 +1303,59 @@ export function ChatStripPanel() { }} > {m.role === "user" && m.checkpointId ? ( + // The name stays "Rewind to this message"; the tip adds what it does, which + // only the confirmation dialog said before. + + + + ) : null} + - ) : null} - + {m.toolCalls?.length ? (
@@ -1546,28 +1560,34 @@ export function ChatStripPanel() { onOpenFullSettings={openProviderSettings} /> ) : null} - + + + + + +
- - , + + + + + , ); } diff --git a/src/components/ai-edition/NewEditorShell.dialogShortcuts.test.tsx b/src/components/ai-edition/NewEditorShell.dialogShortcuts.test.tsx index 7c4201ead..10834f969 100644 --- a/src/components/ai-edition/NewEditorShell.dialogShortcuts.test.tsx +++ b/src/components/ai-edition/NewEditorShell.dialogShortcuts.test.tsx @@ -49,14 +49,17 @@ vi.mock("@/contexts/I18nContext", () => ({ useScopedT: () => (key: string) => key, })); +import { TooltipProvider } from "@/components/ui/tooltip"; import { EditorDialogsProvider } from "@/contexts/EditorDialogsContext"; import { NewEditorShell } from "./NewEditorShell"; function renderShell() { return render( - - - , + + + + + , ); } diff --git a/src/components/ai-edition/NewEditorShell.module.css b/src/components/ai-edition/NewEditorShell.module.css index 35498978a..1fde13eda 100644 --- a/src/components/ai-edition/NewEditorShell.module.css +++ b/src/components/ai-edition/NewEditorShell.module.css @@ -1512,7 +1512,8 @@ textarea.control { background: var(--surface-3); color: var(--fg-2); } -.stripActions button:disabled { +.stripActions button:disabled, +.stripActions button[aria-disabled="true"] { opacity: 0.4; cursor: not-allowed; } @@ -1666,8 +1667,9 @@ textarea.control { cursor: pointer; transition: background var(--motion-fast) var(--ease), transform var(--motion-fast) var(--ease); } -.sendBtn:hover:not(:disabled) { background: var(--brand-lo); transform: scale(1.05); } -.sendBtn:disabled { +.sendBtn:hover:not(:disabled):not([aria-disabled="true"]) { background: var(--brand-lo); transform: scale(1.05); } +.sendBtn:disabled, +.sendBtn[aria-disabled="true"] { background: color-mix(in oklab, var(--fg) 8%, transparent); color: var(--muted); cursor: not-allowed; @@ -2467,7 +2469,8 @@ textarea.control { cursor: pointer; } .iconBtn:hover { background: var(--surface-3); color: var(--fg); } -.iconBtn:disabled { opacity: 0.5; cursor: not-allowed; } +.iconBtn:disabled, +.iconBtn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; } /* A labelled sibling of .iconBtn. Captions lost their own tab (issue #560), so the control that replaces it cannot be an icon you must hover to identify: the whole diff --git a/src/components/ai-edition/NewEditorShell.timelineHeight.test.tsx b/src/components/ai-edition/NewEditorShell.timelineHeight.test.tsx index d16ce4492..9ecf446fc 100644 --- a/src/components/ai-edition/NewEditorShell.timelineHeight.test.tsx +++ b/src/components/ai-edition/NewEditorShell.timelineHeight.test.tsx @@ -26,6 +26,7 @@ vi.mock("@/contexts/I18nContext", () => ({ useScopedT: () => (key: string) => key, })); +import { TooltipProvider } from "@/components/ui/tooltip"; import { EditorDialogsProvider } from "@/contexts/EditorDialogsContext"; import { AUDIO_ROW_EXPANSION_PX } from "@/lib/ai-edition/document/audioTracks"; import { createAudioTrack, createEmptyDocument } from "@/lib/ai-edition/schema"; @@ -39,9 +40,11 @@ import { function renderShell() { return render( - - - , + + + + + , ); } diff --git a/src/components/ai-edition/ProviderSettings.test.tsx b/src/components/ai-edition/ProviderSettings.test.tsx index 1ed6f377e..413aaf2c2 100644 --- a/src/components/ai-edition/ProviderSettings.test.tsx +++ b/src/components/ai-edition/ProviderSettings.test.tsx @@ -10,8 +10,10 @@ import "@testing-library/jest-dom"; import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { TooltipProvider } from "@/components/ui/tooltip"; import { EditorDialogsProvider, useEditorDialogActions } from "@/contexts/EditorDialogsContext"; import { I18nProvider } from "@/contexts/I18nContext"; +import { ShortcutsProvider } from "@/contexts/ShortcutsContext"; import { LOCALE_STORAGE_KEY } from "@/i18n/config"; import { type EditorMode, EditorTopBar } from "./v4/EditorTopBar"; @@ -75,10 +77,14 @@ function renderEditorChrome(locale: string, mode: EditorMode = "edit") { localStorage.setItem(LOCALE_STORAGE_KEY, locale); return render( - - - - + + + + + + + + , ); } diff --git a/src/components/ai-edition/TransportBar.test.tsx b/src/components/ai-edition/TransportBar.test.tsx index ffa50c9b3..e07fe82e1 100644 --- a/src/components/ai-edition/TransportBar.test.tsx +++ b/src/components/ai-edition/TransportBar.test.tsx @@ -1,8 +1,11 @@ // @vitest-environment jsdom import "@testing-library/jest-dom"; -import { act, cleanup, render, screen } from "@testing-library/react"; +import { act, cleanup, render, screen, waitFor } from "@testing-library/react"; +import type { ReactNode } from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { TooltipProvider } from "@/components/ui/tooltip"; import { I18nProvider } from "@/contexts/I18nContext"; +import { ShortcutsProvider } from "@/contexts/ShortcutsContext"; import { useProjectStore } from "@/lib/ai-edition/store/projectStore"; import { TransportBar } from "./TransportBar"; @@ -10,6 +13,22 @@ vi.mock("@/native/client", () => ({ nativeBridgeClient: { aiEdition: {} }, })); +class StubResizeObserver { + observe = vi.fn(); + unobserve = vi.fn(); + disconnect = vi.fn(); +} + +function providers(children: ReactNode) { + return ( + + + {children} + + + ); +} + const clips = [ { id: "clip_a", @@ -49,11 +68,7 @@ describe("TransportBar reads the playhead from the store", () => { ); } - render( - - - , - ); + render(providers()); const rendersAfterMount = parentRenders; expect(screen.getByText("0:00.0")).toBeInTheDocument(); @@ -70,10 +85,70 @@ describe("TransportBar reads the playhead from the store", () => { it("prefers the live scrub override over the store value", () => { useProjectStore.setState({ currentTimeSec: 12.3 }); render( - - - , + providers( + , + ), ); expect(screen.getByText("0:04.5")).toBeInTheDocument(); }); }); + +describe("TransportBar play button", () => { + beforeEach(() => { + vi.stubGlobal("ResizeObserver", StubResizeObserver); + }); + + afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); + (window as unknown as { electronAPI?: unknown }).electronAPI = undefined; + }); + + function renderTransport(playing: boolean) { + render( + providers( + , + ), + ); + } + + // Opens the tooltip the way the keyboard does, without the hover delay. + async function openTooltip(button: HTMLElement) { + act(() => button.focus()); + await screen.findByRole("tooltip"); + return document.querySelector('[data-slot="tooltip-content"]'); + } + + // The icon swaps, so the name swaps with it. It is an action, not a toggle: no aria-pressed. + it("is named for what pressing it does, and swaps with the icon", () => { + renderTransport(false); + const play = screen.getByRole("button", { name: "Play" }); + expect(play).not.toHaveAttribute("aria-pressed"); + expect(play).not.toHaveAttribute("title"); + + cleanup(); + renderTransport(true); + expect(screen.getByRole("button", { name: "Pause" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Play" })).toBeNull(); + }); + + it("shows the default binding as a chip, not as text in the name", async () => { + renderTransport(false); + const tooltip = await openTooltip(screen.getByRole("button", { name: "Play" })); + expect(tooltip?.querySelector("kbd")).toHaveTextContent("Space"); + expect(tooltip?.querySelector("kbd")?.previousSibling?.textContent).toBe("Play"); + }); + + // Play/Pause is remappable, so a key written into the string would lie after the remap. + it("shows the binding the user chose", async () => { + const getShortcuts = vi.fn(async () => ({ playPause: { key: "k" } })); + (window as unknown as { electronAPI?: unknown }).electronAPI = { getShortcuts }; + renderTransport(true); + await waitFor(() => expect(getShortcuts).toHaveBeenCalled()); + // Let the saved bindings land in the provider. + await act(() => Promise.resolve()); + const tooltip = await openTooltip(screen.getByRole("button", { name: "Pause" })); + expect(tooltip?.querySelector("kbd")).toHaveTextContent("K"); + expect(tooltip?.querySelector("kbd")).not.toHaveTextContent("Space"); + }); +}); diff --git a/src/components/ai-edition/TransportBar.tsx b/src/components/ai-edition/TransportBar.tsx index c285dea38..958f2c49d 100644 --- a/src/components/ai-edition/TransportBar.tsx +++ b/src/components/ai-edition/TransportBar.tsx @@ -1,9 +1,12 @@ import { Pause, Play } from "lucide-react"; import { memo } from "react"; +import { Tooltip } from "@/components/ui/tooltip"; import { useScopedT } from "@/contexts/I18nContext"; +import { useShortcuts } from "@/contexts/ShortcutsContext"; import type { AxcutClip } from "@/lib/ai-edition/schema"; import { useProjectStore } from "@/lib/ai-edition/store/projectStore"; import { formatSec } from "@/lib/ai-edition/timeline/format"; +import { formatBinding } from "@/lib/shortcuts"; import styles from "./NewEditorShell.module.css"; interface TransportBarProps { @@ -24,6 +27,7 @@ export const TransportBar = memo(function TransportBar({ onTogglePlay, }: TransportBarProps) { const te = useScopedT("editor"); + const { shortcuts, isMac } = useShortcuts(); // Same reason as PlayheadOverlay (see V4Timeline.tsx): the timecode is animated // during playback, so it subscribes to the playhead directly instead of forcing // V4Timeline — and the whole editor shell above it — to re-render once per frame @@ -35,18 +39,26 @@ export const TransportBar = memo(function TransportBar({ 0, ); + // The icon swaps, so the name swaps with it: an action button, not a toggle (no aria-pressed). + const playLabel = playing ? te("transport.pause") : te("transport.play"); + return (
- + + + {formatSec(currentTimeSec)} / diff --git a/src/components/ai-edition/v4/EditorShellV4.module.css b/src/components/ai-edition/v4/EditorShellV4.module.css index bed8df50d..cae243f28 100644 --- a/src/components/ai-edition/v4/EditorShellV4.module.css +++ b/src/components/ai-edition/v4/EditorShellV4.module.css @@ -235,7 +235,8 @@ background: var(--surface-2); color: var(--fg); } -.iconBtn:disabled { +.iconBtn:disabled, +.iconBtn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; } @@ -1350,6 +1351,11 @@ background: var(--surface-3); color: var(--fg); } +/* Busy (Auto-enhance while a pass runs): still hoverable, so its tooltip opens. */ +.tlToolBtn[aria-disabled="true"] { + opacity: 0.55; + cursor: not-allowed; +} /* Toggle tools (Auto-Focus) read their lit state off aria-pressed, so the styling can't drift from what assistive tech announces. */ .tlToolBtn[aria-pressed="true"] { diff --git a/src/components/ai-edition/v4/EditorTopBar.stylePresets.test.tsx b/src/components/ai-edition/v4/EditorTopBar.stylePresets.test.tsx index 074542471..896ef04ae 100644 --- a/src/components/ai-edition/v4/EditorTopBar.stylePresets.test.tsx +++ b/src/components/ai-edition/v4/EditorTopBar.stylePresets.test.tsx @@ -5,7 +5,9 @@ import "@testing-library/jest-dom"; import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from "vitest"; +import { TooltipProvider } from "@/components/ui/tooltip"; import { I18nProvider } from "@/contexts/I18nContext"; +import { ShortcutsProvider } from "@/contexts/ShortcutsContext"; import { LOCALE_STORAGE_KEY } from "@/i18n/config"; import { DEFAULT_EDITOR_SETTINGS, @@ -92,30 +94,34 @@ function renderPane() { localStorage.setItem(LOCALE_STORAGE_KEY, "en"); return render( - + + + + + , ); } diff --git a/src/components/ai-edition/v4/EditorTopBar.test.tsx b/src/components/ai-edition/v4/EditorTopBar.test.tsx index 6170209cb..f5239a21a 100644 --- a/src/components/ai-edition/v4/EditorTopBar.test.tsx +++ b/src/components/ai-edition/v4/EditorTopBar.test.tsx @@ -1,7 +1,8 @@ // @vitest-environment jsdom import "@testing-library/jest-dom"; -import { fireEvent, render, screen } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { TooltipProvider } from "@/components/ui/tooltip"; // ProjectNameField is a private helper inside EditorTopBar, so reach it through // the public topbar instead. The translator echoes keys; assertions read better @@ -11,6 +12,22 @@ vi.mock("@/contexts/I18nContext", () => ({ useScopedT: () => (key: string) => key, })); +// The platform decides whether the chips read Ctrl or ⌘. +const platform = vi.hoisted(() => ({ isMac: false })); +vi.mock("@/contexts/ShortcutsContext", () => ({ + useShortcuts: () => ({ isMac: platform.isMac }), +})); + +class StubResizeObserver { + observe = vi.fn(); + unobserve = vi.fn(); + disconnect = vi.fn(); +} +beforeEach(() => { + platform.isMac = false; + vi.stubGlobal("ResizeObserver", StubResizeObserver); +}); + const { toggleTheme } = vi.hoisted(() => ({ toggleTheme: vi.fn() })); vi.mock("@/hooks/useTheme", () => ({ useTheme: () => ({ theme: "dark", toggle: toggleTheme }), @@ -35,30 +52,32 @@ function renderTopBar( const onUndo = vi.fn(); const onRedo = vi.fn(); render( - , + + + , ); return { onRename, @@ -324,20 +343,20 @@ describe("AppMenu", () => { }); describe("EditorTopBar responsive affordances and tooltips", () => { - it("provides accessible name and title on the export button", () => { + // The visible text is the label, so a tooltip that repeats it says nothing (and a native + // `title` on top of it is a second, slower tooltip). + it("names the export button by its text, with no title repeating it", () => { renderTopBar("Demo Project"); const exportBtn = screen.getByRole("button", { name: "topbar.export" }); expect(exportBtn).toBeInTheDocument(); - expect(exportBtn).toHaveAttribute("title", "topbar.export"); + expect(exportBtn).not.toHaveAttribute("title"); }); - it("provides title tooltips for mode switch tabs", () => { + it("gives the mode tabs no tooltip: their text is the label", () => { renderTopBar("Demo Project"); const tabs = screen.getAllByRole("tab"); expect(tabs).toHaveLength(3); - expect(tabs[0]).toHaveAttribute("title", "topbar.modes.media"); - expect(tabs[1]).toHaveAttribute("title", "topbar.modes.edit"); - expect(tabs[2]).toHaveAttribute("title", "topbar.modes.rec"); + for (const tab of tabs) expect(tab).not.toHaveAttribute("title"); }); it("says the saved state beside the project name, to the eye and to a screen reader", () => { @@ -352,18 +371,77 @@ describe("EditorTopBar responsive affordances and tooltips", () => { fireEvent.click(screen.getByRole("button", { name: "fixedActions.undo" })); expect(onUndo).toHaveBeenCalledTimes(1); const redo = screen.getByRole("button", { name: "fixedActions.redo" }); - expect(redo).toBeDisabled(); + // `aria-disabled`, not `disabled`: a natively disabled button takes no pointer events, so + // its tooltip could never open. It stays reachable, and doing nothing. + expect(redo).toHaveAttribute("aria-disabled", "true"); + expect(redo).not.toBeDisabled(); fireEvent.click(redo); expect(onRedo).not.toHaveBeenCalled(); + expect(screen.getByRole("button", { name: "fixedActions.undo" })).not.toHaveAttribute( + "aria-disabled", + ); }); - it("keeps the brand trigger accessible by label and title even when text collapses", () => { + it("keeps the brand trigger named by its label, with no title repeating it", () => { renderTopBar("Demo Project"); const brandBtn = screen.getByRole("button", { name: "OpenScreen" }); - expect(brandBtn).toHaveAttribute("title", "OpenScreen"); + expect(brandBtn).not.toHaveAttribute("title"); expect(brandBtn).toHaveAttribute("aria-label", "OpenScreen"); }); + it("leaves a native title only on text that can be cut short", () => { + renderTopBar("Demo Project"); + // The project name is truncated, so the full name is revealed on hover. + expect(screen.getByRole("button", { name: "topbar.renameProject" })).toHaveAttribute( + "title", + "Demo Project", + ); + // The icon-only controls use the shared tooltip instead. + for (const name of ["topbar.toggleChatPanel", "fixedActions.undo", "fixedActions.redo"]) { + expect(screen.getByRole("button", { name })).not.toHaveAttribute("title"); + } + }); + + // Every icon-only control names itself in a tooltip; undo and redo also show the key. + async function tooltipOn(name: string) { + const control = screen.getByRole("button", { name }); + act(() => control.focus()); + const tooltip = await screen.findByRole("tooltip"); + const visible = document.querySelector('[data-slot="tooltip-content"]'); + const result = { text: tooltip.textContent, chip: visible?.querySelector("kbd")?.textContent }; + act(() => control.blur()); + await waitFor(() => expect(screen.queryByRole("tooltip")).toBeNull()); + return result; + } + + it("shows the live shortcut as a chip on undo and redo, with ⌘ on macOS", async () => { + renderTopBar("Demo Project", { canUndo: true, canRedo: true }); + expect((await tooltipOn("fixedActions.undo")).chip).toBe("Ctrl + Z"); + // One chip, the first binding: redo is also on Ctrl + Y. + expect((await tooltipOn("fixedActions.redo")).chip).toBe("Ctrl + Shift + Z"); + }); + + it("reads ⌘ and ⇧ from the platform", async () => { + platform.isMac = true; + renderTopBar("Demo Project", { canUndo: true, canRedo: true }); + expect((await tooltipOn("fixedActions.undo")).chip).toBe("⌘ + Z"); + expect((await tooltipOn("fixedActions.redo")).chip).toBe("⌘ + ⇧ + Z"); + }); + + it("still opens the undo tooltip when there is nothing to undo", async () => { + renderTopBar("Demo Project", { canUndo: false, canRedo: false }); + const undo = await tooltipOn("fixedActions.undo"); + expect(undo.text).toContain("fixedActions.undo"); + expect(undo.chip).toBe("Ctrl + Z"); + }); + + it("names the chat panel toggle with a noun and no chip", async () => { + renderTopBar("Demo Project"); + const toggle = await tooltipOn("topbar.toggleChatPanel"); + expect(toggle.text).toBe("topbar.toggleChatPanel"); + expect(toggle.chip).toBeUndefined(); + }); + it("leaves the language to the menu, out of the bar", () => { renderTopBar("Demo Project"); expect(screen.queryByRole("button", { name: "topbar.changeLanguage" })).not.toBeInTheDocument(); diff --git a/src/components/ai-edition/v4/EditorTopBar.tsx b/src/components/ai-edition/v4/EditorTopBar.tsx index ae6d7a7c9..5ee571417 100644 --- a/src/components/ai-edition/v4/EditorTopBar.tsx +++ b/src/components/ai-edition/v4/EditorTopBar.tsx @@ -18,9 +18,12 @@ import { } from "lucide-react"; import { type KeyboardEvent as ReactKeyboardEvent, useEffect, useRef, useState } from "react"; import logoMark from "@/assets/openscreen-mark.png"; +import { Tooltip } from "@/components/ui/tooltip"; import { useI18n, useScopedT } from "@/contexts/I18nContext"; +import { useShortcuts } from "@/contexts/ShortcutsContext"; import { useTheme } from "@/hooks/useTheme"; import { getAvailableLocales, getLocaleName } from "@/i18n/loader"; +import { formatFirstFixedBinding } from "@/lib/shortcuts"; import { StylePresetsMenu } from "../StylePresetsMenu"; import styles from "./EditorShellV4.module.css"; @@ -72,6 +75,7 @@ export function EditorTopBar({ }: EditorTopBarProps) { const t = useScopedT("editor"); const tShortcuts = useScopedT("shortcuts"); + const { isMac } = useShortcuts(); const savedLabel = dirty ? t("topbar.unsaved") : t("topbar.saved"); // ponytail: the left side panel only renders in "edit" mode (see @@ -85,16 +89,17 @@ export function EditorTopBar({ {showChatToggle ? ( <> - + + + ) : null} @@ -123,7 +128,6 @@ export function EditorTopBar({ type="button" role="tab" aria-selected={mode === m.id} - title={t(m.labelKey)} // Feeds the hidden bold copy that reserves the selected width — see // .modeSwitch button::before. data-label={t(m.labelKey)} @@ -139,31 +143,40 @@ export function EditorTopBar({ rather than in any one pane's header. */} {/* Always mounted, disabled when there is nothing to step to: the bar keeps its width and the pair reads as the history it is. Same handlers as Ctrl+Z / Ctrl+Shift+Z. */} - - + + - - + + + // The name is the pane's title; the tip says what the pane holds. They are two keys + // because the title is also the pane's heading and cannot carry the list. + + + ))}
- + + + {clipPickerOpen && clips.length > 1 ? (
({ + status: "idle" as "idle" | "running", + request: vi.fn(), +})); + +vi.mock("@/lib/ai-edition/store/transcriptionStore", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useAssetTranscriptions: () => ({ + "asset-1": { assetId: "asset-1", status: transcription.status }, + }), + useTranscriptionStore: (select: (state: { request: unknown }) => unknown) => + select({ request: transcription.request }), + }; +}); + +// An echoing translator: the assertions read against keys, not prose that drifts with copy edits. +vi.mock("@/contexts/I18nContext", () => ({ + useI18n: () => ({ locale: "en", setLocale: vi.fn() }), + useScopedT: () => (key: string) => key, +})); + +vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn(), info: vi.fn() } })); + +import { TooltipProvider } from "@/components/ui/tooltip"; +import { assetSchema, createEmptyDocument } from "@/lib/ai-edition/schema"; +import { useProjectStore } from "@/lib/ai-edition/store/projectStore"; +import { MediaStage } from "./MediaStage"; + +class StubResizeObserver { + observe = vi.fn(); + unobserve = vi.fn(); + disconnect = vi.fn(); +} + +beforeEach(() => { + vi.stubGlobal("ResizeObserver", StubResizeObserver); + transcription.status = "idle"; + transcription.request.mockClear(); + const document = createEmptyDocument({ projectId: "p", title: "t" }); + useProjectStore.setState({ + projectId: "p", + document: { + ...document, + assets: [assetSchema.parse({ id: "asset-1", label: "take", originalPath: "/rec/take.mp4" })], + }, + }); +}); + +afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); + useProjectStore.setState({ document: null }); +}); + +async function openDetail() { + render( + + undefined)} /> + , + ); + fireEvent.click(await screen.findByRole("button", { name: /take/ })); + return screen.findByRole("button", { name: "mediaStage.regenerate" }); +} + +describe("MediaStage Regenerate button", () => { + it("asks for a new transcription, and says what it redoes in its tooltip", async () => { + const regenerate = await openDetail(); + expect(regenerate).not.toHaveAttribute("title"); + act(() => regenerate.focus()); + expect((await screen.findByRole("tooltip")).textContent).toBe("mediaStage.regenerateTip"); + fireEvent.click(regenerate); + expect(transcription.request).toHaveBeenCalledWith("asset-1", "auto"); + }); + + it("stays focusable and keeps its tooltip while a transcription runs, and fires nothing", async () => { + transcription.status = "running"; + const regenerate = await openDetail(); + + // Greyed and announced as disabled, but not natively disabled. + expect(regenerate).toHaveAttribute("aria-disabled", "true"); + expect(regenerate).not.toBeDisabled(); + expect(regenerate).toHaveStyle({ opacity: "0.6", cursor: "not-allowed" }); + expect(regenerate.querySelector("svg")).toHaveClass("animate-spin"); + + act(() => regenerate.focus()); + expect(regenerate).toHaveFocus(); + expect((await screen.findByRole("tooltip")).textContent).toBe("mediaStage.regenerateTip"); + + fireEvent.click(regenerate); + expect(transcription.request).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/ai-edition/v4/MediaStage.tsx b/src/components/ai-edition/v4/MediaStage.tsx index df2ee7f68..bb30e2115 100644 --- a/src/components/ai-edition/v4/MediaStage.tsx +++ b/src/components/ai-edition/v4/MediaStage.tsx @@ -1,6 +1,7 @@ import { ArrowDown, Film, Plus, RotateCw, Search, X } from "lucide-react"; import { useMemo, useState } from "react"; import { toast } from "sonner"; +import { Tooltip } from "@/components/ui/tooltip"; import { useI18n, useScopedT } from "@/contexts/I18nContext"; import type { AxcutAsset, AxcutTranscript, TranscriptLanguageCode } from "@/lib/ai-edition/schema"; import { useProjectStore } from "@/lib/ai-edition/store/projectStore"; @@ -420,28 +421,36 @@ export function MediaStage({ ))} - + {/* A bare refresh icon beside a language list: the tip says what it redoes. */} + + +
diff --git a/src/components/ai-edition/v4/RecStage.test.tsx b/src/components/ai-edition/v4/RecStage.test.tsx index aefa16bf7..b79308df3 100644 --- a/src/components/ai-edition/v4/RecStage.test.tsx +++ b/src/components/ai-edition/v4/RecStage.test.tsx @@ -1,13 +1,27 @@ // @vitest-environment jsdom import "@testing-library/jest-dom"; -import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; +import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { TooltipProvider } from "@/components/ui/tooltip"; import { RecStage } from "./RecStage"; vi.mock("@/contexts/I18nContext", () => ({ useScopedT: () => (key: string) => key, })); +// Whether the native helper can leave the system cursor out of the pixels. Its own answer (and +// the HUD's) is tested with the function it calls; here it is a switch. +const editableCursor = vi.hoisted(() => ({ available: true })); +vi.mock("@/hooks/useEditableCursorAvailable", () => ({ + useEditableCursorAvailable: () => editableCursor.available, +})); + +class StubResizeObserver { + observe = vi.fn(); + unobserve = vi.fn(); + disconnect = vi.fn(); +} + const microphoneHook = vi.hoisted(() => ({ call: vi.fn(), value: { @@ -93,10 +107,35 @@ function stubRecordingPrefs( function renderRecStage() { const onStartRecording = vi.fn(); - const view = render(); + const view = render( + + + , + ); return { onStartRecording, ...view }; } +/** A row's On/Off pill: it is named by the row's label, and `aria-pressed` says On or Off. */ +function pill(rowLabelKey: string) { + return screen.getByRole("button", { name: rowLabelKey }); +} + +/** Opens a pill's tooltip the way the keyboard does (focus opens it at once) and returns what + * it says, or null when it opens nothing. */ +async function tooltipOn(control: HTMLElement) { + act(() => control.focus()); + try { + const text = (await screen.findByRole("tooltip", {}, { timeout: 150 })).textContent; + act(() => control.blur()); + await waitFor(() => expect(screen.queryByRole("tooltip")).toBeNull()); + return text; + } catch { + return null; + } finally { + act(() => control.blur()); + } +} + describe("RecStage controls", () => { beforeEach(() => { vi.clearAllMocks(); @@ -110,9 +149,12 @@ describe("RecStage controls", () => { isReady: true, error: null, }; + editableCursor.available = true; + vi.stubGlobal("ResizeObserver", StubResizeObserver); }); afterEach(() => { + vi.unstubAllGlobals(); cleanup(); (window as unknown as { electronAPI?: unknown }).electronAPI = undefined; }); @@ -204,9 +246,8 @@ describe("RecStage controls", () => { await waitFor(() => expect(screen.queryByTestId("rec-auto-zoom-button")).toBeNull()); expect(screen.queryByText("rec.autoZoom")).toBeNull(); - const cursorLabel = screen.getByText("rec.cursorHighlight"); - if (!cursorLabel.parentElement) throw new Error("cursor highlight row is missing"); - fireEvent.click(within(cursorLabel.parentElement).getByRole("button", { name: "rec.off" })); + expect(pill("rec.editableCursor")).toHaveAttribute("aria-pressed", "false"); + fireEvent.click(pill("rec.editableCursor")); expect(await screen.findByTestId("rec-auto-zoom-button")).toHaveAttribute( "aria-pressed", "true", @@ -313,7 +354,11 @@ describe("RecStage controls", () => { isLoading: false, isReady: true, }; - rerender(); + rerender( + + + , + ); expect(audioMeter.call).toHaveBeenLastCalledWith({ enabled: true, deviceId: undefined }); }); @@ -329,7 +374,11 @@ describe("RecStage controls", () => { devices: [], error: "enumeration failed", }; - rerender(); + rerender( + + + , + ); expect(screen.getByText("rec.microphoneUnavailable")).toHaveAttribute( "title", "enumeration failed", @@ -340,15 +389,15 @@ describe("RecStage controls", () => { stubRecordingPrefs({ micEnabled: true }); renderRecStage(); await screen.findByText("rec.noMicrophoneFound"); - const row = screen.getByText("rec.microphone").closest("div"); - if (!row?.parentElement) throw new Error("microphone row is missing"); - const toggle = within(row.parentElement).getByRole("button", { name: "rec.on" }); + const toggle = pill("rec.microphone"); + expect(toggle).toHaveAttribute("aria-pressed", "true"); microphoneHook.call.mockClear(); fireEvent.click(toggle); await waitFor(() => expect(microphoneHook.call).toHaveBeenLastCalledWith(false, undefined, undefined), ); - fireEvent.click(within(row.parentElement).getByRole("button", { name: "rec.off" })); + expect(toggle).toHaveAttribute("aria-pressed", "false"); + fireEvent.click(toggle); await waitFor(() => expect(microphoneHook.call).toHaveBeenLastCalledWith(true, undefined, undefined), ); @@ -359,9 +408,12 @@ describe("RecStage controls", () => { Object.assign(window.electronAPI as object, { getPlatform: () => "win32" }); renderRecStage(); const label = await screen.findByText("rec.hideDesktopIcons"); - expect(label).toHaveAttribute("title", "rec.hideDesktopIconsHintWindows"); - if (!label.parentElement) throw new Error("desktop icons row is missing"); - fireEvent.click(within(label.parentElement).getByRole("button", { name: "rec.off" })); + // The tooltip is on the pill, not the label: the pill is the control. No native title. + expect(label).not.toHaveAttribute("title"); + const desktopIcons = pill("rec.hideDesktopIcons"); + expect(desktopIcons).not.toHaveAttribute("title"); + expect(await tooltipOn(desktopIcons)).toBe("rec.hideDesktopIconsHintWindows"); + fireEvent.click(desktopIcons); await waitFor(() => expect(setRecordingPrefs).toHaveBeenCalledWith({ hideDesktopIcons: true })); cleanup(); @@ -369,7 +421,7 @@ describe("RecStage controls", () => { stubRecordingPrefs({}); Object.assign(window.electronAPI as object, { getPlatform: () => "linux" }); renderRecStage(); - await screen.findByText("rec.cursorHighlight"); + await screen.findByText("rec.editableCursor"); expect(screen.queryByText("rec.hideDesktopIcons")).toBeNull(); }); @@ -385,9 +437,8 @@ describe("RecStage controls", () => { }); renderRecStage(); // The saved prefs have landed once system audio reads on. - const systemAudioRow = (await screen.findByText("rec.systemAudio")).parentElement; - if (!systemAudioRow) throw new Error("system audio row is missing"); - await within(systemAudioRow).findByRole("button", { name: "rec.on" }); + await screen.findByText("rec.systemAudio"); + await waitFor(() => expect(pill("rec.systemAudio")).toHaveAttribute("aria-pressed", "true")); expect(screen.queryByText("rec.microphone")).toBeNull(); expect(microphoneHook.call).toHaveBeenLastCalledWith(false, undefined, undefined); @@ -489,3 +540,110 @@ describe("RecStage controls", () => { expect(selectedSourceListeners).toEqual([]); }); }); + +describe("RecStage names and tooltips", () => { + beforeEach(() => { + vi.clearAllMocks(); + recordingPrefsListeners = []; + selectedSourceListeners = []; + microphoneHook.value = { + devices: [], + selectedDeviceId: "default", + setSelectedDeviceId: vi.fn(), + isLoading: false, + isReady: true, + error: null, + }; + editableCursor.available = true; + vi.stubGlobal("ResizeObserver", StubResizeObserver); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + cleanup(); + (window as unknown as { electronAPI?: unknown }).electronAPI = undefined; + }); + + async function renderReady(prefs: Record = {}) { + const stubs = stubRecordingPrefs({ cursorCaptureMode: "editable-overlay", ...prefs }); + renderRecStage(); + await waitFor(() => expect(stubs.getRecordingPrefs).toHaveBeenCalled()); + await screen.findByText("rec.editableCursor"); + return stubs; + } + + // The cursor row is the HUD's term: "Cursor highlight" suggested a halo, and the HUD's own + // toggle says "Editable cursor" for the same setting. + it("calls the cursor row the Editable cursor, as the HUD does", async () => { + await renderReady(); + expect(screen.getByText("rec.editableCursor")).toBeInTheDocument(); + expect(screen.queryByText("rec.cursorHighlight")).toBeNull(); + }); + + // A pill that reads "On" or "Off" names no control: the row's label is its name, and + // aria-pressed carries the state, so a screen reader never hears "Off, pressed". + it("names every On/Off pill by its row, and says which state with aria-pressed", async () => { + await renderReady({ systemAudioEnabled: true, hideDesktopIcons: false }); + Object.assign(window.electronAPI as object, { getPlatform: () => "win32" }); + for (const row of [ + "rec.systemAudio", + "rec.microphone", + "rec.camera", + "rec.editableCursor", + "rec.autoZoom", + ]) { + const control = pill(row); + expect(control).toHaveAttribute("aria-pressed"); + expect(control).not.toHaveAttribute("title"); + expect(control).toHaveTextContent(/^rec\.(on|off)$/); + } + expect(pill("rec.systemAudio")).toHaveAttribute("aria-pressed", "true"); + expect(pill("rec.camera")).toHaveAttribute("aria-pressed", "false"); + }); + + it("explains system audio and the editable cursor, in a tooltip on the pill", async () => { + await renderReady(); + expect(await tooltipOn(pill("rec.systemAudio"))).toBe("rec.systemAudioTip"); + expect(await tooltipOn(pill("rec.editableCursor"))).toBe("rec.editableCursorTip"); + }); + + // To the left a tooltip sits on the label the user is reading, and below the last row it covers + // Start recording: it opens above the pill. + it("opens the row tooltips above their pill, never over the row's own label", async () => { + stubRecordingPrefs({ cursorCaptureMode: "editable-overlay" }); + // Windows offers the desktop icons row too, so all three tooltips can be checked. + Object.assign(window.electronAPI as object, { getPlatform: () => "win32" }); + renderRecStage(); + await screen.findByText("rec.hideDesktopIcons"); + for (const row of ["rec.systemAudio", "rec.editableCursor", "rec.hideDesktopIcons"]) { + const control = pill(row); + act(() => control.focus()); + await screen.findByRole("tooltip"); + expect( + document.querySelector('[data-slot="tooltip-content"]')?.getAttribute("data-side"), + ).toBe("top"); + act(() => control.blur()); + await waitFor(() => expect(screen.queryByRole("tooltip")).toBeNull()); + } + }); + + // Decided: the rest are labelled and say what they do, so a tooltip would repeat the label. + it("gives the microphone, camera and auto-zoom pills no tooltip", async () => { + await renderReady(); + expect(await tooltipOn(pill("rec.microphone"))).toBeNull(); + expect(await tooltipOn(pill("rec.camera"))).toBeNull(); + expect(await tooltipOn(pill("rec.autoZoom"))).toBeNull(); + }); + + // Without its native helper the browser records, and it always draws the system cursor into + // the video: there is nothing to switch, and Auto-zoom reads what the editable cursor records. + it("hides the cursor row and the auto-zoom row when capture falls back to the browser", async () => { + editableCursor.available = false; + stubRecordingPrefs({ cursorCaptureMode: "editable-overlay", autoZoomEnabled: true }); + renderRecStage(); + await screen.findByText("rec.systemAudio"); + expect(screen.queryByText("rec.editableCursor")).toBeNull(); + expect(screen.queryByText("rec.autoZoom")).toBeNull(); + expect(screen.queryByTestId("rec-auto-zoom-button")).toBeNull(); + }); +}); diff --git a/src/components/ai-edition/v4/RecStage.tsx b/src/components/ai-edition/v4/RecStage.tsx index aeb1a99a2..658ed6b4d 100644 --- a/src/components/ai-edition/v4/RecStage.tsx +++ b/src/components/ai-edition/v4/RecStage.tsx @@ -12,12 +12,14 @@ import { VolumeX, ZoomIn, } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useId, useRef, useState } from "react"; import { AudioLevelMeter } from "@/components/ui/audio-level-meter"; +import { Tooltip } from "@/components/ui/tooltip"; import { useScopedT } from "@/contexts/I18nContext"; import { useAudioLevelMeter } from "@/hooks/useAudioLevelMeter"; import { useCameraDevices } from "@/hooks/useCameraDevices"; import { useCameraPreviewStream } from "@/hooks/useCameraPreviewStream"; +import { useEditableCursorAvailable } from "@/hooks/useEditableCursorAvailable"; import { useMicrophoneDevices } from "@/hooks/useMicrophoneDevices"; import { usePortalOwnsSource } from "@/hooks/usePortalOwnsSource"; import { canRecordMicrophone, getPlatform } from "@/utils/platformUtils"; @@ -245,7 +247,12 @@ export function RecStage({ const windowSources = sources.filter((s) => s.id.startsWith("window:")); const visibleSources = sourceTab === "screen" ? screenSources : windowSources; - const cursorHighlight = prefs.cursorCaptureMode === "editable-overlay"; + const editableCursor = prefs.cursorCaptureMode === "editable-overlay"; + const editableCursorAvailable = useEditableCursorAvailable(); + // Names for the On/Off pills: the row label is the accessible name, and `aria-pressed` says + // which state it is in. A pill named "On" or "Off" says which control it is nowhere. + const rowLabels = useId(); + const rowLabelId = (row: string) => `${rowLabels}-${row}`; // macOS leaves the icons out of the capture; Windows covers them for the take. Linux // records through the portal, which offers neither, so the row would do nothing there. const platform = getPlatform(); @@ -345,23 +352,30 @@ export function RecStage({ )}
-
+
{prefs.systemAudioEnabled ? : } {t("rec.systemAudio")}
- + {/* "System audio" is jargon for what the computer plays: the one row whose label + alone does not say what it records. The row tooltips open above their pill: to + the left they would sit on the label being read, and below the last row they + would cover Start recording. */} + + +
{canRecordMicrophone() ? (
-
+
{prefs.micEnabled ? : } {t("rec.microphone")}
@@ -406,6 +420,7 @@ export function RecStage({
-
-
- - {t("rec.cursorHighlight")} + {/* Without its native helper the browser records, and it always draws the system cursor + into the video: there is nothing to switch, so neither this row nor Auto-zoom, which + reads what the editable cursor records, is shown. Same rule as the HUD's button. */} + {editableCursorAvailable ? ( +
+
+ + {t("rec.editableCursor")} +
+ + +
- -
+ ) : null} {/* Auto-zoom places zooms from the cursor telemetry the editable-overlay mode writes. The system cursor writes none, so the row is not offered there. */} - {cursorHighlight ? ( + {editableCursorAvailable && editableCursor ? (
-
+
{t("rec.autoZoom")}
@@ -493,6 +517,7 @@ export function RecStage({ type="button" data-testid="rec-auto-zoom-button" className={`${styles.recToggleBtn}${prefs.autoZoomEnabled ? ` ${styles.on}` : ""}`} + aria-labelledby={rowLabelId("autoZoom")} aria-pressed={prefs.autoZoomEnabled} onClick={() => updatePrefs({ autoZoomEnabled: !prefs.autoZoomEnabled })} > @@ -503,19 +528,21 @@ export function RecStage({ {desktopIconsHint ? (
-
+
{t("rec.hideDesktopIcons")}
- + + +
) : null}
diff --git a/src/components/ai-edition/v4/V4Timeline.geometry.test.tsx b/src/components/ai-edition/v4/V4Timeline.geometry.test.tsx index 0a379524a..4dfd671c0 100644 --- a/src/components/ai-edition/v4/V4Timeline.geometry.test.tsx +++ b/src/components/ai-edition/v4/V4Timeline.geometry.test.tsx @@ -1,6 +1,6 @@ // @vitest-environment jsdom import "@testing-library/jest-dom"; -import { act, fireEvent, render, screen } from "@testing-library/react"; +import { act, cleanup, fireEvent, render, screen } from "@testing-library/react"; import { Profiler, type ProfilerOnRenderCallback } from "react"; import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; @@ -85,6 +85,11 @@ function clip(startSec: number, endSec: number) { /** The asset every clip above points at. No `cameraTrack`: this recording has no webcam, * which is what the Full Camera button is gated on. */ const NO_CAMERA_ASSET = { id: "a1", label: "rec", durationSec: TOTAL_SEC }; +/** The same recording, with a camera track: the Full Camera button is offered. */ +const CAMERA_ASSET = { + ...NO_CAMERA_ASSET, + cameraTrack: { sourcePath: "/tmp/cam.webm", startMs: 0, offsetMs: 0, visible: true }, +}; /** By default one 30-minute clip carrying a single one-second annotation, and a store that * holds an edit region (so Clear timeline shows); `overrides` replaces any `tl` member. */ @@ -440,7 +445,7 @@ describe("V4Timeline create-from-toolbar", () => { }); it("puts Clear timeline last, behind a divider, after the Add Full Camera button", () => { - renderTimeline(); + renderTimeline(undefined, undefined, [CAMERA_ASSET]); const toolbar = toolbarOf(); const buttons = Array.from(toolbar.querySelectorAll("button")); const clear = screen.getByLabelText("buttons.clearTimeline"); @@ -457,19 +462,15 @@ describe("V4Timeline create-from-toolbar", () => { // #353. A camera-fullscreen region grows the webcam overlay, so with no webcam on the // timeline it renders nothing in the preview and nothing in the export — the region is // stored and forgotten. `addCameraFullscreen` now refuses to write one; the button says - // so before it is clicked instead of looking like it worked. - it("disables Add Full Camera when no clip on the timeline has a camera", () => { + // so before it is clicked instead of looking like it worked. Absent, not greyed out: an + // inoperative control is hidden, and a greyed one with no reason is a puzzle. + it("shows no Add Full Camera button when no clip on the timeline has a camera", () => { renderTimeline(); - expect(screen.getByLabelText("buttons.addCameraFullscreen")).toBeDisabled(); + expect(screen.queryByLabelText("buttons.addCameraFullscreen")).not.toBeInTheDocument(); }); - it("enables Add Full Camera as soon as a clip's asset carries one", () => { - renderTimeline(undefined, undefined, [ - { - ...NO_CAMERA_ASSET, - cameraTrack: { sourcePath: "/tmp/cam.webm", startMs: 0, offsetMs: 0, visible: true }, - }, - ]); + it("shows Add Full Camera as soon as a clip's asset carries one", () => { + renderTimeline(undefined, undefined, [CAMERA_ASSET]); expect(screen.getByLabelText("buttons.addCameraFullscreen")).toBeEnabled(); }); @@ -842,3 +843,107 @@ describe("V4Timeline audio lane drag", () => { expect(placement.endMs - placement.startMs).toBeLessThan(60_000); }); }); + +describe("V4Timeline toolbar tooltips", () => { + afterEach(() => { + (window as unknown as { electronAPI?: unknown }).electronAPI = undefined; + }); + + const toolbarOf = () => screen.getByRole("toolbar", { name: "toolbar.timelineTools" }); + + /** Opens a toolbar button's tooltip the way the keyboard does (focus opens it at once), and + * returns the text and the chip of the visible copy. */ + async function tooltipOn(name: string) { + const control = screen.getByLabelText(name); + act(() => control.focus()); + await screen.findByRole("tooltip"); + const visible = document.querySelector('[data-slot="tooltip-content"]'); + const result = { + text: screen.getByRole("tooltip").textContent, + chip: visible?.querySelector("kbd")?.textContent ?? null, + }; + act(() => control.blur()); + return result; + } + + it("shows each creator's default key as a chip, and no key in its name", async () => { + renderTimeline(undefined, undefined, [CAMERA_ASSET]); + const chips: Record = {}; + for (const name of [ + "buttons.addZoom", + "buttons.addTrim", + "buttons.addSpeed", + "buttons.addAnnotation", + "buttons.addCameraFullscreen", + ]) { + chips[name] = (await tooltipOn(name)).chip; + // The name is the string alone: a "(Z)" in it would be a second, stale copy of the key. + expect(screen.getByLabelText(name)).toHaveAccessibleName(name); + } + expect(chips).toEqual({ + "buttons.addZoom": "Z", + "buttons.addTrim": "T", + "buttons.addSpeed": "S", + "buttons.addAnnotation": "A", + "buttons.addCameraFullscreen": "C", + }); + }); + + // The five creators are remappable, so the chip must follow the saved binding, not the default. + it("shows the key the user remapped, not the default", async () => { + const getShortcuts = vi.fn(async () => ({ + addZoom: { key: "q" }, + addTrim: { key: "t", ctrl: true, shift: true }, + })); + (window as unknown as { electronAPI?: unknown }).electronAPI = { getShortcuts }; + renderTimeline(); + await vi.waitFor(() => expect(getShortcuts).toHaveBeenCalled()); + await act(() => Promise.resolve()); + expect((await tooltipOn("buttons.addZoom")).chip).toBe("Q"); + expect((await tooltipOn("buttons.addTrim")).chip).toBe("Ctrl + Shift + T"); + // Unset actions keep their default. + expect((await tooltipOn("buttons.addSpeed")).chip).toBe("S"); + }); + + it("gives Auto-Focus one name and one tip for both states, with the state in aria-pressed", async () => { + renderTimeline(); + const button = screen.getByLabelText("buttons.autoFocusAll"); + expect(button).toHaveAttribute("aria-pressed"); + const before = await tooltipOn("buttons.autoFocusAll"); + fireEvent.click(button); + expect(screen.getByLabelText("buttons.autoFocusAll")).toBe(button); + const after = await tooltipOn("buttons.autoFocusAll"); + expect(before.text).toBe("buttons.autoFocusAllTip"); + expect(after.text).toBe(before.text); + expect(before.chip).toBeNull(); + }); + + it("says what Clear timeline removes in its tooltip, without a chip", async () => { + renderTimeline(); + const clear = await tooltipOn("buttons.clearTimeline"); + expect(clear.text).toBe("buttons.clearTimeline"); + expect(clear.chip).toBeNull(); + }); + + // A native `title` next to the shared tooltip is a second, slower tooltip on the same button. + it("uses no native title on the toolbar buttons that have the shared tooltip", () => { + renderTimeline(undefined, undefined, [CAMERA_ASSET]); + const buttons = Array.from(toolbarOf().querySelectorAll("button")); + expect(buttons.length).toBeGreaterThanOrEqual(8); + for (const button of buttons) expect(button).not.toHaveAttribute("title"); + }); + + it("names a pill on hover only when it cannot draw its own label", () => { + // A hairline pill draws nothing, so the title is the only place its name is. + renderTimeline(); + const hairline = screen.getByTitle("toolbar.newAnnotation"); + expect(hairline.textContent).toBe(""); + + // One that fills the timeline draws its label, and a title would repeat it. + cleanup(); + renderTimeline(undefined, { id: "ann1", startMs: 0, endMs: TOTAL_SEC * 1000 }); + const wide = document.querySelector("[class*=lanePill]") as HTMLElement; + expect(wide).toHaveTextContent("toolbar.newAnnotation"); + expect(wide).not.toHaveAttribute("title"); + }); +}); diff --git a/src/components/ai-edition/v4/V4Timeline.tsx b/src/components/ai-edition/v4/V4Timeline.tsx index 762a2df5a..d10d30d15 100644 --- a/src/components/ai-edition/v4/V4Timeline.tsx +++ b/src/components/ai-edition/v4/V4Timeline.tsx @@ -543,7 +543,7 @@ const AudioLanePill = memo(function AudioLanePill({ e.nativeEvent.stopPropagation(); onSelect(track.id); }} - title={`${label} — ${slipHint}`} + title={`${label}\n${slipHint}`} > = [ - { id: "cut", label: t("buttons.addTrim"), icon: }, - { id: "comment", label: t("buttons.addAnnotation"), icon: }, - { id: "speed", label: t("buttons.addSpeed"), icon: }, + // `shortcut` is the user's live binding, shown as the tooltip's chip. The strings carry no key: + // these actions can be remapped, and a key written into a translation would then lie. + const tools: Array<{ id: ToolId; label: string; shortcut: string; icon: React.ReactNode }> = [ + { + id: "cut", + label: t("buttons.addTrim"), + shortcut: formatBinding(shortcuts.addTrim, isMac), + icon: , + }, + { + id: "comment", + label: t("buttons.addAnnotation"), + shortcut: formatBinding(shortcuts.addAnnotation, isMac), + icon: , + }, + { + id: "speed", + label: t("buttons.addSpeed"), + shortcut: formatBinding(shortcuts.addSpeed, isMac), + icon: , + }, ]; // Auto-enhance option 1 — the deterministic cursor-telemetry auto-zoom @@ -1683,7 +1700,9 @@ export function V4Timeline({ } : undefined } - title={p.label} + // The name on hover only where the pill cannot show it: a pill that draws its own + // label would repeat it. + title={seg.showContent && roomForLabel ? undefined : p.label} > {seg.interactive ? ( { + if (autoBusy) e.preventDefault(); + }} > {autoBusy ? ( @@ -1870,7 +1894,7 @@ export function V4Timeline({ {tools.map((tool) => ( - + - + {/* One name and one tip for both states: `aria-pressed` carries which one it is. */} + - - - + + + ) : null} {/* Last, behind a divider: every button before it adds a region, this one clears them. Absent with its divider, not greyed out, when there is nothing to clear. One write in the store, so one Ctrl+Z restores every @@ -2044,12 +2070,15 @@ export function V4Timeline({ )} {showLanes ? ( <> - + {/* Its own provider, like the tool strip above: TransportBar has a tooltip too. */} + + +
Shift+Scroll {t("labels.pan")} @@ -2177,7 +2206,9 @@ export function V4Timeline({ onStartDrag={startAudioDrag} onSelect={tl.selectAudioTrack} label={track.label || asset?.label || ts("audioTrack.defaultLabel")} - slipHint={ts("audioTrack.slipHint")} + slipHint={ts("audioTrack.slipHint", { + modifier: isMac ? "Option" : "Alt", + })} slipArmed={slipArmed} outputGain={audioGainScalar(settings.audioGainDb)} ghost={((g) => @@ -2381,7 +2412,7 @@ export function V4Timeline({ className={styles.tlNavWindow} // Whole timeline in view: the thumb goes quiet (see .tlNavWindow[data-full]). data-full={navSpan >= 0.999 || undefined} - title={t("labels.pan")} + title={t("labels.panTip")} style={{ left: `${(nav.start * 100).toFixed(2)}%`, width: `${((nav.end - nav.start) * 100).toFixed(2)}%`, @@ -2393,13 +2424,13 @@ export function V4Timeline({ startNavDrag("left", e)} /> startNavDrag("right", e)} />
diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index a97ab04cc..d92ebe7f4 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useI18n, useScopedT } from "@/contexts/I18nContext"; import { getAvailableLocales, getLocaleName } from "@/i18n/loader"; +import { offersEditableCursor } from "@/lib/editableCursorAvailability"; import { loadUserPreferences, saveUserPreferences } from "@/lib/userPreferences"; import { nativeBridgeClient } from "@/native"; import { canRecordMicrophone } from "@/utils/platformUtils"; @@ -244,31 +245,14 @@ export function LaunchWindow() { // you get two cursors. Verified against a real recording at the // time. The helper is what makes the control mean something, // because it owns the video and never asks WebRTC for anything. - const offersCursorMode = - platform === "win32" || platform === "darwin" || platform === "linux"; setIsLinuxHud(platform === "linux"); // Without its native helper, macOS or Linux records through the browser, which - // always bakes in the system cursor (`effectiveBrowserCursorMode`). Switching - // to the editable cursor then restores nothing: an option that changes nothing - // is not shown, so the toggle waits for the helper's answer, and a failed - // answer keeps it hidden. Windows has no such fallback and no probe. - const probe = - platform === "darwin" - ? window.electronAPI?.isNativeMacCaptureAvailable - : platform === "linux" - ? window.electronAPI?.isNativeLinuxCaptureAvailable - : undefined; - if (!probe) { - setSupportsCursorModeToggle(offersCursorMode); - return; - } - probe() - .then((result) => { - if (!cancelled) setSupportsCursorModeToggle(result.success && result.available); - }) - .catch(() => { - if (!cancelled) setSupportsCursorModeToggle(false); - }); + // always bakes in the system cursor: an option that changes nothing is not + // shown, so the toggle waits for the helper's answer, and a failed answer keeps + // it hidden. The editor's Record mode asks the same question, in one place. + void offersEditableCursor(platform, window.electronAPI).then((offered) => { + if (!cancelled) setSupportsCursorModeToggle(offered); + }); } }) .catch(() => { diff --git a/src/hooks/useEditableCursorAvailable.ts b/src/hooks/useEditableCursorAvailable.ts new file mode 100644 index 000000000..0042d7701 --- /dev/null +++ b/src/hooks/useEditableCursorAvailable.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from "react"; +import { offersEditableCursor } from "@/lib/editableCursorAvailability"; +import { getPlatform } from "@/utils/platformUtils"; + +/** + * Whether the editable cursor is worth offering here (see `offersEditableCursor`). + * + * `false` until the answer arrives, so the row waits for the native helper's answer and a failed + * one keeps it hidden: showing a control that does nothing is the worse mistake. + */ +export function useEditableCursorAvailable(): boolean { + const [available, setAvailable] = useState(false); + + useEffect(() => { + let cancelled = false; + void offersEditableCursor(getPlatform(), window.electronAPI).then((offered) => { + if (!cancelled) setAvailable(offered); + }); + return () => { + cancelled = true; + }; + }, []); + + return available; +} diff --git a/src/i18n/__tests__/tooltipCopy.test.ts b/src/i18n/__tests__/tooltipCopy.test.ts index c363f800f..be1036b9d 100644 --- a/src/i18n/__tests__/tooltipCopy.test.ts +++ b/src/i18n/__tests__/tooltipCopy.test.ts @@ -121,3 +121,136 @@ describe("long-tail tooltip strings", () => { expect(untranslated).toEqual([]); }); }); + +// The editor's own block: the timeline toolbar and transport, the top bar, the inspector rail, +// Record mode and the chat. Kept apart from the list above so the two grow independently. +const EDITOR_TOOLTIP_KEYS: Record = { + timeline: [ + "buttons.addZoom", + "buttons.addTrim", + "buttons.addSpeed", + "buttons.addAnnotation", + "buttons.addCameraFullscreen", + "buttons.autoFocusAll", + "buttons.autoFocusAllTip", + "buttons.clearTimeline", + "toolbar.dragToReorderHint", + "toolbar.editInOutPoints", + "toolbar.deleteClip", + "labels.panTip", + "labels.zoomTip", + ], + editor: [ + "transport.play", + "transport.pause", + "topbar.toggleChatPanel", + "inspector.editClipTip", + "rec.systemAudioTip", + "rec.editableCursor", + "rec.editableCursorTip", + "rec.hideDesktopIconsHintMac", + "rec.hideDesktopIconsHintWindows", + "chat.contextTooltip", + "chat.compactContext", + "chat.aiSettings", + "chat.history", + "chat.newConversation", + "chat.clickToRename", + "chat.renameConversation", + "chat.deleteConversation", + "chat.rewindToMessage", + "chat.rewindTip", + "chat.copyMessage", + "chat.send", + "chat.composerDisabledNoProvider", + "mediaStage.regenerate", + "mediaStage.regenerateTip", + "mediaStage.cpuBackendHint", + ], + settings: [ + "facets.tips.effects", + "facets.tips.layout", + "facets.tips.audio", + "facets.tips.cursor", + "facets.tips.transcript", + "audioTrack.slipHint", + ], + shortcuts: ["fixedActions.undo", "fixedActions.redo"], +}; +// Tooltips that end in a chip built from the user's live binding (formatBinding). A key written +// into the string would lie after a remap, and inside Arabic text it breaks the order of words. +const CHIP_KEYS: Record = { + timeline: [ + "buttons.addZoom", + "buttons.addTrim", + "buttons.addSpeed", + "buttons.addAnnotation", + "buttons.addCameraFullscreen", + ], + editor: ["transport.play", "transport.pause", "chat.send"], +}; +const editorEntries = Object.entries(EDITOR_TOOLTIP_KEYS).flatMap(([namespace, keys]) => + keys.map((key) => [namespace, key] as const), +); +const chipEntries = Object.entries(CHIP_KEYS).flatMap(([namespace, keys]) => + keys.map((key) => [namespace, key] as const), +); + +describe("editor tooltip strings", () => { + it.each(locales)("%s keeps the placeholders of en and stays short", (locale) => { + const placeholderDrift: string[] = []; + const tooLong: string[] = []; + for (const [namespace, key] of editorEntries) { + const text = read(locale, namespace, key); + const source = read("en", namespace, key); + if (placeholders(text).join() !== placeholders(source).join()) { + placeholderDrift.push(`${namespace}.${key}: ${text}`); + } + if (text.length > MAX_LENGTH) tooLong.push(`${namespace}.${key} (${text.length})`); + } + expect(placeholderDrift).toEqual([]); + expect(tooLong).toEqual([]); + }); + + it.each(locales)("%s writes no key into a tooltip that shows a shortcut chip", (locale) => { + const bakedIn = chipEntries + .map(([namespace, key]) => [`${namespace}.${key}`, read(locale, namespace, key)] as const) + // "(Z)", "(Space)", "(Z)" and the like, in any script. + .filter(([, text]) => /[((]\s*(?:[A-Z]|Space|Enter|Ctrl[^))]*)\s*[))]/.test(text)); + expect(bakedIn).toEqual([]); + }); + + // French sets a no-break space before : ; ? !, so a tooltip never wraps with the mark starting + // the next line ("Ajouter Caméra plein écran" / ": affiche ..."). + it("fr keeps punctuation with the word before it", () => { + const loose = editorEntries + .map(([namespace, key]) => [`${namespace}.${key}`, read("fr", namespace, key)] as const) + .filter(([, text]) => / [:;?!]/.test(text)); + expect(loose).toEqual([]); + }); + + // One term per concept: the HUD's toggle and this row are the same setting. + it.each(locales)("%s calls the Record mode cursor row what the HUD calls it", (locale) => { + expect(read(locale, "editor", "rec.editableCursor")).toBe( + read(locale, "launch", "cursor.name"), + ); + }); + + // Clear timeline has no confirmation, so its tooltip is where the loss is said: what goes + // (Full Camera included) and what stays. + it("says what Clear timeline removes and what it keeps", () => { + const text = read("en", "timeline", "buttons.clearTimeline"); + for (const goes of ["zooms", "trims", "speeds", "annotations", "Full Camera"]) { + expect(text).toContain(goes); + } + expect(text).toContain("Clips, audio and captions stay"); + }); + + it.each(locales)("%s does not call the moving line on the timeline the playhead", (locale) => { + // It is the word for the mouse cursor in pt-BR, ru and tr: the strings say "current time". + const uses = editorEntries + .map(([namespace, key]) => [`${namespace}.${key}`, read(locale, namespace, key)] as const) + .filter(([, text]) => /playhead/i.test(text)); + expect(uses).toEqual([]); + }); +}); diff --git a/src/i18n/locales/ar/editor.json b/src/i18n/locales/ar/editor.json index a6064b425..d048ec23d 100644 --- a/src/i18n/locales/ar/editor.json +++ b/src/i18n/locales/ar/editor.json @@ -86,7 +86,7 @@ } }, "topbar": { - "toggleChatPanel": "تبديل لوحة الدردشة", + "toggleChatPanel": "لوحة الدردشة", "openProject": "فتح مشروع", "newProject": "مشروع جديد", "saveProject": "حفظ المشروع", @@ -111,13 +111,15 @@ "selectSource": "اختر ما تريد تسجيله", "systemPicker": "سيسألك النظام عمّا تريد مشاركته", "systemAudio": "صوت النظام", + "systemAudioTip": "يسجّل الصوت الذي يشغّله الكمبيوتر", "microphone": "الميكروفون", "camera": "الكاميرا", - "cursorHighlight": "إبراز المؤشر", + "editableCursor": "مؤشر قابل للتحرير", + "editableCursorTip": "التشغيل: مؤشر قابل للتحرير، مع تغيير الشكل والتكبير التلقائي. الإيقاف: مؤشر النظام، يُسجَّل داخل الفيديو.", "autoZoom": "تكبير تلقائي بعد التسجيل", "hideDesktopIcons": "إخفاء أيقونات سطح المكتب", "hideDesktopIconsHintMac": "يستبعد أيقونات سطح المكتب من تسجيلات الشاشة. تبقى على سطح المكتب لديك.", - "hideDesktopIconsHintWindows": "يغطي أيقونات سطح المكتب بخلفية الشاشة أثناء تسجيل الشاشة. تعود عند انتهاء التسجيل.", + "hideDesktopIconsHintWindows": "يغطي أيقونات سطح المكتب أثناء التسجيل. تعود عند توقفه.", "on": "تشغيل", "off": "إيقاف", "loading": "جارٍ التحميل...", @@ -156,6 +158,7 @@ "regenerateAs": "إعادة الإنشاء بلغة", "auto": "تلقائي", "regenerate": "إعادة الإنشاء", + "regenerateTip": "إعادة الإنشاء: تفريغ النص مرة أخرى باللغة المختارة", "transcriptEmpty": "النص فارغ.", "notGeneratedHint": "لم يُنشأ بعد — اختر لغة وانقر على إعادة الإنشاء.", "transcribing": "جارٍ النسخ", @@ -163,7 +166,7 @@ "initializingModel": "جارٍ تهيئة نموذج الكلام", "pendingTranscription": "بانتظار النسخ", "transcriptionFailed": "فشل النسخ", - "cpuBackendHint": "يعمل على المعالج — أبطأ بنحو 2× من معالج الرسوميات", + "cpuBackendHint": "يعمل على المعالج: أبطأ بنحو مرتين من بطاقة الرسومات", "noTranscript": "لا يوجد نص مكتوب", "generationFailedHint": "فشل الإنشاء — اختر لغة وأعد الإنشاء.", "detectedLanguage": "اللغة المكتشفة: {{language}}", @@ -213,6 +216,7 @@ "trimHiddenDuration": "تم إخفاء {{duration}} ثانية من الوسائط المصدر عن الجدول الزمني المحرر.", "restoreDeleteTrim": "استعادة (حذف القص)", "collapseInspector": "طي لوحة الفحص", + "editClipTip": "تعديل المقطع: الاقتصاص والبداية والنهاية", "captionsDescription": "إنشاء ترجمات مؤقتة بالكلمات من النص وإسقاطها على الجدول الزمني.", "generateCaptions": "إنشاء ترجمات", "cropDescription": "أعد تأطير التسجيل — اختر نسبة العرض إلى الارتفاع وقم بالتكبير على المنطقة التي تريد الاحتفاظ بها.", @@ -248,9 +252,9 @@ "reasoningEffortUpdateFailed": "تعذّر تحديث جهد الاستدلال", "contextTooltip": "{{usedTokens}} / {{budgetTokens}} رمز مقدّر", "contextPercent": "{{percent}}% من السياق", - "compactContext": "ضغط السياق", + "compactContext": "تلخيص الرسائل السابقة لاستخدام سياق أقل", "aiSettings": "إعدادات الذكاء الاصطناعي", - "history": "السجل", + "history": "سجل المحادثات", "newConversation": "محادثة جديدة", "untitledConversation": "محادثة", "clickToRename": "انقر لإعادة التسمية", @@ -270,6 +274,7 @@ "authorUser": "أنت", "authorAssistant": "OpenScreen", "rewindToMessage": "إعادة الضبط إلى هذه الرسالة", + "rewindTip": "إعادة الضبط إلى هذه الرسالة. يتم التراجع عن التغييرات اللاحقة.", "copyMessage": "نسخ الرسالة", "copiedToClipboard": "تم النسخ إلى الحافظة", "copyFailed": "تعذّر النسخ", @@ -279,7 +284,6 @@ "composerDisabledNoProvider": "اربط مزوّداً لبدء المحادثة.", "modelLabel": "النموذج", "reasoningEffortLabel": "جهد الاستدلال", - "sendTitle": "إرسال (Enter)", "send": "إرسال", "rewindConfirmTitle": "إعادة الضبط هنا؟", "rewindConfirmBody": "سيتم التراجع عن تعديلات الوكيل والأدوار اللاحقة بعد هذه النقطة. سيُستعاد المشروع والمحادثة وحالة الوكيل. وسيتم استبدال أي تعديلات أجريتها منذ ذلك الحين أيضًا.", @@ -343,8 +347,8 @@ }, "transport": { "playbackControls": "عناصر التحكم في التشغيل", - "playPause": "تشغيل / إيقاف مؤقت", - "playPauseTitle": "تشغيل / إيقاف مؤقت (مسافة)", + "play": "تشغيل", + "pause": "إيقاف مؤقت", "loop": "تكرار" }, "shell": { diff --git a/src/i18n/locales/ar/settings.json b/src/i18n/locales/ar/settings.json index eaabb79a5..499b16f5f 100644 --- a/src/i18n/locales/ar/settings.json +++ b/src/i18n/locales/ar/settings.json @@ -275,7 +275,7 @@ "fadeIn": "تلاشٍ للداخل", "remove": "حذف المسار", "loop": "تكرار", - "slipHint": "اضغط Alt واسحب لتحريك الصوت بالداخل", + "slipHint": "اضغط باستمرار على {{modifier}} واسحب لتحريك الصوت داخل المسار", "help": "مستوى الصوت والتلاشي والتكرار لهذا المسار الصوتي. يُمزج فوق التسجيل في المعاينة وعند التصدير. ينخفض مسار الموسيقى تلقائيًا أثناء الكلام. اسحب المسار على الخط الزمني لنقله أو تغيير حجمه، أو اضغط Alt واسحب لتحريك الصوت بداخله.", "add": "إضافة مسار صوتي", "mute": "كتم" @@ -334,7 +334,14 @@ }, "facets": { "transcript": "النص", - "captions": "الترجمة" + "captions": "الترجمة", + "tips": { + "effects": "التركيب: الخلفية والإطار والظل والمسافة البادئة", + "layout": "تخطيط الكاميرا: موضع الكاميرا وشكلها وحجمها", + "audio": "الصوت: مستوى إخراج الفيديو كله", + "cursor": "المؤشر: النمط والحجم والتأثيرات", + "transcript": "النص: عدّل الكلمات لقص الفيديو" + } }, "crop": { "title": "اقتصاص", diff --git a/src/i18n/locales/ar/timeline.json b/src/i18n/locales/ar/timeline.json index 4b63494c3..9b2b76bb0 100644 --- a/src/i18n/locales/ar/timeline.json +++ b/src/i18n/locales/ar/timeline.json @@ -1,16 +1,13 @@ { "buttons": { - "addZoom": "إضافة تكبير (Z)", - "suggestZooms": "اقتراح تكبير من المؤشر", - "autoZoomOn": "اقتراحات التكبير التلقائي مفعّلة — انقر لإزالة التكبيرات المقترحة", - "autoZoomOff": "اقتراحات التكبير التلقائي معطّلة — انقر لاقتراح تكبيرات من المؤشر", - "autoFocusAllOn": "التركيز التلقائي مفعّل لجميع التكبيرات — انقر للتبديل إلى يدوي للجميع", - "autoFocusAllOff": "تفعيل التركيز التلقائي لجميع التكبيرات (الكاميرا تتبع المؤشر)", - "clearTimeline": "مسح المخطط الزمني", - "addTrim": "إضافة قص (T)", - "addAnnotation": "إضافة شرح (A)", - "addSpeed": "إضافة سرعة (S)", - "addCameraFullscreen": "إضافة كاميرا كاملة الشاشة (C)" + "addZoom": "إضافة تكبير عند الوقت الحالي", + "autoFocusAll": "التركيز التلقائي لجميع التكبيرات", + "autoFocusAllTip": "تتبع جميع التكبيرات المؤشر. عند الإيقاف: تختار بنفسك تركيز كل تكبير.", + "clearTimeline": "إزالة جميع التكبيرات وعمليات القص والسرعات والشروح والكاميرا كاملة الشاشة. تبقى المقاطع والصوت والترجمة.", + "addTrim": "إضافة قص: يزيل جزءًا من الفيديو عند الوقت الحالي", + "addAnnotation": "إضافة نص أو سهم أو صورة أو تمويه عند الوقت الحالي", + "addSpeed": "إضافة تغيير في السرعة عند الوقت الحالي", + "addCameraFullscreen": "إضافة كاميرا كاملة الشاشة: تعرض الكاميرا بملء الشاشة عند الوقت الحالي" }, "hints": { "pressZoom": "اضغط Z لإضافة تكبير", @@ -22,7 +19,9 @@ }, "labels": { "pan": "تحريك", + "panTip": "تمرير المخطط الزمني", "zoom": "تكبير", + "zoomTip": "تكبير المخطط الزمني", "trim": "قص", "speed": "سرعة", "zoomItem": "تكبير {{index}}", @@ -71,7 +70,7 @@ "arrangeClips": "ترتيب المقاطع", "arrangeClipsHint": "اسحب المقاطع أدناه لإعادة ترتيبها أو إسقاط مقاطع جديدة", "newAnnotation": "شرح", - "dragToReorderHint": "اسحب لإعادة الترتيب · انقر نقرًا مزدوجًا لتعديل نقطتي البداية والنهاية", + "dragToReorderHint": "اسحب لإعادة الترتيب. انقر نقرًا مزدوجًا لتغيير البداية والنهاية.", "editInOutPoints": "تعديل نقطتي البداية والنهاية", "deleteClip": "حذف المقطع", "dropToAdd": "أفلت للإضافة إلى المخطط الزمني", diff --git a/src/i18n/locales/cs/editor.json b/src/i18n/locales/cs/editor.json index 1df540d7c..0dc555630 100644 --- a/src/i18n/locales/cs/editor.json +++ b/src/i18n/locales/cs/editor.json @@ -86,7 +86,7 @@ } }, "topbar": { - "toggleChatPanel": "Přepnout panel chatu", + "toggleChatPanel": "Panel chatu", "openProject": "Otevřít projekt", "newProject": "Nový projekt", "saveProject": "Uložit projekt", @@ -111,13 +111,15 @@ "selectSource": "Vyberte, co nahrávat", "systemPicker": "Systém se zeptá, co sdílet", "systemAudio": "Systémový zvuk", + "systemAudioTip": "Nahrává zvuk, který přehrává počítač", "microphone": "Mikrofon", "camera": "Kamera", - "cursorHighlight": "Zvýraznění kurzoru", + "editableCursor": "Upravitelný kurzor", + "editableCursorTip": "Zapnuto: upravitelný kurzor, se změnou stylu a automatickým přiblížením. Vypnuto: systémový kurzor, zaznamenaný ve videu.", "autoZoom": "Automatické přiblížení po nahrávání", "hideDesktopIcons": "Skrýt ikony na ploše", "hideDesktopIconsHintMac": "Vynechá ikony na ploše ze záznamů obrazovky. Na ploše vám zůstanou.", - "hideDesktopIconsHintWindows": "Během nahrávání obrazovky zakryje ikony na ploše tapetou. Po skončení nahrávání se vrátí.", + "hideDesktopIconsHintWindows": "Zakryje ikony na ploše během nahrávání. Po jeho skončení se vrátí.", "on": "Zapnuto", "off": "Vypnuto", "loading": "Načítání…", @@ -158,11 +160,12 @@ "initializingModel": "Spouštění modelu řeči", "pendingTranscription": "Přepis čeká", "transcriptionFailed": "Přepis selhal", - "cpuBackendHint": "Běží na CPU (přibližně 2× pomalejší než na GPU)", + "cpuBackendHint": "Běží na CPU: přibližně 2× pomalejší než na GPU", "noTranscript": "Žádný přepis", "regenerateAs": "Znovu generovat jako", "auto": "Automaticky", "regenerate": "Znovu generovat", + "regenerateTip": "Znovu generovat: znovu přepsat ve zvoleném jazyce", "transcriptEmpty": "Přepis je prázdný.", "notGeneratedHint": "Ještě nevygenerováno. Vyberte jazyk a klikněte na Znovu generovat.", "generationFailedHint": "Generování selhalo. Vyberte jazyk a znovu generujte.", @@ -213,6 +216,7 @@ "trimHiddenDuration": "{{duration}}s zdrojového média skryto z upravené časové osy.", "restoreDeleteTrim": "Obnovit (smazat oříznutí)", "collapseInspector": "Sbalit inspektor", + "editClipTip": "Upravit klip: oříznutí, začátek a konec", "captionsDescription": "Generujte titulky synchronizované se slovy z přepisu a přetáhněte je na časovou osu.", "generateCaptions": "Generovat titulky", "cropDescription": "Přeformátujte nahrávku: vyberte poměr stran a přibližte oblast, kterou chcete zachovat.", @@ -248,9 +252,9 @@ "reasoningEffortUpdateFailed": "Nepodařilo se aktualizovat úsilí uvažování", "contextTooltip": "{{usedTokens}} / {{budgetTokens}} odhadovaných tokenů", "contextPercent": "{{percent}}% kontextu", - "compactContext": "Komprimovat kontext", + "compactContext": "Shrnout dřívější zprávy, aby se použilo méně kontextu", "aiSettings": "Nastavení AI", - "history": "Historie", + "history": "Historie konverzací", "newConversation": "Nová konverzace", "untitledConversation": "Konverzace", "clickToRename": "Klikněte pro přejmenování", @@ -270,6 +274,7 @@ "authorUser": "Vy", "authorAssistant": "OpenScreen", "rewindToMessage": "Přetočit na tuto zprávu", + "rewindTip": "Přetočit na tuto zprávu. Pozdější změny se vrátí zpět.", "copyMessage": "Kopírovat zprávu", "copiedToClipboard": "Zkopírováno do schránky", "copyFailed": "Kopírování selhalo", @@ -279,7 +284,6 @@ "composerDisabledNoProvider": "Nastavte poskytovatele pro zahájení chatu.", "modelLabel": "Model", "reasoningEffortLabel": "Úsilí uvažování", - "sendTitle": "Odeslat (Enter)", "send": "Odeslat", "rewindConfirmTitle": "Přetočit sem?", "rewindConfirmBody": "Úpravy agenta a navazující zprávy po tomto bodu budou vráceny. Projekt, konverzace a stav agenta budou obnoveny. Jakékoli úpravy, které jste od té doby provedli, budou také nahrazeny.", @@ -343,8 +347,8 @@ }, "transport": { "playbackControls": "Ovládání přehrávání", - "playPause": "Přehrát / Pozastavit", - "playPauseTitle": "Přehrát / Pozastavit (mezerník)", + "play": "Přehrát", + "pause": "Pozastavit", "loop": "Opakovat" }, "shell": { diff --git a/src/i18n/locales/cs/settings.json b/src/i18n/locales/cs/settings.json index 113fefc99..3032960cf 100644 --- a/src/i18n/locales/cs/settings.json +++ b/src/i18n/locales/cs/settings.json @@ -275,7 +275,7 @@ "fadeIn": "Zesílení", "remove": "Smazat stopu", "loop": "Opakovat", - "slipHint": "Alt-přetažením posuňte zvuk uvnitř", + "slipHint": "Podržte {{modifier}} a přetažením posuňte zvuk uvnitř stopy", "help": "Hlasitost, přechody a opakování pro tuto zvukovou stopu. Mixuje se s nahrávkou v náhledu i při exportu. Hudební stopa se při mluvení sama ztiší. Přetáhněte stopu na časové ose pro přesunutí nebo změnu velikosti, nebo podržte Alt a přetáhněte pro posun zvuku uvnitř.", "add": "Přidat zvukovou stopu", "mute": "Ztlumit" @@ -334,7 +334,14 @@ }, "facets": { "transcript": "Přepis", - "captions": "Titulky" + "captions": "Titulky", + "tips": { + "effects": "Kompozice: pozadí, rám, stín a odsazení", + "layout": "Rozvržení kamery: poloha, tvar a velikost kamery", + "audio": "Zvuk: výstupní úroveň celého videa", + "cursor": "Kurzor: styl, velikost a efekty", + "transcript": "Přepis: upravujte slova a stříhejte video" + } }, "crop": { "title": "Oříznutí", diff --git a/src/i18n/locales/cs/timeline.json b/src/i18n/locales/cs/timeline.json index 0a08fa905..aa727dd26 100644 --- a/src/i18n/locales/cs/timeline.json +++ b/src/i18n/locales/cs/timeline.json @@ -1,16 +1,13 @@ { "buttons": { - "addZoom": "Přidat přiblížení (Z)", - "suggestZooms": "Navrhnout přiblížení podle kurzoru", - "autoZoomOn": "Automatické návrhy přiblížení zapnuty, klikněte pro odstranění navržených přiblížení", - "autoZoomOff": "Automatické návrhy přiblížení vypnuty, klikněte pro návrh přiblížení podle kurzoru", - "autoFocusAllOn": "Automatické zaostření zapnuto pro všechna přiblížení, klikněte pro přepnutí všech na ruční", - "autoFocusAllOff": "Automaticky zaostřit všechna přiblížení (kamera sleduje kurzor)", - "clearTimeline": "Vymazat časovou osu", - "addTrim": "Přidat oříznutí (T)", - "addAnnotation": "Přidat anotaci (A)", - "addSpeed": "Přidat rychlost (S)", - "addCameraFullscreen": "Přidat celou kameru (C)" + "addZoom": "Přidat přiblížení v aktuálním čase", + "autoFocusAll": "Automatické zaostření všech přiblížení", + "autoFocusAllTip": "Všechna přiblížení sledují kurzor. Vypnuto: zaostření každého přiblížení volíte sami.", + "clearTimeline": "Odstranit všechna přiblížení, oříznutí, změny rychlosti, anotace a Celou kameru. Klipy, zvuk a titulky zůstanou.", + "addTrim": "Přidat oříznutí: odstraní část videa v aktuálním čase", + "addAnnotation": "Přidat text, šipku, obrázek nebo rozmazání v aktuálním čase", + "addSpeed": "Přidat změnu rychlosti v aktuálním čase", + "addCameraFullscreen": "Přidat Celou kameru: zobrazí kameru na celou obrazovku v aktuálním čase" }, "hints": { "pressZoom": "Stiskněte Z pro přidání přiblížení", @@ -22,7 +19,9 @@ }, "labels": { "pan": "Posun", + "panTip": "Posun časové osy", "zoom": "Přiblížení", + "zoomTip": "Přiblížení časové osy", "trim": "Oříznutí", "speed": "Rychlost", "zoomItem": "Přiblížení {{index}}", @@ -71,7 +70,7 @@ "arrangeClips": "Uspořádat klipy", "arrangeClipsHint": "Přetáhněte klipy níže pro změnu pořadí nebo přidejte nové", "newAnnotation": "Anotace", - "dragToReorderHint": "Přetáhněte pro změnu pořadí · dvojklik pro úpravu vstupních/výstupních bodů", + "dragToReorderHint": "Přetažením změníte pořadí. Dvojklikem upravíte začátek a konec.", "editInOutPoints": "Upravit vstupní/výstupní body", "deleteClip": "Smazat klip", "dropToAdd": "Přetáhněte pro přidání na časovou osu", diff --git a/src/i18n/locales/de/editor.json b/src/i18n/locales/de/editor.json index 132cd865e..25b7887e2 100644 --- a/src/i18n/locales/de/editor.json +++ b/src/i18n/locales/de/editor.json @@ -86,7 +86,7 @@ } }, "topbar": { - "toggleChatPanel": "Chat-Panel ein-/ausblenden", + "toggleChatPanel": "Chat-Panel", "openProject": "Projekt öffnen", "newProject": "Neues Projekt", "saveProject": "Projekt speichern", @@ -111,13 +111,15 @@ "selectSource": "Wähle aus, was du aufnehmen möchtest", "systemPicker": "Dein System fragt gleich, was du teilen möchtest", "systemAudio": "Systemton", + "systemAudioTip": "Nimmt den Ton auf, den dein Computer wiedergibt", "microphone": "Mikrofon", "camera": "Kamera", - "cursorHighlight": "Cursor hervorheben", + "editableCursor": "Bearbeitbarer Cursor", + "editableCursorTip": "An: bearbeitbarer Cursor, mit Stiländerung und Auto-Zoom. Aus: System-Cursor, im Video aufgenommen.", "autoZoom": "Auto-Zoom nach der Aufnahme", "hideDesktopIcons": "Schreibtischsymbole ausblenden", "hideDesktopIconsHintMac": "Lässt die Schreibtischsymbole in Bildschirmaufnahmen weg. Auf deinem Schreibtisch bleiben sie.", - "hideDesktopIconsHintWindows": "Deckt die Desktopsymbole während einer Bildschirmaufnahme mit deinem Hintergrundbild ab. Nach der Aufnahme sind sie wieder da.", + "hideDesktopIconsHintWindows": "Deckt die Schreibtischsymbole während der Aufnahme ab. Danach sind sie wieder da.", "on": "An", "off": "Aus", "loading": "Wird geladen…", @@ -158,11 +160,12 @@ "initializingModel": "Sprachmodell wird gestartet", "pendingTranscription": "Transkription ausstehend", "transcriptionFailed": "Transkription fehlgeschlagen", - "cpuBackendHint": "Läuft auf der CPU — etwa 2× langsamer als auf der GPU", + "cpuBackendHint": "Läuft auf der CPU: etwa 2× langsamer als auf der GPU", "noTranscript": "Kein Transkript", "regenerateAs": "Neu erzeugen als", "auto": "Auto", "regenerate": "Neu erzeugen", + "regenerateTip": "Neu erzeugen: in der gewählten Sprache erneut transkribieren", "transcriptEmpty": "Das Transkript ist leer.", "notGeneratedHint": "Noch nicht erzeugt — wähle eine Sprache und klicke auf Neu erzeugen.", "generationFailedHint": "Erzeugung fehlgeschlagen — wähle eine Sprache und erzeuge neu.", @@ -213,6 +216,7 @@ "trimHiddenDuration": "{{duration}} s des Quellmaterials sind in der bearbeiteten Timeline ausgeblendet.", "restoreDeleteTrim": "Wiederherstellen (Trim löschen)", "collapseInspector": "Inspektor einklappen", + "editClipTip": "Clip bearbeiten: Zuschnitt, Anfang und Ende", "captionsDescription": "Erzeuge wortgenaue Untertitel aus dem Transkript und leg sie auf die Timeline.", "generateCaptions": "Untertitel erzeugen", "cropDescription": "Rahme die Aufnahme neu — wähle ein Seitenverhältnis und zoome auf den Bereich, den du behalten willst.", @@ -248,9 +252,9 @@ "reasoningEffortUpdateFailed": "Denkaufwand konnte nicht aktualisiert werden", "contextTooltip": "{{usedTokens}} / {{budgetTokens}} geschätzte Tokens", "contextPercent": "{{percent}} % Kontext", - "compactContext": "Kontext verdichten", + "compactContext": "Frühere Nachrichten zusammenfassen, um weniger Kontext zu nutzen", "aiSettings": "KI-Einstellungen", - "history": "Verlauf", + "history": "Unterhaltungsverlauf", "newConversation": "Neue Unterhaltung", "untitledConversation": "Unterhaltung", "clickToRename": "Zum Umbenennen klicken", @@ -270,6 +274,7 @@ "authorUser": "Du", "authorAssistant": "OpenScreen", "rewindToMessage": "Zu dieser Nachricht zurückspulen", + "rewindTip": "Zu dieser Nachricht zurückspulen. Spätere Änderungen werden zurückgenommen.", "copyMessage": "Nachricht kopieren", "copiedToClipboard": "In die Zwischenablage kopiert", "copyFailed": "Kopieren fehlgeschlagen", @@ -279,7 +284,6 @@ "composerDisabledNoProvider": "Richte einen Anbieter ein, um zu chatten.", "modelLabel": "Modell", "reasoningEffortLabel": "Denkaufwand", - "sendTitle": "Senden (Enter)", "send": "Senden", "rewindConfirmTitle": "Hierher zurückspulen?", "rewindConfirmBody": "Die Änderungen des Agenten und alle folgenden Züge nach diesem Punkt werden zurückgenommen. Projekt, Unterhaltung und Agentenzustand werden wiederhergestellt. Auch die Änderungen, die du seitdem gemacht hast, werden ersetzt.", @@ -343,8 +347,8 @@ }, "transport": { "playbackControls": "Wiedergabesteuerung", - "playPause": "Wiedergabe / Pause", - "playPauseTitle": "Wiedergabe / Pause (Leertaste)", + "play": "Wiedergabe", + "pause": "Pause", "loop": "Wiederholen" }, "shell": { diff --git a/src/i18n/locales/de/settings.json b/src/i18n/locales/de/settings.json index 1b519552c..47edd219e 100644 --- a/src/i18n/locales/de/settings.json +++ b/src/i18n/locales/de/settings.json @@ -275,7 +275,7 @@ "fadeIn": "Einblenden", "remove": "Spur löschen", "loop": "Wiederholen", - "slipHint": "Mit Alt ziehen, um das Audio darin zu verschieben", + "slipHint": "{{modifier}} halten und ziehen, um das Audio in der Spur zu verschieben", "help": "Lautstärke, Blenden und Wiederholung für diese Audiospur. Sie wird in der Vorschau und im Export über die Aufnahme gemischt. Eine Musikspur wird automatisch leiser, während jemand spricht. Zieh die Spur auf der Timeline, um sie zu verschieben oder ihre Länge zu ändern, oder halte Alt und zieh, um das Audio darin zu verschieben.", "add": "Audiospur hinzufügen", "mute": "Stumm" @@ -334,7 +334,14 @@ }, "facets": { "transcript": "Transkript", - "captions": "Untertitel" + "captions": "Untertitel", + "tips": { + "effects": "Komposition: Hintergrund, Rahmen, Schatten und Abstand", + "layout": "Kamera-Layout: Position, Form und Größe der Kamera", + "audio": "Audio: Ausgangspegel des ganzen Videos", + "cursor": "Cursor: Stil, Größe und Effekte", + "transcript": "Transkript: Wörter bearbeiten, um das Video zu schneiden" + } }, "crop": { "title": "Zuschneiden", diff --git a/src/i18n/locales/de/timeline.json b/src/i18n/locales/de/timeline.json index 51fd44c81..7a636a302 100644 --- a/src/i18n/locales/de/timeline.json +++ b/src/i18n/locales/de/timeline.json @@ -1,16 +1,13 @@ { "buttons": { - "addZoom": "Zoom hinzufügen (Z)", - "suggestZooms": "Zooms aus Cursorbewegung vorschlagen", - "autoZoomOn": "Zoom-Vorschläge an — klicken, um vorgeschlagene Zooms zu entfernen", - "autoZoomOff": "Zoom-Vorschläge aus — klicken, um Zooms aus der Cursorbewegung vorzuschlagen", - "autoFocusAllOn": "Auto-Fokus für alle Zooms an — klicken, um alle auf manuell umzustellen", - "autoFocusAllOff": "Auto-Fokus für alle Zooms (Kamera folgt dem Cursor)", - "clearTimeline": "Timeline leeren", - "addTrim": "Trim hinzufügen (T)", - "addAnnotation": "Anmerkung hinzufügen (A)", - "addSpeed": "Tempo hinzufügen (S)", - "addCameraFullscreen": "Vollbild-Kamera hinzufügen (C)" + "addZoom": "Zoom zum aktuellen Zeitpunkt hinzufügen", + "autoFocusAll": "Auto-Fokus für alle Zooms", + "autoFocusAllTip": "Alle Zooms folgen dem Cursor. Aus: Du wählst den Fokus jedes Zooms selbst.", + "clearTimeline": "Alle Zooms, Trims, Tempowechsel, Anmerkungen und Vollbild-Kameras entfernen. Clips, Audio und Untertitel bleiben.", + "addTrim": "Trim hinzufügen: entfernt einen Abschnitt des Videos zum aktuellen Zeitpunkt", + "addAnnotation": "Text, Pfeil, Bild oder Weichzeichner zum aktuellen Zeitpunkt hinzufügen", + "addSpeed": "Tempowechsel zum aktuellen Zeitpunkt hinzufügen", + "addCameraFullscreen": "Vollbild-Kamera hinzufügen: zeigt die Kamera im Vollbild zum aktuellen Zeitpunkt" }, "hints": { "pressZoom": "Drücke Z, um einen Zoom hinzuzufügen", @@ -22,7 +19,9 @@ }, "labels": { "pan": "Verschieben", + "panTip": "Timeline scrollen", "zoom": "Zoom", + "zoomTip": "Timeline zoomen", "trim": "Trim", "speed": "Tempo", "zoomItem": "Zoom {{index}}", @@ -71,7 +70,7 @@ "arrangeClips": "Clips anordnen", "arrangeClipsHint": "Zieh die Clips unten, um sie neu zu ordnen, oder leg neue hinein", "newAnnotation": "Anmerkung", - "dragToReorderHint": "Ziehen zum Neuordnen · Doppelklick für In-/Out-Punkte", + "dragToReorderHint": "Zum Neuordnen ziehen. Doppelklick, um Anfang und Ende zu ändern.", "editInOutPoints": "In-/Out-Punkte bearbeiten", "deleteClip": "Clip löschen", "dropToAdd": "Ablegen, um zur Timeline hinzuzufügen", diff --git a/src/i18n/locales/en/editor.json b/src/i18n/locales/en/editor.json index e0d825de8..bbb7f1da6 100644 --- a/src/i18n/locales/en/editor.json +++ b/src/i18n/locales/en/editor.json @@ -86,7 +86,7 @@ } }, "topbar": { - "toggleChatPanel": "Toggle chat panel", + "toggleChatPanel": "Chat panel", "openProject": "Open project", "newProject": "New project", "saveProject": "Save project", @@ -111,13 +111,15 @@ "selectSource": "Choose what to record", "systemPicker": "Your system will ask what to share", "systemAudio": "System audio", + "systemAudioTip": "Records the sound your computer plays", "microphone": "Microphone", "camera": "Camera", - "cursorHighlight": "Cursor highlight", + "editableCursor": "Editable cursor", + "editableCursorTip": "On: editable cursor, with restyling and auto-zoom. Off: system cursor, recorded in the video.", "autoZoom": "Auto-zoom after recording", "hideDesktopIcons": "Hide desktop icons", "hideDesktopIconsHintMac": "Leaves the desktop icons out of screen recordings. They stay on your desktop.", - "hideDesktopIconsHintWindows": "Covers the desktop icons with your wallpaper while a screen is recording. They come back when recording stops.", + "hideDesktopIconsHintWindows": "Covers the desktop icons while you record. They return when it stops.", "on": "On", "off": "Off", "loading": "Loading…", @@ -158,11 +160,12 @@ "initializingModel": "Starting speech model", "pendingTranscription": "Pending transcription", "transcriptionFailed": "Transcription failed", - "cpuBackendHint": "Running on CPU — about 2× slower than on the GPU", + "cpuBackendHint": "Running on the CPU: about 2× slower than on the GPU", "noTranscript": "No transcript", "regenerateAs": "Regenerate as", "auto": "Auto", "regenerate": "Regenerate", + "regenerateTip": "Regenerate: transcribe again in the chosen language", "transcriptEmpty": "Transcript is empty.", "notGeneratedHint": "Not generated yet — pick a language and click regenerate.", "generationFailedHint": "Generation failed — pick a language and regenerate.", @@ -213,6 +216,7 @@ "trimHiddenDuration": "{{duration}}s of source media hidden from the edited timeline.", "restoreDeleteTrim": "Bring this part back", "collapseInspector": "Collapse inspector", + "editClipTip": "Edit clip: crop, start and end", "captionsDescription": "Generate word-timed captions from the transcript and drop them onto the timeline.", "generateCaptions": "Generate captions", "cropDescription": "Reframe the recording — pick an aspect ratio and zoom into the region you want to keep.", @@ -248,9 +252,9 @@ "reasoningEffortUpdateFailed": "Could not update reasoning effort", "contextTooltip": "{{usedTokens}} / {{budgetTokens}} estimated tokens", "contextPercent": "{{percent}}% context", - "compactContext": "Compact context", + "compactContext": "Summarize earlier messages to use less context", "aiSettings": "AI settings", - "history": "History", + "history": "Conversation history", "newConversation": "New conversation", "untitledConversation": "Conversation", "clickToRename": "Click to rename", @@ -270,6 +274,7 @@ "authorUser": "You", "authorAssistant": "OpenScreen", "rewindToMessage": "Rewind to this message", + "rewindTip": "Rewind to this message. Later changes are undone.", "copyMessage": "Copy message", "copiedToClipboard": "Copied to clipboard", "copyFailed": "Couldn't copy", @@ -279,7 +284,6 @@ "composerDisabledNoProvider": "Set up a provider to start chatting.", "modelLabel": "Model", "reasoningEffortLabel": "Reasoning effort", - "sendTitle": "Send (Enter)", "send": "Send", "rewindConfirmTitle": "Rewind here?", "rewindConfirmBody": "The agent's edits and follow-up turns after this point get rolled back. Project, conversation, and agent state will be restored. Any edits you made since then are replaced too.", @@ -343,8 +347,8 @@ }, "transport": { "playbackControls": "Playback controls", - "playPause": "Play / Pause", - "playPauseTitle": "Play / Pause (Space)", + "play": "Play", + "pause": "Pause", "loop": "Loop" }, "shell": { diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 4871d461d..1ce746841 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -275,7 +275,7 @@ "fadeIn": "Fade in", "remove": "Delete track", "loop": "Loop", - "slipHint": "Alt-drag to slide the audio inside it", + "slipHint": "Hold {{modifier}} and drag to slide the audio inside the track", "help": "Volume, fades and looping for this audio track. It mixes over the recording in the preview and the export. A music track dips on its own while someone speaks. Drag the track on the timeline to move or resize it, or hold Alt and drag to slide the audio inside it.", "add": "Add audio track", "mute": "Mute" @@ -334,7 +334,14 @@ }, "facets": { "transcript": "Transcript", - "captions": "Captions" + "captions": "Captions", + "tips": { + "effects": "Composition: background, frame, shadow and padding", + "layout": "Camera layout: position, shape and size of the camera", + "audio": "Audio: output level of the whole video", + "cursor": "Cursor: style, size and effects", + "transcript": "Transcript: edit the words to cut the video" + } }, "crop": { "title": "Crop", diff --git a/src/i18n/locales/en/timeline.json b/src/i18n/locales/en/timeline.json index a1ed0c963..74d95019d 100644 --- a/src/i18n/locales/en/timeline.json +++ b/src/i18n/locales/en/timeline.json @@ -1,16 +1,13 @@ { "buttons": { - "addZoom": "Add zoom (Z)", - "suggestZooms": "Suggest zooms from cursor", - "autoZoomOn": "Auto zoom suggestions on — click to remove suggested zooms", - "autoZoomOff": "Auto zoom suggestions off — click to suggest zooms from cursor", - "autoFocusAllOn": "Auto-Focus on for all zooms — click to switch all to manual", - "autoFocusAllOff": "Auto-Focus all zooms (camera follows the cursor)", - "clearTimeline": "Clear timeline", - "addTrim": "Add trim (T)", - "addAnnotation": "Add annotation (A)", - "addSpeed": "Add speed (S)", - "addCameraFullscreen": "Add Full Camera (C)" + "addZoom": "Add a zoom at the current time", + "autoFocusAll": "Auto-Focus for all zooms", + "autoFocusAllTip": "All zooms follow the cursor. Off: you choose the focus of each zoom.", + "clearTimeline": "Remove all zooms, trims, speeds, annotations and Full Camera. Clips, audio and captions stay.", + "addTrim": "Add a trim: removes a section of the video at the current time", + "addAnnotation": "Add text, an arrow, an image or a blur at the current time", + "addSpeed": "Add a speed change at the current time", + "addCameraFullscreen": "Add Full Camera: shows the camera full screen at the current time" }, "hints": { "pressZoom": "Press Z to add zoom", @@ -22,7 +19,9 @@ }, "labels": { "pan": "Pan", + "panTip": "Scroll the timeline", "zoom": "Zoom", + "zoomTip": "Zoom the timeline", "trim": "Trim", "speed": "Speed", "zoomItem": "Zoom {{index}}", @@ -71,7 +70,7 @@ "arrangeClips": "Arrange clips", "arrangeClipsHint": "Drag clips below to reorder or drop new ones in", "newAnnotation": "Annotation", - "dragToReorderHint": "Drag to reorder · double-click to change where it starts and ends", + "dragToReorderHint": "Drag to reorder. Double-click to change the start and end.", "editInOutPoints": "Edit start and end", "deleteClip": "Delete clip", "dropToAdd": "Drop to add to timeline", diff --git a/src/i18n/locales/es/editor.json b/src/i18n/locales/es/editor.json index b5281119f..4cf1453fa 100644 --- a/src/i18n/locales/es/editor.json +++ b/src/i18n/locales/es/editor.json @@ -86,7 +86,7 @@ } }, "topbar": { - "toggleChatPanel": "Alternar panel de chat", + "toggleChatPanel": "Panel de chat", "openProject": "Abrir proyecto", "newProject": "Nuevo proyecto", "saveProject": "Guardar proyecto", @@ -111,13 +111,15 @@ "selectSource": "Elige qué grabar", "systemPicker": "El sistema te preguntará qué compartir", "systemAudio": "Audio del sistema", + "systemAudioTip": "Graba el sonido que reproduce tu equipo", "microphone": "Micrófono", "camera": "Cámara", - "cursorHighlight": "Resaltar cursor", + "editableCursor": "Cursor editable", + "editableCursorTip": "Activado: cursor editable, con cambio de estilo y zoom automático. Desactivado: cursor del sistema, grabado en el video.", "autoZoom": "Zoom automático tras grabar", "hideDesktopIcons": "Ocultar iconos del escritorio", "hideDesktopIconsHintMac": "Deja fuera los iconos del escritorio en las grabaciones de pantalla. Siguen en tu escritorio.", - "hideDesktopIconsHintWindows": "Cubre los iconos del escritorio con tu fondo de pantalla mientras se graba una pantalla. Vuelven al terminar la grabación.", + "hideDesktopIconsHintWindows": "Cubre los iconos del escritorio mientras grabas. Vuelven al terminar.", "on": "Activado", "off": "Desactivado", "loading": "Cargando…", @@ -156,6 +158,7 @@ "regenerateAs": "Regenerar en", "auto": "Auto", "regenerate": "Regenerar", + "regenerateTip": "Regenerar: transcribir de nuevo en el idioma elegido", "transcriptEmpty": "La transcripción está vacía.", "notGeneratedHint": "Aún no generada — elige un idioma y haz clic en regenerar.", "transcribing": "Transcribiendo", @@ -163,7 +166,7 @@ "initializingModel": "Iniciando el modelo de voz", "pendingTranscription": "Transcripción pendiente", "transcriptionFailed": "Error de transcripción", - "cpuBackendHint": "Ejecutándose en la CPU — unas 2× más lento que en la GPU", + "cpuBackendHint": "Ejecutándose en la CPU: aproximadamente 2× más lento que en la GPU", "noTranscript": "Sin transcripción", "generationFailedHint": "Error de generación — elige un idioma y vuelve a generar.", "detectedLanguage": "Idioma detectado: {{language}}", @@ -213,6 +216,7 @@ "trimHiddenDuration": "{{duration}}s de contenido fuente ocultos de la línea de tiempo editada.", "restoreDeleteTrim": "Restaurar (eliminar recorte)", "collapseInspector": "Contraer inspector", + "editClipTip": "Editar clip: recorte, inicio y final", "captionsDescription": "Genera subtítulos sincronizados con las palabras a partir de la transcripción y colócalos en la línea de tiempo.", "generateCaptions": "Generar subtítulos", "cropDescription": "Reencuadra la grabación: elige una relación de aspecto y haz zoom en la región que deseas conservar.", @@ -248,9 +252,9 @@ "reasoningEffortUpdateFailed": "No se pudo actualizar el esfuerzo de razonamiento", "contextTooltip": "{{usedTokens}} / {{budgetTokens}} tokens estimados", "contextPercent": "{{percent}}% de contexto", - "compactContext": "Compactar contexto", + "compactContext": "Resumir los mensajes anteriores para usar menos contexto", "aiSettings": "Configuración de IA", - "history": "Historial", + "history": "Historial de conversaciones", "newConversation": "Nueva conversación", "untitledConversation": "Conversación", "clickToRename": "Haz clic para renombrar", @@ -270,6 +274,7 @@ "authorUser": "Tú", "authorAssistant": "OpenScreen", "rewindToMessage": "Rebobinar a este mensaje", + "rewindTip": "Rebobinar a este mensaje. Los cambios posteriores se deshacen.", "copyMessage": "Copiar mensaje", "copiedToClipboard": "Copiado al portapapeles", "copyFailed": "No se pudo copiar", @@ -279,7 +284,6 @@ "composerDisabledNoProvider": "Configura un proveedor para empezar a chatear.", "modelLabel": "Modelo", "reasoningEffortLabel": "Esfuerzo de razonamiento", - "sendTitle": "Enviar (Intro)", "send": "Enviar", "rewindConfirmTitle": "¿Rebobinar aquí?", "rewindConfirmBody": "Las ediciones del agente y los turnos posteriores a este punto se revertirán. Se restaurará el proyecto, la conversación y el estado del agente. Las ediciones que hayas hecho desde entonces también se reemplazarán.", @@ -343,8 +347,8 @@ }, "transport": { "playbackControls": "Controles de reproducción", - "playPause": "Reproducir / Pausar", - "playPauseTitle": "Reproducir / Pausar (Espacio)", + "play": "Reproducir", + "pause": "Pausar", "loop": "Bucle" }, "shell": { diff --git a/src/i18n/locales/es/settings.json b/src/i18n/locales/es/settings.json index 5d0f1b40f..447e65b30 100644 --- a/src/i18n/locales/es/settings.json +++ b/src/i18n/locales/es/settings.json @@ -275,7 +275,7 @@ "fadeIn": "Aparición", "remove": "Eliminar pista", "loop": "Bucle", - "slipHint": "Alt + arrastrar para desplazar el audio dentro", + "slipHint": "Mantén presionado {{modifier}} y arrastra para desplazar el audio dentro de la pista", "help": "Volumen, fundidos y bucle de esta pista de audio. Se mezcla sobre la grabación en la vista previa y en la exportación. Una pista de música baja sola mientras alguien habla. Arrastra la pista en la línea de tiempo para moverla o redimensionarla, o mantén Alt y arrastra para desplazar el audio dentro.", "add": "Añadir pista de audio", "mute": "Silenciar" @@ -334,7 +334,14 @@ }, "facets": { "transcript": "Transcripción", - "captions": "Subtítulos" + "captions": "Subtítulos", + "tips": { + "effects": "Composición: fondo, marco, sombra y relleno", + "layout": "Disposición de cámara: posición, forma y tamaño de la cámara", + "audio": "Audio: nivel de salida de todo el video", + "cursor": "Cursor: estilo, tamaño y efectos", + "transcript": "Transcripción: edita las palabras para cortar el video" + } }, "crop": { "title": "Recortar", diff --git a/src/i18n/locales/es/timeline.json b/src/i18n/locales/es/timeline.json index fbc674fa9..bb476dbcd 100644 --- a/src/i18n/locales/es/timeline.json +++ b/src/i18n/locales/es/timeline.json @@ -1,16 +1,13 @@ { "buttons": { - "addZoom": "Agregar zoom (Z)", - "suggestZooms": "Sugerir zooms desde el cursor", - "autoZoomOn": "Sugerencias de zoom automático activadas — haz clic para quitar los zooms sugeridos", - "autoZoomOff": "Sugerencias de zoom automático desactivadas — haz clic para sugerir zooms desde el cursor", - "autoFocusAllOn": "Enfoque automático activado para todos los zooms — haz clic para pasar todos a manual", - "autoFocusAllOff": "Activar enfoque automático para todos los zooms (la cámara sigue el cursor)", - "clearTimeline": "Borrar la línea de tiempo", - "addTrim": "Agregar recorte (T)", - "addAnnotation": "Agregar anotación (A)", - "addSpeed": "Agregar velocidad (S)", - "addCameraFullscreen": "Agregar cámara a pantalla completa (C)" + "addZoom": "Agregar un zoom en el momento actual", + "autoFocusAll": "Enfoque automático de todos los zooms", + "autoFocusAllTip": "Todos los zooms siguen el cursor. Desactivado: tú eliges el enfoque de cada zoom.", + "clearTimeline": "Quitar todos los zooms, recortes, velocidades, anotaciones y Cámara a pantalla completa. Se conservan clips, audio y subtítulos.", + "addTrim": "Agregar un recorte: quita una parte del video en el momento actual", + "addAnnotation": "Agregar texto, una flecha, una imagen o un desenfoque en el momento actual", + "addSpeed": "Agregar un cambio de velocidad en el momento actual", + "addCameraFullscreen": "Agregar Cámara a pantalla completa: muestra la cámara a pantalla completa en el momento actual" }, "hints": { "pressZoom": "Presiona Z para agregar zoom", @@ -22,7 +19,9 @@ }, "labels": { "pan": "Desplazar", + "panTip": "Desplazar la línea de tiempo", "zoom": "Zoom", + "zoomTip": "Ampliar la línea de tiempo", "trim": "Recortar", "speed": "Velocidad", "zoomItem": "Zoom {{index}}", @@ -71,7 +70,7 @@ "arrangeClips": "Organizar clips", "arrangeClipsHint": "Arrastra los clips de abajo para reordenarlos o suelta otros nuevos", "newAnnotation": "Anotación", - "dragToReorderHint": "Arrastra para reordenar · doble clic para editar los puntos de entrada/salida", + "dragToReorderHint": "Arrastra para reordenar. Haz doble clic para cambiar el inicio y el final.", "editInOutPoints": "Editar puntos de entrada/salida", "deleteClip": "Eliminar clip", "dropToAdd": "Suelta para añadir a la línea de tiempo", diff --git a/src/i18n/locales/fr/editor.json b/src/i18n/locales/fr/editor.json index 8e6be1958..ddd24224e 100644 --- a/src/i18n/locales/fr/editor.json +++ b/src/i18n/locales/fr/editor.json @@ -86,7 +86,7 @@ } }, "topbar": { - "toggleChatPanel": "Afficher/masquer le panneau de discussion", + "toggleChatPanel": "Panneau de discussion", "openProject": "Ouvrir un projet", "newProject": "Nouveau projet", "saveProject": "Enregistrer le projet", @@ -111,13 +111,15 @@ "selectSource": "Choisissez quoi enregistrer", "systemPicker": "Le système vous demandera quoi partager", "systemAudio": "Audio système", + "systemAudioTip": "Enregistre le son que joue votre ordinateur", "microphone": "Microphone", "camera": "Caméra", - "cursorHighlight": "Curseur en surbrillance", + "editableCursor": "Curseur éditable", + "editableCursorTip": "Activé : curseur éditable, avec changement de style et zoom automatique. Désactivé : curseur système, enregistré dans la vidéo.", "autoZoom": "Zoom automatique après l'enregistrement", "hideDesktopIcons": "Masquer les icônes du bureau", "hideDesktopIconsHintMac": "Retire les icônes du bureau des enregistrements d'écran. Elles restent sur votre bureau.", - "hideDesktopIconsHintWindows": "Recouvre les icônes du bureau avec votre fond d'écran pendant l'enregistrement d'un écran. Elles reviennent à la fin de l'enregistrement.", + "hideDesktopIconsHintWindows": "Recouvre les icônes du bureau pendant l'enregistrement. Elles reviennent à la fin.", "on": "Activé", "off": "Désactivé", "loading": "Chargement…", @@ -156,6 +158,7 @@ "regenerateAs": "Régénérer en", "auto": "Auto", "regenerate": "Régénérer", + "regenerateTip": "Régénérer : transcrire à nouveau dans la langue choisie", "transcriptEmpty": "La transcription est vide.", "notGeneratedHint": "Pas encore générée — choisissez une langue et cliquez sur régénérer.", "transcribing": "Transcription en cours", @@ -163,7 +166,7 @@ "initializingModel": "Démarrage du modèle vocal", "pendingTranscription": "Transcription en attente", "transcriptionFailed": "Échec de la transcription", - "cpuBackendHint": "Exécution sur le processeur — environ 2× plus lent que sur le GPU", + "cpuBackendHint": "Exécution sur le CPU : environ 2× plus lent que sur le GPU", "noTranscript": "Aucune transcription", "generationFailedHint": "Échec de la génération — choisissez une langue et régénérez.", "detectedLanguage": "Langue détectée : {{language}}", @@ -213,6 +216,7 @@ "trimHiddenDuration": "{{duration}} s de média source masqués de la timeline éditée.", "restoreDeleteTrim": "Restaurer (supprimer le découpage)", "collapseInspector": "Réduire l'inspecteur", + "editClipTip": "Modifier le clip : recadrage, début et fin", "captionsDescription": "Générer des sous-titres synchronisés aux mots à partir de la transcription et les déposer sur la timeline.", "generateCaptions": "Générer les sous-titres", "cropDescription": "Recadrer l'enregistrement — choisissez un format et zoomez sur la région à conserver.", @@ -248,9 +252,9 @@ "reasoningEffortUpdateFailed": "Impossible de mettre à jour l'effort de raisonnement", "contextTooltip": "{{usedTokens}} / {{budgetTokens}} jetons estimés", "contextPercent": "{{percent}}% du contexte", - "compactContext": "Compacter le contexte", + "compactContext": "Résumer les messages précédents pour utiliser moins de contexte", "aiSettings": "Réglages IA", - "history": "Historique", + "history": "Historique des conversations", "newConversation": "Nouvelle conversation", "untitledConversation": "Conversation", "clickToRename": "Cliquer pour renommer", @@ -270,6 +274,7 @@ "authorUser": "Vous", "authorAssistant": "OpenScreen", "rewindToMessage": "Revenir à ce message", + "rewindTip": "Revenir à ce message. Les modifications suivantes sont annulées.", "copyMessage": "Copier le message", "copiedToClipboard": "Copié dans le presse-papiers", "copyFailed": "Impossible de copier", @@ -279,7 +284,6 @@ "composerDisabledNoProvider": "Configurez un fournisseur pour commencer à discuter.", "modelLabel": "Modèle", "reasoningEffortLabel": "Effort de raisonnement", - "sendTitle": "Envoyer (Entrée)", "send": "Envoyer", "rewindConfirmTitle": "Revenir ici ?", "rewindConfirmBody": "Les modifications de l'agent et les échanges suivants après ce point seront annulés. Le projet, la conversation et l'état de l'agent seront restaurés. Les modifications que vous avez faites depuis seront elles aussi remplacées.", @@ -343,8 +347,8 @@ }, "transport": { "playbackControls": "Contrôles de lecture", - "playPause": "Lecture / Pause", - "playPauseTitle": "Lecture / Pause (Espace)", + "play": "Lecture", + "pause": "Pause", "loop": "Boucle" }, "shell": { diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json index 321d713e8..4f9501816 100644 --- a/src/i18n/locales/fr/settings.json +++ b/src/i18n/locales/fr/settings.json @@ -275,7 +275,7 @@ "fadeIn": "Fondu d'entrée", "remove": "Supprimer la piste", "loop": "Boucle", - "slipHint": "Alt + glisser pour déplacer l’audio à l’intérieur", + "slipHint": "Maintenez {{modifier}} et faites glisser pour déplacer l'audio dans la piste", "help": "Volume, fondus et boucle de cette piste audio. Elle se mixe par-dessus l’enregistrement dans l’aperçu et à l’export. Une piste de musique baisse d’elle-même quand quelqu’un parle. Faites glisser la piste sur la timeline pour la déplacer ou la redimensionner, ou maintenez Alt en glissant pour déplacer l’audio à l’intérieur.", "add": "Ajouter une piste audio", "mute": "Muet" @@ -334,7 +334,14 @@ }, "facets": { "transcript": "Transcription", - "captions": "Sous-titres" + "captions": "Sous-titres", + "tips": { + "effects": "Composition : arrière-plan, cadre, ombre et marge", + "layout": "Disposition caméra : position, forme et taille de la caméra", + "audio": "Audio : niveau de sortie de toute la vidéo", + "cursor": "Curseur : style, taille et effets", + "transcript": "Transcription : modifiez les mots pour couper la vidéo" + } }, "crop": { "title": "Recadrage", diff --git a/src/i18n/locales/fr/timeline.json b/src/i18n/locales/fr/timeline.json index a872f5788..63a3346a7 100644 --- a/src/i18n/locales/fr/timeline.json +++ b/src/i18n/locales/fr/timeline.json @@ -1,16 +1,13 @@ { "buttons": { - "addZoom": "Ajouter un zoom (Z)", - "suggestZooms": "Suggérer des zooms depuis le curseur", - "autoZoomOn": "Suggestions de zoom automatique activées — cliquez pour retirer les zooms suggérés", - "autoZoomOff": "Suggestions de zoom automatique désactivées — cliquez pour suggérer des zooms depuis le curseur", - "autoFocusAllOn": "Mise au point automatique activée pour tous les zooms — cliquez pour tout passer en manuel", - "autoFocusAllOff": "Activer la mise au point automatique pour tous les zooms (la caméra suit le curseur)", - "clearTimeline": "Effacer la timeline", - "addTrim": "Ajouter une coupe (T)", - "addAnnotation": "Ajouter une annotation (A)", - "addSpeed": "Ajouter une vitesse (S)", - "addCameraFullscreen": "Ajouter Caméra plein écran (C)" + "addZoom": "Ajouter un zoom à l'instant actuel", + "autoFocusAll": "Mise au point auto pour tous les zooms", + "autoFocusAllTip": "Tous les zooms suivent le curseur. Désactivé : vous choisissez la mise au point de chaque zoom.", + "clearTimeline": "Supprimer tous les zooms, coupes, vitesses, annotations et Caméra plein écran. Clips, audio et sous-titres sont conservés.", + "addTrim": "Ajouter une coupe : retire une partie de la vidéo à l'instant actuel", + "addAnnotation": "Ajouter du texte, une flèche, une image ou un flou à l'instant actuel", + "addSpeed": "Ajouter un changement de vitesse à l'instant actuel", + "addCameraFullscreen": "Ajouter Caméra plein écran : affiche la caméra en plein écran à l'instant actuel" }, "hints": { "pressZoom": "Appuyez sur Z pour ajouter un zoom", @@ -22,7 +19,9 @@ }, "labels": { "pan": "Panoramique", + "panTip": "Faire défiler la timeline", "zoom": "Zoom", + "zoomTip": "Zoomer sur la timeline", "trim": "Couper", "speed": "Vitesse", "zoomItem": "Zoom {{index}}", @@ -71,7 +70,7 @@ "arrangeClips": "Organiser les clips", "arrangeClipsHint": "Glissez les clips ci-dessous pour les réorganiser ou en déposer de nouveaux", "newAnnotation": "Annotation", - "dragToReorderHint": "Glissez pour réorganiser · double-cliquez pour modifier les points d'entrée/sortie", + "dragToReorderHint": "Glissez pour réorganiser. Double-cliquez pour modifier le début et la fin.", "editInOutPoints": "Modifier les points d'entrée/sortie", "deleteClip": "Supprimer le clip", "dropToAdd": "Déposez pour ajouter à la timeline", diff --git a/src/i18n/locales/it/editor.json b/src/i18n/locales/it/editor.json index 9541b7a2a..2b79e699e 100644 --- a/src/i18n/locales/it/editor.json +++ b/src/i18n/locales/it/editor.json @@ -86,7 +86,7 @@ } }, "topbar": { - "toggleChatPanel": "Attiva/disattiva pannello chat", + "toggleChatPanel": "Pannello chat", "openProject": "Apri progetto", "newProject": "Nuovo progetto", "saveProject": "Salva progetto", @@ -111,13 +111,15 @@ "selectSource": "Scegli cosa registrare", "systemPicker": "Il sistema ti chiederà cosa condividere", "systemAudio": "Audio di sistema", + "systemAudioTip": "Registra il suono riprodotto dal computer", "microphone": "Microfono", "camera": "Fotocamera", - "cursorHighlight": "Evidenzia cursore", + "editableCursor": "Cursore modificabile", + "editableCursorTip": "Attivo: cursore modificabile, con cambio di stile e zoom automatico. Disattivo: cursore di sistema, registrato nel video.", "autoZoom": "Zoom automatico dopo la registrazione", "hideDesktopIcons": "Nascondi icone della scrivania", "hideDesktopIconsHintMac": "Esclude le icone della scrivania dalle registrazioni dello schermo. Restano sulla tua scrivania.", - "hideDesktopIconsHintWindows": "Copre le icone del desktop con lo sfondo durante la registrazione di uno schermo. Tornano al termine della registrazione.", + "hideDesktopIconsHintWindows": "Copre le icone della scrivania durante la registrazione. Tornano alla fine.", "on": "Attivo", "off": "Disattivo", "loading": "Caricamento…", @@ -156,6 +158,7 @@ "regenerateAs": "Rigenera in", "auto": "Auto", "regenerate": "Rigenera", + "regenerateTip": "Rigenera: trascrivi di nuovo nella lingua scelta", "transcriptEmpty": "La trascrizione è vuota.", "notGeneratedHint": "Non ancora generata — scegli una lingua e clicca su rigenera.", "transcribing": "Trascrizione in corso", @@ -163,7 +166,7 @@ "initializingModel": "Avvio del modello vocale", "pendingTranscription": "Trascrizione in attesa", "transcriptionFailed": "Trascrizione non riuscita", - "cpuBackendHint": "In esecuzione su CPU — circa 2× più lento che su GPU", + "cpuBackendHint": "In esecuzione su CPU: circa 2× più lento che su GPU", "noTranscript": "Nessuna trascrizione", "generationFailedHint": "Generazione non riuscita — scegli una lingua e rigenera.", "detectedLanguage": "Lingua rilevata: {{language}}", @@ -213,6 +216,7 @@ "trimHiddenDuration": "{{duration}}s di media sorgente nascosti dalla timeline modificata.", "restoreDeleteTrim": "Ripristina (elimina taglio)", "collapseInspector": "Comprimi ispettore", + "editClipTip": "Modifica clip: ritaglio, inizio e fine", "captionsDescription": "Genera sottotitoli sincronizzati con le parole dalla trascrizione e trascinali sulla timeline.", "generateCaptions": "Genera sottotitoli", "cropDescription": "Reinquadra la registrazione — scegli un rapporto d'aspetto e ingrandisci l'area da mantenere.", @@ -248,9 +252,9 @@ "reasoningEffortUpdateFailed": "Impossibile aggiornare lo sforzo di ragionamento", "contextTooltip": "{{usedTokens}} / {{budgetTokens}} token stimati", "contextPercent": "{{percent}}% di contesto", - "compactContext": "Comprimi contesto", + "compactContext": "Riassumi i messaggi precedenti per usare meno contesto", "aiSettings": "Impostazioni IA", - "history": "Cronologia", + "history": "Cronologia delle conversazioni", "newConversation": "Nuova conversazione", "untitledConversation": "Conversazione", "clickToRename": "Clicca per rinominare", @@ -270,6 +274,7 @@ "authorUser": "Tu", "authorAssistant": "OpenScreen", "rewindToMessage": "Riavvolgi a questo messaggio", + "rewindTip": "Riavvolgi a questo messaggio. Le modifiche successive vengono annullate.", "copyMessage": "Copia messaggio", "copiedToClipboard": "Copiato negli appunti", "copyFailed": "Impossibile copiare", @@ -279,7 +284,6 @@ "composerDisabledNoProvider": "Configura un fornitore per iniziare a chattare.", "modelLabel": "Modello", "reasoningEffortLabel": "Sforzo di ragionamento", - "sendTitle": "Invia (Invio)", "send": "Invia", "rewindConfirmTitle": "Riavvolgere qui?", "rewindConfirmBody": "Le modifiche dell'agente e i turni successivi a questo punto verranno annullati. Il progetto, la conversazione e lo stato dell'agente verranno ripristinati. Anche le modifiche che hai fatto da allora verranno sostituite.", @@ -343,8 +347,8 @@ }, "transport": { "playbackControls": "Controlli di riproduzione", - "playPause": "Riproduci / Pausa", - "playPauseTitle": "Riproduci / Pausa (Spazio)", + "play": "Riproduci", + "pause": "Pausa", "loop": "Ripeti" }, "shell": { diff --git a/src/i18n/locales/it/settings.json b/src/i18n/locales/it/settings.json index 601dc0a97..e4952c231 100644 --- a/src/i18n/locales/it/settings.json +++ b/src/i18n/locales/it/settings.json @@ -275,7 +275,7 @@ "fadeIn": "Dissolvenza in entrata", "remove": "Elimina traccia", "loop": "Ripeti", - "slipHint": "Alt + trascina per far scorrere l’audio all’interno", + "slipHint": "Tieni premuto {{modifier}} e trascina per far scorrere l'audio nella traccia", "help": "Volume, dissolvenze e loop di questa traccia audio. Viene mixata sopra la registrazione nell’anteprima e nell’esportazione. Una traccia musicale si abbassa da sola mentre qualcuno parla. Trascina la traccia sulla timeline per spostarla o ridimensionarla, oppure tieni premuto Alt e trascina per far scorrere l’audio all’interno.", "add": "Aggiungi traccia audio", "mute": "Muto" @@ -334,7 +334,14 @@ }, "facets": { "transcript": "Trascrizione", - "captions": "Sottotitoli" + "captions": "Sottotitoli", + "tips": { + "effects": "Composizione: sfondo, cornice, ombra e spaziatura", + "layout": "Disposizione camera: posizione, forma e dimensione della fotocamera", + "audio": "Audio: livello di uscita di tutto il video", + "cursor": "Cursore: stile, dimensione ed effetti", + "transcript": "Trascrizione: modifica le parole per tagliare il video" + } }, "crop": { "title": "Ritaglia", diff --git a/src/i18n/locales/it/timeline.json b/src/i18n/locales/it/timeline.json index a74b782b7..b753e9a40 100644 --- a/src/i18n/locales/it/timeline.json +++ b/src/i18n/locales/it/timeline.json @@ -1,16 +1,13 @@ { "buttons": { - "addZoom": "Aggiungi zoom (Z)", - "suggestZooms": "Suggerisci zoom dal cursore", - "autoZoomOn": "Suggerimenti di zoom automatico attivi — clicca per rimuovere gli zoom suggeriti", - "autoZoomOff": "Suggerimenti di zoom automatico disattivi — clicca per suggerire zoom dal cursore", - "autoFocusAllOn": "Messa a fuoco automatica attiva per tutti gli zoom — clicca per passare tutti a manuale", - "autoFocusAllOff": "Attiva la messa a fuoco automatica per tutti gli zoom (la fotocamera segue il cursore)", - "clearTimeline": "Cancella la timeline", - "addTrim": "Aggiungi taglio (T)", - "addAnnotation": "Aggiungi annotazione (A)", - "addSpeed": "Aggiungi velocità (S)", - "addCameraFullscreen": "Aggiungi Camera a schermo intero (C)" + "addZoom": "Aggiungi uno zoom nel momento attuale", + "autoFocusAll": "Messa a fuoco auto per tutti gli zoom", + "autoFocusAllTip": "Tutti gli zoom seguono il cursore. Disattivo: scegli tu il fuoco di ogni zoom.", + "clearTimeline": "Rimuovi tutti gli zoom, tagli, velocità, annotazioni e Camera a schermo intero. Clip, audio e sottotitoli restano.", + "addTrim": "Aggiungi un taglio: rimuove una parte del video nel momento attuale", + "addAnnotation": "Aggiungi testo, una freccia, un'immagine o una sfocatura nel momento attuale", + "addSpeed": "Aggiungi un cambio di velocità nel momento attuale", + "addCameraFullscreen": "Aggiungi Camera a schermo intero: mostra la fotocamera a schermo intero nel momento attuale" }, "hints": { "pressZoom": "Premi Z per aggiungere zoom", @@ -22,7 +19,9 @@ }, "labels": { "pan": "Panoramica", + "panTip": "Scorri la timeline", "zoom": "Zoom", + "zoomTip": "Ingrandisci la timeline", "trim": "Taglio", "speed": "Velocità", "zoomItem": "Zoom {{index}}", @@ -71,7 +70,7 @@ "arrangeClips": "Organizza clip", "arrangeClipsHint": "Trascina le clip qui sotto per riordinarle o rilasciane di nuove", "newAnnotation": "Annotazione", - "dragToReorderHint": "Trascina per riordinare · doppio clic per modificare i punti di entrata/uscita", + "dragToReorderHint": "Trascina per riordinare. Doppio clic per cambiare inizio e fine.", "editInOutPoints": "Modifica punti di entrata/uscita", "deleteClip": "Elimina clip", "dropToAdd": "Rilascia per aggiungere alla timeline", diff --git a/src/i18n/locales/ja-JP/editor.json b/src/i18n/locales/ja-JP/editor.json index 9a8cf550b..5a06ed1de 100644 --- a/src/i18n/locales/ja-JP/editor.json +++ b/src/i18n/locales/ja-JP/editor.json @@ -86,7 +86,7 @@ } }, "topbar": { - "toggleChatPanel": "チャットパネルの表示切り替え", + "toggleChatPanel": "チャットパネル", "openProject": "プロジェクトを開く", "newProject": "新規プロジェクト", "saveProject": "プロジェクトを保存", @@ -111,13 +111,15 @@ "selectSource": "録画する対象を選択", "systemPicker": "共有する対象はシステムが確認します", "systemAudio": "システム音声", + "systemAudioTip": "コンピューターが再生する音を録音します", "microphone": "マイク", "camera": "カメラ", - "cursorHighlight": "カーソルの強調表示", + "editableCursor": "編集可能なカーソル", + "editableCursorTip": "オン:編集可能なカーソル。スタイル変更と自動ズームが使えます。オフ:システムカーソル。動画に録画されます。", "autoZoom": "録画後に自動ズーム", "hideDesktopIcons": "デスクトップアイコンを隠す", "hideDesktopIconsHintMac": "画面収録からデスクトップアイコンを除外します。デスクトップ上には残ります。", - "hideDesktopIconsHintWindows": "画面の収録中、デスクトップアイコンを壁紙で覆います。収録を止めると元に戻ります。", + "hideDesktopIconsHintWindows": "録画中はデスクトップアイコンを覆います。録画が終わると元に戻ります。", "on": "オン", "off": "オフ", "loading": "読み込み中…", @@ -156,6 +158,7 @@ "regenerateAs": "この言語で再生成", "auto": "自動", "regenerate": "再生成", + "regenerateTip": "再生成:選んだ言語でもう一度文字起こしします", "transcriptEmpty": "文字起こしは空です。", "notGeneratedHint": "まだ生成されていません — 言語を選んで再生成をクリックしてください。", "transcribing": "文字起こし中", @@ -163,7 +166,7 @@ "initializingModel": "音声モデルを起動しています", "pendingTranscription": "文字起こし待機中", "transcriptionFailed": "文字起こしに失敗しました", - "cpuBackendHint": "CPU で実行中 — GPU より約 2 倍低速です", + "cpuBackendHint": "CPU で実行中:GPU より約 2 倍低速です", "noTranscript": "文字起こしなし", "generationFailedHint": "生成に失敗しました — 言語を選んで再生成してください。", "detectedLanguage": "検出された言語: {{language}}", @@ -213,6 +216,7 @@ "trimHiddenDuration": "編集済みタイムラインから{{duration}}秒のソースメディアが非表示になっています。", "restoreDeleteTrim": "復元(トリムを削除)", "collapseInspector": "インスペクターを折りたたむ", + "editClipTip": "クリップを編集:切り抜き、開始、終了", "captionsDescription": "文字起こしから単語ごとにタイミングを合わせたキャプションを生成し、タイムラインに配置します。", "generateCaptions": "キャプションを生成", "cropDescription": "録画をリフレーム — アスペクト比を選び、保持したい範囲にズームします。", @@ -248,9 +252,9 @@ "reasoningEffortUpdateFailed": "推論の労力を更新できませんでした", "contextTooltip": "推定トークン数 {{usedTokens}} / {{budgetTokens}}", "contextPercent": "コンテキスト {{percent}}%", - "compactContext": "コンテキストを圧縮", + "compactContext": "以前のメッセージを要約して、コンテキストの使用量を減らします", "aiSettings": "AI設定", - "history": "履歴", + "history": "会話履歴", "newConversation": "新しい会話", "untitledConversation": "会話", "clickToRename": "クリックして名前を変更", @@ -270,6 +274,7 @@ "authorUser": "あなた", "authorAssistant": "OpenScreen", "rewindToMessage": "このメッセージまで巻き戻す", + "rewindTip": "このメッセージまで巻き戻します。以降の変更は元に戻ります。", "copyMessage": "メッセージをコピー", "copiedToClipboard": "クリップボードにコピーしました", "copyFailed": "コピーできませんでした", @@ -279,7 +284,6 @@ "composerDisabledNoProvider": "チャットを始めるにはプロバイダーを設定してください。", "modelLabel": "モデル", "reasoningEffortLabel": "推論の労力", - "sendTitle": "送信(Enter)", "send": "送信", "rewindConfirmTitle": "ここまで巻き戻しますか?", "rewindConfirmBody": "この時点以降のエージェントの編集とやり取りは元に戻されます。プロジェクト、会話、エージェントの状態が復元されます。それ以降にあなたが行った編集も置き換えられます。", @@ -343,8 +347,8 @@ }, "transport": { "playbackControls": "再生コントロール", - "playPause": "再生 / 一時停止", - "playPauseTitle": "再生 / 一時停止(スペース)", + "play": "再生", + "pause": "一時停止", "loop": "ループ" }, "shell": { diff --git a/src/i18n/locales/ja-JP/settings.json b/src/i18n/locales/ja-JP/settings.json index 8e8eae412..22f09e78b 100644 --- a/src/i18n/locales/ja-JP/settings.json +++ b/src/i18n/locales/ja-JP/settings.json @@ -275,7 +275,7 @@ "fadeIn": "フェードイン", "remove": "トラックを削除", "loop": "ループ", - "slipHint": "Alt を押しながらドラッグで中の音声をずらす", + "slipHint": "{{modifier}} を押しながらドラッグすると、トラック内で音声をずらせます", "help": "このオーディオトラックの音量・フェード・ループ。プレビューでも書き出しでも録画に重ねてミックスされます。音楽トラックは話している間、自動で音量が下がります。タイムライン上でドラッグすると移動やサイズ変更、Alt を押しながらドラッグすると中の音声がずれます。", "add": "オーディオトラックを追加", "mute": "ミュート" @@ -334,7 +334,14 @@ }, "facets": { "transcript": "文字起こし", - "captions": "字幕" + "captions": "字幕", + "tips": { + "effects": "コンポジション:背景、フレーム、影、余白", + "layout": "カメラレイアウト:カメラの位置、形状、サイズ", + "audio": "オーディオ:動画全体の出力レベル", + "cursor": "カーソル:スタイル、サイズ、エフェクト", + "transcript": "文字起こし:単語を編集して動画をカットします" + } }, "crop": { "title": "クロップ", diff --git a/src/i18n/locales/ja-JP/timeline.json b/src/i18n/locales/ja-JP/timeline.json index 6797f05e4..8a643b60a 100644 --- a/src/i18n/locales/ja-JP/timeline.json +++ b/src/i18n/locales/ja-JP/timeline.json @@ -1,16 +1,13 @@ { "buttons": { - "addZoom": "ズームを追加 (Z)", - "suggestZooms": "カーソル位置からズームを提案", - "autoZoomOn": "自動ズーム提案がオン — クリックすると提案されたズームを削除します", - "autoZoomOff": "自動ズーム提案がオフ — クリックするとカーソル位置からズームを提案します", - "autoFocusAllOn": "すべてのズームでオートフォーカスがオン — クリックするとすべて手動に切り替わります", - "autoFocusAllOff": "すべてのズームでオートフォーカスをオンにする(カメラがカーソルに追従)", - "clearTimeline": "タイムラインをクリア", - "addTrim": "トリムを追加 (T)", - "addAnnotation": "注釈を追加 (A)", - "addSpeed": "再生速度を追加 (S)", - "addCameraFullscreen": "フルスクリーンカメラを追加 (C)" + "addZoom": "現在の再生位置にズームを追加", + "autoFocusAll": "すべてのズームでオートフォーカス", + "autoFocusAllTip": "すべてのズームがカーソルを追従します。オフ:ズームごとにフォーカスを選べます。", + "clearTimeline": "すべてのズーム、トリム、再生速度、注釈、フルスクリーンカメラを削除します。クリップ、音声、字幕は残ります。", + "addTrim": "トリムを追加:現在の再生位置で動画の一部を削除します", + "addAnnotation": "現在の再生位置にテキスト、矢印、画像、ぼかしを追加", + "addSpeed": "現在の再生位置に再生速度の変更を追加", + "addCameraFullscreen": "フルスクリーンカメラを追加:現在の再生位置でカメラを全画面表示します" }, "hints": { "pressZoom": "Zキーを押してズームを追加", @@ -22,7 +19,9 @@ }, "labels": { "pan": "移動", + "panTip": "タイムラインをスクロール", "zoom": "ズーム", + "zoomTip": "タイムラインを拡大縮小", "trim": "トリム", "speed": "再生速度", "zoomItem": "ズーム {{index}}", @@ -71,7 +70,7 @@ "arrangeClips": "クリップを配置", "arrangeClipsHint": "下のクリップをドラッグして並べ替えるか、新しいクリップをドロップします", "newAnnotation": "注釈", - "dragToReorderHint": "ドラッグして並べ替え・ダブルクリックでイン/アウトポイントを編集", + "dragToReorderHint": "ドラッグで並べ替え。ダブルクリックで開始と終了を変更。", "editInOutPoints": "イン/アウトポイントを編集", "deleteClip": "クリップを削除", "dropToAdd": "ドロップしてタイムラインに追加", diff --git a/src/i18n/locales/ko-KR/editor.json b/src/i18n/locales/ko-KR/editor.json index fc9e44921..c7f304044 100644 --- a/src/i18n/locales/ko-KR/editor.json +++ b/src/i18n/locales/ko-KR/editor.json @@ -86,7 +86,7 @@ } }, "topbar": { - "toggleChatPanel": "채팅 패널 전환", + "toggleChatPanel": "채팅 패널", "openProject": "프로젝트 열기", "newProject": "새 프로젝트", "saveProject": "프로젝트 저장", @@ -111,13 +111,15 @@ "selectSource": "녹화할 대상을 선택하세요", "systemPicker": "공유할 대상을 시스템이 묻습니다", "systemAudio": "시스템 오디오", + "systemAudioTip": "컴퓨터에서 재생되는 소리를 녹음합니다", "microphone": "마이크", "camera": "카메라", - "cursorHighlight": "커서 강조", + "editableCursor": "편집 가능한 커서", + "editableCursorTip": "켜짐: 편집 가능한 커서, 스타일 변경과 자동 줌 가능. 꺼짐: 시스템 커서, 영상에 녹화됩니다.", "autoZoom": "녹화 후 자동 확대", "hideDesktopIcons": "바탕화면 아이콘 숨기기", "hideDesktopIconsHintMac": "화면 녹화에서 바탕화면 아이콘을 제외합니다. 바탕화면에는 그대로 남아 있습니다.", - "hideDesktopIconsHintWindows": "화면을 녹화하는 동안 바탕화면 아이콘을 배경 화면으로 가립니다. 녹화를 멈추면 다시 나타납니다.", + "hideDesktopIconsHintWindows": "녹화하는 동안 바탕화면 아이콘을 가립니다. 녹화가 끝나면 다시 나타납니다.", "on": "켜짐", "off": "꺼짐", "loading": "로딩 중…", @@ -156,6 +158,7 @@ "regenerateAs": "다음 언어로 재생성", "auto": "자동", "regenerate": "재생성", + "regenerateTip": "재생성: 선택한 언어로 다시 받아씁니다", "transcriptEmpty": "대본이 비어 있습니다.", "notGeneratedHint": "아직 생성되지 않음 — 언어를 선택하고 재생성을 클릭하세요.", "transcribing": "받아쓰는 중", @@ -163,7 +166,7 @@ "initializingModel": "음성 모델 시작 중", "pendingTranscription": "받아쓰기 대기 중", "transcriptionFailed": "받아쓰기 실패", - "cpuBackendHint": "CPU에서 실행 중 — GPU보다 약 2배 느립니다", + "cpuBackendHint": "CPU에서 실행 중: GPU보다 약 2배 느립니다", "noTranscript": "대본 없음", "generationFailedHint": "생성 실패 — 언어를 선택하고 다시 생성하세요.", "detectedLanguage": "감지된 언어: {{language}}", @@ -213,6 +216,7 @@ "trimHiddenDuration": "편집된 타임라인에서 {{duration}}초의 원본 미디어가 숨겨졌습니다.", "restoreDeleteTrim": "복원(트림 삭제)", "collapseInspector": "인스펙터 접기", + "editClipTip": "클립 편집: 자르기, 시작, 종료", "captionsDescription": "대본에서 단어 단위 타이밍의 자막을 생성하여 타임라인에 배치합니다.", "generateCaptions": "자막 생성", "cropDescription": "녹화 화면을 다시 프레이밍 — 화면 비율을 선택하고 유지할 영역으로 확대합니다.", @@ -248,9 +252,9 @@ "reasoningEffortUpdateFailed": "추론 노력 수준을 업데이트할 수 없습니다", "contextTooltip": "예상 토큰 {{usedTokens}} / {{budgetTokens}}", "contextPercent": "컨텍스트 {{percent}}%", - "compactContext": "컨텍스트 압축", + "compactContext": "이전 메시지를 요약하여 컨텍스트를 덜 사용합니다", "aiSettings": "AI 설정", - "history": "기록", + "history": "대화 기록", "newConversation": "새 대화", "untitledConversation": "대화", "clickToRename": "클릭하여 이름 변경", @@ -270,6 +274,7 @@ "authorUser": "나", "authorAssistant": "OpenScreen", "rewindToMessage": "이 메시지로 되감기", + "rewindTip": "이 메시지로 되감습니다. 이후의 변경은 취소됩니다.", "copyMessage": "메시지 복사", "copiedToClipboard": "클립보드에 복사됨", "copyFailed": "복사할 수 없습니다", @@ -279,7 +284,6 @@ "composerDisabledNoProvider": "대화를 시작하려면 공급자를 설정하세요.", "modelLabel": "모델", "reasoningEffortLabel": "추론 노력 수준", - "sendTitle": "보내기 (Enter)", "send": "보내기", "rewindConfirmTitle": "여기로 되감을까요?", "rewindConfirmBody": "이 시점 이후의 에이전트 편집과 이후 대화 턴이 롤백됩니다. 프로젝트, 대화, 에이전트 상태가 복원됩니다. 그 이후에 직접 한 편집도 함께 대체됩니다.", @@ -343,8 +347,8 @@ }, "transport": { "playbackControls": "재생 컨트롤", - "playPause": "재생 / 일시정지", - "playPauseTitle": "재생 / 일시정지 (Space)", + "play": "재생", + "pause": "일시정지", "loop": "반복" }, "shell": { diff --git a/src/i18n/locales/ko-KR/settings.json b/src/i18n/locales/ko-KR/settings.json index e964c7816..d57d98046 100644 --- a/src/i18n/locales/ko-KR/settings.json +++ b/src/i18n/locales/ko-KR/settings.json @@ -275,7 +275,7 @@ "fadeIn": "페이드 인", "remove": "트랙 삭제", "loop": "반복", - "slipHint": "Alt를 누른 채 드래그하면 안의 오디오가 이동합니다", + "slipHint": "{{modifier}} 키를 누른 채 드래그하면 트랙 안에서 오디오가 이동합니다", "help": "이 오디오 트랙의 볼륨, 페이드, 반복 설정입니다. 미리보기와 내보내기에서 녹화 위에 믹스됩니다. 음악 트랙은 누군가 말하는 동안 자동으로 작아집니다. 타임라인에서 드래그하면 이동하거나 크기를 바꾸고, Alt를 누른 채 드래그하면 안의 오디오가 이동합니다.", "add": "오디오 트랙 추가", "mute": "음소거" @@ -334,7 +334,14 @@ }, "facets": { "transcript": "대본", - "captions": "자막" + "captions": "자막", + "tips": { + "effects": "컴포지션: 배경, 프레임, 그림자, 여백", + "layout": "카메라 레이아웃: 카메라의 위치, 모양, 크기", + "audio": "오디오: 영상 전체의 출력 레벨", + "cursor": "커서: 스타일, 크기, 효과", + "transcript": "대본: 단어를 편집하여 영상을 자릅니다" + } }, "crop": { "title": "자르기", diff --git a/src/i18n/locales/ko-KR/timeline.json b/src/i18n/locales/ko-KR/timeline.json index 1eddfe7df..8d838cbdd 100644 --- a/src/i18n/locales/ko-KR/timeline.json +++ b/src/i18n/locales/ko-KR/timeline.json @@ -1,16 +1,13 @@ { "buttons": { - "addZoom": "줌 추가 (Z)", - "suggestZooms": "커서 기반 줌 제안", - "autoZoomOn": "자동 줌 제안 켜짐 — 클릭하면 제안된 줌을 제거합니다", - "autoZoomOff": "자동 줌 제안 꺼짐 — 클릭하면 커서 기반으로 줌을 제안합니다", - "autoFocusAllOn": "모든 줌에 자동 초점 켜짐 — 클릭하면 모두 수동으로 전환합니다", - "autoFocusAllOff": "모든 줌에 자동 초점 켜기 (카메라가 커서를 따라갑니다)", - "clearTimeline": "타임라인 지우기", - "addTrim": "트림 추가 (T)", - "addAnnotation": "주석 추가 (A)", - "addSpeed": "속도 추가 (S)", - "addCameraFullscreen": "전체 화면 카메라 추가 (C)" + "addZoom": "현재 시점에 줌 추가", + "autoFocusAll": "모든 줌에 자동 초점", + "autoFocusAllTip": "모든 줌이 커서를 따라갑니다. 꺼짐: 줌마다 초점을 직접 고릅니다.", + "clearTimeline": "모든 줌, 트림, 속도, 주석, 전체 화면 카메라를 제거합니다. 클립, 오디오, 자막은 그대로 남습니다.", + "addTrim": "트림 추가: 현재 시점에서 영상의 한 구간을 제거합니다", + "addAnnotation": "현재 시점에 텍스트, 화살표, 이미지 또는 블러 추가", + "addSpeed": "현재 시점에 속도 변경 추가", + "addCameraFullscreen": "전체 화면 카메라 추가: 현재 시점에 카메라를 전체 화면으로 표시합니다" }, "hints": { "pressZoom": "Z를 눌러 줌 추가", @@ -22,7 +19,9 @@ }, "labels": { "pan": "이동", + "panTip": "타임라인 스크롤", "zoom": "줌", + "zoomTip": "타임라인 확대/축소", "trim": "트림", "speed": "속도", "zoomItem": "줌 {{index}}", @@ -71,7 +70,7 @@ "arrangeClips": "클립 정리", "arrangeClipsHint": "아래 클립을 드래그하여 순서를 바꾸거나 새 클립을 놓으세요", "newAnnotation": "주석", - "dragToReorderHint": "드래그하여 순서 변경 · 더블클릭하여 시작/종료 지점 편집", + "dragToReorderHint": "드래그하여 순서를 바꿉니다. 더블클릭하여 시작과 종료를 바꿉니다.", "editInOutPoints": "시작/종료 지점 편집", "deleteClip": "클립 삭제", "dropToAdd": "타임라인에 추가하려면 놓으세요", diff --git a/src/i18n/locales/pt-BR/editor.json b/src/i18n/locales/pt-BR/editor.json index 9b160d5db..9c5b9c2a7 100644 --- a/src/i18n/locales/pt-BR/editor.json +++ b/src/i18n/locales/pt-BR/editor.json @@ -86,7 +86,7 @@ } }, "topbar": { - "toggleChatPanel": "Alternar painel de chat", + "toggleChatPanel": "Painel de chat", "openProject": "Abrir projeto", "newProject": "Novo projeto", "saveProject": "Salvar projeto", @@ -111,13 +111,15 @@ "selectSource": "Escolha o que gravar", "systemPicker": "O sistema perguntará o que compartilhar", "systemAudio": "Áudio do sistema", + "systemAudioTip": "Grava o som reproduzido pelo computador", "microphone": "Microfone", "camera": "Câmera", - "cursorHighlight": "Destaque do cursor", + "editableCursor": "Cursor editável", + "editableCursorTip": "Ativado: cursor editável, com mudança de estilo e zoom automático. Desativado: cursor do sistema, gravado no vídeo.", "autoZoom": "Zoom automático após a gravação", "hideDesktopIcons": "Ocultar ícones da área de trabalho", "hideDesktopIconsHintMac": "Deixa os ícones da área de trabalho fora das gravações de tela. Eles continuam na sua área de trabalho.", - "hideDesktopIconsHintWindows": "Cobre os ícones da área de trabalho com o seu papel de parede enquanto uma tela é gravada. Eles voltam quando a gravação termina.", + "hideDesktopIconsHintWindows": "Cobre os ícones da área de trabalho durante a gravação. Eles voltam ao terminar.", "on": "Ativado", "off": "Desativado", "loading": "Carregando…", @@ -156,6 +158,7 @@ "regenerateAs": "Regenerar em", "auto": "Automático", "regenerate": "Regenerar", + "regenerateTip": "Regenerar: transcrever de novo no idioma escolhido", "transcriptEmpty": "A transcrição está vazia.", "notGeneratedHint": "Ainda não gerada — escolha um idioma e clique em regenerar.", "transcribing": "Transcrevendo", @@ -163,7 +166,7 @@ "initializingModel": "Iniciando modelo de voz", "pendingTranscription": "Transcrição pendente", "transcriptionFailed": "Falha na transcrição", - "cpuBackendHint": "Executando na CPU — cerca de 2× mais lento do que na GPU", + "cpuBackendHint": "Executando na CPU: cerca de 2× mais lento do que na GPU", "noTranscript": "Sem transcrição", "generationFailedHint": "Falha na geração — escolha um idioma e gere novamente.", "detectedLanguage": "Idioma detectado: {{language}}", @@ -213,6 +216,7 @@ "trimHiddenDuration": "{{duration}}s de mídia de origem ocultos da linha do tempo editada.", "restoreDeleteTrim": "Restaurar (excluir corte)", "collapseInspector": "Recolher inspetor", + "editClipTip": "Editar clipe: corte, início e fim", "captionsDescription": "Gere legendas sincronizadas por palavra a partir da transcrição e solte-as na linha do tempo.", "generateCaptions": "Gerar legendas", "cropDescription": "Reenquadre a gravação — escolha uma proporção e dê zoom na região que deseja manter.", @@ -248,9 +252,9 @@ "reasoningEffortUpdateFailed": "Não foi possível atualizar o esforço de raciocínio", "contextTooltip": "{{usedTokens}} / {{budgetTokens}} tokens estimados", "contextPercent": "{{percent}}% de contexto", - "compactContext": "Compactar contexto", + "compactContext": "Resumir as mensagens anteriores para usar menos contexto", "aiSettings": "Configurações de IA", - "history": "Histórico", + "history": "Histórico de conversas", "newConversation": "Nova conversa", "untitledConversation": "Conversa", "clickToRename": "Clique para renomear", @@ -270,6 +274,7 @@ "authorUser": "Você", "authorAssistant": "OpenScreen", "rewindToMessage": "Rebobinar até esta mensagem", + "rewindTip": "Rebobinar até esta mensagem. As alterações posteriores são desfeitas.", "copyMessage": "Copiar mensagem", "copiedToClipboard": "Copiado para a área de transferência", "copyFailed": "Não foi possível copiar", @@ -279,7 +284,6 @@ "composerDisabledNoProvider": "Configure um fornecedor para começar a conversar.", "modelLabel": "Modelo", "reasoningEffortLabel": "Esforço de raciocínio", - "sendTitle": "Enviar (Enter)", "send": "Enviar", "rewindConfirmTitle": "Rebobinar até aqui?", "rewindConfirmBody": "As edições do agente e as interações seguintes a partir deste ponto serão desfeitas. O projeto, a conversa e o estado do agente serão restaurados. As edições que você fez desde então também serão substituídas.", @@ -343,8 +347,8 @@ }, "transport": { "playbackControls": "Controles de reprodução", - "playPause": "Reproduzir / Pausar", - "playPauseTitle": "Reproduzir / Pausar (Espaço)", + "play": "Reproduzir", + "pause": "Pausar", "loop": "Repetir" }, "shell": { diff --git a/src/i18n/locales/pt-BR/settings.json b/src/i18n/locales/pt-BR/settings.json index 6dd5e952e..835e972bc 100644 --- a/src/i18n/locales/pt-BR/settings.json +++ b/src/i18n/locales/pt-BR/settings.json @@ -275,7 +275,7 @@ "fadeIn": "Fade in", "remove": "Excluir faixa", "loop": "Repetir", - "slipHint": "Alt + arrastar para deslizar o áudio dentro", + "slipHint": "Segure {{modifier}} e arraste para deslizar o áudio dentro da faixa", "help": "Volume, fades e repetição desta faixa de áudio. Ela é mixada sobre a gravação na visualização e na exportação. Uma faixa de música abaixa sozinha enquanto alguém fala. Arraste a faixa na linha do tempo para movê-la ou redimensioná-la, ou segure Alt e arraste para deslizar o áudio dentro dela.", "add": "Adicionar faixa de áudio", "mute": "Silenciar" @@ -334,7 +334,14 @@ }, "facets": { "transcript": "Transcrição", - "captions": "Legendas" + "captions": "Legendas", + "tips": { + "effects": "Composição: fundo, moldura, sombra e espaçamento", + "layout": "Layout da câmera: posição, formato e tamanho da câmera", + "audio": "Áudio: nível de saída de todo o vídeo", + "cursor": "Cursor: estilo, tamanho e efeitos", + "transcript": "Transcrição: edite as palavras para cortar o vídeo" + } }, "crop": { "title": "Cortar", diff --git a/src/i18n/locales/pt-BR/timeline.json b/src/i18n/locales/pt-BR/timeline.json index 3ed3d8d1b..2ff8be1b0 100644 --- a/src/i18n/locales/pt-BR/timeline.json +++ b/src/i18n/locales/pt-BR/timeline.json @@ -1,16 +1,13 @@ { "buttons": { - "addZoom": "Adicionar Zoom (Z)", - "suggestZooms": "Sugerir Zooms a partir do Cursor", - "autoZoomOn": "Sugestões de zoom automático ativadas — clique para remover os zooms sugeridos", - "autoZoomOff": "Sugestões de zoom automático desativadas — clique para sugerir zooms a partir do cursor", - "autoFocusAllOn": "Foco automático ativado para todos os zooms — clique para mudar todos para manual", - "autoFocusAllOff": "Ativar foco automático para todos os zooms (a câmera segue o cursor)", - "clearTimeline": "Limpar a linha do tempo", - "addTrim": "Adicionar Recorte (T)", - "addAnnotation": "Adicionar Anotação (A)", - "addSpeed": "Adicionar Velocidade (S)", - "addCameraFullscreen": "Adicionar Câmera em Tela Cheia (C)" + "addZoom": "Adicionar um zoom no momento atual", + "autoFocusAll": "Foco automático para todos os zooms", + "autoFocusAllTip": "Todos os zooms seguem o cursor. Desativado: você escolhe o foco de cada zoom.", + "clearTimeline": "Remover todos os zooms, recortes, velocidades, anotações e Câmera em Tela Cheia. Clipes, áudio e legendas permanecem.", + "addTrim": "Adicionar um recorte: remove uma parte do vídeo no momento atual", + "addAnnotation": "Adicionar texto, seta, imagem ou desfoque no momento atual", + "addSpeed": "Adicionar uma mudança de velocidade no momento atual", + "addCameraFullscreen": "Adicionar Câmera em Tela Cheia: mostra a câmera em tela cheia no momento atual" }, "hints": { "pressZoom": "Pressione Z para adicionar zoom", @@ -22,7 +19,9 @@ }, "labels": { "pan": "Mover", + "panTip": "Rolar a linha do tempo", "zoom": "Zoom", + "zoomTip": "Ampliar a linha do tempo", "trim": "Recorte", "speed": "Velocidade", "zoomItem": "Zoom {{index}}", @@ -71,7 +70,7 @@ "arrangeClips": "Organizar clipes", "arrangeClipsHint": "Arraste os clipes abaixo para reordená-los ou solte novos", "newAnnotation": "Anotação", - "dragToReorderHint": "Arraste para reordenar · clique duas vezes para editar os pontos de entrada/saída", + "dragToReorderHint": "Arraste para reordenar. Clique duas vezes para mudar o início e o fim.", "editInOutPoints": "Editar pontos de entrada/saída", "deleteClip": "Excluir clipe", "dropToAdd": "Solte para adicionar à linha do tempo", diff --git a/src/i18n/locales/ru/editor.json b/src/i18n/locales/ru/editor.json index d3031008d..12d9d3ece 100644 --- a/src/i18n/locales/ru/editor.json +++ b/src/i18n/locales/ru/editor.json @@ -86,7 +86,7 @@ } }, "topbar": { - "toggleChatPanel": "Показать/скрыть панель чата", + "toggleChatPanel": "Панель чата", "openProject": "Открыть проект", "newProject": "Новый проект", "saveProject": "Сохранить проект", @@ -111,13 +111,15 @@ "selectSource": "Выберите, что записывать", "systemPicker": "Система спросит, чем поделиться", "systemAudio": "Системный звук", + "systemAudioTip": "Записывает звук, который воспроизводит компьютер", "microphone": "Микрофон", "camera": "Камера", - "cursorHighlight": "Подсветка курсора", + "editableCursor": "Редактируемый курсор", + "editableCursorTip": "Вкл.: редактируемый курсор, с изменением стиля и автозумом. Выкл.: системный курсор, записанный в видео.", "autoZoom": "Автомасштабирование после записи", "hideDesktopIcons": "Скрыть значки рабочего стола", "hideDesktopIconsHintMac": "Убирает значки рабочего стола из записи экрана. На рабочем столе они остаются.", - "hideDesktopIconsHintWindows": "Закрывает значки рабочего стола обоями, пока идёт запись экрана. После записи они вернутся.", + "hideDesktopIconsHintWindows": "Закрывает значки рабочего стола во время записи. После неё они возвращаются.", "on": "Вкл", "off": "Выкл", "loading": "Загрузка…", @@ -156,6 +158,7 @@ "regenerateAs": "Пересоздать на", "auto": "Авто", "regenerate": "Пересоздать", + "regenerateTip": "Пересоздать: расшифровать заново на выбранном языке", "transcriptEmpty": "Транскрипт пуст.", "notGeneratedHint": "Ещё не создан — выберите язык и нажмите «Пересоздать».", "transcribing": "Расшифровка", @@ -163,7 +166,7 @@ "initializingModel": "Запуск речевой модели", "pendingTranscription": "Ожидает расшифровки", "transcriptionFailed": "Ошибка расшифровки", - "cpuBackendHint": "Выполняется на процессоре — примерно в 2× медленнее, чем на GPU", + "cpuBackendHint": "Выполняется на CPU: примерно вдвое медленнее, чем на GPU", "noTranscript": "Нет расшифровки", "generationFailedHint": "Ошибка создания — выберите язык и создайте заново.", "detectedLanguage": "Обнаруженный язык: {{language}}", @@ -213,6 +216,7 @@ "trimHiddenDuration": "{{duration}} с исходного медиа скрыто из отредактированной таймлинии.", "restoreDeleteTrim": "Восстановить (удалить обрезку)", "collapseInspector": "Свернуть инспектор", + "editClipTip": "Изменить клип: обрезка, начало и конец", "captionsDescription": "Создайте субтитры с привязкой по словам на основе транскрипции и добавьте их на таймлинию.", "generateCaptions": "Создать субтитры", "cropDescription": "Перекадрируйте запись — выберите соотношение сторон и увеличьте область, которую нужно сохранить.", @@ -248,9 +252,9 @@ "reasoningEffortUpdateFailed": "Не удалось обновить уровень рассуждений", "contextTooltip": "{{usedTokens}} / {{budgetTokens}} расчётных токенов", "contextPercent": "{{percent}}% контекста", - "compactContext": "Сжать контекст", + "compactContext": "Обобщить прежние сообщения, чтобы использовать меньше контекста", "aiSettings": "Настройки ИИ", - "history": "История", + "history": "История бесед", "newConversation": "Новая беседа", "untitledConversation": "Беседа", "clickToRename": "Нажмите, чтобы переименовать", @@ -270,6 +274,7 @@ "authorUser": "Вы", "authorAssistant": "OpenScreen", "rewindToMessage": "Откатить к этому сообщению", + "rewindTip": "Откатить к этому сообщению. Более поздние изменения отменяются.", "copyMessage": "Скопировать сообщение", "copiedToClipboard": "Скопировано в буфер обмена", "copyFailed": "Не удалось скопировать", @@ -279,7 +284,6 @@ "composerDisabledNoProvider": "Подключите провайдера, чтобы начать чат.", "modelLabel": "Модель", "reasoningEffortLabel": "Уровень рассуждений", - "sendTitle": "Отправить (Enter)", "send": "Отправить", "rewindConfirmTitle": "Откатить сюда?", "rewindConfirmBody": "Правки агента и последующие сообщения после этой точки будут отменены. Проект, беседа и состояние агента будут восстановлены. Правки, сделанные вами с тех пор, тоже будут заменены.", @@ -343,8 +347,8 @@ }, "transport": { "playbackControls": "Элементы управления воспроизведением", - "playPause": "Воспроизведение / Пауза", - "playPauseTitle": "Воспроизведение / Пауза (Пробел)", + "play": "Воспроизвести", + "pause": "Пауза", "loop": "Повтор" }, "shell": { diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json index 599ca199e..576b2494d 100644 --- a/src/i18n/locales/ru/settings.json +++ b/src/i18n/locales/ru/settings.json @@ -275,7 +275,7 @@ "fadeIn": "Нарастание", "remove": "Удалить дорожку", "loop": "Повтор", - "slipHint": "Alt + перетаскивание — сдвинуть аудио внутри", + "slipHint": "Удерживайте {{modifier}} и перетаскивайте, чтобы сдвинуть аудио внутри дорожки", "help": "Громкость, фейды и зацикливание этой аудиодорожки. Она подмешивается поверх записи в предпросмотре и при экспорте. Музыкальная дорожка сама становится тише, пока кто-то говорит. Перетащите дорожку на таймлайне, чтобы переместить или изменить её размер, либо удерживайте Alt и тяните, чтобы сдвинуть аудио внутри неё.", "add": "Добавить аудиодорожку", "mute": "Без звука" @@ -334,7 +334,14 @@ }, "facets": { "transcript": "Транскрипт", - "captions": "Субтитры" + "captions": "Субтитры", + "tips": { + "effects": "Композиция: фон, рамка, тень и отступ", + "layout": "Расположение камеры: положение, форма и размер камеры", + "audio": "Аудио: уровень выхода всего видео", + "cursor": "Курсор: стиль, размер и эффекты", + "transcript": "Транскрипт: правьте слова, чтобы вырезать видео" + } }, "crop": { "title": "Обрезка", diff --git a/src/i18n/locales/ru/timeline.json b/src/i18n/locales/ru/timeline.json index 59ddff515..73ca1d173 100644 --- a/src/i18n/locales/ru/timeline.json +++ b/src/i18n/locales/ru/timeline.json @@ -1,16 +1,13 @@ { "buttons": { - "addZoom": "Добавить масштабирование (Z)", - "suggestZooms": "Предложить масштабирование на основе курсора", - "autoZoomOn": "Автоматические предложения масштабирования включены — нажмите, чтобы убрать предложенные зумы", - "autoZoomOff": "Автоматические предложения масштабирования выключены — нажмите, чтобы предложить зумы по курсору", - "autoFocusAllOn": "Автофокус включён для всех зумов — нажмите, чтобы переключить все в ручной режим", - "autoFocusAllOff": "Включить автофокус для всех зумов (камера следует за курсором)", - "clearTimeline": "Очистить таймлайн", - "addTrim": "Добавить обрезку (T)", - "addAnnotation": "Добавить аннотацию (A)", - "addSpeed": "Изменить скорость (S)", - "addCameraFullscreen": "Добавить камеру на весь экран (C)" + "addZoom": "Добавить масштабирование в текущий момент", + "autoFocusAll": "Автофокус для всех зумов", + "autoFocusAllTip": "Все зумы следуют за курсором. Выкл.: фокус каждого зума вы выбираете сами.", + "clearTimeline": "Удалить все зумы, обрезки, изменения скорости, аннотации и камеру на весь экран. Клипы, аудио и субтитры остаются.", + "addTrim": "Добавить обрезку: удаляет фрагмент видео в текущий момент", + "addAnnotation": "Добавить текст, стрелку, изображение или размытие в текущий момент", + "addSpeed": "Добавить изменение скорости в текущий момент", + "addCameraFullscreen": "Добавить камеру на весь экран: показывает камеру на весь экран в текущий момент" }, "hints": { "pressZoom": "Нажмите Z для добавления масштабирования", @@ -22,7 +19,9 @@ }, "labels": { "pan": "Панорамирование", + "panTip": "Прокрутка таймлайна", "zoom": "Масштабирование", + "zoomTip": "Масштаб таймлайна", "trim": "Обрезка", "speed": "Скорость воспроизведения", "zoomItem": "Масштабирование {{index}}", @@ -71,7 +70,7 @@ "arrangeClips": "Упорядочить клипы", "arrangeClipsHint": "Перетащите клипы ниже, чтобы изменить порядок, или добавьте новые", "newAnnotation": "Аннотация", - "dragToReorderHint": "Перетащите для изменения порядка · дважды щёлкните для редактирования точек входа/выхода", + "dragToReorderHint": "Перетащите, чтобы изменить порядок. Дважды щёлкните, чтобы изменить начало и конец.", "editInOutPoints": "Редактировать точки входа/выхода", "deleteClip": "Удалить клип", "dropToAdd": "Отпустите, чтобы добавить на таймлайн", diff --git a/src/i18n/locales/tr/editor.json b/src/i18n/locales/tr/editor.json index 5e401b0f7..9a807c1a4 100644 --- a/src/i18n/locales/tr/editor.json +++ b/src/i18n/locales/tr/editor.json @@ -86,7 +86,7 @@ } }, "topbar": { - "toggleChatPanel": "Sohbet panelini aç/kapat", + "toggleChatPanel": "Sohbet paneli", "openProject": "Proje aç", "newProject": "Yeni proje", "saveProject": "Projeyi kaydet", @@ -111,13 +111,15 @@ "selectSource": "Neyi kaydedeceğinizi seçin", "systemPicker": "Sisteminiz neyi paylaşacağınızı soracak", "systemAudio": "Sistem sesi", + "systemAudioTip": "Bilgisayarın çaldığı sesi kaydeder", "microphone": "Mikrofon", "camera": "Kamera", - "cursorHighlight": "İmleç vurgusu", + "editableCursor": "Düzenlenebilir imleç", + "editableCursorTip": "Açık: düzenlenebilir imleç, stil değiştirme ve otomatik yakınlaştırma ile. Kapalı: sistem imleci, videoya kaydedilir.", "autoZoom": "Kayıttan sonra otomatik yakınlaştırma", "hideDesktopIcons": "Masaüstü simgelerini gizle", "hideDesktopIconsHintMac": "Masaüstü simgelerini ekran kayıtlarının dışında bırakır. Masaüstünüzde kalırlar.", - "hideDesktopIconsHintWindows": "Bir ekran kaydedilirken masaüstü simgelerini duvar kağıdınızla örter. Kayıt bitince geri gelirler.", + "hideDesktopIconsHintWindows": "Kayıt sırasında masaüstü simgelerini örter. Kayıt bitince geri gelirler.", "on": "Açık", "off": "Kapalı", "loading": "Yükleniyor…", @@ -156,6 +158,7 @@ "regenerateAs": "Şu dilde yeniden oluştur", "auto": "Otomatik", "regenerate": "Yeniden oluştur", + "regenerateTip": "Yeniden oluştur: seçilen dilde yeniden metne dök", "transcriptEmpty": "Metin dökümü boş.", "notGeneratedHint": "Henüz oluşturulmadı — bir dil seçin ve yeniden oluştur'a tıklayın.", "transcribing": "Metne dökülüyor", @@ -163,7 +166,7 @@ "initializingModel": "Konuşma modeli başlatılıyor", "pendingTranscription": "Metne dökme bekliyor", "transcriptionFailed": "Metne dökme başarısız oldu", - "cpuBackendHint": "CPU üzerinde çalışıyor — GPU'ya göre yaklaşık 2× daha yavaş", + "cpuBackendHint": "CPU üzerinde çalışıyor: GPU'ya göre yaklaşık 2× daha yavaş", "noTranscript": "Transkript yok", "generationFailedHint": "Oluşturma başarısız oldu — bir dil seçip yeniden oluşturun.", "detectedLanguage": "Algılanan dil: {{language}}", @@ -213,6 +216,7 @@ "trimHiddenDuration": "Düzenlenen zaman çizelgesinden {{duration}} sn kaynak medya gizlendi.", "restoreDeleteTrim": "Geri yükle (kırpmayı sil)", "collapseInspector": "Denetleyiciyi daralt", + "editClipTip": "Klibi düzenle: kırpma, başlangıç ve bitiş", "captionsDescription": "Transkriptten kelime zamanlamalı altyazılar oluşturun ve zaman çizelgesine bırakın.", "generateCaptions": "Altyazı oluştur", "cropDescription": "Kaydı yeniden çerçeveleyin — bir en boy oranı seçin ve saklamak istediğiniz bölgeye yakınlaşın.", @@ -248,9 +252,9 @@ "reasoningEffortUpdateFailed": "Akıl yürütme çabası güncellenemedi", "contextTooltip": "{{usedTokens}} / {{budgetTokens}} tahmini belirteç", "contextPercent": "%{{percent}} bağlam", - "compactContext": "Bağlamı sıkıştır", + "compactContext": "Daha az bağlam kullanmak için önceki mesajları özetle", "aiSettings": "Yapay zeka ayarları", - "history": "Geçmiş", + "history": "Konuşma geçmişi", "newConversation": "Yeni konuşma", "untitledConversation": "Konuşma", "clickToRename": "Yeniden adlandırmak için tıklayın", @@ -270,6 +274,7 @@ "authorUser": "Siz", "authorAssistant": "OpenScreen", "rewindToMessage": "Bu mesaja geri sar", + "rewindTip": "Bu mesaja geri sar. Sonraki değişiklikler geri alınır.", "copyMessage": "Mesajı kopyala", "copiedToClipboard": "Panoya kopyalandı", "copyFailed": "Kopyalanamadı", @@ -279,7 +284,6 @@ "composerDisabledNoProvider": "Sohbete başlamak için bir sağlayıcı kurun.", "modelLabel": "Model", "reasoningEffortLabel": "Akıl yürütme çabası", - "sendTitle": "Gönder (Enter)", "send": "Gönder", "rewindConfirmTitle": "Buraya geri sarılsın mı?", "rewindConfirmBody": "Bu noktadan sonraki aracı düzenlemeleri ve takip eden turlar geri alınacak. Proje, konuşma ve aracı durumu geri yüklenecek. O zamandan beri yaptığınız düzenlemeler de değiştirilir.", @@ -343,8 +347,8 @@ }, "transport": { "playbackControls": "Oynatma denetimleri", - "playPause": "Oynat / Duraklat", - "playPauseTitle": "Oynat / Duraklat (Boşluk)", + "play": "Oynat", + "pause": "Duraklat", "loop": "Döngü" }, "shell": { diff --git a/src/i18n/locales/tr/settings.json b/src/i18n/locales/tr/settings.json index 735d5c76c..ae3389387 100644 --- a/src/i18n/locales/tr/settings.json +++ b/src/i18n/locales/tr/settings.json @@ -275,7 +275,7 @@ "fadeIn": "Açılma", "remove": "Parçayı sil", "loop": "Döngü", - "slipHint": "İçindeki sesi kaydırmak için Alt ile sürükleyin", + "slipHint": "{{modifier}} tuşunu basılı tutup sürükleyerek sesi parça içinde kaydırın", "help": "Bu ses kanalının seviyesi, geçişleri ve döngüsü. Önizlemede ve dışa aktarımda kaydın üzerine miksleniyor. Müzik kanalı, biri konuşurken kendiliğinden kısılır. Kanalı taşımak veya yeniden boyutlandırmak için zaman çizelgesinde sürükleyin; içindeki sesi kaydırmak için Alt tuşunu basılı tutarak sürükleyin.", "add": "Ses parçası ekle", "mute": "Sessiz" @@ -334,7 +334,14 @@ }, "facets": { "transcript": "Metin Dökümü", - "captions": "Altyazılar" + "captions": "Altyazılar", + "tips": { + "effects": "Kompozisyon: arka plan, çerçeve, gölge ve dolgu", + "layout": "Kamera düzeni: kameranın konumu, şekli ve boyutu", + "audio": "Ses: tüm videonun çıkış seviyesi", + "cursor": "İmleç: stil, boyut ve efektler", + "transcript": "Metin Dökümü: videoyu kesmek için kelimeleri düzenleyin" + } }, "crop": { "title": "Kırpma", diff --git a/src/i18n/locales/tr/timeline.json b/src/i18n/locales/tr/timeline.json index 40c8b402f..ceb5b790f 100644 --- a/src/i18n/locales/tr/timeline.json +++ b/src/i18n/locales/tr/timeline.json @@ -1,16 +1,13 @@ { "buttons": { - "addZoom": "Yakınlaştırma Ekle (Z)", - "suggestZooms": "İmleçten Yakınlaştırma Öner", - "autoZoomOn": "Otomatik yakınlaştırma önerileri açık — önerilen yakınlaştırmaları kaldırmak için tıklayın", - "autoZoomOff": "Otomatik yakınlaştırma önerileri kapalı — imleçten yakınlaştırma önermek için tıklayın", - "autoFocusAllOn": "Tüm yakınlaştırmalarda Otomatik Odak açık — hepsini manuel yapmak için tıklayın", - "autoFocusAllOff": "Tüm yakınlaştırmalarda Otomatik Odağı aç (kamera imleci takip eder)", - "clearTimeline": "Zaman çizelgesini temizle", - "addTrim": "Kırpma Ekle (T)", - "addAnnotation": "Açıklama Ekle (A)", - "addSpeed": "Hız Ekle (S)", - "addCameraFullscreen": "Tam Ekran Kamera Ekle (C)" + "addZoom": "Şu anki zamanda yakınlaştırma ekle", + "autoFocusAll": "Tüm yakınlaştırmalar için Otomatik Odak", + "autoFocusAllTip": "Tüm yakınlaştırmalar imleci takip eder. Kapalı: her yakınlaştırmanın odağını siz seçersiniz.", + "clearTimeline": "Tüm yakınlaştırma, kırpma, hız, açıklama ve Tam Ekran Kamera öğelerini kaldır. Klipler, ses ve altyazılar kalır.", + "addTrim": "Kırpma ekle: videonun bir bölümünü şu anki zamanda çıkarır", + "addAnnotation": "Şu anki zamanda metin, ok, resim veya bulanıklık ekle", + "addSpeed": "Şu anki zamanda hız değişikliği ekle", + "addCameraFullscreen": "Tam Ekran Kamera ekle: kamerayı şu anki zamanda tam ekran gösterir" }, "hints": { "pressZoom": "Yakınlaştırma eklemek için Z tuşuna basın", @@ -22,7 +19,9 @@ }, "labels": { "pan": "Kaydır", + "panTip": "Zaman çizelgesini kaydır", "zoom": "Yakınlaştır", + "zoomTip": "Zaman çizelgesini yakınlaştır", "trim": "Kırp", "speed": "Hız", "zoomItem": "Yakınlaştırma {{index}}", @@ -71,7 +70,7 @@ "arrangeClips": "Klipleri düzenle", "arrangeClipsHint": "Yeniden sıralamak için aşağıdaki klipleri sürükleyin veya yenilerini bırakın", "newAnnotation": "Açıklama", - "dragToReorderHint": "Yeniden sıralamak için sürükleyin · giriş/çıkış noktalarını düzenlemek için çift tıklayın", + "dragToReorderHint": "Sıralamak için sürükleyin. Başlangıç ve bitişi değiştirmek için çift tıklayın.", "editInOutPoints": "Giriş/çıkış noktalarını düzenle", "deleteClip": "Klibi sil", "dropToAdd": "Zaman çizelgesine eklemek için bırakın", diff --git a/src/i18n/locales/vi/editor.json b/src/i18n/locales/vi/editor.json index 6ffcf1563..69bfb7978 100644 --- a/src/i18n/locales/vi/editor.json +++ b/src/i18n/locales/vi/editor.json @@ -86,7 +86,7 @@ } }, "topbar": { - "toggleChatPanel": "Bật/tắt bảng trò chuyện", + "toggleChatPanel": "Bảng trò chuyện", "openProject": "Mở dự án", "newProject": "Dự án mới", "saveProject": "Lưu dự án", @@ -111,13 +111,15 @@ "selectSource": "Chọn nội dung để ghi", "systemPicker": "Hệ thống sẽ hỏi bạn muốn chia sẻ gì", "systemAudio": "Âm thanh hệ thống", + "systemAudioTip": "Ghi âm thanh máy tính phát ra", "microphone": "Micro", "camera": "Máy ảnh", - "cursorHighlight": "Làm nổi bật con trỏ", + "editableCursor": "Con trỏ có thể chỉnh sửa", + "editableCursorTip": "Bật: con trỏ có thể chỉnh sửa, có đổi kiểu và thu phóng tự động. Tắt: con trỏ hệ thống, được ghi vào video.", "autoZoom": "Tự động phóng to sau khi ghi", "hideDesktopIcons": "Ẩn biểu tượng màn hình nền", "hideDesktopIconsHintMac": "Loại các biểu tượng màn hình nền khỏi bản ghi màn hình. Chúng vẫn ở trên màn hình nền của bạn.", - "hideDesktopIconsHintWindows": "Che các biểu tượng màn hình nền bằng hình nền trong khi ghi màn hình. Chúng sẽ trở lại khi dừng ghi.", + "hideDesktopIconsHintWindows": "Che các biểu tượng màn hình nền khi ghi. Chúng trở lại khi dừng.", "on": "Bật", "off": "Tắt", "loading": "Đang tải…", @@ -156,6 +158,7 @@ "regenerateAs": "Tạo lại bằng", "auto": "Tự động", "regenerate": "Tạo lại", + "regenerateTip": "Tạo lại: phiên âm lại bằng ngôn ngữ đã chọn", "transcriptEmpty": "Bản ghi lời thoại trống.", "notGeneratedHint": "Chưa được tạo — chọn ngôn ngữ và nhấp vào tạo lại.", "transcribing": "Đang phiên âm", @@ -163,7 +166,7 @@ "initializingModel": "Đang khởi động mô hình giọng nói", "pendingTranscription": "Đang chờ phiên âm", "transcriptionFailed": "Phiên âm thất bại", - "cpuBackendHint": "Đang chạy trên CPU — chậm hơn khoảng 2× so với GPU", + "cpuBackendHint": "Đang chạy trên CPU: chậm hơn khoảng 2× so với GPU", "noTranscript": "Không có bản ghi lời thoại", "generationFailedHint": "Tạo thất bại — chọn ngôn ngữ và tạo lại.", "detectedLanguage": "Ngôn ngữ phát hiện: {{language}}", @@ -213,6 +216,7 @@ "trimHiddenDuration": "{{duration}}s media gốc bị ẩn khỏi dòng thời gian đã chỉnh sửa.", "restoreDeleteTrim": "Khôi phục (xóa cắt)", "collapseInspector": "Thu gọn bảng kiểm tra", + "editClipTip": "Chỉnh sửa clip: cắt xén, điểm bắt đầu và kết thúc", "captionsDescription": "Tạo phụ đề đồng bộ theo từng từ từ bản ghi lời thoại và thả vào dòng thời gian.", "generateCaptions": "Tạo phụ đề", "cropDescription": "Đóng khung lại bản ghi — chọn tỷ lệ khung hình và phóng to vào vùng bạn muốn giữ lại.", @@ -248,9 +252,9 @@ "reasoningEffortUpdateFailed": "Không thể cập nhật mức độ suy luận", "contextTooltip": "{{usedTokens}} / {{budgetTokens}} token ước tính", "contextPercent": "{{percent}}% ngữ cảnh", - "compactContext": "Nén ngữ cảnh", + "compactContext": "Tóm tắt các tin nhắn trước để dùng ít ngữ cảnh hơn", "aiSettings": "Cài đặt AI", - "history": "Lịch sử", + "history": "Lịch sử trò chuyện", "newConversation": "Cuộc trò chuyện mới", "untitledConversation": "Cuộc trò chuyện", "clickToRename": "Nhấp để đổi tên", @@ -270,6 +274,7 @@ "authorUser": "Bạn", "authorAssistant": "OpenScreen", "rewindToMessage": "Tua lại đến tin nhắn này", + "rewindTip": "Tua lại đến tin nhắn này. Các thay đổi sau đó sẽ được hoàn tác.", "copyMessage": "Sao chép tin nhắn", "copiedToClipboard": "Đã sao chép vào bộ nhớ tạm", "copyFailed": "Không thể sao chép", @@ -279,7 +284,6 @@ "composerDisabledNoProvider": "Hãy thiết lập nhà cung cấp để bắt đầu trò chuyện.", "modelLabel": "Mô hình", "reasoningEffortLabel": "Mức độ suy luận", - "sendTitle": "Gửi (Enter)", "send": "Gửi", "rewindConfirmTitle": "Tua lại đến đây?", "rewindConfirmBody": "Các chỉnh sửa của tác nhân và các lượt tiếp theo sau điểm này sẽ được khôi phục lại. Dự án, cuộc trò chuyện và trạng thái tác nhân sẽ được khôi phục. Các chỉnh sửa bạn đã thực hiện từ lúc đó cũng sẽ bị thay thế.", @@ -343,8 +347,8 @@ }, "transport": { "playbackControls": "Điều khiển phát lại", - "playPause": "Phát / Tạm dừng", - "playPauseTitle": "Phát / Tạm dừng (Phím cách)", + "play": "Phát", + "pause": "Tạm dừng", "loop": "Lặp lại" }, "shell": { diff --git a/src/i18n/locales/vi/settings.json b/src/i18n/locales/vi/settings.json index b6ad127cb..283f9c9db 100644 --- a/src/i18n/locales/vi/settings.json +++ b/src/i18n/locales/vi/settings.json @@ -275,7 +275,7 @@ "fadeIn": "Mờ vào", "remove": "Xóa bản nhạc", "loop": "Lặp", - "slipHint": "Giữ Alt và kéo để trượt âm thanh bên trong", + "slipHint": "Giữ {{modifier}} và kéo để trượt âm thanh bên trong bản âm thanh", "help": "Âm lượng, fade và lặp của rãnh âm thanh này. Nó được trộn lên trên bản ghi trong xem trước và khi xuất. Rãnh nhạc tự động nhỏ lại khi có người nói. Kéo rãnh trên dòng thời gian để di chuyển hoặc đổi kích thước, hoặc giữ Alt và kéo để trượt âm thanh bên trong.", "add": "Thêm bản âm thanh", "mute": "Tắt tiếng" @@ -334,7 +334,14 @@ }, "facets": { "transcript": "Bản ghi lời thoại", - "captions": "Phụ đề" + "captions": "Phụ đề", + "tips": { + "effects": "Bố cục hình ảnh: nền, khung, bóng đổ và phần đệm", + "layout": "Bố cục camera: vị trí, hình dạng và kích thước của camera", + "audio": "Âm thanh: mức đầu ra của toàn bộ video", + "cursor": "Con trỏ: kiểu, kích thước và hiệu ứng", + "transcript": "Bản ghi lời thoại: chỉnh sửa từ để cắt video" + } }, "crop": { "title": "Cắt xén", diff --git a/src/i18n/locales/vi/timeline.json b/src/i18n/locales/vi/timeline.json index 6602ce939..3dcc7405a 100644 --- a/src/i18n/locales/vi/timeline.json +++ b/src/i18n/locales/vi/timeline.json @@ -1,16 +1,13 @@ { "buttons": { - "addZoom": "Thêm Thu phóng (Z)", - "suggestZooms": "Đề xuất Thu phóng từ Con trỏ", - "autoZoomOn": "Đề xuất thu phóng tự động đang bật — nhấp để loại bỏ các thu phóng được đề xuất", - "autoZoomOff": "Đề xuất thu phóng tự động đang tắt — nhấp để đề xuất thu phóng từ con trỏ", - "autoFocusAllOn": "Lấy nét tự động đang bật cho tất cả các thu phóng — nhấp để chuyển tất cả sang thủ công", - "autoFocusAllOff": "Bật lấy nét tự động cho tất cả các thu phóng (máy ảnh theo dõi con trỏ)", - "clearTimeline": "Xóa dòng thời gian", - "addTrim": "Thêm Cắt (T)", - "addAnnotation": "Thêm Chú thích (A)", - "addSpeed": "Thêm Tốc độ (S)", - "addCameraFullscreen": "Thêm Camera Toàn màn hình (C)" + "addZoom": "Thêm thu phóng tại thời điểm hiện tại", + "autoFocusAll": "Lấy nét tự động cho mọi thu phóng", + "autoFocusAllTip": "Mọi thu phóng đều theo con trỏ. Tắt: bạn chọn điểm lấy nét cho từng thu phóng.", + "clearTimeline": "Xóa mọi thu phóng, đoạn cắt, thay đổi tốc độ, chú thích và Camera Toàn màn hình. Clip, âm thanh và phụ đề được giữ lại.", + "addTrim": "Thêm cắt: xóa một đoạn video tại thời điểm hiện tại", + "addAnnotation": "Thêm văn bản, mũi tên, hình ảnh hoặc làm mờ tại thời điểm hiện tại", + "addSpeed": "Thêm thay đổi tốc độ tại thời điểm hiện tại", + "addCameraFullscreen": "Thêm Camera Toàn màn hình: hiển thị camera toàn màn hình tại thời điểm hiện tại" }, "hints": { "pressZoom": "Nhấn Z để thêm thu phóng", @@ -22,7 +19,9 @@ }, "labels": { "pan": "Xoay", + "panTip": "Cuộn dòng thời gian", "zoom": "Thu phóng", + "zoomTip": "Thu phóng dòng thời gian", "trim": "Cắt", "speed": "Tốc độ", "zoomItem": "Thu phóng {{index}}", @@ -71,7 +70,7 @@ "arrangeClips": "Sắp xếp clip", "arrangeClipsHint": "Kéo các clip bên dưới để sắp xếp lại hoặc thả clip mới vào", "newAnnotation": "Chú thích", - "dragToReorderHint": "Kéo để sắp xếp lại · nhấp đúp để chỉnh sửa điểm vào/ra", + "dragToReorderHint": "Kéo để sắp xếp lại. Nhấp đúp để đổi điểm bắt đầu và kết thúc.", "editInOutPoints": "Chỉnh sửa điểm vào/ra", "deleteClip": "Xóa clip", "dropToAdd": "Thả để thêm vào dòng thời gian", diff --git a/src/i18n/locales/zh-CN/editor.json b/src/i18n/locales/zh-CN/editor.json index 31d506f06..e86e7cf10 100644 --- a/src/i18n/locales/zh-CN/editor.json +++ b/src/i18n/locales/zh-CN/editor.json @@ -86,7 +86,7 @@ } }, "topbar": { - "toggleChatPanel": "切换聊天面板", + "toggleChatPanel": "聊天面板", "openProject": "打开项目", "newProject": "新建项目", "saveProject": "保存项目", @@ -111,13 +111,15 @@ "selectSource": "选择要录制的内容", "systemPicker": "系统将询问要共享的内容", "systemAudio": "系统音频", + "systemAudioTip": "录制电脑播放的声音", "microphone": "麦克风", "camera": "摄像头", - "cursorHighlight": "光标高亮", + "editableCursor": "可编辑光标", + "editableCursorTip": "开:可编辑光标,可修改样式并使用自动缩放。关:系统光标,直接录进视频。", "autoZoom": "录制后自动缩放", "hideDesktopIcons": "隐藏桌面图标", "hideDesktopIconsHintMac": "在屏幕录制中去掉桌面图标。它们仍保留在你的桌面上。", - "hideDesktopIconsHintWindows": "录制屏幕时用壁纸遮住桌面图标。停止录制后会恢复。", + "hideDesktopIconsHintWindows": "录制时遮住桌面图标。停止后会恢复。", "on": "开", "off": "关", "loading": "加载中…", @@ -156,6 +158,7 @@ "regenerateAs": "重新生成为", "auto": "自动", "regenerate": "重新生成", + "regenerateTip": "重新生成:用所选语言再次转录", "transcriptEmpty": "转录内容为空。", "notGeneratedHint": "尚未生成 — 选择语言并点击重新生成。", "transcribing": "正在转录", @@ -163,7 +166,7 @@ "initializingModel": "正在启动语音模型", "pendingTranscription": "等待转录", "transcriptionFailed": "转录失败", - "cpuBackendHint": "正在使用 CPU 运行 — 比 GPU 慢约 2 倍", + "cpuBackendHint": "正在使用 CPU 运行:比 GPU 慢约 2 倍", "noTranscript": "没有转录文本", "generationFailedHint": "生成失败 — 选择语言并重新生成。", "detectedLanguage": "检测到的语言:{{language}}", @@ -213,6 +216,7 @@ "trimHiddenDuration": "编辑后的时间轴中隐藏了 {{duration}} 秒的源媒体。", "restoreDeleteTrim": "恢复(删除裁剪)", "collapseInspector": "折叠检查器", + "editClipTip": "编辑片段:裁剪、起点和终点", "captionsDescription": "根据转录文本生成按字词计时的字幕,并将其放置到时间轴上。", "generateCaptions": "生成字幕", "cropDescription": "重新构图录制内容 — 选择宽高比并缩放到要保留的区域。", @@ -248,9 +252,9 @@ "reasoningEffortUpdateFailed": "无法更新推理强度", "contextTooltip": "预估 {{usedTokens}} / {{budgetTokens}} 个 token", "contextPercent": "上下文 {{percent}}%", - "compactContext": "压缩上下文", + "compactContext": "总结之前的消息,以占用更少上下文", "aiSettings": "AI 设置", - "history": "历史记录", + "history": "对话历史", "newConversation": "新对话", "untitledConversation": "对话", "clickToRename": "点击重命名", @@ -270,6 +274,7 @@ "authorUser": "你", "authorAssistant": "OpenScreen", "rewindToMessage": "回退到此消息", + "rewindTip": "回退到此消息。之后的更改会被撤销。", "copyMessage": "复制消息", "copiedToClipboard": "已复制到剪贴板", "copyFailed": "无法复制", @@ -279,7 +284,6 @@ "composerDisabledNoProvider": "请先设置一个提供方再开始聊天。", "modelLabel": "模型", "reasoningEffortLabel": "推理强度", - "sendTitle": "发送(回车)", "send": "发送", "rewindConfirmTitle": "要回退到这里吗?", "rewindConfirmBody": "此时间点之后代理的编辑和后续对话轮次将被回滚。项目、对话和代理状态将被恢复。你在那之后所做的编辑也会被替换。", @@ -343,8 +347,8 @@ }, "transport": { "playbackControls": "播放控件", - "playPause": "播放 / 暂停", - "playPauseTitle": "播放 / 暂停(空格)", + "play": "播放", + "pause": "暂停", "loop": "循环" }, "shell": { diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index de1538b84..8ed665e5a 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -275,7 +275,7 @@ "fadeIn": "淡入", "remove": "删除轨道", "loop": "循环", - "slipHint": "按住 Alt 拖动可在其中滑动音频", + "slipHint": "按住 {{modifier}} 并拖动,可在轨道内滑动音频", "help": "此音频轨道的音量、淡入淡出和循环。在预览和导出中都会混合到录制内容之上。有人说话时,音乐轨道会自动降低音量。在时间轴上拖动可移动或调整大小,按住 Alt 拖动则可在其中滑动音频。", "add": "添加音频轨道", "mute": "静音" @@ -334,7 +334,14 @@ }, "facets": { "transcript": "转录文本", - "captions": "字幕" + "captions": "字幕", + "tips": { + "effects": "画面合成:背景、画框、阴影和内边距", + "layout": "摄像头布局:摄像头的位置、形状和大小", + "audio": "音频:整个视频的输出电平", + "cursor": "光标:样式、大小和效果", + "transcript": "转录文本:编辑文字即可剪辑视频" + } }, "crop": { "title": "裁剪", diff --git a/src/i18n/locales/zh-CN/timeline.json b/src/i18n/locales/zh-CN/timeline.json index 3182dba9f..18a2cabbb 100644 --- a/src/i18n/locales/zh-CN/timeline.json +++ b/src/i18n/locales/zh-CN/timeline.json @@ -1,16 +1,13 @@ { "buttons": { - "addZoom": "添加缩放 (Z)", - "suggestZooms": "根据光标建议缩放", - "autoZoomOn": "自动缩放建议已开启 — 点击可移除建议的缩放", - "autoZoomOff": "自动缩放建议已关闭 — 点击可根据光标建议缩放", - "autoFocusAllOn": "所有缩放的自动对焦已开启 — 点击可将全部切换为手动", - "autoFocusAllOff": "为所有缩放开启自动对焦(摄像头跟随光标)", - "clearTimeline": "清除时间轴", - "addTrim": "添加剪辑 (T)", - "addAnnotation": "添加标注 (A)", - "addSpeed": "添加速度 (S)", - "addCameraFullscreen": "添加全屏摄像头 (C)" + "addZoom": "在当前时间添加缩放", + "autoFocusAll": "所有缩放的自动对焦", + "autoFocusAllTip": "所有缩放都跟随光标。关:每个缩放的对焦由你选择。", + "clearTimeline": "移除所有缩放、剪辑、速度、标注和全屏摄像头。片段、音频和字幕会保留。", + "addTrim": "添加剪辑:在当前时间移除一段视频", + "addAnnotation": "在当前时间添加文字、箭头、图片或模糊", + "addSpeed": "在当前时间添加速度变化", + "addCameraFullscreen": "添加全屏摄像头:在当前时间全屏显示摄像头" }, "hints": { "pressZoom": "按 Z 添加缩放", @@ -22,7 +19,9 @@ }, "labels": { "pan": "平移", + "panTip": "滚动时间轴", "zoom": "缩放", + "zoomTip": "缩放时间轴", "trim": "剪辑", "speed": "速度", "zoomItem": "缩放 {{index}}", @@ -71,7 +70,7 @@ "arrangeClips": "排列片段", "arrangeClipsHint": "拖动下方片段以重新排序,或拖入新片段", "newAnnotation": "标注", - "dragToReorderHint": "拖动以重新排序 · 双击以编辑入点/出点", + "dragToReorderHint": "拖动可重新排序。双击可更改起点和终点。", "editInOutPoints": "编辑入点/出点", "deleteClip": "删除片段", "dropToAdd": "拖放以添加到时间轴", diff --git a/src/i18n/locales/zh-TW/editor.json b/src/i18n/locales/zh-TW/editor.json index f57bc9d1e..8e776e46a 100644 --- a/src/i18n/locales/zh-TW/editor.json +++ b/src/i18n/locales/zh-TW/editor.json @@ -86,7 +86,7 @@ } }, "topbar": { - "toggleChatPanel": "切換聊天面板", + "toggleChatPanel": "聊天面板", "openProject": "開啟專案", "newProject": "新專案", "saveProject": "儲存專案", @@ -111,13 +111,15 @@ "selectSource": "選擇要錄製的內容", "systemPicker": "系統將詢問要分享的內容", "systemAudio": "系統音訊", + "systemAudioTip": "錄製電腦播放的聲音", "microphone": "麥克風", "camera": "攝影機", - "cursorHighlight": "游標醒目提示", + "editableCursor": "可編輯游標", + "editableCursorTip": "開:可編輯游標,可修改樣式並使用自動縮放。關:系統游標,直接錄進影片。", "autoZoom": "錄製後自動縮放", "hideDesktopIcons": "隱藏桌面圖示", "hideDesktopIconsHintMac": "在螢幕錄製中略去桌面圖示。它們仍會留在你的桌面上。", - "hideDesktopIconsHintWindows": "錄製螢幕時以桌布遮住桌面圖示。停止錄製後就會恢復。", + "hideDesktopIconsHintWindows": "錄製時遮住桌面圖示。停止後會恢復。", "on": "開", "off": "關", "loading": "載入中…", @@ -156,6 +158,7 @@ "regenerateAs": "以此語言重新產生", "auto": "自動", "regenerate": "重新產生", + "regenerateTip": "重新產生:以所選語言再次轉錄", "transcriptEmpty": "逐字稿為空。", "notGeneratedHint": "尚未產生 — 選擇語言並點擊重新產生。", "transcribing": "轉錄中", @@ -163,7 +166,7 @@ "initializingModel": "正在啟動語音模型", "pendingTranscription": "等待轉錄", "transcriptionFailed": "轉錄失敗", - "cpuBackendHint": "正在使用 CPU 執行 — 比 GPU 慢約 2 倍", + "cpuBackendHint": "正在使用 CPU 執行:比 GPU 慢約 2 倍", "noTranscript": "沒有逐字稿", "generationFailedHint": "產生失敗 — 選擇語言並重新產生。", "detectedLanguage": "偵測到的語言:{{language}}", @@ -213,6 +216,7 @@ "trimHiddenDuration": "已編輯的時間軸中隱藏了 {{duration}} 秒的來源媒體。", "restoreDeleteTrim": "還原(刪除修剪)", "collapseInspector": "摺疊檢查器", + "editClipTip": "編輯片段:裁切、起點和終點", "captionsDescription": "根據逐字稿產生逐字計時的字幕,並放置到時間軸上。", "generateCaptions": "產生字幕", "cropDescription": "重新構圖錄製內容 — 選擇長寬比並縮放至想保留的區域。", @@ -248,9 +252,9 @@ "reasoningEffortUpdateFailed": "無法更新推理強度", "contextTooltip": "預估 {{usedTokens}} / {{budgetTokens}} 個權杖", "contextPercent": "上下文 {{percent}}%", - "compactContext": "壓縮上下文", + "compactContext": "摘要先前的訊息,以佔用較少上下文", "aiSettings": "AI 設定", - "history": "歷史紀錄", + "history": "對話歷史", "newConversation": "新對話", "untitledConversation": "對話", "clickToRename": "點擊重新命名", @@ -270,6 +274,7 @@ "authorUser": "你", "authorAssistant": "OpenScreen", "rewindToMessage": "倒轉至此訊息", + "rewindTip": "倒轉至此訊息。之後的變更會被復原。", "copyMessage": "複製訊息", "copiedToClipboard": "已複製到剪貼簿", "copyFailed": "無法複製", @@ -279,7 +284,6 @@ "composerDisabledNoProvider": "請先設定一個提供者再開始聊天。", "modelLabel": "模型", "reasoningEffortLabel": "推理強度", - "sendTitle": "傳送(Enter)", "send": "傳送", "rewindConfirmTitle": "要倒轉到這裡嗎?", "rewindConfirmBody": "此時間點之後代理的編輯與後續對話回合將被回復。專案、對話與代理狀態將被還原。你在那之後所做的編輯也會被取代。", @@ -343,8 +347,8 @@ }, "transport": { "playbackControls": "播放控制項", - "playPause": "播放 / 暫停", - "playPauseTitle": "播放 / 暫停(空白鍵)", + "play": "播放", + "pause": "暫停", "loop": "循環" }, "shell": { diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json index bd011b90a..1c8912e74 100644 --- a/src/i18n/locales/zh-TW/settings.json +++ b/src/i18n/locales/zh-TW/settings.json @@ -275,7 +275,7 @@ "fadeIn": "淡入", "remove": "刪除軌道", "loop": "循環", - "slipHint": "按住 Alt 拖曳可在其中滑動音訊", + "slipHint": "按住 {{modifier}} 並拖曳,可在音軌內滑動音訊", "help": "此音訊軌道的音量、淡入淡出與循環。在預覽與匯出時都會混合到錄影之上。有人說話時,音樂軌道會自動降低音量。在時間軸上拖曳可移動或調整大小,按住 Alt 拖曳則可在其中滑動音訊。", "add": "新增音訊軌道", "mute": "靜音" @@ -334,7 +334,14 @@ }, "facets": { "transcript": "逐字稿", - "captions": "字幕" + "captions": "字幕", + "tips": { + "effects": "畫面合成:背景、外框、陰影和內邊距", + "layout": "攝影機版面:攝影機的位置、形狀和大小", + "audio": "音訊:整部影片的輸出音量", + "cursor": "游標:樣式、大小和效果", + "transcript": "逐字稿:編輯文字即可剪輯影片" + } }, "crop": { "title": "裁剪", diff --git a/src/i18n/locales/zh-TW/timeline.json b/src/i18n/locales/zh-TW/timeline.json index 5414690a4..17d76dcc6 100644 --- a/src/i18n/locales/zh-TW/timeline.json +++ b/src/i18n/locales/zh-TW/timeline.json @@ -1,16 +1,13 @@ { "buttons": { - "addZoom": "新增縮放 (Z)", - "suggestZooms": "根據游標建議縮放", - "autoZoomOn": "自動縮放建議已開啟 — 點擊可移除建議的縮放", - "autoZoomOff": "自動縮放建議已關閉 — 點擊可根據游標建議縮放", - "autoFocusAllOn": "所有縮放的自動對焦已開啟 — 點擊可將全部切換為手動", - "autoFocusAllOff": "為所有縮放開啟自動對焦(攝影機跟隨游標)", - "clearTimeline": "清除時間軸", - "addTrim": "新增剪輯 (T)", - "addAnnotation": "新增標註 (A)", - "addSpeed": "新增速度 (S)", - "addCameraFullscreen": "新增全螢幕攝影機 (C)" + "addZoom": "在目前時間新增縮放", + "autoFocusAll": "所有縮放的自動對焦", + "autoFocusAllTip": "所有縮放都跟隨游標。關:每個縮放的對焦由你選擇。", + "clearTimeline": "移除所有縮放、剪輯、速度、標註和全螢幕攝影機。片段、音訊和字幕會保留。", + "addTrim": "新增剪輯:在目前時間移除一段影片", + "addAnnotation": "在目前時間新增文字、箭頭、圖片或模糊", + "addSpeed": "在目前時間新增速度變化", + "addCameraFullscreen": "新增全螢幕攝影機:在目前時間全螢幕顯示攝影機" }, "hints": { "pressZoom": "按 Z 新增縮放", @@ -22,7 +19,9 @@ }, "labels": { "pan": "平移", + "panTip": "捲動時間軸", "zoom": "縮放", + "zoomTip": "縮放時間軸", "trim": "剪輯", "speed": "速度", "zoomItem": "縮放 {{index}}", @@ -71,7 +70,7 @@ "arrangeClips": "排列片段", "arrangeClipsHint": "拖曳下方片段以重新排序,或拖曳新片段至此", "newAnnotation": "註解", - "dragToReorderHint": "拖曳以重新排序 · 按兩下以編輯入點/出點", + "dragToReorderHint": "拖曳可重新排序。按兩下可變更起點和終點。", "editInOutPoints": "編輯入點/出點", "deleteClip": "刪除片段", "dropToAdd": "拖放以新增至時間軸", diff --git a/src/lib/editableCursorAvailability.test.ts b/src/lib/editableCursorAvailability.test.ts new file mode 100644 index 000000000..903642732 --- /dev/null +++ b/src/lib/editableCursorAvailability.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it, vi } from "vitest"; +import { offersEditableCursor } from "./editableCursorAvailability"; + +const answer = (available: boolean, success = true) => vi.fn(async () => ({ success, available })); + +describe("offersEditableCursor", () => { + // Windows has no browser fallback, so there is nothing to probe. + it("offers it on Windows without asking a helper", async () => { + expect(await offersEditableCursor("win32", {})).toBe(true); + expect(await offersEditableCursor("win32", undefined)).toBe(true); + }); + + it("offers it on macOS and Linux only when the native helper answers yes", async () => { + expect( + await offersEditableCursor("darwin", { isNativeMacCaptureAvailable: answer(true) }), + ).toBe(true); + expect( + await offersEditableCursor("linux", { isNativeLinuxCaptureAvailable: answer(true) }), + ).toBe(true); + }); + + // Without the helper the browser records, and it always draws the system cursor into the + // video: switching to the editable cursor restores nothing, so it is not offered. + it("hides it when the helper is missing, or its answer is no", async () => { + expect( + await offersEditableCursor("darwin", { isNativeMacCaptureAvailable: answer(false) }), + ).toBe(false); + expect( + await offersEditableCursor("linux", { isNativeLinuxCaptureAvailable: answer(true, false) }), + ).toBe(false); + }); + + it("hides it when the probe itself fails", async () => { + const failing = vi.fn(async () => { + throw new Error("ipc down"); + }); + expect(await offersEditableCursor("darwin", { isNativeMacCaptureAvailable: failing })).toBe( + false, + ); + }); + + it("asks the probe of its own platform", async () => { + const mac = answer(true); + const linux = answer(false); + const api = { isNativeMacCaptureAvailable: mac, isNativeLinuxCaptureAvailable: linux }; + expect(await offersEditableCursor("darwin", api)).toBe(true); + expect(await offersEditableCursor("linux", api)).toBe(false); + expect(mac).toHaveBeenCalledTimes(1); + expect(linux).toHaveBeenCalledTimes(1); + }); + + // A build with no preload (the browser shim) has no probe: the platform decides. + it("falls back to the platform when there is no probe to ask", async () => { + expect(await offersEditableCursor("darwin", undefined)).toBe(true); + expect(await offersEditableCursor("linux", {})).toBe(true); + expect(await offersEditableCursor("freebsd", {})).toBe(false); + }); +}); diff --git a/src/lib/editableCursorAvailability.ts b/src/lib/editableCursorAvailability.ts new file mode 100644 index 000000000..4424b1ac0 --- /dev/null +++ b/src/lib/editableCursorAvailability.ts @@ -0,0 +1,38 @@ +/** The slice of `window.electronAPI` the check below needs. */ +type NativeCaptureProbe = { + isNativeMacCaptureAvailable?: () => Promise<{ success: boolean; available: boolean }>; + isNativeLinuxCaptureAvailable?: () => Promise<{ success: boolean; available: boolean }>; +}; + +/** + * Whether choosing the editable cursor changes anything on this machine. + * + * Every platform with a native capture helper can leave the system cursor out of the pixels and + * describe it separately (`captureCursor` on Windows, `hideSystemCursor` on macOS, the portal's + * METADATA mode on Linux). Without its helper, macOS or Linux records through the browser, which + * always draws the system cursor into the video (`effectiveBrowserCursorMode`): switching then + * restores nothing, and an option that changes nothing is not shown. Windows has no such + * fallback and no probe. A probe that fails counts as no helper. + * + * The one answer for every surface that offers the choice (the HUD, and the editor's Record + * mode), so they cannot disagree about when it exists. + */ +export async function offersEditableCursor( + platform: string, + api: NativeCaptureProbe | undefined, +): Promise { + const offersCursorMode = platform === "win32" || platform === "darwin" || platform === "linux"; + const probe = + platform === "darwin" + ? api?.isNativeMacCaptureAvailable + : platform === "linux" + ? api?.isNativeLinuxCaptureAvailable + : undefined; + if (!probe) return offersCursorMode; + try { + const result = await probe(); + return result.success && result.available; + } catch { + return false; + } +} diff --git a/src/lib/shortcuts.test.ts b/src/lib/shortcuts.test.ts index 6c0c2b3ca..511d7dfff 100644 --- a/src/lib/shortcuts.test.ts +++ b/src/lib/shortcuts.test.ts @@ -4,6 +4,7 @@ import { FIXED_SHORTCUTS, type FixedShortcut, findConflict, + formatFirstFixedBinding, formatFixedShortcut, mergeWithDefaults, SHORTCUT_ACTIONS, @@ -72,3 +73,17 @@ describe("formatFixedShortcut", () => { expect(formatFixedShortcut(fixed("cycleAnnotationsBackward"), true)).toBe("Shift + Tab"); }); }); + +describe("formatFirstFixedBinding", () => { + // A tooltip shows one chip. Redo is bound twice, and the dialog row that lists both is too long. + it("names the first binding of undo and redo, with ⌘ on macOS", () => { + expect(formatFirstFixedBinding("undo", false)).toBe("Ctrl + Z"); + expect(formatFirstFixedBinding("redo", false)).toBe("Ctrl + Shift + Z"); + expect(formatFirstFixedBinding("redo", true)).toBe("⌘ + ⇧ + Z"); + }); + + it("has nothing to show for a row without a binding, or an unknown row", () => { + expect(formatFirstFixedBinding("panTimeline", false)).toBeUndefined(); + expect(formatFirstFixedBinding("nope", false)).toBeUndefined(); + }); +}); diff --git a/src/lib/shortcuts.ts b/src/lib/shortcuts.ts index ee63a8358..03655ef11 100644 --- a/src/lib/shortcuts.ts +++ b/src/lib/shortcuts.ts @@ -193,6 +193,16 @@ export function formatFixedShortcut(shortcut: FixedShortcut, isMac: boolean): st return shortcut.bindings.map((binding) => formatBinding(binding, isMac)).join(" / "); } +/** + * The chip a tooltip shows for a fixed shortcut: its first binding only. Redo has two + * (Ctrl + Shift + Z, Ctrl + Y), and a tooltip names one, so the label a dialog row shows + * (`formatFixedShortcut`) would be too long for it. `undefined` when the row has no binding. + */ +export function formatFirstFixedBinding(i18nKey: string, isMac: boolean): string | undefined { + const first = FIXED_SHORTCUTS.find((shortcut) => shortcut.i18nKey === i18nKey)?.bindings[0]; + return first ? formatBinding(first, isMac) : undefined; +} + export function mergeWithDefaults(partial: Partial): ShortcutsConfig { const merged = { ...DEFAULT_SHORTCUTS }; for (const action of SHORTCUT_ACTIONS) { diff --git a/technical-documentation/engineering/tooltips.md b/technical-documentation/engineering/tooltips.md index 48cd28c0a..1e43fc686 100644 --- a/technical-documentation/engineering/tooltips.md +++ b/technical-documentation/engineering/tooltips.md @@ -58,7 +58,7 @@ If the icon has to swap and the wording has to follow (Play, Pause), it is an ac | | Limit | |---|---| -| English text | 80 characters. 95 for the strings that define the two cursor modes | +| English text | 80 characters. 95 for the strings that define the two cursor modes, and for Clear timeline, which has no confirmation and must say what goes and what stays | | Sentences | 2, one idea each. Punctuation at the end only when there are two | | Layout | `max-w-[260px]`, `dir="auto"`, balanced lines. 3 lines in English, 4 in French, Italian and Spanish | | Gap | 8px from the trigger. Where the trigger sits inside a padded surface (a button in the HUD bar), the gap is measured from that surface's edge | @@ -80,9 +80,9 @@ If the icon has to swap and the wording has to follow (Play, Pause), it is an ac | Mode tabs | title "Media" on the text "Media" | no tooltip | | HUD record | "Display 1" (the source name) | "Start recording", then "Stop and save the recording" | | HUD cancel | "Cancel recording" (deletes it, no confirmation) | "Stop and delete this recording" | -| Timeline | "Add zoom (Z)" | "Add a zoom at the playhead" and a chip | +| Timeline | "Add zoom (Z)" | "Add a zoom at the current time" and a chip | | Timeline | "Auto-Focus on for all zooms, click to switch all to manual", changing with `aria-pressed` | name "Auto-Focus for all zooms", one tooltip | -| Timeline | "Clear timeline" | "Remove all zooms, trims, speeds, annotations and Full Camera. Clips stay." | +| Timeline | "Clear timeline" | "Remove all zooms, trims, speeds, annotations and Full Camera. Clips, audio and captions stay." | | HUD hide | "Hide recording bar" | add how to get it back: "Show it again from the tray icon." | | Chat | "Compact context" | "Summarize earlier messages to use less context" | | Segmented rows | every option has a title equal to its text | title only for an icon-only option | @@ -113,7 +113,7 @@ One term per concept, across the HUD, Record mode and the editor. Where a locale | What the computer plays | **System audio** | computer audio, desktop audio | Gloss it once in the tooltip: "the sound your computer plays". | | The voice input | **Microphone** | mic | Never abbreviate in English. | | One recording session and its file | **Recording** | take, clip (a clip is a piece on the timeline), capture | "Take" is a code word (`discardRecordingId`). It does not appear in any UI string. | -| The moving line on the timeline | **playhead** | current time marker, cursor | Also the reference for "add at the playhead". Never call it "cursor": the cursor is the mouse pointer. | +| The moving line on the timeline | **playhead** | current time marker, cursor | Never call it "cursor": the cursor is the mouse pointer. In a tooltip write "at the current time", not "at the playhead": in pt-BR, ru and tr the word for playhead is the word for the mouse cursor. | | The editing window | **Studio** as the name of the HUD button, **the editor** in sentences | "Editor" as a button name | The HUD tooltip defines the name once: "Open Studio: the editor for your recordings". | | A region on the timeline | zoom, trim, speed change, annotation, Full Camera | segment, block, clip (for a region) | Same words as the region pills ("Zoom 1", "Trim 1"). | | Toggle position | on / off | enabled / disabled, active | Off is a state, not a verb: never "turn off" or "switch on". | diff --git a/technical-documentation/testing/manual-e2e-checklist.md b/technical-documentation/testing/manual-e2e-checklist.md index 253ffdd4b..9432d654e 100644 --- a/technical-documentation/testing/manual-e2e-checklist.md +++ b/technical-documentation/testing/manual-e2e-checklist.md @@ -137,14 +137,14 @@ The in-app source selector below is the Windows one, and the macOS one before 15 Record mode is the editor's pre-flight panel for the HUD: it edits the same settings, and *Start recording* hands them over. The HUD is closed while the editor is open, so every check here that ends in a take goes through that hand-over. - [ ] On a first run, with no saved project (otherwise the editor reopens the most recent one), open the editor and confirm the empty state leads with *Record your screen*, followed by *Import a video* and an *Open project* link; activate *Record your screen* and confirm it opens the **Record** tab. -- [ ] Confirm Record mode lists Source, System audio, Microphone (absent on macOS 13 and 14), Camera, Cursor highlight, Auto-zoom after recording (only while Cursor highlight is On), and Hide desktop icons (macOS and Windows only). +- [ ] Confirm Record mode lists Source, System audio, Microphone (absent on macOS 13 and 14), Camera, Editable cursor, Auto-zoom after recording (only while Editable cursor is On), and Hide desktop icons (macOS and Windows only). Editable cursor and Auto-zoom are both absent when capture falls back to the browser (macOS or Linux without their native helper), as the HUD's cursor button is. - [ ] Turn Camera on and confirm a live preview; turn Microphone on and confirm its level meter moves when you speak; switch each device from its menu and confirm the preview or the meter follows. - [ ] Pick a source from the Source row and confirm its name shows on the row and on the preview's badge. On Linux the row is plain text, "Your system will ask what to share", with nothing to pick. - [ ] Activate *Cancel* and confirm the editor returns to Edit mode. - [ ] Activate *Start recording* and confirm the editor window closes, the HUD comes back with the same source, audio, camera and cursor settings, and the take starts without another click (through the source picker first when no source was picked). -- [ ] With Cursor highlight and Auto-zoom after recording On, click a few places during a take, stop, and confirm the editor opens with zoom regions already on the timeline at those clicks. +- [ ] With Editable cursor and Auto-zoom after recording On, click a few places during a take, stop, and confirm the editor opens with zoom regions already on the timeline at those clicks. - [ ] Turn Auto-zoom after recording Off, record again, and confirm the new take opens with no zoom region; restart the app and confirm the row is still Off. -- [ ] Turn Cursor highlight Off and confirm the Auto-zoom after recording row disappears; confirm a take recorded that way opens without automatic zooms. +- [ ] Turn Editable cursor Off and confirm the Auto-zoom after recording row disappears; confirm a take recorded that way opens without automatic zooms. - [ ] On Windows, turn Hide desktop icons On, record a screen, and confirm the recording shows the wallpaper where the icons were, and that the icons are back when the take stops. On macOS, confirm the icons are absent from the recording while they stay on the desktop. ### Webcam capture quality — v2.0.0 @@ -229,16 +229,17 @@ The helper waits up to 3 s for the camera's first visible frame before starting - [ ] Confirm the dot after the project name reports "Saved" on hover after the rename: edits are saved as they land, with no save step. - [ ] Switch among the Media, Edit, and Record tabs and confirm each selected tab visibly changes state. - [ ] Return to Edit after visiting Media and Record and confirm the preview, timeline, and inspector are back and usable. Media shows the media library over a short arrange-only timeline; Record shows its settings panel and no timeline. -- [ ] In Edit mode, activate "Toggle chat panel" and confirm the chat panel opens or closes without changing the project. +- [ ] In Edit mode, activate the *Chat panel* button in the top bar and confirm the chat panel opens or closes without changing the project. +- [ ] Hover the timeline toolbar, the top bar (chat panel, undo, redo), the inspector rail and the Record mode rows with real mouse moves. Confirm each icon-only control names itself in one tooltip and shows no native one beside it, that the Add buttons, Play or Pause, Undo, Redo and Send show your own key as a chip, and that no tooltip covers the next control you would reach for (the rail opens its tooltips to the left). - [ ] Resize the chat panel by its visible divider and confirm the preview area resizes without moving the timeline content. - [ ] Resize the timeline by its visible top divider and confirm the timeline height changes without a layout crash. ## Transport and preview -The transport sits in the timeline header: Play / Pause and the time readout, nothing else. The ruler seeks, and Left / Right step 1/60 s. +The transport sits in the timeline header: Play (which becomes Pause) and the time readout, nothing else. The ruler seeks, and Left / Right step 1/60 s. -- [ ] Activate Play / Pause (also `Space`) and confirm `[data-testid="preview"]` changes `data-is-playing` from `false` to `true`. -- [ ] Activate play/pause again and confirm playback stops and the preview reports `data-is-playing="false"`. +- [ ] Activate *Play* (also `Space`, or the key you set in the shortcuts dialog) and confirm `[data-testid="preview"]` changes `data-is-playing` from `false` to `true`. +- [ ] Activate *Pause* and confirm playback stops and the preview reports `data-is-playing="false"`. - [ ] Confirm the transport time readout advances while playback is running. - [ ] Confirm the playhead advances with the video instead of remaining at its starting position. - [ ] Seek while paused and confirm the preview frame changes to the selected time. @@ -282,13 +283,13 @@ The transport sits in the timeline header: Play / Pause and the time readout, no ## Regions (trim/skip, zoom, speed, annotation) -The timeline toolbar adds each kind: *Add zoom (Z)*, *Add trim (T)*, *Add speed (S)*, *Add annotation (A)*, *Add Full Camera (C)*. Selecting a pill opens its settings in the inspector. +The timeline toolbar adds each kind at the current time: *Add a zoom* (`Z`), *Add a trim* (`T`), *Add a speed change* (`S`), *Add text, an arrow, an image or a blur* (an annotation, `A`), *Add Full Camera* (`C`, offered only in a project with a camera). The key shown in each tooltip is your own binding. Selecting a pill opens its settings in the inspector. - [ ] Drag a trim region's left edge and confirm its start time changes. - [ ] Drag a trim region's right edge and confirm its end time changes. - [ ] Scrub across a trim region and confirm the preview skips the marked interval during playback. - [ ] Select a trim, activate *Bring this part back* in its inspector, and confirm the interval is restored. -- [ ] Activate *Add zoom (Z)* and confirm a zoom region appears. +- [ ] Activate *Add a zoom at the current time* and confirm a zoom region appears. - [ ] Select the zoom region and pick each level in the *Zoom level* row, then type one in the field beside it; confirm the preview scale follows. Levels deeper than the recording can take without blurring are not offered, and a typed value out of range answers "Zoom goes from 1× to …×". - [ ] Drag the zoom focus point in the preview and confirm the zoom follows the new focus. - [ ] Change the zoom's *3D camera* among Off, 3D Orbit, Screen turned left and Screen turned right, and confirm the preview orientation changes; with 3D Orbit under Auto focus, move the cursor or click from one side of the recording to the other and confirm the screen turns to that side; under Manual focus, drag the focus point to one side and confirm the camera settles on that side and stays there while the cursor moves. @@ -296,13 +297,13 @@ The timeline toolbar adds each kind: *Add zoom (Z)*, *Add trim (T)*, *Add speed - [ ] Set a zoom region's *Focus mode* to Auto and confirm its focus follows cursor telemetry across the whole region. - [ ] Use *Auto-enhance* → *Automatic zooms* and confirm it adds suggested zoom regions when cursor telemetry supports suggestions, or says why not ("No room for automatic zooms" on a take whose zooms were already placed after recording). - [ ] Select a zoom region and activate *Delete zoom* in the inspector; confirm it disappears from the lane. -- [ ] With a zoom, a speed, a trim, an annotation and a Full Camera region on the timeline, plus an imported audio track and captions, activate *Clear timeline* (the eraser button at the end of the toolbar, after *Add Full Camera (C)* and a divider; both are absent while no region exists) and confirm every region disappears in one step while the clips, the audio track and the captions stay, and one `Ctrl+Z` brings all the regions back. -- [ ] Activate *Add speed (S)* and confirm a speed region appears. +- [ ] With a zoom, a speed, a trim, an annotation and a Full Camera region on the timeline, plus an imported audio track and captions, activate *Clear timeline* (the eraser button at the end of the toolbar, after *Add Full Camera* and a divider, or after the last add button when there is no camera; both are absent while no region exists) and confirm every region disappears in one step while the clips, the audio track and the captions stay, and one `Ctrl+Z` brings all the regions back. +- [ ] Activate *Add a speed change at the current time* and confirm a speed region appears. - [ ] Pick each speed in the *Playback speed* row (0.5×, 1×, 1.5×, 2×, 4×) and confirm the lane label and preview timing change. - [ ] Enter a custom speed in the *Custom speed* field, commit it, and confirm the value is kept, with no preset pressed. - [ ] Play across a speed region and confirm the preview reflects the region's speed. - [ ] Select a speed region and activate *Delete speed region*; confirm normal speed returns. -- [ ] Activate *Add annotation (A)* and confirm an annotation region appears. +- [ ] Activate *Add text, an arrow, an image or a blur at the current time* and confirm an annotation region appears. - [ ] Select a text annotation, replace its text, and confirm the new text appears in the preview, its box fitted to the text. - [ ] Change the text's size (24, 32, 48, 72 or typed), its colour, and its *Background* plate (None, Dark, Light); confirm each change is visible in the preview and that the text stays readable on every plate. - [ ] Pick each *Text animation* (the row labelled "Select animation") and confirm the animation runs when the playhead enters the region. @@ -469,7 +470,7 @@ must be visible, and at the insertion point rather than at the end of the clip. ## AI chat and providers — requires a configured provider -- [ ] In Edit mode, open the chat panel with the top bar's *Toggle chat panel* and confirm the chat surface appears. +- [ ] In Edit mode, open the chat panel with the top bar's *Chat panel* button and confirm the chat surface appears. - [ ] Confirm the chat header shows controls for AI settings, history, and a new conversation. - [ ] Send a short request and confirm the user message appears in the conversation. - [ ] Confirm the provider returns an assistant response without an unhandled error. @@ -509,8 +510,8 @@ The agent may only call the fixed tool set in [ai-agent.md](../architecture/ai-a - [ ] Use the rewind control on an earlier user message, confirm in the dialog, and confirm the timeline, the conversation tail, and the later checkpoints all roll back together. - [ ] Cancel a rewind at the confirmation dialog and confirm both the timeline and the conversation are untouched. - [ ] Confirm the context badge shows a percentage and that its tooltip reports used and budget tokens. -- [ ] Activate Compact context on a conversation with enough history and confirm an earlier-context summary message appears and the percentage drops. -- [ ] Activate Compact context on a short conversation and confirm the "not enough history" message rather than a failure. +- [ ] Activate the compact button (*Summarize earlier messages to use less context*) on a conversation with enough history and confirm an earlier-context summary message appears and the percentage drops. +- [ ] Activate the compact button on a short conversation and confirm the "not enough history" message rather than a failure. - [ ] Confirm a compaction failure leaves the conversation history unchanged. - [ ] Use the copy control on an assistant message and confirm the message text reaches the clipboard. - [ ] Open the timeline toolbar's *Auto-enhance* menu, choose *Smart cuts* (it needs a transcript), and confirm the chat panel opens with the prompt prefilled and sent through the normal send path. @@ -598,7 +599,7 @@ The inspector's rail holds five facets: Composition (background, format, frame, - [ ] Open the Cursor facet and toggle *Show cursor* and *Auto-hide when inactive*; confirm the preview changes. - [ ] Change *Size*, *Smoothing* and *Motion blur*, pick each *Click bounce* (None, Light, Strong), and toggle *Click impact*; confirm each committed value remains visible. - [ ] Pick each *Cursor style* and toggle *3D cursor*; confirm the preview cursor changes. Under *Cursor types*, switch a type off and confirm that type is drawn as the arrow. -- [ ] Record a take with *Cursor highlight* Off (the system cursor), open it in the editor, and confirm the rail has no Cursor facet; confirm a take recorded with it On shows the facet, and an imported video does not. +- [ ] Record a take with *Editable cursor* Off (the system cursor), open it in the editor, and confirm the rail has no Cursor facet; confirm a take recorded with it On shows the facet, and an imported video does not. - [ ] Open OpenScreen menu → *Switch to light theme* (or dark) and confirm the editor switches between dark and light themes. - [ ] Open OpenScreen menu → *Change language*, choose a non-English locale, and confirm visible UI strings change. - [ ] Switch back to English and confirm the top bar, transport, inspector, and export labels return to English. @@ -759,12 +760,12 @@ Edits are saved as they land. The dot after the project name reads "Unsaved" (ho #### Permissions window — v2.0.0 -On macOS 15.2+ it opens at launch, until it has been closed once, while one of its rows was never asked. Before 15.2 it opens at launch while Screen Recording is missing or waits on a relaunch. Tray → *Permissions…* and the app menu open it any time. A Record-mode start with Cursor highlight enabled may request Accessibility from macOS; a pending grant does not reopen this window or block recording. To see a first run on a Mac that has run OpenScreen before, its grants have to be reset first, which is the Mac owner's decision. +On macOS 15.2+ it opens at launch, until it has been closed once, while one of its rows was never asked. Before 15.2 it opens at launch while Screen Recording is missing or waits on a relaunch. Tray → *Permissions…* and the app menu open it any time. A Record-mode start with Editable cursor enabled may request Accessibility from macOS; a pending grant does not reopen this window or block recording. To see a first run on a Mac that has run OpenScreen before, its grants have to be reset first, which is the Mac owner's decision. - [ ] On a first launch, confirm the window "OpenScreen needs a few permissions" lists, on macOS 15.2+, System audio (Optional), Accessibility (Recommended), Microphone (Optional) and Camera (Optional); before 15.2, or with `OPENSCREEN_MAC_SOURCE_PICKER=legacy`, Screen & system audio (Required) comes first instead of System audio. - [ ] For each row whose permission has not been requested, activate its button (*Allow*, or *Continue* for the screen row) and confirm macOS raises its prompt; allow it and confirm the row turns to *Allowed* while the window stays open. - [ ] For a permission already requested but still denied, confirm the row offers *Open Settings* on the window's first appearance, and that it opens that permission's System Settings pane instead of raising a new prompt. -- [ ] With Screen Recording ready, Accessibility ungranted, and Cursor highlight enabled in Record mode, press *Start recording*. Confirm the macOS prompt may appear, the countdown still reaches an active take, and a warning says cursor effects may be limited while Accessibility is pending. Stop the take while leaving Accessibility pending, then start another take; confirm the OpenScreen permissions window does not reopen and recording still starts. Grant Accessibility in System Settings and confirm a later take has full cursor effects. +- [ ] With Screen Recording ready, Accessibility ungranted, and Editable cursor enabled in Record mode, press *Start recording*. Confirm the macOS prompt may appear, the countdown still reaches an active take, and a warning says cursor effects may be limited while Accessibility is pending. Stop the take while leaving Accessibility pending, then start another take; confirm the OpenScreen permissions window does not reopen and recording still starts. Grant Accessibility in System Settings and confirm a later take has full cursor effects. - [ ] Refuse a permission, reopen the window from the tray, and confirm its row still offers *Open Settings* and opens the matching System Settings pane. - [ ] Before 15.2: confirm *Get started* stays disabled until Screen Recording is allowed; that the window offers *Restart OpenScreen* when macOS needs a relaunch to apply it; and that after System Settings' Quit & Reopen the window comes back. - [ ] Close the window with *Get started* and confirm it does not open again at the next launch. diff --git a/tests/e2e/v4-shell.spec.ts b/tests/e2e/v4-shell.spec.ts index a4e6504dd..9b0604093 100644 --- a/tests/e2e/v4-shell.spec.ts +++ b/tests/e2e/v4-shell.spec.ts @@ -137,6 +137,14 @@ function makeTwoClipDoc(): ReturnType { return doc; } +// A pill draws its own label when it is wide enough, and carries the label as a `title` only when +// it is not (a title next to a visible label would repeat it), so a pill is found by either. +function zoomPill(page: Page, label: string): Locator { + return page.locator( + `[class*="lanePill"][title="${label}"], [class*="lanePill"]:has-text("${label}")`, + ); +} + async function seedAndOpen(page: Page, doc: ReturnType = makeDoc()): Promise { await page.addInitScript((serialized) => { const parsed = JSON.parse(serialized); @@ -283,7 +291,7 @@ test.describe("v4 editor shell", () => { page, }) => { await seedAndOpen(page, makeZoomDoc()); - await page.locator('[class*="lanePill"][title="1.80×"]').first().click(); + await zoomPill(page, "1.80×").first().click(); const levels = page.getByRole("group", { name: "Zoom Level" }).getByRole("button"); await expect(levels).toHaveCount(4); @@ -331,7 +339,7 @@ test.describe("v4 editor shell", () => { const custom = page.getByRole("textbox", { name: "Custom zoom" }); await custom.fill("2.5"); await custom.press("Enter"); - await expect(page.locator('[class*="lanePill"][title="2.50×"]')).toHaveCount(1); + await expect(zoomPill(page, "2.50×")).toHaveCount(1); await expect(levels.and(page.locator('[aria-pressed="true"]'))).toHaveCount(0); }); @@ -369,7 +377,7 @@ test.describe("v4 editor shell", () => { }); await seedAndOpen(page); - await page.getByRole("button", { name: "Toggle chat panel" }).click(); + await page.getByRole("button", { name: "Chat panel", exact: true }).click(); const composer = page.getByRole("textbox", { name: "Describe the edit you want." }); await expect(composer).toBeVisible(); await composer.fill("selectable user turn text"); diff --git a/website/docs/recording.md b/website/docs/recording.md index f77e7e08a..8369a7ab9 100644 --- a/website/docs/recording.md +++ b/website/docs/recording.md @@ -80,7 +80,7 @@ You don't have to start from the HUD. In the editor, switch the top bar to **Rec - **Source** — same screen/window picker, in a modal, or Apple's system picker on macOS 15.2 and later. On Linux this row also reads *Your system will ask what to share*, and the portal dialog does the choosing. - **System audio**, **Microphone**, **Camera** — each an on/off row; mic and camera expand to a device list, and the camera shows a live preview so you can frame yourself before going live. -- **Cursor highlight** — on means the editable overlay cursor, off means the plain system cursor. +- **Editable cursor** — on means the editable overlay cursor, off means the plain system cursor. **Start recording** opens the recording widget and closes the editor window; cancelling drops you back into Edit mode. This is also the starting point **New project → Screen recording** takes you to. diff --git a/website/i18n/de/docusaurus-plugin-content-docs/current/recording.md b/website/i18n/de/docusaurus-plugin-content-docs/current/recording.md index 823e0aaac..57c6f3b2a 100644 --- a/website/i18n/de/docusaurus-plugin-content-docs/current/recording.md +++ b/website/i18n/de/docusaurus-plugin-content-docs/current/recording.md @@ -80,7 +80,7 @@ Du musst nicht im HUD anfangen. Stelle im Editor die obere Leiste auf **Aufnahme - **Quelle**: dieselbe Auswahl für Bildschirm oder Fenster, in einem modalen Dialog, unter macOS 15.2 und neuer die Systemauswahl von Apple. Unter Linux steht auch in dieser Zeile *Dein System fragt gleich, was du teilen möchtest*, und der Portal-Dialog übernimmt die Auswahl. - **Systemton**, **Mikrofon**, **Kamera**: jeweils eine Zeile mit Ein/Aus. Mikrofon und Kamera klappen zu einer Geräteliste auf, und die Kamera zeigt eine Live-Vorschau, damit du dich vor dem Start ins Bild rücken kannst. -- **Cursor hervorheben**: Eingeschaltet steht für den bearbeitbaren Overlay-Cursor, ausgeschaltet für den einfachen Systemcursor. +- **Bearbeitbarer Cursor**: Eingeschaltet steht für den bearbeitbaren Overlay-Cursor, ausgeschaltet für den einfachen Systemcursor. **Aufnahme starten** öffnet das Aufnahme-Widget und schließt das Editorfenster; ein Abbruch bringt dich zurück in den Modus **Schnitt**. Hier landest du auch über **Neues Projekt → Bildschirmaufnahme**. diff --git a/website/i18n/es/docusaurus-plugin-content-docs/current/recording.md b/website/i18n/es/docusaurus-plugin-content-docs/current/recording.md index ce9cde2e8..95bd032ac 100644 --- a/website/i18n/es/docusaurus-plugin-content-docs/current/recording.md +++ b/website/i18n/es/docusaurus-plugin-content-docs/current/recording.md @@ -80,7 +80,7 @@ No tienes que empezar desde el HUD. En el editor, cambia la barra superior a **G - **Fuente**: el mismo selector de pantalla o ventana, en una ventana modal, o el selector del sistema de Apple en macOS 15.2 y posteriores. En Linux, esta fila también dice *El sistema te preguntará qué compartir*, y el cuadro de diálogo del portal es el que elige. - **Audio del sistema**, **Micrófono**, **Cámara**: cada uno es una fila que se activa o desactiva; el micrófono y la cámara se despliegan en una lista de dispositivos, y la cámara muestra una vista previa en vivo para que te encuadres antes de empezar. -- **Resaltar cursor**: activado significa el cursor editable; desactivado, el cursor normal del sistema. +- **Cursor editable**: activado significa el cursor editable; desactivado, el cursor normal del sistema. **Iniciar grabación** abre el widget de grabación y cierra la ventana del editor; si cancelas, vuelves al modo Editar. Este es también el punto de partida al que te lleva **Nuevo proyecto → Grabación de pantalla**. diff --git a/website/i18n/fr/docusaurus-plugin-content-docs/current/recording.md b/website/i18n/fr/docusaurus-plugin-content-docs/current/recording.md index 1c22993bf..483854fdf 100644 --- a/website/i18n/fr/docusaurus-plugin-content-docs/current/recording.md +++ b/website/i18n/fr/docusaurus-plugin-content-docs/current/recording.md @@ -80,7 +80,7 @@ Vous n'êtes pas obligé de partir du HUD. Dans l'éditeur, choisissez **Enregis - **Source** : le même sélecteur d'écran ou de fenêtre, dans une fenêtre modale, ou le sélecteur du système d'Apple sous macOS 15.2 et ultérieur. Sous Linux, cette ligne affiche aussi *Le système vous demandera quoi partager*, et c'est la boîte de dialogue du portail qui fait le choix. - **Audio système**, **Microphone**, **Caméra** : chacun sur une ligne avec un interrupteur ; le micro et la caméra se déplient en liste de périphériques, et la caméra montre un aperçu en direct pour vous cadrer avant de commencer. -- **Curseur en surbrillance** : activé, c'est le curseur éditable ; désactivé, le simple curseur du système. +- **Curseur éditable** : activé, c'est le curseur éditable ; désactivé, le simple curseur du système. **Démarrer l'enregistrement** ouvre le widget d'enregistrement et ferme la fenêtre de l'éditeur ; annuler vous ramène en mode Édition. C'est aussi là que mène **Nouveau projet → Enregistrement d'écran**. diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/current/recording.md b/website/i18n/ja/docusaurus-plugin-content-docs/current/recording.md index d959cbd59..8e76eb6bd 100644 --- a/website/i18n/ja/docusaurus-plugin-content-docs/current/recording.md +++ b/website/i18n/ja/docusaurus-plugin-content-docs/current/recording.md @@ -80,7 +80,7 @@ HUD から始める必要はありません。エディターのトップバー - **ソース**:同じ画面・ウィンドウのピッカーをモーダルで開きます(macOS 15.2 以降では Apple のシステムピッカー)。Linux ではこの行にも「共有する対象はシステムが確認します」と表示され、選択はポータルのダイアログで行います。 - **システム音声**、**マイク**、**カメラ**:それぞれオン/オフの行です。マイクとカメラはデバイスの一覧に展開でき、カメラはライブプレビューを表示するので、録画を始める前に自分の映り方を調整できます。 -- **カーソルの強調表示**:オンは編集可能なカーソル、オフは通常のシステムカーソルです。 +- **編集可能なカーソル**:オンは編集可能なカーソル、オフは通常のシステムカーソルです。 **録画を開始**を押すと録画ウィジェットが開き、エディターのウィンドウが閉じます。キャンセルすると**編集**モードに戻ります。**新規プロジェクト → 画面録画**を選んだときも、ここから始まります。 diff --git a/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/recording.md b/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/recording.md index b5884c6bb..a6ada177c 100644 --- a/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/recording.md +++ b/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/recording.md @@ -80,7 +80,7 @@ Você não precisa começar pelo HUD. No editor, mude a barra superior para **Gr - **Fonte** — o mesmo seletor de tela/janela, em uma janela modal, ou o seletor do sistema da Apple no macOS 15.2 e posterior. No Linux, essa linha também mostra *O sistema perguntará o que compartilhar*, e a caixa de diálogo do portal faz a escolha. - **Áudio do sistema**, **Microfone**, **Câmera** — cada um é uma linha de ativar/desativar; o microfone e a câmera se expandem em uma lista de dispositivos, e a câmera mostra uma pré-visualização ao vivo para você se enquadrar antes de começar. -- **Destaque do cursor** — ativado significa o cursor da sobreposição editável; desativado significa o cursor comum do sistema. +- **Cursor editável** — ativado significa o cursor da sobreposição editável; desativado significa o cursor comum do sistema. **Iniciar gravação** abre o widget de gravação e fecha a janela do editor; cancelar leva você de volta ao modo Editar. É também aqui que **Novo projeto → Gravação de tela** leva você. diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/recording.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/recording.md index 25fe7a700..1466c2c6d 100644 --- a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/recording.md +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/recording.md @@ -80,7 +80,7 @@ keywords: - **来源**:同样的屏幕/窗口选择器,以对话框形式打开;在 macOS 15.2 及更高版本上则是 Apple 的系统选择器。在 Linux 上,这一行同样显示“*系统将询问要共享的内容*”,由门户对话框来完成选择。 - **系统音频**、**麦克风**、**摄像头**:每项都是一行开关;麦克风和摄像头可以展开设备列表,摄像头还会显示实时预览,方便你在开录之前调整自己的画面。 -- **光标高亮**:打开表示使用可编辑光标,关闭表示使用普通的系统光标。 +- **可编辑光标**:打开表示使用可编辑光标,关闭表示使用普通的系统光标。 **开始录制**会打开录制小组件并关闭编辑器窗口;取消则会回到**编辑**模式。通过**新建项目 → 屏幕录制**进入的也是这个页面。 diff --git a/website/i18n/zh-TW/docusaurus-plugin-content-docs/current/recording.md b/website/i18n/zh-TW/docusaurus-plugin-content-docs/current/recording.md index 7876c69ed..6b2b718b2 100644 --- a/website/i18n/zh-TW/docusaurus-plugin-content-docs/current/recording.md +++ b/website/i18n/zh-TW/docusaurus-plugin-content-docs/current/recording.md @@ -80,7 +80,7 @@ keywords: - **來源**:同樣的螢幕/視窗選擇器,以對話框形式呈現;在 macOS 15.2 及更新版本上則是 Apple 的系統選擇器。在 Linux 上,這一列同樣顯示「系統將詢問要分享的內容」,由 portal 對話框負責選擇。 - **系統音訊**、**麥克風**、**攝影機**:每一項都是一列開關;麥克風與攝影機可以展開裝置清單,攝影機還會顯示即時預覽,讓你在開錄前先調整好自己在畫面中的位置。 -- **游標醒目提示**:開啟代表使用可編輯游標,關閉則代表使用一般的系統游標。 +- **可編輯游標**:開啟代表使用可編輯游標,關閉則代表使用一般的系統游標。 **開始錄製**會開啟錄影小工具並關閉編輯器視窗;取消則會讓你回到編輯模式。這也是 **新專案 → 螢幕錄製** 會帶你前往的起點。