diff --git a/apps/desktop-gpui/locales/GLOSSARY.md b/apps/desktop-gpui/locales/GLOSSARY.md index 8715a29e252..d9569b2e374 100644 --- a/apps/desktop-gpui/locales/GLOSSARY.md +++ b/apps/desktop-gpui/locales/GLOSSARY.md @@ -78,7 +78,11 @@ recording engine, which stays in English. | clip | 片段 | クリップ | clip | clip | Clip | क्लिप | | segment (of a recording) | 分段 | セグメント | segment | segmento | Segment | सेगमेंट | | recover / discard | 恢复 / 丢弃 | 復元 / 破棄 | récupérer / supprimer | recuperar / descartar | wiederherstellen / verwerfen | रिकवर करें / हटाएँ | +| shot (3D camera) | 镜头 | ショット | plan | toma | Einstellung | शॉट | | Personal / Commercial plan | 个人版 / 商业版 | 個人 / 商用 | Personnel / Commercial | Personal / Comercial | Privat / Kommerziell | पर्सनल / कमर्शियल | +| caption (subtitle) | 字幕 | 字幕 | sous-titre | subtítulo | Untertitel | कैप्शन | +| track (timeline) | 轨道 | トラック | piste | pista | Spur | ट्रैक | +| preset | 预设 | プリセット | préréglage | preajuste | Vorlage | प्रीसेट | ## Fitting the UI diff --git a/apps/desktop-gpui/locales/de.toml b/apps/desktop-gpui/locales/de.toml index 0838f104b93..4d0557cdb90 100644 --- a/apps/desktop-gpui/locales/de.toml +++ b/apps/desktop-gpui/locales/de.toml @@ -257,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?" @@ -620,3 +1312,1566 @@ 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" + +# Editor sidebar, segment panels, colour correction, audio library and crop dialog. + +[editor.audio.library] +add_hint = "Fügen Sie Ihrem Video Audio, Musik oder andere Klänge hinzu" +add_title = "Audio hinzufügen" +change_hint = "Wählen Sie einen anderen Track für dieses Segment" +change_title = "Audio ändern" +done = "Fertig" +import = "Datei importieren" + +[editor.color] +apply_to_cursor = "Auf Cursor anwenden" +fine_tune = "Farben feinabstimmen" +title = "Farbkorrektur" + +[editor.color.preset.cinematic] +description = "Petrolfarbene Schatten und orange Lichter mit leichter Körnung" +name = "Kino" + +[editor.color.preset.dreamy] +description = "Weiche, luftige Pastelltöne mit angehobenem Schwarz" +name = "Verträumt" + +[editor.color.preset.frost] +description = "Kühle, klare Töne mit gedämpften Farben" +name = "Frost" + +[editor.color.preset.golden] +description = "Warmes Licht der goldenen Stunde" +name = "Golden" + +[editor.color.preset.midnight] +description = "Dunkles, stimmungsvolles Petrol mit gedämpften Farben" +name = "Mitternacht" + +[editor.color.preset.noir] +description = "Kontrastreiches Schwarzweiß mit starker Körnung" +name = "Noir" + +[editor.color.preset.none] +description = "Originalfarben, keine Korrektur" +name = "Keine" + +[editor.color.preset.vintage] +description = "Verblasster, warmer Filmlook mit weichem Kontrast" +name = "Vintage" + +[editor.color.preset.vivid] +description = "Kräftige Sättigung und mehr Kontrast" +name = "Lebendig" + +[editor.color.slider] +contrast = "Kontrast" +exposure = "Belichtung" +fade = "Verblassen" +grain = "Körnung" +saturation = "Sättigung" +split_tone = "Teiltonung" +strength = "Stärke" +temperature = "Temperatur" +tint = "Tönung" +vignette = "Vignette" + +[editor.crop] +aspect_ratio = "Seitenverhältnis" +center = "Zentrieren" +dimensions = "Größe" +editing_style = "Nur Stil {number} wird bearbeitet" +editing_style_override = "Nur Stil {number} wird bearbeitet · Speichern aktiviert seinen eigenen Hintergrund" +free = "Frei" +full = "Voll" +hint = "Ziehen zum Verschieben · Pfeiltasten zum Feinjustieren · {key} halten, um nicht einzurasten" +loading_frame = "Bild wird geladen…" +position = "Position" +preview = "Vorschau" +rendering_preview = "Vorschau wird gerendert…" +snap_to_ratios = "An Verhältnissen ausrichten" +style_changed = "Dieser Stil wurde beim Zuschneiden geändert. Brechen Sie ab und öffnen Sie Zuschneiden erneut." +title = "Zuschneiden" + +[editor.panel] +delete = "Löschen" +done = "Fertig" +duration = "Dauer" +edge_in = "Ein" +edge_out = "Aus" +enabled = "Aktiviert" +fade_duration = "Ausblenddauer" +flip_horizontally = "Horizontal spiegeln" +flip_vertically = "Vertikal spiegeln" +name = "Name" +opacity = "Deckkraft" + +[editor.panel.audio] +change = "Ändern" +change_hint = "Tippen, um den Track zu ändern" +fade_in = "Einblenden" +fade_out = "Ausblenden" +fallback_name = "Audio" +title = "Audio {number}" +volume = "Lautstärke" + +[editor.panel.camera3d] +already_still = "Bereits eine feste Einstellung" +amount = "Stärke" +angles = "Winkel" +auto_scene = "Auto-Szene" +auto_scene_tooltip = "Erstellt alle 3D-Einstellungen der Spur neu" +bokeh = "Bokeh" +camera = "Kamera" +delete_tooltip = "Diese Einstellung löschen" +depth_blur = "Tiefenunschärfe" +distance = "Abstand" +drag_to_orbit = "Ziehen zum Kreisen" +ease_in = "Anlauf" +ease_out = "Auslauf" +end_same_as_start = "Ende · wie Start" +fine_tune = "Feinabstimmung" +focus = "Fokus" +header = "3D-Einstellung · {look} · {duration} s" +hold_opening = "Die Startpose für die ganze Einstellung halten" +lens = "Objektiv" +look_title = "Look" +motion_style = "Bewegungsstil" +moves = "Fahrten" +nothing_to_swap = "Bei einer festen Einstellung gibt es nichts zu tauschen" +play_shot = "Abspielen" +play_shot_tooltip = "Diese Einstellung abspielen" +pose_end = "Ende · {time}" +pose_start = "Start · {time}" +reset_camera = "Kamera zurücksetzen" +roll = "Rollen" +rotate_x = "Drehung X" +rotate_y = "Drehung Y" +shift_x = "Versatz X" +shift_y = "Versatz Y" +shots = { one = "{count} Einstellung", other = "{count} Einstellungen" } +still_shot = "Feste Einstellung" +swap = "Start und Ende tauschen" +timing_advanced = "Timing & Erweitert" +timing_summary = "{easing} · Objektiv {fov}" + +[editor.panel.camera3d.blur] +angle = "Winkel" +falloff = "Verlauf" +focus_size = "Fokusfeld" +focus_x = "Fokus X" +focus_y = "Fokus Y" +position = "Position" +scan = "Scan" +strength = "Stärke" + +[editor.panel.camera3d.easing] +ease_in = "Beschleunigen" +ease_out = "Abbremsen" +linear = "Linear" +smooth = "Weich" + +[editor.panel.camera3d.focus_mode] +directional = "Gerichtet" +radial = "Radial" +tilt_shift = "Tilt-Shift" + +[editor.panel.camera3d.look] +center = "Mitte" +close_up = "Nahaufnahme" +drift_down = "Absinken" +glide_across = "Seitwärts gleiten" +low_angle = "Untersicht" +perspective = "Perspektive" +pull_back = "Rückzug" +rising_sweep = "Aufschwung" +slide_by = "Vorbeigleiten" +spotlight = "Spotlight" +tilt_away = "Wegkippen" +top_down = "Draufsicht" +unfold = "Aufklappen" + +[editor.panel.camera3d.sequence] +product_tour = "Produkttour" +punch_in = "Heranzoomen" +showcase = "Showcase" + +[editor.panel.camera3d.shot] +custom = "Eigene Fahrt" +still = "Feste Einstellung" + +[editor.panel.caption] +title = "Untertitel {number}" + +[editor.panel.font] +system_mono = "System-Mono" +system_sans = "System-Sans" +system_serif = "System-Serif" + +[editor.panel.font_weight] +black = "Schwarz" +bold = "Fett" +custom = "Benutzerdefiniert ({weight})" +extra_bold = "Extrafett" +light = "Leicht" +medium = "Mittel" +regular = "Normal" +semibold = "Halbfett" + +[editor.panel.image] +arrange = "Auf der Leinwand anordnen" +arrange_help = "Ziehen Sie das Bild, um es zu verschieben. Ziehen Sie an einer Ecke, um die Größe zu ändern, oder drehen Sie es mit der Drehung unten." +center = "Auf der Leinwand zentrieren" +lock_aspect = "Seitenverhältnis sperren" +missing = "Die Bilddatei fehlt. Ersetzen Sie sie, um dieses Segment mit seinem Timing und seinen Transformationen wiederherzustellen." +replace = "Bild ersetzen" +rotation = "Drehung" +rounding = "Rundung" + +[editor.panel.keyboard] +title = "Tastatur {number}" + +[editor.panel.mask] +blur = "Unschärfe" +effect = "Effekt" +highlight = "Hervorheben" +outside_darkness = "Abdunklung außen" +pixel_size = "Pixelgröße" +pixelate = "Verpixeln" +sensitive = "Vertraulich" +title = "Maske {number}" + +[editor.panel.scene] +camera_layout = "Kamera-Layout" +camera_position = "Kameraposition" +camera_zoom = "Kamera-Zoom" +screen_position = "Bildschirmposition" +screen_zoom = "Bildschirm-Zoom" +transition = "Übergang" + +[editor.panel.scene.description] +camera_only = "Zeigt nur das Kamerabild" +default = "Zeigt Bildschirm und Kamera" +floating = "Bildschirm und Kamera schweben nebeneinander als abgerundete Karten über dem Hintergrund" +hide_camera = "Zeigt nur die Bildschirmaufnahme" +split_screen = "Bildschirm und Kamera nebeneinander (im Hochformat automatisch übereinander)" + +[editor.panel.scene.mode] +camera_only = "Nur Kamera" +default = "Standard" +floating = "Schwebend" +hide_camera = "Kamera aus" +split_screen = "Geteilt" + +[editor.panel.style] +camera_only_background = "Hintergrund für Nur Kamera" +camera_only_help = "Verwenden Sie eine Szene „Nur Kamera“. Der Rand zeigt den Hintergrund um die Kamera." +camera_only_padding = "Rand für Nur Kamera" +crop_height = "Höhe" +crop_left = "Links" +crop_title = "Bildschirmzuschnitt (Quellpixel)" +crop_top = "Oben" +crop_width = "Breite" +edit_background = "Hintergrund & Rahmen bearbeiten" +edit_camera = "Kamera bearbeiten" +edit_cursor = "Cursor bearbeiten" +help = "Überschreibungen gelten nur während dieses Segments. Aktivieren Sie eine Gruppe, um ihre globalen Einstellungen zu übernehmen." +reset_crop = "Zuschnitt zurücksetzen" + +[editor.panel.text_animation] +bounce = "Sprung" +fade = "Blende" +letters = "Buchstaben" +none = "Keine" +pop = "Pop" +slide_down = "Nach unten" +slide_left = "Nach links" +slide_right = "Nach rechts" +slide_up = "Nach oben" +tracking = "Laufweite" +typewriter = "Schreibmaschine" +wipe = "Wischen" +words = "Wörter" +zoom = "Zoom" + +[editor.panel.text_background] +box_shape = "Box" +highlight = "Marker" +none = "Kein" +pill = "Pille" + +[editor.panel.text_layout] +fullscreen = "Vollbild" +overlay = "Overlay" + +[editor.panel.text_preset.group] +all = "Alle" +callouts = "Hinweise" +code = "Code" +lower_thirds = "Bauchbinden" +statements = "Aussagen" +titles = "Titel" + +[editor.panel.text_preset.name] +caption = "Beschriftung" +cinematic = "Kino" +code = "Code" +gradient = "Verlauf" +headline = "Schlagzeile" +highlight = "Marker" +kicker = "Dachzeile" +label = "Label" +lower_third = "Bauchbinde" +name_tag = "Namensschild" +neon = "Neon" +quote = "Zitat" +stat = "Große Zahl" +sticker = "Sticker" +title = "Titel" +typewriter = "Schreibmaschine" + +[editor.panel.text_preset.sample] +caption = "Mit Cap aufgenommen" +cinematic = "Kapitel eins" +gradient = "Schöner Text" +headline = "Schneller liefern" +highlight = "das Wichtigste" +kicker = "Neue Funktion" +label = "Profi-Tipp" +name_tag = "Richie · Gründer" +neon = "Glanzauftritt" +quote = "„Mühelos soll es wirken“" +sticker = "Bumm!" +title = "Das ist Cap" +typewriter = "wird getippt…" + +[editor.panel.text_segment] +animation = "Animation" +background = "Hintergrund" +color = "Farbe" +font = "Schrift" +font_size = "Größe" +fullscreen_help = "Hält das Video an, solange der Text angezeigt wird, und setzt es danach an derselben Stelle fort." +glow = "Leuchten" +gradient = "Verlauf" +layout = "Layout" +letter_spacing = "Zeichenabstand" +line_height = "Zeilenhöhe" +look = "Aussehen" +outline = "Kontur" +screen_transition = "Bildschirmübergang" +shadow = "Schatten" +style = "Stil" +title = "Text {number}" + +[editor.panel.timing] +end = "Ende" +start = "Start" +title = "Timing" +to = "bis" + +[editor.panel.zoom] +amount = "Zoomstärke" +auto = "Auto" +auto_help = "Der automatische Zoom folgt Ihrem Cursor über den Bildschirm." +auto_locked = "Der Auto-Modus braucht die Cursor-Erfassung. Aktivieren Sie „Separate Cursor-Erfassung (Studio)“ unter Einstellungen → Allgemein." +how_it_works = "Wie funktioniert das?" +manual = "Manuell" +manual_help = "Der manuelle Zoom bleibt auf der Stelle, die Sie unten wählen." +mixed = "Gemischt" +mode = "Zoom-Modus" +positions_mixed = "Die Segmente zoomen auf unterschiedliche Stellen. Ziehen Sie, um alle auf dieselbe Stelle zu setzen." +title = "Zoom {number}" + +[editor.sidebar] +desktop_import_failed = "Ihr Desktop-Hintergrundbild konnte nicht importiert werden" +importing_image = "Bild wird importiert…" + +[editor.sidebar.animated_gradient] +add_colour = "Farbe hinzufügen" +colours = "Farben" +delete = "Löschen" +delete_failed = "Dieser Verlauf konnte nicht gelöscht werden. Versuchen Sie es erneut." +fine_tune = "Feinabstimmung" +limit_reached = { one = "Sie können bis zu {count} Verlauf speichern. Löschen Sie einen, um einen weiteren hinzuzufügen.", other = "Sie können bis zu {count} Verläufe speichern. Löschen Sie einen, um einen weiteren hinzuzufügen." } +motion = "Bewegung" +name_placeholder = "Name für diesen Verlauf" +name_required = "Geben Sie einen Namen für diesen Verlauf ein." +presets = "Vorlagen" +randomize = "Zufällig" +remember_failed = "Ihre Einstellungen für animierte Verläufe konnten nicht gespeichert werden." +remove_colour = "Farbe entfernen" +reset_tooltip = "Feinabstimmung zurücksetzen" +save_failed = "Dieser Verlauf konnte nicht gespeichert werden. Versuchen Sie es erneut." +save_tooltip = "Aktuellen Verlauf speichern" +saved = "Gespeichert" +still = "Still" + +[editor.sidebar.animated_gradient.control] +contrast = "Kontrast" +curvature = "Krümmung" +detail = "Detail" +direction = "Richtung" +exposure = "Belichtung" +flow_scale = "Flussgröße" +flow_strength = "Flussstärke" +grain_amount = "Körnungsmenge" +grain_size = "Körnungsgröße" +highlights = "Lichter" +motion_speed = "Bewegungstempo" +relief = "Relief" +ripple_size = "Wellengröße" +shading = "Schattierung" +vibrance = "Dynamik" + +[editor.sidebar.animated_gradient.group] +animation = "Animation" +colour = "Farbe" +flow = "Fluss" +lighting = "Licht" +texture = "Textur" + +[editor.sidebar.background] +title = "Hintergrund" + +[editor.sidebar.border] +color = "Rahmenfarbe" +opacity = "Rahmendeckkraft" +title = "Rahmen" +width = "Rahmenbreite" + +[editor.sidebar.corner] +rounded = "Abgerundet" +squircle = "Squircle" +title = "Eckenstil" + +[editor.sidebar.cursor] +circle = "Kreis" +click_ripple = "Klickwelle" +color = "Farbe" +default = "Standard" +description_circle = "Ersetzt alle Cursorformen durch einen Kreis." +description_default = "Behält die Cursorformen und das Aussehen aus Ihrer Aufnahme bei." +description_macos = "Klassisches macOS-Aussehen. Eigene Cursor behalten ihre aufgenommene Form." +description_tahoe = "macOS-Tahoe-Aussehen. Eigene Cursor behalten ihre aufgenommene Form." +description_windows = "Windows-Aussehen. Eigene Cursor behalten ihre aufgenommene Form." +duration = "Dauer" +recorded = "Aufgenommene Cursor" +recorded_tooltip = "Mit diesem Cursor aufgenommen" +ripple_size = "Größe" +strength = "Stärke" + +[editor.sidebar.desktop] +import = "Desktop-Hintergrund importieren" +importing = "Wird importiert..." +prompt = "Das Hintergrundbild Ihres Desktops verwenden" +reimport = "Neu importieren" + +[editor.sidebar.gradient] +angle = "Winkel" +from = "Von" +grain_scale = "Körnungsgröße" +noise = "Rauschen" +to = "Bis" + +[editor.sidebar.image] +prompt = "Klicken zum Auswählen oder Bild hierher ziehen" + +[editor.sidebar.layout] +background_blur = "Hintergrundunschärfe" +custom_position = "Eigene Bildschirmposition (auf der Leinwand verschoben)" +motion_blur = "Bewegungsunschärfe" +padding = "Rand" +rounded_corners = "Abgerundete Ecken" +title = "Layout" + +[editor.sidebar.notch] +description = "Zeichnet eine MacBook-Notch über die Aufnahme. Aufnahmen auf einem Mac mit Notch verwenden ihre eigenen Maße; andernfalls beginnen Sie mit der Größe unten und passen sie an." +height = "Notch-Höhe" +position = "Notch-Position" +title = "MacBook-Notch" +width = "Notch-Breite" + +[editor.sidebar.shadow] +advanced = "Erweiterte Schatteneinstellungen" +blur = "Unschärfe" +opacity = "Deckkraft" +shadow_size = "Größe" +title = "Schatten" + +[editor.sidebar.source] +animated = "Animiert" +color = "Farbe" +desktop = "Desktop" +gradient = "Verlauf" +image = "Bild" +none = "Keiner" +wallpaper = "Hintergrund" + +[editor.sidebar.style_group] +back = "Zurück zum Stil" +background = "Hintergrund & Rahmen" +camera = "Kamera" +cursor = "Cursor" +editing_note = "Nur dieses Segment wird bearbeitet. Die globalen Einstellungen bleiben unverändert." +title = "Stil {number} · {group}" + +[editor.sidebar.tab] +audio = "Audio" +background = "Hintergrund" +camera = "Kamera" +captions = "Untertitel" +cursor = "Cursor" +keyboard = "Tastatur" + +[editor.sidebar.wallpaper_theme] +blue = "Blau" +cities = "Städte" +dark = "Dunkel" +orange = "Orange" +purple = "Lila" + +[editor.canvas] +camera = "Kamera" +camera_follows_zoom = "Die Kameragröße folgt dem Zoom — zum Verschieben ziehen" +edit_zoom = "Zoom bearbeiten" +image = "Bild" +mask = "Maske" +screen = "Bildschirm" +screen_locked = "Der Bildschirm ist gesperrt, solange ein Zoom aktiv ist" +text_item = "Text" + +[editor.clips] +append_error = "Clip konnte nicht hinzugefügt werden: {message}" +append_error_title = "Aufnahmefehler" +back = "Zurück zum Editor" +cap_filter = "Cap-Aufnahme" +clip = "Clip {number}" +empty_description = "Nehmen Sie einen Clip auf oder importieren Sie einen, dann erscheint er hier." +empty_title = "Noch keine Clips" +import = "Importieren" +import_error = "Clip konnte nicht importiert werden: {message}" +import_error_title = "Importfehler" +import_existing = "Vorhandene Aufnahme" +import_mp4 = "MP4-Video…" +importing = "Import läuft…" +mp4_filter = "MP4-Video" +prepare_controls_failed = "Die Aufnahmesteuerung konnte nicht vorbereitet werden: {error}" +record_new = "Neuen Clip aufnehmen" +save_before_recording_failed = "Das aktuelle Projekt konnte vor der Aufnahme nicht gespeichert werden: {error}" +split = "Teil {number}" +title = "Clips" + +[editor.clips.error] +cancelled = "Import abgebrochen" +cannot_decode = "Video {path} kann nicht decodiert werden: {error}" +in_use = "Import nicht möglich, solange diese Aufnahme verwendet wird: {error}" +instant_source = "Instant-Aufnahmen können noch nicht in ein Projekt importiert werden" +instant_target = "Aufnahmen im Instant-Modus können nicht bearbeitet werden" +into_itself = "Eine Aufnahme kann nicht in sich selbst importiert werden" +missing_video = "Videodatei fehlt: {path}" +no_duration = "Das importierte Video hat keine gültige Dauer" +not_a_project = "Wählen Sie einen Cap-Projektordner aus" +source_empty = "Das Cap-Quellprojekt hat keine Aufnahmesegmente" +source_failed = "Das Cap-Quellprojekt ist fehlgeschlagen: {error}" +source_recording = "Das Cap-Quellprojekt wird noch aufgenommen" +source_unimportable = "Das Cap-Quellprojekt hat keine importierbaren Aufnahmesegmente" +unfinished_target = "Schließen Sie diese Aufnahme ab oder stellen Sie sie wieder her, bevor Sie einen Clip importieren" +unsupported_video = "Nicht unterstützte Videodatei: {path}" + +[editor.export] +back = "Zurück zum Editor" +existing_link_description = "Durch erneutes Hochladen wird das Video unter diesem Link durch Ihre neueste Bearbeitung ersetzt. Alle mit dem Link sehen die aktualisierte Version." +existing_link_title = "Bestehenden Link aktualisieren" +failed = "Export fehlgeschlagen" +fps = "{fps} FPS" +frames = { one = "{rendered} / {count} Frame", other = "{rendered} / {count} Frames" } +generating_preview = "Vorschau wird erstellt…" +link_unavailable_cursor = "Exporte nur mit Cursor können nur als Datei oder in die Zwischenablage gespeichert werden." +link_unavailable_transparent = "Transparente Exporte können nur als Datei oder in die Zwischenablage gespeichert werden." +organization = "Organisation" +preview = "Vorschau" +preview_info = "Dies ist ein gerendertes Bild aus Ihrem Video. Passen Sie die Einstellungen an, um die Qualität des fertigen Exports zu sehen." +preview_unavailable = "Vorschau nicht verfügbar" +reupload = "Erneut hochladen" +reupload_tooltip = "Ihre neueste Bearbeitung unter demselben Link hochladen" +reupload_transparent = "Links zum Teilen benötigen einen Hintergrund ohne Transparenz" +sign_in_required = "Sie müssen sich anmelden, um Aufnahmen zu teilen" +task_failed = "Exportvorgang fehlgeschlagen" +title = "Exportieren" +upgrade_message = "Diese Funktion erfordert einen höheren Tarif" +upgrade_title = "Upgrade erforderlich" +upload_failed = "Aufnahme konnte nicht hochgeladen werden" + +[editor.export.advanced] +bits_per_pixel = "Bits pro Pixel" +cursor_only = "Nur Cursor exportieren" +cursor_only_description = "Behält dieselbe Cursorbewegung und dieselben Klicks auf transparentem Hintergrund bei" +custom_bitrate = "Benutzerdefinierte Bitrate aktiv" +force_ffmpeg = "FFmpeg-Decoder erzwingen" +force_ffmpeg_description = "Hardware-Decoder überspringen (automatischer Fallback aktiv)" +title = "Erweitert" +transparent_mov = "Exportiert als transparente MOV-Datei. Die Dateien sind groß und eignen sich am besten zum Compositing oder Bearbeiten." + +[editor.export.cta] +cancel_sign_in = "Anmeldung abbrechen" +clipboard = "In die Zwischenablage exportieren" +create_link = "Link zum Teilen erstellen" +file = "Als Datei exportieren" +reupload = "Unter demselben Link erneut hochladen" +sign_in = "Zum Teilen anmelden" + +[editor.export.destination] +clipboard = "Zwischenablage" +file = "Datei" +link = "Link zum Teilen" + +[editor.export.done] +copy_link = "Link kopieren" +instant_hint = "Verwenden Sie für Ihre nächste Aufnahme den Instant-Modus, wenn Sie direkt nach dem Stoppen einen Link möchten." +link_copied = "Link kopiert" +open_file = "Datei öffnen" +open_link = "Link öffnen" +retry_copy = "Erneut kopieren" +reuploaded = "Ihre neueste Bearbeitung ist unter demselben Link bereit" +uploaded = "Ihr Cap wurde erfolgreich hochgeladen" + +[editor.export.estimate] +gigabytes = "~{value} GB" +gigabytes_range = "~{lower}–{upper} GB" +hours = "~{value} Std." +hours_range = "~{lower}–{upper} Std." +megabytes = "~{value} MB" +megabytes_range = "~{lower}–{upper} MB" +minutes = "~{value} Min." +minutes_range = "~{lower}–{upper} Min." +seconds = "~{value} s" +seconds_range = "~{lower}–{upper} s" +under_megabyte = "< 1 MB" +under_second = "< 1 s" + +[editor.export.optimize] +description = "Kodiert per Software neu für deutlich kleinere Dateien (langsamer)" +title = "Dateigröße optimieren" + +[editor.export.phase] +clipboard_failed = "Export abgeschlossen; Kopieren in die Zwischenablage fehlgeschlagen" +complete = "Export abgeschlossen" +copied = "In die Zwischenablage kopiert" +copying_to_clipboard = "Wird in die Zwischenablage kopiert" +rendering_cursor = "Cursorspur wird gerendert" +rendering_gif = "GIF wird gerendert" +rendering_video = "Video wird gerendert" +reupload_complete = "Erneutes Hochladen abgeschlossen" +reuploading = "Wird erneut unter Ihrem Link hochgeladen" +saving_to_file = "Wird als Datei gespeichert" +starting = "Export wird vorbereitet" +upload_complete = "Hochladen abgeschlossen" +uploading = "Link zum Teilen wird erstellt" + +[editor.export.quality] +larger = "Größere Datei" +maximum = "Maximal" +potato = "Kartoffel" +smaller = "Kleinere Datei" +social = "Social" +web = "Web" + +[editor.export.resources] +proceed = "Trotzdem fortfahren" +title = "Exportressourcen prüfen" + +[editor.export.section] +destination = "Ziel" +format = "Format" +frame_rate = "Bildrate" +quality = "Qualität" +resolution = "Auflösung" + +[editor.export.stats] +calculating = "Wird berechnet…" +duration = "Dauer" +estimated_size = "Geschätzte Größe" +export_time = "Exportdauer" +output = "Ausgabe" +output_value = "{width}×{height} · {fps} fps" +refining_size = "Größe wird präzisiert…" +refining_time = "Dauer wird präzisiert…" +unavailable = "Nicht verfügbar" + +[editor.modal] +cancel_export = "Export abbrechen" +cancel_export_message = "Möchten Sie den Export wirklich abbrechen?" +cancel_export_title = "Export abbrechen?" +keep_exporting = "Weiter exportieren" +recording_retained = "Aufnahme beibehalten" + +[editor.tabs.audio] +isolation_balanced = "Ausgewogen" +isolation_balanced_description = "Klarere Trennung mit natürlichen Stimmdetails." +isolation_light = "Leicht" +isolation_light_description = "Behält mehr von Ihrer ursprünglichen Stimme und dem Raumklang." +isolation_strong = "Stark" +isolation_strong_description = "Mehr Trennung für laute Umgebungen. Kann den Stimmklang verändern." +mic_volume = "Mikrofonlautstärke" +mute = "Audio stummschalten" +stereo_mode = "Stereomodus des Mikrofons" +studio_sound_description = "Reduziert Hintergrundgeräusche und gleicht die Lautstärke Ihrer Stimme aus." +studio_sound_save_failed = "Die Standardeinstellung für Studio Sound konnte nicht gespeichert werden" +system_volume = "Systemaudio-Lautstärke" +title = "Audiosteuerung" +use_for_new = "Für neue Aufnahmen verwenden" + +[editor.tabs.audio.stereo] +mono_left = "Mono L" +mono_right = "Mono R" +stereo = "Stereo" + +[editor.tabs.camera] +advanced_shadow = "Erweiterte Schatteneinstellungen" +background = "Hintergrund" +corner_style = "Eckenstil" +custom_position = "Eigene Position (auf der Leinwand gezogen)" +hide = "Kamera ausblenden" +keep_size_during_zoom = "Originalgröße beim Zoomen beibehalten" +mirror = "Kamera spiegeln" +position = "Position" +reset = "Zurücksetzen" +rounded_corners = "Abgerundete Ecken" +shadow = "Schatten" +shadow_blur = "Unschärfe" +shadow_opacity = "Deckkraft" +shape_label = "Form" +size_during_zoom = "Größe beim Zoomen" +size_label = "Größe" +title = "Kamera" + +[editor.tabs.camera.blur] +heavy = "Starke Unschärfe" +light = "Leichte Unschärfe" +off = "Aus" +remove = "Hintergrund entfernen" + +[editor.tabs.camera.shape] +source = "Original" +square = "Quadrat" + +[editor.tabs.captions] +active_word_highlight = "Aktives Wort hervorheben" +animation_section = "Animation" +animation_style = "Animationsstil" +background_color = "Hintergrundfarbe" +background_opacity = "Hintergrunddeckkraft" +background_settings = "Hintergrundeinstellungen" +beta = "Beta" +delete = "Löschen" +deleting = "Wird gelöscht..." +download = "Modell {model} herunterladen" +download_note = "Einmaliger Download auf Ihr System. Alle Untertitel werden lokal gespeichert." +downloading = "{model} wird heruntergeladen... {percent}%" +fade_duration = "Einblenddauer" +font_family = "Schriftart" +font_settings = "Schrifteinstellungen" +font_weight = "Schriftstärke" +generate = "Untertitel erstellen" +generate_failed = "Untertitel konnten nicht erstellt werden: {reason}" +generating = "Wird erstellt..." +highlight_color = "Hervorhebungsfarbe" +highlight_note = "Dies ist die erste Version der Untertitel in Cap. Die Hervorhebung des aktiven Worts kann in manchen Situationen ungenau sein. Wir arbeiten an einer Lösung, die in kommenden Versionen erscheint." +highlight_style = "Hervorhebungsstil" +horizontal_position = "Horizontale Position" +intel_unavailable = "Parakeet-Untertitelmodelle sind auf Intel-Macs nicht verfügbar. Whisper-Modelle bleiben verfügbar." +keep_open = "Lassen Sie Cap geöffnet, während das Modell heruntergeladen wird. Nach einem Neuladen des Editors wird automatisch wieder verbunden." +language_label = "Sprache" +model_downloaded = "Modell {model} heruntergeladen" +model_label = "Modell" +position = "Position" +regenerate = "Untertitel neu erstellen" +size_label = "Größe" +style = "Stil" +text_color = "Textfarbe" +title = "Untertitel" +uppercase = "Großbuchstaben" +vertical_position = "Vertikale Position" + +[editor.tabs.captions.animation] +bounce = "Hüpfen" +none = "Keine" +pop = "Aufpoppen" + +[editor.tabs.captions.highlight] +color = "Farbe" +pill = "Pille" + +[editor.tabs.captions.language] +ar = "Arabisch" +auto = "Automatisch erkennen" +bn = "Bengalisch" +de = "Deutsch" +en = "Englisch" +es = "Spanisch" +fa = "Persisch" +fr = "Französisch" +gu = "Gujarati" +he = "Hebräisch" +hi = "Hindi" +it = "Italienisch" +ja = "Japanisch" +ko = "Koreanisch" +mr = "Marathi" +nl = "Niederländisch" +pa = "Punjabi" +pl = "Polnisch" +pt = "Portugiesisch" +ru = "Russisch" +sk = "Slowakisch" +ta = "Tamil" +te = "Telugu" +tr = "Türkisch" +ur = "Urdu" +zh = "Chinesisch" + +[editor.tabs.captions.model] +high_accuracy = "Hohe Genauigkeit" +high_accuracy_description = "Größerer Download, höhere Genauigkeit" +medium = "Mittel" +medium_description = "Langsamer, genauer" +recommended = "Empfohlen" +recommended_description = "Beste Balance für die meisten Aufnahmen" +small = "Klein" +small_description = "Kleinster Download" + +[editor.tabs.captions.preset] +classic = "Klassisch" +classic_description = "Klarer Text auf einem einfarbigen, abgerundeten Hintergrund." +highlight = "Hervorgehoben" +highlight_description = "Fette Großbuchstaben mit einer Pille hinter dem aktiven Wort." +karaoke = "Karaoke" +karaoke_description = "Wörter leuchten synchron zur Sprache auf." +minimal = "Minimal" +minimal_description = "Dezenter, umrandeter Text ohne Hintergrund." +pop = "Pop" +pop_description = "Verspielte Großbuchstaben, die mit einem kräftigen Akzent aufpoppen." + +[editor.tabs.corner] +rounded = "Abgerundet" +squircle = "Squircle" + +[editor.tabs.cursor] +friction = "Reibung" +hide_when_idle = "Bei Inaktivität ausblenden" +inactivity_delay = "Verzögerung bei Inaktivität" +mass = "Masse" +movement_style = "Bewegungsstil des Cursors" +show = "Cursor anzeigen" +size_label = "Größe" +smooth_movement = "Gleichmäßige Bewegung" +style_label = "Cursorstil" +svg_cursors = "Hochwertige SVG-Cursor" +tension = "Spannung" +tilt = "Neigung" + +[editor.tabs.cursor.style] +fast = "Schnell" +fast_description = "Zackig, eng folgend" +mellow = "Sanft" +mellow_description = "Ausgewogene, natürliche Bewegung" +slow = "Langsam" +slow_description = "Lange, gleitende Bewegung" +smooth = "Weich" +smooth_description = "Weiche, schwebende Bewegung" + +[editor.tabs.keyboard] +animation = "Animation" +background_color = "Hintergrundfarbe" +background_opacity = "Hintergrunddeckkraft" +background_settings = "Hintergrundeinstellungen" +behavior = "Verhalten" +beta = "Beta" +changed_during_generation = "Die Zeitleiste oder die Tastatureinstellungen haben sich während der Erstellung geändert. Versuchen Sie es erneut." +empty_description = "Klicken Sie auf „Tastatursegmente erstellen“, um Segmente aus aufgezeichneten Tastendrücken zu erstellen." +empty_title = "Noch keine Tastatursegmente." +fade_duration = "Einblenddauer" +font_family = "Schriftart" +font_settings = "Schrifteinstellungen" +font_weight = "Schriftstärke" +generate = "Tastatursegmente erstellen" +generating = "Tastatursegmente werden erstellt..." +grouping_threshold = "Gruppierungsschwelle" +linger_duration = "Anzeigedauer" +load_failed = "Aufnahmedaten konnten nicht geladen werden: {error}" +no_presses = "Es wurden keine aufgezeichneten Tastendrücke gefunden." +position = "Position" +regenerate = "Tastatursegmente neu erstellen" +show_modifiers = "Sondertasten-Modifikatoren anzeigen" +show_special_keys = "Spezialtasten anzeigen" +size_label = "Größe" +text_color = "Textfarbe" +timeline_unavailable = "Die Projektzeitleiste ist nicht verfügbar." +timeline_unreadable = "Die Projektzeitleiste konnte nicht gelesen werden." +title = "Tastatur anzeigen" +uppercase = "Großbuchstaben" + +[editor.tabs.position] +bottom_center = "Unten mittig" +bottom_left = "Unten links" +bottom_right = "Unten rechts" +manual = "Manuell" +top_center = "Oben mittig" +top_left = "Oben links" +top_right = "Oben rechts" + +[editor.tabs.sync] +auto_description = "Cap hat diese Versätze automatisch berechnet, damit der Ton synchron zum Video bleibt. Passen Sie sie an, falls noch etwas nicht passt." +auto_synced = "Automatisch synchronisiert" +camera = "Kameraversatz" +clip = "Clip {number}" +description = "Feinjustieren Sie die Quellversätze, wenn Ton oder Kamera nicht mehr synchron zur Bildschirmaufnahme laufen." +microphone = "Mikrofonversatz" +system_audio = "Systemaudio-Versatz" +title = "Synchronisierung" + +[editor.tabs.weight] +bold = "Fett" +medium = "Mittel" +normal = "Normal" + +[editor.timeline] +add_shot = "Einstellung hinzufügen" +add_shot_with_look = "Einstellung hinzufügen · {look} · {seconds} s" +paused = "Pausiert" + +[editor.timeline.empty] +add_audio = "Audio hinzufügen" +add_scene = "Szene hinzufügen" +audio = "Fügen Sie Hintergrundmusik hinzu oder importieren Sie eigenes Audio" +captions = "Keine Untertitel" +keyboard = "Keine Tastaturereignisse" +scene = "Wechseln Sie zwischen Layouts für Bildschirm und Kamera" + +[editor.timeline.picker] +audio = "Fügen Sie Hintergrundmusik hinzu oder importieren Sie eigenes Audio." +caption = "Transkribieren Sie Ihre Aufnahme automatisch in Untertitel auf dem Bildschirm." +clip = "Ihr aufgenommenes Bildschirmmaterial." +image = "Fügen Sie Ihrer Aufnahme ein Bild hinzu." +keyboard = "Zeigen Sie beim Tippen Tastendrücke auf dem Bildschirm an." +mask = "Verpixeln oder schwärzen Sie private Bereiche des Bildschirms." +scene = "Wechseln Sie zwischen Layouts für Bildschirm und Kamera." +scene_unavailable = "Nehmen Sie mit einer Kamera auf, um Szenen zu verwenden." +style = "Ändern Sie Hintergrund-, Kamera- und Cursoreinstellungen im Zeitverlauf." +text_overlay = "Fügen Sie der Leinwand eigene Texteinblendungen und Titel hinzu." +three_d = "Neigen Sie die Szene in 3D-Perspektive." +zoom = "Sanfte Zooms, die dem Geschehen folgen." + +[editor.timeline.remove] +audio = "Die Audiospur entfernen." +captions = "Die Untertitelspur entfernen." +clip = "Die Clipspur entfernen." +image = "Die Bildspur entfernen." +keyboard = "Die Tastaturspur entfernen." +mask = "Die Maskenspur entfernen." +scene = "Die Szenenspur entfernen." +style = "Die Stilspur entfernen." +text_track = "Die Textspur entfernen." +three_d = "Die 3D-Spur entfernen." +zoom = "Die Zoomspur entfernen." + +[editor.timeline.scene] +camera_only = "Nur Kamera" +default = "Standard" +floating = "Schwebend" +hide_camera = "Kamera ausblenden" +split_screen = "Geteilter Bildschirm" + +[editor.timeline.segment] +audio = "Audio" +automatic_zoom = "Automatischer Zoom" +caption = "Untertitel" +clip = "Clip" +cursor_hidden = "Cursor ausgeblendet" +highlight = "Hervorheben" +label = "Beschriftung" +manual_zoom = "Manueller Zoom" +muted = "Stumm" +numbered_clip = "Clip {number}" +sensitive = "Vertraulich" + +[editor.timeline.track] +audio = "Audio" +captions = "Untertitel" +clip = "Clip" +image = "Bild" +keyboard = "Tastatur" +mask = "Maske" +scene = "Szene" +style = "Stil" +text_track = "Text" +video = "Video" +zoom = "Zoom" + +[editor.window] +audio_filter = "Audio" +audio_segment_changed = "Dieses Audiosegment hat sich geändert. Wählen Sie es aus und wählen Sie erneut einen Ersatz." +crop = "Zuschneiden" +crop_tooltip = "Video zuschneiden" +no_room_for_3d_shot = "Kein Platz für eine weitere 3D-Einstellung" +redo = "Wiederholen" +save_failed = "{path} konnte nicht gespeichert werden. Lassen Sie den Editor geöffnet und versuchen Sie es erneut: {error}" +undo = "Rückgängig" + +[editor.window.add_track] +button = "Spur hinzufügen" +hide_3d = "Die 3D-Spur ausblenden." +hint = "Wählen Sie eine Spur, die Sie Ihrer Zeitleiste hinzufügen möchten." + +[editor.window.aspect] +auto = "Auto" +classic = "Klassisch" +classic_ratio = "Klassisch ⋅4:3" +square = "Quadrat" +square_ratio = "Quadrat ⋅1:1" +tall = "Hoch" +tall_ratio = "Hoch ⋅3:4" +tooltip = "Seitenverhältnis" +vertical = "Vertikal" +vertical_ratio = "Vertikal ⋅9:16" +wide = "Breit" +wide_ratio = "Breit ⋅16:9" + +[editor.window.audio_import] +copy_failed = "Audiodatei konnte nicht kopiert werden: {error}" +directory_failed = "Audioordner konnte nicht erstellt werden: {error}" +no_extension = "Die Audiodatei hat keine Dateiendung" +not_found = "Audiodatei nicht gefunden: {path}" +unsupported = "Nicht unterstütztes Audioformat: .{extension}" + +[editor.window.busy] +clip_import = "Warten Sie, bis der Clip-Import abgeschlossen ist, bevor Sie diese Aufnahme schließen oder löschen." +export = "Warten Sie, bis der Export oder sein Abbruch abgeschlossen ist, bevor Sie diese Aufnahme schließen oder löschen." +opening = "Warten Sie, bis die Aufnahme vollständig geöffnet ist, bevor Sie sie löschen." +save_before_delete_failed = "Die Aufnahme konnte vor dem Löschen nicht gespeichert werden: {error}" + +[editor.window.camera3d_lane] +add_shot = "+ Einstellung hinzufügen" +auto_scene = "Automatische Szene" +how_many = "Wie viele Einstellungen?" +prompt = "Fügen Sie Ihrer Aufnahme filmische 3D-Einstellungen hinzu" + +[editor.window.clip_menu] +audio = "AUDIO" +cursor = "CURSOR" +delete = "Clip löschen" +delete_only_clip = "Der einzige Clip kann nicht gelöscht werden" +hide_cursor = "Cursor ausblenden" +hide_cursor_description = "Wird ausgeblendet, während dieser Clip läuft" +keep_pitch = "Tonhöhe beibehalten" +match_speed = "An Tempo anpassen" +merge_next = "Mit nächstem Clip zusammenführen" +merge_previous = "Mit vorherigem Clip zusammenführen" +mute = "Stumm" +mute_clip = "Clip stummschalten" +mute_clip_description = "Den Ton dieses Clips stummschalten" +rename = "Clip umbenennen" +speed = "TEMPO" +split = "Am Abspielkopf teilen" +split_hint = "Bewegen Sie den Abspielkopf in diesen Clip" +volume = "Lautstärke" + +[editor.window.frame] +add_tooltip = "Rahmen hinzufügen" +browser = "Browser" +browser_description = "Browserfenster mit Adressleiste" +button = "Rahmen" +dark = "Dunkel" +global = "Globaler Rahmen · Gilt überall, wo der Stil den Hintergrund übernimmt." +heading = "Rahmen" +light = "Hell" +macbook_description = "Laptop-Rahmen um die Aufnahme" +macos_description = "Fensterrahmen mit Ampel-Tasten" +none = "Keiner" +none_description = "Die Aufnahme unverändert zeigen" +style_changed = "Dieser Stil hat sich geändert. Schließen und öffnen Sie den Rahmen erneut." +style_only = "Nur Stil {index} · Globale Einstellungen bleiben unverändert." +style_only_override = "Nur Stil {index} · Bearbeiten aktiviert die eigene Hintergrundeinstellung." +theme = "Design" +title = "Titel" +url = "URL" +window_title_placeholder = "Fenstertitel" +windows_description = "Titelleiste mit Fenstersteuerung" + +[editor.window.header] +captions = "Untertitel" +delete_recording = "Aufnahme löschen" +export = "Exportieren" +open_bundle = "Aufnahmepaket öffnen" +presets = "Vorlagen" + +[editor.window.image] +changed = "Dieses Bild hat sich bei der Auswahl eines Ersatzes geändert. Wählen Sie es aus und versuchen Sie es erneut." +filter = "Bilder" +recording_unavailable = "Die Aufnahme ist für dieses Bild nicht mehr verfügbar. Öffnen Sie das Projekt erneut." + +[editor.window.open] +damaged = "Die Videospuren dieser Aufnahme konnten nicht geöffnet werden. Das Paket scheint beschädigt zu sein." +failed = "Diese Aufnahme konnte nicht geöffnet werden" +media_failed = "Die Medien dieser Aufnahme konnten nicht gelesen werden: {error}" +meta_failed = "Die Aufnahme-Metadaten konnten nicht geladen werden: {error}" +no_segments = "Die Aufnahme hat keine Segmente. Möglicherweise muss sie zuerst wiederhergestellt werden." +not_found = "Videopfad {path} nicht gefunden!" +not_studio = "Nur Studio-Aufnahmen können bearbeitet werden" + +[editor.window.presets] +apply = "Anwenden" +create = "Erstellen" +create_new = "Neue Vorlage erstellen" +create_title = "Vorlage erstellen" +default = "Standard" +default_badge = "Standard" +delete = "Löschen" +delete_confirm = "Möchten Sie „{name}“ wirklich löschen?" +delete_title = "Vorlage löschen" +name_placeholder = "Name der Vorlage" +none_saved = "Keine gespeicherten Vorlagen" +rename = "Umbenennen" +rename_title = "Vorlage umbenennen" +save_settings = "Einstellungen in Vorlage speichern" +set_default = "Als Standard festlegen" + +[editor.window.preview] +full = "Voll" +half = "Halb" +label = "Vorschau" +quarter = "Viertel" + +[editor.window.text_menu] +copy_style = "Stil kopieren" +copy_style_first = "Zuerst einen Stil kopieren" +delete_segment = "Textsegment löschen" +delete_segments = "Textsegmente löschen" +paste_style = "Stil einfügen" +paste_style_all = "Stil in alle ausgewählten einfügen" +segments = { one = "{count} Textsegment", other = "{count} Textsegmente" } +untitled = "Text" + +[editor.window.title] +not_saved = "Titel nicht gespeichert" +not_saved_message = "{error}\n\nIhr Titel ist noch im Editor. Versuchen Sie erneut, ihn zu speichern." +read_failed = "Der Aufnahmetitel für {path} konnte nicht gelesen werden: {error}" +save_failed = "Der Aufnahmetitel für {path} konnte nicht gespeichert werden: {error}" + +[editor.window.track] +clear_all = "Alle entfernen" +delete = "Löschen" + +[editor.window.transport] +done_splitting = "Teilen beenden" +play_pause = "Video abspielen/pausieren" +split = "Teilen" +split_tooltip = "Klicken Sie auf ein Segment, um es zu teilen. Esc zum Beenden." +zoom_in = "Vergrößern" +zoom_out = "Verkleinern" + +[editor.window.zoom_lane] +generate = "Erstellen" +generating = "Wird erstellt…" +no_clicks = "Keine Klicks zum Zoomen gefunden. Ziehen Sie über die Spur, um einen hinzuzufügen." +prompt = "Zooms automatisch hinzufügen" + +[ui] +coming_soon = "Demnächst verfügbar" + +[ui.selection] +audio = { one = "{count} Audiosegment ausgewählt", other = "{count} Audiosegmente ausgewählt" } +caption = { one = "{count} Untertitelsegment ausgewählt", other = "{count} Untertitelsegmente ausgewählt" } +delete = "Löschen" +done = "Fertig" +image = { one = "{count} Bildsegment ausgewählt", other = "{count} Bildsegmente ausgewählt" } +keyboard = { one = "{count} Tastatursegment ausgewählt", other = "{count} Tastatursegmente ausgewählt" } +mask = { one = "{count} Maskensegment ausgewählt", other = "{count} Maskensegmente ausgewählt" } +scene = { one = "{count} Szenensegment ausgewählt", other = "{count} Szenensegmente ausgewählt" } +segments = { one = "{count} Segment ausgewählt", other = "{count} Segmente ausgewählt" } +select_all = "Alle auswählen" +style = { one = "{count} Stilsegment ausgewählt", other = "{count} Stilsegmente ausgewählt" } +text_segments = { one = "{count} Textsegment ausgewählt", other = "{count} Textsegmente ausgewählt" } +three_d = { one = "{count} 3D-Segment ausgewählt", other = "{count} 3D-Segmente ausgewählt" } +zoom = { one = "{count} ausgewählt", other = "{count} ausgewählt" } +zoom_all = { one = "Alle {count} ausgewählt", other = "Alle {count} ausgewählt" } +zoom_some = { one = "{count} von {total} ausgewählt", other = "{count} von {total} ausgewählt" } diff --git a/apps/desktop-gpui/locales/en.toml b/apps/desktop-gpui/locales/en.toml index 07745b284cc..0297a8fcaf4 100644 --- a/apps/desktop-gpui/locales/en.toml +++ b/apps/desktop-gpui/locales/en.toml @@ -259,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?" @@ -622,3 +1314,1566 @@ 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" + +# Editor sidebar, segment panels, colour correction, audio library and crop dialog. + +[editor.audio.library] +add_hint = "Add audio, music or other sounds to your video" +add_title = "Add audio" +change_hint = "Pick a different track for this segment" +change_title = "Change audio" +done = "Done" +import = "Import file" + +[editor.color] +apply_to_cursor = "Apply to cursor" +fine_tune = "Fine-tune colors" +title = "Color Correction" + +[editor.color.preset.cinematic] +description = "Teal shadows and orange highlights with light grain" +name = "Cinematic" + +[editor.color.preset.dreamy] +description = "Soft, airy pastels with lifted blacks" +name = "Dreamy" + +[editor.color.preset.frost] +description = "Cool, crisp tones with muted color" +name = "Frost" + +[editor.color.preset.golden] +description = "Warm golden-hour glow" +name = "Golden" + +[editor.color.preset.midnight] +description = "Dark, moody teal with subdued color" +name = "Midnight" + +[editor.color.preset.noir] +description = "High-contrast black and white with heavy grain" +name = "Noir" + +[editor.color.preset.none] +description = "Original colors, no grade applied" +name = "None" + +[editor.color.preset.vintage] +description = "Faded warm film look with soft contrast" +name = "Vintage" + +[editor.color.preset.vivid] +description = "Punchy saturation and contrast boost" +name = "Vivid" + +[editor.color.slider] +contrast = "Contrast" +exposure = "Exposure" +fade = "Fade" +grain = "Grain" +saturation = "Saturation" +split_tone = "Split Tone" +strength = "Strength" +temperature = "Temperature" +tint = "Tint" +vignette = "Vignette" + +[editor.crop] +aspect_ratio = "Aspect ratio" +center = "Center" +dimensions = "Size" +editing_style = { text = "Editing Style {number} only", context = "{number} is the style segment's position, starting at 1." } +editing_style_override = { text = "Editing Style {number} only · Saving enables its background override", context = "{number} is the style segment's position, starting at 1." } +free = "Free" +full = "Full" +hint = { text = "Drag to move · Arrow keys nudge · Hold {key} to skip snapping", context = "{key} is drawn as a Shift key badge; keep it where the sentence needs it." } +loading_frame = "Loading frame…" +position = "Position" +preview = "Preview" +rendering_preview = "Rendering preview…" +snap_to_ratios = "Snap to ratios" +style_changed = "This Style changed while cropping. Cancel and reopen Crop." +title = "Crop" + +[editor.panel] +delete = "Delete" +done = "Done" +duration = "Duration" +edge_in = "In" +edge_out = "Out" +enabled = "Enabled" +fade_duration = "Fade Duration" +flip_horizontally = "Flip horizontally" +flip_vertically = "Flip vertically" +name = "Name" +opacity = "Opacity" + +[editor.panel.audio] +change = "Change" +change_hint = "Tap to change track" +fade_in = "Fade In" +fade_out = "Fade Out" +fallback_name = "Audio" +title = "Audio {number}" +volume = "Volume" + +[editor.panel.camera3d] +already_still = "Already a still shot" +amount = "Amount" +angles = "Angles" +auto_scene = "Auto scene" +auto_scene_tooltip = "Rebuilds every 3D shot on the track" +bokeh = "Bokeh" +camera = "Camera" +delete_tooltip = "Delete this shot" +depth_blur = "Depth blur" +distance = "Distance" +drag_to_orbit = "Drag to orbit" +ease_in = "Ease in" +ease_out = "Ease out" +end_same_as_start = "End · same as start" +fine_tune = "Fine-tune" +focus = "Focus" +header = { text = "3D shot · {look} · {duration}s", context = "{look} is the shot's look name; {duration} is seconds with one decimal, such as 4.0." } +hold_opening = "Hold the opening pose for the whole shot" +lens = "Lens" +look_title = "Look" +motion_style = "Motion style" +moves = "Moves" +nothing_to_swap = "Nothing to swap on a still shot" +play_shot = "Play shot" +play_shot_tooltip = "Play this shot" +pose_end = { text = "End · {time}", context = "{time} is a clock such as 0:04.0." } +pose_start = { text = "Start · {time}", context = "{time} is a clock such as 0:04.0." } +reset_camera = "Reset camera" +roll = "Roll" +rotate_x = "Rotate X" +rotate_y = "Rotate Y" +shift_x = "Shift X" +shift_y = "Shift Y" +shots = { one = "{count} shot", other = "{count} shots", context = "How many shots a ready-made 3D sequence lays out." } +still_shot = "Still shot" +swap = "Swap start and end" +timing_advanced = "Timing & advanced" +timing_summary = { text = "{easing} · Lens {fov}", context = "{easing} is the motion style name; {fov} is the lens angle in degrees, a whole number." } + +[editor.panel.camera3d.blur] +angle = "Angle" +falloff = "Falloff" +focus_size = "Focus size" +focus_x = "Focus X" +focus_y = "Focus Y" +position = "Position" +scan = "Scan" +strength = "Strength" + +[editor.panel.camera3d.easing] +ease_in = "Ease in" +ease_out = "Ease out" +linear = "Linear" +smooth = "Smooth" + +[editor.panel.camera3d.focus_mode] +directional = "Directional" +radial = "Radial" +tilt_shift = "Tilt shift" + +[editor.panel.camera3d.look] +center = "Center" +close_up = "Close up" +drift_down = "Drift down" +glide_across = "Glide across" +low_angle = "Low angle" +perspective = "Perspective" +pull_back = "Pull back" +rising_sweep = "Rising sweep" +slide_by = "Slide by" +spotlight = "Spotlight" +tilt_away = "Tilt away" +top_down = "Top down" +unfold = "Unfold" + +[editor.panel.camera3d.sequence] +product_tour = "Product tour" +punch_in = "Punch in" +showcase = "Showcase" + +[editor.panel.camera3d.shot] +custom = "Custom move" +still = "Still shot" + +[editor.panel.caption] +title = "Caption {number}" + +[editor.panel.font] +system_mono = "System Mono" +system_sans = "System Sans" +system_serif = "System Serif" + +[editor.panel.font_weight] +black = "Black" +bold = "Bold" +custom = { text = "Custom ({weight})", context = "{weight} is a numeric font weight such as 450." } +extra_bold = "Extra Bold" +light = "Light" +medium = "Medium" +regular = "Regular" +semibold = "Semibold" + +[editor.panel.image] +arrange = "Arrange on canvas" +arrange_help = "Drag the image to move it. Pull a corner to resize, or use the rotation control below to turn it." +center = "Center on canvas" +lock_aspect = "Lock aspect ratio" +missing = "Image file is missing. Replace it to restore this segment while keeping its timing and transforms." +replace = "Replace image" +rotation = "Rotation" +rounding = "Rounding" + +[editor.panel.keyboard] +title = "Keyboard {number}" + +[editor.panel.mask] +blur = "Blur" +effect = "Effect" +highlight = "Highlight" +outside_darkness = "Outside Darkness" +pixel_size = "Pixel Size" +pixelate = "Pixelate" +sensitive = "Sensitive" +title = "Mask {number}" + +[editor.panel.scene] +camera_layout = "Camera Layout" +camera_position = "Camera Position" +camera_zoom = "Camera Zoom" +screen_position = "Screen Position" +screen_zoom = "Screen Zoom" +transition = "Transition" + +[editor.panel.scene.description] +camera_only = "Shows only the camera feed" +default = "Shows both screen and camera" +floating = "Screen and camera float side by side as rounded cards over the background" +hide_camera = "Shows only the screen recording" +split_screen = "Screen and camera side by side (auto-stacks in portrait)" + +[editor.panel.scene.mode] +camera_only = "Camera Only" +default = "Default" +floating = "Floating" +hide_camera = "Hide Camera" +split_screen = "Split Screen" + +[editor.panel.style] +camera_only_background = "Camera Only background" +camera_only_help = "Use a Camera Only scene. Padding reveals the background around the camera." +camera_only_padding = "Camera Only padding" +crop_height = "Height" +crop_left = "Left" +crop_title = "Screen crop (source pixels)" +crop_top = "Top" +crop_width = "Width" +edit_background = "Edit Background & frame" +edit_camera = "Edit Camera" +edit_cursor = "Edit Cursor" +help = "Overrides apply only during this segment. Enable a group to copy its global settings." +reset_crop = "Reset crop" + +[editor.panel.text_animation] +bounce = "Bounce" +fade = "Fade" +letters = "Letters" +none = "None" +pop = "Pop" +slide_down = "Slide down" +slide_left = "Slide left" +slide_right = "Slide right" +slide_up = "Slide up" +tracking = "Tracking" +typewriter = "Typewriter" +wipe = "Wipe" +words = "Words" +zoom = "Zoom" + +[editor.panel.text_background] +box_shape = "Box" +highlight = "Highlight" +none = "None" +pill = "Pill" + +[editor.panel.text_layout] +fullscreen = "Fullscreen" +overlay = "Overlay" + +[editor.panel.text_preset.group] +all = "All" +callouts = "Callouts" +code = "Code" +lower_thirds = "Lower thirds" +statements = "Statements" +titles = "Titles" + +[editor.panel.text_preset.name] +caption = "Caption" +cinematic = "Cinematic" +code = "Code" +gradient = "Gradient" +headline = "Headline" +highlight = "Highlight" +kicker = "Kicker" +label = "Label" +lower_third = "Lower third" +name_tag = "Name tag" +neon = "Neon" +quote = "Quote" +stat = "Big stat" +sticker = "Sticker" +title = "Title" +typewriter = "Typewriter" + +[editor.panel.text_preset.sample] +caption = "Recorded with Cap" +cinematic = "Chapter one" +gradient = "Beautiful text" +headline = "Ship faster" +highlight = "the important part" +kicker = "New feature" +label = "Pro tip" +name_tag = { text = "Richie · Founder", context = "Richie is a person's name and stays as it is." } +neon = "Glow up" +quote = "“Make it feel effortless”" +sticker = "Boom!" +title = { text = "Introducing Cap", context = "Sample text a new text segment takes when this preset is applied. Cap is the product name." } +typewriter = "typing it out…" + +[editor.panel.text_segment] +animation = "Animation" +background = "Background" +color = "Color" +font = "Font" +font_size = "Size" +fullscreen_help = "Pauses the video while the text is shown, then resumes where it left off." +glow = "Glow" +gradient = "Gradient" +layout = "Layout" +letter_spacing = "Letter spacing" +line_height = "Line height" +look = "Look" +outline = "Outline" +screen_transition = "Screen transition" +shadow = "Shadow" +style = "Style" +title = "Text {number}" + +[editor.panel.timing] +end = "End" +start = "Start" +title = "Timing" +to = { text = "to", context = "Sits between the start and end time boxes." } + +[editor.panel.zoom] +amount = "Zoom Amount" +auto = "Auto" +auto_help = "Automatic zoom follows your cursor around the screen." +auto_locked = "Auto mode needs cursor capture. Enable \"Custom cursor capture (Studio)\" in Settings → General." +how_it_works = "How does it work?" +manual = "Manual" +manual_help = "Manual zoom stays on a fixed spot you pick below." +mixed = "Mixed" +mode = "Zoom Mode" +positions_mixed = "Segments zoom into different spots. Drag to move them all to the same one." +title = { text = "Zoom {number}", context = "{number} is the segment's position on the track, starting at 1." } + +[editor.sidebar] +desktop_import_failed = { text = "Couldn't import your desktop wallpaper", context = "Alert title. The alert body is a technical error message in English." } +importing_image = "Importing image…" + +[editor.sidebar.animated_gradient] +add_colour = "Add a colour" +colours = "Colours" +delete = "Delete" +delete_failed = "Could not delete this gradient. Try again." +fine_tune = "Fine-tune" +limit_reached = { one = "You can save up to {count} gradient. Delete one to add another.", other = "You can save up to {count} gradients. Delete one to add another." } +motion = "Motion" +name_placeholder = "Name this gradient" +name_required = "Enter a name for this gradient." +presets = "Presets" +randomize = "Randomize" +remember_failed = "Could not remember your animated gradient settings." +remove_colour = "Remove colour" +reset_tooltip = "Reset fine-tune settings" +save_failed = "Could not save this gradient. Try again." +save_tooltip = "Save the current gradient" +saved = "Saved" +still = { text = "Still", context = "Motion speed readout when the speed is zero." } + +[editor.sidebar.animated_gradient.control] +contrast = "Contrast" +curvature = "Curvature" +detail = "Detail" +direction = "Direction" +exposure = "Exposure" +flow_scale = "Flow Scale" +flow_strength = "Flow Strength" +grain_amount = "Grain Amount" +grain_size = "Grain Size" +highlights = "Highlights" +motion_speed = "Motion Speed" +relief = "Relief" +ripple_size = "Ripple Size" +shading = "Shading" +vibrance = "Vibrance" + +[editor.sidebar.animated_gradient.group] +animation = "Animation" +colour = "Colour" +flow = "Flow" +lighting = "Lighting" +texture = "Texture" + +[editor.sidebar.background] +title = "Background" + +[editor.sidebar.border] +color = "Border Color" +opacity = "Border Opacity" +title = "Border" +width = "Border Width" + +[editor.sidebar.corner] +rounded = "Rounded" +squircle = "Squircle" +title = "Corner Style" + +[editor.sidebar.cursor] +circle = "Circle" +click_ripple = "Click Ripple" +color = "Color" +default = "Default" +description_circle = "Replaces all cursor shapes with a circle." +description_default = "Keeps the cursor shapes and appearance from your recording." +description_macos = "Classic macOS appearance. Custom cursors keep their recorded shape." +description_tahoe = "macOS Tahoe appearance. Custom cursors keep their recorded shape." +description_windows = "Windows appearance. Custom cursors keep their recorded shape." +duration = "Duration" +recorded = "Recorded cursors" +recorded_tooltip = "Recorded with this cursor" +ripple_size = "Size" +strength = "Strength" + +[editor.sidebar.desktop] +import = "Import desktop background" +importing = "Importing..." +prompt = "Use the wallpaper from your desktop" +reimport = "Re-import" + +[editor.sidebar.gradient] +angle = "Angle" +from = "From" +grain_scale = "Grain Scale" +noise = "Noise" +to = "To" + +[editor.sidebar.image] +prompt = "Click to select or drag and drop image" + +[editor.sidebar.layout] +background_blur = "Background Blur" +custom_position = "Custom screen position (dragged on canvas)" +motion_blur = "Motion Blur" +padding = "Padding" +rounded_corners = "Rounded Corners" +title = "Layout" + +[editor.sidebar.notch] +description = "Draws a MacBook notch over the recording. Recordings made on a Mac with a notch use their own measurements; otherwise start from the size below and adjust to match." +height = "Notch Height" +position = "Notch Position" +title = "MacBook notch" +width = "Notch Width" + +[editor.sidebar.shadow] +advanced = "Advanced shadow settings" +blur = "Blur" +opacity = "Opacity" +shadow_size = "Size" +title = "Shadow" + +[editor.sidebar.source] +animated = "Animated" +color = "Color" +desktop = "Desktop" +gradient = "Gradient" +image = "Image" +none = "None" +wallpaper = "Wallpaper" + +[editor.sidebar.style_group] +back = "Back to Style" +background = "Background & frame" +camera = "Camera" +cursor = "Cursor" +editing_note = "Editing this segment only. Global settings are unchanged." +title = { text = "Style {number} · {group}", context = "{number} is the style segment's position, starting at 1; {group} is Background & frame, Camera or Cursor." } + +[editor.sidebar.tab] +audio = "Audio" +background = "Background" +camera = "Camera" +captions = "Captions" +cursor = "Cursor" +keyboard = "Keyboard" + +[editor.sidebar.wallpaper_theme] +blue = "Blue" +cities = "Cities" +dark = "Dark" +orange = "Orange" +purple = "Purple" + +[editor.canvas] +camera = "Camera" +camera_follows_zoom = "Camera size follows the zoom — drag to move" +edit_zoom = "Edit zoom" +image = "Image" +mask = "Mask" +screen = "Screen" +screen_locked = "Screen is locked while a zoom is active" +text_item = "Text" + +[editor.clips] +append_error = { text = "Failed to add clip: {message}", context = "{message} explains why." } +append_error_title = "Recording Error" +back = "Back to editor" +cap_filter = { text = "Cap Recording", context = "File type filter name for .cap recordings in the file picker." } +clip = { text = "Clip {number}", context = "Default clip name; {number} starts at 1." } +empty_description = "Record or import a clip and it will show up here." +empty_title = "No clips yet" +import = "Import" +import_error = { text = "Failed to import clip: {message}", context = "{message} explains why." } +import_error_title = "Import Error" +import_existing = "Existing recording" +import_mp4 = "MP4 Video…" +importing = "Importing…" +mp4_filter = { text = "MP4 Video", context = "File type filter name in the file picker." } +prepare_controls_failed = "Could not prepare the recording controls: {error}" +record_new = "Record a new clip" +save_before_recording_failed = "Could not save the current project before recording: {error}" +split = { text = "Split {number}", context = "Default name of a later piece cut from the same clip; {number} starts at 1." } +title = "Clips" + +[editor.clips.error] +cancelled = "Import cancelled" +cannot_decode = "Cannot decode video {path}: {error}" +in_use = "Cannot import while this recording is in use: {error}" +instant_source = "Instant recordings can't be imported into a project yet" +instant_target = "Instant mode recordings cannot be edited" +into_itself = "Cannot import a recording into itself" +missing_video = "Missing video file: {path}" +no_duration = "Imported video has no valid duration" +not_a_project = "Select a Cap project folder" +source_empty = "Source Cap project has no recording segments" +source_failed = "Source Cap project failed: {error}" +source_recording = "Source Cap project is still recording" +source_unimportable = "Source Cap project has no importable recording segments" +unfinished_target = "Finish or recover this recording before importing a clip" +unsupported_video = "Unsupported video file: {path}" + +[editor.export] +back = "Back to editor" +existing_link_description = "Reupload replaces the video at this link with your latest edit. Everyone with the link will see the updated version." +existing_link_title = "Update your existing link" +failed = "Export failed" +fps = { text = "{fps} FPS", context = "Frame rate option, for example 30 FPS." } +frames = { one = "{rendered} / {count} frame", other = "{rendered} / {count} frames", context = "Export progress: {rendered} frames done out of {count}." } +generating_preview = "Generating preview…" +link_unavailable_cursor = "Cursor-only exports can only be saved to a file or clipboard." +link_unavailable_transparent = "Transparent exports can only be saved to a file or clipboard." +organization = "Organization" +preview = "Preview" +preview_info = "This is a rendered frame from your video. Adjust the settings to see the quality of the final export." +preview_unavailable = "Preview unavailable" +reupload = "Reupload" +reupload_tooltip = "Upload your latest edit to the same link" +reupload_transparent = "Share links require a background without transparency" +sign_in_required = "You need to sign in to share recordings" +task_failed = "Export task failed" +title = "Export" +upgrade_message = "This feature requires an upgraded plan" +upgrade_title = "Upgrade required" +upload_failed = "Failed to upload recording" + +[editor.export.advanced] +bits_per_pixel = "Bits per pixel" +cursor_only = "Export cursor only" +cursor_only_description = "Keeps the same cursor motion and clicks on a transparent background" +custom_bitrate = "Using a custom bitrate" +force_ffmpeg = "Force FFmpeg decoder" +force_ffmpeg_description = "Skip hardware decoder (auto-fallback enabled)" +title = "Advanced" +transparent_mov = "Exports as a transparent MOV. Files are large and best for compositing or editing." + +[editor.export.cta] +cancel_sign_in = "Cancel Sign In" +clipboard = "Export to Clipboard" +create_link = "Create shareable link" +file = "Export to File" +reupload = "Reupload to same link" +sign_in = "Sign in to share" + +[editor.export.destination] +clipboard = { text = "Clipboard", context = "Export destination." } +file = { text = "File", context = "Export destination." } +link = { text = "Shareable Link", context = "Export destination." } + +[editor.export.done] +copy_link = "Copy link" +instant_hint = "Use Instant Mode for your next recording if you want a link the moment you stop." +link_copied = "Link copied" +open_file = "Open File" +open_link = "Open link" +retry_copy = "Retry Copy" +reuploaded = "Your latest edit is ready at the same link" +uploaded = "Your Cap has been uploaded successfully" + +[editor.export.estimate] +gigabytes = "~{value} GB" +gigabytes_range = "~{lower}–{upper} GB" +hours = "~{value} hr" +hours_range = "~{lower}–{upper} hr" +megabytes = "~{value} MB" +megabytes_range = "~{lower}–{upper} MB" +minutes = "~{value} min" +minutes_range = "~{lower}–{upper} min" +seconds = "~{value} s" +seconds_range = "~{lower}–{upper} s" +under_megabyte = "< 1 MB" +under_second = "< 1s" + +[editor.export.optimize] +description = "Re-encodes with software for much smaller files (slower)" +title = "Optimize file size" + +[editor.export.phase] +clipboard_failed = "Export complete; clipboard copy failed" +complete = "Export complete" +copied = "Copied to clipboard" +copying_to_clipboard = "Copying to clipboard" +rendering_cursor = "Rendering cursor track" +rendering_gif = "Rendering GIF" +rendering_video = "Rendering video" +reupload_complete = "Reupload complete" +reuploading = "Reuploading to your link" +saving_to_file = "Saving to file" +starting = "Preparing export" +upload_complete = "Upload complete" +uploading = "Creating shareable link" + +[editor.export.quality] +larger = "Larger file" +maximum = { text = "Maximum", context = "Export quality preset." } +potato = { text = "Potato", context = "Export quality preset: the lowest quality, said playfully." } +smaller = "Smaller file" +social = { text = "Social", context = "Export quality preset for social media." } +web = { text = "Web", context = "Export quality preset for the web." } + +[editor.export.resources] +proceed = "Proceed anyway" +title = "Check export resources" + +[editor.export.section] +destination = "Destination" +format = "Format" +frame_rate = "Frame rate" +quality = "Quality" +resolution = "Resolution" + +[editor.export.stats] +calculating = "Calculating…" +duration = "Duration" +estimated_size = "Estimated size" +export_time = "Export time" +output = "Output" +output_value = { text = "{width}×{height} · {fps} fps", context = "Output size and frame rate, for example 1920×1080 · 30 fps." } +refining_size = "Refining size…" +refining_time = "Refining time…" +unavailable = "Unavailable" + +[editor.modal] +cancel_export = "Cancel export" +cancel_export_message = "Are you sure you want to cancel the export?" +cancel_export_title = "Cancel export?" +keep_exporting = "Keep exporting" +recording_retained = "Recording retained" + +[editor.tabs.audio] +isolation_balanced = "Balanced" +isolation_balanced_description = "Clearer isolation with natural voice detail." +isolation_light = "Light" +isolation_light_description = "Keep more of your original voice and room sound." +isolation_strong = "Strong" +isolation_strong_description = "More isolation for noisy spaces. May change voice texture." +mic_volume = "Microphone Volume" +mute = "Mute Audio" +stereo_mode = "Microphone Stereo Mode" +studio_sound_description = "Reduces background noise and balances your voice level." +studio_sound_save_failed = "Could not save the Studio Sound default" +system_volume = "System Audio Volume" +title = "Audio Controls" +use_for_new = "Use for new recordings" + +[editor.tabs.audio.stereo] +mono_left = "Mono L" +mono_right = "Mono R" +stereo = "Stereo" + +[editor.tabs.camera] +advanced_shadow = "Advanced shadow settings" +background = "Background" +corner_style = "Corner Style" +custom_position = "Custom position (dragged on canvas)" +hide = "Hide Camera" +keep_size_during_zoom = "Keep original size during zoom" +mirror = "Mirror Camera" +position = "Position" +reset = "Reset" +rounded_corners = "Rounded Corners" +shadow = "Shadow" +shadow_blur = "Blur" +shadow_opacity = "Opacity" +shape_label = "Shape" +size_during_zoom = "Size During Zoom" +size_label = "Size" +title = "Camera" + +[editor.tabs.camera.blur] +heavy = "Heavy Blur" +light = "Light Blur" +off = "Off" +remove = "Remove Background" + +[editor.tabs.camera.shape] +source = { text = "Source", context = "Camera shape that keeps the camera's own aspect ratio." } +square = "Square" + +[editor.tabs.captions] +active_word_highlight = "Active Word Highlight" +animation_section = "Animation" +animation_style = "Animation Style" +background_color = "Background Color" +background_opacity = "Background Opacity" +background_settings = "Background Settings" +beta = "Beta" +delete = "Delete" +deleting = "Deleting..." +download = "Download {model} Model" +download_note = "One time download to your system. All captions are stored locally." +downloading = "Downloading {model}... {percent}%" +fade_duration = "Fade Duration" +font_family = "Font Family" +font_settings = "Font Settings" +font_weight = "Font Weight" +generate = "Generate Captions" +generate_failed = "Failed to generate captions: {reason}" +generating = "Generating..." +highlight_color = "Highlight Color" +highlight_note = "This is the first version of captions in Cap. Active word highlighting may be inaccurate in some situations. We're working on a fix for this and it will be released in upcoming versions." +highlight_style = "Highlight Style" +horizontal_position = "Horizontal Position" +intel_unavailable = "Parakeet caption models are unavailable on Intel Macs. Whisper models remain available." +keep_open = "Keep Cap open while the model downloads. Editor reloads will reconnect automatically." +language_label = "Language" +model_downloaded = "{model} model downloaded" +model_label = "Model" +position = "Position" +regenerate = "Regenerate Captions" +size_label = "Size" +style = "Style" +text_color = "Text Color" +title = "Captions" +uppercase = "Uppercase" +vertical_position = "Vertical Position" + +[editor.tabs.captions.animation] +bounce = "Bounce" +none = "None" +pop = "Pop" + +[editor.tabs.captions.highlight] +color = "Color" +pill = "Pill" + +[editor.tabs.captions.language] +ar = "Arabic" +auto = "Auto Detect" +bn = "Bengali" +de = "German" +en = "English" +es = "Spanish" +fa = "Persian" +fr = "French" +gu = "Gujarati" +he = "Hebrew" +hi = "Hindi" +it = "Italian" +ja = "Japanese" +ko = "Korean" +mr = "Marathi" +nl = "Dutch" +pa = "Punjabi" +pl = "Polish" +pt = "Portuguese" +ru = "Russian" +sk = "Slovak" +ta = "Tamil" +te = "Telugu" +tr = "Turkish" +ur = "Urdu" +zh = "Chinese" + +[editor.tabs.captions.model] +high_accuracy = "High Accuracy" +high_accuracy_description = "Larger download, higher accuracy" +medium = { text = "Medium", context = "Caption model size." } +medium_description = "Slower, more accurate" +recommended = "Recommended" +recommended_description = "Best balance for most recordings" +small = { text = "Small", context = "Caption model size." } +small_description = "Smallest download" + +[editor.tabs.captions.preset] +classic = "Classic" +classic_description = "Clean text on a solid rounded background." +highlight = "Highlight" +highlight_description = "Bold caps with a pill behind the active word." +karaoke = "Karaoke" +karaoke_description = "Words light up in sync with speech." +minimal = "Minimal" +minimal_description = "Subtle outlined text with no background." +pop = "Pop" +pop_description = "Playful caps that pop in with a vibrant accent." + +[editor.tabs.corner] +rounded = "Rounded" +squircle = "Squircle" + +[editor.tabs.cursor] +friction = "Friction" +hide_when_idle = "Hide When Idle" +inactivity_delay = "Inactivity Delay" +mass = "Mass" +movement_style = "Cursor Movement Style" +show = "Show cursor" +size_label = "Size" +smooth_movement = "Smooth Movement" +style_label = "Cursor Style" +svg_cursors = "High Quality SVG Cursors" +tension = "Tension" +tilt = "Tilt" + +[editor.tabs.cursor.style] +fast = "Fast" +fast_description = "Snappy, close tracking" +mellow = "Mellow" +mellow_description = "Balanced, natural movement" +slow = "Slow" +slow_description = "Long, drifting movement" +smooth = "Smooth" +smooth_description = "Soft, floaty movement" + +[editor.tabs.keyboard] +animation = "Animation" +background_color = "Background Color" +background_opacity = "Background Opacity" +background_settings = "Background Settings" +behavior = "Behavior" +beta = "Beta" +changed_during_generation = "The timeline or keyboard settings changed during generation. Try again." +empty_description = "Click \"Generate Keyboard Segments\" to create segments from recorded keyboard presses." +empty_title = "No keyboard segments yet." +fade_duration = "Fade Duration" +font_family = "Font Family" +font_settings = "Font Settings" +font_weight = "Font Weight" +generate = "Generate Keyboard Segments" +generating = "Generating Keyboard Segments..." +grouping_threshold = "Grouping Threshold" +linger_duration = "Linger Duration" +load_failed = "Failed to load recording data: {error}" +no_presses = "No recorded keyboard presses were found." +position = "Position" +regenerate = "Regenerate Keyboard Segments" +show_modifiers = "Show Modifiers" +show_special_keys = "Show Special Keys" +size_label = "Size" +text_color = "Text Color" +timeline_unavailable = "The project timeline is unavailable." +timeline_unreadable = "The project timeline could not be read." +title = "Show keyboard" +uppercase = "Uppercase" + +[editor.tabs.position] +bottom_center = "Bottom Center" +bottom_left = "Bottom Left" +bottom_right = "Bottom Right" +manual = "Manual" +top_center = "Top Center" +top_left = "Top Left" +top_right = "Top Right" + +[editor.tabs.sync] +auto_description = "Cap calculated these offsets automatically to keep audio in sync with the video. Adjust them if anything still sounds off." +auto_synced = "Auto-synced" +camera = "Camera Offset" +clip = "Clip {number}" +description = "Fine-tune source offsets if audio or camera drifts out of sync with the screen recording." +microphone = "Microphone Offset" +system_audio = "System Audio Offset" +title = "Sync" + +[editor.tabs.weight] +bold = "Bold" +medium = "Medium" +normal = "Normal" + +[editor.timeline] +add_shot = "Add shot" +add_shot_with_look = { text = "Add shot · {look} · {seconds}s", context = "Ghost on the 3D track. {look} is the camera move name, {seconds} a length such as 2.5." } +paused = "Paused" + +[editor.timeline.empty] +add_audio = "Add audio" +add_scene = "Add scene" +audio = "Add background music or import your own audio" +captions = "No captions" +keyboard = "No keyboard events" +scene = "Switch layouts between your screen and camera" + +[editor.timeline.picker] +audio = "Add background music or import your own audio." +caption = "Auto-transcribe your recording into on-screen subtitles." +clip = "Your recorded screen footage." +image = "Add an image to your recording." +keyboard = "Display key presses on screen as you type." +mask = "Blur or black out private areas of the screen." +scene = "Switch layouts between your screen and camera." +scene_unavailable = "Record with a camera to use scenes." +style = "Change background, camera and cursor settings over time." +text_overlay = "Add custom text overlays and titles to the canvas." +three_d = "Tilt the scene in 3D perspective." +zoom = "Smooth zoom-ins that follow the action." + +[editor.timeline.remove] +audio = "Remove the Audio track." +captions = "Remove the Captions track." +clip = "Remove the Clip track." +image = "Remove the Image track." +keyboard = "Remove the Keyboard track." +mask = "Remove the Mask track." +scene = "Remove the Scene track." +style = "Remove the Style track." +text_track = "Remove the Text track." +three_d = "Remove the 3D track." +zoom = "Remove the Zoom track." + +[editor.timeline.scene] +camera_only = "Camera Only" +default = "Default" +floating = "Floating" +hide_camera = "Hide Camera" +split_screen = "Split Screen" + +[editor.timeline.segment] +audio = { text = "Audio", context = "Name of an audio segment that has none." } +automatic_zoom = "Automatic Zoom" +caption = { text = "Caption", context = "Caption segment with no text yet." } +clip = "Clip" +cursor_hidden = "Cursor hidden" +highlight = { text = "Highlight", context = "Mask type that dims everything but the area." } +label = { text = "Label", context = "Text segment with no text yet." } +manual_zoom = "Manual Zoom" +muted = "Muted" +numbered_clip = { text = "Clip {number}", context = "{number} is the recording clip number, starting at 1." } +sensitive = { text = "Sensitive", context = "Mask type that blurs a private area." } + +[editor.timeline.track] +audio = "Audio" +captions = "Captions" +clip = { text = "Clip", context = "Track picker name for the video track." } +image = "Image" +keyboard = "Keyboard" +mask = "Mask" +scene = "Scene" +style = "Style" +text_track = "Text" +video = { text = "Video", context = "Timeline gutter label of the recording's video track." } +zoom = "Zoom" + +[editor.window] +audio_filter = { text = "Audio", context = "File type filter name in the audio file picker." } +audio_segment_changed = "This audio segment changed. Select it and choose a replacement again." +crop = "Crop" +crop_tooltip = "Crop Video" +no_room_for_3d_shot = "No room for another 3D shot" +redo = "Redo" +save_failed = { text = "Could not save {path}. Keep the editor open and try again: {error}", context = "{path} is the recording's folder; {error} is a technical message that stays in English." } +undo = "Undo" + +[editor.window.add_track] +button = "Add track" +hide_3d = "Hide the 3D track." +hint = "Choose a track to add to your timeline." + +[editor.window.aspect] +auto = "Auto" +classic = "Classic" +classic_ratio = "Classic ⋅4:3" +square = "Square" +square_ratio = "Square ⋅1:1" +tall = "Tall" +tall_ratio = "Tall ⋅3:4" +tooltip = "Aspect Ratio" +vertical = "Vertical" +vertical_ratio = "Vertical ⋅9:16" +wide = "Wide" +wide_ratio = "Wide ⋅16:9" + +[editor.window.audio_import] +copy_failed = "Failed to copy audio file: {error}" +directory_failed = "Failed to create audio directory: {error}" +no_extension = "Audio file has no extension" +not_found = "Audio file not found: {path}" +unsupported = { text = "Unsupported audio format: .{extension}", context = "{extension} is a file extension such as ogg." } + +[editor.window.busy] +clip_import = "Wait for the clip import to finish before closing or deleting this recording." +export = "Wait for the export or its cancellation to finish before closing or deleting this recording." +opening = "Wait for the recording to finish opening before deleting it." +save_before_delete_failed = "Could not save the recording before deleting it: {error}" + +[editor.window.camera3d_lane] +add_shot = "+ Add shot" +auto_scene = "Auto scene" +how_many = "How many shots?" +prompt = "Add cinematic 3D shots to your recording" + +[editor.window.clip_menu] +audio = { text = "AUDIO", context = "Section heading in the clip menu, written in capitals." } +cursor = { text = "CURSOR", context = "Section heading in the clip menu, written in capitals." } +delete = "Delete clip" +delete_only_clip = "Can't delete the only clip" +hide_cursor = "Hide cursor" +hide_cursor_description = "Fades out while this clip plays" +keep_pitch = "Keep pitch" +match_speed = "Match speed" +merge_next = "Merge with next clip" +merge_previous = "Merge with previous clip" +mute = "Mute" +mute_clip = "Mute clip" +mute_clip_description = "Silence this clip's audio" +rename = "Rename clip" +speed = { text = "SPEED", context = "Section heading in the clip menu, written in capitals." } +split = "Split at playhead" +split_hint = "Move the playhead into this clip" +volume = "Volume" + +[editor.window.frame] +add_tooltip = "Add a frame" +browser = { text = "Browser", context = "Frame style: browser chrome around the recording." } +browser_description = "Browser chrome with address bar" +button = { text = "Frame", context = "Toolbar button that opens the frame picker." } +dark = "Dark" +global = "Global frame · Applies wherever Style inherits background." +heading = { text = "Frame", context = "Heading of the frame picker popover." } +light = "Light" +macbook_description = "Laptop bezel around the recording" +macos_description = "Window chrome with traffic lights" +none = { text = "None", context = "Frame style: no frame." } +none_description = "Show the recording as-is" +style_changed = "This Style changed. Close and reopen Frame." +style_only = { text = "Style {index} only · Global settings stay unchanged.", context = "{index} is the number of a style segment on the timeline, starting at 1." } +style_only_override = { text = "Style {index} only · Editing enables its background override.", context = "{index} is the number of a style segment on the timeline, starting at 1." } +theme = "Theme" +title = "Title" +url = "URL" +window_title_placeholder = "Window title" +windows_description = "Title bar with window controls" + +[editor.window.header] +captions = "Captions" +delete_recording = "Delete recording" +export = "Export" +open_bundle = "Open recording bundle" +presets = "Presets" + +[editor.window.image] +changed = "This image changed while choosing a replacement. Select it and try again." +filter = { text = "Images", context = "File type filter name in the image file picker." } +recording_unavailable = "The recording is no longer available for this image. Please reopen the project." + +[editor.window.open] +damaged = "This recording's video tracks could not be opened. The bundle looks damaged." +failed = "This recording could not be opened" +media_failed = "Failed to read this recording's media: {error}" +meta_failed = "Failed to load recording meta: {error}" +no_segments = "Recording has no segments. It may need to be recovered first." +not_found = "Video path {path} not found!" +not_studio = "Cannot edit non-studio recordings" + +[editor.window.presets] +apply = "Apply" +create = "Create" +create_new = "Create new preset" +create_title = "Create Preset" +default = { text = "Default", context = "Name of the built-in preset." } +default_badge = { text = "Default", context = "Badge on the preset used for new recordings." } +delete = "Delete" +delete_confirm = { text = "Are you sure you want to delete \"{name}\"?", context = "{name} is the preset's name." } +delete_title = "Delete Preset" +name_placeholder = "Preset name" +none_saved = "No saved presets" +rename = "Rename" +rename_title = "Rename Preset" +save_settings = "Save settings to preset" +set_default = "Set as default" + +[editor.window.preview] +full = { text = "Full", context = "Preview quality option." } +half = { text = "Half", context = "Preview quality option." } +label = { text = "Preview", context = "Label in front of the preview quality picker." } +quarter = { text = "Quarter", context = "Preview quality option." } + +[editor.window.text_menu] +copy_style = "Copy style" +copy_style_first = "Copy a style first" +delete_segment = "Delete text segment" +delete_segments = "Delete text segments" +paste_style = "Paste style" +paste_style_all = "Paste style to all selected" +segments = { one = "{count} text segment", other = "{count} text segments", context = "Title of the text menu; only shown for two or more." } +untitled = { text = "Text", context = "Title of the text menu for a segment with no text." } + +[editor.window.title] +not_saved = "Title not saved" +not_saved_message = { text = "{error}\n\nYour title is still in the editor. Try saving it again.", context = "{error} is the reason, shown above." } +read_failed = "Could not read the recording title for {path}: {error}" +save_failed = "Could not save the recording title for {path}: {error}" + +[editor.window.track] +clear_all = "Clear all" +delete = "Delete" + +[editor.window.transport] +done_splitting = "Done splitting" +play_pause = "Play/Pause video" +split = "Split" +split_tooltip = "Click a segment to split. Esc to finish." +zoom_in = "Zoom in" +zoom_out = "Zoom out" + +[editor.window.zoom_lane] +generate = "Generate" +generating = "Generating…" +no_clicks = "No clicks found to zoom into. Drag across the lane to add one." +prompt = "Add zooms automatically" + +[ui] +coming_soon = "Coming Soon" + +[ui.selection] +audio = { one = "{count} audio segment selected", other = "{count} audio segments selected" } +caption = { one = "{count} caption segment selected", other = "{count} caption segments selected" } +delete = "Delete" +done = "Done" +image = { one = "{count} image segment selected", other = "{count} image segments selected" } +keyboard = { one = "{count} keyboard segment selected", other = "{count} keyboard segments selected" } +mask = { one = "{count} mask segment selected", other = "{count} mask segments selected" } +scene = { one = "{count} scene segment selected", other = "{count} scene segments selected" } +segments = { one = "{count} segment selected", other = "{count} segments selected" } +select_all = "Select all" +style = { one = "{count} style segment selected", other = "{count} style segments selected" } +text_segments = { one = "{count} text segment selected", other = "{count} text segments selected" } +three_d = { one = "{count} 3D segment selected", other = "{count} 3D segments selected" } +zoom = { one = "{count} selected", other = "{count} selected", context = "Zoom segments selected when the track has only one." } +zoom_all = { one = "All {count} selected", other = "All {count} selected", context = "Every zoom segment on the track is selected; only shown for two or more." } +zoom_some = { one = "{count} of {total} selected", other = "{count} of {total} selected", context = "Some of the zoom segments on the track are selected." } diff --git a/apps/desktop-gpui/locales/es.toml b/apps/desktop-gpui/locales/es.toml index 4fb4a66bb9c..ff6fa45abd3 100644 --- a/apps/desktop-gpui/locales/es.toml +++ b/apps/desktop-gpui/locales/es.toml @@ -257,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?" @@ -620,3 +1312,1566 @@ 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" + +# Editor sidebar, segment panels, colour correction, audio library and crop dialog. + +[editor.audio.library] +add_hint = "Añade audio, música u otros sonidos a tu vídeo" +add_title = "Añadir audio" +change_hint = "Elige otra pista para este segmento" +change_title = "Cambiar audio" +done = "Listo" +import = "Importar archivo" + +[editor.color] +apply_to_cursor = "Aplicar al cursor" +fine_tune = "Ajustar colores" +title = "Corrección de color" + +[editor.color.preset.cinematic] +description = "Sombras verde azuladas y luces anaranjadas con grano suave" +name = "Cinemático" + +[editor.color.preset.dreamy] +description = "Pasteles suaves y ligeros con negros levantados" +name = "Onírico" + +[editor.color.preset.frost] +description = "Tonos fríos y nítidos con color apagado" +name = "Escarcha" + +[editor.color.preset.golden] +description = "Brillo cálido de la hora dorada" +name = "Dorado" + +[editor.color.preset.midnight] +description = "Verde azulado oscuro y sombrío con color apagado" +name = "Medianoche" + +[editor.color.preset.noir] +description = "Blanco y negro de alto contraste con mucho grano" +name = "Noir" + +[editor.color.preset.none] +description = "Colores originales, sin corrección" +name = "Ninguno" + +[editor.color.preset.vintage] +description = "Aspecto de película cálida y desvaída con contraste suave" +name = "Vintage" + +[editor.color.preset.vivid] +description = "Saturación y contraste potenciados" +name = "Intenso" + +[editor.color.slider] +contrast = "Contraste" +exposure = "Exposición" +fade = "Desvanecido" +grain = "Grano" +saturation = "Saturación" +split_tone = "Virado dividido" +strength = "Intensidad" +temperature = "Temperatura" +tint = "Tinte" +vignette = "Viñeta" + +[editor.crop] +aspect_ratio = "Relación de aspecto" +center = "Centrar" +dimensions = "Tamaño" +editing_style = "Editando solo el estilo {number}" +editing_style_override = "Editando solo el estilo {number} · Al guardar se activa su fondo propio" +free = "Libre" +full = "Completo" +hint = "Arrastra para mover · Flechas para ajustar · Mantén {key} para no ajustar" +loading_frame = "Cargando fotograma…" +position = "Posición" +preview = "Vista previa" +rendering_preview = "Generando vista previa…" +snap_to_ratios = "Ajustar a proporciones" +style_changed = "Este estilo cambió mientras recortabas. Cancela y vuelve a abrir Recortar." +title = "Recortar" + +[editor.panel] +delete = "Eliminar" +done = "Listo" +duration = "Duración" +edge_in = "Entrada" +edge_out = "Salida" +enabled = "Activado" +fade_duration = "Duración del fundido" +flip_horizontally = "Voltear horizontalmente" +flip_vertically = "Voltear verticalmente" +name = "Nombre" +opacity = "Opacidad" + +[editor.panel.audio] +change = "Cambiar" +change_hint = "Toca para cambiar de pista" +fade_in = "Fundido de entrada" +fade_out = "Fundido de salida" +fallback_name = "Audio" +title = "Audio {number}" +volume = "Volumen" + +[editor.panel.camera3d] +already_still = "Ya es una toma fija" +amount = "Cantidad" +angles = "Ángulos" +auto_scene = "Escena automática" +auto_scene_tooltip = "Rehace todas las tomas 3D de la pista" +bokeh = "Bokeh" +camera = "Cámara" +delete_tooltip = "Eliminar esta toma" +depth_blur = "Desenfoque de profundidad" +distance = "Distancia" +drag_to_orbit = "Arrastra para orbitar" +ease_in = "Entrada" +ease_out = "Salida" +end_same_as_start = "Fin · igual al inicio" +fine_tune = "Ajuste fino" +focus = "Enfoque" +header = "Toma 3D · {look} · {duration} s" +hold_opening = "Mantener la pose inicial durante toda la toma" +lens = "Lente" +look_title = "Encuadre" +motion_style = "Estilo de movimiento" +moves = "Movimientos" +nothing_to_swap = "No hay nada que intercambiar en una toma fija" +play_shot = "Reproducir" +play_shot_tooltip = "Reproducir esta toma" +pose_end = "Fin · {time}" +pose_start = "Inicio · {time}" +reset_camera = "Restablecer cámara" +roll = "Giro" +rotate_x = "Rotar X" +rotate_y = "Rotar Y" +shift_x = "Mover X" +shift_y = "Mover Y" +shots = { one = "{count} toma", other = "{count} tomas" } +still_shot = "Toma fija" +swap = "Intercambiar inicio y fin" +timing_advanced = "Tiempo y avanzado" +timing_summary = "{easing} · Lente {fov}" + +[editor.panel.camera3d.blur] +angle = "Ángulo" +falloff = "Caída" +focus_size = "Área foco" +focus_x = "Foco X" +focus_y = "Foco Y" +position = "Posición" +scan = "Barrido" +strength = "Intensidad" + +[editor.panel.camera3d.easing] +ease_in = "Aceleración" +ease_out = "Desaceleración" +linear = "Lineal" +smooth = "Suave" + +[editor.panel.camera3d.focus_mode] +directional = "Direccional" +radial = "Radial" +tilt_shift = "Tilt-shift" + +[editor.panel.camera3d.look] +center = "Centro" +close_up = "Primer plano" +drift_down = "Descenso" +glide_across = "Deslizar" +low_angle = "Contrapicado" +perspective = "Perspectiva" +pull_back = "Alejar" +rising_sweep = "Barrido ascendente" +slide_by = "Pasar" +spotlight = "Foco" +tilt_away = "Inclinar" +top_down = "Cenital" +unfold = "Desplegar" + +[editor.panel.camera3d.sequence] +product_tour = "Recorrido del producto" +punch_in = "Acercar" +showcase = "Escaparate" + +[editor.panel.camera3d.shot] +custom = "Movimiento personalizado" +still = "Toma fija" + +[editor.panel.caption] +title = "Subtítulo {number}" + +[editor.panel.font] +system_mono = "Mono del sistema" +system_sans = "Sans del sistema" +system_serif = "Serif del sistema" + +[editor.panel.font_weight] +black = "Negra" +bold = "Negrita" +custom = "Personalizado ({weight})" +extra_bold = "Extranegrita" +light = "Ligera" +medium = "Media" +regular = "Normal" +semibold = "Seminegrita" + +[editor.panel.image] +arrange = "Colocar en el lienzo" +arrange_help = "Arrastra la imagen para moverla. Tira de una esquina para cambiar su tamaño o usa el control de rotación de abajo para girarla." +center = "Centrar en el lienzo" +lock_aspect = "Bloquear relación de aspecto" +missing = "Falta el archivo de imagen. Reemplázalo para recuperar este segmento conservando su tiempo y sus transformaciones." +replace = "Reemplazar imagen" +rotation = "Rotación" +rounding = "Redondeo" + +[editor.panel.keyboard] +title = "Teclado {number}" + +[editor.panel.mask] +blur = "Desenfoque" +effect = "Efecto" +highlight = "Resaltar" +outside_darkness = "Oscuridad exterior" +pixel_size = "Tamaño de píxel" +pixelate = "Pixelar" +sensitive = "Sensible" +title = "Máscara {number}" + +[editor.panel.scene] +camera_layout = "Diseño de la cámara" +camera_position = "Posición de cámara" +camera_zoom = "Zoom de cámara" +screen_position = "Posición de pantalla" +screen_zoom = "Zoom de pantalla" +transition = "Transición" + +[editor.panel.scene.description] +camera_only = "Muestra solo la cámara" +default = "Muestra la pantalla y la cámara" +floating = "La pantalla y la cámara flotan lado a lado como tarjetas redondeadas sobre el fondo" +hide_camera = "Muestra solo la grabación de pantalla" +split_screen = "Pantalla y cámara lado a lado (se apilan en vertical)" + +[editor.panel.scene.mode] +camera_only = "Solo cámara" +default = "Predeterminado" +floating = "Flotante" +hide_camera = "Ocultar cámara" +split_screen = "Pantalla dividida" + +[editor.panel.style] +camera_only_background = "Fondo de solo cámara" +camera_only_help = "Usa una escena de solo cámara. El margen muestra el fondo alrededor de la cámara." +camera_only_padding = "Margen de solo cámara" +crop_height = "Alto" +crop_left = "Izquierda" +crop_title = "Recorte de pantalla (píxeles de origen)" +crop_top = "Arriba" +crop_width = "Ancho" +edit_background = "Editar fondo y marco" +edit_camera = "Editar cámara" +edit_cursor = "Editar cursor" +help = "Los cambios solo se aplican durante este segmento. Activa un grupo para copiar sus ajustes generales." +reset_crop = "Restablecer recorte" + +[editor.panel.text_animation] +bounce = "Rebote" +fade = "Fundido" +letters = "Letras" +none = "Ninguna" +pop = "Pop" +slide_down = "Hacia abajo" +slide_left = "A la izquierda" +slide_right = "A la derecha" +slide_up = "Hacia arriba" +tracking = "Espaciado" +typewriter = "Máquina de escribir" +wipe = "Barrido" +words = "Palabras" +zoom = "Zoom" + +[editor.panel.text_background] +box_shape = "Caja" +highlight = "Resaltado" +none = "Ninguno" +pill = "Píldora" + +[editor.panel.text_layout] +fullscreen = "Pantalla completa" +overlay = "Superpuesto" + +[editor.panel.text_preset.group] +all = "Todos" +callouts = "Llamadas" +code = "Código" +lower_thirds = "Rótulos" +statements = "Mensajes" +titles = "Títulos" + +[editor.panel.text_preset.name] +caption = "Leyenda" +cinematic = "Cinemático" +code = "Código" +gradient = "Degradado" +headline = "Titular" +highlight = "Resaltado" +kicker = "Antetítulo" +label = "Etiqueta" +lower_third = "Rótulo" +name_tag = "Etiqueta" +neon = "Neón" +quote = "Cita" +stat = "Cifra grande" +sticker = "Pegatina" +title = "Título" +typewriter = "Máquina de escribir" + +[editor.panel.text_preset.sample] +caption = "Grabado con Cap" +cinematic = "Capítulo uno" +gradient = "Texto bonito" +headline = "Lanza más rápido" +highlight = "la parte importante" +kicker = "Novedad" +label = "Consejo" +name_tag = "Richie · Fundador" +neon = "Brilla" +quote = "«Que parezca fácil»" +sticker = "¡Bum!" +title = "Te presentamos Cap" +typewriter = "escribiendo…" + +[editor.panel.text_segment] +animation = "Animación" +background = "Fondo" +color = "Color" +font = "Fuente" +font_size = "Tamaño" +fullscreen_help = "Pausa el vídeo mientras se muestra el texto y luego continúa donde lo dejó." +glow = "Resplandor" +gradient = "Degradado" +layout = "Diseño" +letter_spacing = "Espaciado" +line_height = "Interlineado" +look = "Aspecto" +outline = "Contorno" +screen_transition = "Transición de pantalla" +shadow = "Sombra" +style = "Estilo" +title = "Texto {number}" + +[editor.panel.timing] +end = "Fin" +start = "Inicio" +title = "Tiempo" +to = "a" + +[editor.panel.zoom] +amount = "Nivel de zoom" +auto = "Auto" +auto_help = "El zoom automático sigue tu cursor por la pantalla." +auto_locked = "El modo automático necesita capturar el cursor. Activa «Captura del cursor por separado (Studio)» en Ajustes → General." +how_it_works = "¿Cómo funciona?" +manual = "Manual" +manual_help = "El zoom manual se queda en el punto fijo que elijas abajo." +mixed = "Mixto" +mode = "Modo de zoom" +positions_mixed = "Los segmentos hacen zoom en puntos distintos. Arrastra para moverlos todos al mismo." +title = "Zoom {number}" + +[editor.sidebar] +desktop_import_failed = "No se pudo importar tu fondo de escritorio" +importing_image = "Importando imagen…" + +[editor.sidebar.animated_gradient] +add_colour = "Añadir un color" +colours = "Colores" +delete = "Eliminar" +delete_failed = "No se pudo eliminar este degradado. Inténtalo de nuevo." +fine_tune = "Ajuste fino" +limit_reached = { one = "Puedes guardar hasta {count} degradado. Elimina uno para añadir otro.", other = "Puedes guardar hasta {count} degradados. Elimina uno para añadir otro." } +motion = "Movimiento" +name_placeholder = "Ponle nombre a este degradado" +name_required = "Escribe un nombre para este degradado." +presets = "Ajustes predefinidos" +randomize = "Aleatorio" +remember_failed = "No se pudieron recordar tus ajustes de degradado animado." +remove_colour = "Quitar color" +reset_tooltip = "Restablecer el ajuste fino" +save_failed = "No se pudo guardar este degradado. Inténtalo de nuevo." +save_tooltip = "Guardar el degradado actual" +saved = "Guardados" +still = "Quieto" + +[editor.sidebar.animated_gradient.control] +contrast = "Contraste" +curvature = "Curvatura" +detail = "Detalle" +direction = "Dirección" +exposure = "Exposición" +flow_scale = "Escala del flujo" +flow_strength = "Fuerza del flujo" +grain_amount = "Cantidad de grano" +grain_size = "Tamaño del grano" +highlights = "Luces" +motion_speed = "Velocidad del movimiento" +relief = "Relieve" +ripple_size = "Tamaño de las ondas" +shading = "Sombreado" +vibrance = "Intensidad" + +[editor.sidebar.animated_gradient.group] +animation = "Animación" +colour = "Color" +flow = "Flujo" +lighting = "Iluminación" +texture = "Textura" + +[editor.sidebar.background] +title = "Fondo" + +[editor.sidebar.border] +color = "Color del borde" +opacity = "Opacidad del borde" +title = "Borde" +width = "Grosor del borde" + +[editor.sidebar.corner] +rounded = "Redondeado" +squircle = "Squircle" +title = "Estilo de esquinas" + +[editor.sidebar.cursor] +circle = "Círculo" +click_ripple = "Onda al hacer clic" +color = "Color" +default = "Predeterminado" +description_circle = "Sustituye todas las formas del cursor por un círculo." +description_default = "Mantiene las formas y el aspecto del cursor de tu grabación." +description_macos = "Aspecto clásico de macOS. Los cursores personalizados mantienen su forma grabada." +description_tahoe = "Aspecto de macOS Tahoe. Los cursores personalizados mantienen su forma grabada." +description_windows = "Aspecto de Windows. Los cursores personalizados mantienen su forma grabada." +duration = "Duración" +recorded = "Cursores grabados" +recorded_tooltip = "Grabado con este cursor" +ripple_size = "Tamaño" +strength = "Intensidad" + +[editor.sidebar.desktop] +import = "Importar fondo de escritorio" +importing = "Importando..." +prompt = "Usa el fondo de tu escritorio" +reimport = "Volver a importar" + +[editor.sidebar.gradient] +angle = "Ángulo" +from = "Desde" +grain_scale = "Tamaño del grano" +noise = "Ruido" +to = "Hasta" + +[editor.sidebar.image] +prompt = "Haz clic para elegir o arrastra y suelta una imagen" + +[editor.sidebar.layout] +background_blur = "Desenfoque del fondo" +custom_position = "Posición de pantalla personalizada (arrastrada en el lienzo)" +motion_blur = "Desenfoque de movimiento" +padding = "Margen" +rounded_corners = "Esquinas redondeadas" +title = "Diseño" + +[editor.sidebar.notch] +description = "Dibuja una muesca de MacBook sobre la grabación. Las grabaciones hechas en un Mac con muesca usan sus propias medidas; si no, empieza con el tamaño de abajo y ajústalo." +height = "Alto de la muesca" +position = "Posición de la muesca" +title = "Muesca del MacBook" +width = "Ancho de la muesca" + +[editor.sidebar.shadow] +advanced = "Ajustes avanzados de sombra" +blur = "Desenfoque" +opacity = "Opacidad" +shadow_size = "Tamaño" +title = "Sombra" + +[editor.sidebar.source] +animated = "Animado" +color = "Color" +desktop = "Escritorio" +gradient = "Degradado" +image = "Imagen" +none = "Ninguno" +wallpaper = "Fondo" + +[editor.sidebar.style_group] +back = "Volver al estilo" +background = "Fondo y marco" +camera = "Cámara" +cursor = "Cursor" +editing_note = "Solo se edita este segmento. Los ajustes generales no cambian." +title = "Estilo {number} · {group}" + +[editor.sidebar.tab] +audio = "Audio" +background = "Fondo" +camera = "Cámara" +captions = "Subtítulos" +cursor = "Cursor" +keyboard = "Teclado" + +[editor.sidebar.wallpaper_theme] +blue = "Azul" +cities = "Ciudades" +dark = "Oscuro" +orange = "Naranja" +purple = "Morado" + +[editor.canvas] +camera = "Cámara" +camera_follows_zoom = "El tamaño de la cámara sigue al zoom; arrastra para moverla" +edit_zoom = "Editar zoom" +image = "Imagen" +mask = "Máscara" +screen = "Pantalla" +screen_locked = "La pantalla está bloqueada mientras haya un zoom activo" +text_item = "Texto" + +[editor.clips] +append_error = "No se pudo añadir el clip: {message}" +append_error_title = "Error de grabación" +back = "Volver al editor" +cap_filter = "Grabación de Cap" +clip = "Clip {number}" +empty_description = "Graba o importa un clip y aparecerá aquí." +empty_title = "Aún no hay clips" +import = "Importar" +import_error = "No se pudo importar el clip: {message}" +import_error_title = "Error de importación" +import_existing = "Grabación existente" +import_mp4 = "Vídeo MP4…" +importing = "Importando…" +mp4_filter = "Vídeo MP4" +prepare_controls_failed = "No se pudieron preparar los controles de grabación: {error}" +record_new = "Grabar un clip nuevo" +save_before_recording_failed = "No se pudo guardar el proyecto actual antes de grabar: {error}" +split = "Parte {number}" +title = "Clips" + +[editor.clips.error] +cancelled = "Importación cancelada" +cannot_decode = "No se puede decodificar el vídeo {path}: {error}" +in_use = "No se puede importar mientras esta grabación está en uso: {error}" +instant_source = "Las grabaciones Instant aún no se pueden importar a un proyecto" +instant_target = "Las grabaciones en modo Instant no se pueden editar" +into_itself = "No se puede importar una grabación en sí misma" +missing_video = "Falta el archivo de vídeo: {path}" +no_duration = "El vídeo importado no tiene una duración válida" +not_a_project = "Selecciona una carpeta de proyecto de Cap" +source_empty = "El proyecto de Cap de origen no tiene segmentos de grabación" +source_failed = "El proyecto de Cap de origen falló: {error}" +source_recording = "El proyecto de Cap de origen todavía se está grabando" +source_unimportable = "El proyecto de Cap de origen no tiene segmentos de grabación importables" +unfinished_target = "Termina o recupera esta grabación antes de importar un clip" +unsupported_video = "Archivo de vídeo no compatible: {path}" + +[editor.export] +back = "Volver al editor" +existing_link_description = "Volver a subir reemplaza el vídeo de este enlace con tu última edición. Todas las personas con el enlace verán la versión actualizada." +existing_link_title = "Actualiza tu enlace existente" +failed = "Error al exportar" +fps = "{fps} FPS" +frames = { one = "{rendered} / {count} fotograma", other = "{rendered} / {count} fotogramas" } +generating_preview = "Generando vista previa…" +link_unavailable_cursor = "Las exportaciones solo del cursor solo se pueden guardar en un archivo o en el portapapeles." +link_unavailable_transparent = "Las exportaciones transparentes solo se pueden guardar en un archivo o en el portapapeles." +organization = "Organización" +preview = "Vista previa" +preview_info = "Este es un fotograma renderizado de tu vídeo. Ajusta la configuración para ver la calidad de la exportación final." +preview_unavailable = "Vista previa no disponible" +reupload = "Volver a subir" +reupload_tooltip = "Sube tu última edición al mismo enlace" +reupload_transparent = "Los enlaces para compartir requieren un fondo sin transparencia" +sign_in_required = "Tienes que iniciar sesión para compartir grabaciones" +task_failed = "Falló la tarea de exportación" +title = "Exportar" +upgrade_message = "Esta función requiere un plan superior" +upgrade_title = "Se requiere una mejora" +upload_failed = "No se pudo subir la grabación" + +[editor.export.advanced] +bits_per_pixel = "Bits por píxel" +cursor_only = "Exportar solo el cursor" +cursor_only_description = "Mantiene el mismo movimiento y clics del cursor sobre un fondo transparente" +custom_bitrate = "Usando una tasa de bits personalizada" +force_ffmpeg = "Forzar el decodificador FFmpeg" +force_ffmpeg_description = "Omitir el decodificador por hardware (respaldo automático activado)" +title = "Avanzado" +transparent_mov = "Exporta como MOV transparente. Los archivos son grandes y sirven mejor para composición o edición." + +[editor.export.cta] +cancel_sign_in = "Cancelar inicio de sesión" +clipboard = "Exportar al portapapeles" +create_link = "Crear enlace para compartir" +file = "Exportar a archivo" +reupload = "Volver a subir al mismo enlace" +sign_in = "Inicia sesión para compartir" + +[editor.export.destination] +clipboard = "Portapapeles" +file = "Archivo" +link = "Enlace para compartir" + +[editor.export.done] +copy_link = "Copiar enlace" +instant_hint = "Usa el modo Instant en tu próxima grabación si quieres un enlace en cuanto te detengas." +link_copied = "Enlace copiado" +open_file = "Abrir archivo" +open_link = "Abrir enlace" +retry_copy = "Reintentar copia" +reuploaded = "Tu última edición está lista en el mismo enlace" +uploaded = "Tu Cap se subió correctamente" + +[editor.export.estimate] +gigabytes = "~{value} GB" +gigabytes_range = "~{lower}–{upper} GB" +hours = "~{value} h" +hours_range = "~{lower}–{upper} h" +megabytes = "~{value} MB" +megabytes_range = "~{lower}–{upper} MB" +minutes = "~{value} min" +minutes_range = "~{lower}–{upper} min" +seconds = "~{value} s" +seconds_range = "~{lower}–{upper} s" +under_megabyte = "< 1 MB" +under_second = "< 1 s" + +[editor.export.optimize] +description = "Vuelve a codificar por software para archivos mucho más pequeños (más lento)" +title = "Optimizar tamaño de archivo" + +[editor.export.phase] +clipboard_failed = "Exportación completada; no se pudo copiar al portapapeles" +complete = "Exportación completada" +copied = "Copiado al portapapeles" +copying_to_clipboard = "Copiando al portapapeles" +rendering_cursor = "Renderizando la pista del cursor" +rendering_gif = "Renderizando GIF" +rendering_video = "Renderizando vídeo" +reupload_complete = "Nueva subida completada" +reuploading = "Volviendo a subir a tu enlace" +saving_to_file = "Guardando en archivo" +starting = "Preparando la exportación" +upload_complete = "Subida completada" +uploading = "Creando enlace para compartir" + +[editor.export.quality] +larger = "Archivo más grande" +maximum = "Máxima" +potato = "Patata" +smaller = "Archivo más pequeño" +social = "Redes" +web = "Web" + +[editor.export.resources] +proceed = "Continuar de todos modos" +title = "Comprobar recursos de exportación" + +[editor.export.section] +destination = "Destino" +format = "Formato" +frame_rate = "Velocidad de fotogramas" +quality = "Calidad" +resolution = "Resolución" + +[editor.export.stats] +calculating = "Calculando…" +duration = "Duración" +estimated_size = "Tamaño estimado" +export_time = "Tiempo de exportación" +output = "Salida" +output_value = "{width}×{height} · {fps} fps" +refining_size = "Ajustando tamaño…" +refining_time = "Ajustando tiempo…" +unavailable = "No disponible" + +[editor.modal] +cancel_export = "Cancelar exportación" +cancel_export_message = "¿Seguro que quieres cancelar la exportación?" +cancel_export_title = "¿Cancelar exportación?" +keep_exporting = "Seguir exportando" +recording_retained = "Grabación conservada" + +[editor.tabs.audio] +isolation_balanced = "Equilibrado" +isolation_balanced_description = "Aislamiento más claro con detalle natural de la voz." +isolation_light = "Suave" +isolation_light_description = "Conserva más de tu voz original y del sonido de la sala." +isolation_strong = "Fuerte" +isolation_strong_description = "Más aislamiento para lugares ruidosos. Puede cambiar la textura de la voz." +mic_volume = "Volumen del micrófono" +mute = "Silenciar audio" +stereo_mode = "Modo estéreo del micrófono" +studio_sound_description = "Reduce el ruido de fondo y equilibra el nivel de tu voz." +studio_sound_save_failed = "No se pudo guardar el ajuste predeterminado de Studio Sound" +system_volume = "Volumen del audio del sistema" +title = "Controles de audio" +use_for_new = "Usar en grabaciones nuevas" + +[editor.tabs.audio.stereo] +mono_left = "Mono I" +mono_right = "Mono D" +stereo = "Estéreo" + +[editor.tabs.camera] +advanced_shadow = "Ajustes avanzados de sombra" +background = "Fondo" +corner_style = "Estilo de esquinas" +custom_position = "Posición personalizada (arrastrada en el lienzo)" +hide = "Ocultar cámara" +keep_size_during_zoom = "Mantener el tamaño original durante el zoom" +mirror = "Reflejar cámara" +position = "Posición" +reset = "Restablecer" +rounded_corners = "Esquinas redondeadas" +shadow = "Sombra" +shadow_blur = "Desenfoque" +shadow_opacity = "Opacidad" +shape_label = "Forma" +size_during_zoom = "Tamaño durante el zoom" +size_label = "Tamaño" +title = "Cámara" + +[editor.tabs.camera.blur] +heavy = "Desenfoque intenso" +light = "Desenfoque suave" +off = "Desactivado" +remove = "Quitar fondo" + +[editor.tabs.camera.shape] +source = "Original" +square = "Cuadrada" + +[editor.tabs.captions] +active_word_highlight = "Resaltar palabra activa" +animation_section = "Animación" +animation_style = "Estilo de animación" +background_color = "Color de fondo" +background_opacity = "Opacidad del fondo" +background_settings = "Ajustes de fondo" +beta = "Beta" +delete = "Eliminar" +deleting = "Eliminando..." +download = "Descargar modelo {model}" +download_note = "Descarga única en tu sistema. Todos los subtítulos se guardan localmente." +downloading = "Descargando {model}... {percent}%" +fade_duration = "Duración del fundido" +font_family = "Fuente" +font_settings = "Ajustes de fuente" +font_weight = "Grosor de fuente" +generate = "Generar subtítulos" +generate_failed = "No se pudieron generar los subtítulos: {reason}" +generating = "Generando..." +highlight_color = "Color de resaltado" +highlight_note = "Esta es la primera versión de los subtítulos en Cap. El resaltado de la palabra activa puede ser impreciso en algunas situaciones. Estamos trabajando en una solución que llegará en próximas versiones." +highlight_style = "Estilo de resaltado" +horizontal_position = "Posición horizontal" +intel_unavailable = "Los modelos de subtítulos Parakeet no están disponibles en Mac con Intel. Los modelos Whisper siguen disponibles." +keep_open = "Mantén Cap abierto mientras se descarga el modelo. Si el editor se recarga, se volverá a conectar automáticamente." +language_label = "Idioma" +model_downloaded = "Modelo {model} descargado" +model_label = "Modelo" +position = "Posición" +regenerate = "Regenerar subtítulos" +size_label = "Tamaño" +style = "Estilo" +text_color = "Color del texto" +title = "Subtítulos" +uppercase = "Mayúsculas" +vertical_position = "Posición vertical" + +[editor.tabs.captions.animation] +bounce = "Rebote" +none = "Ninguna" +pop = "Pop" + +[editor.tabs.captions.highlight] +color = "Color" +pill = "Píldora" + +[editor.tabs.captions.language] +ar = "Árabe" +auto = "Detección automática" +bn = "Bengalí" +de = "Alemán" +en = "Inglés" +es = "Español" +fa = "Persa" +fr = "Francés" +gu = "Guyaratí" +he = "Hebreo" +hi = "Hindi" +it = "Italiano" +ja = "Japonés" +ko = "Coreano" +mr = "Maratí" +nl = "Neerlandés" +pa = "Panyabí" +pl = "Polaco" +pt = "Portugués" +ru = "Ruso" +sk = "Eslovaco" +ta = "Tamil" +te = "Telugu" +tr = "Turco" +ur = "Urdu" +zh = "Chino" + +[editor.tabs.captions.model] +high_accuracy = "Alta precisión" +high_accuracy_description = "Descarga más grande, mayor precisión" +medium = "Mediano" +medium_description = "Más lento, más preciso" +recommended = "Recomendado" +recommended_description = "El mejor equilibrio para la mayoría de las grabaciones" +small = "Pequeño" +small_description = "La descarga más pequeña" + +[editor.tabs.captions.preset] +classic = "Clásico" +classic_description = "Texto limpio sobre un fondo sólido redondeado." +highlight = "Resaltado" +highlight_description = "Mayúsculas en negrita con una píldora detrás de la palabra activa." +karaoke = "Karaoke" +karaoke_description = "Las palabras se iluminan al ritmo de la voz." +minimal = "Mínimo" +minimal_description = "Texto sutil con contorno y sin fondo." +pop = "Pop" +pop_description = "Mayúsculas divertidas que aparecen con un acento vibrante." + +[editor.tabs.corner] +rounded = "Redondeadas" +squircle = "Squircle" + +[editor.tabs.cursor] +friction = "Fricción" +hide_when_idle = "Ocultar en reposo" +inactivity_delay = "Retraso de inactividad" +mass = "Masa" +movement_style = "Estilo de movimiento del cursor" +show = "Mostrar cursor" +size_label = "Tamaño" +smooth_movement = "Movimiento suave" +style_label = "Estilo del cursor" +svg_cursors = "Cursores SVG de alta calidad" +tension = "Tensión" +tilt = "Inclinación" + +[editor.tabs.cursor.style] +fast = "Rápido" +fast_description = "Ágil, seguimiento cercano" +mellow = "Tranquilo" +mellow_description = "Movimiento equilibrado y natural" +slow = "Lento" +slow_description = "Movimiento largo y deslizante" +smooth = "Suave" +smooth_description = "Movimiento suave y flotante" + +[editor.tabs.keyboard] +animation = "Animación" +background_color = "Color de fondo" +background_opacity = "Opacidad del fondo" +background_settings = "Ajustes de fondo" +behavior = "Comportamiento" +beta = "Beta" +changed_during_generation = "La línea de tiempo o los ajustes del teclado cambiaron durante la generación. Inténtalo de nuevo." +empty_description = "Haz clic en «Generar segmentos de teclado» para crear segmentos a partir de las pulsaciones grabadas." +empty_title = "Aún no hay segmentos de teclado." +fade_duration = "Duración del fundido" +font_family = "Fuente" +font_settings = "Ajustes de fuente" +font_weight = "Grosor de fuente" +generate = "Generar segmentos de teclado" +generating = "Generando segmentos de teclado..." +grouping_threshold = "Umbral de agrupación" +linger_duration = "Tiempo en pantalla" +load_failed = "No se pudieron cargar los datos de la grabación: {error}" +no_presses = "No se encontraron pulsaciones de teclado grabadas." +position = "Posición" +regenerate = "Regenerar segmentos de teclado" +show_modifiers = "Mostrar modificadores" +show_special_keys = "Mostrar teclas especiales" +size_label = "Tamaño" +text_color = "Color del texto" +timeline_unavailable = "La línea de tiempo del proyecto no está disponible." +timeline_unreadable = "No se pudo leer la línea de tiempo del proyecto." +title = "Mostrar teclado" +uppercase = "Mayúsculas" + +[editor.tabs.position] +bottom_center = "Abajo al centro" +bottom_left = "Abajo a la izquierda" +bottom_right = "Abajo a la derecha" +manual = "Manual" +top_center = "Arriba al centro" +top_left = "Arriba a la izquierda" +top_right = "Arriba a la derecha" + +[editor.tabs.sync] +auto_description = "Cap calculó estos desfases automáticamente para mantener el audio sincronizado con el vídeo. Ajústalos si algo aún no suena bien." +auto_synced = "Sincronizado automáticamente" +camera = "Desfase de la cámara" +clip = "Clip {number}" +description = "Ajusta los desfases de las fuentes si el audio o la cámara se desincronizan de la grabación de pantalla." +microphone = "Desfase del micrófono" +system_audio = "Desfase del audio del sistema" +title = "Sincronización" + +[editor.tabs.weight] +bold = "Negrita" +medium = "Media" +normal = "Normal" + +[editor.timeline] +add_shot = "Añadir plano" +add_shot_with_look = "Añadir plano · {look} · {seconds} s" +paused = "En pausa" + +[editor.timeline.empty] +add_audio = "Añadir audio" +add_scene = "Añadir escena" +audio = "Añade música de fondo o importa tu propio audio" +captions = "Sin subtítulos" +keyboard = "Sin eventos de teclado" +scene = "Cambia de diseño entre tu pantalla y tu cámara" + +[editor.timeline.picker] +audio = "Añade música de fondo o importa tu propio audio." +caption = "Transcribe automáticamente tu grabación en subtítulos en pantalla." +clip = "Las imágenes grabadas de tu pantalla." +image = "Añade una imagen a tu grabación." +keyboard = "Muestra en pantalla las teclas que pulsas mientras escribes." +mask = "Desenfoca u oculta en negro las zonas privadas de la pantalla." +scene = "Cambia de diseño entre tu pantalla y tu cámara." +scene_unavailable = "Graba con una cámara para usar escenas." +style = "Cambia el fondo, la cámara y el cursor a lo largo del tiempo." +text_overlay = "Añade textos y títulos personalizados sobre el lienzo." +three_d = "Inclina la escena en perspectiva 3D." +zoom = "Zooms suaves que siguen la acción." + +[editor.timeline.remove] +audio = "Quitar la pista Audio." +captions = "Quitar la pista Subtítulos." +clip = "Quitar la pista Clip." +image = "Quitar la pista Imagen." +keyboard = "Quitar la pista Teclado." +mask = "Quitar la pista Máscara." +scene = "Quitar la pista Escena." +style = "Quitar la pista Estilo." +text_track = "Quitar la pista Texto." +three_d = "Quitar la pista 3D." +zoom = "Quitar la pista Zoom." + +[editor.timeline.scene] +camera_only = "Solo cámara" +default = "Predeterminada" +floating = "Flotante" +hide_camera = "Ocultar cámara" +split_screen = "Pantalla dividida" + +[editor.timeline.segment] +audio = "Audio" +automatic_zoom = "Zoom automático" +caption = "Subtítulo" +clip = "Clip" +cursor_hidden = "Cursor oculto" +highlight = "Destacar" +label = "Etiqueta" +manual_zoom = "Zoom manual" +muted = "Silenciado" +numbered_clip = "Clip {number}" +sensitive = "Sensible" + +[editor.timeline.track] +audio = "Audio" +captions = "Subtítulos" +clip = "Clip" +image = "Imagen" +keyboard = "Teclado" +mask = "Máscara" +scene = "Escena" +style = "Estilo" +text_track = "Texto" +video = "Vídeo" +zoom = "Zoom" + +[editor.window] +audio_filter = "Audio" +audio_segment_changed = "Este segmento de audio cambió. Selecciónalo y vuelve a elegir un reemplazo." +crop = "Recortar" +crop_tooltip = "Recortar vídeo" +no_room_for_3d_shot = "No hay espacio para otro plano 3D" +redo = "Rehacer" +save_failed = "No se pudo guardar {path}. Mantén el editor abierto e inténtalo de nuevo: {error}" +undo = "Deshacer" + +[editor.window.add_track] +button = "Añadir pista" +hide_3d = "Ocultar la pista 3D." +hint = "Elige una pista para añadir a tu línea de tiempo." + +[editor.window.aspect] +auto = "Auto" +classic = "Clásico" +classic_ratio = "Clásico ⋅4:3" +square = "Cuadrado" +square_ratio = "Cuadrado ⋅1:1" +tall = "Alto" +tall_ratio = "Alto ⋅3:4" +tooltip = "Relación de aspecto" +vertical = "Vertical" +vertical_ratio = "Vertical ⋅9:16" +wide = "Panorámico" +wide_ratio = "Panorámico ⋅16:9" + +[editor.window.audio_import] +copy_failed = "No se pudo copiar el archivo de audio: {error}" +directory_failed = "No se pudo crear la carpeta de audio: {error}" +no_extension = "El archivo de audio no tiene extensión" +not_found = "No se encontró el archivo de audio: {path}" +unsupported = "Formato de audio no compatible: .{extension}" + +[editor.window.busy] +clip_import = "Espera a que termine la importación del clip antes de cerrar o eliminar esta grabación." +export = "Espera a que termine la exportación o su cancelación antes de cerrar o eliminar esta grabación." +opening = "Espera a que la grabación termine de abrirse antes de eliminarla." +save_before_delete_failed = "No se pudo guardar la grabación antes de eliminarla: {error}" + +[editor.window.camera3d_lane] +add_shot = "+ Añadir plano" +auto_scene = "Escena automática" +how_many = "¿Cuántos planos?" +prompt = "Añade planos 3D cinematográficos a tu grabación" + +[editor.window.clip_menu] +audio = "AUDIO" +cursor = "CURSOR" +delete = "Eliminar clip" +delete_only_clip = "No se puede eliminar el único clip" +hide_cursor = "Ocultar cursor" +hide_cursor_description = "Se desvanece mientras se reproduce este clip" +keep_pitch = "Mantener tono" +match_speed = "Ajustar a la velocidad" +merge_next = "Unir con el clip siguiente" +merge_previous = "Unir con el clip anterior" +mute = "Silenciar" +mute_clip = "Silenciar clip" +mute_clip_description = "Silencia el audio de este clip" +rename = "Renombrar clip" +speed = "VELOCIDAD" +split = "Dividir en el cabezal" +split_hint = "Mueve el cabezal dentro de este clip" +volume = "Volumen" + +[editor.window.frame] +add_tooltip = "Añadir un marco" +browser = "Navegador" +browser_description = "Ventana de navegador con barra de direcciones" +button = "Marco" +dark = "Oscuro" +global = "Marco global · Se aplica donde el estilo hereda el fondo." +heading = "Marco" +light = "Claro" +macbook_description = "Bisel de portátil alrededor de la grabación" +macos_description = "Ventana con botones de semáforo" +none = "Ninguno" +none_description = "Mostrar la grabación tal cual" +style_changed = "Este estilo cambió. Cierra y vuelve a abrir el marco." +style_only = "Solo el estilo {index} · Los ajustes globales no cambian." +style_only_override = "Solo el estilo {index} · Al editar se activa su fondo propio." +theme = "Tema" +title = "Título" +url = "URL" +window_title_placeholder = "Título de la ventana" +windows_description = "Barra de título con controles de ventana" + +[editor.window.header] +captions = "Subtítulos" +delete_recording = "Eliminar grabación" +export = "Exportar" +open_bundle = "Abrir paquete de la grabación" +presets = "Preajustes" + +[editor.window.image] +changed = "Esta imagen cambió mientras elegías un reemplazo. Selecciónala e inténtalo de nuevo." +filter = "Imágenes" +recording_unavailable = "La grabación ya no está disponible para esta imagen. Vuelve a abrir el proyecto." + +[editor.window.open] +damaged = "No se pudieron abrir las pistas de vídeo de esta grabación. El paquete parece dañado." +failed = "No se pudo abrir esta grabación" +media_failed = "No se pudieron leer los archivos de esta grabación: {error}" +meta_failed = "No se pudieron cargar los metadatos de la grabación: {error}" +no_segments = "La grabación no tiene segmentos. Puede que primero haya que recuperarla." +not_found = "¡No se encontró la ruta de vídeo {path}!" +not_studio = "Solo se pueden editar grabaciones de Studio" + +[editor.window.presets] +apply = "Aplicar" +create = "Crear" +create_new = "Crear preajuste nuevo" +create_title = "Crear preajuste" +default = "Predeterminado" +default_badge = "Predeterminado" +delete = "Eliminar" +delete_confirm = "¿Seguro que quieres eliminar «{name}»?" +delete_title = "Eliminar preajuste" +name_placeholder = "Nombre del preajuste" +none_saved = "No hay preajustes guardados" +rename = "Renombrar" +rename_title = "Renombrar preajuste" +save_settings = "Guardar ajustes en el preajuste" +set_default = "Establecer como predeterminado" + +[editor.window.preview] +full = "Completa" +half = "Mitad" +label = "Vista previa" +quarter = "Cuarto" + +[editor.window.text_menu] +copy_style = "Copiar estilo" +copy_style_first = "Copia primero un estilo" +delete_segment = "Eliminar segmento de texto" +delete_segments = "Eliminar segmentos de texto" +paste_style = "Pegar estilo" +paste_style_all = "Pegar estilo en toda la selección" +segments = { one = "{count} segmento de texto", other = "{count} segmentos de texto" } +untitled = "Texto" + +[editor.window.title] +not_saved = "Título no guardado" +not_saved_message = "{error}\n\nTu título sigue en el editor. Intenta guardarlo de nuevo." +read_failed = "No se pudo leer el título de la grabación de {path}: {error}" +save_failed = "No se pudo guardar el título de la grabación de {path}: {error}" + +[editor.window.track] +clear_all = "Borrar todo" +delete = "Eliminar" + +[editor.window.transport] +done_splitting = "Terminar de dividir" +play_pause = "Reproducir/pausar vídeo" +split = "Dividir" +split_tooltip = "Haz clic en un segmento para dividirlo. Esc para terminar." +zoom_in = "Acercar" +zoom_out = "Alejar" + +[editor.window.zoom_lane] +generate = "Generar" +generating = "Generando…" +no_clicks = "No se encontraron clics para hacer zoom. Arrastra sobre la pista para añadir uno." +prompt = "Añadir zooms automáticamente" + +[ui] +coming_soon = "Próximamente" + +[ui.selection] +audio = { one = "{count} segmento de audio seleccionado", other = "{count} segmentos de audio seleccionados" } +caption = { one = "{count} segmento de subtítulos seleccionado", other = "{count} segmentos de subtítulos seleccionados" } +delete = "Eliminar" +done = "Listo" +image = { one = "{count} segmento de imagen seleccionado", other = "{count} segmentos de imagen seleccionados" } +keyboard = { one = "{count} segmento de teclado seleccionado", other = "{count} segmentos de teclado seleccionados" } +mask = { one = "{count} segmento de máscara seleccionado", other = "{count} segmentos de máscara seleccionados" } +scene = { one = "{count} segmento de escena seleccionado", other = "{count} segmentos de escena seleccionados" } +segments = { one = "{count} segmento seleccionado", other = "{count} segmentos seleccionados" } +select_all = "Seleccionar todo" +style = { one = "{count} segmento de estilo seleccionado", other = "{count} segmentos de estilo seleccionados" } +text_segments = { one = "{count} segmento de texto seleccionado", other = "{count} segmentos de texto seleccionados" } +three_d = { one = "{count} segmento 3D seleccionado", other = "{count} segmentos 3D seleccionados" } +zoom = { one = "{count} seleccionado", other = "{count} seleccionados" } +zoom_all = { one = "{count} seleccionado", other = "Los {count} seleccionados" } +zoom_some = { one = "{count} de {total} seleccionado", other = "{count} de {total} seleccionados" } diff --git a/apps/desktop-gpui/locales/fr.toml b/apps/desktop-gpui/locales/fr.toml index a40ca2c63a3..032b8eb34c9 100644 --- a/apps/desktop-gpui/locales/fr.toml +++ b/apps/desktop-gpui/locales/fr.toml @@ -257,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 ?" @@ -620,3 +1312,1566 @@ 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" + +# Editor sidebar, segment panels, colour correction, audio library and crop dialog. + +[editor.audio.library] +add_hint = "Ajoutez de l'audio, de la musique ou d'autres sons à votre vidéo" +add_title = "Ajouter de l'audio" +change_hint = "Choisissez une autre piste pour ce segment" +change_title = "Changer l'audio" +done = "Terminé" +import = "Importer un fichier" + +[editor.color] +apply_to_cursor = "Appliquer au curseur" +fine_tune = "Ajuster les couleurs" +title = "Correction colorimétrique" + +[editor.color.preset.cinematic] +description = "Ombres bleu canard et hautes lumières orangées avec un léger grain" +name = "Cinéma" + +[editor.color.preset.dreamy] +description = "Pastels doux et aériens aux noirs relevés" +name = "Onirique" + +[editor.color.preset.frost] +description = "Tons froids et nets aux couleurs atténuées" +name = "Givre" + +[editor.color.preset.golden] +description = "Lueur chaude de l'heure dorée" +name = "Doré" + +[editor.color.preset.midnight] +description = "Bleu canard sombre et intense aux couleurs sourdes" +name = "Minuit" + +[editor.color.preset.noir] +description = "Noir et blanc très contrasté avec un grain prononcé" +name = "Noir" + +[editor.color.preset.none] +description = "Couleurs d'origine, sans étalonnage" +name = "Aucun" + +[editor.color.preset.vintage] +description = "Aspect pellicule chaude et délavée au contraste doux" +name = "Vintage" + +[editor.color.preset.vivid] +description = "Saturation et contraste renforcés" +name = "Éclatant" + +[editor.color.slider] +contrast = "Contraste" +exposure = "Exposition" +fade = "Délavé" +grain = "Grain" +saturation = "Saturation" +split_tone = "Virage partiel" +strength = "Intensité" +temperature = "Température" +tint = "Teinte" +vignette = "Vignettage" + +[editor.crop] +aspect_ratio = "Proportions" +center = "Centrer" +dimensions = "Taille" +editing_style = "Modification du style {number} uniquement" +editing_style_override = "Modification du style {number} uniquement · L'enregistrement active son arrière-plan personnalisé" +free = "Libre" +full = "Entier" +hint = "Glisser pour déplacer · Flèches pour ajuster · Maintenez {key} pour ne pas aligner" +loading_frame = "Chargement de l'image…" +position = "Position" +preview = "Aperçu" +rendering_preview = "Rendu de l'aperçu…" +snap_to_ratios = "Aligner sur les ratios" +style_changed = "Ce style a changé pendant le recadrage. Annulez puis rouvrez Recadrer." +title = "Recadrer" + +[editor.panel] +delete = "Supprimer" +done = "Terminé" +duration = "Durée" +edge_in = "Entrée" +edge_out = "Sortie" +enabled = "Activé" +fade_duration = "Durée du fondu" +flip_horizontally = "Retourner horizontalement" +flip_vertically = "Retourner verticalement" +name = "Nom" +opacity = "Opacité" + +[editor.panel.audio] +change = "Changer" +change_hint = "Touchez pour changer de piste" +fade_in = "Fondu d'entrée" +fade_out = "Fondu de sortie" +fallback_name = "Audio" +title = "Audio {number}" +volume = "Volume" + +[editor.panel.camera3d] +already_still = "C'est déjà un plan fixe" +amount = "Intensité" +angles = "Angles" +auto_scene = "Scène auto" +auto_scene_tooltip = "Recrée tous les plans 3D de la piste" +bokeh = "Bokeh" +camera = "Caméra" +delete_tooltip = "Supprimer ce plan" +depth_blur = "Flou de profondeur" +distance = "Distance" +drag_to_orbit = "Glisser pour orbiter" +ease_in = "Entrée" +ease_out = "Sortie" +end_same_as_start = "Fin · identique au début" +fine_tune = "Ajuster" +focus = "Focus" +header = "Plan 3D · {look} · {duration} s" +hold_opening = "Garder la pose de départ pendant tout le plan" +lens = "Objectif" +look_title = "Cadrage" +motion_style = "Style de mouvement" +moves = "Mouvements" +nothing_to_swap = "Rien à inverser sur un plan fixe" +play_shot = "Lire" +play_shot_tooltip = "Lire ce plan" +pose_end = "Fin · {time}" +pose_start = "Début · {time}" +reset_camera = "Réinitialiser la caméra" +roll = "Roulis" +rotate_x = "Rotation X" +rotate_y = "Rotation Y" +shift_x = "Pan X" +shift_y = "Pan Y" +shots = { one = "{count} plan", other = "{count} plans" } +still_shot = "Plan fixe" +swap = "Inverser le début et la fin" +timing_advanced = "Minutage et avancé" +timing_summary = "{easing} · Objectif {fov}" + +[editor.panel.camera3d.blur] +angle = "Angle" +falloff = "Fondu" +focus_size = "Netteté" +focus_x = "Point X" +focus_y = "Point Y" +position = "Position" +scan = "Balayage" +strength = "Intensité" + +[editor.panel.camera3d.easing] +ease_in = "Accélération" +ease_out = "Décélération" +linear = "Linéaire" +smooth = "Doux" + +[editor.panel.camera3d.focus_mode] +directional = "Directionnel" +radial = "Radial" +tilt_shift = "Tilt-shift" + +[editor.panel.camera3d.look] +center = "Centre" +close_up = "Gros plan" +drift_down = "Descente" +glide_across = "Glissé latéral" +low_angle = "Contre-plongée" +perspective = "Perspective" +pull_back = "Recul" +rising_sweep = "Envolée" +slide_by = "Passage" +spotlight = "Projecteur" +tilt_away = "Bascule" +top_down = "Plongée" +unfold = "Dépli" + +[editor.panel.camera3d.sequence] +product_tour = "Visite produit" +punch_in = "Zoom avant" +showcase = "Vitrine" + +[editor.panel.camera3d.shot] +custom = "Mouvement personnalisé" +still = "Plan fixe" + +[editor.panel.caption] +title = "Sous-titre {number}" + +[editor.panel.font] +system_mono = "Mono système" +system_sans = "Sans système" +system_serif = "Serif système" + +[editor.panel.font_weight] +black = "Noir" +bold = "Gras" +custom = "Personnalisé ({weight})" +extra_bold = "Extra-gras" +light = "Léger" +medium = "Moyen" +regular = "Normal" +semibold = "Demi-gras" + +[editor.panel.image] +arrange = "Placer sur le canevas" +arrange_help = "Faites glisser l'image pour la déplacer. Tirez un coin pour la redimensionner, ou utilisez la rotation ci-dessous pour la faire pivoter." +center = "Centrer sur le canevas" +lock_aspect = "Verrouiller les proportions" +missing = "Le fichier image est introuvable. Remplacez-le pour restaurer ce segment en conservant son minutage et ses transformations." +replace = "Remplacer l'image" +rotation = "Rotation" +rounding = "Arrondi" + +[editor.panel.keyboard] +title = "Clavier {number}" + +[editor.panel.mask] +blur = "Flou" +effect = "Effet" +highlight = "Mise en avant" +outside_darkness = "Assombrissement extérieur" +pixel_size = "Taille des pixels" +pixelate = "Pixeliser" +sensitive = "Sensible" +title = "Masque {number}" + +[editor.panel.scene] +camera_layout = "Disposition de la caméra" +camera_position = "Position de la caméra" +camera_zoom = "Zoom de la caméra" +screen_position = "Position de l'écran" +screen_zoom = "Zoom de l'écran" +transition = "Transition" + +[editor.panel.scene.description] +camera_only = "Affiche uniquement la caméra" +default = "Affiche l'écran et la caméra" +floating = "L'écran et la caméra flottent côte à côte en cartes arrondies au-dessus de l'arrière-plan" +hide_camera = "Affiche uniquement l'enregistrement d'écran" +split_screen = "Écran et caméra côte à côte (empilés automatiquement en portrait)" + +[editor.panel.scene.mode] +camera_only = "Caméra seule" +default = "Par défaut" +floating = "Flottant" +hide_camera = "Masquer la caméra" +split_screen = "Écran partagé" + +[editor.panel.style] +camera_only_background = "Arrière-plan caméra seule" +camera_only_help = "Utilisez une scène Caméra seule. La marge laisse voir l'arrière-plan autour de la caméra." +camera_only_padding = "Marge caméra seule" +crop_height = "Hauteur" +crop_left = "Gauche" +crop_title = "Recadrage de l'écran (pixels source)" +crop_top = "Haut" +crop_width = "Largeur" +edit_background = "Modifier l'arrière-plan et le cadre" +edit_camera = "Modifier la caméra" +edit_cursor = "Modifier le curseur" +help = "Les remplacements ne s'appliquent que pendant ce segment. Activez un groupe pour copier ses paramètres globaux." +reset_crop = "Réinitialiser le recadrage" + +[editor.panel.text_animation] +bounce = "Rebond" +fade = "Fondu" +letters = "Lettres" +none = "Aucune" +pop = "Pop" +slide_down = "Vers le bas" +slide_left = "Vers la gauche" +slide_right = "Vers la droite" +slide_up = "Vers le haut" +tracking = "Interlettrage" +typewriter = "Machine à écrire" +wipe = "Balayage" +words = "Mots" +zoom = "Zoom" + +[editor.panel.text_background] +box_shape = "Boîte" +highlight = "Surligné" +none = "Aucun" +pill = "Pilule" + +[editor.panel.text_layout] +fullscreen = "Plein écran" +overlay = "Superposé" + +[editor.panel.text_preset.group] +all = "Tous" +callouts = "Accroches" +code = "Code" +lower_thirds = "Bandeaux" +statements = "Messages" +titles = "Titres" + +[editor.panel.text_preset.name] +caption = "Légende" +cinematic = "Cinéma" +code = "Code" +gradient = "Dégradé" +headline = "Gros titre" +highlight = "Surligné" +kicker = "Surtitre" +label = "Étiquette" +lower_third = "Bandeau" +name_tag = "Badge nom" +neon = "Néon" +quote = "Citation" +stat = "Gros chiffre" +sticker = "Sticker" +title = "Titre" +typewriter = "Machine à écrire" + +[editor.panel.text_preset.sample] +caption = "Enregistré avec Cap" +cinematic = "Chapitre un" +gradient = "Un beau texte" +headline = "Livrez plus vite" +highlight = "le point important" +kicker = "Nouveauté" +label = "Astuce" +name_tag = "Richie · Fondateur" +neon = "Éclat" +quote = "« Que tout paraisse facile »" +sticker = "Boum !" +title = "Voici Cap" +typewriter = "en train d'écrire…" + +[editor.panel.text_segment] +animation = "Animation" +background = "Arrière-plan" +color = "Couleur" +font = "Police" +font_size = "Taille" +fullscreen_help = "Met la vidéo en pause pendant l'affichage du texte, puis reprend là où elle s'était arrêtée." +glow = "Lueur" +gradient = "Dégradé" +layout = "Disposition" +letter_spacing = "Espacement" +line_height = "Interligne" +look = "Apparence" +outline = "Contour" +screen_transition = "Transition d'écran" +shadow = "Ombre" +style = "Style" +title = "Texte {number}" + +[editor.panel.timing] +end = "Fin" +start = "Début" +title = "Minutage" +to = "à" + +[editor.panel.zoom] +amount = "Niveau de zoom" +auto = "Auto" +auto_help = "Le zoom automatique suit votre curseur à l'écran." +auto_locked = "Le mode auto nécessite la capture du curseur. Activez « Capture séparée du curseur (Studio) » dans Paramètres → Général." +how_it_works = "Comment ça marche ?" +manual = "Manuel" +manual_help = "Le zoom manuel reste sur le point fixe que vous choisissez ci-dessous." +mixed = "Variable" +mode = "Mode de zoom" +positions_mixed = "Les segments zooment sur des points différents. Faites glisser pour tous les placer au même endroit." +title = "Zoom {number}" + +[editor.sidebar] +desktop_import_failed = "Impossible d'importer votre fond d'écran" +importing_image = "Importation de l'image…" + +[editor.sidebar.animated_gradient] +add_colour = "Ajouter une couleur" +colours = "Couleurs" +delete = "Supprimer" +delete_failed = "Impossible de supprimer ce dégradé. Réessayez." +fine_tune = "Ajuster" +limit_reached = { one = "Vous pouvez enregistrer jusqu'à {count} dégradé. Supprimez-en un pour en ajouter un autre.", other = "Vous pouvez enregistrer jusqu'à {count} dégradés. Supprimez-en un pour en ajouter un autre." } +motion = "Mouvement" +name_placeholder = "Nommez ce dégradé" +name_required = "Saisissez un nom pour ce dégradé." +presets = "Préréglages" +randomize = "Aléatoire" +remember_failed = "Impossible de mémoriser vos réglages de dégradé animé." +remove_colour = "Retirer la couleur" +reset_tooltip = "Réinitialiser les ajustements" +save_failed = "Impossible d'enregistrer ce dégradé. Réessayez." +save_tooltip = "Enregistrer le dégradé actuel" +saved = "Enregistrés" +still = "Immobile" + +[editor.sidebar.animated_gradient.control] +contrast = "Contraste" +curvature = "Courbure" +detail = "Détail" +direction = "Direction" +exposure = "Exposition" +flow_scale = "Échelle du flux" +flow_strength = "Force du flux" +grain_amount = "Quantité de grain" +grain_size = "Taille du grain" +highlights = "Hautes lumières" +motion_speed = "Vitesse du mouvement" +relief = "Relief" +ripple_size = "Taille des ondulations" +shading = "Ombrage" +vibrance = "Vibrance" + +[editor.sidebar.animated_gradient.group] +animation = "Animation" +colour = "Couleur" +flow = "Flux" +lighting = "Éclairage" +texture = "Texture" + +[editor.sidebar.background] +title = "Arrière-plan" + +[editor.sidebar.border] +color = "Couleur de la bordure" +opacity = "Opacité de la bordure" +title = "Bordure" +width = "Épaisseur de la bordure" + +[editor.sidebar.corner] +rounded = "Arrondi" +squircle = "Squircle" +title = "Style des coins" + +[editor.sidebar.cursor] +circle = "Cercle" +click_ripple = "Onde au clic" +color = "Couleur" +default = "Par défaut" +description_circle = "Remplace toutes les formes de curseur par un cercle." +description_default = "Conserve les formes et l'apparence du curseur de votre enregistrement." +description_macos = "Apparence macOS classique. Les curseurs personnalisés gardent leur forme enregistrée." +description_tahoe = "Apparence macOS Tahoe. Les curseurs personnalisés gardent leur forme enregistrée." +description_windows = "Apparence Windows. Les curseurs personnalisés gardent leur forme enregistrée." +duration = "Durée" +recorded = "Curseurs enregistrés" +recorded_tooltip = "Enregistré avec ce curseur" +ripple_size = "Taille" +strength = "Intensité" + +[editor.sidebar.desktop] +import = "Importer le fond du bureau" +importing = "Importation..." +prompt = "Utiliser le fond d'écran de votre bureau" +reimport = "Réimporter" + +[editor.sidebar.gradient] +angle = "Angle" +from = "De" +grain_scale = "Taille du grain" +noise = "Bruit" +to = "À" + +[editor.sidebar.image] +prompt = "Cliquez pour choisir ou glissez-déposez une image" + +[editor.sidebar.layout] +background_blur = "Flou de l'arrière-plan" +custom_position = "Position d'écran personnalisée (déplacée sur le canevas)" +motion_blur = "Flou de mouvement" +padding = "Marge" +rounded_corners = "Coins arrondis" +title = "Disposition" + +[editor.sidebar.notch] +description = "Dessine une encoche de MacBook sur l'enregistrement. Les enregistrements faits sur un Mac à encoche utilisent leurs propres mesures ; sinon, partez de la taille ci-dessous et ajustez-la." +height = "Hauteur de l'encoche" +position = "Position de l'encoche" +title = "Encoche du MacBook" +width = "Largeur de l'encoche" + +[editor.sidebar.shadow] +advanced = "Réglages avancés de l'ombre" +blur = "Flou" +opacity = "Opacité" +shadow_size = "Taille" +title = "Ombre" + +[editor.sidebar.source] +animated = "Animé" +color = "Couleur" +desktop = "Bureau" +gradient = "Dégradé" +image = "Image" +none = "Aucun" +wallpaper = "Fond d'écran" + +[editor.sidebar.style_group] +back = "Retour au style" +background = "Arrière-plan et cadre" +camera = "Caméra" +cursor = "Curseur" +editing_note = "Seul ce segment est modifié. Les paramètres globaux restent inchangés." +title = "Style {number} · {group}" + +[editor.sidebar.tab] +audio = "Audio" +background = "Arrière-plan" +camera = "Caméra" +captions = "Sous-titres" +cursor = "Curseur" +keyboard = "Clavier" + +[editor.sidebar.wallpaper_theme] +blue = "Bleu" +cities = "Villes" +dark = "Sombre" +orange = "Orange" +purple = "Violet" + +[editor.canvas] +camera = "Caméra" +camera_follows_zoom = "La taille de la caméra suit le zoom ; faites glisser pour la déplacer" +edit_zoom = "Modifier le zoom" +image = "Image" +mask = "Masque" +screen = "Écran" +screen_locked = "L'écran est verrouillé tant qu'un zoom est actif" +text_item = "Texte" + +[editor.clips] +append_error = "Impossible d'ajouter le clip : {message}" +append_error_title = "Erreur d'enregistrement" +back = "Retour à l'éditeur" +cap_filter = "Enregistrement Cap" +clip = "Clip {number}" +empty_description = "Enregistrez ou importez un clip et il apparaîtra ici." +empty_title = "Aucun clip pour l'instant" +import = "Importer" +import_error = "Impossible d'importer le clip : {message}" +import_error_title = "Erreur d'importation" +import_existing = "Enregistrement existant" +import_mp4 = "Vidéo MP4…" +importing = "Importation…" +mp4_filter = "Vidéo MP4" +prepare_controls_failed = "Impossible de préparer les commandes d'enregistrement : {error}" +record_new = "Enregistrer un nouveau clip" +save_before_recording_failed = "Impossible d'enregistrer le projet actuel avant l'enregistrement : {error}" +split = "Partie {number}" +title = "Clips" + +[editor.clips.error] +cancelled = "Importation annulée" +cannot_decode = "Impossible de décoder la vidéo {path} : {error}" +in_use = "Importation impossible tant que cet enregistrement est utilisé : {error}" +instant_source = "Les enregistrements Instant ne peuvent pas encore être importés dans un projet" +instant_target = "Les enregistrements en mode Instant ne peuvent pas être modifiés" +into_itself = "Impossible d'importer un enregistrement dans lui-même" +missing_video = "Fichier vidéo manquant : {path}" +no_duration = "La vidéo importée n'a pas de durée valide" +not_a_project = "Sélectionnez un dossier de projet Cap" +source_empty = "Le projet Cap source n'a aucun segment d'enregistrement" +source_failed = "Le projet Cap source a échoué : {error}" +source_recording = "Le projet Cap source est encore en cours d'enregistrement" +source_unimportable = "Le projet Cap source n'a aucun segment d'enregistrement importable" +unfinished_target = "Terminez ou récupérez cet enregistrement avant d'importer un clip" +unsupported_video = "Fichier vidéo non pris en charge : {path}" + +[editor.export] +back = "Retour à l'éditeur" +existing_link_description = "Une nouvelle mise en ligne remplace la vidéo de ce lien par votre dernière modification. Toutes les personnes disposant du lien verront la version mise à jour." +existing_link_title = "Mettre à jour votre lien existant" +failed = "Échec de l'exportation" +fps = "{fps} i/s" +frames = { one = "{rendered} / {count} image", other = "{rendered} / {count} images" } +generating_preview = "Génération de l'aperçu…" +link_unavailable_cursor = "Les exportations du curseur seul ne peuvent être enregistrées que dans un fichier ou le presse-papiers." +link_unavailable_transparent = "Les exportations transparentes ne peuvent être enregistrées que dans un fichier ou le presse-papiers." +organization = "Organisation" +preview = "Aperçu" +preview_info = "Ceci est une image rendue de votre vidéo. Ajustez les paramètres pour voir la qualité de l'exportation finale." +preview_unavailable = "Aperçu indisponible" +reupload = "Remettre en ligne" +reupload_tooltip = "Mettre en ligne votre dernière modification sur le même lien" +reupload_transparent = "Les liens de partage nécessitent un arrière-plan sans transparence" +sign_in_required = "Vous devez vous connecter pour partager des enregistrements" +task_failed = "Échec de la tâche d'exportation" +title = "Exporter" +upgrade_message = "Cette fonctionnalité nécessite une formule supérieure" +upgrade_title = "Mise à niveau requise" +upload_failed = "Impossible de mettre en ligne l'enregistrement" + +[editor.export.advanced] +bits_per_pixel = "Bits par pixel" +cursor_only = "Exporter le curseur seul" +cursor_only_description = "Conserve les mêmes mouvements et clics du curseur sur un arrière-plan transparent" +custom_bitrate = "Débit personnalisé utilisé" +force_ffmpeg = "Forcer le décodeur FFmpeg" +force_ffmpeg_description = "Ignorer le décodeur matériel (repli automatique activé)" +title = "Avancé" +transparent_mov = "Exporte en MOV transparent. Les fichiers sont volumineux et conviennent surtout à la composition ou au montage." + +[editor.export.cta] +cancel_sign_in = "Annuler la connexion" +clipboard = "Exporter vers le presse-papiers" +create_link = "Créer un lien de partage" +file = "Exporter vers un fichier" +reupload = "Remettre en ligne sur le même lien" +sign_in = "Se connecter pour partager" + +[editor.export.destination] +clipboard = "Presse-papiers" +file = "Fichier" +link = "Lien de partage" + +[editor.export.done] +copy_link = "Copier le lien" +instant_hint = "Utilisez le mode Instant pour votre prochain enregistrement si vous voulez un lien dès que vous arrêtez." +link_copied = "Lien copié" +open_file = "Ouvrir le fichier" +open_link = "Ouvrir le lien" +retry_copy = "Réessayer la copie" +reuploaded = "Votre dernière modification est disponible sur le même lien" +uploaded = "Votre Cap a bien été mis en ligne" + +[editor.export.estimate] +gigabytes = "~{value} Go" +gigabytes_range = "~{lower}–{upper} Go" +hours = "~{value} h" +hours_range = "~{lower}–{upper} h" +megabytes = "~{value} Mo" +megabytes_range = "~{lower}–{upper} Mo" +minutes = "~{value} min" +minutes_range = "~{lower}–{upper} min" +seconds = "~{value} s" +seconds_range = "~{lower}–{upper} s" +under_megabyte = "< 1 Mo" +under_second = "< 1 s" + +[editor.export.optimize] +description = "Réencode par logiciel pour des fichiers bien plus petits (plus lent)" +title = "Optimiser la taille du fichier" + +[editor.export.phase] +clipboard_failed = "Exportation terminée ; échec de la copie dans le presse-papiers" +complete = "Exportation terminée" +copied = "Copié dans le presse-papiers" +copying_to_clipboard = "Copie dans le presse-papiers" +rendering_cursor = "Rendu de la piste du curseur" +rendering_gif = "Rendu du GIF" +rendering_video = "Rendu de la vidéo" +reupload_complete = "Nouvelle mise en ligne terminée" +reuploading = "Nouvelle mise en ligne sur votre lien" +saving_to_file = "Enregistrement dans un fichier" +starting = "Préparation de l'exportation" +upload_complete = "Mise en ligne terminée" +uploading = "Création du lien de partage" + +[editor.export.quality] +larger = "Fichier plus lourd" +maximum = "Maximale" +potato = "Patate" +smaller = "Fichier plus léger" +social = "Réseaux" +web = "Web" + +[editor.export.resources] +proceed = "Continuer quand même" +title = "Vérifier les ressources d'exportation" + +[editor.export.section] +destination = "Destination" +format = "Format" +frame_rate = "Fréquence d'images" +quality = "Qualité" +resolution = "Résolution" + +[editor.export.stats] +calculating = "Calcul…" +duration = "Durée" +estimated_size = "Taille estimée" +export_time = "Durée d'exportation" +output = "Sortie" +output_value = "{width}×{height} · {fps} i/s" +refining_size = "Affinage de la taille…" +refining_time = "Affinage de la durée…" +unavailable = "Indisponible" + +[editor.modal] +cancel_export = "Annuler l'exportation" +cancel_export_message = "Voulez-vous vraiment annuler l'exportation ?" +cancel_export_title = "Annuler l'exportation ?" +keep_exporting = "Continuer l'exportation" +recording_retained = "Enregistrement conservé" + +[editor.tabs.audio] +isolation_balanced = "Équilibrée" +isolation_balanced_description = "Isolation plus nette avec des détails de voix naturels." +isolation_light = "Légère" +isolation_light_description = "Conserve davantage votre voix d'origine et le son de la pièce." +isolation_strong = "Forte" +isolation_strong_description = "Plus d'isolation pour les lieux bruyants. Peut modifier le timbre de la voix." +mic_volume = "Volume du micro" +mute = "Couper le son" +stereo_mode = "Mode stéréo du micro" +studio_sound_description = "Réduit le bruit de fond et équilibre le niveau de votre voix." +studio_sound_save_failed = "Impossible d'enregistrer le réglage par défaut de Studio Sound" +system_volume = "Volume de l'audio système" +title = "Commandes audio" +use_for_new = "Utiliser pour les nouveaux enregistrements" + +[editor.tabs.audio.stereo] +mono_left = "Mono G" +mono_right = "Mono D" +stereo = "Stéréo" + +[editor.tabs.camera] +advanced_shadow = "Paramètres d'ombre avancés" +background = "Arrière-plan" +corner_style = "Style des coins" +custom_position = "Position personnalisée (déplacée sur le canevas)" +hide = "Masquer la caméra" +keep_size_during_zoom = "Conserver la taille d'origine pendant le zoom" +mirror = "Effet miroir" +position = "Position" +reset = "Réinitialiser" +rounded_corners = "Coins arrondis" +shadow = "Ombre" +shadow_blur = "Flou" +shadow_opacity = "Opacité" +shape_label = "Forme" +size_during_zoom = "Taille pendant le zoom" +size_label = "Taille" +title = "Caméra" + +[editor.tabs.camera.blur] +heavy = "Flou intense" +light = "Flou léger" +off = "Désactivé" +remove = "Supprimer l'arrière-plan" + +[editor.tabs.camera.shape] +source = "Source" +square = "Carré" + +[editor.tabs.captions] +active_word_highlight = "Surligner le mot actif" +animation_section = "Animation" +animation_style = "Style d'animation" +background_color = "Couleur d'arrière-plan" +background_opacity = "Opacité de l'arrière-plan" +background_settings = "Paramètres d'arrière-plan" +beta = "Bêta" +delete = "Supprimer" +deleting = "Suppression..." +download = "Télécharger le modèle {model}" +download_note = "Téléchargement unique sur votre système. Tous les sous-titres sont stockés localement." +downloading = "Téléchargement de {model}... {percent} %" +fade_duration = "Durée du fondu" +font_family = "Police" +font_settings = "Paramètres de police" +font_weight = "Graisse" +generate = "Générer les sous-titres" +generate_failed = "Impossible de générer les sous-titres : {reason}" +generating = "Génération..." +highlight_color = "Couleur de surlignage" +highlight_note = "Ceci est la première version des sous-titres dans Cap. Le surlignage du mot actif peut être imprécis dans certains cas. Nous travaillons à un correctif qui sera publié dans une prochaine version." +highlight_style = "Style de surlignage" +horizontal_position = "Position horizontale" +intel_unavailable = "Les modèles de sous-titres Parakeet ne sont pas disponibles sur les Mac Intel. Les modèles Whisper restent disponibles." +keep_open = "Laissez Cap ouvert pendant le téléchargement du modèle. Les rechargements de l'éditeur se reconnectent automatiquement." +language_label = "Langue" +model_downloaded = "Modèle {model} téléchargé" +model_label = "Modèle" +position = "Position" +regenerate = "Régénérer les sous-titres" +size_label = "Taille" +style = "Style" +text_color = "Couleur du texte" +title = "Sous-titres" +uppercase = "Majuscules" +vertical_position = "Position verticale" + +[editor.tabs.captions.animation] +bounce = "Rebond" +none = "Aucune" +pop = "Pop" + +[editor.tabs.captions.highlight] +color = "Couleur" +pill = "Pastille" + +[editor.tabs.captions.language] +ar = "Arabe" +auto = "Détection automatique" +bn = "Bengali" +de = "Allemand" +en = "Anglais" +es = "Espagnol" +fa = "Persan" +fr = "Français" +gu = "Gujarati" +he = "Hébreu" +hi = "Hindi" +it = "Italien" +ja = "Japonais" +ko = "Coréen" +mr = "Marathi" +nl = "Néerlandais" +pa = "Pendjabi" +pl = "Polonais" +pt = "Portugais" +ru = "Russe" +sk = "Slovaque" +ta = "Tamoul" +te = "Télougou" +tr = "Turc" +ur = "Ourdou" +zh = "Chinois" + +[editor.tabs.captions.model] +high_accuracy = "Haute précision" +high_accuracy_description = "Téléchargement plus lourd, précision accrue" +medium = "Moyen" +medium_description = "Plus lent, plus précis" +recommended = "Recommandé" +recommended_description = "Meilleur équilibre pour la plupart des enregistrements" +small = "Petit" +small_description = "Téléchargement le plus léger" + +[editor.tabs.captions.preset] +classic = "Classique" +classic_description = "Texte net sur un fond uni arrondi." +highlight = "Surligné" +highlight_description = "Majuscules grasses avec une pastille derrière le mot actif." +karaoke = "Karaoké" +karaoke_description = "Les mots s'illuminent au rythme de la parole." +minimal = "Minimal" +minimal_description = "Texte discret avec contour, sans arrière-plan." +pop = "Pop" +pop_description = "Majuscules ludiques qui apparaissent avec une touche de couleur vive." + +[editor.tabs.corner] +rounded = "Arrondi" +squircle = "Squircle" + +[editor.tabs.cursor] +friction = "Friction" +hide_when_idle = "Masquer en cas d'inactivité" +inactivity_delay = "Délai d'inactivité" +mass = "Masse" +movement_style = "Style de mouvement du curseur" +show = "Afficher le curseur" +size_label = "Taille" +smooth_movement = "Mouvement fluide" +style_label = "Style du curseur" +svg_cursors = "Curseurs SVG haute qualité" +tension = "Tension" +tilt = "Inclinaison" + +[editor.tabs.cursor.style] +fast = "Rapide" +fast_description = "Vif, suivi au plus près" +mellow = "Doux" +mellow_description = "Mouvement équilibré et naturel" +slow = "Lent" +slow_description = "Mouvement long et glissant" +smooth = "Fluide" +smooth_description = "Mouvement souple et flottant" + +[editor.tabs.keyboard] +animation = "Animation" +background_color = "Couleur d'arrière-plan" +background_opacity = "Opacité de l'arrière-plan" +background_settings = "Paramètres d'arrière-plan" +behavior = "Comportement" +beta = "Bêta" +changed_during_generation = "La timeline ou les paramètres du clavier ont changé pendant la génération. Réessayez." +empty_description = "Cliquez sur « Générer les segments clavier » pour créer des segments à partir des touches enregistrées." +empty_title = "Aucun segment clavier pour l'instant." +fade_duration = "Durée du fondu" +font_family = "Police" +font_settings = "Paramètres de police" +font_weight = "Graisse" +generate = "Générer les segments clavier" +generating = "Génération des segments clavier..." +grouping_threshold = "Seuil de regroupement" +linger_duration = "Durée d'affichage" +load_failed = "Impossible de charger les données d'enregistrement : {error}" +no_presses = "Aucune frappe enregistrée n'a été trouvée." +position = "Position" +regenerate = "Régénérer les segments clavier" +show_modifiers = "Afficher les modificateurs" +show_special_keys = "Afficher les touches spéciales" +size_label = "Taille" +text_color = "Couleur du texte" +timeline_unavailable = "La timeline du projet n'est pas disponible." +timeline_unreadable = "Impossible de lire la timeline du projet." +title = "Afficher le clavier" +uppercase = "Majuscules" + +[editor.tabs.position] +bottom_center = "En bas au centre" +bottom_left = "En bas à gauche" +bottom_right = "En bas à droite" +manual = "Manuelle" +top_center = "En haut au centre" +top_left = "En haut à gauche" +top_right = "En haut à droite" + +[editor.tabs.sync] +auto_description = "Cap a calculé ces décalages automatiquement pour garder l'audio synchronisé avec la vidéo. Ajustez-les si quelque chose sonne encore faux." +auto_synced = "Synchronisé automatiquement" +camera = "Décalage de la caméra" +clip = "Clip {number}" +description = "Ajustez finement les décalages des sources si l'audio ou la caméra se désynchronise de l'enregistrement d'écran." +microphone = "Décalage du micro" +system_audio = "Décalage de l'audio système" +title = "Synchronisation" + +[editor.tabs.weight] +bold = "Gras" +medium = "Moyen" +normal = "Normal" + +[editor.timeline] +add_shot = "Ajouter un plan" +add_shot_with_look = "Ajouter un plan · {look} · {seconds} s" +paused = "En pause" + +[editor.timeline.empty] +add_audio = "Ajouter de l'audio" +add_scene = "Ajouter une scène" +audio = "Ajoutez une musique de fond ou importez votre propre audio" +captions = "Aucun sous-titre" +keyboard = "Aucun événement clavier" +scene = "Alternez les dispositions entre votre écran et votre caméra" + +[editor.timeline.picker] +audio = "Ajoutez une musique de fond ou importez votre propre audio." +caption = "Transcrivez automatiquement votre enregistrement en sous-titres à l'écran." +clip = "Les images de votre écran enregistré." +image = "Ajoutez une image à votre enregistrement." +keyboard = "Affichez les touches à l'écran pendant que vous tapez." +mask = "Floutez ou noircissez les zones privées de l'écran." +scene = "Alternez les dispositions entre votre écran et votre caméra." +scene_unavailable = "Enregistrez avec une caméra pour utiliser les scènes." +style = "Modifiez l'arrière-plan, la caméra et le curseur au fil du temps." +text_overlay = "Ajoutez des textes et des titres personnalisés sur le canevas." +three_d = "Inclinez la scène en perspective 3D." +zoom = "Des zooms fluides qui suivent l'action." + +[editor.timeline.remove] +audio = "Supprimer la piste Audio." +captions = "Supprimer la piste Sous-titres." +clip = "Supprimer la piste Clip." +image = "Supprimer la piste Image." +keyboard = "Supprimer la piste Clavier." +mask = "Supprimer la piste Masque." +scene = "Supprimer la piste Scène." +style = "Supprimer la piste Style." +text_track = "Supprimer la piste Texte." +three_d = "Supprimer la piste 3D." +zoom = "Supprimer la piste Zoom." + +[editor.timeline.scene] +camera_only = "Caméra seule" +default = "Par défaut" +floating = "Flottante" +hide_camera = "Masquer la caméra" +split_screen = "Écran partagé" + +[editor.timeline.segment] +audio = "Audio" +automatic_zoom = "Zoom automatique" +caption = "Sous-titre" +clip = "Clip" +cursor_hidden = "Curseur masqué" +highlight = "Mise en valeur" +label = "Libellé" +manual_zoom = "Zoom manuel" +muted = "Muet" +numbered_clip = "Clip {number}" +sensitive = "Sensible" + +[editor.timeline.track] +audio = "Audio" +captions = "Sous-titres" +clip = "Clip" +image = "Image" +keyboard = "Clavier" +mask = "Masque" +scene = "Scène" +style = "Style" +text_track = "Texte" +video = "Vidéo" +zoom = "Zoom" + +[editor.window] +audio_filter = "Audio" +audio_segment_changed = "Ce segment audio a changé. Sélectionnez-le et choisissez de nouveau un remplacement." +crop = "Recadrer" +crop_tooltip = "Recadrer la vidéo" +no_room_for_3d_shot = "Plus de place pour un autre plan 3D" +redo = "Rétablir" +save_failed = "Impossible d'enregistrer {path}. Gardez l'éditeur ouvert et réessayez : {error}" +undo = "Annuler" + +[editor.window.add_track] +button = "Ajouter une piste" +hide_3d = "Masquer la piste 3D." +hint = "Choisissez une piste à ajouter à votre timeline." + +[editor.window.aspect] +auto = "Auto" +classic = "Classique" +classic_ratio = "Classique ⋅4:3" +square = "Carré" +square_ratio = "Carré ⋅1:1" +tall = "Haut" +tall_ratio = "Haut ⋅3:4" +tooltip = "Format d'image" +vertical = "Vertical" +vertical_ratio = "Vertical ⋅9:16" +wide = "Large" +wide_ratio = "Large ⋅16:9" + +[editor.window.audio_import] +copy_failed = "Impossible de copier le fichier audio : {error}" +directory_failed = "Impossible de créer le dossier audio : {error}" +no_extension = "Le fichier audio n'a pas d'extension" +not_found = "Fichier audio introuvable : {path}" +unsupported = "Format audio non pris en charge : .{extension}" + +[editor.window.busy] +clip_import = "Attendez la fin de l'importation du clip avant de fermer ou de supprimer cet enregistrement." +export = "Attendez la fin de l'exportation ou de son annulation avant de fermer ou de supprimer cet enregistrement." +opening = "Attendez que l'enregistrement soit ouvert avant de le supprimer." +save_before_delete_failed = "Impossible d'enregistrer l'enregistrement avant de le supprimer : {error}" + +[editor.window.camera3d_lane] +add_shot = "+ Ajouter un plan" +auto_scene = "Scène auto" +how_many = "Combien de plans ?" +prompt = "Ajoutez des plans 3D cinématiques à votre enregistrement" + +[editor.window.clip_menu] +audio = "AUDIO" +cursor = "CURSEUR" +delete = "Supprimer le clip" +delete_only_clip = "Impossible de supprimer le seul clip" +hide_cursor = "Masquer le curseur" +hide_cursor_description = "Disparaît pendant la lecture de ce clip" +keep_pitch = "Garder la hauteur" +match_speed = "Suivre la vitesse" +merge_next = "Fusionner avec le clip suivant" +merge_previous = "Fusionner avec le clip précédent" +mute = "Muet" +mute_clip = "Couper le son du clip" +mute_clip_description = "Coupe l'audio de ce clip" +rename = "Renommer le clip" +speed = "VITESSE" +split = "Couper à la tête de lecture" +split_hint = "Placez la tête de lecture dans ce clip" +volume = "Volume" + +[editor.window.frame] +add_tooltip = "Ajouter un cadre" +browser = "Navigateur" +browser_description = "Fenêtre de navigateur avec barre d'adresse" +button = "Cadre" +dark = "Sombre" +global = "Cadre global · S'applique partout où le style hérite de l'arrière-plan." +heading = "Cadre" +light = "Clair" +macbook_description = "Contour d'ordinateur portable autour de l'enregistrement" +macos_description = "Fenêtre avec boutons de contrôle colorés" +none = "Aucun" +none_description = "Afficher l'enregistrement tel quel" +style_changed = "Ce style a changé. Fermez puis rouvrez le cadre." +style_only = "Style {index} uniquement · Les paramètres globaux restent inchangés." +style_only_override = "Style {index} uniquement · La modification active son arrière-plan personnalisé." +theme = "Thème" +title = "Titre" +url = "URL" +window_title_placeholder = "Titre de la fenêtre" +windows_description = "Barre de titre avec boutons de fenêtre" + +[editor.window.header] +captions = "Sous-titres" +delete_recording = "Supprimer l'enregistrement" +export = "Exporter" +open_bundle = "Ouvrir le dossier de l'enregistrement" +presets = "Préréglages" + +[editor.window.image] +changed = "Cette image a changé pendant le choix d'un remplacement. Sélectionnez-la et réessayez." +filter = "Images" +recording_unavailable = "L'enregistrement n'est plus disponible pour cette image. Rouvrez le projet." + +[editor.window.open] +damaged = "Impossible d'ouvrir les pistes vidéo de cet enregistrement. Le dossier semble endommagé." +failed = "Impossible d'ouvrir cet enregistrement" +media_failed = "Impossible de lire les médias de cet enregistrement : {error}" +meta_failed = "Impossible de charger les métadonnées de l'enregistrement : {error}" +no_segments = "L'enregistrement n'a aucun segment. Il faut peut-être d'abord le récupérer." +not_found = "Chemin vidéo {path} introuvable !" +not_studio = "Seuls les enregistrements Studio peuvent être modifiés" + +[editor.window.presets] +apply = "Appliquer" +create = "Créer" +create_new = "Créer un préréglage" +create_title = "Créer un préréglage" +default = "Par défaut" +default_badge = "Par défaut" +delete = "Supprimer" +delete_confirm = "Voulez-vous vraiment supprimer « {name} » ?" +delete_title = "Supprimer le préréglage" +name_placeholder = "Nom du préréglage" +none_saved = "Aucun préréglage enregistré" +rename = "Renommer" +rename_title = "Renommer le préréglage" +save_settings = "Enregistrer les réglages dans le préréglage" +set_default = "Définir par défaut" + +[editor.window.preview] +full = "Pleine" +half = "Moitié" +label = "Aperçu" +quarter = "Quart" + +[editor.window.text_menu] +copy_style = "Copier le style" +copy_style_first = "Copiez d'abord un style" +delete_segment = "Supprimer le segment de texte" +delete_segments = "Supprimer les segments de texte" +paste_style = "Coller le style" +paste_style_all = "Coller le style sur toute la sélection" +segments = { one = "{count} segment de texte", other = "{count} segments de texte" } +untitled = "Texte" + +[editor.window.title] +not_saved = "Titre non enregistré" +not_saved_message = "{error}\n\nVotre titre est toujours dans l'éditeur. Essayez de l'enregistrer à nouveau." +read_failed = "Impossible de lire le titre de l'enregistrement pour {path} : {error}" +save_failed = "Impossible d'enregistrer le titre de l'enregistrement pour {path} : {error}" + +[editor.window.track] +clear_all = "Tout effacer" +delete = "Supprimer" + +[editor.window.transport] +done_splitting = "Terminer la découpe" +play_pause = "Lire/mettre en pause la vidéo" +split = "Couper" +split_tooltip = "Cliquez sur un segment pour le couper. Échap pour terminer." +zoom_in = "Zoom avant" +zoom_out = "Zoom arrière" + +[editor.window.zoom_lane] +generate = "Générer" +generating = "Génération…" +no_clicks = "Aucun clic trouvé pour zoomer. Faites glisser sur la piste pour en ajouter un." +prompt = "Ajouter des zooms automatiquement" + +[ui] +coming_soon = "Bientôt disponible" + +[ui.selection] +audio = { one = "{count} segment audio sélectionné", other = "{count} segments audio sélectionnés" } +caption = { one = "{count} segment de sous-titres sélectionné", other = "{count} segments de sous-titres sélectionnés" } +delete = "Supprimer" +done = "Terminé" +image = { one = "{count} segment d'image sélectionné", other = "{count} segments d'image sélectionnés" } +keyboard = { one = "{count} segment clavier sélectionné", other = "{count} segments clavier sélectionnés" } +mask = { one = "{count} segment de masque sélectionné", other = "{count} segments de masque sélectionnés" } +scene = { one = "{count} segment de scène sélectionné", other = "{count} segments de scène sélectionnés" } +segments = { one = "{count} segment sélectionné", other = "{count} segments sélectionnés" } +select_all = "Tout sélectionner" +style = { one = "{count} segment de style sélectionné", other = "{count} segments de style sélectionnés" } +text_segments = { one = "{count} segment de texte sélectionné", other = "{count} segments de texte sélectionnés" } +three_d = { one = "{count} segment 3D sélectionné", other = "{count} segments 3D sélectionnés" } +zoom = { one = "{count} sélectionné", other = "{count} sélectionnés" } +zoom_all = { one = "{count} sélectionné", other = "Les {count} sélectionnés" } +zoom_some = { one = "{count} sur {total} sélectionné", other = "{count} sur {total} sélectionnés" } diff --git a/apps/desktop-gpui/locales/hi.toml b/apps/desktop-gpui/locales/hi.toml index a49b07b4110..f2bd96a33cd 100644 --- a/apps/desktop-gpui/locales/hi.toml +++ b/apps/desktop-gpui/locales/hi.toml @@ -257,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 = "क्या आप वाकई यह रिकॉर्डिंग हटाना चाहते हैं?" @@ -620,3 +1312,1566 @@ 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 = "अपलोड हो रहा है" + +# Editor sidebar, segment panels, colour correction, audio library and crop dialog. + +[editor.audio.library] +add_hint = "अपने वीडियो में ऑडियो, संगीत या दूसरी आवाज़ें जोड़ें" +add_title = "ऑडियो जोड़ें" +change_hint = "इस सेगमेंट के लिए कोई दूसरा ट्रैक चुनें" +change_title = "ऑडियो बदलें" +done = "हो गया" +import = "फ़ाइल इंपोर्ट करें" + +[editor.color] +apply_to_cursor = "कर्सर पर लागू करें" +fine_tune = "रंग बारीकी से सेट करें" +title = "कलर करेक्शन" + +[editor.color.preset.cinematic] +description = "हल्के ग्रेन के साथ टील छाया और नारंगी हाइलाइट्स" +name = "सिनेमैटिक" + +[editor.color.preset.dreamy] +description = "उठे हुए काले रंग के साथ हल्के, मुलायम पेस्टल" +name = "ड्रीमी" + +[editor.color.preset.frost] +description = "हल्के रंगों के साथ ठंडे, साफ़ टोन" +name = "फ़्रॉस्ट" + +[editor.color.preset.golden] +description = "गोल्डन आवर की गर्म चमक" +name = "सुनहरा" + +[editor.color.preset.midnight] +description = "हल्के रंगों के साथ गहरा, मूडी टील" +name = "मिडनाइट" + +[editor.color.preset.noir] +description = "ज़्यादा ग्रेन के साथ हाई-कंट्रास्ट ब्लैक एंड व्हाइट" +name = "नॉयर" + +[editor.color.preset.none] +description = "असली रंग, कोई ग्रेड नहीं" +name = "कोई नहीं" + +[editor.color.preset.vintage] +description = "हल्के कंट्रास्ट के साथ फीका गर्म फ़िल्म लुक" +name = "विंटेज" + +[editor.color.preset.vivid] +description = "दमदार सैचुरेशन और ज़्यादा कंट्रास्ट" +name = "चटकीला" + +[editor.color.slider] +contrast = "कंट्रास्ट" +exposure = "एक्सपोज़र" +fade = "फ़ेड" +grain = "ग्रेन" +saturation = "सैचुरेशन" +split_tone = "स्प्लिट टोन" +strength = "तीव्रता" +temperature = "टेम्परेचर" +tint = "टिंट" +vignette = "विनेट" + +[editor.crop] +aspect_ratio = "आस्पेक्ट रेशियो" +center = "बीच में" +dimensions = "आकार" +editing_style = "सिर्फ़ स्टाइल {number} एडिट हो रही है" +editing_style_override = "सिर्फ़ स्टाइल {number} एडिट हो रही है · सेव करने पर इसका बैकग्राउंड ओवरराइड चालू हो जाएगा" +free = "फ़्री" +full = "पूरा" +hint = "खिसकाने के लिए ड्रैग करें · तीर कुंजियों से थोड़ा खिसकाएँ · स्नैप रोकने के लिए {key} दबाए रखें" +loading_frame = "फ़्रेम लोड हो रहा है…" +position = "पोज़िशन" +preview = "प्रीव्यू" +rendering_preview = "प्रीव्यू रेंडर हो रहा है…" +snap_to_ratios = "रेशियो पर स्नैप करें" +style_changed = "क्रॉप करते समय यह स्टाइल बदल गई। रद्द करें और क्रॉप फिर से खोलें।" +title = "क्रॉप" + +[editor.panel] +delete = "हटाएँ" +done = "हो गया" +duration = "अवधि" +edge_in = "इन" +edge_out = "आउट" +enabled = "चालू" +fade_duration = "फ़ेड अवधि" +flip_horizontally = "क्षैतिज रूप से पलटें" +flip_vertically = "लंबवत रूप से पलटें" +name = "नाम" +opacity = "अपारदर्शिता" + +[editor.panel.audio] +change = "बदलें" +change_hint = "ट्रैक बदलने के लिए टैप करें" +fade_in = "फ़ेड इन" +fade_out = "फ़ेड आउट" +fallback_name = "ऑडियो" +title = "ऑडियो {number}" +volume = "वॉल्यूम" + +[editor.panel.camera3d] +already_still = "पहले से स्थिर शॉट है" +amount = "मात्रा" +angles = "एंगल" +auto_scene = "ऑटो सीन" +auto_scene_tooltip = "ट्रैक के सभी 3D शॉट फिर से बनाता है" +bokeh = "बोकेह" +camera = "कैमरा" +delete_tooltip = "यह शॉट हटाएँ" +depth_blur = "डेप्थ ब्लर" +distance = "दूरी" +drag_to_orbit = "घुमाने के लिए ड्रैग करें" +ease_in = "ईज़ इन" +ease_out = "ईज़ आउट" +end_same_as_start = "अंत · शुरुआत जैसा" +fine_tune = "बारीकी से सेट करें" +focus = "फ़ोकस" +header = "3D शॉट · {look} · {duration}s" +hold_opening = "पूरे शॉट में शुरुआती पोज़ बनाए रखें" +lens = "लेंस" +look_title = "लुक" +motion_style = "मोशन स्टाइल" +moves = "मूव" +nothing_to_swap = "स्थिर शॉट में बदलने को कुछ नहीं है" +play_shot = "चलाएँ" +play_shot_tooltip = "यह शॉट चलाएँ" +pose_end = "अंत · {time}" +pose_start = "शुरू · {time}" +reset_camera = "कैमरा रीसेट करें" +roll = "रोल" +rotate_x = "घुमाव X" +rotate_y = "घुमाव Y" +shift_x = "शिफ़्ट X" +shift_y = "शिफ़्ट Y" +shots = { one = "{count} शॉट", other = "{count} शॉट" } +still_shot = "स्थिर शॉट" +swap = "शुरुआत और अंत बदलें" +timing_advanced = "टाइमिंग और एडवांस्ड" +timing_summary = "{easing} · लेंस {fov}" + +[editor.panel.camera3d.blur] +angle = "कोण" +falloff = "फ़ॉलऑफ़" +focus_size = "फ़ोकस आकार" +focus_x = "फ़ोकस X" +focus_y = "फ़ोकस Y" +position = "पोज़िशन" +scan = "स्कैन" +strength = "तीव्रता" + +[editor.panel.camera3d.easing] +ease_in = "ईज़ इन" +ease_out = "ईज़ आउट" +linear = "लीनियर" +smooth = "स्मूद" + +[editor.panel.camera3d.focus_mode] +directional = "दिशात्मक" +radial = "रेडियल" +tilt_shift = "टिल्ट शिफ़्ट" + +[editor.panel.camera3d.look] +center = "केंद्र" +close_up = "क्लोज़-अप" +drift_down = "नीचे बहाव" +glide_across = "आर-पार ग्लाइड" +low_angle = "लो एंगल" +perspective = "पर्सपेक्टिव" +pull_back = "पीछे खींचें" +rising_sweep = "उठता स्वीप" +slide_by = "स्लाइड बाय" +spotlight = "स्पॉटलाइट" +tilt_away = "झुककर दूर" +top_down = "ऊपर से" +unfold = "खुलना" + +[editor.panel.camera3d.sequence] +product_tour = "प्रोडक्ट टूर" +punch_in = "पंच इन" +showcase = "शोकेस" + +[editor.panel.camera3d.shot] +custom = "कस्टम मूव" +still = "स्थिर शॉट" + +[editor.panel.caption] +title = "कैप्शन {number}" + +[editor.panel.font] +system_mono = "सिस्टम Mono" +system_sans = "सिस्टम Sans" +system_serif = "सिस्टम Serif" + +[editor.panel.font_weight] +black = "ब्लैक" +bold = "बोल्ड" +custom = "कस्टम ({weight})" +extra_bold = "एक्स्ट्रा बोल्ड" +light = "लाइट" +medium = "मीडियम" +regular = "रेगुलर" +semibold = "सेमीबोल्ड" + +[editor.panel.image] +arrange = "कैनवास पर व्यवस्थित करें" +arrange_help = "इमेज को खिसकाने के लिए ड्रैग करें। आकार बदलने के लिए कोना खींचें, या घुमाने के लिए नीचे दिए गए रोटेशन कंट्रोल का इस्तेमाल करें।" +center = "कैनवास के बीच में रखें" +lock_aspect = "आस्पेक्ट रेशियो लॉक करें" +missing = "इमेज फ़ाइल नहीं मिली। इसे बदलें, ताकि यह सेगमेंट अपनी टाइमिंग और बदलावों के साथ वापस आ जाए।" +replace = "इमेज बदलें" +rotation = "रोटेशन" +rounding = "गोलाई" + +[editor.panel.keyboard] +title = "कीबोर्ड {number}" + +[editor.panel.mask] +blur = "ब्लर" +effect = "इफ़ेक्ट" +highlight = "हाइलाइट" +outside_darkness = "बाहरी अंधेरा" +pixel_size = "पिक्सल आकार" +pixelate = "पिक्सलेट" +sensitive = "संवेदनशील" +title = "मास्क {number}" + +[editor.panel.scene] +camera_layout = "कैमरा लेआउट" +camera_position = "कैमरा पोज़िशन" +camera_zoom = "कैमरा ज़ूम" +screen_position = "स्क्रीन पोज़िशन" +screen_zoom = "स्क्रीन ज़ूम" +transition = "ट्रांज़िशन" + +[editor.panel.scene.description] +camera_only = "सिर्फ़ कैमरा फ़ीड दिखाता है" +default = "स्क्रीन और कैमरा दोनों दिखाता है" +floating = "स्क्रीन और कैमरा बैकग्राउंड के ऊपर गोल कोनों वाले कार्ड की तरह साथ-साथ तैरते हैं" +hide_camera = "सिर्फ़ स्क्रीन रिकॉर्डिंग दिखाता है" +split_screen = "स्क्रीन और कैमरा साथ-साथ (पोर्ट्रेट में अपने आप ऊपर-नीचे)" + +[editor.panel.scene.mode] +camera_only = "सिर्फ़ कैमरा" +default = "डिफ़ॉल्ट" +floating = "फ़्लोटिंग" +hide_camera = "कैमरा छिपाएँ" +split_screen = "स्प्लिट स्क्रीन" + +[editor.panel.style] +camera_only_background = "सिर्फ़ कैमरा बैकग्राउंड" +camera_only_help = "सिर्फ़ कैमरा वाला सीन इस्तेमाल करें। पैडिंग से कैमरे के आस-पास बैकग्राउंड दिखता है।" +camera_only_padding = "सिर्फ़ कैमरा पैडिंग" +crop_height = "ऊँचाई" +crop_left = "बाएँ" +crop_title = "स्क्रीन क्रॉप (सोर्स पिक्सल)" +crop_top = "ऊपर" +crop_width = "चौड़ाई" +edit_background = "बैकग्राउंड और फ़्रेम एडिट करें" +edit_camera = "कैमरा एडिट करें" +edit_cursor = "कर्सर एडिट करें" +help = "बदलाव सिर्फ़ इस सेगमेंट के दौरान लागू होते हैं। किसी ग्रुप की ग्लोबल सेटिंग्स कॉपी करने के लिए उसे चालू करें।" +reset_crop = "क्रॉप रीसेट करें" + +[editor.panel.text_animation] +bounce = "बाउंस" +fade = "फ़ेड" +letters = "अक्षर" +none = "कोई नहीं" +pop = "पॉप" +slide_down = "नीचे स्लाइड" +slide_left = "बाएँ स्लाइड" +slide_right = "दाएँ स्लाइड" +slide_up = "ऊपर स्लाइड" +tracking = "ट्रैकिंग" +typewriter = "टाइपराइटर" +wipe = "वाइप" +words = "शब्द" +zoom = "ज़ूम" + +[editor.panel.text_background] +box_shape = "बॉक्स" +highlight = "हाइलाइट" +none = "कोई नहीं" +pill = "पिल" + +[editor.panel.text_layout] +fullscreen = "फ़ुलस्क्रीन" +overlay = "ओवरले" + +[editor.panel.text_preset.group] +all = "सभी" +callouts = "कॉलआउट" +code = "कोड" +lower_thirds = "लोअर थर्ड" +statements = "स्टेटमेंट" +titles = "टाइटल" + +[editor.panel.text_preset.name] +caption = "कैप्शन" +cinematic = "सिनेमैटिक" +code = "कोड" +gradient = "ग्रेडिएंट" +headline = "हेडलाइन" +highlight = "हाइलाइट" +kicker = "किकर" +label = "लेबल" +lower_third = "लोअर थर्ड" +name_tag = "नेम टैग" +neon = "नियॉन" +quote = "कोट" +stat = "बड़ा आंकड़ा" +sticker = "स्टिकर" +title = "टाइटल" +typewriter = "टाइपराइटर" + +[editor.panel.text_preset.sample] +caption = "Cap से रिकॉर्ड किया गया" +cinematic = "पहला अध्याय" +gradient = "सुंदर टेक्स्ट" +headline = "तेज़ी से शिप करें" +highlight = "ज़रूरी हिस्सा" +kicker = "नया फ़ीचर" +label = "प्रो टिप" +name_tag = "Richie · संस्थापक" +neon = "चमक उठो" +quote = "“इसे सहज बनाइए”" +sticker = "धमाका!" +title = "पेश है Cap" +typewriter = "टाइप हो रहा है…" + +[editor.panel.text_segment] +animation = "एनिमेशन" +background = "बैकग्राउंड" +color = "रंग" +font = "फ़ॉन्ट" +font_size = "आकार" +fullscreen_help = "टेक्स्ट दिखने तक वीडियो रुका रहता है, फिर वहीं से दोबारा चलता है।" +glow = "चमक" +gradient = "ग्रेडिएंट" +layout = "लेआउट" +letter_spacing = "अक्षर अंतर" +line_height = "लाइन ऊँचाई" +look = "लुक" +outline = "आउटलाइन" +screen_transition = "स्क्रीन ट्रांज़िशन" +shadow = "छाया" +style = "स्टाइल" +title = "टेक्स्ट {number}" + +[editor.panel.timing] +end = "अंत" +start = "शुरू" +title = "टाइमिंग" +to = "से" + +[editor.panel.zoom] +amount = "ज़ूम मात्रा" +auto = "ऑटो" +auto_help = "ऑटोमैटिक ज़ूम स्क्रीन पर आपके कर्सर के साथ चलता है।" +auto_locked = "ऑटो मोड के लिए कर्सर कैप्चर ज़रूरी है। सेटिंग्स → सामान्य में \"कर्सर को अलग से कैप्चर करें (Studio)\" चालू करें।" +how_it_works = "यह कैसे काम करता है?" +manual = "मैनुअल" +manual_help = "मैनुअल ज़ूम नीचे चुनी गई एक तय जगह पर रहता है।" +mixed = "मिश्रित" +mode = "ज़ूम मोड" +positions_mixed = "सेगमेंट अलग-अलग जगहों पर ज़ूम करते हैं। सभी को एक ही जगह ले जाने के लिए ड्रैग करें।" +title = "ज़ूम {number}" + +[editor.sidebar] +desktop_import_failed = "आपका डेस्कटॉप वॉलपेपर इंपोर्ट नहीं हो सका" +importing_image = "इमेज इंपोर्ट हो रही है…" + +[editor.sidebar.animated_gradient] +add_colour = "रंग जोड़ें" +colours = "रंग" +delete = "हटाएँ" +delete_failed = "यह ग्रेडिएंट हटाया नहीं जा सका। फिर से कोशिश करें।" +fine_tune = "बारीकी से सेट करें" +limit_reached = { one = "आप ज़्यादा से ज़्यादा {count} ग्रेडिएंट सेव कर सकते हैं। नया जोड़ने के लिए एक हटाएँ।", other = "आप ज़्यादा से ज़्यादा {count} ग्रेडिएंट सेव कर सकते हैं। नया जोड़ने के लिए एक हटाएँ।" } +motion = "मोशन" +name_placeholder = "इस ग्रेडिएंट का नाम रखें" +name_required = "इस ग्रेडिएंट का नाम लिखें।" +presets = "प्रीसेट" +randomize = "रैंडम" +remember_failed = "आपकी एनिमेटेड ग्रेडिएंट सेटिंग्स याद नहीं रखी जा सकीं।" +remove_colour = "रंग हटाएँ" +reset_tooltip = "बारीक सेटिंग्स रीसेट करें" +save_failed = "यह ग्रेडिएंट सेव नहीं हो सका। फिर से कोशिश करें।" +save_tooltip = "मौजूदा ग्रेडिएंट सेव करें" +saved = "सेव किए गए" +still = "स्थिर" + +[editor.sidebar.animated_gradient.control] +contrast = "कंट्रास्ट" +curvature = "वक्रता" +detail = "डिटेल" +direction = "दिशा" +exposure = "एक्सपोज़र" +flow_scale = "फ़्लो स्केल" +flow_strength = "फ़्लो की ताकत" +grain_amount = "ग्रेन मात्रा" +grain_size = "ग्रेन आकार" +highlights = "हाइलाइट्स" +motion_speed = "मोशन स्पीड" +relief = "उभार" +ripple_size = "रिपल आकार" +shading = "शेडिंग" +vibrance = "वाइब्रेंस" + +[editor.sidebar.animated_gradient.group] +animation = "एनिमेशन" +colour = "रंग" +flow = "फ़्लो" +lighting = "लाइटिंग" +texture = "टेक्सचर" + +[editor.sidebar.background] +title = "बैकग्राउंड" + +[editor.sidebar.border] +color = "बॉर्डर रंग" +opacity = "बॉर्डर अपारदर्शिता" +title = "बॉर्डर" +width = "बॉर्डर चौड़ाई" + +[editor.sidebar.corner] +rounded = "गोल" +squircle = "स्क्वर्कल" +title = "कोने की स्टाइल" + +[editor.sidebar.cursor] +circle = "गोला" +click_ripple = "क्लिक रिपल" +color = "रंग" +default = "डिफ़ॉल्ट" +description_circle = "सभी कर्सर आकारों को गोले से बदल देता है।" +description_default = "आपकी रिकॉर्डिंग के कर्सर का आकार और रूप वैसा ही रखता है।" +description_macos = "क्लासिक macOS रूप। कस्टम कर्सर का रिकॉर्ड किया गया आकार बना रहता है।" +description_tahoe = "macOS Tahoe रूप। कस्टम कर्सर का रिकॉर्ड किया गया आकार बना रहता है।" +description_windows = "Windows रूप। कस्टम कर्सर का रिकॉर्ड किया गया आकार बना रहता है।" +duration = "अवधि" +recorded = "रिकॉर्ड किए गए कर्सर" +recorded_tooltip = "इस कर्सर से रिकॉर्ड किया गया" +ripple_size = "आकार" +strength = "तीव्रता" + +[editor.sidebar.desktop] +import = "डेस्कटॉप बैकग्राउंड इंपोर्ट करें" +importing = "इंपोर्ट हो रहा है..." +prompt = "अपने डेस्कटॉप का वॉलपेपर इस्तेमाल करें" +reimport = "फिर से इंपोर्ट करें" + +[editor.sidebar.gradient] +angle = "कोण" +from = "से" +grain_scale = "ग्रेन स्केल" +noise = "नॉइज़" +to = "तक" + +[editor.sidebar.image] +prompt = "चुनने के लिए क्लिक करें या इमेज ड्रैग करके छोड़ें" + +[editor.sidebar.layout] +background_blur = "बैकग्राउंड ब्लर" +custom_position = "कस्टम स्क्रीन पोज़िशन (कैनवास पर ड्रैग की गई)" +motion_blur = "मोशन ब्लर" +padding = "पैडिंग" +rounded_corners = "गोल कोने" +title = "लेआउट" + +[editor.sidebar.notch] +description = "रिकॉर्डिंग के ऊपर MacBook नॉच बनाता है। नॉच वाले Mac पर बनी रिकॉर्डिंग अपने माप इस्तेमाल करती हैं; वरना नीचे दिए आकार से शुरू करें और मिलान होने तक बदलें।" +height = "नॉच ऊँचाई" +position = "नॉच पोज़िशन" +title = "MacBook नॉच" +width = "नॉच चौड़ाई" + +[editor.sidebar.shadow] +advanced = "छाया की एडवांस्ड सेटिंग्स" +blur = "ब्लर" +opacity = "अपारदर्शिता" +shadow_size = "आकार" +title = "छाया" + +[editor.sidebar.source] +animated = "एनिमेटेड" +color = "रंग" +desktop = "डेस्कटॉप" +gradient = "ग्रेडिएंट" +image = "इमेज" +none = "कोई नहीं" +wallpaper = "वॉलपेपर" + +[editor.sidebar.style_group] +back = "स्टाइल पर वापस जाएँ" +background = "बैकग्राउंड और फ़्रेम" +camera = "कैमरा" +cursor = "कर्सर" +editing_note = "सिर्फ़ यह सेगमेंट एडिट हो रहा है। ग्लोबल सेटिंग्स नहीं बदलतीं।" +title = "स्टाइल {number} · {group}" + +[editor.sidebar.tab] +audio = "ऑडियो" +background = "बैकग्राउंड" +camera = "कैमरा" +captions = "कैप्शन" +cursor = "कर्सर" +keyboard = "कीबोर्ड" + +[editor.sidebar.wallpaper_theme] +blue = "नीला" +cities = "शहर" +dark = "डार्क" +orange = "नारंगी" +purple = "बैंगनी" + +[editor.canvas] +camera = "कैमरा" +camera_follows_zoom = "कैमरे का आकार ज़ूम के साथ बदलता है; हिलाने के लिए खींचें" +edit_zoom = "ज़ूम संपादित करें" +image = "इमेज" +mask = "मास्क" +screen = "स्क्रीन" +screen_locked = "ज़ूम चालू रहने तक स्क्रीन लॉक रहती है" +text_item = "टेक्स्ट" + +[editor.clips] +append_error = "क्लिप नहीं जोड़ी जा सकी: {message}" +append_error_title = "रिकॉर्डिंग त्रुटि" +back = "एडिटर पर वापस जाएँ" +cap_filter = "Cap रिकॉर्डिंग" +clip = "क्लिप {number}" +empty_description = "कोई क्लिप रिकॉर्ड या इम्पोर्ट करें, वह यहाँ दिखेगी।" +empty_title = "अभी कोई क्लिप नहीं" +import = "इम्पोर्ट करें" +import_error = "क्लिप इम्पोर्ट नहीं हो सकी: {message}" +import_error_title = "इम्पोर्ट त्रुटि" +import_existing = "मौजूदा रिकॉर्डिंग" +import_mp4 = "MP4 वीडियो…" +importing = "इम्पोर्ट हो रहा है…" +mp4_filter = "MP4 वीडियो" +prepare_controls_failed = "रिकॉर्डिंग कंट्रोल तैयार नहीं हो सके: {error}" +record_new = "नई क्लिप रिकॉर्ड करें" +save_before_recording_failed = "रिकॉर्डिंग से पहले मौजूदा प्रोजेक्ट सेव नहीं हो सका: {error}" +split = "हिस्सा {number}" +title = "क्लिप" + +[editor.clips.error] +cancelled = "इम्पोर्ट रद्द किया गया" +cannot_decode = "वीडियो {path} डिकोड नहीं हो सका: {error}" +in_use = "यह रिकॉर्डिंग उपयोग में होने पर इम्पोर्ट नहीं हो सकता: {error}" +instant_source = "Instant रिकॉर्डिंग अभी किसी प्रोजेक्ट में इम्पोर्ट नहीं की जा सकतीं" +instant_target = "Instant मोड की रिकॉर्डिंग संपादित नहीं की जा सकतीं" +into_itself = "किसी रिकॉर्डिंग को उसी में इम्पोर्ट नहीं किया जा सकता" +missing_video = "वीडियो फ़ाइल नहीं मिली: {path}" +no_duration = "इम्पोर्ट किए गए वीडियो की कोई मान्य अवधि नहीं है" +not_a_project = "कोई Cap प्रोजेक्ट फ़ोल्डर चुनें" +source_empty = "स्रोत Cap प्रोजेक्ट में कोई रिकॉर्डिंग सेगमेंट नहीं है" +source_failed = "स्रोत Cap प्रोजेक्ट विफल रहा: {error}" +source_recording = "स्रोत Cap प्रोजेक्ट अभी भी रिकॉर्ड हो रहा है" +source_unimportable = "स्रोत Cap प्रोजेक्ट में इम्पोर्ट करने लायक कोई रिकॉर्डिंग सेगमेंट नहीं है" +unfinished_target = "क्लिप इम्पोर्ट करने से पहले इस रिकॉर्डिंग को पूरा या रिकवर करें" +unsupported_video = "असमर्थित वीडियो फ़ाइल: {path}" + +[editor.export] +back = "एडिटर पर वापस जाएँ" +existing_link_description = "दोबारा अपलोड करने पर इस लिंक का वीडियो आपके नए संपादन से बदल जाता है। लिंक वाले सभी लोग अपडेट किया गया वर्ज़न देखेंगे।" +existing_link_title = "अपना मौजूदा लिंक अपडेट करें" +failed = "एक्सपोर्ट विफल रहा" +fps = "{fps} FPS" +frames = { one = "{rendered} / {count} फ़्रेम", other = "{rendered} / {count} फ़्रेम" } +generating_preview = "प्रीव्यू बनाया जा रहा है…" +link_unavailable_cursor = "केवल कर्सर वाले एक्सपोर्ट सिर्फ़ फ़ाइल या क्लिपबोर्ड में सेव किए जा सकते हैं।" +link_unavailable_transparent = "पारदर्शी एक्सपोर्ट सिर्फ़ फ़ाइल या क्लिपबोर्ड में सेव किए जा सकते हैं।" +organization = "संगठन" +preview = "प्रीव्यू" +preview_info = "यह आपके वीडियो का रेंडर किया गया फ़्रेम है। फ़ाइनल एक्सपोर्ट की क्वालिटी देखने के लिए सेटिंग्स बदलें।" +preview_unavailable = "प्रीव्यू उपलब्ध नहीं है" +reupload = "दोबारा अपलोड करें" +reupload_tooltip = "अपना नया संपादन उसी लिंक पर अपलोड करें" +reupload_transparent = "शेयर लिंक के लिए बिना पारदर्शिता वाला बैकग्राउंड चाहिए" +sign_in_required = "रिकॉर्डिंग शेयर करने के लिए आपको साइन इन करना होगा" +task_failed = "एक्सपोर्ट कार्य विफल रहा" +title = "एक्सपोर्ट" +upgrade_message = "इस सुविधा के लिए अपग्रेड किया गया प्लान चाहिए" +upgrade_title = "अपग्रेड ज़रूरी है" +upload_failed = "रिकॉर्डिंग अपलोड नहीं हो सकी" + +[editor.export.advanced] +bits_per_pixel = "बिट्स प्रति पिक्सेल" +cursor_only = "केवल कर्सर एक्सपोर्ट करें" +cursor_only_description = "पारदर्शी बैकग्राउंड पर वही कर्सर मूवमेंट और क्लिक रखता है" +custom_bitrate = "कस्टम बिटरेट इस्तेमाल हो रहा है" +force_ffmpeg = "FFmpeg डिकोडर ज़बरदस्ती इस्तेमाल करें" +force_ffmpeg_description = "हार्डवेयर डिकोडर छोड़ें (ऑटो-फ़ॉलबैक चालू)" +title = "एडवांस्ड" +transparent_mov = "पारदर्शी MOV के रूप में एक्सपोर्ट करता है। फ़ाइलें बड़ी होती हैं और कंपोज़िटिंग या एडिटिंग के लिए सबसे अच्छी हैं।" + +[editor.export.cta] +cancel_sign_in = "साइन इन रद्द करें" +clipboard = "क्लिपबोर्ड में एक्सपोर्ट करें" +create_link = "शेयर करने लायक लिंक बनाएँ" +file = "फ़ाइल में एक्सपोर्ट करें" +reupload = "उसी लिंक पर दोबारा अपलोड करें" +sign_in = "शेयर करने के लिए साइन इन करें" + +[editor.export.destination] +clipboard = "क्लिपबोर्ड" +file = "फ़ाइल" +link = "शेयर लिंक" + +[editor.export.done] +copy_link = "लिंक कॉपी करें" +instant_hint = "अगर आप रोकते ही लिंक चाहते हैं, तो अगली रिकॉर्डिंग के लिए Instant मोड इस्तेमाल करें।" +link_copied = "लिंक कॉपी हो गया" +open_file = "फ़ाइल खोलें" +open_link = "लिंक खोलें" +retry_copy = "फिर से कॉपी करें" +reuploaded = "आपका नया संपादन उसी लिंक पर तैयार है" +uploaded = "आपका Cap सफलतापूर्वक अपलोड हो गया" + +[editor.export.estimate] +gigabytes = "~{value} GB" +gigabytes_range = "~{lower}–{upper} GB" +hours = "~{value} घंटे" +hours_range = "~{lower}–{upper} घंटे" +megabytes = "~{value} MB" +megabytes_range = "~{lower}–{upper} MB" +minutes = "~{value} मिनट" +minutes_range = "~{lower}–{upper} मिनट" +seconds = "~{value} सेकंड" +seconds_range = "~{lower}–{upper} सेकंड" +under_megabyte = "< 1 MB" +under_second = "< 1 सेकंड" + +[editor.export.optimize] +description = "बहुत छोटी फ़ाइलों के लिए सॉफ़्टवेयर से दोबारा एनकोड करता है (धीमा)" +title = "फ़ाइल साइज़ ऑप्टिमाइज़ करें" + +[editor.export.phase] +clipboard_failed = "एक्सपोर्ट पूरा हुआ; क्लिपबोर्ड में कॉपी विफल रही" +complete = "एक्सपोर्ट पूरा हुआ" +copied = "क्लिपबोर्ड में कॉपी हो गया" +copying_to_clipboard = "क्लिपबोर्ड में कॉपी हो रहा है" +rendering_cursor = "कर्सर ट्रैक रेंडर हो रहा है" +rendering_gif = "GIF रेंडर हो रहा है" +rendering_video = "वीडियो रेंडर हो रहा है" +reupload_complete = "दोबारा अपलोड पूरा हुआ" +reuploading = "आपके लिंक पर दोबारा अपलोड हो रहा है" +saving_to_file = "फ़ाइल में सेव हो रहा है" +starting = "एक्सपोर्ट की तैयारी हो रही है" +upload_complete = "अपलोड पूरा हुआ" +uploading = "शेयर करने लायक लिंक बन रहा है" + +[editor.export.quality] +larger = "बड़ी फ़ाइल" +maximum = "अधिकतम" +potato = "न्यूनतम" +smaller = "छोटी फ़ाइल" +social = "सोशल" +web = "वेब" + +[editor.export.resources] +proceed = "फिर भी जारी रखें" +title = "एक्सपोर्ट संसाधन जाँचें" + +[editor.export.section] +destination = "गंतव्य" +format = "फ़ॉर्मेट" +frame_rate = "फ़्रेम रेट" +quality = "क्वालिटी" +resolution = "रेज़ोल्यूशन" + +[editor.export.stats] +calculating = "गणना हो रही है…" +duration = "अवधि" +estimated_size = "अनुमानित साइज़" +export_time = "एक्सपोर्ट समय" +output = "आउटपुट" +output_value = "{width}×{height} · {fps} fps" +refining_size = "साइज़ सटीक किया जा रहा है…" +refining_time = "समय सटीक किया जा रहा है…" +unavailable = "उपलब्ध नहीं" + +[editor.modal] +cancel_export = "एक्सपोर्ट रद्द करें" +cancel_export_message = "क्या आप वाकई एक्सपोर्ट रद्द करना चाहते हैं?" +cancel_export_title = "एक्सपोर्ट रद्द करें?" +keep_exporting = "एक्सपोर्ट जारी रखें" +recording_retained = "रिकॉर्डिंग रखी गई" + +[editor.tabs.audio] +isolation_balanced = "संतुलित" +isolation_balanced_description = "आवाज़ के स्वाभाविक विवरण के साथ ज़्यादा साफ़ अलगाव।" +isolation_light = "हल्का" +isolation_light_description = "आपकी मूल आवाज़ और कमरे की ध्वनि ज़्यादा बनी रहती है।" +isolation_strong = "मज़बूत" +isolation_strong_description = "शोर वाली जगहों के लिए ज़्यादा अलगाव। आवाज़ का टेक्सचर बदल सकता है।" +mic_volume = "माइक्रोफ़ोन वॉल्यूम" +mute = "ऑडियो म्यूट करें" +stereo_mode = "माइक्रोफ़ोन स्टीरियो मोड" +studio_sound_description = "बैकग्राउंड शोर कम करता है और आपकी आवाज़ का स्तर संतुलित करता है।" +studio_sound_save_failed = "Studio Sound का डिफ़ॉल्ट सेव नहीं हो सका" +system_volume = "सिस्टम ऑडियो वॉल्यूम" +title = "ऑडियो कंट्रोल" +use_for_new = "नई रिकॉर्डिंग के लिए इस्तेमाल करें" + +[editor.tabs.audio.stereo] +mono_left = "मोनो L" +mono_right = "मोनो R" +stereo = "स्टीरियो" + +[editor.tabs.camera] +advanced_shadow = "एडवांस्ड शैडो सेटिंग्स" +background = "बैकग्राउंड" +corner_style = "कोने की स्टाइल" +custom_position = "कस्टम स्थिति (कैनवस पर खींची गई)" +hide = "कैमरा छिपाएँ" +keep_size_during_zoom = "ज़ूम के दौरान मूल आकार बनाए रखें" +mirror = "कैमरा मिरर करें" +position = "स्थिति" +reset = "रीसेट करें" +rounded_corners = "गोल कोने" +shadow = "शैडो" +shadow_blur = "धुंधलापन" +shadow_opacity = "अपारदर्शिता" +shape_label = "आकृति" +size_during_zoom = "ज़ूम के दौरान आकार" +size_label = "आकार" +title = "कैमरा" + +[editor.tabs.camera.blur] +heavy = "ज़्यादा ब्लर" +light = "हल्का ब्लर" +off = "बंद" +remove = "बैकग्राउंड हटाएँ" + +[editor.tabs.camera.shape] +source = "मूल" +square = "वर्ग" + +[editor.tabs.captions] +active_word_highlight = "सक्रिय शब्द हाइलाइट करें" +animation_section = "एनिमेशन" +animation_style = "एनिमेशन स्टाइल" +background_color = "बैकग्राउंड रंग" +background_opacity = "बैकग्राउंड अपारदर्शिता" +background_settings = "बैकग्राउंड सेटिंग्स" +beta = "बीटा" +delete = "हटाएँ" +deleting = "हटाया जा रहा है..." +download = "{model} मॉडल डाउनलोड करें" +download_note = "आपके सिस्टम पर एक बार डाउनलोड। सभी कैप्शन लोकल रूप से सेव रहते हैं।" +downloading = "{model} डाउनलोड हो रहा है... {percent}%" +fade_duration = "फ़ेड अवधि" +font_family = "फ़ॉन्ट" +font_settings = "फ़ॉन्ट सेटिंग्स" +font_weight = "फ़ॉन्ट वज़न" +generate = "कैप्शन बनाएँ" +generate_failed = "कैप्शन नहीं बन सके: {reason}" +generating = "बनाया जा रहा है..." +highlight_color = "हाइलाइट रंग" +highlight_note = "यह Cap में कैप्शन का पहला वर्ज़न है। कुछ स्थितियों में सक्रिय शब्द की हाइलाइटिंग गलत हो सकती है। हम इसे ठीक करने पर काम कर रहे हैं और यह आने वाले वर्ज़न में जारी होगा।" +highlight_style = "हाइलाइट स्टाइल" +horizontal_position = "क्षैतिज स्थिति" +intel_unavailable = "Parakeet कैप्शन मॉडल Intel Mac पर उपलब्ध नहीं हैं। Whisper मॉडल उपलब्ध रहेंगे।" +keep_open = "मॉडल डाउनलोड होने तक Cap खुला रखें। एडिटर दोबारा लोड होने पर अपने आप फिर से कनेक्ट हो जाएगा।" +language_label = "भाषा" +model_downloaded = "{model} मॉडल डाउनलोड हो गया" +model_label = "मॉडल" +position = "स्थिति" +regenerate = "कैप्शन दोबारा बनाएँ" +size_label = "आकार" +style = "स्टाइल" +text_color = "टेक्स्ट रंग" +title = "कैप्शन" +uppercase = "बड़े अक्षर" +vertical_position = "ऊर्ध्वाधर स्थिति" + +[editor.tabs.captions.animation] +bounce = "बाउंस" +none = "कोई नहीं" +pop = "पॉप" + +[editor.tabs.captions.highlight] +color = "रंग" +pill = "पिल" + +[editor.tabs.captions.language] +ar = "अरबी" +auto = "अपने आप पहचानें" +bn = "बांग्ला" +de = "जर्मन" +en = "अंग्रेज़ी" +es = "स्पैनिश" +fa = "फ़ारसी" +fr = "फ़्रेंच" +gu = "गुजराती" +he = "हिब्रू" +hi = "हिन्दी" +it = "इतालवी" +ja = "जापानी" +ko = "कोरियाई" +mr = "मराठी" +nl = "डच" +pa = "पंजाबी" +pl = "पोलिश" +pt = "पुर्तगाली" +ru = "रूसी" +sk = "स्लोवाक" +ta = "तमिल" +te = "तेलुगु" +tr = "तुर्की" +ur = "उर्दू" +zh = "चीनी" + +[editor.tabs.captions.model] +high_accuracy = "उच्च सटीकता" +high_accuracy_description = "बड़ा डाउनलोड, ज़्यादा सटीकता" +medium = "मध्यम" +medium_description = "धीमा, ज़्यादा सटीक" +recommended = "सुझाया गया" +recommended_description = "ज़्यादातर रिकॉर्डिंग के लिए सबसे अच्छा संतुलन" +small = "छोटा" +small_description = "सबसे छोटा डाउनलोड" + +[editor.tabs.captions.preset] +classic = "क्लासिक" +classic_description = "ठोस गोल बैकग्राउंड पर साफ़ टेक्स्ट।" +highlight = "हाइलाइट" +highlight_description = "सक्रिय शब्द के पीछे पिल के साथ बोल्ड बड़े अक्षर।" +karaoke = "कराओके" +karaoke_description = "बोलने के साथ शब्द चमकते हैं।" +minimal = "मिनिमल" +minimal_description = "बिना बैकग्राउंड के हल्का आउटलाइन टेक्स्ट।" +pop = "पॉप" +pop_description = "चटक रंग के साथ उभरते मज़ेदार बड़े अक्षर।" + +[editor.tabs.corner] +rounded = "गोल" +squircle = "स्क्विर्कल" + +[editor.tabs.cursor] +friction = "घर्षण" +hide_when_idle = "निष्क्रिय होने पर छिपाएँ" +inactivity_delay = "निष्क्रियता देरी" +mass = "द्रव्यमान" +movement_style = "कर्सर मूवमेंट स्टाइल" +show = "कर्सर दिखाएँ" +size_label = "आकार" +smooth_movement = "स्मूथ मूवमेंट" +style_label = "कर्सर स्टाइल" +svg_cursors = "उच्च क्वालिटी SVG कर्सर" +tension = "तनाव" +tilt = "झुकाव" + +[editor.tabs.cursor.style] +fast = "तेज़" +fast_description = "फुर्तीला, करीब से फ़ॉलो करता है" +mellow = "नरम" +mellow_description = "संतुलित, स्वाभाविक मूवमेंट" +slow = "धीमा" +slow_description = "लंबा, बहता हुआ मूवमेंट" +smooth = "स्मूथ" +smooth_description = "मुलायम, तैरता हुआ मूवमेंट" + +[editor.tabs.keyboard] +animation = "एनिमेशन" +background_color = "बैकग्राउंड रंग" +background_opacity = "बैकग्राउंड अपारदर्शिता" +background_settings = "बैकग्राउंड सेटिंग्स" +behavior = "व्यवहार" +beta = "बीटा" +changed_during_generation = "बनाते समय टाइमलाइन या कीबोर्ड सेटिंग्स बदल गईं। फिर से कोशिश करें।" +empty_description = "रिकॉर्ड किए गए की-प्रेस से सेगमेंट बनाने के लिए \"कीबोर्ड सेगमेंट बनाएँ\" पर क्लिक करें।" +empty_title = "अभी कोई कीबोर्ड सेगमेंट नहीं।" +fade_duration = "फ़ेड अवधि" +font_family = "फ़ॉन्ट" +font_settings = "फ़ॉन्ट सेटिंग्स" +font_weight = "फ़ॉन्ट वज़न" +generate = "कीबोर्ड सेगमेंट बनाएँ" +generating = "कीबोर्ड सेगमेंट बनाए जा रहे हैं..." +grouping_threshold = "ग्रुपिंग सीमा" +linger_duration = "दिखने की अवधि" +load_failed = "रिकॉर्डिंग डेटा लोड नहीं हो सका: {error}" +no_presses = "कोई रिकॉर्ड किया गया की-प्रेस नहीं मिला।" +position = "स्थिति" +regenerate = "कीबोर्ड सेगमेंट दोबारा बनाएँ" +show_modifiers = "मॉडिफ़ायर दिखाएँ" +show_special_keys = "विशेष कुंजियाँ दिखाएँ" +size_label = "आकार" +text_color = "टेक्स्ट रंग" +timeline_unavailable = "प्रोजेक्ट टाइमलाइन उपलब्ध नहीं है।" +timeline_unreadable = "प्रोजेक्ट टाइमलाइन पढ़ी नहीं जा सकी।" +title = "कीबोर्ड दिखाएँ" +uppercase = "बड़े अक्षर" + +[editor.tabs.position] +bottom_center = "नीचे बीच में" +bottom_left = "नीचे बाएँ" +bottom_right = "नीचे दाएँ" +manual = "मैन्युअल" +top_center = "ऊपर बीच में" +top_left = "ऊपर बाएँ" +top_right = "ऊपर दाएँ" + +[editor.tabs.sync] +auto_description = "ऑडियो को वीडियो के साथ सिंक रखने के लिए Cap ने ये ऑफ़सेट अपने आप निकाले हैं। अगर अब भी कुछ गड़बड़ लगे, तो इन्हें बदलें।" +auto_synced = "अपने आप सिंक" +camera = "कैमरा ऑफ़सेट" +clip = "क्लिप {number}" +description = "अगर ऑडियो या कैमरा स्क्रीन रिकॉर्डिंग से सिंक से बाहर हो जाए, तो स्रोत ऑफ़सेट को बारीकी से ठीक करें।" +microphone = "माइक्रोफ़ोन ऑफ़सेट" +system_audio = "सिस्टम ऑडियो ऑफ़सेट" +title = "सिंक" + +[editor.tabs.weight] +bold = "बोल्ड" +medium = "मध्यम" +normal = "सामान्य" + +[editor.timeline] +add_shot = "शॉट जोड़ें" +add_shot_with_look = "शॉट जोड़ें · {look} · {seconds} सेकंड" +paused = "रुका हुआ" + +[editor.timeline.empty] +add_audio = "ऑडियो जोड़ें" +add_scene = "सीन जोड़ें" +audio = "बैकग्राउंड म्यूज़िक जोड़ें या अपना ऑडियो इम्पोर्ट करें" +captions = "कोई कैप्शन नहीं" +keyboard = "कोई कीबोर्ड इवेंट नहीं" +scene = "अपनी स्क्रीन और कैमरे के बीच लेआउट बदलें" + +[editor.timeline.picker] +audio = "बैकग्राउंड म्यूज़िक जोड़ें या अपना ऑडियो इम्पोर्ट करें।" +caption = "अपनी रिकॉर्डिंग को अपने आप ऑन-स्क्रीन सबटाइटल में बदलें।" +clip = "आपकी रिकॉर्ड की गई स्क्रीन फ़ुटेज।" +image = "अपनी रिकॉर्डिंग में इमेज जोड़ें।" +keyboard = "टाइप करते समय स्क्रीन पर की-प्रेस दिखाएँ।" +mask = "स्क्रीन के निजी हिस्सों को धुंधला या काला करें।" +scene = "अपनी स्क्रीन और कैमरे के बीच लेआउट बदलें।" +scene_unavailable = "सीन इस्तेमाल करने के लिए कैमरे के साथ रिकॉर्ड करें।" +style = "समय के साथ बैकग्राउंड, कैमरा और कर्सर सेटिंग्स बदलें।" +text_overlay = "कैनवस पर कस्टम टेक्स्ट ओवरले और टाइटल जोड़ें।" +three_d = "सीन को 3D परिप्रेक्ष्य में झुकाएँ।" +zoom = "स्मूथ ज़ूम-इन जो एक्शन के साथ चलते हैं।" + +[editor.timeline.remove] +audio = "ऑडियो ट्रैक हटाएँ।" +captions = "कैप्शन ट्रैक हटाएँ।" +clip = "क्लिप ट्रैक हटाएँ।" +image = "इमेज ट्रैक हटाएँ।" +keyboard = "कीबोर्ड ट्रैक हटाएँ।" +mask = "मास्क ट्रैक हटाएँ।" +scene = "सीन ट्रैक हटाएँ।" +style = "स्टाइल ट्रैक हटाएँ।" +text_track = "टेक्स्ट ट्रैक हटाएँ।" +three_d = "3D ट्रैक हटाएँ।" +zoom = "ज़ूम ट्रैक हटाएँ।" + +[editor.timeline.scene] +camera_only = "केवल कैमरा" +default = "डिफ़ॉल्ट" +floating = "फ़्लोटिंग" +hide_camera = "कैमरा छिपाएँ" +split_screen = "स्प्लिट स्क्रीन" + +[editor.timeline.segment] +audio = "ऑडियो" +automatic_zoom = "ऑटोमैटिक ज़ूम" +caption = "कैप्शन" +clip = "क्लिप" +cursor_hidden = "कर्सर छिपा है" +highlight = "हाइलाइट" +label = "लेबल" +manual_zoom = "मैन्युअल ज़ूम" +muted = "म्यूट" +numbered_clip = "क्लिप {number}" +sensitive = "संवेदनशील" + +[editor.timeline.track] +audio = "ऑडियो" +captions = "कैप्शन" +clip = "क्लिप" +image = "इमेज" +keyboard = "कीबोर्ड" +mask = "मास्क" +scene = "सीन" +style = "स्टाइल" +text_track = "टेक्स्ट" +video = "वीडियो" +zoom = "ज़ूम" + +[editor.window] +audio_filter = "ऑडियो" +audio_segment_changed = "यह ऑडियो सेगमेंट बदल गया है। इसे चुनें और फिर से बदलने के लिए फ़ाइल चुनें।" +crop = "क्रॉप" +crop_tooltip = "वीडियो क्रॉप करें" +no_room_for_3d_shot = "एक और 3D शॉट के लिए जगह नहीं है" +redo = "फिर से करें" +save_failed = "{path} सेव नहीं हो सका। एडिटर खुला रखें और फिर से कोशिश करें: {error}" +undo = "पूर्ववत करें" + +[editor.window.add_track] +button = "ट्रैक जोड़ें" +hide_3d = "3D ट्रैक छिपाएँ।" +hint = "अपनी टाइमलाइन में जोड़ने के लिए एक ट्रैक चुनें।" + +[editor.window.aspect] +auto = "ऑटो" +classic = "क्लासिक" +classic_ratio = "क्लासिक ⋅4:3" +square = "वर्ग" +square_ratio = "वर्ग ⋅1:1" +tall = "लंबा" +tall_ratio = "लंबा ⋅3:4" +tooltip = "आस्पेक्ट रेशियो" +vertical = "वर्टिकल" +vertical_ratio = "वर्टिकल ⋅9:16" +wide = "वाइड" +wide_ratio = "वाइड ⋅16:9" + +[editor.window.audio_import] +copy_failed = "ऑडियो फ़ाइल कॉपी नहीं हो सकी: {error}" +directory_failed = "ऑडियो फ़ोल्डर नहीं बन सका: {error}" +no_extension = "ऑडियो फ़ाइल का कोई एक्सटेंशन नहीं है" +not_found = "ऑडियो फ़ाइल नहीं मिली: {path}" +unsupported = "असमर्थित ऑडियो फ़ॉर्मेट: .{extension}" + +[editor.window.busy] +clip_import = "इस रिकॉर्डिंग को बंद करने या हटाने से पहले क्लिप इम्पोर्ट पूरा होने दें।" +export = "इस रिकॉर्डिंग को बंद करने या हटाने से पहले एक्सपोर्ट या उसका रद्द होना पूरा होने दें।" +opening = "हटाने से पहले रिकॉर्डिंग को पूरी तरह खुलने दें।" +save_before_delete_failed = "हटाने से पहले रिकॉर्डिंग सेव नहीं हो सकी: {error}" + +[editor.window.camera3d_lane] +add_shot = "+ शॉट जोड़ें" +auto_scene = "ऑटो सीन" +how_many = "कितने शॉट?" +prompt = "अपनी रिकॉर्डिंग में सिनेमैटिक 3D शॉट जोड़ें" + +[editor.window.clip_menu] +audio = "ऑडियो" +cursor = "कर्सर" +delete = "क्लिप हटाएँ" +delete_only_clip = "इकलौती क्लिप नहीं हटाई जा सकती" +hide_cursor = "कर्सर छिपाएँ" +hide_cursor_description = "यह क्लिप चलते समय फ़ेड हो जाता है" +keep_pitch = "पिच बनाए रखें" +match_speed = "स्पीड से मिलाएँ" +merge_next = "अगली क्लिप से जोड़ें" +merge_previous = "पिछली क्लिप से जोड़ें" +mute = "म्यूट" +mute_clip = "क्लिप म्यूट करें" +mute_clip_description = "इस क्लिप का ऑडियो बंद करें" +rename = "क्लिप का नाम बदलें" +speed = "स्पीड" +split = "प्लेहेड पर विभाजित करें" +split_hint = "प्लेहेड को इस क्लिप के अंदर ले जाएँ" +volume = "वॉल्यूम" + +[editor.window.frame] +add_tooltip = "फ़्रेम जोड़ें" +browser = "ब्राउज़र" +browser_description = "एड्रेस बार के साथ ब्राउज़र फ़्रेम" +button = "फ़्रेम" +dark = "डार्क" +global = "ग्लोबल फ़्रेम · जहाँ भी स्टाइल बैकग्राउंड लेती है, वहाँ लागू होता है।" +heading = "फ़्रेम" +light = "लाइट" +macbook_description = "रिकॉर्डिंग के चारों ओर लैपटॉप बेज़ेल" +macos_description = "ट्रैफ़िक लाइट बटन के साथ विंडो फ़्रेम" +none = "कोई नहीं" +none_description = "रिकॉर्डिंग को जैसी है वैसी दिखाएँ" +style_changed = "यह स्टाइल बदल गई है। फ़्रेम बंद करके फिर से खोलें।" +style_only = "सिर्फ़ स्टाइल {index} · ग्लोबल सेटिंग्स नहीं बदलतीं।" +style_only_override = "सिर्फ़ स्टाइल {index} · बदलाव करने पर इसका अपना बैकग्राउंड चालू हो जाता है।" +theme = "थीम" +title = "टाइटल" +url = "URL" +window_title_placeholder = "विंडो टाइटल" +windows_description = "विंडो कंट्रोल के साथ टाइटल बार" + +[editor.window.header] +captions = "कैप्शन" +delete_recording = "रिकॉर्डिंग हटाएँ" +export = "एक्सपोर्ट" +open_bundle = "रिकॉर्डिंग बंडल खोलें" +presets = "प्रीसेट" + +[editor.window.image] +changed = "बदलने के लिए चुनते समय यह इमेज बदल गई। इसे चुनें और फिर से कोशिश करें।" +filter = "इमेज" +recording_unavailable = "इस इमेज के लिए रिकॉर्डिंग अब उपलब्ध नहीं है। प्रोजेक्ट फिर से खोलें।" + +[editor.window.open] +damaged = "इस रिकॉर्डिंग के वीडियो ट्रैक नहीं खुल सके। बंडल खराब लगता है।" +failed = "यह रिकॉर्डिंग नहीं खुल सकी" +media_failed = "इस रिकॉर्डिंग का मीडिया पढ़ा नहीं जा सका: {error}" +meta_failed = "रिकॉर्डिंग मेटाडेटा लोड नहीं हो सका: {error}" +no_segments = "रिकॉर्डिंग में कोई सेगमेंट नहीं है। हो सकता है पहले इसे रिकवर करना पड़े।" +not_found = "वीडियो पाथ {path} नहीं मिला!" +not_studio = "Studio के अलावा की रिकॉर्डिंग संपादित नहीं की जा सकतीं" + +[editor.window.presets] +apply = "लागू करें" +create = "बनाएँ" +create_new = "नया प्रीसेट बनाएँ" +create_title = "प्रीसेट बनाएँ" +default = "डिफ़ॉल्ट" +default_badge = "डिफ़ॉल्ट" +delete = "हटाएँ" +delete_confirm = "क्या आप वाकई \"{name}\" हटाना चाहते हैं?" +delete_title = "प्रीसेट हटाएँ" +name_placeholder = "प्रीसेट का नाम" +none_saved = "कोई सेव किया गया प्रीसेट नहीं" +rename = "नाम बदलें" +rename_title = "प्रीसेट का नाम बदलें" +save_settings = "सेटिंग्स को प्रीसेट में सेव करें" +set_default = "डिफ़ॉल्ट बनाएँ" + +[editor.window.preview] +full = "पूरा" +half = "आधा" +label = "प्रीव्यू" +quarter = "चौथाई" + +[editor.window.text_menu] +copy_style = "स्टाइल कॉपी करें" +copy_style_first = "पहले कोई स्टाइल कॉपी करें" +delete_segment = "टेक्स्ट सेगमेंट हटाएँ" +delete_segments = "टेक्स्ट सेगमेंट हटाएँ" +paste_style = "स्टाइल पेस्ट करें" +paste_style_all = "सभी चुने गए पर स्टाइल पेस्ट करें" +segments = { one = "{count} टेक्स्ट सेगमेंट", other = "{count} टेक्स्ट सेगमेंट" } +untitled = "टेक्स्ट" + +[editor.window.title] +not_saved = "टाइटल सेव नहीं हुआ" +not_saved_message = "{error}\n\nआपका टाइटल अब भी एडिटर में है। इसे फिर से सेव करने की कोशिश करें।" +read_failed = "{path} का रिकॉर्डिंग टाइटल पढ़ा नहीं जा सका: {error}" +save_failed = "{path} का रिकॉर्डिंग टाइटल सेव नहीं हो सका: {error}" + +[editor.window.track] +clear_all = "सब साफ़ करें" +delete = "हटाएँ" + +[editor.window.transport] +done_splitting = "विभाजन पूरा" +play_pause = "वीडियो चलाएँ/रोकें" +split = "विभाजित करें" +split_tooltip = "विभाजित करने के लिए किसी सेगमेंट पर क्लिक करें। खत्म करने के लिए Esc दबाएँ।" +zoom_in = "ज़ूम इन" +zoom_out = "ज़ूम आउट" + +[editor.window.zoom_lane] +generate = "बनाएँ" +generating = "बनाया जा रहा है…" +no_clicks = "ज़ूम करने के लिए कोई क्लिक नहीं मिला। जोड़ने के लिए लेन पर खींचें।" +prompt = "ज़ूम अपने आप जोड़ें" + +[ui] +coming_soon = "जल्द आ रहा है" + +[ui.selection] +audio = { one = "{count} ऑडियो सेगमेंट चुना गया", other = "{count} ऑडियो सेगमेंट चुने गए" } +caption = { one = "{count} कैप्शन सेगमेंट चुना गया", other = "{count} कैप्शन सेगमेंट चुने गए" } +delete = "हटाएँ" +done = "हो गया" +image = { one = "{count} इमेज सेगमेंट चुना गया", other = "{count} इमेज सेगमेंट चुने गए" } +keyboard = { one = "{count} कीबोर्ड सेगमेंट चुना गया", other = "{count} कीबोर्ड सेगमेंट चुने गए" } +mask = { one = "{count} मास्क सेगमेंट चुना गया", other = "{count} मास्क सेगमेंट चुने गए" } +scene = { one = "{count} सीन सेगमेंट चुना गया", other = "{count} सीन सेगमेंट चुने गए" } +segments = { one = "{count} सेगमेंट चुना गया", other = "{count} सेगमेंट चुने गए" } +select_all = "सभी चुनें" +style = { one = "{count} स्टाइल सेगमेंट चुना गया", other = "{count} स्टाइल सेगमेंट चुने गए" } +text_segments = { one = "{count} टेक्स्ट सेगमेंट चुना गया", other = "{count} टेक्स्ट सेगमेंट चुने गए" } +three_d = { one = "{count} 3D सेगमेंट चुना गया", other = "{count} 3D सेगमेंट चुने गए" } +zoom = { one = "{count} चुना गया", other = "{count} चुने गए" } +zoom_all = { one = "{count} चुना गया", other = "सभी {count} चुने गए" } +zoom_some = { one = "{total} में से {count} चुना गया", other = "{total} में से {count} चुने गए" } diff --git a/apps/desktop-gpui/locales/ja.toml b/apps/desktop-gpui/locales/ja.toml index 5fe9cf767a8..f02af39458d 100644 --- a/apps/desktop-gpui/locales/ja.toml +++ b/apps/desktop-gpui/locales/ja.toml @@ -257,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 = "この録画を削除してもよろしいですか?" @@ -620,3 +1312,1566 @@ 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 = "アップロード中" + +# Editor sidebar, segment panels, colour correction, audio library and crop dialog. + +[editor.audio.library] +add_hint = "動画にオーディオや音楽などのサウンドを追加" +add_title = "オーディオを追加" +change_hint = "このセグメントに別のトラックを選ぶ" +change_title = "オーディオを変更" +done = "完了" +import = "ファイルを読み込む" + +[editor.color] +apply_to_cursor = "カーソルにも適用" +fine_tune = "色を微調整" +title = "色補正" + +[editor.color.preset.cinematic] +description = "ティールの影とオレンジのハイライトに軽い粒子" +name = "シネマティック" + +[editor.color.preset.dreamy] +description = "黒を持ち上げた柔らかく軽やかなパステル" +name = "ドリーミー" + +[editor.color.preset.frost] +description = "落ち着いた色のクールで澄んだトーン" +name = "フロスト" + +[editor.color.preset.golden] +description = "暖かいゴールデンアワーの光" +name = "ゴールデン" + +[editor.color.preset.midnight] +description = "落ち着いた色合いの暗くムーディーなティール" +name = "ミッドナイト" + +[editor.color.preset.noir] +description = "強い粒子のハイコントラストな白黒" +name = "ノワール" + +[editor.color.preset.none] +description = "元の色のまま、補正なし" +name = "なし" + +[editor.color.preset.vintage] +description = "柔らかいコントラストの色あせた暖かいフィルム調" +name = "ヴィンテージ" + +[editor.color.preset.vivid] +description = "彩度とコントラストを力強く強調" +name = "ビビッド" + +[editor.color.slider] +contrast = "コントラスト" +exposure = "露出" +fade = "フェード" +grain = "粒子" +saturation = "彩度" +split_tone = "スプリットトーン" +strength = "強さ" +temperature = "色温度" +tint = "色かぶり" +vignette = "周辺減光" + +[editor.crop] +aspect_ratio = "縦横比" +center = "中央" +dimensions = "サイズ" +editing_style = "スタイル{number}のみを編集中" +editing_style_override = "スタイル{number}のみを編集中 · 保存すると背景の上書きがオンになります" +free = "自由" +full = "全体" +hint = "ドラッグで移動 · 矢印キーで微調整 · {key}を押したままでスナップしない" +loading_frame = "フレームを読み込み中…" +position = "位置" +preview = "プレビュー" +rendering_preview = "プレビューを描画中…" +snap_to_ratios = "比率にスナップ" +style_changed = "切り抜き中にこのスタイルが変更されました。キャンセルして切り抜きを開き直してください。" +title = "切り抜き" + +[editor.panel] +delete = "削除" +done = "完了" +duration = "長さ" +edge_in = "イン" +edge_out = "アウト" +enabled = "有効" +fade_duration = "フェードの長さ" +flip_horizontally = "左右反転" +flip_vertically = "上下反転" +name = "名前" +opacity = "不透明度" + +[editor.panel.audio] +change = "変更" +change_hint = "タップしてトラックを変更" +fade_in = "フェードイン" +fade_out = "フェードアウト" +fallback_name = "オーディオ" +title = "オーディオ{number}" +volume = "音量" + +[editor.panel.camera3d] +already_still = "すでに静止ショットです" +amount = "量" +angles = "アングル" +auto_scene = "自動シーン" +auto_scene_tooltip = "トラック上のすべての3Dショットを作り直します" +bokeh = "ボケ" +camera = "カメラ" +delete_tooltip = "このショットを削除" +depth_blur = "被写界深度ぼかし" +distance = "距離" +drag_to_orbit = "ドラッグで回転" +ease_in = "イーズイン" +ease_out = "イーズアウト" +end_same_as_start = "終了 · 開始と同じ" +fine_tune = "微調整" +focus = "フォーカス" +header = "3Dショット · {look} · {duration}秒" +hold_opening = "ショット全体で開始時のポーズを保持" +lens = "レンズ" +look_title = "ルック" +motion_style = "動きのスタイル" +moves = "動き" +nothing_to_swap = "静止ショットでは入れ替えるものがありません" +play_shot = "再生" +play_shot_tooltip = "このショットを再生" +pose_end = "終了 · {time}" +pose_start = "開始 · {time}" +reset_camera = "カメラをリセット" +roll = "ロール" +rotate_x = "回転X" +rotate_y = "回転Y" +shift_x = "シフトX" +shift_y = "シフトY" +shots = { other = "{count}ショット" } +still_shot = "静止ショット" +swap = "開始と終了を入れ替える" +timing_advanced = "タイミングと詳細" +timing_summary = "{easing} · レンズ{fov}" + +[editor.panel.camera3d.blur] +angle = "角度" +falloff = "減衰" +focus_size = "焦点サイズ" +focus_x = "焦点X" +focus_y = "焦点Y" +position = "位置" +scan = "スキャン" +strength = "強さ" + +[editor.panel.camera3d.easing] +ease_in = "イーズイン" +ease_out = "イーズアウト" +linear = "リニア" +smooth = "スムーズ" + +[editor.panel.camera3d.focus_mode] +directional = "方向" +radial = "放射" +tilt_shift = "ティルトシフト" + +[editor.panel.camera3d.look] +center = "中央" +close_up = "クローズアップ" +drift_down = "ゆっくり下降" +glide_across = "横に滑る" +low_angle = "ローアングル" +perspective = "パース" +pull_back = "引き" +rising_sweep = "上昇スイープ" +slide_by = "スライド" +spotlight = "スポットライト" +tilt_away = "傾けて離す" +top_down = "真上から" +unfold = "展開" + +[editor.panel.camera3d.sequence] +product_tour = "製品ツアー" +punch_in = "パンチイン" +showcase = "ショーケース" + +[editor.panel.camera3d.shot] +custom = "カスタムの動き" +still = "静止ショット" + +[editor.panel.caption] +title = "字幕{number}" + +[editor.panel.font] +system_mono = "システムMono" +system_sans = "システムSans" +system_serif = "システムSerif" + +[editor.panel.font_weight] +black = "ブラック" +bold = "太字" +custom = "カスタム({weight})" +extra_bold = "極太" +light = "細字" +medium = "中字" +regular = "標準" +semibold = "やや太字" + +[editor.panel.image] +arrange = "キャンバス上で配置" +arrange_help = "画像をドラッグして移動します。角を引っ張るとサイズを変更でき、下の回転コントロールで回転できます。" +center = "キャンバスの中央に配置" +lock_aspect = "縦横比を固定" +missing = "画像ファイルが見つかりません。置き換えると、タイミングと変形を保ったままこのセグメントを復元できます。" +replace = "画像を置き換え" +rotation = "回転" +rounding = "角丸" + +[editor.panel.keyboard] +title = "キーボード{number}" + +[editor.panel.mask] +blur = "ぼかし" +effect = "エフェクト" +highlight = "強調" +outside_darkness = "外側の暗さ" +pixel_size = "ピクセルサイズ" +pixelate = "モザイク" +sensitive = "機密" +title = "マスク{number}" + +[editor.panel.scene] +camera_layout = "カメラのレイアウト" +camera_position = "カメラの位置" +camera_zoom = "カメラのズーム" +screen_position = "画面の位置" +screen_zoom = "画面のズーム" +transition = "トランジション" + +[editor.panel.scene.description] +camera_only = "カメラの映像のみを表示" +default = "画面とカメラの両方を表示" +floating = "画面とカメラが角丸のカードとして背景の上に並んで浮かびます" +hide_camera = "画面の録画のみを表示" +split_screen = "画面とカメラを横に並べて表示(縦向きでは自動で縦に並びます)" + +[editor.panel.scene.mode] +camera_only = "カメラのみ" +default = "デフォルト" +floating = "フローティング" +hide_camera = "カメラを隠す" +split_screen = "分割画面" + +[editor.panel.style] +camera_only_background = "カメラのみの背景" +camera_only_help = "カメラのみのシーンを使ってください。余白を付けるとカメラの周りに背景が見えます。" +camera_only_padding = "カメラのみの余白" +crop_height = "高さ" +crop_left = "左" +crop_title = "画面の切り抜き(元のピクセル)" +crop_top = "上" +crop_width = "幅" +edit_background = "背景とフレームを編集" +edit_camera = "カメラを編集" +edit_cursor = "カーソルを編集" +help = "上書きはこのセグメントの間だけ適用されます。グループをオンにすると全体設定がコピーされます。" +reset_crop = "切り抜きをリセット" + +[editor.panel.text_animation] +bounce = "バウンス" +fade = "フェード" +letters = "文字ごと" +none = "なし" +pop = "ポップ" +slide_down = "下へスライド" +slide_left = "左へスライド" +slide_right = "右へスライド" +slide_up = "上へスライド" +tracking = "字間" +typewriter = "タイプライター" +wipe = "ワイプ" +words = "単語ごと" +zoom = "ズーム" + +[editor.panel.text_background] +box_shape = "ボックス" +highlight = "ハイライト" +none = "なし" +pill = "ピル" + +[editor.panel.text_layout] +fullscreen = "全画面" +overlay = "オーバーレイ" + +[editor.panel.text_preset.group] +all = "すべて" +callouts = "コールアウト" +code = "コード" +lower_thirds = "テロップ" +statements = "ステートメント" +titles = "タイトル" + +[editor.panel.text_preset.name] +caption = "キャプション" +cinematic = "シネマティック" +code = "コード" +gradient = "グラデーション" +headline = "見出し" +highlight = "ハイライト" +kicker = "キッカー" +label = "ラベル" +lower_third = "テロップ" +name_tag = "名札" +neon = "ネオン" +quote = "引用" +stat = "大きな数字" +sticker = "ステッカー" +title = "タイトル" +typewriter = "タイプライター" + +[editor.panel.text_preset.sample] +caption = "Capで録画" +cinematic = "第一章" +gradient = "美しいテキスト" +headline = "もっと速くリリース" +highlight = "大事なところ" +kicker = "新機能" +label = "プロのヒント" +name_tag = "Richie · 創業者" +neon = "グローアップ" +quote = "「軽やかに感じさせて」" +sticker = "ドーン!" +title = "Capのご紹介" +typewriter = "タイプ中…" + +[editor.panel.text_segment] +animation = "アニメーション" +background = "背景" +color = "色" +font = "フォント" +font_size = "サイズ" +fullscreen_help = "テキストの表示中は動画を一時停止し、その後止めた位置から再開します。" +glow = "グロー" +gradient = "グラデーション" +layout = "レイアウト" +letter_spacing = "字間" +line_height = "行の高さ" +look = "見た目" +outline = "アウトライン" +screen_transition = "画面の切り替え" +shadow = "影" +style = "スタイル" +title = "テキスト{number}" + +[editor.panel.timing] +end = "終了" +start = "開始" +title = "タイミング" +to = "〜" + +[editor.panel.zoom] +amount = "ズーム量" +auto = "自動" +auto_help = "自動ズームは画面上のカーソルを追いかけます。" +auto_locked = "自動モードにはカーソルのキャプチャが必要です。「設定 → 一般」で「カーソルの個別キャプチャ(Studio)」をオンにしてください。" +how_it_works = "仕組み" +manual = "手動" +manual_help = "手動ズームは下で選んだ位置に固定されます。" +mixed = "混在" +mode = "ズームモード" +positions_mixed = "セグメントごとにズーム位置が異なります。ドラッグするとすべて同じ位置に移動します。" +title = "ズーム{number}" + +[editor.sidebar] +desktop_import_failed = "デスクトップの壁紙を読み込めませんでした" +importing_image = "画像を読み込み中…" + +[editor.sidebar.animated_gradient] +add_colour = "色を追加" +colours = "色" +delete = "削除" +delete_failed = "このグラデーションを削除できませんでした。もう一度お試しください。" +fine_tune = "微調整" +limit_reached = { other = "保存できるグラデーションは{count}個までです。追加するには1つ削除してください。" } +motion = "動き" +name_placeholder = "このグラデーションの名前" +name_required = "このグラデーションの名前を入力してください。" +presets = "プリセット" +randomize = "ランダム" +remember_failed = "アニメーショングラデーションの設定を保存できませんでした。" +remove_colour = "色を削除" +reset_tooltip = "微調整をリセット" +save_failed = "このグラデーションを保存できませんでした。もう一度お試しください。" +save_tooltip = "現在のグラデーションを保存" +saved = "保存済み" +still = "静止" + +[editor.sidebar.animated_gradient.control] +contrast = "コントラスト" +curvature = "曲率" +detail = "ディテール" +direction = "方向" +exposure = "露出" +flow_scale = "流れの大きさ" +flow_strength = "流れの強さ" +grain_amount = "粒子の量" +grain_size = "粒子の大きさ" +highlights = "ハイライト" +motion_speed = "動きの速さ" +relief = "起伏" +ripple_size = "波紋の大きさ" +shading = "陰影" +vibrance = "自然な彩度" + +[editor.sidebar.animated_gradient.group] +animation = "アニメーション" +colour = "色" +flow = "流れ" +lighting = "ライティング" +texture = "テクスチャ" + +[editor.sidebar.background] +title = "背景" + +[editor.sidebar.border] +color = "枠線の色" +opacity = "枠線の不透明度" +title = "枠線" +width = "枠線の太さ" + +[editor.sidebar.corner] +rounded = "角丸" +squircle = "スクワークル" +title = "角のスタイル" + +[editor.sidebar.cursor] +circle = "円" +click_ripple = "クリックの波紋" +color = "色" +default = "デフォルト" +description_circle = "すべてのカーソルを円に置き換えます。" +description_default = "録画時のカーソルの形と見た目をそのまま使います。" +description_macos = "従来のmacOSの見た目です。カスタムカーソルは録画時の形のままです。" +description_tahoe = "macOS Tahoeの見た目です。カスタムカーソルは録画時の形のままです。" +description_windows = "Windowsの見た目です。カスタムカーソルは録画時の形のままです。" +duration = "長さ" +recorded = "録画したカーソル" +recorded_tooltip = "このカーソルで録画" +ripple_size = "サイズ" +strength = "強さ" + +[editor.sidebar.desktop] +import = "デスクトップの背景を読み込む" +importing = "読み込み中..." +prompt = "デスクトップの壁紙を使う" +reimport = "再読み込み" + +[editor.sidebar.gradient] +angle = "角度" +from = "開始" +grain_scale = "粒子の大きさ" +noise = "ノイズ" +to = "終了" + +[editor.sidebar.image] +prompt = "クリックして選択するか画像をドラッグ&ドロップ" + +[editor.sidebar.layout] +background_blur = "背景のぼかし" +custom_position = "カスタムの画面位置(キャンバスでドラッグ)" +motion_blur = "モーションブラー" +padding = "余白" +rounded_corners = "角丸" +title = "レイアウト" + +[editor.sidebar.notch] +description = "録画の上にMacBookのノッチを描きます。ノッチのあるMacで録画した場合はその寸法を使います。それ以外の場合は下のサイズから始めて、合うように調整してください。" +height = "ノッチの高さ" +position = "ノッチの位置" +title = "MacBookのノッチ" +width = "ノッチの幅" + +[editor.sidebar.shadow] +advanced = "影の詳細設定" +blur = "ぼかし" +opacity = "不透明度" +shadow_size = "サイズ" +title = "影" + +[editor.sidebar.source] +animated = "アニメ" +color = "色" +desktop = "デスクトップ" +gradient = "グラデ" +image = "画像" +none = "なし" +wallpaper = "壁紙" + +[editor.sidebar.style_group] +back = "スタイルに戻る" +background = "背景とフレーム" +camera = "カメラ" +cursor = "カーソル" +editing_note = "このセグメントのみを編集しています。全体設定は変わりません。" +title = "スタイル{number} · {group}" + +[editor.sidebar.tab] +audio = "オーディオ" +background = "背景" +camera = "カメラ" +captions = "字幕" +cursor = "カーソル" +keyboard = "キーボード" + +[editor.sidebar.wallpaper_theme] +blue = "ブルー" +cities = "都市" +dark = "ダーク" +orange = "オレンジ" +purple = "パープル" + +[editor.canvas] +camera = "カメラ" +camera_follows_zoom = "カメラのサイズはズームに合わせて変わります。ドラッグで移動できます" +edit_zoom = "ズームを編集" +image = "画像" +mask = "マスク" +screen = "画面" +screen_locked = "ズームが有効な間は画面が固定されます" +text_item = "テキスト" + +[editor.clips] +append_error = "クリップを追加できませんでした:{message}" +append_error_title = "録画エラー" +back = "エディタに戻る" +cap_filter = "Cap録画" +clip = "クリップ{number}" +empty_description = "クリップを録画するか読み込むと、ここに表示されます。" +empty_title = "クリップはまだありません" +import = "読み込む" +import_error = "クリップを読み込めませんでした:{message}" +import_error_title = "読み込みエラー" +import_existing = "既存の録画" +import_mp4 = "MP4ビデオ…" +importing = "読み込み中…" +mp4_filter = "MP4ビデオ" +prepare_controls_failed = "録画コントロールを準備できませんでした:{error}" +record_new = "新しいクリップを録画" +save_before_recording_failed = "録画前に現在のプロジェクトを保存できませんでした:{error}" +split = "分割{number}" +title = "クリップ" + +[editor.clips.error] +cancelled = "読み込みをキャンセルしました" +cannot_decode = "ビデオ{path}をデコードできません:{error}" +in_use = "この録画の使用中は読み込めません:{error}" +instant_source = "Instant録画はまだプロジェクトに読み込めません" +instant_target = "Instantモードの録画は編集できません" +into_itself = "録画をそれ自体に読み込むことはできません" +missing_video = "ビデオファイルがありません:{path}" +no_duration = "読み込んだビデオに有効な長さがありません" +not_a_project = "Capのプロジェクトフォルダを選択してください" +source_empty = "読み込み元のCapプロジェクトに録画セグメントがありません" +source_failed = "読み込み元のCapプロジェクトでエラーが発生しました:{error}" +source_recording = "読み込み元のCapプロジェクトはまだ録画中です" +source_unimportable = "読み込み元のCapプロジェクトに読み込める録画セグメントがありません" +unfinished_target = "クリップを読み込む前に、この録画を完了するか復元してください" +unsupported_video = "対応していないビデオファイルです:{path}" + +[editor.export] +back = "エディタに戻る" +existing_link_description = "再アップロードすると、このリンクのビデオが最新の編集内容に置き換わります。リンクを知っている全員に更新版が表示されます。" +existing_link_title = "既存のリンクを更新" +failed = "書き出しに失敗しました" +fps = "{fps} FPS" +frames = { other = "{rendered} / {count}フレーム" } +generating_preview = "プレビューを生成中…" +link_unavailable_cursor = "カーソルのみの書き出しは、ファイルかクリップボードにのみ保存できます。" +link_unavailable_transparent = "透過の書き出しは、ファイルかクリップボードにのみ保存できます。" +organization = "組織" +preview = "プレビュー" +preview_info = "ビデオからレンダリングしたフレームです。設定を調整すると、最終的な書き出しの品質を確認できます。" +preview_unavailable = "プレビューを表示できません" +reupload = "再アップロード" +reupload_tooltip = "最新の編集内容を同じリンクにアップロード" +reupload_transparent = "共有リンクには透過のない背景が必要です" +sign_in_required = "録画を共有するにはサインインが必要です" +task_failed = "書き出し処理に失敗しました" +title = "書き出し" +upgrade_message = "この機能を使うにはプランのアップグレードが必要です" +upgrade_title = "アップグレードが必要です" +upload_failed = "録画をアップロードできませんでした" + +[editor.export.advanced] +bits_per_pixel = "ピクセルあたりのビット数" +cursor_only = "カーソルのみを書き出す" +cursor_only_description = "同じカーソルの動きとクリックを透過背景の上に保持します" +custom_bitrate = "カスタムビットレートを使用中" +force_ffmpeg = "FFmpegデコーダを強制" +force_ffmpeg_description = "ハードウェアデコーダを使わない(自動フォールバック有効)" +title = "詳細設定" +transparent_mov = "透過MOVとして書き出します。ファイルが大きいため、合成や編集に適しています。" + +[editor.export.cta] +cancel_sign_in = "サインインをキャンセル" +clipboard = "クリップボードに書き出す" +create_link = "共有リンクを作成" +file = "ファイルに書き出す" +reupload = "同じリンクに再アップロード" +sign_in = "サインインして共有" + +[editor.export.destination] +clipboard = "クリップボード" +file = "ファイル" +link = "共有リンク" + +[editor.export.done] +copy_link = "リンクをコピー" +instant_hint = "停止した瞬間にリンクが欲しい場合は、次の録画でInstantモードを使ってください。" +link_copied = "リンクをコピーしました" +open_file = "ファイルを開く" +open_link = "リンクを開く" +retry_copy = "コピーを再試行" +reuploaded = "最新の編集内容が同じリンクで利用できます" +uploaded = "Capのアップロードが完了しました" + +[editor.export.estimate] +gigabytes = "約{value} GB" +gigabytes_range = "約{lower}~{upper} GB" +hours = "約{value}時間" +hours_range = "約{lower}~{upper}時間" +megabytes = "約{value} MB" +megabytes_range = "約{lower}~{upper} MB" +minutes = "約{value}分" +minutes_range = "約{lower}~{upper}分" +seconds = "約{value}秒" +seconds_range = "約{lower}~{upper}秒" +under_megabyte = "1 MB未満" +under_second = "1秒未満" + +[editor.export.optimize] +description = "ソフトウェアで再エンコードし、ファイルを大幅に小さくします(低速)" +title = "ファイルサイズを最適化" + +[editor.export.phase] +clipboard_failed = "書き出しは完了しましたが、クリップボードへのコピーに失敗しました" +complete = "書き出しが完了しました" +copied = "クリップボードにコピーしました" +copying_to_clipboard = "クリップボードにコピー中" +rendering_cursor = "カーソルトラックをレンダリング中" +rendering_gif = "GIFをレンダリング中" +rendering_video = "ビデオをレンダリング中" +reupload_complete = "再アップロードが完了しました" +reuploading = "リンクに再アップロード中" +saving_to_file = "ファイルに保存中" +starting = "書き出しを準備中" +upload_complete = "アップロードが完了しました" +uploading = "共有リンクを作成中" + +[editor.export.quality] +larger = "大きいファイル" +maximum = "最高" +potato = "最低" +smaller = "小さいファイル" +social = "SNS" +web = "Web" + +[editor.export.resources] +proceed = "このまま続行" +title = "書き出しのリソースを確認" + +[editor.export.section] +destination = "書き出し先" +format = "形式" +frame_rate = "フレームレート" +quality = "品質" +resolution = "解像度" + +[editor.export.stats] +calculating = "計算中…" +duration = "長さ" +estimated_size = "推定サイズ" +export_time = "書き出し時間" +output = "出力" +output_value = "{width}×{height} · {fps} fps" +refining_size = "サイズを精査中…" +refining_time = "時間を精査中…" +unavailable = "利用不可" + +[editor.modal] +cancel_export = "書き出しをキャンセル" +cancel_export_message = "書き出しをキャンセルしてもよろしいですか?" +cancel_export_title = "書き出しをキャンセルしますか?" +keep_exporting = "書き出しを続ける" +recording_retained = "録画を保持しました" + +[editor.tabs.audio] +isolation_balanced = "バランス" +isolation_balanced_description = "自然な声の質感を保ちながら、よりはっきり分離します。" +isolation_light = "弱" +isolation_light_description = "元の声や部屋の響きをより多く残します。" +isolation_strong = "強" +isolation_strong_description = "騒がしい場所向けに強く分離します。声の質感が変わることがあります。" +mic_volume = "マイクの音量" +mute = "音声をミュート" +stereo_mode = "マイクのステレオモード" +studio_sound_description = "背景ノイズを抑え、声の音量を整えます。" +studio_sound_save_failed = "Studio Soundのデフォルトを保存できませんでした" +system_volume = "システム音声の音量" +title = "音声コントロール" +use_for_new = "新しい録画で使用" + +[editor.tabs.audio.stereo] +mono_left = "モノラル L" +mono_right = "モノラル R" +stereo = "ステレオ" + +[editor.tabs.camera] +advanced_shadow = "影の詳細設定" +background = "背景" +corner_style = "角のスタイル" +custom_position = "カスタム位置(キャンバス上でドラッグ)" +hide = "カメラを非表示" +keep_size_during_zoom = "ズーム中も元のサイズを保つ" +mirror = "カメラを反転" +position = "位置" +reset = "リセット" +rounded_corners = "角の丸み" +shadow = "影" +shadow_blur = "ぼかし" +shadow_opacity = "不透明度" +shape_label = "形" +size_during_zoom = "ズーム中のサイズ" +size_label = "サイズ" +title = "カメラ" + +[editor.tabs.camera.blur] +heavy = "強いぼかし" +light = "弱いぼかし" +off = "オフ" +remove = "背景を削除" + +[editor.tabs.camera.shape] +source = "元の形" +square = "正方形" + +[editor.tabs.captions] +active_word_highlight = "話している単語をハイライト" +animation_section = "アニメーション" +animation_style = "アニメーションのスタイル" +background_color = "背景色" +background_opacity = "背景の不透明度" +background_settings = "背景の設定" +beta = "ベータ" +delete = "削除" +deleting = "削除中..." +download = "{model}モデルをダウンロード" +download_note = "システムへのダウンロードは1回だけです。字幕はすべてローカルに保存されます。" +downloading = "{model}をダウンロード中... {percent}%" +fade_duration = "フェード時間" +font_family = "フォント" +font_settings = "フォントの設定" +font_weight = "フォントの太さ" +generate = "字幕を生成" +generate_failed = "字幕を生成できませんでした:{reason}" +generating = "生成中..." +highlight_color = "ハイライトの色" +highlight_note = "これはCapの字幕機能の最初のバージョンです。話している単語のハイライトは、状況によって不正確になる場合があります。修正に取り組んでおり、今後のバージョンで公開する予定です。" +highlight_style = "ハイライトのスタイル" +horizontal_position = "水平位置" +intel_unavailable = "IntelのMacではParakeetの字幕モデルを利用できません。Whisperモデルは引き続き利用できます。" +keep_open = "モデルのダウンロード中はCapを開いたままにしてください。エディタを再読み込みしても自動的に再接続されます。" +language_label = "言語" +model_downloaded = "{model}モデルをダウンロード済み" +model_label = "モデル" +position = "位置" +regenerate = "字幕を再生成" +size_label = "サイズ" +style = "スタイル" +text_color = "テキストの色" +title = "字幕" +uppercase = "大文字" +vertical_position = "垂直位置" + +[editor.tabs.captions.animation] +bounce = "バウンス" +none = "なし" +pop = "ポップ" + +[editor.tabs.captions.highlight] +color = "色" +pill = "ピル" + +[editor.tabs.captions.language] +ar = "アラビア語" +auto = "自動検出" +bn = "ベンガル語" +de = "ドイツ語" +en = "英語" +es = "スペイン語" +fa = "ペルシア語" +fr = "フランス語" +gu = "グジャラート語" +he = "ヘブライ語" +hi = "ヒンディー語" +it = "イタリア語" +ja = "日本語" +ko = "韓国語" +mr = "マラーティー語" +nl = "オランダ語" +pa = "パンジャブ語" +pl = "ポーランド語" +pt = "ポルトガル語" +ru = "ロシア語" +sk = "スロバキア語" +ta = "タミル語" +te = "テルグ語" +tr = "トルコ語" +ur = "ウルドゥー語" +zh = "中国語" + +[editor.tabs.captions.model] +high_accuracy = "高精度" +high_accuracy_description = "ダウンロードは大きめ、精度は高め" +medium = "中" +medium_description = "低速だがより正確" +recommended = "推奨" +recommended_description = "ほとんどの録画に最適なバランス" +small = "小" +small_description = "最小のダウンロード" + +[editor.tabs.captions.preset] +classic = "クラシック" +classic_description = "角丸の単色背景にすっきりしたテキスト。" +highlight = "ハイライト" +highlight_description = "太字の大文字で、話している単語の後ろにピルを表示。" +karaoke = "カラオケ" +karaoke_description = "発話に合わせて単語が光ります。" +minimal = "ミニマル" +minimal_description = "背景なしの控えめな縁取りテキスト。" +pop = "ポップ" +pop_description = "鮮やかなアクセントで飛び出す遊び心のある大文字。" + +[editor.tabs.corner] +rounded = "角丸" +squircle = "スクワークル" + +[editor.tabs.cursor] +friction = "摩擦" +hide_when_idle = "操作がないときは非表示" +inactivity_delay = "非表示までの時間" +mass = "質量" +movement_style = "カーソルの動きのスタイル" +show = "カーソルを表示" +size_label = "サイズ" +smooth_movement = "なめらかな動き" +style_label = "カーソルのスタイル" +svg_cursors = "高品質なSVGカーソル" +tension = "張力" +tilt = "傾き" + +[editor.tabs.cursor.style] +fast = "速い" +fast_description = "きびきびと、ぴったり追従" +mellow = "穏やか" +mellow_description = "バランスの取れた自然な動き" +slow = "遅い" +slow_description = "長く漂うような動き" +smooth = "なめらか" +smooth_description = "柔らかく浮遊するような動き" + +[editor.tabs.keyboard] +animation = "アニメーション" +background_color = "背景色" +background_opacity = "背景の不透明度" +background_settings = "背景の設定" +behavior = "動作" +beta = "ベータ" +changed_during_generation = "生成中にタイムラインまたはキーボードの設定が変更されました。もう一度お試しください。" +empty_description = "「キーボードセグメントを生成」をクリックすると、記録されたキー入力からセグメントを作成します。" +empty_title = "キーボードセグメントはまだありません。" +fade_duration = "フェード時間" +font_family = "フォント" +font_settings = "フォントの設定" +font_weight = "フォントの太さ" +generate = "キーボードセグメントを生成" +generating = "キーボードセグメントを生成中..." +grouping_threshold = "グループ化のしきい値" +linger_duration = "表示を残す時間" +load_failed = "録画データを読み込めませんでした:{error}" +no_presses = "記録されたキー入力が見つかりませんでした。" +position = "位置" +regenerate = "キーボードセグメントを再生成" +show_modifiers = "修飾キーを表示" +show_special_keys = "特殊キーを表示" +size_label = "サイズ" +text_color = "テキストの色" +timeline_unavailable = "プロジェクトのタイムラインを利用できません。" +timeline_unreadable = "プロジェクトのタイムラインを読み込めませんでした。" +title = "キーボードを表示" +uppercase = "大文字" + +[editor.tabs.position] +bottom_center = "下中央" +bottom_left = "左下" +bottom_right = "右下" +manual = "手動" +top_center = "上中央" +top_left = "左上" +top_right = "右上" + +[editor.tabs.sync] +auto_description = "Capは音声をビデオと同期させるため、これらのオフセットを自動で計算しました。まだずれて聞こえる場合は調整してください。" +auto_synced = "自動同期済み" +camera = "カメラのオフセット" +clip = "クリップ{number}" +description = "音声やカメラが画面録画とずれる場合は、ソースのオフセットを微調整してください。" +microphone = "マイクのオフセット" +system_audio = "システム音声のオフセット" +title = "同期" + +[editor.tabs.weight] +bold = "太字" +medium = "中" +normal = "標準" + +[editor.timeline] +add_shot = "ショットを追加" +add_shot_with_look = "ショットを追加 · {look} · {seconds}秒" +paused = "一時停止中" + +[editor.timeline.empty] +add_audio = "音声を追加" +add_scene = "シーンを追加" +audio = "BGMを追加するか、自分の音声を読み込みます" +captions = "字幕なし" +keyboard = "キーボードイベントなし" +scene = "画面とカメラのレイアウトを切り替えます" + +[editor.timeline.picker] +audio = "BGMを追加するか、自分の音声を読み込みます。" +caption = "録画を自動で文字起こしし、画面上の字幕にします。" +clip = "録画した画面の映像です。" +image = "録画に画像を追加します。" +keyboard = "入力中のキー操作を画面に表示します。" +mask = "画面のプライベートな部分をぼかすか黒く塗りつぶします。" +scene = "画面とカメラのレイアウトを切り替えます。" +scene_unavailable = "シーンを使うにはカメラ付きで録画してください。" +style = "背景、カメラ、カーソルの設定を時間に合わせて変更します。" +text_overlay = "キャンバスにカスタムのテキストやタイトルを重ねます。" +three_d = "シーンを3Dの遠近感で傾けます。" +zoom = "動きに合わせたなめらかなズームイン。" + +[editor.timeline.remove] +audio = "音声トラックを削除します。" +captions = "字幕トラックを削除します。" +clip = "クリップトラックを削除します。" +image = "画像トラックを削除します。" +keyboard = "キーボードトラックを削除します。" +mask = "マスクトラックを削除します。" +scene = "シーントラックを削除します。" +style = "スタイルトラックを削除します。" +text_track = "テキストトラックを削除します。" +three_d = "3Dトラックを削除します。" +zoom = "ズームトラックを削除します。" + +[editor.timeline.scene] +camera_only = "カメラのみ" +default = "デフォルト" +floating = "フローティング" +hide_camera = "カメラを非表示" +split_screen = "分割画面" + +[editor.timeline.segment] +audio = "音声" +automatic_zoom = "自動ズーム" +caption = "字幕" +clip = "クリップ" +cursor_hidden = "カーソル非表示" +highlight = "強調" +label = "ラベル" +manual_zoom = "手動ズーム" +muted = "ミュート" +numbered_clip = "クリップ{number}" +sensitive = "機密" + +[editor.timeline.track] +audio = "音声" +captions = "字幕" +clip = "クリップ" +image = "画像" +keyboard = "キーボード" +mask = "マスク" +scene = "シーン" +style = "スタイル" +text_track = "テキスト" +video = "ビデオ" +zoom = "ズーム" + +[editor.window] +audio_filter = "音声" +audio_segment_changed = "この音声セグメントは変更されました。選択して、もう一度置き換えるファイルを選んでください。" +crop = "切り抜き" +crop_tooltip = "ビデオを切り抜く" +no_room_for_3d_shot = "別の3Dショットを入れる余地がありません" +redo = "やり直す" +save_failed = "{path}を保存できませんでした。エディタを開いたまま、もう一度お試しください:{error}" +undo = "取り消す" + +[editor.window.add_track] +button = "トラックを追加" +hide_3d = "3Dトラックを非表示にします。" +hint = "タイムラインに追加するトラックを選んでください。" + +[editor.window.aspect] +auto = "自動" +classic = "クラシック" +classic_ratio = "クラシック ⋅4:3" +square = "正方形" +square_ratio = "正方形 ⋅1:1" +tall = "縦長" +tall_ratio = "縦長 ⋅3:4" +tooltip = "アスペクト比" +vertical = "縦" +vertical_ratio = "縦 ⋅9:16" +wide = "ワイド" +wide_ratio = "ワイド ⋅16:9" + +[editor.window.audio_import] +copy_failed = "音声ファイルをコピーできませんでした:{error}" +directory_failed = "音声フォルダを作成できませんでした:{error}" +no_extension = "音声ファイルに拡張子がありません" +not_found = "音声ファイルが見つかりません:{path}" +unsupported = "対応していない音声形式です:.{extension}" + +[editor.window.busy] +clip_import = "この録画を閉じるか削除する前に、クリップの読み込みが終わるまでお待ちください。" +export = "この録画を閉じるか削除する前に、書き出しまたはそのキャンセルが終わるまでお待ちください。" +opening = "削除する前に、録画が開き終わるまでお待ちください。" +save_before_delete_failed = "削除前に録画を保存できませんでした:{error}" + +[editor.window.camera3d_lane] +add_shot = "+ ショットを追加" +auto_scene = "自動シーン" +how_many = "ショットはいくつ?" +prompt = "録画に映画のような3Dショットを追加" + +[editor.window.clip_menu] +audio = "音声" +cursor = "カーソル" +delete = "クリップを削除" +delete_only_clip = "唯一のクリップは削除できません" +hide_cursor = "カーソルを非表示" +hide_cursor_description = "このクリップの再生中はフェードアウトします" +keep_pitch = "ピッチを保持" +match_speed = "速度に合わせる" +merge_next = "次のクリップと結合" +merge_previous = "前のクリップと結合" +mute = "ミュート" +mute_clip = "クリップをミュート" +mute_clip_description = "このクリップの音声を消します" +rename = "クリップ名を変更" +speed = "速度" +split = "再生ヘッドで分割" +split_hint = "再生ヘッドをこのクリップ内に移動してください" +volume = "音量" + +[editor.window.frame] +add_tooltip = "フレームを追加" +browser = "ブラウザ" +browser_description = "アドレスバー付きのブラウザ枠" +button = "フレーム" +dark = "ダーク" +global = "グローバルフレーム · スタイルが背景を継承する箇所すべてに適用されます。" +heading = "フレーム" +light = "ライト" +macbook_description = "録画の周りにノートパソコンのベゼル" +macos_description = "信号ボタン付きのウィンドウ枠" +none = "なし" +none_description = "録画をそのまま表示" +style_changed = "このスタイルは変更されました。フレームを閉じて開き直してください。" +style_only = "スタイル{index}のみ · グローバル設定は変わりません。" +style_only_override = "スタイル{index}のみ · 編集すると独自の背景設定が有効になります。" +theme = "テーマ" +title = "タイトル" +url = "URL" +window_title_placeholder = "ウィンドウタイトル" +windows_description = "ウィンドウボタン付きのタイトルバー" + +[editor.window.header] +captions = "字幕" +delete_recording = "録画を削除" +export = "書き出し" +open_bundle = "録画バンドルを開く" +presets = "プリセット" + +[editor.window.image] +changed = "置き換えを選んでいる間にこの画像が変更されました。選択してもう一度お試しください。" +filter = "画像" +recording_unavailable = "この画像の録画は利用できなくなりました。プロジェクトを開き直してください。" + +[editor.window.open] +damaged = "この録画のビデオトラックを開けませんでした。バンドルが破損しているようです。" +failed = "この録画を開けませんでした" +media_failed = "この録画のメディアを読み込めませんでした:{error}" +meta_failed = "録画のメタデータを読み込めませんでした:{error}" +no_segments = "録画にセグメントがありません。先に復元が必要な可能性があります。" +not_found = "ビデオのパス{path}が見つかりません!" +not_studio = "Studio以外の録画は編集できません" + +[editor.window.presets] +apply = "適用" +create = "作成" +create_new = "新しいプリセットを作成" +create_title = "プリセットを作成" +default = "デフォルト" +default_badge = "デフォルト" +delete = "削除" +delete_confirm = "「{name}」を削除してもよろしいですか?" +delete_title = "プリセットを削除" +name_placeholder = "プリセット名" +none_saved = "保存済みのプリセットはありません" +rename = "名前を変更" +rename_title = "プリセット名を変更" +save_settings = "設定をプリセットに保存" +set_default = "デフォルトに設定" + +[editor.window.preview] +full = "フル" +half = "1/2" +label = "プレビュー" +quarter = "1/4" + +[editor.window.text_menu] +copy_style = "スタイルをコピー" +copy_style_first = "先にスタイルをコピーしてください" +delete_segment = "テキストセグメントを削除" +delete_segments = "テキストセグメントを削除" +paste_style = "スタイルをペースト" +paste_style_all = "選択したすべてにスタイルをペースト" +segments = { other = "{count}個のテキストセグメント" } +untitled = "テキスト" + +[editor.window.title] +not_saved = "タイトルを保存できませんでした" +not_saved_message = "{error}\n\nタイトルはエディタに残っています。もう一度保存してください。" +read_failed = "{path}の録画タイトルを読み込めませんでした:{error}" +save_failed = "{path}の録画タイトルを保存できませんでした:{error}" + +[editor.window.track] +clear_all = "すべて消去" +delete = "削除" + +[editor.window.transport] +done_splitting = "分割を終了" +play_pause = "ビデオを再生/一時停止" +split = "分割" +split_tooltip = "セグメントをクリックして分割します。Escで終了します。" +zoom_in = "ズームイン" +zoom_out = "ズームアウト" + +[editor.window.zoom_lane] +generate = "生成" +generating = "生成中…" +no_clicks = "ズームするクリックが見つかりません。レーン上をドラッグして追加してください。" +prompt = "ズームを自動で追加" + +[ui] +coming_soon = "近日公開" + +[ui.selection] +audio = { other = "{count}個の音声セグメントを選択中" } +caption = { other = "{count}個の字幕セグメントを選択中" } +delete = "削除" +done = "完了" +image = { other = "{count}個の画像セグメントを選択中" } +keyboard = { other = "{count}個のキーボードセグメントを選択中" } +mask = { other = "{count}個のマスクセグメントを選択中" } +scene = { other = "{count}個のシーンセグメントを選択中" } +segments = { other = "{count}個のセグメントを選択中" } +select_all = "すべて選択" +style = { other = "{count}個のスタイルセグメントを選択中" } +text_segments = { other = "{count}個のテキストセグメントを選択中" } +three_d = { other = "{count}個の3Dセグメントを選択中" } +zoom = { other = "{count}個を選択中" } +zoom_all = { other = "{count}個すべてを選択中" } +zoom_some = { other = "{total}個中{count}個を選択中" } diff --git a/apps/desktop-gpui/locales/zh-CN.toml b/apps/desktop-gpui/locales/zh-CN.toml index a37123809c9..26ea525df4a 100644 --- a/apps/desktop-gpui/locales/zh-CN.toml +++ b/apps/desktop-gpui/locales/zh-CN.toml @@ -257,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 = "确定要删除这段录制吗?" @@ -620,3 +1312,1566 @@ 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 = "正在上传" + +# Editor sidebar, segment panels, colour correction, audio library and crop dialog. + +[editor.audio.library] +add_hint = "为视频添加音频、音乐或其他声音" +add_title = "添加音频" +change_hint = "为此片段选择其他音轨" +change_title = "更换音频" +done = "完成" +import = "导入文件" + +[editor.color] +apply_to_cursor = "应用到光标" +fine_tune = "微调颜色" +title = "色彩校正" + +[editor.color.preset.cinematic] +description = "青绿色阴影与橙色高光,带轻微颗粒" +name = "电影感" + +[editor.color.preset.dreamy] +description = "柔和轻盈的粉彩,黑色提亮" +name = "梦幻" + +[editor.color.preset.frost] +description = "清冷通透的色调,颜色柔和" +name = "冰霜" + +[editor.color.preset.golden] +description = "温暖的黄金时刻光晕" +name = "金色" + +[editor.color.preset.midnight] +description = "深沉忧郁的青绿色,颜色低调" +name = "午夜" + +[editor.color.preset.noir] +description = "高对比度黑白,带浓重颗粒" +name = "黑色电影" + +[editor.color.preset.none] +description = "原始颜色,未调色" +name = "无" + +[editor.color.preset.vintage] +description = "褪色暖调胶片感,对比柔和" +name = "复古" + +[editor.color.preset.vivid] +description = "饱和度与对比度大幅提升" +name = "鲜艳" + +[editor.color.slider] +contrast = "对比度" +exposure = "曝光" +fade = "褪色" +grain = "颗粒" +saturation = "饱和度" +split_tone = "分离色调" +strength = "强度" +temperature = "色温" +tint = "色调" +vignette = "暗角" + +[editor.crop] +aspect_ratio = "宽高比" +center = "居中" +dimensions = "大小" +editing_style = "仅编辑样式 {number}" +editing_style_override = "仅编辑样式 {number} · 保存后将启用其背景覆盖" +free = "自由" +full = "全部" +hint = "拖动以移动 · 方向键微调 · 按住 {key} 跳过吸附" +loading_frame = "正在加载画面…" +position = "位置" +preview = "预览" +rendering_preview = "正在渲染预览…" +snap_to_ratios = "吸附到比例" +style_changed = "裁剪期间此样式已更改。请取消并重新打开裁剪。" +title = "裁剪" + +[editor.panel] +delete = "删除" +done = "完成" +duration = "时长" +edge_in = "入场" +edge_out = "出场" +enabled = "已启用" +fade_duration = "淡化时长" +flip_horizontally = "水平翻转" +flip_vertically = "垂直翻转" +name = "名称" +opacity = "不透明度" + +[editor.panel.audio] +change = "更换" +change_hint = "点按以更换音轨" +fade_in = "淡入" +fade_out = "淡出" +fallback_name = "音频" +title = "音频 {number}" +volume = "音量" + +[editor.panel.camera3d] +already_still = "已是静止镜头" +amount = "强度" +angles = "角度" +auto_scene = "自动场景" +auto_scene_tooltip = "重新生成轨道上的所有 3D 镜头" +bokeh = "焦外光斑" +camera = "摄像机" +delete_tooltip = "删除此镜头" +depth_blur = "景深模糊" +distance = "距离" +drag_to_orbit = "拖动以环绕" +ease_in = "缓入" +ease_out = "缓出" +end_same_as_start = "结束 · 与开始相同" +fine_tune = "微调" +focus = "焦点" +header = "3D 镜头 · {look} · {duration} 秒" +hold_opening = "整个镜头保持起始姿态" +lens = "焦距" +look_title = "取景" +motion_style = "运动方式" +moves = "运镜" +nothing_to_swap = "静止镜头无需交换" +play_shot = "播放" +play_shot_tooltip = "播放此镜头" +pose_end = "结束 · {time}" +pose_start = "开始 · {time}" +reset_camera = "重置摄像机" +roll = "滚转" +rotate_x = "旋转 X" +rotate_y = "旋转 Y" +shift_x = "平移 X" +shift_y = "平移 Y" +shots = { other = "{count} 个镜头" } +still_shot = "静止镜头" +swap = "交换开始和结束" +timing_advanced = "时间与高级" +timing_summary = "{easing} · 焦距 {fov}" + +[editor.panel.camera3d.blur] +angle = "角度" +falloff = "衰减" +focus_size = "焦点大小" +focus_x = "焦点 X" +focus_y = "焦点 Y" +position = "位置" +scan = "扫描" +strength = "强度" + +[editor.panel.camera3d.easing] +ease_in = "缓入" +ease_out = "缓出" +linear = "线性" +smooth = "平滑" + +[editor.panel.camera3d.focus_mode] +directional = "定向" +radial = "径向" +tilt_shift = "移轴" + +[editor.panel.camera3d.look] +center = "居中" +close_up = "特写" +drift_down = "缓缓下移" +glide_across = "横向滑过" +low_angle = "低角度" +perspective = "透视" +pull_back = "后拉" +rising_sweep = "上升扫过" +slide_by = "滑过" +spotlight = "聚光" +tilt_away = "倾斜远离" +top_down = "俯视" +unfold = "展开" + +[editor.panel.camera3d.sequence] +product_tour = "产品导览" +punch_in = "推近" +showcase = "展示" + +[editor.panel.camera3d.shot] +custom = "自定义运镜" +still = "静止镜头" + +[editor.panel.caption] +title = "字幕 {number}" + +[editor.panel.font] +system_mono = "系统等宽" +system_sans = "系统无衬线" +system_serif = "系统衬线" + +[editor.panel.font_weight] +black = "特黑" +bold = "粗体" +custom = "自定义({weight})" +extra_bold = "特粗" +light = "细体" +medium = "中等" +regular = "常规" +semibold = "半粗体" + +[editor.panel.image] +arrange = "在画布上排列" +arrange_help = "拖动图片即可移动。拖动角可调整大小,或使用下方的旋转控件进行旋转。" +center = "在画布上居中" +lock_aspect = "锁定宽高比" +missing = "图片文件缺失。替换图片即可恢复此片段,并保留其时间和变换。" +replace = "替换图片" +rotation = "旋转" +rounding = "圆角" + +[editor.panel.keyboard] +title = "键盘 {number}" + +[editor.panel.mask] +blur = "模糊" +effect = "效果" +highlight = "突出显示" +outside_darkness = "外部暗度" +pixel_size = "像素大小" +pixelate = "像素化" +sensitive = "敏感内容" +title = "遮罩 {number}" + +[editor.panel.scene] +camera_layout = "摄像头布局" +camera_position = "摄像头位置" +camera_zoom = "摄像头缩放" +screen_position = "屏幕位置" +screen_zoom = "屏幕缩放" +transition = "过渡" + +[editor.panel.scene.description] +camera_only = "仅显示摄像头画面" +default = "同时显示屏幕和摄像头" +floating = "屏幕和摄像头以圆角卡片的形式并排浮在背景上" +hide_camera = "仅显示屏幕录制" +split_screen = "屏幕和摄像头并排显示(竖屏时自动上下排列)" + +[editor.panel.scene.mode] +camera_only = "仅摄像头" +default = "默认" +floating = "浮动" +hide_camera = "隐藏摄像头" +split_screen = "分屏" + +[editor.panel.style] +camera_only_background = "仅摄像头背景" +camera_only_help = "请使用仅摄像头场景。边距会显示摄像头周围的背景。" +camera_only_padding = "仅摄像头边距" +crop_height = "高度" +crop_left = "左" +crop_title = "屏幕裁剪(源像素)" +crop_top = "上" +crop_width = "宽度" +edit_background = "编辑背景和边框" +edit_camera = "编辑摄像头" +edit_cursor = "编辑光标" +help = "覆盖设置仅在此片段期间生效。启用一个分组即可复制其全局设置。" +reset_crop = "重置裁剪" + +[editor.panel.text_animation] +bounce = "弹跳" +fade = "淡入淡出" +letters = "逐字" +none = "无" +pop = "弹出" +slide_down = "向下滑入" +slide_left = "向左滑入" +slide_right = "向右滑入" +slide_up = "向上滑入" +tracking = "字距" +typewriter = "打字机" +wipe = "擦除" +words = "逐词" +zoom = "缩放" + +[editor.panel.text_background] +box_shape = "方框" +highlight = "高亮" +none = "无" +pill = "胶囊" + +[editor.panel.text_layout] +fullscreen = "全屏" +overlay = "叠加" + +[editor.panel.text_preset.group] +all = "全部" +callouts = "标注" +code = "代码" +lower_thirds = "字幕条" +statements = "要点" +titles = "标题" + +[editor.panel.text_preset.name] +caption = "说明文字" +cinematic = "电影感" +code = "代码" +gradient = "渐变" +headline = "大标题" +highlight = "高亮" +kicker = "引题" +label = "标签" +lower_third = "字幕条" +name_tag = "姓名牌" +neon = "霓虹" +quote = "引言" +stat = "大数字" +sticker = "贴纸" +title = "标题" +typewriter = "打字机" + +[editor.panel.text_preset.sample] +caption = "使用 Cap 录制" +cinematic = "第一章" +gradient = "精美文字" +headline = "更快发布" +highlight = "重点在这里" +kicker = "新功能" +label = "小贴士" +name_tag = "Richie · 创始人" +neon = "闪耀登场" +quote = "“让一切毫不费力”" +sticker = "砰!" +title = "隆重推出 Cap" +typewriter = "逐字输入中…" + +[editor.panel.text_segment] +animation = "动画" +background = "背景" +color = "颜色" +font = "字体" +font_size = "大小" +fullscreen_help = "显示文字时暂停视频,之后从暂停处继续播放。" +glow = "发光" +gradient = "渐变" +layout = "布局" +letter_spacing = "字间距" +line_height = "行高" +look = "外观" +outline = "描边" +screen_transition = "屏幕过渡" +shadow = "阴影" +style = "样式" +title = "文本 {number}" + +[editor.panel.timing] +end = "结束" +start = "开始" +title = "时间" +to = "至" + +[editor.panel.zoom] +amount = "缩放倍数" +auto = "自动" +auto_help = "自动缩放会跟随光标在屏幕上移动。" +auto_locked = "自动模式需要捕获光标。请在“设置 → 通用”中开启“单独捕获光标(Studio)”。" +how_it_works = "这是如何工作的?" +manual = "手动" +manual_help = "手动缩放会固定在你在下方选择的位置。" +mixed = "不一致" +mode = "缩放模式" +positions_mixed = "这些片段缩放到不同位置。拖动可将它们全部移到同一位置。" +title = "缩放 {number}" + +[editor.sidebar] +desktop_import_failed = "无法导入桌面壁纸" +importing_image = "正在导入图片…" + +[editor.sidebar.animated_gradient] +add_colour = "添加颜色" +colours = "颜色" +delete = "删除" +delete_failed = "无法删除此渐变。请重试。" +fine_tune = "微调" +limit_reached = { other = "最多可保存 {count} 个渐变。请删除一个后再添加。" } +motion = "运动" +name_placeholder = "为此渐变命名" +name_required = "请输入此渐变的名称。" +presets = "预设" +randomize = "随机" +remember_failed = "无法记住你的动态渐变设置。" +remove_colour = "移除颜色" +reset_tooltip = "重置微调设置" +save_failed = "无法保存此渐变。请重试。" +save_tooltip = "保存当前渐变" +saved = "已保存" +still = "静止" + +[editor.sidebar.animated_gradient.control] +contrast = "对比度" +curvature = "弯曲度" +detail = "细节" +direction = "方向" +exposure = "曝光" +flow_scale = "流动尺度" +flow_strength = "流动强度" +grain_amount = "颗粒数量" +grain_size = "颗粒大小" +highlights = "高光" +motion_speed = "运动速度" +relief = "浮雕" +ripple_size = "波纹大小" +shading = "阴影" +vibrance = "自然饱和度" + +[editor.sidebar.animated_gradient.group] +animation = "动画" +colour = "颜色" +flow = "流动" +lighting = "光照" +texture = "纹理" + +[editor.sidebar.background] +title = "背景" + +[editor.sidebar.border] +color = "边框颜色" +opacity = "边框不透明度" +title = "边框" +width = "边框宽度" + +[editor.sidebar.corner] +rounded = "圆角" +squircle = "超椭圆" +title = "圆角样式" + +[editor.sidebar.cursor] +circle = "圆形" +click_ripple = "点击波纹" +color = "颜色" +default = "默认" +description_circle = "将所有光标形状替换为圆形。" +description_default = "保留录制时的光标形状和外观。" +description_macos = "经典 macOS 外观。自定义光标保留录制时的形状。" +description_tahoe = "macOS Tahoe 外观。自定义光标保留录制时的形状。" +description_windows = "Windows 外观。自定义光标保留录制时的形状。" +duration = "时长" +recorded = "录制时的光标" +recorded_tooltip = "使用此光标录制" +ripple_size = "大小" +strength = "强度" + +[editor.sidebar.desktop] +import = "导入桌面背景" +importing = "正在导入..." +prompt = "使用桌面上的壁纸" +reimport = "重新导入" + +[editor.sidebar.gradient] +angle = "角度" +from = "起始" +grain_scale = "颗粒大小" +noise = "噪点" +to = "结束" + +[editor.sidebar.image] +prompt = "点击选择或拖放图片" + +[editor.sidebar.layout] +background_blur = "背景模糊" +custom_position = "自定义屏幕位置(在画布上拖动)" +motion_blur = "运动模糊" +padding = "边距" +rounded_corners = "圆角" +title = "布局" + +[editor.sidebar.notch] +description = "在录制画面上绘制 MacBook 刘海。在带刘海的 Mac 上录制的内容会使用其自身的尺寸;否则请从下方的尺寸开始,再调整到匹配为止。" +height = "刘海高度" +position = "刘海位置" +title = "MacBook 刘海" +width = "刘海宽度" + +[editor.sidebar.shadow] +advanced = "高级阴影设置" +blur = "模糊" +opacity = "不透明度" +shadow_size = "大小" +title = "阴影" + +[editor.sidebar.source] +animated = "动态" +color = "颜色" +desktop = "桌面" +gradient = "渐变" +image = "图片" +none = "无" +wallpaper = "壁纸" + +[editor.sidebar.style_group] +back = "返回样式" +background = "背景和边框" +camera = "摄像头" +cursor = "光标" +editing_note = "仅编辑此片段。全局设置不变。" +title = "样式 {number} · {group}" + +[editor.sidebar.tab] +audio = "音频" +background = "背景" +camera = "摄像头" +captions = "字幕" +cursor = "光标" +keyboard = "键盘" + +[editor.sidebar.wallpaper_theme] +blue = "蓝色" +cities = "城市" +dark = "深色" +orange = "橙色" +purple = "紫色" + +[editor.canvas] +camera = "摄像头" +camera_follows_zoom = "摄像头大小随缩放变化;拖动即可移动" +edit_zoom = "编辑缩放" +image = "图片" +mask = "遮罩" +screen = "屏幕" +screen_locked = "缩放生效期间屏幕已锁定" +text_item = "文字" + +[editor.clips] +append_error = "无法添加片段:{message}" +append_error_title = "录制错误" +back = "返回编辑器" +cap_filter = "Cap 录制" +clip = "片段 {number}" +empty_description = "录制或导入一个片段后,它会显示在这里。" +empty_title = "还没有片段" +import = "导入" +import_error = "无法导入片段:{message}" +import_error_title = "导入错误" +import_existing = "已有的录制" +import_mp4 = "MP4 视频…" +importing = "正在导入…" +mp4_filter = "MP4 视频" +prepare_controls_failed = "无法准备录制控件:{error}" +record_new = "录制新片段" +save_before_recording_failed = "录制前无法保存当前项目:{error}" +split = "拆分 {number}" +title = "片段" + +[editor.clips.error] +cancelled = "已取消导入" +cannot_decode = "无法解码视频 {path}:{error}" +in_use = "此录制正在使用中,无法导入:{error}" +instant_source = "暂时无法将 Instant 录制导入项目" +instant_target = "Instant 模式的录制无法编辑" +into_itself = "无法将录制导入到它自身" +missing_video = "缺少视频文件:{path}" +no_duration = "导入的视频没有有效时长" +not_a_project = "请选择一个 Cap 项目文件夹" +source_empty = "源 Cap 项目没有录制分段" +source_failed = "源 Cap 项目出错:{error}" +source_recording = "源 Cap 项目仍在录制" +source_unimportable = "源 Cap 项目没有可导入的录制分段" +unfinished_target = "导入片段前,请先完成或恢复此录制" +unsupported_video = "不支持的视频文件:{path}" + +[editor.export] +back = "返回编辑器" +existing_link_description = "重新上传会用你最新的编辑替换此链接中的视频。拥有链接的所有人都会看到更新后的版本。" +existing_link_title = "更新现有链接" +failed = "导出失败" +fps = "{fps} FPS" +frames = { other = "{rendered} / {count} 帧" } +generating_preview = "正在生成预览…" +link_unavailable_cursor = "仅光标的导出只能保存到文件或剪贴板。" +link_unavailable_transparent = "透明导出只能保存到文件或剪贴板。" +organization = "组织" +preview = "预览" +preview_info = "这是从你的视频渲染出的一帧。调整设置即可查看最终导出的画质。" +preview_unavailable = "预览不可用" +reupload = "重新上传" +reupload_tooltip = "将你最新的编辑上传到同一链接" +reupload_transparent = "分享链接需要不透明的背景" +sign_in_required = "你需要登录才能分享录制" +task_failed = "导出任务失败" +title = "导出" +upgrade_message = "此功能需要升级套餐" +upgrade_title = "需要升级" +upload_failed = "无法上传录制" + +[editor.export.advanced] +bits_per_pixel = "每像素比特数" +cursor_only = "仅导出光标" +cursor_only_description = "在透明背景上保留相同的光标移动和点击" +custom_bitrate = "正在使用自定义码率" +force_ffmpeg = "强制使用 FFmpeg 解码器" +force_ffmpeg_description = "跳过硬件解码器(已启用自动回退)" +title = "高级" +transparent_mov = "导出为透明 MOV。文件较大,最适合合成或剪辑。" + +[editor.export.cta] +cancel_sign_in = "取消登录" +clipboard = "导出到剪贴板" +create_link = "创建分享链接" +file = "导出为文件" +reupload = "重新上传到同一链接" +sign_in = "登录以分享" + +[editor.export.destination] +clipboard = "剪贴板" +file = "文件" +link = "分享链接" + +[editor.export.done] +copy_link = "复制链接" +instant_hint = "如果想在停止录制时立即获得链接,下次录制请使用 Instant 模式。" +link_copied = "链接已复制" +open_file = "打开文件" +open_link = "打开链接" +retry_copy = "重新复制" +reuploaded = "你最新的编辑已在同一链接上就绪" +uploaded = "你的 Cap 已成功上传" + +[editor.export.estimate] +gigabytes = "约 {value} GB" +gigabytes_range = "约 {lower}–{upper} GB" +hours = "约 {value} 小时" +hours_range = "约 {lower}–{upper} 小时" +megabytes = "约 {value} MB" +megabytes_range = "约 {lower}–{upper} MB" +minutes = "约 {value} 分钟" +minutes_range = "约 {lower}–{upper} 分钟" +seconds = "约 {value} 秒" +seconds_range = "约 {lower}–{upper} 秒" +under_megabyte = "< 1 MB" +under_second = "< 1 秒" + +[editor.export.optimize] +description = "用软件重新编码,文件小得多(较慢)" +title = "优化文件大小" + +[editor.export.phase] +clipboard_failed = "导出完成;复制到剪贴板失败" +complete = "导出完成" +copied = "已复制到剪贴板" +copying_to_clipboard = "正在复制到剪贴板" +rendering_cursor = "正在渲染光标轨道" +rendering_gif = "正在渲染 GIF" +rendering_video = "正在渲染视频" +reupload_complete = "重新上传完成" +reuploading = "正在重新上传到你的链接" +saving_to_file = "正在保存到文件" +starting = "正在准备导出" +upload_complete = "上传完成" +uploading = "正在创建分享链接" + +[editor.export.quality] +larger = "文件更大" +maximum = "最高" +potato = "最低" +smaller = "文件更小" +social = "社交" +web = "网页" + +[editor.export.resources] +proceed = "仍然继续" +title = "检查导出资源" + +[editor.export.section] +destination = "目标" +format = "格式" +frame_rate = "帧率" +quality = "画质" +resolution = "分辨率" + +[editor.export.stats] +calculating = "正在计算…" +duration = "时长" +estimated_size = "预计大小" +export_time = "导出用时" +output = "输出" +output_value = "{width}×{height} · {fps} fps" +refining_size = "正在细化大小…" +refining_time = "正在细化用时…" +unavailable = "不可用" + +[editor.modal] +cancel_export = "取消导出" +cancel_export_message = "确定要取消导出吗?" +cancel_export_title = "取消导出?" +keep_exporting = "继续导出" +recording_retained = "录制已保留" + +[editor.tabs.audio] +isolation_balanced = "均衡" +isolation_balanced_description = "更清晰的人声分离,同时保留自然的声音细节。" +isolation_light = "轻度" +isolation_light_description = "保留更多原始人声和房间声音。" +isolation_strong = "强力" +isolation_strong_description = "为嘈杂环境提供更强分离。可能改变声音质感。" +mic_volume = "麦克风音量" +mute = "静音" +stereo_mode = "麦克风立体声模式" +studio_sound_description = "降低背景噪音并平衡你的音量。" +studio_sound_save_failed = "无法保存 Studio Sound 默认设置" +system_volume = "系统音频音量" +title = "音频控制" +use_for_new = "用于新的录制" + +[editor.tabs.audio.stereo] +mono_left = "单声道 L" +mono_right = "单声道 R" +stereo = "立体声" + +[editor.tabs.camera] +advanced_shadow = "高级阴影设置" +background = "背景" +corner_style = "圆角样式" +custom_position = "自定义位置(在画布上拖动)" +hide = "隐藏摄像头" +keep_size_during_zoom = "缩放时保持原始大小" +mirror = "镜像摄像头" +position = "位置" +reset = "重置" +rounded_corners = "圆角" +shadow = "阴影" +shadow_blur = "模糊" +shadow_opacity = "不透明度" +shape_label = "形状" +size_during_zoom = "缩放时的大小" +size_label = "大小" +title = "摄像头" + +[editor.tabs.camera.blur] +heavy = "强模糊" +light = "轻模糊" +off = "关闭" +remove = "移除背景" + +[editor.tabs.camera.shape] +source = "原始" +square = "方形" + +[editor.tabs.captions] +active_word_highlight = "高亮当前单词" +animation_section = "动画" +animation_style = "动画样式" +background_color = "背景颜色" +background_opacity = "背景不透明度" +background_settings = "背景设置" +beta = "Beta" +delete = "删除" +deleting = "正在删除..." +download = "下载{model}模型" +download_note = "只需下载到你的系统一次。所有字幕都保存在本地。" +downloading = "正在下载{model}... {percent}%" +fade_duration = "淡入淡出时长" +font_family = "字体" +font_settings = "字体设置" +font_weight = "字重" +generate = "生成字幕" +generate_failed = "无法生成字幕:{reason}" +generating = "正在生成..." +highlight_color = "高亮颜色" +highlight_note = "这是 Cap 字幕的第一个版本。在某些情况下,当前单词高亮可能不准确。我们正在修复,修复会在后续版本中发布。" +highlight_style = "高亮样式" +horizontal_position = "水平位置" +intel_unavailable = "Parakeet 字幕模型在 Intel Mac 上不可用。Whisper 模型仍可使用。" +keep_open = "下载模型时请保持 Cap 打开。编辑器重新加载后会自动重新连接。" +language_label = "语言" +model_downloaded = "已下载{model}模型" +model_label = "模型" +position = "位置" +regenerate = "重新生成字幕" +size_label = "大小" +style = "样式" +text_color = "文字颜色" +title = "字幕" +uppercase = "大写" +vertical_position = "垂直位置" + +[editor.tabs.captions.animation] +bounce = "弹跳" +none = "无" +pop = "弹出" + +[editor.tabs.captions.highlight] +color = "颜色" +pill = "胶囊" + +[editor.tabs.captions.language] +ar = "阿拉伯语" +auto = "自动检测" +bn = "孟加拉语" +de = "德语" +en = "英语" +es = "西班牙语" +fa = "波斯语" +fr = "法语" +gu = "古吉拉特语" +he = "希伯来语" +hi = "印地语" +it = "意大利语" +ja = "日语" +ko = "韩语" +mr = "马拉地语" +nl = "荷兰语" +pa = "旁遮普语" +pl = "波兰语" +pt = "葡萄牙语" +ru = "俄语" +sk = "斯洛伐克语" +ta = "泰米尔语" +te = "泰卢固语" +tr = "土耳其语" +ur = "乌尔都语" +zh = "中文" + +[editor.tabs.captions.model] +high_accuracy = "高精度" +high_accuracy_description = "下载更大,精度更高" +medium = "中" +medium_description = "较慢,更准确" +recommended = "推荐" +recommended_description = "适合大多数录制的最佳平衡" +small = "小" +small_description = "下载最小" + +[editor.tabs.captions.preset] +classic = "经典" +classic_description = "圆角纯色背景上的清晰文字。" +highlight = "高亮" +highlight_description = "粗体大写,当前单词后有胶囊背景。" +karaoke = "卡拉 OK" +karaoke_description = "单词随语音同步亮起。" +minimal = "极简" +minimal_description = "无背景的低调描边文字。" +pop = "跳跃" +pop_description = "活泼的大写文字,以鲜艳的强调色弹出。" + +[editor.tabs.corner] +rounded = "圆角" +squircle = "超椭圆" + +[editor.tabs.cursor] +friction = "摩擦" +hide_when_idle = "空闲时隐藏" +inactivity_delay = "空闲延迟" +mass = "质量" +movement_style = "光标移动样式" +show = "显示光标" +size_label = "大小" +smooth_movement = "平滑移动" +style_label = "光标样式" +svg_cursors = "高质量 SVG 光标" +tension = "张力" +tilt = "倾斜" + +[editor.tabs.cursor.style] +fast = "快速" +fast_description = "灵敏,紧密跟随" +mellow = "柔和" +mellow_description = "均衡、自然的移动" +slow = "缓慢" +slow_description = "悠长、漂移的移动" +smooth = "顺滑" +smooth_description = "柔软、轻盈的移动" + +[editor.tabs.keyboard] +animation = "动画" +background_color = "背景颜色" +background_opacity = "背景不透明度" +background_settings = "背景设置" +behavior = "行为" +beta = "Beta" +changed_during_generation = "生成过程中时间线或键盘设置发生了变化。请重试。" +empty_description = "点击“生成键盘分段”,根据录制的按键创建分段。" +empty_title = "还没有键盘分段。" +fade_duration = "淡入淡出时长" +font_family = "字体" +font_settings = "字体设置" +font_weight = "字重" +generate = "生成键盘分段" +generating = "正在生成键盘分段..." +grouping_threshold = "分组阈值" +linger_duration = "停留时长" +load_failed = "无法加载录制数据:{error}" +no_presses = "未找到录制的按键。" +position = "位置" +regenerate = "重新生成键盘分段" +show_modifiers = "显示修饰键" +show_special_keys = "显示特殊键" +size_label = "大小" +text_color = "文字颜色" +timeline_unavailable = "项目时间线不可用。" +timeline_unreadable = "无法读取项目时间线。" +title = "显示键盘" +uppercase = "大写" + +[editor.tabs.position] +bottom_center = "底部居中" +bottom_left = "左下" +bottom_right = "右下" +manual = "手动" +top_center = "顶部居中" +top_left = "左上" +top_right = "右上" + +[editor.tabs.sync] +auto_description = "Cap 已自动计算这些偏移,让音频与视频保持同步。如果听起来仍不对,请调整它们。" +auto_synced = "已自动同步" +camera = "摄像头偏移" +clip = "片段 {number}" +description = "如果音频或摄像头与屏幕录制不同步,可以微调各来源的偏移。" +microphone = "麦克风偏移" +system_audio = "系统音频偏移" +title = "同步" + +[editor.tabs.weight] +bold = "粗体" +medium = "中等" +normal = "常规" + +[editor.timeline] +add_shot = "添加镜头" +add_shot_with_look = "添加镜头 · {look} · {seconds} 秒" +paused = "已暂停" + +[editor.timeline.empty] +add_audio = "添加音频" +add_scene = "添加场景" +audio = "添加背景音乐或导入你自己的音频" +captions = "没有字幕" +keyboard = "没有键盘事件" +scene = "在屏幕和摄像头之间切换布局" + +[editor.timeline.picker] +audio = "添加背景音乐或导入你自己的音频。" +caption = "自动转写你的录制,生成屏幕字幕。" +clip = "你录制的屏幕画面。" +image = "为你的录制添加图片。" +keyboard = "在你输入时在屏幕上显示按键。" +mask = "模糊或涂黑屏幕上的隐私区域。" +scene = "在屏幕和摄像头之间切换布局。" +scene_unavailable = "使用摄像头录制后才能使用场景。" +style = "随时间改变背景、摄像头和光标设置。" +text_overlay = "在画布上添加自定义文字叠加和标题。" +three_d = "以 3D 透视倾斜场景。" +zoom = "跟随动作的平滑放大。" + +[editor.timeline.remove] +audio = "移除音频轨道。" +captions = "移除字幕轨道。" +clip = "移除片段轨道。" +image = "移除图片轨道。" +keyboard = "移除键盘轨道。" +mask = "移除遮罩轨道。" +scene = "移除场景轨道。" +style = "移除样式轨道。" +text_track = "移除文字轨道。" +three_d = "移除 3D 轨道。" +zoom = "移除缩放轨道。" + +[editor.timeline.scene] +camera_only = "仅摄像头" +default = "默认" +floating = "悬浮" +hide_camera = "隐藏摄像头" +split_screen = "分屏" + +[editor.timeline.segment] +audio = "音频" +automatic_zoom = "自动缩放" +caption = "字幕" +clip = "片段" +cursor_hidden = "已隐藏光标" +highlight = "突出显示" +label = "标签" +manual_zoom = "手动缩放" +muted = "已静音" +numbered_clip = "片段 {number}" +sensitive = "敏感" + +[editor.timeline.track] +audio = "音频" +captions = "字幕" +clip = "片段" +image = "图片" +keyboard = "键盘" +mask = "遮罩" +scene = "场景" +style = "样式" +text_track = "文字" +video = "视频" +zoom = "缩放" + +[editor.window] +audio_filter = "音频" +audio_segment_changed = "此音频分段已更改。请选中它并重新选择替换文件。" +crop = "裁剪" +crop_tooltip = "裁剪视频" +no_room_for_3d_shot = "没有空间再添加 3D 镜头" +redo = "重做" +save_failed = "无法保存 {path}。请保持编辑器打开并重试:{error}" +undo = "撤销" + +[editor.window.add_track] +button = "添加轨道" +hide_3d = "隐藏 3D 轨道。" +hint = "选择要添加到时间线的轨道。" + +[editor.window.aspect] +auto = "自动" +classic = "经典" +classic_ratio = "经典 ⋅4:3" +square = "方形" +square_ratio = "方形 ⋅1:1" +tall = "竖长" +tall_ratio = "竖长 ⋅3:4" +tooltip = "宽高比" +vertical = "竖屏" +vertical_ratio = "竖屏 ⋅9:16" +wide = "宽屏" +wide_ratio = "宽屏 ⋅16:9" + +[editor.window.audio_import] +copy_failed = "无法复制音频文件:{error}" +directory_failed = "无法创建音频文件夹:{error}" +no_extension = "音频文件没有扩展名" +not_found = "找不到音频文件:{path}" +unsupported = "不支持的音频格式:.{extension}" + +[editor.window.busy] +clip_import = "请等待片段导入完成后再关闭或删除此录制。" +export = "请等待导出或取消导出完成后再关闭或删除此录制。" +opening = "请等待录制打开完成后再删除。" +save_before_delete_failed = "删除前无法保存录制:{error}" + +[editor.window.camera3d_lane] +add_shot = "+ 添加镜头" +auto_scene = "自动场景" +how_many = "要几个镜头?" +prompt = "为你的录制添加电影感 3D 镜头" + +[editor.window.clip_menu] +audio = "音频" +cursor = "光标" +delete = "删除片段" +delete_only_clip = "无法删除唯一的片段" +hide_cursor = "隐藏光标" +hide_cursor_description = "播放此片段时淡出" +keep_pitch = "保持音调" +match_speed = "匹配速度" +merge_next = "与下一个片段合并" +merge_previous = "与上一个片段合并" +mute = "静音" +mute_clip = "静音片段" +mute_clip_description = "关闭此片段的声音" +rename = "重命名片段" +speed = "速度" +split = "在播放头处拆分" +split_hint = "将播放头移到此片段内" +volume = "音量" + +[editor.window.frame] +add_tooltip = "添加边框" +browser = "浏览器" +browser_description = "带地址栏的浏览器边框" +button = "边框" +dark = "深色" +global = "全局边框 · 应用于所有继承背景的样式。" +heading = "边框" +light = "浅色" +macbook_description = "录制画面外的笔记本边框" +macos_description = "带红绿灯按钮的窗口边框" +none = "无" +none_description = "按原样显示录制" +style_changed = "此样式已更改。请关闭并重新打开边框。" +style_only = "仅样式 {index} · 全局设置保持不变。" +style_only_override = "仅样式 {index} · 编辑会启用它自己的背景设置。" +theme = "主题" +title = "标题" +url = "URL" +window_title_placeholder = "窗口标题" +windows_description = "带窗口控制按钮的标题栏" + +[editor.window.header] +captions = "字幕" +delete_recording = "删除录制" +export = "导出" +open_bundle = "打开录制包" +presets = "预设" + +[editor.window.image] +changed = "选择替换图片时此图片已更改。请选中它后重试。" +filter = "图片" +recording_unavailable = "此图片对应的录制已不可用。请重新打开项目。" + +[editor.window.open] +damaged = "无法打开此录制的视频轨道。录制包似乎已损坏。" +failed = "无法打开此录制" +media_failed = "无法读取此录制的媒体:{error}" +meta_failed = "无法加载录制元数据:{error}" +no_segments = "录制没有分段。可能需要先恢复。" +not_found = "找不到视频路径 {path}!" +not_studio = "无法编辑非 Studio 录制" + +[editor.window.presets] +apply = "应用" +create = "创建" +create_new = "创建新预设" +create_title = "创建预设" +default = "默认" +default_badge = "默认" +delete = "删除" +delete_confirm = "确定要删除“{name}”吗?" +delete_title = "删除预设" +name_placeholder = "预设名称" +none_saved = "没有已保存的预设" +rename = "重命名" +rename_title = "重命名预设" +save_settings = "将设置保存到预设" +set_default = "设为默认" + +[editor.window.preview] +full = "全尺寸" +half = "1/2" +label = "预览" +quarter = "1/4" + +[editor.window.text_menu] +copy_style = "复制样式" +copy_style_first = "请先复制一个样式" +delete_segment = "删除文字分段" +delete_segments = "删除文字分段" +paste_style = "粘贴样式" +paste_style_all = "将样式粘贴到所有选中项" +segments = { other = "{count} 个文字分段" } +untitled = "文字" + +[editor.window.title] +not_saved = "标题未保存" +not_saved_message = "{error}\n\n你的标题仍在编辑器中。请再次尝试保存。" +read_failed = "无法读取 {path} 的录制标题:{error}" +save_failed = "无法保存 {path} 的录制标题:{error}" + +[editor.window.track] +clear_all = "全部清除" +delete = "删除" + +[editor.window.transport] +done_splitting = "完成拆分" +play_pause = "播放/暂停视频" +split = "拆分" +split_tooltip = "点击一个分段进行拆分。按 Esc 结束。" +zoom_in = "放大" +zoom_out = "缩小" + +[editor.window.zoom_lane] +generate = "生成" +generating = "正在生成…" +no_clicks = "未找到可缩放的点击。在轨道上拖动即可添加。" +prompt = "自动添加缩放" + +[ui] +coming_soon = "即将推出" + +[ui.selection] +audio = { other = "已选择 {count} 个音频分段" } +caption = { other = "已选择 {count} 个字幕分段" } +delete = "删除" +done = "完成" +image = { other = "已选择 {count} 个图片分段" } +keyboard = { other = "已选择 {count} 个键盘分段" } +mask = { other = "已选择 {count} 个遮罩分段" } +scene = { other = "已选择 {count} 个场景分段" } +segments = { other = "已选择 {count} 个分段" } +select_all = "全选" +style = { other = "已选择 {count} 个样式分段" } +text_segments = { other = "已选择 {count} 个文字分段" } +three_d = { other = "已选择 {count} 个 3D 分段" } +zoom = { other = "已选择 {count} 个" } +zoom_all = { other = "已选择全部 {count} 个" } +zoom_some = { other = "已选择 {count}/{total} 个" } diff --git a/apps/desktop-gpui/src/app_windows.rs b/apps/desktop-gpui/src/app_windows.rs index 93b540b0bcc..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}, @@ -1921,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(); }); } @@ -2410,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; @@ -3097,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!( @@ -3151,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)?; @@ -3187,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 { @@ -3699,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(); }); } @@ -3790,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) { @@ -3816,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) { @@ -3841,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; @@ -3874,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(); }); } @@ -5475,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; @@ -6261,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()); } @@ -6303,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); @@ -6334,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; } }; @@ -6370,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(); } diff --git a/apps/desktop-gpui/src/deeplink.rs b/apps/desktop-gpui/src/deeplink.rs index cd4ee07a9bb..796c55b7ca5 100644 --- a/apps/desktop-gpui/src/deeplink.rs +++ b/apps/desktop-gpui/src/deeplink.rs @@ -611,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 SharedString { match self { - Self::Display => "Screen".into(), - Self::Camera => "Camera".into(), - Self::Mask(_) => "Mask".into(), - Self::Text(_) => "Text".into(), - Self::Image(_) => "Image".into(), + Self::Display => tr::editor::canvas::screen().into(), + Self::Camera => tr::editor::canvas::camera().into(), + Self::Mask(_) => tr::editor::canvas::mask().into(), + Self::Text(_) => tr::editor::canvas::text_item().into(), + Self::Image(_) => tr::editor::canvas::image().into(), } } @@ -1463,12 +1464,12 @@ impl EditorWindow { CanvasSelection::Display => ( self.display_draggable(), self.display_draggable(), - (!self.display_draggable()).then_some("Screen is locked while a zoom is active"), + (!self.display_draggable()).then(tr::editor::canvas::screen_locked), ), CanvasSelection::Camera => ( true, self.camera_resizable(), - (!self.camera_resizable()).then_some("Camera size follows the zoom — drag to move"), + (!self.camera_resizable()).then(tr::editor::canvas::camera_follows_zoom), ), CanvasSelection::Mask(_) | CanvasSelection::Text(_) | CanvasSelection::Image(_) => { (true, true, None) @@ -1569,7 +1570,7 @@ impl EditorWindow { .rounded(px(6.)) .bg(gpui::hsla(0., 0., 1., 0.15)) .font_weight(FontWeight::SEMIBOLD) - .child("Edit zoom") + .child(tr::editor::canvas::edit_zoom()) .on_mouse_down( MouseButton::Left, cx.listener(|this, event, _, cx| { diff --git a/apps/desktop-gpui/src/editor_clips.rs b/apps/desktop-gpui/src/editor_clips.rs index 51ea12b7551..bce9e30a274 100644 --- a/apps/desktop-gpui/src/editor_clips.rs +++ b/apps/desktop-gpui/src/editor_clips.rs @@ -44,6 +44,7 @@ use crate::{ editor_edits::{self as edits, TrackSegmentOps}, editor_timeline::{clip_timeline_offsets, format_time}, editor_window::EditorWindow, + i18n::tr, session::{Phase, RecordingSession}, theme::Theme, ui, @@ -64,7 +65,7 @@ pub(crate) fn format_clip_duration(seconds: f64) -> String { /// `clipLabel` (`:563`). pub(crate) fn clip_label(clip_index: usize) -> String { - format!("Clip {}", clip_index + 1) + tr::editor::clips::clip(clip_index + 1) } /// `segmentClipIndex` (`:565-567`). The `?? Math.min(index, ...)` fallback is @@ -91,7 +92,7 @@ pub(crate) fn segment_label(segments: &[TimelineSegment], index: usize) -> Strin if split_number == 1 { clip_label(segment_clip_index(&segments[index])) } else { - format!("Split {}", split_number - 1) + tr::editor::clips::split(split_number - 1) } } @@ -453,8 +454,8 @@ impl EditorWindow { ) -> impl IntoElement { ui::EditorButton::plain(&self.theme, "clips-pill") .left_icon("icons/clapperboard.svg") - .when(!compact, |button| button.label("Clips")) - .tooltip(&self.theme, "Clips") + .when(!compact, |button| button.label(tr::editor::clips::title())) + .tooltip(&self.theme, tr::editor::clips::title()) .pressed(self.clips.open) .disabled(!self.project_ready()) .on_click(cx.listener(|this, _, window, cx| this.toggle_clips(window, cx))) @@ -795,7 +796,7 @@ impl EditorWindow { .flex_shrink_0() .text_color(Hsla::from(theme.editor.text_2)), ) - .child("Back to editor") + .child(tr::editor::clips::back()) .on_click(cx.listener(|this, _, window, cx| this.close_clips(window, cx))) } @@ -830,7 +831,7 @@ impl EditorWindow { ui::ButtonSize::Md, ) .icon("icons/video.svg") - .label("Record a new clip") + .label(tr::editor::clips::record_new()) .height(px(40.)) .gap(px(8.)) .font_weight(FontWeight::MEDIUM) @@ -858,9 +859,9 @@ impl EditorWindow { ) .icon("icons/circle-plus.svg") .label(if self.clips.importing { - "Importing…" + tr::editor::clips::importing() } else { - "Import" + tr::editor::clips::import() }) .height(px(40.)) .gap(px(8.)) @@ -891,7 +892,7 @@ impl EditorWindow { .text_size(px(14.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.gray_12)) - .child("Clips"), + .child(tr::editor::clips::title()), ) .children((recorded_clip_count > 0).then(|| { div() @@ -959,14 +960,14 @@ impl EditorWindow { .text_size(px(14.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.gray_12)) - .child("No clips yet"), + .child(tr::editor::clips::empty_title()), ) .child( div() .max_w(px(200.)) .text_size(px(12.)) .text_color(Hsla::from(theme.gray_10)) - .child("Record or import a clip and it will show up here."), + .child(tr::editor::clips::empty_description()), ) } @@ -1287,14 +1288,14 @@ impl EditorWindow { self.pending_save().borrow_mut().flush(); if let Err(error) = self.project.write(&self.project_path) { tracing::error!("failed to persist the project config before recording: {error}"); - let message = format!("Could not save the current project before recording: {error}"); + let message = tr::editor::clips::save_before_recording_failed(error); cx.spawn(async move |_, _| show_append_error(&message)) .detach(); return false; } let main = cx.global::().main; if let Err(error) = main.update(cx, |view, _, _| view.cancel_deep_link_start()) { - let message = format!("Could not prepare the recording controls: {error}"); + let message = tr::editor::clips::prepare_controls_failed(error); cx.spawn(async move |_, _| show_append_error(&message)) .detach(); return false; @@ -1460,16 +1461,21 @@ impl EditorWindow { .text_size(px(12.)) .text_color(Hsla::from(theme.gray_12)) .child( - row("clips-import-existing", "Existing recording") - .hover(move |style| style.bg(Hsla::from(theme.gray_3))) - .on_click(cx.listener(|this, _, window, cx| { + row( + "clips-import-existing", + tr::editor::clips::import_existing(), + ) + .hover(move |style| style.bg(Hsla::from(theme.gray_3))) + .on_click(cx.listener( + |this, _, window, cx| { this.clips.import_menu = None; cx.notify(); this.pick_existing_recording(window, cx); - })), + }, + )), ) .child( - row("clips-import-mp4", "MP4 Video…") + row("clips-import-mp4", tr::editor::clips::import_mp4()) .hover(move |style| style.bg(Hsla::from(theme.gray_3))) .on_click(cx.listener(|this, _, window, cx| { this.clips.import_menu = None; @@ -1564,11 +1570,15 @@ impl EditorWindow { #[cfg(target_os = "macos")] let source = crate::platform::open_image_panel(&["mp4"]); #[cfg(target_os = "linux")] - let source = - crate::platform::open_file_panel_async(&[("MP4 Video", &["mp4"])], None, cx).await; + let source = crate::platform::open_file_panel_async( + &[(tr::editor::clips::mp4_filter(), &["mp4"])], + None, + cx, + ) + .await; #[cfg(not(any(target_os = "macos", target_os = "linux")))] let source = rfd::FileDialog::new() - .add_filter("MP4 Video", &["mp4"]) + .add_filter(tr::editor::clips::mp4_filter(), &["mp4"]) .pick_file(); if let Some(source) = source { this.update_in(cx, |this, window, cx| { @@ -1887,20 +1897,20 @@ impl PreparedMp4Import { in_flight: crate::import::InFlightImport, ) -> Result { if cancelled.load(Ordering::Acquire) { - return Err("Import cancelled".to_string()); + return Err(tr::editor::clips::error::cancelled().to_string()); } let target = target.canonicalize().map_err(|error| error.to_string())?; let upload_lock = cap_recording::upload_resume::UploadLock::acquire(&target) - .map_err(|error| format!("Cannot import while this recording is in use: {error}"))?; + .map_err(tr::editor::clips::error::in_use)?; let original_meta = std::fs::read(target.join("recording-meta.json")).map_err(|error| error.to_string())?; let mut meta = RecordingMeta::load_for_project(&target) .map_err(|error| format!("Failed to load target project metadata: {error}"))?; let RecordingMetaInner::Studio(studio) = &meta.inner else { - return Err("Instant mode recordings cannot be edited".to_string()); + return Err(tr::editor::clips::error::instant_target().to_string()); }; if !matches!(studio.status(), StudioRecordingStatus::Complete) { - return Err("Finish or recover this recording before importing a clip".to_string()); + return Err(tr::editor::clips::error::unfinished_target().to_string()); } let segments = ensure_multiple_segments(&mut meta)?.segments.clone(); let fallback_timeline = if config.timeline.is_none() { @@ -1910,7 +1920,7 @@ impl PreparedMp4Import { None }; if cancelled.load(Ordering::Acquire) { - return Err("Import cancelled".to_string()); + return Err(tr::editor::clips::error::cancelled().to_string()); } let index = u32::try_from(segments.len()).map_err(|error| error.to_string())?; let (directory, relative) = unique_segment_dir(&target, index)?; @@ -1929,7 +1939,7 @@ impl PreparedMp4Import { )?; let duration = get_video_duration_secs(&display_path)?; if !duration.is_finite() || duration <= 0.0 { - return Err("Imported video has no valid duration".to_string()); + return Err(tr::editor::clips::error::no_duration().to_string()); } let system_audio = sample_rate.map(|_| AudioMeta { path: format!("{relative}/audio.ogg").into(), @@ -1975,7 +1985,7 @@ impl PreparedMp4Import { write_config: impl FnOnce(&ProjectConfiguration, &Path) -> std::io::Result<()>, ) -> Result { if self.cancelled.load(Ordering::Acquire) { - return Err("Import cancelled".to_string()); + return Err(tr::editor::clips::error::cancelled().to_string()); } let meta_path = self.target.join("recording-meta.json"); if std::fs::read(&meta_path).map_err(|error| error.to_string())? != self.original_meta { @@ -2066,7 +2076,7 @@ async fn pick_existing_recording_path(_cx: &mut gpui::AsyncWindowContext) -> Opt #[cfg(target_os = "linux")] { crate::platform::open_file_panel_async( - &[("Cap Recording", &["cap"])], + &[(tr::editor::clips::cap_filter(), &["cap"])], Some(crate::recording::recordings_dir()), _cx, ) @@ -2076,7 +2086,7 @@ async fn pick_existing_recording_path(_cx: &mut gpui::AsyncWindowContext) -> Opt { rfd::FileDialog::new() .set_directory(crate::recording::recordings_dir()) - .add_filter("Cap Recording", &["cap"]) + .add_filter(tr::editor::clips::cap_filter(), &["cap"]) .pick_file() } } @@ -2085,8 +2095,8 @@ async fn pick_existing_recording_path(_cx: &mut gpui::AsyncWindowContext) -> Opt /// dialog this app uses where the webview has toasts. fn show_import_error(message: &str) { let _ = rfd::MessageDialog::new() - .set_title("Import Error") - .set_description(format!("Failed to import clip: {message}")) + .set_title(tr::editor::clips::import_error_title()) + .set_description(tr::editor::clips::import_error(message)) .set_level(rfd::MessageLevel::Error) .set_buttons(rfd::MessageButtons::Ok) .show(); @@ -2096,8 +2106,8 @@ fn show_import_error(message: &str) { /// stand-in. fn show_append_error(message: &str) { let _ = rfd::MessageDialog::new() - .set_title("Recording Error") - .set_description(format!("Failed to add clip: {message}")) + .set_title(tr::editor::clips::append_error_title()) + .set_description(tr::editor::clips::append_error(message)) .set_level(rfd::MessageLevel::Error) .set_buttons(rfd::MessageButtons::Ok) .show(); @@ -2422,7 +2432,7 @@ fn source_asset_path( /// single-segment studio bundle into the multi-segment shape imports extend. fn ensure_multiple_segments(meta: &mut RecordingMeta) -> Result<&mut MultipleSegments, String> { let RecordingMetaInner::Studio(studio_meta) = &mut meta.inner else { - return Err("Instant mode recordings cannot be edited".to_string()); + return Err(tr::editor::clips::error::instant_target().to_string()); }; if let StudioRecordingMeta::SingleSegment { segment } = studio_meta.as_ref() { @@ -2759,19 +2769,20 @@ fn copy_video_meta( )? else { if required { - return Err(format!( - "Missing video file: {}", - source_project_path.join(source.path.as_str()).display() + return Err(tr::editor::clips::error::missing_video( + source_project_path.join(source.path.as_str()).display(), )); } return Ok(None); }; let can_decode = probe_video_can_decode(&source_path) - .map_err(|e| format!("Cannot decode video {}: {e}", source_path.display()))?; + .map_err(|e| tr::editor::clips::error::cannot_decode(source_path.display(), e))?; if !can_decode { if required { - return Err(format!("Unsupported video file: {}", source_path.display())); + return Err(tr::editor::clips::error::unsupported_video( + source_path.display(), + )); } return Ok(None); } @@ -3099,9 +3110,8 @@ fn get_source_video_duration_secs( VIDEO_IMPORT_EXTENSIONS, )? .ok_or_else(|| { - format!( - "Missing video file: {}", - source_meta.project_path.join(video.path.as_str()).display() + tr::editor::clips::error::missing_video( + source_meta.project_path.join(video.path.as_str()).display(), ) })?; get_video_duration_secs(&source_path) @@ -3294,31 +3304,31 @@ pub(crate) fn append_cap_project_to_editor( source_path: &Path, ) -> Result { if same_project_path(target_project_path, source_path) { - return Err("Cannot import a recording into itself".to_string()); + return Err(tr::editor::clips::error::into_itself().to_string()); } if !is_cap_project_path(source_path) { - return Err("Select a Cap project folder".to_string()); + return Err(tr::editor::clips::error::not_a_project().to_string()); } let source_meta = RecordingMeta::load_for_project(source_path) .map_err(|e| format!("Failed to load source project metadata: {e}"))?; let RecordingMetaInner::Studio(source_studio_meta) = &source_meta.inner else { - return Err("Instant recordings can't be imported into a project yet".to_string()); + return Err(tr::editor::clips::error::instant_source().to_string()); }; match source_studio_meta.status() { StudioRecordingStatus::InProgress => { - return Err("Source Cap project is still recording".to_string()); + return Err(tr::editor::clips::error::source_recording().to_string()); } StudioRecordingStatus::Failed { error } => { - return Err(format!("Source Cap project failed: {error}")); + return Err(tr::editor::clips::error::source_failed(error)); } StudioRecordingStatus::Complete | StudioRecordingStatus::NeedsRemux => {} } let source_segments = studio_segments_for_import(source_studio_meta); if source_segments.is_empty() { - return Err("Source Cap project has no recording segments".to_string()); + return Err(tr::editor::clips::error::source_empty().to_string()); } let source_timeline = source_timeline_segments_for_import(&source_meta, &source_segments)?; @@ -3384,7 +3394,7 @@ pub(crate) fn append_cap_project_to_editor( }; if copied_segments.is_empty() { - return Err("Source Cap project has no importable recording segments".to_string()); + return Err(tr::editor::clips::error::source_unimportable().to_string()); } { diff --git a/apps/desktop-gpui/src/editor_color.rs b/apps/desktop-gpui/src/editor_color.rs index 03311d6cd94..62fb4bcc69c 100644 --- a/apps/desktop-gpui/src/editor_color.rs +++ b/apps/desktop-gpui/src/editor_color.rs @@ -45,7 +45,7 @@ use gpui::{ prelude::FluentBuilder, px, }; -use crate::{editor_window::EditorWindow, ui}; +use crate::{editor_window::EditorWindow, i18n::tr, ui}; // --------------------------------------------------------------------------- // The catalogue @@ -103,8 +103,8 @@ pub const IDENTITY: GradeValues = GradeValues { pub struct ColorPreset { pub id: &'static str, - pub label: &'static str, - pub description: &'static str, + pub label: fn() -> &'static str, + pub description: fn() -> &'static str, pub values: GradeValues, pub filter: &'static [Filter], pub overlay: Option, @@ -118,16 +118,16 @@ const fn rgba(r: u8, g: u8, b: u8, a: f32) -> [f32; 4] { pub static COLOR_CORRECTION_PRESETS: &[ColorPreset] = &[ ColorPreset { id: PRESET_NONE, - label: "None", - description: "Original colors, no grade applied", + label: tr::editor::color::preset::none::name, + description: tr::editor::color::preset::none::description, values: IDENTITY, filter: &[], overlay: None, }, ColorPreset { id: "cinematic", - label: "Cinematic", - description: "Teal shadows and orange highlights with light grain", + label: tr::editor::color::preset::cinematic::name, + description: tr::editor::color::preset::cinematic::description, values: GradeValues { contrast: 0.12, saturation: 0.06, @@ -147,8 +147,8 @@ pub static COLOR_CORRECTION_PRESETS: &[ColorPreset] = &[ }, ColorPreset { id: "noir", - label: "Noir", - description: "High-contrast black and white with heavy grain", + label: tr::editor::color::preset::noir::name, + description: tr::editor::color::preset::noir::description, values: GradeValues { exposure: 0.04, contrast: 0.3, @@ -168,8 +168,8 @@ pub static COLOR_CORRECTION_PRESETS: &[ColorPreset] = &[ }, ColorPreset { id: "vintage", - label: "Vintage", - description: "Faded warm film look with soft contrast", + label: tr::editor::color::preset::vintage::name, + description: tr::editor::color::preset::vintage::description, values: GradeValues { contrast: -0.06, saturation: -0.18, @@ -191,8 +191,8 @@ pub static COLOR_CORRECTION_PRESETS: &[ColorPreset] = &[ }, ColorPreset { id: "frost", - label: "Frost", - description: "Cool, crisp tones with muted color", + label: tr::editor::color::preset::frost::name, + description: tr::editor::color::preset::frost::description, values: GradeValues { contrast: 0.08, saturation: -0.08, @@ -215,8 +215,8 @@ pub static COLOR_CORRECTION_PRESETS: &[ColorPreset] = &[ }, ColorPreset { id: "golden", - label: "Golden", - description: "Warm golden-hour glow", + label: tr::editor::color::preset::golden::name, + description: tr::editor::color::preset::golden::description, values: GradeValues { exposure: 0.06, contrast: 0.06, @@ -241,8 +241,8 @@ pub static COLOR_CORRECTION_PRESETS: &[ColorPreset] = &[ }, ColorPreset { id: "midnight", - label: "Midnight", - description: "Dark, moody teal with subdued color", + label: tr::editor::color::preset::midnight::name, + description: tr::editor::color::preset::midnight::description, values: GradeValues { exposure: -0.08, contrast: 0.16, @@ -267,8 +267,8 @@ pub static COLOR_CORRECTION_PRESETS: &[ColorPreset] = &[ }, ColorPreset { id: "vivid", - label: "Vivid", - description: "Punchy saturation and contrast boost", + label: tr::editor::color::preset::vivid::name, + description: tr::editor::color::preset::vivid::description, values: GradeValues { exposure: 0.02, contrast: 0.14, @@ -281,8 +281,8 @@ pub static COLOR_CORRECTION_PRESETS: &[ColorPreset] = &[ }, ColorPreset { id: "dreamy", - label: "Dreamy", - description: "Soft, airy pastels with lifted blacks", + label: tr::editor::color::preset::dreamy::name, + description: tr::editor::color::preset::dreamy::description, values: GradeValues { exposure: 0.08, contrast: -0.14, @@ -360,16 +360,16 @@ impl GradeSlider { pub fn label(self) -> &'static str { match self { - Self::Grain => "Grain", - Self::Intensity => "Strength", - Self::Exposure => "Exposure", - Self::Contrast => "Contrast", - Self::Saturation => "Saturation", - Self::Temperature => "Temperature", - Self::Tint => "Tint", - Self::Fade => "Fade", - Self::SplitTone => "Split Tone", - Self::Vignette => "Vignette", + Self::Grain => tr::editor::color::slider::grain(), + Self::Intensity => tr::editor::color::slider::strength(), + Self::Exposure => tr::editor::color::slider::exposure(), + Self::Contrast => tr::editor::color::slider::contrast(), + Self::Saturation => tr::editor::color::slider::saturation(), + Self::Temperature => tr::editor::color::slider::temperature(), + Self::Tint => tr::editor::color::slider::tint(), + Self::Fade => tr::editor::color::slider::fade(), + Self::SplitTone => tr::editor::color::slider::split_tone(), + Self::Vignette => tr::editor::color::slider::vignette(), } } @@ -611,10 +611,10 @@ impl EditorWindow { .text_size(px(12.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.gray_12)) - .child(preset.label), + .child((preset.label)()), ) .tooltip({ - let description = SharedString::from(preset.description); + let description = SharedString::from((preset.description)()); move |_window, cx| { ui::Tooltip::new(&theme, description.clone()).view(cx) } @@ -629,9 +629,9 @@ impl EditorWindow { .flex() .flex_col() .gap(px(14.)) - .child(ui::Field::section(&theme, "Color Correction").child(tiles)) + .child(ui::Field::section(&theme, tr::editor::color::title()).child(tiles)) .child(self.slider_field( - "Grain", + GradeSlider::Grain.label(), crate::editor_sidebar::SliderKey::Grade(target, GradeSlider::Grain), "%", cx, @@ -641,7 +641,7 @@ impl EditorWindow { if target == GradeTarget::Screen { let grade_cursor = self.project.color_correction.grade_cursor; section = section.child( - ui::Field::inline(&theme, "Apply to cursor").value( + ui::Field::inline(&theme, tr::editor::color::apply_to_cursor()).value( ui::Toggle::plain(&theme, "grade-cursor", grade_cursor) .on_click(cx.listener(move |this, _, window, cx| { this.edit_project("grade-cursor", window, cx, |project| { @@ -664,7 +664,7 @@ impl EditorWindow { .child(crate::editor_sidebar::disclosure_row( &theme, SharedString::from(format!("grade-adjust-{}", target.key())), - "Fine-tune colors", + tr::editor::color::fine_tune(), open.is_open(), cx.listener(move |this, _, window, cx| { let next = !this.sidebar.grade_open(target).is_open(); diff --git a/apps/desktop-gpui/src/editor_crop.rs b/apps/desktop-gpui/src/editor_crop.rs index ec1f860dad1..ee6600ea9b5 100644 --- a/apps/desktop-gpui/src/editor_crop.rs +++ b/apps/desktop-gpui/src/editor_crop.rs @@ -45,6 +45,7 @@ use std::collections::BTreeSet; use crate::{ editor_window::{EDITOR_PREVIEW_FPS, EditorWindow, letterbox}, + i18n::tr, ui, }; use cap_project::{Crop, XY}; @@ -1472,17 +1473,32 @@ impl CropField { /// groups are simply adjacent -- documented in the README. The check marks and /// the keyboard contract are the real thing. pub fn crop_menu_items(aspect: Option, snap: bool) -> Vec { - let mut items = vec![ui::MenuItem::new("Free", aspect.is_none())]; + let mut items = vec![ui::MenuItem::new( + tr::editor::crop::free(), + aspect.is_none(), + )]; items.extend(COMMON_RATIOS.map(|ratio| { ui::MenuItem::new( SharedString::from(format!("{}:{}", ratio.0, ratio.1)), aspect == Some(ratio), ) })); - items.push(ui::MenuItem::new("Snap to ratios", snap)); + items.push(ui::MenuItem::new(tr::editor::crop::snap_to_ratios(), snap)); items } +/// The hint line under the stage, split around the Shift key badge: the +/// translated sentence places `{key}` wherever its grammar needs it. +pub(crate) fn crop_hint_parts() -> (String, String) { + const KEY: char = '\u{0}'; + let hint = tr::editor::crop::hint(KEY); + let (before, after) = hint.split_once(KEY).unwrap_or((hint.as_str(), "")); + ( + before.trim_end().to_string(), + after.trim_start().to_string(), + ) +} + /// What row `index` of [`crop_menu_items`] does. pub enum CropMenuChoice { Aspect(Option), @@ -1639,7 +1655,7 @@ impl EditorWindow { if let Some(target) = &state.style_target && !target.matches(&self.project) { - state.error = Some("This Style changed while cropping. Cancel and reopen Crop.".into()); + state.error = Some(tr::editor::crop::style_changed().into()); self.crop = Some(state); self.publish_project(); cx.notify(); @@ -2437,14 +2453,14 @@ impl EditorWindow { .text_size(px(14.)) .font_weight(FontWeight::SEMIBOLD) .text_color(Hsla::from(theme.editor.text_1)) - .child("Crop"), + .child(tr::editor::crop::title()), ) .child( ui::EditorButton::plain(&theme, "crop-close") .left_icon("icons/x.svg") .icon_size(px(12.)) .width(px(28.)) - .tooltip(&theme, "Close") + .tooltip(&theme, tr::common::close()) .on_click( cx.listener(|this, _, window, cx| this.cancel_crop("close", window, cx)), ), @@ -2549,40 +2565,46 @@ impl EditorWindow { .p(px(INSPECTOR_PADDING)) .rounded(px(12.)) .bg(Hsla::from(editor.card_2)) - .child(label("Preview")) + .child(label(tr::editor::crop::preview())) .child(self.render_crop_preview(px(preview_width), px(preview_height))) .child(hairline()) - .child(label("Aspect ratio")) + .child(label(tr::editor::crop::aspect_ratio())) .child(chips) - .child(row().child(row_label("Snap to ratios")).child( - ui::Toggle::plain(&theme, "crop-snap", snap).on_click(cx.listener( - |this, _, _window, cx| { - this.crop_snap_to_ratio = !this.crop_snap_to_ratio; - cx.notify(); - }, - )), - )) + .child( + row() + .child(row_label(tr::editor::crop::snap_to_ratios())) + .child( + ui::Toggle::plain(&theme, "crop-snap", snap).on_click(cx.listener( + |this, _, _window, cx| { + this.crop_snap_to_ratio = !this.crop_snap_to_ratio; + cx.notify(); + }, + )), + ), + ) .child(hairline()) .child( - row().child(row_label("Size")).child( - div() - .flex() - .flex_row() - .items_center() - .gap(px(6.)) - .child(self.render_crop_field(CropField::Width)) - .child( - div() - .w(px(8.)) - .text_size(px(12.)) - .text_color(Hsla::from(editor.text_3)) - .child("×"), - ) - .child(self.render_crop_field(CropField::Height)), - ), + row() + .child(row_label(tr::editor::crop::dimensions())) + .child( + div() + .flex() + .flex_row() + .items_center() + .gap(px(6.)) + .child(self.render_crop_field(CropField::Width)) + .child( + div() + .w(px(8.)) + .text_size(px(12.)) + .text_color(Hsla::from(editor.text_3)) + .child("×"), + ) + .child(self.render_crop_field(CropField::Height)), + ), ) .child( - row().child(row_label("Position")).child( + row().child(row_label(tr::editor::crop::position())).child( div() .flex() .flex_row() @@ -2601,7 +2623,7 @@ impl EditorWindow { .gap(px(6.)) .child(self.render_crop_action( "crop-center", - "Center", + tr::editor::crop::center(), centered, cx.listener(|this, _, window, cx| { if let Some(state) = this.crop.as_mut() { @@ -2624,7 +2646,7 @@ impl EditorWindow { )) .child(self.render_crop_action( "crop-full", - "Full", + tr::editor::crop::full(), full, cx.listener(|this, _, window, cx| { if let Some(state) = this.crop.as_mut() { @@ -2636,7 +2658,7 @@ impl EditorWindow { )) .child(self.render_crop_action( "crop-reset", - "Reset", + tr::common::reset(), untouched, cx.listener(|this, _, window, cx| { if let Some(state) = this.crop.as_mut() { @@ -2662,7 +2684,7 @@ impl EditorWindow { let editor = self.theme.editor; let label = match ratio { Some((a, b)) => SharedString::from(format!("{a}:{b}")), - None => SharedString::from("Free"), + None => SharedString::from(tr::editor::crop::free()), }; div() .id(("crop-ratio", index)) @@ -2909,7 +2931,7 @@ impl EditorWindow { .text_size(px(12.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.editor.text_2)) - .child("Loading frame…") + .child(tr::editor::crop::loading_frame()) })) .into_any_element() } @@ -2954,7 +2976,7 @@ impl EditorWindow { .text_size(px(11.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(editor.text_3)) - .child("Rendering preview…") + .child(tr::editor::crop::rendering_preview()) .into_any_element(), ), }) @@ -2964,6 +2986,7 @@ impl EditorWindow { /// The one-line hint under the stage. fn render_crop_hint(&self) -> AnyElement { let editor = self.theme.editor; + let (before, after) = crop_hint_parts(); div() .flex() .flex_row() @@ -2975,7 +2998,7 @@ impl EditorWindow { .pb(px(DIALOG_PADDING_X)) .text_size(px(12.)) .text_color(Hsla::from(editor.text_3)) - .child("Drag to move · Arrow keys nudge · Hold") + .when(!before.is_empty(), |hint| hint.child(before)) .child( div() .px(px(4.)) @@ -2984,7 +3007,7 @@ impl EditorWindow { .text_color(Hsla::from(editor.text_2)) .child("⇧"), ) - .child("to skip snapping") + .when(!after.is_empty(), |hint| hint.child(after)) .into_any_element() } @@ -2997,12 +3020,9 @@ impl EditorWindow { (Some(error), _) => Some((SharedString::from(error.clone()), Hsla::from(theme.red_11))), (None, Some(target)) => Some(( SharedString::from(if target.opting_in { - format!( - "Editing Style {} only · Saving enables its background override", - target.index + 1 - ) + tr::editor::crop::editing_style_override(target.index + 1) } else { - format!("Editing Style {} only", target.index + 1) + tr::editor::crop::editing_style(target.index + 1) }), Hsla::from(editor.text_2), )), @@ -3038,7 +3058,7 @@ impl EditorWindow { .gap(px(8.)) .child( ui::EditorButton::plain(&theme, "crop-cancel") - .label("Cancel") + .label(tr::common::cancel()) .on_click(cx.listener(|this, _, window, cx| { this.cancel_crop("cancel", window, cx) })), @@ -3100,7 +3120,7 @@ impl EditorWindow { .h(px(1.)) .bg(gpui::hsla(0., 0., 1., 0.22)), ) - .child("Save") + .child(tr::common::save()) .into_any_element() } diff --git a/apps/desktop-gpui/src/editor_export.rs b/apps/desktop-gpui/src/editor_export.rs index e3c53ce48ab..f83852968c9 100644 --- a/apps/desktop-gpui/src/editor_export.rs +++ b/apps/desktop-gpui/src/editor_export.rs @@ -25,6 +25,7 @@ use gpui::{ }; use crate::editor_window::EditorWindow; +use crate::i18n::tr; use crate::sign_in::{SignIn, SignInEvent, SignInOrigin, SignInOutcome}; use crate::store::{self, ExportPrefs}; use crate::ui; @@ -38,11 +39,20 @@ const STAGE_PADDING_X: f32 = 24.; const STAGE_VERTICAL_CHROME: f32 = 16. + 22. + 14. + 18. + STATS_HEIGHT + 20.; const STATS_HEIGHT: f32 = 44.; const SEGMENT_HEIGHT: f32 = 26.; -const COMPRESSION_PRESETS: [(ExportCompression, &str); 4] = [ - (ExportCompression::Potato, "Potato"), - (ExportCompression::Web, "Web"), - (ExportCompression::Social, "Social"), - (ExportCompression::Maximum, "Maximum"), +const COMPRESSION_PRESETS: [(ExportCompression, fn() -> &'static str); 4] = [ + ( + ExportCompression::Potato, + tr::editor::export::quality::potato, + ), + (ExportCompression::Web, tr::editor::export::quality::web), + ( + ExportCompression::Social, + tr::editor::export::quality::social, + ), + ( + ExportCompression::Maximum, + tr::editor::export::quality::maximum, + ), ]; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -57,9 +67,9 @@ impl ExportDestination { fn label(self) -> &'static str { match self { - Self::File => "File", - Self::Clipboard => "Clipboard", - Self::Link => "Shareable Link", + Self::File => tr::editor::export::destination::file(), + Self::Clipboard => tr::editor::export::destination::clipboard(), + Self::Link => tr::editor::export::destination::link(), } } @@ -453,29 +463,56 @@ fn export_file_name(pretty_name: &str, extension: &str) -> String { } fn format_estimate_range(range: [f64; 2], time: bool) -> String { + type Single = fn(f64) -> String; + type Range = fn(f64, f64) -> String; if range[1] < 1.0 { - return if time { "< 1s" } else { "< 1 MB" }.into(); + return if time { + tr::editor::export::estimate::under_second() + } else { + tr::editor::export::estimate::under_megabyte() + } + .into(); } - let (scale, unit) = if time { + let (scale, single, range_of): (f64, Single, Range) = if time { if range[1] >= 3600.0 { - (3600.0, "hr") + ( + 3600.0, + tr::editor::export::estimate::hours, + tr::editor::export::estimate::hours_range, + ) } else if range[1] >= 60.0 { - (60.0, "min") + ( + 60.0, + tr::editor::export::estimate::minutes, + tr::editor::export::estimate::minutes_range, + ) } else { - (1.0, "s") + ( + 1.0, + tr::editor::export::estimate::seconds, + tr::editor::export::estimate::seconds_range, + ) } } else if range[1] >= 1024.0 { - (1024.0, "GB") + ( + 1024.0, + tr::editor::export::estimate::gigabytes, + tr::editor::export::estimate::gigabytes_range, + ) } else { - (1.0, "MB") + ( + 1.0, + tr::editor::export::estimate::megabytes, + tr::editor::export::estimate::megabytes_range, + ) }; let precision = if scale == 1.0 { 1.0 } else { 10.0 }; let lower = ((range[0] / scale * precision).round() / precision).max(1.0 / precision); let upper = ((range[1] / scale * precision).round() / precision).max(lower); if lower == upper { - format!("~{lower} {unit}") + single(lower) } else { - format!("~{lower}–{upper} {unit}") + range_of(lower, upper) } } @@ -496,13 +533,13 @@ impl EditorWindow { .child( ui::EditorButton::plain(&self.theme, "editor-reupload") .left_icon("icons/cloud-upload.svg") - .label("Reupload") + .label(tr::editor::export::reupload()) .tooltip( &self.theme, if has_transparent_background(&self.project) { - "Share links require a background without transparency" + tr::editor::export::reupload_transparent() } else { - "Upload your latest edit to the same link" + tr::editor::export::reupload_tooltip() }, ) .disabled(!self.project_ready() || has_transparent_background(&self.project)) @@ -723,7 +760,7 @@ impl EditorWindow { ui.preview_error = Some(error.to_string()); } None => { - ui.preview_error = Some("Preview unavailable".into()); + ui.preview_error = Some(tr::editor::export::preview_unavailable().into()); } }); if !updated { @@ -1012,7 +1049,7 @@ impl EditorWindow { let _ = this.update(cx, |this, cx| { if let Some(ui) = this.export.as_mut() { ui.phase = ExportPhase::Failed; - ui.error = Some("Export task failed".into()); + ui.error = Some(tr::editor::export::task_failed().into()); } cx.notify(); }); @@ -1287,7 +1324,7 @@ impl EditorWindow { show_upload_failure( &this, cx, - "You need to sign in to share recordings", + tr::editor::export::sign_in_required(), true, ); } @@ -1302,8 +1339,8 @@ impl EditorWindow { cx.open_url(crate::auth::PRICING_URL); }); platform::alert_dialog( - "Upgrade required", - "This feature requires an upgraded plan", + tr::editor::export::upgrade_title(), + tr::editor::export::upgrade_message(), ); } Ok(Err(error)) => { @@ -1320,7 +1357,12 @@ impl EditorWindow { } Err(error) => { tracing::error!(%error, "share upload task ended without a result"); - show_upload_failure(&this, cx, "Failed to upload recording", true); + show_upload_failure( + &this, + cx, + tr::editor::export::upload_failed(), + true, + ); } } } @@ -1344,12 +1386,12 @@ impl EditorWindow { cx.notify(); }); if !cancelled { - platform::alert_dialog("Export failed", &error); + platform::alert_dialog(tr::editor::export::failed(), &error); } } Err(error) => { tracing::error!(%error, "share export task ended without a result"); - show_upload_failure(&this, cx, "Export task failed", false); + show_upload_failure(&this, cx, tr::editor::export::task_failed(), false); } } let _ = this.update_in(cx, |this, window, cx| { @@ -1421,7 +1463,7 @@ impl EditorWindow { .size(px(14.)) .text_color(Hsla::from(editor.text_2)), ) - .child("Back to editor") + .child(tr::editor::export::back()) .on_click(cx.listener(|this, _, window, cx| { this.close_export(window, cx); })), @@ -1433,7 +1475,7 @@ impl EditorWindow { .text_size(px(13.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(editor.text_1)) - .child("Export"), + .child(tr::editor::export::title()), ) .child(div().flex_1()); #[cfg(target_os = "windows")] @@ -1539,7 +1581,7 @@ impl EditorWindow { .child( ui.preview_error .clone() - .unwrap_or_else(|| "Generating preview…".into()), + .unwrap_or_else(|| tr::editor::export::generating_preview().into()), ) .into_any_element(), }; @@ -1565,7 +1607,7 @@ impl EditorWindow { .text_size(px(12.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(editor.text_2)) - .child("Preview"), + .child(tr::editor::export::preview()), ) .child( div() @@ -1573,12 +1615,8 @@ impl EditorWindow { .flex() .items_center() .tooltip(move |_window, cx| { - crate::ui::Tooltip::new( - &theme, - "This is a rendered frame from your video. Adjust the \ - settings to see the quality of the final export.", - ) - .view(cx) + crate::ui::Tooltip::new(&theme, tr::editor::export::preview_info()) + .view(cx) }) .child( svg() @@ -1611,24 +1649,28 @@ impl EditorWindow { .shadow(editor.card_shadow()) .child(export_stat( &theme, - "Duration", + tr::editor::export::stats::duration(), stats.map(|_| format_duration(duration)), false, )) .child(export_stat( &theme, - "Output", + tr::editor::export::stats::output(), stats.map(|stats| { - format!("{}×{} · {} fps", stats.width, stats.height, ui.fps) + tr::editor::export::stats::output_value( + stats.width, + stats.height, + ui.fps, + ) }), true, )) .child(export_stat( &theme, if ui.estimate_loading && ui.estimate.is_some() { - "Refining size…" + tr::editor::export::stats::refining_size() } else { - "Estimated size" + tr::editor::export::stats::estimated_size() }, Some( ui.estimate @@ -1638,9 +1680,9 @@ impl EditorWindow { }) .unwrap_or_else(|| { if ui.estimate_loading { - "Calculating…".into() + tr::editor::export::stats::calculating().into() } else { - "Unavailable".into() + tr::editor::export::stats::unavailable().into() } }), ), @@ -1649,9 +1691,9 @@ impl EditorWindow { .child(export_stat( &theme, if ui.estimate_loading && ui.estimate.is_some() { - "Refining time…" + tr::editor::export::stats::refining_time() } else { - "Export time" + tr::editor::export::stats::export_time() }, Some( ui.estimate @@ -1661,9 +1703,9 @@ impl EditorWindow { }) .unwrap_or_else(|| { if ui.estimate_loading { - "Calculating…".into() + tr::editor::export::stats::calculating().into() } else { - "Unavailable".into() + tr::editor::export::stats::unavailable().into() } }), ), @@ -1729,17 +1771,29 @@ impl EditorWindow { let signed_in = !ui.auth_rejected && store::auth_snapshot().signed_in(); let sign_in_pending = SignIn::global(cx).read(cx).is_pending(); let (label, icon, primary) = match ui.destination { - ExportDestination::File => ("Export to File", Some("icons/folder.svg"), true), - ExportDestination::Clipboard => ("Export to Clipboard", Some("icons/copy.svg"), true), - ExportDestination::Link if sign_in_pending => ("Cancel Sign In", None, false), - ExportDestination::Link if !signed_in => { - ("Sign in to share", Some("icons/link.svg"), true) + ExportDestination::File => ( + tr::editor::export::cta::file(), + Some("icons/folder.svg"), + true, + ), + ExportDestination::Clipboard => ( + tr::editor::export::cta::clipboard(), + Some("icons/copy.svg"), + true, + ), + ExportDestination::Link if sign_in_pending => { + (tr::editor::export::cta::cancel_sign_in(), None, false) } + ExportDestination::Link if !signed_in => ( + tr::editor::export::cta::sign_in(), + Some("icons/link.svg"), + true, + ), ExportDestination::Link => ( if self.sharing.is_some() { - "Reupload to same link" + tr::editor::export::cta::reupload() } else { - "Create shareable link" + tr::editor::export::cta::create_link() }, Some("icons/link.svg"), true, @@ -1809,178 +1863,180 @@ impl EditorWindow { ) -> impl IntoElement { let theme = self.theme; let editor = theme.editor; - self.export_section("Destination", "icons/upload-arrow.svg") - .child( - ui::SegmentedControl::editor( - &theme, - "export-destination", - ExportDestination::ALL - .iter() - .map(|dest| { - let label = if *dest == ExportDestination::Link - && self.sharing.is_some() - { - "Reupload" - } else { - dest.label() - }; - let mut option = - ui::SegmentOption::new(label, ui.destination == *dest) - .disabled(*dest == ExportDestination::Link && link_disabled); - option.icon = Some(dest.icon().into()); - option - }) - .collect(), - ) - .stretch() - .item_height(px(30.)) - .on_select(cx.listener(|this, index: &usize, window, cx| { - let Some(dest) = ui::option_at(ExportDestination::ALL, *index) else { + self.export_section( + tr::editor::export::section::destination(), + "icons/upload-arrow.svg", + ) + .child( + ui::SegmentedControl::editor( + &theme, + "export-destination", + ExportDestination::ALL + .iter() + .map(|dest| { + let label = if *dest == ExportDestination::Link && self.sharing.is_some() { + tr::editor::export::reupload() + } else { + dest.label() + }; + let mut option = ui::SegmentOption::new(label, ui.destination == *dest) + .disabled(*dest == ExportDestination::Link && link_disabled); + option.icon = Some(dest.icon().into()); + option + }) + .collect(), + ) + .stretch() + .item_height(px(30.)) + .on_select(cx.listener(|this, index: &usize, window, cx| { + let Some(dest) = ui::option_at(ExportDestination::ALL, *index) else { + return; + }; + if let Some(ui) = this.export.as_mut() { + if dest == ExportDestination::Link + && (has_transparent_background(&this.project) || ui.cursor_only) + { return; - }; - if let Some(ui) = this.export.as_mut() { - if dest == ExportDestination::Link - && (has_transparent_background(&this.project) || ui.cursor_only) - { - return; - } - ui.destination = dest; - if dest == ExportDestination::Link && ui.format == ExportFormatKind::Gif { - ui.format = ExportFormatKind::Mp4; - } - ui.persist(); } - this.refresh_export_preview(window, cx); - cx.notify(); - })), + ui.destination = dest; + if dest == ExportDestination::Link && ui.format == ExportFormatKind::Gif { + ui.format = ExportFormatKind::Mp4; + } + ui.persist(); + } + this.refresh_export_preview(window, cx); + cx.notify(); + })), + ) + .when(link_disabled, |field| { + field.child( + div() + .text_size(px(11.)) + .text_color(Hsla::from(editor.text_3)) + .child(if ui.cursor_only { + tr::editor::export::link_unavailable_cursor() + } else { + tr::editor::export::link_unavailable_transparent() + }), ) - .when(link_disabled, |field| { + }) + .when( + ui.destination == ExportDestination::Link && self.sharing.is_some(), + |field| { + let link = self + .sharing + .as_ref() + .map(|sharing| sharing.link.clone()) + .unwrap_or_default(); field.child( div() - .text_size(px(11.)) - .text_color(Hsla::from(editor.text_3)) - .child(if ui.cursor_only { - "Cursor-only exports can only be saved to a file or clipboard." - } else { - "Transparent exports can only be saved to a file or clipboard." - }), + .p(px(12.)) + .rounded(px(10.)) + .bg(Hsla::from(editor.card_2)) + .flex() + .flex_col() + .gap(px(4.)) + .child( + div() + .text_size(px(12.5)) + .font_weight(FontWeight::MEDIUM) + .text_color(Hsla::from(editor.text_1)) + .child(tr::editor::export::existing_link_title()), + ) + .child( + div() + .text_size(px(11.5)) + .line_height(px(15.)) + .text_color(Hsla::from(editor.text_3)) + .child(tr::editor::export::existing_link_description()), + ) + .child( + div() + .id("reupload-existing-link") + .text_size(px(11.5)) + .text_color(Hsla::from(editor.accent)) + .truncate() + .cursor_pointer() + .child(link.clone()) + .on_click(move |_, _, cx| cx.open_url(&link)), + ), ) - }) - .when( - ui.destination == ExportDestination::Link && self.sharing.is_some(), - |field| { - let link = self.sharing.as_ref() - .map(|sharing| sharing.link.clone()) - .unwrap_or_default(); - field.child( - div() - .p(px(12.)) - .rounded(px(10.)) - .bg(Hsla::from(editor.card_2)) - .flex() - .flex_col() - .gap(px(4.)) - .child( - div() - .text_size(px(12.5)) - .font_weight(FontWeight::MEDIUM) - .text_color(Hsla::from(editor.text_1)) - .child("Update your existing link"), - ) - .child( - div() - .text_size(px(11.5)) - .line_height(px(15.)) - .text_color(Hsla::from(editor.text_3)) - .child("Reupload replaces the video at this link with your latest edit. Everyone with the link will see the updated version."), - ) - .child( - div() - .id("reupload-existing-link") - .text_size(px(11.5)) - .text_color(Hsla::from(editor.accent)) - .truncate() - .cursor_pointer() - .child(link.clone()) - .on_click(move |_, _, cx| cx.open_url(&link)), - ), - ) - }, - ) - .when( - ui.destination == ExportDestination::Link - && self.sharing.is_none() - && store::auth_snapshot().organizations.len() > 1, - |this| { - let orgs = store::auth_snapshot().organizations; - let selected = ui - .organization_id - .clone() - .or_else(|| orgs.first().map(|org| org.id.clone())); - let label = orgs - .iter() - .find(|org| Some(org.id.as_str()) == selected.as_deref()) - .or(orgs.first()) - .map(|org| org.name.clone()) - .unwrap_or_else(|| "Organization".into()); - this.child( - div() - .id("export-organization") - .flex() - .flex_row() - .items_center() - .justify_between() - .h(px(30.)) - .px(px(10.)) - .rounded(px(7.)) - .bg(Hsla::from(editor.ctl)) - .cursor_pointer() - .hover(move |style| style.bg(Hsla::from(editor.ctl_hover))) - .on_click(cx.listener(move |this, _, _, cx| { - let orgs = store::auth_snapshot().organizations; - if orgs.is_empty() { - return; - } - if let Some(ui) = this.export.as_mut() { - let current = ui - .organization_id - .as_deref() - .or(orgs.first().map(|org| org.id.as_str())); - let index = orgs - .iter() - .position(|org| Some(org.id.as_str()) == current) - .unwrap_or(0); - let next = orgs[(index + 1) % orgs.len()].id.clone(); - ui.organization_id = Some(next); - ui.persist(); - } - cx.notify(); - })) - .child( - div() - .text_size(px(12.)) - .text_color(Hsla::from(editor.text_2)) - .child("Organization"), - ) - .child( - div() - .flex() - .flex_row() - .items_center() - .gap(px(4.)) - .text_size(px(12.)) - .text_color(Hsla::from(editor.text_1)) - .child(label) - .child( - svg() - .path("icons/caret-down.svg") - .size(px(14.)) - .text_color(Hsla::from(editor.text_3)), - ), - ), - ) - }, - ) + }, + ) + .when( + ui.destination == ExportDestination::Link + && self.sharing.is_none() + && store::auth_snapshot().organizations.len() > 1, + |this| { + let orgs = store::auth_snapshot().organizations; + let selected = ui + .organization_id + .clone() + .or_else(|| orgs.first().map(|org| org.id.clone())); + let label = orgs + .iter() + .find(|org| Some(org.id.as_str()) == selected.as_deref()) + .or(orgs.first()) + .map(|org| org.name.clone()) + .unwrap_or_else(|| tr::editor::export::organization().into()); + this.child( + div() + .id("export-organization") + .flex() + .flex_row() + .items_center() + .justify_between() + .h(px(30.)) + .px(px(10.)) + .rounded(px(7.)) + .bg(Hsla::from(editor.ctl)) + .cursor_pointer() + .hover(move |style| style.bg(Hsla::from(editor.ctl_hover))) + .on_click(cx.listener(move |this, _, _, cx| { + let orgs = store::auth_snapshot().organizations; + if orgs.is_empty() { + return; + } + if let Some(ui) = this.export.as_mut() { + let current = ui + .organization_id + .as_deref() + .or(orgs.first().map(|org| org.id.as_str())); + let index = orgs + .iter() + .position(|org| Some(org.id.as_str()) == current) + .unwrap_or(0); + let next = orgs[(index + 1) % orgs.len()].id.clone(); + ui.organization_id = Some(next); + ui.persist(); + } + cx.notify(); + })) + .child( + div() + .text_size(px(12.)) + .text_color(Hsla::from(editor.text_2)) + .child(tr::editor::export::organization()), + ) + .child( + div() + .flex() + .flex_row() + .items_center() + .gap(px(4.)) + .text_size(px(12.)) + .text_color(Hsla::from(editor.text_1)) + .child(label) + .child( + svg() + .path("icons/caret-down.svg") + .size(px(14.)) + .text_color(Hsla::from(editor.text_3)), + ), + ), + ) + }, + ) } fn export_format_field( @@ -1991,7 +2047,7 @@ impl EditorWindow { ) -> impl IntoElement { let theme = self.theme; let options = [ExportFormatKind::Mp4, ExportFormatKind::Gif]; - self.export_section("Format", "icons/video.svg") + self.export_section(tr::editor::export::section::format(), "icons/video.svg") .disabled(locked) .child( ui::SegmentedControl::editor( @@ -2043,24 +2099,63 @@ impl EditorWindow { ) -> impl IntoElement { let theme = self.theme; let resolutions = resolutions.to_vec(); - self.export_section("Resolution", "icons/monitor-outline.svg") + self.export_section( + tr::editor::export::section::resolution(), + "icons/monitor-outline.svg", + ) + .child( + ui::SegmentedControl::editor( + &theme, + "export-resolution", + resolutions + .iter() + .map(|res| ui::SegmentOption::new(res.label(), ui.resolution == *res)) + .collect(), + ) + .stretch() + .item_height(px(SEGMENT_HEIGHT)) + .on_select(cx.listener(move |this, index: &usize, window, cx| { + let Some(resolution) = resolutions.get(*index).copied() else { + return; + }; + if let Some(ui) = this.export.as_mut() { + ui.resolution = resolution; + ui.persist(); + } + this.refresh_export_preview(window, cx); + cx.notify(); + })), + ) + } + + fn export_fps_field( + &self, + ui: &ExportUi, + options: &[u32], + cx: &mut Context, + ) -> impl IntoElement { + let theme = self.theme; + let options = options.to_vec(); + self.export_section(tr::editor::export::section::frame_rate(), "icons/gauge.svg") .child( ui::SegmentedControl::editor( &theme, - "export-resolution", - resolutions + "export-fps", + options .iter() - .map(|res| ui::SegmentOption::new(res.label(), ui.resolution == *res)) + .map(|fps| { + ui::SegmentOption::new(tr::editor::export::fps(fps), ui.fps == *fps) + }) .collect(), ) .stretch() .item_height(px(SEGMENT_HEIGHT)) .on_select(cx.listener(move |this, index: &usize, window, cx| { - let Some(resolution) = resolutions.get(*index).copied() else { + let Some(fps) = options.get(*index).copied() else { return; }; if let Some(ui) = this.export.as_mut() { - ui.resolution = resolution; + ui.fps = fps; ui.persist(); } this.refresh_export_preview(window, cx); @@ -2069,43 +2164,10 @@ impl EditorWindow { ) } - fn export_fps_field( - &self, - ui: &ExportUi, - options: &[u32], - cx: &mut Context, - ) -> impl IntoElement { - let theme = self.theme; - let options = options.to_vec(); - self.export_section("Frame rate", "icons/gauge.svg").child( - ui::SegmentedControl::editor( - &theme, - "export-fps", - options - .iter() - .map(|fps| ui::SegmentOption::new(format!("{fps} FPS"), ui.fps == *fps)) - .collect(), - ) - .stretch() - .item_height(px(SEGMENT_HEIGHT)) - .on_select(cx.listener(move |this, index: &usize, window, cx| { - let Some(fps) = options.get(*index).copied() else { - return; - }; - if let Some(ui) = this.export.as_mut() { - ui.fps = fps; - ui.persist(); - } - this.refresh_export_preview(window, cx); - cx.notify(); - })), - ) - } - fn export_quality_field(&self, ui: &ExportUi, cx: &mut Context) -> impl IntoElement { let theme = self.theme; let editor = theme.editor; - self.export_section("Quality", "icons/gem.svg") + self.export_section(tr::editor::export::section::quality(), "icons/gem.svg") .child( ui::SegmentedControl::editor( &theme, @@ -2114,7 +2176,7 @@ impl EditorWindow { .iter() .map(|(value, label)| { ui::SegmentOption::new( - *label, + label(), !ui.is_custom_bpp() && matches_compression(ui.compression, *value), ) }) @@ -2143,8 +2205,8 @@ impl EditorWindow { .px(px(2.)) .text_size(px(10.5)) .text_color(Hsla::from(editor.text_3)) - .child("Smaller file") - .child("Larger file"), + .child(tr::editor::export::quality::smaller()) + .child(tr::editor::export::quality::larger()), ) .child(self.export_optimize_row(ui, cx)) } @@ -2192,8 +2254,8 @@ impl EditorWindow { fn export_optimize_row(&self, ui: &ExportUi, cx: &mut Context) -> impl IntoElement { self.export_toggle_row( "export-optimize", - "Optimize file size", - "Re-encodes with software for much smaller files (slower)", + tr::editor::export::optimize::title(), + tr::editor::export::optimize::description(), ui.optimize_filesize, cx.listener(|this, _, window, cx| { if let Some(ui) = this.export.as_mut() { @@ -2247,7 +2309,7 @@ impl EditorWindow { .size(px(14.)) .text_color(Hsla::from(editor.text_2)), ) - .child("Advanced") + .child(tr::editor::export::advanced::title()) .child( svg() .path(if ui.advanced_open { @@ -2263,8 +2325,8 @@ impl EditorWindow { .when(ui.advanced_open, |this| { this.child(self.export_toggle_row( "export-cursor-only", - "Export cursor only", - "Keeps the same cursor motion and clicks on a transparent background", + tr::editor::export::advanced::cursor_only(), + tr::editor::export::advanced::cursor_only_description(), ui.cursor_only, cx.listener(|this, _, window, cx| { if let Some(ui) = this.export.as_mut() { @@ -2302,9 +2364,7 @@ impl EditorWindow { .text_size(px(11.5)) .line_height(px(15.)) .text_color(Hsla::from(editor.text_2)) - .child( - "Exports as a transparent MOV. Files are large and best for compositing or editing.", - ), + .child(tr::editor::export::advanced::transparent_mov()), ), ) }) @@ -2323,7 +2383,7 @@ impl EditorWindow { .min_w(px(96.)) .text_size(px(13.)) .text_color(Hsla::from(editor.text_1)) - .child("Bits per pixel"), + .child(tr::editor::export::advanced::bits_per_pixel()), ) .child( div() @@ -2351,11 +2411,13 @@ impl EditorWindow { Some(gpui::hsla(0., 0., 0., 0.12)), ) .thumb_shadow() - .on_drag_start(cx.listener( - |this, event: &MouseDownEvent, _window, cx| { - this.export_bpp_mouse_down(event, cx); - }, - )), + .on_drag_start( + cx.listener( + |this, event: &MouseDownEvent, _window, cx| { + this.export_bpp_mouse_down(event, cx); + }, + ), + ), ), ) .child( @@ -2373,15 +2435,15 @@ impl EditorWindow { div() .text_size(px(11.)) .text_color(Hsla::from(editor.text_3)) - .child("Using a custom bitrate"), + .child(tr::editor::export::advanced::custom_bitrate()), ) }) }) .when(cfg!(target_os = "macos") && show_bpp, |this| { this.child(self.export_toggle_row( "export-force-ffmpeg", - "Force FFmpeg decoder", - "Skip hardware decoder (auto-fallback enabled)", + tr::editor::export::advanced::force_ffmpeg(), + tr::editor::export::advanced::force_ffmpeg_description(), ui.force_ffmpeg, cx.listener(|this, _, window, cx| { if let Some(ui) = this.export.as_mut() { @@ -2457,28 +2519,34 @@ impl EditorWindow { Some(ui.rendered as f32 / ui.total_frames as f32) }; let heading = match ui.phase { - ExportPhase::Starting => "Preparing export", - ExportPhase::Rendering if ui.cursor_only => "Rendering cursor track", - ExportPhase::Rendering if ui.format == ExportFormatKind::Gif => "Rendering GIF", - ExportPhase::Rendering => "Rendering video", + ExportPhase::Starting => tr::editor::export::phase::starting(), + ExportPhase::Rendering if ui.cursor_only => { + tr::editor::export::phase::rendering_cursor() + } + ExportPhase::Rendering if ui.format == ExportFormatKind::Gif => { + tr::editor::export::phase::rendering_gif() + } + ExportPhase::Rendering => tr::editor::export::phase::rendering_video(), ExportPhase::Copying if ui.destination == ExportDestination::Clipboard => { - "Copying to clipboard" + tr::editor::export::phase::copying_to_clipboard() } - ExportPhase::Copying => "Saving to file", - ExportPhase::Uploading if ui.reuploading => "Reuploading to your link", - ExportPhase::Uploading => "Creating shareable link", + ExportPhase::Copying => tr::editor::export::phase::saving_to_file(), + ExportPhase::Uploading if ui.reuploading => tr::editor::export::phase::reuploading(), + ExportPhase::Uploading => tr::editor::export::phase::uploading(), ExportPhase::Done if ui.destination == ExportDestination::Clipboard => { - "Copied to clipboard" + tr::editor::export::phase::copied() } ExportPhase::Done if ui.destination == ExportDestination::Link && ui.reuploading => { - "Reupload complete" + tr::editor::export::phase::reupload_complete() + } + ExportPhase::Done if ui.destination == ExportDestination::Link => { + tr::editor::export::phase::upload_complete() } - ExportPhase::Done if ui.destination == ExportDestination::Link => "Upload complete", - ExportPhase::Done => "Export complete", + ExportPhase::Done => tr::editor::export::phase::complete(), ExportPhase::Failed if ui.clipboard_retry_path().is_some() => { - "Export complete; clipboard copy failed" + tr::editor::export::phase::clipboard_failed() } - ExportPhase::Failed => "Export failed", + ExportPhase::Failed => tr::editor::export::failed(), ExportPhase::Idle | ExportPhase::ChoosingFile => "", }; @@ -2523,30 +2591,35 @@ impl EditorWindow { .child(heading), ) .when( - ui.phase == ExportPhase::Done - && ui.destination == ExportDestination::Link, + ui.phase == ExportPhase::Done && ui.destination == ExportDestination::Link, |this| { this.child( div() .text_size(px(12.)) .text_color(Hsla::from(editor.text_2)) .child(if ui.reuploading { - "Your latest edit is ready at the same link" + tr::editor::export::done::reuploaded() } else { - "Your Cap has been uploaded successfully" + tr::editor::export::done::uploaded() }), ) }, ), ) - .when(ui.phase == ExportPhase::Rendering && ui.total_frames > 0, |this| { - this.child( - div() - .text_size(px(12.)) - .text_color(Hsla::from(editor.text_2)) - .child(format!("{} / {} frames", ui.rendered, ui.total_frames)), - ) - }) + .when( + ui.phase == ExportPhase::Rendering && ui.total_frames > 0, + |this| { + this.child( + div() + .text_size(px(12.)) + .text_color(Hsla::from(editor.text_2)) + .child(tr::editor::export::frames( + ui.total_frames as usize, + ui.rendered, + )), + ) + }, + ) .when_some(ui.error.clone(), |this, error| { this.child( div() @@ -2565,7 +2638,7 @@ impl EditorWindow { ui::ButtonVariant::Gray, ui::ButtonSize::Md, ) - .label("Cancel") + .label(tr::common::cancel()) .on_click(cx.listener(|this, _, window, cx| { let Some(ui) = this.export.as_mut() else { return; @@ -2593,10 +2666,10 @@ impl EditorWindow { #[cfg(not(target_os = "linux"))] cx.spawn_in(window, async move |this, cx| { let confirmed = platform::confirm_dialog( - "Cancel export?", - "Are you sure you want to cancel the export?", - "Cancel export", - "Keep exporting", + tr::editor::modal::cancel_export_title(), + tr::editor::modal::cancel_export_message(), + tr::editor::modal::cancel_export(), + tr::editor::modal::keep_exporting(), true, ); let _ = this.update(cx, |this, cx| { @@ -2615,98 +2688,108 @@ impl EditorWindow { .text_size(px(12.)) .text_color(Hsla::from(editor.text_2)) .text_center() - .child( - "Use Instant Mode for your next recording if you want a link the moment you stop.", - ), + .child(tr::editor::export::done::instant_hint()), ) }) - .when(ui.phase == ExportPhase::Done || ui.phase == ExportPhase::Failed, |this| { - this.when(ui.clipboard_retry_path().is_some(), |this| { - this.child( - ui::Button::plain( - &theme, - "export-retry-copy", - ui::ButtonVariant::Gray, - ui::ButtonSize::Md, - ) - .icon("icons/copy.svg") - .label("Retry Copy") - .on_click(cx.listener(|this, _, window, cx| { - this.retry_clipboard_copy(window, cx); - })), - ) - }) - .when( - (ui.phase == ExportPhase::Done && ui.destination == ExportDestination::File) - || ui.clipboard_retry_path().is_some(), - |this| { - let path = ui.output_path.clone(); + .when( + ui.phase == ExportPhase::Done || ui.phase == ExportPhase::Failed, + |this| { + this.when(ui.clipboard_retry_path().is_some(), |this| { this.child( ui::Button::plain( &theme, - "export-open-file", + "export-retry-copy", ui::ButtonVariant::Gray, ui::ButtonSize::Md, ) - .label("Open File") - .on_click(move |_, _, cx| { - if let Some(path) = &path { - library::open_with_system(path, cx); - } - }), + .icon("icons/copy.svg") + .label(tr::editor::export::done::retry_copy()) + .on_click(cx.listener( + |this, _, window, cx| { + this.retry_clipboard_copy(window, cx); + }, + )), ) - }, - ) - .when( - ui.phase == ExportPhase::Done - && ui.destination == ExportDestination::Link - && ui.share_link.is_some(), - |this| { - let copied = ui.copy_link_pressed; - let open = ui.share_link.clone().unwrap_or_default(); - this.child( - ui::Button::plain( - &theme, - "export-copy-link", - ui::ButtonVariant::Gray, - ui::ButtonSize::Md, + }) + .when( + (ui.phase == ExportPhase::Done + && ui.destination == ExportDestination::File) + || ui.clipboard_retry_path().is_some(), + |this| { + let path = ui.output_path.clone(); + this.child( + ui::Button::plain( + &theme, + "export-open-file", + ui::ButtonVariant::Gray, + ui::ButtonSize::Md, + ) + .label(tr::editor::export::done::open_file()) + .on_click(move |_, _, cx| { + if let Some(path) = &path { + library::open_with_system(path, cx); + } + }), ) - .icon(if copied { - "icons/check.svg" - } else { - "icons/copy.svg" - }) - .label(if copied { "Link copied" } else { "Copy link" }) - .on_click(cx.listener(|this, _, window, cx| { - this.copy_share_link(window, cx); - })), - ) - .child( - ui::Button::plain( - &theme, - "export-open-link", - ui::ButtonVariant::Primary, - ui::ButtonSize::Md, + }, + ) + .when( + ui.phase == ExportPhase::Done + && ui.destination == ExportDestination::Link + && ui.share_link.is_some(), + |this| { + let copied = ui.copy_link_pressed; + let open = ui.share_link.clone().unwrap_or_default(); + this.child( + ui::Button::plain( + &theme, + "export-copy-link", + ui::ButtonVariant::Gray, + ui::ButtonSize::Md, + ) + .icon(if copied { + "icons/check.svg" + } else { + "icons/copy.svg" + }) + .label(if copied { + tr::editor::export::done::link_copied() + } else { + tr::editor::export::done::copy_link() + }) + .on_click(cx.listener( + |this, _, window, cx| { + this.copy_share_link(window, cx); + }, + )), ) - .icon("icons/link.svg") - .label("Open link") - .on_click(move |_, _, cx| { - cx.open_url(&open); - }), + .child( + ui::Button::plain( + &theme, + "export-open-link", + ui::ButtonVariant::Primary, + ui::ButtonSize::Md, + ) + .icon("icons/link.svg") + .label(tr::editor::export::done::open_link()) + .on_click(move |_, _, cx| { + cx.open_url(&open); + }), + ) + }, + ) + .child( + ui::Button::plain( + &theme, + "export-back-done", + ui::ButtonVariant::Gray, + ui::ButtonSize::Md, ) - }, - ) - .child( - ui::Button::plain( - &theme, - "export-back-done", - ui::ButtonVariant::Gray, - ui::ButtonSize::Md, + .label(tr::editor::export::back()) + .on_click(cx.listener(|this, _, window, cx| this.close_export(window, cx))), ) - .label("Back to editor") - .on_click(cx.listener(|this, _, window, cx| this.close_export(window, cx))), - ) - }) + }, + ) } } @@ -2748,10 +2831,10 @@ async fn confirm_export_resources( }); let response = (current && !window.has_active_prompt()).then(|| { crate::editor_modal::confirm_action( - "Check export resources", + tr::editor::export::resources::title(), &message, - "Proceed anyway", - "Go back", + tr::editor::export::resources::proceed(), + tr::common::go_back(), window, cx, ) @@ -2785,10 +2868,10 @@ async fn confirm_export_resources( return Ok(false); } platform::confirm_dialog( - "Check export resources", + tr::editor::export::resources::title(), &message, - "Proceed anyway", - "Go back", + tr::editor::export::resources::proceed(), + tr::common::go_back(), false, ) }; @@ -2850,7 +2933,7 @@ fn show_upload_failure( cx.notify(); }); if dialog { - platform::alert_dialog("Failed to upload recording", &message); + platform::alert_dialog(tr::editor::export::upload_failed(), &message); } } @@ -2864,6 +2947,7 @@ fn export_stat( div() .flex() .flex_1() + .min_w_0() .flex_col() .justify_center() .px(px(16.)) @@ -2876,6 +2960,7 @@ fn export_stat( .text_size(px(10.5)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(editor.text_3)) + .truncate() .child(label), ) .child(match value { @@ -3001,6 +3086,20 @@ async fn run_export( #[cfg(test)] mod tests { + #[test] + fn estimate_ranges_keep_their_english_wording() { + use super::format_estimate_range; + + assert_eq!(format_estimate_range([0.2, 0.5], false), "< 1 MB"); + assert_eq!(format_estimate_range([0.2, 0.5], true), "< 1s"); + assert_eq!(format_estimate_range([12.0, 12.0], false), "~12 MB"); + assert_eq!(format_estimate_range([12.3, 40.7], false), "~12–41 MB"); + assert_eq!(format_estimate_range([2048.0, 3500.0], false), "~2–3.4 GB"); + assert_eq!(format_estimate_range([30.0, 45.0], true), "~30–45 s"); + assert_eq!(format_estimate_range([90.0, 150.0], true), "~1.5–2.5 min"); + assert_eq!(format_estimate_range([7200.0, 7200.0], true), "~2 hr"); + } + use super::{ ExportDestination, ExportPhase, ExportUi, cancel_matching_export, export_file_name, }; diff --git a/apps/desktop-gpui/src/editor_modal.rs b/apps/desktop-gpui/src/editor_modal.rs index 245bbef6254..2068338b8af 100644 --- a/apps/desktop-gpui/src/editor_modal.rs +++ b/apps/desktop-gpui/src/editor_modal.rs @@ -7,7 +7,7 @@ use gpui::{ div, prelude::FluentBuilder, px, }; -use crate::{theme::Theme, ui}; +use crate::{i18n::tr, theme::Theme, ui}; #[derive(Clone, Copy)] enum MessageKind { @@ -20,7 +20,8 @@ pub(crate) fn retained_alert( window: &mut Window, cx: &mut App, ) -> impl Future + use<> { - let response = informational_alert("Recording retained", message, window, cx); + let response = + informational_alert(tr::editor::modal::recording_retained(), message, window, cx); async move { let _ = response.await; } @@ -36,7 +37,7 @@ pub(crate) fn informational_alert( MessageKind::Retained, title, message, - vec![PromptButton::ok("OK")], + vec![PromptButton::ok(tr::common::ok())], window, cx, ); @@ -48,10 +49,10 @@ pub(crate) fn confirm_cancel_export( cx: &mut App, ) -> impl Future + use<> { confirm_action( - "Cancel export?", - "Are you sure you want to cancel the export?", - "Cancel export", - "Keep exporting", + tr::editor::modal::cancel_export_title(), + tr::editor::modal::cancel_export_message(), + tr::editor::modal::cancel_export(), + tr::editor::modal::keep_exporting(), window, cx, ) diff --git a/apps/desktop-gpui/src/editor_panels.rs b/apps/desktop-gpui/src/editor_panels.rs index 1c773856704..60b6c5c9d61 100644 --- a/apps/desktop-gpui/src/editor_panels.rs +++ b/apps/desktop-gpui/src/editor_panels.rs @@ -42,6 +42,7 @@ use crate::{ editor_tabs::{OffsetKind, SidebarMenu}, editor_timeline::TrackKind, editor_window::EditorWindow, + i18n::tr, ui, }; @@ -58,49 +59,88 @@ pub const MIN_VOLUME_DB: f32 = -30.; pub const MAX_VOLUME_DB: f32 = 12.; /// `TEXT_SEGMENT_WEIGHT_OPTIONS` (`text-style.tsx:42-50`). -pub const TEXT_SEGMENT_WEIGHTS: [(f32, &str); 7] = [ - (300., "Light"), - (400., "Regular"), - (500., "Medium"), - (600., "Semibold"), - (700., "Bold"), - (800., "Extra Bold"), - (900., "Black"), +pub const TEXT_SEGMENT_WEIGHTS: [(f32, fn() -> &'static str); 7] = [ + (300., tr::editor::panel::font_weight::light), + (400., tr::editor::panel::font_weight::regular), + (500., tr::editor::panel::font_weight::medium), + (600., tr::editor::panel::font_weight::semibold), + (700., tr::editor::panel::font_weight::bold), + (800., tr::editor::panel::font_weight::extra_bold), + (900., tr::editor::panel::font_weight::black), ]; /// `TEXT_ANIMATION_OPTIONS` (`text-style.tsx:52-62`), in the renderer's own /// variant order. -pub const TEXT_ANIMATIONS: [(TextAnimation, &str); 14] = [ - (TextAnimation::None, "None"), - (TextAnimation::Fade, "Fade"), - (TextAnimation::SlideUp, "Slide up"), - (TextAnimation::SlideDown, "Slide down"), - (TextAnimation::SlideLeft, "Slide left"), - (TextAnimation::SlideRight, "Slide right"), - (TextAnimation::Pop, "Pop"), - (TextAnimation::Zoom, "Zoom"), - (TextAnimation::Bounce, "Bounce"), - (TextAnimation::Wipe, "Wipe"), - (TextAnimation::Words, "Words"), - (TextAnimation::Letters, "Letters"), - (TextAnimation::Tracking, "Tracking"), - (TextAnimation::Typewriter, "Typewriter"), +pub const TEXT_ANIMATIONS: [(TextAnimation, fn() -> &'static str); 14] = [ + (TextAnimation::None, tr::editor::panel::text_animation::none), + (TextAnimation::Fade, tr::editor::panel::text_animation::fade), + ( + TextAnimation::SlideUp, + tr::editor::panel::text_animation::slide_up, + ), + ( + TextAnimation::SlideDown, + tr::editor::panel::text_animation::slide_down, + ), + ( + TextAnimation::SlideLeft, + tr::editor::panel::text_animation::slide_left, + ), + ( + TextAnimation::SlideRight, + tr::editor::panel::text_animation::slide_right, + ), + (TextAnimation::Pop, tr::editor::panel::text_animation::pop), + (TextAnimation::Zoom, tr::editor::panel::text_animation::zoom), + ( + TextAnimation::Bounce, + tr::editor::panel::text_animation::bounce, + ), + (TextAnimation::Wipe, tr::editor::panel::text_animation::wipe), + ( + TextAnimation::Words, + tr::editor::panel::text_animation::words, + ), + ( + TextAnimation::Letters, + tr::editor::panel::text_animation::letters, + ), + ( + TextAnimation::Tracking, + tr::editor::panel::text_animation::tracking, + ), + ( + TextAnimation::Typewriter, + tr::editor::panel::text_animation::typewriter, + ), ]; /// `TEXT_BACKGROUND_STYLE_OPTIONS` (`text-style.tsx`). The panel's segmented /// control shows a fourth "None" ahead of these, which is `backgroundColor` /// cleared rather than a style of its own. -pub const TEXT_BACKGROUND_STYLES: [(TextBackgroundStyle, &str); 3] = [ - (TextBackgroundStyle::Box, "Box"), - (TextBackgroundStyle::Pill, "Pill"), - (TextBackgroundStyle::Highlight, "Highlight"), +pub const TEXT_BACKGROUND_STYLES: [(TextBackgroundStyle, fn() -> &'static str); 3] = [ + ( + TextBackgroundStyle::Box, + tr::editor::panel::text_background::box_shape, + ), + ( + TextBackgroundStyle::Pill, + tr::editor::panel::text_background::pill, + ), + ( + TextBackgroundStyle::Highlight, + tr::editor::panel::text_background::highlight, + ), ]; /// `TEXT_LAYOUT_OPTIONS` (`:3583-3590`): the renderer also has `splitLeft` / /// `splitRight`, and the source deliberately exposes only these two. -pub const TEXT_LAYOUTS: [(TextLayout, &str); 2] = [ - (TextLayout::Overlay, "Overlay"), - (TextLayout::Fullscreen, "Fullscreen"), +pub const TEXT_LAYOUTS: [(TextLayout, fn() -> &'static str); 2] = [ + (TextLayout::Overlay, tr::editor::panel::text_layout::overlay), + ( + TextLayout::Fullscreen, + tr::editor::panel::text_layout::fullscreen, + ), ]; /// `TEXT_ALIGN_OPTIONS` (`:3596-3600`). @@ -147,8 +187,8 @@ pub struct TextPresetStyle { pub struct TextPreset { pub id: &'static str, pub group: &'static str, - pub name: &'static str, - pub sample: &'static str, + pub name: fn() -> &'static str, + pub sample: fn() -> &'static str, pub style: TextPresetStyle, pub center: Option>, } @@ -157,9 +197,21 @@ const SANS_STACK: &[&str] = &["Helvetica Neue", "Segoe UI", "Inter", "sans-serif const SERIF_STACK: &[&str] = &["Georgia", "Times New Roman", "serif"]; const MONO_STACK: &[&str] = &["Menlo", "Consolas", "monospace"]; -/// The Style section's chip row, in order. The row draws "All" ahead of these. -pub const TEXT_PRESET_GROUPS: [&str; 5] = - ["Titles", "Lower thirds", "Callouts", "Statements", "Code"]; +/// The Style section's chip row, in order, as `(id, label)`. The row draws +/// "All" ahead of these. +pub const TEXT_PRESET_GROUPS: [(&str, fn() -> &'static str); 5] = [ + ("Titles", tr::editor::panel::text_preset::group::titles), + ( + "Lower thirds", + tr::editor::panel::text_preset::group::lower_thirds, + ), + ("Callouts", tr::editor::panel::text_preset::group::callouts), + ( + "Statements", + tr::editor::panel::text_preset::group::statements, + ), + ("Code", tr::editor::panel::text_preset::group::code), +]; /// The style every preset that does not draw an outline still carries. const PRESET_STROKE: &str = "#000000"; @@ -169,8 +221,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "title", group: "Titles", - name: "Title", - sample: "Introducing Cap", + name: tr::editor::panel::text_preset::name::title, + sample: tr::editor::panel::text_preset::sample::title, center: None, style: TextPresetStyle { font_stack: SANS_STACK, @@ -198,8 +250,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "headline", group: "Titles", - name: "Headline", - sample: "Ship faster", + name: tr::editor::panel::text_preset::name::headline, + sample: tr::editor::panel::text_preset::sample::headline, center: None, style: TextPresetStyle { font_stack: SANS_STACK, @@ -227,8 +279,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "cinematic", group: "Titles", - name: "Cinematic", - sample: "Chapter one", + name: tr::editor::panel::text_preset::name::cinematic, + sample: tr::editor::panel::text_preset::sample::cinematic, center: None, style: TextPresetStyle { font_stack: SERIF_STACK, @@ -256,8 +308,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "gradient", group: "Titles", - name: "Gradient", - sample: "Beautiful text", + name: tr::editor::panel::text_preset::name::gradient, + sample: tr::editor::panel::text_preset::sample::gradient, center: None, style: TextPresetStyle { font_stack: SANS_STACK, @@ -285,8 +337,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "lower-third", group: "Lower thirds", - name: "Lower third", - sample: "Richie McIlroy", + name: tr::editor::panel::text_preset::name::lower_third, + sample: || "Richie McIlroy", center: Some(XY { x: 0.22, y: 0.85 }), style: TextPresetStyle { font_stack: SANS_STACK, @@ -314,8 +366,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "name-tag", group: "Lower thirds", - name: "Name tag", - sample: "Richie \u{b7} Founder", + name: tr::editor::panel::text_preset::name::name_tag, + sample: tr::editor::panel::text_preset::sample::name_tag, center: Some(XY { x: 0.2, y: 0.86 }), style: TextPresetStyle { font_stack: SANS_STACK, @@ -343,8 +395,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "caption", group: "Lower thirds", - name: "Caption", - sample: "Recorded with Cap", + name: tr::editor::panel::text_preset::name::caption, + sample: tr::editor::panel::text_preset::sample::caption, center: Some(XY { x: 0.5, y: 0.88 }), style: TextPresetStyle { font_stack: SANS_STACK, @@ -372,8 +424,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "kicker", group: "Callouts", - name: "Kicker", - sample: "New feature", + name: tr::editor::panel::text_preset::name::kicker, + sample: tr::editor::panel::text_preset::sample::kicker, center: None, style: TextPresetStyle { font_stack: SANS_STACK, @@ -401,8 +453,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "label", group: "Callouts", - name: "Label", - sample: "Pro tip", + name: tr::editor::panel::text_preset::name::label, + sample: tr::editor::panel::text_preset::sample::label, center: None, style: TextPresetStyle { font_stack: SANS_STACK, @@ -430,8 +482,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "highlight", group: "Callouts", - name: "Highlight", - sample: "the important part", + name: tr::editor::panel::text_preset::name::highlight, + sample: tr::editor::panel::text_preset::sample::highlight, center: None, style: TextPresetStyle { font_stack: SANS_STACK, @@ -459,8 +511,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "sticker", group: "Callouts", - name: "Sticker", - sample: "Boom!", + name: tr::editor::panel::text_preset::name::sticker, + sample: tr::editor::panel::text_preset::sample::sticker, center: None, style: TextPresetStyle { font_stack: SANS_STACK, @@ -488,8 +540,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "neon", group: "Callouts", - name: "Neon", - sample: "Glow up", + name: tr::editor::panel::text_preset::name::neon, + sample: tr::editor::panel::text_preset::sample::neon, center: None, style: TextPresetStyle { font_stack: SANS_STACK, @@ -517,8 +569,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "stat", group: "Statements", - name: "Big stat", - sample: "128%", + name: tr::editor::panel::text_preset::name::stat, + sample: || "128%", center: None, style: TextPresetStyle { font_stack: SANS_STACK, @@ -546,8 +598,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "quote", group: "Statements", - name: "Quote", - sample: "\u{201c}Make it feel effortless\u{201d}", + name: tr::editor::panel::text_preset::name::quote, + sample: tr::editor::panel::text_preset::sample::quote, center: None, style: TextPresetStyle { font_stack: SERIF_STACK, @@ -575,8 +627,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "code", group: "Code", - name: "Code", - sample: "$ cap record", + name: tr::editor::panel::text_preset::name::code, + sample: || "$ cap record", center: None, style: TextPresetStyle { font_stack: MONO_STACK, @@ -604,8 +656,8 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ TextPreset { id: "typewriter", group: "Code", - name: "Typewriter", - sample: "typing it out\u{2026}", + name: tr::editor::panel::text_preset::name::typewriter, + sample: tr::editor::panel::text_preset::sample::typewriter, center: None, style: TextPresetStyle { font_stack: MONO_STACK, @@ -634,10 +686,10 @@ pub static TEXT_PRESETS: &[TextPreset] = &[ /// `GENERIC_FONT_OPTIONS` (`utils/fonts.ts:15-19`) -- the three families the /// renderer resolves itself, which head the font picker's list. -pub const GENERIC_FONTS: [(&str, &str); 3] = [ - ("sans-serif", "System Sans"), - ("serif", "System Serif"), - ("monospace", "System Mono"), +pub const GENERIC_FONTS: [(&str, fn() -> &'static str); 3] = [ + ("sans-serif", tr::editor::panel::font::system_sans), + ("serif", tr::editor::panel::font::system_serif), + ("monospace", tr::editor::panel::font::system_mono), ]; /// `fontFamilyLabel` (`utils/fonts.ts:27-32`). @@ -645,7 +697,7 @@ pub fn font_family_label(value: &str) -> String { GENERIC_FONTS .iter() .find(|(generic, _)| *generic == value.trim().to_ascii_lowercase()) - .map_or_else(|| value.to_string(), |(_, label)| (*label).to_string()) + .map_or_else(|| value.to_string(), |(_, label)| label().to_string()) } /// `pickFontFamily` (`text-presets.ts:184-201`): the first family of the stack @@ -809,7 +861,7 @@ pub fn apply_text_preset(segment: &mut TextSegment, preset: &TextPreset, install .animation_in_duration .max(style.animation_out_duration); if matches!(segment.content.trim(), "" | "Text") { - segment.content = preset.sample.to_string(); + segment.content = (preset.sample)().to_string(); } if let Some(center) = preset.center { segment.center = center; @@ -870,7 +922,7 @@ pub fn installed_fonts() -> &'static [String] { pub fn font_picker_options() -> Vec<(String, String)> { GENERIC_FONTS .iter() - .map(|(value, label)| ((*value).to_string(), (*label).to_string())) + .map(|(value, label)| ((*value).to_string(), label().to_string())) .chain( installed_fonts() .iter() @@ -905,73 +957,137 @@ pub fn warm_installed_fonts() { } /// The five scene modes and their descriptions (`:6295-6315`, `:6373-6404`). -pub const SCENE_MODES: [(SceneMode, &str, &str, &str); 5] = [ +pub const SCENE_MODES: [(SceneMode, fn() -> &'static str, &str, fn() -> &'static str); 5] = [ ( SceneMode::Default, - "Default", + tr::editor::panel::scene::mode::default, "icons/monitor-outline.svg", - "Shows both screen and camera", + tr::editor::panel::scene::description::default, ), ( SceneMode::CameraOnly, - "Camera Only", + tr::editor::panel::scene::mode::camera_only, "icons/video.svg", - "Shows only the camera feed", + tr::editor::panel::scene::description::camera_only, ), ( SceneMode::HideCamera, - "Hide Camera", + tr::editor::panel::scene::mode::hide_camera, "icons/eye-off.svg", - "Shows only the screen recording", + tr::editor::panel::scene::description::hide_camera, ), ( SceneMode::SplitScreen, - "Split Screen", + tr::editor::panel::scene::mode::split_screen, "icons/columns-2.svg", - "Screen and camera side by side (auto-stacks in portrait)", + tr::editor::panel::scene::description::split_screen, ), ( SceneMode::Floating, - "Floating", + tr::editor::panel::scene::mode::floating, "icons/panel-right.svg", - "Screen and camera float side by side as rounded cards over the background", + tr::editor::panel::scene::description::floating, ), ]; /// `CAMERA3D_BLUR_SLIDERS` (`:4577-4602`): each mode exposes only the /// parameters it reads, in display order. -pub fn camera3d_blur_sliders(mode: Camera3DBlurMode) -> &'static [(Camera3DBlurKey, &'static str)] { +pub fn camera3d_blur_sliders( + mode: Camera3DBlurMode, +) -> &'static [(Camera3DBlurKey, fn() -> &'static str)] { match mode { Camera3DBlurMode::None => &[], Camera3DBlurMode::Radial => &[ - (Camera3DBlurKey::Strength, "Strength"), - (Camera3DBlurKey::FocusX, "Focus X"), - (Camera3DBlurKey::FocusY, "Focus Y"), - (Camera3DBlurKey::FocusSize, "Focus size"), - (Camera3DBlurKey::Falloff, "Falloff"), + ( + Camera3DBlurKey::Strength, + tr::editor::panel::camera3d::blur::strength, + ), + ( + Camera3DBlurKey::FocusX, + tr::editor::panel::camera3d::blur::focus_x, + ), + ( + Camera3DBlurKey::FocusY, + tr::editor::panel::camera3d::blur::focus_y, + ), + ( + Camera3DBlurKey::FocusSize, + tr::editor::panel::camera3d::blur::focus_size, + ), + ( + Camera3DBlurKey::Falloff, + tr::editor::panel::camera3d::blur::falloff, + ), ], Camera3DBlurMode::Directional => &[ - (Camera3DBlurKey::Strength, "Strength"), - (Camera3DBlurKey::Angle, "Angle"), - (Camera3DBlurKey::DirPosition, "Position"), - (Camera3DBlurKey::Falloff, "Falloff"), + ( + Camera3DBlurKey::Strength, + tr::editor::panel::camera3d::blur::strength, + ), + ( + Camera3DBlurKey::Angle, + tr::editor::panel::camera3d::blur::angle, + ), + ( + Camera3DBlurKey::DirPosition, + tr::editor::panel::camera3d::blur::position, + ), + ( + Camera3DBlurKey::Falloff, + tr::editor::panel::camera3d::blur::falloff, + ), ], Camera3DBlurMode::TiltShift => &[ - (Camera3DBlurKey::Strength, "Strength"), - (Camera3DBlurKey::FocusY, "Scan"), - (Camera3DBlurKey::FocusSize, "Focus size"), - (Camera3DBlurKey::Angle, "Angle"), - (Camera3DBlurKey::Falloff, "Falloff"), + ( + Camera3DBlurKey::Strength, + tr::editor::panel::camera3d::blur::strength, + ), + ( + Camera3DBlurKey::FocusY, + tr::editor::panel::camera3d::blur::scan, + ), + ( + Camera3DBlurKey::FocusSize, + tr::editor::panel::camera3d::blur::focus_size, + ), + ( + Camera3DBlurKey::Angle, + tr::editor::panel::camera3d::blur::angle, + ), + ( + Camera3DBlurKey::Falloff, + tr::editor::panel::camera3d::blur::falloff, + ), ], } } /// `MOTION_EASINGS` (`three-d.ts:957-963`). -pub const MOTION_EASINGS: [(&str, &str, [f64; 2], [f64; 2]); 4] = [ - ("linear", "Linear", [0., 0.], [1., 1.]), - ("smooth", "Smooth", [0.65, 0.], [0.35, 1.]), - ("easeIn", "Ease in", [0.32, 0.], [1., 1.]), - ("easeOut", "Ease out", [0., 0.], [0.68, 1.]), +pub const MOTION_EASINGS: [(&str, fn() -> &'static str, [f64; 2], [f64; 2]); 4] = [ + ( + "linear", + tr::editor::panel::camera3d::easing::linear, + [0., 0.], + [1., 1.], + ), + ( + "smooth", + tr::editor::panel::camera3d::easing::smooth, + [0.65, 0.], + [0.35, 1.], + ), + ( + "easeIn", + tr::editor::panel::camera3d::easing::ease_in, + [0.32, 0.], + [1., 1.], + ), + ( + "easeOut", + tr::editor::panel::camera3d::easing::ease_out, + [0., 0.], + [0.68, 1.], + ), ]; /// `CAMERA3D_RESET_POSE` (`three-d.ts:927-937`): a canonical long lens. @@ -1051,7 +1167,7 @@ fn overhead_blur() -> Camera3DBlur { /// drift it plays out over. pub struct AnglePreset { pub id: &'static str, - pub name: &'static str, + pub name: fn() -> &'static str, pub values: Camera3DProperties, /// The end pose. The source spells it as a partial `drift` merged onto the /// opening pose, which is the same thing written out. @@ -1063,7 +1179,7 @@ pub struct AnglePreset { pub static ANGLE_PRESETS: &[AnglePreset] = &[ AnglePreset { id: "spotlight", - name: "Spotlight", + name: tr::editor::panel::camera3d::look::spotlight, values: Camera3DProperties { zoom: 1.35, pan_x: 0.39, @@ -1081,7 +1197,7 @@ pub static ANGLE_PRESETS: &[AnglePreset] = &[ }, AnglePreset { id: "perspective", - name: "Perspective", + name: tr::editor::panel::camera3d::look::perspective, values: Camera3DProperties { tilt_x: -28., tilt_y: 26., @@ -1105,7 +1221,7 @@ pub static ANGLE_PRESETS: &[AnglePreset] = &[ }, AnglePreset { id: "center", - name: "Center", + name: tr::editor::panel::camera3d::look::center, values: Camera3DProperties { zoom: 2., ..DEFAULT_POSE @@ -1119,7 +1235,7 @@ pub static ANGLE_PRESETS: &[AnglePreset] = &[ }, AnglePreset { id: "low-angle", - name: "Low angle", + name: tr::editor::panel::camera3d::look::low_angle, values: Camera3DProperties { tilt_x: -50., tilt_y: 1., @@ -1138,7 +1254,7 @@ pub static ANGLE_PRESETS: &[AnglePreset] = &[ }, AnglePreset { id: "close-up", - name: "Close up", + name: tr::editor::panel::camera3d::look::close_up, values: Camera3DProperties { tilt_x: 26., tilt_y: -22., @@ -1167,7 +1283,7 @@ pub static ANGLE_PRESETS: &[AnglePreset] = &[ /// template: clicking a card is a complete look, not a pose change. pub struct MotionTemplate { pub id: &'static str, - pub name: &'static str, + pub name: fn() -> &'static str, pub from: Camera3DProperties, pub to: Camera3DProperties, pub blur: fn() -> Camera3DBlur, @@ -1177,7 +1293,7 @@ pub struct MotionTemplate { pub static MOTION_TEMPLATES: &[MotionTemplate] = &[ MotionTemplate { id: "glide-across", - name: "Glide across", + name: tr::editor::panel::camera3d::look::glide_across, from: pose(Camera3DProperties { tilt_x: -46.65, tilt_y: 42.49, @@ -1204,7 +1320,7 @@ pub static MOTION_TEMPLATES: &[MotionTemplate] = &[ }, MotionTemplate { id: "drift-down", - name: "Drift down", + name: tr::editor::panel::camera3d::look::drift_down, from: pose(Camera3DProperties { zoom: 0.8, pan_x: 0.536, @@ -1221,7 +1337,7 @@ pub static MOTION_TEMPLATES: &[MotionTemplate] = &[ }, MotionTemplate { id: "rising-sweep", - name: "Rising sweep", + name: tr::editor::panel::camera3d::look::rising_sweep, from: pose(Camera3DProperties { tilt_x: -57.83, tilt_y: -8.7, @@ -1246,7 +1362,7 @@ pub static MOTION_TEMPLATES: &[MotionTemplate] = &[ }, MotionTemplate { id: "pull-back", - name: "Pull back", + name: tr::editor::panel::camera3d::look::pull_back, from: pose(Camera3DProperties { rotate_x: -14., zoom: 0.715, @@ -1261,7 +1377,7 @@ pub static MOTION_TEMPLATES: &[MotionTemplate] = &[ }, MotionTemplate { id: "top-down", - name: "Top down", + name: tr::editor::panel::camera3d::look::top_down, from: pose(Camera3DProperties { tilt_x: 24.8, tilt_y: 17.04, @@ -1289,7 +1405,7 @@ pub static MOTION_TEMPLATES: &[MotionTemplate] = &[ }, MotionTemplate { id: "tilt-away", - name: "Tilt away", + name: tr::editor::panel::camera3d::look::tilt_away, from: pose(Camera3DProperties { rotate_x: -5., zoom: 0.5, @@ -1304,7 +1420,7 @@ pub static MOTION_TEMPLATES: &[MotionTemplate] = &[ }, MotionTemplate { id: "unfold", - name: "Unfold", + name: tr::editor::panel::camera3d::look::unfold, from: pose(Camera3DProperties { rotate_x: -42.96, zoom: 2.05, @@ -1322,7 +1438,7 @@ pub static MOTION_TEMPLATES: &[MotionTemplate] = &[ }, MotionTemplate { id: "slide-by", - name: "Slide by", + name: tr::editor::panel::camera3d::look::slide_by, from: pose(Camera3DProperties { tilt_x: -30.29, tilt_y: 60., @@ -1380,7 +1496,7 @@ pub struct SceneShot { pub struct Camera3DScene { pub id: &'static str, - pub name: &'static str, + pub name: fn() -> &'static str, pub shots: &'static [SceneShot], } @@ -1405,7 +1521,7 @@ fn showcase_push_blur() -> Camera3DBlur { pub static CAMERA3D_SCENES: &[Camera3DScene] = &[ Camera3DScene { id: "showcase", - name: "Showcase", + name: tr::editor::panel::camera3d::sequence::showcase, // Transcribed verbatim from the hand-built reference project: a tight // close-up truck, a fold-down overhead sweep, then a long push in. shots: &[ @@ -1475,7 +1591,7 @@ pub static CAMERA3D_SCENES: &[Camera3DScene] = &[ }, Camera3DScene { id: "product-tour", - name: "Product tour", + name: tr::editor::panel::camera3d::sequence::product_tour, // Reveal, orbit, settle: `unfold`, `perspective`, `center`. shots: &[ SceneShot { @@ -1533,7 +1649,7 @@ pub static CAMERA3D_SCENES: &[Camera3DScene] = &[ }, Camera3DScene { id: "punch-in", - name: "Punch in", + name: tr::editor::panel::camera3d::sequence::punch_in, // Push in, hold on the detail, then release: `spotlight`, `close-up`, // `pull-back`. shots: &[ @@ -1646,10 +1762,19 @@ const CAMERA3D_DEFAULT_BLUR_FALLOFF: f64 = 0.7; /// The Focus segmented control: the three real modes, in the order the source /// lists them. "None" is the toggle, not an option. -const CAMERA3D_FOCUS_MODES: [(Camera3DBlurMode, &str); 3] = [ - (Camera3DBlurMode::Radial, "Radial"), - (Camera3DBlurMode::Directional, "Directional"), - (Camera3DBlurMode::TiltShift, "Tilt shift"), +const CAMERA3D_FOCUS_MODES: [(Camera3DBlurMode, fn() -> &'static str); 3] = [ + ( + Camera3DBlurMode::Radial, + tr::editor::panel::camera3d::focus_mode::radial, + ), + ( + Camera3DBlurMode::Directional, + tr::editor::panel::camera3d::focus_mode::directional, + ), + ( + Camera3DBlurMode::TiltShift, + tr::editor::panel::camera3d::focus_mode::tilt_shift, + ), ]; // --------------------------------------------------------------------------- @@ -2111,7 +2236,7 @@ pub fn match_camera3d_look(segment: &Camera3DSegment) -> Option { .map(|template| Look { kind: LookKind::Move, id: template.id, - name: template.name, + name: (template.name)(), }) .or_else(|| { ANGLE_PRESETS @@ -2123,7 +2248,7 @@ pub fn match_camera3d_look(segment: &Camera3DSegment) -> Option { .map(|preset| Look { kind: LookKind::Angle, id: preset.id, - name: preset.name, + name: (preset.name)(), }) }) } @@ -2134,9 +2259,9 @@ pub fn camera3d_shot_label(segment: &Camera3DSegment) -> &'static str { return look.name; } if poses_equal(&start_pose(segment), &end_pose(segment)) { - "Still shot" + tr::editor::panel::camera3d::shot::still() } else { - "Custom move" + tr::editor::panel::camera3d::shot::custom() } } @@ -3694,7 +3819,7 @@ impl EditorWindow { TEXT_SEGMENT_WEIGHTS .iter() .map(|(weight, label)| { - ui::MenuItem::new(*label, (*weight - current).abs() < f32::EPSILON) + ui::MenuItem::new(label(), (*weight - current).abs() < f32::EPSILON) }) .collect() } @@ -3706,7 +3831,7 @@ impl EditorWindow { MOTION_EASINGS .iter() .enumerate() - .map(|(index, (_, label, _, _))| ui::MenuItem::new(*label, index == current)) + .map(|(index, (_, label, _, _))| ui::MenuItem::new(label(), index == current)) .collect() } _ => Vec::new(), @@ -4212,13 +4337,13 @@ impl EditorWindow { .flex_col() .gap(px(14.)) .child(self.slider_field( - SharedString::from(format!("Zoom {}", index + 1)), + SharedString::from(tr::editor::panel::zoom::title(index + 1)), SliderKey::Panel(PanelSlider::ZoomAmount, index), "x", cx, )) .child( - ui::Field::section(&theme, "Zoom Mode").child( + ui::Field::section(&theme, tr::editor::panel::zoom::mode()).child( div() .flex() .flex_col() @@ -4307,51 +4432,53 @@ impl EditorWindow { .border_color(Hsla::from(theme.editor.line)) .child({ let mut field = self.slider_field( - "Zoom Amount", + tr::editor::panel::zoom::amount(), SliderKey::Panel(PanelSlider::ZoomAmountAll, 0), "x", cx, ); if mixed_amount { - field = field.value(mixed_badge("Mixed")); + field = field.value(mixed_badge(tr::editor::panel::zoom::mixed())); } field }) .child({ - let mut field = ui::Field::section(&theme, "Zoom Mode").child( - div() - .flex() - .flex_col() - .gap(px(16.)) - .child(self.zoom_mode_tabs( - manual && !mixed_mode, - cx, - |this, want_manual, window, cx| { - this.set_all_zoom_modes(want_manual, window, cx); - }, - )) - .children((!mixed_mode).then(|| self.zoom_mode_helper(manual, cx))) - .children((manual && !mixed_mode).then(|| { - div() - .flex() - .flex_col() - .gap(px(6.)) - .child(self.render_pad(PadKey::ZoomMulti, cx)) - .children(positions_mixed.then(|| { - div() - .text_size(px(12.)) - .text_color(Hsla::from(theme.gray_10)) - .child( - "Segments zoom into different spots. Drag \ - to move them all to the same one.", - ) - .into_any_element() - })) - .into_any_element() - })), - ); + let mut field = ui::Field::section(&theme, tr::editor::panel::zoom::mode()) + .child( + div() + .flex() + .flex_col() + .gap(px(16.)) + .child(self.zoom_mode_tabs( + manual && !mixed_mode, + cx, + |this, want_manual, window, cx| { + this.set_all_zoom_modes(want_manual, window, cx); + }, + )) + .children( + (!mixed_mode).then(|| self.zoom_mode_helper(manual, cx)), + ) + .children((manual && !mixed_mode).then(|| { + div() + .flex() + .flex_col() + .gap(px(6.)) + .child(self.render_pad(PadKey::ZoomMulti, cx)) + .children(positions_mixed.then(|| { + div() + .text_size(px(12.)) + .text_color(Hsla::from(theme.gray_10)) + .child( + tr::editor::panel::zoom::positions_mixed(), + ) + .into_any_element() + })) + .into_any_element() + })), + ); if mixed_mode { - field = field.value(mixed_badge("Mixed")); + field = field.value(mixed_badge(tr::editor::panel::zoom::mixed())); } field }), @@ -4381,7 +4508,9 @@ impl EditorWindow { div() .text_size(px(12.)) .text_color(Hsla::from(theme.gray_11)) - .child(SharedString::from(format!("Zoom {}", index + 1))), + .child(SharedString::from(tr::editor::panel::zoom::title( + index + 1, + ))), ) .child( div() @@ -4464,18 +4593,27 @@ impl EditorWindow { .border_1() .border_color(Hsla::from(theme.gray_12)) .p(px(1.)) - .child(tab("Auto", "zoom-mode-auto", !manual, false, auto_locked)) - .child(tab("Manual", "zoom-mode-manual", manual, true, false)), + .child(tab( + tr::editor::panel::zoom::auto(), + "zoom-mode-auto", + !manual, + false, + auto_locked, + )) + .child(tab( + tr::editor::panel::zoom::manual(), + "zoom-mode-manual", + manual, + true, + false, + )), ) // The explainer under the tabs (`:5648-5653`, `:6024-6029`). .children(auto_locked.then(|| { div() .text_size(px(12.)) .text_color(Hsla::from(theme.gray_11)) - .child( - "Auto mode needs cursor capture. Enable \"Custom cursor capture \ - (Studio)\" in Settings \u{2192} General.", - ) + .child(tr::editor::panel::zoom::auto_locked()) })) .into_any_element() } @@ -4510,7 +4648,7 @@ impl EditorWindow { .size(px(14.)) .text_color(Hsla::from(theme.gray_11)), ) - .child("How does it work?") + .child(tr::editor::panel::zoom::how_it_works()) .child( div().ml_auto().child( svg() @@ -4580,9 +4718,9 @@ impl EditorWindow { .text_size(px(12.)) .text_color(Hsla::from(theme.gray_11)) .child(if manual { - "Manual zoom stays on a fixed spot you pick below." + tr::editor::panel::zoom::manual_help() } else { - "Automatic zoom follows your cursor around the screen." + tr::editor::panel::zoom::auto_help() }), ) .into_any_element(), @@ -4949,9 +5087,9 @@ impl EditorWindow { let sample_size = (style.font_size * 0.2).clamp(11., 22.); let sample_color = style.color.and_then(hex_color).unwrap_or_else(gpui::white); let sample_text: SharedString = if style.uppercase { - SharedString::from(preset.sample.to_uppercase()) + SharedString::from((preset.sample)().to_uppercase()) } else { - SharedString::from(preset.sample) + SharedString::from((preset.sample)()) }; let run = div() .max_w_full() @@ -5027,7 +5165,8 @@ impl EditorWindow { .text_size(px(10.)) .font_weight(FontWeight::MEDIUM) .text_color(with_alpha(gpui::white(), 0.55)) - .child(preset.name), + .truncate() + .child((preset.name)()), ) .on_click(cx.listener(move |this, _, window, cx| { this.apply_text_preset_to(index, id, window, cx); @@ -5037,7 +5176,12 @@ impl EditorWindow { let shown: Vec<&'static TextPreset> = TEXT_PRESETS .iter() - .filter(|preset| group == 0 || TEXT_PRESET_GROUPS.get(group - 1) == Some(&preset.group)) + .filter(|preset| { + group == 0 + || TEXT_PRESET_GROUPS + .get(group - 1) + .is_some_and(|(id, _)| *id == preset.group) + }) .collect(); div() @@ -5051,12 +5195,12 @@ impl EditorWindow { .flex_row() .gap(px(4.)) .flex_wrap() - .child(chip(0, "All", cx)) + .child(chip(0, tr::editor::panel::text_preset::group::all(), cx)) .children( TEXT_PRESET_GROUPS .iter() .enumerate() - .map(|(slot, label)| chip(slot + 1, label, cx)), + .map(|(slot, (_, label))| chip(slot + 1, label(), cx)), ), ) .children(shown.chunks(2).map(|row| { @@ -5214,6 +5358,8 @@ impl EditorWindow { .child(text_animation_depiction(animation, color, accent)) .child( div() + .max_w_full() + .truncate() .text_size(px(10.5)) .font_weight(FontWeight::MEDIUM) .child(label), @@ -5250,7 +5396,7 @@ impl EditorWindow { .iter() .enumerate() .map(|(column, (animation, label))| { - tile(row * 3 + column, *animation, label, cx) + tile(row * 3 + column, *animation, label(), cx) }), ) })) @@ -5283,8 +5429,8 @@ impl EditorWindow { .iter() .find(|(weight, _)| (*weight - segment.font_weight).abs() < f32::EPSILON) .map_or_else( - || SharedString::from(format!("Custom ({})", segment.font_weight)), - |(_, label)| SharedString::from(*label), + || SharedString::from(tr::editor::panel::font_weight::custom(segment.font_weight)), + |(_, label)| SharedString::from(label()), ); let is_out = self.sidebar.text_anim_edge; let edge_animation = if is_out { @@ -5307,109 +5453,109 @@ impl EditorWindow { .gap(px(16.)) // -- A. Text --------------------------------------------------- .child( - ui::Field::section(&theme, SharedString::from(format!("Text {}", index + 1))) - .value( - ui::Toggle::plain( - &theme, - SharedString::from(format!("text-enabled-{index}")), - enabled, - ) - .on_click(cx.listener(move |this, _, window, cx| { - this.edit_text_segment( - "text-enabled", - index, - window, - cx, - move |segment| { - segment.enabled = !enabled; - true - }, - ); - })) - .into_any_element(), + ui::Field::section( + &theme, + SharedString::from(tr::editor::panel::text_segment::title(index + 1)), + ) + .value( + ui::Toggle::plain( + &theme, + SharedString::from(format!("text-enabled-{index}")), + enabled, ) - .child(self.render_text_content_input(index)) - .child( - div() - .flex() - .flex_row() - .items_center() - .gap(px(8.)) - .child(self.text_case_chip( - SharedString::from(format!("text-italic-{index}")), - "I", - true, - italic, - cx.listener(move |this, _, window, cx| { - this.edit_text_segment( - "text-italic", - index, - window, - cx, - move |segment| { - segment.italic = !italic; - true - }, - ); - }), - )) - .child(self.text_case_chip( - SharedString::from(format!("text-uppercase-{index}")), - "AA", - false, - uppercase, - cx.listener(move |this, _, window, cx| { - this.edit_text_segment( - "text-uppercase", - index, - window, - cx, - move |segment| { - segment.uppercase = !uppercase; - true - }, - ); - }), - )) - .child( - div().ml_auto().child( - ui::SegmentedControl::editor( - &theme, - SharedString::from(format!("text-align-{index}")), - TEXT_ALIGNS - .iter() - .map(|(value, icon)| { - ui::SegmentOption::icon(*icon, *value == align) - }) - .collect(), - ) - .on_select(cx.listener( - move |this, choice: &usize, window, cx| { - let Some((value, _)) = TEXT_ALIGNS.get(*choice) else { - return; - }; - let value = *value; - this.edit_text_segment( - "text-align", - index, - window, - cx, - move |segment| { - segment.align = value; - true - }, - ); - }, - )), - ), + .on_click(cx.listener(move |this, _, window, cx| { + this.edit_text_segment("text-enabled", index, window, cx, move |segment| { + segment.enabled = !enabled; + true + }); + })) + .into_any_element(), + ) + .child(self.render_text_content_input(index)) + .child( + div() + .flex() + .flex_row() + .items_center() + .gap(px(8.)) + .child(self.text_case_chip( + SharedString::from(format!("text-italic-{index}")), + "I", + true, + italic, + cx.listener(move |this, _, window, cx| { + this.edit_text_segment( + "text-italic", + index, + window, + cx, + move |segment| { + segment.italic = !italic; + true + }, + ); + }), + )) + .child(self.text_case_chip( + SharedString::from(format!("text-uppercase-{index}")), + "AA", + false, + uppercase, + cx.listener(move |this, _, window, cx| { + this.edit_text_segment( + "text-uppercase", + index, + window, + cx, + move |segment| { + segment.uppercase = !uppercase; + true + }, + ); + }), + )) + .child( + div().ml_auto().child( + ui::SegmentedControl::editor( + &theme, + SharedString::from(format!("text-align-{index}")), + TEXT_ALIGNS + .iter() + .map(|(value, icon)| { + ui::SegmentOption::icon(*icon, *value == align) + }) + .collect(), + ) + .on_select(cx.listener( + move |this, choice: &usize, window, cx| { + let Some((value, _)) = TEXT_ALIGNS.get(*choice) else { + return; + }; + let value = *value; + this.edit_text_segment( + "text-align", + index, + window, + cx, + move |segment| { + segment.align = value; + true + }, + ); + }, + )), ), - ), + ), + ), ) // -- B. Style -------------------------------------------------- - .child(ui::Field::section(&theme, "Style").child(self.render_text_presets(index, cx))) + .child( + ui::Field::section(&theme, tr::editor::panel::text_segment::style()) + .child(self.render_text_presets(index, cx)), + ) // -- C. Font --------------------------------------------------- .child( - ui::Field::section(&theme, "Font").child( + ui::Field::section(&theme, tr::editor::panel::text_segment::font()).child( div() .flex() .flex_col() @@ -5427,19 +5573,19 @@ impl EditorWindow { cx, )) .child(self.slider_field( - "Size", + tr::editor::panel::text_segment::font_size(), SliderKey::Panel(PanelSlider::TextFontSize, index), "int", cx, )) .child(self.slider_field( - "Line height", + tr::editor::panel::text_segment::line_height(), SliderKey::Panel(PanelSlider::TextLineHeight, index), "", cx, )) .child(self.slider_field( - "Letter spacing", + tr::editor::panel::text_segment::letter_spacing(), SliderKey::Panel(PanelSlider::TextLetterSpacing, index), "px", cx, @@ -5448,61 +5594,67 @@ impl EditorWindow { ) // -- D. Look --------------------------------------------------- .child( - ui::Field::section(&theme, "Look").child( + ui::Field::section(&theme, tr::editor::panel::text_segment::look()).child( div() .flex() .flex_col() .gap(px(8.)) .child( - ui::Field::stacked(&theme, "Color").child(self.render_color_input( - ColorTarget::TextColor(index), - &color, - cx, - )), - ) - .child( - ui::Field::inline(&theme, "Gradient").child( - ui::Toggle::plain( - &theme, - SharedString::from(format!("text-gradient-{index}")), - gradient_color.is_some(), - ) - .on_click(cx.listener( - move |this, _, window, cx| { - this.edit_text_segment( - "text-gradient", - index, - window, - cx, - move |segment| { - segment.gradient_color = - match segment.gradient_color.is_some() { - true => None, - false => Some("#7c9cff".to_string()), - }; - true - }, - ); - }, + ui::Field::stacked(&theme, tr::editor::panel::text_segment::color()) + .child(self.render_color_input( + ColorTarget::TextColor(index), + &color, + cx, )), - ), ) - .children(gradient_color.map(|gradient| { - self.render_color_input(ColorTarget::TextGradient(index), &gradient, cx) - })) .child( - ui::Field::stacked(&theme, "Background").child( - ui::SegmentedControl::editor( - &theme, - SharedString::from(format!("text-background-{index}")), - std::iter::once(ui::SegmentOption::new( - "None", - background_slot == 0, - )) - .chain(TEXT_BACKGROUND_STYLES.iter().enumerate().map( + ui::Field::inline(&theme, tr::editor::panel::text_segment::gradient()) + .child( + ui::Toggle::plain( + &theme, + SharedString::from(format!("text-gradient-{index}")), + gradient_color.is_some(), + ) + .on_click(cx.listener( + move |this, _, window, cx| { + this.edit_text_segment( + "text-gradient", + index, + window, + cx, + move |segment| { + segment.gradient_color = + match segment.gradient_color.is_some() { + true => None, + false => Some("#7c9cff".to_string()), + }; + true + }, + ); + }, + )), + ), + ) + .children(gradient_color.map(|gradient| { + self.render_color_input(ColorTarget::TextGradient(index), &gradient, cx) + })) + .child( + ui::Field::stacked( + &theme, + tr::editor::panel::text_segment::background(), + ) + .child( + ui::SegmentedControl::editor( + &theme, + SharedString::from(format!("text-background-{index}")), + std::iter::once(ui::SegmentOption::new( + tr::editor::panel::text_background::none(), + background_slot == 0, + )) + .chain(TEXT_BACKGROUND_STYLES.iter().enumerate().map( |(slot, (_, label))| { ui::SegmentOption::new( - *label, + label(), background_slot == slot + 1, ) }, @@ -5551,7 +5703,7 @@ impl EditorWindow { ) })) .child(self.slider_field( - "Outline", + tr::editor::panel::text_segment::outline(), SliderKey::Panel(PanelSlider::TextStroke, index), "px", cx, @@ -5564,19 +5716,19 @@ impl EditorWindow { ) })) .child(self.slider_field( - "Shadow", + tr::editor::panel::text_segment::shadow(), SliderKey::Panel(PanelSlider::TextShadow, index), "x100%", cx, )) .child(self.slider_field( - "Glow", + tr::editor::panel::text_segment::glow(), SliderKey::Panel(PanelSlider::TextGlow, index), "x100%", cx, )) .child(self.slider_field( - "Opacity", + tr::editor::panel::opacity(), SliderKey::Panel(PanelSlider::TextOpacity, index), "x100%", cx, @@ -5585,14 +5737,14 @@ impl EditorWindow { ) // -- E. Animation ---------------------------------------------- .child( - ui::Field::section(&theme, "Animation") + ui::Field::section(&theme, tr::editor::panel::text_segment::animation()) .value( ui::SegmentedControl::editor( &theme, SharedString::from(format!("text-anim-edge-{index}")), vec![ - ui::SegmentOption::new("In", !is_out), - ui::SegmentOption::new("Out", is_out), + ui::SegmentOption::new(tr::editor::panel::edge_in(), !is_out), + ui::SegmentOption::new(tr::editor::panel::edge_out(), is_out), ], ) .on_select(cx.listener(move |this, choice: &usize, _window, cx| { @@ -5609,7 +5761,7 @@ impl EditorWindow { .child(self.render_text_animation_tiles(index, cx)) .children((edge_animation != TextAnimation::None).then(|| { self.slider_field( - "Duration", + tr::editor::panel::duration(), SliderKey::Panel( if is_out { PanelSlider::TextAnimOutDuration @@ -5627,7 +5779,7 @@ impl EditorWindow { ) // -- F. Layout ------------------------------------------------- .child( - ui::Field::section(&theme, "Layout").child( + ui::Field::section(&theme, tr::editor::panel::text_segment::layout()).child( div() .flex() .flex_col() @@ -5639,7 +5791,7 @@ impl EditorWindow { TEXT_LAYOUTS .iter() .map(|(value, label)| { - ui::SegmentOption::new(*label, *value == layout) + ui::SegmentOption::new(label(), *value == layout) }) .collect(), ) @@ -5675,15 +5827,12 @@ impl EditorWindow { div() .text_size(px(12.)) .text_color(Hsla::from(theme.editor.text_3)) - .child( - "Pauses the video while the text is shown, then resumes \ - where it left off.", - ) + .child(tr::editor::panel::text_segment::fullscreen_help()) .into_any_element() })) .children((layout != TextLayout::Overlay).then(|| { self.slider_field( - "Screen transition", + tr::editor::panel::text_segment::screen_transition(), SliderKey::Panel(PanelSlider::TextLayoutTransition, index), "secs", cx, @@ -5711,8 +5860,11 @@ impl EditorWindow { .flex_col() .gap(px(16.)) .child( - ui::Field::section(&theme, SharedString::from(format!("Caption {}", index + 1))) - .child(self.render_field_input(FieldKey::CaptionText(index), Some(96.))), + ui::Field::section( + &theme, + SharedString::from(tr::editor::panel::caption::title(index + 1)), + ) + .child(self.render_field_input(FieldKey::CaptionText(index), Some(96.))), ) .child(self.timing_field( FieldKey::CaptionStart(index), @@ -5742,7 +5894,7 @@ impl EditorWindow { .child( ui::Field::section( &theme, - SharedString::from(format!("Keyboard {}", index + 1)), + SharedString::from(tr::editor::panel::keyboard::title(index + 1)), ) .child(self.render_field_input(FieldKey::KeyboardText(index), None)), ) @@ -5754,7 +5906,7 @@ impl EditorWindow { cx, )) .child(self.slider_field( - "Fade Duration", + tr::editor::panel::fade_duration(), SliderKey::Panel(PanelSlider::KeyboardFade, index), "", cx, @@ -5799,7 +5951,7 @@ impl EditorWindow { .child(self.render_field_input(key, None)) }; - ui::Field::section(&theme, "Timing") + ui::Field::section(&theme, tr::editor::panel::timing::title()) .child( div() .flex() @@ -5816,16 +5968,20 @@ impl EditorWindow { .flex_row() .gap(px(8.)) .items_start() - .child(box_for("Start", start, start_key)) + .child(box_for( + tr::editor::panel::timing::start(), + start, + start_key, + )) .child( div() .pt(px(40.)) .text_size(px(12.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.gray_10)) - .child("to"), + .child(tr::editor::panel::timing::to()), ) - .child(box_for("End", end, end_key)), + .child(box_for(tr::editor::panel::timing::end(), end, end_key)), ) .child( div() @@ -5839,7 +5995,7 @@ impl EditorWindow { .bg(crate::editor_sidebar::with_alpha(theme.gray_1, 0.7)) .text_size(px(12.)) .text_color(Hsla::from(theme.gray_11)) - .child("Duration") + .child(tr::editor::panel::duration()) .child( div() .font_weight(FontWeight::MEDIUM) @@ -5868,156 +6024,160 @@ impl EditorWindow { .name .clone() .filter(|name| !name.is_empty()) - .unwrap_or_else(|| "Audio".to_string()); + .unwrap_or_else(|| tr::editor::panel::audio::fallback_name().to_string()); div() .flex() .flex_col() .gap(px(16.)) .child( - ui::Field::section(&theme, SharedString::from(format!("Audio {}", index + 1))) - .child( - div() - .flex() - .flex_col() - .gap(px(12.)) - // The "Tap to change track" row opens the audio - // library panel, which is not part of this rev -- - // see the README's deviation. - .child( - div() - .id(SharedString::from(format!("audio-replace-{index}"))) - .flex() - .flex_row() - .gap(px(12.)) - .items_center() - .p(px(8.)) - .w_full() - .rounded(px(12.)) - .border_1() - .border_color(Hsla::from(theme.editor.line)) - .bg(Hsla::from(theme.gray_2)) - .cursor_pointer() - .tab_index(0) - .on_click(cx.listener(move |this, _, _, cx| { - this.open_audio_replace(index, cx); - })) - .child( - div() - .size(px(40.)) - .flex_none() - .rounded(px(8.)) - .bg(TrackKind::Audio.color()), - ) - .child( - div() - .flex() - .flex_col() - .flex_1() - .min_w_0() - .child( - div() - .text_size(px(14.)) - .font_weight(FontWeight::MEDIUM) - .truncate() - .text_color(Hsla::from(theme.gray_12)) - .child(SharedString::from(name)), - ) - .child( - div() - .text_size(px(12.)) - .text_color(Hsla::from(theme.gray_10)) - .child("Tap to change track"), - ), - ) - .child( - div() - .flex() - .flex_row() - .gap(px(4.)) - .items_center() - .px(px(8.)) - .h(px(28.)) - .flex_none() - .rounded(px(8.)) - .border_1() - .border_color(Hsla::from(theme.editor.line)) - .bg(Hsla::from(theme.gray_1)) - .text_size(px(12.)) - .font_weight(FontWeight::MEDIUM) - .text_color(Hsla::from(theme.gray_11)) - .opacity(0.5) - .child( - svg() - .path("icons/refresh-cw.svg") - .size(px(14.)) - .text_color(Hsla::from(theme.gray_11)), - ) - .child("Change"), - ), - ) - .child( - div() - .flex() - .flex_row() - .gap(px(12.)) - .items_center() - .child(div().flex_1().min_w_0().child( - self.render_field_input(FieldKey::AudioName(index), None), - )) - .child( - div() - .flex() - .flex_col() - .items_center() - .gap(px(8.)) - .child( - div() - .text_size(px(12.)) - .text_color(Hsla::from(theme.gray_11)) - .child("Enabled"), - ) - .child( - ui::Toggle::plain( - &theme, - SharedString::from(format!( - "audio-enabled-{index}" - )), - enabled, - ) - .on_click(cx.listener( - move |this, _, window, cx| { - this.edit_audio_segment( - "audio-enabled", - index, - window, - cx, - move |segment| { - segment.enabled = !enabled; - true - }, - ); - }, + ui::Field::section( + &theme, + SharedString::from(tr::editor::panel::audio::title(index + 1)), + ) + .child( + div() + .flex() + .flex_col() + .gap(px(12.)) + // The "Tap to change track" row opens the audio + // library panel, which is not part of this rev -- + // see the README's deviation. + .child( + div() + .id(SharedString::from(format!("audio-replace-{index}"))) + .flex() + .flex_row() + .gap(px(12.)) + .items_center() + .p(px(8.)) + .w_full() + .rounded(px(12.)) + .border_1() + .border_color(Hsla::from(theme.editor.line)) + .bg(Hsla::from(theme.gray_2)) + .cursor_pointer() + .tab_index(0) + .on_click(cx.listener(move |this, _, _, cx| { + this.open_audio_replace(index, cx); + })) + .child( + div() + .size(px(40.)) + .flex_none() + .rounded(px(8.)) + .bg(TrackKind::Audio.color()), + ) + .child( + div() + .flex() + .flex_col() + .flex_1() + .min_w_0() + .child( + div() + .text_size(px(14.)) + .font_weight(FontWeight::MEDIUM) + .truncate() + .text_color(Hsla::from(theme.gray_12)) + .child(SharedString::from(name)), + ) + .child( + div() + .text_size(px(12.)) + .truncate() + .text_color(Hsla::from(theme.gray_10)) + .child(tr::editor::panel::audio::change_hint()), + ), + ) + .child( + div() + .flex() + .flex_row() + .gap(px(4.)) + .items_center() + .px(px(8.)) + .h(px(28.)) + .flex_none() + .rounded(px(8.)) + .border_1() + .border_color(Hsla::from(theme.editor.line)) + .bg(Hsla::from(theme.gray_1)) + .text_size(px(12.)) + .font_weight(FontWeight::MEDIUM) + .text_color(Hsla::from(theme.gray_11)) + .opacity(0.5) + .child( + svg() + .path("icons/refresh-cw.svg") + .size(px(14.)) + .text_color(Hsla::from(theme.gray_11)), + ) + .child(tr::editor::panel::audio::change()), + ), + ) + .child( + div() + .flex() + .flex_row() + .gap(px(12.)) + .items_center() + .child(div().flex_1().min_w_0().child( + self.render_field_input(FieldKey::AudioName(index), None), + )) + .child( + div() + .flex() + .flex_col() + .items_center() + .gap(px(8.)) + .child( + div() + .text_size(px(12.)) + .text_color(Hsla::from(theme.gray_11)) + .child(tr::editor::panel::enabled()), + ) + .child( + ui::Toggle::plain( + &theme, + SharedString::from(format!( + "audio-enabled-{index}" )), + enabled, + ) + .on_click( + cx.listener(move |this, _, window, cx| { + this.edit_audio_segment( + "audio-enabled", + index, + window, + cx, + move |segment| { + segment.enabled = !enabled; + true + }, + ); + }), ), - ), - ), - ), + ), + ), + ), + ), ) .child(self.slider_field( - "Volume", + tr::editor::panel::audio::volume(), SliderKey::Panel(PanelSlider::AudioVolume, index), "db", cx, )) .child(self.slider_field( - "Fade In", + tr::editor::panel::audio::fade_in(), SliderKey::Panel(PanelSlider::AudioFadeIn, index), "s", cx, )) .child(self.slider_field( - "Fade Out", + tr::editor::panel::audio::fade_out(), SliderKey::Panel(PanelSlider::AudioFadeOut, index), "s", cx, @@ -6039,7 +6199,11 @@ impl EditorWindow { let effect = mask_effect(segment); let mut panel = div().flex().flex_col().gap(px(16.)).child( - ui::Field::section(&theme, SharedString::from(format!("Mask {}", index + 1))).child( + ui::Field::section( + &theme, + SharedString::from(tr::editor::panel::mask::title(index + 1)), + ) + .child( div() .flex() .flex_row() @@ -6048,7 +6212,10 @@ impl EditorWindow { .gap(px(16.)) .child(div().flex_1().min_w_0().child(self.radio_row( SharedString::from(format!("mask-kind-{index}")), - vec![("Sensitive", sensitive), ("Highlight", !sensitive)], + vec![ + (tr::editor::panel::mask::sensitive(), sensitive), + (tr::editor::panel::mask::highlight(), !sensitive), + ], cx.listener(move |this, choice: &usize, window, cx| { let want_sensitive = *choice == 0; this.edit_mask_segment( @@ -6086,7 +6253,7 @@ impl EditorWindow { div() .text_size(px(12.)) .text_color(Hsla::from(theme.gray_11)) - .child("Enabled"), + .child(tr::editor::panel::enabled()), ) .child( ui::Toggle::plain( @@ -6115,32 +6282,45 @@ impl EditorWindow { if sensitive { panel = panel - .child(ui::Field::section(&theme, "Effect").child(self.radio_row( - SharedString::from(format!("mask-effect-{index}")), - vec![ - ("Blur", effect == MaskEffect::Blur), - ("Pixelate", effect == MaskEffect::Pixelate), - ], - cx.listener(move |this, choice: &usize, window, cx| { - let next = if *choice == 0 { - MaskEffect::Blur - } else { - MaskEffect::Pixelate - }; - this.edit_mask_segment("mask-effect", index, window, cx, move |segment| { - let amount = mask_effect_amount(segment); - segment.pixelation = encode_mask_effect(next, amount); - segment.opacity = 1.; - segment.keyframes.intensity.clear(); - true - }); - }), - ))) + .child( + ui::Field::section(&theme, tr::editor::panel::mask::effect()).child( + self.radio_row( + SharedString::from(format!("mask-effect-{index}")), + vec![ + (tr::editor::panel::mask::blur(), effect == MaskEffect::Blur), + ( + tr::editor::panel::mask::pixelate(), + effect == MaskEffect::Pixelate, + ), + ], + cx.listener(move |this, choice: &usize, window, cx| { + let next = if *choice == 0 { + MaskEffect::Blur + } else { + MaskEffect::Pixelate + }; + this.edit_mask_segment( + "mask-effect", + index, + window, + cx, + move |segment| { + let amount = mask_effect_amount(segment); + segment.pixelation = encode_mask_effect(next, amount); + segment.opacity = 1.; + segment.keyframes.intensity.clear(); + true + }, + ); + }), + ), + ), + ) .child(self.slider_field( if effect == MaskEffect::Blur { - "Blur" + tr::editor::panel::mask::blur() } else { - "Pixel Size" + tr::editor::panel::mask::pixel_size() }, SliderKey::Panel(PanelSlider::MaskAmount, index), "px", @@ -6149,13 +6329,13 @@ impl EditorWindow { } else { panel = panel .child(self.slider_field( - "Outside Darkness", + tr::editor::panel::mask::outside_darkness(), SliderKey::Panel(PanelSlider::MaskDarkness, index), "", cx, )) .child(self.slider_field( - "Fade Duration", + tr::editor::panel::fade_duration(), SliderKey::Panel(PanelSlider::MaskFade, index), "s", cx, @@ -6265,7 +6445,10 @@ impl EditorWindow { .find(|(candidate, ..)| { std::mem::discriminant(candidate) == std::mem::discriminant(&mode) }) - .map_or("Shows both screen and camera", |(_, _, _, text)| *text); + .map_or( + tr::editor::panel::scene::description::default(), + |(_, _, _, text)| text(), + ); let mut panel = div() .flex() @@ -6281,7 +6464,7 @@ impl EditorWindow { .child( ui::EditorButton::plain(&theme, "scene-done") .left_icon("icons/check.svg") - .label("Done") + .label(tr::editor::panel::done()) .on_click( cx.listener(|this, _, _window, cx| this.set_selection(None, cx)), ), @@ -6290,14 +6473,14 @@ impl EditorWindow { ui::EditorButton::plain(&theme, "scene-delete") .danger(&theme) .left_icon("icons/trash.svg") - .label("Delete") + .label(tr::editor::panel::delete()) .on_click(cx.listener(|this, _, window, cx| { this.delete_selected_segments(window, cx) })), ), ) .child( - ui::Field::section(&theme, "Camera Layout").child( + ui::Field::section(&theme, tr::editor::panel::scene::camera_layout()).child( div() .flex() .flex_col() @@ -6346,7 +6529,7 @@ impl EditorWindow { Hsla::from(theme.gray_11) }, )) - .child(*label) + .child(label()) .when(!disabled, |this| { this.on_click(cx.listener( move |this, _, window, cx| { @@ -6376,25 +6559,29 @@ impl EditorWindow { ), ) .child( - ui::Field::section(&theme, "Transition").child( + ui::Field::section(&theme, tr::editor::panel::scene::transition()).child( div() .flex() .flex_col() .gap(px(12.)) - .child(ui::Subfield::plain(&theme, "In").child( - div().flex_1().min_w_0().ml(px(16.)).child(self.slider_flex( - SliderKey::Panel(PanelSlider::SceneTransitionIn, index), - "s2", - cx, - )), - )) - .child(ui::Subfield::plain(&theme, "Out").child( - div().flex_1().min_w_0().ml(px(16.)).child(self.slider_flex( - SliderKey::Panel(PanelSlider::SceneTransitionOut, index), - "s2", - cx, - )), - )), + .child( + ui::Subfield::plain(&theme, tr::editor::panel::edge_in()).child( + div().flex_1().min_w_0().ml(px(16.)).child(self.slider_flex( + SliderKey::Panel(PanelSlider::SceneTransitionIn, index), + "s2", + cx, + )), + ), + ) + .child( + ui::Subfield::plain(&theme, tr::editor::panel::edge_out()).child( + div().flex_1().min_w_0().ml(px(16.)).child(self.slider_flex( + SliderKey::Panel(PanelSlider::SceneTransitionOut, index), + "s2", + cx, + )), + ), + ), ), ); @@ -6402,24 +6589,24 @@ impl EditorWindow { panel = panel .child(dashed_divider(Hsla::from(theme.editor.line))) .child(self.slider_field( - "Screen Zoom", + tr::editor::panel::scene::screen_zoom(), SliderKey::Panel(PanelSlider::SceneScreenZoom, index), "", cx, )) .child( - ui::Field::stacked(&theme, "Screen Position") + ui::Field::stacked(&theme, tr::editor::panel::scene::screen_position()) .child(self.render_pad(PadKey::SceneScreen(index), cx)), ) .child(dashed_divider(Hsla::from(theme.editor.line))) .child(self.slider_field( - "Camera Zoom", + tr::editor::panel::scene::camera_zoom(), SliderKey::Panel(PanelSlider::SceneCameraZoom, index), "", cx, )) .child( - ui::Field::stacked(&theme, "Camera Position") + ui::Field::stacked(&theme, tr::editor::panel::scene::camera_position()) .child(self.render_pad(PadKey::SceneCamera(index), cx)), ); } @@ -6535,7 +6722,7 @@ impl EditorWindow { ) -> AnyElement { let theme = self.theme; let id = look.id; - let name = look.name; + let name = (look.name)(); let pose = look.from; div() .id(SharedString::from(format!("c3d-look-{index}-{id}"))) @@ -6672,13 +6859,13 @@ impl EditorWindow { .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.editor.text_1)) .truncate() - .child(scene.name), + .child((scene.name)()), ) .child( div() .text_size(px(10.)) .text_color(Hsla::from(theme.editor.text_2)) - .child(format!("{shots} shots")), + .child(tr::editor::panel::camera3d::shots(shots)), ) .child(div().flex().flex_row().gap(px(3.)).pt(px(3.)).children( (0..shots).map(|_| { @@ -6698,23 +6885,32 @@ impl EditorWindow { .flex_col() .gap(px(10.)) .child( - self.camera3d_group_label("Look").child( - ui::SegmentedControl::editor( - &theme, - "camera3d-look-tab", - vec![ - ui::SegmentOption::new("Moves", !angles), - ui::SegmentOption::new("Angles", angles), - ], - ) - .text_size(px(11.)) - .item_height(px(21.)) - .item_padding(px(9.), px(0.)) - .on_select(cx.listener(|this, choice: &usize, _, cx| { - this.sidebar.camera3d_angles = *choice == 1; - cx.notify(); - })), - ), + self.camera3d_group_label(tr::editor::panel::camera3d::look_title()) + .child( + ui::SegmentedControl::editor( + &theme, + "camera3d-look-tab", + vec![ + ui::SegmentOption::new( + tr::editor::panel::camera3d::moves(), + !angles, + ), + ui::SegmentOption::new( + tr::editor::panel::camera3d::angles(), + angles, + ), + ], + ) + .text_size(px(11.)) + .item_height(px(21.)) + .item_padding(px(9.), px(0.)) + .on_select(cx.listener( + |this, choice: &usize, _, cx| { + this.sidebar.camera3d_angles = *choice == 1; + cx.notify(); + }, + )), + ), ) .child(grid) .child(sequences) @@ -6804,7 +7000,7 @@ impl EditorWindow { .text_center() .text_size(px(10.)) .text_color(Hsla::from(theme.editor.text_2)) - .child("Drag to orbit"), + .child(tr::editor::panel::camera3d::drag_to_orbit()), ) .on_mouse_down( gpui::MouseButton::Left, @@ -6870,7 +7066,13 @@ impl EditorWindow { .gap(px(8.)) .text_size(px(12.)) .text_color(Hsla::from(theme.editor.text_2)) - .child(div().w(px(CAMERA3D_ROW_LABEL)).flex_none().child(label)) + .child( + div() + .w(px(CAMERA3D_ROW_LABEL)) + .flex_none() + .truncate() + .child(label), + ) .child( div() .flex_1() @@ -6930,13 +7132,11 @@ impl EditorWindow { // the hold it is. let dim = still && end; let caption = if dim { - "End \u{b7} same as start".to_string() + tr::editor::panel::camera3d::end_same_as_start().to_string() + } else if end { + tr::editor::panel::camera3d::pose_end(format_time(time)) } else { - format!( - "{} \u{b7} {}", - if end { "End" } else { "Start" }, - format_time(time) - ) + tr::editor::panel::camera3d::pose_start(format_time(time)) }; div() @@ -7053,9 +7253,9 @@ impl EditorWindow { "icons/arrow-left-right.svg", None, if still { - "Nothing to swap on a still shot" + tr::editor::panel::camera3d::nothing_to_swap() } else { - "Swap start and end" + tr::editor::panel::camera3d::swap() }, false, ) @@ -7083,11 +7283,11 @@ impl EditorWindow { self.camera3d_action( "camera3d-still", "icons/pause.svg", - Some("Still shot"), + Some(tr::editor::panel::camera3d::still_shot()), if still { - "Already a still shot" + tr::editor::panel::camera3d::already_still() } else { - "Hold the opening pose for the whole shot" + tr::editor::panel::camera3d::hold_opening() }, still, ) @@ -7122,14 +7322,15 @@ impl EditorWindow { .text_size(px(12.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.editor.text_2)) - .child("Auto scene"), + .child(tr::editor::panel::camera3d::auto_scene()), ) .child(div().flex_1()) .child(self.render_camera3d_count_pills("panel", count, maximum, cx)) .id("camera3d-auto-row") .tooltip_show_delay(ui::TOOLTIP_SHOW_DELAY) .tooltip(move |_window, cx| { - ui::Tooltip::new(&theme, "Rebuilds every 3D shot on the track").view(cx) + ui::Tooltip::new(&theme, tr::editor::panel::camera3d::auto_scene_tooltip()) + .view(cx) }) .into_any_element(), ) @@ -7207,7 +7408,7 @@ impl EditorWindow { let blur = segment.blur; let blur_on = blur.mode != Camera3DBlurMode::None; let easing_index = motion_easing(segment); - let easing_label = MOTION_EASINGS[easing_index].1; + let easing_label = (MOTION_EASINGS[easing_index].1)(); let duration = segment.end - segment.start; let look = camera3d_shot_label(segment); let advanced = self.sidebar.section(PanelSection::Camera3DAdvanced); @@ -7222,7 +7423,7 @@ impl EditorWindow { .child( ui::EditorButton::plain(&theme, "camera3d-done") .left_icon("icons/check.svg") - .label("Done") + .label(tr::editor::panel::done()) .text_size(px(12.)) .on_click(cx.listener(|this, _, _window, cx| this.set_selection(None, cx))), ) @@ -7233,15 +7434,18 @@ impl EditorWindow { .truncate() .text_size(px(12.)) .text_color(Hsla::from(theme.editor.text_2)) - .child(format!("3D shot \u{b7} {look} \u{b7} {duration:.1}s")), + .child(tr::editor::panel::camera3d::header( + look, + format!("{duration:.1}"), + )), ) .child( ui::EditorButton::plain(&theme, "camera3d-play-shot") .left_icon("icons/play.svg") .icon_size(px(12.)) - .label("Play shot") + .label(tr::editor::panel::camera3d::play_shot()) .text_size(px(12.)) - .tooltip(&theme, "Play this shot") + .tooltip(&theme, tr::editor::panel::camera3d::play_shot_tooltip()) .on_click(cx.listener(move |this, _, _window, cx| { this.play_camera3d_shot(index, cx); })), @@ -7251,9 +7455,9 @@ impl EditorWindow { .danger(&theme) .left_icon("icons/trash.svg") .icon_size(px(13.)) - .label("Delete") + .label(tr::editor::panel::delete()) .text_size(px(12.)) - .tooltip(&theme, "Delete this shot") + .tooltip(&theme, tr::editor::panel::camera3d::delete_tooltip()) .on_click(cx.listener(|this, _, window, cx| { this.delete_selection(window, cx); })), @@ -7277,7 +7481,7 @@ impl EditorWindow { .text_size(px(12.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.editor.text_2)) - .child("Camera"), + .child(tr::editor::panel::camera3d::camera()), ) .child(div().flex_1()) .child( @@ -7285,7 +7489,7 @@ impl EditorWindow { "camera3d-flip-h", "icons/flip-horizontal-2.svg", None, - "Flip horizontally", + tr::editor::panel::flip_horizontally(), false, ) .on_click(cx.listener( @@ -7299,7 +7503,7 @@ impl EditorWindow { "camera3d-flip-v", "icons/flip-vertical-2.svg", None, - "Flip vertically", + tr::editor::panel::flip_vertically(), false, ) .on_click(cx.listener( @@ -7323,7 +7527,7 @@ impl EditorWindow { .flex() .flex_col() .child(self.camera3d_slider_row( - "Distance", + tr::editor::panel::camera3d::distance(), SliderKey::Panel( PanelSlider::Camera3DPose(Camera3DProperty::Zoom), index, @@ -7332,7 +7536,7 @@ impl EditorWindow { cx, )) .child(self.camera3d_slider_row( - "Roll", + tr::editor::panel::camera3d::roll(), SliderKey::Panel( PanelSlider::Camera3DPose(Camera3DProperty::Roll), index, @@ -7341,7 +7545,7 @@ impl EditorWindow { cx, )) .child(self.camera3d_slider_row( - "Shift X", + tr::editor::panel::camera3d::shift_x(), SliderKey::Panel( PanelSlider::Camera3DPose(Camera3DProperty::PanX), index, @@ -7350,7 +7554,7 @@ impl EditorWindow { cx, )) .child(self.camera3d_slider_row( - "Shift Y", + tr::editor::panel::camera3d::shift_y(), SliderKey::Panel( PanelSlider::Camera3DPose(Camera3DProperty::PanY), index, @@ -7363,21 +7567,22 @@ impl EditorWindow { // -- Depth blur ------------------------------------------------------- let mut depth = div().flex().flex_col().gap(px(6.)).child( - self.camera3d_group_label("Depth blur").child( - ui::Toggle::plain( - &theme, - SharedString::from(format!("camera3d-blur-{index}")), - blur_on, - ) - .on_click(cx.listener(move |this, _, window, cx| { - this.toggle_camera3d_blur(index, window, cx); - })), - ), + self.camera3d_group_label(tr::editor::panel::camera3d::depth_blur()) + .child( + ui::Toggle::plain( + &theme, + SharedString::from(format!("camera3d-blur-{index}")), + blur_on, + ) + .on_click(cx.listener(move |this, _, window, cx| { + this.toggle_camera3d_blur(index, window, cx); + })), + ), ); if blur_on { depth = depth .child(self.camera3d_slider_row( - "Amount", + tr::editor::panel::camera3d::amount(), SliderKey::Panel(PanelSlider::Camera3DBlur(Camera3DBlurKey::Strength), index), "int", cx, @@ -7391,7 +7596,13 @@ impl EditorWindow { .gap(px(8.)) .text_size(px(12.)) .text_color(Hsla::from(theme.editor.text_2)) - .child(div().w(px(CAMERA3D_ROW_LABEL)).flex_none().child("Focus")) + .child( + div() + .w(px(CAMERA3D_ROW_LABEL)) + .flex_none() + .truncate() + .child(tr::editor::panel::camera3d::focus()), + ) .child( div().flex_1().min_w_0().child( ui::SegmentedControl::editor( @@ -7400,7 +7611,7 @@ impl EditorWindow { CAMERA3D_FOCUS_MODES .iter() .map(|(mode, label)| { - ui::SegmentOption::new(*label, blur.mode == *mode) + ui::SegmentOption::new(label(), blur.mode == *mode) }) .collect(), ) @@ -7423,7 +7634,7 @@ impl EditorWindow { .child(crate::editor_sidebar::disclosure_row( &theme, "camera3d-blur-tune", - "Fine-tune", + tr::editor::panel::camera3d::fine_tune(), tune.is_open(), cx.listener(|this, _, window, cx| { this.sidebar @@ -7443,7 +7654,7 @@ impl EditorWindow { .filter(|(key, _)| *key != Camera3DBlurKey::Strength) .map(|(key, label)| { self.camera3d_slider_row( - label, + label(), SliderKey::Panel(PanelSlider::Camera3DBlur(*key), index), if *key == Camera3DBlurKey::Angle { "deg" @@ -7463,7 +7674,13 @@ impl EditorWindow { .gap(px(8.)) .text_size(px(12.)) .text_color(Hsla::from(theme.editor.text_2)) - .child(div().w(px(CAMERA3D_ROW_LABEL)).flex_none().child("Bokeh")) + .child( + div() + .w(px(CAMERA3D_ROW_LABEL)) + .flex_none() + .truncate() + .child(tr::editor::panel::camera3d::bokeh()), + ) .child(div().flex_1()) .child( ui::Toggle::plain( @@ -7502,15 +7719,21 @@ impl EditorWindow { .text_size(px(12.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.editor.text_1)) - .child("Timing & advanced") + .child( + div() + .min_w_0() + .truncate() + .child(tr::editor::panel::camera3d::timing_advanced()), + ) .child(div().flex_1()) .child( div() + .flex_none() .font_weight(FontWeight::NORMAL) .text_color(Hsla::from(theme.editor.text_2)) - .child(format!( - "{easing_label} \u{b7} Lens {}", - start.fov.round() as i32 + .child(tr::editor::panel::camera3d::timing_summary( + easing_label, + start.fov.round() as i32, )), ) .child( @@ -7540,42 +7763,43 @@ impl EditorWindow { .flex_col() .gap(px(2.)) .child( - ui::Subfield::plain(&theme, "Motion style").child( - div() - .w(px(150.)) - // A still shot has no span to shape and nowhere - // to store a curve (`:5357-5360`). - .when(still, |this| this.opacity(0.5)) - .child(if still { - ui::Select::plain(&theme, "camera3d-easing", easing_label) - .stretch_label() - .disabled(true) - .into_any_element() - } else { - self.easing_select(index, easing_index, cx) - }), - ), + ui::Subfield::plain(&theme, tr::editor::panel::camera3d::motion_style()) + .child( + div() + .w(px(150.)) + // A still shot has no span to shape and nowhere + // to store a curve (`:5357-5360`). + .when(still, |this| this.opacity(0.5)) + .child(if still { + ui::Select::plain(&theme, "camera3d-easing", easing_label) + .stretch_label() + .disabled(true) + .into_any_element() + } else { + self.easing_select(index, easing_index, cx) + }), + ), ) .child(self.camera3d_slider_row( - "Ease in", + tr::editor::panel::camera3d::ease_in(), SliderKey::Panel(PanelSlider::Camera3DTransitionIn, index), "secs", cx, )) .child(self.camera3d_slider_row( - "Ease out", + tr::editor::panel::camera3d::ease_out(), SliderKey::Panel(PanelSlider::Camera3DTransitionOut, index), "secs", cx, )) .child(self.camera3d_slider_row( - "Lens", + tr::editor::panel::camera3d::lens(), SliderKey::Panel(PanelSlider::Camera3DPose(Camera3DProperty::Fov), index), "deg", cx, )) .child(self.camera3d_slider_row( - "Rotate X", + tr::editor::panel::camera3d::rotate_x(), SliderKey::Panel( PanelSlider::Camera3DPose(Camera3DProperty::RotateX), index, @@ -7584,7 +7808,7 @@ impl EditorWindow { cx, )) .child(self.camera3d_slider_row( - "Rotate Y", + tr::editor::panel::camera3d::rotate_y(), SliderKey::Panel( PanelSlider::Camera3DPose(Camera3DProperty::RotateY), index, @@ -7596,7 +7820,7 @@ impl EditorWindow { div().pt(px(4.)).pb(px(4.)).child( ui::EditorButton::plain(&theme, "camera3d-reset") .left_icon("icons/rotate-ccw.svg") - .label("Reset camera") + .label(tr::editor::panel::camera3d::reset_camera()) .on_click(cx.listener(move |this, _, window, cx| { this.reset_camera3d_pose(index, window, cx); })), @@ -7836,7 +8060,7 @@ impl EditorWindow { self.menu_select( SidebarMenu::Camera3DEasing(index), "camera3d-easing", - MOTION_EASINGS[current].1, + (MOTION_EASINGS[current].1)(), cx, ) } @@ -8492,7 +8716,7 @@ mod tests { ); // Every preset is reachable from a chip. assert!( - TEXT_PRESET_GROUPS.contains(&preset.group), + TEXT_PRESET_GROUPS.iter().any(|(id, _)| *id == preset.group), "{} is in {}", preset.id, preset.group @@ -8502,7 +8726,7 @@ mod tests { #[test] fn the_animation_catalogue_is_the_renderers_own_order() { - let labels: Vec<_> = TEXT_ANIMATIONS.iter().map(|(_, label)| *label).collect(); + let labels: Vec<_> = TEXT_ANIMATIONS.iter().map(|(_, label)| label()).collect(); assert_eq!( labels, [ @@ -8907,7 +9131,7 @@ mod tests { Some(Look { kind: LookKind::Move, id: template.id, - name: template.name + name: (template.name)() }), "{} did not match itself", template.id @@ -8921,7 +9145,7 @@ mod tests { Some(Look { kind: LookKind::Angle, id: preset.id, - name: preset.name + name: (preset.name)() }), "{} did not match itself", preset.id @@ -9056,9 +9280,9 @@ pub enum ImageProperty { impl ImageProperty { fn label(self) -> &'static str { match self { - Self::Opacity => "Opacity", - Self::Rotation => "Rotation", - Self::Rounding => "Rounding", + Self::Opacity => tr::editor::panel::opacity(), + Self::Rotation => tr::editor::panel::image::rotation(), + Self::Rounding => tr::editor::panel::image::rounding(), } } fn limits(self) -> (f32, f32, f32) { @@ -9101,7 +9325,7 @@ impl EditorWindow { .flex_col() .gap(px(16.)) .child(self.labelled_small( - "Name", + tr::editor::panel::name(), self.render_field_input(FieldKey::ImageName(index), None), )); panel = panel @@ -9124,13 +9348,13 @@ impl EditorWindow { .text_size(px(12.)) .font_weight(FontWeight::MEDIUM) .child(svg().path("icons/move.svg").size(px(16.))) - .child("Arrange on canvas"), + .child(tr::editor::panel::image::arrange()), ) .child( div() .text_size(px(12.)) .text_color(Hsla::from(self.theme.gray_10)) - .child("Drag the image to move it. Pull a corner to resize, or use the rotation control below to turn it."), + .child(tr::editor::panel::image::arrange_help()), ) .child( ui::Button::plain( @@ -9140,19 +9364,21 @@ impl EditorWindow { ui::ButtonSize::Sm, ) .icon("icons/move.svg") - .label("Center on canvas") - .on_click(cx.listener(move |this, _, window, cx| { - this.edit_image_segment( - "image-center", - index, - window, - cx, - |segment| { - segment.center = XY::new(0.5, 0.5); - true - }, - ); - })), + .label(tr::editor::panel::image::center()) + .on_click(cx.listener( + move |this, _, window, cx| { + this.edit_image_segment( + "image-center", + index, + window, + cx, + |segment| { + segment.center = XY::new(0.5, 0.5); + true + }, + ); + }, + )), ), ) .child( @@ -9162,7 +9388,7 @@ impl EditorWindow { ui::ButtonVariant::Gray, ui::ButtonSize::Md, ) - .label("Replace image") + .label(tr::editor::panel::image::replace()) .disabled(self.sidebar.picking_image) .on_click(cx.listener(move |this, _, window, cx| { this.replace_timeline_image(index, window, cx) @@ -9174,16 +9400,24 @@ impl EditorWindow { .as_ref() .is_some_and(|(path, present)| path == &segment.path && !present) { - panel = panel.child(div().text_size(px(12.)).child("Image file is missing. Replace it to restore this segment while keeping its timing and transforms.")); + panel = panel.child( + div() + .text_size(px(12.)) + .child(tr::editor::panel::image::missing()), + ); } if let Some(error) = &self.sidebar.image_import_error { panel = panel.child(div().text_size(px(12.)).child(error.clone())); } for (key, label, value) in [ - (0, "Enabled", segment.enabled), - (1, "Lock aspect ratio", segment.lock_aspect), - (2, "Flip horizontally", segment.flip_x), - (3, "Flip vertically", segment.flip_y), + (0, tr::editor::panel::enabled(), segment.enabled), + ( + 1, + tr::editor::panel::image::lock_aspect(), + segment.lock_aspect, + ), + (2, tr::editor::panel::flip_horizontally(), segment.flip_x), + (3, tr::editor::panel::flip_vertically(), segment.flip_y), ] { panel = panel.child( ui::Subfield::plain(&self.theme, label).child( @@ -9239,10 +9473,40 @@ impl EditorWindow { return div().into_any_element(); }; let enabled = segment.enabled; - let mut panel = div().flex().flex_col().gap(px(16.)) - .child(div().text_size(px(12.)).child("Overrides apply only during this segment. Enable a group to copy its global settings.")) - .child(self.labelled_small("Name", self.render_field_input(FieldKey::StyleName(index), None))) - .child(ui::Subfield::plain(&self.theme,"Enabled").child(ui::Toggle::plain(&self.theme,SharedString::from(format!("style-enabled-{index}")),enabled).on_click(cx.listener(move |this,_,window,cx| this.edit_style_segment("style-enabled",index,window,cx,move |segment| { segment.enabled = !enabled; true }))))); + let mut panel = div() + .flex() + .flex_col() + .gap(px(16.)) + .child( + div() + .text_size(px(12.)) + .child(tr::editor::panel::style::help()), + ) + .child(self.labelled_small( + tr::editor::panel::name(), + self.render_field_input(FieldKey::StyleName(index), None), + )) + .child( + ui::Subfield::plain(&self.theme, tr::editor::panel::enabled()).child( + ui::Toggle::plain( + &self.theme, + SharedString::from(format!("style-enabled-{index}")), + enabled, + ) + .on_click(cx.listener(move |this, _, window, cx| { + this.edit_style_segment( + "style-enabled", + index, + window, + cx, + move |segment| { + segment.enabled = !enabled; + true + }, + ) + })), + ), + ); for (group, active) in [ ( StyleGroup::Background, @@ -9313,7 +9577,17 @@ impl EditorWindow { .items_center() .gap(px(6.)) .child(svg().path(group.icon()).size(px(14.))) - .child(format!("Edit {}", group.label())), + .child(match group { + StyleGroup::Background => { + tr::editor::panel::style::edit_background() + } + StyleGroup::Camera => { + tr::editor::panel::style::edit_camera() + } + StyleGroup::Cursor => { + tr::editor::panel::style::edit_cursor() + } + }), ) .on_click(cx.listener(move |this, _, window, cx| { this.open_style_group(index, group, window, cx) @@ -9325,9 +9599,14 @@ impl EditorWindow { let mut crop = div().flex().flex_col().gap(px(8.)).child( div() .text_size(px(12.)) - .child("Screen crop (source pixels)"), + .child(tr::editor::panel::style::crop_title()), ); - for (axis, label) in [(0, "Left"), (1, "Top"), (2, "Width"), (3, "Height")] { + for (axis, label) in [ + (0, tr::editor::panel::style::crop_left()), + (1, tr::editor::panel::style::crop_top()), + (2, tr::editor::panel::style::crop_width()), + (3, tr::editor::panel::style::crop_height()), + ] { crop = crop.child(self.labelled_small( label, self.render_number_field(FieldKey::StyleCrop(index, axis), "px", 80.), @@ -9337,7 +9616,7 @@ impl EditorWindow { div() .id(SharedString::from(format!("style-crop-reset-{index}"))) .cursor_pointer() - .child("Reset crop") + .child(tr::editor::panel::style::reset_crop()) .on_click(cx.listener(move |this, _, window, cx| { this.edit_style_segment( "style-crop-reset", @@ -9359,7 +9638,11 @@ impl EditorWindow { } let padding = segment.overrides.camera_only_padding.is_some(); panel = panel.child( - ui::Subfield::plain(&self.theme, "Camera Only background").child( + ui::Subfield::plain( + &self.theme, + tr::editor::panel::style::camera_only_background(), + ) + .child( ui::Toggle::plain( &self.theme, SharedString::from(format!("style-camera-only-{index}")), @@ -9383,7 +9666,7 @@ impl EditorWindow { panel = panel .child( self.labelled_small( - "Camera Only padding", + tr::editor::panel::style::camera_only_padding(), self.slider( SliderKey::Panel(PanelSlider::StyleCameraOnlyPadding, index), "%", @@ -9392,9 +9675,11 @@ impl EditorWindow { .into_any_element(), ), ) - .child(div().text_size(px(11.)).child( - "Use a Camera Only scene. Padding reveals the background around the camera.", - )); + .child( + div() + .text_size(px(11.)) + .child(tr::editor::panel::style::camera_only_help()), + ); } panel.into_any_element() } diff --git a/apps/desktop-gpui/src/editor_sidebar.rs b/apps/desktop-gpui/src/editor_sidebar.rs index ee004dbae73..443ce9226d2 100644 --- a/apps/desktop-gpui/src/editor_sidebar.rs +++ b/apps/desktop-gpui/src/editor_sidebar.rs @@ -58,6 +58,7 @@ use crate::{ editor_tabs::{AudioSlider, CameraSlider, CaptionSlider, CursorSlider, KeyboardSlider}, editor_timeline::TrackKind, editor_window::EditorWindow, + i18n::tr, library, ui::{self, CollapsibleState, SliderTrack}, }; @@ -138,6 +139,17 @@ pub const BACKGROUND_THEMES: [(&str, &str); 6] = [ ("orange", "Orange"), ]; +pub fn background_theme_label(id: &str) -> &'static str { + match id { + "dark" => tr::editor::sidebar::wallpaper_theme::dark(), + "blue" => tr::editor::sidebar::wallpaper_theme::blue(), + "cities" => tr::editor::sidebar::wallpaper_theme::cities(), + "purple" => tr::editor::sidebar::wallpaper_theme::purple(), + "orange" => tr::editor::sidebar::wallpaper_theme::orange(), + _ => "macOS", + } +} + /// `BACKGROUND_COLORS` (`:259-277`). The last one is transparent and renders as /// a checkerboard rather than a swatch. pub const BACKGROUND_COLORS: [&str; 17] = [ @@ -266,8 +278,19 @@ impl SidebarTab { } } - /// The tab's own name. Used by the timeline's own logging and by the tests - /// that pin the rail's order; the rail itself is icon-only. + pub fn title(self) -> &'static str { + match self { + Self::Background => tr::editor::sidebar::tab::background(), + Self::Camera => tr::editor::sidebar::tab::camera(), + Self::Audio => tr::editor::sidebar::tab::audio(), + Self::Cursor => tr::editor::sidebar::tab::cursor(), + Self::Keyboard => tr::editor::sidebar::tab::keyboard(), + Self::Captions => tr::editor::sidebar::tab::captions(), + } + } + + /// The tab's own English name, which `CAP_GPUI_AUTO_SIDEBAR` matches + /// and the tests that pin the rail's order read. #[allow(dead_code)] pub fn label(self) -> &'static str { match self { @@ -310,13 +333,13 @@ impl SourceTab { pub fn label(self) -> &'static str { match self { - Self::Desktop => "Desktop", - Self::Wallpaper => "Wallpaper", - Self::Image => "Image", - Self::Color => "Color", - Self::Gradient => "Gradient", - Self::AnimatedGradient => "Animated", - Self::None => "None", + Self::Desktop => tr::editor::sidebar::source::desktop(), + Self::Wallpaper => tr::editor::sidebar::source::wallpaper(), + Self::Image => tr::editor::sidebar::source::image(), + Self::Color => tr::editor::sidebar::source::color(), + Self::Gradient => tr::editor::sidebar::source::gradient(), + Self::AnimatedGradient => tr::editor::sidebar::source::animated(), + Self::None => tr::editor::sidebar::source::none(), } } } @@ -2437,7 +2460,10 @@ impl EditorWindow { self.sidebar.picker_task = Some(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::editor::sidebar::source::image(), + &BACKGROUND_IMAGE_EXTENSIONS[..], + )], None, cx, ) @@ -2516,7 +2542,11 @@ impl EditorWindow { } /// `importDesktopBackground` (`:2033-2052`). - pub(crate) fn import_desktop_background(&mut self, window: &mut Window, cx: &mut Context) { + pub(crate) fn import_desktop_background( + &mut self, + window: &mut Window, + cx: &mut Context, + ) { if self.sidebar.importing_desktop { return; } @@ -2550,7 +2580,7 @@ impl EditorWindow { tracing::error!("couldn't import your desktop wallpaper: {error}"); cx.spawn(async move |_, _| { crate::platform::alert_dialog( - "Couldn't import your desktop wallpaper", + tr::editor::sidebar::desktop_import_failed(), &error, ); }) @@ -2601,7 +2631,7 @@ impl EditorWindow { // the indicator is hidden (`:586-592, 667-677`). ui::TabRailItem::new( tab.icon(), - tab.label(), + tab.title(), selection.is_none() && tab == selected_tab, disabled, ) @@ -2642,11 +2672,11 @@ impl EditorWindow { .text_color(Hsla::from(theme.editor.text_1)) .child(error.clone()) })) - .children( - self.sidebar - .picking_image - .then(|| div().p(px(12.)).child("Importing image…")), - ) + .children(self.sidebar.picking_image.then(|| { + div() + .p(px(12.)) + .child(tr::editor::sidebar::importing_image()) + })) .child(if let Some((index, group)) = self.sidebar.style_target { self.render_style_group(index, group, cx) } else if self.audio_picker.is_some() { @@ -2729,18 +2759,18 @@ impl EditorWindow { .child(self.render_source_field(cx)) .child(dashed_divider(Hsla::from(theme.editor.line))) .child( - ui::Field::section(&theme, "Layout").child( + ui::Field::section(&theme, tr::editor::sidebar::layout::title()).child( div() .flex() .flex_col() .child(self.slider_field( - "Background Blur", + tr::editor::sidebar::layout::background_blur(), SliderKey::Bg(BgSlider::Blur), "%", cx, )) .child(self.slider_field( - "Padding", + tr::editor::sidebar::layout::padding(), SliderKey::Bg(BgSlider::Padding), "%", cx, @@ -2756,14 +2786,16 @@ impl EditorWindow { .h(px(34.)) .child( div() + .min_w_0() + .truncate() .text_size(px(12.)) .text_color(Hsla::from(theme.editor.text_2)) - .child("Custom screen position (dragged on canvas)"), + .child(tr::editor::sidebar::layout::custom_position()), ) .child(section_action( &theme, "reset-display-position", - "Reset", + tr::common::reset(), None, false, cx.listener(|this, _, window, cx| { @@ -2781,7 +2813,7 @@ impl EditorWindow { .into_any_element() })) .child(self.slider_field( - "Rounded Corners", + tr::editor::sidebar::layout::rounded_corners(), SliderKey::Bg(BgSlider::Rounding), "%", cx, @@ -2789,7 +2821,7 @@ impl EditorWindow { .child(self.render_corner_style(cx)) .children(self.sidebar.style_target.is_none().then(|| { self.slider_field( - "Motion Blur", + tr::editor::sidebar::layout::motion_blur(), SliderKey::Bg(BgSlider::MotionBlur), "x100%", cx, @@ -2824,7 +2856,7 @@ impl EditorWindow { .map(|item| ui::SegmentOption::new(item.label(), tab == item)) .collect(); - ui::Field::section(&theme, "Background") + ui::Field::section(&theme, tr::editor::sidebar::background::title()) .value(section_action( &theme, "source-none", @@ -2875,9 +2907,9 @@ impl EditorWindow { let theme = self.theme; let importing = self.sidebar.importing_desktop; let label = if importing { - "Importing..." + tr::editor::sidebar::desktop::importing() } else { - "Import desktop background" + tr::editor::sidebar::desktop::import() }; match self.sidebar.desktop_background.clone() { @@ -2902,7 +2934,7 @@ impl EditorWindow { div() .text_size(px(13.)) .text_color(Hsla::from(theme.editor.text_1)) - .child("Use the wallpaper from your desktop"), + .child(tr::editor::sidebar::desktop::prompt()), ) .child( ui::EditorButton::plain(&theme, "import-desktop") @@ -2964,9 +2996,9 @@ impl EditorWindow { .left_icon("icons/monitor-outline.svg") .icon_size(px(16.)) .label(if importing { - "Importing..." + tr::editor::sidebar::desktop::importing() } else { - "Re-import" + tr::editor::sidebar::desktop::reimport() }) .disabled(importing) .on_click(cx.listener(|this, _, window, cx| { @@ -3020,7 +3052,7 @@ impl EditorWindow { BACKGROUND_THEMES .iter() .enumerate() - .map(|(index, (_, label))| { + .map(|(index, (id, _))| { let selected = index == selected_theme; div() .id(SharedString::from(format!("wallpaper-theme-{index}"))) @@ -3041,7 +3073,7 @@ impl EditorWindow { .cursor_pointer() .hover(|this| this.bg(Hsla::from(theme.editor.ctl))) }) - .child(*label) + .child(background_theme_label(id)) .on_click(cx.listener(move |this, _, window, cx| { this.sidebar.wallpaper_theme = index; this.ensure_wallpapers(window, cx); @@ -3134,7 +3166,7 @@ impl EditorWindow { .child( div() .text_color(Hsla::from(theme.editor.text_1)) - .child("Click to select or drag and drop image"), + .child(tr::editor::sidebar::image::prompt()), ) .on_click(cx.listener(|this, _, window, cx| { this.pick_background_image(window, cx); @@ -3378,14 +3410,14 @@ impl EditorWindow { .items_end() .child(self.render_gradient_stop( "gradient-from", - "From", + tr::editor::sidebar::gradient::from(), ColorTarget::GradientFrom, from, cx, )) .child(self.render_gradient_stop( "gradient-to", - "To", + tr::editor::sidebar::gradient::to(), ColorTarget::GradientTo, to, cx, @@ -3397,13 +3429,13 @@ impl EditorWindow { .flex() .flex_col() .child(self.slider_field( - "Angle", + tr::editor::sidebar::gradient::angle(), SliderKey::Bg(BgSlider::GradientAngle), "deg", cx, )) .child(self.slider_field( - "Noise", + tr::editor::sidebar::gradient::noise(), SliderKey::Bg(BgSlider::GradientNoise), "%", cx, @@ -3411,7 +3443,7 @@ impl EditorWindow { // Grain Scale appears only while noise is on (`:204-221`). .children((noise > 0.).then(|| { self.slider_field( - "Grain Scale", + tr::editor::sidebar::gradient::grain_scale(), SliderKey::Bg(BgSlider::GradientGrain), "%", cx, @@ -3556,16 +3588,18 @@ impl EditorWindow { let theme = self.theme; let current = self.project.background.rounding_type; let label = match current { - CornerStyle::Squircle => "Squircle", - CornerStyle::Rounded => "Rounded", + CornerStyle::Squircle => tr::editor::sidebar::corner::squircle(), + CornerStyle::Rounded => tr::editor::sidebar::corner::rounded(), }; - ui::Field::inline(&theme, "Corner Style").child(div().w(px(150.)).child(self.menu_select( - crate::editor_tabs::SidebarMenu::BackgroundCornerStyle, - "corner-style", - label, - cx, - ))) + ui::Field::inline(&theme, tr::editor::sidebar::corner::title()).child( + div().w(px(150.)).child(self.menu_select( + crate::editor_tabs::SidebarMenu::BackgroundCornerStyle, + "corner-style", + label, + cx, + )), + ) } fn render_border_field(&self, cx: &mut Context) -> impl IntoElement { @@ -3578,7 +3612,7 @@ impl EditorWindow { .flex() .flex_col() .child( - ui::Field::inline(&theme, "Border").value( + ui::Field::inline(&theme, tr::editor::sidebar::border::title()).value( ui::Toggle::plain(&theme, "border-enabled", enabled) .on_click(cx.listener(move |this, _, window, cx| { let next = !enabled; @@ -3612,21 +3646,23 @@ impl EditorWindow { .flex() .flex_col() .child(self.slider_field( - "Border Width", + tr::editor::sidebar::border::width(), SliderKey::Bg(BgSlider::BorderWidth), "px", cx, )) .child( - ui::Field::stacked(&theme, "Border Color").child(self.render_rgb_input( - "border-color", - ColorTarget::BorderColor, - color, - cx, - )), + ui::Field::stacked(&theme, tr::editor::sidebar::border::color()).child( + self.render_rgb_input( + "border-color", + ColorTarget::BorderColor, + color, + cx, + ), + ), ) .child(self.slider_field( - "Border Opacity", + tr::editor::sidebar::border::opacity(), SliderKey::Bg(BgSlider::BorderOpacity), "%", cx, @@ -3648,7 +3684,7 @@ impl EditorWindow { .flex() .flex_col() .child( - ui::Field::inline(&theme, "MacBook notch").value( + ui::Field::inline(&theme, tr::editor::sidebar::notch::title()).value( ui::Toggle::plain(&theme, "notch-enabled", enabled) .on_click(cx.listener(move |this, _, window, cx| { let next = !enabled; @@ -3685,26 +3721,22 @@ impl EditorWindow { .pt(px(4.)) .text_size(px(12.)) .text_color(Hsla::from(theme.editor.text_2)) - .child( - "Draws a MacBook notch over the recording. Recordings made \ - on a Mac with a notch use their own measurements; \ - otherwise start from the size below and adjust to match.", - ), + .child(tr::editor::sidebar::notch::description()), ) .child(self.slider_field( - "Notch Width", + tr::editor::sidebar::notch::width(), SliderKey::Bg(BgSlider::NotchWidth), "pct", cx, )) .child(self.slider_field( - "Notch Height", + tr::editor::sidebar::notch::height(), SliderKey::Bg(BgSlider::NotchHeight), "pct", cx, )) .child(self.slider_field( - "Notch Position", + tr::editor::sidebar::notch::position(), SliderKey::Bg(BgSlider::NotchX), "pct", cx, @@ -3720,7 +3752,12 @@ impl EditorWindow { div() .flex() .flex_col() - .child(self.slider_field("Shadow", SliderKey::Bg(BgSlider::Shadow), "%", cx)) + .child(self.slider_field( + tr::editor::sidebar::shadow::title(), + SliderKey::Bg(BgSlider::Shadow), + "%", + cx, + )) // `ShadowSettings` (`ShadowSettings.tsx:37-86`): its own trigger // row with a rotating chevron, over three sliders. .child( @@ -3730,7 +3767,7 @@ impl EditorWindow { .child(disclosure_row( &theme, "shadow-advanced", - "Advanced shadow settings", + tr::editor::sidebar::shadow::advanced(), open, cx.listener(|this, _, window, cx| { this.sidebar.shadow_open.toggle(); @@ -3743,19 +3780,19 @@ impl EditorWindow { .flex() .flex_col() .child(self.slider_field( - "Size", + tr::editor::sidebar::shadow::shadow_size(), SliderKey::Bg(BgSlider::ShadowSize), "", cx, )) .child(self.slider_field( - "Opacity", + tr::editor::sidebar::shadow::opacity(), SliderKey::Bg(BgSlider::ShadowOpacity), "", cx, )) .child(self.slider_field( - "Blur", + tr::editor::sidebar::shadow::blur(), SliderKey::Bg(BgSlider::ShadowBlur), "", cx, @@ -4483,9 +4520,9 @@ pub(crate) enum StyleGroup { impl StyleGroup { pub(crate) fn label(self) -> &'static str { match self { - Self::Background => "Background & frame", - Self::Camera => "Camera", - Self::Cursor => "Cursor", + Self::Background => tr::editor::sidebar::style_group::background(), + Self::Camera => tr::editor::sidebar::style_group::camera(), + Self::Cursor => tr::editor::sidebar::style_group::cursor(), } } @@ -4688,12 +4725,15 @@ impl EditorWindow { .gap(px(8.)) .font_weight(gpui::FontWeight::MEDIUM) .child(svg().path(group.icon()).size(px(16.))) - .child(format!("Style {} · {}", index + 1, group.label())), + .child(tr::editor::sidebar::style_group::title( + index + 1, + group.label(), + )), ) .child( div() .text_size(px(11.)) - .child("Editing this segment only. Global settings are unchanged."), + .child(tr::editor::sidebar::style_group::editing_note()), ) .child( div() @@ -4707,7 +4747,7 @@ impl EditorWindow { .items_center() .gap(px(4.)) .child(svg().path("icons/arrow-left.svg").size(px(14.))) - .child("Back to Style"), + .child(tr::editor::sidebar::style_group::back()), ) .on_click(cx.listener(|this, _, window, cx| { this.end_field_edit(cx); diff --git a/apps/desktop-gpui/src/editor_sidebar/animated_gradient.rs b/apps/desktop-gpui/src/editor_sidebar/animated_gradient.rs index eeda7203a0d..5f6662ddd9b 100644 --- a/apps/desktop-gpui/src/editor_sidebar/animated_gradient.rs +++ b/apps/desktop-gpui/src/editor_sidebar/animated_gradient.rs @@ -107,6 +107,76 @@ impl AnimatedGradientState { } } +/// The catalog's own labels are the English ones the Tauri app shares. +fn control_label(control: &AnimatedGradientControl) -> SharedString { + SharedString::new_static(match control.key { + AnimatedGradientParameter::Direction => { + tr::editor::sidebar::animated_gradient::control::direction() + } + AnimatedGradientParameter::FlowScale => { + tr::editor::sidebar::animated_gradient::control::flow_scale() + } + AnimatedGradientParameter::FlowStrength => { + tr::editor::sidebar::animated_gradient::control::flow_strength() + } + AnimatedGradientParameter::Curvature => { + tr::editor::sidebar::animated_gradient::control::curvature() + } + AnimatedGradientParameter::Detail => { + tr::editor::sidebar::animated_gradient::control::detail() + } + AnimatedGradientParameter::Relief => { + tr::editor::sidebar::animated_gradient::control::relief() + } + AnimatedGradientParameter::Light => { + tr::editor::sidebar::animated_gradient::control::highlights() + } + AnimatedGradientParameter::Shade => { + tr::editor::sidebar::animated_gradient::control::shading() + } + AnimatedGradientParameter::Ripples => { + tr::editor::sidebar::animated_gradient::control::ripple_size() + } + AnimatedGradientParameter::GrainAmount => { + tr::editor::sidebar::animated_gradient::control::grain_amount() + } + AnimatedGradientParameter::GrainSize => { + tr::editor::sidebar::animated_gradient::control::grain_size() + } + AnimatedGradientParameter::Exposure => { + tr::editor::sidebar::animated_gradient::control::exposure() + } + AnimatedGradientParameter::Contrast => { + tr::editor::sidebar::animated_gradient::control::contrast() + } + AnimatedGradientParameter::Vibrance => { + tr::editor::sidebar::animated_gradient::control::vibrance() + } + AnimatedGradientParameter::MotionSpeed => { + tr::editor::sidebar::animated_gradient::control::motion_speed() + } + }) +} + +fn group_label(name: &str) -> SharedString { + match name { + "Flow" => SharedString::new_static(tr::editor::sidebar::animated_gradient::group::flow()), + "Lighting" => { + SharedString::new_static(tr::editor::sidebar::animated_gradient::group::lighting()) + } + "Texture" => { + SharedString::new_static(tr::editor::sidebar::animated_gradient::group::texture()) + } + "Colour" => { + SharedString::new_static(tr::editor::sidebar::animated_gradient::group::colour()) + } + "Animation" => { + SharedString::new_static(tr::editor::sidebar::animated_gradient::group::animation()) + } + _ => SharedString::from(name.to_string()), + } +} + fn control_groups(catalog: &AnimatedGradientCatalog) -> Vec<(&str, Vec<&AnimatedGradientControl>)> { let mut groups: Vec<(&str, Vec<&AnimatedGradientControl>)> = Vec::new(); for control in &catalog.controls { @@ -238,7 +308,9 @@ fn slider_unit(key: AnimatedGradientParameter) -> &'static str { fn format_control_value(control: &AnimatedGradientControl, value: f32) -> String { match control.key { AnimatedGradientParameter::Direction => format!("{}\u{b0}", value.round() as i32), - AnimatedGradientParameter::MotionSpeed if value == 0. => "Still".into(), + AnimatedGradientParameter::MotionSpeed if value == 0. => { + tr::editor::sidebar::animated_gradient::still().into() + } _ if control.step < 1. => format!("{value:.1}"), _ => format!("{}", value.round() as i32), } @@ -453,7 +525,7 @@ impl EditorWindow { } let input = cx.new(|cx| { let mut input = ui::TextInputState::single_line(window, cx); - input.set_placeholder("Name this gradient"); + input.set_placeholder(tr::editor::sidebar::animated_gradient::name_placeholder()); input }); self.push_text_subscription(cx.subscribe_in( @@ -692,9 +764,9 @@ impl EditorWindow { let mut library = read_library(); if library.save_preset(&name, &config).is_none() { self.sidebar.animated_gradient.error = Some(if name.trim().is_empty() { - "Enter a name for this gradient.".to_string() + tr::editor::sidebar::animated_gradient::name_required().to_string() } else { - format!("You can save up to {MAX_SAVED} gradients. Delete one to add another.") + tr::editor::sidebar::animated_gradient::limit_reached(MAX_SAVED) }); } else if write_setting("presets", &library.presets) { self.sidebar.animated_gradient.library.presets = library.presets; @@ -702,7 +774,7 @@ impl EditorWindow { self.close_animated_gradient_save(cx); } else { self.sidebar.animated_gradient.error = - Some("Could not save this gradient. Try again.".into()); + Some(tr::editor::sidebar::animated_gradient::save_failed().into()); } cx.notify(); } @@ -715,7 +787,7 @@ impl EditorWindow { self.sidebar.animated_gradient.error = None; } else { self.sidebar.animated_gradient.error = - Some("Could not delete this gradient. Try again.".into()); + Some(tr::editor::sidebar::animated_gradient::delete_failed().into()); } cx.notify(); } @@ -729,7 +801,7 @@ impl EditorWindow { let error = state.error.clone().or_else(|| { state .persistence_failed - .then(|| "Could not remember your animated gradient settings.".to_string()) + .then(|| tr::editor::sidebar::animated_gradient::remember_failed().to_string()) }); div() @@ -772,7 +844,10 @@ impl EditorWindow { .text_color(Hsla::from(theme.editor.text_3)) .cursor_pointer() .hover(|this| this.bg(Hsla::from(theme.editor.ctl_hover))) - .tooltip(move |_window, cx| ui::Tooltip::new(&theme, "Randomize").view(cx)) + .tooltip(move |_window, cx| { + ui::Tooltip::new(&theme, tr::editor::sidebar::animated_gradient::randomize()) + .view(cx) + }) .child( svg() .path("icons/shuffle.svg") @@ -800,6 +875,12 @@ impl EditorWindow { let save_row = state.save_open.then(|| { let input = state.name_input.clone(); + if let Some(input) = &input { + input.update(cx, |input, _| { + input + .set_placeholder(tr::editor::sidebar::animated_gradient::name_placeholder()) + }); + } let name_empty = input .as_ref() .is_none_or(|input| input.read(cx).text().trim().is_empty()); @@ -833,13 +914,13 @@ impl EditorWindow { this.focus_root(window, cx); })) }) - .child("Save"), + .child(tr::common::save()), ) .child(icon_button( &theme, "animated-gradient-save-cancel", "icons/x.svg", - "Cancel", + tr::common::cancel(), false, cx.listener(|this, _, window, cx| { this.close_animated_gradient_save(cx); @@ -849,14 +930,14 @@ impl EditorWindow { .into_any_element() }); - ui::Field::section(&theme, "Presets") + ui::Field::section(&theme, tr::editor::sidebar::animated_gradient::presets()) .value( header_button( &theme, "animated-gradient-save-toggle", "icons/save.svg", - "Save", - "Save the current gradient", + tr::common::save(), + tr::editor::sidebar::animated_gradient::save_tooltip(), save_disabled, save_open, cx.listener(move |this, _, window, cx| { @@ -882,7 +963,7 @@ impl EditorWindow { div() .text_size(px(11.)) .text_color(Hsla::from(theme.editor.text_3)) - .child("Saved"), + .child(tr::editor::sidebar::animated_gradient::saved()), ) .child(swatch_rows(saved)) }), @@ -938,7 +1019,13 @@ impl EditorWindow { .shadow_sm() .cursor_pointer() .hover(|this| this.bg(Hsla::from(theme.red_11))) - .tooltip(move |_window, cx| ui::Tooltip::new(&theme, "Delete").view(cx)) + .tooltip(move |_window, cx| { + ui::Tooltip::new( + &theme, + tr::editor::sidebar::animated_gradient::delete(), + ) + .view(cx) + }) .child( svg() .path("icons/x.svg") @@ -1074,7 +1161,7 @@ impl EditorWindow { &theme, "animated-gradient-remove-stop", "icons/trash.svg", - "Remove colour", + tr::editor::sidebar::animated_gradient::remove_colour(), count <= MIN_STOPS, cx.listener(move |this, _, window, cx| { this.remove_animated_gradient_stop(selected, window, cx); @@ -1082,14 +1169,14 @@ impl EditorWindow { )) }); - ui::Field::section(&theme, "Colours") + ui::Field::section(&theme, tr::editor::sidebar::animated_gradient::colours()) .value( header_button( &theme, "animated-gradient-add-stop", "icons/plus.svg", - "Add", - "Add a colour", + tr::common::add(), + tr::editor::sidebar::animated_gradient::add_colour(), count >= MAX_STOPS, false, cx.listener(|this, _, window, cx| { @@ -1117,7 +1204,7 @@ impl EditorWindow { let theme = self.theme; let control = AnimatedGradientParameter::MotionSpeed.control(); let value = AnimatedGradientParameter::MotionSpeed.get(config); - ui::Field::inline(&theme, "Motion") + ui::Field::inline(&theme, tr::editor::sidebar::animated_gradient::motion()) .value_text(format_control_value(&control, value)) .child(self.slider_flex( SliderKey::AnimatedGradient(AnimatedGradientParameter::MotionSpeed), @@ -1144,7 +1231,9 @@ impl EditorWindow { groups .iter() .enumerate() - .map(|(index, (name, _))| ui::SegmentOption::new(name.to_string(), index == active)) + .map(|(index, (name, _))| { + ui::SegmentOption::new(group_label(name), index == active) + }) .collect(), ) .item_padding(px(6.), px(4.)) @@ -1160,7 +1249,7 @@ impl EditorWindow { .iter() .map(|control| { let value = control.key.get(config); - ui::Field::inline(&theme, control.label.clone()) + ui::Field::inline(&theme, control_label(control)) .value_text(format_control_value(control, value)) .child(self.slider_flex( SliderKey::AnimatedGradient(control.key), @@ -1181,7 +1270,7 @@ impl EditorWindow { .child(div().flex().flex_1().child(disclosure_row( &theme, "animated-gradient-fine-tune", - "Fine-tune", + tr::editor::sidebar::animated_gradient::fine_tune(), open, cx.listener(|this, _, window, cx| { this.sidebar.animated_gradient.fine_tune_open.toggle(); @@ -1193,8 +1282,8 @@ impl EditorWindow { &theme, "animated-gradient-reset", "icons/rotate-ccw.svg", - "Reset", - "Reset fine-tune settings", + tr::common::reset(), + tr::editor::sidebar::animated_gradient::reset_tooltip(), false, false, cx.listener(|this, _, window, cx| { diff --git a/apps/desktop-gpui/src/editor_sidebar/cursor.rs b/apps/desktop-gpui/src/editor_sidebar/cursor.rs index beac97dc78d..910310e9b18 100644 --- a/apps/desktop-gpui/src/editor_sidebar/cursor.rs +++ b/apps/desktop-gpui/src/editor_sidebar/cursor.rs @@ -21,11 +21,11 @@ enum CursorCard { impl CursorCard { fn label(self) -> &'static str { match self { - Self::Default => "Default", + Self::Default => tr::editor::sidebar::cursor::default(), Self::Family(CursorFamily::MacOS) => "macOS", Self::Family(CursorFamily::MacOSTahoe) => "macOS Tahoe", Self::Family(CursorFamily::Windows) => "Windows", - Self::Circle => "Circle", + Self::Circle => tr::editor::sidebar::cursor::circle(), } } @@ -201,12 +201,16 @@ impl EditorWindow { ARROW_BOX, ), ) - .child(div().text_size(px(12.)).child("Default")) + .child( + div() + .text_size(px(12.)) + .child(tr::editor::sidebar::cursor::default()), + ) .child( div() .text_size(px(11.)) .text_color(Hsla::from(theme.gray_11)) - .child("Recorded cursors"), + .child(tr::editor::sidebar::cursor::recorded()), ) .into_any_element(), CursorCard::Family(family) => self.cursor_art(family.arrow(), ARROW_BOX), @@ -237,7 +241,8 @@ impl EditorWindow { }) .when(recorded, |this| { this.tooltip(move |_window, cx| { - ui::Tooltip::new(&theme, "Recorded with this cursor").view(cx) + ui::Tooltip::new(&theme, tr::editor::sidebar::cursor::recorded_tooltip()) + .view(cx) }) }) .child(art) @@ -307,17 +312,17 @@ impl EditorWindow { let recorded = self.recorded_cursor_family; let description = match selected { - CursorCard::Default => "Keeps the cursor shapes and appearance from your recording.", + CursorCard::Default => tr::editor::sidebar::cursor::description_default(), CursorCard::Family(CursorFamily::MacOS) => { - "Classic macOS appearance. Custom cursors keep their recorded shape." + tr::editor::sidebar::cursor::description_macos() } CursorCard::Family(CursorFamily::MacOSTahoe) => { - "macOS Tahoe appearance. Custom cursors keep their recorded shape." + tr::editor::sidebar::cursor::description_tahoe() } CursorCard::Family(CursorFamily::Windows) => { - "Windows appearance. Custom cursors keep their recorded shape." + tr::editor::sidebar::cursor::description_windows() } - CursorCard::Circle => "Replaces all cursor shapes with a circle.", + CursorCard::Circle => tr::editor::sidebar::cursor::description_circle(), }; div() .flex() @@ -357,7 +362,7 @@ impl EditorWindow { .flex() .flex_col() .child( - ui::Field::inline(&theme, "Click Ripple").value( + ui::Field::inline(&theme, tr::editor::sidebar::cursor::click_ripple()).value( ui::Toggle::plain(&theme, "cursor-ripple", enabled) .on_click(cx.listener(move |this, _, window, cx| { let next = !this.style_control_project().cursor.ripple.enabled; @@ -379,27 +384,29 @@ impl EditorWindow { .pt(px(4.)) .pb(px(8.)) .child( - ui::Subfield::plain(&theme, "Color").child(self.render_rgb_input( - "cursor-ripple-color", - ColorTarget::CursorRipple, - color, - cx, - )), + ui::Subfield::plain(&theme, tr::editor::sidebar::cursor::color()).child( + self.render_rgb_input( + "cursor-ripple-color", + ColorTarget::CursorRipple, + color, + cx, + ), + ), ) .child(self.slider_field( - "Strength", + tr::editor::sidebar::cursor::strength(), SliderKey::Cursor(CursorSlider::RippleStrength), "%", cx, )) .child(self.slider_field( - "Size", + tr::editor::sidebar::cursor::ripple_size(), SliderKey::Cursor(CursorSlider::RippleSize), "%", cx, )) .child(self.slider_field( - "Duration", + tr::editor::sidebar::cursor::duration(), SliderKey::Cursor(CursorSlider::RippleDuration), "secs", cx, diff --git a/apps/desktop-gpui/src/editor_tabs.rs b/apps/desktop-gpui/src/editor_tabs.rs index 7801a8d0f4a..97b0ef8bf69 100644 --- a/apps/desktop-gpui/src/editor_tabs.rs +++ b/apps/desktop-gpui/src/editor_tabs.rs @@ -38,6 +38,7 @@ use crate::{ editor_color::GradeTarget, editor_sidebar::{SliderKey, collapsible, dashed_divider}, editor_window::EditorWindow, + i18n::tr, store, theme::Theme, transcription, ui, @@ -47,128 +48,160 @@ use crate::{ // Catalogues // --------------------------------------------------------------------------- +/// A catalogue label, read in the current language each time it is drawn. +pub type Label = fn() -> &'static str; + /// `CAMERA_SHAPES` (`ConfigSidebar.tsx:381-391`). -pub const CAMERA_SHAPES: [(CameraShape, &str); 2] = [ - (CameraShape::Square, "Square"), - (CameraShape::Source, "Source"), +pub const CAMERA_SHAPES: [(CameraShape, Label); 2] = [ + (CameraShape::Square, tr::editor::tabs::camera::shape::square), + (CameraShape::Source, tr::editor::tabs::camera::shape::source), ]; -pub const CAMERA_BLUR_MODES: &[(BackgroundBlurMode, &str)] = &[ - (BackgroundBlurMode::Off, "Off"), - (BackgroundBlurMode::Light, "Light Blur"), - (BackgroundBlurMode::Heavy, "Heavy Blur"), +pub const CAMERA_BLUR_MODES: &[(BackgroundBlurMode, Label)] = &[ + (BackgroundBlurMode::Off, tr::editor::tabs::camera::blur::off), + ( + BackgroundBlurMode::Light, + tr::editor::tabs::camera::blur::light, + ), + ( + BackgroundBlurMode::Heavy, + tr::editor::tabs::camera::blur::heavy, + ), #[cfg(target_os = "macos")] - (BackgroundBlurMode::Remove, "Remove Background"), + ( + BackgroundBlurMode::Remove, + tr::editor::tabs::camera::blur::remove, + ), ]; /// `CORNER_STYLE_OPTIONS` (`:399-402`). -pub const CORNER_STYLES: [(CornerStyle, &str); 2] = [ - (CornerStyle::Squircle, "Squircle"), - (CornerStyle::Rounded, "Rounded"), +pub const CORNER_STYLES: [(CornerStyle, Label); 2] = [ + (CornerStyle::Squircle, tr::editor::tabs::corner::squircle), + (CornerStyle::Rounded, tr::editor::tabs::corner::rounded), ]; /// `STEREO_MODES` (`:375-379`). -pub const STEREO_MODES: [(StereoMode, &str); 3] = [ - (StereoMode::Stereo, "Stereo"), - (StereoMode::MonoL, "Mono L"), - (StereoMode::MonoR, "Mono R"), +pub const STEREO_MODES: [(StereoMode, Label); 3] = [ + (StereoMode::Stereo, tr::editor::tabs::audio::stereo::stereo), + ( + StereoMode::MonoL, + tr::editor::tabs::audio::stereo::mono_left, + ), + ( + StereoMode::MonoR, + tr::editor::tabs::audio::stereo::mono_right, + ), ]; /// `CURSOR_ANIMATION_STYLE_OPTIONS` (`:434-476`) -- the four presets plus the /// `custom` value the physics sliders drop the picker into. -pub const CURSOR_STYLES: [(CursorAnimationStyle, &str, &str); 4] = [ +pub const CURSOR_STYLES: [(CursorAnimationStyle, Label, Label); 4] = [ ( CursorAnimationStyle::Slow, - "Slow", - "Long, drifting movement", + tr::editor::tabs::cursor::style::slow, + tr::editor::tabs::cursor::style::slow_description, ), ( CursorAnimationStyle::Smooth, - "Smooth", - "Soft, floaty movement", + tr::editor::tabs::cursor::style::smooth, + tr::editor::tabs::cursor::style::smooth_description, ), ( CursorAnimationStyle::Mellow, - "Mellow", - "Balanced, natural movement", + tr::editor::tabs::cursor::style::mellow, + tr::editor::tabs::cursor::style::mellow_description, + ), + ( + CursorAnimationStyle::Fast, + tr::editor::tabs::cursor::style::fast, + tr::editor::tabs::cursor::style::fast_description, ), - (CursorAnimationStyle::Fast, "Fast", "Snappy, close tracking"), ]; /// `FONT_OPTIONS` (`text-style.tsx:12-16`). pub const FONT_OPTIONS: [&str; 3] = ["System Sans-Serif", "System Serif", "System Monospace"]; /// `TEXT_WEIGHT_OPTIONS` (`text-style.tsx:36-40`). -pub const TEXT_WEIGHTS: [(u32, &str); 3] = [(400, "Normal"), (500, "Medium"), (700, "Bold")]; +pub const TEXT_WEIGHTS: [(u32, Label); 3] = [ + (400, tr::editor::tabs::weight::normal), + (500, tr::editor::tabs::weight::medium), + (700, tr::editor::tabs::weight::bold), +]; /// `KEYBOARD_POSITION_OPTIONS` (`text-style.tsx:28-35`). -pub const KEYBOARD_POSITIONS: [(&str, &str); 6] = [ - ("top-left", "Top Left"), - ("top-center", "Top Center"), - ("top-right", "Top Right"), - ("bottom-left", "Bottom Left"), - ("bottom-center", "Bottom Center"), - ("bottom-right", "Bottom Right"), +pub const KEYBOARD_POSITIONS: [(&str, Label); 6] = [ + ("top-left", tr::editor::tabs::position::top_left), + ("top-center", tr::editor::tabs::position::top_center), + ("top-right", tr::editor::tabs::position::top_right), + ("bottom-left", tr::editor::tabs::position::bottom_left), + ("bottom-center", tr::editor::tabs::position::bottom_center), + ("bottom-right", tr::editor::tabs::position::bottom_right), ]; /// `CAPTION_POSITION_OPTIONS` (`text-style.tsx:18-26`) -- the keyboard's six /// with `manual` in front. -pub const CAPTION_POSITIONS: [(&str, &str); 7] = [ - ("manual", "Manual"), - ("top-left", "Top Left"), - ("top-center", "Top Center"), - ("top-right", "Top Right"), - ("bottom-left", "Bottom Left"), - ("bottom-center", "Bottom Center"), - ("bottom-right", "Bottom Right"), +pub const CAPTION_POSITIONS: [(&str, Label); 7] = [ + ("manual", tr::editor::tabs::position::manual), + ("top-left", tr::editor::tabs::position::top_left), + ("top-center", tr::editor::tabs::position::top_center), + ("top-right", tr::editor::tabs::position::top_right), + ("bottom-left", tr::editor::tabs::position::bottom_left), + ("bottom-center", tr::editor::tabs::position::bottom_center), + ("bottom-right", tr::editor::tabs::position::bottom_right), ]; /// `CAPTION_ANIMATION_OPTIONS` (`text-style.tsx:64-68`). -pub const CAPTION_ANIMATIONS: [(&str, &str); 3] = - [("none", "None"), ("bounce", "Bounce"), ("pop", "Pop")]; +pub const CAPTION_ANIMATIONS: [(&str, Label); 3] = [ + ("none", tr::editor::tabs::captions::animation::none), + ("bounce", tr::editor::tabs::captions::animation::bounce), + ("pop", tr::editor::tabs::captions::animation::pop), +]; /// `CAPTION_HIGHLIGHT_STYLE_OPTIONS` (`text-style.tsx:70-73`). -pub const CAPTION_HIGHLIGHT_STYLES: [(&str, &str); 2] = [("color", "Color"), ("pill", "Pill")]; +pub const CAPTION_HIGHLIGHT_STYLES: [(&str, Label); 2] = [ + ("color", tr::editor::tabs::captions::highlight::color), + ("pill", tr::editor::tabs::captions::highlight::pill), +]; /// One row of `MODEL_OPTIONS` (`CaptionsTab.tsx:72-78`). pub struct CaptionModel { pub name: &'static str, - pub label: &'static str, + pub label: Label, /// The engine identity the source shows in an info tooltip. pub model_name: &'static str, pub size: &'static str, - pub description: &'static str, + pub description: Label, } /// `MODEL_OPTIONS` (`CaptionsTab.tsx:87-116`). pub static CAPTION_MODELS: &[CaptionModel] = &[ CaptionModel { name: "best", - label: "Recommended", + label: tr::editor::tabs::captions::model::recommended, model_name: "parakeet-tdt-0.6b-v3 int8", size: "~640MB", - description: "Best balance for most recordings", + description: tr::editor::tabs::captions::model::recommended_description, }, CaptionModel { name: "best-max", - label: "High Accuracy", + label: tr::editor::tabs::captions::model::high_accuracy, model_name: "parakeet-tdt-0.6b-v3", size: "~2.4GB", - description: "Larger download, higher accuracy", + description: tr::editor::tabs::captions::model::high_accuracy_description, }, CaptionModel { name: "small", - label: "Small", + label: tr::editor::tabs::captions::model::small, model_name: "whisper.cpp small", size: "466MB", - description: "Smallest download", + description: tr::editor::tabs::captions::model::small_description, }, CaptionModel { name: "medium", - label: "Medium", + label: tr::editor::tabs::captions::model::medium, model_name: "whisper.cpp medium", size: "1.5GB", - description: "Slower, more accurate", + description: tr::editor::tabs::captions::model::medium_description, }, ]; @@ -185,40 +218,40 @@ pub fn available_caption_models() -> &'static [CaptionModel] { /// `LANGUAGE_OPTIONS` (`CaptionsTab.tsx:118-148`), de-duplicated: the array /// repeats `ar`/`hi`/`bn`/`ta` at the end, and a Kobalte listbox keyed on the /// code renders each once. -pub const CAPTION_LANGUAGES: [(&str, &str); 26] = [ - ("auto", "Auto Detect"), - ("en", "English"), - ("es", "Spanish"), - ("fr", "French"), - ("de", "German"), - ("it", "Italian"), - ("pt", "Portuguese"), - ("nl", "Dutch"), - ("pl", "Polish"), - ("ru", "Russian"), - ("sk", "Slovak"), - ("tr", "Turkish"), - ("ja", "Japanese"), - ("ko", "Korean"), - ("zh", "Chinese"), - ("ar", "Arabic"), - ("hi", "Hindi"), - ("bn", "Bengali"), - ("ta", "Tamil"), - ("te", "Telugu"), - ("mr", "Marathi"), - ("gu", "Gujarati"), - ("pa", "Punjabi"), - ("ur", "Urdu"), - ("fa", "Persian"), - ("he", "Hebrew"), +pub const CAPTION_LANGUAGES: [(&str, Label); 26] = [ + ("auto", tr::editor::tabs::captions::language::auto), + ("en", tr::editor::tabs::captions::language::en), + ("es", tr::editor::tabs::captions::language::es), + ("fr", tr::editor::tabs::captions::language::fr), + ("de", tr::editor::tabs::captions::language::de), + ("it", tr::editor::tabs::captions::language::it), + ("pt", tr::editor::tabs::captions::language::pt), + ("nl", tr::editor::tabs::captions::language::nl), + ("pl", tr::editor::tabs::captions::language::pl), + ("ru", tr::editor::tabs::captions::language::ru), + ("sk", tr::editor::tabs::captions::language::sk), + ("tr", tr::editor::tabs::captions::language::tr), + ("ja", tr::editor::tabs::captions::language::ja), + ("ko", tr::editor::tabs::captions::language::ko), + ("zh", tr::editor::tabs::captions::language::zh), + ("ar", tr::editor::tabs::captions::language::ar), + ("hi", tr::editor::tabs::captions::language::hi), + ("bn", tr::editor::tabs::captions::language::bn), + ("ta", tr::editor::tabs::captions::language::ta), + ("te", tr::editor::tabs::captions::language::te), + ("mr", tr::editor::tabs::captions::language::mr), + ("gu", tr::editor::tabs::captions::language::gu), + ("pa", tr::editor::tabs::captions::language::pa), + ("ur", tr::editor::tabs::captions::language::ur), + ("fa", tr::editor::tabs::captions::language::fa), + ("he", tr::editor::tabs::captions::language::he), ]; /// `CAPTION_STYLE_PRESETS` (`store/captions.ts:57-160`). pub struct CaptionPreset { pub id: &'static str, - pub label: &'static str, - pub description: &'static str, + pub label: Label, + pub description: Label, pub font_weight: u32, pub size: u32, pub color: &'static str, @@ -237,8 +270,8 @@ pub struct CaptionPreset { pub static CAPTION_PRESETS: &[CaptionPreset] = &[ CaptionPreset { id: "classic", - label: "Classic", - description: "Clean text on a solid rounded background.", + label: tr::editor::tabs::captions::preset::classic, + description: tr::editor::tabs::captions::preset::classic_description, font_weight: 700, size: 50, color: "#FFFFFF", @@ -255,8 +288,8 @@ pub static CAPTION_PRESETS: &[CaptionPreset] = &[ }, CaptionPreset { id: "karaoke", - label: "Karaoke", - description: "Words light up in sync with speech.", + label: tr::editor::tabs::captions::preset::karaoke, + description: tr::editor::tabs::captions::preset::karaoke_description, font_weight: 700, size: 52, color: "#FFFFFF", @@ -273,8 +306,8 @@ pub static CAPTION_PRESETS: &[CaptionPreset] = &[ }, CaptionPreset { id: "highlight", - label: "Highlight", - description: "Bold caps with a pill behind the active word.", + label: tr::editor::tabs::captions::preset::highlight, + description: tr::editor::tabs::captions::preset::highlight_description, font_weight: 700, size: 54, color: "#FFFFFF", @@ -291,8 +324,8 @@ pub static CAPTION_PRESETS: &[CaptionPreset] = &[ }, CaptionPreset { id: "pop", - label: "Pop", - description: "Playful caps that pop in with a vibrant accent.", + label: tr::editor::tabs::captions::preset::pop, + description: tr::editor::tabs::captions::preset::pop_description, font_weight: 700, size: 56, color: "#FFFFFF", @@ -309,8 +342,8 @@ pub static CAPTION_PRESETS: &[CaptionPreset] = &[ }, CaptionPreset { id: "minimal", - label: "Minimal", - description: "Subtle outlined text with no background.", + label: tr::editor::tabs::captions::preset::minimal, + description: tr::editor::tabs::captions::preset::minimal_description, font_weight: 600, size: 46, color: "#FFFFFF", @@ -739,20 +772,20 @@ impl EditorWindow { SidebarMenu::BackgroundCornerStyle => CORNER_STYLES .iter() .map(|(style, label)| { - ui::MenuItem::new(*label, *style == project.background.rounding_type) + ui::MenuItem::new(label(), *style == project.background.rounding_type) }) .collect(), SidebarMenu::CameraBlur => CAMERA_BLUR_MODES .iter() .map(|(mode, label)| { - ui::MenuItem::new(*label, *mode == project.camera.background_blur.mode) + ui::MenuItem::new(label(), *mode == project.camera.background_blur.mode) }) .collect(), SidebarMenu::CameraShape => CAMERA_SHAPES .iter() .map(|(shape, label)| { ui::MenuItem::new( - *label, + label(), std::mem::discriminant(shape) == std::mem::discriminant(&project.camera.shape), ) @@ -761,13 +794,13 @@ impl EditorWindow { SidebarMenu::CameraCornerStyle => CORNER_STYLES .iter() .map(|(style, label)| { - ui::MenuItem::new(*label, *style == project.camera.rounding_type) + ui::MenuItem::new(label(), *style == project.camera.rounding_type) }) .collect(), SidebarMenu::AudioStereo => STEREO_MODES .iter() .map(|(mode, label)| { - ui::MenuItem::new(*label, *mode == project.audio.mic_stereo_mode) + ui::MenuItem::new(label(), *mode == project.audio.mic_stereo_mode) }) .collect(), SidebarMenu::CaptionModel => { @@ -778,7 +811,9 @@ impl EditorWindow { ui::MenuItem::new( SharedString::from(format!( "{} · {} · {}", - model.label, model.size, model.description + (model.label)(), + model.size, + (model.description)() )), model.name == selected, ) @@ -788,7 +823,7 @@ impl EditorWindow { SidebarMenu::CaptionLanguage => CAPTION_LANGUAGES .iter() .map(|(code, label)| { - ui::MenuItem::new(*label, *code == self.sidebar.caption_language) + ui::MenuItem::new(label(), *code == self.sidebar.caption_language) }) .collect(), SidebarMenu::CaptionFont => FONT_OPTIONS @@ -797,19 +832,21 @@ impl EditorWindow { .collect(), SidebarMenu::CaptionHighlightStyle => CAPTION_HIGHLIGHT_STYLES .iter() - .map(|(value, label)| ui::MenuItem::new(*label, *value == captions.highlight_style)) + .map(|(value, label)| { + ui::MenuItem::new(label(), *value == captions.highlight_style) + }) .collect(), SidebarMenu::CaptionPosition => CAPTION_POSITIONS .iter() - .map(|(value, label)| ui::MenuItem::new(*label, *value == captions.position)) + .map(|(value, label)| ui::MenuItem::new(label(), *value == captions.position)) .collect(), SidebarMenu::CaptionAnimation => CAPTION_ANIMATIONS .iter() - .map(|(value, label)| ui::MenuItem::new(*label, *value == captions.animation)) + .map(|(value, label)| ui::MenuItem::new(label(), *value == captions.animation)) .collect(), SidebarMenu::CaptionWeight => TEXT_WEIGHTS .iter() - .map(|(weight, label)| ui::MenuItem::new(*label, *weight == captions.font_weight)) + .map(|(weight, label)| ui::MenuItem::new(label(), *weight == captions.font_weight)) .collect(), SidebarMenu::KeyboardFont => FONT_OPTIONS .iter() @@ -817,11 +854,11 @@ impl EditorWindow { .collect(), SidebarMenu::KeyboardPosition => KEYBOARD_POSITIONS .iter() - .map(|(value, label)| ui::MenuItem::new(*label, *value == keyboard.position)) + .map(|(value, label)| ui::MenuItem::new(label(), *value == keyboard.position)) .collect(), SidebarMenu::KeyboardWeight => TEXT_WEIGHTS .iter() - .map(|(weight, label)| ui::MenuItem::new(*label, *weight == keyboard.font_weight)) + .map(|(weight, label)| ui::MenuItem::new(label(), *weight == keyboard.font_weight)) .collect(), SidebarMenu::TextFontFamily(index) | SidebarMenu::TextWeight(index) @@ -1401,79 +1438,97 @@ impl EditorWindow { .find(|(shape, _)| { std::mem::discriminant(shape) == std::mem::discriminant(&camera.shape) }) - .map_or("Square", |(_, label)| *label); + .map_or(tr::editor::tabs::camera::shape::square(), |(_, label)| { + label() + }); let blur_label = CAMERA_BLUR_MODES .iter() .find(|(mode, _)| *mode == camera.background_blur.mode) - .map_or("Off", |(_, label)| *label); + .map_or(tr::editor::tabs::camera::blur::off(), |(_, label)| label()); let corner_label = CORNER_STYLES .iter() .find(|(style, _)| *style == camera.rounding_type) - .map_or("Squircle", |(_, label)| *label); + .map_or(tr::editor::tabs::corner::squircle(), |(_, label)| label()); div() .flex() .flex_col() .gap(px(14.)) .child( - ui::Field::section(&theme, "Camera").child( + ui::Field::section(&theme, tr::editor::tabs::camera::title()).child( div() .flex() .flex_col() .gap(px(14.)) .child(self.render_camera_position(cx)) - .child(ui::Subfield::plain(&theme, "Hide Camera").child( - ui::Toggle::plain(&theme, "camera-hide", camera.hide).on_click( - cx.listener(|this, _, window, cx| { - let next = !this.style_control_project().camera.hide; - this.edit_project("camera-hide", window, cx, move |p| { - p.camera.hide = next; - true - }); - }), + .child( + ui::Subfield::plain(&theme, tr::editor::tabs::camera::hide()).child( + ui::Toggle::plain(&theme, "camera-hide", camera.hide).on_click( + cx.listener(|this, _, window, cx| { + let next = !this.style_control_project().camera.hide; + this.edit_project("camera-hide", window, cx, move |p| { + p.camera.hide = next; + true + }); + }), + ), ), - )) - .child(ui::Subfield::plain(&theme, "Mirror Camera").child( - ui::Toggle::plain(&theme, "camera-mirror", camera.mirror).on_click( - cx.listener(|this, _, window, cx| { - let next = !this.style_control_project().camera.mirror; - this.edit_project("camera-mirror", window, cx, move |p| { - p.camera.mirror = next; - true - }); - }), + ) + .child( + ui::Subfield::plain(&theme, tr::editor::tabs::camera::mirror()).child( + ui::Toggle::plain(&theme, "camera-mirror", camera.mirror).on_click( + cx.listener(|this, _, window, cx| { + let next = !this.style_control_project().camera.mirror; + this.edit_project("camera-mirror", window, cx, move |p| { + p.camera.mirror = next; + true + }); + }), + ), ), - )) - .child(ui::Subfield::plain(&theme, "Background").child( - div().w(px(160.)).child(self.menu_select( - SidebarMenu::CameraBlur, - "camera-blur", - blur_label, - cx, - )), - )) - .child(ui::Subfield::plain(&theme, "Shape").child( - div().w(px(160.)).child(self.menu_select( - SidebarMenu::CameraShape, - "camera-shape", - shape_label, - cx, - )), - )), + ) + .child( + ui::Subfield::plain(&theme, tr::editor::tabs::camera::background()) + .child(div().w(px(160.)).child(self.menu_select( + SidebarMenu::CameraBlur, + "camera-blur", + blur_label, + cx, + ))), + ) + .child( + ui::Subfield::plain(&theme, tr::editor::tabs::camera::shape_label()) + .child(div().w(px(160.)).child(self.menu_select( + SidebarMenu::CameraShape, + "camera-shape", + shape_label, + cx, + ))), + ), ), ) // `
` .child(dashed_divider(Hsla::from(theme.editor.line))) - .child(self.slider_field("Size", SliderKey::Camera(CameraSlider::Size), "%", cx)) .child(self.slider_field( - "Size During Zoom", + tr::editor::tabs::camera::size_label(), + SliderKey::Camera(CameraSlider::Size), + "%", + cx, + )) + .child(self.slider_field( + tr::editor::tabs::camera::size_during_zoom(), SliderKey::Camera(CameraSlider::ZoomSize), "%", cx, )) .child( - ui::Subfield::plain(&theme, "Keep original size during zoom").child( - ui::Toggle::plain(&theme, "camera-keep-size", camera.scale_during_zoom >= 1.) + ui::Subfield::plain(&theme, tr::editor::tabs::camera::keep_size_during_zoom()) + .child( + ui::Toggle::plain( + &theme, + "camera-keep-size", + camera.scale_during_zoom >= 1., + ) .on_click(cx.listener(|this, _, window, cx| { // `keep ? 1 : DEFAULT_CAMERA_SCALE_DURING_ZOOM`. let keep = this.style_control_project().camera.scale_during_zoom >= 1.; @@ -1487,14 +1542,14 @@ impl EditorWindow { true }); })), - ), + ), ) .child( div() .flex() .flex_col() .child(self.slider_field( - "Rounded Corners", + tr::editor::tabs::camera::rounded_corners(), SliderKey::Camera(CameraSlider::Rounding), "%", cx, @@ -1511,7 +1566,7 @@ impl EditorWindow { .flex() .flex_col() .child(self.slider_field( - "Shadow", + tr::editor::tabs::camera::shadow(), SliderKey::Camera(CameraSlider::Shadow), "%", cx, @@ -1537,11 +1592,11 @@ impl EditorWindow { div() .text_size(px(12.)) .text_color(Hsla::from(theme.gray_11)) - .child("Custom position (dragged on canvas)"), + .child(tr::editor::tabs::camera::custom_position()), ) .child( ui::EditorButton::plain(&theme, "camera-position-reset") - .label("Reset") + .label(tr::editor::tabs::camera::reset()) .on_click(cx.listener(|this, _, window, cx| { this.edit_project("camera-manual-position", window, cx, |p| { p.camera.manual_position = None; @@ -1562,7 +1617,7 @@ impl EditorWindow { label: &'static str, cx: &mut Context, ) -> AnyElement { - ui::Field::inline(&self.theme, "Corner Style") + ui::Field::inline(&self.theme, tr::editor::tabs::camera::corner_style()) .child( div() .w(px(150.)) @@ -1660,7 +1715,10 @@ impl EditorWindow { div() .flex() .flex_col() - .child(ui::Subfield::plain(&theme, "Position")) + .child(ui::Subfield::plain( + &theme, + tr::editor::tabs::camera::position(), + )) .child(grid) .into_any_element() } @@ -1677,7 +1735,7 @@ impl EditorWindow { .child(crate::editor_sidebar::disclosure_row( &theme, "camera-shadow-advanced", - "Advanced shadow settings", + tr::editor::tabs::camera::advanced_shadow(), open, cx.listener(|this, _, window, cx| { this.sidebar.camera_shadow_open.toggle(); @@ -1691,19 +1749,19 @@ impl EditorWindow { .flex_col() .pt(px(4.)) .child(self.slider_field( - "Size", + tr::editor::tabs::camera::size_label(), SliderKey::Camera(CameraSlider::ShadowSize), "", cx, )) .child(self.slider_field( - "Opacity", + tr::editor::tabs::camera::shadow_opacity(), SliderKey::Camera(CameraSlider::ShadowOpacity), "", cx, )) .child(self.slider_field( - "Blur", + tr::editor::tabs::camera::shadow_blur(), SliderKey::Camera(CameraSlider::ShadowBlur), "", cx, @@ -1727,33 +1785,37 @@ impl EditorWindow { let stereo_label = STEREO_MODES .iter() .find(|(mode, _)| *mode == audio.mic_stereo_mode) - .map_or("Stereo", |(_, label)| *label); + .map_or(tr::editor::tabs::audio::stereo::stereo(), |(_, label)| { + label() + }); div() .flex() .flex_col() .gap(px(14.)) .child( - ui::Field::section(&theme, "Audio Controls").child( + ui::Field::section(&theme, tr::editor::tabs::audio::title()).child( div() .flex() .flex_col() .gap(px(16.)) - .child(ui::Subfield::plain(&theme, "Mute Audio").child( - ui::Toggle::plain(&theme, "audio-mute", audio.mute).on_click( - cx.listener(|this, _, window, cx| { - let next = !this.project.audio.mute; - this.edit_project("audio-mute", window, cx, move |p| { - p.audio.mute = next; - true - }); - }), + .child( + ui::Subfield::plain(&theme, tr::editor::tabs::audio::mute()).child( + ui::Toggle::plain(&theme, "audio-mute", audio.mute).on_click( + cx.listener(|this, _, window, cx| { + let next = !this.project.audio.mute; + this.edit_project("audio-mute", window, cx, move |p| { + p.audio.mute = next; + true + }); + }), + ), ), - )) + ) // Only a two-channel microphone gets the stereo row // (`:709-711`). .children(stereo_mic.then(|| { - ui::Subfield::plain(&theme, "Microphone Stereo Mode") + ui::Subfield::plain(&theme, tr::editor::tabs::audio::stereo_mode()) .child(div().w(px(160.)).child(self.menu_select( SidebarMenu::AudioStereo, "audio-stereo", @@ -1767,7 +1829,7 @@ impl EditorWindow { .children(has_microphone.then(|| self.render_studio_sound_card(cx))) .children(has_microphone.then(|| { self.slider_field_disabled( - "Microphone Volume", + tr::editor::tabs::audio::mic_volume(), SliderKey::Audio(AudioSlider::MicVolume), "db", muted, @@ -1777,7 +1839,7 @@ impl EditorWindow { })) .children(has_system_audio.then(|| { self.slider_field_disabled( - "System Audio Volume", + tr::editor::tabs::audio::system_volume(), SliderKey::Audio(AudioSlider::SystemVolume), "db", muted, @@ -1797,18 +1859,18 @@ impl EditorWindow { let isolation_options = [ ( cap_project::VoiceIsolation::Light, - "Light", - "Keep more of your original voice and room sound.", + tr::editor::tabs::audio::isolation_light(), + tr::editor::tabs::audio::isolation_light_description(), ), ( cap_project::VoiceIsolation::Balanced, - "Balanced", - "Clearer isolation with natural voice detail.", + tr::editor::tabs::audio::isolation_balanced(), + tr::editor::tabs::audio::isolation_balanced_description(), ), ( cap_project::VoiceIsolation::Strong, - "Strong", - "More isolation for noisy spaces. May change voice texture.", + tr::editor::tabs::audio::isolation_strong(), + tr::editor::tabs::audio::isolation_strong_description(), ), ]; let enabled = self.project.audio.improve; @@ -1868,9 +1930,7 @@ impl EditorWindow { .text_size(px(12.)) .line_height(px(16.)) .text_color(Hsla::from(theme.editor.text_3)) - .child( - "Reduces background noise and balances your voice level.", - ), + .child(tr::editor::tabs::audio::studio_sound_description()), ), ) .child( @@ -1922,7 +1982,10 @@ impl EditorWindow { if store::set_studio_sound_isolation(isolation) { None } else { - Some("Could not save the Studio Sound default".into()) + Some( + tr::editor::tabs::audio::studio_sound_save_failed() + .into(), + ) }; cx.notify(); } @@ -1958,7 +2021,7 @@ impl EditorWindow { div() .text_size(px(12.)) .text_color(Hsla::from(theme.editor.text_2)) - .child("Use for new recordings"), + .child(tr::editor::tabs::audio::use_for_new()), ) .child( ui::Toggle::plain(&theme, "audio-improve-default", enabled_by_default) @@ -1971,8 +2034,9 @@ impl EditorWindow { this.sidebar.audio_enhancement_default = next; this.sidebar.audio_enhancement_error = None; } else { - this.sidebar.audio_enhancement_error = - Some("Could not save the Studio Sound default".into()); + this.sidebar.audio_enhancement_error = Some( + tr::editor::tabs::audio::studio_sound_save_failed().into(), + ); } cx.notify(); })), @@ -1998,7 +2062,7 @@ impl EditorWindow { let hidden = cursor.hide; let mut body = div().flex().flex_col().gap(px(14.)).child( - ui::Field::inline(&theme, "Show cursor").value( + ui::Field::inline(&theme, tr::editor::tabs::cursor::show()).value( ui::Toggle::plain(&theme, "cursor-show", !hidden) .on_click(cx.listener(move |this, _, window, cx| { this.edit_project("cursor-hide", window, cx, move |p| { @@ -2022,14 +2086,24 @@ impl EditorWindow { body = body .child( - ui::Field::stacked(&theme, "Cursor Style") + ui::Field::stacked(&theme, tr::editor::tabs::cursor::style_label()) .child(self.render_cursor_style_picker(cx)), ) - .child(self.slider_field("Size", SliderKey::Cursor(CursorSlider::Size), "", cx)) - .child(self.slider_field("Tilt", SliderKey::Cursor(CursorSlider::Tilt), "x100%", cx)) + .child(self.slider_field( + tr::editor::tabs::cursor::size_label(), + SliderKey::Cursor(CursorSlider::Size), + "", + cx, + )) + .child(self.slider_field( + tr::editor::tabs::cursor::tilt(), + SliderKey::Cursor(CursorSlider::Tilt), + "x100%", + cx, + )) .child(self.render_cursor_ripple(cx)) .child( - ui::Field::inline(&theme, "Hide When Idle").value( + ui::Field::inline(&theme, tr::editor::tabs::cursor::hide_when_idle()).value( ui::Toggle::plain(&theme, "cursor-idle", idle) .on_click(cx.listener(move |this, _, window, cx| { this.edit_project("cursor-idle", window, cx, move |p| { @@ -2042,11 +2116,11 @@ impl EditorWindow { ); if idle { - // `` + // `` // with a `w-12 text-right` readout beside the slider (`:945-967`). let readout = format!("{:.1}s", cursor.hide_when_idle_delay); body = body.child( - ui::Subfield::plain(&theme, "Inactivity Delay") + ui::Subfield::plain(&theme, tr::editor::tabs::cursor::inactivity_delay()) .gap(px(16.)) .child( div() @@ -2074,14 +2148,14 @@ impl EditorWindow { let smooth = !cursor.raw; let mut body = body .child( - ui::Field::section(&theme, "Cursor Movement Style").child( + ui::Field::section(&theme, tr::editor::tabs::cursor::movement_style()).child( ui::RadioCards::plain( &theme, "cursor-style", CURSOR_STYLES .iter() .map(|(_, label, description)| { - ui::RadioCard::new(*label, Some(description)) + ui::RadioCard::new(label(), Some(description())) }) .collect(), style_index, @@ -2112,18 +2186,19 @@ impl EditorWindow { .flex() .flex_col() .child( - ui::Field::inline(&theme, "Smooth Movement").value( - ui::Toggle::plain(&theme, "cursor-smooth", smooth) - .on_click(cx.listener(move |this, _, window, cx| { - this.sidebar.cursor_physics_open.set_open(!smooth); - this.animate_collapsibles(window, cx); - this.edit_project("cursor-raw", window, cx, move |p| { - p.cursor.raw = smooth; - true - }); - })) - .into_any_element(), - ), + ui::Field::inline(&theme, tr::editor::tabs::cursor::smooth_movement()) + .value( + ui::Toggle::plain(&theme, "cursor-smooth", smooth) + .on_click(cx.listener(move |this, _, window, cx| { + this.sidebar.cursor_physics_open.set_open(!smooth); + this.animate_collapsibles(window, cx); + this.edit_project("cursor-raw", window, cx, move |p| { + p.cursor.raw = smooth; + true + }); + })) + .into_any_element(), + ), ) .child(collapsible( &self.sidebar.cursor_physics_open, @@ -2134,19 +2209,19 @@ impl EditorWindow { .pt(px(4.)) .pb(px(8.)) .child(self.slider_field( - "Tension", + tr::editor::tabs::cursor::tension(), SliderKey::Cursor(CursorSlider::Tension), "", cx, )) .child(self.slider_field( - "Friction", + tr::editor::tabs::cursor::friction(), SliderKey::Cursor(CursorSlider::Friction), "", cx, )) .child(self.slider_field( - "Mass", + tr::editor::tabs::cursor::mass(), SliderKey::Cursor(CursorSlider::Mass), "", cx, @@ -2159,7 +2234,7 @@ impl EditorWindow { // showing a setting the renderer is already overriding. if cursor.cursor_type().family().is_none() { body = body.child( - ui::Field::inline(&theme, "High Quality SVG Cursors").value( + ui::Field::inline(&theme, tr::editor::tabs::cursor::svg_cursors()).value( ui::Toggle::plain(&theme, "cursor-svg", cursor.use_svg) .on_click(cx.listener(|this, _, window, cx| { let next = !this.style_control_project().cursor.use_svg; @@ -2183,13 +2258,14 @@ impl EditorWindow { return; } let Some(timeline) = self.project.timeline.clone() else { - self.keyboard_generation_message = Some("The project timeline is unavailable.".into()); + self.keyboard_generation_message = + Some(tr::editor::tabs::keyboard::timeline_unavailable().into()); cx.notify(); return; }; let Ok(timeline_fingerprint) = serde_json::to_vec(&timeline) else { self.keyboard_generation_message = - Some("The project timeline could not be read.".into()); + Some(tr::editor::tabs::keyboard::timeline_unreadable().into()); cx.notify(); return; }; @@ -2206,7 +2282,7 @@ impl EditorWindow { .background_executor() .spawn(async move { let meta = RecordingMeta::load_for_project(&path) - .map_err(|error| format!("Failed to load recording data: {error}"))?; + .map_err(tr::editor::tabs::keyboard::load_failed)?; cap_project::generate_project_keyboard_segments(&meta, &timeline, &settings) }) .await; @@ -2233,10 +2309,8 @@ impl EditorWindow { keyboard_generation_settings_fingerprint(&keyboard_settings(&this.project)) == settings_fingerprint; if !timeline_unchanged || !settings_unchanged { - this.keyboard_generation_message = Some( - "The timeline or keyboard settings changed during generation. Try again." - .into(), - ); + this.keyboard_generation_message = + Some(tr::editor::tabs::keyboard::changed_during_generation().into()); cx.notify(); window.refresh(); return; @@ -2249,7 +2323,7 @@ impl EditorWindow { .is_none_or(|timeline| timeline.keyboard_segments.is_empty()); if !has_segments && already_empty { this.keyboard_generation_message = - Some("No recorded keyboard presses were found.".into()); + Some(tr::editor::tabs::keyboard::no_presses().into()); cx.notify(); window.refresh(); return; @@ -2278,7 +2352,7 @@ impl EditorWindow { this.set_selection(None, cx); } this.keyboard_generation_message = - Some("No recorded keyboard presses were found.".into()); + Some(tr::editor::tabs::keyboard::no_presses().into()); cx.notify(); window.refresh(); } @@ -2302,11 +2376,13 @@ impl EditorWindow { let position_label = KEYBOARD_POSITIONS .iter() .find(|(value, _)| *value == settings.position) - .map_or("Bottom Center", |(_, label)| *label); + .map_or(tr::editor::tabs::position::bottom_center(), |(_, label)| { + label() + }); let weight_label = TEXT_WEIGHTS .iter() .find(|(weight, _)| *weight == settings.font_weight) - .map_or("Normal", |(_, label)| *label); + .map_or(tr::editor::tabs::weight::normal(), |(_, label)| label()); // `class={cx("space-y-4", !enabled && "opacity-50 pointer-events-none")}` let body = div() @@ -2315,13 +2391,13 @@ impl EditorWindow { .gap(px(16.)) .when(!enabled, |this| this.opacity(0.5)) .child( - ui::Field::section(&theme, "Font Settings").child( + ui::Field::section(&theme, tr::editor::tabs::keyboard::font_settings()).child( div() .flex() .flex_col() .gap(px(12.)) .child(self.labelled_inline( - "Font Family", + tr::editor::tabs::keyboard::font_family(), self.menu_select( SidebarMenu::KeyboardFont, "keyboard-font", @@ -2331,7 +2407,7 @@ impl EditorWindow { )) .child( self.slider_field( - "Size", + tr::editor::tabs::keyboard::size_label(), SliderKey::Keyboard(KeyboardSlider::Size), "", cx, @@ -2339,7 +2415,7 @@ impl EditorWindow { .into_any_element(), ) .child(self.labelled( - "Text Color", + tr::editor::tabs::keyboard::text_color(), self.render_hex_field( crate::editor_panels::FieldKey::KeyboardColor, &settings.color, @@ -2349,66 +2425,71 @@ impl EditorWindow { ), ) .child( - ui::Field::section(&theme, "Background Settings").child( - div() - .flex() - .flex_col() - .gap(px(12.)) - .child(self.labelled( - "Background Color", - self.render_hex_field( - crate::editor_panels::FieldKey::KeyboardBackground, - &settings.background_color, - cx, + ui::Field::section(&theme, tr::editor::tabs::keyboard::background_settings()) + .child( + div() + .flex() + .flex_col() + .gap(px(12.)) + .child(self.labelled( + tr::editor::tabs::keyboard::background_color(), + self.render_hex_field( + crate::editor_panels::FieldKey::KeyboardBackground, + &settings.background_color, + cx, + ), + )) + .child( + self.slider_field( + tr::editor::tabs::keyboard::background_opacity(), + SliderKey::Keyboard(KeyboardSlider::BackgroundOpacity), + "", + cx, + ) + .into_any_element(), ), - )) - .child( - self.slider_field( - "Background Opacity", - SliderKey::Keyboard(KeyboardSlider::BackgroundOpacity), - "", - cx, - ) - .into_any_element(), - ), - ), + ), ) .child( - ui::Field::inline(&theme, "Position").child(self.menu_select( - SidebarMenu::KeyboardPosition, - "keyboard-position", - position_label, - cx, - )), + ui::Field::inline(&theme, tr::editor::tabs::keyboard::position()).child( + self.menu_select( + SidebarMenu::KeyboardPosition, + "keyboard-position", + position_label, + cx, + ), + ), ) .child( - ui::Field::inline(&theme, "Font Weight").child(self.menu_select( - SidebarMenu::KeyboardWeight, - "keyboard-weight", - weight_label, - cx, - )), + ui::Field::inline(&theme, tr::editor::tabs::keyboard::font_weight()).child( + self.menu_select( + SidebarMenu::KeyboardWeight, + "keyboard-weight", + weight_label, + cx, + ), + ), ) .child( - ui::Field::section(&theme, "Animation").child( + ui::Field::section(&theme, tr::editor::tabs::keyboard::animation()).child( div() .flex() .flex_col() .gap(px(12.)) .child(self.labelled_readout( - "Fade Duration", + tr::editor::tabs::keyboard::fade_duration(), SliderKey::Keyboard(KeyboardSlider::FadeDuration), format!("{:.0}ms", settings.fade_duration * 1000.), cx, )) .child(self.labelled_readout( - "Linger Duration", + tr::editor::tabs::keyboard::linger_duration(), SliderKey::Keyboard(KeyboardSlider::LingerDuration), format!("{:.1}s", settings.linger_duration), cx, )) .child(self.labelled_readout( - "Grouping Threshold", + tr::editor::tabs::keyboard::grouping_threshold(), SliderKey::Keyboard(KeyboardSlider::GroupingThreshold), format!("{:.0}ms", settings.grouping_threshold_ms), cx, @@ -2416,13 +2497,17 @@ impl EditorWindow { ), ) .child( - ui::Field::section(&theme, "Behavior").child( + ui::Field::section(&theme, tr::editor::tabs::keyboard::behavior()).child( div() .flex() .flex_col() .gap(px(12.)) .child( - ui::Subfield::plain(&theme, "Show Modifiers").child( + ui::Subfield::plain( + &theme, + tr::editor::tabs::keyboard::show_modifiers(), + ) + .child( ui::Toggle::plain( &theme, "keyboard-modifiers", @@ -2442,7 +2527,11 @@ impl EditorWindow { ), ) .child( - ui::Subfield::plain(&theme, "Show Special Keys").child( + ui::Subfield::plain( + &theme, + tr::editor::tabs::keyboard::show_special_keys(), + ) + .child( ui::Toggle::plain( &theme, "keyboard-special", @@ -2463,18 +2552,25 @@ impl EditorWindow { ), ) .child( - ui::Subfield::plain(&theme, "Uppercase").child( - ui::Toggle::plain(&theme, "keyboard-uppercase", settings.uppercase) - .on_click(cx.listener(|this, _, window, cx| { - let next = !keyboard_settings(&this.project).uppercase; - this.set_keyboard_setting( - "keyboard-uppercase", - window, - cx, - move |settings| settings.uppercase = next, - ); - })), - ), + ui::Subfield::plain(&theme, tr::editor::tabs::keyboard::uppercase()) + .child( + ui::Toggle::plain( + &theme, + "keyboard-uppercase", + settings.uppercase, + ) + .on_click(cx.listener( + |this, _, window, cx| { + let next = !keyboard_settings(&this.project).uppercase; + this.set_keyboard_setting( + "keyboard-uppercase", + window, + cx, + move |settings| settings.uppercase = next, + ); + }, + )), + ), ), ), ) @@ -2487,11 +2583,11 @@ impl EditorWindow { ui::ButtonSize::Md, ) .label(if self.generating_keyboard { - "Generating Keyboard Segments..." + tr::editor::tabs::keyboard::generating() } else if has_segments { - "Regenerate Keyboard Segments" + tr::editor::tabs::keyboard::regenerate() } else { - "Generate Keyboard Segments" + tr::editor::tabs::keyboard::generate() }) .full_width() .disabled(self.generating_keyboard || self.project.timeline.is_none()) @@ -2518,24 +2614,21 @@ impl EditorWindow { div() .text_size(px(14.)) .text_color(Hsla::from(theme.gray_11)) - .child("No keyboard segments yet."), + .child(tr::editor::tabs::keyboard::empty_title()), ) .child( div() .text_size(px(12.)) .text_color(Hsla::from(theme.gray_10)) - .child( - "Click \"Generate Keyboard Segments\" to create segments from \ - recorded keyboard presses.", - ), + .child(tr::editor::tabs::keyboard::empty_description()), ) .into_any_element() })); // The whole tab is one `Field` with the master toggle in its header and // a `Beta` badge (`KeyboardTab.tsx:128-135`). - ui::Field::section(&theme, "Show keyboard") - .badge("Beta") + ui::Field::section(&theme, tr::editor::tabs::keyboard::title()) + .badge(tr::editor::tabs::keyboard::beta()) .value( ui::Toggle::plain(&theme, "keyboard-enabled", enabled) .on_click(cx.listener(move |this, _, window, cx| { @@ -2601,24 +2694,35 @@ impl EditorWindow { let language_label = CAPTION_LANGUAGES .iter() .find(|(code, _)| *code == self.sidebar.caption_language) - .map_or("Auto Detect", |(_, label)| *label); + .map_or( + tr::editor::tabs::captions::language::auto(), + |(_, label)| label(), + ); let font_label = settings.font.clone(); let position_label = CAPTION_POSITIONS .iter() .find(|(value, _)| *value == settings.position) - .map_or("Bottom Center", |(_, label)| *label); + .map_or(tr::editor::tabs::position::bottom_center(), |(_, label)| { + label() + }); let animation_label = CAPTION_ANIMATIONS .iter() .find(|(value, _)| *value == settings.animation) - .map_or("Bounce", |(_, label)| *label); + .map_or( + tr::editor::tabs::captions::animation::bounce(), + |(_, label)| label(), + ); let highlight_label = CAPTION_HIGHLIGHT_STYLES .iter() .find(|(value, _)| *value == settings.highlight_style) - .map_or("Color", |(_, label)| *label); + .map_or( + tr::editor::tabs::captions::highlight::color(), + |(_, label)| label(), + ); let weight_label = TEXT_WEIGHTS .iter() .find(|(weight, _)| *weight == settings.font_weight) - .map_or("Bold", |(_, label)| *label); + .map_or(tr::editor::tabs::weight::bold(), |(_, label)| label()); let model_downloaded = snapshot.downloaded.contains(model.name); let active_download = snapshot @@ -2631,7 +2735,7 @@ impl EditorWindow { }); let downloading_label = active_download .and_then(|download| models.iter().find(|entry| entry.name == download.model)) - .map_or(model.label, |entry| entry.label); + .map_or((model.label)(), |entry| (entry.label)()); let download_message = active_download .map(|download| download.message.clone()) .filter(|message| !message.is_empty()); @@ -2680,14 +2784,14 @@ impl EditorWindow { .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.gray_12)) .truncate() - .child(model.label), + .child((model.label)()), ) .child( div() .text_size(px(12.)) .text_color(Hsla::from(theme.gray_11)) .truncate() - .child(model.description), + .child((model.description)()), ), ) .child( @@ -2727,11 +2831,11 @@ impl EditorWindow { ui::ButtonSize::Md, ) .label(if generating { - "Generating..." + tr::editor::tabs::captions::generating() } else if has_captions { - "Regenerate Captions" + tr::editor::tabs::captions::regenerate() } else { - "Generate Captions" + tr::editor::tabs::captions::generate() }) .full_width() .disabled(generating || any_deleting) @@ -2766,10 +2870,11 @@ impl EditorWindow { .text_size(px(12.)) .text_color(Hsla::from(theme.gray_10)) .truncate() - .child(SharedString::from(format!( - "{} model downloaded", - model.label - ))), + .child(SharedString::from( + tr::editor::tabs::captions::model_downloaded( + (model.label)(), + ), + )), ), ) .child( @@ -2780,7 +2885,11 @@ impl EditorWindow { ui::ButtonSize::Sm, ) .icon("icons/trash.svg") - .label(if deleting { "Deleting..." } else { "Delete" }) + .label(if deleting { + tr::editor::tabs::captions::deleting() + } else { + tr::editor::tabs::captions::delete() + }) .disabled(generating || is_downloading || deleting) .on_click(cx.listener(|this, _, _window, cx| { this.delete_caption_model(cx); @@ -2809,13 +2918,14 @@ impl EditorWindow { this.start_caption_model_download(cx); })); let download_button = if is_downloading { - download_button.label(format!( - "Downloading {downloading_label}... {download_percent}%" + download_button.label(tr::editor::tabs::captions::downloading( + downloading_label, + download_percent, )) } else { download_button .icon("icons/download.svg") - .label(format!("Download {} Model", model.label)) + .label(tr::editor::tabs::captions::download((model.label)())) }; let mut column = div().flex().flex_col().gap(px(8.)).child(download_button); @@ -2841,9 +2951,7 @@ impl EditorWindow { .text_size(px(12.)) .text_color(Hsla::from(theme.gray_10)) .child(download_message.unwrap_or_else(|| { - "Keep Cap open while the model downloads. Editor reloads will \ - reconnect automatically." - .to_string() + tr::editor::tabs::captions::keep_open().to_string() })), ); } @@ -2863,32 +2971,30 @@ impl EditorWindow { .flex_col() .gap(px(16.)) .child( - ui::Subfield::plain(&theme, "Model").child(div().w(px(220.)).child(model_trigger)), + ui::Subfield::plain(&theme, tr::editor::tabs::captions::model_label()) + .child(div().w(px(220.)).child(model_trigger)), ) .children((!transcription::supports_parakeet()).then(|| { div() .text_size(px(12.)) .text_color(Hsla::from(theme.gray_10)) - .child( - "Parakeet caption models are unavailable on Intel Macs. Whisper models \ - remain available.", - ) + .child(tr::editor::tabs::captions::intel_unavailable()) })) .child( div() .text_size(px(12.)) .text_color(Hsla::from(theme.gray_10)) - .child("One time download to your system. All captions are stored locally."), + .child(tr::editor::tabs::captions::download_note()), ) .child( - ui::Subfield::plain(&theme, "Language").child(div().w(px(200.)).child( - self.menu_select( + ui::Subfield::plain(&theme, tr::editor::tabs::captions::language_label()).child( + div().w(px(200.)).child(self.menu_select( SidebarMenu::CaptionLanguage, "caption-language", language_label, cx, - ), - )), + )), + ), ) .child(div().pt(px(8.)).child(action)); @@ -2898,17 +3004,17 @@ impl EditorWindow { .gap(px(16.)) .when(!has_captions, |this| this.opacity(0.5)) .child( - ui::Field::section(&theme, "Style") + ui::Field::section(&theme, tr::editor::tabs::captions::style()) .child(self.render_caption_presets(&settings, cx)), ) .child( - ui::Field::section(&theme, "Font Settings").child( + ui::Field::section(&theme, tr::editor::tabs::captions::font_settings()).child( div() .flex() .flex_col() .gap(px(12.)) .child(self.labelled_inline( - "Font Family", + tr::editor::tabs::captions::font_family(), self.menu_select( SidebarMenu::CaptionFont, "caption-font", @@ -2918,7 +3024,7 @@ impl EditorWindow { )) .child( self.slider_field( - "Size", + tr::editor::tabs::captions::size_label(), SliderKey::Caption(CaptionSlider::Size), "", cx, @@ -2926,18 +3032,25 @@ impl EditorWindow { .into_any_element(), ) .child( - ui::Subfield::plain(&theme, "Uppercase").child( - ui::Toggle::plain(&theme, "caption-uppercase", settings.uppercase) - .on_click(cx.listener(|this, _, window, cx| { - let next = !caption_settings(&this.project).uppercase; - this.set_caption_setting( - "caption-uppercase", - window, - cx, - move |settings| settings.uppercase = next, - ); - })), - ), + ui::Subfield::plain(&theme, tr::editor::tabs::captions::uppercase()) + .child( + ui::Toggle::plain( + &theme, + "caption-uppercase", + settings.uppercase, + ) + .on_click(cx.listener( + |this, _, window, cx| { + let next = !caption_settings(&this.project).uppercase; + this.set_caption_setting( + "caption-uppercase", + window, + cx, + move |settings| settings.uppercase = next, + ); + }, + )), + ), ) .child( div() @@ -2945,7 +3058,11 @@ impl EditorWindow { .flex_col() .gap(px(8.)) .child( - ui::Subfield::plain(&theme, "Active Word Highlight").child( + ui::Subfield::plain( + &theme, + tr::editor::tabs::captions::active_word_highlight(), + ) + .child( ui::Toggle::plain( &theme, "caption-active-word", @@ -2971,17 +3088,12 @@ impl EditorWindow { div() .text_size(px(12.)) .text_color(Hsla::from(theme.gray_10)) - .child( - "This is the first version of captions in Cap. \ - Active word highlighting may be inaccurate in \ - some situations. We're working on a fix for this \ - and it will be released in upcoming versions.", - ), + .child(tr::editor::tabs::captions::highlight_note()), ), ) .children(settings.active_word_highlight.then(|| { self.labelled_inline( - "Highlight Style", + tr::editor::tabs::captions::highlight_style(), self.menu_select( SidebarMenu::CaptionHighlightStyle, "caption-highlight-style", @@ -2991,7 +3103,7 @@ impl EditorWindow { ) })) .child(self.labelled( - "Text Color", + tr::editor::tabs::captions::text_color(), self.render_hex_field( crate::editor_panels::FieldKey::CaptionColor, &settings.color, @@ -3001,37 +3113,40 @@ impl EditorWindow { ), ) .child( - ui::Field::section(&theme, "Background Settings").child( - div() - .flex() - .flex_col() - .gap(px(12.)) - .child(self.labelled( - "Background Color", - self.render_hex_field( - crate::editor_panels::FieldKey::CaptionBackground, - &settings.background_color, - cx, + ui::Field::section(&theme, tr::editor::tabs::captions::background_settings()) + .child( + div() + .flex() + .flex_col() + .gap(px(12.)) + .child(self.labelled( + tr::editor::tabs::captions::background_color(), + self.render_hex_field( + crate::editor_panels::FieldKey::CaptionBackground, + &settings.background_color, + cx, + ), + )) + .child( + self.slider_field( + tr::editor::tabs::captions::background_opacity(), + SliderKey::Caption(CaptionSlider::BackgroundOpacity), + "", + cx, + ) + .into_any_element(), ), - )) - .child( - self.slider_field( - "Background Opacity", - SliderKey::Caption(CaptionSlider::BackgroundOpacity), - "", - cx, - ) - .into_any_element(), - ), - ), + ), ) .child( - ui::Field::inline(&theme, "Position").child(self.menu_select( - SidebarMenu::CaptionPosition, - "caption-position", - position_label, - cx, - )), + ui::Field::inline(&theme, tr::editor::tabs::captions::position()).child( + self.menu_select( + SidebarMenu::CaptionPosition, + "caption-position", + position_label, + cx, + ), + ), ) .children((settings.position == "manual").then(|| { div() @@ -3040,7 +3155,7 @@ impl EditorWindow { .gap(px(12.)) .child( self.slider_field( - "Horizontal Position", + tr::editor::tabs::captions::horizontal_position(), SliderKey::Caption(CaptionSlider::ManualX), "%", cx, @@ -3049,7 +3164,7 @@ impl EditorWindow { ) .child( self.slider_field( - "Vertical Position", + tr::editor::tabs::captions::vertical_position(), SliderKey::Caption(CaptionSlider::ManualY), "%", cx, @@ -3058,13 +3173,13 @@ impl EditorWindow { ) })) .child( - ui::Field::section(&theme, "Animation").child( + ui::Field::section(&theme, tr::editor::tabs::captions::animation_section()).child( div() .flex() .flex_col() .gap(px(12.)) .child(self.labelled_inline( - "Animation Style", + tr::editor::tabs::captions::animation_style(), self.menu_select( SidebarMenu::CaptionAnimation, "caption-animation", @@ -3073,7 +3188,7 @@ impl EditorWindow { ), )) .child(self.labelled( - "Highlight Color", + tr::editor::tabs::captions::highlight_color(), self.render_hex_field( crate::editor_panels::FieldKey::CaptionHighlight, &settings.highlight_color, @@ -3081,7 +3196,7 @@ impl EditorWindow { ), )) .child(self.labelled_readout( - "Fade Duration", + tr::editor::tabs::captions::fade_duration(), SliderKey::Caption(CaptionSlider::FadeDuration), format!("{:.0}ms", settings.fade_duration * 1000.), cx, @@ -3089,17 +3204,19 @@ impl EditorWindow { ), ) .child( - ui::Field::inline(&theme, "Font Weight").child(self.menu_select( - SidebarMenu::CaptionWeight, - "caption-weight", - weight_label, - cx, - )), + ui::Field::inline(&theme, tr::editor::tabs::captions::font_weight()).child( + self.menu_select( + SidebarMenu::CaptionWeight, + "caption-weight", + weight_label, + cx, + ), + ), ); // An extra flex ancestor here repeats intrinsic layout while scrolling. - ui::Field::section(&theme, "Captions") - .badge("Beta") + ui::Field::section(&theme, tr::editor::tabs::captions::title()) + .badge(tr::editor::tabs::captions::beta()) .child(transcription) .child(style.mt(px(8.))) .into_any_element() @@ -3155,10 +3272,10 @@ impl EditorWindow { .text_size(px(12.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.gray_12)) - .child(preset.label), + .child((preset.label)()), ) .tooltip({ - let description = SharedString::from(preset.description); + let description = SharedString::from((preset.description)()); move |_window, cx| ui::Tooltip::new(&theme, description.clone()).view(cx) }) .on_click(cx.listener(move |this, _, window, cx| { @@ -3433,9 +3550,8 @@ impl EditorWindow { tracing::error!("caption generation failed: {error}"); transcription::finish_generation( &project_path, - Some(format!( - "Failed to generate captions: {}", - transcription::caption_generation_error_message(&error) + Some(tr::editor::tabs::captions::generate_failed( + transcription::caption_generation_error_message(&error), )), ); let _ = this.update(cx, |_, cx| cx.notify()); @@ -3499,16 +3615,13 @@ impl EditorWindow { .text_size(px(12.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.editor.text_2)) - .child("Sync"), + .child(tr::editor::tabs::sync::title()), ) .child( div() .text_size(px(12.)) .text_color(Hsla::from(theme.editor.text_3)) - .child( - "Fine-tune source offsets if audio or camera drifts out of \ - sync with the screen recording.", - ), + .child(tr::editor::tabs::sync::description()), ), ) .children((0..clips).map(|clip| { @@ -3527,25 +3640,21 @@ impl EditorWindow { .text_size(px(12.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.editor.text_2)) - .child(SharedString::from(format!("Clip {clip}"))) + .child(SharedString::from(tr::editor::tabs::sync::clip(clip))) .into_any_element() })) .children(auto.then(|| { div() .text_size(px(12.)) .text_color(Hsla::from(theme.editor.text_3)) - .child( - "Cap calculated these offsets automatically to keep audio in \ - sync with the video. Adjust them if anything still sounds \ - off.", - ) + .child(tr::editor::tabs::sync::auto_description()) .into_any_element() })) .children(summary.has_system_audio.then(|| { self.render_offset_field( clip, OffsetKind::SystemAudio, - "System Audio Offset", + tr::editor::tabs::sync::system_audio(), auto, cx, ) @@ -3554,7 +3663,7 @@ impl EditorWindow { self.render_offset_field( clip, OffsetKind::Mic, - "Microphone Offset", + tr::editor::tabs::sync::microphone(), auto, cx, ) @@ -3563,7 +3672,7 @@ impl EditorWindow { self.render_offset_field( clip, OffsetKind::Camera, - "Camera Offset", + tr::editor::tabs::sync::camera(), auto, cx, ) @@ -3631,7 +3740,7 @@ impl EditorWindow { let mut field = ui::Field::section(&theme, name); if auto { - field = field.badge("Auto-synced"); + field = field.badge(tr::editor::tabs::sync::auto_synced()); } field diff --git a/apps/desktop-gpui/src/editor_timeline.rs b/apps/desktop-gpui/src/editor_timeline.rs index ad0f22557c5..8bd821d4e19 100644 --- a/apps/desktop-gpui/src/editor_timeline.rs +++ b/apps/desktop-gpui/src/editor_timeline.rs @@ -51,6 +51,7 @@ use gpui::{ use crate::{ editor_edits::{Selection, clip_is_muted}, + i18n::tr, theme::Theme, }; @@ -953,17 +954,17 @@ impl TrackKind { match self { // The clip row's gutter label is "Video", not the definition's // "Clip" (`TL/index.tsx:1334`). - Self::Clip => "Video", - Self::Style => "Style", - Self::Image => "Image", - Self::Caption => "Captions", - Self::Keyboard => "Keyboard", - Self::Text => "Text", - Self::Mask => "Mask", - Self::Audio => "Audio", - Self::Zoom => "Zoom", + Self::Clip => tr::editor::timeline::track::video(), + Self::Style => tr::editor::timeline::track::style(), + Self::Image => tr::editor::timeline::track::image(), + Self::Caption => tr::editor::timeline::track::captions(), + Self::Keyboard => tr::editor::timeline::track::keyboard(), + Self::Text => tr::editor::timeline::track::text_track(), + Self::Mask => tr::editor::timeline::track::mask(), + Self::Audio => tr::editor::timeline::track::audio(), + Self::Zoom => tr::editor::timeline::track::zoom(), Self::ThreeD => "3D", - Self::Scene => "Scene", + Self::Scene => tr::editor::timeline::track::scene(), } } @@ -1002,30 +1003,48 @@ impl TrackKind { pub fn picker_label(self) -> &'static str { match self { - Self::Clip => "Clip", + Self::Clip => tr::editor::timeline::track::clip(), other => other.label(), } } pub fn picker_description(self) -> &'static str { match self { - Self::Clip => "Your recorded screen footage.", - Self::Style => "Change background, camera and cursor settings over time.", - Self::Image => "Add an image to your recording.", - Self::Zoom => "Smooth zoom-ins that follow the action.", - Self::Caption => "Auto-transcribe your recording into on-screen subtitles.", - Self::Keyboard => "Display key presses on screen as you type.", - Self::Text => "Add custom text overlays and titles to the canvas.", - Self::Mask => "Blur or black out private areas of the screen.", - Self::Audio => "Add background music or import your own audio.", - Self::Scene => "Switch layouts between your screen and camera.", - Self::ThreeD => "Tilt the scene in 3D perspective.", + Self::Clip => tr::editor::timeline::picker::clip(), + Self::Style => tr::editor::timeline::picker::style(), + Self::Image => tr::editor::timeline::picker::image(), + Self::Zoom => tr::editor::timeline::picker::zoom(), + Self::Caption => tr::editor::timeline::picker::caption(), + Self::Keyboard => tr::editor::timeline::picker::keyboard(), + Self::Text => tr::editor::timeline::picker::text_overlay(), + Self::Mask => tr::editor::timeline::picker::mask(), + Self::Audio => tr::editor::timeline::picker::audio(), + Self::Scene => tr::editor::timeline::picker::scene(), + Self::ThreeD => tr::editor::timeline::picker::three_d(), + } + } + + /// What the add-track hint says over the tile of a single-lane track + /// that is already on the timeline. + pub fn picker_remove(self) -> &'static str { + match self { + Self::Clip => tr::editor::timeline::remove::clip(), + Self::Style => tr::editor::timeline::remove::style(), + Self::Image => tr::editor::timeline::remove::image(), + Self::Caption => tr::editor::timeline::remove::captions(), + Self::Keyboard => tr::editor::timeline::remove::keyboard(), + Self::Text => tr::editor::timeline::remove::text_track(), + Self::Mask => tr::editor::timeline::remove::mask(), + Self::Audio => tr::editor::timeline::remove::audio(), + Self::Zoom => tr::editor::timeline::remove::zoom(), + Self::ThreeD => tr::editor::timeline::remove::three_d(), + Self::Scene => tr::editor::timeline::remove::scene(), } } pub fn picker_unavailable(self) -> &'static str { match self { - Self::Scene => "Record with a camera to use scenes.", + Self::Scene => tr::editor::timeline::picker::scene_unavailable(), _ => "", } } @@ -1388,7 +1407,7 @@ impl TimelineModel { lane: segment.track, detail: SegmentDetail::Text { content: if segment.content.is_empty() { - SharedString::new_static("Label") + SharedString::new_static(tr::editor::timeline::segment::label()) } else { SharedString::from(segment.content.clone()) }, @@ -1413,8 +1432,8 @@ impl TimelineModel { lane: segment.track, detail: SegmentDetail::Mask { label: match segment.mask_type { - MaskKind::Sensitive => "Sensitive", - MaskKind::Highlight => "Highlight", + MaskKind::Sensitive => tr::editor::timeline::segment::sensitive(), + MaskKind::Highlight => tr::editor::timeline::segment::highlight(), }, }, }) @@ -1432,7 +1451,10 @@ impl TimelineModel { .name .clone() .filter(|name| !name.is_empty()) - .map_or_else(|| SharedString::new_static("Audio"), SharedString::from), + .map_or_else( + || SharedString::new_static(tr::editor::timeline::segment::audio()), + SharedString::from, + ), enabled: segment.enabled, fade_in: segment.fade_in, fade_out: segment.fade_out, @@ -1451,7 +1473,7 @@ impl TimelineModel { lane: 0, detail: SegmentDetail::Caption { text: if segment.text.is_empty() { - SharedString::new_static("Caption") + SharedString::new_static(tr::editor::timeline::segment::caption()) } else { SharedString::from(segment.text.clone()) }, @@ -1702,10 +1724,10 @@ fn clip_rows( // clips sidebar (`displayName`). name: match segment.name.as_deref().map(str::trim) { Some(name) if !name.is_empty() => SharedString::from(name.to_string()), - _ if multiple_clips => { - SharedString::from(format!("Clip {}", segment.recording_clip)) - } - _ => SharedString::new_static("Clip"), + _ if multiple_clips => SharedString::from( + tr::editor::timeline::segment::numbered_clip(segment.recording_clip), + ), + _ => SharedString::new_static(tr::editor::timeline::segment::clip()), }, source_start: segment.start, // The label shows `formatTime(seg.end - seg.start)` -- the @@ -1743,11 +1765,11 @@ fn parse_hex_color(value: &str) -> Option { /// `getSceneLabel` / `getSceneIcon` (`TL/SceneTrack.tsx:80-102`). fn scene_label(mode: SceneMode) -> &'static str { match mode { - SceneMode::CameraOnly => "Camera Only", - SceneMode::HideCamera => "Hide Camera", - SceneMode::SplitScreen => "Split Screen", - SceneMode::Floating => "Floating", - SceneMode::Default => "Default", + SceneMode::CameraOnly => tr::editor::timeline::scene::camera_only(), + SceneMode::HideCamera => tr::editor::timeline::scene::hide_camera(), + SceneMode::SplitScreen => tr::editor::timeline::scene::split_screen(), + SceneMode::Floating => tr::editor::timeline::scene::floating(), + SceneMode::Default => tr::editor::timeline::scene::default(), } } @@ -2478,12 +2500,12 @@ pub fn new_camera3d_ghost(model: &TimelineModel, preview: f64) -> Option<(f64, f /// nothing but the plus. fn camera3d_ghost_label(width: f32, duration: f64) -> Option { if width >= 208. { - Some(format!( - "Add shot \u{b7} {} \u{b7} {duration:.1}s", - crate::editor_panels::CAMERA3D_DEFAULT_LOOK_NAME + Some(tr::editor::timeline::add_shot_with_look( + crate::editor_panels::CAMERA3D_DEFAULT_LOOK_NAME, + format!("{duration:.1}"), )) } else if width >= 84. { - Some("Add shot".to_string()) + Some(tr::editor::timeline::add_shot().to_string()) } else { None } @@ -2639,18 +2661,18 @@ fn render_empty_track( ) -> Option { let (copy, action) = match kind { // `TL/CaptionsTrack.tsx:146-160`. - TrackKind::Caption => ("No captions", None), + TrackKind::Caption => (tr::editor::timeline::empty::captions(), None), // `TL/KeyboardTrack.tsx:146-153`. - TrackKind::Keyboard => ("No keyboard events", None), + TrackKind::Keyboard => (tr::editor::timeline::empty::keyboard(), None), // `TL/AudioTrack.tsx:400-427`, `TL/ThreeDTrack.tsx:319-337`, and the // scene lane, which had no empty state at all. TrackKind::Audio => ( - kind.picker_description().trim_end_matches('.'), - Some("Add audio"), + tr::editor::timeline::empty::audio(), + Some(tr::editor::timeline::empty::add_audio()), ), TrackKind::Scene => ( - kind.picker_description().trim_end_matches('.'), - Some("Add scene"), + tr::editor::timeline::empty::scene(), + Some(tr::editor::timeline::empty::add_scene()), ), _ => return None, }; @@ -3244,7 +3266,7 @@ fn render_hold( .text_size(px(10.)) .font_weight(FontWeight::MEDIUM) .text_color(ink) - .child("Paused"), + .child(tr::editor::timeline::paused()), ) }) } @@ -3488,14 +3510,19 @@ fn label_body( ) => label_row() .child(label_primary(theme, color).child(name.clone())) .when(*muted, |this| { - this.child(clip_badge(theme, color, "icons/volume-x.svg", "Muted")) + this.child(clip_badge( + theme, + color, + "icons/volume-x.svg", + tr::editor::timeline::segment::muted(), + )) }) .when(*cursor_hidden, |this| { this.child(clip_badge( theme, color, "icons/mouse-pointer-ban.svg", - "Cursor hidden", + tr::editor::timeline::segment::cursor_hidden(), )) }) .child(label_secondary(theme, color).child(format_clip_time(*source_duration))) @@ -3547,12 +3574,12 @@ fn label_body( // least 140px wide (`TL/ZoomTrack.tsx:700-704`). if visible_width >= 140. { if *automatic { - "Automatic Zoom" + tr::editor::timeline::segment::automatic_zoom() } else { - "Manual Zoom" + tr::editor::timeline::segment::manual_zoom() } } else { - "Zoom" + tr::editor::timeline::track::zoom() }, ))) .child(label_secondary(theme, color).child(format!("{amount:.1}x"))) @@ -3566,7 +3593,7 @@ fn label_body( // -- Scene (`TL/SceneTrack.tsx:543-566`) -------------------------- (SegmentDetail::Scene { mode }, LabelTier::Full) => label_row() - .child(label_primary(theme, color).child("Scene")) + .child(label_primary(theme, color).child(tr::editor::timeline::track::scene())) .child(label_secondary(theme, color).child(scene_label(*mode))) .into_any_element(), (SegmentDetail::Scene { mode }, LabelTier::Compact) => label_row() @@ -3638,7 +3665,7 @@ fn label_body( // -- Mask (`TL/MaskTrack.tsx:481-495`) ---------------------------- (SegmentDetail::Mask { label }, LabelTier::Full) => label_row() - .child(label_primary(theme, color).child("Mask")) + .child(label_primary(theme, color).child(tr::editor::timeline::track::mask())) .child(label_secondary(theme, color).child(*label)) .into_any_element(), (SegmentDetail::Mask { label }, LabelTier::Compact) => label_row() diff --git a/apps/desktop-gpui/src/editor_window.rs b/apps/desktop-gpui/src/editor_window.rs index 046a18b1f47..da55e82fcd7 100644 --- a/apps/desktop-gpui/src/editor_window.rs +++ b/apps/desktop-gpui/src/editor_window.rs @@ -67,6 +67,7 @@ use gpui::{ div, point, prelude::FluentBuilder, px, svg, }; +use crate::i18n::tr; use crate::theme::LocaleFont as _; use crate::{ editor_audio::{AudioImportRequest, probe_audio_duration, replace_audio_asset}, @@ -379,7 +380,7 @@ pub struct ProjectSummary { /// Blocking; callers run it on the background executor. pub fn preflight(path: &std::path::Path) -> Result { if !path.exists() { - return Err(format!("Video path {} not found!", path.display())); + return Err(tr::editor::window::open::not_found(path.display())); } // Repairs tracks older recorders stamped at the wrong rate, and display @@ -391,11 +392,11 @@ pub fn preflight(path: &std::path::Path) -> Result { Err(error) => tracing::warn!(?path, "Track heal check failed: {error:#}"), } - let meta = RecordingMeta::load_for_project(path) - .map_err(|error| format!("Failed to load recording meta: {error}"))?; + let meta = + RecordingMeta::load_for_project(path).map_err(tr::editor::window::open::meta_failed)?; let RecordingMetaInner::Studio(studio) = &meta.inner else { - return Err("Cannot edit non-studio recordings".to_string()); + return Err(tr::editor::window::open::not_studio().to_string()); }; let (segment_count, has_camera, multiple_recording_segments) = match studio.as_ref() { @@ -420,7 +421,7 @@ pub fn preflight(path: &std::path::Path) -> Result { }; if segment_count == 0 { - return Err("Recording has no segments. It may need to be recovered first.".to_string()); + return Err(tr::editor::window::open::no_segments().to_string()); } // The panicking call, contained. `AssertUnwindSafe` because neither @@ -430,10 +431,8 @@ pub fn preflight(path: &std::path::Path) -> Result { let recordings = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { ProjectRecordingsMeta::new(&owned_path, studio.as_ref()) })) - .map_err(|_| { - "This recording's video tracks could not be opened. The bundle looks damaged.".to_string() - })? - .map_err(|error| format!("Failed to read this recording's media: {error}"))?; + .map_err(|_| tr::editor::window::open::damaged().to_string())? + .map_err(tr::editor::window::open::media_failed)?; let recordings = Arc::new(recordings); // `RecordingMeta::project_config()` loads `project-config.json` (falling @@ -508,9 +507,6 @@ pub fn preflight(path: &std::path::Path) -> Result { }) } -const NO_AUTO_ZOOM_CLICKS_MESSAGE: &str = - "No clicks found to zoom into. Drag across the lane to add one."; - fn load_recording_clicks( project_path: &std::path::Path, ) -> Vec> { @@ -1455,7 +1451,9 @@ pub struct EditorWindow { /// `isGeneratingAutoZoom` / `sessionDismissedGenerateZoomPrompt` /// (`TL/ZoomTrack.tsx:60-65`). generating_auto_zoom: bool, - auto_zoom_message: Option<&'static str>, + auto_zoom_message: Option &'static str>, + /// The language the timeline model's placeholder labels were built in. + timeline_locale: crate::i18n::Locale, zoom_prompt_dismissed: bool, hovering_generate_zoom: bool, pub(crate) generating_keyboard: bool, @@ -1938,7 +1936,10 @@ impl EditorWindow { return false; } cx.spawn(async move |_| { - crate::platform::alert_dialog("Recording retained", &message); + crate::platform::alert_dialog( + tr::editor::modal::recording_retained(), + &message, + ); }) .detach(); return false; @@ -1995,7 +1996,7 @@ impl EditorWindow { )); let preset_name_input = cx.new(|cx| { let mut input = ui::TextInputState::single_line(window, cx); - input.set_placeholder("Preset name"); + input.set_placeholder(tr::editor::window::presets::name_placeholder()); input }); text_events.push(cx.subscribe_in( @@ -2099,6 +2100,7 @@ impl EditorWindow { playback_tick: 0, generating_auto_zoom: false, auto_zoom_message: None, + timeline_locale: crate::i18n::current(), zoom_prompt_dismissed: false, hovering_generate_zoom: false, generating_keyboard: false, @@ -2197,12 +2199,10 @@ impl EditorWindow { pub(crate) fn flush_pending_saves(&mut self, cx: &mut Context) -> Result<(), String> { self.commit_pretty_name(cx)?; - self.pending_save.borrow_mut().try_flush().map_err(|error| { - format!( - "Could not save {}. Keep the editor open and try again: {error}", - self.project_path.display() - ) - }) + self.pending_save + .borrow_mut() + .try_flush() + .map_err(|error| tr::editor::window::save_failed(self.project_path.display(), error)) } /// Every lazily-created field's subscription lives here for the window's @@ -4787,7 +4787,7 @@ impl EditorWindow { }); let Some((preview, (start, end))) = ghost else { if kind == TrackKind::ThreeD { - self.show_notice(scenes::CAMERA3D_NO_ROOM, window, cx); + self.show_notice(tr::editor::window::no_room_for_3d_shot(), window, cx); } return; }; @@ -5560,11 +5560,7 @@ impl EditorWindow { fn close_stale_audio_picker(&mut self, window: &mut Window, cx: &mut Context) { self.audio_picker = None; self.audio_picker_request = None; - self.show_notice( - "This audio segment changed. Select it and choose a replacement again.", - window, - cx, - ); + self.show_notice(tr::editor::window::audio_segment_changed(), window, cx); } pub(crate) fn add_library_track( @@ -5677,7 +5673,7 @@ impl EditorWindow { cx.spawn_in(window, async move |this, cx| { #[cfg(target_os = "linux")] let source = crate::platform::open_file_panel_async( - &[("Audio", AUDIO_IMPORT_EXTENSIONS)], + &[(tr::editor::window::audio_filter(), AUDIO_IMPORT_EXTENSIONS)], None, cx, ) @@ -5765,7 +5761,7 @@ impl EditorWindow { }); if segments.is_empty() { tracing::info!("auto zoom produced no segments"); - this.auto_zoom_message = Some(NO_AUTO_ZOOM_CLICKS_MESSAGE); + this.auto_zoom_message = Some(tr::editor::window::zoom_lane::no_clicks); cx.notify(); return; } @@ -6583,6 +6579,23 @@ impl EditorWindow { self.theme.refresh(window, cx, false); } + fn auto_zoom_prompt(&self) -> &'static str { + self.auto_zoom_message + .unwrap_or(tr::editor::window::zoom_lane::prompt)() + } + + /// Track, clip and segment placeholder names live in the timeline model, + /// so a language change rebuilds it. + fn sync_locale(&mut self) { + let locale = crate::i18n::current(); + if self.timeline_locale != locale { + self.timeline_locale = locale; + if self.project_ready() { + self.rebuild_timeline(); + } + } + } + /// `NameEditor`'s commit (`Header.tsx:303-318`), verbatim. /// /// Return and Escape both call `blur()` there, and it is `onBlur` that @@ -6603,8 +6616,8 @@ impl EditorWindow { tracing::error!(%error, "recording title was not saved"); cx.spawn(async move |_, _| { crate::platform::alert_dialog( - "Title not saved", - &format!("{error}\n\nYour title is still in the editor. Try saving it again."), + tr::editor::window::title::not_saved(), + &tr::editor::window::title::not_saved_message(error), ); }) .detach(); @@ -6644,19 +6657,11 @@ impl EditorWindow { // history is `createStoreHistory` over the *project* store alone // (`ED/context.ts:1920-1930`), so a rename is not undoable there either. let path = self.project_path.clone(); - let mut meta = RecordingMeta::load_for_project(&path).map_err(|error| { - format!( - "Could not read the recording title for {}: {error}", - path.display() - ) - })?; + let mut meta = RecordingMeta::load_for_project(&path) + .map_err(|error| tr::editor::window::title::read_failed(path.display(), error))?; meta.pretty_name = draft.clone(); - meta.save_for_project().map_err(|error| { - format!( - "Could not save the recording title for {}: {error}", - path.display() - ) - })?; + meta.save_for_project() + .map_err(|error| tr::editor::window::title::save_failed(path.display(), error))?; if let LoadState::Ready(summary) = &mut self.state { summary.pretty_name = draft.clone(); } @@ -6912,7 +6917,14 @@ impl EditorWindow { ui::EditorButton::plain(&theme, id) .left_icon(icon) .disabled(!enabled) - .tooltip(&theme, if undo { "Undo" } else { "Redo" }) + .tooltip( + &theme, + if undo { + tr::editor::window::undo() + } else { + tr::editor::window::redo() + }, + ) .on_click(cx.listener(move |this, _, window, cx| { if !(this.history.can_undo() || this.history.can_redo() || this.selection.is_some()) { @@ -6933,31 +6945,47 @@ impl EditorWindow { fn aspect_ratio_label(aspect: Option<&cap_project::AspectRatio>) -> &'static str { match aspect { - None => "Auto", - Some(cap_project::AspectRatio::Wide) => "Wide", - Some(cap_project::AspectRatio::Vertical) => "Vertical", - Some(cap_project::AspectRatio::Square) => "Square", - Some(cap_project::AspectRatio::Classic) => "Classic", - Some(cap_project::AspectRatio::Tall) => "Tall", + None => tr::editor::window::aspect::auto(), + Some(cap_project::AspectRatio::Wide) => tr::editor::window::aspect::wide(), + Some(cap_project::AspectRatio::Vertical) => tr::editor::window::aspect::vertical(), + Some(cap_project::AspectRatio::Square) => tr::editor::window::aspect::square(), + Some(cap_project::AspectRatio::Classic) => tr::editor::window::aspect::classic(), + Some(cap_project::AspectRatio::Tall) => tr::editor::window::aspect::tall(), } } fn toolbar_menu_items(&self, kind: ToolbarMenu) -> Vec { match kind { ToolbarMenu::AspectRatio => { - const OPTIONS: &[(Option, &str)] = &[ - (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"), + type Label = fn() -> &'static str; + const OPTIONS: &[(Option, Label)] = &[ + (None, tr::editor::window::aspect::auto), + ( + Some(cap_project::AspectRatio::Wide), + tr::editor::window::aspect::wide_ratio, + ), + ( + Some(cap_project::AspectRatio::Vertical), + tr::editor::window::aspect::vertical_ratio, + ), + ( + Some(cap_project::AspectRatio::Square), + tr::editor::window::aspect::square_ratio, + ), + ( + Some(cap_project::AspectRatio::Classic), + tr::editor::window::aspect::classic_ratio, + ), + ( + Some(cap_project::AspectRatio::Tall), + tr::editor::window::aspect::tall_ratio, + ), ]; OPTIONS .iter() .map(|(value, label)| { ui::MenuItem::new( - *label, + label(), aspect_ratio_eq(value, &self.project.aspect_ratio), ) }) @@ -7111,11 +7139,9 @@ impl EditorWindow { .as_ref() .is_some_and(|export| export.phase.is_busy()) { - Some( - "Wait for the export or its cancellation to finish before closing or deleting this recording.", - ) + Some(tr::editor::window::busy::export()) } else if self.clips.is_importing() { - Some("Wait for the clip import to finish before closing or deleting this recording.") + Some(tr::editor::window::busy::clip_import()) } else { None } @@ -7124,7 +7150,7 @@ impl EditorWindow { pub(crate) fn deletion_blocked_reason(&self) -> Option<&'static str> { self.busy_reason().or((self.instance.is_none() && !matches!(self.state, LoadState::Failed(_))) - .then_some("Wait for the recording to finish opening before deleting it.")) + .then(tr::editor::window::busy::opening)) } pub(crate) fn prepare_for_deletion( @@ -7137,7 +7163,7 @@ impl EditorWindow { self.pending_save .borrow_mut() .try_flush() - .map_err(|error| format!("Could not save the recording before deleting it: {error}"))?; + .map_err(tr::editor::window::busy::save_before_delete_failed)?; self.save_task = None; self.stop_playback(cx); self.export = None; @@ -7701,14 +7727,20 @@ impl EditorWindow { cx.spawn_in(window, async move |this, cx| { #[cfg(target_os = "linux")] let source = crate::platform::open_file_panel_async( - &[("Images", crate::import::OVERLAY_IMAGE_EXTENSIONS)], + &[( + tr::editor::window::image::filter(), + crate::import::OVERLAY_IMAGE_EXTENSIONS, + )], None, cx, ) .await; #[cfg(not(target_os = "linux"))] let source = crate::import::pick_import_file( - &[("Images", crate::import::OVERLAY_IMAGE_EXTENSIONS)], + &[( + tr::editor::window::image::filter(), + crate::import::OVERLAY_IMAGE_EXTENSIONS, + )], cx, ) .await; @@ -7731,14 +7763,23 @@ impl EditorWindow { match imported { Some(Ok(imported)) => { if let Some((index, fingerprint)) = replace { - if edits::replace_image_asset(&mut this.project, index, &fingerprint, imported.path, imported.name) { + if edits::replace_image_asset( + &mut this.project, + index, + &fingerprint, + imported.path, + imported.name, + ) { this.project_changed(window, cx); this.refresh_image_asset_status(); } else { - this.sidebar.image_import_error = Some("This image changed while choosing a replacement. Select it and try again.".into()); + this.sidebar.image_import_error = + Some(tr::editor::window::image::changed().into()); cx.notify(); } - } else { this.commit_image_import(lane, time, imported, window, cx); } + } else { + this.commit_image_import(lane, time, imported, window, cx); + } } Some(Err(error)) => { this.sidebar.image_import_error = Some(error); @@ -7762,27 +7803,21 @@ impl EditorWindow { cx: &mut Context, ) { if !edits::ensure_timeline(&mut self.project, &self.clip_display_durations) { - self.sidebar.image_import_error = Some( - "The recording is no longer available for this image. Please reopen the project." - .into(), - ); + self.sidebar.image_import_error = + Some(tr::editor::window::image::recording_unavailable().into()); cx.notify(); return; } let total = self.total_duration(); if total <= 0.0 { - self.sidebar.image_import_error = Some( - "The recording is no longer available for this image. Please reopen the project." - .into(), - ); + self.sidebar.image_import_error = + Some(tr::editor::window::image::recording_unavailable().into()); cx.notify(); return; } let Some(timeline) = self.project.timeline.as_ref() else { - self.sidebar.image_import_error = Some( - "The recording is no longer available for this image. Please reopen the project." - .into(), - ); + self.sidebar.image_import_error = + Some(tr::editor::window::image::recording_unavailable().into()); cx.notify(); return; }; @@ -8120,25 +8155,25 @@ impl EditorWindow { let count = indices.len(); let can_paste = self.text_style_clipboard.is_some(); let title: SharedString = if count > 1 { - format!("{count} text segments").into() + tr::editor::window::text_menu::segments(count).into() } else { let content = segment.content.trim(); if content.is_empty() { - "Text".into() + tr::editor::window::text_menu::untitled().into() } else { content.to_string().into() } }; let summary = crate::editor_clips::format_clip_duration(segment.end - segment.start); let paste_label: &'static str = if count > 1 { - "Paste style to all selected" + tr::editor::window::text_menu::paste_style_all() } else { - "Paste style" + tr::editor::window::text_menu::paste_style() }; let delete_label: &'static str = if count > 1 { - "Delete text segments" + tr::editor::window::text_menu::delete_segments() } else { - "Delete text segment" + tr::editor::window::text_menu::delete_segment() }; let divider = || div().h(px(1.)).bg(Hsla::from(theme.editor.line)); @@ -8203,7 +8238,7 @@ impl EditorWindow { &theme, "text-menu-copy-style", "icons/copy.svg", - "Copy style", + tr::editor::window::text_menu::copy_style(), ClipMenuTrailing::None, true, false, @@ -8225,7 +8260,9 @@ impl EditorWindow { if can_paste { ClipMenuTrailing::None } else { - ClipMenuTrailing::Hint("Copy a style first") + ClipMenuTrailing::Hint( + tr::editor::window::text_menu::copy_style_first(), + ) }, can_paste, false, @@ -8533,9 +8570,18 @@ impl EditorWindow { .map(|(_, input)| input); let speeds = [0.25, 0.5, 1.0, 1.5, 2.0, 4.0, 8.0]; let audio_modes = [ - (ClipSpeedAudioMode::Mute, "Mute"), - (ClipSpeedAudioMode::MaintainPitch, "Keep pitch"), - (ClipSpeedAudioMode::MatchSpeed, "Match speed"), + ( + ClipSpeedAudioMode::Mute, + tr::editor::window::clip_menu::mute(), + ), + ( + ClipSpeedAudioMode::MaintainPitch, + tr::editor::window::clip_menu::keep_pitch(), + ), + ( + ClipSpeedAudioMode::MatchSpeed, + tr::editor::window::clip_menu::match_speed(), + ), ]; let chip = |id: SharedString, selected: bool, label: String| { div() @@ -8630,7 +8676,10 @@ impl EditorWindow { .flex() .flex_col() .gap(px(4.)) - .child(clip_menu_section(&theme, "SPEED")) + .child(clip_menu_section( + &theme, + tr::editor::window::clip_menu::speed(), + )) .child(chip_row().children(speeds.into_iter().map(|speed| { let selected = (timescale - speed).abs() < 1e-6; chip( @@ -8650,9 +8699,12 @@ impl EditorWindow { .flex() .flex_col() .gap(px(4.)) - .child(clip_menu_section(&theme, "AUDIO")) + .child(clip_menu_section( + &theme, + tr::editor::window::clip_menu::audio(), + )) .child(self.slider_field_disabled( - "Volume", + tr::editor::window::clip_menu::volume(), crate::editor_sidebar::SliderKey::ClipVolume(index), "%", muted, @@ -8663,8 +8715,8 @@ impl EditorWindow { &theme, "clip-menu-mute", "icons/volume-x.svg", - "Mute clip", - "Silence this clip's audio", + tr::editor::window::clip_menu::mute_clip(), + tr::editor::window::clip_menu::mute_clip_description(), muted, ) .on_click(cx.listener(move |this, _, window, cx| { @@ -8695,14 +8747,17 @@ impl EditorWindow { .flex() .flex_col() .gap(px(4.)) - .child(clip_menu_section(&theme, "CURSOR")) + .child(clip_menu_section( + &theme, + tr::editor::window::clip_menu::cursor(), + )) .child( clip_menu_toggle_row( &theme, "clip-menu-hide-cursor", "icons/mouse-pointer-ban.svg", - "Hide cursor", - "Fades out while this clip plays", + tr::editor::window::clip_menu::hide_cursor(), + tr::editor::window::clip_menu::hide_cursor_description(), cursor_hidden, ) .on_click(cx.listener( @@ -8723,11 +8778,11 @@ impl EditorWindow { &theme, "clip-menu-split", "icons/scissors.svg", - "Split at playhead", + tr::editor::window::clip_menu::split(), if can_split { ClipMenuTrailing::Kbd("C") } else { - ClipMenuTrailing::Hint("Move the playhead into this clip") + ClipMenuTrailing::Hint(tr::editor::window::clip_menu::split_hint()) }, can_split, false, @@ -8743,7 +8798,7 @@ impl EditorWindow { &theme, "clip-menu-merge-previous", "icons/arrow-left-to-line.svg", - "Merge with previous clip", + tr::editor::window::clip_menu::merge_previous(), merge_previous.map_or(ClipMenuTrailing::None, |blocker| { ClipMenuTrailing::Hint(blocker.hint()) }), @@ -8766,7 +8821,7 @@ impl EditorWindow { &theme, "clip-menu-merge-next", "icons/arrow-right-to-line.svg", - "Merge with next clip", + tr::editor::window::clip_menu::merge_next(), merge_next.map_or(ClipMenuTrailing::None, |blocker| { ClipMenuTrailing::Hint(blocker.hint()) }), @@ -8792,7 +8847,7 @@ impl EditorWindow { &theme, "clip-menu-rename-row", "icons/pencil.svg", - "Rename clip", + tr::editor::window::clip_menu::rename(), ClipMenuTrailing::None, true, false, @@ -8809,11 +8864,11 @@ impl EditorWindow { &theme, "clip-menu-delete", "icons/trash.svg", - "Delete clip", + tr::editor::window::clip_menu::delete(), if can_delete { ClipMenuTrailing::Kbd("backspace") } else { - ClipMenuTrailing::Hint("Can't delete the only clip") + ClipMenuTrailing::Hint(tr::editor::window::clip_menu::delete_only_clip()) }, can_delete, true, @@ -8855,16 +8910,16 @@ impl EditorWindow { let theme = self.theme; let scene_available = self.scene_available(); let hint: SharedString = match menu.hovered { - None => "Choose a track to add to your timeline.".into(), + None => tr::editor::window::add_track::hint().into(), Some(kind) => { if kind == TrackKind::Scene && !scene_available { kind.picker_unavailable().into() } else if kind == TrackKind::ThreeD && self.tracks.is_active(kind) { // The 3D tile only shows and hides the lane; the shots on // it survive either way. - "Hide the 3D track.".into() + tr::editor::window::add_track::hide_3d().into() } else if !kind.supports_multiple() && self.tracks.is_active(kind) { - format!("Remove the {} track.", kind.picker_label()).into() + kind.picker_remove().into() } else { kind.picker_description().into() } @@ -9114,7 +9169,7 @@ impl EditorWindow { let mut rows: Vec = vec![self.render_preset_row( PresetRow::Default, - "Default".into(), + tr::editor::window::presets::default().into(), store.default.is_none(), cx, )]; @@ -9127,7 +9182,7 @@ impl EditorWindow { .h(px(32.)) .text_size(px(12.)) .text_color(Hsla::from(theme.editor.text_3)) - .child("No saved presets") + .child(tr::editor::window::presets::none_saved()) .into_any_element(), ); } else { @@ -9207,7 +9262,11 @@ impl EditorWindow { .hover(|this| this.bg(Hsla::from(theme.editor.ctl_hover))) .text_size(px(14.)) .text_color(Hsla::from(theme.editor.text_1)) - .child(div().flex_1().child("Create new preset")) + .child( + div() + .flex_1() + .child(tr::editor::window::presets::create_new()), + ) .child( svg() .path("icons/circle-plus.svg") @@ -9275,7 +9334,7 @@ impl EditorWindow { .text_size(px(11.)) .bg(Hsla::from(theme.editor.ctl)) .text_color(Hsla::from(theme.editor.text_2)) - .child("Default"), + .child(tr::editor::window::presets::default_badge()), ) }) .child( @@ -9369,20 +9428,26 @@ impl EditorWindow { .bg(Hsla::from(theme.editor.card)) .shadow(theme.editor.pop_shadow()) .child( - menu_row("preset-apply".into(), "Apply", false).on_click(cx.listener( - move |this, _, window, cx| match row { - PresetRow::Default => this.apply_default_preset(window, cx), - PresetRow::Saved(index) => this.apply_preset_at(index, window, cx), - }, - )), + menu_row( + "preset-apply".into(), + tr::editor::window::presets::apply(), + false, + ) + .on_click(cx.listener(move |this, _, window, cx| match row { + PresetRow::Default => this.apply_default_preset(window, cx), + PresetRow::Saved(index) => this.apply_preset_at(index, window, cx), + })), ) .when(row == PresetRow::Default, |this| { this.child( - menu_row("preset-clear-default".into(), "Set as default", false).on_click( - cx.listener(move |this, _, _window, cx| { - this.mutate_presets(cx, |store| store.clear_default()); - }), - ), + menu_row( + "preset-clear-default".into(), + tr::editor::window::presets::set_default(), + false, + ) + .on_click(cx.listener(move |this, _, _window, cx| { + this.mutate_presets(cx, |store| store.clear_default()); + })), ) }) .when_some( @@ -9392,29 +9457,49 @@ impl EditorWindow { }, |this, index| { this.child( - menu_row("preset-save".into(), "Save settings to preset", false).on_click( - cx.listener(move |this, _, _window, cx| { + menu_row( + "preset-save".into(), + tr::editor::window::presets::save_settings(), + false, + ) + .on_click(cx.listener( + move |this, _, _window, cx| { let config = crate::presets::preset_config(&this.project); this.mutate_presets(cx, |store| store.save_to(index, config)); - }), - ), + }, + )), ) .child( - menu_row("preset-default".into(), "Set as default", false).on_click( - cx.listener(move |this, _, _window, cx| { + menu_row( + "preset-default".into(), + tr::editor::window::presets::set_default(), + false, + ) + .on_click(cx.listener( + move |this, _, _window, cx| { this.mutate_presets(cx, |store| store.set_default(index)); - }), - ), + }, + )), ) .child( - menu_row("preset-rename".into(), "Rename", false).on_click(cx.listener( + menu_row( + "preset-rename".into(), + tr::editor::window::presets::rename(), + false, + ) + .on_click(cx.listener( move |this, _, window, cx| { this.open_preset_dialog(PresetDialog::Rename { index }, window, cx); }, )), ) .child( - menu_row("preset-delete".into(), "Delete", true).on_click(cx.listener( + menu_row( + "preset-delete".into(), + tr::editor::window::presets::delete(), + true, + ) + .on_click(cx.listener( move |this, _, window, cx| { this.open_preset_dialog(PresetDialog::Delete { index }, window, cx); }, @@ -9431,24 +9516,40 @@ impl EditorWindow { let dialog = self.preset_dialog?; let theme = self.theme; + self.preset_name_input.update(cx, |input, _| { + input.set_placeholder(tr::editor::window::presets::name_placeholder()) + }); let (title, primary, danger) = match dialog { - PresetDialog::Create { .. } => ("Create Preset", "Create", false), - PresetDialog::Rename { .. } => ("Rename Preset", "Rename", false), - PresetDialog::Delete { .. } => ("Delete Preset", "Delete", true), + PresetDialog::Create { .. } => ( + tr::editor::window::presets::create_title(), + tr::editor::window::presets::create(), + false, + ), + PresetDialog::Rename { .. } => ( + tr::editor::window::presets::rename_title(), + tr::editor::window::presets::rename(), + false, + ), + PresetDialog::Delete { .. } => ( + tr::editor::window::presets::delete_title(), + tr::editor::window::presets::delete(), + true, + ), }; let body: gpui::AnyElement = match dialog { PresetDialog::Delete { index } => div() .text_size(px(14.)) .text_color(Hsla::from(theme.editor.text_2)) - .child(SharedString::from(format!( - "Are you sure you want to delete \"{}\"?", - crate::presets::PresetsStore::load() - .presets - .get(index) - .map(|entry| entry.name.clone()) - .unwrap_or_default() - ))) + .child(SharedString::from( + tr::editor::window::presets::delete_confirm( + crate::presets::PresetsStore::load() + .presets + .get(index) + .map(|entry| entry.name.clone()) + .unwrap_or_default(), + ), + )) .into_any_element(), PresetDialog::Create { default } => div() .flex() @@ -9471,7 +9572,7 @@ impl EditorWindow { div() .text_size(px(14.)) .text_color(Hsla::from(theme.editor.text_1)) - .child("Set as default"), + .child(tr::editor::window::presets::set_default()), ) .child( ui::Toggle::plain(&theme, "preset-default", default) @@ -9557,7 +9658,7 @@ impl EditorWindow { ui::ButtonVariant::Gray, ui::ButtonSize::Md, ) - .label("Cancel") + .label(tr::common::cancel()) .on_click(cx.listener( |this, _, _window, cx| { this.preset_dialog = None; @@ -9765,7 +9866,7 @@ impl EditorWindow { .child( ui::EditorButton::plain(&theme, "open-recording-bundle") .left_icon("icons/folder.svg") - .tooltip(&theme, "Open recording bundle") + .tooltip(&theme, tr::editor::window::header::open_bundle()) .disabled(!self.visual_ready()) .on_click(cx.listener(|this, _, _window, cx| { this.open_recording_bundle(cx); @@ -9774,7 +9875,7 @@ impl EditorWindow { .child( ui::EditorButton::plain(&theme, "delete-recording") .left_icon("icons/trash.svg") - .tooltip(&theme, "Delete recording") + .tooltip(&theme, tr::editor::window::header::delete_recording()) .disabled(!self.visual_ready()) .on_click(cx.listener(|this, _, window, cx| { this.delete_recording(window, cx); @@ -9797,8 +9898,10 @@ impl EditorWindow { .child( ui::EditorButton::plain(&theme, "presets") .left_icon("icons/presets.svg") - .when(!compact, |button| button.label("Presets")) - .tooltip(&theme, "Presets") + .when(!compact, |button| { + button.label(tr::editor::window::header::presets()) + }) + .tooltip(&theme, tr::editor::window::header::presets()) .disabled(!self.project_ready()) .right_icon("icons/chevron-down.svg") .pressed(self.presets_menu.is_some()) @@ -9809,10 +9912,13 @@ impl EditorWindow { .child(self.render_clips_pill(compact, cx)) // ``: the button only exists // once a transcript with words does. - .children( - has_captions - .then(|| self.header_pill("icons/captions.svg", "Captions", compact)), - ) + .children(has_captions.then(|| { + self.header_pill( + "icons/captions.svg", + tr::editor::window::header::captions(), + compact, + ) + })) .child(div().w(px(6.)).flex_none()) .when(self.sharing.is_some(), |group| { group.child(self.render_reupload_button(cx)) @@ -9928,7 +10034,7 @@ impl EditorWindow { .flex_shrink_0() .text_color(gpui::white()), ) - .child("Export") + .child(tr::editor::window::header::export()) .when(self.project_ready(), |button| { button.on_click(cx.listener(|this, _, window, cx| { if this.project_ready() { @@ -10088,7 +10194,7 @@ impl EditorWindow { .text_size(px(12.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.editor.text_2)) - .child("Preview"), + .child(tr::editor::window::preview::label()), ) .child(self.render_preview_quality(cx)), ) @@ -10115,7 +10221,7 @@ impl EditorWindow { .as_ref(), )) }) - .tooltip(&theme, "Aspect Ratio") + .tooltip(&theme, tr::editor::window::aspect::tooltip()) .disabled(!self.project_ready()) .pressed( self.toolbar_menu @@ -10132,8 +10238,8 @@ impl EditorWindow { ui::EditorButton::plain(&theme, "crop") .text(&theme) .left_icon("icons/crop.svg") - .when(!narrow, |button| button.label("Crop")) - .tooltip(&theme, "Crop Video") + .when(!narrow, |button| button.label(tr::editor::window::crop())) + .tooltip(&theme, tr::editor::window::crop_tooltip()) .disabled(!self.project_ready()) .pressed(self.crop.is_some()) .on_click(cx.listener(|this, _, window, cx| { @@ -10192,7 +10298,17 @@ impl EditorWindow { .hover(|style| style.bg(Hsla::from(editor.ctl_hover))) } }) - .child(quality.label()) + .child(match quality { + crate::store::EditorPreviewQuality::Quarter => { + tr::editor::window::preview::quarter() + } + crate::store::EditorPreviewQuality::Half => { + tr::editor::window::preview::half() + } + crate::store::EditorPreviewQuality::Full => { + tr::editor::window::preview::full() + } + }) .when(self.project_ready(), |button| { button.on_click(cx.listener(move |this, _, _window, cx| { this.set_preview_quality(quality, cx); @@ -10271,7 +10387,7 @@ impl EditorWindow { .text_size(px(15.)) .font_weight(FontWeight::MEDIUM) .text_color(Hsla::from(theme.editor.text_1)) - .child("This recording could not be opened"), + .child(tr::editor::window::open::failed()), ) .child( div() @@ -10300,9 +10416,9 @@ impl EditorWindow { ) -> impl IntoElement { let theme = self.theme; let (label, key) = if factor > 1. { - ("Zoom out", "-") + (tr::editor::window::transport::zoom_out(), "-") } else { - ("Zoom in", "+") + (tr::editor::window::transport::zoom_in(), "+") }; let modifier = if cfg!(target_os = "macos") { "meta" @@ -10471,9 +10587,12 @@ impl EditorWindow { .flex() .tooltip_show_delay(ui::TOOLTIP_SHOW_DELAY) .tooltip(move |_window, cx| { - ui::Tooltip::new(&theme, "Play/Pause video") - .keys(["Space"]) - .view(cx) + ui::Tooltip::new( + &theme, + tr::editor::window::transport::play_pause(), + ) + .keys(["Space"]) + .view(cx) }) .child( ui::IconButton::new("transport-play", icon) @@ -10513,9 +10632,12 @@ impl EditorWindow { .id("transport-split-tooltip") .tooltip_show_delay(crate::ui::TOOLTIP_SHOW_DELAY) .tooltip(move |_, cx| { - ui::Tooltip::new(&theme, "Click a segment to split. Esc to finish.") - .keys(["S"]) - .view(cx) + ui::Tooltip::new( + &theme, + tr::editor::window::transport::split_tooltip(), + ) + .keys(["S"]) + .view(cx) }) .child( ui::Button::plain( @@ -10530,9 +10652,9 @@ impl EditorWindow { ) .icon("icons/scissors.svg") .label(if self.split_mode { - "Done splitting" + tr::editor::window::transport::done_splitting() } else { - "Split" + tr::editor::window::transport::split() }) .radius(px(7.)) .disabled(!self.project_ready()) @@ -10848,7 +10970,12 @@ impl EditorWindow { .flex_none() .text_color(Hsla::from(theme.editor.card)), ) - .child(div().flex_none().whitespace_nowrap().child("Add track")) + .child( + div() + .flex_none() + .whitespace_nowrap() + .child(tr::editor::window::add_track::button()), + ) .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .on_click(cx.listener(|this, _: &gpui::ClickEvent, window, cx| { this.open_add_track(window, cx); @@ -10986,14 +11113,16 @@ impl EditorWindow { // segments. The clip row has none. let delete_label = match kind { TrackKind::Clip => None, - TrackKind::Caption | TrackKind::Keyboard => Some("Delete"), + TrackKind::Caption | TrackKind::Keyboard => { + Some(tr::editor::window::track::delete()) + } TrackKind::Text | TrackKind::Mask | TrackKind::Audio | TrackKind::Style - | TrackKind::Image => Some("Delete"), + | TrackKind::Image => Some(tr::editor::window::track::delete()), TrackKind::Zoom | TrackKind::ThreeD | TrackKind::Scene => { - (!model.segments(kind).is_empty()).then_some("Clear all") + (!model.segments(kind).is_empty()).then(tr::editor::window::track::clear_all) } }; rows = rows.child( @@ -11160,9 +11289,9 @@ impl EditorWindow { cx.notify(); } })) - .child(div().min_w_0().truncate().child( - self.auto_zoom_message.unwrap_or("Add zooms automatically"), - )) + .child( + div().min_w_0().truncate().child(self.auto_zoom_prompt()), + ) .child( ui::Button::plain( &theme, @@ -11171,9 +11300,9 @@ impl EditorWindow { ui::ButtonSize::Sm, ) .label(if generating { - "Generating…" + tr::editor::window::zoom_lane::generating() } else { - "Generate" + tr::editor::window::zoom_lane::generate() }) .radius(px(7.)) .disabled(generating) @@ -11238,7 +11367,7 @@ impl EditorWindow { div() .min_w_0() .truncate() - .child("Add cinematic 3D shots to your recording"), + .child(tr::editor::window::camera3d_lane::prompt()), ) .child( ui::Button::plain( @@ -11247,7 +11376,7 @@ impl EditorWindow { ui::ButtonVariant::Primary, ui::ButtonSize::Sm, ) - .label("Auto scene") + .label(tr::editor::window::camera3d_lane::auto_scene()) .radius(px(7.)) .on_click(cx.listener( |this, _, _window, cx| { @@ -11265,7 +11394,7 @@ impl EditorWindow { .min_w_0() .truncate() .text_color(Hsla::from(theme.editor.text_2)) - .child("How many shots?"), + .child(tr::editor::window::camera3d_lane::how_many()), ) .child(self.render_camera3d_count_pills("lane", 0, maximum, cx)) .child( @@ -11286,7 +11415,7 @@ impl EditorWindow { ui::ButtonVariant::Outline, ui::ButtonSize::Sm, ) - .label("+ Add shot") + .label(tr::editor::window::camera3d_lane::add_shot()) .radius(px(7.)) .on_click(cx.listener( |this, _, window, cx| { @@ -11566,6 +11695,7 @@ fn is_playback_shortcut( impl Render for EditorWindow { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { self.sync_appearance(window, cx); + self.sync_locale(); let title_disabled = !self.visual_ready(); if self.name_input.read(cx).is_disabled() != title_disabled { self.name_input @@ -12111,12 +12241,14 @@ fn import_audio_file( .extension() .and_then(|ext| ext.to_str()) .map(|ext| ext.to_ascii_lowercase()) - .ok_or_else(|| "Audio file has no extension".to_string())?; + .ok_or_else(|| tr::editor::window::audio_import::no_extension().to_string())?; if !AUDIO_IMPORT_EXTENSIONS.contains(&extension.as_str()) { - return Err(format!("Unsupported audio format: .{extension}")); + return Err(tr::editor::window::audio_import::unsupported(extension)); } if !source.exists() { - return Err(format!("Audio file not found: {}", source.display())); + return Err(tr::editor::window::audio_import::not_found( + source.display(), + )); } let display_name = source .file_stem() @@ -12125,14 +12257,14 @@ fn import_audio_file( .to_string(); let audio_dir = project_path.join("assets").join("audio"); std::fs::create_dir_all(&audio_dir) - .map_err(|error| format!("Failed to create audio directory: {error}"))?; + .map_err(tr::editor::window::audio_import::directory_failed)?; let stamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|duration| duration.as_nanos()) .unwrap_or(0); let dest_name = format!("{stamp}.{extension}"); let dest = audio_dir.join(&dest_name); - std::fs::copy(source, &dest).map_err(|error| format!("Failed to copy audio file: {error}"))?; + std::fs::copy(source, &dest).map_err(tr::editor::window::audio_import::copy_failed)?; Ok(ImportedAudio { path: format!("assets/audio/{dest_name}"), name: display_name, diff --git a/apps/desktop-gpui/src/editor_window/frame.rs b/apps/desktop-gpui/src/editor_window/frame.rs index 1394bc61437..ba13e7bd36a 100644 --- a/apps/desktop-gpui/src/editor_window/frame.rs +++ b/apps/desktop-gpui/src/editor_window/frame.rs @@ -8,37 +8,39 @@ use gpui::{ }; use super::EditorWindow; -use crate::ui; +use crate::{i18n::tr, ui}; -const FRAME_STYLES: [(FrameStyle, &str, &str, &str); 5] = [ +type Text = fn() -> &'static str; + +const FRAME_STYLES: [(FrameStyle, Text, Text, &str); 5] = [ ( FrameStyle::None, - "None", - "Show the recording as-is", + tr::editor::window::frame::none, + tr::editor::window::frame::none_description, "icons/ban.svg", ), ( FrameStyle::MacOS, - "macOS", - "Window chrome with traffic lights", + || "macOS", + tr::editor::window::frame::macos_description, "icons/app-window-mac.svg", ), ( FrameStyle::Windows, - "Windows", - "Title bar with window controls", + || "Windows", + tr::editor::window::frame::windows_description, "icons/app-window.svg", ), ( FrameStyle::Browser, - "Browser", - "Browser chrome with address bar", + tr::editor::window::frame::browser, + tr::editor::window::frame::browser_description, "icons/globe.svg", ), ( FrameStyle::Macbook, - "MacBook", - "Laptop bezel around the recording", + || "MacBook", + tr::editor::window::frame::macbook_description, "icons/laptop.svg", ), ]; @@ -102,8 +104,15 @@ impl FrameField { fn label(self) -> &'static str { match self { - Self::Url => "URL", - Self::Title => "Title", + Self::Url => tr::editor::window::frame::url(), + Self::Title => tr::editor::window::frame::title(), + } + } + + fn placeholder(self) -> &'static str { + match self { + Self::Url => "cap.so", + Self::Title => tr::editor::window::frame::window_title_placeholder(), } } @@ -174,13 +183,19 @@ fn apply_targeted_frame_change( fn button_content(style: FrameStyle) -> (&'static str, &'static str) { if style == FrameStyle::None { - return ("Frame", "icons/app-window-mac.svg"); + return ( + tr::editor::window::frame::button(), + "icons/app-window-mac.svg", + ); } FRAME_STYLES .iter() .find(|option| option.0 == style) - .map(|option| (option.1, option.3)) - .unwrap_or(("Frame", "icons/app-window-mac.svg")) + .map(|option| ((option.1)(), option.3)) + .unwrap_or(( + tr::editor::window::frame::button(), + "icons/app-window-mac.svg", + )) } impl EditorWindow { @@ -252,7 +267,7 @@ impl EditorWindow { .left_icon(icon) .right_icon("icons/chevron-down.svg") .when(!narrow, |button| button.label(label)) - .tooltip(&self.theme, "Add a frame") + .tooltip(&self.theme, tr::editor::window::frame::add_tooltip()) .pressed(self.frame_controls.open) .disabled(self.instance.is_none()) .on_click(cx.listener(|this, _, window, cx| { @@ -287,10 +302,7 @@ impl EditorWindow { self.frame_controls.fields = Some([FrameField::Url, FrameField::Title].map(|field| { let input = cx.new(|cx| { let mut input = ui::TextInputState::single_line(window, cx); - input.set_placeholder(match field { - FrameField::Url => "cap.so", - FrameField::Title => "Window title", - }); + input.set_placeholder(field.placeholder()); input }); let subscription = @@ -305,6 +317,7 @@ impl EditorWindow { if let Some(inputs) = &self.frame_controls.fields { for field in [FrameField::Url, FrameField::Title] { let input = &inputs[field.index()]; + input.update(cx, |input, _| input.set_placeholder(field.placeholder())); if !input.read(cx).focus_handle().is_focused(window) { input.update(cx, |input, cx| { input.set_text(field.text(&frame).to_owned(), cx) @@ -415,13 +428,13 @@ impl EditorWindow { .text_size(px(13.)) .font_weight(FontWeight::SEMIBOLD) .text_color(theme.gray(12)) - .child("Frame"), + .child(tr::editor::window::frame::heading()), ) .child( div().text_size(px(11.)).text_color(theme.gray(10)).child( match &self.frame_controls.style_target { Some(target) if !target.matches(&self.project) => { - "This Style changed. Close and reopen Frame.".to_string() + tr::editor::window::frame::style_changed().to_string() } Some(target) if self @@ -435,19 +448,12 @@ impl EditorWindow { segment.overrides.background.is_none() }) => { - format!( - "Style {} only · Editing enables its background override.", - target.index + 1 - ) + tr::editor::window::frame::style_only_override(target.index + 1) } - Some(target) => format!( - "Style {} only · Global settings stay unchanged.", - target.index + 1 - ), - None => { - "Global frame · Applies wherever Style inherits background." - .to_string() + Some(target) => { + tr::editor::window::frame::style_only(target.index + 1) } + None => tr::editor::window::frame::global().to_string(), }, ), ), @@ -455,6 +461,7 @@ impl EditorWindow { .child(div().flex().flex_col().gap(px(2.)).p(px(6.)).children( FRAME_STYLES.into_iter().enumerate().map( |(index, (value, label, description, icon))| { + let (label, description) = (label(), description()); let selected = value == style; div() .id(("frame-style", index)) @@ -543,7 +550,7 @@ impl EditorWindow { .text_size(px(12.)) .font_weight(FontWeight::MEDIUM) .text_color(theme.gray(11)) - .child("Theme"), + .child(tr::editor::window::frame::theme()), ) .child( div() @@ -556,8 +563,14 @@ impl EditorWindow { .p(px(1.)) .children( [ - (FrameTheme::Light, "Light"), - (FrameTheme::Dark, "Dark"), + ( + FrameTheme::Light, + tr::editor::window::frame::light(), + ), + ( + FrameTheme::Dark, + tr::editor::window::frame::dark(), + ), ] .into_iter() .map( @@ -671,7 +684,7 @@ mod tests { #[test] fn frame_styles_and_button_labels_match_tauri() { assert_eq!( - FRAME_STYLES.map(|option| option.1), + FRAME_STYLES.map(|option| (option.1)()), ["None", "macOS", "Windows", "Browser", "MacBook"] ); assert_eq!( @@ -679,7 +692,7 @@ mod tests { ("Frame", "icons/app-window-mac.svg") ); for (style, label, _, icon) in FRAME_STYLES.into_iter().skip(1) { - assert_eq!(button_content(style), (label, icon)); + assert_eq!(button_content(style), (label(), icon)); } } diff --git a/apps/desktop-gpui/src/editor_window/scenes.rs b/apps/desktop-gpui/src/editor_window/scenes.rs index 4b2cd575237..b9ccebb3541 100644 --- a/apps/desktop-gpui/src/editor_window/scenes.rs +++ b/apps/desktop-gpui/src/editor_window/scenes.rs @@ -21,9 +21,6 @@ use cap_project::Camera3DSegment; /// What `Auto scene` builds when nobody has said how many shots they want. pub(crate) const CAMERA3D_AUTO_SHOTS: usize = 3; -/// What the editor says when the track has no room left. -pub(crate) const CAMERA3D_NO_ROOM: &str = "No room for another 3D shot"; - impl EditorWindow { /// Every shot's box, which is what the placement helper reasons over. pub(crate) fn camera3d_shot_ranges(&self) -> Vec<(f64, f64)> { @@ -75,7 +72,7 @@ impl EditorWindow { CAMERA3D_DEFAULT_SHOT_DURATION, self.total_duration(), ) else { - self.show_notice(CAMERA3D_NO_ROOM, window, cx); + self.show_notice(tr::editor::window::no_room_for_3d_shot(), window, cx); return; }; self.insert_camera3d_shots( @@ -142,7 +139,7 @@ impl EditorWindow { let cuts = self.camera3d_clip_cuts(); let generated = auto_camera3d_layout(count, 0., total, &cuts); if generated.is_empty() { - self.show_notice(CAMERA3D_NO_ROOM, window, cx); + self.show_notice(tr::editor::window::no_room_for_3d_shot(), window, cx); return; } self.camera3d_picker = false; diff --git a/apps/desktop-gpui/src/hotkeys.rs b/apps/desktop-gpui/src/hotkeys.rs index d2fa8cc522c..2aee434c0a4 100644 --- a/apps/desktop-gpui/src/hotkeys.rs +++ b/apps/desktop-gpui/src/hotkeys.rs @@ -27,6 +27,7 @@ use gpui::{App, Global, PromptButton, PromptLevel}; use crate::{ app_windows, + i18n::tr, main_window::{Mode, TargetType}, session::{Phase, RecordingSession}, store, @@ -320,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 @@ -402,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), @@ -575,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; @@ -619,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(()), @@ -686,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 @@ -999,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/editor_panels_surface_tests.rs b/apps/desktop-gpui/src/i18n/editor_panels_surface_tests.rs new file mode 100644 index 00000000000..48f60c9cdc5 --- /dev/null +++ b/apps/desktop-gpui/src/i18n/editor_panels_surface_tests.rs @@ -0,0 +1,944 @@ +use super::*; + +pub(super) fn is_editor_panels_surface_key(key: &str) -> bool { + [ + "editor.panel.", + "editor.sidebar.", + "editor.color.", + "editor.audio.", + "editor.crop.", + ] + .iter() + .any(|prefix| key.starts_with(prefix)) +} + +const FORMATTED: &[&str] = &[ + "editor.panel.zoom.title", + "editor.panel.font_weight.custom", + "editor.panel.text_segment.title", + "editor.panel.caption.title", + "editor.panel.keyboard.title", + "editor.panel.audio.title", + "editor.panel.mask.title", + "editor.panel.camera3d.shots", + "editor.panel.camera3d.pose_start", + "editor.panel.camera3d.pose_end", + "editor.panel.camera3d.header", + "editor.panel.camera3d.timing_summary", + "editor.sidebar.style_group.title", + "editor.sidebar.animated_gradient.limit_reached", + "editor.crop.hint", + "editor.crop.editing_style", + "editor.crop.editing_style_override", +]; + +#[test] +fn editor_panels_match_the_literals_they_replaced() { + let pairs: &[(&str, &str)] = &[ + (tr::editor::panel::done(), "Done"), + (tr::editor::panel::delete(), "Delete"), + (tr::editor::panel::enabled(), "Enabled"), + (tr::editor::panel::name(), "Name"), + (tr::editor::panel::duration(), "Duration"), + (tr::editor::panel::fade_duration(), "Fade Duration"), + (tr::editor::panel::flip_horizontally(), "Flip horizontally"), + (tr::editor::panel::flip_vertically(), "Flip vertically"), + (tr::editor::panel::opacity(), "Opacity"), + (tr::editor::panel::edge_in(), "In"), + (tr::editor::panel::edge_out(), "Out"), + (tr::editor::panel::zoom::mode(), "Zoom Mode"), + (tr::editor::panel::zoom::amount(), "Zoom Amount"), + (tr::editor::panel::zoom::mixed(), "Mixed"), + ( + tr::editor::panel::zoom::positions_mixed(), + "Segments zoom into different spots. Drag to move them all to the same one.", + ), + (tr::editor::panel::zoom::auto(), "Auto"), + (tr::editor::panel::zoom::manual(), "Manual"), + ( + tr::editor::panel::zoom::auto_locked(), + "Auto mode needs cursor capture. Enable \"Custom cursor capture (Studio)\" in Settings → General.", + ), + (tr::editor::panel::zoom::how_it_works(), "How does it work?"), + ( + tr::editor::panel::zoom::manual_help(), + "Manual zoom stays on a fixed spot you pick below.", + ), + ( + tr::editor::panel::zoom::auto_help(), + "Automatic zoom follows your cursor around the screen.", + ), + (tr::editor::panel::font_weight::light(), "Light"), + (tr::editor::panel::font_weight::regular(), "Regular"), + (tr::editor::panel::font_weight::medium(), "Medium"), + (tr::editor::panel::font_weight::semibold(), "Semibold"), + (tr::editor::panel::font_weight::bold(), "Bold"), + (tr::editor::panel::font_weight::extra_bold(), "Extra Bold"), + (tr::editor::panel::font_weight::black(), "Black"), + (tr::editor::panel::text_animation::none(), "None"), + (tr::editor::panel::text_animation::fade(), "Fade"), + (tr::editor::panel::text_animation::slide_up(), "Slide up"), + ( + tr::editor::panel::text_animation::slide_down(), + "Slide down", + ), + ( + tr::editor::panel::text_animation::slide_left(), + "Slide left", + ), + ( + tr::editor::panel::text_animation::slide_right(), + "Slide right", + ), + (tr::editor::panel::text_animation::pop(), "Pop"), + (tr::editor::panel::text_animation::zoom(), "Zoom"), + (tr::editor::panel::text_animation::bounce(), "Bounce"), + (tr::editor::panel::text_animation::wipe(), "Wipe"), + (tr::editor::panel::text_animation::words(), "Words"), + (tr::editor::panel::text_animation::letters(), "Letters"), + (tr::editor::panel::text_animation::tracking(), "Tracking"), + ( + tr::editor::panel::text_animation::typewriter(), + "Typewriter", + ), + (tr::editor::panel::text_background::none(), "None"), + (tr::editor::panel::text_background::box_shape(), "Box"), + (tr::editor::panel::text_background::pill(), "Pill"), + (tr::editor::panel::text_background::highlight(), "Highlight"), + (tr::editor::panel::text_layout::overlay(), "Overlay"), + (tr::editor::panel::text_layout::fullscreen(), "Fullscreen"), + (tr::editor::panel::text_preset::group::all(), "All"), + (tr::editor::panel::text_preset::group::titles(), "Titles"), + ( + tr::editor::panel::text_preset::group::lower_thirds(), + "Lower thirds", + ), + ( + tr::editor::panel::text_preset::group::callouts(), + "Callouts", + ), + ( + tr::editor::panel::text_preset::group::statements(), + "Statements", + ), + (tr::editor::panel::text_preset::group::code(), "Code"), + (tr::editor::panel::text_preset::name::title(), "Title"), + (tr::editor::panel::text_preset::name::headline(), "Headline"), + ( + tr::editor::panel::text_preset::name::cinematic(), + "Cinematic", + ), + (tr::editor::panel::text_preset::name::gradient(), "Gradient"), + ( + tr::editor::panel::text_preset::name::lower_third(), + "Lower third", + ), + (tr::editor::panel::text_preset::name::name_tag(), "Name tag"), + (tr::editor::panel::text_preset::name::caption(), "Caption"), + (tr::editor::panel::text_preset::name::kicker(), "Kicker"), + (tr::editor::panel::text_preset::name::label(), "Label"), + ( + tr::editor::panel::text_preset::name::highlight(), + "Highlight", + ), + (tr::editor::panel::text_preset::name::sticker(), "Sticker"), + (tr::editor::panel::text_preset::name::neon(), "Neon"), + (tr::editor::panel::text_preset::name::stat(), "Big stat"), + (tr::editor::panel::text_preset::name::quote(), "Quote"), + (tr::editor::panel::text_preset::name::code(), "Code"), + ( + tr::editor::panel::text_preset::name::typewriter(), + "Typewriter", + ), + ( + tr::editor::panel::text_preset::sample::title(), + "Introducing Cap", + ), + ( + tr::editor::panel::text_preset::sample::headline(), + "Ship faster", + ), + ( + tr::editor::panel::text_preset::sample::cinematic(), + "Chapter one", + ), + ( + tr::editor::panel::text_preset::sample::gradient(), + "Beautiful text", + ), + ( + tr::editor::panel::text_preset::sample::name_tag(), + "Richie · Founder", + ), + ( + tr::editor::panel::text_preset::sample::caption(), + "Recorded with Cap", + ), + ( + tr::editor::panel::text_preset::sample::kicker(), + "New feature", + ), + (tr::editor::panel::text_preset::sample::label(), "Pro tip"), + ( + tr::editor::panel::text_preset::sample::highlight(), + "the important part", + ), + (tr::editor::panel::text_preset::sample::sticker(), "Boom!"), + (tr::editor::panel::text_preset::sample::neon(), "Glow up"), + ( + tr::editor::panel::text_preset::sample::quote(), + "“Make it feel effortless”", + ), + ( + tr::editor::panel::text_preset::sample::typewriter(), + "typing it out…", + ), + (tr::editor::panel::font::system_sans(), "System Sans"), + (tr::editor::panel::font::system_serif(), "System Serif"), + (tr::editor::panel::font::system_mono(), "System Mono"), + (tr::editor::panel::text_segment::style(), "Style"), + (tr::editor::panel::text_segment::font(), "Font"), + (tr::editor::panel::text_segment::font_size(), "Size"), + ( + tr::editor::panel::text_segment::line_height(), + "Line height", + ), + ( + tr::editor::panel::text_segment::letter_spacing(), + "Letter spacing", + ), + (tr::editor::panel::text_segment::look(), "Look"), + (tr::editor::panel::text_segment::color(), "Color"), + (tr::editor::panel::text_segment::gradient(), "Gradient"), + (tr::editor::panel::text_segment::background(), "Background"), + (tr::editor::panel::text_segment::outline(), "Outline"), + (tr::editor::panel::text_segment::shadow(), "Shadow"), + (tr::editor::panel::text_segment::glow(), "Glow"), + (tr::editor::panel::text_segment::animation(), "Animation"), + (tr::editor::panel::text_segment::layout(), "Layout"), + ( + tr::editor::panel::text_segment::fullscreen_help(), + "Pauses the video while the text is shown, then resumes where it left off.", + ), + ( + tr::editor::panel::text_segment::screen_transition(), + "Screen transition", + ), + (tr::editor::panel::timing::title(), "Timing"), + (tr::editor::panel::timing::start(), "Start"), + (tr::editor::panel::timing::to(), "to"), + (tr::editor::panel::timing::end(), "End"), + (tr::editor::panel::audio::fallback_name(), "Audio"), + ( + tr::editor::panel::audio::change_hint(), + "Tap to change track", + ), + (tr::editor::panel::audio::change(), "Change"), + (tr::editor::panel::audio::volume(), "Volume"), + (tr::editor::panel::audio::fade_in(), "Fade In"), + (tr::editor::panel::audio::fade_out(), "Fade Out"), + (tr::editor::panel::mask::sensitive(), "Sensitive"), + (tr::editor::panel::mask::highlight(), "Highlight"), + (tr::editor::panel::mask::effect(), "Effect"), + (tr::editor::panel::mask::blur(), "Blur"), + (tr::editor::panel::mask::pixelate(), "Pixelate"), + (tr::editor::panel::mask::pixel_size(), "Pixel Size"), + ( + tr::editor::panel::mask::outside_darkness(), + "Outside Darkness", + ), + (tr::editor::panel::scene::camera_layout(), "Camera Layout"), + (tr::editor::panel::scene::transition(), "Transition"), + (tr::editor::panel::scene::screen_zoom(), "Screen Zoom"), + ( + tr::editor::panel::scene::screen_position(), + "Screen Position", + ), + (tr::editor::panel::scene::camera_zoom(), "Camera Zoom"), + ( + tr::editor::panel::scene::camera_position(), + "Camera Position", + ), + (tr::editor::panel::scene::mode::default(), "Default"), + (tr::editor::panel::scene::mode::camera_only(), "Camera Only"), + (tr::editor::panel::scene::mode::hide_camera(), "Hide Camera"), + ( + tr::editor::panel::scene::mode::split_screen(), + "Split Screen", + ), + (tr::editor::panel::scene::mode::floating(), "Floating"), + ( + tr::editor::panel::scene::description::default(), + "Shows both screen and camera", + ), + ( + tr::editor::panel::scene::description::camera_only(), + "Shows only the camera feed", + ), + ( + tr::editor::panel::scene::description::hide_camera(), + "Shows only the screen recording", + ), + ( + tr::editor::panel::scene::description::split_screen(), + "Screen and camera side by side (auto-stacks in portrait)", + ), + ( + tr::editor::panel::scene::description::floating(), + "Screen and camera float side by side as rounded cards over the background", + ), + (tr::editor::panel::camera3d::look_title(), "Look"), + (tr::editor::panel::camera3d::moves(), "Moves"), + (tr::editor::panel::camera3d::angles(), "Angles"), + ( + tr::editor::panel::camera3d::drag_to_orbit(), + "Drag to orbit", + ), + ( + tr::editor::panel::camera3d::end_same_as_start(), + "End · same as start", + ), + ( + tr::editor::panel::camera3d::nothing_to_swap(), + "Nothing to swap on a still shot", + ), + (tr::editor::panel::camera3d::swap(), "Swap start and end"), + (tr::editor::panel::camera3d::still_shot(), "Still shot"), + ( + tr::editor::panel::camera3d::already_still(), + "Already a still shot", + ), + ( + tr::editor::panel::camera3d::hold_opening(), + "Hold the opening pose for the whole shot", + ), + (tr::editor::panel::camera3d::auto_scene(), "Auto scene"), + ( + tr::editor::panel::camera3d::auto_scene_tooltip(), + "Rebuilds every 3D shot on the track", + ), + (tr::editor::panel::camera3d::play_shot(), "Play shot"), + ( + tr::editor::panel::camera3d::play_shot_tooltip(), + "Play this shot", + ), + ( + tr::editor::panel::camera3d::delete_tooltip(), + "Delete this shot", + ), + (tr::editor::panel::camera3d::camera(), "Camera"), + (tr::editor::panel::camera3d::distance(), "Distance"), + (tr::editor::panel::camera3d::roll(), "Roll"), + (tr::editor::panel::camera3d::shift_x(), "Shift X"), + (tr::editor::panel::camera3d::shift_y(), "Shift Y"), + (tr::editor::panel::camera3d::depth_blur(), "Depth blur"), + (tr::editor::panel::camera3d::amount(), "Amount"), + (tr::editor::panel::camera3d::focus(), "Focus"), + (tr::editor::panel::camera3d::fine_tune(), "Fine-tune"), + (tr::editor::panel::camera3d::bokeh(), "Bokeh"), + ( + tr::editor::panel::camera3d::timing_advanced(), + "Timing & advanced", + ), + (tr::editor::panel::camera3d::motion_style(), "Motion style"), + (tr::editor::panel::camera3d::ease_in(), "Ease in"), + (tr::editor::panel::camera3d::ease_out(), "Ease out"), + (tr::editor::panel::camera3d::lens(), "Lens"), + (tr::editor::panel::camera3d::rotate_x(), "Rotate X"), + (tr::editor::panel::camera3d::rotate_y(), "Rotate Y"), + (tr::editor::panel::camera3d::reset_camera(), "Reset camera"), + (tr::editor::panel::camera3d::shot::still(), "Still shot"), + (tr::editor::panel::camera3d::shot::custom(), "Custom move"), + (tr::editor::panel::camera3d::look::spotlight(), "Spotlight"), + ( + tr::editor::panel::camera3d::look::perspective(), + "Perspective", + ), + (tr::editor::panel::camera3d::look::center(), "Center"), + (tr::editor::panel::camera3d::look::low_angle(), "Low angle"), + (tr::editor::panel::camera3d::look::close_up(), "Close up"), + ( + tr::editor::panel::camera3d::look::glide_across(), + "Glide across", + ), + ( + tr::editor::panel::camera3d::look::drift_down(), + "Drift down", + ), + ( + tr::editor::panel::camera3d::look::rising_sweep(), + "Rising sweep", + ), + (tr::editor::panel::camera3d::look::pull_back(), "Pull back"), + (tr::editor::panel::camera3d::look::top_down(), "Top down"), + (tr::editor::panel::camera3d::look::tilt_away(), "Tilt away"), + (tr::editor::panel::camera3d::look::unfold(), "Unfold"), + (tr::editor::panel::camera3d::look::slide_by(), "Slide by"), + ( + tr::editor::panel::camera3d::sequence::showcase(), + "Showcase", + ), + ( + tr::editor::panel::camera3d::sequence::product_tour(), + "Product tour", + ), + ( + tr::editor::panel::camera3d::sequence::punch_in(), + "Punch in", + ), + (tr::editor::panel::camera3d::easing::linear(), "Linear"), + (tr::editor::panel::camera3d::easing::smooth(), "Smooth"), + (tr::editor::panel::camera3d::easing::ease_in(), "Ease in"), + (tr::editor::panel::camera3d::easing::ease_out(), "Ease out"), + (tr::editor::panel::camera3d::focus_mode::radial(), "Radial"), + ( + tr::editor::panel::camera3d::focus_mode::directional(), + "Directional", + ), + ( + tr::editor::panel::camera3d::focus_mode::tilt_shift(), + "Tilt shift", + ), + (tr::editor::panel::camera3d::blur::strength(), "Strength"), + (tr::editor::panel::camera3d::blur::focus_x(), "Focus X"), + (tr::editor::panel::camera3d::blur::focus_y(), "Focus Y"), + ( + tr::editor::panel::camera3d::blur::focus_size(), + "Focus size", + ), + (tr::editor::panel::camera3d::blur::falloff(), "Falloff"), + (tr::editor::panel::camera3d::blur::angle(), "Angle"), + (tr::editor::panel::camera3d::blur::position(), "Position"), + (tr::editor::panel::camera3d::blur::scan(), "Scan"), + (tr::editor::panel::image::arrange(), "Arrange on canvas"), + ( + tr::editor::panel::image::arrange_help(), + "Drag the image to move it. Pull a corner to resize, or use the rotation control below to turn it.", + ), + (tr::editor::panel::image::center(), "Center on canvas"), + (tr::editor::panel::image::replace(), "Replace image"), + ( + tr::editor::panel::image::missing(), + "Image file is missing. Replace it to restore this segment while keeping its timing and transforms.", + ), + (tr::editor::panel::image::lock_aspect(), "Lock aspect ratio"), + (tr::editor::panel::image::rotation(), "Rotation"), + (tr::editor::panel::image::rounding(), "Rounding"), + ( + tr::editor::panel::style::help(), + "Overrides apply only during this segment. Enable a group to copy its global settings.", + ), + ( + tr::editor::panel::style::edit_background(), + "Edit Background & frame", + ), + (tr::editor::panel::style::edit_camera(), "Edit Camera"), + (tr::editor::panel::style::edit_cursor(), "Edit Cursor"), + ( + tr::editor::panel::style::crop_title(), + "Screen crop (source pixels)", + ), + (tr::editor::panel::style::crop_left(), "Left"), + (tr::editor::panel::style::crop_top(), "Top"), + (tr::editor::panel::style::crop_width(), "Width"), + (tr::editor::panel::style::crop_height(), "Height"), + (tr::editor::panel::style::reset_crop(), "Reset crop"), + ( + tr::editor::panel::style::camera_only_background(), + "Camera Only background", + ), + ( + tr::editor::panel::style::camera_only_padding(), + "Camera Only padding", + ), + ( + tr::editor::panel::style::camera_only_help(), + "Use a Camera Only scene. Padding reveals the background around the camera.", + ), + (tr::editor::sidebar::tab::background(), "Background"), + (tr::editor::sidebar::tab::camera(), "Camera"), + (tr::editor::sidebar::tab::audio(), "Audio"), + (tr::editor::sidebar::tab::cursor(), "Cursor"), + (tr::editor::sidebar::tab::keyboard(), "Keyboard"), + (tr::editor::sidebar::tab::captions(), "Captions"), + (tr::editor::sidebar::source::desktop(), "Desktop"), + (tr::editor::sidebar::source::wallpaper(), "Wallpaper"), + (tr::editor::sidebar::source::image(), "Image"), + (tr::editor::sidebar::source::color(), "Color"), + (tr::editor::sidebar::source::gradient(), "Gradient"), + (tr::editor::sidebar::source::animated(), "Animated"), + (tr::editor::sidebar::source::none(), "None"), + (tr::editor::sidebar::wallpaper_theme::dark(), "Dark"), + (tr::editor::sidebar::wallpaper_theme::blue(), "Blue"), + (tr::editor::sidebar::wallpaper_theme::cities(), "Cities"), + (tr::editor::sidebar::wallpaper_theme::purple(), "Purple"), + (tr::editor::sidebar::wallpaper_theme::orange(), "Orange"), + (tr::editor::sidebar::importing_image(), "Importing image…"), + ( + tr::editor::sidebar::desktop_import_failed(), + "Couldn't import your desktop wallpaper", + ), + (tr::editor::sidebar::layout::title(), "Layout"), + ( + tr::editor::sidebar::layout::background_blur(), + "Background Blur", + ), + (tr::editor::sidebar::layout::padding(), "Padding"), + ( + tr::editor::sidebar::layout::custom_position(), + "Custom screen position (dragged on canvas)", + ), + ( + tr::editor::sidebar::layout::rounded_corners(), + "Rounded Corners", + ), + (tr::editor::sidebar::layout::motion_blur(), "Motion Blur"), + (tr::editor::sidebar::background::title(), "Background"), + (tr::editor::sidebar::desktop::importing(), "Importing..."), + ( + tr::editor::sidebar::desktop::import(), + "Import desktop background", + ), + ( + tr::editor::sidebar::desktop::prompt(), + "Use the wallpaper from your desktop", + ), + (tr::editor::sidebar::desktop::reimport(), "Re-import"), + ( + tr::editor::sidebar::image::prompt(), + "Click to select or drag and drop image", + ), + (tr::editor::sidebar::gradient::from(), "From"), + (tr::editor::sidebar::gradient::to(), "To"), + (tr::editor::sidebar::gradient::angle(), "Angle"), + (tr::editor::sidebar::gradient::noise(), "Noise"), + (tr::editor::sidebar::gradient::grain_scale(), "Grain Scale"), + (tr::editor::sidebar::corner::title(), "Corner Style"), + (tr::editor::sidebar::corner::squircle(), "Squircle"), + (tr::editor::sidebar::corner::rounded(), "Rounded"), + (tr::editor::sidebar::border::title(), "Border"), + (tr::editor::sidebar::border::width(), "Border Width"), + (tr::editor::sidebar::border::color(), "Border Color"), + (tr::editor::sidebar::border::opacity(), "Border Opacity"), + (tr::editor::sidebar::notch::title(), "MacBook notch"), + ( + tr::editor::sidebar::notch::description(), + "Draws a MacBook notch over the recording. Recordings made on a Mac with a notch use their own measurements; otherwise start from the size below and adjust to match.", + ), + (tr::editor::sidebar::notch::width(), "Notch Width"), + (tr::editor::sidebar::notch::height(), "Notch Height"), + (tr::editor::sidebar::notch::position(), "Notch Position"), + (tr::editor::sidebar::shadow::title(), "Shadow"), + ( + tr::editor::sidebar::shadow::advanced(), + "Advanced shadow settings", + ), + (tr::editor::sidebar::shadow::shadow_size(), "Size"), + (tr::editor::sidebar::shadow::opacity(), "Opacity"), + (tr::editor::sidebar::shadow::blur(), "Blur"), + ( + tr::editor::sidebar::style_group::background(), + "Background & frame", + ), + (tr::editor::sidebar::style_group::camera(), "Camera"), + (tr::editor::sidebar::style_group::cursor(), "Cursor"), + ( + tr::editor::sidebar::style_group::editing_note(), + "Editing this segment only. Global settings are unchanged.", + ), + (tr::editor::sidebar::style_group::back(), "Back to Style"), + (tr::editor::sidebar::cursor::default(), "Default"), + (tr::editor::sidebar::cursor::circle(), "Circle"), + (tr::editor::sidebar::cursor::recorded(), "Recorded cursors"), + ( + tr::editor::sidebar::cursor::recorded_tooltip(), + "Recorded with this cursor", + ), + ( + tr::editor::sidebar::cursor::description_default(), + "Keeps the cursor shapes and appearance from your recording.", + ), + ( + tr::editor::sidebar::cursor::description_macos(), + "Classic macOS appearance. Custom cursors keep their recorded shape.", + ), + ( + tr::editor::sidebar::cursor::description_tahoe(), + "macOS Tahoe appearance. Custom cursors keep their recorded shape.", + ), + ( + tr::editor::sidebar::cursor::description_windows(), + "Windows appearance. Custom cursors keep their recorded shape.", + ), + ( + tr::editor::sidebar::cursor::description_circle(), + "Replaces all cursor shapes with a circle.", + ), + (tr::editor::sidebar::cursor::click_ripple(), "Click Ripple"), + (tr::editor::sidebar::cursor::color(), "Color"), + (tr::editor::sidebar::cursor::strength(), "Strength"), + (tr::editor::sidebar::cursor::ripple_size(), "Size"), + (tr::editor::sidebar::cursor::duration(), "Duration"), + (tr::editor::sidebar::animated_gradient::still(), "Still"), + ( + tr::editor::sidebar::animated_gradient::name_placeholder(), + "Name this gradient", + ), + ( + tr::editor::sidebar::animated_gradient::name_required(), + "Enter a name for this gradient.", + ), + ( + tr::editor::sidebar::animated_gradient::save_failed(), + "Could not save this gradient. Try again.", + ), + ( + tr::editor::sidebar::animated_gradient::delete_failed(), + "Could not delete this gradient. Try again.", + ), + ( + tr::editor::sidebar::animated_gradient::remember_failed(), + "Could not remember your animated gradient settings.", + ), + ( + tr::editor::sidebar::animated_gradient::randomize(), + "Randomize", + ), + (tr::editor::sidebar::animated_gradient::presets(), "Presets"), + ( + tr::editor::sidebar::animated_gradient::save_tooltip(), + "Save the current gradient", + ), + (tr::editor::sidebar::animated_gradient::saved(), "Saved"), + (tr::editor::sidebar::animated_gradient::delete(), "Delete"), + ( + tr::editor::sidebar::animated_gradient::remove_colour(), + "Remove colour", + ), + (tr::editor::sidebar::animated_gradient::colours(), "Colours"), + ( + tr::editor::sidebar::animated_gradient::add_colour(), + "Add a colour", + ), + (tr::editor::sidebar::animated_gradient::motion(), "Motion"), + ( + tr::editor::sidebar::animated_gradient::fine_tune(), + "Fine-tune", + ), + ( + tr::editor::sidebar::animated_gradient::reset_tooltip(), + "Reset fine-tune settings", + ), + ( + tr::editor::sidebar::animated_gradient::control::direction(), + "Direction", + ), + ( + tr::editor::sidebar::animated_gradient::control::flow_scale(), + "Flow Scale", + ), + ( + tr::editor::sidebar::animated_gradient::control::flow_strength(), + "Flow Strength", + ), + ( + tr::editor::sidebar::animated_gradient::control::curvature(), + "Curvature", + ), + ( + tr::editor::sidebar::animated_gradient::control::detail(), + "Detail", + ), + ( + tr::editor::sidebar::animated_gradient::control::relief(), + "Relief", + ), + ( + tr::editor::sidebar::animated_gradient::control::highlights(), + "Highlights", + ), + ( + tr::editor::sidebar::animated_gradient::control::shading(), + "Shading", + ), + ( + tr::editor::sidebar::animated_gradient::control::ripple_size(), + "Ripple Size", + ), + ( + tr::editor::sidebar::animated_gradient::control::grain_amount(), + "Grain Amount", + ), + ( + tr::editor::sidebar::animated_gradient::control::grain_size(), + "Grain Size", + ), + ( + tr::editor::sidebar::animated_gradient::control::exposure(), + "Exposure", + ), + ( + tr::editor::sidebar::animated_gradient::control::contrast(), + "Contrast", + ), + ( + tr::editor::sidebar::animated_gradient::control::vibrance(), + "Vibrance", + ), + ( + tr::editor::sidebar::animated_gradient::control::motion_speed(), + "Motion Speed", + ), + ( + tr::editor::sidebar::animated_gradient::group::flow(), + "Flow", + ), + ( + tr::editor::sidebar::animated_gradient::group::lighting(), + "Lighting", + ), + ( + tr::editor::sidebar::animated_gradient::group::texture(), + "Texture", + ), + ( + tr::editor::sidebar::animated_gradient::group::colour(), + "Colour", + ), + ( + tr::editor::sidebar::animated_gradient::group::animation(), + "Animation", + ), + (tr::editor::color::title(), "Color Correction"), + (tr::editor::color::apply_to_cursor(), "Apply to cursor"), + (tr::editor::color::fine_tune(), "Fine-tune colors"), + (tr::editor::color::preset::none::name(), "None"), + ( + tr::editor::color::preset::none::description(), + "Original colors, no grade applied", + ), + (tr::editor::color::preset::cinematic::name(), "Cinematic"), + ( + tr::editor::color::preset::cinematic::description(), + "Teal shadows and orange highlights with light grain", + ), + (tr::editor::color::preset::noir::name(), "Noir"), + ( + tr::editor::color::preset::noir::description(), + "High-contrast black and white with heavy grain", + ), + (tr::editor::color::preset::vintage::name(), "Vintage"), + ( + tr::editor::color::preset::vintage::description(), + "Faded warm film look with soft contrast", + ), + (tr::editor::color::preset::frost::name(), "Frost"), + ( + tr::editor::color::preset::frost::description(), + "Cool, crisp tones with muted color", + ), + (tr::editor::color::preset::golden::name(), "Golden"), + ( + tr::editor::color::preset::golden::description(), + "Warm golden-hour glow", + ), + (tr::editor::color::preset::midnight::name(), "Midnight"), + ( + tr::editor::color::preset::midnight::description(), + "Dark, moody teal with subdued color", + ), + (tr::editor::color::preset::vivid::name(), "Vivid"), + ( + tr::editor::color::preset::vivid::description(), + "Punchy saturation and contrast boost", + ), + (tr::editor::color::preset::dreamy::name(), "Dreamy"), + ( + tr::editor::color::preset::dreamy::description(), + "Soft, airy pastels with lifted blacks", + ), + (tr::editor::color::slider::grain(), "Grain"), + (tr::editor::color::slider::strength(), "Strength"), + (tr::editor::color::slider::exposure(), "Exposure"), + (tr::editor::color::slider::contrast(), "Contrast"), + (tr::editor::color::slider::saturation(), "Saturation"), + (tr::editor::color::slider::temperature(), "Temperature"), + (tr::editor::color::slider::tint(), "Tint"), + (tr::editor::color::slider::fade(), "Fade"), + (tr::editor::color::slider::split_tone(), "Split Tone"), + (tr::editor::color::slider::vignette(), "Vignette"), + (tr::editor::audio::library::done(), "Done"), + (tr::editor::audio::library::change_title(), "Change audio"), + (tr::editor::audio::library::add_title(), "Add audio"), + ( + tr::editor::audio::library::change_hint(), + "Pick a different track for this segment", + ), + ( + tr::editor::audio::library::add_hint(), + "Add audio, music or other sounds to your video", + ), + (tr::editor::audio::library::import(), "Import file"), + (tr::editor::crop::title(), "Crop"), + (tr::editor::crop::free(), "Free"), + (tr::editor::crop::snap_to_ratios(), "Snap to ratios"), + ( + tr::editor::crop::style_changed(), + "This Style changed while cropping. Cancel and reopen Crop.", + ), + (tr::editor::crop::preview(), "Preview"), + (tr::editor::crop::aspect_ratio(), "Aspect ratio"), + (tr::editor::crop::dimensions(), "Size"), + (tr::editor::crop::position(), "Position"), + (tr::editor::crop::center(), "Center"), + (tr::editor::crop::full(), "Full"), + (tr::editor::crop::loading_frame(), "Loading frame…"), + (tr::editor::crop::rendering_preview(), "Rendering preview…"), + ]; + for (translated, literal) in pairs { + assert_eq!(translated, literal); + } + let plain_keys = KEYS + .iter() + .filter(|key| is_editor_panels_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}"); + } +} + +#[test] +fn editor_panel_messages_format_like_the_code_they_replaced() { + for index in [0_usize, 1, 4, 11] { + let number = index + 1; + assert_eq!( + tr::editor::panel::zoom::title(number), + format!("Zoom {}", index + 1) + ); + assert_eq!( + tr::editor::panel::text_segment::title(number), + format!("Text {}", index + 1) + ); + assert_eq!( + tr::editor::panel::caption::title(number), + format!("Caption {}", index + 1) + ); + assert_eq!( + tr::editor::panel::keyboard::title(number), + format!("Keyboard {}", index + 1) + ); + assert_eq!( + tr::editor::panel::audio::title(number), + format!("Audio {}", index + 1) + ); + assert_eq!( + tr::editor::panel::mask::title(number), + format!("Mask {}", index + 1) + ); + assert_eq!( + tr::editor::crop::editing_style(number), + format!("Editing Style {} only", index + 1) + ); + assert_eq!( + tr::editor::crop::editing_style_override(number), + format!( + "Editing Style {} only · Saving enables its background override", + index + 1 + ) + ); + for (group, label) in [ + ( + tr::editor::sidebar::style_group::background(), + "Background & frame", + ), + (tr::editor::sidebar::style_group::camera(), "Camera"), + (tr::editor::sidebar::style_group::cursor(), "Cursor"), + ] { + assert_eq!( + tr::editor::sidebar::style_group::title(number, group), + format!("Style {} · {}", index + 1, label) + ); + } + } + + for weight in [450_f32, 350., 725.5, 1000.] { + assert_eq!( + tr::editor::panel::font_weight::custom(weight), + format!("Custom ({})", weight) + ); + } + + for scene in crate::editor_panels::CAMERA3D_SCENES { + let shots = scene.shots.len(); + assert_eq!( + tr::editor::panel::camera3d::shots(shots), + format!("{shots} shots") + ); + } + assert_eq!(tr::editor::panel::camera3d::shots(0), "0 shots"); + assert_eq!(tr::editor::panel::camera3d::shots(1), "1 shot"); + assert_eq!(tr::editor::panel::camera3d::shots(2), "2 shots"); + assert_eq!(tr::editor::panel::camera3d::shots(5), "5 shots"); + + for time in ["0:00.0", "0:04.5", "12:03.2", "1:02:03.4"] { + assert_eq!( + tr::editor::panel::camera3d::pose_start(time), + format!("{} \u{b7} {}", "Start", time) + ); + assert_eq!( + tr::editor::panel::camera3d::pose_end(time), + format!("{} \u{b7} {}", "End", time) + ); + } + + for (look, duration) in [ + ("Glide across", 4.0_f64), + ("Still shot", 0.25), + ("Custom move", 12.96), + ] { + assert_eq!( + tr::editor::panel::camera3d::header(look, format!("{duration:.1}")), + format!("3D shot \u{b7} {look} \u{b7} {duration:.1}s") + ); + } + + for (easing_label, fov) in [("Smooth", 24.4_f64), ("Linear", 45.), ("Ease out", 119.6)] { + assert_eq!( + tr::editor::panel::camera3d::timing_summary(easing_label, fov.round() as i32), + format!("{easing_label} \u{b7} Lens {}", fov.round() as i32) + ); + } + + for saved in [2_usize, 5, 12] { + assert_eq!( + tr::editor::sidebar::animated_gradient::limit_reached(saved), + format!("You can save up to {saved} gradients. Delete one to add another.") + ); + } + assert_eq!( + tr::editor::sidebar::animated_gradient::limit_reached(0), + "You can save up to 0 gradients. Delete one to add another." + ); + assert_eq!( + tr::editor::sidebar::animated_gradient::limit_reached(1), + "You can save up to 1 gradient. Delete one to add another." + ); + + assert_eq!( + tr::editor::crop::hint("\u{21e7}"), + "Drag to move \u{b7} Arrow keys nudge \u{b7} Hold \u{21e7} to skip snapping" + ); + assert_eq!( + crate::editor_crop::crop_hint_parts(), + ( + "Drag to move · Arrow keys nudge · Hold".to_string(), + "to skip snapping".to_string() + ) + ); +} diff --git a/apps/desktop-gpui/src/i18n/editor_shell_surface_tests.rs b/apps/desktop-gpui/src/i18n/editor_shell_surface_tests.rs new file mode 100644 index 00000000000..aae6efb6922 --- /dev/null +++ b/apps/desktop-gpui/src/i18n/editor_shell_surface_tests.rs @@ -0,0 +1,1453 @@ +use super::*; + +pub(super) fn is_editor_shell_surface_key(key: &str) -> bool { + [ + "editor.canvas.", + "editor.clips.", + "editor.export.", + "editor.modal.", + "editor.preparing.", + "editor.tabs.", + "editor.timeline.", + "editor.window.", + "ui.", + ] + .iter() + .any(|prefix| key.starts_with(prefix)) +} + +const FORMATTED: &[&str] = &[ + "editor.clips.append_error", + "editor.clips.clip", + "editor.clips.error.cannot_decode", + "editor.clips.error.in_use", + "editor.clips.error.missing_video", + "editor.clips.error.source_failed", + "editor.clips.error.unsupported_video", + "editor.clips.import_error", + "editor.clips.prepare_controls_failed", + "editor.clips.save_before_recording_failed", + "editor.clips.split", + "editor.export.estimate.gigabytes", + "editor.export.estimate.gigabytes_range", + "editor.export.estimate.hours", + "editor.export.estimate.hours_range", + "editor.export.estimate.megabytes", + "editor.export.estimate.megabytes_range", + "editor.export.estimate.minutes", + "editor.export.estimate.minutes_range", + "editor.export.estimate.seconds", + "editor.export.estimate.seconds_range", + "editor.export.fps", + "editor.export.frames", + "editor.export.stats.output_value", + "editor.tabs.captions.download", + "editor.tabs.captions.downloading", + "editor.tabs.captions.generate_failed", + "editor.tabs.captions.model_downloaded", + "editor.tabs.keyboard.load_failed", + "editor.tabs.sync.clip", + "editor.timeline.add_shot_with_look", + "editor.timeline.segment.numbered_clip", + "editor.window.audio_import.copy_failed", + "editor.window.audio_import.directory_failed", + "editor.window.audio_import.not_found", + "editor.window.audio_import.unsupported", + "editor.window.busy.save_before_delete_failed", + "editor.window.frame.style_only", + "editor.window.frame.style_only_override", + "editor.window.open.media_failed", + "editor.window.open.meta_failed", + "editor.window.open.not_found", + "editor.window.presets.delete_confirm", + "editor.window.save_failed", + "editor.window.text_menu.segments", + "editor.window.title.not_saved_message", + "editor.window.title.read_failed", + "editor.window.title.save_failed", + "ui.selection.audio", + "ui.selection.caption", + "ui.selection.image", + "ui.selection.keyboard", + "ui.selection.mask", + "ui.selection.scene", + "ui.selection.segments", + "ui.selection.style", + "ui.selection.text_segments", + "ui.selection.three_d", + "ui.selection.zoom", + "ui.selection.zoom_all", + "ui.selection.zoom_some", +]; + +#[test] +fn editor_shell_surfaces_match_the_literals_they_replaced() { + let pairs: &[(&str, &str)] = &[ + (tr::editor::canvas::camera(), "Camera"), + ( + tr::editor::canvas::camera_follows_zoom(), + "Camera size follows the zoom — drag to move", + ), + (tr::editor::canvas::edit_zoom(), "Edit zoom"), + (tr::editor::canvas::image(), "Image"), + (tr::editor::canvas::mask(), "Mask"), + (tr::editor::canvas::screen(), "Screen"), + ( + tr::editor::canvas::screen_locked(), + "Screen is locked while a zoom is active", + ), + (tr::editor::canvas::text_item(), "Text"), + (tr::editor::clips::append_error_title(), "Recording Error"), + (tr::editor::clips::back(), "Back to editor"), + (tr::editor::clips::cap_filter(), "Cap Recording"), + ( + tr::editor::clips::empty_description(), + "Record or import a clip and it will show up here.", + ), + (tr::editor::clips::empty_title(), "No clips yet"), + (tr::editor::clips::error::cancelled(), "Import cancelled"), + ( + tr::editor::clips::error::instant_source(), + "Instant recordings can't be imported into a project yet", + ), + ( + tr::editor::clips::error::instant_target(), + "Instant mode recordings cannot be edited", + ), + ( + tr::editor::clips::error::into_itself(), + "Cannot import a recording into itself", + ), + ( + tr::editor::clips::error::no_duration(), + "Imported video has no valid duration", + ), + ( + tr::editor::clips::error::not_a_project(), + "Select a Cap project folder", + ), + ( + tr::editor::clips::error::source_empty(), + "Source Cap project has no recording segments", + ), + ( + tr::editor::clips::error::source_recording(), + "Source Cap project is still recording", + ), + ( + tr::editor::clips::error::source_unimportable(), + "Source Cap project has no importable recording segments", + ), + ( + tr::editor::clips::error::unfinished_target(), + "Finish or recover this recording before importing a clip", + ), + (tr::editor::clips::import(), "Import"), + (tr::editor::clips::import_error_title(), "Import Error"), + (tr::editor::clips::import_existing(), "Existing recording"), + (tr::editor::clips::import_mp4(), "MP4 Video…"), + (tr::editor::clips::importing(), "Importing…"), + (tr::editor::clips::mp4_filter(), "MP4 Video"), + (tr::editor::clips::record_new(), "Record a new clip"), + (tr::editor::clips::title(), "Clips"), + ( + tr::editor::export::advanced::bits_per_pixel(), + "Bits per pixel", + ), + ( + tr::editor::export::advanced::cursor_only(), + "Export cursor only", + ), + ( + tr::editor::export::advanced::cursor_only_description(), + "Keeps the same cursor motion and clicks on a transparent background", + ), + ( + tr::editor::export::advanced::custom_bitrate(), + "Using a custom bitrate", + ), + ( + tr::editor::export::advanced::force_ffmpeg(), + "Force FFmpeg decoder", + ), + ( + tr::editor::export::advanced::force_ffmpeg_description(), + "Skip hardware decoder (auto-fallback enabled)", + ), + (tr::editor::export::advanced::title(), "Advanced"), + ( + tr::editor::export::advanced::transparent_mov(), + "Exports as a transparent MOV. Files are large and best for compositing or editing.", + ), + (tr::editor::export::back(), "Back to editor"), + (tr::editor::export::cta::cancel_sign_in(), "Cancel Sign In"), + (tr::editor::export::cta::clipboard(), "Export to Clipboard"), + ( + tr::editor::export::cta::create_link(), + "Create shareable link", + ), + (tr::editor::export::cta::file(), "Export to File"), + (tr::editor::export::cta::reupload(), "Reupload to same link"), + (tr::editor::export::cta::sign_in(), "Sign in to share"), + (tr::editor::export::destination::clipboard(), "Clipboard"), + (tr::editor::export::destination::file(), "File"), + (tr::editor::export::destination::link(), "Shareable Link"), + (tr::editor::export::done::copy_link(), "Copy link"), + ( + tr::editor::export::done::instant_hint(), + "Use Instant Mode for your next recording if you want a link the moment you stop.", + ), + (tr::editor::export::done::link_copied(), "Link copied"), + (tr::editor::export::done::open_file(), "Open File"), + (tr::editor::export::done::open_link(), "Open link"), + (tr::editor::export::done::retry_copy(), "Retry Copy"), + ( + tr::editor::export::done::reuploaded(), + "Your latest edit is ready at the same link", + ), + ( + tr::editor::export::done::uploaded(), + "Your Cap has been uploaded successfully", + ), + (tr::editor::export::estimate::under_megabyte(), "< 1 MB"), + (tr::editor::export::estimate::under_second(), "< 1s"), + ( + tr::editor::export::existing_link_description(), + "Reupload replaces the video at this link with your latest edit. Everyone with the link will see the updated version.", + ), + ( + tr::editor::export::existing_link_title(), + "Update your existing link", + ), + (tr::editor::export::failed(), "Export failed"), + ( + tr::editor::export::generating_preview(), + "Generating preview…", + ), + ( + tr::editor::export::link_unavailable_cursor(), + "Cursor-only exports can only be saved to a file or clipboard.", + ), + ( + tr::editor::export::link_unavailable_transparent(), + "Transparent exports can only be saved to a file or clipboard.", + ), + ( + tr::editor::export::optimize::description(), + "Re-encodes with software for much smaller files (slower)", + ), + (tr::editor::export::optimize::title(), "Optimize file size"), + (tr::editor::export::organization(), "Organization"), + ( + tr::editor::export::phase::clipboard_failed(), + "Export complete; clipboard copy failed", + ), + (tr::editor::export::phase::complete(), "Export complete"), + (tr::editor::export::phase::copied(), "Copied to clipboard"), + ( + tr::editor::export::phase::copying_to_clipboard(), + "Copying to clipboard", + ), + ( + tr::editor::export::phase::rendering_cursor(), + "Rendering cursor track", + ), + (tr::editor::export::phase::rendering_gif(), "Rendering GIF"), + ( + tr::editor::export::phase::rendering_video(), + "Rendering video", + ), + ( + tr::editor::export::phase::reupload_complete(), + "Reupload complete", + ), + ( + tr::editor::export::phase::reuploading(), + "Reuploading to your link", + ), + ( + tr::editor::export::phase::saving_to_file(), + "Saving to file", + ), + (tr::editor::export::phase::starting(), "Preparing export"), + ( + tr::editor::export::phase::upload_complete(), + "Upload complete", + ), + ( + tr::editor::export::phase::uploading(), + "Creating shareable link", + ), + (tr::editor::export::preview(), "Preview"), + ( + tr::editor::export::preview_info(), + "This is a rendered frame from your video. Adjust the settings to see the quality of the final export.", + ), + ( + tr::editor::export::preview_unavailable(), + "Preview unavailable", + ), + (tr::editor::export::quality::larger(), "Larger file"), + (tr::editor::export::quality::maximum(), "Maximum"), + (tr::editor::export::quality::potato(), "Potato"), + (tr::editor::export::quality::smaller(), "Smaller file"), + (tr::editor::export::quality::social(), "Social"), + (tr::editor::export::quality::web(), "Web"), + (tr::editor::export::resources::proceed(), "Proceed anyway"), + ( + tr::editor::export::resources::title(), + "Check export resources", + ), + (tr::editor::export::reupload(), "Reupload"), + ( + tr::editor::export::reupload_tooltip(), + "Upload your latest edit to the same link", + ), + ( + tr::editor::export::reupload_transparent(), + "Share links require a background without transparency", + ), + (tr::editor::export::section::destination(), "Destination"), + (tr::editor::export::section::format(), "Format"), + (tr::editor::export::section::frame_rate(), "Frame rate"), + (tr::editor::export::section::quality(), "Quality"), + (tr::editor::export::section::resolution(), "Resolution"), + ( + tr::editor::export::sign_in_required(), + "You need to sign in to share recordings", + ), + (tr::editor::export::stats::calculating(), "Calculating…"), + (tr::editor::export::stats::duration(), "Duration"), + ( + tr::editor::export::stats::estimated_size(), + "Estimated size", + ), + (tr::editor::export::stats::export_time(), "Export time"), + (tr::editor::export::stats::output(), "Output"), + (tr::editor::export::stats::refining_size(), "Refining size…"), + (tr::editor::export::stats::refining_time(), "Refining time…"), + (tr::editor::export::stats::unavailable(), "Unavailable"), + (tr::editor::export::task_failed(), "Export task failed"), + (tr::editor::export::title(), "Export"), + ( + tr::editor::export::upgrade_message(), + "This feature requires an upgraded plan", + ), + (tr::editor::export::upgrade_title(), "Upgrade required"), + ( + tr::editor::export::upload_failed(), + "Failed to upload recording", + ), + (tr::editor::modal::cancel_export(), "Cancel export"), + ( + tr::editor::modal::cancel_export_message(), + "Are you sure you want to cancel the export?", + ), + (tr::editor::modal::cancel_export_title(), "Cancel export?"), + (tr::editor::modal::keep_exporting(), "Keep exporting"), + ( + tr::editor::modal::recording_retained(), + "Recording retained", + ), + (tr::editor::tabs::audio::isolation_balanced(), "Balanced"), + ( + tr::editor::tabs::audio::isolation_balanced_description(), + "Clearer isolation with natural voice detail.", + ), + (tr::editor::tabs::audio::isolation_light(), "Light"), + ( + tr::editor::tabs::audio::isolation_light_description(), + "Keep more of your original voice and room sound.", + ), + (tr::editor::tabs::audio::isolation_strong(), "Strong"), + ( + tr::editor::tabs::audio::isolation_strong_description(), + "More isolation for noisy spaces. May change voice texture.", + ), + (tr::editor::tabs::audio::mic_volume(), "Microphone Volume"), + (tr::editor::tabs::audio::mute(), "Mute Audio"), + (tr::editor::tabs::audio::stereo::mono_left(), "Mono L"), + (tr::editor::tabs::audio::stereo::mono_right(), "Mono R"), + (tr::editor::tabs::audio::stereo::stereo(), "Stereo"), + ( + tr::editor::tabs::audio::stereo_mode(), + "Microphone Stereo Mode", + ), + ( + tr::editor::tabs::audio::studio_sound_description(), + "Reduces background noise and balances your voice level.", + ), + ( + tr::editor::tabs::audio::studio_sound_save_failed(), + "Could not save the Studio Sound default", + ), + ( + tr::editor::tabs::audio::system_volume(), + "System Audio Volume", + ), + (tr::editor::tabs::audio::title(), "Audio Controls"), + ( + tr::editor::tabs::audio::use_for_new(), + "Use for new recordings", + ), + ( + tr::editor::tabs::camera::advanced_shadow(), + "Advanced shadow settings", + ), + (tr::editor::tabs::camera::background(), "Background"), + (tr::editor::tabs::camera::blur::heavy(), "Heavy Blur"), + (tr::editor::tabs::camera::blur::light(), "Light Blur"), + (tr::editor::tabs::camera::blur::off(), "Off"), + ( + tr::editor::tabs::camera::blur::remove(), + "Remove Background", + ), + (tr::editor::tabs::camera::corner_style(), "Corner Style"), + ( + tr::editor::tabs::camera::custom_position(), + "Custom position (dragged on canvas)", + ), + (tr::editor::tabs::camera::hide(), "Hide Camera"), + ( + tr::editor::tabs::camera::keep_size_during_zoom(), + "Keep original size during zoom", + ), + (tr::editor::tabs::camera::mirror(), "Mirror Camera"), + (tr::editor::tabs::camera::position(), "Position"), + (tr::editor::tabs::camera::reset(), "Reset"), + ( + tr::editor::tabs::camera::rounded_corners(), + "Rounded Corners", + ), + (tr::editor::tabs::camera::shadow(), "Shadow"), + (tr::editor::tabs::camera::shadow_blur(), "Blur"), + (tr::editor::tabs::camera::shadow_opacity(), "Opacity"), + (tr::editor::tabs::camera::shape::source(), "Source"), + (tr::editor::tabs::camera::shape::square(), "Square"), + (tr::editor::tabs::camera::shape_label(), "Shape"), + ( + tr::editor::tabs::camera::size_during_zoom(), + "Size During Zoom", + ), + (tr::editor::tabs::camera::size_label(), "Size"), + (tr::editor::tabs::camera::title(), "Camera"), + ( + tr::editor::tabs::captions::active_word_highlight(), + "Active Word Highlight", + ), + (tr::editor::tabs::captions::animation::bounce(), "Bounce"), + (tr::editor::tabs::captions::animation::none(), "None"), + (tr::editor::tabs::captions::animation::pop(), "Pop"), + (tr::editor::tabs::captions::animation_section(), "Animation"), + ( + tr::editor::tabs::captions::animation_style(), + "Animation Style", + ), + ( + tr::editor::tabs::captions::background_color(), + "Background Color", + ), + ( + tr::editor::tabs::captions::background_opacity(), + "Background Opacity", + ), + ( + tr::editor::tabs::captions::background_settings(), + "Background Settings", + ), + (tr::editor::tabs::captions::beta(), "Beta"), + (tr::editor::tabs::captions::delete(), "Delete"), + (tr::editor::tabs::captions::deleting(), "Deleting..."), + ( + tr::editor::tabs::captions::download_note(), + "One time download to your system. All captions are stored locally.", + ), + (tr::editor::tabs::captions::fade_duration(), "Fade Duration"), + (tr::editor::tabs::captions::font_family(), "Font Family"), + (tr::editor::tabs::captions::font_settings(), "Font Settings"), + (tr::editor::tabs::captions::font_weight(), "Font Weight"), + (tr::editor::tabs::captions::generate(), "Generate Captions"), + (tr::editor::tabs::captions::generating(), "Generating..."), + (tr::editor::tabs::captions::highlight::color(), "Color"), + (tr::editor::tabs::captions::highlight::pill(), "Pill"), + ( + tr::editor::tabs::captions::highlight_color(), + "Highlight Color", + ), + ( + tr::editor::tabs::captions::highlight_note(), + "This is the first version of captions in Cap. Active word highlighting may be inaccurate in some situations. We're working on a fix for this and it will be released in upcoming versions.", + ), + ( + tr::editor::tabs::captions::highlight_style(), + "Highlight Style", + ), + ( + tr::editor::tabs::captions::horizontal_position(), + "Horizontal Position", + ), + ( + tr::editor::tabs::captions::intel_unavailable(), + "Parakeet caption models are unavailable on Intel Macs. Whisper models remain available.", + ), + ( + tr::editor::tabs::captions::keep_open(), + "Keep Cap open while the model downloads. Editor reloads will reconnect automatically.", + ), + (tr::editor::tabs::captions::language::ar(), "Arabic"), + (tr::editor::tabs::captions::language::auto(), "Auto Detect"), + (tr::editor::tabs::captions::language::bn(), "Bengali"), + (tr::editor::tabs::captions::language::de(), "German"), + (tr::editor::tabs::captions::language::en(), "English"), + (tr::editor::tabs::captions::language::es(), "Spanish"), + (tr::editor::tabs::captions::language::fa(), "Persian"), + (tr::editor::tabs::captions::language::fr(), "French"), + (tr::editor::tabs::captions::language::gu(), "Gujarati"), + (tr::editor::tabs::captions::language::he(), "Hebrew"), + (tr::editor::tabs::captions::language::hi(), "Hindi"), + (tr::editor::tabs::captions::language::it(), "Italian"), + (tr::editor::tabs::captions::language::ja(), "Japanese"), + (tr::editor::tabs::captions::language::ko(), "Korean"), + (tr::editor::tabs::captions::language::mr(), "Marathi"), + (tr::editor::tabs::captions::language::nl(), "Dutch"), + (tr::editor::tabs::captions::language::pa(), "Punjabi"), + (tr::editor::tabs::captions::language::pl(), "Polish"), + (tr::editor::tabs::captions::language::pt(), "Portuguese"), + (tr::editor::tabs::captions::language::ru(), "Russian"), + (tr::editor::tabs::captions::language::sk(), "Slovak"), + (tr::editor::tabs::captions::language::ta(), "Tamil"), + (tr::editor::tabs::captions::language::te(), "Telugu"), + (tr::editor::tabs::captions::language::tr(), "Turkish"), + (tr::editor::tabs::captions::language::ur(), "Urdu"), + (tr::editor::tabs::captions::language::zh(), "Chinese"), + (tr::editor::tabs::captions::language_label(), "Language"), + ( + tr::editor::tabs::captions::model::high_accuracy(), + "High Accuracy", + ), + ( + tr::editor::tabs::captions::model::high_accuracy_description(), + "Larger download, higher accuracy", + ), + (tr::editor::tabs::captions::model::medium(), "Medium"), + ( + tr::editor::tabs::captions::model::medium_description(), + "Slower, more accurate", + ), + ( + tr::editor::tabs::captions::model::recommended(), + "Recommended", + ), + ( + tr::editor::tabs::captions::model::recommended_description(), + "Best balance for most recordings", + ), + (tr::editor::tabs::captions::model::small(), "Small"), + ( + tr::editor::tabs::captions::model::small_description(), + "Smallest download", + ), + (tr::editor::tabs::captions::model_label(), "Model"), + (tr::editor::tabs::captions::position(), "Position"), + (tr::editor::tabs::captions::preset::classic(), "Classic"), + ( + tr::editor::tabs::captions::preset::classic_description(), + "Clean text on a solid rounded background.", + ), + (tr::editor::tabs::captions::preset::highlight(), "Highlight"), + ( + tr::editor::tabs::captions::preset::highlight_description(), + "Bold caps with a pill behind the active word.", + ), + (tr::editor::tabs::captions::preset::karaoke(), "Karaoke"), + ( + tr::editor::tabs::captions::preset::karaoke_description(), + "Words light up in sync with speech.", + ), + (tr::editor::tabs::captions::preset::minimal(), "Minimal"), + ( + tr::editor::tabs::captions::preset::minimal_description(), + "Subtle outlined text with no background.", + ), + (tr::editor::tabs::captions::preset::pop(), "Pop"), + ( + tr::editor::tabs::captions::preset::pop_description(), + "Playful caps that pop in with a vibrant accent.", + ), + ( + tr::editor::tabs::captions::regenerate(), + "Regenerate Captions", + ), + (tr::editor::tabs::captions::size_label(), "Size"), + (tr::editor::tabs::captions::style(), "Style"), + (tr::editor::tabs::captions::text_color(), "Text Color"), + (tr::editor::tabs::captions::title(), "Captions"), + (tr::editor::tabs::captions::uppercase(), "Uppercase"), + ( + tr::editor::tabs::captions::vertical_position(), + "Vertical Position", + ), + (tr::editor::tabs::corner::rounded(), "Rounded"), + (tr::editor::tabs::corner::squircle(), "Squircle"), + (tr::editor::tabs::cursor::friction(), "Friction"), + (tr::editor::tabs::cursor::hide_when_idle(), "Hide When Idle"), + ( + tr::editor::tabs::cursor::inactivity_delay(), + "Inactivity Delay", + ), + (tr::editor::tabs::cursor::mass(), "Mass"), + ( + tr::editor::tabs::cursor::movement_style(), + "Cursor Movement Style", + ), + (tr::editor::tabs::cursor::show(), "Show cursor"), + (tr::editor::tabs::cursor::size_label(), "Size"), + ( + tr::editor::tabs::cursor::smooth_movement(), + "Smooth Movement", + ), + (tr::editor::tabs::cursor::style::fast(), "Fast"), + ( + tr::editor::tabs::cursor::style::fast_description(), + "Snappy, close tracking", + ), + (tr::editor::tabs::cursor::style::mellow(), "Mellow"), + ( + tr::editor::tabs::cursor::style::mellow_description(), + "Balanced, natural movement", + ), + (tr::editor::tabs::cursor::style::slow(), "Slow"), + ( + tr::editor::tabs::cursor::style::slow_description(), + "Long, drifting movement", + ), + (tr::editor::tabs::cursor::style::smooth(), "Smooth"), + ( + tr::editor::tabs::cursor::style::smooth_description(), + "Soft, floaty movement", + ), + (tr::editor::tabs::cursor::style_label(), "Cursor Style"), + ( + tr::editor::tabs::cursor::svg_cursors(), + "High Quality SVG Cursors", + ), + (tr::editor::tabs::cursor::tension(), "Tension"), + (tr::editor::tabs::cursor::tilt(), "Tilt"), + (tr::editor::tabs::keyboard::animation(), "Animation"), + ( + tr::editor::tabs::keyboard::background_color(), + "Background Color", + ), + ( + tr::editor::tabs::keyboard::background_opacity(), + "Background Opacity", + ), + ( + tr::editor::tabs::keyboard::background_settings(), + "Background Settings", + ), + (tr::editor::tabs::keyboard::behavior(), "Behavior"), + (tr::editor::tabs::keyboard::beta(), "Beta"), + ( + tr::editor::tabs::keyboard::changed_during_generation(), + "The timeline or keyboard settings changed during generation. Try again.", + ), + ( + tr::editor::tabs::keyboard::empty_description(), + "Click \"Generate Keyboard Segments\" to create segments from recorded keyboard presses.", + ), + ( + tr::editor::tabs::keyboard::empty_title(), + "No keyboard segments yet.", + ), + (tr::editor::tabs::keyboard::fade_duration(), "Fade Duration"), + (tr::editor::tabs::keyboard::font_family(), "Font Family"), + (tr::editor::tabs::keyboard::font_settings(), "Font Settings"), + (tr::editor::tabs::keyboard::font_weight(), "Font Weight"), + ( + tr::editor::tabs::keyboard::generate(), + "Generate Keyboard Segments", + ), + ( + tr::editor::tabs::keyboard::generating(), + "Generating Keyboard Segments...", + ), + ( + tr::editor::tabs::keyboard::grouping_threshold(), + "Grouping Threshold", + ), + ( + tr::editor::tabs::keyboard::linger_duration(), + "Linger Duration", + ), + ( + tr::editor::tabs::keyboard::no_presses(), + "No recorded keyboard presses were found.", + ), + (tr::editor::tabs::keyboard::position(), "Position"), + ( + tr::editor::tabs::keyboard::regenerate(), + "Regenerate Keyboard Segments", + ), + ( + tr::editor::tabs::keyboard::show_modifiers(), + "Show Modifiers", + ), + ( + tr::editor::tabs::keyboard::show_special_keys(), + "Show Special Keys", + ), + (tr::editor::tabs::keyboard::size_label(), "Size"), + (tr::editor::tabs::keyboard::text_color(), "Text Color"), + ( + tr::editor::tabs::keyboard::timeline_unavailable(), + "The project timeline is unavailable.", + ), + ( + tr::editor::tabs::keyboard::timeline_unreadable(), + "The project timeline could not be read.", + ), + (tr::editor::tabs::keyboard::title(), "Show keyboard"), + (tr::editor::tabs::keyboard::uppercase(), "Uppercase"), + (tr::editor::tabs::position::bottom_center(), "Bottom Center"), + (tr::editor::tabs::position::bottom_left(), "Bottom Left"), + (tr::editor::tabs::position::bottom_right(), "Bottom Right"), + (tr::editor::tabs::position::manual(), "Manual"), + (tr::editor::tabs::position::top_center(), "Top Center"), + (tr::editor::tabs::position::top_left(), "Top Left"), + (tr::editor::tabs::position::top_right(), "Top Right"), + ( + tr::editor::tabs::sync::auto_description(), + "Cap calculated these offsets automatically to keep audio in sync with the video. Adjust them if anything still sounds off.", + ), + (tr::editor::tabs::sync::auto_synced(), "Auto-synced"), + (tr::editor::tabs::sync::camera(), "Camera Offset"), + ( + tr::editor::tabs::sync::description(), + "Fine-tune source offsets if audio or camera drifts out of sync with the screen recording.", + ), + (tr::editor::tabs::sync::microphone(), "Microphone Offset"), + ( + tr::editor::tabs::sync::system_audio(), + "System Audio Offset", + ), + (tr::editor::tabs::sync::title(), "Sync"), + (tr::editor::tabs::weight::bold(), "Bold"), + (tr::editor::tabs::weight::medium(), "Medium"), + (tr::editor::tabs::weight::normal(), "Normal"), + (tr::editor::timeline::add_shot(), "Add shot"), + (tr::editor::timeline::empty::add_audio(), "Add audio"), + (tr::editor::timeline::empty::add_scene(), "Add scene"), + ( + tr::editor::timeline::empty::audio(), + "Add background music or import your own audio", + ), + (tr::editor::timeline::empty::captions(), "No captions"), + ( + tr::editor::timeline::empty::keyboard(), + "No keyboard events", + ), + ( + tr::editor::timeline::empty::scene(), + "Switch layouts between your screen and camera", + ), + (tr::editor::timeline::paused(), "Paused"), + ( + tr::editor::timeline::picker::audio(), + "Add background music or import your own audio.", + ), + ( + tr::editor::timeline::picker::caption(), + "Auto-transcribe your recording into on-screen subtitles.", + ), + ( + tr::editor::timeline::picker::clip(), + "Your recorded screen footage.", + ), + ( + tr::editor::timeline::picker::image(), + "Add an image to your recording.", + ), + ( + tr::editor::timeline::picker::keyboard(), + "Display key presses on screen as you type.", + ), + ( + tr::editor::timeline::picker::mask(), + "Blur or black out private areas of the screen.", + ), + ( + tr::editor::timeline::picker::scene(), + "Switch layouts between your screen and camera.", + ), + ( + tr::editor::timeline::picker::scene_unavailable(), + "Record with a camera to use scenes.", + ), + ( + tr::editor::timeline::picker::style(), + "Change background, camera and cursor settings over time.", + ), + ( + tr::editor::timeline::picker::text_overlay(), + "Add custom text overlays and titles to the canvas.", + ), + ( + tr::editor::timeline::picker::three_d(), + "Tilt the scene in 3D perspective.", + ), + ( + tr::editor::timeline::picker::zoom(), + "Smooth zoom-ins that follow the action.", + ), + ( + tr::editor::timeline::remove::audio(), + "Remove the Audio track.", + ), + ( + tr::editor::timeline::remove::captions(), + "Remove the Captions track.", + ), + ( + tr::editor::timeline::remove::clip(), + "Remove the Clip track.", + ), + ( + tr::editor::timeline::remove::image(), + "Remove the Image track.", + ), + ( + tr::editor::timeline::remove::keyboard(), + "Remove the Keyboard track.", + ), + ( + tr::editor::timeline::remove::mask(), + "Remove the Mask track.", + ), + ( + tr::editor::timeline::remove::scene(), + "Remove the Scene track.", + ), + ( + tr::editor::timeline::remove::style(), + "Remove the Style track.", + ), + ( + tr::editor::timeline::remove::text_track(), + "Remove the Text track.", + ), + ( + tr::editor::timeline::remove::three_d(), + "Remove the 3D track.", + ), + ( + tr::editor::timeline::remove::zoom(), + "Remove the Zoom track.", + ), + (tr::editor::timeline::scene::camera_only(), "Camera Only"), + (tr::editor::timeline::scene::default(), "Default"), + (tr::editor::timeline::scene::floating(), "Floating"), + (tr::editor::timeline::scene::hide_camera(), "Hide Camera"), + (tr::editor::timeline::scene::split_screen(), "Split Screen"), + (tr::editor::timeline::segment::audio(), "Audio"), + ( + tr::editor::timeline::segment::automatic_zoom(), + "Automatic Zoom", + ), + (tr::editor::timeline::segment::caption(), "Caption"), + (tr::editor::timeline::segment::clip(), "Clip"), + ( + tr::editor::timeline::segment::cursor_hidden(), + "Cursor hidden", + ), + (tr::editor::timeline::segment::highlight(), "Highlight"), + (tr::editor::timeline::segment::label(), "Label"), + (tr::editor::timeline::segment::manual_zoom(), "Manual Zoom"), + (tr::editor::timeline::segment::muted(), "Muted"), + (tr::editor::timeline::segment::sensitive(), "Sensitive"), + (tr::editor::timeline::track::audio(), "Audio"), + (tr::editor::timeline::track::captions(), "Captions"), + (tr::editor::timeline::track::clip(), "Clip"), + (tr::editor::timeline::track::image(), "Image"), + (tr::editor::timeline::track::keyboard(), "Keyboard"), + (tr::editor::timeline::track::mask(), "Mask"), + (tr::editor::timeline::track::scene(), "Scene"), + (tr::editor::timeline::track::style(), "Style"), + (tr::editor::timeline::track::text_track(), "Text"), + (tr::editor::timeline::track::video(), "Video"), + (tr::editor::timeline::track::zoom(), "Zoom"), + (tr::editor::window::add_track::button(), "Add track"), + ( + tr::editor::window::add_track::hide_3d(), + "Hide the 3D track.", + ), + ( + tr::editor::window::add_track::hint(), + "Choose a track to add to your timeline.", + ), + (tr::editor::window::aspect::auto(), "Auto"), + (tr::editor::window::aspect::classic(), "Classic"), + (tr::editor::window::aspect::classic_ratio(), "Classic ⋅4:3"), + (tr::editor::window::aspect::square(), "Square"), + (tr::editor::window::aspect::square_ratio(), "Square ⋅1:1"), + (tr::editor::window::aspect::tall(), "Tall"), + (tr::editor::window::aspect::tall_ratio(), "Tall ⋅3:4"), + (tr::editor::window::aspect::tooltip(), "Aspect Ratio"), + (tr::editor::window::aspect::vertical(), "Vertical"), + ( + tr::editor::window::aspect::vertical_ratio(), + "Vertical ⋅9:16", + ), + (tr::editor::window::aspect::wide(), "Wide"), + (tr::editor::window::aspect::wide_ratio(), "Wide ⋅16:9"), + (tr::editor::window::audio_filter(), "Audio"), + ( + tr::editor::window::audio_import::no_extension(), + "Audio file has no extension", + ), + ( + tr::editor::window::audio_segment_changed(), + "This audio segment changed. Select it and choose a replacement again.", + ), + ( + tr::editor::window::busy::clip_import(), + "Wait for the clip import to finish before closing or deleting this recording.", + ), + ( + tr::editor::window::busy::export(), + "Wait for the export or its cancellation to finish before closing or deleting this recording.", + ), + ( + tr::editor::window::busy::opening(), + "Wait for the recording to finish opening before deleting it.", + ), + (tr::editor::window::camera3d_lane::add_shot(), "+ Add shot"), + ( + tr::editor::window::camera3d_lane::auto_scene(), + "Auto scene", + ), + ( + tr::editor::window::camera3d_lane::how_many(), + "How many shots?", + ), + ( + tr::editor::window::camera3d_lane::prompt(), + "Add cinematic 3D shots to your recording", + ), + (tr::editor::window::clip_menu::audio(), "AUDIO"), + (tr::editor::window::clip_menu::cursor(), "CURSOR"), + (tr::editor::window::clip_menu::delete(), "Delete clip"), + ( + tr::editor::window::clip_menu::delete_only_clip(), + "Can't delete the only clip", + ), + (tr::editor::window::clip_menu::hide_cursor(), "Hide cursor"), + ( + tr::editor::window::clip_menu::hide_cursor_description(), + "Fades out while this clip plays", + ), + (tr::editor::window::clip_menu::keep_pitch(), "Keep pitch"), + (tr::editor::window::clip_menu::match_speed(), "Match speed"), + ( + tr::editor::window::clip_menu::merge_next(), + "Merge with next clip", + ), + ( + tr::editor::window::clip_menu::merge_previous(), + "Merge with previous clip", + ), + (tr::editor::window::clip_menu::mute(), "Mute"), + (tr::editor::window::clip_menu::mute_clip(), "Mute clip"), + ( + tr::editor::window::clip_menu::mute_clip_description(), + "Silence this clip's audio", + ), + (tr::editor::window::clip_menu::rename(), "Rename clip"), + (tr::editor::window::clip_menu::speed(), "SPEED"), + (tr::editor::window::clip_menu::split(), "Split at playhead"), + ( + tr::editor::window::clip_menu::split_hint(), + "Move the playhead into this clip", + ), + (tr::editor::window::clip_menu::volume(), "Volume"), + (tr::editor::window::crop(), "Crop"), + (tr::editor::window::crop_tooltip(), "Crop Video"), + (tr::editor::window::frame::add_tooltip(), "Add a frame"), + (tr::editor::window::frame::browser(), "Browser"), + ( + tr::editor::window::frame::browser_description(), + "Browser chrome with address bar", + ), + (tr::editor::window::frame::button(), "Frame"), + (tr::editor::window::frame::dark(), "Dark"), + ( + tr::editor::window::frame::global(), + "Global frame · Applies wherever Style inherits background.", + ), + (tr::editor::window::frame::heading(), "Frame"), + (tr::editor::window::frame::light(), "Light"), + ( + tr::editor::window::frame::macbook_description(), + "Laptop bezel around the recording", + ), + ( + tr::editor::window::frame::macos_description(), + "Window chrome with traffic lights", + ), + (tr::editor::window::frame::none(), "None"), + ( + tr::editor::window::frame::none_description(), + "Show the recording as-is", + ), + ( + tr::editor::window::frame::style_changed(), + "This Style changed. Close and reopen Frame.", + ), + (tr::editor::window::frame::theme(), "Theme"), + (tr::editor::window::frame::title(), "Title"), + (tr::editor::window::frame::url(), "URL"), + ( + tr::editor::window::frame::window_title_placeholder(), + "Window title", + ), + ( + tr::editor::window::frame::windows_description(), + "Title bar with window controls", + ), + (tr::editor::window::header::captions(), "Captions"), + ( + tr::editor::window::header::delete_recording(), + "Delete recording", + ), + (tr::editor::window::header::export(), "Export"), + ( + tr::editor::window::header::open_bundle(), + "Open recording bundle", + ), + (tr::editor::window::header::presets(), "Presets"), + ( + tr::editor::window::image::changed(), + "This image changed while choosing a replacement. Select it and try again.", + ), + (tr::editor::window::image::filter(), "Images"), + ( + tr::editor::window::image::recording_unavailable(), + "The recording is no longer available for this image. Please reopen the project.", + ), + ( + tr::editor::window::no_room_for_3d_shot(), + "No room for another 3D shot", + ), + ( + tr::editor::window::open::damaged(), + "This recording's video tracks could not be opened. The bundle looks damaged.", + ), + ( + tr::editor::window::open::failed(), + "This recording could not be opened", + ), + ( + tr::editor::window::open::no_segments(), + "Recording has no segments. It may need to be recovered first.", + ), + ( + tr::editor::window::open::not_studio(), + "Cannot edit non-studio recordings", + ), + (tr::editor::window::presets::apply(), "Apply"), + (tr::editor::window::presets::create(), "Create"), + ( + tr::editor::window::presets::create_new(), + "Create new preset", + ), + (tr::editor::window::presets::create_title(), "Create Preset"), + (tr::editor::window::presets::default(), "Default"), + (tr::editor::window::presets::default_badge(), "Default"), + (tr::editor::window::presets::delete(), "Delete"), + (tr::editor::window::presets::delete_title(), "Delete Preset"), + ( + tr::editor::window::presets::name_placeholder(), + "Preset name", + ), + ( + tr::editor::window::presets::none_saved(), + "No saved presets", + ), + (tr::editor::window::presets::rename(), "Rename"), + (tr::editor::window::presets::rename_title(), "Rename Preset"), + ( + tr::editor::window::presets::save_settings(), + "Save settings to preset", + ), + (tr::editor::window::presets::set_default(), "Set as default"), + (tr::editor::window::preview::full(), "Full"), + (tr::editor::window::preview::half(), "Half"), + (tr::editor::window::preview::label(), "Preview"), + (tr::editor::window::preview::quarter(), "Quarter"), + (tr::editor::window::redo(), "Redo"), + (tr::editor::window::text_menu::copy_style(), "Copy style"), + ( + tr::editor::window::text_menu::copy_style_first(), + "Copy a style first", + ), + ( + tr::editor::window::text_menu::delete_segment(), + "Delete text segment", + ), + ( + tr::editor::window::text_menu::delete_segments(), + "Delete text segments", + ), + (tr::editor::window::text_menu::paste_style(), "Paste style"), + ( + tr::editor::window::text_menu::paste_style_all(), + "Paste style to all selected", + ), + (tr::editor::window::text_menu::untitled(), "Text"), + (tr::editor::window::title::not_saved(), "Title not saved"), + (tr::editor::window::track::clear_all(), "Clear all"), + (tr::editor::window::track::delete(), "Delete"), + ( + tr::editor::window::transport::done_splitting(), + "Done splitting", + ), + ( + tr::editor::window::transport::play_pause(), + "Play/Pause video", + ), + (tr::editor::window::transport::split(), "Split"), + ( + tr::editor::window::transport::split_tooltip(), + "Click a segment to split. Esc to finish.", + ), + (tr::editor::window::transport::zoom_in(), "Zoom in"), + (tr::editor::window::transport::zoom_out(), "Zoom out"), + (tr::editor::window::undo(), "Undo"), + (tr::editor::window::zoom_lane::generate(), "Generate"), + (tr::editor::window::zoom_lane::generating(), "Generating…"), + ( + tr::editor::window::zoom_lane::no_clicks(), + "No clicks found to zoom into. Drag across the lane to add one.", + ), + ( + tr::editor::window::zoom_lane::prompt(), + "Add zooms automatically", + ), + (tr::ui::coming_soon(), "Coming Soon"), + (tr::ui::selection::delete(), "Delete"), + (tr::ui::selection::done(), "Done"), + (tr::ui::selection::select_all(), "Select all"), + ]; + for (translated, literal) in pairs { + assert_eq!(translated, literal); + } + let plain_keys = KEYS + .iter() + .filter(|key| is_editor_shell_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}"); + } +} + +#[test] +fn derived_track_copy_matches_the_sentences_it_replaced() { + use crate::editor_timeline::TrackKind; + for kind in [ + TrackKind::Clip, + TrackKind::Style, + TrackKind::Image, + TrackKind::Caption, + TrackKind::Keyboard, + TrackKind::Text, + TrackKind::Mask, + TrackKind::Audio, + TrackKind::Zoom, + TrackKind::ThreeD, + TrackKind::Scene, + ] { + assert_eq!( + kind.picker_remove(), + format!("Remove the {} track.", kind.picker_label()) + ); + } + assert_eq!( + tr::editor::timeline::empty::audio(), + TrackKind::Audio.picker_description().trim_end_matches('.') + ); + assert_eq!( + tr::editor::timeline::empty::scene(), + TrackKind::Scene.picker_description().trim_end_matches('.') + ); + assert_eq!(tr::editor::window::preview::quarter(), "Quarter"); + assert_eq!(tr::editor::window::preview::half(), "Half"); + assert_eq!(tr::editor::window::preview::full(), "Full"); +} + +#[test] +fn editor_shell_messages_format_like_the_code_they_replaced() { + let error = "disk full".to_string(); + let path = std::path::Path::new("/tmp/Demo.cap"); + let message = "Source Cap project is still recording".to_string(); + + assert_eq!( + tr::editor::clips::append_error(&message), + format!("Failed to add clip: {message}") + ); + assert_eq!( + tr::editor::clips::import_error(&message), + format!("Failed to import clip: {message}") + ); + for index in [0_usize, 1, 2, 5] { + assert_eq!( + tr::editor::clips::clip(index + 1), + format!("Clip {}", index + 1) + ); + assert_eq!(tr::editor::clips::split(index), format!("Split {}", index)); + assert_eq!(tr::editor::tabs::sync::clip(index), format!("Clip {index}")); + assert_eq!( + tr::editor::timeline::segment::numbered_clip(index as u32), + format!("Clip {}", index as u32) + ); + assert_eq!( + tr::editor::window::frame::style_only(index + 1), + format!("Style {} only · Global settings stay unchanged.", index + 1) + ); + assert_eq!( + tr::editor::window::frame::style_only_override(index + 1), + format!( + "Style {} only · Editing enables its background override.", + index + 1 + ) + ); + } + assert_eq!( + tr::editor::clips::error::cannot_decode(path.display(), &error), + format!("Cannot decode video {}: {error}", path.display()) + ); + assert_eq!( + tr::editor::clips::error::in_use(&error), + format!("Cannot import while this recording is in use: {error}") + ); + assert_eq!( + tr::editor::clips::error::missing_video(path.display()), + format!("Missing video file: {}", path.display()) + ); + assert_eq!( + tr::editor::clips::error::source_failed(&error), + format!("Source Cap project failed: {error}") + ); + assert_eq!( + tr::editor::clips::error::unsupported_video(path.display()), + format!("Unsupported video file: {}", path.display()) + ); + assert_eq!( + tr::editor::clips::prepare_controls_failed(&error), + format!("Could not prepare the recording controls: {error}") + ); + assert_eq!( + tr::editor::clips::save_before_recording_failed(&error), + format!("Could not save the current project before recording: {error}") + ); + + for (lower, upper) in [(1.5_f64, 2.0_f64), (12.0, 41.0), (0.1, 3.4)] { + for (single, range, unit) in [ + ( + tr::editor::export::estimate::gigabytes(lower), + tr::editor::export::estimate::gigabytes_range(lower, upper), + "GB", + ), + ( + tr::editor::export::estimate::megabytes(lower), + tr::editor::export::estimate::megabytes_range(lower, upper), + "MB", + ), + ( + tr::editor::export::estimate::hours(lower), + tr::editor::export::estimate::hours_range(lower, upper), + "hr", + ), + ( + tr::editor::export::estimate::minutes(lower), + tr::editor::export::estimate::minutes_range(lower, upper), + "min", + ), + ( + tr::editor::export::estimate::seconds(lower), + tr::editor::export::estimate::seconds_range(lower, upper), + "s", + ), + ] { + assert_eq!(single, format!("~{lower} {unit}")); + assert_eq!(range, format!("~{lower}–{upper} {unit}")); + } + } + for fps in [24_u32, 30, 60] { + assert_eq!(tr::editor::export::fps(&fps), format!("{fps} FPS")); + assert_eq!( + tr::editor::export::stats::output_value(1920_u32, 1080_u32, fps), + format!("{}×{} · {} fps", 1920, 1080, fps) + ); + } + for total in [0_u32, 1, 2, 5] { + let rendered = total / 2; + let expected = if total == 1 { + format!("{rendered} / {total} frame") + } else { + format!("{rendered} / {total} frames") + }; + assert_eq!( + tr::editor::export::frames(total as usize, rendered), + expected + ); + } + + let model = "Recommended"; + assert_eq!( + tr::editor::tabs::captions::download(model), + format!("Download {} Model", model) + ); + for percent in [0_u32, 1, 42, 100] { + assert_eq!( + tr::editor::tabs::captions::downloading(model, percent), + format!("Downloading {model}... {percent}%") + ); + } + assert_eq!( + tr::editor::tabs::captions::model_downloaded(model), + format!("{} model downloaded", model) + ); + let reason = "No audio found in the video file".to_string(); + assert_eq!( + tr::editor::tabs::captions::generate_failed(&reason), + format!("Failed to generate captions: {}", reason) + ); + assert_eq!( + tr::editor::tabs::keyboard::load_failed(&error), + format!("Failed to load recording data: {error}") + ); + + let look = "Glide across"; + for duration in [0.5_f64, 2.0, 3.25] { + assert_eq!( + tr::editor::timeline::add_shot_with_look(look, format!("{duration:.1}")), + format!("Add shot \u{b7} {} \u{b7} {duration:.1}s", look) + ); + } + + assert_eq!( + tr::editor::window::audio_import::copy_failed(&error), + format!("Failed to copy audio file: {error}") + ); + assert_eq!( + tr::editor::window::audio_import::directory_failed(&error), + format!("Failed to create audio directory: {error}") + ); + assert_eq!( + tr::editor::window::audio_import::not_found(path.display()), + format!("Audio file not found: {}", path.display()) + ); + let extension = "ogg".to_string(); + assert_eq!( + tr::editor::window::audio_import::unsupported(&extension), + format!("Unsupported audio format: .{extension}") + ); + assert_eq!( + tr::editor::window::busy::save_before_delete_failed(&error), + format!("Could not save the recording before deleting it: {error}") + ); + assert_eq!( + tr::editor::window::open::media_failed(&error), + format!("Failed to read this recording's media: {error}") + ); + assert_eq!( + tr::editor::window::open::meta_failed(&error), + format!("Failed to load recording meta: {error}") + ); + assert_eq!( + tr::editor::window::open::not_found(path.display()), + format!("Video path {} not found!", path.display()) + ); + let name = "Webinar".to_string(); + assert_eq!( + tr::editor::window::presets::delete_confirm(&name), + format!("Are you sure you want to delete \"{}\"?", name) + ); + assert_eq!( + tr::editor::window::save_failed(path.display(), &error), + format!( + "Could not save {}. Keep the editor open and try again: {error}", + path.display() + ) + ); + for count in [2_usize, 3, 5] { + assert_eq!( + tr::editor::window::text_menu::segments(count), + format!("{count} text segments") + ); + } + assert_eq!( + tr::editor::window::title::not_saved_message(&error), + format!("{error}\n\nYour title is still in the editor. Try saving it again.") + ); + assert_eq!( + tr::editor::window::title::read_failed(path.display(), &error), + format!( + "Could not read the recording title for {}: {error}", + path.display() + ) + ); + assert_eq!( + tr::editor::window::title::save_failed(path.display(), &error), + format!( + "Could not save the recording title for {}: {error}", + path.display() + ) + ); +} + +#[test] +fn selection_headers_read_as_whole_sentences() { + let old = |noun: &str, count: usize| { + format!( + "{count} {noun} segment{} selected", + if count == 1 { "" } else { "s" } + ) + }; + for count in [0, 1, 2, 5] { + for noun in [ + "style", "image", "text", "caption", "audio", "mask", "keyboard", "3D", "scene", + ] { + assert_eq!(crate::ui::selection_label(noun, count), old(noun, count)); + } + assert_eq!( + tr::ui::selection::segments(count), + if count == 1 { + format!("{count} segment selected") + } else { + format!("{count} segments selected") + } + ); + } + let old_zoom = |count: usize, total: usize| { + if total > 1 && count == total { + format!("All {total} selected") + } else if total > 1 { + format!("{count} of {total} selected") + } else { + format!("{count} selected") + } + }; + for (count, total) in [(0, 0), (1, 1), (1, 2), (2, 2), (2, 5), (5, 5)] { + assert_eq!( + crate::ui::zoom_selection_label(count, total), + old_zoom(count, total) + ); + } +} diff --git a/apps/desktop-gpui/src/i18n/mod.rs b/apps/desktop-gpui/src/i18n/mod.rs index d7d487d1a50..447afcba3e4 100644 --- a/apps/desktop-gpui/src/i18n/mod.rs +++ b/apps/desktop-gpui/src/i18n/mod.rs @@ -158,6 +158,14 @@ pub fn apply(preference: Preference, cx: &mut App) { cx.refresh_windows(); } +#[cfg(test)] +mod dialogs_surface_tests; +#[cfg(test)] +mod editor_panels_surface_tests; +#[cfg(test)] +mod editor_shell_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 bd33dd14ef3..859c5314a6b 100644 --- a/apps/desktop-gpui/src/i18n/tests.rs +++ b/apps/desktop-gpui/src/i18n/tests.rs @@ -487,6 +487,10 @@ 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) + && !super::editor_panels_surface_tests::is_editor_panels_surface_key(key) + && !super::editor_shell_surface_tests::is_editor_shell_surface_key(key) }) .count() // `tray.version`, `excluded_windows.missing_body`, `self_host.confirm_change`, 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/main_window.rs b/apps/desktop-gpui/src/main_window.rs index 82377d3bf1f..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(), @@ -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/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/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 6549f0b5225..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 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")] 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 `