diff --git a/apps/desktop-gpui/Cargo.lock b/apps/desktop-gpui/Cargo.lock index ce99f3d0504..900791a5cb8 100644 --- a/apps/desktop-gpui/Cargo.lock +++ b/apps/desktop-gpui/Cargo.lock @@ -1184,7 +1184,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -1541,7 +1541,7 @@ dependencies = [ "anyhow", "ashpd 0.11.1", "base64 0.22.1", - "block2 0.5.1", + "block2 0.6.2", "bytes", "cap-audio", "cap-camera", @@ -1585,9 +1585,11 @@ dependencies = [ "libc", "lyon_tessellation", "minisign-verify", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", "parakeet-rs", "raw-window-handle", "reqwest 0.12.28", @@ -1914,7 +1916,7 @@ dependencies = [ "khronos-egl", "libc", "objc", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "parking_lot", "pipewire", @@ -1972,7 +1974,7 @@ dependencies = [ "libloading 0.8.9", "log", "metal 0.31.0", - "objc2 0.6.4", + "objc2 0.6.5", "rayon", "reactive_graph", "resvg 0.45.1", @@ -2327,7 +2329,7 @@ checksum = "afede46921767868c5c7f8f55202bdd8bec0bab6bc9605174200f45924f93c62" dependencies = [ "clipboard-win", "image 0.25.10", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "windows 0.59.0", @@ -3396,7 +3398,7 @@ dependencies = [ "bitflags 2.13.1", "block2 0.6.2", "libc", - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -4450,7 +4452,7 @@ checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7" dependencies = [ "crossbeam-channel", "keyboard-types", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "once_cell", "thiserror 2.0.20", @@ -4750,7 +4752,7 @@ dependencies = [ "media", "metal 0.33.0", "objc", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "objc2-user-notifications", @@ -6458,7 +6460,7 @@ checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" dependencies = [ "cc", "log", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", "time", "uuid", @@ -6779,7 +6781,7 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -7231,9 +7233,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -7264,7 +7266,7 @@ dependencies = [ "bitflags 2.13.1", "block2 0.6.2", "libc", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-cloud-kit", "objc2-core-data 0.3.2", "objc2-core-foundation", @@ -7285,7 +7287,7 @@ dependencies = [ "bitflags 2.13.1", "block2 0.6.2", "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-avf-audio", "objc2-core-audio-types", "objc2-core-foundation", @@ -7305,7 +7307,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -7316,7 +7318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -7327,7 +7329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" dependencies = [ "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-audio-types", "objc2-core-foundation", ] @@ -7339,7 +7341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -7361,7 +7363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -7373,7 +7375,7 @@ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.13.1", "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -7383,10 +7385,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.13.1", + "block2 0.6.2", "dispatch2", - "objc2 0.6.4", + "libc", + "objc2 0.6.5", "objc2-core-foundation", "objc2-io-surface", + "objc2-metal 0.3.2", ] [[package]] @@ -7407,7 +7412,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -7417,7 +7422,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -7429,7 +7434,7 @@ checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d" dependencies = [ "bitflags 2.13.1", "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-audio", "objc2-core-audio-types", "objc2-core-foundation", @@ -7443,7 +7448,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-core-graphics", ] @@ -7455,7 +7460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-core-graphics", "objc2-io-surface", @@ -7497,7 +7502,7 @@ dependencies = [ "bitflags 2.13.1", "block2 0.6.2", "libc", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", ] @@ -7507,7 +7512,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b0446e98cf4a784cc7a0177715ff317eeaa8463841c616cfc78aa4f953c4ea" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-core-graphics", ] @@ -7529,7 +7534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", ] @@ -7539,7 +7544,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd9fdde720df3da7046bb9097811000c1e7ab5cd579fa89d96b27d56781fb30" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-audio-types", "objc2-core-foundation", "objc2-core-media", @@ -7565,7 +7570,7 @@ checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ "bitflags 2.13.1", "block2 0.6.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", ] @@ -7589,8 +7594,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ "bitflags 2.13.1", - "objc2 0.6.4", + "block2 0.6.2", + "libc", + "objc2 0.6.5", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-video", "objc2-foundation 0.3.2", "objc2-metal 0.3.2", ] @@ -7603,7 +7612,7 @@ checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.13.1", "block2 0.6.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-cloud-kit", "objc2-core-data 0.3.2", "objc2-core-foundation", @@ -7624,7 +7633,7 @@ checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ "bitflags 2.13.1", "block2 0.6.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-location", "objc2-foundation 0.3.2", ] @@ -7903,7 +7912,7 @@ dependencies = [ "android_system_properties", "log", "nix 0.31.3", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", "objc2-ui-kit", "serde", @@ -8994,7 +9003,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-foundation 0.3.2", "objc2-quartz-core 0.3.2", @@ -9379,7 +9388,7 @@ dependencies = [ "dispatch2", "js-sys", "log", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -9672,7 +9681,7 @@ version = "0.1.0" dependencies = [ "cidre", "futures", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "tracing", @@ -11691,7 +11700,7 @@ dependencies = [ "dirs 6.0.0", "libappindicator", "muda", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", @@ -12746,7 +12755,7 @@ dependencies = [ "log", "naga 29.0.4", "ndk-sys 0.6.0+11769913", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-foundation 0.3.2", "objc2-metal 0.3.2", @@ -13787,7 +13796,7 @@ dependencies = [ "num-complex", "num-integer", "num-rational", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-core-graphics", "open", diff --git a/apps/desktop-gpui/Cargo.toml b/apps/desktop-gpui/Cargo.toml index 7c0a9e79b82..8b9c003a1b4 100644 --- a/apps/desktop-gpui/Cargo.toml +++ b/apps/desktop-gpui/Cargo.toml @@ -62,7 +62,10 @@ cap-project = { path = "../../crates/project" } # a `RenderImage` (`src/editor_sidebar/cursor.rs`). cap-cursor-info = { path = "../../crates/cursor-info" } resvg = "0.45" -rodio = { version = "0.19.0", default-features = false, features = ["mp3", "vorbis"] } +rodio = { version = "0.19.0", default-features = false, features = [ + "mp3", + "vorbis", +] } # The editor stack. `cap-rendering` is not a new subtree -- cap-recording # already depends on it, so wgpu 25 and its vendored wgpu-hal have been built # by this workspace since the recording unit; naming it here only makes the @@ -100,7 +103,11 @@ ffmpeg = { package = "ffmpeg-next", git = "https://github.com/CapSoftware/rust-f # `webp` is the config sidebar's four background-source illustrations, which # the app ships as webp; features are additive, so enabling it here does not # move gpui's own pin. -image = { version = "0.25.1", default-features = false, features = ["jpeg", "png", "webp"] } +image = { version = "0.25.1", default-features = false, features = [ + "jpeg", + "png", + "webp", +] } jpeg-decoder = "0.3" smallvec = "1" # The OS language list the UI language follows when set to System default. @@ -131,7 +138,10 @@ sanitize-filename = "0.6.0" semver = "1" base64 = "0.22" minisign-verify = "0.2.4" -rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "tokio"] } +rfd = { version = "0.15", default-features = false, features = [ + "xdg-portal", + "tokio", +] } # The screenshot export compositor (`src/screenshot_export.rs`): the canvas-2d # work `renderScreenshotExportCanvas` does in the webview -- shape rasterizing, # text, and the share fingerprint. All three are already in this lockfile @@ -179,7 +189,14 @@ gpui = { git = "https://github.com/wingleeio/zed", rev = "5d1f83d9f27a19bec1fb24 # decoders, renderer and preview tasks are all tokio-spawned -- a current-thread # runtime would deadlock the moment the test thread blocks on a frame. Kept in # dev-dependencies so the shipped binary's tokio features are unchanged. -tokio = { version = "1", features = ["rt-multi-thread", "time", "test-util", "net", "io-util", "macros"] } +tokio = { version = "1", features = [ + "rt-multi-thread", + "time", + "test-util", + "net", + "io-util", + "macros", +] } # Caption oracles (`src/transcription/verify_tests.rs`): fixture projects and # audio markers for the fake transcriber. cap-verify = { path = "../../crates/verify" } @@ -220,15 +237,37 @@ x11rb = "0.13" [target.'cfg(target_os = "macos")'.dependencies] # Native window treatment (panel levels, Spaces) gpui does not expose. Same -# versions gpui's own tree compiles, so no second objc2 universe gets built. -objc2 = "0.5.2" +# direct 0.6 line used by this app's class definitions and AppKit bindings. +objc2 = { version = "0.6.5", features = ["exception"] } # `NSVisualEffectView` is the pre-macOS-26 window-material fallback. Features # only -- the versions stay pinned, or a second objc2 universe gets built. -objc2-app-kit = { version = "0.2.2", features = ["NSAppearance", "NSWindow", "NSView", "NSResponder", "NSGraphics", "NSPasteboard", "NSVisualEffectView", "NSWorkspace", "NSAccessibility"] } +objc2-app-kit = { version = "0.3.2", features = [ + "NSAppearance", + "NSWindow", + "NSView", + "NSResponder", + "NSGraphics", + "NSPasteboard", + "NSMenuItemBadge", + "NSVisualEffectView", + "NSWorkspace", + "NSAccessibility", +] } +objc2-quartz-core = { version = "0.3.2", features = [ + "CALayer", + "CATransaction", + "objc2-core-foundation", +] } +objc2-core-graphics = { version = "0.3.2", features = ["CGColor"] } # NSRect/NSPoint/NSSize for `setFrame:display:`. Already in the tree as # objc2-app-kit's own dependency, so this adds nothing to the build. -objc2-foundation = { version = "0.2.2", features = ["NSNotification", "NSOperation", "NSString", "block2"] } -block2 = "0.5.1" +objc2-foundation = { version = "0.3.2", features = [ + "NSNotification", + "NSOperation", + "NSString", + "block2", +] } +block2 = "0.6" raw-window-handle = "0.6" core-foundation = "0.10" core-video = { version = "0.5.2", features = ["metal"] } diff --git a/apps/desktop-gpui/README.md b/apps/desktop-gpui/README.md index fc9fadcad4f..6830cfa41ff 100644 --- a/apps/desktop-gpui/README.md +++ b/apps/desktop-gpui/README.md @@ -636,8 +636,8 @@ Sizes are the Tauri app's, from `apps/desktop/src-tauri/src/windows.rs`. | Teleprompter | 560×320 | **Done, with deviations** — resizable to the 420×220 floor, level 101 on all Spaces, the `"teleprompter"` material at radius 22, traffic lights at (14, 11) — the Tauri `(14, 14)` is a `position_window_controls` inset, not a top-left, auto-scroll from the ported `teleprompter-utils` maths, the full footer and settings popover, native window opacity, the `teleprompter` store section, capture exclusion + content protection. The script editor is a real multi-line field and Mirror is inert — see below | | Editor | 1275×800 | **E1–E5 done — window, shell, playback, timeline, editing, config sidebar.** The real 1275×800 window with the traffic lights at (20, 20) — the Tauri `(20, 32)` is a `position_window_controls` inset, not a top-left, the header, the letterboxed player, the 260px timeline strip and the 416px config sidebar with its six-tab rail; a real project through `EditorInstance`; play/pause (button + Space), a 60fps live playhead and clock, real audio, click/drag seeking, end-of-media stop; the timeline at 1:1 — all nine track types from the project's own config, waveforms, the ruler's resolution ladder, minimap, edge fade, hover ghost, zoom (keys, buttons, slider, wheel, pinch) and pan; and **timeline editing**: selection (single, ⌘-multi, ⇧-range, ⌘A), trim, move, split (S + C, with snapping), zoom-segment create/resize/move, delete, undo/redo and the debounced write back to `project-config.json`. and **the config sidebar in full**: the six-tab rail, the scroll body, selection routing, all six tabs at 1:1, the shared colour-correction section on both its targets, and the eight per-segment panels with multi-select where the source has it. Crop and export are the remaining units | | Screenshot editor | 1240×800 (min 800×600) | **Done, with deviations** — the real window (native traffic lights, resizable to the 800×600 floor), one editor per bundle keyed like the video editor's registry, opened from capture, Recents, the settings Screenshots page, the tray's Previous and image import. The still renders through the same GPU path the Tauri instance uses (`FrameRenderer::render_immediate`, `preserve_screen_alpha`, no camera) on a config watch, painted with `paint_image` instead of shipped over the frame websocket; edits publish to the renderer live, ride the 1000ms debounced `project-config.json` write, and walk a real undo history (⌘Z / ⌘⇧Z / ⌘Y, a drag's sixty intermediate values collapsed into one entry). Styling: background source tabs (color swatches, gradient presets, wallpaper grid with theme tabs and cached thumbnails, image picker), hex fields, blur/padding/rounding/shadow sliders at the popovers' ranges plus the advanced-shadow collapsible, squircle/rounded corner style, border toggle + width/color/opacity, aspect-ratio menu. Annotations: the full engine — all seven tools with their single-key shortcuts, the frame-space geometry of `layout.ts`/`arrow.ts` verbatim, selection handles, the per-type config bar (stroke/fill swatches with the colour popover, width/opacity/mask-level/text-size sliders), the layers panel with drag reorder, blur/pixelate masks resampled live off the rendered frame, inline text editing, ⌘C/⌘V duplicate, delete. Crop dialog: the `editor_crop` engine wholesale over `original.png` (`screenshot_crop.rs`) — Size/Position number boxes, the round ratio button and its options menu (right-click opens the same one at the cursor) with the snap-to-ratio toggle persisted in the shared store, Full/Reset, arrow-key nudges, one history entry on Save, Escape/backdrop cancel. Export: Copy/Save/Share all bake the annotations in (`screenshot_export.rs` — the `renderScreenshotExportCanvas` pass: masks re-filtered at export scale from an unmodified copy, shapes via tiny-skia, text via cosmic-text at the preview's Helvetica baseline, the output expanded to the union of image and annotation boxes with the source's white-fill rules); Copy composites over white and lands PNG on the clipboard, Save writes PNG, Share fingerprints the config (`sha256` over recursively key-sorted JSON), short-circuits to the stored link on a hash match, and otherwise uploads JPEG-0.9 (PNG when transparency is actually needed) through create-or-get + presigned PUT — reusing `sharing.id` so the link survives edits — then persists `SharingMeta { id, link, content_hash }`, with Tauri's exact auth gates and toast ladder; reveal, delete behind a native confirm. Deviations: no OCR text-selection overlay (Vision OCR feeding an invisible DOM-style selectable text layer has no gpui equivalent yet; the layer is invisible in the Tauri editor too, so zero visual impact); the colour swatches are inert — no native colour panel is wired to this window, the hex fields are the input; the skeleton's Cap logo is drawn still (the app ships no bare glyph to spin, only the full lockup); the crop options menu has no separator row (`ui::Menu` has none, so its two groups sit adjacent); no `PendingScreenshots` in-memory hand-off (the PNG is on disk before the editor opens) | -| Tray (status item) | menu bar | **Done, with deviations** — a native `NSStatusItem` (no gpui API exists for one; built on AppKit with the color-panel channel discipline), menu byte-identical to `build_tray_menu`: Open Main Window, Record Display/Window/Area (screenshot-mode relabels), Select Mode ▸ with the ✓, Previous ▸ with 32px cover-cropped thumbnails and the 🎬⚡📷 prefixes, View all recordings / screenshots, Settings, version row, Quit Cap. The icon follows the mode and becomes the stop icon while recording, when clicking the item stops the capture (the menu is detached so the button's action can fire). Select Mode writes the same `recording_settings.mode` store key the Tauri app reads. Take a Screenshot captures the display under the cursor and Import Media… routes video/image imports; a Previous screenshot opens the screenshot editor. Deviations: Upload Logs renders disabled (no log-upload infrastructure yet) | -| App menu + shortcuts | menu bar | **Done** — the `build_macos_app_menu` structure (Cap / File / Edit / View / Window / Help) via `cx.set_menus`, with ⌘W closing the key window through each window's own close path (main hides, Tauri's `CloseRequested` arm transcribed: camera bubble closed, overlays closed, feeds released when idle), ⌘M / Zoom / ⌃⌘F on the key window, ⌘H / ⌥⌘H, ⌘Q, and Edit items dispatching the text-input actions. Dock-icon policy (Regular ↔ Accessory per `hideDockIcon` + visible windows) synced on every window show/hide seam | +| Tray (status item) | menu bar | **Done, with deviations** — a native `NSStatusItem` (no gpui API exists for one; built on AppKit with the color-panel channel discipline), menu in `build_menu`'s own grouping: Open Main Window; the current mode as the group label (an `NSMenu` section header reading e.g. `Studio` plus a palette item with the mode icons on macOS 14+ (the header follows the palette's highlight in italic), a ✓-ticked `Recording Mode ▸` submenu on older macOS and on Windows); Record Display/Window/Area + Take a Screenshot (screenshot-mode relabels); Previous ▸ (clock glyph) with 32px cover-cropped thumbnails and the 🎬⚡📷 prefixes, View all recordings / screenshots; Import Media…, Settings (⌘,), Upload Logs; Quit Cap with the version as a trailing section header on macOS and as a disabled row above Quit elsewhere. The four record rows, Previous ▸, Import Media… and Settings each carry an SF Symbol (`display`, `interface.window` (`macwindow` below macOS 26), `rectangle.dashed`, `camera.on.rectangle`, `clock.arrow.circlepath`, `square.and.arrow.down`, `gear`) drawn straight from the system catalog on macOS 11+ with no fallback — an unresolvable name leaves the row bare — while Windows draws the cached 32×32 raster of the same SVG their in-app buttons use (`ROW_ICON_SOURCES` / `svg_icon_png`); the macOS glyphs are template images the menu tints, and Previous's own rows keep their photo thumbnails; the chrome rows (Open Main Window, View all …, Upload Logs, Quit) stay bare. The icon follows the mode and becomes the stop icon while recording, when clicking the item stops the capture (the menu is detached so the button's action can fire). The mode picker writes the same `recording_settings.mode` store key the Tauri app reads. Take a Screenshot captures the display under the cursor and Import Media… routes video/image imports; a Previous screenshot opens the screenshot editor. Deviations: Upload Logs renders disabled (no log-upload infrastructure yet) | +| App menu + shortcuts | menu bar | **Done** — the `build_macos_app_menu` structure (Cap / File / Edit / View / Window / Help) via `cx.set_menus`, with ⌘W closing the key window through each window's own close path (main hides, Tauri's `CloseRequested` arm transcribed: camera bubble closed, overlays closed, feeds released when idle), ⌘M / Zoom / ⌃⌘F on the key window, ⌘H / ⌥⌘H, ⌘Q, and Edit items dispatching the text-input actions. The Cap menu carries About (AppKit's own `orderFrontStandardAboutPanel:`, patched in by row position, so there is no gpui About action), Check for Update and Preferences… with ⌘,. The update row resolves to three states — `Check for Update`, `Download update…` and `Restart to update…`, with `Checking for Update…` greyed out while a check runs — and is rewritten in place in the live `NSMenu` (`platform::set_update_menu_item`, by row position, `setBadge:` on macOS 14+) so an open menu survives a status change; the version, and the download percentage on macOS, ride the badge, and the menu bar is only reinstalled on the other platforms when the state itself changes. File carries Open File… (an existing recording or screenshot, routed through the tray's `open_previous_item`), Import Media… (the same picker the tray runs), the AppKit-managed Open Recent list — every editor open registers its bundle with `add_recent_document`, and a click comes back through `application:openURLs:` → `deeplink` → `open_editor` — and Close Window. Help carries the Changelog and the cap.so/help links. Dock-icon policy (Regular ↔ Accessory per `hideDockIcon` + visible windows) synced on every window show/hide seam | ## Recording diff --git a/apps/desktop-gpui/locales/de.toml b/apps/desktop-gpui/locales/de.toml index 6a03591f376..153717efb67 100644 --- a/apps/desktop-gpui/locales/de.toml +++ b/apps/desktop-gpui/locales/de.toml @@ -24,9 +24,17 @@ quit = "Cap beenden" services = "Dienste" settings = "Einstellungen…" +[menu.about] +licensed = "Lizenziert unter AGPL-3.0" +website = "Website" +commit = "Commit" + [menu.file] close_window = "Fenster schließen" title = "Ablage" +open_file = "Datei öffnen…" +open_recent = "Zuletzt geöffnet" +import_media = "Medien importieren…" [menu.edit] copy = "Kopieren" @@ -49,12 +57,22 @@ zoom = "Zoomen" [menu.help] title = "Hilfe" +feedback = "Feedback geben…" +changelog = "Änderungsprotokoll" +dashboard = "Dashboard" +documentation = "Dokumentation" +faq = "FAQ" +self_hosting = "Selbsthosting" +help_center = "Hilfe-Center" +system_status = "Systemstatus" +discord = "Discord-Community beitreten" [tray] import_media = "Medien importieren..." no_recent_items = "Noch keine Einträge" open_main_window = "Hauptfenster öffnen" previous = "Zuletzt erstellt" +recording_mode = "Aufnahmemodus" quit = "Cap beenden" record_area = "Bereich aufnehmen" record_display = "Bildschirm aufnehmen" @@ -239,6 +257,698 @@ title = "Datenschutz" [settings.general.privacy.telemetry] description = "Cap nutzt anonyme Telemetrie, um die Zuverlässigkeit zu verbessern und Fehler zu beheben. Wir erfassen niemals Aufnahmeinhalte, Fenstertitel, Dateipfade oder persönliche Daten." label = "Anonyme Telemetrie teilen" +[settings.prompt] +ok = "OK" + +[settings.sidebar] +account = "Konto" +cancel_sign_in = "Anmeldung abbrechen" +check_for_updates = "Nach Updates suchen" +checking_for_updates = "Suche nach Updates …" +click_to_sign_in = "Zum Anmelden klicken" +downloading = "v{version} wird geladen …" +downloading_progress = "v{version} wird geladen … {percent} %" +installing = "v{version} wird installiert …" +previous_versions = "Frühere Versionen anzeigen" +restart_to_update = "Neu starten, um auf v{version} zu aktualisieren" +sign_in = "Anmelden" +sign_out = "Abmelden" +signed_in = "Angemeldet" + +[settings.recordings] +corrupted_body = "Die Aufnahme ist fehlgeschlagen, daher kann diese Datei im Editor Probleme machen! Wenn Sie die Datei nicht wiederherstellen können, wenden Sie sich bitte an den Support!" +corrupted_title = "Die Aufnahme ist möglicherweise beschädigt" +delete = "Löschen" +description = "Verwalten Sie Ihre Aufnahmen und führen Sie Aktionen aus." +edit = "Bearbeiten" +empty_library = "Keine Aufnahmen gefunden" +failed = "Aufnahme fehlgeschlagen" +import = "Importieren" +in_progress = "Aufnahme läuft" +load_more = "Mehr laden" +open_bundle = "Aufnahmepaket öffnen" +open_link = "Link öffnen" +retry_upload = "Hochladen wiederholen" +show_all = "Alle anzeigen" +title = "Aufnahmen" + +[settings.recordings.empty] +all = "Keine Aufnahmen" +all_matching = "Keine passenden Aufnahmen" +instant = "Keine Instant-Aufnahmen" +instant_matching = "Keine passenden Instant-Aufnahmen" +studio = "Keine Studio-Aufnahmen" +studio_matching = "Keine passenden Studio-Aufnahmen" + +[settings.screenshots] +copy = "In die Zwischenablage kopieren" +delete = "Löschen" +description = "Verwalten Sie Ihre Bildschirmfotos und führen Sie Aktionen aus." +empty = "Keine Bildschirmfotos" +empty_library = "Keine Bildschirmfotos gefunden" +empty_matching = "Keine passenden Bildschirmfotos" +import = "Bild importieren" +load_more = "Mehr laden" +open_folder = "Ordner öffnen" +save_as = "Speichern unter" +title = "Bildschirmfotos" + +[settings.quality] +save_failed = "Ihre Aufnahmeeinstellungen konnten nicht gespeichert werden. Bitte versuchen Sie es erneut." + +[settings.quality.studio] +balanced = "Ausgewogen" +balanced_description = "Scharfe Details bei vernünftiger Dateigröße. Richtig für die meisten Aufnahmen." +compatibility = "Kleinere Dateien" +compatibility_description = "Spart Speicherplatz. Feine Details können weicher werden, vor allem bei der Kamera." +description = "Werden zum Bearbeiten auf Ihrem Computer gespeichert. Die Exportgröße wählen Sie später im Editor." +recommended = "Empfohlen" +title = "Studio-Aufnahmen" +ultra = "Maximale Details" +ultra_description = "Behält die meisten Details für aufwendige Bearbeitung. Belegt den meisten Speicherplatz." + +[settings.quality.instant] +description = "Wird während der Aufnahme hochgeladen, sodass der Link bereit ist, sobald Sie stoppen. Für die beste Qualität nutzen Sie den Studio-Modus: Er nimmt in der höchsten Qualität auf, die Ihr Bildschirm erlaubt, und Sie können bis 4K exportieren." +free_plan = "720p im kostenlosen Tarif. Cap Pro nimmt bis zu 4K auf." +max_resolution = "Maximale Auflösung" +res_1080p = "Scharfer Text bei praktischer Upload-Größe." +res_1440p = "Mehr Details für große, hochauflösende Bildschirme." +res_4k = "Die meisten Details. Am besten mit schneller Verbindung." +res_720p = "Kleinste Uploads, daher sind Links am schnellsten bereit." +title = "Instant-Aufnahmen" + +[settings.quality.sharing] +auto_open = "Links zum Teilen automatisch öffnen" +auto_open_description = "Öffnet den Link in Ihrem Browser, wenn ein Upload abgeschlossen ist." +title = "Teilen" + +[settings.options.studio_quality] +balanced = "Ausgewogen" +compatibility = "Kompatibilität" +ultra = "Ultra" + +[settings.options.main_window_start] +close = "Schließen" +minimise = "Minimieren" + +[settings.options.post_studio] +do_nothing = "Nichts tun" +open_editor = "Editor öffnen" +show_cap = "Cap anzeigen" + +[settings.options.post_screenshot] +copy = "In Zwischenablage kopieren" +copy_and_open_editor = "Kopieren, Editor öffnen" +do_nothing = "Nichts tun" +open_editor = "Editor öffnen" + +[settings.options.post_deletion] +do_nothing = "Nichts tun" +reopen = "Rekorder wieder öffnen" + +[settings.options.preview_quality] +full = "Voll" +half = "Halb" +quarter = "Viertel" + +[settings.shortcuts] +blocked_while_recording = "Tastenkürzel können nicht geändert werden, solange diese Aufnahme ihre Stopp-Taste belegt. Stoppen Sie zuerst die Aufnahme." +needs_modifier_macos = "Verwenden Sie ⌘, ⌃ oder ⌥, damit das Tastenkürzel beim Tippen nicht stört." +needs_modifier_other = "Verwenden Sie Strg, Alt oder Win, damit das Tastenkürzel beim Tippen nicht stört." +record_shortcut = "Kürzel festlegen" +recording_description = "Starten Sie eine Aufnahme, ohne Cap vorher zu öffnen." +recording_hint = "Drücken Sie die gewünschten Tasten. Esc bricht ab, Entf entfernt das Kürzel." +recording_title = "Aufnahme" +saved = "Gespeichert. Drücken Sie {keys} in einer beliebigen App, um {effect}." +saved_moved = "Gespeichert. Drücken Sie {keys} in einer beliebigen App, um {effect}. Von „{shortcut}“ entfernt." +screenshots_description = "Tastenkürzel funktionieren in jeder App. Klicken Sie auf eines, um es zu ändern." +screenshots_title = "Bildschirmfotos" +type_shortcut = "Kürzel eingeben …" +unavailable = "Eine andere App oder das System verwendet dieses Tastenkürzel bereits. Klicken Sie, um ein anderes zu wählen." +unsupported_key = "Diese Taste kann nicht in einem Tastenkürzel verwendet werden. Versuchen Sie einen Buchstaben, eine Zahl oder eine F-Taste." +while_recording_title = "Während der Aufnahme" + +[settings.shortcuts.after_screenshot] +hint = "Für jedes Bildschirmfoto, per Tastenkürzel oder aus Cap" +title = "Nach einem Bildschirmfoto" + +[settings.shortcuts.after_studio] +hint = "Beim Stoppen" +title = "Nach einer Studio-Aufnahme" + +[settings.shortcuts.after_instant] +hint = "Der Link zum Teilen wird immer kopiert" +title = "Nach einer Instant-Aufnahme" + +[settings.shortcuts.outcome] +instant_copy_link = "Nur Link kopieren" +instant_copy_link_hint = "Nichts wird geöffnet" +instant_open_link = "Link öffnen" +instant_open_link_hint = "In Ihrem Browser" +saved_to_library = "In der Bibliothek gespeichert" +screenshot_copy = "Kopieren" +screenshot_copy_and_edit = "Kopieren und bearbeiten" +screenshot_copy_and_edit_hint = "Beides zugleich" +screenshot_copy_hint = "Überall einfügen" +screenshot_do_nothing = "Nichts tun" +screenshot_open_editor = "Editor öffnen" +screenshot_open_editor_hint = "Erst beschriften" +studio_do_nothing = "Nichts tun" +studio_open_editor = "Editor öffnen" +studio_open_editor_hint = "Sofort bearbeiten" +studio_show_cap = "Cap anzeigen" +studio_show_cap_hint = "Zurück zum Rekorder" + +[settings.shortcuts.label] +cycle_mode = "Aufnahmemodus wechseln" +fallback = "Tastenkürzel" +open_picker = "Aufnahmeauswahl öffnen" +pause = "Pausieren oder fortsetzen" +record_area = "Bereich aufnehmen" +record_display = "Bildschirm aufnehmen" +record_window = "Fenster aufnehmen" +restart = "Aufnahme neu starten" +screenshot_area = "Bereich fotografieren" +screenshot_display = "Bildschirm fotografieren" +screenshot_window = "Fenster fotografieren" +start_instant = "Instant-Aufnahme starten" +start_studio = "Studio-Aufnahme starten" +stop = "Aufnahme stoppen" + +[settings.shortcuts.effect] +cycle_mode = "zwischen Studio-, Instant- und Bildschirmfoto-Modus zu wechseln" +instant_copy = "den ausgewählten Bildschirm oder das ausgewählte Fenster im Instant-Modus aufzunehmen und beim Stoppen den Link zum Teilen zu kopieren" +instant_copy_and_open = "den ausgewählten Bildschirm oder das ausgewählte Fenster im Instant-Modus aufzunehmen und beim Stoppen den Link zum Teilen zu kopieren und zu öffnen" +open_picker = "Cap zu öffnen und auszuwählen, was Sie aufnehmen möchten" +pause = "die laufende Aufnahme zu pausieren oder fortzusetzen" +picker_area_instant = "einen Bereich auszuwählen und ihn im Instant-Modus aufzunehmen" +picker_area_screenshot = "einen Bereich auszuwählen und ein Bildschirmfoto davon zu machen" +picker_area_studio = "einen Bereich auszuwählen und ihn im Studio-Modus aufzunehmen" +picker_display_instant = "einen Bildschirm zu wählen und ihn im Instant-Modus aufzunehmen" +picker_display_screenshot = "einen Bildschirm zu wählen und ein Bildschirmfoto davon zu machen" +picker_display_studio = "einen Bildschirm zu wählen und ihn im Studio-Modus aufzunehmen" +picker_window_instant = "ein Fenster zu wählen und es im Instant-Modus aufzunehmen" +picker_window_screenshot = "ein Fenster zu wählen und ein Bildschirmfoto davon zu machen" +picker_window_studio = "ein Fenster zu wählen und es im Studio-Modus aufzunehmen" +restart = "die aktuelle Aufnahme zu verwerfen und neu zu beginnen" +screenshot_area_copy = "einen Bereich auszuwählen und das Bildschirmfoto in die Zwischenablage zu kopieren" +screenshot_area_copy_and_open_editor = "einen Bereich auszuwählen, das Bildschirmfoto zu kopieren und den Editor zu öffnen" +screenshot_area_open_editor = "einen Bereich auszuwählen und das Bildschirmfoto im Editor zu öffnen" +screenshot_area_save = "einen Bereich auszuwählen und das Bildschirmfoto in Ihrer Bibliothek zu speichern, ohne etwas zu öffnen" +screenshot_display_copy = "den Bildschirm unter Ihrem Zeiger aufzunehmen und das Bildschirmfoto in die Zwischenablage zu kopieren" +screenshot_display_copy_and_open_editor = "den Bildschirm unter Ihrem Zeiger aufzunehmen, das Bildschirmfoto zu kopieren und den Editor zu öffnen" +screenshot_display_open_editor = "den Bildschirm unter Ihrem Zeiger aufzunehmen und das Bildschirmfoto im Editor zu öffnen" +screenshot_display_save = "den Bildschirm unter Ihrem Zeiger aufzunehmen und das Bildschirmfoto in Ihrer Bibliothek zu speichern, ohne etwas zu öffnen" +screenshot_window_copy = "das Fenster unter Ihrem Zeiger aufzunehmen und das Bildschirmfoto in die Zwischenablage zu kopieren" +screenshot_window_copy_and_open_editor = "das Fenster unter Ihrem Zeiger aufzunehmen, das Bildschirmfoto zu kopieren und den Editor zu öffnen" +screenshot_window_open_editor = "das Fenster unter Ihrem Zeiger aufzunehmen und das Bildschirmfoto im Editor zu öffnen" +screenshot_window_save = "das Fenster unter Ihrem Zeiger aufzunehmen und das Bildschirmfoto in Ihrer Bibliothek zu speichern, ohne etwas zu öffnen" +stop = "die laufende Aufnahme zu stoppen" +studio = "den ausgewählten Bildschirm oder das ausgewählte Fenster im Studio-Modus aufzunehmen" +studio_open_editor = "den ausgewählten Bildschirm oder das ausgewählte Fenster im Studio-Modus aufzunehmen und beim Stoppen den Editor zu öffnen" + +[settings.shortcuts.sentence] +cycle_mode = "Zwischen Studio-, Instant- und Bildschirmfoto-Modus wechseln." +instant_copy = "Ausgewählten Bildschirm oder ausgewähltes Fenster im Instant-Modus aufnehmen und beim Stoppen den Link zum Teilen kopieren." +instant_copy_and_open = "Ausgewählten Bildschirm oder ausgewähltes Fenster im Instant-Modus aufnehmen und beim Stoppen den Link zum Teilen kopieren und öffnen." +open_picker = "Cap öffnen und auswählen, was Sie aufnehmen möchten." +pause = "Die laufende Aufnahme pausieren oder fortsetzen." +picker_area_instant = "Bereich auswählen und im Instant-Modus aufnehmen." +picker_area_screenshot = "Bereich auswählen und ein Bildschirmfoto davon machen." +picker_area_studio = "Bereich auswählen und im Studio-Modus aufnehmen." +picker_display_instant = "Bildschirm wählen und im Instant-Modus aufnehmen." +picker_display_screenshot = "Bildschirm wählen und ein Bildschirmfoto davon machen." +picker_display_studio = "Bildschirm wählen und im Studio-Modus aufnehmen." +picker_window_instant = "Fenster wählen und im Instant-Modus aufnehmen." +picker_window_screenshot = "Fenster wählen und ein Bildschirmfoto davon machen." +picker_window_studio = "Fenster wählen und im Studio-Modus aufnehmen." +restart = "Die aktuelle Aufnahme verwerfen und neu beginnen." +screenshot_area_copy = "Bereich auswählen und das Bildschirmfoto in die Zwischenablage kopieren." +screenshot_area_copy_and_open_editor = "Bereich auswählen, das Bildschirmfoto kopieren und den Editor öffnen." +screenshot_area_open_editor = "Bereich auswählen und das Bildschirmfoto im Editor öffnen." +screenshot_area_save = "Bereich auswählen und das Bildschirmfoto in Ihrer Bibliothek speichern, ohne etwas zu öffnen." +screenshot_display_copy = "Bildschirm unter dem Zeiger aufnehmen und das Bildschirmfoto in die Zwischenablage kopieren." +screenshot_display_copy_and_open_editor = "Bildschirm unter dem Zeiger aufnehmen, das Bildschirmfoto kopieren und den Editor öffnen." +screenshot_display_open_editor = "Bildschirm unter dem Zeiger aufnehmen und das Bildschirmfoto im Editor öffnen." +screenshot_display_save = "Bildschirm unter dem Zeiger aufnehmen und das Bildschirmfoto in Ihrer Bibliothek speichern, ohne etwas zu öffnen." +screenshot_window_copy = "Fenster unter dem Zeiger aufnehmen und das Bildschirmfoto in die Zwischenablage kopieren." +screenshot_window_copy_and_open_editor = "Fenster unter dem Zeiger aufnehmen, das Bildschirmfoto kopieren und den Editor öffnen." +screenshot_window_open_editor = "Fenster unter dem Zeiger aufnehmen und das Bildschirmfoto im Editor öffnen." +screenshot_window_save = "Fenster unter dem Zeiger aufnehmen und das Bildschirmfoto in Ihrer Bibliothek speichern, ohne etwas zu öffnen." +stop = "Die laufende Aufnahme stoppen." +studio = "Ausgewählten Bildschirm oder ausgewähltes Fenster im Studio-Modus aufnehmen." +studio_open_editor = "Ausgewählten Bildschirm oder ausgewähltes Fenster im Studio-Modus aufnehmen und beim Stoppen den Editor öffnen." + +[settings.cli] +command = "Befehl" +copy = "Kopieren" +description = "Installieren Sie den Cap-Befehl für Terminals, Agenten, Skripte und lokale Automatisierung." +install = "CLI installieren" +installed = "Installiert" +installing = "Wird installiert ..." +installs_command = "Die Desktop-App installiert einen lokalen\u00A0{command}\u00A0Befehl, der auf die mitgelieferte CLI verweist." +load_failed = "CLI-Status konnte nicht geladen werden: {error}" +not_installed = "Nicht installiert" +path_configured = "cap wurde zu Ihrem PATH hinzugefügt. Starten Sie Ihr Terminal neu, um es zu verwenden, oder führen Sie jetzt Folgendes aus:" +path_missing = "Fügen Sie {path} zu Ihrem PATH hinzu, um cap in einem neuen Terminal zu verwenden." +remove = "Entfernen" +removing = "Wird entfernt ..." +repair = "Reparieren" +repairing = "Wird repariert ..." +retry = "Erneut versuchen" +target = "Ziel" +title = "Befehlszeile" + +[settings.transcription] +active_title = "Aktive Hinweise" +applied_note = "Diese Hinweise werden angewendet, wenn Sie im Editor Untertitel erstellen." +clear = "Leeren" +count = { one = "{count} Eintrag", other = "{count} Einträge" } +description = "Fügen Sie Namen, Schreibweisen, Domains und Vorgaben zur Groß- und Kleinschreibung hinzu, die bei der Untertitelerstellung berücksichtigt werden sollen." +placeholder = "Begriff hinzufügen" +saved = "Gespeichert" +saving = "Wird gespeichert ..." +terms_description = "Fügen Sie jeweils einen Begriff hinzu, um Tipp- und Formatierungsfehler zu vermeiden." +terms_title = "Gemerkte Begriffe" +title = "Transkription" + +[settings.experimental] +developer = "Entwickler" +developer_description = "Wird nur in Entwicklungs-Builds angezeigt." +muxer = "Muxer in separatem Prozess" +muxer_description = "Führt den Muxer für fragmentiertes MP4 in einem isolierten Unterprozess aus, damit Muxer-Abstürze Ihre Aufnahme nicht beenden. Erfordert das mitgelieferte Programm cap-muxer." +native_app = "Native App verwenden" +native_app_description = "Ausschalten, um diese App zu schließen und die klassische Tauri-App in der Entwicklungsumgebung wieder zu öffnen. Aufnahmen und Einstellungen werden geteilt." +reliability = "Zuverlässigkeit" + +[settings.experimental.switch] +busy = "Schließen Sie Ihre Aufnahme, den Export, Upload, Import oder die Transkription ab, bevor Sie zur klassischen App wechseln." +check_failed = "Es konnte nicht geprüft werden, ob die klassische Cap-App geöffnet wurde: {error}" +data_stays = "Ihre Aufnahmen und Einstellungen bleiben genau dort, wo sie sind." +failed = "Die klassische Cap-App konnte nicht geöffnet werden." +not_found = "Die klassische Cap-App, zu der gewechselt werden soll, wurde nicht gefunden." +open_failed = "Die klassische Cap-App konnte nicht geöffnet werden: {error}" +opening = "Die klassische Cap-App wird geöffnet" +opening_detail = "Dieses Fenster wird geschlossen, sobald die klassische Cap-App bereit ist." +save_failed = "Ihre App-Einstellung konnte nicht gespeichert werden. Cap bleibt geöffnet." +switching = "Zurück zur klassischen Cap-App." +timed_out = "Die klassische App wurde nicht geöffnet. Prüfen Sie das Entwicklungsterminal auf Build-Fehler und versuchen Sie es dann erneut." + +[settings.feedback] +app_version = "App-Version" +capture_support = "Aufnahmeunterstützung" +capture_supported = "Bildschirmaufnahme: Unterstützt" +capture_unsupported = "Bildschirmaufnahme: Nicht unterstützt" +community_description = "Haben Sie Fragen, möchten Sie Ideen teilen oder einfach dabei sein? Treten Sie der Cap-Community auf Discord bei." +community_title = "Der Community beitreten" +debug_description = "Laden Sie Ihre Protokolle hoch, damit wir Probleme mit Cap untersuchen können. Es sind keine persönlichen Daten enthalten." +debug_title = "Debug-Informationen" +description = "Helfen Sie uns, Cap zu verbessern, indem Sie Feedback senden oder Fehler melden. Wir kümmern uns sofort darum." +join_discord = "Discord beitreten" +operating_system = "Betriebssystem" +placeholder = "Sagen Sie uns, was Sie von Cap halten ..." +submit = "Feedback senden" +submit_failed = "Feedback konnte nicht gesendet werden" +submitting = "Wird gesendet ..." +system_loading = "Systeminformationen werden geladen ..." +system_title = "Systeminformationen" +thanks = "Vielen Dank für Ihr Feedback!" +title = "Feedback" +upload_interrupted = "Der Upload wurde unterbrochen" +upload_logs = "Protokolle hochladen" +upload_sent = "Gesendet. Vielen Dank, das hilft uns, das Problem zu finden." +uploading = "Wird hochgeladen ..." + +[settings.feedback.diagnostic] +confirm_body = "Cap belegt Ihren Bildschirm mit einem blinkenden Muster und spielt pro Durchlauf etwa {seconds} Sekunden lang laute Pieptöne ab. Nehmen Sie die Kopfhörer ab, lassen Sie die Lautstärke hörbar und benutzen Sie den Computer erst wieder, wenn der Test fertig ist." +confirm_title = "Diagnose starten?" +description = "Nimmt ein kurzes blinkendes und piepsendes Testmuster auf, misst, wie weit Bild und Ton auseinanderdriften, und erfasst Ihre Bildschirme, Kameras, Mikrofone, den Speicherplatz und die Aufnahmeeinstellungen. Es wird nichts gesendet, bis Sie sich dafür entscheiden." +mic_description = "Nimmt auch Ihr Mikrofon auf und prüft, ob es die Pieptöne rechtzeitig hört. Benötigt Lautsprecher, die das Mikrofon tatsächlich hören kann." +mic_label = "Mikrofon testen" +mode_description = "Welcher Ablauf getestet wird. Beide führt erst den Studio-Teil und dann den Instant-Teil aus." +mode_label = "Aufnahmemodus" +no_cli = "Das Cap-Befehlszeilenprogramm wurde neben diesem Build nicht gefunden, daher umfasst der Bericht nur Ihre Umgebung." +no_cli_sync = "Das Cap-Befehlszeilenprogramm wurde neben diesem Build nicht gefunden, daher konnte der Synchronisationstest nicht laufen." +run = "Diagnose starten" +run_again = "Erneut ausführen" +send = "An Cap senden" +sending = "Wird gesendet ..." +show_file = "Datei anzeigen" +stop_recording_first = "Stoppen Sie die laufende Aufnahme, bevor Sie eine Diagnose starten." +title = "Diagnosebericht" +try_again = "Erneut versuchen" +warning_body = "Ein Vollbildfenster blinkt schwarz-weiß, während 1-kHz-Pieptöne über Ihre Lautsprecher laufen, und Cap nimmt das auf und misst, wie weit Bild und Ton auseinandergedriftet sind. Benutzen Sie den Computer während des Tests nicht. Beide Modi zusammen dauern ein paar Minuten." +warning_title = "Der Test belegt Ihren Bildschirm und macht Geräusche." + +[settings.feedback.diagnostic.verdict] +environment_only = "NUR UMGEBUNG" +fail = "FEHLGESCHLAGEN" +inconclusive = "NICHT EINDEUTIG" +pass = "BESTANDEN" +warn = "WARNUNG" + +[settings.sign_in_required] +body = "Bitte melden Sie sich an, um fortzufahren. Alternativ schreiben Sie an hello@cap.so oder treten Sie unserem Discord unter cap.link/discord bei" +title = "Anmeldung erforderlich" + +[settings.changelog] +fetch_failed = "Änderungsprotokoll konnte nicht abgerufen werden" +loading = "Änderungsprotokoll wird geladen ..." +new = "NEU" +version = "Version {version} - {date}" + +[settings.license] +activate = "Lizenz aktivieren" +activating = "Wird aktiviert ..." +active_title = "Desktop-Lizenz" +checking = "Wird geprüft …" +deactivate = "Lizenz deaktivieren" +description = "Ihr Cap-Tarif und Ihre Desktop-Lizenz an einem Ort." +desktop_description = "Ihre Desktop-Lizenz deckt kommerzielles Aufnehmen und Bearbeiten ab. Cap Pro bietet zusätzlich Teilen über die Cloud und Funktionen für die Zusammenarbeit." +desktop_name = "Desktop-Lizenz" +expires = "Läuft ab" +explore_description = "Vergleichen Sie aktuelle Preise und alle enthaltenen Leistungen auf unserer Website." +explore_desktop = "Desktop-Lizenz · Kommerzielle Nutzung von Desktop-Rekorder und -Editor." +explore_pro = "Cap Pro · Eine Desktop-Lizenz plus Teilen über die Cloud, KI-Funktionen und Zusammenarbeit." +explore_title = "Tarife entdecken" +free_description = "Lokal aufnehmen und bearbeiten für die private Nutzung. Wählen Sie einen kostenpflichtigen Tarif für die kommerzielle Nutzung oder mehr Cloud-Funktionen." +have_key = "Haben Sie einen Lizenzschlüssel?" +key = "Lizenzschlüssel" +no_instance = "Keine Instanz-ID gefunden" +opens_pricing = "Öffnet cap.so/pricing in Ihrem Browser." +placeholder = "Lizenzschlüssel" +plan_title = "Ihr Tarif" +pro_description = "Ihr Konto umfasst Teilen über die Cloud, Pro-Funktionen und eine Desktop-Lizenz für die kommerzielle Nutzung." +refresh = "Tarif aktualisieren" +refresh_failed = "Ihr Tarif konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut." +save_failed = "Die Lizenz konnte nicht gespeichert werden" +sign_in_hint = "Sie haben bereits Cap Pro? Melden Sie sich über die Seitenleiste mit Ihrem Konto an." +title = "Tarif & Lizenz" +view_pricing = "Tarife & Preise ansehen ↗" + +[settings.integrations] +configure = "Einrichten" +description = "Richten Sie Integrationen ein, um den Funktionsumfang von Cap zu erweitern und Dienste von Drittanbietern anzubinden." +google_drive_description = "Verbinden Sie Google Drive für neue Uploads mit Links zum Teilen. Cap speichert neue Videos in einem privaten Cap-Ordner in Ihrem Drive und stellt sie nach den üblichen Zugriffsprüfungen weiterhin über Cap bereit." +managed = "Von Ihrer Organisation verwaltet" +managed_by = "Von Ihrer Organisation verwaltet: {organization}" +request_timed_out = "Die Anfrage hat zu lange gedauert" +s3_description = "Verbinden Sie Ihren eigenen S3-Bucket für die volle Kontrolle über Ihre Datenspeicherung. Alle neuen Uploads mit Links zum Teilen werden automatisch in Ihren konfigurierten S3-Bucket hochgeladen, sodass Ihre Inhalte vollständig Ihnen gehören und unter Ihrer Kontrolle bleiben. Ideal für Organisationen, die Datenhoheit und eigene Speicherrichtlinien benötigen." +s3_name = "S3-Konfiguration" +title = "Integrationen" +upgrade = "Auf Pro upgraden" + +[settings.integrations.s3] +access_key = "Zugriffsschlüssel-ID" +bucket = "Bucket-Name" +configuration = "Konfiguration" +delete_failed = "S3-Konfiguration konnte nicht gelöscht werden" +deleted = "S3-Konfiguration erfolgreich gelöscht" +endpoint = "Endpunkt" +fetch_failed = "S3-Konfiguration konnte nicht abgerufen werden" +guide = "Anleitung zur Speicherkonfiguration" +guide_intro = "Das Einrichten und Verbinden Ihres Speicher-Buckets mit Cap sollte keine 10 Minuten dauern. Lesen Sie die\u00A0{guide},\u00A0um loszulegen." +other_provider = "Anderer S3-kompatibler Dienst" +provider = "Speicheranbieter" +region = "Region" +remove = "Konfiguration entfernen" +removing = "Wird entfernt ..." +save_failed = "S3-Konfiguration konnte nicht gespeichert werden" +saved = "S3-Konfiguration erfolgreich gespeichert" +saving = "Wird gespeichert ..." +secret_key = "Geheimer Zugriffsschlüssel" +test = "Verbindung testen" +test_failed = "S3-Verbindungstest fehlgeschlagen. Prüfen Sie Ihre Konfiguration und Netzwerkverbindung." +test_ok = "S3-Konfigurationstest erfolgreich! Die Verbindung funktioniert." +test_timeout = "Der Verbindungstest wurde nach 5 Sekunden abgebrochen. Bitte prüfen Sie die Endpunkt-URL und Ihre Netzwerkverbindung." +testing = "Wird getestet ..." + +[settings.integrations.gdrive] +active = "Aktiv" +active_for_uploads = "Aktiv für neue Uploads" +cached = "Zwischengespeichert {time}" +connect = "Google Drive verbinden" +connect_failed = "Die Verbindung mit Google Drive konnte nicht gestartet werden" +connected_inactive = "Verbunden, aber nicht aktiv" +connection_description = "Google Drive speichert neue Uploads in einem privaten Cap-Ordner in Ihrem Drive. Bestehende von Cap gehostete und S3-Videos bleiben in ihrem aktuellen Speicher." +connection_title = "Verbindung" +disconnect = "Trennen" +disconnect_failed = "Google Drive konnte nicht getrennt werden" +disconnected = "Google Drive getrennt" +drive_files = "Drive-Dateien" +finish_in_browser = "Schließen Sie die Verbindung mit Google Drive in Ihrem Browser ab, kehren Sie dann hierher zurück und aktualisieren Sie." +not_connected = "Nicht verbunden" +opening = "Wird geöffnet ..." +refresh = "Aktualisieren" +refreshing = "Wird aktualisiert ..." +remaining = "Verbleibend" +set_active_failed = "Der aktive Speicheranbieter konnte nicht geändert werden" +storage = "Speicher" +test = "Testen" +test_failed = "Google-Drive-Verbindungstest fehlgeschlagen" +testing = "Wird getestet ..." +trash = "Papierkorb" +updated = "Aktualisiert {time}" +use_drive = "Google Drive verwenden" +use_s3 = "S3 verwenden" +used = "{usage} belegt" +used_of = "{usage} von {limit} belegt" +waiting = "Warten ..." +working = "Die Google-Drive-Verbindung funktioniert" +working_for = "Die Google-Drive-Verbindung funktioniert für {email}" + +[settings.automations] +action_no_effect = "Diese Aktion hat beim gewählten Auslöser keine Wirkung." +add_action = "Aktion hinzufügen" +add_condition = "Bedingung hinzufügen" +auto_name_empty = "{trigger}-Automatisierung" +browse = "Durchsuchen" +check_compatibility = "Kompatibilität prüfen" +compatibility_checked = "Kompatibilität geprüft: Diese Automatisierung läuft noch nicht in Cap. Gespeicherte Regeln bleiben für die Cap-CLI verfügbar. Es wurden keine Aktionen ausgeführt." +condition_never_matches = "Diese Bedingung trifft beim gewählten Auslöser nie zu." +copy_link = "Link in die Zwischenablage kopieren" +dangerous = "In der Cap-CLI führt diese Automatisierung Befehle aus oder sendet Netzwerkanfragen mit Ihren Berechtigungen. Verwenden Sie nur Werte, denen Sie vertrauen." +delete = "Löschen" +description = "Regeln werden mit der Cap-CLI geteilt. Cap speichert diese Regeln, führt Automatisierungen aber noch nicht aus." +empty_description = "Wählen Sie unten eine Vorlage, um mit einem Klick zu starten, oder erstellen Sie Ihre eigene von Grund auf." +empty_title = "Noch keine Automatisierungen" +enabled = "Aktiviert" +match_all = "Alle erfüllt" +match_any = "Eine erfüllt" +name = "Name" +new = "Neue Automatisierung" +no_presets = "Noch keine Vorgaben. Erstellen Sie zuerst eine im Editor." +only_if = "NUR AUSFÜHREN, WENN" +open_in_browser = "Im Browser öffnen" +retry_save = "Speichern wiederholen" +run_through_shell = "Über die Shell ausführen" +save_failed = "Automatisierungen konnten nicht gespeichert werden. Ihre Änderungen wurden nicht gespeichert." +start_from_scratch = "Neu beginnen" +summary_empty = "{trigger} → noch keine Aktionen" +templates_description = "Mit einem Klick eine fertige Automatisierung hinzufügen. Danach lässt sich alles anpassen." +templates_title = "Vorlagen" +then = "DANN DIES TUN" +title = "Automatisierungen" +when = "WENN DIES PASSIERT" + +[settings.automations.status] +disabled_saved = "Deaktiviert · Gespeichert" +disabled_unsaved = "Deaktiviert · Nicht gespeichert" +enabled_saved = "Aktiviert · Gespeichert" +enabled_unsaved = "Aktiviert · Nicht gespeichert" + +[settings.automations.field] +arguments = "Argumente (durch Leerzeichen getrennt)" +body = "Text" +body_template = "Body-Vorlage (optional)" +compression = "Komprimierung" +destination = "Zielordner (optional, leer = Projektordner)" +filename = "Dateinamenvorlage (optional)" +folder = "Ordner" +format = "Format" +frame_rate = "Bildrate" +method = "Methode" +organization = "Organisations-ID (optional)" +preset = "Vorgabe" +program = "Programm" +resolution = "Auflösung" +source = "Quelle" +title = "Titel" +url = "URL" + +[settings.automations.placeholder] +organization_id = "Organisations-ID" +project_folder = "Projektordner" +window_title = "z. B. Slack" + +[settings.automations.clipboard] +original = "Originalaufnahme" +rendered = "Bearbeitet / gerendert" + +[settings.automations.target] +area = "Bereich" +display = "Bildschirm" +window = "Fenster" + +[settings.automations.compression] +maximum = "Maximal" +potato = "Kartoffel" +social = "Social Media" +web = "Web" + +[settings.automations.trigger.screenshot_taken] +label = "Bei aufgenommenem Bildschirmfoto" +noun = "Bildschirmfoto" +phrase = "Bildschirmfoto aufgenommen" +runs = "Wird bei jedem Bildschirmfoto ausgeführt." + +[settings.automations.trigger.studio_finished] +label = "Bei beendeter Studio-Aufnahme" +noun = "Studio-Aufnahme" +phrase = "Studio-Aufnahme endet" +runs = "Wird nach jeder Studio-Aufnahme ausgeführt." + +[settings.automations.trigger.instant_finished] +label = "Bei beendeter Instant-Aufnahme" +noun = "Instant-Aufnahme" +phrase = "Instant-Aufnahme endet" +runs = "Wird nach jeder Instant-Aufnahme ausgeführt." + +[settings.automations.trigger.recording_started] +label = "Bei gestarteter Aufnahme" +noun = "Aufnahmestart" +phrase = "Aufnahme startet" +runs = "Wird bei jedem Aufnahmestart ausgeführt." + +[settings.automations.trigger.upload_completed] +label = "Bei abgeschlossenem Upload" +noun = "Upload" +phrase = "Upload abgeschlossen" +runs = "Wird nach jedem abgeschlossenen Upload ausgeführt." + +[settings.automations.trigger.video_imported] +label = "Bei importiertem Video" +noun = "Import" +phrase = "Video importiert" +runs = "Wird bei jedem importierten Video ausgeführt." + +[settings.automations.trigger.recording_deleted] +label = "Bei gelöschter Aufnahme" +noun = "Löschen" +phrase = "Aufnahme gelöscht" +runs = "Wird bei jeder gelöschten Aufnahme ausgeführt." + +[settings.automations.action.copy_to_clipboard] +label = "In die Zwischenablage kopieren" +noun = "Zwischenablage" +short = "In die Zwischenablage kopieren" + +[settings.automations.action.save_to_location] +label = "An Speicherort sichern" +noun = "Ordner" +short = "In Ordner speichern" + +[settings.automations.action.export] +label = "Mit Profil exportieren" +noun = "Export" +short = "Exportieren" + +[settings.automations.action.upload] +label = "Hochladen + Link kopieren" +noun = "Upload" +short = "Hochladen & Link kopieren" + +[settings.automations.action.reveal] +label = "Im Dateimanager anzeigen" +noun = "Anzeigen" +short = "Im Dateimanager anzeigen" + +[settings.automations.action.open_file] +label = "Datei öffnen" +noun = "Öffnen" +short = "Datei öffnen" + +[settings.automations.action.ocr] +label = "Text erkennen (OCR) in die Zwischenablage" +noun = "Text" +short = "Text kopieren (OCR)" + +[settings.automations.action.notify] +label = "Mitteilung anzeigen" +noun = "Mitteilung" +short = "Benachrichtigen" + +[settings.automations.action.open_editor] +label = "Editor öffnen" +noun = "Editor" +short = "Editor öffnen" + +[settings.automations.action.skip_editor] +label = "Editor überspringen (ohne Oberfläche)" +noun = "Ohne Editor" +short = "Editor überspringen" + +[settings.automations.action.apply_preset] +label = "Editor-Vorgabe anwenden" +noun = "Vorgabe" +short = "Vorgabe anwenden" + +[settings.automations.action.run_command] +label = "Befehl ausführen" +noun = "Befehl" +short = "Befehl ausführen" + +[settings.automations.action.webhook] +label = "Webhook senden" +noun = "Webhook" +short = "Webhook senden" + +[settings.automations.action.delete_local_files] +label = "Lokale Dateien löschen" +noun = "Löschen" +short = "Lokale Dateien löschen" + +[settings.automations.condition] +capture_target = "Aufnahmeziel ist" +duration_at_least = "Dauer mindestens (Sekunden)" +duration_at_most = "Dauer höchstens (Sekunden)" +organization = "Organisation ist" +recording_mode = "Aufnahmemodus ist" +window_title = "Fenstertitel enthält" + +[settings.automations.template.copy_screenshot] +description = "Bildschirmfoto machen, und es ist sofort zum Einfügen bereit." +name = "Neue Bildschirmfotos automatisch kopieren" + +[settings.automations.template.ocr_screenshot] +description = "Cap liest den Text in Ihrem Bildschirmfoto und kopiert ihn für Sie." +name = "Text aus Bildschirmfotos holen" + +[settings.automations.template.save_screenshot] +description = "Jedes neue Bildschirmfoto direkt in einen Ordner Ihrer Wahl." +name = "Bildschirmfotos in einem Ordner ablegen" + +[settings.automations.template.reveal_screenshot] +description = "Jedes Bildschirmfoto sofort nach der Aufnahme im Finder anzeigen." +name = "Zu jedem neuen Bildschirmfoto springen" + +[settings.automations.template.export_studio] +description = "Ein MP4 rendern, sobald eine Studio-Aufnahme endet." +name = "Nach der Aufnahme automatisch exportieren" + +[settings.automations.template.upload_share] +description = "Aufnahme beenden, und der Link liegt schon in der Zwischenablage." +name = "Hochladen und Link zum Teilen holen" + +[settings.automations.template.notify_upload] +body = "Ihre Aufnahme kann jetzt geteilt werden." +description = "Eine dezente Desktop-Mitteilung, sobald Ihre Aufnahme geteilt werden kann." +name = "Benachrichtigen, wenn ein Upload fertig ist" + +[settings.automations.template.webhook_share] +description = "Den Link zum Teilen an Slack, Discord oder Ihren eigenen Webhook senden." +name = "Slack beim Teilen benachrichtigen" + [main] delete_recording_confirm = "Möchten Sie diese Aufnahme wirklich löschen?" @@ -586,3 +1296,397 @@ click_or_press = "Hier klicken oder drücken:" get_started = "Los geht’s" space_key = "Leertaste" tagline = "Die Open-Source-App zum Teilen des Bildschirms für alle." + +[menu.update] +badge_progress = "v{version} · {percent} %" +badge_version = "v{version}" +check = "Nach Updates suchen" +download = "Update herunterladen…" +checking = "Suche nach Updates…" +downloading = "Update wird geladen…" +restart = "Zum Aktualisieren neu starten…" + +[settings.general.updates.download_policy] +auto = "Automatisch" +description = "Wähle, ob Cap Updates automatisch herunterlädt." +label = "Update-Downloads" +manual = "Vorher fragen" +never = "Nie" + +[auth] +sign_in_failed = "Anmeldung fehlgeschlagen" + +[diagnostics] +already_running = "Es läuft bereits eine Diagnose." +mode_both = "Beide" + +[diagnostics.error] +create_folder = "Der Diagnoseordner konnte nicht erstellt werden: {error}" +exited = "Der Selbsttest wurde beendet mit {status}" +no_output = "Der Selbsttest hat keine Ausgabe geliefert" +serialize = "Der Bericht konnte nicht serialisiert werden: {error}" +start_failed = "{program} konnte nicht gestartet werden: {error}" +upload_logs = "Protokolle konnten nicht hochgeladen werden: {error}" +upload_status = "Upload fehlgeschlagen mit Status: {status}" +wait_failed = "Auf den Selbsttest konnte nicht gewartet werden: {error}" +write_failed = "{path} konnte nicht geschrieben werden: {error}" + +[diagnostics.stage] +analyzing = "Aufnahme wird analysiert..." +analyzing_instant = "Instant-Aufnahme wird analysiert..." +analyzing_studio = "Studio-Aufnahme wird analysiert..." +cancelling = "Wird abgebrochen..." +collecting = "Systeminformationen werden gesammelt..." +done = "Synchronisationstest wird abgeschlossen..." +exporting = "Aufnahme wird exportiert..." +exporting_instant = "Instant-Aufnahme wird exportiert..." +exporting_studio = "Studio-Aufnahme wird exportiert..." +finalizing = "Aufnahme wird fertiggestellt..." +finalizing_instant = "Instant-Aufnahme wird fertiggestellt..." +finalizing_studio = "Studio-Aufnahme wird fertiggestellt..." +pattern = "Testmuster wird abgespielt..." +pattern_instant = "Testmuster wird abgespielt (Instant)..." +pattern_studio = "Testmuster wird abgespielt (Studio)..." +preparing = "Test wird vorbereitet..." +preparing_instant = "Instant-Test wird vorbereitet..." +preparing_studio = "Studio-Test wird vorbereitet..." +recording = "Test wird aufgenommen..." +recording_instant = "Instant-Test wird aufgenommen..." +recording_studio = "Studio-Test wird aufgenommen..." +starting = "Diagnose wird gestartet..." + +[dialog] +no_microphone_body = "Diese Aufnahme enthält Ihre Stimme nicht." +open_recording_failed = "Diese Aufnahme konnte nicht geöffnet werden: {error}" + +[dialog.capture] +approve_shortcut = "Bestätigen Sie ein Aufnahmekürzel oder aktivieren Sie das Cap-Stopp-Symbol in der Taskleiste, sobald es erscheint. Die Aufnahme hat noch nicht begonnen." +area_unavailable = "Der ausgewählte Bereich hat sich geändert oder ist nicht mehr verfügbar. Wählen Sie ihn erneut aus." +camera_needs_x11 = "Instant-Kameraeinblendungen benötigen unter Linux einen X11-Desktop" +camera_notice = "Cap blendet die Kameravorschau und die Aufnahmesteuerung aus. Eine ausgewählte Kamera wird weiterhin als separate, bearbeitbare Spur aufgenommen." +camera_notice_instant = "Cap blendet Vorschau und Steuerung aus. Eine ausgewählte Kamera erscheint an der Position ihrer Vorschau, mit der gewählten Form und den gewählten Effekten. Halten Sie die gesamte Vorschau innerhalb des Aufnahmebereichs. Wenn Sie Cap öffnen, wird diese Instant-Aufnahme beendet, bevor die Steuerung erscheint." +camera_outside_area = "Verschieben Sie die gesamte Kameravorschau in den Aufnahmebereich, bevor Sie die Instant-Aufnahme starten" +camera_preview_closed = "Öffnen Sie die Vorschau der ausgewählten Kamera vor der Instant-Aufnahme" +cleanup_incomplete = "Die Aufräumarbeiten der Aufnahme sind unvollständig: {error}. Eine neue Aufnahme ist gesperrt." +devices_changed = "Die ausgewählten Geräte haben sich geändert. Starten Sie erneut, um die neuen Aufnahmequellen zu bestätigen." +devices_changed_before_start = "Die ausgewählten Geräte haben sich vor Beginn der Aufnahme geändert. Starten Sie erneut, um die Quellen zu bestätigen." +shortcut_conflict = "Ctrl+Shift+F9 ist einer anderen Cap-Aktion zugewiesen. Ändern Sie dieses Kürzel vor der Aufnahme." +shortcut_description = "Diese Cap-Aufnahme starten oder beenden" +shortcut_hint = "Drücken Sie {shortcut} und lassen Sie los, um zu starten. Mit demselben Kürzel beenden Sie die Aufnahme. Dieses vorübergehende Kürzel ändert Ihre Einstellungen nicht." +tray_stop_hint = "Aktivieren Sie das Cap-Stopp-Symbol in der Taskleiste, um zu starten. Aktivieren Sie es erneut, um zu beenden. Die Aufnahme beginnt erst, wenn Sie dieses Steuerelement verwenden." +window_unavailable = "Das ausgewählte Fenster ist nicht mehr verfügbar. Wählen Sie es erneut aus." + +[dialog.clipboard] +copy_file = "Die Datei konnte nicht in die Zwischenablage kopiert werden: {error}" +copy_file_failed = "Die Datei konnte nicht in die Zwischenablage kopiert werden" +copy_image = "Das Bild konnte nicht in die Zwischenablage kopiert werden: {error}" +copy_image_failed = "Das Bild konnte nicht in die Zwischenablage kopiert werden" +not_a_file = "Nur Dateien können in die Zwischenablage kopiert werden" +unavailable = "Zwischenablage nicht verfügbar" +unavailable_error = "Zwischenablage nicht verfügbar: {error}" + +[dialog.delete_recording] +already_deleting = "Diese Aufnahme wird bereits gelöscht." +close_failed = "Der Editor konnte nicht geschlossen werden: {error}" +editor_changed = "Der Editor hat sich während der Bestätigung geändert. Die Aufnahme wurde nicht gelöscht." +editor_closed = "Der Editor wurde bereits geschlossen. Die Aufnahme wurde nicht gelöscht." +failed = "Die Aufnahme konnte nicht gelöscht werden: {error}" +failed_title = "Aufnahme konnte nicht gelöscht werden" +finishing_body = "Cap wartet auf das Ende der Aufräumarbeiten, bevor das Löschen abgeschlossen wird." +finishing_title = "Das Löschen läuft noch" +in_use = "Diese Aufnahme wird noch verwendet. Schließen Sie den Upload ab oder brechen Sie ihn ab und versuchen Sie es erneut: {error}" +outside_library = "Diese Aufnahme liegt außerhalb der Aufnahmebibliothek. Ihre Dateien wurden nicht geändert." +recording_active = "Beenden oder verwerfen Sie die Aufnahme, bevor Sie dieses Projekt löschen." +retained_title = "Aufnahme beibehalten" + +[dialog.file] +audio_filter = "Audio" +busy = "Ein Dateidialog ist bereits geöffnet. Schließen Sie ihn ab oder brechen Sie ihn ab, bevor Sie es erneut versuchen." +export_filter = "Export" +failed = "Der Dateidialog konnte nicht abgeschlossen werden: {error}" +images_filter = "Bilder" +unavailable_title = "Dateidialog nicht verfügbar" +unconfirmed = "Cap konnte nicht bestätigen, dass der Dateidialog geschlossen wurde. Schließen Sie alle verbleibenden Dateidialoge und starten Sie Cap neu, bevor Sie es erneut versuchen." + +[dialog.mode_select] +instant = "Sofort per Link teilen. Lädt während der Aufnahme hoch." +screenshot = "Bildschirmfotos sofort aufnehmen und beschriften." +studio = "Lokale Aufnahme in höchster Qualität zum späteren Bearbeiten." +subtitle = "Wählen Sie, wie Sie Ihren Bildschirm aufnehmen möchten" +title = "Aufnahmemodus wählen" + +[dialog.storage] +stopped_kept = "Die Aufnahme wurde wegen wenig Speicherplatz beendet. Ihre Aufnahmedateien wurden behalten. Geben Sie Speicherplatz frei und stellen Sie die Aufnahme dann unten wieder her." +stopped_saved = "Die Aufnahme wurde wegen wenig Speicherplatz beendet. Ihre Aufnahme wurde gespeichert." +stopping = "Wenig Speicherplatz. Ihre Aufnahme wird beendet und gespeichert…" + +[dialog.teleprompter] +cue_markers = "Cue-Marker" +hidden = "Dieses Fenster ist in Cap-Aufnahmen ausgeblendet" +mirror = "Text spiegeln" +paused = "Scrollen mit der Aufnahme pausiert" +placeholder = "Skript einfügen oder eingeben…" + +[import] +error_title = "Importfehler" +image_failed = "Bild konnte nicht importiert werden: {error}" +unsupported = "Nicht unterstützte Mediendatei." +video_failed = "Video konnte nicht importiert werden: {error}" + +[import.filter] +image = "Bilddateien" +media = "Mediendateien" +video = "Videodateien" + +[import.progress] +analyzing = "Videodatei wird analysiert..." +complete = "Import abgeschlossen!" +converting = "Video wird konvertiert... {percent} %" +image = "Bild wird importiert..." +metadata = "Projektmetadaten werden erstellt..." +starting = "Konvertierung wird gestartet..." + +[overlay] +choose_mode = "Aufnahmemodus wählen" +countdown_tooltip = "Countdown vor der Aufnahme" +display_fallback = "Bildschirm {id}" + +[overlay.area] +draw = "Bereich aufziehen" +free_ratio = "Frei" +hint = "Klicken und ziehen Sie, um einen Bereich auszuwählen" +minimum_size = "Die Mindestgröße beträgt {width} x {height}" +too_small = "{width} x {height} ist zu klein" + +[overlay.camera_only] +description = "Nur mit Kamera und Mikrofon aufnehmen" +select_camera = "Bitte wählen Sie eine Kamera" + +[overlay.device] +camera_allow = "Kamerazugriff erlauben…" +camera_turn_on = "Kamerazugriff in den Einstellungen aktivieren…" +microphone_allow = "Mikrofonzugriff erlauben…" +microphone_turn_on = "Mikrofonzugriff in den Einstellungen aktivieren…" + +[overlay.start] +instant = "Instant-Modus: Link zum Teilen aufnehmen" +instant_sign_in = "Instant-Modus: Zum Aufnehmen anmelden" +sign_in = "Zum Nutzen anmelden" +start_recording = "Aufnahme starten" +studio = "Studio-Modus: Lokal aufnehmen und bearbeiten" +take_screenshot = "Bildschirmfoto aufnehmen" + +[permission.guide] +accessibility_body = "Legen Sie es oben in der Liste „Bedienungshilfen“ ab und achten Sie darauf, dass der Schalter aktiviert ist." +screen_body = "Legen Sie es oben in der Liste „Bildschirm- & Systemaudioaufnahme“ ab und achten Sie darauf, dass der Schalter aktiviert ist." +title = "Ziehen Sie Cap in die Liste" + +[permission.name] +accessibility = "Bedienungshilfen" +camera = "Kamera" +microphone = "Mikrofon" +screen_recording = "Bildschirmaufnahme" + +[permission.onboarding] +open_settings_again = "Systemeinstellungen erneut öffnen" +skip = "Vorerst überspringen" +switch_on_title = "Cap in den Systemeinstellungen aktivieren" + +[permission.onboarding.accessibility] +allow = "Bedienungshilfen erlauben" +body = "Mit Bedienungshilfen sieht Cap Ihre Klicks und Ihren Mauszeiger, nur auf diesem Mac, damit der automatische Zoom weiß, wohin er schauen soll." +done_body = "Der automatische Zoom kann Ihren Klicks folgen." +done_title = "Bedienungshilfen sind aktiviert" +title = "Lassen Sie Cap Ihrem Mauszeiger folgen" + +[permission.onboarding.all_set] +body = "Cap hat alles, was es braucht. Als Nächstes ein kurzer Blick darauf, was es kann." +regained = "Danke. Cap kann wieder aufnehmen." +title = "Alles bereit" + +[permission.onboarding.camera] +allow = "Kamera erlauben" +body = "Zeigen Sie sich in einer Ecke Ihrer Aufnahmen. Überspringen Sie dies, wenn Sie es nicht brauchen." +done_body = "Sie können Ihre Kamera zu jeder Aufnahme hinzufügen." +done_title = "Kamera ist aktiviert" +title = "Lassen Sie Cap Sie sehen" + +[permission.onboarding.checklist] +lede = "macOS bittet Sie zuerst, ein paar Dinge zu erlauben. Das dauert nur eine Minute." +regained_lede = "In den Systemeinstellungen wurde etwas deaktiviert. Aktivieren Sie es wieder, um weiter aufzunehmen." +regained_title = "Cap benötigt eine Berechtigung zurück" +title = "Machen wir Cap startklar" + +[permission.onboarding.hint] +drag_into_list = "Ziehen Sie Cap aus dem Feld unter den Systemeinstellungen in die Liste. Schon aufgeführt? Dann einfach aktivieren." +find_in_list = "Suchen Sie Cap in der Liste und aktivieren Sie es. Wenn macOS zuerst gefragt hat, wählen Sie „Systemeinstellungen öffnen“." +no_prompt = "Keine Abfrage erschienen? Aktivieren Sie Cap in den Systemeinstellungen." +open_settings = "Öffnen Sie die Systemeinstellungen, suchen Sie Cap in der Liste und aktivieren Sie es." +turned_off = "Dies wurde zuvor deaktiviert. Aktivieren Sie Cap daher in den Systemeinstellungen oder überspringen Sie es vorerst." + +[permission.onboarding.microphone] +allow = "Mikrofon erlauben" +body = "Fügen Sie Ihren Aufnahmen Ihre Stimme hinzu. Sie können dies überspringen und später aktivieren." +done_body = "Ihre Stimme kann in Ihren Aufnahmen zu hören sein." +done_title = "Mikrofon ist aktiviert" +title = "Lassen Sie Cap Sie hören" + +[permission.onboarding.relaunch] +body = "macOS aktiviert die Bildschirmaufnahme, sobald Cap neu startet. Sie machen genau hier weiter." +button = "Neu starten" +hint = "Aktiviert, aber es wartet noch? Starten Sie Cap neu, um es zu übernehmen." +later = "macOS übernimmt dies nach einem Neustart von Cap. Das können Sie tun, sobald Sie hier fertig sind." +title = "Ein Neustart, und alles ist bereit" + +[permission.onboarding.screen] +allow = "Bildschirmaufnahme erlauben" +body = "Mit der Bildschirmaufnahme kann Cap Ihre Bildschirme, Fenster und das Systemaudio aufnehmen." +done_body = "Cap kann Ihren Bildschirm aufnehmen." +done_title = "Bildschirmaufnahme ist aktiviert" +title = "Lassen Sie Cap Ihren Bildschirm sehen" + +[permission.onboarding.status] +allowed = "Erlaubt" +off = "In den Systemeinstellungen deaktiviert" +optional = "Optional" +required = "Erforderlich" +skipped = "Vorerst übersprungen" + +[permission.onboarding.waiting] +prompt = "Wählen Sie „Erlauben“ in der macOS-Abfrage" +system_settings = "Warten auf die Systemeinstellungen" + +[screenshot_editor] +loading_preview = "Vorschau wird geladen..." +zoom_in = "Vergrößern" +zoom_out = "Verkleinern" + +[screenshot_editor.annotation] +default_text = "Text" +done = "Fertig" +fill = "Füllung" +intensity = "Stärke" +mask_style = "Stil" +pixelate = "Verpixeln" +stroke = "Kontur" +transparent = "Transparent" + +[screenshot_editor.aspect] +auto = "Auto" +classic = "Klassisch ⋅4:3" +square = "Quadratisch ⋅1:1" +tall = "Hoch ⋅3:4" +vertical = "Vertikal ⋅9:16" +wide = "Breit ⋅16:9" + +[screenshot_editor.background] +animated_gradient = "Der animierte Verlauf wird bei Bildschirmfotos als Standbild dargestellt. Wählen Sie einen anderen Hintergrund, um ihn zu ersetzen." +blur_title = "Hintergrundunschärfe" +choose_image = "Klicken Sie zum Auswählen oder ziehen Sie ein Bild hierher" +color = "Farbe" +gradient = "Verlauf" +image = "Bild" +image_files = "Bild" +image_title = "Hintergrundbild" +wallpaper = "Wallpaper" + +[screenshot_editor.corner] +rounded = "Abgerundet" +squircle = "Squircle" +style = "ECKENSTIL" + +[screenshot_editor.crop] +full = "Ganz" +loading_frame = "Bild wird geladen…" +position = "Position" + +[screenshot_editor.error] +encode = "Das exportierte Bildschirmfoto konnte nicht kodiert werden: {error}" +load_meta = "Metadaten des Bildschirmfotos konnten nicht geladen werden: {error}" +load_metadata = "Metadaten des Bildschirmfotos konnten nicht geladen werden: {error}" +no_image = "Kein PNG in {path} gefunden" +not_screenshot = "Kein Bildschirmfoto-Paket" +open_image = "Bild konnte nicht geöffnet werden: {error}" +renderer_failed = "Der GPU-Renderer konnte nicht starten: {error}" +renderer_not_running = "Der Bildschirmfoto-Renderer läuft nicht" +renderer_stopped = "Der Renderer wurde vor dem Ende des Exports beendet" +save_project = "Fehler beim Speichern des Projekts: {error}" +serialize = "Die Bildschirmfoto-Konfiguration konnte nicht serialisiert werden: {error}" +sign_in_to_share = "Melden Sie sich im Browser bei Cap an und teilen Sie dann erneut" +too_large = "Die Bildgröße überschreitet das Maximum: {width}x{height}" +upgrade_required = "Für diese Funktion ist ein höherer Tarif erforderlich" +upload_task = "Der Upload ist fehlgeschlagen: {error}" + +[screenshot_editor.header] +aspect_ratio = "Seitenverhältnis" +copy = "In die Zwischenablage kopieren" +crop_image = "Bild zuschneiden" +more_actions = "Weitere Aktionen" + +[screenshot_editor.layers] +empty = "Noch keine Ebenen" +empty_hint = "Fügen Sie mit den Werkzeugen oben Anmerkungen hinzu" +reorder_hint = "Zum Sortieren ziehen • Oben = vorne" +title = "Ebenen" + +[screenshot_editor.menu] +delete = "Löschen" +open_folder = "Ordner öffnen" + +[screenshot_editor.share] +create_link = "Link zum Teilen erstellen" +preparing = "Upload wird vorbereitet" +rendering = "Bildschirmfoto wird gerendert" +uploading = "Bildschirmfoto wird hochgeladen" + +[screenshot_editor.style] +advanced_shadow = "Erweiterte Schatteneinstellungen" +background = "Hintergrund" +blur = "Unschärfe" +border = "Rahmen" +color = "Farbe" +corner_rounding = "Eckenrundung" +opacity = "Deckkraft" +padding = "Innenabstand" +rounding = "Rundung" +shadow = "Schatten" +size_label = "Größe" +width = "Breite" + +[screenshot_editor.toast] +copied = "Bildschirmfoto in die Zwischenablage kopiert!" +link_copied = "Link zum Teilen in die Zwischenablage kopiert" +saved = "Bildschirmfoto gespeichert!" + +[screenshot_editor.tool] +arrow = "Pfeil" +circle = "Kreis" +draw = "Zeichnen" +mask = "Maske" +rectangle = "Rechteck" +select = "Auswählen" +text_label = "Text" + +[transcription.download] +complete = "Download abgeschlossen" +failed = "Download fehlgeschlagen: {error}" +file_progress = "{file} wird geladen: {percent} %" +preparing = "Modell-Download wird vorbereitet" +progress = "Modell wird geladen: {percent} %" + +[transcription.error] +intel_mac = "Parakeet-Modelle sind auf Intel-Macs nicht verfügbar. Verwenden Sie stattdessen ein Whisper-Modell" +model_load = "Das Untertitelmodell konnte nicht geladen werden. Laden Sie es erneut herunter" +model_missing = "Untertitelmodell nicht gefunden. Bitte laden Sie es zuerst herunter" +no_audio = "Keine Audiospur in der Videodatei gefunden" +unsupported_language = "Die Modelle „Empfohlen“ und „Hohe Genauigkeit“ transkribieren nur Englisch und 24 weitere europäische Sprachen. Laden Sie das Modell „Klein“ oder „Mittel“ herunter, um diese Sprache zu untertiteln" + +[upload.status] +failed = "Upload benötigt Aufmerksamkeit" +paused = "Upload pausiert" +prepared = "Upload vorbereitet" +processing = "Upload wird verarbeitet" +retrying = "Upload wird wiederholt" +sign_in = "Zum erneuten Hochladen anmelden" +uploaded = "Hochgeladen" +uploading = "Wird hochgeladen" diff --git a/apps/desktop-gpui/locales/en.toml b/apps/desktop-gpui/locales/en.toml index c4415d07108..c5b2e323436 100644 --- a/apps/desktop-gpui/locales/en.toml +++ b/apps/desktop-gpui/locales/en.toml @@ -24,9 +24,17 @@ quit = "Quit Cap" services = "Services" settings = "Settings…" +[menu.about] +licensed = "Licensed under AGPL-3.0" +website = "Website" +commit = "Commit" + [menu.file] close_window = "Close Window" title = "File" +open_file = "Open File…" +open_recent = "Open Recent" +import_media = "Import Media…" [menu.edit] copy = "Copy" @@ -49,12 +57,22 @@ zoom = "Zoom" [menu.help] title = "Help" +feedback = "Give Feedback…" +changelog = "Changelog" +dashboard = "Dashboard" +documentation = "Documentation" +faq = "FAQ" +self_hosting = "Self-hosting" +help_center = "Help Center" +system_status = "System Status" +discord = "Join Discord Community" [tray] import_media = "Import Media..." no_recent_items = "No recent items" open_main_window = "Open Main Window" previous = { text = "Previous", context = "Submenu listing the most recent recordings and screenshots." } +recording_mode = "Recording Mode" quit = "Quit Cap" record_area = "Record Area" record_display = "Record Display" @@ -241,6 +259,698 @@ title = "Privacy" [settings.general.privacy.telemetry] description = "Cap uses anonymous telemetry to improve reliability and fix bugs. We never collect recording contents, window titles, file paths, or personal information." label = "Share anonymous telemetry" +[settings.prompt] +ok = { text = "Ok", context = "The only button of an informational dialog." } + +[settings.sidebar] +account = "Account" +cancel_sign_in = { text = "Cancel Sign In", max_px = 180, size = 13 } +check_for_updates = "Check for updates" +checking_for_updates = "Checking for updates…" +click_to_sign_in = "Click to sign in" +downloading = { text = "Downloading v{version}…", context = "{version} is a version number such as 0.6.2." } +downloading_progress = { text = "Downloading v{version}… {percent}%", context = "{version} is a version number such as 0.6.2; {percent} is a whole number from 0 to 100." } +installing = { text = "Installing v{version}…", context = "{version} is a version number such as 0.6.2." } +previous_versions = "View previous versions" +restart_to_update = { text = "Restart to update to v{version}", context = "{version} is a version number such as 0.6.2." } +sign_in = { text = "Sign In", max_px = 180, size = 13 } +sign_out = { text = "Sign Out", max_px = 180, size = 13 } +signed_in = "Signed in" + +[settings.recordings] +corrupted_body = { text = "The recording failed so this file may have issues in the editor! If your having issues recovering the file please reach out to support!", context = "Shown before opening a failed recording in the editor. The English has a typo (\"your\" for \"you're\"); translate the meaning." } +corrupted_title = "Recording is potentially corrupted" +delete = "Delete" +description = "Manage your recordings and perform actions." +edit = "Edit" +empty_library = "No recordings found" +failed = "Recording failed" +import = "Import" +in_progress = "Recording in progress" +load_more = "Load more" +open_bundle = "Open recording bundle" +open_link = "Open link" +retry_upload = "Retry upload" +show_all = "Show all" +title = "Recordings" + +[settings.recordings.empty] +all = "No recordings" +all_matching = "No matching recordings" +instant = "No instant recordings" +instant_matching = "No matching instant recordings" +studio = "No studio recordings" +studio_matching = "No matching studio recordings" + +[settings.screenshots] +copy = "Copy to clipboard" +delete = "Delete" +description = "Manage your screenshots and perform actions." +empty = "No screenshots" +empty_library = "No screenshots found" +empty_matching = "No matching screenshots" +import = "Import image" +load_more = "Load more" +open_folder = "Open folder" +save_as = "Save as" +title = "Screenshots" + +[settings.quality] +save_failed = "Couldn't save your recording settings. Please try again." + +[settings.quality.studio] +balanced = "Balanced" +balanced_description = "Sharp detail at a sensible file size. Right for most recordings." +compatibility = "Smaller files" +compatibility_description = "Saves disk space. Fine detail can soften, most of all on camera." +description = "Saved to your computer for editing. You choose the export size later, in the editor." +recommended = "Recommended" +title = "Studio recordings" +ultra = "Maximum detail" +ultra_description = "Keeps the most detail for heavy editing. Uses the most disk space." + +[settings.quality.instant] +description = "Uploaded while you record, so the link is ready the moment you stop. For the best quality, use Studio mode: it records at the highest quality your screen allows, and you can export up to 4K." +free_plan = "720p on the free plan. Cap Pro records up to 4K." +max_resolution = "Maximum resolution" +res_1080p = "Sharp text at a practical upload size." +res_1440p = "Extra detail for large, high-resolution screens." +res_4k = "The most detail. Best on a fast connection." +res_720p = "Smallest uploads, so links are ready fastest." +title = "Instant recordings" + +[settings.quality.sharing] +auto_open = "Open share links automatically" +auto_open_description = "Open the link in your browser when an upload finishes." +title = "Sharing" + +[settings.options.studio_quality] +balanced = "Balanced" +compatibility = "Compatibility" +ultra = "Ultra" + +[settings.options.main_window_start] +close = { text = "Close", max_px = 180, size = 12 } +minimise = { text = "Minimise", max_px = 180, size = 12 } + +[settings.options.post_studio] +do_nothing = "Do nothing" +open_editor = "Open editor" +show_cap = "Show Cap" + +[settings.options.post_screenshot] +copy = "Copy to clipboard" +copy_and_open_editor = "Copy and open editor" +do_nothing = "Do nothing" +open_editor = "Open editor" + +[settings.options.post_deletion] +do_nothing = { text = "Do nothing", max_px = 180, size = 12 } +reopen = { text = "Reopen recording window", max_px = 180, size = 12 } + +[settings.options.preview_quality] +full = "Full" +half = "Half" +quarter = "Quarter" + +[settings.shortcuts] +blocked_while_recording = "Shortcuts can't be changed while this recording holds its stop key. Stop recording first." +needs_modifier_macos = "Include ⌘, ⌃ or ⌥ so the shortcut doesn't get in the way of typing." +needs_modifier_other = "Include Ctrl, Alt or Win so the shortcut doesn't get in the way of typing." +record_shortcut = "Record shortcut" +recording_description = "Start a recording without opening Cap first." +recording_hint = "Press the keys you want to use. Esc cancels, Delete removes it." +recording_title = "Recording" +saved = { text = "Saved. Press {keys} in any app to {effect}.", context = "{keys} is the shortcut, such as ⌘⇧4. {effect} is one of the settings.shortcuts.effect messages and completes the sentence." } +saved_moved = { text = "Saved. Press {keys} in any app to {effect}. Removed from {shortcut}.", context = "Like settings.shortcuts.saved; {shortcut} is the label of the shortcut that used these keys before, such as Record an area." } +screenshots_description = "Shortcuts work from any app. Click one to change it." +screenshots_title = "Screenshots" +type_shortcut = "Type shortcut…" +unavailable = "Another app or the system already uses this shortcut. Click to choose a different one." +unsupported_key = "That key can't be used in a shortcut. Try a letter, number or F-key." +while_recording_title = "While recording" + +[settings.shortcuts.after_screenshot] +hint = "For every screenshot, from a shortcut or from Cap" +title = "After a screenshot" + +[settings.shortcuts.after_studio] +hint = "When you stop" +title = "After a Studio recording" + +[settings.shortcuts.after_instant] +hint = "The share link is always copied" +title = "After an Instant recording" + +[settings.shortcuts.outcome] +instant_copy_link = "Copy link only" +instant_copy_link_hint = "Nothing opens" +instant_open_link = "Open link" +instant_open_link_hint = "In your browser" +saved_to_library = "Saved to library" +screenshot_copy = "Copy" +screenshot_copy_and_edit = "Copy and edit" +screenshot_copy_and_edit_hint = "Both at once" +screenshot_copy_hint = "Paste anywhere" +screenshot_do_nothing = "Do nothing" +screenshot_open_editor = "Open editor" +screenshot_open_editor_hint = "Annotate first" +studio_do_nothing = "Do nothing" +studio_open_editor = "Open editor" +studio_open_editor_hint = "Edit right away" +studio_show_cap = "Show Cap" +studio_show_cap_hint = "Back to the recorder" + +[settings.shortcuts.label] +cycle_mode = "Cycle recording mode" +fallback = "Shortcut" +open_picker = "Open recording picker" +pause = "Pause or resume" +record_area = "Record an area" +record_display = "Record a display" +record_window = "Record a window" +restart = "Restart recording" +screenshot_area = "Screenshot an area" +screenshot_display = "Screenshot the display" +screenshot_window = "Screenshot a window" +start_instant = "Start Instant recording" +start_studio = "Start Studio recording" +stop = "Stop recording" + +[settings.shortcuts.effect] +cycle_mode = "switch between Studio, Instant and Screenshot mode" +instant_copy = "record your selected screen or window in Instant mode, then copy its share link when you stop" +instant_copy_and_open = "record your selected screen or window in Instant mode, then copy and open its share link when you stop" +open_picker = "open Cap so you can choose what to record" +pause = "pause the current recording, or resume it" +picker_area_instant = "select an area, then record it in Instant mode" +picker_area_screenshot = "select an area, then take a screenshot of it" +picker_area_studio = "select an area, then record it in Studio mode" +picker_display_instant = "choose a display, then record it in Instant mode" +picker_display_screenshot = "choose a display, then take a screenshot of it" +picker_display_studio = "choose a display, then record it in Studio mode" +picker_window_instant = "choose a window, then record it in Instant mode" +picker_window_screenshot = "choose a window, then take a screenshot of it" +picker_window_studio = "choose a window, then record it in Studio mode" +restart = "throw away the current take and start again" +screenshot_area_copy = "select an area and copy it to your clipboard" +screenshot_area_copy_and_open_editor = "select an area and copy it and open the editor" +screenshot_area_open_editor = { text = "select an area and open it in the editor", context = "Completes settings.shortcuts.saved after \"to\"; written the way that message needs it." } +screenshot_area_save = "select an area and save it to your library without opening anything" +screenshot_display_copy = "capture the display under your pointer and copy it to your clipboard" +screenshot_display_copy_and_open_editor = "capture the display under your pointer and copy it and open the editor" +screenshot_display_open_editor = "capture the display under your pointer and open it in the editor" +screenshot_display_save = "capture the display under your pointer and save it to your library without opening anything" +screenshot_window_copy = "capture the window under your pointer and copy it to your clipboard" +screenshot_window_copy_and_open_editor = "capture the window under your pointer and copy it and open the editor" +screenshot_window_open_editor = "capture the window under your pointer and open it in the editor" +screenshot_window_save = "capture the window under your pointer and save it to your library without opening anything" +stop = "stop the current recording" +studio = "record your selected screen or window in Studio mode" +studio_open_editor = "record your selected screen or window in Studio mode, then open the editor when you stop" + +[settings.shortcuts.sentence] +cycle_mode = "Switch between Studio, Instant and Screenshot mode." +instant_copy = "Record your selected screen or window in Instant mode, then copy its share link when you stop." +instant_copy_and_open = "Record your selected screen or window in Instant mode, then copy and open its share link when you stop." +open_picker = "Open Cap so you can choose what to record." +pause = "Pause the current recording, or resume it." +picker_area_instant = "Select an area, then record it in Instant mode." +picker_area_screenshot = "Select an area, then take a screenshot of it." +picker_area_studio = "Select an area, then record it in Studio mode." +picker_display_instant = "Choose a display, then record it in Instant mode." +picker_display_screenshot = "Choose a display, then take a screenshot of it." +picker_display_studio = "Choose a display, then record it in Studio mode." +picker_window_instant = "Choose a window, then record it in Instant mode." +picker_window_screenshot = "Choose a window, then take a screenshot of it." +picker_window_studio = "Choose a window, then record it in Studio mode." +restart = "Throw away the current take and start again." +screenshot_area_copy = "Select an area and copy it to your clipboard." +screenshot_area_copy_and_open_editor = "Select an area and copy it and open the editor." +screenshot_area_open_editor = "Select an area and open it in the editor." +screenshot_area_save = "Select an area and save it to your library without opening anything." +screenshot_display_copy = "Capture the display under your pointer and copy it to your clipboard." +screenshot_display_copy_and_open_editor = "Capture the display under your pointer and copy it and open the editor." +screenshot_display_open_editor = "Capture the display under your pointer and open it in the editor." +screenshot_display_save = "Capture the display under your pointer and save it to your library without opening anything." +screenshot_window_copy = "Capture the window under your pointer and copy it to your clipboard." +screenshot_window_copy_and_open_editor = "Capture the window under your pointer and copy it and open the editor." +screenshot_window_open_editor = "Capture the window under your pointer and open it in the editor." +screenshot_window_save = "Capture the window under your pointer and save it to your library without opening anything." +stop = "Stop the current recording." +studio = "Record your selected screen or window in Studio mode." +studio_open_editor = "Record your selected screen or window in Studio mode, then open the editor when you stop." + +[settings.cli] +command = "Command" +copy = "Copy" +description = "Install the Cap command for terminals, agents, scripts, and local automation." +install = "Install CLI" +installed = "Installed" +installing = "Installing..." +installs_command = { text = "The desktop app installs a local\u00A0{command}\u00A0command that points back to the bundled CLI.", context = "{command} is the command name cap, shown in a code font. Keep the non-breaking spaces around it." } +load_failed = { text = "Couldn't load CLI status: {error}", context = "{error} is a technical error message, usually in English." } +not_installed = "Not installed" +path_configured = "Added cap to your PATH. Restart your terminal to use it, or run this now:" +path_missing = { text = "Add {path} to your PATH to use cap from a new terminal.", context = "{path} is a folder such as ~/.local/bin." } +remove = "Remove" +removing = "Removing..." +repair = "Repair" +repairing = "Repairing..." +retry = "Retry" +target = "Target" +title = "Command Line" + +[settings.transcription] +active_title = "Active hints" +applied_note = "These hints are applied when you generate captions in the editor." +clear = "Clear" +count = { one = "{count} item", other = "{count} items" } +description = "Add names, spellings, domains, and capitalization preferences that caption generation should keep in mind." +placeholder = "Add a term" +saved = "Saved" +saving = "Saving..." +terms_description = "Add one term at a time to reduce typos and formatting mistakes." +terms_title = "Remembered terms" +title = "Transcription" + +[settings.experimental] +developer = "Developer" +developer_description = "Only shown in development builds." +muxer = "Out-of-process muxer" +muxer_description = "Run the fragmented-MP4 muxer in an isolated subprocess so muxer crashes can't take down your recording. Requires the bundled cap-muxer binary." +native_app = "Use native app" +native_app_description = "Turn off to close this app and reopen the classic Tauri app in the dev harness. Recordings and settings are shared." +reliability = "Reliability" + +[settings.experimental.switch] +busy = "Finish your recording, export, upload, import, or transcription task before switching to the classic app." +check_failed = { text = "Couldn't check whether the classic Cap app opened: {error}", context = "{error} is a technical error message, usually in English." } +data_stays = "Your recordings and settings stay exactly where they are." +failed = "Couldn't open the classic Cap app." +not_found = "Couldn't find the classic Cap app to switch back to." +open_failed = { text = "Couldn't open the classic Cap app: {error}", context = "{error} is a technical error message, usually in English." } +opening = "Opening the classic Cap app" +opening_detail = "This window will close when the classic Cap app is ready." +save_failed = "Couldn't save your app preference. Cap is still open." +switching = "Switching back to the classic Cap app." +timed_out = "The classic app hasn't opened. Check the dev terminal for build errors, then try again." + +[settings.feedback] +app_version = "App Version" +capture_support = "Capture Support" +capture_supported = "Screen Capture: Supported" +capture_unsupported = "Screen Capture: Not Supported" +community_description = "Have questions, want to share ideas, or just hang out? Join the Cap Discord community." +community_title = "Join the Community" +debug_description = "Upload your logs to help us diagnose issues with Cap. No personal information is included." +debug_title = "Debug Information" +description = "Help us improve Cap by submitting feedback or reporting bugs. We'll get right on it." +join_discord = "Join Discord" +operating_system = "Operating System" +placeholder = "Tell us what you think about Cap..." +submit = "Submit Feedback" +submit_failed = "Failed to submit feedback" +submitting = "Submitting..." +system_loading = "Loading system information..." +system_title = "System Information" +thanks = "Thank you for your feedback!" +title = "Feedback" +upload_interrupted = "The upload was interrupted" +upload_logs = "Upload Logs" +upload_sent = "Sent. Thank you -- this helps us find the problem." +uploading = "Uploading..." + +[settings.feedback.diagnostic] +confirm_body = { text = "Cap will take over your screen with a flashing pattern and play loud beeps for about {seconds} seconds per pipeline. Take your headphones off, leave the volume audible, and leave the machine alone until it finishes.", context = "{seconds} is a whole number of seconds." } +confirm_title = "Run diagnostic?" +description = "Records a short flashing/beeping test pattern, measures how far the picture and sound drift apart, and snapshots your displays, cameras, microphones, disk space and recording settings. Nothing is sent anywhere until you choose to send it." +mic_description = "Also record your microphone and check that it hears the beeps in time. Needs speakers the mic can actually hear." +mic_label = "Test microphone" +mode_description = "Which pipeline to test. Both runs the studio leg and then the instant one." +mode_label = "Recording mode" +no_cli = "The Cap command-line tool was not found next to this build, so the report will cover your environment only." +no_cli_sync = "The Cap command-line tool was not found next to this build, so the sync test could not run." +run = "Run Diagnostic" +run_again = "Run Again" +send = "Send to Cap" +sending = "Sending..." +show_file = "Show File" +stop_recording_first = "Stop the current recording before running a diagnostic." +title = "Diagnostic Report" +try_again = "Try Again" +warning_body = "A fullscreen window flashes black and white while 1kHz beeps play through your speakers, and Cap records it and measures how far the picture and the sound have drifted apart. Leave the machine alone while it runs. Both modes together take a couple of minutes." +warning_title = "The test takes over your screen and makes noise." + +[settings.feedback.diagnostic.verdict] +environment_only = "ENVIRONMENT ONLY" +fail = "FAIL" +inconclusive = "INCONCLUSIVE" +pass = "PASS" +warn = "WARN" + +[settings.sign_in_required] +body = "Please sign in to continue. Alternatively, email hello@cap.so or join our Discord at cap.link/discord" +title = "Sign in required" + +[settings.changelog] +fetch_failed = "Failed to fetch changelog" +loading = "Loading changelog..." +new = "NEW" +version = { text = "Version {version} - {date}", context = "{version} is a version number such as 0.6.2; {date} is a date such as 8/17/2026." } + +[settings.license] +activate = "Activate License" +activating = "Activating..." +active_title = "Desktop license" +checking = "Checking…" +deactivate = "Deactivate License" +description = "Your Cap plan and desktop license, in one place." +desktop_description = "Your desktop license covers commercial recording and editing. Cap Pro adds cloud sharing and collaboration features." +desktop_name = "Desktop License" +expires = "Expires" +explore_description = "Compare current pricing and everything included on our website." +explore_desktop = "Desktop License · Commercial use of the desktop recorder and editor." +explore_pro = "Cap Pro · A desktop license, plus cloud sharing, AI features and collaboration." +explore_title = "Explore plans" +free_description = "Record and edit locally for personal use. Choose a paid plan for commercial use or more cloud features." +have_key = "Have a license key?" +key = "License Key" +no_instance = "No instance ID found" +opens_pricing = "Opens cap.so/pricing in your browser." +placeholder = "License key" +plan_title = "Your plan" +pro_description = "Your account includes cloud sharing, Pro features and a desktop license for commercial use." +refresh = "Refresh plan" +refresh_failed = "Couldn't refresh your plan. Please try again." +save_failed = "Failed to save the license" +sign_in_hint = "Already have Cap Pro? Sign in with your account from the sidebar." +title = "Plan & license" +view_pricing = "View plans & pricing ↗" + +[settings.integrations] +configure = "Configure" +description = "Configure integrations to extend Cap's functionality and connect with third-party services." +google_drive_description = "Connect Google Drive for new shareable link uploads. Cap stores new videos in a private Cap folder in your Drive and continues serving them through Cap after normal access checks." +managed = "Managed by your organization" +managed_by = { text = "Managed by your organization: {organization}", context = "{organization} is the name of the user's organization." } +request_timed_out = "The request timed out" +s3_description = "Connect your own S3 bucket for complete control over your data storage. All new shareable link uploads will be automatically uploaded to your configured S3 bucket, ensuring you maintain complete ownership and control over your content. Perfect for organizations requiring data sovereignty and custom storage policies." +s3_name = "S3 Config" +title = "Integrations" +upgrade = "Upgrade to Pro" + +[settings.integrations.s3] +access_key = "Access Key ID" +bucket = "Bucket Name" +configuration = "Configuration" +delete_failed = "Failed to delete S3 config" +deleted = "S3 configuration deleted successfully" +endpoint = "Endpoint" +fetch_failed = "Failed to fetch S3 config" +guide = "Storage Config Guide" +guide_intro = { text = "It should take under 10 minutes to set up and connect your storage bucket to Cap. View the\u00A0{guide}\u00A0to get started.", context = "{guide} is the link text settings.integrations.s3.guide. Keep the non-breaking spaces around it." } +other_provider = "Other S3-Compatible" +provider = "Storage Provider" +region = "Region" +remove = "Remove Config" +removing = "Removing..." +save_failed = "Failed to save S3 config" +saved = "S3 configuration saved successfully" +saving = "Saving..." +secret_key = "Secret Access Key" +test = "Test Connection" +test_failed = "S3 connection test failed. Check your config and network connection." +test_ok = "S3 configuration test successful! Connection is working." +test_timeout = "Connection test timed out after 5 seconds. Please check your endpoint URL and network connection." +testing = "Testing..." + +[settings.integrations.gdrive] +active = "Active" +active_for_uploads = "Active for new uploads" +cached = { text = "Cached {time}", context = "{time} is a date and time such as Aug 17, 2026, 10:30 PM." } +connect = "Connect Google Drive" +connect_failed = "Failed to start Google Drive connection" +connected_inactive = "Connected but not active" +connection_description = "Google Drive stores new uploads in a private Cap folder in your Drive. Existing Cap-hosted and S3 videos keep using their current storage." +connection_title = "Connection" +disconnect = "Disconnect" +disconnect_failed = "Failed to disconnect Google Drive" +disconnected = "Google Drive disconnected" +drive_files = "Drive files" +finish_in_browser = "Finish connecting Google Drive in your browser, then return here and refresh." +not_connected = "Not connected" +opening = "Opening..." +refresh = "Refresh" +refreshing = "Refreshing..." +remaining = "Remaining" +set_active_failed = "Failed to update active storage provider" +storage = "Storage" +test = "Test" +test_failed = "Google Drive connection test failed" +testing = "Testing..." +trash = "Trash" +updated = { text = "Updated {time}", context = "{time} is a date and time such as Aug 17, 2026, 10:30 PM." } +use_drive = "Use Google Drive" +use_s3 = "Use S3" +used = { text = "{usage} used", context = "{usage} is a size such as 2.0 GB." } +used_of = { text = "{usage} of {limit} used", context = "{usage} and {limit} are sizes such as 2.0 GB." } +waiting = "Waiting..." +working = "Google Drive connection is working" +working_for = { text = "Google Drive connection is working for {email}", context = "{email} is the Google account's email address." } + +[settings.automations] +action_no_effect = "This action has no effect for the selected trigger." +add_action = "Add action" +add_condition = "Add condition" +auto_name_empty = { text = "{trigger} automation", context = "{trigger} is a settings.automations.trigger.*.noun." } +browse = "Browse" +check_compatibility = "Check compatibility" +compatibility_checked = "Compatibility checked: this automation doesn't run in Cap yet. Saved rules remain available to the Cap CLI. No actions were run." +condition_never_matches = "This condition never matches for the selected trigger." +copy_link = "Copy link to clipboard" +dangerous = "In the Cap CLI, this automation runs commands or sends network requests with your permissions. Only use values you trust." +delete = "Delete" +description = "Rules are shared with the Cap CLI. Cap saves these rules but does not run automations yet." +empty_description = "Pick a template below to get started in one click, or build your own from scratch." +empty_title = "No automations yet" +enabled = "Enabled" +match_all = "Match all" +match_any = "Match any" +name = "Name" +new = "New automation" +no_presets = "No presets yet — create one in the editor first." +only_if = { text = "ONLY RUN IF", context = "A small all-caps group label above the conditions." } +open_in_browser = "Open in browser" +retry_save = "Retry save" +run_through_shell = "Run through shell" +save_failed = "Failed to save automations. Your changes have not been saved." +start_from_scratch = "Start from scratch" +summary_empty = { text = "{trigger} → no actions yet", context = "{trigger} is a settings.automations.trigger.*.phrase." } +templates_description = "One click to add a ready-made automation. Tweak anything afterwards." +templates_title = "Templates" +then = { text = "THEN DO THIS", context = "A small all-caps group label above the actions." } +title = "Automations" +when = { text = "WHEN THIS HAPPENS", context = "A small all-caps group label above the trigger select." } + +[settings.automations.status] +disabled_saved = "Disabled · Saved" +disabled_unsaved = "Disabled · Not saved" +enabled_saved = "Enabled · Saved" +enabled_unsaved = "Enabled · Not saved" + +[settings.automations.field] +arguments = "Arguments (space-separated)" +body = "Body" +body_template = "Body template (optional)" +compression = "Compression" +destination = "Destination folder (optional, blank = project folder)" +filename = "Filename template (optional)" +folder = "Folder" +format = "Format" +frame_rate = "Frame rate" +method = "Method" +organization = "Organization ID (optional)" +preset = "Preset" +program = "Program" +resolution = "Resolution" +source = "Source" +title = "Title" +url = "URL" + +[settings.automations.placeholder] +organization_id = "Organization ID" +project_folder = "Project folder" +window_title = { text = "e.g. Slack", context = "Placeholder in the window-title condition field; Slack is an app name." } + +[settings.automations.clipboard] +original = "Original capture" +rendered = "Edited / rendered" + +[settings.automations.target] +area = "Area" +display = "Display" +window = "Window" + +[settings.automations.compression] +maximum = "Maximum" +potato = "Potato" +social = "Social" +web = "Web" + +[settings.automations.trigger.screenshot_taken] +label = "On screenshot taken" +noun = "Screenshot" +phrase = "Screenshot taken" +runs = "Runs for every screenshot taken." + +[settings.automations.trigger.studio_finished] +label = "On studio recording finished" +noun = "Studio recording" +phrase = "Studio recording ends" +runs = "Runs for every studio recording ends." + +[settings.automations.trigger.instant_finished] +label = "On instant recording finished" +noun = "Instant recording" +phrase = "Instant recording ends" +runs = "Runs for every instant recording ends." + +[settings.automations.trigger.recording_started] +label = "On recording started" +noun = "Recording start" +phrase = "Recording starts" +runs = "Runs for every recording starts." + +[settings.automations.trigger.upload_completed] +label = "On upload completed" +noun = "Upload" +phrase = "Upload completes" +runs = "Runs for every upload completes." + +[settings.automations.trigger.video_imported] +label = "On video imported" +noun = "Import" +phrase = "Video imported" +runs = "Runs for every video imported." + +[settings.automations.trigger.recording_deleted] +label = "On recording deleted" +noun = "Deletion" +phrase = "Recording deleted" +runs = "Runs for every recording deleted." + +[settings.automations.action.copy_to_clipboard] +label = "Copy to clipboard" +noun = "Clipboard" +short = "Copy to clipboard" + +[settings.automations.action.save_to_location] +label = "Save to location" +noun = "Folder" +short = "Save to folder" + +[settings.automations.action.export] +label = "Export with profile" +noun = "Export" +short = "Export" + +[settings.automations.action.upload] +label = "Upload + copy link" +noun = "Upload" +short = "Upload & copy link" + +[settings.automations.action.reveal] +label = "Reveal in file manager" +noun = "Reveal" +short = "Reveal in file manager" + +[settings.automations.action.open_file] +label = "Open file" +noun = "Open" +short = "Open file" + +[settings.automations.action.ocr] +label = "Recognize text (OCR) to clipboard" +noun = "Text" +short = "Copy text (OCR)" + +[settings.automations.action.notify] +label = "Show notification" +noun = "Notify" +short = "Notify" + +[settings.automations.action.open_editor] +label = "Open editor" +noun = "Editor" +short = "Open editor" + +[settings.automations.action.skip_editor] +label = "Skip editor (headless)" +noun = "Skip editor" +short = "Skip editor" + +[settings.automations.action.apply_preset] +label = "Apply editor preset" +noun = "Preset" +short = "Apply preset" + +[settings.automations.action.run_command] +label = "Run command" +noun = "Command" +short = "Run command" + +[settings.automations.action.webhook] +label = "Send webhook" +noun = "Webhook" +short = "Send webhook" + +[settings.automations.action.delete_local_files] +label = "Delete local files" +noun = "Delete" +short = "Delete local files" + +[settings.automations.condition] +capture_target = "Capture target is" +duration_at_least = "Duration at least (seconds)" +duration_at_most = "Duration at most (seconds)" +organization = "Organization is" +recording_mode = "Recording mode is" +window_title = "Window title contains" + +[settings.automations.template.copy_screenshot] +description = "Snap a screenshot and it's right there, ready to paste." +name = "Auto-copy new screenshots to clipboard" + +[settings.automations.template.ocr_screenshot] +description = "Cap reads the text in your screenshot and copies it for you." +name = "Pull the text out of screenshots" + +[settings.automations.template.save_screenshot] +description = "Send every new screenshot straight to a folder you pick." +name = "Tuck screenshots into a folder" + +[settings.automations.template.reveal_screenshot] +description = "Pop open every screenshot in Finder the moment you take it." +name = "Jump to each new screenshot" + +[settings.automations.template.export_studio] +description = "Render an MP4 the second a Studio recording wraps up." +name = "Auto-export when you finish recording" + +[settings.automations.template.upload_share] +description = "Finish a recording and the link is waiting on your clipboard." +name = "Upload and grab the share link" + +[settings.automations.template.notify_upload] +body = { text = "Your recording is ready to share.", context = "The default text of the notification this template creates; the user can edit it." } +description = "Get a gentle desktop nudge once your recording is shareable." +name = "Ping me when an upload is ready" + +[settings.automations.template.webhook_share] +description = "Send the share link to Slack, Discord, or your own webhook." +name = "Tell Slack when you share something" + [main] delete_recording_confirm = "Are you sure you want to delete this recording?" @@ -588,3 +1298,397 @@ click_or_press = "Click here, or press" get_started = "Get Started" space_key = "Space" tagline = "The open source screen sharing app for everyone." + +[menu.update] +badge_progress = "v{version} · {percent}%" +badge_version = "v{version}" +check = "Check for Update" +download = "Download update…" +checking = "Checking for Update…" +downloading = "Downloading update…" +restart = "Restart to update…" + +[settings.general.updates.download_policy] +auto = "Auto" +description = "Choose whether Cap downloads updates automatically." +label = "Update downloads" +manual = "Ask first" +never = "Never" + +[auth] +sign_in_failed = "Sign in failed" + +[diagnostics] +already_running = "A diagnostic is already running." +mode_both = { text = "Both", context = "Segment beside Studio and Instant: run the sync test in both modes." } + +[diagnostics.error] +create_folder = { text = "Failed to create the diagnostics folder: {error}", context = "{error} is a technical error message, usually in English." } +exited = { text = "The self-test exited with {status}", context = "{status} is a process exit status such as \"exit status: 1\"." } +no_output = "The self-test produced no output stream" +serialize = { text = "Failed to serialize the report: {error}", context = "{error} is a technical error message, usually in English." } +start_failed = { text = "Failed to start {program}: {error}", context = "{program} is a file path; {error} is a technical error message, usually in English." } +upload_logs = { text = "Failed to upload logs: {error}", context = "{error} is a technical error message, usually in English." } +upload_status = { text = "Upload failed with status: {status}", context = "{status} is an HTTP status such as 500 Internal Server Error." } +wait_failed = { text = "The self-test could not be waited on: {error}", context = "{error} is a technical error message, usually in English." } +write_failed = { text = "Failed to write {path}: {error}", context = "{path} is a file path; {error} is a technical error message, usually in English." } + +[diagnostics.stage] +analyzing = "Analyzing the recording..." +analyzing_instant = "Analyzing the instant recording..." +analyzing_studio = "Analyzing the studio recording..." +cancelling = "Cancelling..." +collecting = "Collecting system information..." +done = "Wrapping up the sync test..." +exporting = "Exporting the recording..." +exporting_instant = "Exporting the instant recording..." +exporting_studio = "Exporting the studio recording..." +finalizing = "Finalizing the recording..." +finalizing_instant = "Finalizing the instant recording..." +finalizing_studio = "Finalizing the studio recording..." +pattern = "Playing the test pattern..." +pattern_instant = "Playing the test pattern instant..." +pattern_studio = "Playing the test pattern studio..." +preparing = "Preparing the test..." +preparing_instant = "Preparing the instant test..." +preparing_studio = "Preparing the studio test..." +recording = "Recording the test..." +recording_instant = "Recording the instant test..." +recording_studio = "Recording the studio test..." +starting = "Starting the diagnostic..." + +[dialog] +no_microphone_body = "This recording will not include your voice." +open_recording_failed = { text = "Opening this recording failed: {error}", context = "{error} is a technical error message, usually in English." } + +[dialog.capture] +approve_shortcut = "Approve a recording shortcut, or activate the Cap Stop tray icon when it appears. Recording has not started." +area_unavailable = "The selected area changed or is no longer available. Select it again." +camera_needs_x11 = "Instant camera overlays require an X11 desktop on Linux" +camera_notice = "Cap will hide its camera preview and recording controls. Any selected camera will keep recording as a separate editable track." +camera_notice_instant = "Cap will hide its preview and controls. Any selected camera will appear at its preview position with the selected shape and effects. Keep the whole preview inside the capture area. Opening Cap will stop this Instant recording before showing its controls." +camera_outside_area = "Move the whole camera preview inside the capture area before starting Instant recording" +camera_preview_closed = "Open the selected camera preview before Instant recording" +cleanup_incomplete = { text = "Recording cleanup is incomplete: {error}. A new recording is blocked.", context = "{error} is a technical error message, usually in English." } +devices_changed = "Selected devices changed. Start again to confirm the new recording inputs." +devices_changed_before_start = "Selected devices changed before recording started. Start again to confirm the inputs." +shortcut_conflict = "Ctrl+Shift+F9 is assigned to another Cap action. Change that shortcut before recording." +shortcut_description = { text = "Start or stop this Cap recording", context = "Shown by the Linux desktop when it asks the user to approve a global shortcut." } +shortcut_hint = { text = "Press and release {shortcut} to start. Use the same shortcut to stop. This temporary shortcut does not change your settings.", context = "{shortcut} is a key combination such as Ctrl+Shift+F9." } +tray_stop_hint = "Activate the Cap Stop tray icon to start. Activate the same icon again to stop. Recording will not start until you use that control." +window_unavailable = "The selected window is no longer available. Select it again." + +[dialog.clipboard] +copy_file = { text = "Could not copy the file to the clipboard: {error}", context = "{error} is a technical error message, usually in English." } +copy_file_failed = "Failed to copy file to clipboard" +copy_image = { text = "Could not copy the image to the clipboard: {error}", context = "{error} is a technical error message, usually in English." } +copy_image_failed = "Failed to copy image to clipboard" +not_a_file = "Only files can be copied to the clipboard" +unavailable = "Clipboard unavailable" +unavailable_error = { text = "Clipboard unavailable: {error}", context = "{error} is a technical error message, usually in English." } + +[dialog.delete_recording] +already_deleting = "This recording is already being deleted." +close_failed = { text = "Could not close the editor: {error}", context = "{error} is a technical error message, usually in English." } +editor_changed = "The editor changed while deletion was being confirmed. The recording was not deleted." +editor_closed = "The editor has already closed. The recording was not deleted." +failed = { text = "Recording deletion failed: {error}", context = "{error} is a technical error message, usually in English." } +failed_title = "Could not delete recording" +finishing_body = "Cap is waiting for recording cleanup to finish before completing this deletion." +finishing_title = "Deletion is still finishing" +in_use = { text = "This recording is still in use. Finish or cancel its upload and try again: {error}", context = "{error} is a technical error message, usually in English." } +outside_library = "This recording is outside the recordings library. Its files were not changed." +recording_active = "Finish or cancel the recording before deleting this project." +retained_title = "Recording retained" + +[dialog.file] +audio_filter = { text = "Audio", context = "File type name in the file picker's filter list." } +busy = "A file dialog is already open. Finish or cancel it before trying again." +export_filter = { text = "Export", context = "File type name in the save dialog's filter list." } +failed = { text = "The file dialog could not complete: {error}", context = "{error} is a technical error message, usually in English." } +images_filter = { text = "Images", context = "File type name in the file picker's filter list." } +unavailable_title = "File dialog unavailable" +unconfirmed = "Cap could not confirm that the file dialog closed. Close any remaining file dialog and restart Cap before trying again." + +[dialog.mode_select] +instant = "Share instantly with a link. Uploads as you record." +screenshot = "Capture and annotate screenshots instantly." +studio = "Highest quality local recording for editing later." +subtitle = "Select how you want to capture your screen" +title = "Choose Recording Mode" + +[dialog.storage] +stopped_kept = "Recording stopped because storage is low. Your recording files were kept. Free up space, then recover the recording below." +stopped_saved = "Recording stopped because storage is low. Your recording was saved." +stopping = "Low storage. Stopping and saving your recording…" + +[dialog.teleprompter] +cue_markers = { text = "Cue markers", context = "Toggle in a 192px popover." } +hidden = "This window is hidden from Cap recordings" +mirror = { text = "Mirror text", context = "Toggle in a 192px popover." } +paused = "Scrolling paused with recording" +placeholder = "Paste or type your script…" + +[import] +error_title = "Import Error" +image_failed = { text = "Failed to import image: {error}", context = "{error} is a technical error message, usually in English." } +unsupported = "Unsupported media file." +video_failed = { text = "Failed to import video: {error}", context = "{error} is a technical error message, usually in English." } + +[import.filter] +image = { text = "Image Files", context = "File type name in the file picker's filter list." } +media = { text = "Media Files", context = "File type name in the file picker's filter list." } +video = { text = "Video Files", context = "File type name in the file picker's filter list." } + +[import.progress] +analyzing = "Analyzing video file..." +complete = "Import complete!" +converting = { text = "Converting video... {percent}%", context = "{percent} is a whole number." } +image = "Importing image..." +metadata = "Creating project metadata..." +starting = "Starting conversion..." + +[overlay] +choose_mode = "Choose recording mode" +countdown_tooltip = "Recording countdown" +display_fallback = { text = "Display {id}", context = "Name for a display the system does not name; {id} is a number." } + +[overlay.area] +draw = "Draw an area" +free_ratio = { text = "Free", context = "Aspect ratio button with no ratio locked; the button is about 56px wide." } +hint = "Click and drag to select an area" +minimum_size = { text = "Minimum size is {width} x {height}", context = "{width} and {height} are pixel sizes." } +too_small = { text = "{width} x {height} is too small", context = "{width} and {height} are pixel sizes." } + +[overlay.camera_only] +description = "Record using only your camera and microphone" +select_camera = "Please select a camera" + +[overlay.device] +camera_allow = "Allow camera access…" +camera_turn_on = "Turn on camera access in Settings…" +microphone_allow = "Allow microphone access…" +microphone_turn_on = "Turn on microphone access in Settings…" + +[overlay.start] +instant = { text = "Instant Mode — Record shareable link", context = "Second line of the start button, 11px, truncates." } +instant_sign_in = { text = "Instant Mode — Sign in to record instantly", context = "Second line of the start button, 11px, truncates." } +sign_in = "Sign In To Use" +start_recording = "Start Recording" +studio = { text = "Studio Mode — Record and edit locally", context = "Second line of the start button, 11px, truncates." } +take_screenshot = "Take Screenshot" + +[permission.guide] +accessibility_body = { text = "Drop it on the Accessibility list above, then make sure its switch is on.", context = "Accessibility is the name of the macOS System Settings pane; use the name macOS shows in this language." } +screen_body = { text = "Drop it on the Screen & System Audio Recording list above, then make sure its switch is on.", context = "Screen & System Audio Recording is the name of the macOS System Settings pane; use the name macOS shows in this language." } +title = "Drag Cap into the list" + +[permission.name] +accessibility = "Accessibility" +camera = "Camera" +microphone = "Microphone" +screen_recording = "Screen Recording" + +[permission.onboarding] +open_settings_again = "Open System Settings again" +skip = "Skip for now" +switch_on_title = "Switch Cap on in System Settings" + +[permission.onboarding.accessibility] +allow = "Allow accessibility" +body = "Accessibility lets Cap see your clicks and cursor, on this Mac only, so auto zoom knows where to look." +done_body = "Auto zoom can follow your clicks." +done_title = "Accessibility is on" +title = "Let Cap follow your cursor" + +[permission.onboarding.all_set] +body = "Cap has everything it needs. Next, a quick look at what it can do." +regained = "Thanks. Cap can record again." +title = "You're all set" + +[permission.onboarding.camera] +allow = "Allow camera" +body = "Show yourself in a corner of your recordings. Skip it if you don't need it." +done_body = "You can add your camera to any recording." +done_title = "Camera is on" +title = "Let Cap see you" + +[permission.onboarding.checklist] +lede = "macOS asks you to OK a few things first. It only takes a minute." +regained_lede = "Something was switched off in System Settings. Turn it back on to keep recording." +regained_title = "Cap needs a permission back" +title = "Let's get Cap ready" + +[permission.onboarding.hint] +drag_into_list = "Drag Cap from the panel under System Settings into the list. Already listed? Just switch it on." +find_in_list = "Find Cap in the list and switch it on. If macOS asked first, choose Open System Settings." +no_prompt = "No prompt showed up? Switch Cap on in System Settings." +open_settings = "Open System Settings, find Cap in the list and switch it on." +turned_off = "It was turned off earlier, so switch Cap on in System Settings. Or skip it for now." + +[permission.onboarding.microphone] +allow = "Allow microphone" +body = "Add your voice to your recordings. You can skip this and turn it on later." +done_body = "Your voice can be in your recordings." +done_title = "Microphone is on" +title = "Let Cap hear you" + +[permission.onboarding.relaunch] +body = "macOS turns on screen recording once Cap restarts. You'll pick up right here." +button = { text = "Relaunch", context = "Small secondary button beside a hint." } +hint = "Switched on but still waiting? Restart Cap to apply it." +later = "macOS applies it after Cap restarts. You can do that once you're done here." +title = "One restart and you're set" + +[permission.onboarding.screen] +allow = "Allow screen recording" +body = "Screen Recording lets Cap capture your displays, windows and system audio." +done_body = "Cap can capture your screen." +done_title = "Screen recording is on" +title = "Let Cap see your screen" + +[permission.onboarding.status] +allowed = "Allowed" +off = "Off in System Settings" +optional = "Optional" +required = "Required" +skipped = "Skipped for now" + +[permission.onboarding.waiting] +prompt = "Choose Allow in the macOS prompt" +system_settings = "Waiting for System Settings" + +[screenshot_editor] +loading_preview = "Loading preview..." +zoom_in = "Zoom In" +zoom_out = "Zoom Out" + +[screenshot_editor.annotation] +default_text = { text = "Text", context = "Initial content of a new text annotation drawn on a screenshot." } +done = "Done" +fill = "Fill" +intensity = "Intensity" +mask_style = { text = "Style", context = "Picks how a mask hides content: blur or pixelate." } +pixelate = "Pixelate" +stroke = { text = "Stroke", context = "Outline colour of a shape annotation." } +transparent = "Transparent" + +[screenshot_editor.aspect] +auto = { text = "Auto", context = "Aspect ratio follows the image. Shown in an 80px button." } +classic = "Classic ⋅4:3" +square = "Square ⋅1:1" +tall = "Tall ⋅3:4" +vertical = "Vertical ⋅9:16" +wide = "Wide ⋅16:9" + +[screenshot_editor.background] +animated_gradient = "Animated Gradient is rendered as a still image for screenshots. Choose another background to replace it." +blur_title = "Background Blur" +choose_image = "Click to select or drag and drop image" +color = { text = "Color", context = "Background source tab; four tabs share a 400px popover." } +gradient = { text = "Gradient", context = "Background source tab; four tabs share a 400px popover." } +image = { text = "Image", context = "Background source tab; four tabs share a 400px popover." } +image_files = { text = "Image", context = "File type name in the file picker's filter list." } +image_title = "Background Image" +wallpaper = { text = "Wallpaper", context = "Background source tab; four tabs share a 400px popover." } + +[screenshot_editor.corner] +rounded = "Rounded" +squircle = "Squircle" +style = { text = "CORNER STYLE", context = "Small all-caps heading above the corner style picker." } + +[screenshot_editor.crop] +full = "Full" +loading_frame = "Loading frame…" +position = "Position" + +[screenshot_editor.error] +encode = { text = "Failed to encode screenshot export: {error}", context = "{error} is a technical error message, usually in English." } +load_meta = { text = "Failed to load screenshot meta: {error}", context = "{error} is a technical error message, usually in English." } +load_metadata = { text = "Failed to load screenshot metadata: {error}", context = "{error} is a technical error message, usually in English." } +no_image = { text = "No PNG found in {path}", context = "{path} is a folder path." } +not_screenshot = "Not a screenshot bundle" +open_image = { text = "Failed to open image: {error}", context = "{error} is a technical error message, usually in English." } +renderer_failed = { text = "The GPU renderer failed to start: {error}", context = "{error} is a technical error message, usually in English." } +renderer_not_running = "The screenshot renderer is not running" +renderer_stopped = "The renderer stopped before the export finished" +save_project = { text = "Error saving project: {error}", context = "{error} is a technical error message, usually in English." } +serialize = { text = "Failed to serialize screenshot config: {error}", context = "{error} is a technical error message, usually in English." } +sign_in_to_share = "Sign in to Cap in your browser, then share again" +too_large = { text = "Image dimensions exceed maximum: {width}x{height}", context = "{width} and {height} are pixel sizes." } +upgrade_required = "This feature requires an upgraded plan" +upload_task = { text = "The upload task failed: {error}", context = "{error} is a technical error message, usually in English." } + +[screenshot_editor.header] +aspect_ratio = "Aspect Ratio" +copy = "Copy to Clipboard" +crop_image = "Crop Image" +more_actions = "More Actions" + +[screenshot_editor.layers] +empty = "No layers yet" +empty_hint = "Use the tools above to add annotations" +reorder_hint = "Drag to reorder • Top = front" +title = "Layers" + +[screenshot_editor.menu] +delete = "Delete" +open_folder = "Open Folder" + +[screenshot_editor.share] +create_link = "Create shareable link" +preparing = "Preparing upload" +rendering = "Rendering screenshot" +uploading = "Uploading screenshot" + +[screenshot_editor.style] +advanced_shadow = "Advanced shadow settings" +background = "Background" +blur = "Blur" +border = "Border" +color = "Color" +corner_rounding = "Corner Rounding" +opacity = "Opacity" +padding = "Padding" +rounding = "Rounding" +shadow = "Shadow" +size_label = "Size" +width = "Width" + +[screenshot_editor.toast] +copied = "Screenshot copied to clipboard!" +link_copied = "Share link copied to clipboard" +saved = "Screenshot saved!" + +[screenshot_editor.tool] +arrow = "Arrow" +circle = "Circle" +draw = "Draw" +mask = "Mask" +rectangle = "Rectangle" +select = "Select" +text_label = "Text" + +[transcription.download] +complete = "Download complete" +failed = { text = "Download failed: {error}", context = "{error} is a technical error message, usually in English." } +file_progress = { text = "Downloading {file}: {percent}%", context = "{file} is a model file name; {percent} is a whole number." } +preparing = "Preparing model download" +progress = { text = "Downloading model: {percent}%", context = "{percent} is a whole number." } + +[transcription.error] +intel_mac = "Parakeet models are not available on Intel Macs. Use a Whisper model instead" +model_load = "Failed to load the caption model. Try downloading it again" +model_missing = "Caption model not found. Please download it first" +no_audio = "No audio found in the video file" +unsupported_language = "The Recommended and High Accuracy models only transcribe English and 24 other European languages. Download the Small or Medium model to caption this language" + +[upload.status] +failed = "Upload needs attention" +paused = "Upload paused" +prepared = "Upload prepared" +processing = "Processing upload" +retrying = "Upload will retry" +sign_in = "Sign in to retry upload" +uploaded = "Uploaded" +uploading = "Uploading" diff --git a/apps/desktop-gpui/locales/es.toml b/apps/desktop-gpui/locales/es.toml index 00c4c9b74f7..a348a53367e 100644 --- a/apps/desktop-gpui/locales/es.toml +++ b/apps/desktop-gpui/locales/es.toml @@ -24,9 +24,17 @@ quit = "Salir de Cap" services = "Servicios" settings = "Ajustes…" +[menu.about] +licensed = "Licenciado bajo AGPL-3.0" +website = "Sitio web" +commit = "Commit" + [menu.file] close_window = "Cerrar ventana" title = "Archivo" +open_file = "Abrir archivo…" +open_recent = "Abrir reciente" +import_media = "Importar contenido…" [menu.edit] copy = "Copiar" @@ -49,12 +57,22 @@ zoom = "Zoom" [menu.help] title = "Ayuda" +feedback = "Dar comentarios…" +changelog = "Registro de cambios" +dashboard = "Panel" +documentation = "Documentación" +faq = "Preguntas frecuentes" +self_hosting = "Autoalojamiento" +help_center = "Centro de ayuda" +system_status = "Estado del sistema" +discord = "Unirse a la comunidad de Discord" [tray] import_media = "Importar contenido..." no_recent_items = "No hay elementos recientes" open_main_window = "Abrir ventana principal" previous = "Recientes" +recording_mode = "Modo de grabación" quit = "Salir de Cap" record_area = "Grabar área" record_display = "Grabar pantalla" @@ -239,6 +257,698 @@ title = "Privacidad" [settings.general.privacy.telemetry] description = "Cap usa telemetría anónima para mejorar la fiabilidad y corregir errores. Nunca recopilamos el contenido de las grabaciones, los títulos de las ventanas, las rutas de los archivos ni información personal." label = "Compartir telemetría anónima" +[settings.prompt] +ok = "Aceptar" + +[settings.sidebar] +account = "Cuenta" +cancel_sign_in = "Cancelar acceso" +check_for_updates = "Buscar actualizaciones" +checking_for_updates = "Buscando actualizaciones…" +click_to_sign_in = "Haz clic para iniciar sesión" +downloading = "Descargando v{version}…" +downloading_progress = "Descargando v{version}… {percent} %" +installing = "Instalando v{version}…" +previous_versions = "Ver versiones anteriores" +restart_to_update = "Reinicia para actualizar a v{version}" +sign_in = "Iniciar sesión" +sign_out = "Cerrar sesión" +signed_in = "Sesión iniciada" + +[settings.recordings] +corrupted_body = "La grabación falló, así que este archivo podría dar problemas en el editor. Si tienes problemas para recuperar el archivo, contacta con soporte." +corrupted_title = "Es posible que la grabación esté dañada" +delete = "Eliminar" +description = "Gestiona tus grabaciones y realiza acciones." +edit = "Editar" +empty_library = "No se encontraron grabaciones" +failed = "Error en la grabación" +import = "Importar" +in_progress = "Grabación en curso" +load_more = "Cargar más" +open_bundle = "Abrir el paquete de la grabación" +open_link = "Abrir enlace" +retry_upload = "Reintentar la subida" +show_all = "Mostrar todo" +title = "Grabaciones" + +[settings.recordings.empty] +all = "No hay grabaciones" +all_matching = "No hay grabaciones que coincidan" +instant = "No hay grabaciones Instant" +instant_matching = "No hay grabaciones Instant que coincidan" +studio = "No hay grabaciones Studio" +studio_matching = "No hay grabaciones Studio que coincidan" + +[settings.screenshots] +copy = "Copiar al portapapeles" +delete = "Eliminar" +description = "Gestiona tus capturas de pantalla y realiza acciones." +empty = "No hay capturas de pantalla" +empty_library = "No se encontraron capturas de pantalla" +empty_matching = "No hay capturas de pantalla que coincidan" +import = "Importar imagen" +load_more = "Cargar más" +open_folder = "Abrir carpeta" +save_as = "Guardar como" +title = "Capturas de pantalla" + +[settings.quality] +save_failed = "No se pudieron guardar tus ajustes de grabación. Inténtalo de nuevo." + +[settings.quality.studio] +balanced = "Equilibrado" +balanced_description = "Detalle nítido con un tamaño de archivo razonable. Ideal para la mayoría de grabaciones." +compatibility = "Archivos más pequeños" +compatibility_description = "Ahorra espacio en disco. Los detalles finos pueden suavizarse, sobre todo en la cámara." +description = "Se guardan en tu ordenador para editarlas. El tamaño de exportación lo eliges después, en el editor." +recommended = "Recomendado" +title = "Grabaciones Studio" +ultra = "Máximo detalle" +ultra_description = "Conserva el máximo detalle para ediciones intensas. Usa más espacio en disco." + +[settings.quality.instant] +description = "Se suben mientras grabas, así que el enlace está listo en cuanto paras. Para la mejor calidad, usa el modo Studio: graba con la máxima calidad que permite tu pantalla y puedes exportar hasta 4K." +free_plan = "720p en el plan gratuito. Cap Pro graba hasta 4K." +max_resolution = "Resolución máxima" +res_1080p = "Texto nítido con un tamaño de subida práctico." +res_1440p = "Más detalle para pantallas grandes de alta resolución." +res_4k = "El máximo detalle. Mejor con una conexión rápida." +res_720p = "Subidas más pequeñas, así los enlaces están listos antes." +title = "Grabaciones Instant" + +[settings.quality.sharing] +auto_open = "Abrir automáticamente los enlaces para compartir" +auto_open_description = "Abre el enlace en tu navegador cuando termine una subida." +title = "Compartir" + +[settings.options.studio_quality] +balanced = "Equilibrado" +compatibility = "Compatibilidad" +ultra = "Ultra" + +[settings.options.main_window_start] +close = "Cerrar" +minimise = "Minimizar" + +[settings.options.post_studio] +do_nothing = "No hacer nada" +open_editor = "Abrir el editor" +show_cap = "Mostrar Cap" + +[settings.options.post_screenshot] +copy = "Copiar al portapapeles" +copy_and_open_editor = "Copiar y abrir el editor" +do_nothing = "No hacer nada" +open_editor = "Abrir el editor" + +[settings.options.post_deletion] +do_nothing = "No hacer nada" +reopen = "Reabrir la grabadora" + +[settings.options.preview_quality] +full = "Completa" +half = "Mitad" +quarter = "Un cuarto" + +[settings.shortcuts] +blocked_while_recording = "No se pueden cambiar los atajos mientras esta grabación usa su tecla de detener. Detén la grabación primero." +needs_modifier_macos = "Incluye ⌘, ⌃ o ⌥ para que el atajo no interfiera al escribir." +needs_modifier_other = "Incluye Ctrl, Alt o Win para que el atajo no interfiera al escribir." +record_shortcut = "Grabar atajo" +recording_description = "Empieza a grabar sin abrir Cap primero." +recording_hint = "Pulsa las teclas que quieras usar. Esc cancela y Supr lo quita." +recording_title = "Grabación" +saved = "Guardado. Pulsa {keys} en cualquier app para {effect}." +saved_moved = "Guardado. Pulsa {keys} en cualquier app para {effect}. Se quitó de «{shortcut}»." +screenshots_description = "Los atajos funcionan desde cualquier app. Haz clic en uno para cambiarlo." +screenshots_title = "Capturas de pantalla" +type_shortcut = "Escribe el atajo…" +unavailable = "Otra app o el sistema ya usa este atajo. Haz clic para elegir otro." +unsupported_key = "Esa tecla no se puede usar en un atajo. Prueba con una letra, un número o una tecla F." +while_recording_title = "Durante la grabación" + +[settings.shortcuts.after_screenshot] +hint = "Para cada captura, desde un atajo o desde Cap" +title = "Después de una captura" + +[settings.shortcuts.after_studio] +hint = "Al detenerla" +title = "Después de una grabación Studio" + +[settings.shortcuts.after_instant] +hint = "El enlace para compartir siempre se copia" +title = "Después de una grabación Instant" + +[settings.shortcuts.outcome] +instant_copy_link = "Solo copiar el enlace" +instant_copy_link_hint = "No se abre nada" +instant_open_link = "Abrir el enlace" +instant_open_link_hint = "En tu navegador" +saved_to_library = "Se guarda en la biblioteca" +screenshot_copy = "Copiar" +screenshot_copy_and_edit = "Copiar y editar" +screenshot_copy_and_edit_hint = "Las dos cosas a la vez" +screenshot_copy_hint = "Pega donde quieras" +screenshot_do_nothing = "No hacer nada" +screenshot_open_editor = "Abrir el editor" +screenshot_open_editor_hint = "Anotar primero" +studio_do_nothing = "No hacer nada" +studio_open_editor = "Abrir el editor" +studio_open_editor_hint = "Editar enseguida" +studio_show_cap = "Mostrar Cap" +studio_show_cap_hint = "Volver a la grabadora" + +[settings.shortcuts.label] +cycle_mode = "Cambiar el modo de grabación" +fallback = "Atajo" +open_picker = "Abrir el selector de grabación" +pause = "Pausar o reanudar" +record_area = "Grabar un área" +record_display = "Grabar una pantalla" +record_window = "Grabar una ventana" +restart = "Reiniciar la grabación" +screenshot_area = "Capturar un área" +screenshot_display = "Capturar la pantalla" +screenshot_window = "Capturar una ventana" +start_instant = "Iniciar grabación Instant" +start_studio = "Iniciar grabación Studio" +stop = "Detener la grabación" + +[settings.shortcuts.effect] +cycle_mode = "cambiar entre los modos Studio, Instant y Captura de pantalla" +instant_copy = "grabar la pantalla o ventana seleccionada en modo Instant y copiar el enlace para compartir al detenerla" +instant_copy_and_open = "grabar la pantalla o ventana seleccionada en modo Instant y copiar y abrir el enlace para compartir al detenerla" +open_picker = "abrir Cap para elegir qué grabar" +pause = "pausar la grabación actual o reanudarla" +picker_area_instant = "seleccionar un área y luego grabarla en modo Instant" +picker_area_screenshot = "seleccionar un área y luego hacerle una captura de pantalla" +picker_area_studio = "seleccionar un área y luego grabarla en modo Studio" +picker_display_instant = "elegir una pantalla y luego grabarla en modo Instant" +picker_display_screenshot = "elegir una pantalla y luego hacerle una captura de pantalla" +picker_display_studio = "elegir una pantalla y luego grabarla en modo Studio" +picker_window_instant = "elegir una ventana y luego grabarla en modo Instant" +picker_window_screenshot = "elegir una ventana y luego hacerle una captura de pantalla" +picker_window_studio = "elegir una ventana y luego grabarla en modo Studio" +restart = "descartar la toma actual y empezar de nuevo" +screenshot_area_copy = "seleccionar un área y copiar la captura al portapapeles" +screenshot_area_copy_and_open_editor = "seleccionar un área, copiar la captura y abrir el editor" +screenshot_area_open_editor = "seleccionar un área y abrir la captura en el editor" +screenshot_area_save = "seleccionar un área y guardar la captura en tu biblioteca sin abrir nada" +screenshot_display_copy = "capturar la pantalla bajo tu puntero y copiar la captura al portapapeles" +screenshot_display_copy_and_open_editor = "capturar la pantalla bajo tu puntero, copiar la captura y abrir el editor" +screenshot_display_open_editor = "capturar la pantalla bajo tu puntero y abrir la captura en el editor" +screenshot_display_save = "capturar la pantalla bajo tu puntero y guardar la captura en tu biblioteca sin abrir nada" +screenshot_window_copy = "capturar la ventana bajo tu puntero y copiar la captura al portapapeles" +screenshot_window_copy_and_open_editor = "capturar la ventana bajo tu puntero, copiar la captura y abrir el editor" +screenshot_window_open_editor = "capturar la ventana bajo tu puntero y abrir la captura en el editor" +screenshot_window_save = "capturar la ventana bajo tu puntero y guardar la captura en tu biblioteca sin abrir nada" +stop = "detener la grabación actual" +studio = "grabar la pantalla o ventana seleccionada en modo Studio" +studio_open_editor = "grabar la pantalla o ventana seleccionada en modo Studio y abrir el editor al detenerla" + +[settings.shortcuts.sentence] +cycle_mode = "Cambiar entre los modos Studio, Instant y Captura de pantalla." +instant_copy = "Grabar la pantalla o ventana seleccionada en modo Instant y copiar el enlace para compartir al detenerla." +instant_copy_and_open = "Grabar la pantalla o ventana seleccionada en modo Instant y copiar y abrir el enlace para compartir al detenerla." +open_picker = "Abrir Cap para elegir qué grabar." +pause = "Pausar la grabación actual o reanudarla." +picker_area_instant = "Seleccionar un área y luego grabarla en modo Instant." +picker_area_screenshot = "Seleccionar un área y luego hacerle una captura de pantalla." +picker_area_studio = "Seleccionar un área y luego grabarla en modo Studio." +picker_display_instant = "Elegir una pantalla y luego grabarla en modo Instant." +picker_display_screenshot = "Elegir una pantalla y luego hacerle una captura de pantalla." +picker_display_studio = "Elegir una pantalla y luego grabarla en modo Studio." +picker_window_instant = "Elegir una ventana y luego grabarla en modo Instant." +picker_window_screenshot = "Elegir una ventana y luego hacerle una captura de pantalla." +picker_window_studio = "Elegir una ventana y luego grabarla en modo Studio." +restart = "Descartar la toma actual y empezar de nuevo." +screenshot_area_copy = "Seleccionar un área y copiar la captura al portapapeles." +screenshot_area_copy_and_open_editor = "Seleccionar un área, copiar la captura y abrir el editor." +screenshot_area_open_editor = "Seleccionar un área y abrir la captura en el editor." +screenshot_area_save = "Seleccionar un área y guardar la captura en tu biblioteca sin abrir nada." +screenshot_display_copy = "Capturar la pantalla bajo tu puntero y copiar la captura al portapapeles." +screenshot_display_copy_and_open_editor = "Capturar la pantalla bajo tu puntero, copiar la captura y abrir el editor." +screenshot_display_open_editor = "Capturar la pantalla bajo tu puntero y abrir la captura en el editor." +screenshot_display_save = "Capturar la pantalla bajo tu puntero y guardar la captura en tu biblioteca sin abrir nada." +screenshot_window_copy = "Capturar la ventana bajo tu puntero y copiar la captura al portapapeles." +screenshot_window_copy_and_open_editor = "Capturar la ventana bajo tu puntero, copiar la captura y abrir el editor." +screenshot_window_open_editor = "Capturar la ventana bajo tu puntero y abrir la captura en el editor." +screenshot_window_save = "Capturar la ventana bajo tu puntero y guardar la captura en tu biblioteca sin abrir nada." +stop = "Detener la grabación actual." +studio = "Grabar la pantalla o ventana seleccionada en modo Studio." +studio_open_editor = "Grabar la pantalla o ventana seleccionada en modo Studio y abrir el editor al detenerla." + +[settings.cli] +command = "Comando" +copy = "Copiar" +description = "Instala el comando Cap para terminales, agentes, scripts y automatización local." +install = "Instalar la CLI" +installed = "Instalada" +installing = "Instalando..." +installs_command = "La app de escritorio instala un comando local\u00A0{command}\u00A0que apunta a la CLI incluida." +load_failed = "No se pudo cargar el estado de la CLI: {error}" +not_installed = "No instalada" +path_configured = "Se añadió cap a tu PATH. Reinicia la terminal para usarlo o ejecuta esto ahora:" +path_missing = "Añade {path} a tu PATH para usar cap desde una nueva terminal." +remove = "Quitar" +removing = "Quitando..." +repair = "Reparar" +repairing = "Reparando..." +retry = "Reintentar" +target = "Destino" +title = "Línea de comandos" + +[settings.transcription] +active_title = "Pistas activas" +applied_note = "Estas pistas se aplican cuando generas subtítulos en el editor." +clear = "Borrar" +count = { one = "{count} elemento", many = "{count} de elementos", other = "{count} elementos" } +description = "Añade nombres, grafías, dominios y preferencias de mayúsculas que la generación de subtítulos debe tener en cuenta." +placeholder = "Añadir un término" +saved = "Guardado" +saving = "Guardando..." +terms_description = "Añade un término cada vez para reducir errores de escritura y de formato." +terms_title = "Términos guardados" +title = "Transcripción" + +[settings.experimental] +developer = "Desarrollador" +developer_description = "Solo se muestra en las compilaciones de desarrollo." +muxer = "Multiplexor fuera de proceso" +muxer_description = "Ejecuta el multiplexor de MP4 fragmentado en un subproceso aislado para que sus fallos no arruinen tu grabación. Requiere el binario cap-muxer incluido." +native_app = "Usar la app nativa" +native_app_description = "Desactívalo para cerrar esta app y volver a abrir la app Tauri clásica en el entorno de desarrollo. Las grabaciones y los ajustes se comparten." +reliability = "Fiabilidad" + +[settings.experimental.switch] +busy = "Termina la grabación, exportación, subida, importación o transcripción antes de cambiar a la app clásica." +check_failed = "No se pudo comprobar si la app clásica de Cap se abrió: {error}" +data_stays = "Tus grabaciones y ajustes se quedan exactamente donde están." +failed = "No se pudo abrir la app clásica de Cap." +not_found = "No se encontró la app clásica de Cap a la que volver." +open_failed = "No se pudo abrir la app clásica de Cap: {error}" +opening = "Abriendo la app clásica de Cap" +opening_detail = "Esta ventana se cerrará cuando la app clásica de Cap esté lista." +save_failed = "No se pudo guardar tu preferencia de app. Cap sigue abierto." +switching = "Volviendo a la app clásica de Cap." +timed_out = "La app clásica no se ha abierto. Revisa los errores de compilación en la terminal de desarrollo y vuelve a intentarlo." + +[settings.feedback] +app_version = "Versión de la app" +capture_support = "Compatibilidad de captura" +capture_supported = "Captura de pantalla: compatible" +capture_unsupported = "Captura de pantalla: no compatible" +community_description = "¿Tienes preguntas, quieres compartir ideas o simplemente pasar el rato? Únete a la comunidad de Cap en Discord." +community_title = "Únete a la comunidad" +debug_description = "Sube tus registros para ayudarnos a diagnosticar problemas con Cap. No se incluye información personal." +debug_title = "Información de depuración" +description = "Ayúdanos a mejorar Cap enviando comentarios o informando de errores. Nos pondremos con ello enseguida." +join_discord = "Unirse a Discord" +operating_system = "Sistema operativo" +placeholder = "Cuéntanos qué te parece Cap..." +submit = "Enviar comentarios" +submit_failed = "No se pudieron enviar los comentarios" +submitting = "Enviando..." +system_loading = "Cargando información del sistema..." +system_title = "Información del sistema" +thanks = "¡Gracias por tus comentarios!" +title = "Comentarios" +upload_interrupted = "La subida se interrumpió" +upload_logs = "Subir registros" +upload_sent = "Enviado. Gracias, esto nos ayuda a encontrar el problema." +uploading = "Subiendo..." + +[settings.feedback.diagnostic] +confirm_body = "Cap ocupará tu pantalla con un patrón parpadeante y emitirá pitidos fuertes durante unos {seconds} segundos por cada proceso. Quítate los auriculares, deja el volumen audible y no toques el equipo hasta que termine." +confirm_title = "¿Ejecutar el diagnóstico?" +description = "Graba un breve patrón de prueba con destellos y pitidos, mide cuánto se desfasan la imagen y el sonido, y registra tus pantallas, cámaras, micrófonos, espacio en disco y ajustes de grabación. No se envía nada hasta que tú decidas enviarlo." +mic_description = "También graba tu micrófono y comprueba que capta los pitidos a tiempo. Necesita altavoces que el micrófono pueda oír." +mic_label = "Probar el micrófono" +mode_description = "Qué proceso probar. Ambos ejecuta primero la parte Studio y luego la Instant." +mode_label = "Modo de grabación" +no_cli = "No se encontró la herramienta de línea de comandos de Cap junto a esta compilación, así que el informe solo cubrirá tu entorno." +no_cli_sync = "No se encontró la herramienta de línea de comandos de Cap junto a esta compilación, así que no se pudo ejecutar la prueba de sincronización." +run = "Ejecutar diagnóstico" +run_again = "Volver a ejecutar" +send = "Enviar a Cap" +sending = "Enviando..." +show_file = "Mostrar archivo" +stop_recording_first = "Detén la grabación actual antes de ejecutar un diagnóstico." +title = "Informe de diagnóstico" +try_again = "Intentar de nuevo" +warning_body = "Una ventana a pantalla completa parpadea en blanco y negro mientras suenan pitidos de 1 kHz por tus altavoces, y Cap lo graba y mide cuánto se han desfasado la imagen y el sonido. No toques el equipo mientras se ejecuta. Los dos modos juntos tardan un par de minutos." +warning_title = "La prueba ocupa tu pantalla y hace ruido." + +[settings.feedback.diagnostic.verdict] +environment_only = "SOLO ENTORNO" +fail = "FALLO" +inconclusive = "NO CONCLUYENTE" +pass = "CORRECTO" +warn = "AVISO" + +[settings.sign_in_required] +body = "Inicia sesión para continuar. También puedes escribir a hello@cap.so o unirte a nuestro Discord en cap.link/discord" +title = "Es necesario iniciar sesión" + +[settings.changelog] +fetch_failed = "No se pudo obtener el registro de cambios" +loading = "Cargando el registro de cambios..." +new = "NUEVO" +version = "Versión {version} - {date}" + +[settings.license] +activate = "Activar licencia" +activating = "Activando..." +active_title = "Licencia de escritorio" +checking = "Comprobando…" +deactivate = "Desactivar licencia" +description = "Tu plan de Cap y tu licencia de escritorio, en un solo lugar." +desktop_description = "Tu licencia de escritorio cubre la grabación y edición comercial. Cap Pro añade compartir en la nube y funciones de colaboración." +desktop_name = "Licencia de escritorio" +expires = "Caduca" +explore_description = "Compara los precios actuales y todo lo que incluye en nuestra web." +explore_desktop = "Licencia de escritorio · Uso comercial de la grabadora y el editor de escritorio." +explore_pro = "Cap Pro · Una licencia de escritorio, más compartir en la nube, funciones de IA y colaboración." +explore_title = "Explorar planes" +free_description = "Graba y edita en local para uso personal. Elige un plan de pago para uso comercial o más funciones en la nube." +have_key = "¿Tienes una clave de licencia?" +key = "Clave de licencia" +no_instance = "No se encontró el ID de instancia" +opens_pricing = "Abre cap.so/pricing en tu navegador." +placeholder = "Clave de licencia" +plan_title = "Tu plan" +pro_description = "Tu cuenta incluye compartir en la nube, funciones Pro y una licencia de escritorio para uso comercial." +refresh = "Actualizar el plan" +refresh_failed = "No se pudo actualizar tu plan. Inténtalo de nuevo." +save_failed = "No se pudo guardar la licencia" +sign_in_hint = "¿Ya tienes Cap Pro? Inicia sesión con tu cuenta desde la barra lateral." +title = "Plan y licencia" +view_pricing = "Ver planes y precios ↗" + +[settings.integrations] +configure = "Configurar" +description = "Configura integraciones para ampliar las funciones de Cap y conectarlo con servicios de terceros." +google_drive_description = "Conecta Google Drive para las nuevas subidas con enlace para compartir. Cap guarda los vídeos nuevos en una carpeta privada de Cap en tu Drive y los sigue sirviendo a través de Cap tras las comprobaciones de acceso habituales." +managed = "Gestionado por tu organización" +managed_by = "Gestionado por tu organización: {organization}" +request_timed_out = "La solicitud agotó el tiempo de espera" +s3_description = "Conecta tu propio bucket de S3 para tener control total sobre el almacenamiento de tus datos. Todas las nuevas subidas con enlace para compartir se subirán automáticamente a tu bucket de S3 configurado, para que conserves la propiedad y el control total de tu contenido. Ideal para organizaciones que necesitan soberanía de datos y políticas de almacenamiento propias." +s3_name = "Configuración de S3" +title = "Integraciones" +upgrade = "Mejorar a Pro" + +[settings.integrations.s3] +access_key = "ID de clave de acceso" +bucket = "Nombre del bucket" +configuration = "Configuración" +delete_failed = "No se pudo eliminar la configuración de S3" +deleted = "La configuración de S3 se eliminó correctamente" +endpoint = "Endpoint" +fetch_failed = "No se pudo obtener la configuración de S3" +guide = "guía de configuración del almacenamiento" +guide_intro = "Configurar y conectar tu bucket de almacenamiento a Cap debería llevar menos de 10 minutos. Consulta la\u00A0{guide}\u00A0para empezar." +other_provider = "Otro compatible con S3" +provider = "Proveedor de almacenamiento" +region = "Región" +remove = "Quitar configuración" +removing = "Quitando..." +save_failed = "No se pudo guardar la configuración de S3" +saved = "La configuración de S3 se guardó correctamente" +saving = "Guardando..." +secret_key = "Clave de acceso secreta" +test = "Probar conexión" +test_failed = "La prueba de conexión de S3 falló. Revisa la configuración y la conexión de red." +test_ok = "¡La prueba de la configuración de S3 fue correcta! La conexión funciona." +test_timeout = "La prueba de conexión agotó el tiempo tras 5 segundos. Revisa la URL del endpoint y la conexión de red." +testing = "Probando..." + +[settings.integrations.gdrive] +active = "Activo" +active_for_uploads = "Activo para las nuevas subidas" +cached = "En caché desde {time}" +connect = "Conectar Google Drive" +connect_failed = "No se pudo iniciar la conexión con Google Drive" +connected_inactive = "Conectado pero no activo" +connection_description = "Google Drive guarda las nuevas subidas en una carpeta privada de Cap en tu Drive. Los vídeos existentes alojados en Cap y en S3 siguen usando su almacenamiento actual." +connection_title = "Conexión" +disconnect = "Desconectar" +disconnect_failed = "No se pudo desconectar Google Drive" +disconnected = "Google Drive desconectado" +drive_files = "Archivos de Drive" +finish_in_browser = "Termina de conectar Google Drive en tu navegador, luego vuelve aquí y actualiza." +not_connected = "No conectado" +opening = "Abriendo..." +refresh = "Actualizar" +refreshing = "Actualizando..." +remaining = "Disponible" +set_active_failed = "No se pudo actualizar el proveedor de almacenamiento activo" +storage = "Almacenamiento" +test = "Probar" +test_failed = "La prueba de conexión de Google Drive falló" +testing = "Probando..." +trash = "Papelera" +updated = "Actualizado el {time}" +use_drive = "Usar Google Drive" +use_s3 = "Usar S3" +used = "{usage} usados" +used_of = "{usage} usados de {limit}" +waiting = "Esperando..." +working = "La conexión de Google Drive funciona" +working_for = "La conexión de Google Drive funciona para {email}" + +[settings.automations] +action_no_effect = "Esta acción no tiene efecto con el disparador seleccionado." +add_action = "Añadir acción" +add_condition = "Añadir condición" +auto_name_empty = "Automatización de {trigger}" +browse = "Examinar" +check_compatibility = "Comprobar compatibilidad" +compatibility_checked = "Compatibilidad comprobada: esta automatización todavía no se ejecuta en Cap. Las reglas guardadas siguen disponibles para la CLI de Cap. No se ejecutó ninguna acción." +condition_never_matches = "Esta condición nunca se cumple con el disparador seleccionado." +copy_link = "Copiar el enlace al portapapeles" +dangerous = "En la CLI de Cap, esta automatización ejecuta comandos o envía solicitudes de red con tus permisos. Usa solo valores de confianza." +delete = "Eliminar" +description = "Las reglas se comparten con la CLI de Cap. Cap guarda estas reglas, pero todavía no ejecuta automatizaciones." +empty_description = "Elige una plantilla de abajo para empezar con un clic o crea la tuya desde cero." +empty_title = "Aún no hay automatizaciones" +enabled = "Activada" +match_all = "Cumplir todas" +match_any = "Cumplir alguna" +name = "Nombre" +new = "Nueva automatización" +no_presets = "Aún no hay ajustes preestablecidos; crea uno primero en el editor." +only_if = "SOLO SI" +open_in_browser = "Abrir en el navegador" +retry_save = "Reintentar guardar" +run_through_shell = "Ejecutar a través del shell" +save_failed = "No se pudieron guardar las automatizaciones. Tus cambios no se han guardado." +start_from_scratch = "Empezar desde cero" +summary_empty = "{trigger} → aún no hay acciones" +templates_description = "Un clic para añadir una automatización lista para usar. Ajusta lo que quieras después." +templates_title = "Plantillas" +then = "ENTONCES HACER ESTO" +title = "Automatizaciones" +when = "CUANDO OCURRA ESTO" + +[settings.automations.status] +disabled_saved = "Desactivada · Guardada" +disabled_unsaved = "Desactivada · Sin guardar" +enabled_saved = "Activada · Guardada" +enabled_unsaved = "Activada · Sin guardar" + +[settings.automations.field] +arguments = "Argumentos (separados por espacios)" +body = "Cuerpo" +body_template = "Plantilla del cuerpo (opcional)" +compression = "Compresión" +destination = "Carpeta de destino (opcional, vacío = carpeta del proyecto)" +filename = "Plantilla de nombre de archivo (opcional)" +folder = "Carpeta" +format = "Formato" +frame_rate = "Velocidad de fotogramas" +method = "Método" +organization = "ID de la organización (opcional)" +preset = "Ajuste preestablecido" +program = "Programa" +resolution = "Resolución" +source = "Origen" +title = "Título" +url = "URL" + +[settings.automations.placeholder] +organization_id = "ID de la organización" +project_folder = "Carpeta del proyecto" +window_title = "p. ej., Slack" + +[settings.automations.clipboard] +original = "Captura original" +rendered = "Editada / renderizada" + +[settings.automations.target] +area = "Área" +display = "Pantalla" +window = "Ventana" + +[settings.automations.compression] +maximum = "Máxima" +potato = "Patata" +social = "Redes sociales" +web = "Web" + +[settings.automations.trigger.screenshot_taken] +label = "Al hacer una captura de pantalla" +noun = "captura de pantalla" +phrase = "Captura de pantalla hecha" +runs = "Se ejecuta con cada captura de pantalla." + +[settings.automations.trigger.studio_finished] +label = "Al terminar una grabación Studio" +noun = "grabación Studio" +phrase = "Termina una grabación Studio" +runs = "Se ejecuta al terminar cada grabación Studio." + +[settings.automations.trigger.instant_finished] +label = "Al terminar una grabación Instant" +noun = "grabación Instant" +phrase = "Termina una grabación Instant" +runs = "Se ejecuta al terminar cada grabación Instant." + +[settings.automations.trigger.recording_started] +label = "Al empezar una grabación" +noun = "inicio de grabación" +phrase = "Empieza una grabación" +runs = "Se ejecuta al empezar cada grabación." + +[settings.automations.trigger.upload_completed] +label = "Al completarse una subida" +noun = "subida" +phrase = "Se completa una subida" +runs = "Se ejecuta al completarse cada subida." + +[settings.automations.trigger.video_imported] +label = "Al importar un vídeo" +noun = "importación" +phrase = "Vídeo importado" +runs = "Se ejecuta con cada vídeo importado." + +[settings.automations.trigger.recording_deleted] +label = "Al eliminar una grabación" +noun = "eliminación" +phrase = "Grabación eliminada" +runs = "Se ejecuta con cada grabación eliminada." + +[settings.automations.action.copy_to_clipboard] +label = "Copiar al portapapeles" +noun = "Portapapeles" +short = "Copiar al portapapeles" + +[settings.automations.action.save_to_location] +label = "Guardar en una ubicación" +noun = "Carpeta" +short = "Guardar en carpeta" + +[settings.automations.action.export] +label = "Exportar con un perfil" +noun = "Exportación" +short = "Exportar" + +[settings.automations.action.upload] +label = "Subir + copiar enlace" +noun = "Subida" +short = "Subir y copiar enlace" + +[settings.automations.action.reveal] +label = "Mostrar en el gestor de archivos" +noun = "Mostrar" +short = "Mostrar en el gestor de archivos" + +[settings.automations.action.open_file] +label = "Abrir archivo" +noun = "Abrir" +short = "Abrir archivo" + +[settings.automations.action.ocr] +label = "Reconocer texto (OCR) al portapapeles" +noun = "Texto" +short = "Copiar texto (OCR)" + +[settings.automations.action.notify] +label = "Mostrar notificación" +noun = "Aviso" +short = "Notificar" + +[settings.automations.action.open_editor] +label = "Abrir el editor" +noun = "Editor" +short = "Abrir el editor" + +[settings.automations.action.skip_editor] +label = "Omitir el editor (sin interfaz)" +noun = "Sin editor" +short = "Omitir el editor" + +[settings.automations.action.apply_preset] +label = "Aplicar un ajuste del editor" +noun = "Ajuste" +short = "Aplicar ajuste" + +[settings.automations.action.run_command] +label = "Ejecutar comando" +noun = "Comando" +short = "Ejecutar comando" + +[settings.automations.action.webhook] +label = "Enviar webhook" +noun = "Webhook" +short = "Enviar webhook" + +[settings.automations.action.delete_local_files] +label = "Eliminar archivos locales" +noun = "Eliminar" +short = "Eliminar archivos locales" + +[settings.automations.condition] +capture_target = "El objetivo de captura es" +duration_at_least = "Duración mínima (segundos)" +duration_at_most = "Duración máxima (segundos)" +organization = "La organización es" +recording_mode = "El modo de grabación es" +window_title = "El título de la ventana contiene" + +[settings.automations.template.copy_screenshot] +description = "Haz una captura y la tendrás lista para pegar." +name = "Copiar automáticamente las nuevas capturas" + +[settings.automations.template.ocr_screenshot] +description = "Cap lee el texto de tu captura y lo copia por ti." +name = "Sacar el texto de las capturas" + +[settings.automations.template.save_screenshot] +description = "Envía cada captura nueva directamente a la carpeta que elijas." +name = "Guardar las capturas en una carpeta" + +[settings.automations.template.reveal_screenshot] +description = "Muestra cada captura en el Finder en cuanto la hagas." +name = "Ir a cada captura nueva" + +[settings.automations.template.export_studio] +description = "Renderiza un MP4 en cuanto termina una grabación Studio." +name = "Exportar automáticamente al terminar de grabar" + +[settings.automations.template.upload_share] +description = "Termina una grabación y el enlace te espera en el portapapeles." +name = "Subir y obtener el enlace para compartir" + +[settings.automations.template.notify_upload] +body = "Tu grabación está lista para compartir." +description = "Recibe un aviso discreto en el escritorio cuando tu grabación se pueda compartir." +name = "Avisarme cuando una subida esté lista" + +[settings.automations.template.webhook_share] +description = "Envía el enlace para compartir a Slack, Discord o tu propio webhook." +name = "Avisar a Slack cuando compartas algo" + [main] delete_recording_confirm = "¿Seguro que quieres eliminar esta grabación?" @@ -586,3 +1296,397 @@ click_or_press = "Haz clic aquí o pulsa" get_started = "Empezar" space_key = "Espacio" tagline = "La app de código abierto para compartir pantalla, para todo el mundo." + +[menu.update] +badge_progress = "v{version} · {percent} %" +badge_version = "v{version}" +check = "Buscar actualizaciones" +download = "Descargar actualización…" +checking = "Buscando actualizaciones…" +downloading = "Descargando actualización…" +restart = "Reiniciar para actualizar…" + +[settings.general.updates.download_policy] +auto = "Automático" +description = "Elige si Cap descarga las actualizaciones automáticamente." +label = "Descargas de actualizaciones" +manual = "Preguntar antes" +never = "Nunca" + +[auth] +sign_in_failed = "No se pudo iniciar sesión" + +[diagnostics] +already_running = "Ya hay un diagnóstico en curso." +mode_both = "Ambos" + +[diagnostics.error] +create_folder = "No se pudo crear la carpeta de diagnóstico: {error}" +exited = "La autoprueba terminó con {status}" +no_output = "La autoprueba no produjo ningún flujo de salida" +serialize = "No se pudo serializar el informe: {error}" +start_failed = "No se pudo iniciar {program}: {error}" +upload_logs = "No se pudieron subir los registros: {error}" +upload_status = "La subida falló con el estado: {status}" +wait_failed = "No se pudo esperar a la autoprueba: {error}" +write_failed = "No se pudo escribir {path}: {error}" + +[diagnostics.stage] +analyzing = "Analizando la grabación..." +analyzing_instant = "Analizando la grabación Instant..." +analyzing_studio = "Analizando la grabación Studio..." +cancelling = "Cancelando..." +collecting = "Recopilando información del sistema..." +done = "Terminando la prueba de sincronización..." +exporting = "Exportando la grabación..." +exporting_instant = "Exportando la grabación Instant..." +exporting_studio = "Exportando la grabación Studio..." +finalizing = "Finalizando la grabación..." +finalizing_instant = "Finalizando la grabación Instant..." +finalizing_studio = "Finalizando la grabación Studio..." +pattern = "Reproduciendo el patrón de prueba..." +pattern_instant = "Reproduciendo el patrón de prueba (Instant)..." +pattern_studio = "Reproduciendo el patrón de prueba (Studio)..." +preparing = "Preparando la prueba..." +preparing_instant = "Preparando la prueba Instant..." +preparing_studio = "Preparando la prueba Studio..." +recording = "Grabando la prueba..." +recording_instant = "Grabando la prueba Instant..." +recording_studio = "Grabando la prueba Studio..." +starting = "Iniciando el diagnóstico..." + +[dialog] +no_microphone_body = "Esta grabación no incluirá tu voz." +open_recording_failed = "No se pudo abrir esta grabación: {error}" + +[dialog.capture] +approve_shortcut = "Aprueba un atajo de grabación o activa el icono de detener de Cap en la bandeja cuando aparezca. La grabación no ha empezado." +area_unavailable = "El área seleccionada cambió o ya no está disponible. Vuelve a seleccionarla." +camera_needs_x11 = "En Linux, las superposiciones de cámara de Instant requieren un escritorio X11" +camera_notice = "Cap ocultará la vista previa de la cámara y los controles de grabación. La cámara seleccionada seguirá grabándose como una pista editable aparte." +camera_notice_instant = "Cap ocultará su vista previa y sus controles. La cámara seleccionada aparecerá en la posición de su vista previa con la forma y los efectos elegidos. Mantén toda la vista previa dentro del área de captura. Abrir Cap detendrá esta grabación Instant antes de mostrar sus controles." +camera_outside_area = "Mueve toda la vista previa de la cámara dentro del área de captura antes de iniciar la grabación Instant" +camera_preview_closed = "Abre la vista previa de la cámara seleccionada antes de grabar en Instant" +cleanup_incomplete = "La limpieza de la grabación no se completó: {error}. Se bloqueó una nueva grabación." +devices_changed = "Los dispositivos seleccionados cambiaron. Vuelve a empezar para confirmar las nuevas entradas de grabación." +devices_changed_before_start = "Los dispositivos seleccionados cambiaron antes de empezar la grabación. Vuelve a empezar para confirmar las entradas." +shortcut_conflict = "Ctrl+Shift+F9 está asignado a otra acción de Cap. Cambia ese atajo antes de grabar." +shortcut_description = "Iniciar o detener esta grabación de Cap" +shortcut_hint = "Pulsa y suelta {shortcut} para empezar. Usa el mismo atajo para detener. Este atajo temporal no cambia tus ajustes." +tray_stop_hint = "Activa el icono de detener de Cap en la bandeja para empezar. Vuelve a activarlo para detener. La grabación no empezará hasta que uses ese control." +window_unavailable = "La ventana seleccionada ya no está disponible. Vuelve a seleccionarla." + +[dialog.clipboard] +copy_file = "No se pudo copiar el archivo al portapapeles: {error}" +copy_file_failed = "No se pudo copiar el archivo al portapapeles" +copy_image = "No se pudo copiar la imagen al portapapeles: {error}" +copy_image_failed = "No se pudo copiar la imagen al portapapeles" +not_a_file = "Solo se pueden copiar archivos al portapapeles" +unavailable = "Portapapeles no disponible" +unavailable_error = "Portapapeles no disponible: {error}" + +[dialog.delete_recording] +already_deleting = "Esta grabación ya se está eliminando." +close_failed = "No se pudo cerrar el editor: {error}" +editor_changed = "El editor cambió mientras se confirmaba la eliminación. La grabación no se eliminó." +editor_closed = "El editor ya se cerró. La grabación no se eliminó." +failed = "No se pudo eliminar la grabación: {error}" +failed_title = "No se pudo eliminar la grabación" +finishing_body = "Cap está esperando a que termine la limpieza de la grabación antes de completar la eliminación." +finishing_title = "La eliminación aún está terminando" +in_use = "Esta grabación aún está en uso. Termina o cancela su subida e inténtalo de nuevo: {error}" +outside_library = "Esta grabación está fuera de la biblioteca de grabaciones. Sus archivos no se modificaron." +recording_active = "Termina o cancela la grabación antes de eliminar este proyecto." +retained_title = "Grabación conservada" + +[dialog.file] +audio_filter = "Audio" +busy = "Ya hay un cuadro de diálogo de archivos abierto. Termina o cancélalo antes de volver a intentarlo." +export_filter = "Exportación" +failed = "El cuadro de diálogo de archivos no pudo completarse: {error}" +images_filter = "Imágenes" +unavailable_title = "Cuadro de diálogo de archivos no disponible" +unconfirmed = "Cap no pudo confirmar que el cuadro de diálogo de archivos se cerró. Cierra cualquier cuadro de diálogo abierto y reinicia Cap antes de volver a intentarlo." + +[dialog.mode_select] +instant = "Comparte al instante con un enlace. Se sube mientras grabas." +screenshot = "Haz y anota capturas de pantalla al instante." +studio = "Grabación local de máxima calidad para editar después." +subtitle = "Elige cómo quieres capturar tu pantalla" +title = "Elegir modo de grabación" + +[dialog.storage] +stopped_kept = "La grabación se detuvo porque queda poco espacio. Tus archivos de grabación se conservaron. Libera espacio y recupera la grabación abajo." +stopped_saved = "La grabación se detuvo porque queda poco espacio. Tu grabación se guardó." +stopping = "Queda poco espacio. Deteniendo y guardando tu grabación…" + +[dialog.teleprompter] +cue_markers = "Marcadores" +hidden = "Esta ventana no aparece en las grabaciones de Cap" +mirror = "Texto en espejo" +paused = "Desplazamiento en pausa junto con la grabación" +placeholder = "Pega o escribe tu guion…" + +[import] +error_title = "Error de importación" +image_failed = "No se pudo importar la imagen: {error}" +unsupported = "Archivo multimedia no compatible." +video_failed = "No se pudo importar el video: {error}" + +[import.filter] +image = "Archivos de imagen" +media = "Archivos multimedia" +video = "Archivos de video" + +[import.progress] +analyzing = "Analizando el archivo de video..." +complete = "¡Importación completada!" +converting = "Convirtiendo el video... {percent} %" +image = "Importando la imagen..." +metadata = "Creando los metadatos del proyecto..." +starting = "Iniciando la conversión..." + +[overlay] +choose_mode = "Elegir modo de grabación" +countdown_tooltip = "Cuenta atrás de la grabación" +display_fallback = "Pantalla {id}" + +[overlay.area] +draw = "Dibuja un área" +free_ratio = "Libre" +hint = "Haz clic y arrastra para seleccionar un área" +minimum_size = "El tamaño mínimo es {width} x {height}" +too_small = "{width} x {height} es demasiado pequeño" + +[overlay.camera_only] +description = "Graba solo con tu cámara y tu micrófono" +select_camera = "Selecciona una cámara" + +[overlay.device] +camera_allow = "Permitir acceso a la cámara…" +camera_turn_on = "Activar el acceso a la cámara en Ajustes…" +microphone_allow = "Permitir acceso al micrófono…" +microphone_turn_on = "Activar el acceso al micrófono en Ajustes…" + +[overlay.start] +instant = "Modo Instant: graba un enlace para compartir" +instant_sign_in = "Modo Instant: inicia sesión para grabar al instante" +sign_in = "Inicia sesión para usar" +start_recording = "Iniciar grabación" +studio = "Modo Studio: graba y edita en local" +take_screenshot = "Hacer captura de pantalla" + +[permission.guide] +accessibility_body = "Suéltalo en la lista «Accesibilidad» de arriba y asegúrate de que su interruptor esté activado." +screen_body = "Suéltalo en la lista «Grabación de pantalla y audio del sistema» de arriba y asegúrate de que su interruptor esté activado." +title = "Arrastra Cap a la lista" + +[permission.name] +accessibility = "Accesibilidad" +camera = "Cámara" +microphone = "Micrófono" +screen_recording = "Grabación de pantalla" + +[permission.onboarding] +open_settings_again = "Abrir Ajustes del Sistema otra vez" +skip = "Omitir por ahora" +switch_on_title = "Activa Cap en Ajustes del Sistema" + +[permission.onboarding.accessibility] +allow = "Permitir accesibilidad" +body = "La accesibilidad permite que Cap vea tus clics y tu cursor, solo en este Mac, para que el zoom automático sepa dónde mirar." +done_body = "El zoom automático puede seguir tus clics." +done_title = "La accesibilidad está activada" +title = "Deja que Cap siga tu cursor" + +[permission.onboarding.all_set] +body = "Cap tiene todo lo que necesita. Ahora, un vistazo rápido a lo que puede hacer." +regained = "Gracias. Cap ya puede volver a grabar." +title = "Todo listo" + +[permission.onboarding.camera] +allow = "Permitir cámara" +body = "Aparece en una esquina de tus grabaciones. Omítelo si no lo necesitas." +done_body = "Puedes añadir tu cámara a cualquier grabación." +done_title = "La cámara está activada" +title = "Deja que Cap te vea" + +[permission.onboarding.checklist] +lede = "macOS te pedirá primero que autorices algunas cosas. Solo lleva un minuto." +regained_lede = "Algo se desactivó en Ajustes del Sistema. Vuelve a activarlo para seguir grabando." +regained_title = "Cap necesita recuperar un permiso" +title = "Preparemos Cap" + +[permission.onboarding.hint] +drag_into_list = "Arrastra Cap desde el panel bajo Ajustes del Sistema a la lista. ¿Ya aparece? Solo actívalo." +find_in_list = "Busca Cap en la lista y actívalo. Si macOS te preguntó antes, elige «Abrir Ajustes del Sistema»." +no_prompt = "¿No apareció ningún aviso? Activa Cap en Ajustes del Sistema." +open_settings = "Abre Ajustes del Sistema, busca Cap en la lista y actívalo." +turned_off = "Se desactivó antes, así que activa Cap en Ajustes del Sistema. O omítelo por ahora." + +[permission.onboarding.microphone] +allow = "Permitir micrófono" +body = "Añade tu voz a tus grabaciones. Puedes omitirlo y activarlo más tarde." +done_body = "Tu voz puede aparecer en tus grabaciones." +done_title = "El micrófono está activado" +title = "Deja que Cap te oiga" + +[permission.onboarding.relaunch] +body = "macOS activa la grabación de pantalla cuando Cap se reinicia. Continuarás justo aquí." +button = "Reiniciar" +hint = "¿Lo activaste y sigue esperando? Reinicia Cap para aplicarlo." +later = "macOS lo aplica cuando Cap se reinicia. Puedes hacerlo cuando termines aquí." +title = "Un reinicio y listo" + +[permission.onboarding.screen] +allow = "Permitir grabación de pantalla" +body = "La grabación de pantalla permite que Cap capture tus pantallas, ventanas y el audio del sistema." +done_body = "Cap puede capturar tu pantalla." +done_title = "La grabación de pantalla está activada" +title = "Deja que Cap vea tu pantalla" + +[permission.onboarding.status] +allowed = "Permitido" +off = "Desactivado en Ajustes del Sistema" +optional = "Opcional" +required = "Obligatorio" +skipped = "Omitido por ahora" + +[permission.onboarding.waiting] +prompt = "Elige «Permitir» en el aviso de macOS" +system_settings = "Esperando a Ajustes del Sistema" + +[screenshot_editor] +loading_preview = "Cargando vista previa..." +zoom_in = "Acercar" +zoom_out = "Alejar" + +[screenshot_editor.annotation] +default_text = "Texto" +done = "Listo" +fill = "Relleno" +intensity = "Intensidad" +mask_style = "Estilo" +pixelate = "Pixelar" +stroke = "Trazo" +transparent = "Transparente" + +[screenshot_editor.aspect] +auto = "Auto" +classic = "Clásico ⋅4:3" +square = "Cuadrado ⋅1:1" +tall = "Alto ⋅3:4" +vertical = "Vertical ⋅9:16" +wide = "Panorámico ⋅16:9" + +[screenshot_editor.background] +animated_gradient = "El degradado animado se muestra como imagen fija en las capturas de pantalla. Elige otro fondo para reemplazarlo." +blur_title = "Desenfoque del fondo" +choose_image = "Haz clic para elegir o arrastra y suelta una imagen" +color = "Color" +gradient = "Degradado" +image = "Imagen" +image_files = "Imagen" +image_title = "Imagen de fondo" +wallpaper = "Fondo" + +[screenshot_editor.corner] +rounded = "Redondeado" +squircle = "Squircle" +style = "ESTILO DE ESQUINAS" + +[screenshot_editor.crop] +full = "Completo" +loading_frame = "Cargando fotograma…" +position = "Posición" + +[screenshot_editor.error] +encode = "No se pudo codificar la captura de pantalla exportada: {error}" +load_meta = "No se pudieron cargar los metadatos de la captura de pantalla: {error}" +load_metadata = "No se pudieron cargar los metadatos de la captura de pantalla: {error}" +no_image = "No se encontró ningún PNG en {path}" +not_screenshot = "No es un paquete de captura de pantalla" +open_image = "No se pudo abrir la imagen: {error}" +renderer_failed = "No se pudo iniciar el renderizador de GPU: {error}" +renderer_not_running = "El renderizador de capturas de pantalla no está en ejecución" +renderer_stopped = "El renderizador se detuvo antes de terminar la exportación" +save_project = "Error al guardar el proyecto: {error}" +serialize = "No se pudo serializar la configuración de la captura de pantalla: {error}" +sign_in_to_share = "Inicia sesión en Cap en tu navegador y vuelve a compartir" +too_large = "Las dimensiones de la imagen superan el máximo: {width}x{height}" +upgrade_required = "Esta función requiere un plan superior" +upload_task = "La tarea de subida falló: {error}" + +[screenshot_editor.header] +aspect_ratio = "Relación de aspecto" +copy = "Copiar al portapapeles" +crop_image = "Recortar imagen" +more_actions = "Más acciones" + +[screenshot_editor.layers] +empty = "Aún no hay capas" +empty_hint = "Usa las herramientas de arriba para añadir anotaciones" +reorder_hint = "Arrastra para reordenar • Arriba = delante" +title = "Capas" + +[screenshot_editor.menu] +delete = "Eliminar" +open_folder = "Abrir carpeta" + +[screenshot_editor.share] +create_link = "Crear enlace para compartir" +preparing = "Preparando la subida" +rendering = "Renderizando la captura de pantalla" +uploading = "Subiendo la captura de pantalla" + +[screenshot_editor.style] +advanced_shadow = "Ajustes avanzados de sombra" +background = "Fondo" +blur = "Desenfoque" +border = "Borde" +color = "Color" +corner_rounding = "Redondeo de esquinas" +opacity = "Opacidad" +padding = "Margen" +rounding = "Redondeo" +shadow = "Sombra" +size_label = "Tamaño" +width = "Ancho" + +[screenshot_editor.toast] +copied = "¡Captura de pantalla copiada al portapapeles!" +link_copied = "Enlace para compartir copiado al portapapeles" +saved = "¡Captura de pantalla guardada!" + +[screenshot_editor.tool] +arrow = "Flecha" +circle = "Círculo" +draw = "Dibujar" +mask = "Máscara" +rectangle = "Rectángulo" +select = "Seleccionar" +text_label = "Texto" + +[transcription.download] +complete = "Descarga completada" +failed = "La descarga falló: {error}" +file_progress = "Descargando {file}: {percent} %" +preparing = "Preparando la descarga del modelo" +progress = "Descargando el modelo: {percent} %" + +[transcription.error] +intel_mac = "Los modelos Parakeet no están disponibles en Mac con Intel. Usa un modelo Whisper" +model_load = "No se pudo cargar el modelo de subtítulos. Intenta descargarlo de nuevo" +model_missing = "No se encontró el modelo de subtítulos. Descárgalo primero" +no_audio = "No se encontró audio en el archivo de video" +unsupported_language = "Los modelos Recomendado y Alta precisión solo transcriben inglés y otros 24 idiomas europeos. Descarga el modelo Pequeño o Mediano para subtitular este idioma" + +[upload.status] +failed = "La subida necesita atención" +paused = "Subida en pausa" +prepared = "Subida preparada" +processing = "Procesando la subida" +retrying = "La subida se reintentará" +sign_in = "Inicia sesión para reintentar la subida" +uploaded = "Subido" +uploading = "Subiendo" diff --git a/apps/desktop-gpui/locales/fr.toml b/apps/desktop-gpui/locales/fr.toml index e70141f4c4e..5c3b50f3b8f 100644 --- a/apps/desktop-gpui/locales/fr.toml +++ b/apps/desktop-gpui/locales/fr.toml @@ -24,9 +24,17 @@ quit = "Quitter Cap" services = "Services" settings = "Paramètres…" +[menu.about] +licensed = "Sous licence AGPL-3.0" +website = "Site web" +commit = "Commit" + [menu.file] close_window = "Fermer la fenêtre" title = "Fichier" +open_file = "Ouvrir un fichier…" +open_recent = "Ouvrir récent" +import_media = "Importer des médias…" [menu.edit] copy = "Copier" @@ -49,12 +57,22 @@ zoom = "Réduire/agrandir" [menu.help] title = "Aide" +feedback = "Donner son avis…" +changelog = "Journal des modifications" +dashboard = "Tableau de bord" +documentation = "Documentation" +faq = "FAQ" +self_hosting = "Auto-hébergement" +help_center = "Centre d'aide" +system_status = "État du système" +discord = "Rejoindre la communauté Discord" [tray] import_media = "Importer des médias..." no_recent_items = "Aucun élément récent" open_main_window = "Ouvrir la fenêtre principale" previous = "Récents" +recording_mode = "Mode d'enregistrement" quit = "Quitter Cap" record_area = "Enregistrer une zone" record_display = "Enregistrer l'écran" @@ -239,6 +257,698 @@ title = "Confidentialité" [settings.general.privacy.telemetry] description = "Cap utilise une télémétrie anonyme pour améliorer sa fiabilité et corriger des bugs. Nous ne collectons jamais le contenu des enregistrements, les titres de fenêtres, les chemins de fichiers ni aucune information personnelle." label = "Partager la télémétrie anonyme" +[settings.prompt] +ok = "OK" + +[settings.sidebar] +account = "Compte" +cancel_sign_in = "Annuler la connexion" +check_for_updates = "Rechercher des mises à jour" +checking_for_updates = "Recherche de mises à jour…" +click_to_sign_in = "Cliquez pour vous connecter" +downloading = "Téléchargement de la v{version}…" +downloading_progress = "Téléchargement de la v{version}… {percent} %" +installing = "Installation de la v{version}…" +previous_versions = "Voir les versions précédentes" +restart_to_update = "Redémarrer pour passer à la v{version}" +sign_in = "Se connecter" +sign_out = "Se déconnecter" +signed_in = "Connecté" + +[settings.recordings] +corrupted_body = "L'enregistrement a échoué, ce fichier risque de poser problème dans l'éditeur ! Si vous n'arrivez pas à récupérer le fichier, contactez le support !" +corrupted_title = "L'enregistrement est peut-être corrompu" +delete = "Supprimer" +description = "Gérez vos enregistrements et effectuez des actions." +edit = "Modifier" +empty_library = "Aucun enregistrement trouvé" +failed = "Échec de l'enregistrement" +import = "Importer" +in_progress = "Enregistrement en cours" +load_more = "Afficher plus" +open_bundle = "Ouvrir le dossier de l'enregistrement" +open_link = "Ouvrir le lien" +retry_upload = "Relancer la mise en ligne" +show_all = "Tout afficher" +title = "Enregistrements" + +[settings.recordings.empty] +all = "Aucun enregistrement" +all_matching = "Aucun enregistrement correspondant" +instant = "Aucun enregistrement Instant" +instant_matching = "Aucun enregistrement Instant correspondant" +studio = "Aucun enregistrement Studio" +studio_matching = "Aucun enregistrement Studio correspondant" + +[settings.screenshots] +copy = "Copier dans le presse-papiers" +delete = "Supprimer" +description = "Gérez vos captures d'écran et effectuez des actions." +empty = "Aucune capture d'écran" +empty_library = "Aucune capture d'écran trouvée" +empty_matching = "Aucune capture d'écran correspondante" +import = "Importer une image" +load_more = "Afficher plus" +open_folder = "Ouvrir le dossier" +save_as = "Enregistrer sous" +title = "Captures d'écran" + +[settings.quality] +save_failed = "Impossible d'enregistrer vos paramètres d'enregistrement. Veuillez réessayer." + +[settings.quality.studio] +balanced = "Équilibré" +balanced_description = "Des détails nets pour une taille de fichier raisonnable. Idéal pour la plupart des enregistrements." +compatibility = "Fichiers plus légers" +compatibility_description = "Économise de l'espace disque. Les détails fins peuvent s'adoucir, surtout sur la caméra." +description = "Enregistrés sur votre ordinateur pour être modifiés. Vous choisirez la taille d'export plus tard, dans l'éditeur." +recommended = "Recommandé" +title = "Enregistrements Studio" +ultra = "Détail maximal" +ultra_description = "Conserve un maximum de détails pour les retouches poussées. Utilise le plus d'espace disque." + +[settings.quality.instant] +description = "Mis en ligne pendant l'enregistrement, le lien est prêt dès que vous arrêtez. Pour une qualité optimale, utilisez le mode Studio : il enregistre dans la meilleure qualité permise par votre écran et vous pouvez exporter jusqu'en 4K." +free_plan = "720p avec l'offre gratuite. Cap Pro enregistre jusqu'en 4K." +max_resolution = "Résolution maximale" +res_1080p = "Texte net pour une taille de mise en ligne raisonnable." +res_1440p = "Plus de détails pour les grands écrans haute résolution." +res_4k = "Le plus de détails. Idéal avec une connexion rapide." +res_720p = "Mises en ligne les plus légères, les liens sont prêts plus vite." +title = "Enregistrements Instant" + +[settings.quality.sharing] +auto_open = "Ouvrir automatiquement les liens de partage" +auto_open_description = "Ouvre le lien dans votre navigateur à la fin d'une mise en ligne." +title = "Partage" + +[settings.options.studio_quality] +balanced = "Équilibré" +compatibility = "Compatibilité" +ultra = "Ultra" + +[settings.options.main_window_start] +close = "Fermer" +minimise = "Réduire" + +[settings.options.post_studio] +do_nothing = "Ne rien faire" +open_editor = "Ouvrir l'éditeur" +show_cap = "Afficher Cap" + +[settings.options.post_screenshot] +copy = "Copier dans le presse-papiers" +copy_and_open_editor = "Copier et ouvrir l'éditeur" +do_nothing = "Ne rien faire" +open_editor = "Ouvrir l'éditeur" + +[settings.options.post_deletion] +do_nothing = "Ne rien faire" +reopen = "Rouvrir l'enregistreur" + +[settings.options.preview_quality] +full = "Complète" +half = "Moitié" +quarter = "Quart" + +[settings.shortcuts] +blocked_while_recording = "Impossible de modifier les raccourcis tant que cet enregistrement utilise sa touche d'arrêt. Arrêtez d'abord l'enregistrement." +needs_modifier_macos = "Incluez ⌘, ⌃ ou ⌥ pour que le raccourci ne gêne pas la saisie." +needs_modifier_other = "Incluez Ctrl, Alt ou Win pour que le raccourci ne gêne pas la saisie." +record_shortcut = "Définir un raccourci" +recording_description = "Lancez un enregistrement sans ouvrir Cap au préalable." +recording_hint = "Appuyez sur les touches à utiliser. Échap annule, Suppr retire le raccourci." +recording_title = "Enregistrement" +saved = "Enregistré. Appuyez sur {keys} dans n'importe quelle app pour {effect}." +saved_moved = "Enregistré. Appuyez sur {keys} dans n'importe quelle app pour {effect}. Retiré de « {shortcut} »." +screenshots_description = "Les raccourcis fonctionnent depuis n'importe quelle app. Cliquez sur un raccourci pour le modifier." +screenshots_title = "Captures d'écran" +type_shortcut = "Saisissez un raccourci…" +unavailable = "Une autre app ou le système utilise déjà ce raccourci. Cliquez pour en choisir un autre." +unsupported_key = "Cette touche ne peut pas être utilisée dans un raccourci. Essayez une lettre, un chiffre ou une touche F." +while_recording_title = "Pendant l'enregistrement" + +[settings.shortcuts.after_screenshot] +hint = "Pour chaque capture, depuis un raccourci ou depuis Cap" +title = "Après une capture d'écran" + +[settings.shortcuts.after_studio] +hint = "À l'arrêt" +title = "Après un enregistrement Studio" + +[settings.shortcuts.after_instant] +hint = "Le lien de partage est toujours copié" +title = "Après un enregistrement Instant" + +[settings.shortcuts.outcome] +instant_copy_link = "Copier le lien seulement" +instant_copy_link_hint = "Rien ne s'ouvre" +instant_open_link = "Ouvrir le lien" +instant_open_link_hint = "Dans votre navigateur" +saved_to_library = "Enregistré dans la bibliothèque" +screenshot_copy = "Copier" +screenshot_copy_and_edit = "Copier et modifier" +screenshot_copy_and_edit_hint = "Les deux à la fois" +screenshot_copy_hint = "Coller n'importe où" +screenshot_do_nothing = "Ne rien faire" +screenshot_open_editor = "Ouvrir l'éditeur" +screenshot_open_editor_hint = "Annoter d'abord" +studio_do_nothing = "Ne rien faire" +studio_open_editor = "Ouvrir l'éditeur" +studio_open_editor_hint = "Modifier tout de suite" +studio_show_cap = "Afficher Cap" +studio_show_cap_hint = "Retour à l'enregistreur" + +[settings.shortcuts.label] +cycle_mode = "Changer de mode d'enregistrement" +fallback = "Raccourci" +open_picker = "Ouvrir le sélecteur d'enregistrement" +pause = "Pause ou reprise" +record_area = "Enregistrer une zone" +record_display = "Enregistrer un écran" +record_window = "Enregistrer une fenêtre" +restart = "Recommencer l'enregistrement" +screenshot_area = "Capturer une zone" +screenshot_display = "Capturer l'écran" +screenshot_window = "Capturer une fenêtre" +start_instant = "Démarrer un enregistrement Instant" +start_studio = "Démarrer un enregistrement Studio" +stop = "Arrêter l'enregistrement" + +[settings.shortcuts.effect] +cycle_mode = "passer d'un mode à l'autre : Studio, Instant et Capture d'écran" +instant_copy = "enregistrer l'écran ou la fenêtre sélectionnés en mode Instant, puis copier le lien de partage à l'arrêt" +instant_copy_and_open = "enregistrer l'écran ou la fenêtre sélectionnés en mode Instant, puis copier et ouvrir le lien de partage à l'arrêt" +open_picker = "ouvrir Cap pour choisir ce que vous voulez enregistrer" +pause = "mettre en pause l'enregistrement en cours, ou le reprendre" +picker_area_instant = "sélectionner une zone, puis l'enregistrer en mode Instant" +picker_area_screenshot = "sélectionner une zone, puis en faire une capture d'écran" +picker_area_studio = "sélectionner une zone, puis l'enregistrer en mode Studio" +picker_display_instant = "choisir un écran, puis l'enregistrer en mode Instant" +picker_display_screenshot = "choisir un écran, puis en faire une capture d'écran" +picker_display_studio = "choisir un écran, puis l'enregistrer en mode Studio" +picker_window_instant = "choisir une fenêtre, puis l'enregistrer en mode Instant" +picker_window_screenshot = "choisir une fenêtre, puis en faire une capture d'écran" +picker_window_studio = "choisir une fenêtre, puis l'enregistrer en mode Studio" +restart = "abandonner la prise en cours et recommencer" +screenshot_area_copy = "sélectionner une zone et copier la capture dans le presse-papiers" +screenshot_area_copy_and_open_editor = "sélectionner une zone, copier la capture et ouvrir l'éditeur" +screenshot_area_open_editor = "sélectionner une zone et ouvrir la capture dans l'éditeur" +screenshot_area_save = "sélectionner une zone et enregistrer la capture dans votre bibliothèque sans rien ouvrir" +screenshot_display_copy = "capturer l'écran sous votre pointeur et copier la capture dans le presse-papiers" +screenshot_display_copy_and_open_editor = "capturer l'écran sous votre pointeur, copier la capture et ouvrir l'éditeur" +screenshot_display_open_editor = "capturer l'écran sous votre pointeur et ouvrir la capture dans l'éditeur" +screenshot_display_save = "capturer l'écran sous votre pointeur et enregistrer la capture dans votre bibliothèque sans rien ouvrir" +screenshot_window_copy = "capturer la fenêtre sous votre pointeur et copier la capture dans le presse-papiers" +screenshot_window_copy_and_open_editor = "capturer la fenêtre sous votre pointeur, copier la capture et ouvrir l'éditeur" +screenshot_window_open_editor = "capturer la fenêtre sous votre pointeur et ouvrir la capture dans l'éditeur" +screenshot_window_save = "capturer la fenêtre sous votre pointeur et enregistrer la capture dans votre bibliothèque sans rien ouvrir" +stop = "arrêter l'enregistrement en cours" +studio = "enregistrer l'écran ou la fenêtre sélectionnés en mode Studio" +studio_open_editor = "enregistrer l'écran ou la fenêtre sélectionnés en mode Studio, puis ouvrir l'éditeur à l'arrêt" + +[settings.shortcuts.sentence] +cycle_mode = "Passer d'un mode à l'autre : Studio, Instant et Capture d'écran." +instant_copy = "Enregistrer l'écran ou la fenêtre sélectionnés en mode Instant, puis copier le lien de partage à l'arrêt." +instant_copy_and_open = "Enregistrer l'écran ou la fenêtre sélectionnés en mode Instant, puis copier et ouvrir le lien de partage à l'arrêt." +open_picker = "Ouvrir Cap pour choisir ce que vous voulez enregistrer." +pause = "Mettre en pause l'enregistrement en cours, ou le reprendre." +picker_area_instant = "Sélectionner une zone, puis l'enregistrer en mode Instant." +picker_area_screenshot = "Sélectionner une zone, puis en faire une capture d'écran." +picker_area_studio = "Sélectionner une zone, puis l'enregistrer en mode Studio." +picker_display_instant = "Choisir un écran, puis l'enregistrer en mode Instant." +picker_display_screenshot = "Choisir un écran, puis en faire une capture d'écran." +picker_display_studio = "Choisir un écran, puis l'enregistrer en mode Studio." +picker_window_instant = "Choisir une fenêtre, puis l'enregistrer en mode Instant." +picker_window_screenshot = "Choisir une fenêtre, puis en faire une capture d'écran." +picker_window_studio = "Choisir une fenêtre, puis l'enregistrer en mode Studio." +restart = "Abandonner la prise en cours et recommencer." +screenshot_area_copy = "Sélectionner une zone et copier la capture dans le presse-papiers." +screenshot_area_copy_and_open_editor = "Sélectionner une zone, copier la capture et ouvrir l'éditeur." +screenshot_area_open_editor = "Sélectionner une zone et ouvrir la capture dans l'éditeur." +screenshot_area_save = "Sélectionner une zone et enregistrer la capture dans votre bibliothèque sans rien ouvrir." +screenshot_display_copy = "Capturer l'écran sous votre pointeur et copier la capture dans le presse-papiers." +screenshot_display_copy_and_open_editor = "Capturer l'écran sous votre pointeur, copier la capture et ouvrir l'éditeur." +screenshot_display_open_editor = "Capturer l'écran sous votre pointeur et ouvrir la capture dans l'éditeur." +screenshot_display_save = "Capturer l'écran sous votre pointeur et enregistrer la capture dans votre bibliothèque sans rien ouvrir." +screenshot_window_copy = "Capturer la fenêtre sous votre pointeur et copier la capture dans le presse-papiers." +screenshot_window_copy_and_open_editor = "Capturer la fenêtre sous votre pointeur, copier la capture et ouvrir l'éditeur." +screenshot_window_open_editor = "Capturer la fenêtre sous votre pointeur et ouvrir la capture dans l'éditeur." +screenshot_window_save = "Capturer la fenêtre sous votre pointeur et enregistrer la capture dans votre bibliothèque sans rien ouvrir." +stop = "Arrêter l'enregistrement en cours." +studio = "Enregistrer l'écran ou la fenêtre sélectionnés en mode Studio." +studio_open_editor = "Enregistrer l'écran ou la fenêtre sélectionnés en mode Studio, puis ouvrir l'éditeur à l'arrêt." + +[settings.cli] +command = "Commande" +copy = "Copier" +description = "Installez la commande Cap pour les terminaux, les agents, les scripts et l'automatisation locale." +install = "Installer la CLI" +installed = "Installée" +installing = "Installation..." +installs_command = "L'app de bureau installe une commande locale\u00A0{command}\u00A0qui renvoie vers la CLI intégrée." +load_failed = "Impossible de charger l'état de la CLI : {error}" +not_installed = "Non installée" +path_configured = "cap a été ajouté à votre PATH. Redémarrez votre terminal pour l'utiliser, ou exécutez ceci maintenant :" +path_missing = "Ajoutez {path} à votre PATH pour utiliser cap depuis un nouveau terminal." +remove = "Supprimer" +removing = "Suppression..." +repair = "Réparer" +repairing = "Réparation..." +retry = "Réessayer" +target = "Cible" +title = "Ligne de commande" + +[settings.transcription] +active_title = "Indications actives" +applied_note = "Ces indications sont appliquées lorsque vous générez des sous-titres dans l'éditeur." +clear = "Effacer" +count = { one = "{count} élément", many = "{count} d'éléments", other = "{count} éléments" } +description = "Ajoutez les noms, orthographes, domaines et préférences de majuscules que la génération des sous-titres doit prendre en compte." +placeholder = "Ajouter un terme" +saved = "Enregistré" +saving = "Enregistrement..." +terms_description = "Ajoutez un terme à la fois pour limiter les fautes de frappe et de mise en forme." +terms_title = "Termes mémorisés" +title = "Transcription" + +[settings.experimental] +developer = "Développeur" +developer_description = "Affiché uniquement dans les versions de développement." +muxer = "Multiplexeur hors processus" +muxer_description = "Exécute le multiplexeur MP4 fragmenté dans un sous-processus isolé pour que ses plantages ne fassent pas échouer votre enregistrement. Nécessite le binaire cap-muxer fourni." +native_app = "Utiliser l'app native" +native_app_description = "Désactivez pour fermer cette app et rouvrir l'app Tauri classique dans l'environnement de développement. Les enregistrements et les paramètres sont partagés." +reliability = "Fiabilité" + +[settings.experimental.switch] +busy = "Terminez votre enregistrement, export, mise en ligne, import ou transcription avant de passer à l'app classique." +check_failed = "Impossible de vérifier si l'app Cap classique s'est ouverte : {error}" +data_stays = "Vos enregistrements et vos paramètres restent exactement où ils sont." +failed = "Impossible d'ouvrir l'app Cap classique." +not_found = "Impossible de trouver l'app Cap classique vers laquelle revenir." +open_failed = "Impossible d'ouvrir l'app Cap classique : {error}" +opening = "Ouverture de l'app Cap classique" +opening_detail = "Cette fenêtre se fermera quand l'app Cap classique sera prête." +save_failed = "Impossible d'enregistrer votre préférence d'app. Cap reste ouvert." +switching = "Retour à l'app Cap classique." +timed_out = "L'app classique ne s'est pas ouverte. Vérifiez les erreurs de compilation dans le terminal de développement, puis réessayez." + +[settings.feedback] +app_version = "Version de l'app" +capture_support = "Prise en charge de la capture" +capture_supported = "Capture d'écran : prise en charge" +capture_unsupported = "Capture d'écran : non prise en charge" +community_description = "Des questions, des idées à partager, ou simplement envie de discuter ? Rejoignez la communauté Discord de Cap." +community_title = "Rejoindre la communauté" +debug_description = "Envoyez vos journaux pour nous aider à diagnostiquer les problèmes de Cap. Aucune information personnelle n'est incluse." +debug_title = "Informations de débogage" +description = "Aidez-nous à améliorer Cap en envoyant vos commentaires ou en signalant des bugs. Nous nous en occupons tout de suite." +join_discord = "Rejoindre Discord" +operating_system = "Système d'exploitation" +placeholder = "Dites-nous ce que vous pensez de Cap..." +submit = "Envoyer les commentaires" +submit_failed = "Échec de l'envoi des commentaires" +submitting = "Envoi..." +system_loading = "Chargement des informations système..." +system_title = "Informations système" +thanks = "Merci pour vos commentaires !" +title = "Commentaires" +upload_interrupted = "L'envoi a été interrompu" +upload_logs = "Envoyer les journaux" +upload_sent = "Envoyé. Merci, cela nous aide à trouver le problème." +uploading = "Envoi..." + +[settings.feedback.diagnostic] +confirm_body = "Cap va occuper votre écran avec un motif clignotant et émettre des bips forts pendant environ {seconds} secondes par chaîne de traitement. Retirez votre casque, laissez le volume audible et ne touchez pas à l'ordinateur jusqu'à la fin." +confirm_title = "Lancer le diagnostic ?" +description = "Enregistre une courte mire clignotante et sonore, mesure le décalage entre l'image et le son, et relève vos écrans, caméras, micros, espace disque et paramètres d'enregistrement. Rien n'est envoyé tant que vous ne le décidez pas." +mic_description = "Enregistre aussi votre micro et vérifie qu'il capte les bips à temps. Nécessite des haut-parleurs que le micro peut réellement entendre." +mic_label = "Tester le micro" +mode_description = "La chaîne de traitement à tester. Les deux exécute d'abord la partie Studio, puis la partie Instant." +mode_label = "Mode d'enregistrement" +no_cli = "L'outil en ligne de commande Cap est introuvable à côté de cette version, le rapport ne couvrira donc que votre environnement." +no_cli_sync = "L'outil en ligne de commande Cap est introuvable à côté de cette version, le test de synchronisation n'a donc pas pu s'exécuter." +run = "Lancer le diagnostic" +run_again = "Relancer" +send = "Envoyer à Cap" +sending = "Envoi..." +show_file = "Afficher le fichier" +stop_recording_first = "Arrêtez l'enregistrement en cours avant de lancer un diagnostic." +title = "Rapport de diagnostic" +try_again = "Réessayer" +warning_body = "Une fenêtre plein écran clignote en noir et blanc pendant que des bips à 1 kHz sortent de vos haut-parleurs ; Cap l'enregistre et mesure le décalage entre l'image et le son. Ne touchez pas à l'ordinateur pendant le test. Les deux modes prennent quelques minutes au total." +warning_title = "Le test occupe votre écran et fait du bruit." + +[settings.feedback.diagnostic.verdict] +environment_only = "ENVIRONNEMENT SEUL" +fail = "ÉCHEC" +inconclusive = "NON CONCLUANT" +pass = "RÉUSSI" +warn = "AVERTISSEMENT" + +[settings.sign_in_required] +body = "Connectez-vous pour continuer. Vous pouvez aussi écrire à hello@cap.so ou rejoindre notre Discord sur cap.link/discord" +title = "Connexion requise" + +[settings.changelog] +fetch_failed = "Impossible de récupérer les nouveautés" +loading = "Chargement des nouveautés..." +new = "NOUVEAU" +version = "Version {version} - {date}" + +[settings.license] +activate = "Activer la licence" +activating = "Activation..." +active_title = "Licence de bureau" +checking = "Vérification…" +deactivate = "Désactiver la licence" +description = "Votre offre Cap et votre licence de bureau, au même endroit." +desktop_description = "Votre licence de bureau couvre l'enregistrement et le montage à usage commercial. Cap Pro ajoute le partage dans le cloud et les fonctionnalités collaboratives." +desktop_name = "Licence de bureau" +expires = "Expire le" +explore_description = "Comparez les tarifs actuels et tout ce qui est inclus sur notre site." +explore_desktop = "Licence de bureau · Usage commercial de l'enregistreur et de l'éditeur de bureau." +explore_pro = "Cap Pro · Une licence de bureau, plus le partage dans le cloud, les fonctionnalités IA et la collaboration." +explore_title = "Découvrir les offres" +free_description = "Enregistrez et montez en local pour un usage personnel. Choisissez une offre payante pour un usage commercial ou plus de fonctionnalités cloud." +have_key = "Vous avez une clé de licence ?" +key = "Clé de licence" +no_instance = "Aucun identifiant d'instance trouvé" +opens_pricing = "Ouvre cap.so/pricing dans votre navigateur." +placeholder = "Clé de licence" +plan_title = "Votre offre" +pro_description = "Votre compte inclut le partage dans le cloud, les fonctionnalités Pro et une licence de bureau pour un usage commercial." +refresh = "Actualiser l'offre" +refresh_failed = "Impossible d'actualiser votre offre. Veuillez réessayer." +save_failed = "Impossible d'enregistrer la licence" +sign_in_hint = "Vous avez déjà Cap Pro ? Connectez-vous à votre compte depuis la barre latérale." +title = "Offre et licence" +view_pricing = "Voir les offres et tarifs ↗" + +[settings.integrations] +configure = "Configurer" +description = "Configurez des intégrations pour étendre les fonctionnalités de Cap et le connecter à des services tiers." +google_drive_description = "Connectez Google Drive pour les nouvelles mises en ligne avec lien de partage. Cap stocke les nouvelles vidéos dans un dossier Cap privé de votre Drive et continue de les diffuser via Cap après les vérifications d'accès habituelles." +managed = "Géré par votre organisation" +managed_by = "Géré par votre organisation : {organization}" +request_timed_out = "La requête a expiré" +s3_description = "Connectez votre propre bucket S3 pour garder la maîtrise totale du stockage de vos données. Toutes les nouvelles mises en ligne avec lien de partage iront automatiquement dans le bucket S3 configuré, pour que vous restiez propriétaire de votre contenu et en gardiez le contrôle. Idéal pour les organisations qui exigent la souveraineté des données et des politiques de stockage personnalisées." +s3_name = "Configuration S3" +title = "Intégrations" +upgrade = "Passer à Pro" + +[settings.integrations.s3] +access_key = "ID de clé d'accès" +bucket = "Nom du bucket" +configuration = "Configuration" +delete_failed = "Impossible de supprimer la configuration S3" +deleted = "Configuration S3 supprimée" +endpoint = "Point de terminaison" +fetch_failed = "Impossible de récupérer la configuration S3" +guide = "guide de configuration du stockage" +guide_intro = "La configuration et la connexion de votre bucket à Cap devraient prendre moins de 10 minutes. Consultez le\u00A0{guide}\u00A0pour commencer." +other_provider = "Autre compatible S3" +provider = "Fournisseur de stockage" +region = "Région" +remove = "Supprimer la configuration" +removing = "Suppression..." +save_failed = "Impossible d'enregistrer la configuration S3" +saved = "Configuration S3 enregistrée" +saving = "Enregistrement..." +secret_key = "Clé d'accès secrète" +test = "Tester la connexion" +test_failed = "Échec du test de connexion S3. Vérifiez votre configuration et votre connexion réseau." +test_ok = "Test de la configuration S3 réussi ! La connexion fonctionne." +test_timeout = "Le test de connexion a expiré au bout de 5 secondes. Vérifiez l'URL du point de terminaison et votre connexion réseau." +testing = "Test..." + +[settings.integrations.gdrive] +active = "Actif" +active_for_uploads = "Actif pour les nouvelles mises en ligne" +cached = "En cache depuis le {time}" +connect = "Connecter Google Drive" +connect_failed = "Impossible de lancer la connexion à Google Drive" +connected_inactive = "Connecté mais inactif" +connection_description = "Google Drive stocke les nouvelles mises en ligne dans un dossier Cap privé de votre Drive. Les vidéos existantes hébergées par Cap ou sur S3 restent sur leur stockage actuel." +connection_title = "Connexion" +disconnect = "Déconnecter" +disconnect_failed = "Impossible de déconnecter Google Drive" +disconnected = "Google Drive déconnecté" +drive_files = "Fichiers Drive" +finish_in_browser = "Terminez la connexion à Google Drive dans votre navigateur, puis revenez ici et actualisez." +not_connected = "Non connecté" +opening = "Ouverture..." +refresh = "Actualiser" +refreshing = "Actualisation..." +remaining = "Restant" +set_active_failed = "Impossible de changer de fournisseur de stockage actif" +storage = "Stockage" +test = "Tester" +test_failed = "Échec du test de connexion Google Drive" +testing = "Test..." +trash = "Corbeille" +updated = "Mis à jour le {time}" +use_drive = "Utiliser Google Drive" +use_s3 = "Utiliser S3" +used = "{usage} utilisés" +used_of = "{usage} utilisés sur {limit}" +waiting = "En attente..." +working = "La connexion Google Drive fonctionne" +working_for = "La connexion Google Drive fonctionne pour {email}" + +[settings.automations] +action_no_effect = "Cette action n'a aucun effet avec le déclencheur sélectionné." +add_action = "Ajouter une action" +add_condition = "Ajouter une condition" +auto_name_empty = "Automatisation {trigger}" +browse = "Parcourir" +check_compatibility = "Vérifier la compatibilité" +compatibility_checked = "Compatibilité vérifiée : cette automatisation ne s'exécute pas encore dans Cap. Les règles enregistrées restent disponibles pour la CLI Cap. Aucune action n'a été exécutée." +condition_never_matches = "Cette condition ne correspond jamais au déclencheur sélectionné." +copy_link = "Copier le lien dans le presse-papiers" +dangerous = "Dans la CLI Cap, cette automatisation exécute des commandes ou envoie des requêtes réseau avec vos autorisations. N'utilisez que des valeurs de confiance." +delete = "Supprimer" +description = "Les règles sont partagées avec la CLI Cap. Cap enregistre ces règles mais n'exécute pas encore les automatisations." +empty_description = "Choisissez un modèle ci-dessous pour démarrer en un clic, ou créez la vôtre de zéro." +empty_title = "Aucune automatisation pour l'instant" +enabled = "Activée" +match_all = "Toutes les conditions" +match_any = "Au moins une condition" +name = "Nom" +new = "Nouvelle automatisation" +no_presets = "Aucun préréglage pour l'instant : créez-en un d'abord dans l'éditeur." +only_if = "UNIQUEMENT SI" +open_in_browser = "Ouvrir dans le navigateur" +retry_save = "Réessayer l'enregistrement" +run_through_shell = "Exécuter via le shell" +save_failed = "Impossible d'enregistrer les automatisations. Vos modifications n'ont pas été enregistrées." +start_from_scratch = "Partir de zéro" +summary_empty = "{trigger} → aucune action pour l'instant" +templates_description = "Un clic pour ajouter une automatisation toute prête. Ajustez ce que vous voulez ensuite." +templates_title = "Modèles" +then = "ALORS FAIRE CECI" +title = "Automatisations" +when = "QUAND CECI SE PRODUIT" + +[settings.automations.status] +disabled_saved = "Désactivée · Enregistrée" +disabled_unsaved = "Désactivée · Non enregistrée" +enabled_saved = "Activée · Enregistrée" +enabled_unsaved = "Activée · Non enregistrée" + +[settings.automations.field] +arguments = "Arguments (séparés par des espaces)" +body = "Corps" +body_template = "Modèle de corps (facultatif)" +compression = "Compression" +destination = "Dossier de destination (facultatif, vide = dossier du projet)" +filename = "Modèle de nom de fichier (facultatif)" +folder = "Dossier" +format = "Format" +frame_rate = "Fréquence d'images" +method = "Méthode" +organization = "ID de l'organisation (facultatif)" +preset = "Préréglage" +program = "Programme" +resolution = "Résolution" +source = "Source" +title = "Titre" +url = "URL" + +[settings.automations.placeholder] +organization_id = "ID de l'organisation" +project_folder = "Dossier du projet" +window_title = "ex. Slack" + +[settings.automations.clipboard] +original = "Capture d'origine" +rendered = "Modifiée / rendue" + +[settings.automations.target] +area = "Zone" +display = "Écran" +window = "Fenêtre" + +[settings.automations.compression] +maximum = "Maximale" +potato = "Patate" +social = "Réseaux sociaux" +web = "Web" + +[settings.automations.trigger.screenshot_taken] +label = "À chaque capture d'écran" +noun = "capture d'écran" +phrase = "Capture d'écran prise" +runs = "S'exécute à chaque capture d'écran." + +[settings.automations.trigger.studio_finished] +label = "À la fin d'un enregistrement Studio" +noun = "enregistrement Studio" +phrase = "Fin d'un enregistrement Studio" +runs = "S'exécute à la fin de chaque enregistrement Studio." + +[settings.automations.trigger.instant_finished] +label = "À la fin d'un enregistrement Instant" +noun = "enregistrement Instant" +phrase = "Fin d'un enregistrement Instant" +runs = "S'exécute à la fin de chaque enregistrement Instant." + +[settings.automations.trigger.recording_started] +label = "Au début d'un enregistrement" +noun = "début d'enregistrement" +phrase = "Début d'un enregistrement" +runs = "S'exécute au début de chaque enregistrement." + +[settings.automations.trigger.upload_completed] +label = "À la fin d'une mise en ligne" +noun = "mise en ligne" +phrase = "Fin d'une mise en ligne" +runs = "S'exécute à la fin de chaque mise en ligne." + +[settings.automations.trigger.video_imported] +label = "À chaque import de vidéo" +noun = "import" +phrase = "Vidéo importée" +runs = "S'exécute à chaque import de vidéo." + +[settings.automations.trigger.recording_deleted] +label = "À chaque suppression d'enregistrement" +noun = "suppression" +phrase = "Enregistrement supprimé" +runs = "S'exécute à chaque suppression d'enregistrement." + +[settings.automations.action.copy_to_clipboard] +label = "Copier dans le presse-papiers" +noun = "Presse-papiers" +short = "Copier dans le presse-papiers" + +[settings.automations.action.save_to_location] +label = "Enregistrer à un emplacement" +noun = "Dossier" +short = "Enregistrer dans un dossier" + +[settings.automations.action.export] +label = "Exporter avec un profil" +noun = "Export" +short = "Exporter" + +[settings.automations.action.upload] +label = "Mettre en ligne + copier le lien" +noun = "Mise en ligne" +short = "Mettre en ligne et copier le lien" + +[settings.automations.action.reveal] +label = "Afficher dans le gestionnaire de fichiers" +noun = "Affichage" +short = "Afficher dans le gestionnaire de fichiers" + +[settings.automations.action.open_file] +label = "Ouvrir le fichier" +noun = "Ouverture" +short = "Ouvrir le fichier" + +[settings.automations.action.ocr] +label = "Reconnaître le texte (OCR) vers le presse-papiers" +noun = "Texte" +short = "Copier le texte (OCR)" + +[settings.automations.action.notify] +label = "Afficher une notification" +noun = "Notification" +short = "Notifier" + +[settings.automations.action.open_editor] +label = "Ouvrir l'éditeur" +noun = "Éditeur" +short = "Ouvrir l'éditeur" + +[settings.automations.action.skip_editor] +label = "Ignorer l'éditeur (sans interface)" +noun = "Sans éditeur" +short = "Ignorer l'éditeur" + +[settings.automations.action.apply_preset] +label = "Appliquer un préréglage de l'éditeur" +noun = "Préréglage" +short = "Appliquer un préréglage" + +[settings.automations.action.run_command] +label = "Exécuter une commande" +noun = "Commande" +short = "Exécuter une commande" + +[settings.automations.action.webhook] +label = "Envoyer un webhook" +noun = "Webhook" +short = "Envoyer un webhook" + +[settings.automations.action.delete_local_files] +label = "Supprimer les fichiers locaux" +noun = "Suppression" +short = "Supprimer les fichiers locaux" + +[settings.automations.condition] +capture_target = "La cible de capture est" +duration_at_least = "Durée minimale (secondes)" +duration_at_most = "Durée maximale (secondes)" +organization = "L'organisation est" +recording_mode = "Le mode d'enregistrement est" +window_title = "Le titre de la fenêtre contient" + +[settings.automations.template.copy_screenshot] +description = "Prenez une capture, elle est aussitôt prête à coller." +name = "Copier automatiquement les nouvelles captures" + +[settings.automations.template.ocr_screenshot] +description = "Cap lit le texte de votre capture et le copie pour vous." +name = "Extraire le texte des captures" + +[settings.automations.template.save_screenshot] +description = "Envoyez chaque nouvelle capture directement dans le dossier de votre choix." +name = "Ranger les captures dans un dossier" + +[settings.automations.template.reveal_screenshot] +description = "Affichez chaque capture dans le Finder dès que vous la prenez." +name = "Afficher chaque nouvelle capture" + +[settings.automations.template.export_studio] +description = "Générez un MP4 dès la fin d'un enregistrement Studio." +name = "Exporter automatiquement en fin d'enregistrement" + +[settings.automations.template.upload_share] +description = "Terminez un enregistrement et le lien vous attend dans le presse-papiers." +name = "Mettre en ligne et récupérer le lien" + +[settings.automations.template.notify_upload] +body = "Votre enregistrement est prêt à être partagé." +description = "Recevez une discrète notification dès que votre enregistrement peut être partagé." +name = "Me prévenir quand une mise en ligne est prête" + +[settings.automations.template.webhook_share] +description = "Envoyez le lien de partage à Slack, Discord ou votre propre webhook." +name = "Prévenir Slack quand vous partagez" + [main] delete_recording_confirm = "Voulez-vous vraiment supprimer cet enregistrement ?" @@ -586,3 +1296,397 @@ click_or_press = "Cliquez ici ou appuyez sur" get_started = "Commencer" space_key = "Espace" tagline = "L'app open source de partage d'écran pour tout le monde." + +[menu.update] +badge_progress = "v{version} · {percent} %" +badge_version = "v{version}" +check = "Rechercher des mises à jour" +download = "Télécharger la mise à jour…" +checking = "Recherche de mises à jour…" +downloading = "Téléchargement de la mise à jour…" +restart = "Redémarrer pour mettre à jour…" + +[settings.general.updates.download_policy] +auto = "Automatique" +description = "Choisissez si Cap télécharge les mises à jour automatiquement." +label = "Téléchargements de mises à jour" +manual = "Demander d'abord" +never = "Jamais" + +[auth] +sign_in_failed = "Échec de la connexion" + +[diagnostics] +already_running = "Un diagnostic est déjà en cours." +mode_both = "Les deux" + +[diagnostics.error] +create_folder = "Impossible de créer le dossier de diagnostic : {error}" +exited = "L'autotest s'est arrêté avec {status}" +no_output = "L'autotest n'a produit aucun flux de sortie" +serialize = "Impossible de sérialiser le rapport : {error}" +start_failed = "Impossible de lancer {program} : {error}" +upload_logs = "Impossible d'envoyer les journaux : {error}" +upload_status = "Échec de l'envoi, statut : {status}" +wait_failed = "Impossible d'attendre la fin de l'autotest : {error}" +write_failed = "Impossible d'écrire {path} : {error}" + +[diagnostics.stage] +analyzing = "Analyse de l'enregistrement..." +analyzing_instant = "Analyse de l'enregistrement Instant..." +analyzing_studio = "Analyse de l'enregistrement Studio..." +cancelling = "Annulation..." +collecting = "Collecte des informations système..." +done = "Finalisation du test de synchronisation..." +exporting = "Export de l'enregistrement..." +exporting_instant = "Export de l'enregistrement Instant..." +exporting_studio = "Export de l'enregistrement Studio..." +finalizing = "Finalisation de l'enregistrement..." +finalizing_instant = "Finalisation de l'enregistrement Instant..." +finalizing_studio = "Finalisation de l'enregistrement Studio..." +pattern = "Lecture de la mire de test..." +pattern_instant = "Lecture de la mire de test (Instant)..." +pattern_studio = "Lecture de la mire de test (Studio)..." +preparing = "Préparation du test..." +preparing_instant = "Préparation du test Instant..." +preparing_studio = "Préparation du test Studio..." +recording = "Enregistrement du test..." +recording_instant = "Enregistrement du test Instant..." +recording_studio = "Enregistrement du test Studio..." +starting = "Démarrage du diagnostic..." + +[dialog] +no_microphone_body = "Cet enregistrement n'inclura pas votre voix." +open_recording_failed = "Impossible d'ouvrir cet enregistrement : {error}" + +[dialog.capture] +approve_shortcut = "Approuvez un raccourci d'enregistrement, ou activez l'icône d'arrêt de Cap dans la zone de notification lorsqu'elle apparaît. L'enregistrement n'a pas commencé." +area_unavailable = "La zone sélectionnée a changé ou n'est plus disponible. Sélectionnez-la à nouveau." +camera_needs_x11 = "Sous Linux, les incrustations de caméra en mode Instant nécessitent un bureau X11" +camera_notice = "Cap masquera son aperçu de caméra et ses commandes d'enregistrement. La caméra sélectionnée continuera d'être enregistrée sur une piste distincte modifiable." +camera_notice_instant = "Cap masquera son aperçu et ses commandes. La caméra sélectionnée apparaîtra à l'emplacement de son aperçu, avec la forme et les effets choisis. Gardez tout l'aperçu dans la zone de capture. Ouvrir Cap arrêtera cet enregistrement Instant avant d'afficher ses commandes." +camera_outside_area = "Placez tout l'aperçu de la caméra dans la zone de capture avant de lancer l'enregistrement Instant" +camera_preview_closed = "Ouvrez l'aperçu de la caméra sélectionnée avant l'enregistrement Instant" +cleanup_incomplete = "Le nettoyage de l'enregistrement est incomplet : {error}. Un nouvel enregistrement est bloqué." +devices_changed = "Les appareils sélectionnés ont changé. Recommencez pour confirmer les nouvelles entrées d'enregistrement." +devices_changed_before_start = "Les appareils sélectionnés ont changé avant le début de l'enregistrement. Recommencez pour confirmer les entrées." +shortcut_conflict = "Ctrl+Shift+F9 est attribué à une autre action de Cap. Modifiez ce raccourci avant d'enregistrer." +shortcut_description = "Lancer ou arrêter cet enregistrement Cap" +shortcut_hint = "Appuyez sur {shortcut} puis relâchez pour démarrer. Utilisez le même raccourci pour arrêter. Ce raccourci temporaire ne modifie pas vos réglages." +tray_stop_hint = "Activez l'icône d'arrêt de Cap dans la zone de notification pour démarrer. Activez-la à nouveau pour arrêter. L'enregistrement ne commencera pas tant que vous n'aurez pas utilisé cette commande." +window_unavailable = "La fenêtre sélectionnée n'est plus disponible. Sélectionnez-la à nouveau." + +[dialog.clipboard] +copy_file = "Impossible de copier le fichier dans le presse-papiers : {error}" +copy_file_failed = "Impossible de copier le fichier dans le presse-papiers" +copy_image = "Impossible de copier l'image dans le presse-papiers : {error}" +copy_image_failed = "Impossible de copier l'image dans le presse-papiers" +not_a_file = "Seuls des fichiers peuvent être copiés dans le presse-papiers" +unavailable = "Presse-papiers indisponible" +unavailable_error = "Presse-papiers indisponible : {error}" + +[dialog.delete_recording] +already_deleting = "Cet enregistrement est déjà en cours de suppression." +close_failed = "Impossible de fermer l'éditeur : {error}" +editor_changed = "L'éditeur a changé pendant la confirmation de la suppression. L'enregistrement n'a pas été supprimé." +editor_closed = "L'éditeur est déjà fermé. L'enregistrement n'a pas été supprimé." +failed = "La suppression de l'enregistrement a échoué : {error}" +failed_title = "Impossible de supprimer l'enregistrement" +finishing_body = "Cap attend la fin du nettoyage de l'enregistrement avant de terminer cette suppression." +finishing_title = "La suppression est toujours en cours" +in_use = "Cet enregistrement est encore utilisé. Terminez ou annulez son envoi, puis réessayez : {error}" +outside_library = "Cet enregistrement se trouve hors de la bibliothèque d'enregistrements. Ses fichiers n'ont pas été modifiés." +recording_active = "Terminez ou annulez l'enregistrement avant de supprimer ce projet." +retained_title = "Enregistrement conservé" + +[dialog.file] +audio_filter = "Audio" +busy = "Une boîte de dialogue de fichier est déjà ouverte. Terminez-la ou annulez-la avant de réessayer." +export_filter = "Export" +failed = "La boîte de dialogue de fichier n'a pas pu aboutir : {error}" +images_filter = "Images" +unavailable_title = "Boîte de dialogue de fichier indisponible" +unconfirmed = "Cap n'a pas pu confirmer la fermeture de la boîte de dialogue de fichier. Fermez toute boîte de dialogue restante et redémarrez Cap avant de réessayer." + +[dialog.mode_select] +instant = "Partagez instantanément avec un lien. L'envoi se fait pendant l'enregistrement." +screenshot = "Prenez et annotez des captures d'écran instantanément." +studio = "Enregistrement local en qualité maximale, à modifier plus tard." +subtitle = "Choisissez comment capturer votre écran" +title = "Choisir le mode d'enregistrement" + +[dialog.storage] +stopped_kept = "L'enregistrement s'est arrêté, car l'espace de stockage est faible. Vos fichiers ont été conservés. Libérez de l'espace, puis récupérez l'enregistrement ci-dessous." +stopped_saved = "L'enregistrement s'est arrêté, car l'espace de stockage est faible. Votre enregistrement a été sauvegardé." +stopping = "Espace de stockage faible. Arrêt et sauvegarde de votre enregistrement…" + +[dialog.teleprompter] +cue_markers = "Repères" +hidden = "Cette fenêtre est masquée dans les enregistrements Cap" +mirror = "Texte en miroir" +paused = "Défilement en pause avec l'enregistrement" +placeholder = "Collez ou saisissez votre script…" + +[import] +error_title = "Erreur d'importation" +image_failed = "Impossible d'importer l'image : {error}" +unsupported = "Fichier multimédia non pris en charge." +video_failed = "Impossible d'importer la vidéo : {error}" + +[import.filter] +image = "Fichiers image" +media = "Fichiers multimédias" +video = "Fichiers vidéo" + +[import.progress] +analyzing = "Analyse du fichier vidéo..." +complete = "Importation terminée !" +converting = "Conversion de la vidéo... {percent} %" +image = "Importation de l'image..." +metadata = "Création des métadonnées du projet..." +starting = "Démarrage de la conversion..." + +[overlay] +choose_mode = "Choisir le mode d'enregistrement" +countdown_tooltip = "Compte à rebours de l'enregistrement" +display_fallback = "Écran {id}" + +[overlay.area] +draw = "Dessinez une zone" +free_ratio = "Libre" +hint = "Cliquez et faites glisser pour sélectionner une zone" +minimum_size = "La taille minimale est de {width} x {height}" +too_small = "{width} x {height} est trop petit" + +[overlay.camera_only] +description = "Enregistrez uniquement avec votre caméra et votre micro" +select_camera = "Veuillez choisir une caméra" + +[overlay.device] +camera_allow = "Autoriser l'accès à la caméra…" +camera_turn_on = "Activer l'accès à la caméra dans les réglages…" +microphone_allow = "Autoriser l'accès au micro…" +microphone_turn_on = "Activer l'accès au micro dans les réglages…" + +[overlay.start] +instant = "Mode Instant : enregistrer un lien de partage" +instant_sign_in = "Mode Instant : connectez-vous pour enregistrer" +sign_in = "Se connecter pour utiliser" +start_recording = "Lancer l'enregistrement" +studio = "Mode Studio : enregistrer et modifier en local" +take_screenshot = "Prendre une capture d'écran" + +[permission.guide] +accessibility_body = "Déposez-le dans la liste « Accessibilité » ci-dessus, puis vérifiez que son interrupteur est activé." +screen_body = "Déposez-le dans la liste « Enregistrement de l'écran et de l'audio du système » ci-dessus, puis vérifiez que son interrupteur est activé." +title = "Faites glisser Cap dans la liste" + +[permission.name] +accessibility = "Accessibilité" +camera = "Caméra" +microphone = "Micro" +screen_recording = "Enregistrement de l'écran" + +[permission.onboarding] +open_settings_again = "Rouvrir Réglages Système" +skip = "Ignorer pour l'instant" +switch_on_title = "Activez Cap dans Réglages Système" + +[permission.onboarding.accessibility] +allow = "Autoriser l'accessibilité" +body = "L'accessibilité permet à Cap de voir vos clics et votre curseur, uniquement sur ce Mac, pour que le zoom automatique sache où regarder." +done_body = "Le zoom automatique peut suivre vos clics." +done_title = "L'accessibilité est activée" +title = "Laissez Cap suivre votre curseur" + +[permission.onboarding.all_set] +body = "Cap a tout ce qu'il lui faut. Voyons maintenant rapidement ce qu'il sait faire." +regained = "Merci. Cap peut de nouveau enregistrer." +title = "Tout est prêt" + +[permission.onboarding.camera] +allow = "Autoriser la caméra" +body = "Apparaissez dans un coin de vos enregistrements. Ignorez cette étape si vous n'en avez pas besoin." +done_body = "Vous pouvez ajouter votre caméra à n'importe quel enregistrement." +done_title = "La caméra est activée" +title = "Laissez Cap vous voir" + +[permission.onboarding.checklist] +lede = "macOS vous demande d'abord d'autoriser quelques éléments. Cela ne prend qu'une minute." +regained_lede = "Un élément a été désactivé dans Réglages Système. Réactivez-le pour continuer à enregistrer." +regained_title = "Cap a besoin de récupérer une autorisation" +title = "Préparons Cap" + +[permission.onboarding.hint] +drag_into_list = "Faites glisser Cap depuis le panneau sous Réglages Système vers la liste. Déjà dans la liste ? Activez-le simplement." +find_in_list = "Trouvez Cap dans la liste et activez-le. Si macOS vous a d'abord posé la question, choisissez « Ouvrir Réglages Système »." +no_prompt = "Aucune demande n'est apparue ? Activez Cap dans Réglages Système." +open_settings = "Ouvrez Réglages Système, trouvez Cap dans la liste et activez-le." +turned_off = "Cette autorisation a été désactivée auparavant : activez Cap dans Réglages Système, ou ignorez cette étape pour l'instant." + +[permission.onboarding.microphone] +allow = "Autoriser le micro" +body = "Ajoutez votre voix à vos enregistrements. Vous pouvez ignorer cette étape et l'activer plus tard." +done_body = "Votre voix peut figurer dans vos enregistrements." +done_title = "Le micro est activé" +title = "Laissez Cap vous entendre" + +[permission.onboarding.relaunch] +body = "macOS active l'enregistrement de l'écran une fois Cap redémarré. Vous reprendrez ici même." +button = "Relancer" +hint = "Activé mais toujours en attente ? Redémarrez Cap pour l'appliquer." +later = "macOS l'applique après le redémarrage de Cap. Vous pourrez le faire une fois terminé ici." +title = "Un redémarrage et c'est prêt" + +[permission.onboarding.screen] +allow = "Autoriser l'enregistrement de l'écran" +body = "L'enregistrement de l'écran permet à Cap de capturer vos écrans, vos fenêtres et l'audio du système." +done_body = "Cap peut capturer votre écran." +done_title = "L'enregistrement de l'écran est activé" +title = "Laissez Cap voir votre écran" + +[permission.onboarding.status] +allowed = "Autorisé" +off = "Désactivé dans Réglages Système" +optional = "Facultatif" +required = "Obligatoire" +skipped = "Ignoré pour l'instant" + +[permission.onboarding.waiting] +prompt = "Choisissez « Autoriser » dans la demande de macOS" +system_settings = "En attente de Réglages Système" + +[screenshot_editor] +loading_preview = "Chargement de l'aperçu..." +zoom_in = "Zoom avant" +zoom_out = "Zoom arrière" + +[screenshot_editor.annotation] +default_text = "Texte" +done = "Terminé" +fill = "Remplissage" +intensity = "Intensité" +mask_style = "Style" +pixelate = "Pixeliser" +stroke = "Contour" +transparent = "Transparent" + +[screenshot_editor.aspect] +auto = "Auto" +classic = "Classique ⋅4:3" +square = "Carré ⋅1:1" +tall = "Haut ⋅3:4" +vertical = "Vertical ⋅9:16" +wide = "Large ⋅16:9" + +[screenshot_editor.background] +animated_gradient = "Le dégradé animé est rendu comme une image fixe dans les captures d'écran. Choisissez un autre arrière-plan pour le remplacer." +blur_title = "Flou de l'arrière-plan" +choose_image = "Cliquez pour choisir ou glissez-déposez une image" +color = "Couleur" +gradient = "Dégradé" +image = "Image" +image_files = "Image" +image_title = "Image d'arrière-plan" +wallpaper = "Fond d'écran" + +[screenshot_editor.corner] +rounded = "Arrondi" +squircle = "Squircle" +style = "STYLE DES COINS" + +[screenshot_editor.crop] +full = "Entier" +loading_frame = "Chargement de l'image…" +position = "Position" + +[screenshot_editor.error] +encode = "Impossible d'encoder l'export de la capture d'écran : {error}" +load_meta = "Impossible de charger les métadonnées de la capture d'écran : {error}" +load_metadata = "Impossible de charger les métadonnées de la capture d'écran : {error}" +no_image = "Aucun PNG trouvé dans {path}" +not_screenshot = "Ce n'est pas un dossier de capture d'écran" +open_image = "Impossible d'ouvrir l'image : {error}" +renderer_failed = "Le moteur de rendu GPU n'a pas pu démarrer : {error}" +renderer_not_running = "Le moteur de rendu des captures d'écran n'est pas lancé" +renderer_stopped = "Le moteur de rendu s'est arrêté avant la fin de l'export" +save_project = "Erreur lors de l'enregistrement du projet : {error}" +serialize = "Impossible de sérialiser la configuration de la capture d'écran : {error}" +sign_in_to_share = "Connectez-vous à Cap dans votre navigateur, puis partagez à nouveau" +too_large = "Les dimensions de l'image dépassent le maximum : {width}x{height}" +upgrade_required = "Cette fonctionnalité nécessite une offre supérieure" +upload_task = "La tâche d'envoi a échoué : {error}" + +[screenshot_editor.header] +aspect_ratio = "Format" +copy = "Copier dans le presse-papiers" +crop_image = "Recadrer l'image" +more_actions = "Plus d'actions" + +[screenshot_editor.layers] +empty = "Aucun calque pour l'instant" +empty_hint = "Utilisez les outils ci-dessus pour ajouter des annotations" +reorder_hint = "Glissez pour réorganiser • Haut = premier plan" +title = "Calques" + +[screenshot_editor.menu] +delete = "Supprimer" +open_folder = "Ouvrir le dossier" + +[screenshot_editor.share] +create_link = "Créer un lien de partage" +preparing = "Préparation de l'envoi" +rendering = "Rendu de la capture d'écran" +uploading = "Envoi de la capture d'écran" + +[screenshot_editor.style] +advanced_shadow = "Réglages avancés de l'ombre" +background = "Arrière-plan" +blur = "Flou" +border = "Bordure" +color = "Couleur" +corner_rounding = "Arrondi des coins" +opacity = "Opacité" +padding = "Marge" +rounding = "Arrondi" +shadow = "Ombre" +size_label = "Taille" +width = "Largeur" + +[screenshot_editor.toast] +copied = "Capture d'écran copiée dans le presse-papiers !" +link_copied = "Lien de partage copié dans le presse-papiers" +saved = "Capture d'écran enregistrée !" + +[screenshot_editor.tool] +arrow = "Flèche" +circle = "Cercle" +draw = "Dessin" +mask = "Masque" +rectangle = "Rectangle" +select = "Sélection" +text_label = "Texte" + +[transcription.download] +complete = "Téléchargement terminé" +failed = "Échec du téléchargement : {error}" +file_progress = "Téléchargement de {file} : {percent} %" +preparing = "Préparation du téléchargement du modèle" +progress = "Téléchargement du modèle : {percent} %" + +[transcription.error] +intel_mac = "Les modèles Parakeet ne sont pas disponibles sur les Mac Intel. Utilisez plutôt un modèle Whisper" +model_load = "Impossible de charger le modèle de sous-titres. Essayez de le télécharger à nouveau" +model_missing = "Modèle de sous-titres introuvable. Téléchargez-le d'abord" +no_audio = "Aucun audio trouvé dans le fichier vidéo" +unsupported_language = "Les modèles Recommandé et Haute précision ne transcrivent que l'anglais et 24 autres langues européennes. Téléchargez le modèle Petit ou Moyen pour sous-titrer cette langue" + +[upload.status] +failed = "L'envoi nécessite votre attention" +paused = "Envoi en pause" +prepared = "Envoi préparé" +processing = "Traitement de l'envoi" +retrying = "L'envoi va être relancé" +sign_in = "Connectez-vous pour relancer l'envoi" +uploaded = "Envoyé" +uploading = "Envoi en cours" diff --git a/apps/desktop-gpui/locales/hi.toml b/apps/desktop-gpui/locales/hi.toml index 0ee072f4c59..7dae86387af 100644 --- a/apps/desktop-gpui/locales/hi.toml +++ b/apps/desktop-gpui/locales/hi.toml @@ -24,9 +24,17 @@ quit = "Cap से बाहर निकलें" services = "सेवाएँ" settings = "सेटिंग्स…" +[menu.about] +licensed = "AGPL-3.0 के अंतर्गत लाइसेंस प्राप्त" +website = "वेबसाइट" +commit = "कमिट" + [menu.file] close_window = "विंडो बंद करें" title = "फ़ाइल" +open_file = "फ़ाइल खोलें…" +open_recent = "हाल ही में खोलें" +import_media = "मीडिया इम्पोर्ट करें…" [menu.edit] copy = "कॉपी करें" @@ -49,12 +57,22 @@ zoom = "ज़ूम करें" [menu.help] title = "सहायता" +feedback = "प्रतिक्रिया दें…" +changelog = "चेंजलॉग" +dashboard = "डैशबोर्ड" +documentation = "दस्तावेज़ीकरण" +faq = "सामान्य प्रश्न" +self_hosting = "सेल्फ-होस्टिंग" +help_center = "सहायता केंद्र" +system_status = "सिस्टम स्थिति" +discord = "Discord समुदाय से जुड़ें" [tray] import_media = "मीडिया इम्पोर्ट करें..." no_recent_items = "कोई हालिया आइटम नहीं" open_main_window = "मुख्य विंडो खोलें" previous = "हाल के आइटम" +recording_mode = "रिकॉर्डिंग मोड" quit = "Cap से बाहर निकलें" record_area = "क्षेत्र रिकॉर्ड करें" record_display = "डिस्प्ले रिकॉर्ड करें" @@ -239,6 +257,698 @@ title = "गोपनीयता" [settings.general.privacy.telemetry] description = "Cap विश्वसनीयता बेहतर करने और गड़बड़ियाँ ठीक करने के लिए अनाम टेलीमेट्री का उपयोग करता है। हम रिकॉर्डिंग की सामग्री, विंडो के शीर्षक, फ़ाइल पाथ या निजी जानकारी कभी इकट्ठा नहीं करते।" label = "अनाम टेलीमेट्री साझा करें" +[settings.prompt] +ok = "ठीक है" + +[settings.sidebar] +account = "खाता" +cancel_sign_in = "साइन इन रद्द करें" +check_for_updates = "अपडेट देखें" +checking_for_updates = "अपडेट देखे जा रहे हैं…" +click_to_sign_in = "साइन इन करने के लिए क्लिक करें" +downloading = "v{version} डाउनलोड हो रहा है…" +downloading_progress = "v{version} डाउनलोड हो रहा है… {percent}%" +installing = "v{version} इंस्टॉल हो रहा है…" +previous_versions = "पिछले वर्शन देखें" +restart_to_update = "v{version} पर अपडेट करने के लिए रीस्टार्ट करें" +sign_in = "साइन इन करें" +sign_out = "साइन आउट करें" +signed_in = "साइन इन है" + +[settings.recordings] +corrupted_body = "रिकॉर्डिंग विफल रही, इसलिए एडिटर में इस फ़ाइल के साथ समस्याएँ हो सकती हैं! अगर फ़ाइल रिकवर करने में दिक्कत हो, तो कृपया सपोर्ट से संपर्क करें!" +corrupted_title = "रिकॉर्डिंग शायद खराब हो गई है" +delete = "डिलीट करें" +description = "अपनी रिकॉर्डिंग मैनेज करें और उन पर कार्रवाई करें।" +edit = "एडिट करें" +empty_library = "कोई रिकॉर्डिंग नहीं मिली" +failed = "रिकॉर्डिंग विफल रही" +import = "इम्पोर्ट करें" +in_progress = "रिकॉर्डिंग जारी है" +load_more = "और लोड करें" +open_bundle = "रिकॉर्डिंग बंडल खोलें" +open_link = "लिंक खोलें" +retry_upload = "अपलोड फिर से करें" +show_all = "सभी दिखाएँ" +title = "रिकॉर्डिंग" + +[settings.recordings.empty] +all = "कोई रिकॉर्डिंग नहीं" +all_matching = "कोई मिलती-जुलती रिकॉर्डिंग नहीं" +instant = "कोई Instant रिकॉर्डिंग नहीं" +instant_matching = "कोई मिलती-जुलती Instant रिकॉर्डिंग नहीं" +studio = "कोई Studio रिकॉर्डिंग नहीं" +studio_matching = "कोई मिलती-जुलती Studio रिकॉर्डिंग नहीं" + +[settings.screenshots] +copy = "क्लिपबोर्ड पर कॉपी करें" +delete = "डिलीट करें" +description = "अपने स्क्रीनशॉट मैनेज करें और उन पर कार्रवाई करें।" +empty = "कोई स्क्रीनशॉट नहीं" +empty_library = "कोई स्क्रीनशॉट नहीं मिला" +empty_matching = "कोई मिलता-जुलता स्क्रीनशॉट नहीं" +import = "इमेज इम्पोर्ट करें" +load_more = "और लोड करें" +open_folder = "फ़ोल्डर खोलें" +save_as = "इस रूप में सेव करें" +title = "स्क्रीनशॉट" + +[settings.quality] +save_failed = "आपकी रिकॉर्डिंग सेटिंग्स सेव नहीं हो सकीं। कृपया फिर से कोशिश करें।" + +[settings.quality.studio] +balanced = "संतुलित" +balanced_description = "उचित फ़ाइल साइज़ में साफ़ डिटेल। ज़्यादातर रिकॉर्डिंग के लिए सही।" +compatibility = "छोटी फ़ाइलें" +compatibility_description = "डिस्क स्पेस बचाता है। बारीक डिटेल थोड़ी धुंधली हो सकती है, ख़ासकर कैमरा में।" +description = "एडिटिंग के लिए आपके कंप्यूटर पर सेव होती हैं। एक्सपोर्ट का साइज़ आप बाद में एडिटर में चुनते हैं।" +recommended = "सुझाया गया" +title = "Studio रिकॉर्डिंग" +ultra = "अधिकतम डिटेल" +ultra_description = "ज़्यादा एडिटिंग के लिए सबसे ज़्यादा डिटेल रखता है। सबसे ज़्यादा डिस्क स्पेस लेता है।" + +[settings.quality.instant] +description = "रिकॉर्ड करते समय ही अपलोड होती है, इसलिए रोकते ही लिंक तैयार रहता है। सबसे अच्छी क्वालिटी के लिए Studio मोड इस्तेमाल करें: यह आपकी स्क्रीन की अधिकतम क्वालिटी में रिकॉर्ड करता है, और आप 4K तक एक्सपोर्ट कर सकते हैं।" +free_plan = "फ़्री प्लान पर 720p। Cap Pro 4K तक रिकॉर्ड करता है।" +max_resolution = "अधिकतम रेज़ोल्यूशन" +res_1080p = "व्यावहारिक अपलोड साइज़ पर साफ़ टेक्स्ट।" +res_1440p = "बड़ी, हाई-रेज़ोल्यूशन स्क्रीन के लिए ज़्यादा डिटेल।" +res_4k = "सबसे ज़्यादा डिटेल। तेज़ कनेक्शन पर सबसे अच्छा।" +res_720p = "सबसे छोटे अपलोड, इसलिए लिंक सबसे जल्दी तैयार होते हैं।" +title = "Instant रिकॉर्डिंग" + +[settings.quality.sharing] +auto_open = "शेयर लिंक अपने आप खोलें" +auto_open_description = "अपलोड पूरा होने पर लिंक आपके ब्राउज़र में खोलें।" +title = "शेयरिंग" + +[settings.options.studio_quality] +balanced = "संतुलित" +compatibility = "कम्पैटिबिलिटी" +ultra = "अल्ट्रा" + +[settings.options.main_window_start] +close = "बंद करें" +minimise = "छोटा करें" + +[settings.options.post_studio] +do_nothing = "कुछ न करें" +open_editor = "एडिटर खोलें" +show_cap = "Cap दिखाएँ" + +[settings.options.post_screenshot] +copy = "क्लिपबोर्ड पर कॉपी करें" +copy_and_open_editor = "कॉपी करके एडिटर खोलें" +do_nothing = "कुछ न करें" +open_editor = "एडिटर खोलें" + +[settings.options.post_deletion] +do_nothing = "कुछ न करें" +reopen = "रिकॉर्डर फिर से खोलें" + +[settings.options.preview_quality] +full = "पूरा" +half = "आधा" +quarter = "एक-चौथाई" + +[settings.shortcuts] +blocked_while_recording = "जब तक यह रिकॉर्डिंग अपनी स्टॉप की इस्तेमाल कर रही है, शॉर्टकट नहीं बदले जा सकते। पहले रिकॉर्डिंग रोकें।" +needs_modifier_macos = "⌘, ⌃ या ⌥ शामिल करें ताकि शॉर्टकट टाइपिंग में बाधा न बने।" +needs_modifier_other = "Ctrl, Alt या Win शामिल करें ताकि शॉर्टकट टाइपिंग में बाधा न बने।" +record_shortcut = "शॉर्टकट सेट करें" +recording_description = "पहले Cap खोले बिना रिकॉर्डिंग शुरू करें।" +recording_hint = "जिन कीज़ का इस्तेमाल करना है उन्हें दबाएँ। Esc रद्द करता है, Delete इसे हटाता है।" +recording_title = "रिकॉर्डिंग" +saved = "सेव हो गया। किसी भी ऐप में {keys} दबाएँ और {effect}।" +saved_moved = "सेव हो गया। किसी भी ऐप में {keys} दबाएँ और {effect}। “{shortcut}” से हटा दिया गया।" +screenshots_description = "शॉर्टकट किसी भी ऐप में काम करते हैं। बदलने के लिए किसी पर क्लिक करें।" +screenshots_title = "स्क्रीनशॉट" +type_shortcut = "शॉर्टकट दबाएँ…" +unavailable = "कोई दूसरा ऐप या सिस्टम पहले से यह शॉर्टकट इस्तेमाल कर रहा है। कोई दूसरा चुनने के लिए क्लिक करें।" +unsupported_key = "यह की शॉर्टकट में इस्तेमाल नहीं हो सकती। कोई अक्षर, संख्या या F-की आज़माएँ।" +while_recording_title = "रिकॉर्डिंग के दौरान" + +[settings.shortcuts.after_screenshot] +hint = "हर स्क्रीनशॉट के लिए, शॉर्टकट से या Cap से" +title = "स्क्रीनशॉट के बाद" + +[settings.shortcuts.after_studio] +hint = "रोकने पर" +title = "Studio रिकॉर्डिंग के बाद" + +[settings.shortcuts.after_instant] +hint = "शेयर लिंक हमेशा कॉपी होता है" +title = "Instant रिकॉर्डिंग के बाद" + +[settings.shortcuts.outcome] +instant_copy_link = "सिर्फ़ लिंक कॉपी करें" +instant_copy_link_hint = "कुछ नहीं खुलता" +instant_open_link = "लिंक खोलें" +instant_open_link_hint = "आपके ब्राउज़र में" +saved_to_library = "लाइब्रेरी में सेव" +screenshot_copy = "कॉपी करें" +screenshot_copy_and_edit = "कॉपी और एडिट" +screenshot_copy_and_edit_hint = "दोनों एक साथ" +screenshot_copy_hint = "कहीं भी पेस्ट करें" +screenshot_do_nothing = "कुछ न करें" +screenshot_open_editor = "एडिटर खोलें" +screenshot_open_editor_hint = "पहले एनोटेट करें" +studio_do_nothing = "कुछ न करें" +studio_open_editor = "एडिटर खोलें" +studio_open_editor_hint = "तुरंत एडिट करें" +studio_show_cap = "Cap दिखाएँ" +studio_show_cap_hint = "रिकॉर्डर पर वापस" + +[settings.shortcuts.label] +cycle_mode = "रिकॉर्डिंग मोड बदलें" +fallback = "शॉर्टकट" +open_picker = "रिकॉर्डिंग पिकर खोलें" +pause = "पॉज़ या फिर से शुरू करें" +record_area = "क्षेत्र रिकॉर्ड करें" +record_display = "डिस्प्ले रिकॉर्ड करें" +record_window = "विंडो रिकॉर्ड करें" +restart = "रिकॉर्डिंग फिर से शुरू करें" +screenshot_area = "क्षेत्र का स्क्रीनशॉट" +screenshot_display = "डिस्प्ले का स्क्रीनशॉट" +screenshot_window = "विंडो का स्क्रीनशॉट" +start_instant = "Instant रिकॉर्डिंग शुरू करें" +start_studio = "Studio रिकॉर्डिंग शुरू करें" +stop = "रिकॉर्डिंग रोकें" + +[settings.shortcuts.effect] +cycle_mode = "Studio, Instant और स्क्रीनशॉट मोड के बीच बदलें" +instant_copy = "चुनी गई स्क्रीन या विंडो को Instant मोड में रिकॉर्ड करें, फिर रोकने पर शेयर लिंक कॉपी करें" +instant_copy_and_open = "चुनी गई स्क्रीन या विंडो को Instant मोड में रिकॉर्ड करें, फिर रोकने पर शेयर लिंक कॉपी करके खोलें" +open_picker = "Cap खोलकर चुनें कि क्या रिकॉर्ड करना है" +pause = "मौजूदा रिकॉर्डिंग पॉज़ करें या फिर से शुरू करें" +picker_area_instant = "क्षेत्र चुनकर उसे Instant मोड में रिकॉर्ड करें" +picker_area_screenshot = "क्षेत्र चुनकर उसका स्क्रीनशॉट लें" +picker_area_studio = "क्षेत्र चुनकर उसे Studio मोड में रिकॉर्ड करें" +picker_display_instant = "डिस्प्ले चुनकर उसे Instant मोड में रिकॉर्ड करें" +picker_display_screenshot = "डिस्प्ले चुनकर उसका स्क्रीनशॉट लें" +picker_display_studio = "डिस्प्ले चुनकर उसे Studio मोड में रिकॉर्ड करें" +picker_window_instant = "विंडो चुनकर उसे Instant मोड में रिकॉर्ड करें" +picker_window_screenshot = "विंडो चुनकर उसका स्क्रीनशॉट लें" +picker_window_studio = "विंडो चुनकर उसे Studio मोड में रिकॉर्ड करें" +restart = "मौजूदा टेक हटाकर फिर से शुरू करें" +screenshot_area_copy = "क्षेत्र चुनकर स्क्रीनशॉट को क्लिपबोर्ड पर कॉपी करें" +screenshot_area_copy_and_open_editor = "क्षेत्र चुनकर स्क्रीनशॉट कॉपी करें और एडिटर खोलें" +screenshot_area_open_editor = "क्षेत्र चुनकर स्क्रीनशॉट को एडिटर में खोलें" +screenshot_area_save = "क्षेत्र चुनकर कुछ खोले बिना स्क्रीनशॉट को अपनी लाइब्रेरी में सेव करें" +screenshot_display_copy = "पॉइंटर वाला डिस्प्ले कैप्चर करके स्क्रीनशॉट को क्लिपबोर्ड पर कॉपी करें" +screenshot_display_copy_and_open_editor = "पॉइंटर वाला डिस्प्ले कैप्चर करके स्क्रीनशॉट कॉपी करें और एडिटर खोलें" +screenshot_display_open_editor = "पॉइंटर वाला डिस्प्ले कैप्चर करके स्क्रीनशॉट को एडिटर में खोलें" +screenshot_display_save = "पॉइंटर वाला डिस्प्ले कैप्चर करके कुछ खोले बिना स्क्रीनशॉट को अपनी लाइब्रेरी में सेव करें" +screenshot_window_copy = "पॉइंटर के नीचे की विंडो कैप्चर करके स्क्रीनशॉट को क्लिपबोर्ड पर कॉपी करें" +screenshot_window_copy_and_open_editor = "पॉइंटर के नीचे की विंडो कैप्चर करके स्क्रीनशॉट कॉपी करें और एडिटर खोलें" +screenshot_window_open_editor = "पॉइंटर के नीचे की विंडो कैप्चर करके स्क्रीनशॉट को एडिटर में खोलें" +screenshot_window_save = "पॉइंटर के नीचे की विंडो कैप्चर करके कुछ खोले बिना स्क्रीनशॉट को अपनी लाइब्रेरी में सेव करें" +stop = "मौजूदा रिकॉर्डिंग रोकें" +studio = "चुनी गई स्क्रीन या विंडो को Studio मोड में रिकॉर्ड करें" +studio_open_editor = "चुनी गई स्क्रीन या विंडो को Studio मोड में रिकॉर्ड करें, फिर रोकने पर एडिटर खोलें" + +[settings.shortcuts.sentence] +cycle_mode = "Studio, Instant और स्क्रीनशॉट मोड के बीच बदलें।" +instant_copy = "चुनी गई स्क्रीन या विंडो को Instant मोड में रिकॉर्ड करें, फिर रोकने पर शेयर लिंक कॉपी करें।" +instant_copy_and_open = "चुनी गई स्क्रीन या विंडो को Instant मोड में रिकॉर्ड करें, फिर रोकने पर शेयर लिंक कॉपी करके खोलें।" +open_picker = "Cap खोलकर चुनें कि क्या रिकॉर्ड करना है।" +pause = "मौजूदा रिकॉर्डिंग पॉज़ करें या फिर से शुरू करें।" +picker_area_instant = "क्षेत्र चुनकर उसे Instant मोड में रिकॉर्ड करें।" +picker_area_screenshot = "क्षेत्र चुनकर उसका स्क्रीनशॉट लें।" +picker_area_studio = "क्षेत्र चुनकर उसे Studio मोड में रिकॉर्ड करें।" +picker_display_instant = "डिस्प्ले चुनकर उसे Instant मोड में रिकॉर्ड करें।" +picker_display_screenshot = "डिस्प्ले चुनकर उसका स्क्रीनशॉट लें।" +picker_display_studio = "डिस्प्ले चुनकर उसे Studio मोड में रिकॉर्ड करें।" +picker_window_instant = "विंडो चुनकर उसे Instant मोड में रिकॉर्ड करें।" +picker_window_screenshot = "विंडो चुनकर उसका स्क्रीनशॉट लें।" +picker_window_studio = "विंडो चुनकर उसे Studio मोड में रिकॉर्ड करें।" +restart = "मौजूदा टेक हटाकर फिर से शुरू करें।" +screenshot_area_copy = "क्षेत्र चुनकर स्क्रीनशॉट को क्लिपबोर्ड पर कॉपी करें।" +screenshot_area_copy_and_open_editor = "क्षेत्र चुनकर स्क्रीनशॉट कॉपी करें और एडिटर खोलें।" +screenshot_area_open_editor = "क्षेत्र चुनकर स्क्रीनशॉट को एडिटर में खोलें।" +screenshot_area_save = "क्षेत्र चुनकर कुछ खोले बिना स्क्रीनशॉट को अपनी लाइब्रेरी में सेव करें।" +screenshot_display_copy = "पॉइंटर वाला डिस्प्ले कैप्चर करके स्क्रीनशॉट को क्लिपबोर्ड पर कॉपी करें।" +screenshot_display_copy_and_open_editor = "पॉइंटर वाला डिस्प्ले कैप्चर करके स्क्रीनशॉट कॉपी करें और एडिटर खोलें।" +screenshot_display_open_editor = "पॉइंटर वाला डिस्प्ले कैप्चर करके स्क्रीनशॉट को एडिटर में खोलें।" +screenshot_display_save = "पॉइंटर वाला डिस्प्ले कैप्चर करके कुछ खोले बिना स्क्रीनशॉट को अपनी लाइब्रेरी में सेव करें।" +screenshot_window_copy = "पॉइंटर के नीचे की विंडो कैप्चर करके स्क्रीनशॉट को क्लिपबोर्ड पर कॉपी करें।" +screenshot_window_copy_and_open_editor = "पॉइंटर के नीचे की विंडो कैप्चर करके स्क्रीनशॉट कॉपी करें और एडिटर खोलें।" +screenshot_window_open_editor = "पॉइंटर के नीचे की विंडो कैप्चर करके स्क्रीनशॉट को एडिटर में खोलें।" +screenshot_window_save = "पॉइंटर के नीचे की विंडो कैप्चर करके कुछ खोले बिना स्क्रीनशॉट को अपनी लाइब्रेरी में सेव करें।" +stop = "मौजूदा रिकॉर्डिंग रोकें।" +studio = "चुनी गई स्क्रीन या विंडो को Studio मोड में रिकॉर्ड करें।" +studio_open_editor = "चुनी गई स्क्रीन या विंडो को Studio मोड में रिकॉर्ड करें, फिर रोकने पर एडिटर खोलें।" + +[settings.cli] +command = "कमांड" +copy = "कॉपी करें" +description = "टर्मिनल, एजेंट, स्क्रिप्ट और लोकल ऑटोमेशन के लिए Cap कमांड इंस्टॉल करें।" +install = "CLI इंस्टॉल करें" +installed = "इंस्टॉल है" +installing = "इंस्टॉल हो रहा है..." +installs_command = "डेस्कटॉप ऐप एक लोकल\u00A0{command}\u00A0कमांड इंस्टॉल करता है जो बंडल की गई CLI की ओर इशारा करता है।" +load_failed = "CLI की स्थिति लोड नहीं हो सकी: {error}" +not_installed = "इंस्टॉल नहीं है" +path_configured = "cap को आपके PATH में जोड़ दिया गया है। इस्तेमाल करने के लिए टर्मिनल रीस्टार्ट करें, या अभी यह चलाएँ:" +path_missing = "नए टर्मिनल से cap इस्तेमाल करने के लिए {path} को अपने PATH में जोड़ें।" +remove = "हटाएँ" +removing = "हटाया जा रहा है..." +repair = "ठीक करें" +repairing = "ठीक किया जा रहा है..." +retry = "फिर से कोशिश करें" +target = "टारगेट" +title = "कमांड लाइन" + +[settings.transcription] +active_title = "सक्रिय संकेत" +applied_note = "जब आप एडिटर में कैप्शन बनाते हैं, तब ये संकेत लागू होते हैं।" +clear = "साफ़ करें" +count = { one = "{count} आइटम", other = "{count} आइटम" } +description = "वे नाम, स्पेलिंग, डोमेन और कैपिटलाइज़ेशन की पसंद जोड़ें जिनका कैप्शन बनाते समय ध्यान रखा जाना चाहिए।" +placeholder = "शब्द जोड़ें" +saved = "सेव हो गया" +saving = "सेव हो रहा है..." +terms_description = "टाइपो और फ़ॉर्मैटिंग की गलतियाँ कम करने के लिए एक बार में एक शब्द जोड़ें।" +terms_title = "याद रखे गए शब्द" +title = "ट्रांसक्रिप्शन" + +[settings.experimental] +developer = "डेवलपर" +developer_description = "सिर्फ़ डेवलपमेंट बिल्ड में दिखता है।" +muxer = "अलग प्रोसेस में मक्सर" +muxer_description = "फ़्रैगमेंटेड-MP4 मक्सर को अलग सबप्रोसेस में चलाता है ताकि मक्सर क्रैश होने से आपकी रिकॉर्डिंग न रुके। इसके लिए बंडल की गई cap-muxer बाइनरी ज़रूरी है।" +native_app = "नेटिव ऐप इस्तेमाल करें" +native_app_description = "बंद करने पर यह ऐप बंद होगा और डेवलपमेंट हार्नेस में क्लासिक Tauri ऐप फिर से खुलेगा। रिकॉर्डिंग और सेटिंग्स साझा रहती हैं।" +reliability = "विश्वसनीयता" + +[settings.experimental.switch] +busy = "क्लासिक ऐप पर जाने से पहले अपनी रिकॉर्डिंग, एक्सपोर्ट, अपलोड, इम्पोर्ट या ट्रांसक्रिप्शन का काम पूरा करें।" +check_failed = "यह जाँच नहीं हो सकी कि क्लासिक Cap ऐप खुला या नहीं: {error}" +data_stays = "आपकी रिकॉर्डिंग और सेटिंग्स ठीक वहीं रहती हैं जहाँ वे हैं।" +failed = "क्लासिक Cap ऐप नहीं खुल सका।" +not_found = "वापस जाने के लिए क्लासिक Cap ऐप नहीं मिला।" +open_failed = "क्लासिक Cap ऐप नहीं खुल सका: {error}" +opening = "क्लासिक Cap ऐप खुल रहा है" +opening_detail = "क्लासिक Cap ऐप तैयार होने पर यह विंडो बंद हो जाएगी।" +save_failed = "आपकी ऐप पसंद सेव नहीं हो सकी। Cap अभी भी खुला है।" +switching = "क्लासिक Cap ऐप पर वापस जा रहे हैं।" +timed_out = "क्लासिक ऐप नहीं खुला। डेव टर्मिनल में बिल्ड की गड़बड़ियाँ देखें, फिर से कोशिश करें।" + +[settings.feedback] +app_version = "ऐप वर्शन" +capture_support = "कैप्चर सपोर्ट" +capture_supported = "स्क्रीन कैप्चर: सपोर्टेड" +capture_unsupported = "स्क्रीन कैप्चर: सपोर्टेड नहीं" +community_description = "कोई सवाल है, आइडिया शेयर करना है, या बस बातचीत करनी है? Cap की Discord कम्युनिटी से जुड़ें।" +community_title = "कम्युनिटी से जुड़ें" +debug_description = "Cap की समस्याएँ पहचानने में हमारी मदद के लिए अपने लॉग अपलोड करें। इसमें कोई निजी जानकारी शामिल नहीं होती।" +debug_title = "डीबग जानकारी" +description = "फ़ीडबैक भेजकर या बग रिपोर्ट करके Cap को बेहतर बनाने में हमारी मदद करें। हम तुरंत इस पर काम करेंगे।" +join_discord = "Discord से जुड़ें" +operating_system = "ऑपरेटिंग सिस्टम" +placeholder = "हमें बताएँ कि आप Cap के बारे में क्या सोचते हैं..." +submit = "फ़ीडबैक सबमिट करें" +submit_failed = "फ़ीडबैक सबमिट नहीं हो सका" +submitting = "सबमिट हो रहा है..." +system_loading = "सिस्टम जानकारी लोड हो रही है..." +system_title = "सिस्टम जानकारी" +thanks = "आपके फ़ीडबैक के लिए धन्यवाद!" +title = "फ़ीडबैक" +upload_interrupted = "अपलोड बीच में रुक गया" +upload_logs = "लॉग अपलोड करें" +upload_sent = "भेज दिया गया। धन्यवाद, इससे हमें समस्या ढूँढने में मदद मिलती है।" +uploading = "अपलोड हो रहा है..." + +[settings.feedback.diagnostic] +confirm_body = "Cap आपकी स्क्रीन पर चमकता पैटर्न दिखाएगा और हर पाइपलाइन के लिए लगभग {seconds} सेकंड तक तेज़ बीप बजाएगा। हेडफ़ोन उतार दें, आवाज़ सुनाई देने लायक रखें, और पूरा होने तक कंप्यूटर को न छुएँ।" +confirm_title = "डायग्नोस्टिक चलाएँ?" +description = "एक छोटा चमकता/बीप करता टेस्ट पैटर्न रिकॉर्ड करता है, मापता है कि तस्वीर और आवाज़ कितनी अलग हो जाती हैं, और आपके डिस्प्ले, कैमरा, माइक्रोफ़ोन, डिस्क स्पेस और रिकॉर्डिंग सेटिंग्स का स्नैपशॉट लेता है। जब तक आप भेजना न चुनें, कुछ भी कहीं नहीं भेजा जाता।" +mic_description = "आपका माइक्रोफ़ोन भी रिकॉर्ड करता है और जाँचता है कि वह बीप को सही समय पर सुनता है या नहीं। ऐसे स्पीकर चाहिए जिन्हें माइक सच में सुन सके।" +mic_label = "माइक्रोफ़ोन टेस्ट करें" +mode_description = "कौन-सी पाइपलाइन टेस्ट करनी है। दोनों चुनने पर पहले Studio और फिर Instant हिस्सा चलता है।" +mode_label = "रिकॉर्डिंग मोड" +no_cli = "इस बिल्ड के साथ Cap कमांड-लाइन टूल नहीं मिला, इसलिए रिपोर्ट में सिर्फ़ आपके एनवायरनमेंट की जानकारी होगी।" +no_cli_sync = "इस बिल्ड के साथ Cap कमांड-लाइन टूल नहीं मिला, इसलिए सिंक टेस्ट नहीं चल सका।" +run = "डायग्नोस्टिक चलाएँ" +run_again = "फिर से चलाएँ" +send = "Cap को भेजें" +sending = "भेजा जा रहा है..." +show_file = "फ़ाइल दिखाएँ" +stop_recording_first = "डायग्नोस्टिक चलाने से पहले मौजूदा रिकॉर्डिंग रोकें।" +title = "डायग्नोस्टिक रिपोर्ट" +try_again = "फिर से कोशिश करें" +warning_body = "एक फ़ुलस्क्रीन विंडो काले और सफ़ेद रंग में चमकती है जबकि आपके स्पीकर से 1kHz की बीप बजती है, और Cap इसे रिकॉर्ड करके मापता है कि तस्वीर और आवाज़ कितनी अलग हो गई हैं। चलते समय कंप्यूटर को न छुएँ। दोनों मोड मिलाकर कुछ मिनट लगते हैं।" +warning_title = "टेस्ट आपकी पूरी स्क्रीन लेता है और आवाज़ करता है।" + +[settings.feedback.diagnostic.verdict] +environment_only = "सिर्फ़ एनवायरनमेंट" +fail = "फ़ेल" +inconclusive = "अनिर्णायक" +pass = "पास" +warn = "चेतावनी" + +[settings.sign_in_required] +body = "जारी रखने के लिए कृपया साइन इन करें। या hello@cap.so पर ईमेल करें या cap.link/discord पर हमारे Discord से जुड़ें" +title = "साइन इन ज़रूरी है" + +[settings.changelog] +fetch_failed = "चेंजलॉग नहीं मिल सका" +loading = "चेंजलॉग लोड हो रहा है..." +new = "नया" +version = "वर्शन {version} - {date}" + +[settings.license] +activate = "लाइसेंस सक्रिय करें" +activating = "सक्रिय हो रहा है..." +active_title = "डेस्कटॉप लाइसेंस" +checking = "जाँच हो रही है…" +deactivate = "लाइसेंस निष्क्रिय करें" +description = "आपका Cap प्लान और डेस्कटॉप लाइसेंस, एक ही जगह।" +desktop_description = "आपका डेस्कटॉप लाइसेंस कमर्शियल रिकॉर्डिंग और एडिटिंग को कवर करता है। Cap Pro इसमें क्लाउड शेयरिंग और कोलैबोरेशन फ़ीचर जोड़ता है।" +desktop_name = "डेस्कटॉप लाइसेंस" +expires = "समाप्ति" +explore_description = "हमारी वेबसाइट पर मौजूदा कीमतें और उनमें शामिल सब कुछ तुलना करें।" +explore_desktop = "डेस्कटॉप लाइसेंस · डेस्कटॉप रिकॉर्डर और एडिटर का कमर्शियल इस्तेमाल।" +explore_pro = "Cap Pro · डेस्कटॉप लाइसेंस, साथ में क्लाउड शेयरिंग, AI फ़ीचर और कोलैबोरेशन।" +explore_title = "प्लान देखें" +free_description = "निजी इस्तेमाल के लिए लोकल रूप से रिकॉर्ड और एडिट करें। कमर्शियल इस्तेमाल या ज़्यादा क्लाउड फ़ीचर के लिए पेड प्लान चुनें।" +have_key = "क्या आपके पास लाइसेंस की है?" +key = "लाइसेंस की" +no_instance = "कोई इंस्टेंस ID नहीं मिली" +opens_pricing = "आपके ब्राउज़र में cap.so/pricing खोलता है।" +placeholder = "लाइसेंस की" +plan_title = "आपका प्लान" +pro_description = "आपके खाते में क्लाउड शेयरिंग, Pro फ़ीचर और कमर्शियल इस्तेमाल के लिए डेस्कटॉप लाइसेंस शामिल है।" +refresh = "प्लान रीफ़्रेश करें" +refresh_failed = "आपका प्लान रीफ़्रेश नहीं हो सका। कृपया फिर से कोशिश करें।" +save_failed = "लाइसेंस सेव नहीं हो सका" +sign_in_hint = "पहले से Cap Pro है? साइडबार से अपने खाते से साइन इन करें।" +title = "प्लान और लाइसेंस" +view_pricing = "प्लान और कीमतें देखें ↗" + +[settings.integrations] +configure = "कॉन्फ़िगर करें" +description = "Cap की क्षमताएँ बढ़ाने और थर्ड-पार्टी सेवाओं से जुड़ने के लिए इंटीग्रेशन कॉन्फ़िगर करें।" +google_drive_description = "नए शेयर करने लायक लिंक वाले अपलोड के लिए Google Drive कनेक्ट करें। Cap नए वीडियो आपके Drive में एक निजी Cap फ़ोल्डर में रखता है और सामान्य एक्सेस जाँच के बाद उन्हें Cap के ज़रिए दिखाता रहता है।" +managed = "आपके संगठन द्वारा मैनेज किया जाता है" +managed_by = "आपके संगठन द्वारा मैनेज किया जाता है: {organization}" +request_timed_out = "अनुरोध का समय समाप्त हो गया" +s3_description = "अपने डेटा स्टोरेज पर पूरा नियंत्रण पाने के लिए अपना S3 बकेट कनेक्ट करें। सभी नए शेयर करने लायक लिंक वाले अपलोड अपने आप आपके कॉन्फ़िगर किए गए S3 बकेट में अपलोड होंगे, ताकि आपकी सामग्री पर पूरा स्वामित्व और नियंत्रण आपके पास रहे। डेटा संप्रभुता और कस्टम स्टोरेज नीतियों की ज़रूरत वाले संगठनों के लिए बढ़िया।" +s3_name = "S3 कॉन्फ़िग" +title = "इंटीग्रेशन" +upgrade = "Pro में अपग्रेड करें" + +[settings.integrations.s3] +access_key = "एक्सेस की ID" +bucket = "बकेट का नाम" +configuration = "कॉन्फ़िगरेशन" +delete_failed = "S3 कॉन्फ़िग डिलीट नहीं हो सका" +deleted = "S3 कॉन्फ़िगरेशन डिलीट हो गया" +endpoint = "एंडपॉइंट" +fetch_failed = "S3 कॉन्फ़िग नहीं मिल सका" +guide = "स्टोरेज कॉन्फ़िग गाइड" +guide_intro = "अपना स्टोरेज बकेट सेट अप करके Cap से कनेक्ट करने में 10 मिनट से कम लगने चाहिए। शुरू करने के लिए\u00A0{guide}\u00A0देखें।" +other_provider = "अन्य S3-कम्पैटिबल" +provider = "स्टोरेज प्रोवाइडर" +region = "रीजन" +remove = "कॉन्फ़िग हटाएँ" +removing = "हटाया जा रहा है..." +save_failed = "S3 कॉन्फ़िग सेव नहीं हो सका" +saved = "S3 कॉन्फ़िगरेशन सेव हो गया" +saving = "सेव हो रहा है..." +secret_key = "सीक्रेट एक्सेस की" +test = "कनेक्शन टेस्ट करें" +test_failed = "S3 कनेक्शन टेस्ट विफल रहा। अपना कॉन्फ़िग और नेटवर्क कनेक्शन जाँचें।" +test_ok = "S3 कॉन्फ़िगरेशन टेस्ट सफल रहा! कनेक्शन काम कर रहा है।" +test_timeout = "कनेक्शन टेस्ट 5 सेकंड बाद टाइम आउट हो गया। कृपया अपना एंडपॉइंट URL और नेटवर्क कनेक्शन जाँचें।" +testing = "टेस्ट हो रहा है..." + +[settings.integrations.gdrive] +active = "सक्रिय" +active_for_uploads = "नए अपलोड के लिए सक्रिय" +cached = "{time} का कैश" +connect = "Google Drive कनेक्ट करें" +connect_failed = "Google Drive कनेक्शन शुरू नहीं हो सका" +connected_inactive = "कनेक्टेड है पर सक्रिय नहीं" +connection_description = "Google Drive नए अपलोड आपके Drive में एक निजी Cap फ़ोल्डर में रखता है। Cap पर होस्ट किए गए और S3 के मौजूदा वीडियो अपने मौजूदा स्टोरेज का ही इस्तेमाल करते रहते हैं।" +connection_title = "कनेक्शन" +disconnect = "डिस्कनेक्ट करें" +disconnect_failed = "Google Drive डिस्कनेक्ट नहीं हो सका" +disconnected = "Google Drive डिस्कनेक्ट हो गया" +drive_files = "Drive फ़ाइलें" +finish_in_browser = "अपने ब्राउज़र में Google Drive कनेक्ट करना पूरा करें, फिर यहाँ लौटकर रीफ़्रेश करें।" +not_connected = "कनेक्ट नहीं है" +opening = "खुल रहा है..." +refresh = "रीफ़्रेश करें" +refreshing = "रीफ़्रेश हो रहा है..." +remaining = "बाकी" +set_active_failed = "सक्रिय स्टोरेज प्रोवाइडर अपडेट नहीं हो सका" +storage = "स्टोरेज" +test = "टेस्ट करें" +test_failed = "Google Drive कनेक्शन टेस्ट विफल रहा" +testing = "टेस्ट हो रहा है..." +trash = "ट्रैश" +updated = "{time} को अपडेट हुआ" +use_drive = "Google Drive इस्तेमाल करें" +use_s3 = "S3 इस्तेमाल करें" +used = "{usage} इस्तेमाल हुआ" +used_of = "{limit} में से {usage} इस्तेमाल हुआ" +waiting = "इंतज़ार हो रहा है..." +working = "Google Drive कनेक्शन काम कर रहा है" +working_for = "{email} के लिए Google Drive कनेक्शन काम कर रहा है" + +[settings.automations] +action_no_effect = "चुने गए ट्रिगर के लिए इस कार्रवाई का कोई असर नहीं होता।" +add_action = "कार्रवाई जोड़ें" +add_condition = "शर्त जोड़ें" +auto_name_empty = "{trigger} ऑटोमेशन" +browse = "ब्राउज़ करें" +check_compatibility = "कम्पैटिबिलिटी जाँचें" +compatibility_checked = "कम्पैटिबिलिटी जाँची गई: यह ऑटोमेशन अभी Cap में नहीं चलता। सेव किए गए नियम Cap CLI के लिए उपलब्ध रहते हैं। कोई कार्रवाई नहीं चलाई गई।" +condition_never_matches = "चुने गए ट्रिगर के लिए यह शर्त कभी पूरी नहीं होती।" +copy_link = "लिंक क्लिपबोर्ड पर कॉपी करें" +dangerous = "Cap CLI में यह ऑटोमेशन आपकी अनुमतियों से कमांड चलाता है या नेटवर्क अनुरोध भेजता है। सिर्फ़ भरोसेमंद वैल्यू इस्तेमाल करें।" +delete = "डिलीट करें" +description = "नियम Cap CLI के साथ साझा होते हैं। Cap इन नियमों को सेव करता है, लेकिन अभी ऑटोमेशन नहीं चलाता।" +empty_description = "एक क्लिक में शुरू करने के लिए नीचे कोई टेम्पलेट चुनें, या शुरुआत से अपना बनाएँ।" +empty_title = "अभी कोई ऑटोमेशन नहीं" +enabled = "चालू" +match_all = "सभी मिलें" +match_any = "कोई भी मिले" +name = "नाम" +new = "नया ऑटोमेशन" +no_presets = "अभी कोई प्रीसेट नहीं है, पहले एडिटर में एक बनाएँ।" +only_if = "सिर्फ़ तब चलाएँ जब" +open_in_browser = "ब्राउज़र में खोलें" +retry_save = "फिर से सेव करें" +run_through_shell = "शेल के ज़रिए चलाएँ" +save_failed = "ऑटोमेशन सेव नहीं हो सके। आपके बदलाव सेव नहीं हुए हैं।" +start_from_scratch = "शुरुआत से बनाएँ" +summary_empty = "{trigger} → अभी कोई कार्रवाई नहीं" +templates_description = "एक क्लिक में तैयार ऑटोमेशन जोड़ें। बाद में कुछ भी बदलें।" +templates_title = "टेम्पलेट" +then = "फिर यह करें" +title = "ऑटोमेशन" +when = "जब यह हो" + +[settings.automations.status] +disabled_saved = "बंद · सेव है" +disabled_unsaved = "बंद · सेव नहीं है" +enabled_saved = "चालू · सेव है" +enabled_unsaved = "चालू · सेव नहीं है" + +[settings.automations.field] +arguments = "आर्ग्युमेंट (स्पेस से अलग)" +body = "मुख्य भाग" +body_template = "बॉडी टेम्पलेट (वैकल्पिक)" +compression = "कम्प्रेशन" +destination = "डेस्टिनेशन फ़ोल्डर (वैकल्पिक, खाली = प्रोजेक्ट फ़ोल्डर)" +filename = "फ़ाइल नाम टेम्पलेट (वैकल्पिक)" +folder = "फ़ोल्डर" +format = "फ़ॉर्मैट" +frame_rate = "फ़्रेम रेट" +method = "मेथड" +organization = "संगठन ID (वैकल्पिक)" +preset = "प्रीसेट" +program = "प्रोग्राम" +resolution = "रेज़ोल्यूशन" +source = "सोर्स" +title = "शीर्षक" +url = "URL" + +[settings.automations.placeholder] +organization_id = "संगठन ID" +project_folder = "प्रोजेक्ट फ़ोल्डर" +window_title = "जैसे Slack" + +[settings.automations.clipboard] +original = "ओरिजिनल कैप्चर" +rendered = "एडिट की गई / रेंडर की गई" + +[settings.automations.target] +area = "क्षेत्र" +display = "डिस्प्ले" +window = "विंडो" + +[settings.automations.compression] +maximum = "अधिकतम" +potato = "सबसे कम" +social = "सोशल" +web = "वेब" + +[settings.automations.trigger.screenshot_taken] +label = "स्क्रीनशॉट लेने पर" +noun = "स्क्रीनशॉट" +phrase = "स्क्रीनशॉट लिया गया" +runs = "हर स्क्रीनशॉट पर चलता है।" + +[settings.automations.trigger.studio_finished] +label = "Studio रिकॉर्डिंग खत्म होने पर" +noun = "Studio रिकॉर्डिंग" +phrase = "Studio रिकॉर्डिंग खत्म हुई" +runs = "हर Studio रिकॉर्डिंग खत्म होने पर चलता है।" + +[settings.automations.trigger.instant_finished] +label = "Instant रिकॉर्डिंग खत्म होने पर" +noun = "Instant रिकॉर्डिंग" +phrase = "Instant रिकॉर्डिंग खत्म हुई" +runs = "हर Instant रिकॉर्डिंग खत्म होने पर चलता है।" + +[settings.automations.trigger.recording_started] +label = "रिकॉर्डिंग शुरू होने पर" +noun = "रिकॉर्डिंग शुरू" +phrase = "रिकॉर्डिंग शुरू हुई" +runs = "हर रिकॉर्डिंग शुरू होने पर चलता है।" + +[settings.automations.trigger.upload_completed] +label = "अपलोड पूरा होने पर" +noun = "अपलोड" +phrase = "अपलोड पूरा हुआ" +runs = "हर अपलोड पूरा होने पर चलता है।" + +[settings.automations.trigger.video_imported] +label = "वीडियो इम्पोर्ट होने पर" +noun = "इम्पोर्ट" +phrase = "वीडियो इम्पोर्ट हुआ" +runs = "हर वीडियो इम्पोर्ट होने पर चलता है।" + +[settings.automations.trigger.recording_deleted] +label = "रिकॉर्डिंग डिलीट होने पर" +noun = "डिलीट" +phrase = "रिकॉर्डिंग डिलीट हुई" +runs = "हर रिकॉर्डिंग डिलीट होने पर चलता है।" + +[settings.automations.action.copy_to_clipboard] +label = "क्लिपबोर्ड पर कॉपी करें" +noun = "क्लिपबोर्ड" +short = "क्लिपबोर्ड पर कॉपी" + +[settings.automations.action.save_to_location] +label = "किसी जगह पर सेव करें" +noun = "फ़ोल्डर" +short = "फ़ोल्डर में सेव" + +[settings.automations.action.export] +label = "प्रोफ़ाइल के साथ एक्सपोर्ट करें" +noun = "एक्सपोर्ट" +short = "एक्सपोर्ट" + +[settings.automations.action.upload] +label = "अपलोड + लिंक कॉपी करें" +noun = "अपलोड" +short = "अपलोड और लिंक कॉपी" + +[settings.automations.action.reveal] +label = "फ़ाइल मैनेजर में दिखाएँ" +noun = "दिखाएँ" +short = "फ़ाइल मैनेजर में दिखाएँ" + +[settings.automations.action.open_file] +label = "फ़ाइल खोलें" +noun = "खोलें" +short = "फ़ाइल खोलें" + +[settings.automations.action.ocr] +label = "टेक्स्ट पहचानकर (OCR) क्लिपबोर्ड पर" +noun = "टेक्स्ट" +short = "टेक्स्ट कॉपी (OCR)" + +[settings.automations.action.notify] +label = "नोटिफ़िकेशन दिखाएँ" +noun = "सूचना" +short = "सूचित करें" + +[settings.automations.action.open_editor] +label = "एडिटर खोलें" +noun = "एडिटर" +short = "एडिटर खोलें" + +[settings.automations.action.skip_editor] +label = "एडिटर छोड़ें (हेडलेस)" +noun = "एडिटर छोड़ें" +short = "एडिटर छोड़ें" + +[settings.automations.action.apply_preset] +label = "एडिटर प्रीसेट लागू करें" +noun = "प्रीसेट" +short = "प्रीसेट लागू करें" + +[settings.automations.action.run_command] +label = "कमांड चलाएँ" +noun = "कमांड" +short = "कमांड चलाएँ" + +[settings.automations.action.webhook] +label = "वेबहुक भेजें" +noun = "वेबहुक" +short = "वेबहुक भेजें" + +[settings.automations.action.delete_local_files] +label = "लोकल फ़ाइलें डिलीट करें" +noun = "डिलीट" +short = "लोकल फ़ाइलें डिलीट करें" + +[settings.automations.condition] +capture_target = "कैप्चर टारगेट है" +duration_at_least = "अवधि कम से कम (सेकंड)" +duration_at_most = "अवधि ज़्यादा से ज़्यादा (सेकंड)" +organization = "संगठन है" +recording_mode = "रिकॉर्डिंग मोड है" +window_title = "विंडो के शीर्षक में शामिल है" + +[settings.automations.template.copy_screenshot] +description = "स्क्रीनशॉट लें और वह तुरंत पेस्ट करने के लिए तैयार है।" +name = "नए स्क्रीनशॉट अपने आप क्लिपबोर्ड पर कॉपी करें" + +[settings.automations.template.ocr_screenshot] +description = "Cap आपके स्क्रीनशॉट का टेक्स्ट पढ़कर आपके लिए कॉपी कर देता है।" +name = "स्क्रीनशॉट से टेक्स्ट निकालें" + +[settings.automations.template.save_screenshot] +description = "हर नया स्क्रीनशॉट सीधे आपके चुने फ़ोल्डर में भेजें।" +name = "स्क्रीनशॉट एक फ़ोल्डर में रखें" + +[settings.automations.template.reveal_screenshot] +description = "स्क्रीनशॉट लेते ही उसे Finder में खोलें।" +name = "हर नए स्क्रीनशॉट पर जाएँ" + +[settings.automations.template.export_studio] +description = "Studio रिकॉर्डिंग खत्म होते ही MP4 रेंडर करें।" +name = "रिकॉर्डिंग खत्म होते ही अपने आप एक्सपोर्ट करें" + +[settings.automations.template.upload_share] +description = "रिकॉर्डिंग खत्म करें और लिंक आपके क्लिपबोर्ड पर तैयार मिलेगा।" +name = "अपलोड करें और शेयर लिंक पाएँ" + +[settings.automations.template.notify_upload] +body = "आपकी रिकॉर्डिंग शेयर करने के लिए तैयार है।" +description = "रिकॉर्डिंग शेयर करने लायक होते ही डेस्कटॉप पर हल्का-सा संकेत पाएँ।" +name = "अपलोड तैयार होने पर मुझे बताएँ" + +[settings.automations.template.webhook_share] +description = "शेयर लिंक Slack, Discord या अपने वेबहुक पर भेजें।" +name = "कुछ शेयर करने पर Slack को बताएँ" + [main] delete_recording_confirm = "क्या आप वाकई यह रिकॉर्डिंग हटाना चाहते हैं?" @@ -586,3 +1296,397 @@ click_or_press = "यहाँ क्लिक करें, या दबाए get_started = "शुरू करें" space_key = "स्पेस" tagline = "सबके लिए ओपन सोर्स स्क्रीन शेयरिंग ऐप।" + +[menu.update] +badge_progress = "v{version} · {percent}%" +badge_version = "v{version}" +check = "अपडेट जाँचें" +download = "अपडेट डाउनलोड करें…" +checking = "अपडेट जाँच रहे हैं…" +downloading = "अपडेट डाउनलोड हो रहा है…" +restart = "अपडेट करने के लिए फिर से शुरू करें…" + +[settings.general.updates.download_policy] +auto = "स्वचालित" +description = "चुनें कि Cap अपडेट स्वचालित रूप से डाउनलोड करे या नहीं।" +label = "अपडेट डाउनलोड" +manual = "पहले पूछें" +never = "कभी नहीं" + +[auth] +sign_in_failed = "साइन इन विफल रहा" + +[diagnostics] +already_running = "एक डायग्नॉस्टिक पहले से चल रहा है।" +mode_both = "दोनों" + +[diagnostics.error] +create_folder = "डायग्नॉस्टिक फ़ोल्डर नहीं बन सका: {error}" +exited = "सेल्फ़-टेस्ट {status} के साथ बंद हुआ" +no_output = "सेल्फ़-टेस्ट से कोई आउटपुट स्ट्रीम नहीं मिली" +serialize = "रिपोर्ट सीरियलाइज़ नहीं हो सकी: {error}" +start_failed = "{program} शुरू नहीं हो सका: {error}" +upload_logs = "लॉग अपलोड नहीं हो सके: {error}" +upload_status = "अपलोड विफल, स्टेटस: {status}" +wait_failed = "सेल्फ़-टेस्ट के पूरा होने का इंतज़ार नहीं हो सका: {error}" +write_failed = "{path} नहीं लिखा जा सका: {error}" + +[diagnostics.stage] +analyzing = "रिकॉर्डिंग का विश्लेषण हो रहा है..." +analyzing_instant = "Instant रिकॉर्डिंग का विश्लेषण हो रहा है..." +analyzing_studio = "Studio रिकॉर्डिंग का विश्लेषण हो रहा है..." +cancelling = "रद्द हो रहा है..." +collecting = "सिस्टम जानकारी इकट्ठा हो रही है..." +done = "सिंक टेस्ट पूरा हो रहा है..." +exporting = "रिकॉर्डिंग एक्सपोर्ट हो रही है..." +exporting_instant = "Instant रिकॉर्डिंग एक्सपोर्ट हो रही है..." +exporting_studio = "Studio रिकॉर्डिंग एक्सपोर्ट हो रही है..." +finalizing = "रिकॉर्डिंग पूरी की जा रही है..." +finalizing_instant = "Instant रिकॉर्डिंग पूरी की जा रही है..." +finalizing_studio = "Studio रिकॉर्डिंग पूरी की जा रही है..." +pattern = "टेस्ट पैटर्न चल रहा है..." +pattern_instant = "टेस्ट पैटर्न चल रहा है (Instant)..." +pattern_studio = "टेस्ट पैटर्न चल रहा है (Studio)..." +preparing = "टेस्ट की तैयारी हो रही है..." +preparing_instant = "Instant टेस्ट की तैयारी हो रही है..." +preparing_studio = "Studio टेस्ट की तैयारी हो रही है..." +recording = "टेस्ट रिकॉर्ड हो रहा है..." +recording_instant = "Instant टेस्ट रिकॉर्ड हो रहा है..." +recording_studio = "Studio टेस्ट रिकॉर्ड हो रहा है..." +starting = "डायग्नॉस्टिक शुरू हो रहा है..." + +[dialog] +no_microphone_body = "इस रिकॉर्डिंग में आपकी आवाज़ शामिल नहीं होगी।" +open_recording_failed = "यह रिकॉर्डिंग नहीं खुल सकी: {error}" + +[dialog.capture] +approve_shortcut = "रिकॉर्डिंग शॉर्टकट को मंज़ूरी दें, या Cap Stop ट्रे आइकन दिखने पर उसे सक्रिय करें। रिकॉर्डिंग अभी शुरू नहीं हुई है।" +area_unavailable = "चुना गया क्षेत्र बदल गया है या अब उपलब्ध नहीं है। इसे फिर से चुनें।" +camera_needs_x11 = "Linux पर Instant कैमरा ओवरले के लिए X11 डेस्कटॉप चाहिए" +camera_notice = "Cap अपना कैमरा प्रीव्यू और रिकॉर्डिंग कंट्रोल छिपा देगा। चुना गया कैमरा एक अलग, एडिट करने लायक ट्रैक के रूप में रिकॉर्ड होता रहेगा।" +camera_notice_instant = "Cap अपना प्रीव्यू और कंट्रोल छिपा देगा। चुना गया कैमरा चुने गए आकार और इफ़ेक्ट के साथ अपने प्रीव्यू की जगह पर दिखेगा। पूरा प्रीव्यू कैप्चर क्षेत्र के अंदर रखें। Cap खोलने पर कंट्रोल दिखाने से पहले यह Instant रिकॉर्डिंग रुक जाएगी।" +camera_outside_area = "Instant रिकॉर्डिंग शुरू करने से पहले पूरा कैमरा प्रीव्यू कैप्चर क्षेत्र के अंदर ले जाएँ" +camera_preview_closed = "Instant रिकॉर्डिंग से पहले चुने गए कैमरा का प्रीव्यू खोलें" +cleanup_incomplete = "रिकॉर्डिंग की सफ़ाई अधूरी है: {error}। नई रिकॉर्डिंग रोकी गई है।" +devices_changed = "चुने गए डिवाइस बदल गए। नए रिकॉर्डिंग इनपुट की पुष्टि के लिए फिर से शुरू करें।" +devices_changed_before_start = "रिकॉर्डिंग शुरू होने से पहले चुने गए डिवाइस बदल गए। इनपुट की पुष्टि के लिए फिर से शुरू करें।" +shortcut_conflict = "Ctrl+Shift+F9 किसी दूसरे Cap एक्शन को दिया गया है। रिकॉर्ड करने से पहले वह शॉर्टकट बदलें।" +shortcut_description = "यह Cap रिकॉर्डिंग शुरू या बंद करें" +shortcut_hint = "शुरू करने के लिए {shortcut} दबाकर छोड़ें। रोकने के लिए वही शॉर्टकट इस्तेमाल करें। यह अस्थायी शॉर्टकट आपकी सेटिंग्स नहीं बदलता।" +tray_stop_hint = "शुरू करने के लिए Cap Stop ट्रे आइकन सक्रिय करें। रोकने के लिए वही आइकन फिर से सक्रिय करें। जब तक आप उस कंट्रोल का इस्तेमाल नहीं करते, रिकॉर्डिंग शुरू नहीं होगी।" +window_unavailable = "चुनी गई विंडो अब उपलब्ध नहीं है। इसे फिर से चुनें।" + +[dialog.clipboard] +copy_file = "फ़ाइल क्लिपबोर्ड पर कॉपी नहीं हो सकी: {error}" +copy_file_failed = "फ़ाइल क्लिपबोर्ड पर कॉपी नहीं हो सकी" +copy_image = "इमेज क्लिपबोर्ड पर कॉपी नहीं हो सकी: {error}" +copy_image_failed = "इमेज क्लिपबोर्ड पर कॉपी नहीं हो सकी" +not_a_file = "क्लिपबोर्ड पर सिर्फ़ फ़ाइलें कॉपी की जा सकती हैं" +unavailable = "क्लिपबोर्ड उपलब्ध नहीं है" +unavailable_error = "क्लिपबोर्ड उपलब्ध नहीं है: {error}" + +[dialog.delete_recording] +already_deleting = "यह रिकॉर्डिंग पहले से डिलीट हो रही है।" +close_failed = "एडिटर बंद नहीं हो सका: {error}" +editor_changed = "डिलीट की पुष्टि के दौरान एडिटर बदल गया। रिकॉर्डिंग डिलीट नहीं हुई।" +editor_closed = "एडिटर पहले ही बंद हो चुका है। रिकॉर्डिंग डिलीट नहीं हुई।" +failed = "रिकॉर्डिंग डिलीट नहीं हो सकी: {error}" +failed_title = "रिकॉर्डिंग डिलीट नहीं हो सकी" +finishing_body = "यह डिलीट पूरा करने से पहले Cap रिकॉर्डिंग की सफ़ाई पूरी होने का इंतज़ार कर रहा है।" +finishing_title = "डिलीट अभी पूरा हो रहा है" +in_use = "यह रिकॉर्डिंग अभी इस्तेमाल में है। इसका अपलोड पूरा या रद्द करें और फिर कोशिश करें: {error}" +outside_library = "यह रिकॉर्डिंग रिकॉर्डिंग लाइब्रेरी से बाहर है। इसकी फ़ाइलें नहीं बदली गईं।" +recording_active = "यह प्रोजेक्ट डिलीट करने से पहले रिकॉर्डिंग पूरी या रद्द करें।" +retained_title = "रिकॉर्डिंग रखी गई" + +[dialog.file] +audio_filter = "ऑडियो" +busy = "एक फ़ाइल डायलॉग पहले से खुला है। दोबारा कोशिश करने से पहले उसे पूरा या रद्द करें।" +export_filter = "एक्सपोर्ट" +failed = "फ़ाइल डायलॉग पूरा नहीं हो सका: {error}" +images_filter = "इमेज" +unavailable_title = "फ़ाइल डायलॉग उपलब्ध नहीं है" +unconfirmed = "Cap पुष्टि नहीं कर सका कि फ़ाइल डायलॉग बंद हुआ। बचे हुए फ़ाइल डायलॉग बंद करें और दोबारा कोशिश करने से पहले Cap रीस्टार्ट करें।" + +[dialog.mode_select] +instant = "लिंक से तुरंत शेयर करें। रिकॉर्ड करते समय ही अपलोड होता है।" +screenshot = "तुरंत स्क्रीनशॉट लें और उन पर एनोटेशन जोड़ें।" +studio = "बाद में एडिट करने के लिए सबसे अच्छी क्वालिटी में लोकल रिकॉर्डिंग।" +subtitle = "चुनें कि आप अपनी स्क्रीन कैसे कैप्चर करना चाहते हैं" +title = "रिकॉर्डिंग मोड चुनें" + +[dialog.storage] +stopped_kept = "स्टोरेज कम होने की वजह से रिकॉर्डिंग रुक गई। आपकी रिकॉर्डिंग फ़ाइलें रखी गई हैं। जगह खाली करें, फिर नीचे रिकॉर्डिंग रिकवर करें।" +stopped_saved = "स्टोरेज कम होने की वजह से रिकॉर्डिंग रुक गई। आपकी रिकॉर्डिंग सहेज ली गई है।" +stopping = "स्टोरेज कम है। आपकी रिकॉर्डिंग रोककर सहेजी जा रही है…" + +[dialog.teleprompter] +cue_markers = "क्यू मार्कर" +hidden = "यह विंडो Cap रिकॉर्डिंग में नहीं दिखती" +mirror = "टेक्स्ट मिरर करें" +paused = "रिकॉर्डिंग के साथ स्क्रॉलिंग रुकी है" +placeholder = "अपनी स्क्रिप्ट पेस्ट करें या लिखें…" + +[import] +error_title = "इंपोर्ट त्रुटि" +image_failed = "इमेज इंपोर्ट नहीं हो सकी: {error}" +unsupported = "यह मीडिया फ़ाइल समर्थित नहीं है।" +video_failed = "वीडियो इंपोर्ट नहीं हो सका: {error}" + +[import.filter] +image = "इमेज फ़ाइलें" +media = "मीडिया फ़ाइलें" +video = "वीडियो फ़ाइलें" + +[import.progress] +analyzing = "वीडियो फ़ाइल का विश्लेषण हो रहा है..." +complete = "इंपोर्ट पूरा हुआ!" +converting = "वीडियो कन्वर्ट हो रहा है... {percent}%" +image = "इमेज इंपोर्ट हो रही है..." +metadata = "प्रोजेक्ट मेटाडेटा बन रहा है..." +starting = "कन्वर्ज़न शुरू हो रहा है..." + +[overlay] +choose_mode = "रिकॉर्डिंग मोड चुनें" +countdown_tooltip = "रिकॉर्डिंग काउंटडाउन" +display_fallback = "डिस्प्ले {id}" + +[overlay.area] +draw = "एक क्षेत्र बनाएँ" +free_ratio = "फ़्री" +hint = "क्षेत्र चुनने के लिए क्लिक करके खींचें" +minimum_size = "न्यूनतम साइज़ {width} x {height} है" +too_small = "{width} x {height} बहुत छोटा है" + +[overlay.camera_only] +description = "सिर्फ़ अपने कैमरा और माइक्रोफ़ोन से रिकॉर्ड करें" +select_camera = "कृपया एक कैमरा चुनें" + +[overlay.device] +camera_allow = "कैमरा एक्सेस की अनुमति दें…" +camera_turn_on = "सेटिंग्स में कैमरा एक्सेस चालू करें…" +microphone_allow = "माइक्रोफ़ोन एक्सेस की अनुमति दें…" +microphone_turn_on = "सेटिंग्स में माइक्रोफ़ोन एक्सेस चालू करें…" + +[overlay.start] +instant = "Instant मोड: शेयर करने लायक लिंक रिकॉर्ड करें" +instant_sign_in = "Instant मोड: तुरंत रिकॉर्ड करने के लिए साइन इन करें" +sign_in = "इस्तेमाल के लिए साइन इन करें" +start_recording = "रिकॉर्डिंग शुरू करें" +studio = "Studio मोड: लोकल रूप से रिकॉर्ड और एडिट करें" +take_screenshot = "स्क्रीनशॉट लें" + +[permission.guide] +accessibility_body = "इसे ऊपर दी गई ऐक्सेसिबिलिटी लिस्ट में छोड़ें, फिर पक्का करें कि इसका स्विच चालू है।" +screen_body = "इसे ऊपर दी गई स्क्रीन और सिस्टम ऑडियो रिकॉर्डिंग लिस्ट में छोड़ें, फिर पक्का करें कि इसका स्विच चालू है।" +title = "Cap को लिस्ट में खींचें" + +[permission.name] +accessibility = "ऐक्सेसिबिलिटी" +camera = "कैमरा" +microphone = "माइक्रोफ़ोन" +screen_recording = "स्क्रीन रिकॉर्डिंग" + +[permission.onboarding] +open_settings_again = "सिस्टम सेटिंग्स फिर से खोलें" +skip = "अभी के लिए छोड़ें" +switch_on_title = "सिस्टम सेटिंग्स में Cap चालू करें" + +[permission.onboarding.accessibility] +allow = "ऐक्सेसिबिलिटी की अनुमति दें" +body = "ऐक्सेसिबिलिटी से Cap सिर्फ़ इसी Mac पर आपके क्लिक और कर्सर देख पाता है, ताकि ऑटो ज़ूम को पता रहे कि कहाँ देखना है।" +done_body = "ऑटो ज़ूम अब आपके क्लिक फ़ॉलो कर सकता है।" +done_title = "ऐक्सेसिबिलिटी चालू है" +title = "Cap को अपना कर्सर फ़ॉलो करने दें" + +[permission.onboarding.all_set] +body = "Cap के पास ज़रूरत की हर चीज़ है। अब एक झलक देखें कि यह क्या-क्या कर सकता है।" +regained = "धन्यवाद। Cap फिर से रिकॉर्ड कर सकता है।" +title = "सब तैयार है" + +[permission.onboarding.camera] +allow = "कैमरा की अनुमति दें" +body = "अपनी रिकॉर्डिंग के एक कोने में खुद को दिखाएँ। ज़रूरत न हो तो इसे छोड़ दें।" +done_body = "आप किसी भी रिकॉर्डिंग में अपना कैमरा जोड़ सकते हैं।" +done_title = "कैमरा चालू है" +title = "Cap को आपको देखने दें" + +[permission.onboarding.checklist] +lede = "macOS पहले कुछ चीज़ों की अनुमति माँगता है। इसमें बस एक मिनट लगता है।" +regained_lede = "सिस्टम सेटिंग्स में कुछ बंद कर दिया गया था। रिकॉर्डिंग जारी रखने के लिए इसे फिर से चालू करें।" +regained_title = "Cap को एक अनुमति वापस चाहिए" +title = "चलिए Cap को तैयार करें" + +[permission.onboarding.hint] +drag_into_list = "सिस्टम सेटिंग्स के नीचे वाले पैनल से Cap को लिस्ट में खींचें। पहले से लिस्ट में है? बस उसे चालू कर दें।" +find_in_list = "लिस्ट में Cap ढूँढें और उसे चालू करें। अगर macOS ने पहले पूछा था, तो ‘सिस्टम सेटिंग्स खोलें’ चुनें।" +no_prompt = "कोई प्रॉम्प्ट नहीं दिखा? सिस्टम सेटिंग्स में Cap चालू करें।" +open_settings = "सिस्टम सेटिंग्स खोलें, लिस्ट में Cap ढूँढें और उसे चालू करें।" +turned_off = "इसे पहले बंद कर दिया गया था, इसलिए सिस्टम सेटिंग्स में Cap चालू करें। या अभी के लिए इसे छोड़ दें।" + +[permission.onboarding.microphone] +allow = "माइक्रोफ़ोन की अनुमति दें" +body = "अपनी रिकॉर्डिंग में अपनी आवाज़ जोड़ें। आप इसे अभी छोड़कर बाद में चालू कर सकते हैं।" +done_body = "अब आपकी आवाज़ आपकी रिकॉर्डिंग में आ सकती है।" +done_title = "माइक्रोफ़ोन चालू है" +title = "Cap को आपको सुनने दें" + +[permission.onboarding.relaunch] +body = "Cap के रीस्टार्ट होते ही macOS स्क्रीन रिकॉर्डिंग चालू कर देता है। आप यहीं से आगे बढ़ेंगे।" +button = "रीलॉन्च" +hint = "चालू कर दिया पर अब भी इंतज़ार? लागू करने के लिए Cap रीस्टार्ट करें।" +later = "macOS इसे Cap के रीस्टार्ट होने के बाद लागू करता है। यहाँ का काम पूरा होने पर आप ऐसा कर सकते हैं।" +title = "बस एक रीस्टार्ट और सब तैयार" + +[permission.onboarding.screen] +allow = "स्क्रीन रिकॉर्डिंग की अनुमति दें" +body = "स्क्रीन रिकॉर्डिंग से Cap आपके डिस्प्ले, विंडो और सिस्टम ऑडियो कैप्चर कर पाता है।" +done_body = "Cap अब आपकी स्क्रीन कैप्चर कर सकता है।" +done_title = "स्क्रीन रिकॉर्डिंग चालू है" +title = "Cap को अपनी स्क्रीन देखने दें" + +[permission.onboarding.status] +allowed = "अनुमति है" +off = "सिस्टम सेटिंग्स में बंद" +optional = "वैकल्पिक" +required = "ज़रूरी" +skipped = "अभी के लिए छोड़ा गया" + +[permission.onboarding.waiting] +prompt = "macOS प्रॉम्प्ट में ‘अनुमति दें’ चुनें" +system_settings = "सिस्टम सेटिंग्स का इंतज़ार है" + +[screenshot_editor] +loading_preview = "प्रीव्यू लोड हो रहा है..." +zoom_in = "ज़ूम इन" +zoom_out = "ज़ूम आउट" + +[screenshot_editor.annotation] +default_text = "टेक्स्ट" +done = "हो गया" +fill = "फ़िल" +intensity = "तीव्रता" +mask_style = "स्टाइल" +pixelate = "पिक्सेलेट" +stroke = "स्ट्रोक" +transparent = "पारदर्शी" + +[screenshot_editor.aspect] +auto = "ऑटो" +classic = "क्लासिक ⋅4:3" +square = "स्क्वेयर ⋅1:1" +tall = "लंबा ⋅3:4" +vertical = "वर्टिकल ⋅9:16" +wide = "वाइड ⋅16:9" + +[screenshot_editor.background] +animated_gradient = "स्क्रीनशॉट में एनिमेटेड ग्रेडिएंट एक स्थिर इमेज के रूप में दिखता है। इसे बदलने के लिए कोई दूसरा बैकग्राउंड चुनें।" +blur_title = "बैकग्राउंड ब्लर" +choose_image = "चुनने के लिए क्लिक करें या इमेज खींचकर छोड़ें" +color = "रंग" +gradient = "ग्रेडिएंट" +image = "इमेज" +image_files = "इमेज" +image_title = "बैकग्राउंड इमेज" +wallpaper = "वॉलपेपर" + +[screenshot_editor.corner] +rounded = "गोल" +squircle = "स्क्वर्कल" +style = "कोने की स्टाइल" + +[screenshot_editor.crop] +full = "पूरा" +loading_frame = "फ़्रेम लोड हो रहा है…" +position = "पोज़िशन" + +[screenshot_editor.error] +encode = "स्क्रीनशॉट एक्सपोर्ट एनकोड नहीं हो सका: {error}" +load_meta = "स्क्रीनशॉट मेटाडेटा लोड नहीं हो सका: {error}" +load_metadata = "स्क्रीनशॉट मेटाडेटा लोड नहीं हो सका: {error}" +no_image = "{path} में कोई PNG नहीं मिला" +not_screenshot = "यह स्क्रीनशॉट बंडल नहीं है" +open_image = "इमेज नहीं खुल सकी: {error}" +renderer_failed = "GPU रेंडरर शुरू नहीं हो सका: {error}" +renderer_not_running = "स्क्रीनशॉट रेंडरर नहीं चल रहा है" +renderer_stopped = "एक्सपोर्ट पूरा होने से पहले रेंडरर रुक गया" +save_project = "प्रोजेक्ट सहेजते समय त्रुटि: {error}" +serialize = "स्क्रीनशॉट कॉन्फ़िग सीरियलाइज़ नहीं हो सका: {error}" +sign_in_to_share = "अपने ब्राउज़र में Cap में साइन इन करें, फिर दोबारा शेयर करें" +too_large = "इमेज का आकार सीमा से ज़्यादा है: {width}x{height}" +upgrade_required = "इस फ़ीचर के लिए अपग्रेड किया गया प्लान चाहिए" +upload_task = "अपलोड टास्क विफल रहा: {error}" + +[screenshot_editor.header] +aspect_ratio = "आस्पेक्ट रेशियो" +copy = "क्लिपबोर्ड पर कॉपी करें" +crop_image = "इमेज क्रॉप करें" +more_actions = "और कार्रवाइयाँ" + +[screenshot_editor.layers] +empty = "अभी कोई लेयर नहीं" +empty_hint = "एनोटेशन जोड़ने के लिए ऊपर दिए टूल इस्तेमाल करें" +reorder_hint = "क्रम बदलने के लिए खींचें • ऊपर = सबसे आगे" +title = "लेयर" + +[screenshot_editor.menu] +delete = "डिलीट करें" +open_folder = "फ़ोल्डर खोलें" + +[screenshot_editor.share] +create_link = "शेयर करने लायक लिंक बनाएँ" +preparing = "अपलोड की तैयारी हो रही है" +rendering = "स्क्रीनशॉट रेंडर हो रहा है" +uploading = "स्क्रीनशॉट अपलोड हो रहा है" + +[screenshot_editor.style] +advanced_shadow = "एडवांस्ड शैडो सेटिंग्स" +background = "बैकग्राउंड" +blur = "ब्लर" +border = "बॉर्डर" +color = "रंग" +corner_rounding = "कोनों की गोलाई" +opacity = "अपारदर्शिता" +padding = "पैडिंग" +rounding = "गोलाई" +shadow = "शैडो" +size_label = "साइज़" +width = "चौड़ाई" + +[screenshot_editor.toast] +copied = "स्क्रीनशॉट क्लिपबोर्ड पर कॉपी हो गया!" +link_copied = "शेयर लिंक क्लिपबोर्ड पर कॉपी हो गया" +saved = "स्क्रीनशॉट सहेज लिया गया!" + +[screenshot_editor.tool] +arrow = "ऐरो" +circle = "सर्कल" +draw = "ड्रॉ" +mask = "मास्क" +rectangle = "रेक्टेंगल" +select = "चुनें" +text_label = "टेक्स्ट" + +[transcription.download] +complete = "डाउनलोड पूरा हुआ" +failed = "डाउनलोड विफल: {error}" +file_progress = "{file} डाउनलोड हो रहा है: {percent}%" +preparing = "मॉडल डाउनलोड की तैयारी हो रही है" +progress = "मॉडल डाउनलोड हो रहा है: {percent}%" + +[transcription.error] +intel_mac = "Parakeet मॉडल Intel Mac पर उपलब्ध नहीं हैं। इसके बजाय Whisper मॉडल इस्तेमाल करें" +model_load = "कैप्शन मॉडल लोड नहीं हो सका। इसे फिर से डाउनलोड करके देखें" +model_missing = "कैप्शन मॉडल नहीं मिला। कृपया पहले इसे डाउनलोड करें" +no_audio = "वीडियो फ़ाइल में कोई ऑडियो नहीं मिला" +unsupported_language = "सुझाया गया और हाई एक्यूरेसी मॉडल सिर्फ़ अंग्रेज़ी और 24 दूसरी यूरोपीय भाषाएँ ट्रांसक्राइब करते हैं। इस भाषा के कैप्शन के लिए Small या Medium मॉडल डाउनलोड करें" + +[upload.status] +failed = "अपलोड पर ध्यान देने की ज़रूरत है" +paused = "अपलोड रुका हुआ है" +prepared = "अपलोड तैयार है" +processing = "अपलोड प्रोसेस हो रहा है" +retrying = "अपलोड फिर से कोशिश करेगा" +sign_in = "अपलोड दोबारा करने के लिए साइन इन करें" +uploaded = "अपलोड हो गया" +uploading = "अपलोड हो रहा है" diff --git a/apps/desktop-gpui/locales/ja.toml b/apps/desktop-gpui/locales/ja.toml index 44dd3f99308..a38c092085b 100644 --- a/apps/desktop-gpui/locales/ja.toml +++ b/apps/desktop-gpui/locales/ja.toml @@ -24,9 +24,17 @@ quit = "Capを終了" services = "サービス" settings = "設定…" +[menu.about] +licensed = "AGPL-3.0ライセンス" +website = "ウェブサイト" +commit = "コミット" + [menu.file] close_window = "ウィンドウを閉じる" title = "ファイル" +open_file = "ファイルを開く…" +open_recent = "最近使った項目を開く" +import_media = "メディアを読み込む…" [menu.edit] copy = "コピー" @@ -49,12 +57,22 @@ zoom = "拡大/縮小" [menu.help] title = "ヘルプ" +feedback = "フィードバックを送る…" +changelog = "変更履歴" +dashboard = "ダッシュボード" +documentation = "ドキュメント" +faq = "FAQ" +self_hosting = "セルフホスティング" +help_center = "ヘルプセンター" +system_status = "システムステータス" +discord = "Discordコミュニティに参加" [tray] import_media = "メディアを読み込む..." no_recent_items = "最近の項目はありません" open_main_window = "メインウィンドウを開く" previous = "最近の項目" +recording_mode = "録画モード" quit = "Capを終了" record_area = "範囲を録画" record_display = "ディスプレイを録画" @@ -239,6 +257,698 @@ title = "プライバシー" [settings.general.privacy.telemetry] description = "Capは信頼性の向上と不具合の修正のために匿名のテレメトリを使用します。録画の内容、ウィンドウのタイトル、ファイルパス、個人情報を収集することはありません。" label = "匿名のテレメトリを共有" +[settings.prompt] +ok = "OK" + +[settings.sidebar] +account = "アカウント" +cancel_sign_in = "サインインをキャンセル" +check_for_updates = "アップデートを確認" +checking_for_updates = "アップデートを確認中…" +click_to_sign_in = "クリックしてサインイン" +downloading = "v{version}をダウンロード中…" +downloading_progress = "v{version}をダウンロード中… {percent}%" +installing = "v{version}をインストール中…" +previous_versions = "以前のバージョンを表示" +restart_to_update = "再起動してv{version}にアップデート" +sign_in = "サインイン" +sign_out = "サインアウト" +signed_in = "サインイン済み" + +[settings.recordings] +corrupted_body = "録画に失敗したため、このファイルはエディタで問題が起きる可能性があります。ファイルを復元できない場合は、サポートにお問い合わせください。" +corrupted_title = "録画が破損している可能性があります" +delete = "削除" +description = "録画を管理し、操作を行います。" +edit = "編集" +empty_library = "録画が見つかりません" +failed = "録画に失敗しました" +import = "読み込む" +in_progress = "録画中" +load_more = "さらに読み込む" +open_bundle = "録画バンドルを開く" +open_link = "リンクを開く" +retry_upload = "アップロードを再試行" +show_all = "すべて表示" +title = "録画" + +[settings.recordings.empty] +all = "録画はありません" +all_matching = "一致する録画はありません" +instant = "Instant録画はありません" +instant_matching = "一致するInstant録画はありません" +studio = "Studio録画はありません" +studio_matching = "一致するStudio録画はありません" + +[settings.screenshots] +copy = "クリップボードにコピー" +delete = "削除" +description = "スクリーンショットを管理し、操作を行います。" +empty = "スクリーンショットはありません" +empty_library = "スクリーンショットが見つかりません" +empty_matching = "一致するスクリーンショットはありません" +import = "画像を読み込む" +load_more = "さらに読み込む" +open_folder = "フォルダを開く" +save_as = "名前を付けて保存" +title = "スクリーンショット" + +[settings.quality] +save_failed = "録画設定を保存できませんでした。もう一度お試しください。" + +[settings.quality.studio] +balanced = "バランス" +balanced_description = "適度なファイルサイズで鮮明なディテール。ほとんどの録画に最適です。" +compatibility = "小さいファイル" +compatibility_description = "ディスク容量を節約します。細部がやわらかくなることがあり、特にカメラ映像で目立ちます。" +description = "編集用にコンピュータへ保存されます。書き出しサイズは後でエディタで選びます。" +recommended = "推奨" +title = "Studio録画" +ultra = "最大限のディテール" +ultra_description = "本格的な編集のためにディテールを最大限残します。ディスク容量を最も多く使います。" + +[settings.quality.instant] +description = "録画しながらアップロードされるため、停止した瞬間にリンクが使えます。最高画質が必要な場合はStudioモードを使ってください。画面が許す最高画質で録画し、最大4Kで書き出せます。" +free_plan = "無料プランでは720pです。Cap Proでは最大4Kで録画できます。" +max_resolution = "最大解像度" +res_1080p = "実用的なアップロードサイズで文字がくっきり。" +res_1440p = "大きな高解像度画面向けのより細かいディテール。" +res_4k = "最も細かいディテール。高速な回線向きです。" +res_720p = "アップロードが最も小さく、リンクが最も早く使えます。" +title = "Instant録画" + +[settings.quality.sharing] +auto_open = "共有リンクを自動で開く" +auto_open_description = "アップロードが完了したらブラウザでリンクを開きます。" +title = "共有" + +[settings.options.studio_quality] +balanced = "バランス" +compatibility = "互換性重視" +ultra = "最高" + +[settings.options.main_window_start] +close = "閉じる" +minimise = "最小化" + +[settings.options.post_studio] +do_nothing = "何もしない" +open_editor = "エディタを開く" +show_cap = "Capを表示" + +[settings.options.post_screenshot] +copy = "クリップボードにコピー" +copy_and_open_editor = "コピーしてエディタを開く" +do_nothing = "何もしない" +open_editor = "エディタを開く" + +[settings.options.post_deletion] +do_nothing = "何もしない" +reopen = "録画ウィンドウを再表示" + +[settings.options.preview_quality] +full = "フル" +half = "1/2" +quarter = "1/4" + +[settings.shortcuts] +blocked_while_recording = "録画が停止キーを使用している間はショートカットを変更できません。先に録画を停止してください。" +needs_modifier_macos = "入力の邪魔にならないよう、⌘、⌃、⌥のいずれかを含めてください。" +needs_modifier_other = "入力の邪魔にならないよう、Ctrl、Alt、Winのいずれかを含めてください。" +record_shortcut = "ショートカットを記録" +recording_description = "Capを開かずに録画を開始できます。" +recording_hint = "使いたいキーを押してください。Escでキャンセル、Deleteで削除します。" +recording_title = "録画" +saved = "保存しました。どのアプリでも{keys}を押すと{effect}。" +saved_moved = "保存しました。どのアプリでも{keys}を押すと{effect}。「{shortcut}」からは削除しました。" +screenshots_description = "ショートカットはどのアプリからでも使えます。クリックすると変更できます。" +screenshots_title = "スクリーンショット" +type_shortcut = "ショートカットを入力…" +unavailable = "このショートカットは別のアプリまたはシステムがすでに使用しています。クリックして別のものを選んでください。" +unsupported_key = "このキーはショートカットに使えません。文字、数字、ファンクションキーを試してください。" +while_recording_title = "録画中" + +[settings.shortcuts.after_screenshot] +hint = "ショートカットでもCapからでも、すべてのスクリーンショットに適用" +title = "スクリーンショットの後" + +[settings.shortcuts.after_studio] +hint = "停止したとき" +title = "Studio録画の後" + +[settings.shortcuts.after_instant] +hint = "共有リンクは常にコピーされます" +title = "Instant録画の後" + +[settings.shortcuts.outcome] +instant_copy_link = "リンクのコピーのみ" +instant_copy_link_hint = "何も開きません" +instant_open_link = "リンクを開く" +instant_open_link_hint = "ブラウザで" +saved_to_library = "ライブラリに保存" +screenshot_copy = "コピー" +screenshot_copy_and_edit = "コピーして編集" +screenshot_copy_and_edit_hint = "両方を同時に" +screenshot_copy_hint = "どこにでも貼り付け" +screenshot_do_nothing = "何もしない" +screenshot_open_editor = "エディタを開く" +screenshot_open_editor_hint = "先に注釈を付ける" +studio_do_nothing = "何もしない" +studio_open_editor = "エディタを開く" +studio_open_editor_hint = "すぐに編集" +studio_show_cap = "Capを表示" +studio_show_cap_hint = "レコーダーに戻る" + +[settings.shortcuts.label] +cycle_mode = "録画モードを切り替え" +fallback = "ショートカット" +open_picker = "録画ピッカーを開く" +pause = "一時停止または再開" +record_area = "範囲を録画" +record_display = "ディスプレイを録画" +record_window = "ウィンドウを録画" +restart = "録画をやり直す" +screenshot_area = "範囲のスクリーンショット" +screenshot_display = "ディスプレイのスクリーンショット" +screenshot_window = "ウィンドウのスクリーンショット" +start_instant = "Instant録画を開始" +start_studio = "Studio録画を開始" +stop = "録画を停止" + +[settings.shortcuts.effect] +cycle_mode = "Studio、Instant、スクリーンショットの各モードを切り替えます" +instant_copy = "選択した画面またはウィンドウをInstantモードで録画し、停止すると共有リンクをコピーします" +instant_copy_and_open = "選択した画面またはウィンドウをInstantモードで録画し、停止すると共有リンクをコピーして開きます" +open_picker = "Capを開いて録画する対象を選びます" +pause = "現在の録画を一時停止または再開します" +picker_area_instant = "範囲を選択してInstantモードで録画します" +picker_area_screenshot = "範囲を選択してスクリーンショットを撮ります" +picker_area_studio = "範囲を選択してStudioモードで録画します" +picker_display_instant = "ディスプレイを選んでInstantモードで録画します" +picker_display_screenshot = "ディスプレイを選んでスクリーンショットを撮ります" +picker_display_studio = "ディスプレイを選んでStudioモードで録画します" +picker_window_instant = "ウィンドウを選んでInstantモードで録画します" +picker_window_screenshot = "ウィンドウを選んでスクリーンショットを撮ります" +picker_window_studio = "ウィンドウを選んでStudioモードで録画します" +restart = "現在のテイクを破棄してやり直します" +screenshot_area_copy = "範囲を選択してクリップボードにコピーします" +screenshot_area_copy_and_open_editor = "範囲を選択してコピーし、エディタを開きます" +screenshot_area_open_editor = "範囲を選択してエディタで開きます" +screenshot_area_save = "範囲を選択し、何も開かずにライブラリに保存します" +screenshot_display_copy = "ポインタのあるディスプレイをキャプチャしてクリップボードにコピーします" +screenshot_display_copy_and_open_editor = "ポインタのあるディスプレイをキャプチャしてコピーし、エディタを開きます" +screenshot_display_open_editor = "ポインタのあるディスプレイをキャプチャしてエディタで開きます" +screenshot_display_save = "ポインタのあるディスプレイをキャプチャし、何も開かずにライブラリに保存します" +screenshot_window_copy = "ポインタの下のウィンドウをキャプチャしてクリップボードにコピーします" +screenshot_window_copy_and_open_editor = "ポインタの下のウィンドウをキャプチャしてコピーし、エディタを開きます" +screenshot_window_open_editor = "ポインタの下のウィンドウをキャプチャしてエディタで開きます" +screenshot_window_save = "ポインタの下のウィンドウをキャプチャし、何も開かずにライブラリに保存します" +stop = "現在の録画を停止します" +studio = "選択した画面またはウィンドウをStudioモードで録画します" +studio_open_editor = "選択した画面またはウィンドウをStudioモードで録画し、停止するとエディタを開きます" + +[settings.shortcuts.sentence] +cycle_mode = "Studio、Instant、スクリーンショットの各モードを切り替えます。" +instant_copy = "選択した画面またはウィンドウをInstantモードで録画し、停止すると共有リンクをコピーします。" +instant_copy_and_open = "選択した画面またはウィンドウをInstantモードで録画し、停止すると共有リンクをコピーして開きます。" +open_picker = "Capを開いて録画する対象を選びます。" +pause = "現在の録画を一時停止または再開します。" +picker_area_instant = "範囲を選択してInstantモードで録画します。" +picker_area_screenshot = "範囲を選択してスクリーンショットを撮ります。" +picker_area_studio = "範囲を選択してStudioモードで録画します。" +picker_display_instant = "ディスプレイを選んでInstantモードで録画します。" +picker_display_screenshot = "ディスプレイを選んでスクリーンショットを撮ります。" +picker_display_studio = "ディスプレイを選んでStudioモードで録画します。" +picker_window_instant = "ウィンドウを選んでInstantモードで録画します。" +picker_window_screenshot = "ウィンドウを選んでスクリーンショットを撮ります。" +picker_window_studio = "ウィンドウを選んでStudioモードで録画します。" +restart = "現在のテイクを破棄してやり直します。" +screenshot_area_copy = "範囲を選択してクリップボードにコピーします。" +screenshot_area_copy_and_open_editor = "範囲を選択してコピーし、エディタを開きます。" +screenshot_area_open_editor = "範囲を選択してエディタで開きます。" +screenshot_area_save = "範囲を選択し、何も開かずにライブラリに保存します。" +screenshot_display_copy = "ポインタのあるディスプレイをキャプチャしてクリップボードにコピーします。" +screenshot_display_copy_and_open_editor = "ポインタのあるディスプレイをキャプチャしてコピーし、エディタを開きます。" +screenshot_display_open_editor = "ポインタのあるディスプレイをキャプチャしてエディタで開きます。" +screenshot_display_save = "ポインタのあるディスプレイをキャプチャし、何も開かずにライブラリに保存します。" +screenshot_window_copy = "ポインタの下のウィンドウをキャプチャしてクリップボードにコピーします。" +screenshot_window_copy_and_open_editor = "ポインタの下のウィンドウをキャプチャしてコピーし、エディタを開きます。" +screenshot_window_open_editor = "ポインタの下のウィンドウをキャプチャしてエディタで開きます。" +screenshot_window_save = "ポインタの下のウィンドウをキャプチャし、何も開かずにライブラリに保存します。" +stop = "現在の録画を停止します。" +studio = "選択した画面またはウィンドウをStudioモードで録画します。" +studio_open_editor = "選択した画面またはウィンドウをStudioモードで録画し、停止するとエディタを開きます。" + +[settings.cli] +command = "コマンド" +copy = "コピー" +description = "ターミナル、エージェント、スクリプト、ローカル自動化で使えるCapコマンドをインストールします。" +install = "CLIをインストール" +installed = "インストール済み" +installing = "インストール中..." +installs_command = "デスクトップアプリは、同梱のCLIを指すローカルの\u00A0{command}\u00A0コマンドをインストールします。" +load_failed = "CLIの状態を読み込めませんでした: {error}" +not_installed = "未インストール" +path_configured = "capをPATHに追加しました。使うにはターミナルを再起動するか、今すぐ次を実行してください:" +path_missing = "新しいターミナルでcapを使うには、{path}をPATHに追加してください。" +remove = "削除" +removing = "削除中..." +repair = "修復" +repairing = "修復中..." +retry = "再試行" +target = "参照先" +title = "コマンドライン" + +[settings.transcription] +active_title = "有効なヒント" +applied_note = "これらのヒントは、エディタで字幕を生成するときに適用されます。" +clear = "クリア" +count = { other = "{count}件" } +description = "字幕の生成時に考慮してほしい名前、表記、ドメイン、大文字小文字の使い分けを追加します。" +placeholder = "用語を追加" +saved = "保存しました" +saving = "保存中..." +terms_description = "誤字や書式の誤りを減らすため、用語は1つずつ追加してください。" +terms_title = "登録した用語" +title = "文字起こし" + +[settings.experimental] +developer = "開発者" +developer_description = "開発ビルドでのみ表示されます。" +muxer = "別プロセスのマルチプレクサ" +muxer_description = "フラグメント化MP4のマルチプレクサを独立したサブプロセスで実行し、マルチプレクサがクラッシュしても録画が失われないようにします。同梱のcap-muxerバイナリが必要です。" +native_app = "ネイティブアプリを使う" +native_app_description = "オフにするとこのアプリを閉じ、開発用ハーネスで従来のTauriアプリを開き直します。録画と設定は共有されます。" +reliability = "信頼性" + +[settings.experimental.switch] +busy = "従来のアプリに切り替える前に、録画、書き出し、アップロード、読み込み、文字起こしの作業を完了してください。" +check_failed = "従来のCapアプリが開いたかどうか確認できませんでした: {error}" +data_stays = "録画と設定はそのまま残ります。" +failed = "従来のCapアプリを開けませんでした。" +not_found = "切り替え先の従来のCapアプリが見つかりませんでした。" +open_failed = "従来のCapアプリを開けませんでした: {error}" +opening = "従来のCapアプリを開いています" +opening_detail = "従来のCapアプリの準備ができると、このウィンドウは閉じます。" +save_failed = "アプリの設定を保存できませんでした。Capは開いたままです。" +switching = "従来のCapアプリに戻しています。" +timed_out = "従来のアプリが開きません。開発用ターミナルでビルドエラーを確認してから、もう一度お試しください。" + +[settings.feedback] +app_version = "アプリのバージョン" +capture_support = "キャプチャ対応" +capture_supported = "画面キャプチャ: 対応" +capture_unsupported = "画面キャプチャ: 非対応" +community_description = "質問やアイデアの共有、ちょっとした雑談に。CapのDiscordコミュニティに参加しましょう。" +community_title = "コミュニティに参加" +debug_description = "ログをアップロードして、Capの問題の診断にご協力ください。個人情報は含まれません。" +debug_title = "デバッグ情報" +description = "フィードバックやバグ報告を送って、Capの改善にご協力ください。すぐに対応します。" +join_discord = "Discordに参加" +operating_system = "OS" +placeholder = "Capについてのご意見をお聞かせください..." +submit = "フィードバックを送信" +submit_failed = "フィードバックを送信できませんでした" +submitting = "送信中..." +system_loading = "システム情報を読み込み中..." +system_title = "システム情報" +thanks = "フィードバックをありがとうございます。" +title = "フィードバック" +upload_interrupted = "アップロードが中断されました" +upload_logs = "ログをアップロード" +upload_sent = "送信しました。ありがとうございます。問題の特定に役立てます。" +uploading = "アップロード中..." + +[settings.feedback.diagnostic] +confirm_body = "Capは画面全体に点滅パターンを表示し、処理ごとに約{seconds}秒間大きなビープ音を鳴らします。ヘッドフォンを外し、音が聞こえる音量にして、終わるまでコンピュータに触れないでください。" +confirm_title = "診断を実行しますか?" +description = "点滅とビープ音の短いテストパターンを録画し、映像と音声のずれを測定して、ディスプレイ、カメラ、マイク、ディスク容量、録画設定の状態を記録します。送信を選ぶまで、何も送信されません。" +mic_description = "マイクも録音し、ビープ音を正しいタイミングで拾えているか確認します。マイクに実際に聞こえるスピーカーが必要です。" +mic_label = "マイクをテスト" +mode_description = "テストする処理を選びます。両方を選ぶとStudioを実行してからInstantを実行します。" +mode_label = "録画モード" +no_cli = "このビルドの横にCapコマンドラインツールが見つからないため、レポートには環境情報のみが含まれます。" +no_cli_sync = "このビルドの横にCapコマンドラインツールが見つからないため、同期テストを実行できませんでした。" +run = "診断を実行" +run_again = "もう一度実行" +send = "Capに送信" +sending = "送信中..." +show_file = "ファイルを表示" +stop_recording_first = "診断を実行する前に、現在の録画を停止してください。" +title = "診断レポート" +try_again = "再試行" +warning_body = "全画面のウィンドウが白黒に点滅し、スピーカーから1kHzのビープ音が鳴ります。Capはそれを録画し、映像と音声のずれを測定します。実行中はコンピュータに触れないでください。両方のモードで数分かかります。" +warning_title = "テスト中は画面全体が使われ、音が鳴ります。" + +[settings.feedback.diagnostic.verdict] +environment_only = "環境情報のみ" +fail = "不合格" +inconclusive = "判定不能" +pass = "合格" +warn = "警告" + +[settings.sign_in_required] +body = "続けるにはサインインしてください。または、hello@cap.soにメールするか、cap.link/discordからDiscordに参加してください" +title = "サインインが必要です" + +[settings.changelog] +fetch_failed = "変更履歴を取得できませんでした" +loading = "変更履歴を読み込み中..." +new = "NEW" +version = "バージョン{version} - {date}" + +[settings.license] +activate = "ライセンスを有効化" +activating = "有効化中..." +active_title = "デスクトップライセンス" +checking = "確認中…" +deactivate = "ライセンスを無効化" +description = "Capのプランとデスクトップライセンスをまとめて確認できます。" +desktop_description = "デスクトップライセンスで商用の録画と編集ができます。Cap Proではさらにクラウド共有とコラボレーション機能が使えます。" +desktop_name = "デスクトップライセンス" +expires = "有効期限" +explore_description = "現在の料金と含まれる内容をウェブサイトで比較できます。" +explore_desktop = "デスクトップライセンス · デスクトップのレコーダーとエディタを商用利用できます。" +explore_pro = "Cap Pro · デスクトップライセンスに加え、クラウド共有、AI機能、コラボレーション。" +explore_title = "プランを見る" +free_description = "個人利用ならローカルで録画と編集ができます。商用利用やより多くのクラウド機能には有料プランを選んでください。" +have_key = "ライセンスキーをお持ちですか?" +key = "ライセンスキー" +no_instance = "インスタンスIDが見つかりません" +opens_pricing = "ブラウザでcap.so/pricingを開きます。" +placeholder = "ライセンスキー" +plan_title = "ご利用中のプラン" +pro_description = "お使いのアカウントには、クラウド共有、Pro機能、商用利用できるデスクトップライセンスが含まれます。" +refresh = "プランを更新" +refresh_failed = "プランを更新できませんでした。もう一度お試しください。" +save_failed = "ライセンスを保存できませんでした" +sign_in_hint = "すでにCap Proをお持ちですか?サイドバーからアカウントでサインインしてください。" +title = "プランとライセンス" +view_pricing = "プランと料金を見る ↗" + +[settings.integrations] +configure = "設定" +description = "連携を設定して、Capの機能を拡張し、外部サービスと接続します。" +google_drive_description = "Google Driveを接続すると、新しい共有リンクの動画がアップロードされます。Capは新しい動画をDrive内の非公開のCapフォルダに保存し、通常のアクセス確認の後にCap経由で配信し続けます。" +managed = "組織が管理しています" +managed_by = "組織が管理しています: {organization}" +request_timed_out = "リクエストがタイムアウトしました" +s3_description = "独自のS3バケットを接続して、データの保存を完全に管理できます。新しい共有リンクのアップロードはすべて設定したS3バケットに自動でアップロードされ、コンテンツの所有権と管理を完全に保てます。データ主権や独自の保存ポリシーが必要な組織に最適です。" +s3_name = "S3設定" +title = "連携" +upgrade = "Proにアップグレード" + +[settings.integrations.s3] +access_key = "アクセスキーID" +bucket = "バケット名" +configuration = "設定" +delete_failed = "S3設定を削除できませんでした" +deleted = "S3設定を削除しました" +endpoint = "エンドポイント" +fetch_failed = "S3設定を取得できませんでした" +guide = "ストレージ設定ガイド" +guide_intro = "ストレージバケットの設定とCapへの接続は10分以内で完了します。始めるには\u00A0{guide}\u00A0をご覧ください。" +other_provider = "その他のS3互換" +provider = "ストレージプロバイダ" +region = "リージョン" +remove = "設定を削除" +removing = "削除中..." +save_failed = "S3設定を保存できませんでした" +saved = "S3設定を保存しました" +saving = "保存中..." +secret_key = "シークレットアクセスキー" +test = "接続をテスト" +test_failed = "S3の接続テストに失敗しました。設定とネットワーク接続を確認してください。" +test_ok = "S3設定のテストに成功しました。接続は正常です。" +test_timeout = "接続テストが5秒でタイムアウトしました。エンドポイントのURLとネットワーク接続を確認してください。" +testing = "テスト中..." + +[settings.integrations.gdrive] +active = "使用中" +active_for_uploads = "新しいアップロードに使用中" +cached = "{time}時点のキャッシュ" +connect = "Google Driveを接続" +connect_failed = "Google Driveの接続を開始できませんでした" +connected_inactive = "接続済み(未使用)" +connection_description = "Google Driveは新しいアップロードをDrive内の非公開のCapフォルダに保存します。既存のCapホストの動画とS3の動画は、現在の保存先を使い続けます。" +connection_title = "接続" +disconnect = "接続を解除" +disconnect_failed = "Google Driveの接続を解除できませんでした" +disconnected = "Google Driveの接続を解除しました" +drive_files = "Driveのファイル" +finish_in_browser = "ブラウザでGoogle Driveの接続を完了してから、ここに戻って更新してください。" +not_connected = "未接続" +opening = "開いています..." +refresh = "更新" +refreshing = "更新中..." +remaining = "残り" +set_active_failed = "使用するストレージプロバイダを更新できませんでした" +storage = "ストレージ" +test = "テスト" +test_failed = "Google Driveの接続テストに失敗しました" +testing = "テスト中..." +trash = "ゴミ箱" +updated = "{time}に更新" +use_drive = "Google Driveを使う" +use_s3 = "S3を使う" +used = "{usage}使用" +used_of = "{limit}中{usage}使用" +waiting = "待機中..." +working = "Google Driveの接続は正常です" +working_for = "{email}のGoogle Drive接続は正常です" + +[settings.automations] +action_no_effect = "このアクションは選択したトリガーでは効果がありません。" +add_action = "アクションを追加" +add_condition = "条件を追加" +auto_name_empty = "{trigger}のオートメーション" +browse = "参照" +check_compatibility = "互換性を確認" +compatibility_checked = "互換性を確認しました: このオートメーションはまだCapでは実行されません。保存したルールはCap CLIで引き続き使えます。アクションは実行されていません。" +condition_never_matches = "この条件は選択したトリガーでは一致しません。" +copy_link = "リンクをクリップボードにコピー" +dangerous = "Cap CLIでは、このオートメーションはあなたの権限でコマンドを実行したりネットワークリクエストを送信したりします。信頼できる値だけを使ってください。" +delete = "削除" +description = "ルールはCap CLIと共有されます。Capはこれらのルールを保存しますが、オートメーションはまだ実行しません。" +empty_description = "下のテンプレートを選べばワンクリックで始められます。一から自分で作ることもできます。" +empty_title = "オートメーションはまだありません" +enabled = "有効" +match_all = "すべて一致" +match_any = "いずれか一致" +name = "名前" +new = "新しいオートメーション" +no_presets = "プリセットがまだありません。先にエディタで作成してください。" +only_if = "実行する条件" +open_in_browser = "ブラウザで開く" +retry_save = "保存を再試行" +run_through_shell = "シェル経由で実行" +save_failed = "オートメーションを保存できませんでした。変更は保存されていません。" +start_from_scratch = "一から作成" +summary_empty = "{trigger} → アクションはまだありません" +templates_description = "ワンクリックで既製のオートメーションを追加できます。後から自由に調整できます。" +templates_title = "テンプレート" +then = "実行する内容" +title = "オートメーション" +when = "このとき" + +[settings.automations.status] +disabled_saved = "無効 · 保存済み" +disabled_unsaved = "無効 · 未保存" +enabled_saved = "有効 · 保存済み" +enabled_unsaved = "有効 · 未保存" + +[settings.automations.field] +arguments = "引数(スペース区切り)" +body = "本文" +body_template = "本文テンプレート(任意)" +compression = "圧縮" +destination = "保存先フォルダ(任意、空欄 = プロジェクトフォルダ)" +filename = "ファイル名テンプレート(任意)" +folder = "フォルダ" +format = "形式" +frame_rate = "フレームレート" +method = "メソッド" +organization = "組織ID(任意)" +preset = "プリセット" +program = "プログラム" +resolution = "解像度" +source = "ソース" +title = "タイトル" +url = "URL" + +[settings.automations.placeholder] +organization_id = "組織ID" +project_folder = "プロジェクトフォルダ" +window_title = "例: Slack" + +[settings.automations.clipboard] +original = "元のキャプチャ" +rendered = "編集済み / レンダリング済み" + +[settings.automations.target] +area = "範囲" +display = "ディスプレイ" +window = "ウィンドウ" + +[settings.automations.compression] +maximum = "最大" +potato = "最小" +social = "ソーシャル" +web = "Web" + +[settings.automations.trigger.screenshot_taken] +label = "スクリーンショットを撮ったとき" +noun = "スクリーンショット" +phrase = "スクリーンショット撮影" +runs = "スクリーンショットを撮るたびに実行されます。" + +[settings.automations.trigger.studio_finished] +label = "Studio録画が終了したとき" +noun = "Studio録画" +phrase = "Studio録画の終了" +runs = "Studio録画が終了するたびに実行されます。" + +[settings.automations.trigger.instant_finished] +label = "Instant録画が終了したとき" +noun = "Instant録画" +phrase = "Instant録画の終了" +runs = "Instant録画が終了するたびに実行されます。" + +[settings.automations.trigger.recording_started] +label = "録画を開始したとき" +noun = "録画開始" +phrase = "録画の開始" +runs = "録画を開始するたびに実行されます。" + +[settings.automations.trigger.upload_completed] +label = "アップロードが完了したとき" +noun = "アップロード" +phrase = "アップロードの完了" +runs = "アップロードが完了するたびに実行されます。" + +[settings.automations.trigger.video_imported] +label = "動画を読み込んだとき" +noun = "読み込み" +phrase = "動画の読み込み" +runs = "動画を読み込むたびに実行されます。" + +[settings.automations.trigger.recording_deleted] +label = "録画を削除したとき" +noun = "削除" +phrase = "録画の削除" +runs = "録画を削除するたびに実行されます。" + +[settings.automations.action.copy_to_clipboard] +label = "クリップボードにコピー" +noun = "クリップボード" +short = "クリップボードにコピー" + +[settings.automations.action.save_to_location] +label = "指定の場所に保存" +noun = "フォルダ" +short = "フォルダに保存" + +[settings.automations.action.export] +label = "プロファイルで書き出し" +noun = "書き出し" +short = "書き出し" + +[settings.automations.action.upload] +label = "アップロードしてリンクをコピー" +noun = "アップロード" +short = "アップロードしてリンクをコピー" + +[settings.automations.action.reveal] +label = "ファイルマネージャで表示" +noun = "表示" +short = "ファイルマネージャで表示" + +[settings.automations.action.open_file] +label = "ファイルを開く" +noun = "開く" +short = "ファイルを開く" + +[settings.automations.action.ocr] +label = "文字認識(OCR)してクリップボードへ" +noun = "テキスト" +short = "文字をコピー(OCR)" + +[settings.automations.action.notify] +label = "通知を表示" +noun = "通知" +short = "通知" + +[settings.automations.action.open_editor] +label = "エディタを開く" +noun = "エディタ" +short = "エディタを開く" + +[settings.automations.action.skip_editor] +label = "エディタを省略(ヘッドレス)" +noun = "エディタ省略" +short = "エディタを省略" + +[settings.automations.action.apply_preset] +label = "エディタのプリセットを適用" +noun = "プリセット" +short = "プリセットを適用" + +[settings.automations.action.run_command] +label = "コマンドを実行" +noun = "コマンド" +short = "コマンドを実行" + +[settings.automations.action.webhook] +label = "Webhookを送信" +noun = "Webhook" +short = "Webhookを送信" + +[settings.automations.action.delete_local_files] +label = "ローカルファイルを削除" +noun = "削除" +short = "ローカルファイルを削除" + +[settings.automations.condition] +capture_target = "キャプチャ対象が次と一致" +duration_at_least = "長さが次以上(秒)" +duration_at_most = "長さが次以下(秒)" +organization = "組織が次と一致" +recording_mode = "録画モードが次と一致" +window_title = "ウィンドウタイトルに次を含む" + +[settings.automations.template.copy_screenshot] +description = "スクリーンショットを撮ったら、すぐに貼り付けられます。" +name = "新しいスクリーンショットを自動でクリップボードにコピー" + +[settings.automations.template.ocr_screenshot] +description = "Capがスクリーンショット内の文字を読み取り、コピーします。" +name = "スクリーンショットから文字を取り出す" + +[settings.automations.template.save_screenshot] +description = "新しいスクリーンショットを、選んだフォルダに直接送ります。" +name = "スクリーンショットをフォルダにまとめる" + +[settings.automations.template.reveal_screenshot] +description = "スクリーンショットを撮った瞬間にFinderで表示します。" +name = "新しいスクリーンショットをすぐに表示" + +[settings.automations.template.export_studio] +description = "Studio録画が終わった瞬間にMP4を書き出します。" +name = "録画終了時に自動で書き出す" + +[settings.automations.template.upload_share] +description = "録画を終えると、リンクがクリップボードに入っています。" +name = "アップロードして共有リンクを取得" + +[settings.automations.template.notify_upload] +body = "録画を共有できるようになりました。" +description = "録画を共有できるようになったら、デスクトップでそっとお知らせします。" +name = "アップロードの準備ができたら通知" + +[settings.automations.template.webhook_share] +description = "共有リンクをSlack、Discord、または独自のWebhookに送ります。" +name = "共有したらSlackに知らせる" + [main] delete_recording_confirm = "この録画を削除してもよろしいですか?" @@ -586,3 +1296,397 @@ click_or_press = "ここをクリックするか、次のキーを押す:" get_started = "はじめる" space_key = "スペース" tagline = "誰もが使えるオープンソースの画面共有アプリ。" + +[menu.update] +badge_progress = "v{version} · {percent}%" +badge_version = "v{version}" +check = "アップデートを確認" +download = "アップデートをダウンロード…" +checking = "アップデートを確認中…" +downloading = "アップデートをダウンロード中…" +restart = "アップデートのために再起動…" + +[settings.general.updates.download_policy] +auto = "自動" +description = "Cap がアップデートを自動でダウンロードするか選択します。" +label = "アップデートのダウンロード" +manual = "確認してから" +never = "しない" + +[auth] +sign_in_failed = "サインインに失敗しました" + +[diagnostics] +already_running = "診断はすでに実行中です。" +mode_both = "両方" + +[diagnostics.error] +create_folder = "診断フォルダを作成できませんでした:{error}" +exited = "セルフテストが終了しました:{status}" +no_output = "セルフテストから出力が得られませんでした" +serialize = "レポートをシリアライズできませんでした:{error}" +start_failed = "{program} を起動できませんでした:{error}" +upload_logs = "ログをアップロードできませんでした:{error}" +upload_status = "アップロードに失敗しました。ステータス:{status}" +wait_failed = "セルフテストの終了を待てませんでした:{error}" +write_failed = "{path} に書き込めませんでした:{error}" + +[diagnostics.stage] +analyzing = "録画を分析中..." +analyzing_instant = "Instant録画を分析中..." +analyzing_studio = "Studio録画を分析中..." +cancelling = "キャンセル中..." +collecting = "システム情報を収集中..." +done = "同期テストを仕上げ中..." +exporting = "録画を書き出し中..." +exporting_instant = "Instant録画を書き出し中..." +exporting_studio = "Studio録画を書き出し中..." +finalizing = "録画を仕上げ中..." +finalizing_instant = "Instant録画を仕上げ中..." +finalizing_studio = "Studio録画を仕上げ中..." +pattern = "テストパターンを再生中..." +pattern_instant = "テストパターンを再生中(Instant)..." +pattern_studio = "テストパターンを再生中(Studio)..." +preparing = "テストを準備中..." +preparing_instant = "Instantテストを準備中..." +preparing_studio = "Studioテストを準備中..." +recording = "テストを録画中..." +recording_instant = "Instantテストを録画中..." +recording_studio = "Studioテストを録画中..." +starting = "診断を開始中..." + +[dialog] +no_microphone_body = "この録画にはあなたの声は入りません。" +open_recording_failed = "この録画を開けませんでした:{error}" + +[dialog.capture] +approve_shortcut = "録画用のショートカットを承認するか、Cap の停止トレイアイコンが表示されたらそれをクリックしてください。録画はまだ始まっていません。" +area_unavailable = "選択した範囲が変更されたか、使用できなくなりました。もう一度選択してください。" +camera_needs_x11 = "Linux で Instant のカメラオーバーレイを使うには X11 デスクトップが必要です" +camera_notice = "Cap はカメラのプレビューと録画コントロールを非表示にします。選択したカメラは、個別に編集できるトラックとして録画され続けます。" +camera_notice_instant = "Cap はプレビューとコントロールを非表示にします。選択したカメラは、選んだ形とエフェクトでプレビューの位置に表示されます。プレビュー全体をキャプチャ範囲内に収めてください。Cap を開くと、コントロールを表示する前にこの Instant 録画が停止します。" +camera_outside_area = "Instant 録画を始める前に、カメラのプレビュー全体をキャプチャ範囲内に移動してください" +camera_preview_closed = "Instant 録画の前に、選択したカメラのプレビューを開いてください" +cleanup_incomplete = "録画のクリーンアップが完了していません:{error}。新しい録画は開始できません。" +devices_changed = "選択したデバイスが変更されました。新しい録画の入力を確認するため、もう一度開始してください。" +devices_changed_before_start = "録画の開始前に選択したデバイスが変更されました。入力を確認するため、もう一度開始してください。" +shortcut_conflict = "Ctrl+Shift+F9 は Cap の別の操作に割り当てられています。録画の前にそのショートカットを変更してください。" +shortcut_description = "この Cap 録画を開始または停止" +shortcut_hint = "{shortcut} を押して離すと開始します。同じショートカットで停止できます。この一時的なショートカットは設定を変更しません。" +tray_stop_hint = "Cap の停止トレイアイコンをクリックすると開始します。同じアイコンをもう一度クリックすると停止します。このコントロールを使うまで録画は始まりません。" +window_unavailable = "選択したウィンドウは使用できなくなりました。もう一度選択してください。" + +[dialog.clipboard] +copy_file = "ファイルをクリップボードにコピーできませんでした:{error}" +copy_file_failed = "ファイルをクリップボードにコピーできませんでした" +copy_image = "画像をクリップボードにコピーできませんでした:{error}" +copy_image_failed = "画像をクリップボードにコピーできませんでした" +not_a_file = "クリップボードにコピーできるのはファイルだけです" +unavailable = "クリップボードを使用できません" +unavailable_error = "クリップボードを使用できません:{error}" + +[dialog.delete_recording] +already_deleting = "この録画はすでに削除中です。" +close_failed = "エディタを閉じられませんでした:{error}" +editor_changed = "削除の確認中にエディタが変更されました。録画は削除されていません。" +editor_closed = "エディタはすでに閉じられています。録画は削除されていません。" +failed = "録画を削除できませんでした:{error}" +failed_title = "録画を削除できませんでした" +finishing_body = "Cap は録画のクリーンアップが終わるのを待ってから、この削除を完了します。" +finishing_title = "削除はまだ完了していません" +in_use = "この録画はまだ使用中です。アップロードを完了またはキャンセルしてから、もう一度お試しください:{error}" +outside_library = "この録画は録画ライブラリの外にあります。ファイルは変更されていません。" +recording_active = "このプロジェクトを削除する前に、録画を終了またはキャンセルしてください。" +retained_title = "録画は残されています" + +[dialog.file] +audio_filter = "オーディオ" +busy = "ファイルダイアログがすでに開いています。完了するかキャンセルしてから、もう一度お試しください。" +export_filter = "書き出し" +failed = "ファイルダイアログを完了できませんでした:{error}" +images_filter = "画像" +unavailable_title = "ファイルダイアログを使用できません" +unconfirmed = "Cap はファイルダイアログが閉じたことを確認できませんでした。残っているファイルダイアログを閉じ、Cap を再起動してからもう一度お試しください。" + +[dialog.mode_select] +instant = "リンクですぐに共有。録画しながらアップロードします。" +screenshot = "スクリーンショットを撮ってすぐに注釈を付けられます。" +studio = "あとで編集するための最高画質のローカル録画です。" +subtitle = "画面のキャプチャ方法を選んでください" +title = "録画モードを選択" + +[dialog.storage] +stopped_kept = "ストレージが不足しているため録画を停止しました。録画ファイルは残されています。空き容量を確保してから、下で録画を復元してください。" +stopped_saved = "ストレージが不足しているため録画を停止しました。録画は保存されています。" +stopping = "ストレージが不足しています。録画を停止して保存しています…" + +[dialog.teleprompter] +cue_markers = "キューマーカー" +hidden = "このウィンドウは Cap の録画に映りません" +mirror = "テキストを反転" +paused = "録画とともにスクロールを一時停止中" +placeholder = "台本を貼り付けるか入力してください…" + +[import] +error_title = "読み込みエラー" +image_failed = "画像を読み込めませんでした:{error}" +unsupported = "サポートされていないメディアファイルです。" +video_failed = "動画を読み込めませんでした:{error}" + +[import.filter] +image = "画像ファイル" +media = "メディアファイル" +video = "動画ファイル" + +[import.progress] +analyzing = "動画ファイルを分析中..." +complete = "読み込みが完了しました!" +converting = "動画を変換中... {percent}%" +image = "画像を読み込み中..." +metadata = "プロジェクトのメタデータを作成中..." +starting = "変換を開始しています..." + +[overlay] +choose_mode = "録画モードを選択" +countdown_tooltip = "録画のカウントダウン" +display_fallback = "ディスプレイ {id}" + +[overlay.area] +draw = "範囲を描く" +free_ratio = "自由" +hint = "クリックしてドラッグし、範囲を選択" +minimum_size = "最小サイズは {width} x {height} です" +too_small = "{width} x {height} は小さすぎます" + +[overlay.camera_only] +description = "カメラとマイクだけで録画します" +select_camera = "カメラを選択してください" + +[overlay.device] +camera_allow = "カメラへのアクセスを許可…" +camera_turn_on = "設定でカメラへのアクセスをオンにする…" +microphone_allow = "マイクへのアクセスを許可…" +microphone_turn_on = "設定でマイクへのアクセスをオンにする…" + +[overlay.start] +instant = "Instantモード:共有リンクを録画" +instant_sign_in = "Instantモード:サインインしてすぐに録画" +sign_in = "サインインして使用" +start_recording = "録画を開始" +studio = "Studioモード:ローカルで録画して編集" +take_screenshot = "スクリーンショットを撮る" + +[permission.guide] +accessibility_body = "上の「アクセシビリティ」のリストにドロップし、スイッチがオンになっていることを確認してください。" +screen_body = "上の「画面収録とシステムオーディオ録音」のリストにドロップし、スイッチがオンになっていることを確認してください。" +title = "Cap をリストにドラッグ" + +[permission.name] +accessibility = "アクセシビリティ" +camera = "カメラ" +microphone = "マイク" +screen_recording = "画面収録" + +[permission.onboarding] +open_settings_again = "システム設定をもう一度開く" +skip = "今はスキップ" +switch_on_title = "システム設定で Cap をオンにする" + +[permission.onboarding.accessibility] +allow = "アクセシビリティを許可" +body = "アクセシビリティを許可すると、Cap がこの Mac 上だけでクリックとカーソルを把握し、自動ズームがどこを映すべきかわかるようになります。" +done_body = "自動ズームがクリックを追えるようになりました。" +done_title = "アクセシビリティはオンです" +title = "Cap がカーソルを追えるようにする" + +[permission.onboarding.all_set] +body = "Cap に必要なものはすべてそろいました。次は、できることをざっと見てみましょう。" +regained = "ありがとうございます。Cap で再び録画できます。" +title = "準備完了です" + +[permission.onboarding.camera] +allow = "カメラを許可" +body = "録画の隅に自分を映せます。必要なければスキップしてください。" +done_body = "どの録画にもカメラを追加できます。" +done_title = "カメラはオンです" +title = "Cap にあなたを映させる" + +[permission.onboarding.checklist] +lede = "最初に macOS がいくつかの許可を求めます。1分ほどで終わります。" +regained_lede = "システム設定でオフにされた項目があります。録画を続けるにはオンに戻してください。" +regained_title = "Cap に権限を戻してください" +title = "Cap の準備をしましょう" + +[permission.onboarding.hint] +drag_into_list = "システム設定の下にあるパネルから Cap をリストにドラッグしてください。すでにリストにある場合は、オンにするだけです。" +find_in_list = "リストで Cap を見つけてオンにしてください。macOS から先に確認された場合は、「システム設定を開く」を選んでください。" +no_prompt = "確認ダイアログが表示されませんか?システム設定で Cap をオンにしてください。" +open_settings = "システム設定を開き、リストで Cap を見つけてオンにしてください。" +turned_off = "以前オフにされているため、システム設定で Cap をオンにしてください。今はスキップすることもできます。" + +[permission.onboarding.microphone] +allow = "マイクを許可" +body = "録画に声を入れられます。スキップして後でオンにすることもできます。" +done_body = "録画に声を入れられるようになりました。" +done_title = "マイクはオンです" +title = "Cap に声を届ける" + +[permission.onboarding.relaunch] +body = "Cap を再起動すると macOS が画面収録をオンにします。この画面から続けられます。" +button = "再起動" +hint = "オンにしたのにまだ待機中ですか?Cap を再起動すると反映されます。" +later = "macOS は Cap の再起動後にこれを反映します。ここでの設定が終わってから再起動できます。" +title = "再起動すれば準備完了です" + +[permission.onboarding.screen] +allow = "画面収録を許可" +body = "画面収録を許可すると、Cap がディスプレイ、ウィンドウ、システムオーディオをキャプチャできます。" +done_body = "Cap で画面をキャプチャできるようになりました。" +done_title = "画面収録はオンです" +title = "Cap に画面を見せる" + +[permission.onboarding.status] +allowed = "許可済み" +off = "システム設定でオフ" +optional = "任意" +required = "必須" +skipped = "今はスキップ" + +[permission.onboarding.waiting] +prompt = "macOS の確認ダイアログで「許可」を選択" +system_settings = "システム設定を待っています" + +[screenshot_editor] +loading_preview = "プレビューを読み込み中..." +zoom_in = "拡大" +zoom_out = "縮小" + +[screenshot_editor.annotation] +default_text = "テキスト" +done = "完了" +fill = "塗りつぶし" +intensity = "強さ" +mask_style = "スタイル" +pixelate = "ピクセル化" +stroke = "線" +transparent = "透明" + +[screenshot_editor.aspect] +auto = "自動" +classic = "クラシック ⋅4:3" +square = "正方形 ⋅1:1" +tall = "縦長 ⋅3:4" +vertical = "縦 ⋅9:16" +wide = "ワイド ⋅16:9" + +[screenshot_editor.background] +animated_gradient = "スクリーンショットではアニメーショングラデーションは静止画として描画されます。置き換えるには別の背景を選んでください。" +blur_title = "背景のぼかし" +choose_image = "クリックして選択するか、画像をドラッグ&ドロップ" +color = "カラー" +gradient = "グラデーション" +image = "画像" +image_files = "画像" +image_title = "背景画像" +wallpaper = "壁紙" + +[screenshot_editor.corner] +rounded = "角丸" +squircle = "スクワークル" +style = "角のスタイル" + +[screenshot_editor.crop] +full = "全体" +loading_frame = "フレームを読み込み中…" +position = "位置" + +[screenshot_editor.error] +encode = "スクリーンショットの書き出しをエンコードできませんでした:{error}" +load_meta = "スクリーンショットのメタデータを読み込めませんでした:{error}" +load_metadata = "スクリーンショットのメタデータを読み込めませんでした:{error}" +no_image = "{path} に PNG が見つかりません" +not_screenshot = "スクリーンショットのバンドルではありません" +open_image = "画像を開けませんでした:{error}" +renderer_failed = "GPU レンダラーを起動できませんでした:{error}" +renderer_not_running = "スクリーンショットのレンダラーが動作していません" +renderer_stopped = "書き出しが終わる前にレンダラーが停止しました" +save_project = "プロジェクトの保存中にエラーが発生しました:{error}" +serialize = "スクリーンショットの設定をシリアライズできませんでした:{error}" +sign_in_to_share = "ブラウザで Cap にサインインしてから、もう一度共有してください" +too_large = "画像サイズが上限を超えています:{width}x{height}" +upgrade_required = "この機能を使うにはプランのアップグレードが必要です" +upload_task = "アップロード処理に失敗しました:{error}" + +[screenshot_editor.header] +aspect_ratio = "アスペクト比" +copy = "クリップボードにコピー" +crop_image = "画像を切り抜く" +more_actions = "その他の操作" + +[screenshot_editor.layers] +empty = "レイヤーはまだありません" +empty_hint = "上のツールで注釈を追加できます" +reorder_hint = "ドラッグで並べ替え • 上 = 最前面" +title = "レイヤー" + +[screenshot_editor.menu] +delete = "削除" +open_folder = "フォルダを開く" + +[screenshot_editor.share] +create_link = "共有リンクを作成" +preparing = "アップロードを準備中" +rendering = "スクリーンショットを描画中" +uploading = "スクリーンショットをアップロード中" + +[screenshot_editor.style] +advanced_shadow = "シャドウの詳細設定" +background = "背景" +blur = "ぼかし" +border = "枠線" +color = "色" +corner_rounding = "角の丸み" +opacity = "不透明度" +padding = "余白" +rounding = "角の丸み" +shadow = "シャドウ" +size_label = "サイズ" +width = "幅" + +[screenshot_editor.toast] +copied = "スクリーンショットをクリップボードにコピーしました!" +link_copied = "共有リンクをクリップボードにコピーしました" +saved = "スクリーンショットを保存しました!" + +[screenshot_editor.tool] +arrow = "矢印" +circle = "円" +draw = "描画" +mask = "マスク" +rectangle = "四角形" +select = "選択" +text_label = "テキスト" + +[transcription.download] +complete = "ダウンロード完了" +failed = "ダウンロードに失敗しました:{error}" +file_progress = "{file} をダウンロード中:{percent}%" +preparing = "モデルのダウンロードを準備中" +progress = "モデルをダウンロード中:{percent}%" + +[transcription.error] +intel_mac = "Parakeet モデルは Intel Mac では使えません。代わりに Whisper モデルを使ってください" +model_load = "字幕モデルを読み込めませんでした。もう一度ダウンロードしてください" +model_missing = "字幕モデルが見つかりません。先にダウンロードしてください" +no_audio = "動画ファイルに音声がありません" +unsupported_language = "「推奨」モデルと「高精度」モデルは英語とその他24のヨーロッパ言語のみ文字起こしできます。この言語の字幕を作るには「小」または「中」モデルをダウンロードしてください" + +[upload.status] +failed = "アップロードに対応が必要です" +paused = "アップロードを一時停止中" +prepared = "アップロードの準備完了" +processing = "アップロードを処理中" +retrying = "アップロードを再試行します" +sign_in = "サインインしてアップロードを再試行" +uploaded = "アップロード済み" +uploading = "アップロード中" diff --git a/apps/desktop-gpui/locales/zh-CN.toml b/apps/desktop-gpui/locales/zh-CN.toml index faaf888950e..b247d990ec4 100644 --- a/apps/desktop-gpui/locales/zh-CN.toml +++ b/apps/desktop-gpui/locales/zh-CN.toml @@ -24,9 +24,17 @@ quit = "退出 Cap" services = "服务" settings = "设置…" +[menu.about] +licensed = "基于 AGPL-3.0 许可" +website = "网站" +commit = "提交" + [menu.file] close_window = "关闭窗口" title = "文件" +open_file = "打开文件…" +open_recent = "打开最近使用" +import_media = "导入媒体…" [menu.edit] copy = "拷贝" @@ -49,12 +57,22 @@ zoom = "缩放" [menu.help] title = "帮助" +feedback = "提供反馈…" +changelog = "更新日志" +dashboard = "仪表盘" +documentation = "文档" +faq = "常见问题" +self_hosting = "自托管" +help_center = "帮助中心" +system_status = "系统状态" +discord = "加入 Discord 社区" [tray] import_media = "导入媒体..." no_recent_items = "没有最近的项目" open_main_window = "打开主窗口" previous = "最近项目" +recording_mode = "录制模式" quit = "退出 Cap" record_area = "录制区域" record_display = "录制屏幕" @@ -239,6 +257,698 @@ title = "隐私" [settings.general.privacy.telemetry] description = "Cap 使用匿名遥测数据来提升稳定性并修复问题。我们绝不会收集录制内容、窗口标题、文件路径或任何个人信息。" label = "共享匿名遥测数据" +[settings.prompt] +ok = "好" + +[settings.sidebar] +account = "账户" +cancel_sign_in = "取消登录" +check_for_updates = "检查更新" +checking_for_updates = "正在检查更新…" +click_to_sign_in = "点击登录" +downloading = "正在下载 v{version}…" +downloading_progress = "正在下载 v{version}… {percent}%" +installing = "正在安装 v{version}…" +previous_versions = "查看以前的版本" +restart_to_update = "重新启动以更新到 v{version}" +sign_in = "登录" +sign_out = "退出登录" +signed_in = "已登录" + +[settings.recordings] +corrupted_body = "录制失败,因此此文件在编辑器中可能会出现问题!如果你在恢复文件时遇到问题,请联系支持团队!" +corrupted_title = "录制可能已损坏" +delete = "删除" +description = "管理你的录制并执行操作。" +edit = "编辑" +empty_library = "未找到录制" +failed = "录制失败" +import = "导入" +in_progress = "正在录制" +load_more = "加载更多" +open_bundle = "打开录制文件包" +open_link = "打开链接" +retry_upload = "重新上传" +show_all = "全部显示" +title = "录制" + +[settings.recordings.empty] +all = "没有录制" +all_matching = "没有匹配的录制" +instant = "没有 Instant 录制" +instant_matching = "没有匹配的 Instant 录制" +studio = "没有 Studio 录制" +studio_matching = "没有匹配的 Studio 录制" + +[settings.screenshots] +copy = "复制到剪贴板" +delete = "删除" +description = "管理你的截图并执行操作。" +empty = "没有截图" +empty_library = "未找到截图" +empty_matching = "没有匹配的截图" +import = "导入图片" +load_more = "加载更多" +open_folder = "打开文件夹" +save_as = "另存为" +title = "截图" + +[settings.quality] +save_failed = "无法保存你的录制设置,请重试。" + +[settings.quality.studio] +balanced = "均衡" +balanced_description = "细节清晰,文件大小适中。适合大多数录制。" +compatibility = "更小的文件" +compatibility_description = "节省磁盘空间。细节可能变得柔和,摄像头画面尤其明显。" +description = "保存到你的电脑上以便编辑。导出尺寸稍后在编辑器中选择。" +recommended = "推荐" +title = "Studio 录制" +ultra = "最高细节" +ultra_description = "保留最多细节,适合大量编辑。占用的磁盘空间最多。" + +[settings.quality.instant] +description = "录制时同步上传,停止录制时链接即可使用。如需最佳画质,请使用 Studio 模式:它会以屏幕支持的最高画质录制,并可导出最高 4K。" +free_plan = "免费版为 720p。Cap Pro 最高可录制 4K。" +max_resolution = "最高分辨率" +res_1080p = "文字清晰,上传体积适中。" +res_1440p = "为高分辨率大屏幕提供更多细节。" +res_4k = "细节最丰富。最适合高速网络。" +res_720p = "上传体积最小,链接最快可用。" +title = "Instant 录制" + +[settings.quality.sharing] +auto_open = "自动打开分享链接" +auto_open_description = "上传完成后在浏览器中打开链接。" +title = "分享" + +[settings.options.studio_quality] +balanced = "均衡" +compatibility = "兼容" +ultra = "超高" + +[settings.options.main_window_start] +close = "关闭" +minimise = "最小化" + +[settings.options.post_studio] +do_nothing = "不执行任何操作" +open_editor = "打开编辑器" +show_cap = "显示 Cap" + +[settings.options.post_screenshot] +copy = "复制到剪贴板" +copy_and_open_editor = "复制并打开编辑器" +do_nothing = "不执行任何操作" +open_editor = "打开编辑器" + +[settings.options.post_deletion] +do_nothing = "不执行任何操作" +reopen = "重新打开录制窗口" + +[settings.options.preview_quality] +full = "完整" +half = "一半" +quarter = "四分之一" + +[settings.shortcuts] +blocked_while_recording = "当前录制占用了停止键,无法更改快捷键。请先停止录制。" +needs_modifier_macos = "请包含 ⌘、⌃ 或 ⌥,以免快捷键干扰输入。" +needs_modifier_other = "请包含 Ctrl、Alt 或 Win,以免快捷键干扰输入。" +record_shortcut = "录制快捷键" +recording_description = "无需先打开 Cap 即可开始录制。" +recording_hint = "按下你想使用的按键。按 Esc 取消,按 Delete 移除。" +recording_title = "录制" +saved = "已保存。在任意应用中按 {keys} 即可{effect}。" +saved_moved = "已保存。在任意应用中按 {keys} 即可{effect}。已从“{shortcut}”中移除。" +screenshots_description = "快捷键在任何应用中都可用。点击即可更改。" +screenshots_title = "截图" +type_shortcut = "输入快捷键…" +unavailable = "其他应用或系统已在使用此快捷键。点击以选择其他快捷键。" +unsupported_key = "该按键不能用于快捷键。请尝试字母、数字或功能键。" +while_recording_title = "录制期间" + +[settings.shortcuts.after_screenshot] +hint = "适用于每张截图,无论来自快捷键还是 Cap" +title = "截图之后" + +[settings.shortcuts.after_studio] +hint = "停止录制时" +title = "Studio 录制之后" + +[settings.shortcuts.after_instant] +hint = "分享链接总会被复制" +title = "Instant 录制之后" + +[settings.shortcuts.outcome] +instant_copy_link = "仅复制链接" +instant_copy_link_hint = "不打开任何内容" +instant_open_link = "打开链接" +instant_open_link_hint = "在浏览器中" +saved_to_library = "保存到资料库" +screenshot_copy = "复制" +screenshot_copy_and_edit = "复制并编辑" +screenshot_copy_and_edit_hint = "两者兼顾" +screenshot_copy_hint = "随处粘贴" +screenshot_do_nothing = "不执行任何操作" +screenshot_open_editor = "打开编辑器" +screenshot_open_editor_hint = "先添加标注" +studio_do_nothing = "不执行任何操作" +studio_open_editor = "打开编辑器" +studio_open_editor_hint = "立即编辑" +studio_show_cap = "显示 Cap" +studio_show_cap_hint = "回到录制器" + +[settings.shortcuts.label] +cycle_mode = "切换录制模式" +fallback = "快捷键" +open_picker = "打开录制选择器" +pause = "暂停或继续" +record_area = "录制区域" +record_display = "录制屏幕" +record_window = "录制窗口" +restart = "重新开始录制" +screenshot_area = "截取区域" +screenshot_display = "截取屏幕" +screenshot_window = "截取窗口" +start_instant = "开始 Instant 录制" +start_studio = "开始 Studio 录制" +stop = "停止录制" + +[settings.shortcuts.effect] +cycle_mode = "在 Studio、Instant 和截图模式之间切换" +instant_copy = "以 Instant 模式录制所选屏幕或窗口,停止后复制分享链接" +instant_copy_and_open = "以 Instant 模式录制所选屏幕或窗口,停止后复制并打开分享链接" +open_picker = "打开 Cap 以选择要录制的内容" +pause = "暂停当前录制,或继续录制" +picker_area_instant = "选择一个区域,然后以 Instant 模式录制" +picker_area_screenshot = "选择一个区域,然后截图" +picker_area_studio = "选择一个区域,然后以 Studio 模式录制" +picker_display_instant = "选择一个屏幕,然后以 Instant 模式录制" +picker_display_screenshot = "选择一个屏幕,然后截图" +picker_display_studio = "选择一个屏幕,然后以 Studio 模式录制" +picker_window_instant = "选择一个窗口,然后以 Instant 模式录制" +picker_window_screenshot = "选择一个窗口,然后截图" +picker_window_studio = "选择一个窗口,然后以 Studio 模式录制" +restart = "丢弃当前这段并重新开始" +screenshot_area_copy = "选择一个区域并复制到剪贴板" +screenshot_area_copy_and_open_editor = "选择一个区域,复制后打开编辑器" +screenshot_area_open_editor = "选择一个区域并在编辑器中打开" +screenshot_area_save = "选择一个区域并保存到你的资料库,不打开任何内容" +screenshot_display_copy = "截取指针所在的屏幕并复制到剪贴板" +screenshot_display_copy_and_open_editor = "截取指针所在的屏幕,复制后打开编辑器" +screenshot_display_open_editor = "截取指针所在的屏幕并在编辑器中打开" +screenshot_display_save = "截取指针所在的屏幕并保存到你的资料库,不打开任何内容" +screenshot_window_copy = "截取指针下方的窗口并复制到剪贴板" +screenshot_window_copy_and_open_editor = "截取指针下方的窗口,复制后打开编辑器" +screenshot_window_open_editor = "截取指针下方的窗口并在编辑器中打开" +screenshot_window_save = "截取指针下方的窗口并保存到你的资料库,不打开任何内容" +stop = "停止当前录制" +studio = "以 Studio 模式录制所选屏幕或窗口" +studio_open_editor = "以 Studio 模式录制所选屏幕或窗口,停止后打开编辑器" + +[settings.shortcuts.sentence] +cycle_mode = "在 Studio、Instant 和截图模式之间切换。" +instant_copy = "以 Instant 模式录制所选屏幕或窗口,停止后复制分享链接。" +instant_copy_and_open = "以 Instant 模式录制所选屏幕或窗口,停止后复制并打开分享链接。" +open_picker = "打开 Cap 以选择要录制的内容。" +pause = "暂停当前录制,或继续录制。" +picker_area_instant = "选择一个区域,然后以 Instant 模式录制。" +picker_area_screenshot = "选择一个区域,然后截图。" +picker_area_studio = "选择一个区域,然后以 Studio 模式录制。" +picker_display_instant = "选择一个屏幕,然后以 Instant 模式录制。" +picker_display_screenshot = "选择一个屏幕,然后截图。" +picker_display_studio = "选择一个屏幕,然后以 Studio 模式录制。" +picker_window_instant = "选择一个窗口,然后以 Instant 模式录制。" +picker_window_screenshot = "选择一个窗口,然后截图。" +picker_window_studio = "选择一个窗口,然后以 Studio 模式录制。" +restart = "丢弃当前这段并重新开始。" +screenshot_area_copy = "选择一个区域并复制到剪贴板。" +screenshot_area_copy_and_open_editor = "选择一个区域,复制后打开编辑器。" +screenshot_area_open_editor = "选择一个区域并在编辑器中打开。" +screenshot_area_save = "选择一个区域并保存到你的资料库,不打开任何内容。" +screenshot_display_copy = "截取指针所在的屏幕并复制到剪贴板。" +screenshot_display_copy_and_open_editor = "截取指针所在的屏幕,复制后打开编辑器。" +screenshot_display_open_editor = "截取指针所在的屏幕并在编辑器中打开。" +screenshot_display_save = "截取指针所在的屏幕并保存到你的资料库,不打开任何内容。" +screenshot_window_copy = "截取指针下方的窗口并复制到剪贴板。" +screenshot_window_copy_and_open_editor = "截取指针下方的窗口,复制后打开编辑器。" +screenshot_window_open_editor = "截取指针下方的窗口并在编辑器中打开。" +screenshot_window_save = "截取指针下方的窗口并保存到你的资料库,不打开任何内容。" +stop = "停止当前录制。" +studio = "以 Studio 模式录制所选屏幕或窗口。" +studio_open_editor = "以 Studio 模式录制所选屏幕或窗口,停止后打开编辑器。" + +[settings.cli] +command = "命令" +copy = "复制" +description = "安装 Cap 命令,供终端、智能体、脚本和本地自动化使用。" +install = "安装 CLI" +installed = "已安装" +installing = "正在安装..." +installs_command = "桌面应用会安装一个本地\u00A0{command}\u00A0命令,它指向内置的 CLI。" +load_failed = "无法加载 CLI 状态:{error}" +not_installed = "未安装" +path_configured = "已将 cap 添加到你的 PATH。重新启动终端即可使用,或现在运行:" +path_missing = "将 {path} 添加到你的 PATH,即可在新终端中使用 cap。" +remove = "移除" +removing = "正在移除..." +repair = "修复" +repairing = "正在修复..." +retry = "重试" +target = "目标" +title = "命令行" + +[settings.transcription] +active_title = "已启用的提示" +applied_note = "在编辑器中生成字幕时会应用这些提示。" +clear = "清除" +count = { other = "{count} 项" } +description = "添加生成字幕时需要注意的名称、拼写、域名和大小写偏好。" +placeholder = "添加词语" +saved = "已保存" +saving = "正在保存..." +terms_description = "每次添加一个词语,以减少错别字和格式错误。" +terms_title = "记住的词语" +title = "转写" + +[settings.experimental] +developer = "开发者" +developer_description = "仅在开发版本中显示。" +muxer = "进程外混流器" +muxer_description = "在独立的子进程中运行分段 MP4 混流器,混流器崩溃时不会影响你的录制。需要内置的 cap-muxer 程序。" +native_app = "使用原生应用" +native_app_description = "关闭后会退出此应用,并在开发环境中重新打开经典的 Tauri 应用。录制和设置是共享的。" +reliability = "可靠性" + +[settings.experimental.switch] +busy = "请先完成录制、导出、上传、导入或转写任务,再切换到经典版应用。" +check_failed = "无法确认经典版 Cap 应用是否已打开:{error}" +data_stays = "你的录制和设置会原样保留。" +failed = "无法打开经典版 Cap 应用。" +not_found = "找不到可以切换回去的经典版 Cap 应用。" +open_failed = "无法打开经典版 Cap 应用:{error}" +opening = "正在打开经典版 Cap 应用" +opening_detail = "经典版 Cap 应用准备就绪后,此窗口将关闭。" +save_failed = "无法保存你的应用偏好设置。Cap 仍保持打开。" +switching = "正在切换回经典版 Cap 应用。" +timed_out = "经典版应用尚未打开。请在开发终端中检查构建错误,然后重试。" + +[settings.feedback] +app_version = "应用版本" +capture_support = "捕获支持" +capture_supported = "屏幕捕获:支持" +capture_unsupported = "屏幕捕获:不支持" +community_description = "有问题、想分享想法,或者只是想聊聊?加入 Cap 的 Discord 社区吧。" +community_title = "加入社区" +debug_description = "上传你的日志,帮助我们诊断 Cap 的问题。不包含任何个人信息。" +debug_title = "调试信息" +description = "提交反馈或报告问题,帮助我们改进 Cap。我们会尽快处理。" +join_discord = "加入 Discord" +operating_system = "操作系统" +placeholder = "告诉我们你对 Cap 的看法..." +submit = "提交反馈" +submit_failed = "提交反馈失败" +submitting = "正在提交..." +system_loading = "正在加载系统信息..." +system_title = "系统信息" +thanks = "感谢你的反馈!" +title = "反馈" +upload_interrupted = "上传被中断" +upload_logs = "上传日志" +upload_sent = "已发送。谢谢,这能帮助我们找到问题。" +uploading = "正在上传..." + +[settings.feedback.diagnostic] +confirm_body = "Cap 将以闪烁图样占据你的屏幕,并在每条处理流程中播放约 {seconds} 秒的响亮提示音。请摘下耳机,保持音量可闻,并在完成前不要操作电脑。" +confirm_title = "运行诊断?" +description = "录制一段闪烁并发出提示音的简短测试图样,测量画面与声音的偏移程度,并记录你的显示器、摄像头、麦克风、磁盘空间和录制设置。在你选择发送之前,不会发送任何内容。" +mic_description = "同时录制你的麦克风,检查它是否及时听到提示音。需要麦克风能实际听到的扬声器。" +mic_label = "测试麦克风" +mode_description = "要测试的处理流程。“两者”会先运行 Studio 部分,再运行 Instant 部分。" +mode_label = "录制模式" +no_cli = "未在此版本旁找到 Cap 命令行工具,因此报告只会包含你的环境信息。" +no_cli_sync = "未在此版本旁找到 Cap 命令行工具,因此无法运行同步测试。" +run = "运行诊断" +run_again = "再次运行" +send = "发送给 Cap" +sending = "正在发送..." +show_file = "显示文件" +stop_recording_first = "请先停止当前录制,再运行诊断。" +title = "诊断报告" +try_again = "重试" +warning_body = "一个全屏窗口会黑白闪烁,同时扬声器播放 1kHz 提示音,Cap 会录制下来并测量画面与声音的偏移程度。运行期间请不要操作电脑。两种模式加起来大约需要几分钟。" +warning_title = "测试会占据你的屏幕并发出声音。" + +[settings.feedback.diagnostic.verdict] +environment_only = "仅环境信息" +fail = "未通过" +inconclusive = "无法确定" +pass = "通过" +warn = "警告" + +[settings.sign_in_required] +body = "请登录以继续。你也可以发送邮件至 hello@cap.so,或通过 cap.link/discord 加入我们的 Discord" +title = "需要登录" + +[settings.changelog] +fetch_failed = "获取更新日志失败" +loading = "正在加载更新日志..." +new = "新" +version = "版本 {version} - {date}" + +[settings.license] +activate = "激活许可证" +activating = "正在激活..." +active_title = "桌面许可证" +checking = "正在检查…" +deactivate = "停用许可证" +description = "你的 Cap 套餐和桌面许可证,集中在一处。" +desktop_description = "你的桌面许可证涵盖商用录制和编辑。Cap Pro 还提供云端分享和协作功能。" +desktop_name = "桌面许可证" +expires = "到期时间" +explore_description = "在我们的网站上比较当前价格和所含的全部内容。" +explore_desktop = "桌面许可证 · 可商用桌面录制器和编辑器。" +explore_pro = "Cap Pro · 桌面许可证,另含云端分享、AI 功能和协作。" +explore_title = "了解套餐" +free_description = "可在本地录制和编辑,仅限个人使用。如需商用或更多云端功能,请选择付费套餐。" +have_key = "已有许可证密钥?" +key = "许可证密钥" +no_instance = "未找到实例 ID" +opens_pricing = "在浏览器中打开 cap.so/pricing。" +placeholder = "许可证密钥" +plan_title = "你的套餐" +pro_description = "你的账户包含云端分享、Pro 功能以及可商用的桌面许可证。" +refresh = "刷新套餐" +refresh_failed = "无法刷新你的套餐,请重试。" +save_failed = "保存许可证失败" +sign_in_hint = "已有 Cap Pro?请在侧边栏中登录你的账户。" +title = "套餐与许可证" +view_pricing = "查看套餐和价格 ↗" + +[settings.integrations] +configure = "配置" +description = "配置集成以扩展 Cap 的功能,并连接第三方服务。" +google_drive_description = "连接 Google Drive,用于上传新的分享链接视频。Cap 会将新视频存放在你 Drive 中的私有 Cap 文件夹,并在常规访问检查后继续通过 Cap 提供。" +managed = "由你的组织管理" +managed_by = "由你的组织管理:{organization}" +request_timed_out = "请求超时" +s3_description = "连接你自己的 S3 存储桶,完全掌控数据存储。所有新的分享链接上传都会自动上传到你配置的 S3 存储桶,确保你对内容拥有完整的所有权和控制权。非常适合需要数据主权和自定义存储策略的组织。" +s3_name = "S3 配置" +title = "集成" +upgrade = "升级到 Pro" + +[settings.integrations.s3] +access_key = "访问密钥 ID" +bucket = "存储桶名称" +configuration = "配置" +delete_failed = "删除 S3 配置失败" +deleted = "S3 配置已成功删除" +endpoint = "端点" +fetch_failed = "获取 S3 配置失败" +guide = "存储配置指南" +guide_intro = "设置存储桶并将其连接到 Cap 应该不到 10 分钟。查看\u00A0{guide}\u00A0即可开始。" +other_provider = "其他兼容 S3 的服务" +provider = "存储服务商" +region = "区域" +remove = "移除配置" +removing = "正在移除..." +save_failed = "保存 S3 配置失败" +saved = "S3 配置已成功保存" +saving = "正在保存..." +secret_key = "秘密访问密钥" +test = "测试连接" +test_failed = "S3 连接测试失败。请检查你的配置和网络连接。" +test_ok = "S3 配置测试成功!连接正常。" +test_timeout = "连接测试在 5 秒后超时。请检查你的端点 URL 和网络连接。" +testing = "正在测试..." + +[settings.integrations.gdrive] +active = "已启用" +active_for_uploads = "已用于新的上传" +cached = "缓存于 {time}" +connect = "连接 Google Drive" +connect_failed = "无法开始连接 Google Drive" +connected_inactive = "已连接但未启用" +connection_description = "Google Drive 会将新的上传内容存放在你 Drive 中的私有 Cap 文件夹。已有的 Cap 托管视频和 S3 视频继续使用当前存储。" +connection_title = "连接" +disconnect = "断开连接" +disconnect_failed = "无法断开 Google Drive 连接" +disconnected = "已断开 Google Drive 连接" +drive_files = "Drive 文件" +finish_in_browser = "请在浏览器中完成 Google Drive 的连接,然后回到这里刷新。" +not_connected = "未连接" +opening = "正在打开..." +refresh = "刷新" +refreshing = "正在刷新..." +remaining = "剩余" +set_active_failed = "无法更新当前使用的存储服务商" +storage = "存储空间" +test = "测试" +test_failed = "Google Drive 连接测试失败" +testing = "正在测试..." +trash = "回收站" +updated = "更新于 {time}" +use_drive = "使用 Google Drive" +use_s3 = "使用 S3" +used = "已使用 {usage}" +used_of = "已使用 {usage},共 {limit}" +waiting = "正在等待..." +working = "Google Drive 连接正常" +working_for = "{email} 的 Google Drive 连接正常" + +[settings.automations] +action_no_effect = "对所选触发器而言,此操作没有效果。" +add_action = "添加操作" +add_condition = "添加条件" +auto_name_empty = "{trigger}自动化" +browse = "浏览" +check_compatibility = "检查兼容性" +compatibility_checked = "已检查兼容性:此自动化暂时还不会在 Cap 中运行。已保存的规则仍可供 Cap CLI 使用。未执行任何操作。" +condition_never_matches = "对所选触发器而言,此条件永远不会满足。" +copy_link = "将链接复制到剪贴板" +dangerous = "在 Cap CLI 中,此自动化会以你的权限运行命令或发送网络请求。请只使用你信任的值。" +delete = "删除" +description = "规则与 Cap CLI 共享。Cap 会保存这些规则,但暂时还不会运行自动化。" +empty_description = "选择下方的模板一键开始,或从头创建你自己的自动化。" +empty_title = "还没有自动化" +enabled = "已启用" +match_all = "全部满足" +match_any = "满足任一" +name = "名称" +new = "新建自动化" +no_presets = "还没有预设,请先在编辑器中创建一个。" +only_if = "仅在以下情况下运行" +open_in_browser = "在浏览器中打开" +retry_save = "重新保存" +run_through_shell = "通过 shell 运行" +save_failed = "保存自动化失败。你的更改尚未保存。" +start_from_scratch = "从头开始" +summary_empty = "{trigger} → 还没有操作" +templates_description = "一键添加现成的自动化,之后可随意调整。" +templates_title = "模板" +then = "然后执行" +title = "自动化" +when = "当发生以下情况时" + +[settings.automations.status] +disabled_saved = "已停用 · 已保存" +disabled_unsaved = "已停用 · 未保存" +enabled_saved = "已启用 · 已保存" +enabled_unsaved = "已启用 · 未保存" + +[settings.automations.field] +arguments = "参数(以空格分隔)" +body = "正文" +body_template = "正文模板(可选)" +compression = "压缩" +destination = "目标文件夹(可选,留空 = 项目文件夹)" +filename = "文件名模板(可选)" +folder = "文件夹" +format = "格式" +frame_rate = "帧率" +method = "方法" +organization = "组织 ID(可选)" +preset = "预设" +program = "程序" +resolution = "分辨率" +source = "来源" +title = "标题" +url = "URL" + +[settings.automations.placeholder] +organization_id = "组织 ID" +project_folder = "项目文件夹" +window_title = "例如 Slack" + +[settings.automations.clipboard] +original = "原始捕获内容" +rendered = "已编辑 / 已渲染" + +[settings.automations.target] +area = "区域" +display = "屏幕" +window = "窗口" + +[settings.automations.compression] +maximum = "最高" +potato = "极低画质" +social = "社交" +web = "网页" + +[settings.automations.trigger.screenshot_taken] +label = "截图完成时" +noun = "截图" +phrase = "截图完成" +runs = "每次截图都会运行。" + +[settings.automations.trigger.studio_finished] +label = "Studio 录制结束时" +noun = "Studio 录制" +phrase = "Studio 录制结束" +runs = "每次 Studio 录制结束时都会运行。" + +[settings.automations.trigger.instant_finished] +label = "Instant 录制结束时" +noun = "Instant 录制" +phrase = "Instant 录制结束" +runs = "每次 Instant 录制结束时都会运行。" + +[settings.automations.trigger.recording_started] +label = "录制开始时" +noun = "录制开始" +phrase = "录制开始" +runs = "每次开始录制时都会运行。" + +[settings.automations.trigger.upload_completed] +label = "上传完成时" +noun = "上传" +phrase = "上传完成" +runs = "每次上传完成时都会运行。" + +[settings.automations.trigger.video_imported] +label = "导入视频时" +noun = "导入" +phrase = "视频已导入" +runs = "每次导入视频时都会运行。" + +[settings.automations.trigger.recording_deleted] +label = "删除录制时" +noun = "删除" +phrase = "录制已删除" +runs = "每次删除录制时都会运行。" + +[settings.automations.action.copy_to_clipboard] +label = "复制到剪贴板" +noun = "剪贴板" +short = "复制到剪贴板" + +[settings.automations.action.save_to_location] +label = "保存到指定位置" +noun = "文件夹" +short = "保存到文件夹" + +[settings.automations.action.export] +label = "按配置导出" +noun = "导出" +short = "导出" + +[settings.automations.action.upload] +label = "上传并复制链接" +noun = "上传" +short = "上传并复制链接" + +[settings.automations.action.reveal] +label = "在文件管理器中显示" +noun = "显示" +short = "在文件管理器中显示" + +[settings.automations.action.open_file] +label = "打开文件" +noun = "打开" +short = "打开文件" + +[settings.automations.action.ocr] +label = "识别文字(OCR)到剪贴板" +noun = "文字" +short = "复制文字(OCR)" + +[settings.automations.action.notify] +label = "显示通知" +noun = "通知" +short = "通知" + +[settings.automations.action.open_editor] +label = "打开编辑器" +noun = "编辑器" +short = "打开编辑器" + +[settings.automations.action.skip_editor] +label = "跳过编辑器(无界面)" +noun = "跳过编辑器" +short = "跳过编辑器" + +[settings.automations.action.apply_preset] +label = "应用编辑器预设" +noun = "预设" +short = "应用预设" + +[settings.automations.action.run_command] +label = "运行命令" +noun = "命令" +short = "运行命令" + +[settings.automations.action.webhook] +label = "发送 Webhook" +noun = "Webhook" +short = "发送 Webhook" + +[settings.automations.action.delete_local_files] +label = "删除本地文件" +noun = "删除" +short = "删除本地文件" + +[settings.automations.condition] +capture_target = "捕获对象为" +duration_at_least = "时长至少(秒)" +duration_at_most = "时长至多(秒)" +organization = "组织为" +recording_mode = "录制模式为" +window_title = "窗口标题包含" + +[settings.automations.template.copy_screenshot] +description = "截图后即可直接粘贴。" +name = "自动将新截图复制到剪贴板" + +[settings.automations.template.ocr_screenshot] +description = "Cap 会识别截图中的文字并替你复制。" +name = "从截图中提取文字" + +[settings.automations.template.save_screenshot] +description = "将每张新截图直接保存到你选择的文件夹。" +name = "把截图收进文件夹" + +[settings.automations.template.reveal_screenshot] +description = "每张截图一拍下就在 Finder 中显示。" +name = "跳转到每张新截图" + +[settings.automations.template.export_studio] +description = "Studio 录制一结束就渲染出 MP4。" +name = "录制结束后自动导出" + +[settings.automations.template.upload_share] +description = "录制完成后,链接已在剪贴板中等你。" +name = "上传并获取分享链接" + +[settings.automations.template.notify_upload] +body = "你的录制已可以分享。" +description = "录制可以分享时,桌面会轻轻提醒你。" +name = "上传就绪时提醒我" + +[settings.automations.template.webhook_share] +description = "将分享链接发送到 Slack、Discord 或你自己的 Webhook。" +name = "分享内容时通知 Slack" + [main] delete_recording_confirm = "确定要删除这段录制吗?" @@ -586,3 +1296,397 @@ click_or_press = "点击此处,或按下" get_started = "开始使用" space_key = "空格" tagline = "人人可用的开源屏幕分享应用。" + +[menu.update] +badge_progress = "v{version} · {percent}%" +badge_version = "v{version}" +check = "检查更新" +download = "下载更新…" +checking = "正在检查更新…" +downloading = "正在下载更新…" +restart = "重新启动以更新…" + +[settings.general.updates.download_policy] +auto = "自动" +description = "选择 Cap 是否自动下载更新。" +label = "更新下载" +manual = "先询问" +never = "从不" + +[auth] +sign_in_failed = "登录失败" + +[diagnostics] +already_running = "已有一项诊断正在运行。" +mode_both = "两者" + +[diagnostics.error] +create_folder = "无法创建诊断文件夹:{error}" +exited = "自检已退出,状态:{status}" +no_output = "自检没有产生输出流" +serialize = "无法序列化报告:{error}" +start_failed = "无法启动 {program}:{error}" +upload_logs = "无法上传日志:{error}" +upload_status = "上传失败,状态:{status}" +wait_failed = "无法等待自检结束:{error}" +write_failed = "无法写入 {path}:{error}" + +[diagnostics.stage] +analyzing = "正在分析录制..." +analyzing_instant = "正在分析 Instant 录制..." +analyzing_studio = "正在分析 Studio 录制..." +cancelling = "正在取消..." +collecting = "正在收集系统信息..." +done = "正在完成同步测试..." +exporting = "正在导出录制..." +exporting_instant = "正在导出 Instant 录制..." +exporting_studio = "正在导出 Studio 录制..." +finalizing = "正在完成录制..." +finalizing_instant = "正在完成 Instant 录制..." +finalizing_studio = "正在完成 Studio 录制..." +pattern = "正在播放测试图案..." +pattern_instant = "正在播放测试图案(Instant)..." +pattern_studio = "正在播放测试图案(Studio)..." +preparing = "正在准备测试..." +preparing_instant = "正在准备 Instant 测试..." +preparing_studio = "正在准备 Studio 测试..." +recording = "正在录制测试..." +recording_instant = "正在录制 Instant 测试..." +recording_studio = "正在录制 Studio 测试..." +starting = "正在启动诊断..." + +[dialog] +no_microphone_body = "此录制将不包含你的声音。" +open_recording_failed = "无法打开此录制:{error}" + +[dialog.capture] +approve_shortcut = "请批准一个录制快捷键,或在 Cap 停止托盘图标出现时点按它。录制尚未开始。" +area_unavailable = "所选区域已变化或不再可用。请重新选择。" +camera_needs_x11 = "在 Linux 上,Instant 摄像头叠加层需要 X11 桌面" +camera_notice = "Cap 将隐藏摄像头预览和录制控件。所选摄像头会继续作为单独的可编辑轨道录制。" +camera_notice_instant = "Cap 将隐藏预览和控件。所选摄像头会以所选的形状和效果显示在其预览位置。请让整个预览保持在捕获区域内。打开 Cap 会先停止此 Instant 录制,然后再显示控件。" +camera_outside_area = "开始 Instant 录制前,请将整个摄像头预览移到捕获区域内" +camera_preview_closed = "Instant 录制前,请先打开所选摄像头的预览" +cleanup_incomplete = "录制清理未完成:{error}。无法开始新的录制。" +devices_changed = "所选设备已更改。请重新开始以确认新的录制输入。" +devices_changed_before_start = "录制开始前所选设备已更改。请重新开始以确认输入。" +shortcut_conflict = "Ctrl+Shift+F9 已分配给另一个 Cap 操作。请在录制前更改该快捷键。" +shortcut_description = "开始或停止此 Cap 录制" +shortcut_hint = "按下并松开 {shortcut} 开始录制。再次使用同一快捷键即可停止。此临时快捷键不会更改你的设置。" +tray_stop_hint = "点按 Cap 停止托盘图标即可开始。再次点按同一图标即可停止。在你使用该控件之前,录制不会开始。" +window_unavailable = "所选窗口已不可用。请重新选择。" + +[dialog.clipboard] +copy_file = "无法将文件复制到剪贴板:{error}" +copy_file_failed = "无法将文件复制到剪贴板" +copy_image = "无法将图像复制到剪贴板:{error}" +copy_image_failed = "无法将图像复制到剪贴板" +not_a_file = "只能将文件复制到剪贴板" +unavailable = "剪贴板不可用" +unavailable_error = "剪贴板不可用:{error}" + +[dialog.delete_recording] +already_deleting = "此录制正在删除中。" +close_failed = "无法关闭编辑器:{error}" +editor_changed = "确认删除期间编辑器发生了变化。录制未被删除。" +editor_closed = "编辑器已关闭。录制未被删除。" +failed = "删除录制失败:{error}" +failed_title = "无法删除录制" +finishing_body = "Cap 正在等待录制清理完成,然后再完成此次删除。" +finishing_title = "删除仍在进行中" +in_use = "此录制仍在使用中。请完成或取消其上传后重试:{error}" +outside_library = "此录制不在录制库中。其文件未被更改。" +recording_active = "请先完成或取消录制,再删除此项目。" +retained_title = "录制已保留" + +[dialog.file] +audio_filter = "音频" +busy = "已有一个文件对话框处于打开状态。请先完成或取消它,然后再试。" +export_filter = "导出" +failed = "文件对话框无法完成:{error}" +images_filter = "图像" +unavailable_title = "文件对话框不可用" +unconfirmed = "Cap 无法确认文件对话框已关闭。请关闭所有剩余的文件对话框并重启 Cap,然后再试。" + +[dialog.mode_select] +instant = "通过链接即时分享。边录边上传。" +screenshot = "即时截图并添加标注。" +studio = "最高画质的本地录制,便于稍后编辑。" +subtitle = "选择你想如何捕获屏幕" +title = "选择录制模式" + +[dialog.storage] +stopped_kept = "存储空间不足,录制已停止。录制文件已保留。请释放空间,然后在下方恢复录制。" +stopped_saved = "存储空间不足,录制已停止。你的录制已保存。" +stopping = "存储空间不足。正在停止并保存你的录制…" + +[dialog.teleprompter] +cue_markers = "提示标记" +hidden = "此窗口不会出现在 Cap 录制中" +mirror = "镜像文字" +paused = "滚动已随录制暂停" +placeholder = "粘贴或输入你的脚本…" + +[import] +error_title = "导入错误" +image_failed = "无法导入图像:{error}" +unsupported = "不支持的媒体文件。" +video_failed = "无法导入视频:{error}" + +[import.filter] +image = "图像文件" +media = "媒体文件" +video = "视频文件" + +[import.progress] +analyzing = "正在分析视频文件..." +complete = "导入完成!" +converting = "正在转换视频... {percent}%" +image = "正在导入图像..." +metadata = "正在创建项目元数据..." +starting = "正在开始转换..." + +[overlay] +choose_mode = "选择录制模式" +countdown_tooltip = "录制倒计时" +display_fallback = "显示器 {id}" + +[overlay.area] +draw = "绘制一个区域" +free_ratio = "自由" +hint = "点击并拖动以选择区域" +minimum_size = "最小尺寸为 {width} x {height}" +too_small = "{width} x {height} 太小" + +[overlay.camera_only] +description = "仅使用摄像头和麦克风录制" +select_camera = "请选择一个摄像头" + +[overlay.device] +camera_allow = "允许访问摄像头…" +camera_turn_on = "在设置中开启摄像头访问…" +microphone_allow = "允许访问麦克风…" +microphone_turn_on = "在设置中开启麦克风访问…" + +[overlay.start] +instant = "Instant 模式:录制分享链接" +instant_sign_in = "Instant 模式:登录后即可即时录制" +sign_in = "登录后使用" +start_recording = "开始录制" +studio = "Studio 模式:在本地录制和编辑" +take_screenshot = "截图" + +[permission.guide] +accessibility_body = "将它拖放到上方的“辅助功能”列表中,然后确认它的开关已打开。" +screen_body = "将它拖放到上方的“录屏与系统录音”列表中,然后确认它的开关已打开。" +title = "将 Cap 拖到列表中" + +[permission.name] +accessibility = "辅助功能" +camera = "摄像头" +microphone = "麦克风" +screen_recording = "屏幕录制" + +[permission.onboarding] +open_settings_again = "再次打开系统设置" +skip = "暂时跳过" +switch_on_title = "在系统设置中打开 Cap" + +[permission.onboarding.accessibility] +allow = "允许辅助功能" +body = "辅助功能让 Cap 能看到你的点击和光标(仅限这台 Mac),这样自动缩放就知道该看哪里。" +done_body = "自动缩放可以跟随你的点击了。" +done_title = "辅助功能已开启" +title = "让 Cap 跟随你的光标" + +[permission.onboarding.all_set] +body = "Cap 已获得所需的一切。接下来,快速了解它能做什么。" +regained = "谢谢。Cap 可以再次录制了。" +title = "一切就绪" + +[permission.onboarding.camera] +allow = "允许摄像头" +body = "在录制画面的一角显示你自己。不需要的话可以跳过。" +done_body = "你可以在任何录制中加入摄像头画面。" +done_title = "摄像头已开启" +title = "让 Cap 看到你" + +[permission.onboarding.checklist] +lede = "macOS 会先请你允许几项权限,只需一分钟。" +regained_lede = "系统设置中有项权限被关闭了。请重新开启以继续录制。" +regained_title = "Cap 需要重新获得一项权限" +title = "让我们准备好 Cap" + +[permission.onboarding.hint] +drag_into_list = "将 Cap 从系统设置下方的面板拖到列表中。已经在列表里了?直接打开即可。" +find_in_list = "在列表中找到 Cap 并打开。如果 macOS 先弹出了询问,请选择“打开系统设置”。" +no_prompt = "没有出现提示?请在系统设置中打开 Cap。" +open_settings = "打开系统设置,在列表中找到 Cap 并打开。" +turned_off = "它之前被关闭了,请在系统设置中打开 Cap,或暂时跳过。" + +[permission.onboarding.microphone] +allow = "允许麦克风" +body = "在录制中加入你的声音。你可以先跳过,之后再开启。" +done_body = "你的声音可以出现在录制中了。" +done_title = "麦克风已开启" +title = "让 Cap 听到你" + +[permission.onboarding.relaunch] +body = "Cap 重启后,macOS 会开启屏幕录制。你会回到这里继续。" +button = "重新启动" +hint = "已经打开但仍在等待?重启 Cap 以生效。" +later = "macOS 会在 Cap 重启后应用此设置。你可以在这里完成后再重启。" +title = "重启一次就好" + +[permission.onboarding.screen] +allow = "允许屏幕录制" +body = "屏幕录制权限让 Cap 能捕获你的显示器、窗口和系统音频。" +done_body = "Cap 可以捕获你的屏幕了。" +done_title = "屏幕录制已开启" +title = "让 Cap 看到你的屏幕" + +[permission.onboarding.status] +allowed = "已允许" +off = "已在系统设置中关闭" +optional = "可选" +required = "必需" +skipped = "暂时跳过" + +[permission.onboarding.waiting] +prompt = "在 macOS 提示中选择“允许”" +system_settings = "正在等待系统设置" + +[screenshot_editor] +loading_preview = "正在加载预览..." +zoom_in = "放大" +zoom_out = "缩小" + +[screenshot_editor.annotation] +default_text = "文本" +done = "完成" +fill = "填充" +intensity = "强度" +mask_style = "样式" +pixelate = "像素化" +stroke = "描边" +transparent = "透明" + +[screenshot_editor.aspect] +auto = "自动" +classic = "经典 ⋅4:3" +square = "方形 ⋅1:1" +tall = "竖版 ⋅3:4" +vertical = "竖屏 ⋅9:16" +wide = "宽屏 ⋅16:9" + +[screenshot_editor.background] +animated_gradient = "截图中的动态渐变会渲染为静态图像。选择其他背景即可替换。" +blur_title = "背景模糊" +choose_image = "点击选择或拖放图像" +color = "颜色" +gradient = "渐变" +image = "图像" +image_files = "图像" +image_title = "背景图像" +wallpaper = "壁纸" + +[screenshot_editor.corner] +rounded = "圆角" +squircle = "超椭圆" +style = "圆角样式" + +[screenshot_editor.crop] +full = "全部" +loading_frame = "正在加载画面…" +position = "位置" + +[screenshot_editor.error] +encode = "无法编码导出的截图:{error}" +load_meta = "无法加载截图元数据:{error}" +load_metadata = "无法加载截图元数据:{error}" +no_image = "在 {path} 中找不到 PNG" +not_screenshot = "这不是截图包" +open_image = "无法打开图像:{error}" +renderer_failed = "GPU 渲染器无法启动:{error}" +renderer_not_running = "截图渲染器未运行" +renderer_stopped = "渲染器在导出完成前停止了" +save_project = "保存项目时出错:{error}" +serialize = "无法序列化截图配置:{error}" +sign_in_to_share = "请在浏览器中登录 Cap,然后再次分享" +too_large = "图像尺寸超过上限:{width}x{height}" +upgrade_required = "此功能需要升级套餐" +upload_task = "上传任务失败:{error}" + +[screenshot_editor.header] +aspect_ratio = "宽高比" +copy = "复制到剪贴板" +crop_image = "裁剪图像" +more_actions = "更多操作" + +[screenshot_editor.layers] +empty = "还没有图层" +empty_hint = "使用上方的工具添加标注" +reorder_hint = "拖动以重新排序 • 顶部 = 最前" +title = "图层" + +[screenshot_editor.menu] +delete = "删除" +open_folder = "打开文件夹" + +[screenshot_editor.share] +create_link = "创建分享链接" +preparing = "正在准备上传" +rendering = "正在渲染截图" +uploading = "正在上传截图" + +[screenshot_editor.style] +advanced_shadow = "高级阴影设置" +background = "背景" +blur = "模糊" +border = "边框" +color = "颜色" +corner_rounding = "圆角" +opacity = "不透明度" +padding = "内边距" +rounding = "圆角" +shadow = "阴影" +size_label = "大小" +width = "宽度" + +[screenshot_editor.toast] +copied = "截图已复制到剪贴板!" +link_copied = "分享链接已复制到剪贴板" +saved = "截图已保存!" + +[screenshot_editor.tool] +arrow = "箭头" +circle = "圆形" +draw = "绘制" +mask = "遮罩" +rectangle = "矩形" +select = "选择" +text_label = "文本" + +[transcription.download] +complete = "下载完成" +failed = "下载失败:{error}" +file_progress = "正在下载 {file}:{percent}%" +preparing = "正在准备下载模型" +progress = "正在下载模型:{percent}%" + +[transcription.error] +intel_mac = "Parakeet 模型不支持 Intel Mac。请改用 Whisper 模型" +model_load = "无法加载字幕模型。请尝试重新下载" +model_missing = "未找到字幕模型。请先下载" +no_audio = "视频文件中没有音频" +unsupported_language = "“推荐”和“高精度”模型只能转写英语和其他 24 种欧洲语言。要为此语言生成字幕,请下载“小”或“中”模型" + +[upload.status] +failed = "上传需要处理" +paused = "上传已暂停" +prepared = "上传已准备" +processing = "正在处理上传" +retrying = "将重试上传" +sign_in = "登录以重试上传" +uploaded = "已上传" +uploading = "正在上传" diff --git a/apps/desktop-gpui/src/app_windows.rs b/apps/desktop-gpui/src/app_windows.rs index aafc67eb919..a5adf9ea1b1 100644 --- a/apps/desktop-gpui/src/app_windows.rs +++ b/apps/desktop-gpui/src/app_windows.rs @@ -25,6 +25,7 @@ use crate::{ controls_window::ControlsWindow, editor_timeline, editor_window::{self, EditorWindow}, + i18n::tr, main_window::{MainWindow, Mode, TargetType}, mode_select_window::{self, ModeSelectWindow}, onboarding_window::{self, OnboardingWindow}, @@ -582,6 +583,7 @@ pub fn init(main: WindowHandle, session: Entity, c cx.spawn(async move |cx| { while thumbnails.recv_async().await.is_ok() { cx.update(|cx| { + crate::tray::refresh_previous(cx); refresh_library_after_delete(cx); crate::menus::recheck_pending_quit(cx); }); @@ -1460,6 +1462,7 @@ pub fn open_onboarding(cx: &mut App) { .detach(); hide_main_and_park_camera_preview(cx); crate::tray::refresh_menu(cx); + crate::menus::refresh(cx); } pub fn onboarding_finished(cx: &mut App) { @@ -1470,6 +1473,7 @@ pub fn onboarding_finished(cx: &mut App) { .ok(); } crate::tray::refresh_menu(cx); + crate::menus::refresh(cx); show_main_window(cx); } @@ -1486,6 +1490,7 @@ pub fn onboarding_closed(cx: &mut App) { crate::permission_guide::stop(cx); cx.global_mut::().onboarding.take(); crate::tray::refresh_menu(cx); + crate::menus::refresh(cx); if !crate::store::should_show_onboarding() { show_main_window(cx); } @@ -1917,7 +1922,7 @@ fn reopens_overlays(armed: Option, mode: TargetType, recording_mode: pub(crate) fn reject_unavailable_window(cx: &mut App) { dismiss_target_overlays(cx); RecordingSession::global(cx).update(cx, |session, cx| { - session.error = Some("The selected window is no longer available. Select it again.".into()); + session.error = Some(tr::dialog::capture::window_unavailable().into()); cx.notify(); }); } @@ -2406,9 +2411,7 @@ pub fn start_recording_from_overlay(target: ScreenCaptureTarget, cx: &mut App) { let Some(handle) = handle else { dismiss_target_overlays(cx); RecordingSession::global(cx).update(cx, |session, cx| { - session.error = Some( - "The selected area changed or is no longer available. Select it again.".into(), - ); + session.error = Some(tr::dialog::capture::area_unavailable().into()); cx.notify(); }); return; @@ -3093,7 +3096,7 @@ fn linux_camera_presentation( && y >= 0 && x + i64::from(rect.width) <= i64::from(capture.width) && y + i64::from(rect.height) <= i64::from(capture.height), - "Move the whole camera preview inside the capture area before starting Instant recording" + tr::dialog::capture::camera_outside_area() ); let radius = snapshot.corner_radius_pixels.round(); anyhow::ensure!( @@ -3147,11 +3150,12 @@ pub(crate) fn refresh_linux_instant_camera( } anyhow::ensure!( !cap_recording::screenshot::uses_wayland_portal(), - "Instant camera overlays require an X11 desktop on Linux" + tr::dialog::capture::camera_needs_x11() ); - let camera = cx.global::().camera.ok_or_else(|| { - anyhow::anyhow!("Open the selected camera preview before Instant recording") - })?; + let camera = cx + .global::() + .camera + .ok_or_else(|| anyhow::anyhow!(tr::dialog::capture::camera_preview_closed()))?; let snapshot = camera.update(cx, |camera, window, _| camera.recording_snapshot(window))??; let capture = linux_capture_bounds(&config.target)?; let presentation = linux_camera_presentation(snapshot, capture)?; @@ -3183,11 +3187,11 @@ pub(crate) fn clean_capture_camera_message(cx: &App) -> &'static str { .and_then(|lease| lease.config.as_ref()) .is_some_and(|config| config.mode == RecordingMode::Instant) { - return "Cap will hide its preview and controls. Any selected camera will appear at its preview position with the selected shape and effects. Keep the whole preview inside the capture area. Opening Cap will stop this Instant recording before showing its controls."; + return tr::dialog::capture::camera_notice_instant(); } #[cfg(not(target_os = "linux"))] let _ = cx; - "Cap will hide its camera preview and recording controls. Any selected camera will keep recording as a separate editable track." + tr::dialog::capture::camera_notice() } pub fn clean_capture_owned(cx: &App) -> bool { @@ -3695,7 +3699,7 @@ fn begin_retained_capture_restore(cx: &mut App) { } tracing::error!(%error, "Recording UI restoration or Stop cleanup is unconfirmed"); RecordingSession::global(cx).update(cx, |session, cx| { - session.error = Some(format!("Recording cleanup is incomplete: {error}. A new recording is blocked.")); + session.error = Some(tr::dialog::capture::cleanup_incomplete(error)); cx.notify(); }); } @@ -3786,12 +3790,7 @@ pub fn handle_clean_capture_shortcut(state: global_hotkey::HotKeyState, cx: &mut .is_none_or(|lease| lease.requested_inputs != current_inputs) { restore_clean_capture_ui(cx); - report_clean_capture_error( - anyhow::anyhow!( - "Selected devices changed. Start again to confirm the new recording inputs." - ), - cx, - ); + report_clean_capture_error(anyhow::anyhow!(tr::dialog::capture::devices_changed()), cx); return; } if let Err(error) = refresh_linux_instant_camera(&mut config, cx) { @@ -3812,7 +3811,10 @@ pub fn handle_clean_capture_shortcut(state: global_hotkey::HotKeyState, cx: &mut }; let visibility = gpui_tokio::Tokio::spawn(cx, visibility); cx.spawn(async move |cx| { - let result = visibility.await.map_err(anyhow::Error::from).and_then(|result| result); + let result = visibility + .await + .map_err(anyhow::Error::from) + .and_then(|result| result); if let Err(error) = result { cx.update(|cx| { if clean_capture_generation_matches(generation, cx) { @@ -3837,7 +3839,7 @@ pub fn handle_clean_capture_shortcut(state: global_hotkey::HotKeyState, cx: &mut { restore_clean_capture_ui(cx); report_clean_capture_error( - anyhow::anyhow!("Selected devices changed before recording started. Start again to confirm the inputs."), + anyhow::anyhow!(tr::dialog::capture::devices_changed_before_start()), cx, ); return; @@ -3870,9 +3872,7 @@ pub fn begin_recording(mut config: StartConfig, cx: &mut App) { if let Some(lease) = cx.global::().clean_capture.as_ref() { if let Some(error) = lease.restoration_error.clone() { session.update(cx, |session, cx| { - session.error = Some(format!( - "Recording cleanup is incomplete: {error}. A new recording is blocked." - )); + session.error = Some(tr::dialog::capture::cleanup_incomplete(error)); cx.notify(); }); } @@ -4888,6 +4888,8 @@ fn open_editor_window( return None; } + cx.add_recent_document(&project_path); + if let Some(window_id) = cx .global::() .editors @@ -5469,11 +5471,7 @@ fn load_editor_project_candidate( Err(join_error) => { handle .update(cx, |view, window, cx| { - view.set_error( - format!("Opening this recording failed: {join_error}"), - window, - cx, - ) + view.set_error(tr::dialog::open_recording_failed(join_error), window, cx) }) .ok(); return; @@ -6255,34 +6253,29 @@ fn editor_deletion_allowed( root.canonicalize() .is_ok_and(|root| project_path != root && project_path.starts_with(root)) }) { - return Err( - "This recording is outside the recordings library. Its files were not changed.".into(), - ); + return Err(tr::dialog::delete_recording::outside_library().into()); } let windows = cx.global::(); if windows.deleting_editors.contains(project_path) { - return Err("This recording is already being deleted.".into()); + return Err(tr::dialog::delete_recording::already_deleting().into()); } if !windows .editors .iter() .any(|(path, current)| path == project_path && *current == handle) { - return Err( - "The editor changed while deletion was being confirmed. The recording was not deleted." - .into(), - ); + return Err(tr::dialog::delete_recording::editor_changed().into()); } let session = RecordingSession::global(cx); if session.read(cx).phase != Phase::Idle || session.read(cx).editor_recording_target().is_some() || clean_capture_owned(cx) { - return Err("Finish or cancel the recording before deleting this project.".into()); + return Err(tr::dialog::delete_recording::recording_active().into()); } let editor = handle .read(cx) - .map_err(|_| "The editor has already closed. The recording was not deleted.")?; + .map_err(|_| tr::dialog::delete_recording::editor_closed())?; if let Some(reason) = editor.deletion_blocked_reason() { return Err(reason.into()); } @@ -6297,16 +6290,16 @@ pub(crate) fn request_editor_deletion( let key = editor_key(&project_path); cx.spawn(async move |cx| { if let Err(error) = cx.update(|cx| editor_deletion_allowed(&key, handle, cx)) { - platform::alert_dialog("Recording retained", &error); + platform::alert_dialog(tr::dialog::delete_recording::retained_title(), &error); return; } - if !platform::confirm_dialog("Cap", "Are you sure you want to delete this recording?", "Yes", "No", false) { + if !platform::confirm_dialog("Cap", tr::main::delete_recording_confirm(), tr::common::yes(), tr::common::no(), false) { return; } let prepared = cx.update(|cx| { editor_deletion_allowed(&key, handle, cx)?; let ownership = cap_recording::upload_resume::UploadLock::acquire(&key) - .map_err(|error| format!("This recording is still in use. Finish or cancel its upload and try again: {error}"))?; + .map_err(tr::dialog::delete_recording::in_use)?; let windows = cx.global_mut::(); windows.deleting_editors.insert(key.clone()); windows.editors.retain(|(_, current)| *current != handle); @@ -6328,14 +6321,14 @@ pub(crate) fn request_editor_deletion( } Err(error) => { cx.global_mut::().deleting_editors.remove(&key); - Err(format!("Could not close the editor: {error}")) + Err(tr::dialog::delete_recording::close_failed(error)) } } }); let (instance, ownership) = match prepared { Ok(prepared) => prepared, Err(error) => { - platform::alert_dialog("Recording retained", &error); + platform::alert_dialog(tr::dialog::delete_recording::retained_title(), &error); return; } }; @@ -6364,17 +6357,17 @@ pub(crate) fn request_editor_deletion( let result = match futures_util::future::select(&mut task, &mut notice).await { futures_util::future::Either::Left((result, _)) => result, futures_util::future::Either::Right(_) => { - platform::alert_dialog("Deletion is still finishing", "Cap is waiting for recording cleanup to finish before completing this deletion."); + platform::alert_dialog(tr::dialog::delete_recording::finishing_title(), tr::dialog::delete_recording::finishing_body()); task.await } - }.unwrap_or_else(|error| Err(format!("Recording deletion failed: {error}"))); + }.unwrap_or_else(|error| Err(tr::dialog::delete_recording::failed(error))); cx.update(|cx| { cx.global_mut::().deleting_editors.remove(&key); refresh_library_after_delete(cx); }); if let Err(error) = result { tracing::error!(path = %key.display(), %error, "recording deletion failed"); - platform::alert_dialog("Could not delete recording", &error); + platform::alert_dialog(tr::dialog::delete_recording::failed_title(), &error); } }).detach(); } @@ -6865,6 +6858,8 @@ pub fn open_screenshot_editor(path: PathBuf, cx: &mut App) { }; let key = editor_key(&bundle); + cx.add_recent_document(&bundle); + if let Some(handle) = cx .global::() .screenshot_editors diff --git a/apps/desktop-gpui/src/deeplink.rs b/apps/desktop-gpui/src/deeplink.rs index 8a1523477da..796c55b7ca5 100644 --- a/apps/desktop-gpui/src/deeplink.rs +++ b/apps/desktop-gpui/src/deeplink.rs @@ -540,6 +540,11 @@ impl DeepLinkAction { DeepLinkAction::OpenEditor { project_path } => { use cap_project::{RecordingMeta, RecordingMetaInner, StudioRecordingStatus}; + if crate::library::is_screenshot_bundle(&project_path) { + cx.defer(move |cx| app_windows::open_screenshot_editor(project_path, cx)); + return Ok(()); + } + let meta = RecordingMeta::load_for_project(&project_path).map_err(|v| v.to_string())?; match &meta.inner { @@ -606,7 +611,9 @@ fn camera_selection(id: DeviceOrModelID) -> SelectedCamera { .map(|info| info.display_name().to_string()) .unwrap_or_else(|| match &id { DeviceOrModelID::DeviceID(device) => device.clone(), - DeviceOrModelID::ModelID(_) => "Camera".to_string(), + DeviceOrModelID::ModelID(_) => { + crate::i18n::tr::main::device::fallback_camera().to_string() + } }), id, } diff --git a/apps/desktop-gpui/src/devices.rs b/apps/desktop-gpui/src/devices.rs index 745d9d46bc0..19305cd8184 100644 --- a/apps/desktop-gpui/src/devices.rs +++ b/apps/desktop-gpui/src/devices.rs @@ -63,8 +63,8 @@ impl MicrophoneOption { let sample_rate = self.sample_rate?; let khz = sample_rate as f32 / 1000.; let layout = match self.channels { - Some(1) => "Mono".to_string(), - Some(2) => "Stereo".to_string(), + Some(1) => crate::i18n::tr::main::device::format::mono().to_string(), + Some(2) => crate::i18n::tr::main::device::format::stereo().to_string(), Some(n) => format!("{n}ch"), None => return Some(format!("{khz}kHz")), }; diff --git a/apps/desktop-gpui/src/diagnostics.rs b/apps/desktop-gpui/src/diagnostics.rs index af250bff901..3e685de80a2 100644 --- a/apps/desktop-gpui/src/diagnostics.rs +++ b/apps/desktop-gpui/src/diagnostics.rs @@ -29,6 +29,7 @@ use std::{ use serde_json::{Value, json}; +use crate::i18n::tr; use crate::{permissions, store}; /// The rolling log file's prefix. Deliberately *not* the Tauri app's @@ -261,7 +262,7 @@ impl SyncMode { match self { Self::Studio => "Studio", Self::Instant => "Instant", - Self::Both => "Both", + Self::Both => tr::diagnostics::mode_both(), } } } @@ -325,7 +326,7 @@ impl RunGuard { DIAGNOSTIC_RUNNING .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) .map(|_| Self) - .map_err(|_| ALREADY_RUNNING.to_string()) + .map_err(|_| tr::diagnostics::already_running().to_string()) } } @@ -335,8 +336,6 @@ impl Drop for RunGuard { } } -pub const ALREADY_RUNNING: &str = "A diagnostic is already running."; - /// Kills the self-test if it is dropped before the run's own kill/wait /// sequence. `std::process::Child` does not kill on drop, and an orphaned /// self-test owns the screen with a fullscreen flashing window while it is @@ -409,24 +408,51 @@ pub const CANCEL_STAGE: &str = "cancelling"; /// The user-facing words for a stage. The CLI's stage names are stable API but /// they are not sentences. pub fn stage_label(stage: &str, mode: Option<&str>) -> String { - let leg = match mode { - Some("studio") => " studio", - Some("instant") => " instant", - _ => "", + let leg = |both: fn() -> &'static str, + studio: fn() -> &'static str, + instant: fn() -> &'static str| match mode { + Some("studio") => studio(), + Some("instant") => instant(), + _ => both(), }; match stage { - "collecting" => format!("Preparing the{leg} test..."), - "recording" => format!("Recording the{leg} test..."), - "pattern-run" => format!("Playing the test pattern{leg}..."), - "remuxing" => format!("Finalizing the{leg} recording..."), - "analyzing" => format!("Analyzing the{leg} recording..."), - "exporting" => format!("Exporting the{leg} recording..."), - "done" => "Wrapping up the sync test...".to_string(), - START_STAGE => "Starting the diagnostic...".to_string(), - COLLECT_STAGE => "Collecting system information...".to_string(), - CANCEL_STAGE => "Cancelling...".to_string(), - other => format!("{other}..."), + "collecting" => leg( + tr::diagnostics::stage::preparing, + tr::diagnostics::stage::preparing_studio, + tr::diagnostics::stage::preparing_instant, + ), + "recording" => leg( + tr::diagnostics::stage::recording, + tr::diagnostics::stage::recording_studio, + tr::diagnostics::stage::recording_instant, + ), + "pattern-run" => leg( + tr::diagnostics::stage::pattern, + tr::diagnostics::stage::pattern_studio, + tr::diagnostics::stage::pattern_instant, + ), + "remuxing" => leg( + tr::diagnostics::stage::finalizing, + tr::diagnostics::stage::finalizing_studio, + tr::diagnostics::stage::finalizing_instant, + ), + "analyzing" => leg( + tr::diagnostics::stage::analyzing, + tr::diagnostics::stage::analyzing_studio, + tr::diagnostics::stage::analyzing_instant, + ), + "exporting" => leg( + tr::diagnostics::stage::exporting, + tr::diagnostics::stage::exporting_studio, + tr::diagnostics::stage::exporting_instant, + ), + "done" => tr::diagnostics::stage::done(), + START_STAGE => tr::diagnostics::stage::starting(), + COLLECT_STAGE => tr::diagnostics::stage::collecting(), + CANCEL_STAGE => tr::diagnostics::stage::cancelling(), + other => return format!("{other}..."), } + .to_string() } fn trim_to_tail(text: &mut String, max_bytes: usize) { @@ -475,14 +501,14 @@ pub fn run_sync_test( .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() - .map_err(|error| format!("Failed to start {}: {error}", binary.display()))?, + .map_err(|error| tr::diagnostics::error::start_failed(binary.display(), error))?, ); let stdout = child .0 .stdout .take() - .ok_or_else(|| "The self-test produced no output stream".to_string())?; + .ok_or_else(|| tr::diagnostics::error::no_output().to_string())?; let stderr = child.0.stderr.take(); let stderr_tail = Arc::new(Mutex::new(String::new())); @@ -553,8 +579,8 @@ pub fn run_sync_test( .map(|tail| tail.trim().to_string()) .unwrap_or_default(); let status = match status { - Ok(status) => format!("The self-test exited with {status}"), - Err(error) => format!("The self-test could not be waited on: {error}"), + Ok(status) => tr::diagnostics::error::exited(status), + Err(error) => tr::diagnostics::error::wait_failed(error), }; Err( [Some(status), error_line, (!tail.is_empty()).then_some(tail)] @@ -824,8 +850,7 @@ fn write_report_into(dir: &Path, report: &Value) -> Result { } fn write_report_at(dir: &Path, report: &Value, stamp: u64) -> Result { - std::fs::create_dir_all(dir) - .map_err(|error| format!("Failed to create the diagnostics folder: {error}"))?; + std::fs::create_dir_all(dir).map_err(tr::diagnostics::error::create_folder)?; // A run with no sidecar finishes in well under a second, so two of them // would otherwise write the same name and the second would overwrite the // first. The prefix and suffix stay exactly as the pruning filter expects. @@ -834,10 +859,9 @@ fn write_report_at(dir: &Path, report: &Value, stamp: u64) -> Result bool { hotkeys.paused = true; hotkeys.release_idle_manager(); tracing::info!("global hotkeys paused while a shortcut is recorded"); + crate::tray::refresh_menu(cx); true } @@ -245,6 +247,16 @@ pub fn resume(cx: &mut App) -> bool { true } +/// The registered combo for an action, or `None` when nothing is bound. The +/// tray menu renders it as the row's accelerator. +pub(crate) fn accelerator(action: HotkeyAction, cx: &App) -> Option { + cx.try_global::()? + .bindings + .iter() + .find(|(_, bound)| *bound == action) + .map(|(hotkey, _)| *hotkey) +} + pub fn unavailable_actions(cx: &App) -> Vec<&'static str> { cx.try_global::() .map(|hotkeys| { @@ -309,15 +321,12 @@ pub fn clean_capture_stop_message(cx: &App) -> String { if let Some(stop) = cx.try_global::() { return stop.label.clone(); } - "Press and release Ctrl+Shift+F9 to start. Use the same shortcut to stop. This temporary shortcut does not change your settings.".into() + tr::dialog::capture::shortcut_hint("Ctrl+Shift+F9") } #[cfg(target_os = "linux")] fn clean_capture_shortcut_label(trigger: &str) -> String { - let trigger = readable_trigger(trigger); - format!( - "Press and release {trigger} to start. Use the same shortcut to stop. This temporary shortcut does not change your settings." - ) + tr::dialog::capture::shortcut_hint(readable_trigger(trigger)) } /// Portal backends describe triggers in their own words: GNOME says @@ -391,7 +400,7 @@ fn reserve_wayland_stop(generation: u64, cx: &mut App) -> anyhow::Result<()> { gpui_tokio::Tokio::handle(cx).spawn(run_wayland_stop(generation, cancelled, events)); cx.set_global(WaylandStop { generation, - label: "Approve a recording shortcut, or activate the Cap Stop tray icon when it appears. Recording has not started.".into(), + label: tr::dialog::capture::approve_shortcut().into(), ready: false, cancel, worker: Some(worker), @@ -564,7 +573,7 @@ async fn run_tray_stop( })?; let received = tray.events(); let outcome = async { - events.send(WaylandStopEvent::Ready("Activate the Cap Stop tray icon to start. Activate the same icon again to stop. Recording will not start until you use that control.".into()))?; + events.send(WaylandStopEvent::Ready(tr::dialog::capture::tray_stop_hint().into()))?; loop { tokio::select! { biased; @@ -608,7 +617,7 @@ async fn run_portal_stop( let changed = portal.receive_shortcuts_changed().await?; let closed = session.receive_closed().await?; futures_util::pin_mut!(activated, deactivated, changed, closed); - let shortcuts = [NewShortcut::new(&shortcut_id, "Start or stop this Cap recording").preferred_trigger("CTRL+SHIFT+F9")]; + let shortcuts = [NewShortcut::new(&shortcut_id, tr::dialog::capture::shortcut_description()).preferred_trigger("CTRL+SHIFT+F9")]; let bind = portal.bind_shortcuts(&session, &shortcuts, None); let response = tokio::select! { _ = cancel.recv_async() => return Ok(()), @@ -675,9 +684,7 @@ pub fn reserve_clean_capture_stop( let key = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9); let actions = actions_for(&hotkeys.bindings, key.id()); if clean_capture_shortcut_conflicts(&actions) { - anyhow::bail!( - "Ctrl+Shift+F9 is assigned to another Cap action. Change that shortcut before recording." - ); + anyhow::bail!(tr::dialog::capture::shortcut_conflict()); } if actions.is_empty() { let registered = hotkeys @@ -801,6 +808,7 @@ fn register_from_store(cx: &mut App) { } hotkeys.release_idle_manager(); tracing::info!(count = hotkeys.bindings.len(), "global hotkeys registered"); + crate::tray::refresh_menu(cx); } /// `CAP_GPUI_AUTO_HOTKEY=`: run an action's dispatch arm without @@ -987,11 +995,11 @@ fn start_from_hotkey(mode: Mode, cx: &mut App) { main.update(cx, |_, window, cx| { window.prompt( PromptLevel::Warning, - "No microphone detected", - Some("This recording will not include your voice."), + tr::main::mic_warning::title(), + Some(tr::dialog::no_microphone_body()), &[ - PromptButton::ok("Record without microphone"), - PromptButton::cancel("Cancel"), + PromptButton::ok(tr::main::mic_warning::record_without()), + PromptButton::cancel(tr::common::cancel()), ], cx, ) diff --git a/apps/desktop-gpui/src/i18n/dialogs_surface_tests.rs b/apps/desktop-gpui/src/i18n/dialogs_surface_tests.rs new file mode 100644 index 00000000000..1cd744f95c6 --- /dev/null +++ b/apps/desktop-gpui/src/i18n/dialogs_surface_tests.rs @@ -0,0 +1,996 @@ +use super::*; + +pub(super) fn is_dialogs_surface_key(key: &str) -> bool { + [ + "auth.", + "diagnostics.", + "dialog.", + "import.", + "overlay.", + "permission.", + "screenshot_editor.", + "transcription.", + "upload.", + ] + .iter() + .any(|prefix| key.starts_with(prefix)) +} + +const FORMATTED: &[&str] = &[ + "diagnostics.error.create_folder", + "diagnostics.error.exited", + "diagnostics.error.serialize", + "diagnostics.error.start_failed", + "diagnostics.error.upload_logs", + "diagnostics.error.upload_status", + "diagnostics.error.wait_failed", + "diagnostics.error.write_failed", + "dialog.open_recording_failed", + "dialog.delete_recording.close_failed", + "dialog.delete_recording.failed", + "dialog.delete_recording.in_use", + "dialog.capture.cleanup_incomplete", + "dialog.capture.shortcut_hint", + "dialog.clipboard.copy_file", + "dialog.clipboard.copy_image", + "dialog.clipboard.unavailable_error", + "dialog.file.failed", + "import.image_failed", + "import.video_failed", + "import.progress.converting", + "overlay.display_fallback", + "overlay.area.minimum_size", + "overlay.area.too_small", + "screenshot_editor.error.encode", + "screenshot_editor.error.load_meta", + "screenshot_editor.error.load_metadata", + "screenshot_editor.error.no_image", + "screenshot_editor.error.open_image", + "screenshot_editor.error.renderer_failed", + "screenshot_editor.error.save_project", + "screenshot_editor.error.serialize", + "screenshot_editor.error.too_large", + "screenshot_editor.error.upload_task", + "transcription.download.failed", + "transcription.download.file_progress", + "transcription.download.progress", +]; + +#[test] +fn dialog_surfaces_match_the_literals_they_replaced() { + let pairs: &[(&str, &str)] = &[ + ( + tr::diagnostics::already_running(), + "A diagnostic is already running.", + ), + (tr::diagnostics::mode_both(), "Both"), + ( + tr::diagnostics::error::no_output(), + "The self-test produced no output stream", + ), + ( + tr::diagnostics::stage::analyzing(), + "Analyzing the recording...", + ), + ( + tr::diagnostics::stage::analyzing_instant(), + "Analyzing the instant recording...", + ), + ( + tr::diagnostics::stage::analyzing_studio(), + "Analyzing the studio recording...", + ), + (tr::diagnostics::stage::cancelling(), "Cancelling..."), + ( + tr::diagnostics::stage::collecting(), + "Collecting system information...", + ), + ( + tr::diagnostics::stage::done(), + "Wrapping up the sync test...", + ), + ( + tr::diagnostics::stage::exporting(), + "Exporting the recording...", + ), + ( + tr::diagnostics::stage::exporting_instant(), + "Exporting the instant recording...", + ), + ( + tr::diagnostics::stage::exporting_studio(), + "Exporting the studio recording...", + ), + ( + tr::diagnostics::stage::finalizing(), + "Finalizing the recording...", + ), + ( + tr::diagnostics::stage::finalizing_instant(), + "Finalizing the instant recording...", + ), + ( + tr::diagnostics::stage::finalizing_studio(), + "Finalizing the studio recording...", + ), + ( + tr::diagnostics::stage::pattern(), + "Playing the test pattern...", + ), + ( + tr::diagnostics::stage::pattern_instant(), + "Playing the test pattern instant...", + ), + ( + tr::diagnostics::stage::pattern_studio(), + "Playing the test pattern studio...", + ), + (tr::diagnostics::stage::preparing(), "Preparing the test..."), + ( + tr::diagnostics::stage::preparing_instant(), + "Preparing the instant test...", + ), + ( + tr::diagnostics::stage::preparing_studio(), + "Preparing the studio test...", + ), + (tr::diagnostics::stage::recording(), "Recording the test..."), + ( + tr::diagnostics::stage::recording_instant(), + "Recording the instant test...", + ), + ( + tr::diagnostics::stage::recording_studio(), + "Recording the studio test...", + ), + ( + tr::diagnostics::stage::starting(), + "Starting the diagnostic...", + ), + ( + tr::dialog::no_microphone_body(), + "This recording will not include your voice.", + ), + ( + tr::dialog::delete_recording::already_deleting(), + "This recording is already being deleted.", + ), + ( + tr::dialog::delete_recording::editor_changed(), + "The editor changed while deletion was being confirmed. The recording was not deleted.", + ), + ( + tr::dialog::delete_recording::editor_closed(), + "The editor has already closed. The recording was not deleted.", + ), + ( + tr::dialog::delete_recording::failed_title(), + "Could not delete recording", + ), + ( + tr::dialog::delete_recording::finishing_body(), + "Cap is waiting for recording cleanup to finish before completing this deletion.", + ), + ( + tr::dialog::delete_recording::finishing_title(), + "Deletion is still finishing", + ), + ( + tr::dialog::delete_recording::outside_library(), + "This recording is outside the recordings library. Its files were not changed.", + ), + ( + tr::dialog::delete_recording::recording_active(), + "Finish or cancel the recording before deleting this project.", + ), + ( + tr::dialog::delete_recording::retained_title(), + "Recording retained", + ), + ( + tr::dialog::capture::approve_shortcut(), + "Approve a recording shortcut, or activate the Cap Stop tray icon when it appears. Recording has not started.", + ), + ( + tr::dialog::capture::area_unavailable(), + "The selected area changed or is no longer available. Select it again.", + ), + ( + tr::dialog::capture::camera_needs_x11(), + "Instant camera overlays require an X11 desktop on Linux", + ), + ( + tr::dialog::capture::camera_notice(), + "Cap will hide its camera preview and recording controls. Any selected camera will keep recording as a separate editable track.", + ), + ( + tr::dialog::capture::camera_notice_instant(), + "Cap will hide its preview and controls. Any selected camera will appear at its preview position with the selected shape and effects. Keep the whole preview inside the capture area. Opening Cap will stop this Instant recording before showing its controls.", + ), + ( + tr::dialog::capture::camera_outside_area(), + "Move the whole camera preview inside the capture area before starting Instant recording", + ), + ( + tr::dialog::capture::camera_preview_closed(), + "Open the selected camera preview before Instant recording", + ), + ( + tr::dialog::capture::devices_changed(), + "Selected devices changed. Start again to confirm the new recording inputs.", + ), + ( + tr::dialog::capture::devices_changed_before_start(), + "Selected devices changed before recording started. Start again to confirm the inputs.", + ), + ( + tr::dialog::capture::shortcut_conflict(), + "Ctrl+Shift+F9 is assigned to another Cap action. Change that shortcut before recording.", + ), + ( + tr::dialog::capture::shortcut_description(), + "Start or stop this Cap recording", + ), + ( + tr::dialog::capture::tray_stop_hint(), + "Activate the Cap Stop tray icon to start. Activate the same icon again to stop. Recording will not start until you use that control.", + ), + ( + tr::dialog::capture::window_unavailable(), + "The selected window is no longer available. Select it again.", + ), + ( + tr::dialog::clipboard::copy_file_failed(), + "Failed to copy file to clipboard", + ), + ( + tr::dialog::clipboard::copy_image_failed(), + "Failed to copy image to clipboard", + ), + ( + tr::dialog::clipboard::not_a_file(), + "Only files can be copied to the clipboard", + ), + ( + tr::dialog::clipboard::unavailable(), + "Clipboard unavailable", + ), + (tr::dialog::file::audio_filter(), "Audio"), + ( + tr::dialog::file::busy(), + "A file dialog is already open. Finish or cancel it before trying again.", + ), + (tr::dialog::file::export_filter(), "Export"), + (tr::dialog::file::images_filter(), "Images"), + ( + tr::dialog::file::unavailable_title(), + "File dialog unavailable", + ), + ( + tr::dialog::file::unconfirmed(), + "Cap could not confirm that the file dialog closed. Close any remaining file dialog and restart Cap before trying again.", + ), + ( + tr::dialog::storage::stopped_kept(), + "Recording stopped because storage is low. Your recording files were kept. Free up space, then recover the recording below.", + ), + ( + tr::dialog::storage::stopped_saved(), + "Recording stopped because storage is low. Your recording was saved.", + ), + ( + tr::dialog::storage::stopping(), + "Low storage. Stopping and saving your recording…", + ), + (tr::dialog::teleprompter::cue_markers(), "Cue markers"), + ( + tr::dialog::teleprompter::hidden(), + "This window is hidden from Cap recordings", + ), + (tr::dialog::teleprompter::mirror(), "Mirror text"), + ( + tr::dialog::teleprompter::paused(), + "Scrolling paused with recording", + ), + ( + tr::dialog::teleprompter::placeholder(), + "Paste or type your script…", + ), + (tr::auth::sign_in_failed(), "Sign in failed"), + (tr::import::error_title(), "Import Error"), + (tr::import::unsupported(), "Unsupported media file."), + (tr::import::filter::image(), "Image Files"), + (tr::import::filter::media(), "Media Files"), + (tr::import::filter::video(), "Video Files"), + (tr::import::progress::analyzing(), "Analyzing video file..."), + (tr::import::progress::complete(), "Import complete!"), + (tr::import::progress::image(), "Importing image..."), + ( + tr::import::progress::metadata(), + "Creating project metadata...", + ), + (tr::import::progress::starting(), "Starting conversion..."), + (tr::upload::status::failed(), "Upload needs attention"), + (tr::upload::status::paused(), "Upload paused"), + (tr::upload::status::prepared(), "Upload prepared"), + (tr::upload::status::processing(), "Processing upload"), + (tr::upload::status::retrying(), "Upload will retry"), + (tr::upload::status::sign_in(), "Sign in to retry upload"), + (tr::upload::status::uploaded(), "Uploaded"), + (tr::upload::status::uploading(), "Uploading"), + (tr::overlay::choose_mode(), "Choose recording mode"), + (tr::overlay::countdown_tooltip(), "Recording countdown"), + (tr::overlay::area::draw(), "Draw an area"), + (tr::overlay::area::free_ratio(), "Free"), + ( + tr::overlay::area::hint(), + "Click and drag to select an area", + ), + ( + tr::overlay::camera_only::description(), + "Record using only your camera and microphone", + ), + ( + tr::overlay::camera_only::select_camera(), + "Please select a camera", + ), + (tr::overlay::device::camera_allow(), "Allow camera access…"), + ( + tr::overlay::device::camera_turn_on(), + "Turn on camera access in Settings…", + ), + ( + tr::overlay::device::microphone_allow(), + "Allow microphone access…", + ), + ( + tr::overlay::device::microphone_turn_on(), + "Turn on microphone access in Settings…", + ), + ( + tr::overlay::start::instant(), + "Instant Mode — Record shareable link", + ), + ( + tr::overlay::start::instant_sign_in(), + "Instant Mode — Sign in to record instantly", + ), + (tr::overlay::start::sign_in(), "Sign In To Use"), + (tr::overlay::start::start_recording(), "Start Recording"), + ( + tr::overlay::start::studio(), + "Studio Mode — Record and edit locally", + ), + (tr::overlay::start::take_screenshot(), "Take Screenshot"), + ( + tr::dialog::mode_select::instant(), + "Share instantly with a link. Uploads as you record.", + ), + ( + tr::dialog::mode_select::screenshot(), + "Capture and annotate screenshots instantly.", + ), + ( + tr::dialog::mode_select::studio(), + "Highest quality local recording for editing later.", + ), + ( + tr::dialog::mode_select::subtitle(), + "Select how you want to capture your screen", + ), + (tr::dialog::mode_select::title(), "Choose Recording Mode"), + ( + tr::permission::onboarding::open_settings_again(), + "Open System Settings again", + ), + (tr::permission::onboarding::skip(), "Skip for now"), + ( + tr::permission::onboarding::switch_on_title(), + "Switch Cap on in System Settings", + ), + ( + tr::permission::onboarding::accessibility::allow(), + "Allow accessibility", + ), + ( + tr::permission::onboarding::accessibility::body(), + "Accessibility lets Cap see your clicks and cursor, on this Mac only, so auto zoom knows where to look.", + ), + ( + tr::permission::onboarding::accessibility::done_body(), + "Auto zoom can follow your clicks.", + ), + ( + tr::permission::onboarding::accessibility::done_title(), + "Accessibility is on", + ), + ( + tr::permission::onboarding::accessibility::title(), + "Let Cap follow your cursor", + ), + ( + tr::permission::onboarding::all_set::body(), + "Cap has everything it needs. Next, a quick look at what it can do.", + ), + ( + tr::permission::onboarding::all_set::regained(), + "Thanks. Cap can record again.", + ), + ( + tr::permission::onboarding::all_set::title(), + "You're all set", + ), + (tr::permission::onboarding::camera::allow(), "Allow camera"), + ( + tr::permission::onboarding::camera::body(), + "Show yourself in a corner of your recordings. Skip it if you don't need it.", + ), + ( + tr::permission::onboarding::camera::done_body(), + "You can add your camera to any recording.", + ), + ( + tr::permission::onboarding::camera::done_title(), + "Camera is on", + ), + ( + tr::permission::onboarding::camera::title(), + "Let Cap see you", + ), + ( + tr::permission::onboarding::checklist::lede(), + "macOS asks you to OK a few things first. It only takes a minute.", + ), + ( + tr::permission::onboarding::checklist::regained_lede(), + "Something was switched off in System Settings. Turn it back on to keep recording.", + ), + ( + tr::permission::onboarding::checklist::regained_title(), + "Cap needs a permission back", + ), + ( + tr::permission::onboarding::checklist::title(), + "Let's get Cap ready", + ), + ( + tr::permission::onboarding::hint::drag_into_list(), + "Drag Cap from the panel under System Settings into the list. Already listed? Just switch it on.", + ), + ( + tr::permission::onboarding::hint::find_in_list(), + "Find Cap in the list and switch it on. If macOS asked first, choose Open System Settings.", + ), + ( + tr::permission::onboarding::hint::no_prompt(), + "No prompt showed up? Switch Cap on in System Settings.", + ), + ( + tr::permission::onboarding::hint::open_settings(), + "Open System Settings, find Cap in the list and switch it on.", + ), + ( + tr::permission::onboarding::hint::turned_off(), + "It was turned off earlier, so switch Cap on in System Settings. Or skip it for now.", + ), + ( + tr::permission::onboarding::microphone::allow(), + "Allow microphone", + ), + ( + tr::permission::onboarding::microphone::body(), + "Add your voice to your recordings. You can skip this and turn it on later.", + ), + ( + tr::permission::onboarding::microphone::done_body(), + "Your voice can be in your recordings.", + ), + ( + tr::permission::onboarding::microphone::done_title(), + "Microphone is on", + ), + ( + tr::permission::onboarding::microphone::title(), + "Let Cap hear you", + ), + ( + tr::permission::onboarding::relaunch::body(), + "macOS turns on screen recording once Cap restarts. You'll pick up right here.", + ), + (tr::permission::onboarding::relaunch::button(), "Relaunch"), + ( + tr::permission::onboarding::relaunch::hint(), + "Switched on but still waiting? Restart Cap to apply it.", + ), + ( + tr::permission::onboarding::relaunch::later(), + "macOS applies it after Cap restarts. You can do that once you're done here.", + ), + ( + tr::permission::onboarding::relaunch::title(), + "One restart and you're set", + ), + ( + tr::permission::onboarding::screen::allow(), + "Allow screen recording", + ), + ( + tr::permission::onboarding::screen::body(), + "Screen Recording lets Cap capture your displays, windows and system audio.", + ), + ( + tr::permission::onboarding::screen::done_body(), + "Cap can capture your screen.", + ), + ( + tr::permission::onboarding::screen::done_title(), + "Screen recording is on", + ), + ( + tr::permission::onboarding::screen::title(), + "Let Cap see your screen", + ), + (tr::permission::onboarding::status::allowed(), "Allowed"), + ( + tr::permission::onboarding::status::off(), + "Off in System Settings", + ), + (tr::permission::onboarding::status::optional(), "Optional"), + (tr::permission::onboarding::status::required(), "Required"), + ( + tr::permission::onboarding::status::skipped(), + "Skipped for now", + ), + ( + tr::permission::onboarding::waiting::prompt(), + "Choose Allow in the macOS prompt", + ), + ( + tr::permission::onboarding::waiting::system_settings(), + "Waiting for System Settings", + ), + ( + tr::permission::guide::accessibility_body(), + "Drop it on the Accessibility list above, then make sure its switch is on.", + ), + ( + tr::permission::guide::screen_body(), + "Drop it on the Screen & System Audio Recording list above, then make sure its switch is on.", + ), + (tr::permission::guide::title(), "Drag Cap into the list"), + (tr::permission::name::accessibility(), "Accessibility"), + (tr::permission::name::camera(), "Camera"), + (tr::permission::name::microphone(), "Microphone"), + (tr::permission::name::screen_recording(), "Screen Recording"), + ( + tr::screenshot_editor::loading_preview(), + "Loading preview...", + ), + (tr::screenshot_editor::zoom_in(), "Zoom In"), + (tr::screenshot_editor::zoom_out(), "Zoom Out"), + (tr::screenshot_editor::annotation::default_text(), "Text"), + (tr::screenshot_editor::annotation::done(), "Done"), + (tr::screenshot_editor::annotation::fill(), "Fill"), + (tr::screenshot_editor::annotation::intensity(), "Intensity"), + (tr::screenshot_editor::annotation::mask_style(), "Style"), + (tr::screenshot_editor::annotation::pixelate(), "Pixelate"), + (tr::screenshot_editor::annotation::stroke(), "Stroke"), + ( + tr::screenshot_editor::annotation::transparent(), + "Transparent", + ), + (tr::screenshot_editor::aspect::auto(), "Auto"), + (tr::screenshot_editor::aspect::classic(), "Classic ⋅4:3"), + (tr::screenshot_editor::aspect::square(), "Square ⋅1:1"), + (tr::screenshot_editor::aspect::tall(), "Tall ⋅3:4"), + (tr::screenshot_editor::aspect::vertical(), "Vertical ⋅9:16"), + (tr::screenshot_editor::aspect::wide(), "Wide ⋅16:9"), + ( + tr::screenshot_editor::background::animated_gradient(), + "Animated Gradient is rendered as a still image for screenshots. Choose another background to replace it.", + ), + ( + tr::screenshot_editor::background::blur_title(), + "Background Blur", + ), + ( + tr::screenshot_editor::background::choose_image(), + "Click to select or drag and drop image", + ), + (tr::screenshot_editor::background::color(), "Color"), + (tr::screenshot_editor::background::gradient(), "Gradient"), + (tr::screenshot_editor::background::image(), "Image"), + (tr::screenshot_editor::background::image_files(), "Image"), + ( + tr::screenshot_editor::background::image_title(), + "Background Image", + ), + (tr::screenshot_editor::background::wallpaper(), "Wallpaper"), + (tr::screenshot_editor::corner::rounded(), "Rounded"), + (tr::screenshot_editor::corner::squircle(), "Squircle"), + (tr::screenshot_editor::corner::style(), "CORNER STYLE"), + (tr::screenshot_editor::crop::full(), "Full"), + ( + tr::screenshot_editor::crop::loading_frame(), + "Loading frame…", + ), + (tr::screenshot_editor::crop::position(), "Position"), + ( + tr::screenshot_editor::error::not_screenshot(), + "Not a screenshot bundle", + ), + ( + tr::screenshot_editor::error::renderer_not_running(), + "The screenshot renderer is not running", + ), + ( + tr::screenshot_editor::error::renderer_stopped(), + "The renderer stopped before the export finished", + ), + ( + tr::screenshot_editor::error::sign_in_to_share(), + "Sign in to Cap in your browser, then share again", + ), + ( + tr::screenshot_editor::error::upgrade_required(), + "This feature requires an upgraded plan", + ), + ( + tr::screenshot_editor::header::aspect_ratio(), + "Aspect Ratio", + ), + (tr::screenshot_editor::header::copy(), "Copy to Clipboard"), + (tr::screenshot_editor::header::crop_image(), "Crop Image"), + ( + tr::screenshot_editor::header::more_actions(), + "More Actions", + ), + (tr::screenshot_editor::layers::empty(), "No layers yet"), + ( + tr::screenshot_editor::layers::empty_hint(), + "Use the tools above to add annotations", + ), + ( + tr::screenshot_editor::layers::reorder_hint(), + "Drag to reorder • Top = front", + ), + (tr::screenshot_editor::layers::title(), "Layers"), + (tr::screenshot_editor::menu::delete(), "Delete"), + (tr::screenshot_editor::menu::open_folder(), "Open Folder"), + ( + tr::screenshot_editor::share::create_link(), + "Create shareable link", + ), + ( + tr::screenshot_editor::share::preparing(), + "Preparing upload", + ), + ( + tr::screenshot_editor::share::rendering(), + "Rendering screenshot", + ), + ( + tr::screenshot_editor::share::uploading(), + "Uploading screenshot", + ), + ( + tr::screenshot_editor::style::advanced_shadow(), + "Advanced shadow settings", + ), + (tr::screenshot_editor::style::background(), "Background"), + (tr::screenshot_editor::style::blur(), "Blur"), + (tr::screenshot_editor::style::border(), "Border"), + (tr::screenshot_editor::style::color(), "Color"), + ( + tr::screenshot_editor::style::corner_rounding(), + "Corner Rounding", + ), + (tr::screenshot_editor::style::opacity(), "Opacity"), + (tr::screenshot_editor::style::padding(), "Padding"), + (tr::screenshot_editor::style::rounding(), "Rounding"), + (tr::screenshot_editor::style::shadow(), "Shadow"), + (tr::screenshot_editor::style::size_label(), "Size"), + (tr::screenshot_editor::style::width(), "Width"), + ( + tr::screenshot_editor::toast::copied(), + "Screenshot copied to clipboard!", + ), + ( + tr::screenshot_editor::toast::link_copied(), + "Share link copied to clipboard", + ), + (tr::screenshot_editor::toast::saved(), "Screenshot saved!"), + (tr::screenshot_editor::tool::arrow(), "Arrow"), + (tr::screenshot_editor::tool::circle(), "Circle"), + (tr::screenshot_editor::tool::draw(), "Draw"), + (tr::screenshot_editor::tool::mask(), "Mask"), + (tr::screenshot_editor::tool::rectangle(), "Rectangle"), + (tr::screenshot_editor::tool::select(), "Select"), + (tr::screenshot_editor::tool::text_label(), "Text"), + (tr::transcription::download::complete(), "Download complete"), + ( + tr::transcription::download::preparing(), + "Preparing model download", + ), + ( + tr::transcription::error::intel_mac(), + "Parakeet models are not available on Intel Macs. Use a Whisper model instead", + ), + ( + tr::transcription::error::model_load(), + "Failed to load the caption model. Try downloading it again", + ), + ( + tr::transcription::error::model_missing(), + "Caption model not found. Please download it first", + ), + ( + tr::transcription::error::no_audio(), + "No audio found in the video file", + ), + ( + tr::transcription::error::unsupported_language(), + "The Recommended and High Accuracy models only transcribe English and 24 other European languages. Download the Small or Medium model to caption this language", + ), + ]; + for (translated, literal) in pairs { + assert_eq!(translated, literal); + } + + let plain_keys = KEYS + .iter() + .filter(|key| is_dialogs_surface_key(key)) + .count() + - FORMATTED.len(); + assert_eq!( + pairs.len(), + plain_keys, + "every plain key has a literal above" + ); + for key in FORMATTED { + assert!(KEYS.contains(key), "{key}"); + } +} + +fn old_stage_label(stage: &str, mode: Option<&str>) -> String { + let leg = match mode { + Some("studio") => " studio", + Some("instant") => " instant", + _ => "", + }; + match stage { + "collecting" => format!("Preparing the{leg} test..."), + "recording" => format!("Recording the{leg} test..."), + "pattern-run" => format!("Playing the test pattern{leg}..."), + "remuxing" => format!("Finalizing the{leg} recording..."), + "analyzing" => format!("Analyzing the{leg} recording..."), + "exporting" => format!("Exporting the{leg} recording..."), + "done" => "Wrapping up the sync test...".to_string(), + crate::diagnostics::START_STAGE => "Starting the diagnostic...".to_string(), + crate::diagnostics::COLLECT_STAGE => "Collecting system information...".to_string(), + crate::diagnostics::CANCEL_STAGE => "Cancelling...".to_string(), + other => format!("{other}..."), + } +} + +#[test] +fn assembled_dialog_messages_match_the_code_they_replaced() { + for stage in [ + "collecting", + "recording", + "pattern-run", + "remuxing", + "analyzing", + "exporting", + "done", + crate::diagnostics::START_STAGE, + crate::diagnostics::COLLECT_STAGE, + crate::diagnostics::CANCEL_STAGE, + "uploading", + ] { + for mode in [None, Some("studio"), Some("instant"), Some("both")] { + assert_eq!( + crate::diagnostics::stage_label(stage, mode), + old_stage_label(stage, mode), + "{stage} {mode:?}" + ); + } + } + for (body, pane) in [ + (tr::permission::guide::accessibility_body(), "Accessibility"), + ( + tr::permission::guide::screen_body(), + "Screen & System Audio Recording", + ), + ] { + assert_eq!( + body, + format!("Drop it on the {pane} list above, then make sure its switch is on.") + ); + } + let message = "Invalid data found when processing input"; + for (media, formatted) in [ + ("video", tr::import::video_failed(message)), + ("image", tr::import::image_failed(message)), + ] { + assert_eq!(formatted, format!("Failed to import {media}: {message}")); + } + assert_eq!( + tr::dialog::capture::shortcut_hint("Ctrl+Shift+F9"), + "Press and release Ctrl+Shift+F9 to start. Use the same shortcut to stop. This temporary shortcut does not change your settings." + ); + for permission in crate::permissions::OSPermission::ALL { + assert_eq!(permission.display_name(), permission.label()); + } +} + +#[test] +fn dialog_messages_format_like_the_code_they_replaced() { + let error = "permission denied (os error 13)"; + let path = std::path::Path::new("/tmp/Cap/report.json"); + let pairs: Vec<(String, String)> = vec![ + ( + tr::diagnostics::error::create_folder(error), + format!("Failed to create the diagnostics folder: {error}"), + ), + ( + tr::diagnostics::error::serialize(error), + format!("Failed to serialize the report: {error}"), + ), + ( + tr::diagnostics::error::start_failed(path.display(), error), + format!("Failed to start {}: {error}", path.display()), + ), + ( + tr::diagnostics::error::upload_logs(error), + format!("Failed to upload logs: {error}"), + ), + ( + tr::diagnostics::error::wait_failed(error), + format!("The self-test could not be waited on: {error}"), + ), + ( + tr::diagnostics::error::write_failed(path.display(), error), + format!("Failed to write {}: {error}", path.display()), + ), + ( + tr::dialog::open_recording_failed(error), + format!("Opening this recording failed: {error}"), + ), + ( + tr::dialog::delete_recording::close_failed(error), + format!("Could not close the editor: {error}"), + ), + ( + tr::dialog::delete_recording::failed(error), + format!("Recording deletion failed: {error}"), + ), + ( + tr::dialog::delete_recording::in_use(error), + format!( + "This recording is still in use. Finish or cancel its upload and try again: {error}" + ), + ), + ( + tr::dialog::capture::cleanup_incomplete(error), + format!("Recording cleanup is incomplete: {error}. A new recording is blocked."), + ), + ( + tr::dialog::capture::shortcut_hint("Super+F9"), + format!( + "Press and release {} to start. Use the same shortcut to stop. This temporary shortcut does not change your settings.", + "Super+F9" + ), + ), + ( + tr::dialog::clipboard::copy_file(error), + format!("Could not copy the file to the clipboard: {error}"), + ), + ( + tr::dialog::clipboard::copy_image(error), + format!("Could not copy the image to the clipboard: {error}"), + ), + ( + tr::dialog::clipboard::unavailable_error(error), + format!("Clipboard unavailable: {error}"), + ), + ( + tr::dialog::file::failed(error), + format!("The file dialog could not complete: {error}"), + ), + ( + tr::screenshot_editor::error::encode(error), + format!("Failed to encode screenshot export: {error}"), + ), + ( + tr::screenshot_editor::error::load_meta(error), + format!("Failed to load screenshot meta: {error}"), + ), + ( + tr::screenshot_editor::error::load_metadata(error), + format!("Failed to load screenshot metadata: {error}"), + ), + ( + tr::screenshot_editor::error::no_image(path.display()), + format!("No PNG found in {}", path.display()), + ), + ( + tr::screenshot_editor::error::open_image(error), + format!("Failed to open image: {error}"), + ), + ( + tr::screenshot_editor::error::renderer_failed(error), + format!("The GPU renderer failed to start: {error}"), + ), + ( + tr::screenshot_editor::error::save_project(error), + format!("Error saving project: {error}"), + ), + ( + tr::screenshot_editor::error::serialize(error), + format!("Failed to serialize screenshot config: {error}"), + ), + ( + tr::screenshot_editor::error::upload_task(error), + format!("The upload task failed: {error}"), + ), + ( + tr::transcription::download::failed(error), + format!("Download failed: {error}"), + ), + ]; + for (translated, literal) in &pairs { + assert_eq!(translated, literal); + } + + for status in ["exit status: 1", "signal: 9 (SIGKILL)"] { + assert_eq!( + tr::diagnostics::error::exited(status), + format!("The self-test exited with {status}") + ); + } + for status in ["500 Internal Server Error", "413 Payload Too Large"] { + assert_eq!( + tr::diagnostics::error::upload_status(status), + format!("Upload failed with status: {status}") + ); + } + for (width, height) in [(16_385_u32, 900_u32), (20_000, 20_000)] { + assert_eq!( + tr::screenshot_editor::error::too_large(width, height), + format!("Image dimensions exceed maximum: {width}x{height}") + ); + } + for progress in [0.0_f64, 0.426, 99.6, 100.0] { + assert_eq!( + tr::transcription::download::progress(format!("{progress:.0}")), + format!("Downloading model: {progress:.0}%") + ); + let filename = "ggml-small.bin"; + assert_eq!( + tr::transcription::download::file_progress(filename, format!("{progress:.0}")), + format!("Downloading {filename}: {progress:.0}%") + ); + } + for progress in [0.0_f64, 0.5, 0.999, 1.0] { + assert_eq!( + tr::import::progress::converting((progress * 100.0) as u32), + format!("Converting video... {}%", (progress * 100.0) as u32) + ); + } + for id in [1_u32, 69_733_632] { + assert_eq!(tr::overlay::display_fallback(id), format!("Display {}", id)); + } + for min in [150_f64, 200.] { + assert_eq!( + tr::overlay::area::minimum_size(min as u32, min as u32), + format!("Minimum size is {} x {}", min as u32, min as u32) + ); + } + for (width, height) in [(12.4_f64, 80.6_f64), (100., 3.)] { + assert_eq!( + tr::overlay::area::too_small(width.round(), height.round()), + format!("{} x {} is too small", width.round(), height.round()) + ); + } +} diff --git a/apps/desktop-gpui/src/i18n/mod.rs b/apps/desktop-gpui/src/i18n/mod.rs index d7d487d1a50..ae0fd36ad94 100644 --- a/apps/desktop-gpui/src/i18n/mod.rs +++ b/apps/desktop-gpui/src/i18n/mod.rs @@ -158,6 +158,10 @@ pub fn apply(preference: Preference, cx: &mut App) { cx.refresh_windows(); } +#[cfg(test)] +mod dialogs_surface_tests; +#[cfg(test)] +mod settings_surface_tests; #[cfg(test)] mod surface_tests; #[cfg(test)] diff --git a/apps/desktop-gpui/src/i18n/settings_surface_tests.rs b/apps/desktop-gpui/src/i18n/settings_surface_tests.rs new file mode 100644 index 00000000000..1d1cd608f15 --- /dev/null +++ b/apps/desktop-gpui/src/i18n/settings_surface_tests.rs @@ -0,0 +1,1611 @@ +use super::*; + +/// Every key the settings surfaces own. The shortcut effects are pinned +/// separately, against every combination of settings that builds them, in +/// `settings_pages`'s `shortcut_effects_match_the_sentences_they_replaced`. +const PREFIXES: &[&str] = &[ + "settings.automations.", + "settings.changelog.", + "settings.cli.", + "settings.experimental.", + "settings.feedback.", + "settings.integrations.", + "settings.license.", + "settings.options.", + "settings.prompt.", + "settings.quality.", + "settings.recordings.", + "settings.screenshots.", + "settings.shortcuts.", + "settings.sidebar.", + "settings.sign_in_required.", + "settings.transcription.", +]; + +const EFFECT_PREFIXES: &[&str] = &["settings.shortcuts.effect.", "settings.shortcuts.sentence."]; + +pub(super) fn is_settings_surface_key(key: &str) -> bool { + PREFIXES.iter().any(|prefix| key.starts_with(prefix)) +} + +const FORMATTED: &[&str] = &[ + "settings.sidebar.downloading", + "settings.sidebar.downloading_progress", + "settings.sidebar.installing", + "settings.sidebar.restart_to_update", + "settings.shortcuts.saved", + "settings.shortcuts.saved_moved", + "settings.cli.load_failed", + "settings.cli.installs_command", + "settings.cli.path_missing", + "settings.transcription.count", + "settings.experimental.switch.open_failed", + "settings.experimental.switch.check_failed", + "settings.feedback.diagnostic.confirm_body", + "settings.changelog.version", + "settings.integrations.managed_by", + "settings.integrations.s3.guide_intro", + "settings.integrations.gdrive.working_for", + "settings.integrations.gdrive.used_of", + "settings.integrations.gdrive.used", + "settings.integrations.gdrive.cached", + "settings.integrations.gdrive.updated", + "settings.automations.summary_empty", + "settings.automations.auto_name_empty", +]; + +#[test] +fn settings_surfaces_match_the_literals_they_replaced() { + let pairs: &[(&str, &str)] = &[ + (tr::settings::prompt::ok(), "Ok"), + (tr::settings::sidebar::account(), "Account"), + ( + tr::settings::sidebar::click_to_sign_in(), + "Click to sign in", + ), + (tr::settings::sidebar::signed_in(), "Signed in"), + (tr::settings::sidebar::sign_in(), "Sign In"), + (tr::settings::sidebar::sign_out(), "Sign Out"), + (tr::settings::sidebar::cancel_sign_in(), "Cancel Sign In"), + ( + tr::settings::sidebar::check_for_updates(), + "Check for updates", + ), + ( + tr::settings::sidebar::checking_for_updates(), + "Checking for updates…", + ), + ( + tr::settings::sidebar::previous_versions(), + "View previous versions", + ), + (tr::settings::recordings::title(), "Recordings"), + ( + tr::settings::recordings::description(), + "Manage your recordings and perform actions.", + ), + (tr::settings::recordings::import(), "Import"), + ( + tr::settings::recordings::empty_library(), + "No recordings found", + ), + (tr::settings::recordings::show_all(), "Show all"), + (tr::settings::recordings::empty::all(), "No recordings"), + ( + tr::settings::recordings::empty::all_matching(), + "No matching recordings", + ), + ( + tr::settings::recordings::empty::instant(), + "No instant recordings", + ), + ( + tr::settings::recordings::empty::instant_matching(), + "No matching instant recordings", + ), + ( + tr::settings::recordings::empty::studio(), + "No studio recordings", + ), + (tr::settings::recordings::load_more(), "Load more"), + ( + tr::settings::recordings::in_progress(), + "Recording in progress", + ), + (tr::settings::recordings::failed(), "Recording failed"), + (tr::settings::recordings::open_link(), "Open link"), + (tr::settings::recordings::edit(), "Edit"), + (tr::settings::recordings::retry_upload(), "Retry upload"), + ( + tr::settings::recordings::open_bundle(), + "Open recording bundle", + ), + (tr::settings::recordings::delete(), "Delete"), + ( + tr::settings::recordings::corrupted_title(), + "Recording is potentially corrupted", + ), + ( + tr::settings::recordings::corrupted_body(), + "The recording failed so this file may have issues in the editor! If your having issues recovering the file please reach out to support!", + ), + (tr::settings::screenshots::title(), "Screenshots"), + ( + tr::settings::screenshots::description(), + "Manage your screenshots and perform actions.", + ), + (tr::settings::screenshots::import(), "Import image"), + ( + tr::settings::screenshots::empty_library(), + "No screenshots found", + ), + (tr::settings::screenshots::empty(), "No screenshots"), + ( + tr::settings::screenshots::empty_matching(), + "No matching screenshots", + ), + (tr::settings::screenshots::load_more(), "Load more"), + (tr::settings::screenshots::copy(), "Copy to clipboard"), + (tr::settings::screenshots::save_as(), "Save as"), + (tr::settings::screenshots::open_folder(), "Open folder"), + (tr::settings::screenshots::delete(), "Delete"), + ( + tr::settings::quality::save_failed(), + "Couldn't save your recording settings. Please try again.", + ), + (tr::settings::quality::studio::title(), "Studio recordings"), + ( + tr::settings::quality::studio::description(), + "Saved to your computer for editing. You choose the export size later, in the editor.", + ), + (tr::settings::quality::studio::recommended(), "Recommended"), + (tr::settings::quality::studio::balanced(), "Balanced"), + ( + tr::settings::quality::studio::balanced_description(), + "Sharp detail at a sensible file size. Right for most recordings.", + ), + ( + tr::settings::quality::studio::compatibility(), + "Smaller files", + ), + ( + tr::settings::quality::studio::compatibility_description(), + "Saves disk space. Fine detail can soften, most of all on camera.", + ), + (tr::settings::quality::studio::ultra(), "Maximum detail"), + ( + tr::settings::quality::studio::ultra_description(), + "Keeps the most detail for heavy editing. Uses the most disk space.", + ), + ( + tr::settings::quality::instant::title(), + "Instant recordings", + ), + ( + tr::settings::quality::instant::description(), + "Uploaded while you record, so the link is ready the moment you stop. For the best quality, use Studio mode: it records at the highest quality your screen allows, and you can export up to 4K.", + ), + ( + tr::settings::quality::instant::max_resolution(), + "Maximum resolution", + ), + ( + tr::settings::quality::instant::free_plan(), + "720p on the free plan. Cap Pro records up to 4K.", + ), + ( + tr::settings::quality::instant::res_720p(), + "Smallest uploads, so links are ready fastest.", + ), + ( + tr::settings::quality::instant::res_1080p(), + "Sharp text at a practical upload size.", + ), + ( + tr::settings::quality::instant::res_1440p(), + "Extra detail for large, high-resolution screens.", + ), + ( + tr::settings::quality::instant::res_4k(), + "The most detail. Best on a fast connection.", + ), + (tr::settings::quality::sharing::title(), "Sharing"), + ( + tr::settings::quality::sharing::auto_open(), + "Open share links automatically", + ), + ( + tr::settings::quality::sharing::auto_open_description(), + "Open the link in your browser when an upload finishes.", + ), + ( + tr::settings::options::studio_quality::compatibility(), + "Compatibility", + ), + ( + tr::settings::options::studio_quality::balanced(), + "Balanced", + ), + (tr::settings::options::studio_quality::ultra(), "Ultra"), + (tr::settings::options::main_window_start::close(), "Close"), + ( + tr::settings::options::main_window_start::minimise(), + "Minimise", + ), + ( + tr::settings::options::post_studio::open_editor(), + "Open editor", + ), + (tr::settings::options::post_studio::show_cap(), "Show Cap"), + ( + tr::settings::options::post_studio::do_nothing(), + "Do nothing", + ), + ( + tr::settings::options::post_screenshot::open_editor(), + "Open editor", + ), + ( + tr::settings::options::post_screenshot::copy(), + "Copy to clipboard", + ), + ( + tr::settings::options::post_screenshot::copy_and_open_editor(), + "Copy and open editor", + ), + ( + tr::settings::options::post_screenshot::do_nothing(), + "Do nothing", + ), + ( + tr::settings::options::post_deletion::do_nothing(), + "Do nothing", + ), + ( + tr::settings::options::post_deletion::reopen(), + "Reopen recording window", + ), + (tr::settings::options::preview_quality::quarter(), "Quarter"), + (tr::settings::options::preview_quality::half(), "Half"), + (tr::settings::options::preview_quality::full(), "Full"), + (tr::settings::shortcuts::screenshots_title(), "Screenshots"), + ( + tr::settings::shortcuts::screenshots_description(), + "Shortcuts work from any app. Click one to change it.", + ), + (tr::settings::shortcuts::recording_title(), "Recording"), + ( + tr::settings::shortcuts::recording_description(), + "Start a recording without opening Cap first.", + ), + ( + tr::settings::shortcuts::while_recording_title(), + "While recording", + ), + ( + tr::settings::shortcuts::after_screenshot::title(), + "After a screenshot", + ), + ( + tr::settings::shortcuts::after_screenshot::hint(), + "For every screenshot, from a shortcut or from Cap", + ), + ( + tr::settings::shortcuts::after_studio::title(), + "After a Studio recording", + ), + ( + tr::settings::shortcuts::after_studio::hint(), + "When you stop", + ), + ( + tr::settings::shortcuts::after_instant::title(), + "After an Instant recording", + ), + ( + tr::settings::shortcuts::after_instant::hint(), + "The share link is always copied", + ), + ( + tr::settings::shortcuts::outcome::screenshot_open_editor(), + "Open editor", + ), + ( + tr::settings::shortcuts::outcome::screenshot_open_editor_hint(), + "Annotate first", + ), + (tr::settings::shortcuts::outcome::screenshot_copy(), "Copy"), + ( + tr::settings::shortcuts::outcome::screenshot_copy_hint(), + "Paste anywhere", + ), + ( + tr::settings::shortcuts::outcome::screenshot_copy_and_edit(), + "Copy and edit", + ), + ( + tr::settings::shortcuts::outcome::screenshot_copy_and_edit_hint(), + "Both at once", + ), + ( + tr::settings::shortcuts::outcome::screenshot_do_nothing(), + "Do nothing", + ), + ( + tr::settings::shortcuts::outcome::saved_to_library(), + "Saved to library", + ), + ( + tr::settings::shortcuts::outcome::studio_open_editor(), + "Open editor", + ), + ( + tr::settings::shortcuts::outcome::studio_open_editor_hint(), + "Edit right away", + ), + ( + tr::settings::shortcuts::outcome::studio_show_cap(), + "Show Cap", + ), + ( + tr::settings::shortcuts::outcome::studio_show_cap_hint(), + "Back to the recorder", + ), + ( + tr::settings::shortcuts::outcome::studio_do_nothing(), + "Do nothing", + ), + ( + tr::settings::shortcuts::outcome::instant_open_link(), + "Open link", + ), + ( + tr::settings::shortcuts::outcome::instant_open_link_hint(), + "In your browser", + ), + ( + tr::settings::shortcuts::outcome::instant_copy_link(), + "Copy link only", + ), + ( + tr::settings::shortcuts::outcome::instant_copy_link_hint(), + "Nothing opens", + ), + ( + tr::settings::shortcuts::label::screenshot_area(), + "Screenshot an area", + ), + ( + tr::settings::shortcuts::label::screenshot_window(), + "Screenshot a window", + ), + ( + tr::settings::shortcuts::label::screenshot_display(), + "Screenshot the display", + ), + ( + tr::settings::shortcuts::label::start_studio(), + "Start Studio recording", + ), + ( + tr::settings::shortcuts::label::start_instant(), + "Start Instant recording", + ), + ( + tr::settings::shortcuts::label::record_area(), + "Record an area", + ), + ( + tr::settings::shortcuts::label::record_window(), + "Record a window", + ), + ( + tr::settings::shortcuts::label::record_display(), + "Record a display", + ), + ( + tr::settings::shortcuts::label::open_picker(), + "Open recording picker", + ), + ( + tr::settings::shortcuts::label::cycle_mode(), + "Cycle recording mode", + ), + (tr::settings::shortcuts::label::stop(), "Stop recording"), + (tr::settings::shortcuts::label::pause(), "Pause or resume"), + ( + tr::settings::shortcuts::label::restart(), + "Restart recording", + ), + (tr::settings::shortcuts::label::fallback(), "Shortcut"), + ( + tr::settings::shortcuts::needs_modifier_macos(), + "Include ⌘, ⌃ or ⌥ so the shortcut doesn't get in the way of typing.", + ), + ( + tr::settings::shortcuts::needs_modifier_other(), + "Include Ctrl, Alt or Win so the shortcut doesn't get in the way of typing.", + ), + ( + tr::settings::shortcuts::unsupported_key(), + "That key can't be used in a shortcut. Try a letter, number or F-key.", + ), + ( + tr::settings::shortcuts::recording_hint(), + "Press the keys you want to use. Esc cancels, Delete removes it.", + ), + ( + tr::settings::shortcuts::blocked_while_recording(), + "Shortcuts can't be changed while this recording holds its stop key. Stop recording first.", + ), + ( + tr::settings::shortcuts::unavailable(), + "Another app or the system already uses this shortcut. Click to choose a different one.", + ), + (tr::settings::shortcuts::type_shortcut(), "Type shortcut…"), + ( + tr::settings::shortcuts::record_shortcut(), + "Record shortcut", + ), + (tr::settings::cli::title(), "Command Line"), + ( + tr::settings::cli::description(), + "Install the Cap command for terminals, agents, scripts, and local automation.", + ), + (tr::settings::cli::retry(), "Retry"), + (tr::settings::cli::repairing(), "Repairing..."), + (tr::settings::cli::installing(), "Installing..."), + (tr::settings::cli::repair(), "Repair"), + (tr::settings::cli::install(), "Install CLI"), + (tr::settings::cli::installed(), "Installed"), + (tr::settings::cli::not_installed(), "Not installed"), + (tr::settings::cli::removing(), "Removing..."), + (tr::settings::cli::remove(), "Remove"), + (tr::settings::cli::command(), "Command"), + (tr::settings::cli::target(), "Target"), + ( + tr::settings::cli::path_configured(), + "Added cap to your PATH. Restart your terminal to use it, or run this now:", + ), + (tr::settings::cli::copy(), "Copy"), + (tr::settings::transcription::title(), "Transcription"), + ( + tr::settings::transcription::description(), + "Add names, spellings, domains, and capitalization preferences that caption generation should keep in mind.", + ), + ( + tr::settings::transcription::terms_title(), + "Remembered terms", + ), + ( + tr::settings::transcription::terms_description(), + "Add one term at a time to reduce typos and formatting mistakes.", + ), + (tr::settings::transcription::clear(), "Clear"), + (tr::settings::transcription::saving(), "Saving..."), + (tr::settings::transcription::saved(), "Saved"), + (tr::settings::transcription::placeholder(), "Add a term"), + ( + tr::settings::transcription::applied_note(), + "These hints are applied when you generate captions in the editor.", + ), + (tr::settings::transcription::active_title(), "Active hints"), + (tr::settings::experimental::reliability(), "Reliability"), + (tr::settings::experimental::muxer(), "Out-of-process muxer"), + ( + tr::settings::experimental::muxer_description(), + "Run the fragmented-MP4 muxer in an isolated subprocess so muxer crashes can't take down your recording. Requires the bundled cap-muxer binary.", + ), + (tr::settings::experimental::developer(), "Developer"), + ( + tr::settings::experimental::developer_description(), + "Only shown in development builds.", + ), + (tr::settings::experimental::native_app(), "Use native app"), + ( + tr::settings::experimental::native_app_description(), + "Turn off to close this app and reopen the classic Tauri app in the dev harness. Recordings and settings are shared.", + ), + ( + tr::settings::experimental::switch::switching(), + "Switching back to the classic Cap app.", + ), + ( + tr::settings::experimental::switch::data_stays(), + "Your recordings and settings stay exactly where they are.", + ), + ( + tr::settings::experimental::switch::opening(), + "Opening the classic Cap app", + ), + ( + tr::settings::experimental::switch::opening_detail(), + "This window will close when the classic Cap app is ready.", + ), + ( + tr::settings::experimental::switch::busy(), + "Finish your recording, export, upload, import, or transcription task before switching to the classic app.", + ), + ( + tr::settings::experimental::switch::not_found(), + "Couldn't find the classic Cap app to switch back to.", + ), + ( + tr::settings::experimental::switch::save_failed(), + "Couldn't save your app preference. Cap is still open.", + ), + ( + tr::settings::experimental::switch::timed_out(), + "The classic app hasn't opened. Check the dev terminal for build errors, then try again.", + ), + ( + tr::settings::experimental::switch::failed(), + "Couldn't open the classic Cap app.", + ), + (tr::settings::feedback::title(), "Feedback"), + ( + tr::settings::feedback::description(), + "Help us improve Cap by submitting feedback or reporting bugs. We'll get right on it.", + ), + ( + tr::settings::feedback::placeholder(), + "Tell us what you think about Cap...", + ), + ( + tr::settings::feedback::submit_failed(), + "Failed to submit feedback", + ), + ( + tr::settings::feedback::thanks(), + "Thank you for your feedback!", + ), + (tr::settings::feedback::submitting(), "Submitting..."), + (tr::settings::feedback::submit(), "Submit Feedback"), + ( + tr::settings::feedback::community_title(), + "Join the Community", + ), + ( + tr::settings::feedback::community_description(), + "Have questions, want to share ideas, or just hang out? Join the Cap Discord community.", + ), + (tr::settings::feedback::join_discord(), "Join Discord"), + (tr::settings::feedback::debug_title(), "Debug Information"), + ( + tr::settings::feedback::debug_description(), + "Upload your logs to help us diagnose issues with Cap. No personal information is included.", + ), + (tr::settings::feedback::uploading(), "Uploading..."), + (tr::settings::feedback::upload_logs(), "Upload Logs"), + ( + tr::settings::feedback::upload_sent(), + "Sent. Thank you -- this helps us find the problem.", + ), + ( + tr::settings::feedback::upload_interrupted(), + "The upload was interrupted", + ), + (tr::settings::feedback::system_title(), "System Information"), + ( + tr::settings::feedback::system_loading(), + "Loading system information...", + ), + ( + tr::settings::feedback::operating_system(), + "Operating System", + ), + (tr::settings::feedback::capture_support(), "Capture Support"), + (tr::settings::feedback::app_version(), "App Version"), + ( + tr::settings::feedback::diagnostic::title(), + "Diagnostic Report", + ), + ( + tr::settings::feedback::diagnostic::description(), + "Records a short flashing/beeping test pattern, measures how far the picture and sound drift apart, and snapshots your displays, cameras, microphones, disk space and recording settings. Nothing is sent anywhere until you choose to send it.", + ), + ( + tr::settings::feedback::diagnostic::stop_recording_first(), + "Stop the current recording before running a diagnostic.", + ), + ( + tr::settings::feedback::diagnostic::confirm_title(), + "Run diagnostic?", + ), + (tr::settings::feedback::diagnostic::run(), "Run Diagnostic"), + ( + tr::settings::feedback::diagnostic::no_cli(), + "The Cap command-line tool was not found next to this build, so the report will cover your environment only.", + ), + ( + tr::settings::feedback::diagnostic::no_cli_sync(), + "The Cap command-line tool was not found next to this build, so the sync test could not run.", + ), + ( + tr::settings::feedback::diagnostic::warning_title(), + "The test takes over your screen and makes noise.", + ), + ( + tr::settings::feedback::diagnostic::warning_body(), + "A fullscreen window flashes black and white while 1kHz beeps play through your speakers, and Cap records it and measures how far the picture and the sound have drifted apart. Leave the machine alone while it runs. Both modes together take a couple of minutes.", + ), + ( + tr::settings::feedback::diagnostic::mode_label(), + "Recording mode", + ), + ( + tr::settings::feedback::diagnostic::mode_description(), + "Which pipeline to test. Both runs the studio leg and then the instant one.", + ), + ( + tr::settings::feedback::diagnostic::mic_label(), + "Test microphone", + ), + ( + tr::settings::feedback::diagnostic::mic_description(), + "Also record your microphone and check that it hears the beeps in time. Needs speakers the mic can actually hear.", + ), + (tr::settings::feedback::diagnostic::sending(), "Sending..."), + (tr::settings::feedback::diagnostic::send(), "Send to Cap"), + (tr::settings::feedback::diagnostic::show_file(), "Show File"), + (tr::settings::feedback::diagnostic::run_again(), "Run Again"), + (tr::settings::feedback::diagnostic::try_again(), "Try Again"), + (tr::settings::feedback::diagnostic::verdict::pass(), "PASS"), + (tr::settings::feedback::diagnostic::verdict::warn(), "WARN"), + (tr::settings::feedback::diagnostic::verdict::fail(), "FAIL"), + ( + tr::settings::feedback::diagnostic::verdict::inconclusive(), + "INCONCLUSIVE", + ), + ( + tr::settings::feedback::diagnostic::verdict::environment_only(), + "ENVIRONMENT ONLY", + ), + (tr::settings::sign_in_required::title(), "Sign in required"), + ( + tr::settings::sign_in_required::body(), + "Please sign in to continue. Alternatively, email hello@cap.so or join our Discord at cap.link/discord", + ), + (tr::settings::changelog::loading(), "Loading changelog..."), + ( + tr::settings::changelog::fetch_failed(), + "Failed to fetch changelog", + ), + (tr::settings::changelog::new(), "NEW"), + (tr::settings::license::title(), "Plan & license"), + ( + tr::settings::license::description(), + "Your Cap plan and desktop license, in one place.", + ), + (tr::settings::license::plan_title(), "Your plan"), + ( + tr::settings::license::pro_description(), + "Your account includes cloud sharing, Pro features and a desktop license for commercial use.", + ), + (tr::settings::license::desktop_name(), "Desktop License"), + ( + tr::settings::license::desktop_description(), + "Your desktop license covers commercial recording and editing. Cap Pro adds cloud sharing and collaboration features.", + ), + ( + tr::settings::license::free_description(), + "Record and edit locally for personal use. Choose a paid plan for commercial use or more cloud features.", + ), + (tr::settings::license::checking(), "Checking…"), + (tr::settings::license::refresh(), "Refresh plan"), + ( + tr::settings::license::refresh_failed(), + "Couldn't refresh your plan. Please try again.", + ), + ( + tr::settings::license::sign_in_hint(), + "Already have Cap Pro? Sign in with your account from the sidebar.", + ), + (tr::settings::license::explore_title(), "Explore plans"), + ( + tr::settings::license::explore_description(), + "Compare current pricing and everything included on our website.", + ), + ( + tr::settings::license::explore_desktop(), + "Desktop License · Commercial use of the desktop recorder and editor.", + ), + ( + tr::settings::license::explore_pro(), + "Cap Pro · A desktop license, plus cloud sharing, AI features and collaboration.", + ), + ( + tr::settings::license::view_pricing(), + "View plans & pricing ↗", + ), + ( + tr::settings::license::opens_pricing(), + "Opens cap.so/pricing in your browser.", + ), + (tr::settings::license::active_title(), "Desktop license"), + (tr::settings::license::key(), "License Key"), + (tr::settings::license::expires(), "Expires"), + (tr::settings::license::deactivate(), "Deactivate License"), + (tr::settings::license::have_key(), "Have a license key?"), + (tr::settings::license::placeholder(), "License key"), + (tr::settings::license::activating(), "Activating..."), + (tr::settings::license::activate(), "Activate License"), + (tr::settings::license::no_instance(), "No instance ID found"), + ( + tr::settings::license::save_failed(), + "Failed to save the license", + ), + (tr::settings::integrations::title(), "Integrations"), + ( + tr::settings::integrations::description(), + "Configure integrations to extend Cap's functionality and connect with third-party services.", + ), + ( + tr::settings::integrations::google_drive_description(), + "Connect Google Drive for new shareable link uploads. Cap stores new videos in a private Cap folder in your Drive and continues serving them through Cap after normal access checks.", + ), + (tr::settings::integrations::s3_name(), "S3 Config"), + ( + tr::settings::integrations::s3_description(), + "Connect your own S3 bucket for complete control over your data storage. All new shareable link uploads will be automatically uploaded to your configured S3 bucket, ensuring you maintain complete ownership and control over your content. Perfect for organizations requiring data sovereignty and custom storage policies.", + ), + ( + tr::settings::integrations::managed(), + "Managed by your organization", + ), + (tr::settings::integrations::upgrade(), "Upgrade to Pro"), + (tr::settings::integrations::configure(), "Configure"), + ( + tr::settings::integrations::request_timed_out(), + "The request timed out", + ), + ( + tr::settings::integrations::s3::configuration(), + "Configuration", + ), + ( + tr::settings::integrations::s3::guide(), + "Storage Config Guide", + ), + ( + tr::settings::integrations::s3::provider(), + "Storage Provider", + ), + ( + tr::settings::integrations::s3::other_provider(), + "Other S3-Compatible", + ), + ( + tr::settings::integrations::s3::access_key(), + "Access Key ID", + ), + ( + tr::settings::integrations::s3::secret_key(), + "Secret Access Key", + ), + (tr::settings::integrations::s3::endpoint(), "Endpoint"), + (tr::settings::integrations::s3::bucket(), "Bucket Name"), + (tr::settings::integrations::s3::region(), "Region"), + (tr::settings::integrations::s3::removing(), "Removing..."), + (tr::settings::integrations::s3::remove(), "Remove Config"), + (tr::settings::integrations::s3::testing(), "Testing..."), + (tr::settings::integrations::s3::test(), "Test Connection"), + (tr::settings::integrations::s3::saving(), "Saving..."), + ( + tr::settings::integrations::s3::fetch_failed(), + "Failed to fetch S3 config", + ), + ( + tr::settings::integrations::s3::saved(), + "S3 configuration saved successfully", + ), + ( + tr::settings::integrations::s3::save_failed(), + "Failed to save S3 config", + ), + ( + tr::settings::integrations::s3::deleted(), + "S3 configuration deleted successfully", + ), + ( + tr::settings::integrations::s3::delete_failed(), + "Failed to delete S3 config", + ), + ( + tr::settings::integrations::s3::test_ok(), + "S3 configuration test successful! Connection is working.", + ), + ( + tr::settings::integrations::s3::test_failed(), + "S3 connection test failed. Check your config and network connection.", + ), + ( + tr::settings::integrations::s3::test_timeout(), + "Connection test timed out after 5 seconds. Please check your endpoint URL and network connection.", + ), + ( + tr::settings::integrations::gdrive::connection_title(), + "Connection", + ), + ( + tr::settings::integrations::gdrive::connection_description(), + "Google Drive stores new uploads in a private Cap folder in your Drive. Existing Cap-hosted and S3 videos keep using their current storage.", + ), + ( + tr::settings::integrations::gdrive::active_for_uploads(), + "Active for new uploads", + ), + ( + tr::settings::integrations::gdrive::connected_inactive(), + "Connected but not active", + ), + ( + tr::settings::integrations::gdrive::not_connected(), + "Not connected", + ), + ( + tr::settings::integrations::gdrive::refreshing(), + "Refreshing...", + ), + (tr::settings::integrations::gdrive::refresh(), "Refresh"), + (tr::settings::integrations::gdrive::waiting(), "Waiting..."), + (tr::settings::integrations::gdrive::opening(), "Opening..."), + ( + tr::settings::integrations::gdrive::connect(), + "Connect Google Drive", + ), + (tr::settings::integrations::gdrive::active(), "Active"), + ( + tr::settings::integrations::gdrive::use_drive(), + "Use Google Drive", + ), + (tr::settings::integrations::gdrive::use_s3(), "Use S3"), + (tr::settings::integrations::gdrive::testing(), "Testing..."), + (tr::settings::integrations::gdrive::test(), "Test"), + ( + tr::settings::integrations::gdrive::disconnect(), + "Disconnect", + ), + ( + tr::settings::integrations::gdrive::connect_failed(), + "Failed to start Google Drive connection", + ), + ( + tr::settings::integrations::gdrive::finish_in_browser(), + "Finish connecting Google Drive in your browser, then return here and refresh.", + ), + ( + tr::settings::integrations::gdrive::working(), + "Google Drive connection is working", + ), + ( + tr::settings::integrations::gdrive::test_failed(), + "Google Drive connection test failed", + ), + ( + tr::settings::integrations::gdrive::set_active_failed(), + "Failed to update active storage provider", + ), + ( + tr::settings::integrations::gdrive::disconnected(), + "Google Drive disconnected", + ), + ( + tr::settings::integrations::gdrive::disconnect_failed(), + "Failed to disconnect Google Drive", + ), + (tr::settings::integrations::gdrive::storage(), "Storage"), + (tr::settings::integrations::gdrive::remaining(), "Remaining"), + ( + tr::settings::integrations::gdrive::drive_files(), + "Drive files", + ), + (tr::settings::integrations::gdrive::trash(), "Trash"), + (tr::settings::automations::title(), "Automations"), + ( + tr::settings::automations::description(), + "Rules are shared with the Cap CLI. Cap saves these rules but does not run automations yet.", + ), + (tr::settings::automations::templates_title(), "Templates"), + ( + tr::settings::automations::templates_description(), + "One click to add a ready-made automation. Tweak anything afterwards.", + ), + ( + tr::settings::automations::save_failed(), + "Failed to save automations. Your changes have not been saved.", + ), + (tr::settings::automations::retry_save(), "Retry save"), + ( + tr::settings::automations::empty_title(), + "No automations yet", + ), + ( + tr::settings::automations::empty_description(), + "Pick a template below to get started in one click, or build your own from scratch.", + ), + ( + tr::settings::automations::start_from_scratch(), + "Start from scratch", + ), + (tr::settings::automations::new(), "New automation"), + ( + tr::settings::automations::status::enabled_saved(), + "Enabled · Saved", + ), + ( + tr::settings::automations::status::disabled_saved(), + "Disabled · Saved", + ), + ( + tr::settings::automations::status::enabled_unsaved(), + "Enabled · Not saved", + ), + ( + tr::settings::automations::status::disabled_unsaved(), + "Disabled · Not saved", + ), + (tr::settings::automations::enabled(), "Enabled"), + (tr::settings::automations::name(), "Name"), + (tr::settings::automations::match_all(), "Match all"), + (tr::settings::automations::match_any(), "Match any"), + (tr::settings::automations::add_condition(), "Add condition"), + (tr::settings::automations::add_action(), "Add action"), + ( + tr::settings::automations::check_compatibility(), + "Check compatibility", + ), + (tr::settings::automations::delete(), "Delete"), + ( + tr::settings::automations::dangerous(), + "In the Cap CLI, this automation runs commands or sends network requests with your permissions. Only use values you trust.", + ), + ( + tr::settings::automations::compatibility_checked(), + "Compatibility checked: this automation doesn't run in Cap yet. Saved rules remain available to the Cap CLI. No actions were run.", + ), + ( + tr::settings::automations::condition_never_matches(), + "This condition never matches for the selected trigger.", + ), + ( + tr::settings::automations::action_no_effect(), + "This action has no effect for the selected trigger.", + ), + (tr::settings::automations::browse(), "Browse"), + ( + tr::settings::automations::copy_link(), + "Copy link to clipboard", + ), + ( + tr::settings::automations::open_in_browser(), + "Open in browser", + ), + ( + tr::settings::automations::run_through_shell(), + "Run through shell", + ), + ( + tr::settings::automations::no_presets(), + "No presets yet — create one in the editor first.", + ), + (tr::settings::automations::field::source(), "Source"), + (tr::settings::automations::field::folder(), "Folder"), + ( + tr::settings::automations::field::filename(), + "Filename template (optional)", + ), + ( + tr::settings::automations::field::organization(), + "Organization ID (optional)", + ), + (tr::settings::automations::field::program(), "Program"), + ( + tr::settings::automations::field::arguments(), + "Arguments (space-separated)", + ), + (tr::settings::automations::field::url(), "URL"), + (tr::settings::automations::field::method(), "Method"), + ( + tr::settings::automations::field::body_template(), + "Body template (optional)", + ), + (tr::settings::automations::field::title(), "Title"), + (tr::settings::automations::field::body(), "Body"), + (tr::settings::automations::field::preset(), "Preset"), + (tr::settings::automations::field::format(), "Format"), + (tr::settings::automations::field::resolution(), "Resolution"), + (tr::settings::automations::field::frame_rate(), "Frame rate"), + ( + tr::settings::automations::field::compression(), + "Compression", + ), + ( + tr::settings::automations::field::destination(), + "Destination folder (optional, blank = project folder)", + ), + ( + tr::settings::automations::placeholder::window_title(), + "e.g. Slack", + ), + ( + tr::settings::automations::placeholder::organization_id(), + "Organization ID", + ), + ( + tr::settings::automations::placeholder::project_folder(), + "Project folder", + ), + ( + tr::settings::automations::clipboard::original(), + "Original capture", + ), + ( + tr::settings::automations::clipboard::rendered(), + "Edited / rendered", + ), + (tr::settings::automations::target::display(), "Display"), + (tr::settings::automations::target::window(), "Window"), + (tr::settings::automations::target::area(), "Area"), + (tr::settings::automations::compression::maximum(), "Maximum"), + (tr::settings::automations::compression::social(), "Social"), + (tr::settings::automations::compression::web(), "Web"), + (tr::settings::automations::compression::potato(), "Potato"), + ( + tr::settings::automations::trigger::screenshot_taken::label(), + "On screenshot taken", + ), + ( + tr::settings::automations::trigger::screenshot_taken::phrase(), + "Screenshot taken", + ), + ( + tr::settings::automations::trigger::screenshot_taken::noun(), + "Screenshot", + ), + ( + tr::settings::automations::trigger::studio_finished::label(), + "On studio recording finished", + ), + ( + tr::settings::automations::trigger::studio_finished::phrase(), + "Studio recording ends", + ), + ( + tr::settings::automations::trigger::studio_finished::noun(), + "Studio recording", + ), + ( + tr::settings::automations::trigger::instant_finished::label(), + "On instant recording finished", + ), + ( + tr::settings::automations::trigger::instant_finished::phrase(), + "Instant recording ends", + ), + ( + tr::settings::automations::trigger::instant_finished::noun(), + "Instant recording", + ), + ( + tr::settings::automations::trigger::recording_started::label(), + "On recording started", + ), + ( + tr::settings::automations::trigger::recording_started::phrase(), + "Recording starts", + ), + ( + tr::settings::automations::trigger::recording_started::noun(), + "Recording start", + ), + ( + tr::settings::automations::trigger::upload_completed::label(), + "On upload completed", + ), + ( + tr::settings::automations::trigger::upload_completed::phrase(), + "Upload completes", + ), + ( + tr::settings::automations::trigger::upload_completed::noun(), + "Upload", + ), + ( + tr::settings::automations::trigger::video_imported::label(), + "On video imported", + ), + ( + tr::settings::automations::trigger::video_imported::phrase(), + "Video imported", + ), + ( + tr::settings::automations::trigger::video_imported::noun(), + "Import", + ), + ( + tr::settings::automations::trigger::recording_deleted::label(), + "On recording deleted", + ), + ( + tr::settings::automations::trigger::recording_deleted::phrase(), + "Recording deleted", + ), + ( + tr::settings::automations::trigger::recording_deleted::noun(), + "Deletion", + ), + ( + tr::settings::automations::action::copy_to_clipboard::label(), + "Copy to clipboard", + ), + ( + tr::settings::automations::action::copy_to_clipboard::short(), + "Copy to clipboard", + ), + ( + tr::settings::automations::action::copy_to_clipboard::noun(), + "Clipboard", + ), + ( + tr::settings::automations::action::save_to_location::label(), + "Save to location", + ), + ( + tr::settings::automations::action::save_to_location::short(), + "Save to folder", + ), + ( + tr::settings::automations::action::save_to_location::noun(), + "Folder", + ), + ( + tr::settings::automations::action::export::label(), + "Export with profile", + ), + (tr::settings::automations::action::export::short(), "Export"), + (tr::settings::automations::action::export::noun(), "Export"), + ( + tr::settings::automations::action::upload::label(), + "Upload + copy link", + ), + ( + tr::settings::automations::action::upload::short(), + "Upload & copy link", + ), + (tr::settings::automations::action::upload::noun(), "Upload"), + ( + tr::settings::automations::action::reveal::label(), + "Reveal in file manager", + ), + ( + tr::settings::automations::action::reveal::short(), + "Reveal in file manager", + ), + (tr::settings::automations::action::reveal::noun(), "Reveal"), + ( + tr::settings::automations::action::open_file::label(), + "Open file", + ), + ( + tr::settings::automations::action::open_file::short(), + "Open file", + ), + (tr::settings::automations::action::open_file::noun(), "Open"), + ( + tr::settings::automations::action::ocr::label(), + "Recognize text (OCR) to clipboard", + ), + ( + tr::settings::automations::action::ocr::short(), + "Copy text (OCR)", + ), + (tr::settings::automations::action::ocr::noun(), "Text"), + ( + tr::settings::automations::action::notify::label(), + "Show notification", + ), + (tr::settings::automations::action::notify::short(), "Notify"), + (tr::settings::automations::action::notify::noun(), "Notify"), + ( + tr::settings::automations::action::open_editor::label(), + "Open editor", + ), + ( + tr::settings::automations::action::open_editor::short(), + "Open editor", + ), + ( + tr::settings::automations::action::open_editor::noun(), + "Editor", + ), + ( + tr::settings::automations::action::skip_editor::label(), + "Skip editor (headless)", + ), + ( + tr::settings::automations::action::skip_editor::short(), + "Skip editor", + ), + ( + tr::settings::automations::action::skip_editor::noun(), + "Skip editor", + ), + ( + tr::settings::automations::action::apply_preset::label(), + "Apply editor preset", + ), + ( + tr::settings::automations::action::apply_preset::short(), + "Apply preset", + ), + ( + tr::settings::automations::action::apply_preset::noun(), + "Preset", + ), + ( + tr::settings::automations::action::run_command::label(), + "Run command", + ), + ( + tr::settings::automations::action::run_command::short(), + "Run command", + ), + ( + tr::settings::automations::action::run_command::noun(), + "Command", + ), + ( + tr::settings::automations::action::webhook::label(), + "Send webhook", + ), + ( + tr::settings::automations::action::webhook::short(), + "Send webhook", + ), + ( + tr::settings::automations::action::webhook::noun(), + "Webhook", + ), + ( + tr::settings::automations::action::delete_local_files::label(), + "Delete local files", + ), + ( + tr::settings::automations::action::delete_local_files::short(), + "Delete local files", + ), + ( + tr::settings::automations::action::delete_local_files::noun(), + "Delete", + ), + ( + tr::settings::automations::condition::capture_target(), + "Capture target is", + ), + ( + tr::settings::automations::condition::recording_mode(), + "Recording mode is", + ), + ( + tr::settings::automations::condition::duration_at_least(), + "Duration at least (seconds)", + ), + ( + tr::settings::automations::condition::duration_at_most(), + "Duration at most (seconds)", + ), + ( + tr::settings::automations::condition::window_title(), + "Window title contains", + ), + ( + tr::settings::automations::condition::organization(), + "Organization is", + ), + ( + tr::settings::automations::template::copy_screenshot::name(), + "Auto-copy new screenshots to clipboard", + ), + ( + tr::settings::automations::template::copy_screenshot::description(), + "Snap a screenshot and it's right there, ready to paste.", + ), + ( + tr::settings::automations::template::ocr_screenshot::name(), + "Pull the text out of screenshots", + ), + ( + tr::settings::automations::template::ocr_screenshot::description(), + "Cap reads the text in your screenshot and copies it for you.", + ), + ( + tr::settings::automations::template::save_screenshot::name(), + "Tuck screenshots into a folder", + ), + ( + tr::settings::automations::template::save_screenshot::description(), + "Send every new screenshot straight to a folder you pick.", + ), + ( + tr::settings::automations::template::reveal_screenshot::name(), + "Jump to each new screenshot", + ), + ( + tr::settings::automations::template::reveal_screenshot::description(), + "Pop open every screenshot in Finder the moment you take it.", + ), + ( + tr::settings::automations::template::export_studio::name(), + "Auto-export when you finish recording", + ), + ( + tr::settings::automations::template::export_studio::description(), + "Render an MP4 the second a Studio recording wraps up.", + ), + ( + tr::settings::automations::template::upload_share::name(), + "Upload and grab the share link", + ), + ( + tr::settings::automations::template::upload_share::description(), + "Finish a recording and the link is waiting on your clipboard.", + ), + ( + tr::settings::automations::template::notify_upload::name(), + "Ping me when an upload is ready", + ), + ( + tr::settings::automations::template::notify_upload::description(), + "Get a gentle desktop nudge once your recording is shareable.", + ), + ( + tr::settings::automations::template::notify_upload::body(), + "Your recording is ready to share.", + ), + ( + tr::settings::automations::template::webhook_share::name(), + "Tell Slack when you share something", + ), + ( + tr::settings::automations::template::webhook_share::description(), + "Send the share link to Slack, Discord, or your own webhook.", + ), + ]; + for (translated, literal) in pairs { + assert_eq!(translated, literal); + } + + let assembled: &[(&str, String)] = &[ + ( + tr::settings::recordings::empty::studio_matching(), + format!("No matching {} recordings", "studio"), + ), + ( + tr::settings::feedback::capture_supported(), + format!("Screen Capture: {}", "Supported"), + ), + ( + tr::settings::feedback::capture_unsupported(), + format!("Screen Capture: {}", "Not Supported"), + ), + ( + tr::settings::automations::when(), + "When this happens".to_uppercase(), + ), + ( + tr::settings::automations::only_if(), + "Only run if".to_uppercase(), + ), + ( + tr::settings::automations::then(), + "Then do this".to_uppercase(), + ), + ]; + for (translated, literal) in assembled { + assert_eq!(*translated, literal.as_str()); + } + + use tr::settings::automations::trigger; + let runs = [ + (trigger::screenshot_taken::runs(), "Screenshot taken"), + (trigger::studio_finished::runs(), "Studio recording ends"), + (trigger::instant_finished::runs(), "Instant recording ends"), + (trigger::recording_started::runs(), "Recording starts"), + (trigger::upload_completed::runs(), "Upload completes"), + (trigger::video_imported::runs(), "Video imported"), + (trigger::recording_deleted::runs(), "Recording deleted"), + ]; + for (translated, phrase) in runs { + assert_eq!( + translated, + format!("Runs for every {}.", phrase.to_lowercase()) + ); + } + + let plain_keys = KEYS + .iter() + .filter(|key| is_settings_surface_key(key)) + .filter(|key| !EFFECT_PREFIXES.iter().any(|prefix| key.starts_with(prefix))) + .count() + - FORMATTED.len(); + assert_eq!( + pairs.len() + assembled.len() + runs.len(), + plain_keys, + "every plain key has a literal above" + ); + for key in FORMATTED { + assert!(KEYS.contains(key), "{key}"); + } +} + +#[test] +fn settings_surface_messages_format_like_the_code_they_replaced() { + let version = "0.6.2"; + assert_eq!( + tr::settings::sidebar::downloading(version), + format!("Downloading v{version}…") + ); + for fraction in [0.0_f32, 0.425, 1.0] { + assert_eq!( + tr::settings::sidebar::downloading_progress(version, (fraction * 100.).round() as u32), + format!( + "Downloading v{version}… {}%", + (fraction * 100.).round() as u32 + ) + ); + } + assert_eq!( + tr::settings::sidebar::restart_to_update(version), + format!("Restart to update to v{version}") + ); + assert_eq!( + tr::settings::sidebar::installing(version), + format!("Installing v{version}…") + ); + + let keys = "⌘⇧4".to_string(); + let effect = tr::settings::shortcuts::effect::screenshot_area_copy(); + let mut moved = format!("Saved. Press {} in any app to {effect}.", keys); + assert_eq!(tr::settings::shortcuts::saved(&keys, effect), moved); + let from = tr::settings::shortcuts::label::record_area(); + moved.push_str(&format!(" Removed from {}.", from)); + assert_eq!( + tr::settings::shortcuts::saved_moved(&keys, effect, from), + moved + ); + + let error = "permission denied".to_string(); + assert_eq!( + tr::settings::cli::load_failed(&error), + format!("Couldn't load CLI status: {error}") + ); + assert_eq!( + tr::settings::experimental::switch::open_failed(&error), + format!("Couldn't open the classic Cap app: {error}") + ); + assert_eq!( + tr::settings::experimental::switch::check_failed(&error), + format!("Couldn't check whether the classic Cap app opened: {error}") + ); + let path_entry = "~/.local/bin".to_string(); + assert_eq!( + tr::settings::cli::path_missing(&path_entry), + format!( + "Add {} to your PATH to use cap from a new terminal.", + path_entry + ) + ); + + let marker = "\u{1}"; + let installs = tr::settings::cli::installs_command(marker); + assert_eq!( + installs.split_once(marker), + Some(( + "The desktop app installs a local\u{a0}", + "\u{a0}command that points back to the bundled CLI." + )) + ); + let guide = tr::settings::integrations::s3::guide_intro(marker); + assert_eq!( + guide.split_once(marker), + Some(( + "It should take under 10 minutes to set up and connect your storage \ + bucket to Cap. View the\u{a0}", + "\u{a0}to get started." + )) + ); + + for count in [0_usize, 1, 2, 5] { + assert_eq!( + tr::settings::transcription::count(count), + format!("{} {}", count, if count == 1 { "item" } else { "items" }) + ); + } + + assert_eq!( + tr::settings::feedback::diagnostic::confirm_body(crate::diagnostics::DEFAULT_DURATION_SECS), + format!( + "Cap will take over your screen with a flashing pattern and play loud beeps for \ + about {} seconds per pipeline. Take your headphones off, leave the volume \ + audible, and leave the machine alone until it finishes.", + crate::diagnostics::DEFAULT_DURATION_SECS + ) + ); + + let date = "8/17/2026".to_string(); + assert_eq!( + tr::settings::changelog::version("0.6.2", &date), + format!("Version {} - {}", "0.6.2", date) + ); + + let organization = "Acme".to_string(); + assert_eq!( + tr::settings::integrations::managed_by(&organization), + format!("Managed by your organization: {organization}") + ); + let email = "taylor@example.com"; + assert_eq!( + tr::settings::integrations::gdrive::working_for(email), + format!("Google Drive connection is working for {email}") + ); + let (usage, limit) = ("2.0 GB".to_string(), "15 GB".to_string()); + assert_eq!( + tr::settings::integrations::gdrive::used_of(&usage, &limit), + format!("{usage} of {limit} used") + ); + assert_eq!( + tr::settings::integrations::gdrive::used(&usage), + format!("{usage} used") + ); + let formatted = "Aug 17, 2026, 10:30 PM".to_string(); + assert_eq!( + tr::settings::integrations::gdrive::cached(&formatted), + format!("{} {formatted}", "Cached") + ); + assert_eq!( + tr::settings::integrations::gdrive::updated(&formatted), + format!("{} {formatted}", "Updated") + ); + + use tr::settings::automations::trigger; + for (phrase, noun) in [ + ( + trigger::screenshot_taken::phrase(), + trigger::screenshot_taken::noun(), + ), + ( + trigger::studio_finished::phrase(), + trigger::studio_finished::noun(), + ), + ( + trigger::instant_finished::phrase(), + trigger::instant_finished::noun(), + ), + ( + trigger::recording_started::phrase(), + trigger::recording_started::noun(), + ), + ( + trigger::upload_completed::phrase(), + trigger::upload_completed::noun(), + ), + ( + trigger::video_imported::phrase(), + trigger::video_imported::noun(), + ), + ( + trigger::recording_deleted::phrase(), + trigger::recording_deleted::noun(), + ), + ] { + assert_eq!( + tr::settings::automations::summary_empty(phrase), + format!("{phrase} → no actions yet") + ); + assert_eq!( + tr::settings::automations::auto_name_empty(noun), + format!("{noun} automation") + ); + } +} diff --git a/apps/desktop-gpui/src/i18n/tests.rs b/apps/desktop-gpui/src/i18n/tests.rs index c3b43e28840..72ae3a223f3 100644 --- a/apps/desktop-gpui/src/i18n/tests.rs +++ b/apps/desktop-gpui/src/i18n/tests.rs @@ -24,6 +24,9 @@ fn english_matches_the_literals_it_replaced() { (tr::menu::app::services(), "Services".into()), (tr::menu::app::settings(), "Settings…".into()), (tr::menu::file::close_window(), "Close Window".into()), + (tr::menu::file::open_file(), "Open File…".into()), + (tr::menu::file::open_recent(), "Open Recent".into()), + (tr::menu::file::import_media(), "Import Media…".into()), (tr::menu::file::title(), "File".into()), (tr::menu::edit::copy(), "Copy".into()), (tr::menu::edit::cut(), "Cut".into()), @@ -42,8 +45,32 @@ fn english_matches_the_literals_it_replaced() { (tr::menu::window::title(), "Window".into()), (tr::menu::window::zoom(), "Zoom".into()), (tr::menu::help::title(), "Help".into()), + (tr::menu::help::feedback(), "Give Feedback…".into()), + (tr::menu::help::changelog(), "Changelog".into()), + ( + tr::menu::about::licensed(), + "Licensed under AGPL-3.0".into(), + ), + (tr::menu::about::website(), "Website".into()), + (tr::menu::about::commit(), "Commit".into()), + (tr::menu::help::dashboard(), "Dashboard".into()), + (tr::menu::help::documentation(), "Documentation".into()), + (tr::menu::help::faq(), "FAQ".into()), + (tr::menu::help::self_hosting(), "Self-hosting".into()), + (tr::menu::help::help_center(), "Help Center".into()), + (tr::menu::help::system_status(), "System Status".into()), + (tr::menu::help::discord(), "Join Discord Community".into()), + (tr::menu::update::check(), "Check for Update".into()), + (tr::menu::update::download(), "Download update…".into()), + (tr::menu::update::checking(), "Checking for Update…".into()), + ( + tr::menu::update::downloading(), + "Downloading update…".into(), + ), + (tr::menu::update::restart(), "Restart to update…".into()), (tr::tray::import_media(), "Import Media...".into()), (tr::tray::no_recent_items(), "No recent items".into()), + (tr::tray::recording_mode(), "Recording Mode".into()), (tr::tray::open_main_window(), "Open Main Window".into()), (tr::tray::previous(), "Previous".into()), (tr::tray::quit(), "Quit Cap".into()), @@ -398,6 +425,26 @@ fn english_matches_the_literals_it_replaced() { "Versioned releases (recommended)".into(), ), (tr::settings::general::updates::title(), "Updates".into()), + ( + tr::settings::general::updates::download_policy::label(), + "Update downloads".into(), + ), + ( + tr::settings::general::updates::download_policy::description(), + "Choose whether Cap downloads updates automatically.".into(), + ), + ( + tr::settings::general::updates::download_policy::manual(), + "Ask first".into(), + ), + ( + tr::settings::general::updates::download_policy::auto(), + "Auto".into(), + ), + ( + tr::settings::general::updates::download_policy::never(), + "Never".into(), + ), ( tr::settings::general::updates::channel::description(), "Which release channel Cap updates from.".into(), @@ -440,9 +487,13 @@ fn english_matches_the_literals_it_replaced() { .filter(|key| { !key.starts_with(NEW_IN_THE_LANGUAGE_PICKER) && !super::surface_tests::is_main_surface_key(key) + && !super::settings_surface_tests::is_settings_surface_key(key) + && !super::dialogs_surface_tests::is_dialogs_surface_key(key) }) .count() - - 3; + // `tray.version`, `excluded_windows.missing_body`, `self_host.confirm_change`, + // `menu.update.badge_progress` and `menu.update.badge_version` take arguments. + - 5; assert_eq!( pairs.len(), plain_keys, @@ -770,6 +821,12 @@ fn english_messages_format_like_the_literals_they_replaced() { to sign in again." ) ); + + assert_eq!( + tr::menu::update::badge_progress("0.4.0", 42), + "v0.4.0 · 42%" + ); + assert_eq!(tr::menu::update::badge_version("0.4.0"), "v0.4.0"); } #[test] diff --git a/apps/desktop-gpui/src/import.rs b/apps/desktop-gpui/src/import.rs index 165d7244dcf..abcede4716a 100644 --- a/apps/desktop-gpui/src/import.rs +++ b/apps/desktop-gpui/src/import.rs @@ -25,6 +25,8 @@ use cap_project::{ use ffmpeg::{ChannelLayout, codec as avcodec, format as avformat}; use gpui::{App, Global}; +use crate::i18n::tr; + /// `import.rs:38-44` in the Tauri binary. pub const VIDEO_IMPORT_EXTENSIONS: &[&str] = &["mp4", "mov", "avi", "mkv", "webm", "wmv", "m4v", "flv"]; @@ -292,7 +294,11 @@ pub fn pick_and_import_video(cx: &mut App) { cx.spawn(async move |cx| { // Blocking modal, so from a spawned task with no borrow held -- the // `save_file_panel` rule. - let Some(path) = pick_import_file(&[("Video Files", VIDEO_IMPORT_EXTENSIONS)], cx).await + let Some(path) = pick_import_file( + &[(tr::import::filter::video(), VIDEO_IMPORT_EXTENSIONS)], + cx, + ) + .await else { return; }; @@ -303,7 +309,11 @@ pub fn pick_and_import_video(cx: &mut App) { pub fn pick_and_import_image(cx: &mut App) { cx.spawn(async move |cx| { - let Some(path) = pick_import_file(&[("Image Files", IMAGE_IMPORT_EXTENSIONS)], cx).await + let Some(path) = pick_import_file( + &[(tr::import::filter::image(), IMAGE_IMPORT_EXTENSIONS)], + cx, + ) + .await else { return; }; @@ -318,9 +328,9 @@ pub fn pick_and_import_media(cx: &mut App) { cx.spawn(async move |cx| { let Some(path) = pick_import_file( &[ - ("Media Files", MEDIA_IMPORT_EXTENSIONS), - ("Video Files", VIDEO_IMPORT_EXTENSIONS), - ("Image Files", IMAGE_IMPORT_EXTENSIONS), + (tr::import::filter::media(), MEDIA_IMPORT_EXTENSIONS), + (tr::import::filter::video(), VIDEO_IMPORT_EXTENSIONS), + (tr::import::filter::image(), IMAGE_IMPORT_EXTENSIONS), ], cx, ) @@ -334,7 +344,7 @@ pub fn pick_and_import_media(cx: &mut App) { cx.update(|cx| import_image_from_path(path, cx)); } else { tracing::error!(path = %path.display(), "unsupported media import path"); - show_error_dialog("Unsupported media file.".to_string()); + show_error_dialog(tr::import::unsupported().to_string()); } }) .detach(); @@ -426,12 +436,15 @@ fn show_import_error(kind: ImportKind, message: &str) { ImportKind::Image => "image", }; tracing::error!("failed to import {media}: {message}"); - show_error_dialog(format!("Failed to import {media}: {message}")); + show_error_dialog(match kind { + ImportKind::Video => tr::import::video_failed(message), + ImportKind::Image => tr::import::image_failed(message), + }); } fn show_error_dialog(description: String) { let _ = rfd::MessageDialog::new() - .set_title("Import Error") + .set_title(tr::import::error_title()) .set_description(description) .set_level(rfd::MessageLevel::Error) .set_buttons(rfd::MessageButtons::Ok) @@ -478,7 +491,7 @@ fn run_video_import(source_path: &Path, tx: &flume::Sender) { pretty_name: &project_name, }; - sink.send(ImportStage::Probing, 0.0, "Analyzing video file..."); + sink.send(ImportStage::Probing, 0.0, tr::import::progress::analyzing()); match probe_video_can_decode(source_path) { Ok(true) => {} Ok(false) => { @@ -534,7 +547,11 @@ fn run_video_import(source_path: &Path, tx: &flume::Sender) { return; } - sink.send(ImportStage::Converting, 0.0, "Starting conversion..."); + sink.send( + ImportStage::Converting, + 0.0, + tr::import::progress::starting(), + ); let result = transcode_video( source_path, &output_video_path, @@ -544,7 +561,7 @@ fn run_video_import(source_path: &Path, tx: &flume::Sender) { sink.send( ImportStage::Converting, progress, - &format!("Converting video... {}%", (progress * 100.0) as u32), + &tr::import::progress::converting((progress * 100.0) as u32), ); }, None, @@ -583,7 +600,7 @@ fn run_video_import(source_path: &Path, tx: &flume::Sender) { sink.send( ImportStage::Finalizing, 0.95, - "Creating project metadata...", + tr::import::progress::metadata(), ); // `import.rs:1490-1504`: an Opus file this small is headers with no @@ -618,7 +635,7 @@ fn run_video_import(source_path: &Path, tx: &flume::Sender) { tracing::warn!("could not write the imported video's thumbnail: {error}"); } - sink.send(ImportStage::Complete, 1.0, "Import complete!"); + sink.send(ImportStage::Complete, 1.0, tr::import::progress::complete()); tracing::info!(path = %project_path.display(), "video import complete"); } @@ -665,7 +682,7 @@ fn run_image_import(source_path: &Path, tx: &flume::Sender) { pretty_name: &project_name, }; - sink.send(ImportStage::Probing, 0.0, "Importing image..."); + sink.send(ImportStage::Probing, 0.0, tr::import::progress::image()); let (width, height, rgba) = match decode_image_rgba(source_path) { Ok(decoded) => decoded, @@ -729,7 +746,7 @@ fn run_image_import(source_path: &Path, tx: &flume::Sender) { return; } - sink.send(ImportStage::Complete, 1.0, "Import complete!"); + sink.send(ImportStage::Complete, 1.0, tr::import::progress::complete()); tracing::info!(path = %project_path.display(), "image import complete"); } diff --git a/apps/desktop-gpui/src/library.rs b/apps/desktop-gpui/src/library.rs index 5e3f9c31f47..8c4906d58bf 100644 --- a/apps/desktop-gpui/src/library.rs +++ b/apps/desktop-gpui/src/library.rs @@ -754,6 +754,14 @@ fn screenshot_items_in(dir: &Path) -> impl Iterator { }) } +/// Whether a `.cap` directory is a screenshot rather than a recording. The +/// same rule `list_screenshots` uses, so the two never disagree on a bundle. +pub fn is_screenshot_bundle(bundle: &Path) -> bool { + bundle.extension().and_then(|ext| ext.to_str()) == Some("cap") + && bundle.is_dir() + && screenshot_png(bundle).is_some() +} + fn screenshot_png(bundle: &Path) -> Option<(PathBuf, f64)> { let original = bundle.join("original.png"); if let Ok(metadata) = original.metadata() { diff --git a/apps/desktop-gpui/src/main.rs b/apps/desktop-gpui/src/main.rs index e2edff04f36..db32a0fa617 100644 --- a/apps/desktop-gpui/src/main.rs +++ b/apps/desktop-gpui/src/main.rs @@ -47,6 +47,7 @@ mod i18n; mod import; mod installer; mod jpeg_thumbnail; +mod library; #[cfg(not(any(target_os = "macos", target_os = "windows")))] mod linux_dialog; #[cfg(target_os = "linux")] @@ -55,7 +56,6 @@ mod linux_display; mod linux_portal_id; #[cfg(target_os = "linux")] mod linux_wallpaper; -mod library; mod main_window; mod media_access; mod menus; @@ -893,8 +893,8 @@ mod runtime_tests { #[cfg(test)] mod logging_tests { use super::create_log_appender; - use tracing_subscriber::Layer as _; use std::{io::Write, path::PathBuf}; + use tracing_subscriber::{Layer as _, layer::SubscriberExt as _}; struct LogDirectory(PathBuf); @@ -920,7 +920,6 @@ mod logging_tests { #[test] fn the_default_filter_drops_zbus_property_cache_warnings_only() { use std::sync::{Arc, Mutex}; - use tracing_subscriber::layer::SubscriberExt as _; #[derive(Clone, Default)] struct Lines(Arc>>); diff --git a/apps/desktop-gpui/src/main_window.rs b/apps/desktop-gpui/src/main_window.rs index 48989b3b09f..f3f8cfe9155 100644 --- a/apps/desktop-gpui/src/main_window.rs +++ b/apps/desktop-gpui/src/main_window.rs @@ -169,7 +169,7 @@ impl Mode { /// [`Self::panel_title`] in the UI language. `panel_title` stays English: /// it names element ids and log fields. - fn display_name(self) -> &'static str { + pub(crate) fn display_name(self) -> &'static str { match self { Self::Instant => tr::main::mode::name::instant(), Self::Studio => tr::main::mode::name::studio(), @@ -1112,7 +1112,7 @@ impl MainWindow { Self { theme, // `rawOptions.mode` -- the recording mode is a persisted setting, - // and the tray's Select Mode submenu writes the same key, so the + // and the tray's mode picker writes the same key, so the // window has to start where the store left it rather than at a // hardcoded Instant. mode: Mode::from_store(), @@ -1916,7 +1916,7 @@ impl MainWindow { // `RecordingSettingsStore::set_mode` plus the two tray refreshes it // triggers (`handle_mode_selection`). Every mode affordance -- the // pill, the info panel, the mode select window and the tray's own - // Select Mode submenu -- reaches this method, so this is the one place + // mode picker -- reaches this method, so this is the one place // the setting is written. // Written inline, like every other store write in this app (and like // `RecordingSettingsStore::set_mode` itself): a background write would @@ -4069,7 +4069,7 @@ impl MainWindow { .id("panel-back") .when(self.keyboard_nav, ui::keyboard_nav::stop) .role(gpui::Role::Button) - .aria_label("Back") + .aria_label(tr::common::back()) .flex() .flex_row() .items_center() diff --git a/apps/desktop-gpui/src/menus.rs b/apps/desktop-gpui/src/menus.rs index 118f858230b..675c96b63cb 100644 --- a/apps/desktop-gpui/src/menus.rs +++ b/apps/desktop-gpui/src/menus.rs @@ -7,8 +7,9 @@ //! Tauri app decides between Regular and Accessory from exactly the set of //! windows ⌘W changes. //! -//! The menu itself is [`build_menus`], a byte-for-byte transcription of -//! `build_macos_app_menu` (`apps/desktop/src-tauri/src/lib.rs:479-566`). gpui +//! The menu itself is [`build_menus`], the GPUI model for the native menu bar. +//! macOS standard menu items are upgraded in `platform` after GPUI installs it. +//! gpui //! builds the real `NSMenu` from it and takes each item's key equivalent from //! the *keymap*, so an action with no `KeyBinding` renders without a shortcut //! and an action with no live handler renders disabled @@ -36,12 +37,83 @@ use crate::{ settings_window::Page, store, ui::text_input, + updates, }; /// `productName` in `tauri.conf.json`, which is what macOS renames the first /// submenu to. pub const APP_NAME: &str = "Cap"; +struct MenuRegistration(UpdateRow); + +impl Global for MenuRegistration {} + +/// What the Check for Update row currently says. Download progress only moves +/// the badge, and only macOS applies it without rebuilding the menu bar, so a +/// percentage tick never touches the menu on the other platforms. +#[derive(Clone, PartialEq)] +struct UpdateRow { + title: &'static str, + enabled: bool, + badge: Option, +} + +impl UpdateRow { + fn of(status: &updates::UpdateStatus) -> Self { + use updates::UpdateStatus; + + let title = match status { + UpdateStatus::Idle => tr::menu::update::check(), + UpdateStatus::Checking => tr::menu::update::checking(), + UpdateStatus::Downloading { fraction: None, .. } => tr::menu::update::download(), + UpdateStatus::Downloading { .. } | UpdateStatus::Installing { .. } => { + tr::menu::update::downloading() + } + UpdateStatus::Ready { .. } | UpdateStatus::Installed { .. } => { + tr::menu::update::restart() + } + }; + Self { + title, + enabled: !matches!( + status, + UpdateStatus::Checking + | UpdateStatus::Downloading { + fraction: Some(_), + .. + } + | UpdateStatus::Installing { .. } + ), + badge: update_menu_badge(status), + } + } +} + +#[cfg(target_os = "macos")] +fn update_menu_badge(status: &updates::UpdateStatus) -> Option { + use updates::UpdateStatus; + + match status { + UpdateStatus::Downloading { + version, + fraction: Some(fraction), + } => Some(tr::menu::update::badge_progress( + version, + (fraction * 100.0).round() as u32, + )), + UpdateStatus::Downloading { version, .. } + | UpdateStatus::Ready { version } + | UpdateStatus::Installing { version } + | UpdateStatus::Installed { version, .. } => Some(tr::menu::update::badge_version(version)), + UpdateStatus::Idle | UpdateStatus::Checking => None, + } +} + +#[cfg(not(target_os = "macos"))] +fn update_menu_badge(_: &updates::UpdateStatus) -> Option { + None +} + /// `env!("CARGO_PKG_VERSION")`, exactly as `build_tray_menu` spells it -- this /// crate's version, which is the gpui app's own (0.1.x) rather than the /// shipping desktop app's. Noted as a deviation in the report: there is no @@ -55,12 +127,26 @@ actions!( [ /// `PredefinedMenuItem::about` -- `orderFrontStandardAboutPanel`. About, + CheckForUpdate, /// `PredefinedMenuItem::hide` -- ⌘H. HideSelf, /// `PredefinedMenuItem::hide_others` -- ⌥⌘H. HideOthers, /// The Cap menu's explicit Quit item -- ⌘Q. Quit, + /// Settings… -- ⌘, on macOS, Ctrl+, on Windows and Linux. + OpenSettings, + OpenFile, + ImportMedia, + Changelog, + HelpDashboard, + HelpDocs, + HelpFaq, + HelpSelfHosting, + HelpCenter, + HelpStatus, + HelpDiscord, + HelpFeedback, /// `PredefinedMenuItem::close_window` -- ⌘W. Present twice in the /// Tauri menu (File and Window), one action either way. CloseWindow, @@ -71,8 +157,6 @@ actions!( Zoom, /// `PredefinedMenuItem::fullscreen` -- ⌃⌘F. ToggleFullscreen, - /// Settings… -- ⌘, on macOS, Ctrl+, on Windows and Linux. - OpenSettings, ] ); @@ -87,15 +171,52 @@ pub fn init(cx: &mut App) { cx.bind_keys(bindings()); cx.on_action(|_: &About, cx: &mut App| { - // Ordering a window front re-enters gpui's window callbacks, so it runs - // from a task with no borrow held (`platform::place_overlay_panel`'s - // rule). cx.spawn(async move |_| platform::show_about_panel(APP_NAME, app_version())) .detach(); }); + cx.on_action(|_: &CheckForUpdate, cx: &mut App| { + updates::check_manually(cx); + }); cx.on_action(|_: &HideSelf, cx: &mut App| cx.hide()); cx.on_action(|_: &HideOthers, cx: &mut App| cx.hide_other_apps()); cx.on_action(|_: &Quit, cx: &mut App| quit(cx)); + cx.on_action(|_: &OpenFile, cx: &mut App| { + let (files, directories) = if cfg!(target_os = "macos") { + (true, false) + } else { + (false, true) + }; + let paths = cx.prompt_for_paths(gpui::PathPromptOptions { + files, + directories, + multiple: false, + prompt: Some("Open Recording".into()), + }); + cx.spawn(async move |cx| { + if let Ok(Ok(Some(paths))) = paths.await + && let Some(path) = paths.into_iter().next() + { + cx.update(|cx| crate::tray::open_previous_item(path, cx)); + } + }) + .detach(); + }); + cx.on_action(|_: &ImportMedia, cx: &mut App| { + crate::import::pick_and_import_media(cx); + }); + cx.on_action(|_: &Changelog, cx: &mut App| { + app_windows::open_settings(crate::settings_window::Page::Changelog, cx) + }); + cx.on_action(|_: &HelpDashboard, cx: &mut App| cx.open_url("https://cap.so/dashboard")); + cx.on_action(|_: &HelpDocs, cx: &mut App| cx.open_url("https://cap.so/docs")); + cx.on_action(|_: &HelpFaq, cx: &mut App| cx.open_url("https://cap.so/faq")); + cx.on_action(|_: &HelpSelfHosting, cx: &mut App| cx.open_url("https://cap.so/self-hosting")); + cx.on_action(|_: &HelpCenter, cx: &mut App| cx.open_url("https://help.cap.so/")); + cx.on_action(|_: &HelpStatus, cx: &mut App| cx.open_url("https://cap.openstatus.dev/")); + cx.on_action(|_: &HelpDiscord, cx: &mut App| cx.open_url("https://cap.link/discord")); + cx.on_action(|_: &HelpFeedback, cx: &mut App| { + app_windows::open_settings(crate::settings_window::Page::Feedback, cx) + }); cx.on_action(|_: &CloseWindow, cx: &mut App| close_key_window(cx)); cx.on_action(|_: &Minimize, cx: &mut App| with_key_native(cx, platform::minimize_native)); cx.on_action(|_: &Zoom, cx: &mut App| with_key_native(cx, platform::zoom_native)); @@ -133,7 +254,8 @@ pub fn init(cx: &mut App) { with_key_native(cx, platform::toggle_fullscreen_native) }); - cx.set_menus(build_menus()); + cx.set_global(MenuRegistration(UpdateRow::of(&updates::status(cx)))); + install_menus(cx); MENUS_INSTALLED.store(true, Ordering::Relaxed); #[cfg(target_os = "macos")] if crate::i18n::current() != crate::i18n::Locale::En { @@ -148,7 +270,7 @@ pub fn refresh(cx: &mut App) { if !MENUS_INSTALLED.load(Ordering::Relaxed) { return; } - cx.set_menus(build_menus()); + install_menus(cx); #[cfg(target_os = "macos")] register_system_menus(); } @@ -164,6 +286,51 @@ fn register_system_menus() { ); } +pub(crate) fn refresh_update_menu(cx: &mut App) { + if !cx.has_global::() { + return; + } + + let row = UpdateRow::of(&updates::status(cx)); + if cx.global::().0 == row { + return; + } + cx.global_mut::().0 = row.clone(); + + #[cfg(target_os = "macos")] + platform::set_update_menu_item(row.title, row.badge.as_deref(), row.enabled); + #[cfg(not(target_os = "macos"))] + install_menus(cx); +} + +pub(crate) fn reapply_update_row(cx: &mut App) { + if !cx.has_global::() { + return; + } + let row = UpdateRow::of(&updates::status(cx)); + cx.global_mut::().0 = row.clone(); + #[cfg(target_os = "macos")] + platform::set_update_menu_item(row.title, row.badge.as_deref(), row.enabled); + #[cfg(not(target_os = "macos"))] + install_menus(cx); +} + +fn install_menus(cx: &mut App) { + let update_status = updates::status(cx); + let settings_enabled = !crate::app_windows::onboarding_is_open(cx); + cx.set_menus(build_menus(&update_status, settings_enabled)); + #[cfg(target_os = "macos")] + { + platform::patch_standard_application_menu(); + // The badge lives only on the native item, so a rebuilt menu bar + // (a language change, onboarding finishing) needs it put back. + let row = UpdateRow::of(&update_status); + if row.badge.is_some() { + platform::set_update_menu_item(row.title, row.badge.as_deref(), row.enabled); + } + } +} + pub(crate) fn bindings() -> Vec { vec![ KeyBinding::new("cmd-q", Quit, None), @@ -184,15 +351,19 @@ fn settings_shortcut_allowed(idle: bool, onboarding_open: bool, hotkeys_paused: /// /// One-to-one with `build_macos_app_menu`, including the empty Help submenu /// (`Submenu::with_id_and_items(.., HELP_SUBMENU_ID, "Help", true, &[])`). -pub fn build_menus() -> Vec { +pub fn build_menus(update_status: &updates::UpdateStatus, settings_enabled: bool) -> Vec { + let update_row = UpdateRow::of(update_status); + vec![ Menu { name: APP_NAME.into(), disabled: false, items: vec![ MenuItem::action(tr::menu::app::about(), About), + MenuItem::action(update_row.title, CheckForUpdate).disabled(!update_row.enabled), MenuItem::separator(), - MenuItem::action(tr::menu::app::settings(), OpenSettings), + MenuItem::action(tr::menu::app::settings(), OpenSettings) + .disabled(!settings_enabled), MenuItem::separator(), MenuItem::os_submenu(tr::menu::app::services(), SystemMenuType::Services), MenuItem::separator(), @@ -205,10 +376,12 @@ pub fn build_menus() -> Vec { Menu { name: tr::menu::file::title().into(), disabled: false, - items: vec![MenuItem::action( - tr::menu::file::close_window(), - CloseWindow, - )], + items: vec![ + MenuItem::action(tr::menu::file::open_file(), OpenFile), + MenuItem::action(tr::menu::file::import_media(), ImportMedia), + MenuItem::separator(), + MenuItem::action(tr::menu::file::close_window(), CloseWindow), + ], }, Menu { name: tr::menu::edit::title().into(), @@ -262,7 +435,19 @@ pub fn build_menus() -> Vec { Menu { name: tr::menu::help::title().into(), disabled: false, - items: vec![], + items: vec![ + MenuItem::action(tr::menu::help::feedback(), HelpFeedback), + MenuItem::separator(), + MenuItem::action(tr::menu::help::changelog(), Changelog), + MenuItem::separator(), + MenuItem::action(tr::menu::help::dashboard(), HelpDashboard), + MenuItem::action(tr::menu::help::documentation(), HelpDocs), + MenuItem::action(tr::menu::help::faq(), HelpFaq), + MenuItem::action(tr::menu::help::self_hosting(), HelpSelfHosting), + MenuItem::action(tr::menu::help::help_center(), HelpCenter), + MenuItem::action(tr::menu::help::system_status(), HelpStatus), + MenuItem::action(tr::menu::help::discord(), HelpDiscord), + ], }, ] } @@ -1046,8 +1231,17 @@ mod tests { } #[test] - fn menu_bar_matches_the_tauri_structure() { - let menus: Vec<_> = build_menus().iter().map(describe).collect(); + fn menu_bar_includes_native_and_cap_destinations() { + let menus: Vec<_> = build_menus(&updates::UpdateStatus::Idle, true) + .iter() + .map(describe) + .collect(); + let expected_file_items = vec![ + "Open File…".into(), + "Import Media…".into(), + "-".into(), + "Close Window".into(), + ]; assert_eq!( menus, vec![ @@ -1055,8 +1249,9 @@ mod tests { "Cap".to_string(), vec![ "About Cap".to_string(), + "Check for Update".into(), "-".into(), - "Settings…".into(), + tr::menu::app::settings().into(), "-".into(), "Services".into(), "-".into(), @@ -1066,7 +1261,7 @@ mod tests { "Quit Cap".into(), ] ), - ("File".into(), vec!["Close Window".into()]), + ("File".into(), expected_file_items,), ( "Edit".into(), vec![ @@ -1089,11 +1284,134 @@ mod tests { "Close Window".into(), ] ), - ("Help".into(), vec![]), + ( + "Help".into(), + vec![ + "Give Feedback…".into(), + "-".into(), + "Changelog".into(), + "-".into(), + "Dashboard".into(), + "Documentation".into(), + "FAQ".into(), + "Self-hosting".into(), + "Help Center".into(), + "System Status".into(), + "Join Discord Community".into(), + ], + ), ] ); } + /// The row is the second item of the App menu (`menu_bar_...` pins that + /// order); it greys out while a check or download is running so a second + /// click cannot fire another one. + #[test] + fn the_update_row_greys_out_while_busy() { + fn row(status: &updates::UpdateStatus) -> (String, bool) { + let menus = build_menus(status, true); + match &menus[0].items[1] { + MenuItem::Action { name, disabled, .. } => (name.to_string(), *disabled), + _ => panic!("the update row is an action"), + } + } + + assert_eq!( + row(&updates::UpdateStatus::Idle), + ("Check for Update".to_string(), false) + ); + assert_eq!( + row(&updates::UpdateStatus::Checking), + ("Checking for Update…".to_string(), true) + ); + assert_eq!( + row(&updates::UpdateStatus::Downloading { + version: semver::Version::parse("0.4.0").unwrap(), + fraction: Some(0.42), + }), + ("Downloading update…".to_string(), true) + ); + assert_eq!( + row(&updates::UpdateStatus::Installing { + version: semver::Version::parse("0.4.0").unwrap(), + }), + ("Downloading update…".to_string(), true) + ); + } + + /// The version lives only in the badge, and a percentage tick changes + /// nothing else -- that is what keeps a download from reinstalling the + /// menu bar on every progress event. + #[test] + fn the_update_row_keeps_the_version_out_of_the_title() { + use semver::Version; + use updates::UpdateStatus; + + let version = Version::parse("0.4.0").unwrap(); + let downloading = |fraction| UpdateStatus::Downloading { + version: version.clone(), + fraction, + }; + + let cold = UpdateRow::of(&downloading(None)); + let warm = UpdateRow::of(&downloading(Some(0.42))); + assert_eq!(cold.title, "Download update…"); + assert!(cold.enabled); + assert_eq!(warm.title, "Downloading update…"); + assert!(!warm.enabled); + + let ready = UpdateRow::of(&UpdateStatus::Ready { + version: version.clone(), + }); + let installed = UpdateRow::of(&UpdateStatus::Installed { + version: version.clone(), + executable: std::path::PathBuf::from("/Applications/Cap.app"), + }); + assert_eq!(ready.title, "Restart to update…"); + assert_eq!(installed.title, ready.title); + assert_eq!(installed.badge, ready.badge); + + let installing = UpdateRow::of(&UpdateStatus::Installing { + version: version.clone(), + }); + assert_eq!(installing.title, "Downloading update…"); + assert!(!installing.enabled); + } + + #[cfg(target_os = "macos")] + #[test] + fn the_update_badge_shows_the_version_and_live_progress() { + use semver::Version; + use updates::UpdateStatus; + + let version = Version::parse("0.4.0").unwrap(); + let badge = |status| UpdateRow::of(&status).badge; + + assert_eq!( + badge(UpdateStatus::Downloading { + version: version.clone(), + fraction: None, + }), + Some("v0.4.0".to_string()) + ); + assert_eq!( + badge(UpdateStatus::Downloading { + version: version.clone(), + fraction: Some(0.42), + }), + Some("v0.4.0 · 42%".to_string()) + ); + assert_eq!( + badge(UpdateStatus::Ready { + version: version.clone(), + }), + Some("v0.4.0".to_string()) + ); + assert_eq!(badge(UpdateStatus::Idle), None); + assert_eq!(badge(UpdateStatus::Checking), None); + } + /// `sync_macos_dock_visibility`'s three branches. #[test] fn dock_policy_matches_the_tauri_rule() { diff --git a/apps/desktop-gpui/src/mode_select_window.rs b/apps/desktop-gpui/src/mode_select_window.rs index 739576a183e..70e28e36949 100644 --- a/apps/desktop-gpui/src/mode_select_window.rs +++ b/apps/desktop-gpui/src/mode_select_window.rs @@ -13,6 +13,7 @@ use gpui::{ div, prelude::FluentBuilder, px, svg, }; +use crate::i18n::tr; use crate::theme::LocaleFont as _; use crate::{main_window::Mode, theme::Theme, ui::keyboard_nav}; @@ -31,9 +32,9 @@ impl Mode { /// app carries all three; so do we. fn card_description(self) -> &'static str { match self { - Self::Instant => "Share instantly with a link. Uploads as you record.", - Self::Studio => "Highest quality local recording for editing later.", - Self::Screenshot => "Capture and annotate screenshots instantly.", + Self::Instant => tr::dialog::mode_select::instant(), + Self::Studio => tr::dialog::mode_select::studio(), + Self::Screenshot => tr::dialog::mode_select::screenshot(), } } } @@ -111,7 +112,7 @@ impl ModeSelectWindow { .id(SharedString::from(mode.panel_title())) .when(self.keyboard_nav, keyboard_nav::stop) .role(Role::Button) - .aria_label(mode.panel_title()) + .aria_label(mode.display_name()) .aria_description(mode.card_description()) .aria_selected(selected) .relative() @@ -227,7 +228,7 @@ impl ModeSelectWindow { } else { Hsla::from(theme.gray_12) }) - .child(mode.panel_title()), + .child(mode.display_name()), ) .child( // `text-xs leading-relaxed text-gray-11 line-clamp-3` @@ -292,14 +293,14 @@ impl Render for ModeSelectWindow { .text_size(px(20.)) .font_weight(FontWeight::BOLD) .text_color(Hsla::from(theme.gray_12)) - .child("Choose Recording Mode"), + .child(tr::dialog::mode_select::title()), ) .child( // `text-sm text-gray-11` div() .text_size(px(14.)) .text_color(Hsla::from(theme.gray_11)) - .child("Select how you want to capture your screen"), + .child(tr::dialog::mode_select::subtitle()), ), ) .child( diff --git a/apps/desktop-gpui/src/onboarding_window.rs b/apps/desktop-gpui/src/onboarding_window.rs index a19f48ab12b..3703af15c26 100644 --- a/apps/desktop-gpui/src/onboarding_window.rs +++ b/apps/desktop-gpui/src/onboarding_window.rs @@ -1246,9 +1246,13 @@ impl OnboardingWindow { .justify_center() .gap(px(4.)) .child( - copy(tr::onboarding::welcome::click_or_press(), 11., ink.opacity(0.55)) - .font_weight(FontWeight::NORMAL) - .line_height(px(14.)), + copy( + tr::onboarding::welcome::click_or_press(), + 11., + ink.opacity(0.55), + ) + .font_weight(FontWeight::NORMAL) + .line_height(px(14.)), ) .child( copy(tr::onboarding::welcome::space_key(), 10., ink.opacity(0.7)) @@ -1290,18 +1294,14 @@ impl OnboardingWindow { .child(welcome_script(written, title_opacity)), ) .child( - copy( - tr::onboarding::welcome::tagline(), - 22., - ink.opacity(0.75), - ) - .relative() - .top(px(subtitle_y)) - .opacity(subtitle_opacity) - .font_family("Source Serif 4") - .font_weight(FontWeight::LIGHT) - .line_height(px(32.)) - .whitespace_nowrap(), + copy(tr::onboarding::welcome::tagline(), 22., ink.opacity(0.75)) + .relative() + .top(px(subtitle_y)) + .opacity(subtitle_opacity) + .font_family("Source Serif 4") + .font_weight(FontWeight::LIGHT) + .line_height(px(32.)) + .whitespace_nowrap(), ) .child(button) .when_some(self.error.clone(), |el, error| { @@ -1342,18 +1342,64 @@ impl OnboardingWindow { fn render_overview(&self) -> AnyElement { let theme = self.theme; - div().flex().flex_col().items_center().justify_center().min_h_full().px(px(40.)).gap(px(32.)) - .child(self.heading(tr::onboarding::overview::title(), tr::onboarding::overview::subtitle())) - .child(div().flex().gap(px(16.)).w_full().max_w(px(540.)).children(MODES.iter().enumerate().map(|(index, mode)| { - let paper = stage::Paper::new(&theme); - let card = self.paper_card().flex_1().min_w_0().flex().flex_col().items_center().gap(px(10.)).px(px(20.)).pt(px(22.)).pb(px(24.)) - .child(crate::doodle::doodle(mode_doodle(mode.mode), paper.palette(), self.step_at + Duration::from_millis(index as u64 * 180), !self.animated, 96., 84.)) - .child(copy((mode.title)(), 15., paper.ink).font_weight(FontWeight::MEDIUM).text_center().mt(px(4.))) - .child(copy((mode.tagline)(), 12., paper.soft).font_weight(FontWeight::NORMAL).line_height(px(16.)).text_center()); - if !self.animated { return card.into_any_element(); } - card.with_animation(("overview-card", index), Animation::new(Duration::from_millis(700 + index as u64 * 100)), - |el, t| el.relative().top(px(16. * (1. - ease(t)))).opacity(ease(t))).into_any_element() - }))).into_any_element() + div() + .flex() + .flex_col() + .items_center() + .justify_center() + .min_h_full() + .px(px(40.)) + .gap(px(32.)) + .child(self.heading( + tr::onboarding::overview::title(), + tr::onboarding::overview::subtitle(), + )) + .child(div().flex().gap(px(16.)).w_full().max_w(px(540.)).children( + MODES.iter().enumerate().map(|(index, mode)| { + let paper = stage::Paper::new(&theme); + let card = self + .paper_card() + .flex_1() + .min_w_0() + .flex() + .flex_col() + .items_center() + .gap(px(10.)) + .px(px(20.)) + .pt(px(22.)) + .pb(px(24.)) + .child(crate::doodle::doodle( + mode_doodle(mode.mode), + paper.palette(), + self.step_at + Duration::from_millis(index as u64 * 180), + !self.animated, + 96., + 84., + )) + .child( + copy((mode.title)(), 15., paper.ink) + .font_weight(FontWeight::MEDIUM) + .text_center() + .mt(px(4.)), + ) + .child( + copy((mode.tagline)(), 12., paper.soft) + .font_weight(FontWeight::NORMAL) + .line_height(px(16.)) + .text_center(), + ); + if !self.animated { + return card.into_any_element(); + } + card.with_animation( + ("overview-card", index), + Animation::new(Duration::from_millis(700 + index as u64 * 100)), + |el, t| el.relative().top(px(16. * (1. - ease(t)))).opacity(ease(t)), + ) + .into_any_element() + }), + )) + .into_any_element() } fn render_detail(&self, index: usize) -> AnyElement { @@ -1540,18 +1586,60 @@ impl OnboardingWindow { fn render_shortcuts(&self) -> AnyElement { let theme = self.theme; - div().flex().flex_col().items_center().justify_center().min_h_full().px(px(48.)).gap(px(24.)) - .child(self.heading(tr::onboarding::customize::title(), tr::onboarding::customize::subtitle())) - .child(div().flex().flex_col().w_full().max_w(px(420.)).gap(px(8.)).children([ - (tr::onboarding::customize::shortcuts::title(), tr::onboarding::customize::shortcuts::description()), - (tr::onboarding::customize::s3::title(), tr::onboarding::customize::s3::description()), - (tr::onboarding::customize::domain::title(), tr::onboarding::customize::domain::description()), - (tr::onboarding::customize::preferences::title(), tr::onboarding::customize::preferences::description()), - ].into_iter().map(|(title, description)| { - self.paper_card().rounded(px(16.)).px(px(18.)).py(px(13.)).flex().flex_col().gap(px(4.)) - .child(copy(title, 13., theme.gray_12).font_weight(FontWeight::MEDIUM)) - .child(copy(description, 11., theme.gray_10).line_height(px(15.))) - }))) + div() + .flex() + .flex_col() + .items_center() + .justify_center() + .min_h_full() + .px(px(48.)) + .gap(px(24.)) + .child(self.heading( + tr::onboarding::customize::title(), + tr::onboarding::customize::subtitle(), + )) + .child( + div() + .flex() + .flex_col() + .w_full() + .max_w(px(420.)) + .gap(px(8.)) + .children( + [ + ( + tr::onboarding::customize::shortcuts::title(), + tr::onboarding::customize::shortcuts::description(), + ), + ( + tr::onboarding::customize::s3::title(), + tr::onboarding::customize::s3::description(), + ), + ( + tr::onboarding::customize::domain::title(), + tr::onboarding::customize::domain::description(), + ), + ( + tr::onboarding::customize::preferences::title(), + tr::onboarding::customize::preferences::description(), + ), + ] + .into_iter() + .map(|(title, description)| { + self.paper_card() + .rounded(px(16.)) + .px(px(18.)) + .py(px(13.)) + .flex() + .flex_col() + .gap(px(4.)) + .child( + copy(title, 13., theme.gray_12).font_weight(FontWeight::MEDIUM), + ) + .child(copy(description, 11., theme.gray_10).line_height(px(15.))) + }), + ), + ) .child(copy(tr::onboarding::customize::footer(), 12., theme.gray_9)) .into_any_element() } @@ -1621,13 +1709,17 @@ impl OnboardingWindow { ) .when(index == 0, |el| { el.child( - copy(tr::onboarding::faq::pricing(), 13., theme.blue_10) - .id("onboarding-faq-pricing") - .cursor_pointer() - .mt(px(8.)) - .on_click(|_, _, cx| { - cx.open_url(crate::auth::PRICING_URL) - }), + copy( + tr::onboarding::faq::pricing(), + 13., + theme.blue_10, + ) + .id("onboarding-faq-pricing") + .cursor_pointer() + .mt(px(8.)) + .on_click(|_, _, cx| { + cx.open_url(crate::auth::PRICING_URL) + }), ) }), ) @@ -1912,12 +2004,17 @@ impl OnboardingWindow { .flex() .flex_col() .child( - copy(tr::onboarding::demo::start_recording(), 15., gpui::white()) - .font_weight(FontWeight::MEDIUM) - .whitespace_nowrap(), + copy( + tr::onboarding::demo::start_recording(), + 15., + gpui::white(), + ) + .font_weight(FontWeight::MEDIUM) + .whitespace_nowrap(), ) .child( - copy((MODES[index].title)(), 11., gpui::white()).opacity(0.9), + copy((MODES[index].title)(), 11., gpui::white()) + .opacity(0.9), ), ), ) @@ -2265,7 +2362,10 @@ impl OnboardingWindow { .border_color(theme.gray_3) .bg(theme.body_fill(2)), ) - .child(copy(tr::onboarding::demo::background(), 8., theme.gray_9).mt(px(4.))) + .child( + copy(tr::onboarding::demo::background(), 8., theme.gray_9) + .mt(px(4.)), + ) .child(div().flex().gap(px(4.)).children( [0x667eea, 0xf78c88, 0xcccccc].map(|color| { div() @@ -2557,7 +2657,11 @@ impl OnboardingWindow { 12., self.success_palette().2, )) - .child(copy(tr::onboarding::demo::copied_to_clipboard(), 11., theme.gray_12)), + .child(copy( + tr::onboarding::demo::copied_to_clipboard(), + 11., + theme.gray_12, + )), ), ) }), diff --git a/apps/desktop-gpui/src/onboarding_window/stage.rs b/apps/desktop-gpui/src/onboarding_window/stage.rs index 68396015e58..cc30fa10e10 100644 --- a/apps/desktop-gpui/src/onboarding_window/stage.rs +++ b/apps/desktop-gpui/src/onboarding_window/stage.rs @@ -15,6 +15,7 @@ use gpui::{ use super::OnboardingWindow; use crate::doodle::{self, Doodle, Palette}; +use crate::i18n::tr; use crate::onboarding_doodles as art; use crate::permissions::{self, OSPermission, OSPermissionStatus}; use crate::permissions_ui::RowAction; @@ -146,35 +147,35 @@ struct Copy { fn copy_for(permission: OSPermission) -> Copy { match permission { OSPermission::ScreenRecording => Copy { - title: "Let Cap see your screen", - body: "Screen Recording lets Cap capture your displays, windows and system audio.", - done_title: "Screen recording is on", - done_body: "Cap can capture your screen.", - allow: "Allow screen recording", + title: tr::permission::onboarding::screen::title(), + body: tr::permission::onboarding::screen::body(), + done_title: tr::permission::onboarding::screen::done_title(), + done_body: tr::permission::onboarding::screen::done_body(), + allow: tr::permission::onboarding::screen::allow(), doodle: &art::SCREEN, }, OSPermission::Accessibility => Copy { - title: "Let Cap follow your cursor", - body: "Accessibility lets Cap see your clicks and cursor, on this Mac only, so auto zoom knows where to look.", - done_title: "Accessibility is on", - done_body: "Auto zoom can follow your clicks.", - allow: "Allow accessibility", + title: tr::permission::onboarding::accessibility::title(), + body: tr::permission::onboarding::accessibility::body(), + done_title: tr::permission::onboarding::accessibility::done_title(), + done_body: tr::permission::onboarding::accessibility::done_body(), + allow: tr::permission::onboarding::accessibility::allow(), doodle: &art::CURSOR, }, OSPermission::Microphone => Copy { - title: "Let Cap hear you", - body: "Add your voice to your recordings. You can skip this and turn it on later.", - done_title: "Microphone is on", - done_body: "Your voice can be in your recordings.", - allow: "Allow microphone", + title: tr::permission::onboarding::microphone::title(), + body: tr::permission::onboarding::microphone::body(), + done_title: tr::permission::onboarding::microphone::done_title(), + done_body: tr::permission::onboarding::microphone::done_body(), + allow: tr::permission::onboarding::microphone::allow(), doodle: &art::MICROPHONE, }, OSPermission::Camera => Copy { - title: "Let Cap see you", - body: "Show yourself in a corner of your recordings. Skip it if you don't need it.", - done_title: "Camera is on", - done_body: "You can add your camera to any recording.", - allow: "Allow camera", + title: tr::permission::onboarding::camera::title(), + body: tr::permission::onboarding::camera::body(), + done_title: tr::permission::onboarding::camera::done_title(), + done_body: tr::permission::onboarding::camera::done_body(), + allow: tr::permission::onboarding::camera::allow(), doodle: &art::CAMERA, }, } @@ -329,13 +330,13 @@ impl OnboardingWindow { fn render_checklist(&self, paper: &Paper, cx: &mut Context) -> Div { let (title, lede) = if self.permissions_only { ( - "Cap needs a permission back", - "Something was switched off in System Settings. Turn it back on to keep recording.", + tr::permission::onboarding::checklist::regained_title(), + tr::permission::onboarding::checklist::regained_lede(), ) } else { ( - "Let's get Cap ready", - "macOS asks you to OK a few things first. It only takes a minute.", + tr::permission::onboarding::checklist::title(), + tr::permission::onboarding::checklist::lede(), ) }; let stage = self.current_stage(); @@ -398,15 +399,15 @@ impl OnboardingWindow { Mark::Skipped => &art::MARK_SKIPPED, }; let (caption, caption_color) = if status.permitted() { - ("Allowed", paper.green) + (tr::permission::onboarding::status::allowed(), paper.green) } else if self.is_skipped(permission) { - ("Skipped for now", paper.soft) + (tr::permission::onboarding::status::skipped(), paper.soft) } else if status == OSPermissionStatus::Denied { - ("Off in System Settings", paper.red) + (tr::permission::onboarding::status::off(), paper.red) } else if permission.required() { - ("Required", paper.soft) + (tr::permission::onboarding::status::required(), paper.soft) } else { - ("Optional", paper.soft) + (tr::permission::onboarding::status::optional(), paper.soft) }; let paper = *paper; div() @@ -447,6 +448,7 @@ impl OnboardingWindow { div() .flex() .flex_col() + .min_w_0() .gap(px(1.)) .child( div() @@ -454,7 +456,8 @@ impl OnboardingWindow { .line_height(px(18.)) .font_weight(FontWeight::MEDIUM) .text_color(paper.ink) - .child(permission.label()), + .truncate() + .child(permission.display_name()), ) .child( div() @@ -462,6 +465,7 @@ impl OnboardingWindow { .line_height(px(16.)) .font_weight(FontWeight::NORMAL) .text_color(caption_color) + .truncate() .child(caption), ), ) @@ -498,15 +502,18 @@ impl OnboardingWindow { 168., 146., )) - .child(stage_title(paper, "One restart and you're set")) + .child(stage_title( + paper, + tr::permission::onboarding::relaunch::title(), + )) .child(stage_body( paper, - "macOS turns on screen recording once Cap restarts. You'll pick up right here.", + tr::permission::onboarding::relaunch::body(), )) .child(div().mt(px(24.)).child(paper_button( paper, "perm-relaunch-done", - "Relaunch Cap", + tr::onboarding::nav::relaunch(), true, true, cx.listener(|_, _, _, cx| permissions::relaunch(cx)), @@ -520,13 +527,16 @@ impl OnboardingWindow { 168., 146., )) - .child(stage_title(paper, "You're all set")) + .child(stage_title( + paper, + tr::permission::onboarding::all_set::title(), + )) .child(stage_body( paper, if self.permissions_only { - "Thanks. Cap can record again." + tr::permission::onboarding::all_set::regained() } else { - "Cap has everything it needs. Next, a quick look at what it can do." + tr::permission::onboarding::all_set::body() }, )), Stage::Permission(permission) => { @@ -548,19 +558,18 @@ impl OnboardingWindow { copy.done_title } else if self.stage.waiting_on == Some(permission) && permission.required() { - "Switch Cap on in System Settings" + tr::permission::onboarding::switch_on_title() } else { copy.title }, )); if granted { - let done_body = if permission == OSPermission::ScreenRecording - && self.needs_relaunch() - { - "macOS applies it after Cap restarts. You can do that once you're done here." - } else { - copy.done_body - }; + let done_body = + if permission == OSPermission::ScreenRecording && self.needs_relaunch() { + tr::permission::onboarding::relaunch::later() + } else { + copy.done_body + }; card.child(stage_body(paper, done_body)).child(allowed_pill( paper, self.stage.marks[index(permission)].1, @@ -590,21 +599,21 @@ impl OnboardingWindow { let guided = crate::permission_guide::eligible(permission); let waiting_required = waiting && permission.required(); let body = if waiting_required && guided { - "Drag Cap from the panel under System Settings into the list. Already listed? Just switch it on." + tr::permission::onboarding::hint::drag_into_list() } else if waiting_required { - "Find Cap in the list and switch it on. If macOS asked first, choose Open System Settings." + tr::permission::onboarding::hint::find_in_list() } else if status == OSPermissionStatus::Denied && !permission.required() { - "It was turned off earlier, so switch Cap on in System Settings. Or skip it for now." + tr::permission::onboarding::hint::turned_off() } else if action == Some(RowAction::OpenSettings) && permission.required() { - "Open System Settings, find Cap in the list and switch it on." + tr::permission::onboarding::hint::open_settings() } else { copy.body }; let primary_label = if waiting_required { - "Open System Settings again" + tr::permission::onboarding::open_settings_again() } else { match action { - Some(RowAction::OpenSettings) => "Open System Settings", + Some(RowAction::OpenSettings) => tr::main::device::access::open_system_settings(), _ => copy.allow, } }; @@ -629,7 +638,7 @@ impl OnboardingWindow { row.child(paper_button( &paper_copy, SharedString::from(format!("perm-skip-{}", permission.label())), - "Skip for now", + tr::permission::onboarding::skip(), false, true, cx.listener(move |this, _, _, cx| this.skip_permission(permission, cx)), @@ -658,12 +667,12 @@ impl OnboardingWindow { .text_size(px(12.)) .font_weight(FontWeight::NORMAL) .text_color(paper.soft) - .child("No prompt showed up? Switch Cap on in System Settings."), + .child(tr::permission::onboarding::hint::no_prompt()), ) .child(paper_button( paper, SharedString::from(format!("perm-settings-{}", permission.label())), - "Open System Settings", + tr::main::device::access::open_system_settings(), false, true, cx.listener(move |this, _, _, cx| { @@ -675,9 +684,9 @@ impl OnboardingWindow { ) } else if waiting { let hint = if permission.required() || action == Some(RowAction::OpenSettings) { - "Waiting for System Settings" + tr::permission::onboarding::waiting::system_settings() } else { - "Choose Allow in the macOS prompt" + tr::permission::onboarding::waiting::prompt() }; card.child( div() @@ -728,12 +737,12 @@ impl OnboardingWindow { .line_height(px(17.)) .font_weight(FontWeight::NORMAL) .text_color(paper.soft) - .child("Switched on but still waiting? Restart Cap to apply it."), + .child(tr::permission::onboarding::relaunch::hint()), ) .child(paper_button( paper, "perm-relaunch", - "Relaunch", + tr::permission::onboarding::relaunch::button(), false, true, cx.listener(|_, _, _, cx| permissions::relaunch(cx)), @@ -790,7 +799,7 @@ fn allowed_pill(paper: &Paper, shown_at: Instant, still: bool) -> Div { 22., 22., )) - .child("Allowed") + .child(tr::permission::onboarding::status::allowed()) } /// The extension's `.cta`: an ink pill. Primary is filled; secondary is the diff --git a/apps/desktop-gpui/src/permission_guide.rs b/apps/desktop-gpui/src/permission_guide.rs index 5bd32bdcb28..0bd4458c6a6 100644 --- a/apps/desktop-gpui/src/permission_guide.rs +++ b/apps/desktop-gpui/src/permission_guide.rs @@ -22,6 +22,7 @@ use gpui::{ }; use crate::doodle; +use crate::i18n::tr; use crate::onboarding_doodles as art; use crate::permissions::OSPermission; use crate::platform; @@ -325,9 +326,9 @@ impl Render for PermissionGuide { let palette = paper.palette(); // The arrow bobs forever; a hidden panel must not keep drawing. let still = self.hidden; - let pane = match self.permission { - OSPermission::Accessibility => "Accessibility", - _ => "Screen & System Audio Recording", + let body = match self.permission { + OSPermission::Accessibility => tr::permission::guide::accessibility_body(), + _ => tr::permission::guide::screen_body(), }; div() .size_full() @@ -362,16 +363,14 @@ impl Render for PermissionGuide { } }), ) - .child( - div().absolute().inset_0().child(doodle::doodle( - &art::DRAG_FRAME, - palette, - self.shown_at, - still, - 80., - 80., - )), - ) + .child(div().absolute().inset_0().child(doodle::doodle( + &art::DRAG_FRAME, + palette, + self.shown_at, + still, + 80., + 80., + ))) .child( div() .absolute() @@ -395,7 +394,7 @@ impl Render for PermissionGuide { .line_height(px(19.)) .font_weight(FontWeight::MEDIUM) .text_color(paper.ink) - .child("Drag Cap into the list"), + .child(tr::permission::guide::title()), ) .child( div() @@ -403,9 +402,7 @@ impl Render for PermissionGuide { .line_height(px(17.)) .font_weight(FontWeight::NORMAL) .text_color(paper.soft) - .child(format!( - "Drop it on the {pane} list above, then make sure its switch is on." - )), + .child(body), ), ) .child(div().flex_none().child(doodle::doodle( diff --git a/apps/desktop-gpui/src/permissions.rs b/apps/desktop-gpui/src/permissions.rs index 7a89abd4d35..b41ad8b8891 100644 --- a/apps/desktop-gpui/src/permissions.rs +++ b/apps/desktop-gpui/src/permissions.rs @@ -28,6 +28,8 @@ //! actually failed -- the per-permission `attempted` flags in //! [`classify`] are this module's version of Tauri's `initial_check`. +use crate::i18n::tr; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum OSPermission { ScreenRecording, @@ -53,6 +55,15 @@ impl OSPermission { } } + pub fn display_name(self) -> &'static str { + match self { + Self::ScreenRecording => tr::permission::name::screen_recording(), + Self::Accessibility => tr::permission::name::accessibility(), + Self::Microphone => tr::permission::name::microphone(), + Self::Camera => tr::permission::name::camera(), + } + } + /// Screen recording and accessibility gate the app; mic and camera are /// optional. Same split as the Tauri app's `necessary_granted`. pub fn required(self) -> bool { diff --git a/apps/desktop-gpui/src/platform.rs b/apps/desktop-gpui/src/platform.rs index 27df7a67dc5..4ade84cfeb6 100644 --- a/apps/desktop-gpui/src/platform.rs +++ b/apps/desktop-gpui/src/platform.rs @@ -29,9 +29,13 @@ pub(crate) enum LinuxFileDialogError { impl LinuxFileDialogError { pub(crate) fn message(&self) -> String { match self { - Self::Busy => "A file dialog is already open. Finish or cancel it before trying again.".into(), - Self::BeforeDispatch(error) | Self::NativeResponse(error) => format!("The file dialog could not complete: {error}"), - Self::Indeterminate(_) | Self::RestartRequired => "Cap could not confirm that the file dialog closed. Close any remaining file dialog and restart Cap before trying again.".into(), + Self::Busy => crate::i18n::tr::dialog::file::busy().into(), + Self::BeforeDispatch(error) | Self::NativeResponse(error) => { + crate::i18n::tr::dialog::file::failed(error) + } + Self::Indeterminate(_) | Self::RestartRequired => { + crate::i18n::tr::dialog::file::unconfirmed().into() + } } } } @@ -313,7 +317,7 @@ async fn linux_save_file_panel( let filters = if extensions.is_empty() { Vec::new() } else { - portal_filters(&[("Export", extensions)]) + portal_filters(&[(crate::i18n::tr::dialog::file::export_filter(), extensions)]) }; run_linux_file_dialog(cx, owner, parent, move |identifier| async move { let request = ashpd::desktop::file_chooser::SaveFileRequest::default() @@ -355,7 +359,7 @@ async fn finish_linux_file_dialog( return None; } Some(crate::editor_modal::informational_alert( - "File dialog unavailable", + crate::i18n::tr::dialog::file::unavailable_title(), &message, window, cx, @@ -553,7 +557,7 @@ fn clipboard_file_path(path: &std::path::Path) -> Result = std::sync::OnceLock::new(); + /// Rows of `menus::build_menus`' Cap and File menus. Both menus are built + /// from that model in this order, so the positions are fixed. + const APP_MENU: isize = 0; + const ABOUT_ROW: isize = 0; + const UPDATE_ROW: isize = 1; + const SETTINGS_ROW: isize = 3; + const FILE_MENU: isize = 1; + const OPEN_RECENT_ROW: isize = 1; + const HELP_MENU: isize = 5; + const FEEDBACK_ROW: isize = 0; + + /// `PredefinedMenuItem::about`, which on macOS is `orderFrontStandardAboutPanelWithOptions:`. + /// Ordering a window front re-enters gpui's window callbacks, so call it + /// from a task, never from inside an update. + pub fn show_about_panel(name: &str, version: &str) { + use objc2::ClassType; + use objc2_app_kit::{ + NSAboutPanelOptionApplicationName, NSAboutPanelOptionApplicationVersion, + NSAboutPanelOptionCredits, NSApplication, + }; + use objc2_foundation::{MainThreadMarker, NSDictionary, NSString}; + + let Some(main_thread) = MainThreadMarker::new() else { + tracing::error!("The About panel must be shown from the main thread"); + return; + }; + let name_value = NSString::from_str(name); + let version_value = NSString::from_str(version); + let credits = about_credits(); + + let keys = unsafe { + [ + NSAboutPanelOptionApplicationName, + NSAboutPanelOptionApplicationVersion, + NSAboutPanelOptionCredits, + ] + }; + let values: [&AnyObject; 3] = [ + name_value.as_super(), + version_value.as_super(), + credits.as_super(), + ]; + let options = NSDictionary::from_slices(&keys, &values); + + unsafe { + NSApplication::sharedApplication(main_thread) + .orderFrontStandardAboutPanelWithOptions(&options); + } + } + + /// Each entry is one centred line. A `Some` URL makes the line a link. + /// Kept short: a long credits block makes AppKit add a scroll view. + fn about_credit_lines() -> Vec<(String, Option<&'static str>)> { + use crate::i18n::tr; + vec![ + ( + "Copyright \u{a9} 2023-present Cap Software, Inc.".into(), + None, + ), + ( + tr::menu::about::licensed().into(), + Some("https://github.com/CapSoftware/Cap/blob/main/LICENSE"), + ), + (tr::menu::about::website().into(), Some("https://cap.so")), + ] + } + + const COMMIT_FONT_SIZE: f64 = 10.0; + + /// The build's commit, when the build recorded one. + fn commit_line() -> Option { + option_env!("CAP_BUILD_REVISION").map(|revision| { + let short = &revision[..revision.len().min(7)]; + format!("{}: {short}", crate::i18n::tr::menu::about::commit()) + }) + } + + fn about_credits() -> Retained { + use objc2::{AnyThread, ClassType}; + use objc2_app_kit::{ + NSColor, NSFont, NSFontAttributeName, NSForegroundColorAttributeName, + NSLinkAttributeName, NSMutableParagraphStyle, NSParagraphStyleAttributeName, + NSTextAlignment, + }; + use objc2_foundation::{ + NSAttributedString, NSDictionary, NSMutableAttributedString, NSString, NSURL, + }; + + let style = NSMutableParagraphStyle::new(); + style.setAlignment(NSTextAlignment::Center); + let centred: &AnyObject = style.as_super(); + + let spaced = NSMutableParagraphStyle::new(); + spaced.setAlignment(NSTextAlignment::Center); + spaced.setParagraphSpacing(12.0); + let spaced: &AnyObject = spaced.as_super(); + + let commit_style = NSMutableParagraphStyle::new(); + commit_style.setAlignment(NSTextAlignment::Center); + commit_style.setParagraphSpacingBefore(12.0); + let commit_style: &AnyObject = commit_style.as_super(); + + let credits = NSMutableAttributedString::from_nsstring(&NSString::from_str("")); + let append = |text: &str, link: Option<&str>, paragraph: &AnyObject, subdued: bool| { + let line_text = NSString::from_str(&format!("{text}\n")); + let link = link.and_then(|url| NSURL::URLWithString(&NSString::from_str(url))); + + let mut keys: Vec<&NSString> = vec![unsafe { NSParagraphStyleAttributeName }]; + let mut values: Vec<&AnyObject> = vec![paragraph]; + if let Some(link) = link.as_deref() { + keys.push(unsafe { NSLinkAttributeName }); + values.push(link.as_super()); + } + let color; + let font; + if subdued { + color = NSColor::tertiaryLabelColor(); + font = NSFont::systemFontOfSize(COMMIT_FONT_SIZE); + keys.push(unsafe { NSForegroundColorAttributeName }); + values.push(color.as_super()); + keys.push(unsafe { NSFontAttributeName }); + values.push(font.as_super()); + } + let attributes = NSDictionary::from_slices(&keys, &values); + let line = unsafe { + NSAttributedString::initWithString_attributes( + NSAttributedString::alloc(), + &line_text, + Some(&attributes), + ) + }; + credits.appendAttributedString(&line); + }; + + let lines = about_credit_lines(); + for (index, (text, link)) in lines.iter().enumerate() { + let paragraph = if index == 0 { spaced } else { centred }; + append(text, *link, paragraph, false); + } + if let Some(commit) = commit_line() { + append(&commit, None, commit_style, true); + } + credits + } + + pub fn patch_standard_application_menu() { + use objc2::{exception, msg_send, runtime::AnyObject, sel}; + + let Some(main_thread) = objc2_foundation::MainThreadMarker::new() else { + tracing::error!("Cannot patch the application menu off the main thread"); + return; + }; + let app = NSApplication::sharedApplication(main_thread); + let Some(main_menu) = app.mainMenu() else { + tracing::warn!("GPUI did not install an application menu"); + return; + }; + let Some(app_menu) = main_menu + .itemAtIndex(APP_MENU) + .and_then(|item| item.submenu()) + else { + return; + }; + + // gpui's `validateMenuItem:` answers only "is this action available?", + // which is always yes for Cap's global listeners -- so AppKit's + // auto-enable pass overwrites `MenuItem::Action`'s `disabled` every + // time the menu opens. The model's enabled state has to be + // authoritative instead, as it already is for the tray menu and for + // muda. + app_menu.setAutoenablesItems(false); + + if objc2::available!(macos = 15.0) { + if let Some(about) = app_menu.itemAtIndex(ABOUT_ROW) { + set_nsmenu_system_symbol(&about, "info.icon"); + } + if let Some(settings) = app_menu.itemAtIndex(SETTINGS_ROW) { + set_nsmenu_system_symbol(&settings, "gear"); + set_nsmenu_item_prefers_visible_image(&settings); + } + if let Some(feedback) = main_menu + .itemAtIndex(HELP_MENU) + .and_then(|item| item.submenu()) + .and_then(|menu| menu.itemAtIndex(FEEDBACK_ROW)) + { + set_nsmenu_system_symbol(&feedback, "exclamationmark.bubble"); + set_nsmenu_item_prefers_visible_image(&feedback); + } + if let Some(quit) = &app_menu + .numberOfItems() + .checked_sub(1) + .and_then(|index| app_menu.itemAtIndex(index)) + { + set_nsmenu_system_symbol(quit, "xmark.rectangle"); + } + } + + let Some(file_menu) = main_menu + .itemAtIndex(FILE_MENU) + .and_then(|item| item.submenu()) + else { + return; + }; + + let has_recent_menu = file_menu.itemAtIndex(OPEN_RECENT_ROW).is_some_and(|item| { + item.title().isEqualToString(&NSString::from_str( + crate::i18n::tr::menu::file::open_recent(), + )) + }); + if has_recent_menu { + return; + } + + let recent_menu = NSMenu::new(main_thread); + recent_menu.setTitle(&NSString::from_str( + crate::i18n::tr::menu::file::open_recent(), + )); + let name = NSString::from_str("NSRecentDocumentsMenu"); + let key = NSString::from_str("menuName"); + let registered = exception::catch(std::panic::AssertUnwindSafe(|| { + unsafe { recent_menu.setValue_forKey(Some(&*name as &AnyObject), &key) }; + })); + if let Err(error) = registered { + tracing::warn!("Could not register Open Recent menu name: {error:?}"); + } + + let recent_item = NSMenuItem::new(main_thread); + recent_item.setTitle(&NSString::from_str( + crate::i18n::tr::menu::file::open_recent(), + )); + recent_item.setSubmenu(Some(&recent_menu)); + recent_item.setAllowsAutomaticKeyEquivalentLocalization(true); + file_menu.insertItem_atIndex(&recent_item, OPEN_RECENT_ROW); + + // NSDocumentController owns the recent items and the standard + // "Clear Menu" validation; it only needs the empty, named submenu. + let controller = NSDocumentController::sharedDocumentController(main_thread); + let can_install_menu: bool = + unsafe { msg_send![&*controller, respondsToSelector: sel!(_installOpenRecentMenus)] }; + // Each menu-bar rebuild creates a fresh named submenu, so bind the + // controller to this instance. Recent-document updates afterwards are + // handled by NSDocumentController, not by another install call. + if can_install_menu { + unsafe { + let _: () = msg_send![&*controller, _installOpenRecentMenus]; + } + } else { + tracing::warn!("NSDocumentController cannot install the Open Recent menu"); + } + } + + /// Rewrites the Check for Update row in place. A menu item edited this way + /// keeps whatever menu the user has open on screen; reinstalling the menu + /// bar for every download-percentage tick would close it. + pub fn set_update_menu_item(title: &str, badge: Option<&str>, enabled: bool) { + use objc2::runtime::AnyClass; + use objc2_app_kit::NSMenuItemBadge; + use objc2_foundation::MainThreadMarker; + + let Some(main_thread) = MainThreadMarker::new() else { + return; + }; + let Some(item) = update_menu_item(main_thread) else { + tracing::warn!("The update row is missing from the application menu"); + return; + }; + item.setTitle(&NSString::from_str(title)); + item.setEnabled(enabled); + + // `NSMenuItemBadge` and `setBadge:` are macOS 14+, and objc2's class + // lookup panics when the class is absent. + if AnyClass::get(c"NSMenuItemBadge").is_none() { + return; + } + let badge = badge.map(|text| { + NSMenuItemBadge::initWithString(main_thread.alloc(), &NSString::from_str(text)) + }); + item.setBadge(badge.as_deref()); + } + + fn update_menu_item( + main_thread: objc2_foundation::MainThreadMarker, + ) -> Option> { + NSApplication::sharedApplication(main_thread) + .mainMenu()? + .itemAtIndex(APP_MENU)? + .submenu()? + .itemAtIndex(UPDATE_ROW) + } + unsafe extern "C-unwind" fn application_should_terminate( _: &AnyObject, _: Sel, @@ -726,7 +1023,7 @@ mod mac { .as_ref() .ok_or("Application delegate is unavailable")?; let delegate_class = delegate.class(); - if delegate_class.name() != "GPUIApplicationDelegate" { + if delegate_class.name() != c"GPUIApplicationDelegate" { return Err("Native Quit requires the owned GPUI application delegate".into()); } let selector = sel!(applicationShouldTerminate:); @@ -741,12 +1038,12 @@ mod mac { unsafe extern "C-unwind" fn(&AnyObject, Sel, *mut AnyObject) -> usize, Imp, >(application_should_terminate); - if !objc2::runtime::Bool::from_raw(ffi::class_addMethod( + if !ffi::class_addMethod( delegate_class as *const _ as *mut _, - selector.as_ptr(), - Some(implementation), + selector, + implementation, c"Q@:@".as_ptr(), - )) + ) .as_bool() { return Err("Could not install the native Quit handler".into()); @@ -795,9 +1092,7 @@ mod mac { NSAppearance::appearanceNamed(unsafe { NSAppearanceNameDarkAqua }) } }; - unsafe { - NSAppearanceCustomization::setAppearance(&*native.0, named.as_deref()); - } + NSAppearanceCustomization::setAppearance(&*native.0, named.as_deref()); } pub fn window_is_dark(window: &Window) -> Option { @@ -808,8 +1103,8 @@ mod mac { }; let ns = ns_window(window)?; - let effective = unsafe { NSAppearanceCustomization::effectiveAppearance(&*ns) }; - let name = unsafe { effective.name() }; + let effective = NSAppearanceCustomization::effectiveAppearance(&*ns); + let name = effective.name(); Some(unsafe { name.isEqualToString(NSAppearanceNameDarkAqua) || name.isEqualToString(NSAppearanceNameVibrantDark) @@ -900,12 +1195,12 @@ mod mac { // `NSGlassEffectView` exists only on macOS 26+; its absence is what // sends `macos-window-material.ts` down the vibrancy branch. if material_override.as_deref() != Some("vibrancy") - && let Some(glass_class) = AnyClass::get("NSGlassEffectView") + && let Some(glass_class) = AnyClass::get(c"NSGlassEffectView") { unsafe { let glass: *mut AnyObject = msg_send![glass_class, alloc]; let glass: *mut AnyObject = msg_send![glass, initWithFrame: bounds]; - if let Some(glass) = Id::from_raw(glass) { + if let Some(glass) = Retained::from_raw(glass) { set_clip_radius(radius); let identifier = NSString::from_str(LIQUID_GLASS_IDENTIFIER); let _: () = msg_send![&*glass, setIdentifier: &*identifier]; @@ -922,7 +1217,7 @@ mod mac { let _: () = msg_send![ &*content_view, addSubview: &*glass, - positioned: NSWindowOrderingMode::NSWindowBelow, + positioned: NSWindowOrderingMode::Below, relativeTo: std::ptr::null_mut::(), ]; return Some(MaterialKind::LiquidGlass); @@ -935,21 +1230,15 @@ mod mac { // 16 })`. Tauri's `Effect.WindowBackground` is `NSVisualEffectMaterial` // `windowBackground` (12) blended behind the window; the radius is // already covered by the content-view squircle clip above. - let vibrancy = unsafe { NSVisualEffectView::initWithFrame(mtm.alloc(), bounds) }; - unsafe { - vibrancy.setMaterial(NSVisualEffectMaterial::WindowBackground); - vibrancy.setBlendingMode(NSVisualEffectBlendingMode::BehindWindow); - vibrancy.setState(NSVisualEffectState::FollowsWindowActiveState); - vibrancy.setAutoresizingMask( - NSAutoresizingMaskOptions::NSViewWidthSizable - | NSAutoresizingMaskOptions::NSViewHeightSizable, - ); - content_view.addSubview_positioned_relativeTo( - &vibrancy, - NSWindowOrderingMode::NSWindowBelow, - None, - ); - } + let vibrancy = NSVisualEffectView::initWithFrame(mtm.alloc(), bounds); + vibrancy.setMaterial(NSVisualEffectMaterial::WindowBackground); + vibrancy.setBlendingMode(NSVisualEffectBlendingMode::BehindWindow); + vibrancy.setState(NSVisualEffectState::FollowsWindowActiveState); + vibrancy.setAutoresizingMask( + NSAutoresizingMaskOptions::ViewWidthSizable + | NSAutoresizingMaskOptions::ViewHeightSizable, + ); + content_view.addSubview_positioned_relativeTo(&vibrancy, NSWindowOrderingMode::Below, None); Some(MaterialKind::Vibrancy) } @@ -995,7 +1284,7 @@ mod mac { use objc2::ffi::class_addMethod; use objc2_foundation::NSRect; - unsafe extern "C" fn constrain_shim( + unsafe extern "C-unwind" fn constrain_shim( _this: *mut AnyObject, _sel: Sel, frame: NSRect, @@ -1005,22 +1294,30 @@ mod mac { } for name in ["GPUIWindow", "GPUIPanel"] { - let Some(class) = objc2::runtime::AnyClass::get(name) else { + let Some(class) = objc2::runtime::AnyClass::get(match name { + "GPUIWindow" => c"GPUIWindow", + "GPUIPanel" => c"GPUIPanel", + _ => continue, + }) else { continue; }; let added = unsafe { class_addMethod( - (class as *const objc2::runtime::AnyClass as *mut objc2::ffi::objc_class) - .cast(), - objc2::sel!(constrainFrameRect:toScreen:).as_ptr(), - Some(std::mem::transmute::< - unsafe extern "C" fn(*mut AnyObject, Sel, NSRect, *mut AnyObject) -> NSRect, - unsafe extern "C" fn(), - >(constrain_shim)), + (class as *const objc2::runtime::AnyClass).cast_mut(), + objc2::sel!(constrainFrameRect:toScreen:), + std::mem::transmute::< + unsafe extern "C-unwind" fn( + *mut AnyObject, + Sel, + NSRect, + *mut AnyObject, + ) -> NSRect, + objc2::runtime::Imp, + >(constrain_shim), c"{CGRect={CGPoint=dd}{CGSize=dd}}@:{CGRect={CGPoint=dd}{CGSize=dd}}@".as_ptr(), ) }; - if objc2::runtime::Bool::from_raw(added).as_bool() { + if added.as_bool() { tracing::info!("installed frame-constraint shim on {name}"); } } @@ -1093,7 +1390,7 @@ mod mac { /// A retained `NSWindow` that can outlive a `&Window` borrow -- what /// [`place_overlay_panel`] operates on. - pub struct NativeWindow(Id); + pub struct NativeWindow(Retained); /// The retained `NSWindow` behind a gpui window, for AppKit calls that /// must run *outside* any gpui update (see [`place_overlay_panel`]). @@ -1149,7 +1446,11 @@ mod mac { pub fn lock_borderless_style(native: &NativeWindow) { use objc2::{ffi, msg_send, runtime::AnyClass}; - unsafe extern "C" fn guarded_set_style_mask(this: *mut AnyObject, sel: Sel, mask: usize) { + unsafe extern "C-unwind" fn guarded_set_style_mask( + this: *mut AnyObject, + sel: Sel, + mask: usize, + ) { let Some(&(_, imp)) = SET_STYLE_MASK_SUPER.get() else { return; }; @@ -1162,7 +1463,7 @@ mod mac { tracing::info!(mask, "kept the main window's borderless style mask"); BORDERLESS_STYLE_MASK }; - let imp: unsafe extern "C" fn(*mut AnyObject, Sel, usize) = + let imp: unsafe extern "C-unwind" fn(*mut AnyObject, Sel, usize) = unsafe { std::mem::transmute(imp) }; unsafe { imp(this, sel, mask) }; } @@ -1172,25 +1473,27 @@ mod mac { let sel = objc2::sel!(setStyleMask:); let &(installed_on, _) = SET_STYLE_MASK_SUPER.get_or_init(|| { let superclass = ffi::class_getSuperclass(class.cast()); - let Some(imp) = ffi::method_getImplementation(ffi::class_getInstanceMethod( - superclass, - sel.as_ptr(), - )) else { + let Some(imp) = + ffi::method_getImplementation(ffi::class_getInstanceMethod(superclass, sel)) + else { tracing::warn!( "no inherited setStyleMask: for the main window style-mask lock" ); return (0, 0); }; + let added = ffi::class_addMethod( class.cast_mut().cast(), - sel.as_ptr(), - Some(std::mem::transmute::< - unsafe extern "C" fn(*mut AnyObject, Sel, usize), - unsafe extern "C" fn(), - >(guarded_set_style_mask)), - c"v@:Q".as_ptr(), - ); - if !objc2::runtime::Bool::from_raw(added).as_bool() { + sel, + std::mem::transmute::< + unsafe extern "C-unwind" fn(*mut AnyObject, Sel, usize), + objc2::runtime::Imp, + >(guarded_set_style_mask), + c"v@:Q".as_ptr(), // (void) + ) + .as_bool(); + + if !added { tracing::warn!("could not install the main window style-mask lock"); return (0, 0); } @@ -1291,10 +1594,8 @@ mod mac { apply_fullscreen_overlay_behavior(ns_window); // `.shadow(false)` in the Tauri builder. ns_window.setHasShadow(false); - unsafe { - ns_window.setAnimationBehavior(objc2_app_kit::NSWindowAnimationBehavior::None); - ns_window.orderFrontRegardless(); - } + ns_window.setAnimationBehavior(objc2_app_kit::NSWindowAnimationBehavior::None); + ns_window.orderFrontRegardless(); } /// The window's raw AppKit frame (bottom-left origin). The dev-restore @@ -1429,12 +1730,12 @@ mod mac { /// rebuild and must not steal focus from whatever the user is typing in. /// Same task-only rule as every other ordering call here. pub fn order_front_native(native: &NativeWindow) { - unsafe { native.0.orderFrontRegardless() }; + native.0.orderFrontRegardless(); rerun_occlusion_handler_after_show(&native.0); } /// The `NSWindow` behind a gpui window. - pub fn ns_window(window: &Window) -> Option> { + pub fn ns_window(window: &Window) -> Option> { // Fully qualified: `Window` also has an inherent `window_handle()` // returning gpui's own `AnyWindowHandle`. let handle = HasWindowHandle::window_handle(window).ok()?; @@ -1459,7 +1760,7 @@ mod mac { ns_window.setLevel(behavior.level); tracing::debug!( requested = behavior.level, - actual = unsafe { ns_window.level() }, + actual = ns_window.level(), "panel level applied" ); if behavior.join_all_spaces { @@ -1543,7 +1844,7 @@ mod mac { pub fn set_window_click_through(native: &NativeWindow, click_through: bool) -> bool { native.0.setIgnoresMouseEvents(click_through); - unsafe { native.0.ignoresMouseEvents() == click_through } + native.0.ignoresMouseEvents() == click_through } /// `orderOut:` -- hide without closing, the way the Tauri main window @@ -1641,7 +1942,7 @@ mod mac { static SOURCE: OnceLock = OnceLock::new(); *SOURCE.get_or_init(|| { - let Some(mut builder) = ClassBuilder::new("CapFileDragSource", NSObject::class()) + let Some(mut builder) = ClassBuilder::new(c"CapFileDragSource", NSObject::class()) else { return 0; }; @@ -1657,7 +1958,7 @@ mod mac { ) -> usize, ); } - if let Some(protocol) = AnyProtocol::get("NSDraggingSource") { + if let Some(protocol) = AnyProtocol::get(c"NSDraggingSource") { builder.add_protocol(protocol); } let class = builder.register(); @@ -1804,7 +2105,7 @@ mod mac { use objc2::{class, msg_send}; use objc2_foundation::NSString; unsafe { - let mut image: Option> = None; + let mut image: Option> = None; let cap_app = NSString::from_str("/Applications/Cap.app"); let exists: bool = { @@ -1816,7 +2117,7 @@ mod mac { if !workspace.is_null() { let raw: *mut AnyObject = msg_send![workspace, iconForFile: &*cap_app]; // `iconForFile:` is autoreleased; retain for the setter. - image = Id::retain(raw); + image = Retained::retain(raw); if let Some(image) = &image { // The workspace icon's nominal size is small (32pt); // the reps go up to 1024. Ask for dock resolution. @@ -1837,7 +2138,7 @@ mod mac { if !data.is_null() { let alloc: *mut AnyObject = msg_send![class!(NSImage), alloc]; let raw: *mut AnyObject = msg_send![alloc, initWithData: data]; - image = Id::from_raw(raw); + image = Retained::from_raw(raw); } } @@ -2120,7 +2421,7 @@ mod mac { /// Tauri app calls on the recording controls panel. pub fn show_window_without_focus(window: &Window) { if let Some(ns_window) = ns_window(window) { - unsafe { ns_window.orderFrontRegardless() }; + ns_window.orderFrontRegardless(); rerun_occlusion_handler_after_show(&ns_window); } } @@ -2128,7 +2429,7 @@ mod mac { /// The CGWindowID, which is also what `scap_targets::WindowId` wraps on /// macOS -- used to exclude our own controls bar from the capture. pub fn window_number(window: &Window) -> Option { - ns_window(window).map(|w| unsafe { w.windowNumber() }) + ns_window(window).map(|w| w.windowNumber()) } // -- The open panel ------------------------------------------------------ @@ -2172,11 +2473,11 @@ mod mac { let _: () = msg_send![panel, setCanCreateDirectories: true]; let _: () = msg_send![panel, setNameFieldStringValue: &*NSString::from_str(suggested)]; if !extensions.is_empty() { - let types: Vec> = extensions + let types: Vec> = extensions .iter() .map(|extension| NSString::from_str(extension)) .collect(); - let types = NSArray::from_vec(types); + let types = NSArray::from_retained_slice(&types); let _: () = msg_send![panel, setAllowedFileTypes: &*types]; } let response: isize = msg_send![panel, runModal]; @@ -2211,7 +2512,7 @@ mod mac { autoreleasepool(|_| unsafe { let pasteboard: *mut AnyObject = msg_send![class!(NSPasteboard), generalPasteboard]; if pasteboard.is_null() { - return Err("Clipboard unavailable".into()); + return Err(crate::i18n::tr::dialog::clipboard::unavailable().into()); } let url_class = class!(NSURL); let ns_path = NSString::from_str(path); @@ -2225,7 +2526,7 @@ mod mac { if ok.as_bool() { Ok(()) } else { - Err("Failed to copy file to clipboard".into()) + Err(crate::i18n::tr::dialog::clipboard::copy_file_failed().into()) } }) } @@ -2245,19 +2546,19 @@ mod mac { autoreleasepool(|_| unsafe { let pasteboard: *mut AnyObject = msg_send![class!(NSPasteboard), generalPasteboard]; if pasteboard.is_null() { - return Err("Clipboard unavailable".into()); + return Err(crate::i18n::tr::dialog::clipboard::unavailable().into()); } let data = NSData::with_bytes(&image.0); let image: *mut AnyObject = msg_send![class!(NSImage), alloc]; let image: *mut AnyObject = msg_send![image, initWithData: &*data]; - let image = Id::from_raw(image).ok_or("Failed to load image")?; + let image = Retained::from_raw(image).ok_or("Failed to load image")?; let objects: *mut AnyObject = msg_send![class!(NSArray), arrayWithObject: &*image]; let _: isize = msg_send![pasteboard, clearContents]; let ok: Bool = msg_send![pasteboard, writeObjects: objects]; if ok.as_bool() { Ok(()) } else { - Err("Failed to copy image to clipboard".into()) + Err(crate::i18n::tr::dialog::clipboard::copy_image_failed().into()) } }) } @@ -2275,11 +2576,11 @@ mod mac { let _: () = msg_send![panel, setCanChooseDirectories: false]; let _: () = msg_send![panel, setAllowsMultipleSelection: false]; - let types: Vec> = extensions + let types: Vec> = extensions .iter() .map(|extension| NSString::from_str(extension)) .collect(); - let types = NSArray::from_vec(types); + let types = NSArray::from_retained_slice(&types); let _: () = msg_send![panel, setAllowedFileTypes: &*types]; // `NSModalResponseOK`. @@ -2320,7 +2621,7 @@ mod mac { cancel: &str, warning: bool, ) -> bool { - use objc2::{class, msg_send, msg_send_id}; + use objc2::{class, msg_send}; use objc2_foundation::NSString; /// `NSAlertStyle`: `Warning = 0`, `Informational = 1`. `NSUInteger`, @@ -2333,7 +2634,7 @@ mod mac { const NS_ALERT_FIRST_BUTTON_RETURN: isize = 1000; unsafe { - let alert: Id = msg_send_id![class!(NSAlert), new]; + let alert: Retained = msg_send![class!(NSAlert), new]; let _: () = msg_send![&*alert, setMessageText: &*NSString::from_str(title)]; let _: () = msg_send![&*alert, setInformativeText: &*NSString::from_str(message)]; let _: () = msg_send![ @@ -2356,13 +2657,13 @@ mod mac { } pub fn alert_dialog(title: &str, message: &str) { - use objc2::{class, msg_send, msg_send_id}; + use objc2::{class, msg_send}; use objc2_foundation::NSString; const NS_ALERT_STYLE_INFORMATIONAL: usize = 1; unsafe { - let alert: Id = msg_send_id![class!(NSAlert), new]; + let alert: Retained = msg_send![class!(NSAlert), new]; let _: () = msg_send![&*alert, setMessageText: &*NSString::from_str(title)]; let _: () = msg_send![&*alert, setInformativeText: &*NSString::from_str(message)]; let _: () = msg_send![&*alert, setAlertStyle: NS_ALERT_STYLE_INFORMATIONAL]; @@ -2619,48 +2920,6 @@ mod mac { } } - // -- The About panel ------------------------------------------------------ - - /// `PredefinedMenuItem::about(.., AboutMetadata { name, version, .. })`, - /// which on macOS is `orderFrontStandardAboutPanelWithOptions:`. - /// - /// The Tauri metadata also carries `copyright` and `publisher` from - /// `tauri.conf.json` -- neither key exists in that file, so both are `None` - /// there and there is nothing to pass here either. - /// - /// Spins no modal run loop of its own (the panel is an ordinary window), but - /// ordering a window front re-enters gpui's window callbacks, so it keeps - /// the [`place_overlay_panel`] rule: call it from a task, never inside an - /// update. - pub fn show_about_panel(name: &str, version: &str) { - use objc2::{class, msg_send}; - use objc2_foundation::{NSArray, NSString}; - - unsafe { - let app: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication]; - if app.is_null() { - return; - } - // `dictionaryWithObjects:forKeys:` rather than a typed - // `NSDictionary` constructor, for the reason - // [`desktop_picture_path`] gives: the typed binding needs another - // objc2-app-kit/foundation feature, and the rule here is that those - // versions stay pinned to gpui's. - let values = - NSArray::from_vec(vec![NSString::from_str(name), NSString::from_str(version)]); - let keys = NSArray::from_vec(vec![ - NSString::from_str("ApplicationName"), - NSString::from_str("ApplicationVersion"), - ]); - let options: *mut AnyObject = msg_send![ - class!(NSDictionary), - dictionaryWithObjects: &*values, - forKeys: &*keys, - ]; - let _: () = msg_send![app, orderFrontStandardAboutPanelWithOptions: options]; - } - } - /// `NSApp.windowsMenu` and `NSApp.helpMenu`, looked up by the titles the /// menu bar was just built with. `None` clears the Help menu. pub fn register_system_menus(window_title: &str, help_title: Option<&str>) { @@ -2706,12 +2965,27 @@ mod mac { "occlusion_raw={:#x} visible={} on_active_space={} alpha={} level={} number={}", w.occlusionState().bits(), w.isVisible(), - unsafe { w.isOnActiveSpace() }, - unsafe { w.alphaValue() }, - unsafe { w.level() }, - unsafe { w.windowNumber() }, + w.isOnActiveSpace(), + w.alphaValue(), + w.level(), + w.windowNumber(), )) } + + pub fn set_nsmenu_item_prefers_visible_image(item: &NSMenuItem) { + if objc2::available!(macos = 27.0) { + // NSMenuItemImageVisibility: automatic = 0, visible = 1, hidden = 2 + let _: () = unsafe { objc2::msg_send![item, setPreferredImageVisibility: 1isize] }; + } + } + + fn set_nsmenu_system_symbol(item: &NSMenuItem, symbol: &str) { + let image = objc2_app_kit::NSImage::imageWithSystemSymbolName_accessibilityDescription( + &objc2_foundation::NSString::from_str(symbol), + None, + ); + item.setImage(image.as_deref()); + } } #[cfg(target_os = "windows")] @@ -2911,6 +3185,7 @@ mod stub { pub fn close_native(_native: &NativeWindow) {} pub fn set_dock_icon(_png: &[u8]) {} + pub fn show_about_panel(_name: &str, _version: &str) {} pub fn escape_hotkey_events() -> flume::Receiver<()> { // A channel whose sender is dropped immediately: the drain task's // `recv` errors once and the task exits. @@ -3004,7 +3279,7 @@ mod stub { cx.try_write_to_clipboard( gpui::ClipboardEntry::ExternalPaths(gpui::ExternalPaths(vec![path].into())).into(), ) - .map_err(|error| format!("Could not copy the file to the clipboard: {error}")) + .map_err(crate::i18n::tr::dialog::clipboard::copy_file) } pub struct ClipboardImage(gpui::ClipboardItem); @@ -3027,7 +3302,7 @@ mod stub { pub fn write_clipboard_image(image: ClipboardImage, cx: &gpui::App) -> Result<(), String> { cx.try_write_to_clipboard(image.0) - .map_err(|error| format!("Could not copy the image to the clipboard: {error}")) + .map_err(crate::i18n::tr::dialog::clipboard::copy_image) } pub fn desktop_picture_path() -> Option { #[cfg(target_os = "linux")] @@ -3045,7 +3320,6 @@ mod stub { pub fn activation_policy() -> isize { -1 } - pub fn show_about_panel(_name: &str, _version: &str) {} } #[cfg(not(any(target_os = "macos", target_os = "windows")))] diff --git a/apps/desktop-gpui/src/platform/macos_exception_guard.rs b/apps/desktop-gpui/src/platform/macos_exception_guard.rs index 0348df8652a..e3c1f875df5 100644 --- a/apps/desktop-gpui/src/platform/macos_exception_guard.rs +++ b/apps/desktop-gpui/src/platform/macos_exception_guard.rs @@ -16,7 +16,7 @@ use std::sync::OnceLock; use objc2::{ ffi, msg_send, - rc::Id, + rc::Retained, runtime::{AnyClass, AnyObject, Imp, Sel}, sel, }; @@ -46,9 +46,7 @@ impl GuardedMethod { if self.original.get().is_some() || class.verify_sel::<(), ()>(selector).is_err() { return false; } - let method = unsafe { - ffi::class_getInstanceMethod((class as *const AnyClass).cast(), selector.as_ptr()) - }; + let method = unsafe { ffi::class_getInstanceMethod(class, selector) }; if method.is_null() { return false; } @@ -63,7 +61,7 @@ impl GuardedMethod { if self.original.set(original).is_err() { return false; } - unsafe { ffi::method_setImplementation(method, Some(guard_imp)) }; + let _ = unsafe { ffi::method_setImplementation(method, guard_imp) }; true } @@ -72,12 +70,12 @@ impl GuardedMethod { return false; }; let exception = unsafe { cap_call_void_imp_catching(original, receiver, selector) }; - let Some(exception) = (unsafe { Id::from_raw(exception) }) else { + let Some(exception) = (unsafe { Retained::from_raw(exception) }) else { return false; }; tracing::warn!( exception = %describe(&exception), - selector = %selector.name(), + selector = %selector.name().to_string_lossy(), "ignored an AppKit exception that would have crashed the app" ); true @@ -110,7 +108,7 @@ unsafe extern "C" fn touch_bar_finder_invalidate(receiver: *mut AnyObject, selec /// Call once on the main thread, before the app's run loop starts. pub(super) fn install() { - let Some(class) = AnyClass::get("_NSTouchBarFinderObservation") else { + let Some(class) = AnyClass::get(c"_NSTouchBarFinderObservation") else { tracing::info!("AppKit has no Touch Bar finder observation class; nothing to guard"); return; }; @@ -124,7 +122,7 @@ mod tests { use std::sync::atomic::{AtomicUsize, Ordering}; use objc2::{ - ClassType, msg_send, msg_send_id, + ClassType, msg_send, runtime::{ClassBuilder, NSObject}, }; @@ -145,7 +143,7 @@ mod tests { static CLASS: OnceLock<&'static AnyClass> = OnceLock::new(); CLASS.get_or_init(|| { let mut builder = - ClassBuilder::new("CapExceptionGuardTestObservation", NSObject::class()).unwrap(); + ClassBuilder::new(c"CapExceptionGuardTestObservation", NSObject::class()).unwrap(); unsafe { builder.add_method( sel!(invalidate), @@ -170,7 +168,7 @@ mod tests { sel!(invalidate), ) }; - let exception = unsafe { Id::from_raw(exception) } + let exception = unsafe { Retained::from_raw(exception) } .expect("removing an unregistered observer must throw"); let description = describe(&exception); assert!( @@ -186,7 +184,7 @@ mod tests { assert!(TEST_GUARD.install(class, sel!(invalidate), guarded)); assert!(!TEST_GUARD.install(class, sel!(invalidate), guarded)); - let instance: Id = unsafe { msg_send_id![class, new] }; + let instance: Retained = unsafe { msg_send![class, new] }; for _ in 0..3 { let before = removal_calls(); let _: () = unsafe { msg_send![&instance, invalidate] }; @@ -197,10 +195,10 @@ mod tests { #[test] fn appkit_still_has_the_touch_bar_finder_method_this_guards() { assert!( - AnyClass::get("NSApplication").is_some(), + AnyClass::get(c"NSApplication").is_some(), "AppKit is not loaded" ); - let class = AnyClass::get("_NSTouchBarFinderObservation") + let class = AnyClass::get(c"_NSTouchBarFinderObservation") .expect("AppKit renamed _NSTouchBarFinderObservation; re-check the guard"); assert!(class.verify_sel::<(), ()>(sel!(invalidate)).is_ok()); } diff --git a/apps/desktop-gpui/src/platform/macos_occlusion.rs b/apps/desktop-gpui/src/platform/macos_occlusion.rs index 0c440597919..6e6dc8fffba 100644 --- a/apps/desktop-gpui/src/platform/macos_occlusion.rs +++ b/apps/desktop-gpui/src/platform/macos_occlusion.rs @@ -7,7 +7,7 @@ use objc2::{ sel, }; -type OcclusionStateFn = unsafe extern "C" fn(*mut AnyObject, Sel) -> usize; +type OcclusionStateFn = unsafe extern "C-unwind" fn(*mut AnyObject, Sel) -> usize; struct OcclusionShim { original: OnceLock, @@ -46,15 +46,12 @@ impl OcclusionShim { let added = unsafe { class_addMethod( (class as *const AnyClass).cast_mut().cast(), - selector.as_ptr(), - Some(std::mem::transmute::< - OcclusionStateFn, - unsafe extern "C" fn(), - >(shim)), + selector, + std::mem::transmute::(shim), c"Q@:".as_ptr(), ) }; - Ok(Bool::from_raw(added).as_bool()) + Ok(added.as_bool()) } unsafe fn state(&self, receiver: *mut AnyObject, selector: Sel) -> usize { @@ -121,23 +118,29 @@ unsafe fn is_visible(receiver: *mut AnyObject) -> bool { static WINDOW_SHIM: OcclusionShim = OcclusionShim::new(); static PANEL_SHIM: OcclusionShim = OcclusionShim::new(); -unsafe extern "C" fn window_occlusion_state(receiver: *mut AnyObject, selector: Sel) -> usize { +unsafe extern "C-unwind" fn window_occlusion_state( + receiver: *mut AnyObject, + selector: Sel, +) -> usize { unsafe { WINDOW_SHIM.state(receiver, selector) } } -unsafe extern "C" fn panel_occlusion_state(receiver: *mut AnyObject, selector: Sel) -> usize { +unsafe extern "C-unwind" fn panel_occlusion_state( + receiver: *mut AnyObject, + selector: Sel, +) -> usize { unsafe { PANEL_SHIM.state(receiver, selector) } } pub(super) fn install() { for (name, state, implementation) in [ ( - "GPUIWindow", + c"GPUIWindow", &WINDOW_SHIM, window_occlusion_state as OcclusionStateFn, ), ( - "GPUIPanel", + c"GPUIPanel", &PANEL_SHIM, panel_occlusion_state as OcclusionStateFn, ), @@ -146,20 +149,27 @@ pub(super) fn install() { continue; }; match state.install(class, implementation) { - Ok(true) => tracing::info!("installed macOS occlusion shim on {name}"), + Ok(true) => { + tracing::info!(name = %name.to_string_lossy(), "installed macOS occlusion shim") + } Ok(false) => {} - Err(error) => tracing::warn!(%error, name, "could not install macOS occlusion shim"), + Err(error) => { + tracing::warn!(%error, name = %name.to_string_lossy(), "could not install macOS occlusion shim") + } } } } #[cfg(test)] mod tests { - use std::cell::Cell; + use std::{ + cell::Cell, + ffi::{CStr, CString}, + }; use objc2::{ - ClassType, msg_send_id, - rc::Id, + ClassType, msg_send, + rc::Retained, runtime::{ClassBuilder, NSObject}, }; @@ -181,37 +191,49 @@ mod tests { overridden_panel: &'static AnyClass, } - unsafe extern "C" fn native_window_state(_receiver: *mut AnyObject, _selector: Sel) -> usize { + unsafe extern "C-unwind" fn native_window_state( + _receiver: *mut AnyObject, + _selector: Sel, + ) -> usize { WINDOW_CALLS.set(WINDOW_CALLS.get() + 1); RAW_STATE.get() } - unsafe extern "C" fn native_is_visible(_receiver: *mut AnyObject, _selector: Sel) -> Bool { + unsafe extern "C-unwind" fn native_is_visible( + _receiver: *mut AnyObject, + _selector: Sel, + ) -> Bool { Bool::new(VISIBLE.get()) } - unsafe extern "C" fn native_panel_state(_receiver: *mut AnyObject, _selector: Sel) -> usize { + unsafe extern "C-unwind" fn native_panel_state( + _receiver: *mut AnyObject, + _selector: Sel, + ) -> usize { PANEL_CALLS.set(PANEL_CALLS.get() + 1); RAW_STATE.get() } - unsafe extern "C" fn overriding_window_state( + unsafe extern "C-unwind" fn overriding_window_state( receiver: *mut AnyObject, _selector: Sel, ) -> usize { unsafe { msg_send![super(receiver, classes().window), occlusionState] } } - unsafe extern "C" fn overriding_panel_state(receiver: *mut AnyObject, _selector: Sel) -> usize { + unsafe extern "C-unwind" fn overriding_panel_state( + receiver: *mut AnyObject, + _selector: Sel, + ) -> usize { unsafe { msg_send![super(receiver, classes().panel), occlusionState] } } - fn inherit(name: &str, superclass: &AnyClass) -> &'static AnyClass { + fn inherit(name: &CStr, superclass: &AnyClass) -> &'static AnyClass { ClassBuilder::new(name, superclass).unwrap().register() } fn with_method( - name: &str, + name: &CStr, superclass: &AnyClass, implementation: OcclusionStateFn, ) -> &'static AnyClass { @@ -225,7 +247,7 @@ mod tests { CLASSES.get_or_init(|| { let native_window = { let mut builder = - ClassBuilder::new("CapOcclusionTestNativeWindow", NSObject::class()).unwrap(); + ClassBuilder::new(c"CapOcclusionTestNativeWindow", NSObject::class()).unwrap(); unsafe { builder.add_method( sel!(occlusionState), @@ -233,40 +255,38 @@ mod tests { ); builder.add_method( sel!(isVisible), - native_is_visible as unsafe extern "C" fn(_, _) -> _, + native_is_visible as unsafe extern "C-unwind" fn(_, _) -> _, ); } builder.register() }; let native_panel = with_method( - "CapOcclusionTestNativePanel", + c"CapOcclusionTestNativePanel", native_window, native_panel_state, ); - let window = inherit("CapOcclusionTestWindow", native_window); - let panel = inherit("CapOcclusionTestPanel", native_panel); + let window = inherit(c"CapOcclusionTestWindow", native_window); + let panel = inherit(c"CapOcclusionTestPanel", native_panel); assert!(WINDOW_SHIM.install(window, window_occlusion_state).unwrap()); assert!(PANEL_SHIM.install(panel, panel_occlusion_state).unwrap()); let mut inherited_window = window; let mut inherited_panel = panel; for depth in 0..4 { - inherited_window = inherit( - &format!("CapOcclusionTestWindowSubclass{depth}"), - inherited_window, - ); - inherited_panel = inherit( - &format!("CapOcclusionTestPanelSubclass{depth}"), - inherited_panel, - ); + let window_name = + CString::new(format!("CapOcclusionTestWindowSubclass{depth}")).unwrap(); + inherited_window = inherit(&window_name, inherited_window); + let panel_name = + CString::new(format!("CapOcclusionTestPanelSubclass{depth}")).unwrap(); + inherited_panel = inherit(&panel_name, inherited_panel); } let window_override = with_method( - "CapOcclusionTestWindowOverride", + c"CapOcclusionTestWindowOverride", window, overriding_window_state, ); let panel_override = with_method( - "CapOcclusionTestPanelOverride", + c"CapOcclusionTestPanelOverride", panel, overriding_panel_state, ); @@ -276,16 +296,19 @@ mod tests { inherited_window, inherited_panel, overridden_window: inherit( - "CapOcclusionTestInheritedWindowOverride", + c"CapOcclusionTestInheritedWindowOverride", window_override, ), - overridden_panel: inherit("CapOcclusionTestInheritedPanelOverride", panel_override), + overridden_panel: inherit( + c"CapOcclusionTestInheritedPanelOverride", + panel_override, + ), } }) } fn verify_states(class: &AnyClass, expected_window_calls: usize, expected_panel_calls: usize) { - let instance: Id = unsafe { msg_send_id![class, new] }; + let instance: Retained = unsafe { msg_send![class, new] }; for visible in [true, false] { VISIBLE.set(visible); for raw in [0, 0x2, 0x2000, 0x2002] { @@ -372,7 +395,7 @@ mod tests { #[test] fn promoting_a_window_switches_to_the_panel_implementation() { let classes = classes(); - let instance: Id = unsafe { msg_send_id![classes.window, new] }; + let instance: Retained = unsafe { msg_send![classes.window, new] }; RAW_STATE.set(0x2000); WINDOW_CALLS.set(0); PANEL_CALLS.set(0); @@ -392,17 +415,20 @@ mod tests { #[test] fn incompatible_native_method_does_not_install_a_shim() { - unsafe extern "C" fn wrong_signature(_receiver: *mut AnyObject, _selector: Sel) -> f64 { + unsafe extern "C-unwind" fn wrong_signature( + _receiver: *mut AnyObject, + _selector: Sel, + ) -> f64 { 0.0 } let state = OcclusionShim::new(); let mut builder = - ClassBuilder::new("CapOcclusionTestWrongSignature", NSObject::class()).unwrap(); + ClassBuilder::new(c"CapOcclusionTestWrongSignature", NSObject::class()).unwrap(); unsafe { builder.add_method( sel!(occlusionState), - wrong_signature as unsafe extern "C" fn(_, _) -> _, + wrong_signature as unsafe extern "C-unwind" fn(_, _) -> _, ); } let class = builder.register(); @@ -414,12 +440,12 @@ mod tests { fn existing_class_override_is_not_replaced() { let state = OcclusionShim::new(); let class = with_method( - "CapOcclusionTestExistingOverride", + c"CapOcclusionTestExistingOverride", NSObject::class(), native_window_state, ); assert!(!state.install(class, window_occlusion_state).unwrap()); - let instance: Id = unsafe { msg_send_id![class, new] }; + let instance: Retained = unsafe { msg_send![class, new] }; RAW_STATE.set(0x2000); let raw: usize = unsafe { msg_send![&*instance, occlusionState] }; assert_eq!(raw, 0x2000); @@ -428,7 +454,7 @@ mod tests { #[test] fn missing_native_method_does_not_install_a_shim() { let state = OcclusionShim::new(); - let class = inherit("CapOcclusionTestMissingMethod", NSObject::class()); + let class = inherit(c"CapOcclusionTestMissingMethod", NSObject::class()); assert!(state.install(class, window_occlusion_state).is_err()); assert!(state.original.get().is_none()); assert!(class.instance_method(sel!(occlusionState)).is_none()); diff --git a/apps/desktop-gpui/src/platform/windows.rs b/apps/desktop-gpui/src/platform/windows.rs index a0468465b38..d7e5f693f66 100644 --- a/apps/desktop-gpui/src/platform/windows.rs +++ b/apps/desktop-gpui/src/platform/windows.rs @@ -558,14 +558,17 @@ pub fn window_number(window: &Window) -> Option { pub fn open_image_panel(extensions: &[&str]) -> Option { let mut dialog = rfd::FileDialog::new(); if !extensions.is_empty() { - dialog = dialog.add_filter("Images", extensions); + dialog = dialog.add_filter(crate::i18n::tr::dialog::file::images_filter(), extensions); } dialog.pick_file() } pub fn open_audio_panel() -> Option { rfd::FileDialog::new() - .add_filter("Audio", &["mp3", "wav", "m4a", "ogg", "flac", "aac"]) + .add_filter( + crate::i18n::tr::dialog::file::audio_filter(), + &["mp3", "wav", "m4a", "ogg", "flac", "aac"], + ) .pick_file() } @@ -629,7 +632,7 @@ pub fn set_dock_icon(_png: &[u8]) {} pub fn save_file_panel(suggested: &str, extensions: &[&str]) -> Option { let mut dialog = rfd::FileDialog::new().set_file_name(suggested); if !extensions.is_empty() { - dialog = dialog.add_filter("Export", extensions); + dialog = dialog.add_filter(crate::i18n::tr::dialog::file::export_filter(), extensions); } dialog.save_file() } @@ -642,9 +645,9 @@ pub fn copy_file_to_clipboard(path: &Path, _cx: &gpui::App) -> Result<(), String .to_str() .ok_or("The clipboard file path is not valid Unicode")?; ClipboardContext::new() - .map_err(|error| format!("Clipboard unavailable: {error}"))? + .map_err(crate::i18n::tr::dialog::clipboard::unavailable_error)? .set_files(vec![path.to_owned()]) - .map_err(|error| format!("Could not copy the file to the clipboard: {error}")) + .map_err(crate::i18n::tr::dialog::clipboard::copy_file) } pub struct ClipboardImage(clipboard_rs::RustImageData); @@ -662,9 +665,9 @@ pub fn write_clipboard_image(image: ClipboardImage, _cx: &gpui::App) -> Result<( use clipboard_rs::{Clipboard, ClipboardContext}; ClipboardContext::new() - .map_err(|error| format!("Clipboard unavailable: {error}"))? + .map_err(crate::i18n::tr::dialog::clipboard::unavailable_error)? .set_image(image.0) - .map_err(|error| format!("Could not copy the image to the clipboard: {error}")) + .map_err(crate::i18n::tr::dialog::clipboard::copy_image) } pub fn desktop_picture_path() -> Option { diff --git a/apps/desktop-gpui/src/screenshot_annotations.rs b/apps/desktop-gpui/src/screenshot_annotations.rs index 11bd4e64a7a..a493e263073 100644 --- a/apps/desktop-gpui/src/screenshot_annotations.rs +++ b/apps/desktop-gpui/src/screenshot_annotations.rs @@ -42,6 +42,7 @@ use gpui::{ }; use crate::editor_sidebar::{BACKGROUND_COLORS, hex_digit_count, hex_to_rgb}; +use crate::i18n::tr; use crate::screenshot_editor::ScreenshotEditorWindow; use crate::theme::Theme; use crate::ui; @@ -88,7 +89,7 @@ impl Tool { } } - pub fn label(self) -> &'static str { + pub fn id(self) -> &'static str { match self { Tool::Select => "Select", Tool::Draw => "Draw", @@ -100,6 +101,18 @@ impl Tool { } } + pub fn label(self) -> &'static str { + match self { + Tool::Select => tr::screenshot_editor::tool::select(), + Tool::Draw => tr::screenshot_editor::tool::draw(), + Tool::Arrow => tr::screenshot_editor::tool::arrow(), + Tool::Rectangle => tr::screenshot_editor::tool::rectangle(), + Tool::Mask => tr::screenshot_editor::tool::mask(), + Tool::Circle => tr::screenshot_editor::tool::circle(), + Tool::Text => tr::screenshot_editor::tool::text_label(), + } + } + /// The `kbd` chip on the button's tooltip, and the bare key `Editor.tsx` /// binds it to (`:120-152`). pub fn shortcut(self) -> &'static str { @@ -143,12 +156,12 @@ pub fn annotation_icon(annotation_type: AnnotationType) -> &'static str { /// `ANNOTATION_TYPE_LABELS` (`LayersPanel.tsx:23-30`). pub fn annotation_label(annotation_type: AnnotationType) -> &'static str { match annotation_type { - AnnotationType::Arrow => "Arrow", - AnnotationType::Rectangle => "Rectangle", - AnnotationType::Circle => "Circle", - AnnotationType::Mask => "Mask", - AnnotationType::Text => "Text", - AnnotationType::Draw => "Draw", + AnnotationType::Arrow => tr::screenshot_editor::tool::arrow(), + AnnotationType::Rectangle => tr::screenshot_editor::tool::rectangle(), + AnnotationType::Circle => tr::screenshot_editor::tool::circle(), + AnnotationType::Mask => tr::screenshot_editor::tool::mask(), + AnnotationType::Text => tr::screenshot_editor::tool::text_label(), + AnnotationType::Draw => tr::screenshot_editor::tool::draw(), } } @@ -1636,7 +1649,7 @@ impl ScreenshotEditorWindow { fill_color: TRANSPARENT.to_string(), opacity: style.as_ref().map_or(1., |(_, _, opacity)| *opacity), rotation: 0., - text: is_text.then(|| "Text".to_string()), + text: is_text.then(|| tr::screenshot_editor::annotation::default_text().to_string()), mask_type: is_mask.then_some(MaskType::Pixelate), mask_level: is_mask.then_some(MASK_DEFAULT_LEVEL), points: (annotation_type == AnnotationType::Draw).then(|| vec![[x, y]]), @@ -2526,7 +2539,11 @@ impl ScreenshotEditorWindow { if !is_mask { row = row.child(config_item( &theme, - if is_text { "Color" } else { "Stroke" }, + if is_text { + tr::screenshot_editor::style::color() + } else { + tr::screenshot_editor::annotation::stroke() + }, None, self.render_color_button(ColorTarget::Stroke, cx), )); @@ -2534,7 +2551,7 @@ impl ScreenshotEditorWindow { if !is_mask && !is_text { row = row.child(config_item( &theme, - "Width", + tr::screenshot_editor::style::width(), Some(format!("{}px", annotation.stroke_width.round() as i64)), self.annotation_slider(AnnotationSlider::Width, px(80.), cx) .into_any_element(), @@ -2543,7 +2560,7 @@ impl ScreenshotEditorWindow { if matches!(kind, AnnotationType::Rectangle | AnnotationType::Circle) { row = row.child(config_item( &theme, - "Fill", + tr::screenshot_editor::annotation::fill(), None, self.render_color_button(ColorTarget::Fill, cx), )); @@ -2551,7 +2568,7 @@ impl ScreenshotEditorWindow { if !is_mask { row = row.child(config_item( &theme, - "Opacity", + tr::screenshot_editor::style::opacity(), Some(format!("{}%", (annotation.opacity * 100.).round() as i64)), self.annotation_slider(AnnotationSlider::Opacity, px(80.), cx) .into_any_element(), @@ -2562,19 +2579,19 @@ impl ScreenshotEditorWindow { row = row .child(config_item( &theme, - "Style", + tr::screenshot_editor::annotation::mask_style(), None, div() .flex() .flex_row() .gap(px(4.)) - .child(self.render_mask_pill("Blur", MaskType::Blur, mask_type, cx)) - .child(self.render_mask_pill("Pixelate", MaskType::Pixelate, mask_type, cx)) + .child(self.render_mask_pill(MaskType::Blur, mask_type, cx)) + .child(self.render_mask_pill(MaskType::Pixelate, mask_type, cx)) .into_any_element(), )) .child(config_item( &theme, - "Intensity", + tr::screenshot_editor::annotation::intensity(), Some(format!( "{}", annotation.mask_level.unwrap_or(MASK_FALLBACK_LEVEL).round() as i64 @@ -2586,7 +2603,7 @@ impl ScreenshotEditorWindow { if is_text { row = row.child(config_item( &theme, - "Size", + tr::screenshot_editor::style::size_label(), Some(format!("{}px", annotation.height.round() as i64)), self.annotation_slider(AnnotationSlider::TextSize, px(80.), cx) .into_any_element(), @@ -2599,16 +2616,19 @@ impl ScreenshotEditorWindow { /// The two `Style` pills (`AnnotationConfig.tsx:100-123`). fn render_mask_pill( &self, - label: &'static str, value: MaskType, current: MaskType, cx: &mut Context, ) -> AnyElement { let theme = self.theme; let active = value == current; + let (id, label) = match value { + MaskType::Blur => ("Blur", tr::screenshot_editor::style::blur()), + MaskType::Pixelate => ("Pixelate", tr::screenshot_editor::annotation::pixelate()), + }; div() .id(gpui::SharedString::from(format!( - "screenshot-mask-style-{label}" + "screenshot-mask-style-{id}" ))) .flex() .items_center() @@ -2920,7 +2940,11 @@ impl ScreenshotEditorWindow { .when(selected, |this| this.border_2()) .children(checker_quarters(20.)) .tooltip(move |_window, cx| { - ui::Tooltip::new(&theme, "Transparent").view(cx) + ui::Tooltip::new( + &theme, + tr::screenshot_editor::annotation::transparent(), + ) + .view(cx) }) .on_click(cx.listener(move |this, _, window, cx| { cx.stop_propagation(); @@ -3164,14 +3188,14 @@ impl ScreenshotEditorWindow { div() .text_size(px(12.)) .text_color(theme.gray_10) - .child("No layers yet"), + .child(tr::screenshot_editor::layers::empty()), ) .child( div() .mt(px(4.)) .text_size(px(10.)) .text_color(theme.gray_8) - .child("Use the tools above to add annotations"), + .child(tr::screenshot_editor::layers::empty_hint()), ) .into_any_element(); } diff --git a/apps/desktop-gpui/src/screenshot_crop.rs b/apps/desktop-gpui/src/screenshot_crop.rs index a882b471625..90b9ba6db17 100644 --- a/apps/desktop-gpui/src/screenshot_crop.rs +++ b/apps/desktop-gpui/src/screenshot_crop.rs @@ -37,6 +37,7 @@ use crate::editor_crop::{ self as engine, CropAnim, CropBounds, CropDrag, CropField, CropHit, CropMenuChoice, CropState, HANDLES, ResizeSession, SE_HANDLE, Vec2, }; +use crate::i18n::tr; use crate::screenshot_editor::ScreenshotEditorWindow; use crate::theme::Theme; use crate::ui; @@ -757,8 +758,18 @@ impl ScreenshotEditorWindow { .flex_row() .gap(px(32.)) .flex_none() - .child(group("Size", CropField::Width, CropField::Height, self)) - .child(group("Position", CropField::X, CropField::Y, self)), + .child(group( + tr::screenshot_editor::style::size_label(), + CropField::Width, + CropField::Height, + self, + )) + .child(group( + tr::screenshot_editor::crop::position(), + CropField::X, + CropField::Y, + self, + )), ) .child( div() @@ -807,7 +818,7 @@ impl ScreenshotEditorWindow { .child( ui::EditorButton::plain(&theme, "screenshot-crop-full") .left_icon("icons/maximize.svg") - .label("Full") + .label(tr::screenshot_editor::crop::full()) .disabled(full) .on_click(cx.listener(|this, _, window, cx| { if let Some(dialog) = this.crop.as_mut() { @@ -820,7 +831,7 @@ impl ScreenshotEditorWindow { .child( ui::EditorButton::plain(&theme, "screenshot-crop-reset") .left_icon("icons/circle-x.svg") - .label("Reset") + .label(tr::common::reset()) .disabled(untouched) .on_click(cx.listener(|this, _, window, cx| { if let Some(dialog) = this.crop.as_mut() { @@ -1046,7 +1057,7 @@ impl ScreenshotEditorWindow { .text_size(px(12.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.gray_10)) - .child("Loading frame…") + .child(tr::screenshot_editor::crop::loading_frame()) .into_any_element(), ), }) @@ -1089,7 +1100,7 @@ impl ScreenshotEditorWindow { ui::ButtonVariant::Primary, ui::ButtonSize::Md, ) - .label("Save") + .label(tr::common::save()) .on_click(cx.listener(|this, _, window, cx| this.save_crop_dialog(window, cx))), ) .into_any_element() diff --git a/apps/desktop-gpui/src/screenshot_editor.rs b/apps/desktop-gpui/src/screenshot_editor.rs index af940970534..b550b239155 100644 --- a/apps/desktop-gpui/src/screenshot_editor.rs +++ b/apps/desktop-gpui/src/screenshot_editor.rs @@ -46,6 +46,7 @@ use crate::editor_sidebar::{ self, BACKGROUND_COLORS, BACKGROUND_IMAGE_EXTENSIONS, BACKGROUND_THEMES, DEFAULT_GRADIENT_FROM, DEFAULT_GRADIENT_TO, GRADIENT_PRESETS, color_to_hsla, hex_to_rgb, }; +use crate::i18n::tr; use crate::screenshot_annotations::{self as annotations, AnnotationState, Tool}; use crate::theme::LocaleFont as _; use crate::theme::Theme; @@ -134,21 +135,21 @@ pub(crate) fn bundle_image_path(bundle: &Path) -> Option { /// Read `original.png` (or the first PNG in the bundle), the recording meta /// and the project config -- the disk half of `create_standalone_instance`. pub fn load_source(bundle: &Path) -> Result { - let image_path = - bundle_image_path(bundle).ok_or_else(|| format!("No PNG found in {}", bundle.display()))?; + let image_path = bundle_image_path(bundle) + .ok_or_else(|| tr::screenshot_editor::error::no_image(bundle.display()))?; - let image = image::open(&image_path).map_err(|e| format!("Failed to open image: {e}"))?; + let image = image::open(&image_path).map_err(tr::screenshot_editor::error::open_image)?; let (width, height) = (image.width(), image.height()); if width > MAX_DIMENSION || height > MAX_DIMENSION { - return Err(format!("Image dimensions exceed maximum: {width}x{height}")); + return Err(tr::screenshot_editor::error::too_large(width, height)); } - let meta = RecordingMeta::load_for_project(bundle) - .map_err(|e| format!("Failed to load screenshot meta: {e}"))?; + let meta = + RecordingMeta::load_for_project(bundle).map_err(tr::screenshot_editor::error::load_meta)?; let studio_meta = match &meta.inner { RecordingMetaInner::Studio(inner) => (**inner).clone(), RecordingMetaInner::Instant(_) => { - return Err("Not a screenshot bundle".to_string()); + return Err(tr::screenshot_editor::error::not_screenshot().to_string()); } }; let config = ProjectConfiguration::load(bundle).unwrap_or_default(); @@ -219,7 +220,7 @@ pub async fn run_still_renderer( } { Ok(constants) => constants, Err(error) => { - let _ = setup_tx.send(Err(format!("The GPU renderer failed to start: {error}"))); + let _ = setup_tx.send(Err(tr::screenshot_editor::error::renderer_failed(error))); return; } }; @@ -480,12 +481,25 @@ enum BgTab { } impl BgTab { - const ALL: [(Self, &'static str); 4] = [ - (Self::Wallpaper, "Wallpaper"), - (Self::Image, "Image"), - (Self::Color, "Color"), - (Self::Gradient, "Gradient"), - ]; + const ALL: [Self; 4] = [Self::Wallpaper, Self::Image, Self::Color, Self::Gradient]; + + fn id(self) -> &'static str { + match self { + Self::Wallpaper => "Wallpaper", + Self::Image => "Image", + Self::Color => "Color", + Self::Gradient => "Gradient", + } + } + + fn label(self) -> &'static str { + match self { + Self::Wallpaper => tr::screenshot_editor::background::wallpaper(), + Self::Image => tr::screenshot_editor::background::image(), + Self::Color => tr::screenshot_editor::background::color(), + Self::Gradient => tr::screenshot_editor::background::gradient(), + } + } fn for_source(source: &BackgroundSource) -> Self { match source { @@ -543,11 +557,11 @@ impl Popover { fn tooltip(self) -> &'static str { match self { - Self::Background => "Background", - Self::Padding => "Padding", - Self::Rounding => "Corner Rounding", - Self::Shadow => "Shadow", - Self::Border => "Border", + Self::Background => tr::screenshot_editor::style::background(), + Self::Padding => tr::screenshot_editor::style::padding(), + Self::Rounding => tr::screenshot_editor::style::corner_rounding(), + Self::Shadow => tr::screenshot_editor::style::shadow(), + Self::Border => tr::screenshot_editor::style::border(), } } @@ -1539,7 +1553,10 @@ impl ScreenshotEditorWindow { cx.spawn_in(window, async move |this, cx| { #[cfg(target_os = "linux")] let picked = crate::platform::open_file_panel_async( - &[("Image", &BACKGROUND_IMAGE_EXTENSIONS[..])], + &[( + tr::screenshot_editor::background::image_files(), + &BACKGROUND_IMAGE_EXTENSIONS[..], + )], None, cx, ) @@ -1945,10 +1962,10 @@ impl ScreenshotEditorWindow { let rendered = if export_tx.send(request).await.is_ok() { match reply_rx.await { Ok(result) => result, - Err(_) => Err("The renderer stopped before the export finished".into()), + Err(_) => Err(tr::screenshot_editor::error::renderer_stopped().into()), } } else { - Err("The screenshot renderer is not running".into()) + Err(tr::screenshot_editor::error::renderer_not_running().into()) }; // The canvas pass counts as "rendering" over there too @@ -2002,7 +2019,7 @@ impl ScreenshotEditorWindow { { Ok(()) => { this.toast_success( - "Screenshot copied to clipboard!", + tr::screenshot_editor::toast::copied(), window, cx, ); @@ -2033,7 +2050,11 @@ impl ScreenshotEditorWindow { match written { Ok(()) => { this.update_in(cx, |this, window, cx| { - this.toast_success("Screenshot saved!", window, cx) + this.toast_success( + tr::screenshot_editor::toast::saved(), + window, + cx, + ) }) .ok(); } @@ -2113,7 +2134,7 @@ impl ScreenshotEditorWindow { self.toast_update( toast, ToastKind::Success, - "Share link copied to clipboard", + tr::screenshot_editor::toast::link_copied(), window, cx, ); @@ -2145,7 +2166,7 @@ impl ScreenshotEditorWindow { }; self.exporting = true; self.export_status = ExportStatus::Encoding; - let toast = self.toast_loading("Preparing upload", cx); + let toast = self.toast_loading(tr::screenshot_editor::share::preparing(), cx); cx.notify(); let config = self.project.clone(); @@ -2173,7 +2194,7 @@ impl ScreenshotEditorWindow { .background_executor() .spawn(async move { RecordingMeta::load_for_project(&meta_bundle) - .map_err(|error| format!("Failed to load screenshot metadata: {error}")) + .map_err(tr::screenshot_editor::error::load_metadata) }) .await; let mut meta = match meta { @@ -2205,7 +2226,7 @@ impl ScreenshotEditorWindow { this.share_progress( toast, ExportStatus::Rendering, - "Rendering screenshot", + tr::screenshot_editor::share::rendering(), window, cx, ); @@ -2219,10 +2240,10 @@ impl ScreenshotEditorWindow { let rendered = if export_tx.send(request).await.is_ok() { match reply_rx.await { Ok(result) => result, - Err(_) => Err("The renderer stopped before the export finished".into()), + Err(_) => Err(tr::screenshot_editor::error::renderer_stopped().into()), } } else { - Err("The screenshot renderer is not running".into()) + Err(tr::screenshot_editor::error::renderer_not_running().into()) }; let raw = match rendered { Ok(raw) => raw, @@ -2245,7 +2266,7 @@ impl ScreenshotEditorWindow { this.share_progress( toast, ExportStatus::Encoding, - "Preparing upload", + tr::screenshot_editor::share::preparing(), window, cx, ); @@ -2274,7 +2295,7 @@ impl ScreenshotEditorWindow { this.share_progress( toast, ExportStatus::Uploading, - "Uploading screenshot", + tr::screenshot_editor::share::uploading(), window, cx, ); @@ -2288,7 +2309,7 @@ impl ScreenshotEditorWindow { }); let outcome = match upload.await { Ok(outcome) => outcome, - Err(error) => Err(format!("The upload task failed: {error}")), + Err(error) => Err(tr::screenshot_editor::error::upload_task(error)), }; let uploaded = match outcome { @@ -2297,7 +2318,7 @@ impl ScreenshotEditorWindow { this.update_in(cx, |this, window, cx| { this.finish_share_error( toast, - "Sign in to Cap in your browser, then share again".into(), + tr::screenshot_editor::error::sign_in_to_share().into(), window, cx, ); @@ -2312,7 +2333,7 @@ impl ScreenshotEditorWindow { this.update_in(cx, |this, window, cx| { this.finish_share_error( toast, - "This feature requires an upgraded plan".into(), + tr::screenshot_editor::error::upgrade_required().into(), window, cx, ) @@ -2342,7 +2363,7 @@ impl ScreenshotEditorWindow { .background_executor() .spawn(async move { meta.save_for_project() - .map_err(|error| format!("Error saving project: {error}")) + .map_err(tr::screenshot_editor::error::save_project) }) .await; @@ -2362,9 +2383,9 @@ impl ScreenshotEditorWindow { cx.spawn_in(window, async move |_this, cx| { if !crate::platform::confirm_dialog( "Cap", - "Are you sure you want to delete this screenshot?", - "Yes", - "No", + tr::main::delete_screenshot_confirm(), + tr::common::yes(), + tr::common::no(), false, ) { return; @@ -2388,20 +2409,31 @@ impl ScreenshotEditorWindow { // -- Aspect ratio ------------------------------------------------------- - const ASPECT_OPTIONS: [(Option, &'static str); 6] = [ - (None, "Auto"), - (Some(cap_project::AspectRatio::Wide), "Wide ⋅16:9"), - (Some(cap_project::AspectRatio::Vertical), "Vertical ⋅9:16"), - (Some(cap_project::AspectRatio::Square), "Square ⋅1:1"), - (Some(cap_project::AspectRatio::Classic), "Classic ⋅4:3"), - (Some(cap_project::AspectRatio::Tall), "Tall ⋅3:4"), + const ASPECT_OPTIONS: [Option; 6] = [ + None, + Some(cap_project::AspectRatio::Wide), + Some(cap_project::AspectRatio::Vertical), + Some(cap_project::AspectRatio::Square), + Some(cap_project::AspectRatio::Classic), + Some(cap_project::AspectRatio::Tall), ]; + fn aspect_option_label(value: &Option) -> &'static str { + match value { + None => tr::screenshot_editor::aspect::auto(), + Some(cap_project::AspectRatio::Wide) => tr::screenshot_editor::aspect::wide(), + Some(cap_project::AspectRatio::Vertical) => tr::screenshot_editor::aspect::vertical(), + Some(cap_project::AspectRatio::Square) => tr::screenshot_editor::aspect::square(), + Some(cap_project::AspectRatio::Classic) => tr::screenshot_editor::aspect::classic(), + Some(cap_project::AspectRatio::Tall) => tr::screenshot_editor::aspect::tall(), + } + } + /// `KSelect.Value` (`AspectRatioSelect.tsx:78-88`) -- the trigger shows the /// bare ratio, not the option's name. fn aspect_label(&self) -> &'static str { match &self.project.aspect_ratio { - None => "Auto", + None => tr::screenshot_editor::aspect::auto(), Some(cap_project::AspectRatio::Wide) => "16:9", Some(cap_project::AspectRatio::Vertical) => "9:16", Some(cap_project::AspectRatio::Square) => "1:1", @@ -2413,15 +2445,18 @@ impl ScreenshotEditorWindow { fn aspect_menu_items(&self) -> Vec { Self::ASPECT_OPTIONS .iter() - .map(|(value, label)| { - ui::MenuItem::new(*label, aspect_eq(value, &self.project.aspect_ratio)) + .map(|value| { + ui::MenuItem::new( + Self::aspect_option_label(value), + aspect_eq(value, &self.project.aspect_ratio), + ) }) .collect() } fn choose_aspect(&mut self, index: usize, window: &mut Window, cx: &mut Context) { self.menu = None; - let Some((next, _)) = Self::ASPECT_OPTIONS.get(index) else { + let Some(next) = Self::ASPECT_OPTIONS.get(index) else { return; }; let next = next.clone(); @@ -2438,29 +2473,33 @@ impl ScreenshotEditorWindow { ); } - const CORNER_STYLES: [(CornerStyle, &'static str); 2] = [ - (CornerStyle::Squircle, "Squircle"), - (CornerStyle::Rounded, "Rounded"), - ]; + const CORNER_STYLES: [CornerStyle; 2] = [CornerStyle::Squircle, CornerStyle::Rounded]; - fn corner_style_label(&self) -> &'static str { - match self.project.background.rounding_type { - CornerStyle::Squircle => "Squircle", - CornerStyle::Rounded => "Rounded", + fn corner_style_name(style: CornerStyle) -> &'static str { + match style { + CornerStyle::Squircle => tr::screenshot_editor::corner::squircle(), + CornerStyle::Rounded => tr::screenshot_editor::corner::rounded(), } } + fn corner_style_label(&self) -> &'static str { + Self::corner_style_name(self.project.background.rounding_type) + } + fn menu_items(&self, kind: MenuKind) -> Vec { match kind { MenuKind::Aspect => self.aspect_menu_items(), MenuKind::More => vec![ - ui::MenuItem::new("Open Folder", false), - ui::MenuItem::new("Delete", false), + ui::MenuItem::new(tr::screenshot_editor::menu::open_folder(), false), + ui::MenuItem::new(tr::screenshot_editor::menu::delete(), false), ], MenuKind::CornerStyle => Self::CORNER_STYLES .iter() - .map(|(style, label)| { - ui::MenuItem::new(*label, *style == self.project.background.rounding_type) + .map(|style| { + ui::MenuItem::new( + Self::corner_style_name(*style), + *style == self.project.background.rounding_type, + ) }) .collect(), } @@ -2483,7 +2522,7 @@ impl ScreenshotEditorWindow { } MenuKind::CornerStyle => { self.menu = None; - let Some((style, _)) = Self::CORNER_STYLES.get(index) else { + let Some(style) = Self::CORNER_STYLES.get(index) else { return; }; let style = *style; @@ -2742,7 +2781,7 @@ impl ScreenshotEditorWindow { &theme, "screenshot-layers-toggle", "icons/layers.svg", - "Layers", + tr::screenshot_editor::layers::title(), "L", layers_open, cx.listener(|this, _, _window, cx| { @@ -2754,7 +2793,7 @@ impl ScreenshotEditorWindow { .children(Tool::ALL.map(|tool| { tool_button( &theme, - gpui::SharedString::from(format!("screenshot-tool-{}", tool.label())), + gpui::SharedString::from(format!("screenshot-tool-{}", tool.id())), tool.icon(), tool.label(), tool.shortcut(), @@ -2773,10 +2812,10 @@ impl ScreenshotEditorWindow { let crop_enabled = self.image_size.is_some(); let exporting = self.exporting; let share_tooltip = match self.export_status { - ExportStatus::Rendering => "Rendering screenshot", - ExportStatus::Encoding => "Preparing upload", - ExportStatus::Uploading => "Uploading screenshot", - ExportStatus::Idle => "Create shareable link", + ExportStatus::Rendering => tr::screenshot_editor::share::rendering(), + ExportStatus::Encoding => tr::screenshot_editor::share::preparing(), + ExportStatus::Uploading => tr::screenshot_editor::share::uploading(), + ExportStatus::Idle => tr::screenshot_editor::share::create_link(), }; let tools = div() @@ -2800,7 +2839,7 @@ impl ScreenshotEditorWindow { .as_ref() .is_some_and(|(kind, _)| *kind == MenuKind::Aspect), ) - .tooltip(&theme, "Aspect Ratio") + .tooltip(&theme, tr::screenshot_editor::header::aspect_ratio()) .on_click(cx.listener(|this, _, window, cx| { cx.stop_propagation(); this.toggle_menu(MenuKind::Aspect, Anchor::Aspect, window, cx); @@ -2812,7 +2851,7 @@ impl ScreenshotEditorWindow { .left_icon("icons/crop.svg") .icon_size(px(16.)) .disabled(!crop_enabled) - .tooltip(&theme, "Crop Image") + .tooltip(&theme, tr::screenshot_editor::header::crop_image()) .on_click(cx.listener(|this, _, window, cx| { cx.stop_propagation(); this.open_crop_dialog(window, cx); @@ -2910,7 +2949,7 @@ impl ScreenshotEditorWindow { .left_icon("icons/copy.svg") .icon_size(px(16.)) .disabled(exporting) - .tooltip(&theme, "Copy to Clipboard") + .tooltip(&theme, tr::screenshot_editor::header::copy()) .on_click(cx.listener(|this, _, window, cx| { cx.stop_propagation(); this.export_image(ExportDestination::Clipboard, window, cx); @@ -2921,7 +2960,7 @@ impl ScreenshotEditorWindow { .left_icon("icons/save.svg") .icon_size(px(16.)) .disabled(exporting) - .tooltip(&theme, "Save") + .tooltip(&theme, tr::common::save()) .on_click(cx.listener(|this, _, window, cx| { cx.stop_propagation(); this.export_image(ExportDestination::File, window, cx); @@ -2950,7 +2989,7 @@ impl ScreenshotEditorWindow { .as_ref() .is_some_and(|(kind, _)| *kind == MenuKind::More), ) - .tooltip(&theme, "More Actions") + .tooltip(&theme, tr::screenshot_editor::header::more_actions()) .on_click(cx.listener(|this, _, window, cx| { cx.stop_propagation(); this.toggle_menu(MenuKind::More, Anchor::More, window, cx); @@ -3015,7 +3054,7 @@ impl ScreenshotEditorWindow { .size(px(16.)) .text_color(theme.gray_12), ) - .child("Layers"), + .child(tr::screenshot_editor::layers::title()), ) .child( div() @@ -3057,7 +3096,7 @@ impl ScreenshotEditorWindow { .border_color(theme.gray_3) .text_size(px(10.)) .text_color(theme.gray_9) - .child("Drag to reorder • Top = front"), + .child(tr::screenshot_editor::layers::reorder_hint()), ) } @@ -3104,7 +3143,7 @@ impl ScreenshotEditorWindow { .font_weight(FontWeight::MEDIUM) .text_color(theme.blue_11) .hover(|style| style.text_color(theme.blue_9)) - .child("Done") + .child(tr::screenshot_editor::annotation::done()) .on_click(cx.listener(|this, _, _window, cx| { cx.stop_propagation(); this.selected_annotation = None; @@ -3225,13 +3264,15 @@ impl ScreenshotEditorWindow { .flex_row() .items_center() .gap(px(8.)) - .children(BgTab::ALL.map(|(tab, label)| { + .children(BgTab::ALL.map(|tab| { let selected = tab == current; div() .id(gpui::SharedString::from(format!( - "screenshot-bg-tab-{label}" + "screenshot-bg-tab-{}", + tab.id() ))) .flex_1() + .min_w_0() .flex() .justify_center() .py(px(10.)) @@ -3250,7 +3291,7 @@ impl ScreenshotEditorWindow { .text_color(theme.gray_11) .hover(|style| style.border_color(theme.gray_7)) }) - .child(label) + .child(div().truncate().child(tab.label())) .on_click(cx.listener(move |this, _, window, cx| { cx.stop_propagation(); this.choose_bg_tab(tab, window, cx); @@ -3448,7 +3489,7 @@ impl ScreenshotEditorWindow { div() .text_size(px(13.)) .text_color(theme.gray_12) - .child("Click to select or drag and drop image"), + .child(tr::screenshot_editor::background::choose_image()), ) .on_click(cx.listener(|this, _, window, cx| { cx.stop_propagation(); @@ -3576,7 +3617,7 @@ impl ScreenshotEditorWindow { return div() .text_size(px(12.)) .text_color(theme.gray_11) - .child("Animated Gradient is rendered as a still image for screenshots. Choose another background to replace it.") + .child(tr::screenshot_editor::background::animated_gradient()) .into_any_element(); } let (from, to, angle) = match &self.project.background.source { @@ -3668,7 +3709,7 @@ impl ScreenshotEditorWindow { .flex_col() .gap(px(24.)) .child( - ui::Field::plain(&theme, "Background Image") + ui::Field::plain(&theme, tr::screenshot_editor::background::image_title()) .icon("icons/image.svg") .icon_size(px(16.)) .child(self.render_bg_tabs(cx)) @@ -3683,10 +3724,15 @@ impl ScreenshotEditorWindow { .child(tab_content), ) .child( - ui::Field::plain(&theme, "Background Blur") + ui::Field::plain(&theme, tr::screenshot_editor::background::blur_title()) .icon("icons/bg-blur.svg") .icon_size(px(16.)) - .child(self.slider_row("Blur", StyleSlider::Blur, "%", cx)), + .child(self.slider_row( + tr::screenshot_editor::style::blur(), + StyleSlider::Blur, + "%", + cx, + )), ) .into_any_element() } @@ -3694,7 +3740,12 @@ impl ScreenshotEditorWindow { fn render_padding_popover(&self, cx: &mut Context) -> AnyElement { div() .p(px(16.)) - .child(self.slider_row("Padding", StyleSlider::Padding, "px", cx)) + .child(self.slider_row( + tr::screenshot_editor::style::padding(), + StyleSlider::Padding, + "px", + cx, + )) .into_any_element() } @@ -3705,7 +3756,12 @@ impl ScreenshotEditorWindow { .flex() .flex_col() .gap(px(16.)) - .child(self.slider_row("Rounding", StyleSlider::Rounding, "px", cx)) + .child(self.slider_row( + tr::screenshot_editor::style::rounding(), + StyleSlider::Rounding, + "px", + cx, + )) .child( div() .flex() @@ -3715,7 +3771,7 @@ impl ScreenshotEditorWindow { div() .text_size(px(10.4)) .text_color(theme.gray_11) - .child("CORNER STYLE"), + .child(tr::screenshot_editor::corner::style()), ) .child( self.anchored( @@ -3766,35 +3822,38 @@ impl ScreenshotEditorWindow { fn render_shadow_popover(&self, cx: &mut Context) -> AnyElement { let theme = self.theme; let open = self.shadow_advanced.is_open(); - let body = div() - .mt(px(16.)) - .flex() - .flex_col() - .gap(px(24.)) - .font_weight(FontWeight::MEDIUM) - .child(ui::Field::plain(&theme, "Size").child(self.slider( - StyleSlider::ShadowSize, - None, - cx, - ))) - .child(ui::Field::plain(&theme, "Opacity").child(self.slider( - StyleSlider::ShadowOpacity, - None, - cx, - ))) - .child(ui::Field::plain(&theme, "Blur").child(self.slider( - StyleSlider::ShadowBlur, - None, - cx, - ))) - .into_any_element(); + let body = + div() + .mt(px(16.)) + .flex() + .flex_col() + .gap(px(24.)) + .font_weight(FontWeight::MEDIUM) + .child( + ui::Field::plain(&theme, tr::screenshot_editor::style::size_label()) + .child(self.slider(StyleSlider::ShadowSize, None, cx)), + ) + .child( + ui::Field::plain(&theme, tr::screenshot_editor::style::opacity()) + .child(self.slider(StyleSlider::ShadowOpacity, None, cx)), + ) + .child( + ui::Field::plain(&theme, tr::screenshot_editor::style::blur()) + .child(self.slider(StyleSlider::ShadowBlur, None, cx)), + ) + .into_any_element(); div() .p(px(16.)) .flex() .flex_col() .gap(px(16.)) - .child(self.slider_row("Shadow", StyleSlider::Shadow, "%", cx)) + .child(self.slider_row( + tr::screenshot_editor::style::shadow(), + StyleSlider::Shadow, + "%", + cx, + )) .child( div() .w_full() @@ -3811,7 +3870,7 @@ impl ScreenshotEditorWindow { .font_weight(FontWeight::MEDIUM) .text_color(theme.gray_12) .hover(|style| style.text_color(theme.gray_10)) - .child("Advanced shadow settings") + .child(tr::screenshot_editor::style::advanced_shadow()) .child( svg() .path("icons/chevron-down.svg") @@ -3854,19 +3913,19 @@ impl ScreenshotEditorWindow { .flex_col() .gap(px(16.)) .child( - ui::Field::plain(&theme, "Width") + ui::Field::plain(&theme, tr::screenshot_editor::style::width()) .icon("icons/enlarge.svg") .icon_size(px(16.)) .child(self.slider(StyleSlider::BorderWidth, None, cx)), ) .child( - ui::Field::plain(&theme, "Color") + ui::Field::plain(&theme, tr::screenshot_editor::style::color()) .icon("icons/image.svg") .icon_size(px(16.)) .child(self.render_rgb_input(HexTarget::BorderColor, color)), ) .child( - ui::Field::plain(&theme, "Opacity") + ui::Field::plain(&theme, tr::screenshot_editor::style::opacity()) .icon("icons/shadow.svg") .icon_size(px(16.)) .child(self.slider(StyleSlider::BorderOpacity, None, cx)), @@ -3889,7 +3948,7 @@ impl ScreenshotEditorWindow { .text_size(px(12.)) .font_weight(FontWeight::MEDIUM) .text_color(theme.gray_11) - .child("Border"), + .child(tr::screenshot_editor::style::border()), ) .child( ui::Toggle::plain(&theme, "screenshot-border-toggle", enabled).on_click( @@ -4167,7 +4226,7 @@ impl ScreenshotEditorWindow { div() .text_size(px(13.)) .text_color(theme.gray_11) - .child("Loading preview...") + .child(tr::screenshot_editor::loading_preview()) .into_any_element(), ), }) @@ -4210,7 +4269,7 @@ impl ScreenshotEditorWindow { }]) .child(kbd_tooltip( &theme, - "Zoom Out", + tr::screenshot_editor::zoom_out(), &["meta", "-"], ui::EditorButton::plain(&theme, "screenshot-zoom-out") .left_icon("icons/zoom-out.svg") @@ -4223,7 +4282,7 @@ impl ScreenshotEditorWindow { .child(self.slider(StyleSlider::Zoom, Some(px(80.)), cx)) .child(kbd_tooltip( &theme, - "Zoom In", + tr::screenshot_editor::zoom_in(), &["meta", "+"], ui::EditorButton::plain(&theme, "screenshot-zoom-in") .left_icon("icons/zoom-in.svg") diff --git a/apps/desktop-gpui/src/screenshot_export.rs b/apps/desktop-gpui/src/screenshot_export.rs index 525858d4fea..6c99da0ba90 100644 --- a/apps/desktop-gpui/src/screenshot_export.rs +++ b/apps/desktop-gpui/src/screenshot_export.rs @@ -23,6 +23,7 @@ use std::sync::{Mutex, OnceLock}; use cap_gpui_kernels::{codec, screenshot as pixels}; use cap_project::{Annotation, AnnotationType, ProjectConfiguration}; +use crate::i18n::tr; use crate::screenshot_annotations::{self as annotations, Rect}; use crate::screenshot_editor::has_no_visible_background; @@ -226,8 +227,7 @@ pub fn flatten_onto_white(out: &Composited) -> Composited { /// frame straight to PNG" path, still callable through /// `screenshot_editor::render_export_png`. pub fn encode_rgba_png(rgba: &[u8], width: u32, height: u32) -> Result, String> { - codec::encode_png_rgba(rgba, width, height) - .map_err(|e| format!("Failed to encode screenshot export: {e}")) + codec::encode_png_rgba(rgba, width, height).map_err(tr::screenshot_editor::error::encode) } fn encode_png(out: &Composited) -> Result, String> { @@ -240,7 +240,7 @@ fn encode_png(out: &Composited) -> Result, String> { fn encode_jpeg(out: &Composited) -> Result, String> { let rgb = codec::flatten_rgba_to_rgb_over_white(&out.rgba); codec::encode_jpeg_rgb(&rgb, out.width, out.height, 90) - .map_err(|e| format!("Failed to encode screenshot export: {e}")) + .map_err(tr::screenshot_editor::error::encode) } /// An encoded export, tagged with the content type the upload sends. @@ -320,8 +320,7 @@ fn stable_value(value: &serde_json::Value) -> serde_json::Value { } fn stable_stringify(value: &serde_json::Value) -> Result { - serde_json::to_string(&stable_value(value)) - .map_err(|e| format!("Failed to serialize screenshot config: {e}")) + serde_json::to_string(&stable_value(value)).map_err(tr::screenshot_editor::error::serialize) } /// The content hash the share flow compares against `sharing.contentHash`: @@ -330,8 +329,7 @@ fn stable_stringify(value: &serde_json::Value) -> Result { /// simply misses and re-uploads against the same video id. pub fn content_hash(config: &ProjectConfiguration) -> Result { use sha2::Digest as _; - let value = serde_json::to_value(config) - .map_err(|e| format!("Failed to serialize screenshot config: {e}"))?; + let value = serde_json::to_value(config).map_err(tr::screenshot_editor::error::serialize)?; let payload = stable_stringify(&value)?; let digest = sha2::Sha256::digest(payload.as_bytes()); let mut hex = String::with_capacity(64); diff --git a/apps/desktop-gpui/src/session.rs b/apps/desktop-gpui/src/session.rs index 20ef9f7ecc3..910daf8732f 100644 --- a/apps/desktop-gpui/src/session.rs +++ b/apps/desktop-gpui/src/session.rs @@ -15,6 +15,7 @@ use cap_utils::disk_space::{DiskSpaceStatus, RecordingStorageMonitor}; use gpui::{App, AppContext as _, Context, Entity, Global, Task}; use crate::app_sounds::AppSound; +use crate::i18n::tr; use crate::recording::{self, ActiveRecording, StartConfig}; /// `Idle -> Starting -> Recording -> Stopping -> Idle`; pause is a flag on @@ -1077,8 +1078,7 @@ impl RecordingSession { DiskSpaceStatus::Exhausted => { this.storage_warning = true; this.stopped_for_low_storage = true; - this.storage_notice = - Some("Low storage. Stopping and saving your recording…".into()); + this.storage_notice = Some(tr::dialog::storage::stopping().into()); this.stop(cx); false } @@ -1366,7 +1366,7 @@ impl RecordingSession { tracing::info!(dir = %project_dir.display(), "recording finished"); stop_sound.call_once(|| AppSound::StopRecording.play()); if low_storage { - this.storage_notice = Some("Recording stopped because storage is low. Your recording was saved.".into()); + this.storage_notice = Some(tr::dialog::storage::stopped_saved().into()); } if this.mode() == Some(crate::recording::RecordingMode::Studio) && !low_storage { this.finished_studio = Some(project_dir); @@ -1383,12 +1383,12 @@ impl RecordingSession { } Ok(Err(error)) => { tracing::error!("recording failed to stop cleanly: {error:#}"); - this.storage_notice = low_storage.then(|| "Recording stopped because storage is low. Your recording files were kept. Free up space, then recover the recording below.".into()); + this.storage_notice = low_storage.then(|| tr::dialog::storage::stopped_kept().into()); this.error = Some(format!("{error:#}")); } Err(join_error) => { tracing::error!("recording stop task died: {join_error}"); - this.storage_notice = low_storage.then(|| "Recording stopped because storage is low. Your recording files were kept. Free up space, then recover the recording below.".into()); + this.storage_notice = low_storage.then(|| tr::dialog::storage::stopped_kept().into()); this.error = Some("Stop task failed.".into()); } } diff --git a/apps/desktop-gpui/src/settings_pages.rs b/apps/desktop-gpui/src/settings_pages.rs index 536f5a2c88e..d7f73b1ed5b 100644 --- a/apps/desktop-gpui/src/settings_pages.rs +++ b/apps/desktop-gpui/src/settings_pages.rs @@ -31,6 +31,7 @@ use serde_json::{Map, Value, json}; use crate::{ diagnostics, + i18n::tr, settings_window::{MenuKind, Page, SettingsWindow}, store::{ self, Action, AutomationExportCompression, AutomationRecordingMode, AutomationRule, @@ -51,6 +52,10 @@ const MONO_FONT: &str = if cfg!(target_os = "macos") { "monospace" }; +/// Stands in for an inline element inside a translated sentence, so the +/// sentence can be split around it without assuming where the language puts it. +const COMMAND_MARKER: &str = "\u{1}"; + // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- @@ -193,7 +198,7 @@ struct DriveQuota { struct ListeningHotkey { action: &'static str, modifiers: gpui::Modifiers, - error: Option<&'static str>, + error: Option &'static str>, } struct SavedShortcut { @@ -211,20 +216,23 @@ const S3_REGION: usize = 4; /// `DEFAULT_CONFIG` in s3-config.tsx, indexed like the fields above. const S3_DEFAULTS: [&str; 5] = ["", "", "https://s3.amazonaws.com", "", "us-east-1"]; -const S3_FIELDS: [(&str, &str); 5] = [ - ("Access Key ID", "PL31OADSQNK"), - ("Secret Access Key", "PL31OADSQNK"), - ("Endpoint", "https://s3.amazonaws.com"), - ("Bucket Name", "my-bucket"), - ("Region", "us-east-1"), +const S3_FIELDS: [(fn() -> &'static str, &str); 5] = [ + (tr::settings::integrations::s3::access_key, "PL31OADSQNK"), + (tr::settings::integrations::s3::secret_key, "PL31OADSQNK"), + ( + tr::settings::integrations::s3::endpoint, + "https://s3.amazonaws.com", + ), + (tr::settings::integrations::s3::bucket, "my-bucket"), + (tr::settings::integrations::s3::region, "us-east-1"), ]; -const S3_PROVIDERS: [(&str, &str); 5] = [ - ("aws", "AWS S3"), - ("cloudflare", "Cloudflare R2"), - ("supabase", "Supabase"), - ("minio", "MinIO"), - ("other", "Other S3-Compatible"), +const S3_PROVIDERS: [(&str, fn() -> &'static str); 5] = [ + ("aws", || "AWS S3"), + ("cloudflare", || "Cloudflare R2"), + ("supabase", || "Supabase"), + ("minio", || "MinIO"), + ("other", tr::settings::integrations::s3::other_provider), ]; struct S3Page { @@ -371,21 +379,9 @@ pub(crate) struct PagesState { impl PagesState { pub(crate) fn new(window: &mut Window, cx: &mut Context) -> Self { - let hint_input = cx.new(|cx| { - let mut input = ui::TextInputState::single_line(window, cx); - input.set_placeholder("Add a term"); - input - }); - let feedback_input = cx.new(|cx| { - let mut input = ui::TextInputState::multi_line(window, cx); - input.set_placeholder("Tell us what you think about Cap..."); - input - }); - let license_input = cx.new(|cx| { - let mut input = ui::TextInputState::single_line(window, cx); - input.set_placeholder("License key"); - input - }); + let hint_input = cx.new(|cx| ui::TextInputState::single_line(window, cx)); + let feedback_input = cx.new(|cx| ui::TextInputState::multi_line(window, cx)); + let license_input = cx.new(|cx| ui::TextInputState::single_line(window, cx)); let s3_inputs = std::array::from_fn(|index| { cx.new(|cx| { let mut input = ui::TextInputState::single_line(window, cx); @@ -580,7 +576,7 @@ impl SettingsWindow { match kind { MenuKind::S3Provider => S3_PROVIDERS .iter() - .map(|(value, label)| ui::MenuItem::new(*label, *value == self.pages.s3.provider)) + .map(|(value, label)| ui::MenuItem::new(label(), *value == self.pages.s3.provider)) .collect(), MenuKind::AutomationTrigger(rule) => { let current = self.rule_at(rule).map(|rule| rule.trigger); @@ -593,10 +589,13 @@ impl SettingsWindow { } MenuKind::AutomationMatchMode(rule) => { let current = self.rule_at(rule).map(|rule| rule.match_mode); - [(MatchMode::All, "Match all"), (MatchMode::Any, "Match any")] - .iter() - .map(|(mode, label)| ui::MenuItem::new(*label, Some(*mode) == current)) - .collect() + [ + (MatchMode::All, tr::settings::automations::match_all()), + (MatchMode::Any, tr::settings::automations::match_any()), + ] + .iter() + .map(|(mode, label)| ui::MenuItem::new(*label, Some(*mode) == current)) + .collect() } MenuKind::AutomationConditionType(rule, index) => { let current = self @@ -618,7 +617,7 @@ impl SettingsWindow { }; CAPTURE_TARGETS .iter() - .map(|(target, label)| ui::MenuItem::new(*label, Some(*target) == current)) + .map(|(target, label)| ui::MenuItem::new(label(), Some(*target) == current)) .collect() } MenuKind::AutomationConditionMode(rule, index) => { @@ -651,7 +650,7 @@ impl SettingsWindow { }; CLIPBOARD_SOURCES .iter() - .map(|(source, label)| ui::MenuItem::new(*label, Some(*source) == current)) + .map(|(source, label)| ui::MenuItem::new(label(), Some(*source) == current)) .collect() } MenuKind::AutomationWebhookMethod(rule, index) => { @@ -699,7 +698,7 @@ impl SettingsWindow { .unwrap_or(AutomationExportCompression::Web); COMPRESSIONS .iter() - .map(|(compression, label)| ui::MenuItem::new(*label, *compression == current)) + .map(|(compression, label)| ui::MenuItem::new(label(), *compression == current)) .collect() } MenuKind::AutomationPreset(rule, index) => { @@ -896,7 +895,13 @@ impl SettingsWindow { /// `commands.globalMessageDialog(..)`. fn info_dialog(&mut self, message: &str, window: &mut Window, cx: &mut Context) { - let answer = window.prompt(gpui::PromptLevel::Info, message, None, &["Ok"], cx); + let answer = window.prompt( + gpui::PromptLevel::Info, + message, + None, + &[tr::settings::prompt::ok()], + cx, + ); cx.spawn(async move |_this, _cx| { let _ = answer.await; }) @@ -952,8 +957,8 @@ const SHORTCUT_SAVED_NOTICE: std::time::Duration = std::time::Duration::from_sec #[derive(Clone, Copy)] struct OutcomeOption { icon: &'static str, - label: &'static str, - hint: &'static str, + label: fn() -> &'static str, + hint: fn() -> &'static str, } const SCREENSHOT_OUTCOMES: [(store::PostScreenshotBehaviour, OutcomeOption); 4] = [ @@ -961,32 +966,32 @@ const SCREENSHOT_OUTCOMES: [(store::PostScreenshotBehaviour, OutcomeOption); 4] store::PostScreenshotBehaviour::OpenEditor, OutcomeOption { icon: "icons/edit.svg", - label: "Open editor", - hint: "Annotate first", + label: tr::settings::shortcuts::outcome::screenshot_open_editor, + hint: tr::settings::shortcuts::outcome::screenshot_open_editor_hint, }, ), ( store::PostScreenshotBehaviour::Copy, OutcomeOption { icon: "icons/copy.svg", - label: "Copy", - hint: "Paste anywhere", + label: tr::settings::shortcuts::outcome::screenshot_copy, + hint: tr::settings::shortcuts::outcome::screenshot_copy_hint, }, ), ( store::PostScreenshotBehaviour::CopyAndOpenEditor, OutcomeOption { icon: "icons/layers.svg", - label: "Copy and edit", - hint: "Both at once", + label: tr::settings::shortcuts::outcome::screenshot_copy_and_edit, + hint: tr::settings::shortcuts::outcome::screenshot_copy_and_edit_hint, }, ), ( store::PostScreenshotBehaviour::Save, OutcomeOption { icon: "icons/folder-down.svg", - label: "Do nothing", - hint: "Saved to library", + label: tr::settings::shortcuts::outcome::screenshot_do_nothing, + hint: tr::settings::shortcuts::outcome::saved_to_library, }, ), ]; @@ -996,24 +1001,24 @@ const STUDIO_OUTCOMES: [(store::PostStudioBehaviour, OutcomeOption); 3] = [ store::PostStudioBehaviour::OpenEditor, OutcomeOption { icon: "icons/edit.svg", - label: "Open editor", - hint: "Edit right away", + label: tr::settings::shortcuts::outcome::studio_open_editor, + hint: tr::settings::shortcuts::outcome::studio_open_editor_hint, }, ), ( store::PostStudioBehaviour::ShowOverlay, OutcomeOption { icon: "icons/app-window.svg", - label: "Show Cap", - hint: "Back to the recorder", + label: tr::settings::shortcuts::outcome::studio_show_cap, + hint: tr::settings::shortcuts::outcome::studio_show_cap_hint, }, ), ( store::PostStudioBehaviour::DoNothing, OutcomeOption { icon: "icons/folder-down.svg", - label: "Do nothing", - hint: "Saved to library", + label: tr::settings::shortcuts::outcome::studio_do_nothing, + hint: tr::settings::shortcuts::outcome::saved_to_library, }, ), ]; @@ -1024,94 +1029,206 @@ const INSTANT_OUTCOMES: [(bool, OutcomeOption); 2] = [ false, OutcomeOption { icon: "icons/globe.svg", - label: "Open link", - hint: "In your browser", + label: tr::settings::shortcuts::outcome::instant_open_link, + hint: tr::settings::shortcuts::outcome::instant_open_link_hint, }, ), ( true, OutcomeOption { icon: "icons/link.svg", - label: "Copy link only", - hint: "Nothing opens", + label: tr::settings::shortcuts::outcome::instant_copy_link, + hint: tr::settings::shortcuts::outcome::instant_copy_link_hint, }, ), ]; fn shortcut_label(action: &str) -> &'static str { match action { - "screenshotArea" => "Screenshot an area", - "screenshotWindow" => "Screenshot a window", - "screenshotDisplay" => "Screenshot the display", - "startStudioRecording" => "Start Studio recording", - "startInstantRecording" => "Start Instant recording", - "openRecordingPickerArea" => "Record an area", - "openRecordingPickerWindow" => "Record a window", - "openRecordingPickerDisplay" => "Record a display", - "openRecordingPicker" => "Open recording picker", - "cycleRecordingMode" => "Cycle recording mode", - "stopRecording" => "Stop recording", - "togglePauseRecording" => "Pause or resume", - "restartRecording" => "Restart recording", - _ => "Shortcut", + "screenshotArea" => tr::settings::shortcuts::label::screenshot_area(), + "screenshotWindow" => tr::settings::shortcuts::label::screenshot_window(), + "screenshotDisplay" => tr::settings::shortcuts::label::screenshot_display(), + "startStudioRecording" => tr::settings::shortcuts::label::start_studio(), + "startInstantRecording" => tr::settings::shortcuts::label::start_instant(), + "openRecordingPickerArea" => tr::settings::shortcuts::label::record_area(), + "openRecordingPickerWindow" => tr::settings::shortcuts::label::record_window(), + "openRecordingPickerDisplay" => tr::settings::shortcuts::label::record_display(), + "openRecordingPicker" => tr::settings::shortcuts::label::open_picker(), + "cycleRecordingMode" => tr::settings::shortcuts::label::cycle_mode(), + "stopRecording" => tr::settings::shortcuts::label::stop(), + "togglePauseRecording" => tr::settings::shortcuts::label::pause(), + "restartRecording" => tr::settings::shortcuts::label::restart(), + _ => tr::settings::shortcuts::label::fallback(), } } -fn screenshot_outcome(behaviour: store::PostScreenshotBehaviour) -> &'static str { - match behaviour { - store::PostScreenshotBehaviour::OpenEditor => "open it in the editor", - store::PostScreenshotBehaviour::Copy => "copy it to your clipboard", - store::PostScreenshotBehaviour::CopyAndOpenEditor => "copy it and open the editor", - store::PostScreenshotBehaviour::Save => "save it to your library without opening anything", - } -} - -fn picker_outcome(mode: crate::main_window::Mode) -> &'static str { - match mode { - crate::main_window::Mode::Studio => "record it in Studio mode", - crate::main_window::Mode::Instant => "record it in Instant mode", - crate::main_window::Mode::Screenshot => "take a screenshot of it", - } +/// What pressing a shortcut does. Languages word the two uses differently, +/// so each effect is a pair of whole messages rather than one phrase that is +/// capitalized for the description. +#[derive(Clone, Copy)] +struct ShortcutEffect { + /// Completes "Saved. Press ⌘⇧4 in any app to ...". + phrase: fn() -> &'static str, + /// The row's description. + sentence: fn() -> &'static str, } -/// What pressing the shortcut does, as a phrase that completes "Press ⌘⇧4 -/// to ...". It follows the after-capture settings and the current mode, so -/// the row always describes what will actually happen. +/// What pressing the shortcut does. It follows the after-capture settings +/// and the current mode, so the row always describes what will actually +/// happen. fn shortcut_effect( action: &str, settings: &store::GeneralSettings, mode: crate::main_window::Mode, -) -> String { - let screenshot = screenshot_outcome(settings.post_screenshot_behaviour); - let studio_stop = match settings.post_studio_recording_behaviour { - store::PostStudioBehaviour::OpenEditor => ", then open the editor when you stop", - store::PostStudioBehaviour::ShowOverlay | store::PostStudioBehaviour::DoNothing => "", - }; - let instant_stop = if settings.disable_auto_open_links { - ", then copy its share link when you stop" - } else { - ", then copy and open its share link when you stop" - }; - match action { - "screenshotArea" => format!("select an area and {screenshot}"), - "screenshotWindow" => format!("capture the window under your pointer and {screenshot}"), - "screenshotDisplay" => format!("capture the display under your pointer and {screenshot}"), - "startStudioRecording" => { - format!("record your selected screen or window in Studio mode{studio_stop}") - } +) -> Option { + use crate::main_window::Mode; + use store::{PostScreenshotBehaviour as Shot, PostStudioBehaviour as Studio}; + + let shot = settings.post_screenshot_behaviour; + let effect = match action { + "screenshotArea" => match shot { + Shot::OpenEditor => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::screenshot_area_open_editor, + sentence: tr::settings::shortcuts::sentence::screenshot_area_open_editor, + }, + Shot::Copy => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::screenshot_area_copy, + sentence: tr::settings::shortcuts::sentence::screenshot_area_copy, + }, + Shot::CopyAndOpenEditor => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::screenshot_area_copy_and_open_editor, + sentence: tr::settings::shortcuts::sentence::screenshot_area_copy_and_open_editor, + }, + Shot::Save => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::screenshot_area_save, + sentence: tr::settings::shortcuts::sentence::screenshot_area_save, + }, + }, + "screenshotWindow" => match shot { + Shot::OpenEditor => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::screenshot_window_open_editor, + sentence: tr::settings::shortcuts::sentence::screenshot_window_open_editor, + }, + Shot::Copy => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::screenshot_window_copy, + sentence: tr::settings::shortcuts::sentence::screenshot_window_copy, + }, + Shot::CopyAndOpenEditor => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::screenshot_window_copy_and_open_editor, + sentence: tr::settings::shortcuts::sentence::screenshot_window_copy_and_open_editor, + }, + Shot::Save => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::screenshot_window_save, + sentence: tr::settings::shortcuts::sentence::screenshot_window_save, + }, + }, + "screenshotDisplay" => match shot { + Shot::OpenEditor => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::screenshot_display_open_editor, + sentence: tr::settings::shortcuts::sentence::screenshot_display_open_editor, + }, + Shot::Copy => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::screenshot_display_copy, + sentence: tr::settings::shortcuts::sentence::screenshot_display_copy, + }, + Shot::CopyAndOpenEditor => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::screenshot_display_copy_and_open_editor, + sentence: + tr::settings::shortcuts::sentence::screenshot_display_copy_and_open_editor, + }, + Shot::Save => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::screenshot_display_save, + sentence: tr::settings::shortcuts::sentence::screenshot_display_save, + }, + }, + "startStudioRecording" => match settings.post_studio_recording_behaviour { + Studio::OpenEditor => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::studio_open_editor, + sentence: tr::settings::shortcuts::sentence::studio_open_editor, + }, + Studio::ShowOverlay | Studio::DoNothing => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::studio, + sentence: tr::settings::shortcuts::sentence::studio, + }, + }, "startInstantRecording" => { - format!("record your selected screen or window in Instant mode{instant_stop}") + if settings.disable_auto_open_links { + ShortcutEffect { + phrase: tr::settings::shortcuts::effect::instant_copy, + sentence: tr::settings::shortcuts::sentence::instant_copy, + } + } else { + ShortcutEffect { + phrase: tr::settings::shortcuts::effect::instant_copy_and_open, + sentence: tr::settings::shortcuts::sentence::instant_copy_and_open, + } + } } - "openRecordingPickerArea" => format!("select an area, then {}", picker_outcome(mode)), - "openRecordingPickerWindow" => format!("choose a window, then {}", picker_outcome(mode)), - "openRecordingPickerDisplay" => format!("choose a display, then {}", picker_outcome(mode)), - "openRecordingPicker" => "open Cap so you can choose what to record".into(), - "cycleRecordingMode" => "switch between Studio, Instant and Screenshot mode".into(), - "stopRecording" => "stop the current recording".into(), - "togglePauseRecording" => "pause the current recording, or resume it".into(), - "restartRecording" => "throw away the current take and start again".into(), - _ => String::new(), - } + "openRecordingPickerArea" => match mode { + Mode::Studio => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::picker_area_studio, + sentence: tr::settings::shortcuts::sentence::picker_area_studio, + }, + Mode::Instant => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::picker_area_instant, + sentence: tr::settings::shortcuts::sentence::picker_area_instant, + }, + Mode::Screenshot => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::picker_area_screenshot, + sentence: tr::settings::shortcuts::sentence::picker_area_screenshot, + }, + }, + "openRecordingPickerWindow" => match mode { + Mode::Studio => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::picker_window_studio, + sentence: tr::settings::shortcuts::sentence::picker_window_studio, + }, + Mode::Instant => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::picker_window_instant, + sentence: tr::settings::shortcuts::sentence::picker_window_instant, + }, + Mode::Screenshot => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::picker_window_screenshot, + sentence: tr::settings::shortcuts::sentence::picker_window_screenshot, + }, + }, + "openRecordingPickerDisplay" => match mode { + Mode::Studio => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::picker_display_studio, + sentence: tr::settings::shortcuts::sentence::picker_display_studio, + }, + Mode::Instant => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::picker_display_instant, + sentence: tr::settings::shortcuts::sentence::picker_display_instant, + }, + Mode::Screenshot => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::picker_display_screenshot, + sentence: tr::settings::shortcuts::sentence::picker_display_screenshot, + }, + }, + "openRecordingPicker" => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::open_picker, + sentence: tr::settings::shortcuts::sentence::open_picker, + }, + "cycleRecordingMode" => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::cycle_mode, + sentence: tr::settings::shortcuts::sentence::cycle_mode, + }, + "stopRecording" => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::stop, + sentence: tr::settings::shortcuts::sentence::stop, + }, + "togglePauseRecording" => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::pause, + sentence: tr::settings::shortcuts::sentence::pause, + }, + "restartRecording" => ShortcutEffect { + phrase: tr::settings::shortcuts::effect::restart, + sentence: tr::settings::shortcuts::sentence::restart, + }, + _ => return None, + }; + Some(effect) } fn description_follows_outcome(action: &str) -> bool { @@ -1119,14 +1236,6 @@ fn description_follows_outcome(action: &str) -> bool { || matches!(action, "startStudioRecording" | "startInstantRecording") } -fn capitalized_sentence(phrase: &str) -> String { - let mut chars = phrase.chars(); - match chars.next() { - Some(first) => format!("{}{}.", first.to_uppercase(), chars.as_str()), - None => String::new(), - } -} - /// The gpui keystroke name for a key, as the W3C `KeyboardEvent.code` string /// the Tauri app stores (`e.code` in hotkeys.tsx, `global_hotkey::Code` in /// hotkeys.rs). `None` for keys neither side can bind. @@ -1188,22 +1297,13 @@ fn shortcut_needs_modifier(hotkey: &Hotkey) -> bool { !(hotkey.meta || hotkey.ctrl || hotkey.alt) && !is_function_key(&hotkey.code) } -const NEEDS_MODIFIER: &str = if cfg!(target_os = "macos") { - "Include ⌘, ⌃ or ⌥ so the shortcut doesn't get in the way of typing." +const NEEDS_MODIFIER: fn() -> &'static str = if cfg!(target_os = "macos") { + tr::settings::shortcuts::needs_modifier_macos } else { - "Include Ctrl, Alt or Win so the shortcut doesn't get in the way of typing." + tr::settings::shortcuts::needs_modifier_other }; -const UNSUPPORTED_KEY: &str = - "That key can't be used in a shortcut. Try a letter, number or F-key."; - -const RECORDING_HINT: &str = "Press the keys you want to use. Esc cancels, Delete removes it."; - -const BLOCKED_WHILE_RECORDING: &str = - "Shortcuts can't be changed while this recording holds its stop key. Stop recording first."; - -const UNAVAILABLE: &str = - "Another app or the system already uses this shortcut. Click to choose a different one."; +const UNSUPPORTED_KEY: fn() -> &'static str = tr::settings::shortcuts::unsupported_key; /// `HotkeyText`'s key list: modifiers in ⌘⌃⌥⇧ order, then the main key. /// [`ui::kbd_symbol`] does the per-OS glyph mapping. @@ -1481,8 +1581,8 @@ impl SettingsWindow { screenshots.push( self.render_outcome_panel( "outcome-screenshot", - "After a screenshot", - "For every screenshot, from a shortcut or from Cap", + tr::settings::shortcuts::after_screenshot::title(), + tr::settings::shortcuts::after_screenshot::hint(), &SCREENSHOT_OUTCOMES .iter() .map(|(value, option)| { @@ -1503,8 +1603,8 @@ impl SettingsWindow { recording.push( self.render_outcome_panel( "outcome-studio", - "After a Studio recording", - "When you stop", + tr::settings::shortcuts::after_studio::title(), + tr::settings::shortcuts::after_studio::hint(), &STUDIO_OUTCOMES .iter() .map(|(value, option)| { @@ -1526,8 +1626,8 @@ impl SettingsWindow { recording.push( self.render_outcome_panel( "outcome-instant", - "After an Instant recording", - "The share link is always copied", + tr::settings::shortcuts::after_instant::title(), + tr::settings::shortcuts::after_instant::hint(), &INSTANT_OUTCOMES .iter() .map(|(disabled, option)| { @@ -1561,21 +1661,21 @@ impl SettingsWindow { vec![ self.section( - "Screenshots", - Some("Shortcuts work from any app. Click one to change it."), + tr::settings::shortcuts::screenshots_title(), + Some(tr::settings::shortcuts::screenshots_description()), None, vec![card("shortcuts-screenshots", screenshots, cx)], ) .into_any_element(), self.section( - "Recording", - Some("Start a recording without opening Cap first."), + tr::settings::shortcuts::recording_title(), + Some(tr::settings::shortcuts::recording_description()), None, vec![card("shortcuts-recording", recording, cx)], ) .into_any_element(), self.section( - "While recording", + tr::settings::shortcuts::while_recording_title(), None, None, vec![card("shortcuts-while-recording", while_recording, cx)], @@ -1654,14 +1754,14 @@ impl SettingsWindow { .text_size(px(12.)) .font_weight(gpui::FontWeight::MEDIUM) .text_color(ink) - .child(option.label), + .child((option.label)()), ) .child( div() .text_size(px(11.)) .line_height(px(14.)) .text_color(theme.settings_muted()) - .child(option.hint), + .child((option.hint)()), ), ) .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) @@ -1728,29 +1828,42 @@ impl SettingsWindow { let blocked = self.pages.shortcut_blocked == Some(action) && crate::hotkeys::editing_blocked(cx); let (detail, detail_color, detail_icon): (String, Hsla, Option<(&str, Hsla)>) = if blocked { - (BLOCKED_WHILE_RECORDING.into(), red, None) + ( + tr::settings::shortcuts::blocked_while_recording().into(), + red, + None, + ) } else if let Some(listening) = listening { match listening.error { - Some(error) => (error.into(), red, None), - None => (RECORDING_HINT.into(), theme.settings_muted(), None), + Some(error) => (error().into(), red, None), + None => ( + tr::settings::shortcuts::recording_hint().into(), + theme.settings_muted(), + None, + ), } } else if unavailable && binding.is_some() { - (UNAVAILABLE.into(), amber, None) + (tr::settings::shortcuts::unavailable().into(), amber, None) } else if let (Some(saved), Some(binding)) = (saved, binding.as_ref()) { - let mut text = format!( - "Saved. Press {} in any app to {effect}.", - hotkey_inline_text(binding) - ); - if let Some(from) = saved.moved_from { - text.push_str(&format!(" Removed from {}.", shortcut_label(from))); - } + let keys = hotkey_inline_text(binding); + let phrase = effect.map_or("", |effect| (effect.phrase)()); + let text = match saved.moved_from { + Some(from) => { + tr::settings::shortcuts::saved_moved(keys, phrase, shortcut_label(from)) + } + None => tr::settings::shortcuts::saved(keys, phrase), + }; ( text, theme.settings_text(), Some(("icons/circle-check.svg", theme.settings_text())), ) } else { - (capitalized_sentence(&effect), theme.settings_muted(), None) + ( + effect.map_or_else(String::new, |effect| (effect.sentence)().to_string()), + theme.settings_muted(), + None, + ) }; let group: SharedString = format!("shortcut-row-{action}").into(); @@ -1887,7 +2000,7 @@ impl SettingsWindow { .px(px(4.)) .text_size(px(12.)) .text_color(theme.settings_muted()) - .child("Type shortcut…"), + .child(tr::settings::shortcuts::type_shortcut()), ); } else { field = field.child( @@ -1923,7 +2036,7 @@ impl SettingsWindow { .px(px(4.)) .text_size(px(12.)) .text_color(theme.settings_muted()) - .child("Record shortcut"), + .child(tr::settings::shortcuts::record_shortcut()), ) .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .on_click(cx.listener(move |this, _, window, cx| { @@ -2015,12 +2128,12 @@ impl SettingsWindow { .text_size(px(12.)) .line_height(px(18.)) .text_color(Hsla::from(theme.red_9)) - .child(format!("Couldn't load CLI status: {error}")), + .child(tr::settings::cli::load_failed(error)), ) .child(div().child(self.button( "cli-retry", (ui::ButtonVariant::Gray, None), - "Retry", + tr::settings::cli::retry(), false, cx, |this, window, cx| this.cli_refresh(window, cx), @@ -2033,10 +2146,8 @@ impl SettingsWindow { vec![ self.section( - "Command Line", - Some( - "Install the Cap command for terminals, agents, scripts, and local automation.", - ), + tr::settings::cli::title(), + Some(tr::settings::cli::description()), None, vec![self.card(true).child(body).into_any_element()], ) @@ -2055,15 +2166,19 @@ impl SettingsWindow { let install_label = if installing { if status.installed { - "Repairing..." + tr::settings::cli::repairing() } else { - "Installing..." + tr::settings::cli::installing() } } else if status.installed { - "Repair" + tr::settings::cli::repair() } else { - "Install CLI" + tr::settings::cli::install() }; + let installs_command = tr::settings::cli::installs_command(COMMAND_MARKER); + let (before_command, after_command) = installs_command + .split_once(COMMAND_MARKER) + .unwrap_or((installs_command.as_str(), "")); // ``. let code_span = |text: String| { @@ -2086,9 +2201,9 @@ impl SettingsWindow { .gap(px(4.)) .min_w_0() .child(div().text_size(px(13.)).child(if status.installed { - "Installed" + tr::settings::cli::installed() } else { - "Not installed" + tr::settings::cli::not_installed() })) .child( div() @@ -2099,9 +2214,9 @@ impl SettingsWindow { .text_size(px(12.)) .line_height(px(16.)) .text_color(theme.settings_muted()) - .child("The desktop app installs a local\u{a0}") + .child(before_command.to_string()) .child(code_span("cap".into()).text_size(px(12.))) - .child("\u{a0}command that points back to the bundled CLI."), + .child(after_command.to_string()), ), ) .child( @@ -2115,9 +2230,9 @@ impl SettingsWindow { "cli-remove", (ui::ButtonVariant::Gray, None), if uninstalling { - "Removing..." + tr::settings::cli::removing() } else { - "Remove" + tr::settings::cli::remove() }, uninstalling, cx, @@ -2144,7 +2259,7 @@ impl SettingsWindow { .min_w_0() .child( div() - .w(px(64.)) + .min_w(px(64.)) .flex_shrink_0() .text_size(px(12.)) .text_color(theme.settings_muted()) @@ -2177,8 +2292,14 @@ impl SettingsWindow { .flex() .flex_col() .gap(px(8.)) - .child(path_row("Command", status.shim_path.clone())) - .child(path_row("Target", status.target_path.clone())), + .child(path_row( + tr::settings::cli::command(), + status.shim_path.clone(), + )) + .child(path_row( + tr::settings::cli::target(), + status.target_path.clone(), + )), ) .when_some(status.conflict.clone(), |this, conflict| { // `rounded-lg border border-red-300/40 bg-red-500/10 px-3 py-2 @@ -2213,20 +2334,14 @@ impl SettingsWindow { .text_size(px(12.)) .line_height(px(18.)) .text_color(theme.settings_muted()) - .child( - "Added cap to your PATH. Restart your terminal to use it, or run \ - this now:", - ) + .child(tr::settings::cli::path_configured()) .into_any_element() } else { div() .text_size(px(12.)) .line_height(px(18.)) .text_color(theme.settings_muted()) - .child(format!( - "Add {} to your PATH to use cap from a new terminal.", - status.path_entry - )) + .child(tr::settings::cli::path_missing(&status.path_entry)) .into_any_element() }; this.child( @@ -2265,9 +2380,9 @@ impl SettingsWindow { "cli-copy-path", (ui::ButtonVariant::Gray, None), if self.pages.cli_copied { - "Copied" + tr::common::copied() } else { - "Copy" + tr::settings::cli::copy() }, false, cx, @@ -2372,10 +2487,13 @@ impl SettingsWindow { let theme = self.theme; let hints = self.pages.hints.clone(); let save_label = match self.pages.hint_save { - HintSave::Saving => "Saving...", - HintSave::Saved => "Saved", + HintSave::Saving => tr::settings::transcription::saving(), + HintSave::Saved => tr::settings::transcription::saved(), HintSave::Idle => "", }; + self.pages.hint_input.update(cx, |input, _| { + input.set_placeholder(tr::settings::transcription::placeholder()) + }); let header = div() .flex() @@ -2389,15 +2507,17 @@ impl SettingsWindow { .flex_col() .gap(px(2.)) .min_w_0() - .child(div().text_size(px(13.)).child("Remembered terms")) + .child( + div() + .text_size(px(13.)) + .child(tr::settings::transcription::terms_title()), + ) .child( div() .text_size(px(12.)) .line_height(px(16.)) .text_color(theme.settings_muted()) - .child( - "Add one term at a time to reduce typos and formatting mistakes.", - ), + .child(tr::settings::transcription::terms_description()), ), ) .child( @@ -2410,7 +2530,7 @@ impl SettingsWindow { this.child(self.button( "hints-clear", (ui::ButtonVariant::Gray, None), - "Clear", + tr::settings::transcription::clear(), false, cx, |this, _window, cx| { @@ -2444,7 +2564,7 @@ impl SettingsWindow { .child(self.button( "hints-add", (ui::ButtonVariant::Primary, Some("icons/plus.svg")), - "Add", + tr::common::add(), add_disabled, cx, |this, _window, cx| this.transcription_add_hint(cx), @@ -2462,16 +2582,13 @@ impl SettingsWindow { .text_size(px(12.)) .line_height(px(18.)) .text_color(theme.settings_muted()) - .child("These hints are applied when you generate captions in the editor."), + .child(tr::settings::transcription::applied_note()), ); let mut sections = vec![ self.section( - "Transcription", - Some( - "Add names, spellings, domains, and capitalization preferences that caption \ - generation should keep in mind.", - ), + tr::settings::transcription::title(), + Some(tr::settings::transcription::description()), None, vec![card.into_any_element()], ) @@ -2482,11 +2599,7 @@ impl SettingsWindow { let count = div() .text_size(px(12.)) .text_color(theme.settings_muted()) - .child(format!( - "{} {}", - hints.len(), - if hints.len() == 1 { "item" } else { "items" } - )) + .child(tr::settings::transcription::count(hints.len())) .into_any_element(); let chips = div().flex().flex_row().flex_wrap().gap(px(8.)).children( hints.into_iter().enumerate().map(|(index, hint)| { @@ -2523,7 +2636,7 @@ impl SettingsWindow { sections.push( self.section( - "Active hints", + tr::settings::transcription::active_title(), None, Some(count), vec![self.card(true).child(chips).into_any_element()], @@ -2550,9 +2663,9 @@ pub(crate) enum SwitchBack { /// failed and hand the user back their app with a pointer at the terminal. const CLASSIC_WAIT_TIMEOUT: Duration = Duration::from_secs(10 * 60); -const SWITCH_SENTENCES: &[&str] = &[ - "Switching back to the classic Cap app.", - "Your recordings and settings stay exactly where they are.", +const SWITCH_SENTENCES: &[fn() -> &'static str] = &[ + tr::settings::experimental::switch::switching, + tr::settings::experimental::switch::data_stays, ]; const SWITCH_SENTENCE_MS: u64 = 2000; const SWITCH_FADE_MS: f32 = 400.; @@ -2610,21 +2723,17 @@ impl SettingsWindow { // left untouched in the shared store -- the Tauri app still uses it. let mut sections = vec![ self.section( - "Reliability", + tr::settings::experimental::reliability(), None, None, vec![ self.rows(vec![ self.setting_row( - "Out-of-process muxer", - Some( - "Run the fragmented-MP4 muxer in an isolated subprocess so muxer \ - crashes can't take down your recording. Requires the bundled \ - cap-muxer binary.", - ), + tr::settings::experimental::muxer(), + Some(tr::settings::experimental::muxer_description()), self.toggle( "out-of-process-muxer", - "Out-of-process muxer", + tr::settings::experimental::muxer(), self.settings.out_of_process_muxer, cx, |this, cx| { @@ -2645,8 +2754,8 @@ impl SettingsWindow { if dev_supervisor_available() { sections.push( self.section( - "Developer", - Some("Only shown in development builds."), + tr::settings::experimental::developer(), + Some(tr::settings::experimental::developer_description()), None, vec![self.rows(vec![self.classic_app_row(cx)]).into_any_element()], ) @@ -2662,14 +2771,11 @@ impl SettingsWindow { fn classic_app_row(&self, cx: &mut Context) -> gpui::AnyElement { let checked = !matches!(self.pages.switch_back, Some(SwitchBack::Running(_))); self.setting_row( - "Use native app", - Some( - "Turn off to close this app and reopen the classic Tauri app in the dev harness. \ - Recordings and settings are shared.", - ), + tr::settings::experimental::native_app(), + Some(tr::settings::experimental::native_app_description()), self.toggle( "enable-gpui-app", - "Use native app", + tr::settings::experimental::native_app(), checked, cx, |this, cx| { @@ -2725,7 +2831,7 @@ impl SettingsWindow { .text_size(px(15.)) .text_center() .text_color(gpui::hsla(0., 0., 1., alpha)) - .child(SWITCH_SENTENCES[index]), + .child(SWITCH_SENTENCES[index]()), ); } SwitchBack::WaitingForClassic => { @@ -2737,7 +2843,7 @@ impl SettingsWindow { .font_weight(FontWeight::SEMIBOLD) .text_center() .text_color(white) - .child("Opening the classic Cap app"), + .child(tr::settings::experimental::switch::opening()), ) .child( div() @@ -2745,7 +2851,7 @@ impl SettingsWindow { .text_size(px(14.)) .text_center() .text_color(gpui::hsla(0., 0., 1., 0.7)) - .child("This window will close when the classic Cap app is ready."), + .child(tr::settings::experimental::switch::opening_detail()), ); } SwitchBack::Failed(error) => { @@ -2783,9 +2889,9 @@ impl SettingsWindow { .hover(|style| style.bg(gpui::hsla(0., 0., 1., 0.12))) .on_click(cx.listener(|this, _, _window, cx| this.cancel_switch_back(cx))) .child(if matches!(switch, SwitchBack::Failed(_)) { - "Close" + tr::common::close() } else { - "Cancel" + tr::common::cancel() }), ) .into_any_element(), @@ -2841,8 +2947,7 @@ impl SettingsWindow { } self.pages.switch_back = Some(SwitchBack::Failed( - "Finish your recording, export, upload, import, or transcription task before switching to the classic app." - .to_string(), + tr::settings::experimental::switch::busy().to_string(), )); cx.notify(); true @@ -2856,7 +2961,7 @@ impl SettingsWindow { } if !dev_supervisor_available() { self.pages.switch_back = Some(SwitchBack::Failed( - "Couldn't find the classic Cap app to switch back to.".to_string(), + tr::settings::experimental::switch::not_found().to_string(), )); cx.notify(); return; @@ -2869,7 +2974,7 @@ impl SettingsWindow { if !store::set_store_setting(GENERAL_SETTINGS, "enableGpuiApp", Value::Bool(false)) { self.pages.switch_back = Some(SwitchBack::Failed( - "Couldn't save your app preference. Cap is still open.".to_string(), + tr::settings::experimental::switch::save_failed().to_string(), )); cx.notify(); return; @@ -2887,7 +2992,7 @@ impl SettingsWindow { .await; let mut failure = result .err() - .map(|error| format!("Couldn't open the classic Cap app: {error}")); + .map(tr::settings::experimental::switch::open_failed); let started = std::time::Instant::now(); while failure.is_none() { match store::classic_pending_path().try_exists() { @@ -2898,17 +3003,12 @@ impl SettingsWindow { } Ok(true) => {} Err(error) => { - failure = Some(format!( - "Couldn't check whether the classic Cap app opened: {error}" - )); + failure = Some(tr::settings::experimental::switch::check_failed(error)); break; } } if started.elapsed() >= CLASSIC_WAIT_TIMEOUT { - failure = Some( - "The classic app hasn't opened. Check the dev terminal for build errors, then try again." - .to_string(), - ); + failure = Some(tr::settings::experimental::switch::timed_out().to_string()); break; } cx.background_executor() @@ -2916,7 +3016,7 @@ impl SettingsWindow { .await; } let message = - failure.unwrap_or_else(|| "Couldn't open the classic Cap app.".to_string()); + failure.unwrap_or_else(|| tr::settings::experimental::switch::failed().to_string()); tracing::error!("{message}"); store::clear_classic_pending(); store::set_store_setting(GENERAL_SETTINGS, "enableGpuiApp", Value::Bool(true)); @@ -2934,6 +3034,11 @@ impl SettingsWindow { // Feedback (feedback.tsx) // --------------------------------------------------------------------------- +/// Written into the diagnostic report, which stays English; the page shows +/// `settings.feedback.diagnostic.no_cli_sync` in its place. +const NO_SELFTEST_BINARY: &str = + "The Cap command-line tool was not found next to this build, so the sync test could not run."; + impl SettingsWindow { /// The page's two one-shot probes: the OS string and whether there is a /// `cap` sidecar to run the sync test with. Both stat the filesystem, so @@ -2968,11 +3073,8 @@ impl SettingsWindow { let auth = store::auth_snapshot(); let Some(token) = auth.token else { // `protectedHeaders()`'s throw. - self.pages.feedback = FeedbackStatus::Error( - "Please sign in to continue. Alternatively, email hello@cap.so or join our \ - Discord at cap.link/discord" - .to_string(), - ); + self.pages.feedback = + FeedbackStatus::Error(tr::settings::sign_in_required::body().to_string()); cx.notify(); return; }; @@ -3015,7 +3117,9 @@ impl SettingsWindow { input.update(cx, |input, cx| input.set_disabled(false, cx)); this.pages.feedback = match result { Ok((200, _)) => FeedbackStatus::Success, - Ok(_) => FeedbackStatus::Error("Failed to submit feedback".to_string()), + Ok(_) => { + FeedbackStatus::Error(tr::settings::feedback::submit_failed().to_string()) + } Err(error) => FeedbackStatus::Error(error.text()), }; }, @@ -3039,7 +3143,7 @@ impl SettingsWindow { // would land in whatever the user is currently recording. if crate::session::RecordingSession::recording_in_flight(cx) { self.pages.diagnostic = DiagnosticStatus::Error( - "Stop the current recording before running a diagnostic.".to_string(), + tr::settings::feedback::diagnostic::stop_recording_first().to_string(), ); cx.notify(); return; @@ -3047,18 +3151,14 @@ impl SettingsWindow { // A stray click costs a couple of minutes of hijacked screen and loud // beeps, so it is confirmed the way the Tauri app confirms it. - let message = format!( - "Cap will take over your screen with a flashing pattern and play loud beeps for \ - about {} seconds per pipeline. Take your headphones off, leave the volume \ - audible, and leave the machine alone until it finishes.", - diagnostics::DEFAULT_DURATION_SECS - ); + let message = + tr::settings::feedback::diagnostic::confirm_body(diagnostics::DEFAULT_DURATION_SECS); cx.spawn_in(window, async move |this, cx| { if !crate::platform::confirm_dialog( - "Run diagnostic?", + tr::settings::feedback::diagnostic::confirm_title(), &message, - "Run Diagnostic", - "Cancel", + tr::settings::feedback::diagnostic::run(), + tr::common::cancel(), true, ) { return; @@ -3120,14 +3220,7 @@ impl SettingsWindow { } // No sidecar: the environment half of the report is // still worth having, with the reason recorded in it. - None => ( - None, - Some( - "The Cap command-line tool was not found next to this build, so \ - the sync test could not run." - .to_string(), - ), - ), + None => (None, Some(NO_SELFTEST_BINARY.to_string())), }; if cancel.load(Ordering::Relaxed) { return Err(diagnostics::CANCELLED.to_string()); @@ -3309,7 +3402,7 @@ impl SettingsWindow { }; let result = task .await - .unwrap_or_else(|_| Err("The upload was interrupted".to_string())); + .unwrap_or_else(|_| Err(tr::settings::feedback::upload_interrupted().to_string())); this.update_in(cx, |this, window, cx| { *this.upload_status_mut(target) = match result { @@ -3329,9 +3422,12 @@ impl SettingsWindow { let theme = self.theme; let (text, color): (String, Hsla) = match self.upload_status(target) { UploadStatus::Idle => return None, - UploadStatus::Pending => ("Uploading...".to_string(), theme.settings_muted()), + UploadStatus::Pending => ( + tr::settings::feedback::uploading().to_string(), + theme.settings_muted(), + ), UploadStatus::Success => ( - "Sent. Thank you -- this helps us find the problem.".to_string(), + tr::settings::feedback::upload_sent().to_string(), theme.settings_muted(), ), UploadStatus::Error(error) => (error.clone(), Hsla::from(theme.red_9)), @@ -3359,19 +3455,14 @@ impl SettingsWindow { .line_height(px(18.)) .text_color(theme.settings_text()) .font_weight(FontWeight::MEDIUM) - .child("The test takes over your screen and makes noise."), + .child(tr::settings::feedback::diagnostic::warning_title()), ) .child( div() .text_size(px(12.)) .line_height(px(18.)) .text_color(theme.settings_muted()) - .child( - "A fullscreen window flashes black and white while 1kHz beeps play \ - through your speakers, and Cap records it and measures how far the \ - picture and the sound have drifted apart. Leave the machine alone \ - while it runs. Both modes together take a couple of minutes.", - ), + .child(tr::settings::feedback::diagnostic::warning_body()), ); let mode = self.pages.diagnostic_mode; @@ -3394,19 +3485,16 @@ impl SettingsWindow { let options = self.rows(vec![ self.setting_row( - "Recording mode", - Some("Which pipeline to test. Both runs the studio leg and then the instant one."), + tr::settings::feedback::diagnostic::mode_label(), + Some(tr::settings::feedback::diagnostic::mode_description()), mode_control.into_any_element(), ), self.setting_row( - "Test microphone", - Some( - "Also record your microphone and check that it hears the beeps in time. \ - Needs speakers the mic can actually hear.", - ), + tr::settings::feedback::diagnostic::mic_label(), + Some(tr::settings::feedback::diagnostic::mic_description()), self.toggle( "diagnostic-mic", - "Test microphone", + tr::settings::feedback::diagnostic::mic_label(), self.pages.diagnostic_mic, cx, |this, cx| { @@ -3429,7 +3517,7 @@ impl SettingsWindow { ui::ButtonVariant::Dark, ui::ButtonSize::Md, ) - .label("Run Diagnostic") + .label(tr::settings::feedback::diagnostic::run()) .on_click( cx.listener(|this, _, window, cx| this.diagnostic_run(window, cx)), ), @@ -3442,10 +3530,7 @@ impl SettingsWindow { div() .text_size(px(11.)) .text_color(theme.settings_muted()) - .child( - "The Cap command-line tool was not found next to this build, so \ - the report will cover your environment only.", - ), + .child(tr::settings::feedback::diagnostic::no_cli()), ); } } @@ -3465,7 +3550,7 @@ impl SettingsWindow { ui::ButtonVariant::Gray, ui::ButtonSize::Md, ) - .label("Cancel") + .label(tr::common::cancel()) .disabled_settings( &theme, self.pages.diagnostic_cancel.is_none() @@ -3492,12 +3577,17 @@ impl SettingsWindow { ); } if let Some(error) = sync_test_error { + let error = if error == NO_SELFTEST_BINARY { + tr::settings::feedback::diagnostic::no_cli_sync().to_string() + } else { + error.clone() + }; body = body.child( div() .text_size(px(12.)) .line_height(px(18.)) .text_color(Hsla::from(theme.red_9)) - .child(error.clone()), + .child(error), ); } @@ -3509,9 +3599,9 @@ impl SettingsWindow { ui::ButtonSize::Md, ) .label(if self.pages.diagnostic_upload == UploadStatus::Pending { - "Sending..." + tr::settings::feedback::diagnostic::sending() } else { - "Send to Cap" + tr::settings::feedback::diagnostic::send() }) .disabled_settings( &theme, @@ -3531,7 +3621,7 @@ impl SettingsWindow { ui::ButtonVariant::Gray, ui::ButtonSize::Md, ) - .label("Show File") + .label(tr::settings::feedback::diagnostic::show_file()) .on_click(move |_, _, cx| crate::library::reveal_path(&path, cx)), ); } @@ -3542,7 +3632,7 @@ impl SettingsWindow { ui::ButtonVariant::Gray, ui::ButtonSize::Md, ) - .label("Run Again") + .label(tr::settings::feedback::diagnostic::run_again()) .on_click(cx.listener(|this, _, window, cx| this.diagnostic_run(window, cx))), ); body = body @@ -3566,7 +3656,7 @@ impl SettingsWindow { ui::ButtonVariant::Gray, ui::ButtonSize::Md, ) - .label("Try Again") + .label(tr::settings::feedback::diagnostic::try_again()) .on_click( cx.listener(|this, _, window, cx| this.diagnostic_run(window, cx)), ), @@ -3594,24 +3684,28 @@ impl SettingsWindow { // failures, so they stay in the muted grey. let (label, fill, text): (&str, Hsla, Hsla) = match verdict { Some("pass") => ( - "PASS", + tr::settings::feedback::diagnostic::verdict::pass(), gpui::rgb(0x22c55e).into(), gpui::rgb(0x4ade80).into(), ), Some("warn") => ( - "WARN", + tr::settings::feedback::diagnostic::verdict::warn(), gpui::rgb(0xf59e0b).into(), gpui::rgb(0xfbbf24).into(), ), - Some("fail") => ("FAIL", theme.red_9.into(), theme.red_9.into()), + Some("fail") => ( + tr::settings::feedback::diagnostic::verdict::fail(), + theme.red_9.into(), + theme.red_9.into(), + ), Some("inconclusive") => ( - "INCONCLUSIVE", + tr::settings::feedback::diagnostic::verdict::inconclusive(), theme.settings_muted(), theme.settings_muted(), ), // No sync test in the report: the environment half still ran. _ => ( - "ENVIRONMENT ONLY", + tr::settings::feedback::diagnostic::verdict::environment_only(), theme.settings_muted(), theme.settings_muted(), ), @@ -3640,6 +3734,9 @@ impl SettingsWindow { let pending = self.pages.feedback == FeedbackStatus::Pending; // `disabled={feedback().trim().length < 4}`. let submit_disabled = pending || self.pages.feedback_draft.trim().chars().count() < 4; + self.pages.feedback_input.update(cx, |input, _| { + input.set_placeholder(tr::settings::feedback::placeholder()) + }); // The `