From 4622a1af1edc0982c2e4ccb1c272513f38ec8176 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Fri, 21 Aug 2026 15:35:10 +0800 Subject: [PATCH] i18n: six new languages; packs become fully data-driven MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New complete language packs (431 keys each, key-aligned with en-US): French, German, Russian, Japanese, Spanish, Portuguese — contributed via the translation subagent, reviewed for key parity and genuine translations. Runtime discovery replaces compiled-in packs: include_str! and the Language enum are gone; every .yaml found in the searched directories registers a language, so adding one needs neither a rebuild nor a code change. Each pack names itself via its "language.name" key (the endonym), which the pickers display. The menu bar language submenu is built from the discovered packs (menu ids in the new LANG_ITEM_BASE range, dispatched before the registry), the preferences combo enumerates them the same way, and switching goes through set_language_code. The C++-style LangZh/LangEn registry actions are removed (menu ids 303/304 stay reserved). pack_dirs gains the system-install layout /../share/oak/i18n, and en.yaml is renamed en-US.yaml so the discovered code matches the fallback code. Test hardening (a Windows/Linux CI flake chain exposed by this work): the two language-test mutexes are one shared poison-tolerant mutex; every test that flips the process language restores en-US before releasing it; tests that match menus by localized labels take the lock and pin en-US first. --- assets/i18n/de.yaml | 433 ++++++++++++++++++++++++++++ assets/i18n/{en.yaml => en-US.yaml} | 3 +- assets/i18n/es.yaml | 433 ++++++++++++++++++++++++++++ assets/i18n/fr.yaml | 433 ++++++++++++++++++++++++++++ assets/i18n/ja.yaml | 433 ++++++++++++++++++++++++++++ assets/i18n/pt.yaml | 433 ++++++++++++++++++++++++++++ assets/i18n/ru.yaml | 433 ++++++++++++++++++++++++++++ assets/i18n/zh-CN.yaml | 3 +- src/actions.rs | 23 +- src/app.rs | 130 +++++---- src/dialogs.rs | 46 +-- src/i18n.rs | 251 +++++++--------- src/menus/shared.rs | 46 +++ src/panels/mod.rs | 14 +- src/panels/node_editor.rs | 4 + 15 files changed, 2874 insertions(+), 244 deletions(-) create mode 100644 assets/i18n/de.yaml rename assets/i18n/{en.yaml => en-US.yaml} (99%) create mode 100644 assets/i18n/es.yaml create mode 100644 assets/i18n/fr.yaml create mode 100644 assets/i18n/ja.yaml create mode 100644 assets/i18n/pt.yaml create mode 100644 assets/i18n/ru.yaml diff --git a/assets/i18n/de.yaml b/assets/i18n/de.yaml new file mode 100644 index 000000000..c885e5ca0 --- /dev/null +++ b/assets/i18n/de.yaml @@ -0,0 +1,433 @@ +# Oak Video Editor UI strings — Deutsch (de) +# Format: "i18n.key": "Translated text". Add new languages as new files named .yaml. +"menu.file": "Datei(D)" +"menu.edit": "Bearbeiten(B)" +"menu.view": "Ansicht(A)" +"menu.playback": "Wiedergabe(W)" +"menu.sequence": "Sequenz(S)" +"menu.window": "Fenster(F)" +"menu.tools": "Werkzeuge(W)" +"menu.help": "Hilfe(H)" +"menu.file.new": "Neu" +"menu.file.new_project": "Neues Projekt…" +"menu.file.new_sequence": "Neue Sequenz…" +"menu.file.new_folder": "Neuer Ordner" +"menu.file.open_project": "Projektdatei öffnen…" +"menu.file.open_library": "Aus Bibliothek öffnen…" +"menu.file.open_recent": "Zuletzt geöffnet" +"menu.file.clear_recent": "Liste der zuletzt geöffneten Dateien löschen" +"menu.file.project_manager": "Projektmanager…" +"menu.file.import_footage": "Material importieren…" +"menu.file.export_project": "Projektdatei exportieren…" +"menu.file.save_as": "Speichern unter…" +"menu.file.revert": "Wiederherstellen" +"menu.file.export_media": "Medien exportieren…" +"menu.file.project_properties": "Projekteigenschaften…" +"menu.file.close": "Projekt schließen" +"menu.file.quit": "Beenden" +"menu.edit.undo": "Rückgängig" +"menu.edit.redo": "Wiederholen" +"menu.edit.cut": "Ausschneiden" +"menu.edit.copy": "Kopieren" +"menu.edit.paste": "Einfügen" +"menu.edit.paste_insert": "Insert einfügen" +"menu.edit.duplicate": "Duplizieren" +"menu.edit.rename": "Umbenennen" +"menu.edit.delete": "Löschen" +"menu.edit.ripple_delete": "Ripple-Löschen" +"menu.edit.split": "Am Abspielkopf teilen" +"menu.edit.speed_duration": "Geschwindigkeit/Dauer…" +"menu.edit.default_transition": "Als Standardübergang festlegen" +"menu.edit.link_unlink": "Verknüpfen/Verknüpfung aufheben" +"menu.edit.enable_disable": "Aktivieren/Deaktivieren" +"menu.edit.nest": "Verschachteln" +"menu.edit.select_all": "Alles auswählen" +"menu.edit.deselect_all": "Auswahl aufheben" +"menu.edit.insert": "Einsetzen" +"menu.edit.overwrite": "Überschreiben" +"menu.edit.ripple_to_in": "Ripple zum Einstiegspunkt" +"menu.edit.ripple_to_out": "Ripple zum Ausstiegspunkt" +"menu.edit.edit_to_in": "Bis zum Einstiegspunkt bearbeiten" +"menu.edit.edit_to_out": "Bis zum Ausstiegspunkt bearbeiten" +"menu.edit.nudge_left": "Nach links schieben" +"menu.edit.nudge_right": "Nach rechts schieben" +"menu.edit.move_in_to_playhead": "Einstiegspunkt zum Abspielkopf verschieben" +"menu.edit.move_out_to_playhead": "Ausstiegspunkt zum Abspielkopf verschieben" +"menu.edit.set_in_point": "Einstiegspunkt setzen" +"menu.edit.set_out_point": "Ausstiegspunkt setzen" +"menu.edit.reset_in": "Einstiegspunkt zurücksetzen" +"menu.edit.reset_out": "Ausstiegspunkt zurücksetzen" +"menu.edit.clear_in_out": "Ein/Aus löschen" +"menu.edit.delete_in_out": "Von Einstieg bis Ausstieg löschen" +"menu.edit.ripple_delete_in_out": "Ripple-Löschen von Einstieg bis Ausstieg" +"menu.edit.marker": "Marker hinzufügen" +"menu.view.theme": "Design" +"menu.view.theme.dark": "Olive Dark" +"menu.view.theme.light": "Olive Light" +"menu.view.language": "Sprache" +"language.name": "Deutsch" +"menu.view.zoom_in": "Vergrößern" +"menu.view.zoom_out": "Verkleinern" +"menu.view.increase_track_height": "Spurhöhe erhöhen" +"menu.view.decrease_track_height": "Spurhöhe verringern" +"menu.view.show_all": "Alle Spuren anzeigen" +"menu.view.full_screen": "Vollbild" +"menu.view.full_screen_viewer": "Viewer im Vollbild" +"menu.view.preferences": "Einstellungen…" +"menu.playback.play_pause": "Wiedergabe/Pause" +"menu.playback.play_in_to_out": "Von Ein bis Aus abspielen" +"menu.playback.loop": "Schleife" +"menu.playback.shuttle_left": "Shuttle links" +"menu.playback.shuttle_stop": "Shuttle anhalten" +"menu.playback.shuttle_right": "Shuttle rechts" +"menu.playback.prev_frame": "Vorheriges Einzelbild" +"menu.playback.next_frame": "Nächstes Einzelbild" +"menu.playback.to_start": "Zum Sequenzanfang springen" +"menu.playback.go_to_end": "Zum Sequenzende springen" +"menu.playback.prev_cut": "Zum vorherigen Schnitt springen" +"menu.playback.next_cut": "Zum nächsten Schnitt springen" +"menu.playback.go_to_in": "Zum Einstiegspunkt springen" +"menu.playback.go_to_out": "Zum Ausstiegspunkt springen" +"menu.sequence.add_video_track": "Videospur hinzufügen" +"menu.sequence.add_audio_track": "Audiospur hinzufügen" +"menu.sequence.remove_track": "Ausgewählte Spur entfernen" +"menu.sequence.remove_marker": "Marker entfernen" +"menu.sequence.set_workarea": "Arbeitsbereich festlegen" +"menu.sequence.clear_workarea": "Arbeitsbereich löschen" +"menu.sequence.cache": "Render-Cache" +"menu.sequence.cache_in_out": "Render-Cache von Ein bis Aus" +"menu.sequence.cache_clear": "Render-Cache löschen" +"menu.sequence.settings": "Sequenzeinstellungen…" +"menu.window.project": "Projekt" +"menu.window.source_viewer": "Quell-Viewer" +"menu.window.program_viewer": "Programm-Viewer" +"menu.window.node_editor": "Node-Editor" +"menu.window.inspector": "Inspektor" +"menu.window.history": "Verlauf" +"menu.window.timeline": "Zeitleiste" +"menu.window.effect_library": "Effektbibliothek" +"menu.window.multicam": "Multikamera" +"menu.window.maximize_panel": "Panel maximieren" +"menu.window.reset_layout": "Layout zurücksetzen" +"menu.tools.pointer": "Zeiger" +"menu.tools.track_select": "Spur auswählen" +"menu.tools.edit": "Bearbeiten" +"menu.tools.ripple": "Ripple-Bearbeitung" +"menu.tools.rolling": "Rolling-Bearbeitung" +"menu.tools.razor_tool": "Rasierer" +"menu.tools.slip": "Slip-Bearbeitung" +"menu.tools.slide": "Slide-Bearbeitung" +"menu.tools.hand": "Hand" +"menu.tools.zoom_tool": "Zoom-Werkzeug" +"menu.tools.transition": "Übergang" +"menu.tools.add": "Hinzufügen" +"menu.tools.record": "Aufnehmen" +"menu.tools.addable.empty": "Leerer Clip" +"menu.tools.addable.bars": "Farbbalken" +"menu.tools.addable.shape": "Form" +"menu.tools.addable.solid": "Einfarbig" +"menu.tools.addable.title": "Titel" +"menu.tools.addable.tone": "Testton" +"menu.tools.addable.subtitle": "Untertitel" +"menu.tools.snapping": "Einrasten" +"menu.tools.use_proxy": "Proxy-Medien verwenden" +"menu.tools.proxy_settings": "Proxy-Einstellungen…" +"menu.help.action_search": "Aktionen durchsuchen…" +"menu.help.feedback": "Feedback senden…" +"menu.help.about": "Über Oak…" +"shortcut.space": "Leertaste" +"panel.project": "Projekt" +"panel.source_viewer": "Quell-Viewer" +"panel.program_viewer": "Programm-Viewer" +"panel.node_editor": "Node-Editor" +"panel.inspector": "Inspektor" +"panel.history": "Verlauf" +"panel.timeline": "Zeitleiste" +"panel.effect_library": "Effektbibliothek" +"panel.multicam": "Multikamera" +"multicam.no_multicam": "Kein Multikamera-Clip erkannt" +"multicam.switch_1": "Zu Kamera 1 wechseln" +"multicam.switch_2": "Zu Kamera 2 wechseln" +"multicam.switch_3": "Zu Kamera 3 wechseln" +"multicam.switch_4": "Zu Kamera 4 wechseln" +"multicam.switch_5": "Zu Kamera 5 wechseln" +"multicam.switch_6": "Zu Kamera 6 wechseln" +"multicam.switch_7": "Zu Kamera 7 wechseln" +"multicam.switch_8": "Zu Kamera 8 wechseln" +"multicam.switch_9": "Zu Kamera 9 wechseln" +"effect_library.hint": "Doppelklicken, um zum ausgewählten Clip hinzuzufügen" +"effect_library.group.builtin": "Integriert" +"status.ready": "Bereit" +"status.cache": "Cache: Aktiviert" +"status.proxy.on": "Proxy: Ein" +"status.proxy.off": "Proxy: Aus" +"status.storage.written": "Bibliothek: gespeichert" +"status.storage.unbound": "Bibliothek: aus" +"status.storage.error": "Bibliothek: Schreibfehler" +"status.untitled": "Unbenanntes Projekt" +"status.backend": "Render-Engine:" +"manager.title": "Projektmanager" +"manager.new": "Neues Projekt" +"manager.new.default_name": "Unbenanntes Projekt" +"manager.open": "Öffnen" +"manager.rename": "Umbenennen…" +"manager.rename.title": "Projekt umbenennen" +"manager.rename.label": "Neuer Name" +"manager.duplicate": "Duplizieren" +"manager.delete": "Löschen" +"manager.delete.title": "Projekt löschen" +"manager.delete.confirm": "Projekt „{name}“ aus der Bibliothek löschen? Dieser Vorgang kann nicht rückgängig gemacht werden." +"manager.import": "Importieren…" +"manager.import.title": "Projekt importieren" +"manager.export": "Exportieren…" +"manager.export.title": "Projekt exportieren" +"manager.col.name": "Name" +"manager.col.modified": "Geändert" +"manager.col.duration": "Dauer" +"manager.col.tracks": "Spuren" +"manager.col.clips": "Clips" +"manager.col.footage": "Material" +"manager.empty": "Noch keine Projekte in der Bibliothek." +"timeline.tool.select": "Auswahl" +"timeline.tool.razor": "Rasierer" +"timeline.tool.ripple": "Ripple-Bearbeitung" +"timeline.tool.slip": "Slip-Bearbeitung" +"timeline.tool.roll": "Rolling-Bearbeitung" +"timeline.tool.zoom": "Zoom-Werkzeug" +"timeline.tool.slide": "Slide-Bearbeitung" +"timeline.tool.track_select": "Spur auswählen" +"timeline.zoom": "Zoomen" +"timeline.zoom_in": "Vergrößern" +"timeline.zoom_out": "Verkleinern" +"timeline.track_height": "Spurhöhe" +"timeline.snap": "Einrasten" +"timeline.add_video_track": "Videospur hinzufügen" +"timeline.add_audio_track": "Audiospur hinzufügen" +"bin.footage": "Material" +"bin.music": "Musik" +"history.command": "Befehl" +"history.empty": "Kein Verlauf" +"history.jump_here": "Zu diesem Schritt springen" +"node.fit": "Anpassen" +"viewer.picture": "Bild" +"viewer.scopes": "Oszilloskope" +"scope.histogram": "Histogramm" +"scope.waveform": "Wellenform" +"scope.vectorscope": "Vektorskop" +"viewer.in_point": "Einstiegspunkt setzen" +"viewer.step_back": "Vorheriges Einzelbild" +"viewer.play": "Abspielen" +"viewer.pause": "Pause" +"viewer.step_forward": "Nächstes Einzelbild" +"viewer.out_point": "Ausstiegspunkt setzen" +"viewer.clear_range": "Ein/Aus-Bereich löschen" +"explorer.tree": "Baum" +"explorer.icons": "Symbole" +"viewer.safe_frames": "Sichere Ränder" +"viewer.zoom": "Zoomen" +"viewer.no_frame_source": "Keine Bildquelle" +"effect_stack.empty": "Keine Auswahl" +"effect_stack.add": "+ Effekt hinzufügen" +"inspector.params": "Keine bearbeitbaren Parameter" +"inspector.add_effect": "Effekt hinzufügen" +"inspector.badge.openfx": "OpenFX" +"ofx.progress.title": "OpenFX-Plugin-Fortschritt" +"ofx.color.hex": "Hexadezimal" +"ofx.color.ok": "Okay" +"ofx.color.cancel": "Abbrechen" +"ofx.color.invalid": "Ungültige Hexadezimalfarbe" +"dialog.cancel": "Abbrechen" +"dialog.close": "Schließen" +"file.open.title": "Projekt öffnen" +"file.import_footage.title": "Material importieren" +"preferences.title": "Einstellungen" +"preferences.section.general": "Allgemein" +"preferences.section.render": "Rendering" +"preferences.section.cache": "Cache" +"preferences.section.proxy": "Proxy" +"preferences.section.color": "Farbe" +"preferences.color.enable": "Farbmanagement anzeigen (Monitor-ICC)" +"preferences.color.custom": "Benutzerdefiniertes ICC-Profil" +"preferences.color.browse": "Durchsuchen…" +"preferences.color.restart_hint": "Eine Änderung des Modus wird nach einem Neustart wirksam." +"preferences.section.project": "Projekt" +"preferences.section.audio": "Audio" +"preferences.backend": "Renderer-Backend" +"preferences.hwdecode.enable": "Hardwarebeschleunigte Videodekodierung (VideoToolbox / VA-API / NVDEC / D3D11VA)" +"preferences.backend.placeholder": "Backend auswählen…" +"preferences.language": "Sprache" +"preferences.language.placeholder": "Sprache auswählen…" +"preferences.theme": "Design" +"preferences.theme.dark": "Olive Dark" +"preferences.theme.light": "Olive Light" +"preferences.cache.dir": "Verzeichnis für den Platten-Cache" +"preferences.cache.browse": "Durchsuchen…" +"preferences.proxy.enable": "Proxy-Medien verwenden" +"preferences.proxy.resolution": "Proxy-Auflösung" +"preferences.proxy.full": "Volle Auflösung" +"preferences.snapshot.interval": "Intervall der automatischen Speicherung (Momentaufnahme) in Sekunden" +"preferences.transition.default": "Standardlänge von Übergängen in Sekunden" +"preferences.audio.output": "Audioausgabegerät" +"preferences.audio.output.placeholder": "Ausgabegerät auswählen…" +"preferences.audio.input": "Audioeingabegerät" +"preferences.audio.input.placeholder": "Eingabegerät auswählen…" +"preferences.audio.default": "Systemstandard" +"preferences.hint": "Das Renderer-Backend wird beim nächsten Start auf den Render-Worker angewendet; alle anderen Einstellungen wirken sofort und werden beim Beenden gespeichert." +"preferences.tab.general": "Allgemein" +"preferences.tab.keyboard": "Tastatur" +"preferences.section.keyboard": "Tastatur" +"preferences.keyboard.search": "Nach Aktion oder Tastenkürzel suchen" +"preferences.keyboard.action": "Aktion" +"preferences.keyboard.shortcut": "Tastenkürzel" +"preferences.keyboard.click_to_edit": "Zum Festlegen des Tastenkürzels klicken…" +"preferences.keyboard.capturing": "Tasten drücken… (Esc zum Abbrechen)" +"preferences.keyboard.unbound": "Keine" +"preferences.keyboard.import": "Importieren…" +"preferences.keyboard.export": "Exportieren…" +"preferences.keyboard.reset_selected": "Auswahl zurücksetzen" +"preferences.keyboard.reset_all": "Alle zurücksetzen" +"preferences.keyboard.reset_all.confirm": "Möchten Sie wirklich alle Tastenkürzel auf die Standardwerte zurücksetzen?" +"preferences.keyboard.conflict": "Das Tastenkürzel ist bereits an {action} gebunden; die Zuordnung wurde verschoben." +"preferences.keyboard.cleared": "Tastenkürzel gelöscht (Aktion nicht mehr zugeordnet)." +"preferences.keyboard.reset": "Tastenkürzel auf den Standardwert zurückgesetzt." +"preferences.keyboard.reset_all_done": "Alle Tastenkürzel wurden auf die Standardwerte zurückgesetzt." +"preferences.keyboard.imported": "Tastenkürzel erfolgreich importiert." +"preferences.keyboard.exported": "Tastenkürzel erfolgreich exportiert." +"preferences.keyboard.import_failed": "Die Datei konnte nicht zum Lesen geöffnet werden." +"preferences.keyboard.export_failed": "Die Datei konnte nicht zum Schreiben geöffnet werden." +"actionsearch.search_placeholder": "Nach Aktion suchen…" +"actionsearch.empty": "Keine passenden Aktionen" +"actionsearch.no_actions": "Keine Aktionen verfügbar" +"export.title": "Sequenz exportieren" +"export.format": "Format" +"export.format.placeholder": "Format auswählen…" +"export.path": "Ausgabepfad" +"export.run": "Exportieren" +"export.hint": "Die Sequenz wird über den oaktask-Exportpfad exportiert; der Fortschritt wird im Dialog angezeigt." +"export.progress.title": "Exportieren" +"export.progress.label": "Frames werden gerendert…" +"proxydialog.title": "Proxy-Einstellungen" +"proxydialog.footage_group": "Material" +"proxydialog.no_footage": "Kein Material im Projekt" +"proxydialog.custom": "Benutzerdefinierte Einstellungen für das Material verwenden" +"proxydialog.custom_suffix": " (benutzerdefinierte Einstellungen)" +"proxydialog.global": "Globale Proxy-Einstellungen" +"proxydialog.resolution": "Proxy-Auflösung" +"proxydialog.resolution.custom": "Benutzerdefinierte Größe" +"proxydialog.resolution.half": "1/2 der Quelle" +"proxydialog.resolution.quarter": "1/4 der Quelle" +"proxydialog.resolution.eighth": "1/8 der Quelle" +"proxydialog.width": "Proxy-Breite" +"proxydialog.height": "Proxy-Höhe" +"proxydialog.crf": "Proxy-CRF" +"proxydialog.preset": "Proxy-Preset" +"proxydialog.include_audio": "Audio in Proxys einschließen" +"proxydialog.ffmpeg": "ffmpeg-Programm" +"proxydialog.generate": "Proxys generieren" +"proxydialog.delete": "Proxys löschen" +"proxydialog.close": "Schließen" +"proxydialog.state.missing": "Fehlt" +"proxydialog.state.generating": "Wird generiert" +"proxydialog.state.ready": "Bereit" +"proxydialog.state.failed": "Fehlgeschlagen" +"menu.color.label": "Farbkennzeichnung" +"menu.color.red": "Rot" +"menu.color.maroon": "Kastanienbraun" +"menu.color.orange": "Orange" +"menu.color.brown": "Braun" +"menu.color.yellow": "Gelb" +"menu.color.oak": "Eiche" +"menu.color.lime": "Limette" +"menu.color.green": "Grün" +"menu.color.cyan": "Cyan" +"menu.color.teal": "Blaugrün" +"menu.color.blue": "Blau" +"menu.color.navy": "Marineblau" +"menu.color.pink": "Rosa" +"menu.color.purple": "Violett" +"menu.color.silver": "Silber" +"menu.color.gray": "Grau" +"menu.context.properties": "Eigenschaften" +"timeline.context.sync_source_time": "Nach Quellzeit synchronisieren" +"timeline.context.sync_waveform": "Nach Wellenform synchronisieren" +"timeline.context.sync_waveform_speed": "Nach Wellenform synchronisieren (Geschwindigkeit anpassen)" +"timeline.context.cache": "Cache" +"timeline.context.auto_cache": "Automatischer Cache" +"timeline.context.cache_all": "Alle zwischenspeichern" +"timeline.context.cache_in_out": "Ein/Aus-Bereich zwischenspeichern" +"timeline.context.cache_discard": "Verwerfen" +"timeline.context.proxy": "Proxy" +"timeline.context.generate_proxy": "Proxy generieren" +"timeline.context.use_proxy": "Proxy verwenden" +"timeline.context.reveal_proxy": "Proxy anzeigen" +"timeline.context.delete_proxy": "Proxy löschen" +"timeline.context.reveal_in_footage_viewer": "Im Material-Viewer anzeigen" +"timeline.context.reveal_in_project": "Im Projekt anzeigen" +"timeline.context.multicam": "Multikamera" +"timeline.context.use_audio_time_units": "Audiozeiteinheiten verwenden" +"timeline.context.show_thumbnails": "Miniaturen anzeigen" +"timeline.context.thumbnails_off": "Deaktiviert" +"timeline.context.thumbnails_at_in_points": "Nur an Einstiegspunkten" +"timeline.context.thumbnails_on": "Aktiviert" +"timeline.context.show_waveforms": "Wellenformen anzeigen" +"timeline.context.add_video_track": "Videospur hinzufügen" +"timeline.context.add_audio_track": "Audiospur hinzufügen" +"timeline.context.delete_track": "Löschen" +"timeline.context.delete_all_empty": "Alle leeren Spuren löschen" +"timeline.context.timecode_drop_frame": "Drop-Frame" +"timeline.context.timecode_non_drop_frame": "Non-Drop-Frame" +"timeline.context.timecode_seconds": "Sekunden" +"timeline.context.timecode_frames": "Einzelbilder" +"timeline.context.timecode_milliseconds": "Millisekunden" +"node.category.output": "Ausgabe" +"node.category.effect": "Effekt" +"node.category.generator": "Erzeuger" +"node.category.input": "Eingabe" +"node.category.math": "Mathematik" +"node.category.color": "Farbe" +"node.category.distort": "Verzerren" +"node.category.filter": "Filtern" +"node.category.keying": "Freistellen" +"node.category.openfx": "OpenFX" +"node.category.group": "Gruppe" +"project.context.new": "Neu" +"project.context.reveal_in_finder": "Im Finder anzeigen" +"project.context.replace_footage": "Material ersetzen" +"project.context.rename": "Umbenennen" +"project.context.delete": "Löschen" +"project.context.open_in_new_tab": "In neuem Tab öffnen" +"project.context.open_in_new_window": "In neuem Fenster öffnen" +"viewer.context.zoom": "Zoomen" +"viewer.context.zoom_fit": "Anpassen" +"viewer.context.full_screen": "Vollbild" +"viewer.context.playback_resolution": "Wiedergabeauflösung" +"viewer.context.res_full": "Voll" +"viewer.context.res_half": "1/2" +"viewer.context.res_quarter": "1/4" +"viewer.context.res_eighth": "1/8" +"viewer.context.safe_margins": "Sichere Ränder" +"viewer.context.safe_off": "Aus" +"viewer.context.safe_on": "Ein" +"viewer.context.safe_custom": "Benutzerdefiniertes Seitenverhältnis" +"viewer.context.stop_on_last": "Wiedergabe am letzten Einzelbild anhalten" +"viewer.context.audio_waveform": "Audiowellenform" +"viewer.context.wf_automatic": "Automatisch ein-/ausblenden" +"viewer.context.wf_only": "Nur Wellenform anzeigen" +"viewer.context.wf_both": "Viewer und Wellenform anzeigen" +"viewer.context.show_fps": "FPS anzeigen" +"viewer.context.save_frame": "Einzelbild als Bild speichern" +"node.context.group": "Gruppieren" +"node.context.ungroup": "Gruppierung aufheben" +"node.context.open_in_viewer": "Im Viewer öffnen" +"node.context.show_in_param_editor": "Im Parametereditor anzeigen" +"node.context.smooth_edges": "Kanten glätten" +"node.context.direction": "Richtung" +"node.context.dir_top_bottom": "Von oben nach unten" +"node.context.dir_bottom_top": "Von unten nach oben" +"node.context.dir_left_right": "Von links nach rechts" +"node.context.dir_right_left": "Von rechts nach links" +"node.context.add": "Hinzufügen" +"inspector.context.enable": "Aktivieren" +"inspector.context.disable": "Deaktivieren" +"inspector.context.remove": "Entfernen" +"inspector.context.rename": "Umbenennen" diff --git a/assets/i18n/en.yaml b/assets/i18n/en-US.yaml similarity index 99% rename from assets/i18n/en.yaml rename to assets/i18n/en-US.yaml index 54add5061..98c12829d 100644 --- a/assets/i18n/en.yaml +++ b/assets/i18n/en-US.yaml @@ -65,8 +65,7 @@ "menu.view.theme.dark": "Olive Dark" "menu.view.theme.light": "Olive Light" "menu.view.language": "Language" -"menu.view.language.en": "English" -"menu.view.language.zh": "简体中文" +"language.name": "English" "menu.view.zoom_in": "Zoom In" "menu.view.zoom_out": "Zoom Out" "menu.view.increase_track_height": "Increase Track Height" diff --git a/assets/i18n/es.yaml b/assets/i18n/es.yaml new file mode 100644 index 000000000..db7f71313 --- /dev/null +++ b/assets/i18n/es.yaml @@ -0,0 +1,433 @@ +# Oak Video Editor UI strings — Español (es) +# Format: "i18n.key": "Translated text". Add new languages as new files named .yaml. +"menu.file": "Archivo(A)" +"menu.edit": "Editar(E)" +"menu.view": "Ver(V)" +"menu.playback": "Reproducción(R)" +"menu.sequence": "Secuencia(S)" +"menu.window": "Ventana(V)" +"menu.tools": "Herramientas(H)" +"menu.help": "Ayuda(A)" +"menu.file.new": "Nuevo" +"menu.file.new_project": "Nuevo proyecto…" +"menu.file.new_sequence": "Nueva secuencia…" +"menu.file.new_folder": "Nueva carpeta" +"menu.file.open_project": "Abrir archivo de proyecto…" +"menu.file.open_library": "Abrir desde la biblioteca…" +"menu.file.open_recent": "Abrir recientes" +"menu.file.clear_recent": "Borrar lista de recientes" +"menu.file.project_manager": "Administrador de proyectos…" +"menu.file.import_footage": "Importar material…" +"menu.file.export_project": "Exportar archivo de proyecto…" +"menu.file.save_as": "Guardar como…" +"menu.file.revert": "Revertir" +"menu.file.export_media": "Exportar medios…" +"menu.file.project_properties": "Propiedades del proyecto…" +"menu.file.close": "Cerrar proyecto" +"menu.file.quit": "Salir" +"menu.edit.undo": "Deshacer" +"menu.edit.redo": "Rehacer" +"menu.edit.cut": "Cortar" +"menu.edit.copy": "Copiar" +"menu.edit.paste": "Pegar" +"menu.edit.paste_insert": "Pegar como inserción" +"menu.edit.duplicate": "Duplicar" +"menu.edit.rename": "Cambiar nombre" +"menu.edit.delete": "Eliminar" +"menu.edit.ripple_delete": "Eliminación en cadena" +"menu.edit.split": "Dividir en el cabezal" +"menu.edit.speed_duration": "Velocidad/Duración…" +"menu.edit.default_transition": "Establecer como transición predeterminada" +"menu.edit.link_unlink": "Vincular/Desvincular" +"menu.edit.enable_disable": "Activar/Desactivar" +"menu.edit.nest": "Anidar" +"menu.edit.select_all": "Seleccionar todo" +"menu.edit.deselect_all": "Deseleccionar todo" +"menu.edit.insert": "Insertar" +"menu.edit.overwrite": "Sobrescribir" +"menu.edit.ripple_to_in": "Ondulación hasta la entrada" +"menu.edit.ripple_to_out": "Ondulación hasta la salida" +"menu.edit.edit_to_in": "Editar hasta la entrada" +"menu.edit.edit_to_out": "Editar hasta la salida" +"menu.edit.nudge_left": "Desplazar a la izquierda" +"menu.edit.nudge_right": "Desplazar a la derecha" +"menu.edit.move_in_to_playhead": "Mover el punto de entrada al cabezal" +"menu.edit.move_out_to_playhead": "Mover el punto de salida al cabezal" +"menu.edit.set_in_point": "Establecer punto de entrada" +"menu.edit.set_out_point": "Establecer punto de salida" +"menu.edit.reset_in": "Restablecer punto de entrada" +"menu.edit.reset_out": "Restablecer punto de salida" +"menu.edit.clear_in_out": "Borrar entrada/salida" +"menu.edit.delete_in_out": "Eliminar de entrada a salida" +"menu.edit.ripple_delete_in_out": "Eliminación en cadena de entrada a salida" +"menu.edit.marker": "Añadir marcador" +"menu.view.theme": "Tema" +"menu.view.theme.dark": "Olive Dark" +"menu.view.theme.light": "Olive Light" +"menu.view.language": "Idioma" +"language.name": "Español" +"menu.view.zoom_in": "Acercar" +"menu.view.zoom_out": "Alejar" +"menu.view.increase_track_height": "Aumentar altura de pista" +"menu.view.decrease_track_height": "Reducir altura de pista" +"menu.view.show_all": "Mostrar todas las pistas" +"menu.view.full_screen": "Pantalla completa" +"menu.view.full_screen_viewer": "Visor a pantalla completa" +"menu.view.preferences": "Preferencias…" +"menu.playback.play_pause": "Reproducir/Pausar" +"menu.playback.play_in_to_out": "Reproducir de entrada a salida" +"menu.playback.loop": "Bucle" +"menu.playback.shuttle_left": "Lanzadera a la izquierda" +"menu.playback.shuttle_stop": "Detener lanzadera" +"menu.playback.shuttle_right": "Lanzadera a la derecha" +"menu.playback.prev_frame": "Fotograma anterior" +"menu.playback.next_frame": "Fotograma siguiente" +"menu.playback.to_start": "Ir al inicio de la secuencia" +"menu.playback.go_to_end": "Ir al final de la secuencia" +"menu.playback.prev_cut": "Ir al corte anterior" +"menu.playback.next_cut": "Ir al corte siguiente" +"menu.playback.go_to_in": "Ir al punto de entrada" +"menu.playback.go_to_out": "Ir al punto de salida" +"menu.sequence.add_video_track": "Añadir pista de vídeo" +"menu.sequence.add_audio_track": "Añadir pista de audio" +"menu.sequence.remove_track": "Eliminar pista seleccionada" +"menu.sequence.remove_marker": "Quitar marcador" +"menu.sequence.set_workarea": "Establecer área de trabajo" +"menu.sequence.clear_workarea": "Borrar área de trabajo" +"menu.sequence.cache": "Caché de renderizado" +"menu.sequence.cache_in_out": "Caché de renderizado de entrada a salida" +"menu.sequence.cache_clear": "Borrar caché de renderizado" +"menu.sequence.settings": "Configuración de secuencia…" +"menu.window.project": "Proyecto" +"menu.window.source_viewer": "Visor de origen" +"menu.window.program_viewer": "Visor de programa" +"menu.window.node_editor": "Editor de nodos" +"menu.window.inspector": "Inspector" +"menu.window.history": "Historial" +"menu.window.timeline": "Línea de tiempo" +"menu.window.effect_library": "Biblioteca de efectos" +"menu.window.multicam": "Multicámara" +"menu.window.maximize_panel": "Maximizar panel" +"menu.window.reset_layout": "Restablecer distribución" +"menu.tools.pointer": "Puntero" +"menu.tools.track_select": "Selección de pista" +"menu.tools.edit": "Edición" +"menu.tools.ripple": "Ondulación" +"menu.tools.rolling": "Rodadura" +"menu.tools.razor_tool": "Navaja" +"menu.tools.slip": "Deslizamiento" +"menu.tools.slide": "Desplazamiento" +"menu.tools.hand": "Mano" +"menu.tools.zoom_tool": "Zoom" +"menu.tools.transition": "Transición" +"menu.tools.add": "Añadir" +"menu.tools.record": "Grabar" +"menu.tools.addable.empty": "Clip vacío" +"menu.tools.addable.bars": "Barras de color" +"menu.tools.addable.shape": "Forma" +"menu.tools.addable.solid": "Color sólido" +"menu.tools.addable.title": "Título" +"menu.tools.addable.tone": "Tono de prueba" +"menu.tools.addable.subtitle": "Subtítulo" +"menu.tools.snapping": "Ajuste" +"menu.tools.use_proxy": "Usar medios proxy" +"menu.tools.proxy_settings": "Configuración de proxy…" +"menu.help.action_search": "Buscar acciones…" +"menu.help.feedback": "Enviar comentarios…" +"menu.help.about": "Acerca de Oak…" +"shortcut.space": "Barra espaciadora" +"panel.project": "Proyecto" +"panel.source_viewer": "Visor de origen" +"panel.program_viewer": "Visor de programa" +"panel.node_editor": "Editor de nodos" +"panel.inspector": "Inspector" +"panel.history": "Historial" +"panel.timeline": "Línea de tiempo" +"panel.effect_library": "Biblioteca de efectos" +"panel.multicam": "Multicámara" +"multicam.no_multicam": "No se ha detectado ningún clip multicámara" +"multicam.switch_1": "Cambiar a cámara 1" +"multicam.switch_2": "Cambiar a cámara 2" +"multicam.switch_3": "Cambiar a cámara 3" +"multicam.switch_4": "Cambiar a cámara 4" +"multicam.switch_5": "Cambiar a cámara 5" +"multicam.switch_6": "Cambiar a cámara 6" +"multicam.switch_7": "Cambiar a cámara 7" +"multicam.switch_8": "Cambiar a cámara 8" +"multicam.switch_9": "Cambiar a cámara 9" +"effect_library.hint": "Haz doble clic para añadir al clip seleccionado" +"effect_library.group.builtin": "Integradas" +"status.ready": "Listo" +"status.cache": "Caché: activada" +"status.proxy.on": "Proxy: activado" +"status.proxy.off": "Proxy: desactivado" +"status.storage.written": "Biblioteca: guardada" +"status.storage.unbound": "Biblioteca: desactivada" +"status.storage.error": "Biblioteca: error de escritura" +"status.untitled": "Proyecto sin título" +"status.backend": "Motor:" +"manager.title": "Administrador de proyectos" +"manager.new": "Nuevo proyecto" +"manager.new.default_name": "Proyecto sin título" +"manager.open": "Abrir" +"manager.rename": "Cambiar nombre…" +"manager.rename.title": "Cambiar nombre del proyecto" +"manager.rename.label": "Nuevo nombre" +"manager.duplicate": "Duplicar" +"manager.delete": "Eliminar" +"manager.delete.title": "Eliminar proyecto" +"manager.delete.confirm": "¿Eliminar el proyecto «{name}» de la biblioteca? Esta acción no se puede deshacer." +"manager.import": "Importar…" +"manager.import.title": "Importar proyecto" +"manager.export": "Exportar…" +"manager.export.title": "Exportar proyecto" +"manager.col.name": "Nombre" +"manager.col.modified": "Modificado" +"manager.col.duration": "Duración" +"manager.col.tracks": "Pistas" +"manager.col.clips": "Clips" +"manager.col.footage": "Material" +"manager.empty": "Aún no hay proyectos en la biblioteca." +"timeline.tool.select": "Selección" +"timeline.tool.razor": "Navaja" +"timeline.tool.ripple": "Ondulación" +"timeline.tool.slip": "Deslizamiento" +"timeline.tool.roll": "Rodadura" +"timeline.tool.zoom": "Zoom" +"timeline.tool.slide": "Desplazamiento" +"timeline.tool.track_select": "Selección de pista" +"timeline.zoom": "Zoom" +"timeline.zoom_in": "Acercar" +"timeline.zoom_out": "Alejar" +"timeline.track_height": "Altura de pista" +"timeline.snap": "Ajuste" +"timeline.add_video_track": "Añadir pista de vídeo" +"timeline.add_audio_track": "Añadir pista de audio" +"bin.footage": "Material" +"bin.music": "Música" +"history.command": "Comando" +"history.empty": "Sin historial" +"history.jump_here": "Saltar a este paso" +"node.fit": "Ajustar" +"viewer.picture": "Imagen" +"viewer.scopes": "Osciloscopios" +"scope.histogram": "Histograma" +"scope.waveform": "Forma de onda" +"scope.vectorscope": "Vectorscopio" +"viewer.in_point": "Establecer punto de entrada" +"viewer.step_back": "Fotograma anterior" +"viewer.play": "Reproducir" +"viewer.pause": "Pausar" +"viewer.step_forward": "Fotograma siguiente" +"viewer.out_point": "Establecer punto de salida" +"viewer.clear_range": "Borrar rango entrada/salida" +"explorer.tree": "Árbol" +"explorer.icons": "Iconos" +"viewer.safe_frames": "Marcos de seguridad" +"viewer.zoom": "Zoom" +"viewer.no_frame_source": "Sin fuente de fotogramas" +"effect_stack.empty": "Sin selección" +"effect_stack.add": "+ Añadir efecto" +"inspector.params": "No hay parámetros editables" +"inspector.add_effect": "Añadir efecto" +"inspector.badge.openfx": "OpenFX" +"ofx.progress.title": "Progreso del complemento OpenFX" +"ofx.color.hex": "Hexadecimal" +"ofx.color.ok": "Aceptar" +"ofx.color.cancel": "Cancelar" +"ofx.color.invalid": "Color hexadecimal no válido" +"dialog.cancel": "Cancelar" +"dialog.close": "Cerrar" +"file.open.title": "Abrir proyecto" +"file.import_footage.title": "Importar material" +"preferences.title": "Preferencias" +"preferences.section.general": "General" +"preferences.section.render": "Renderizado" +"preferences.section.cache": "Caché" +"preferences.section.proxy": "Proxy" +"preferences.section.color": "Color" +"preferences.color.enable": "Mostrar gestión de color (ICC de pantalla)" +"preferences.color.custom": "Perfil ICC personalizado" +"preferences.color.browse": "Examinar…" +"preferences.color.restart_hint": "Un cambio de modo surte efecto tras reiniciar." +"preferences.section.project": "Proyecto" +"preferences.section.audio": "Audio" +"preferences.backend": "Motor de renderizado" +"preferences.hwdecode.enable": "Decodificación de vídeo acelerada por hardware (VideoToolbox / VA-API / NVDEC / D3D11VA)" +"preferences.backend.placeholder": "Seleccionar un motor…" +"preferences.language": "Idioma" +"preferences.language.placeholder": "Seleccionar un idioma…" +"preferences.theme": "Tema" +"preferences.theme.dark": "Olive Dark" +"preferences.theme.light": "Olive Light" +"preferences.cache.dir": "Directorio de caché en disco" +"preferences.cache.browse": "Examinar…" +"preferences.proxy.enable": "Usar medios proxy" +"preferences.proxy.resolution": "Resolución del proxy" +"preferences.proxy.full": "Resolución completa" +"preferences.snapshot.interval": "Intervalo de guardado automático (instantánea), en segundos" +"preferences.transition.default": "Duración predeterminada de las transiciones, en segundos" +"preferences.audio.output": "Dispositivo de salida de audio" +"preferences.audio.output.placeholder": "Seleccionar un dispositivo de salida…" +"preferences.audio.input": "Dispositivo de entrada de audio" +"preferences.audio.input.placeholder": "Seleccionar un dispositivo de entrada…" +"preferences.audio.default": "Predeterminado del sistema" +"preferences.hint": "El motor de renderizado se aplica al trabajador de renderizado en el próximo inicio; el resto de ajustes surten efecto de inmediato y se guardan al salir." +"preferences.tab.general": "General" +"preferences.tab.keyboard": "Teclado" +"preferences.section.keyboard": "Teclado" +"preferences.keyboard.search": "Buscar acción o atajo" +"preferences.keyboard.action": "Acción" +"preferences.keyboard.shortcut": "Atajo" +"preferences.keyboard.click_to_edit": "Haz clic para establecer el atajo…" +"preferences.keyboard.capturing": "Pulsa las teclas… (Esc para cancelar)" +"preferences.keyboard.unbound": "Ninguno" +"preferences.keyboard.import": "Importar…" +"preferences.keyboard.export": "Exportar…" +"preferences.keyboard.reset_selected": "Restablecer selección" +"preferences.keyboard.reset_all": "Restablecer todo" +"preferences.keyboard.reset_all.confirm": "¿Seguro que quieres restablecer todos los atajos de teclado a sus valores predeterminados?" +"preferences.keyboard.conflict": "El atajo ya está asignado a {action}; la asignación se ha movido." +"preferences.keyboard.cleared": "Atajo borrado (acción sin asignar)." +"preferences.keyboard.reset": "Atajo restablecido a su valor predeterminado." +"preferences.keyboard.reset_all_done": "Todos los atajos se han restablecido a sus valores predeterminados." +"preferences.keyboard.imported": "Atajos importados correctamente." +"preferences.keyboard.exported": "Atajos exportados correctamente." +"preferences.keyboard.import_failed": "No se pudo abrir el archivo para leerlo." +"preferences.keyboard.export_failed": "No se pudo abrir el archivo para escribir." +"actionsearch.search_placeholder": "Buscar acción…" +"actionsearch.empty": "No hay acciones coincidentes" +"actionsearch.no_actions": "No hay acciones disponibles" +"export.title": "Exportar secuencia" +"export.format": "Formato" +"export.format.placeholder": "Seleccionar un formato…" +"export.path": "Ruta de salida" +"export.run": "Exportar" +"export.hint": "La secuencia se exporta a través de la ruta de exportación oaktask; el progreso se muestra en el diálogo." +"export.progress.title": "Exportando" +"export.progress.label": "Renderizando fotogramas…" +"proxydialog.title": "Configuración de proxy" +"proxydialog.footage_group": "Material" +"proxydialog.no_footage": "No hay material en el proyecto" +"proxydialog.custom": "Usar configuración personalizada para el material" +"proxydialog.custom_suffix": " (configuración personalizada)" +"proxydialog.global": "Configuración global de proxy" +"proxydialog.resolution": "Resolución del proxy" +"proxydialog.resolution.custom": "Tamaño personalizado" +"proxydialog.resolution.half": "1/2 del origen" +"proxydialog.resolution.quarter": "1/4 del origen" +"proxydialog.resolution.eighth": "1/8 del origen" +"proxydialog.width": "Ancho del proxy" +"proxydialog.height": "Alto del proxy" +"proxydialog.crf": "CRF del proxy" +"proxydialog.preset": "Preajuste del proxy" +"proxydialog.include_audio": "Incluir audio en los proxies" +"proxydialog.ffmpeg": "Ejecutable de ffmpeg" +"proxydialog.generate": "Generar proxies" +"proxydialog.delete": "Eliminar proxies" +"proxydialog.close": "Cerrar" +"proxydialog.state.missing": "Falta" +"proxydialog.state.generating": "Generando" +"proxydialog.state.ready": "Listo" +"proxydialog.state.failed": "Error" +"menu.color.label": "Etiqueta de color" +"menu.color.red": "Rojo" +"menu.color.maroon": "Granate" +"menu.color.orange": "Naranja" +"menu.color.brown": "Marrón" +"menu.color.yellow": "Amarillo" +"menu.color.oak": "Roble" +"menu.color.lime": "Lima" +"menu.color.green": "Verde" +"menu.color.cyan": "Cian" +"menu.color.teal": "Verde azulado" +"menu.color.blue": "Azul" +"menu.color.navy": "Azul marino" +"menu.color.pink": "Rosa" +"menu.color.purple": "Púrpura" +"menu.color.silver": "Plata" +"menu.color.gray": "Gris" +"menu.context.properties": "Propiedades" +"timeline.context.sync_source_time": "Sincronizar por tiempo de origen" +"timeline.context.sync_waveform": "Sincronizar por forma de onda" +"timeline.context.sync_waveform_speed": "Sincronizar por forma de onda (ajustar velocidad)" +"timeline.context.cache": "Caché" +"timeline.context.auto_cache": "Caché automática" +"timeline.context.cache_all": "Almacenar todo en caché" +"timeline.context.cache_in_out": "Almacenar entrada/salida en caché" +"timeline.context.cache_discard": "Descartar" +"timeline.context.proxy": "Proxy" +"timeline.context.generate_proxy": "Generar proxy" +"timeline.context.use_proxy": "Usar proxy" +"timeline.context.reveal_proxy": "Mostrar proxy" +"timeline.context.delete_proxy": "Eliminar proxy" +"timeline.context.reveal_in_footage_viewer": "Mostrar en el visor de material" +"timeline.context.reveal_in_project": "Mostrar en el proyecto" +"timeline.context.multicam": "Multicámara" +"timeline.context.use_audio_time_units": "Usar unidades de tiempo de audio" +"timeline.context.show_thumbnails": "Mostrar miniaturas" +"timeline.context.thumbnails_off": "Desactivadas" +"timeline.context.thumbnails_at_in_points": "Solo en puntos de entrada" +"timeline.context.thumbnails_on": "Activadas" +"timeline.context.show_waveforms": "Mostrar formas de onda" +"timeline.context.add_video_track": "Añadir pista de vídeo" +"timeline.context.add_audio_track": "Añadir pista de audio" +"timeline.context.delete_track": "Eliminar" +"timeline.context.delete_all_empty": "Eliminar todas las pistas vacías" +"timeline.context.timecode_drop_frame": "Drop-frame" +"timeline.context.timecode_non_drop_frame": "Non-drop-frame" +"timeline.context.timecode_seconds": "Segundos" +"timeline.context.timecode_frames": "Fotogramas" +"timeline.context.timecode_milliseconds": "Milisegundos" +"node.category.output": "Salida" +"node.category.effect": "Efecto" +"node.category.generator": "Generador" +"node.category.input": "Entrada" +"node.category.math": "Matemáticas" +"node.category.color": "Color" +"node.category.distort": "Distorsión" +"node.category.filter": "Filtro" +"node.category.keying": "Incrustación" +"node.category.openfx": "OpenFX" +"node.category.group": "Grupo" +"project.context.new": "Nuevo" +"project.context.reveal_in_finder": "Mostrar en Finder" +"project.context.replace_footage": "Reemplazar material" +"project.context.rename": "Cambiar nombre" +"project.context.delete": "Eliminar" +"project.context.open_in_new_tab": "Abrir en pestaña nueva" +"project.context.open_in_new_window": "Abrir en ventana nueva" +"viewer.context.zoom": "Zoom" +"viewer.context.zoom_fit": "Ajustar" +"viewer.context.full_screen": "Pantalla completa" +"viewer.context.playback_resolution": "Resolución de reproducción" +"viewer.context.res_full": "Completa" +"viewer.context.res_half": "1/2" +"viewer.context.res_quarter": "1/4" +"viewer.context.res_eighth": "1/8" +"viewer.context.safe_margins": "Márgenes seguros" +"viewer.context.safe_off": "Desactivado" +"viewer.context.safe_on": "Activado" +"viewer.context.safe_custom": "Relación de aspecto personalizada" +"viewer.context.stop_on_last": "Detener reproducción en el último fotograma" +"viewer.context.audio_waveform": "Forma de onda de audio" +"viewer.context.wf_automatic": "Mostrar/ocultar automáticamente" +"viewer.context.wf_only": "Mostrar solo la forma de onda" +"viewer.context.wf_both": "Mostrar visor y forma de onda" +"viewer.context.show_fps": "Mostrar FPS" +"viewer.context.save_frame": "Guardar fotograma como imagen" +"node.context.group": "Agrupar" +"node.context.ungroup": "Desagrupar" +"node.context.open_in_viewer": "Abrir en el visor" +"node.context.show_in_param_editor": "Mostrar en el editor de parámetros" +"node.context.smooth_edges": "Suavizar bordes" +"node.context.direction": "Dirección" +"node.context.dir_top_bottom": "De arriba abajo" +"node.context.dir_bottom_top": "De abajo arriba" +"node.context.dir_left_right": "De izquierda a derecha" +"node.context.dir_right_left": "De derecha a izquierda" +"node.context.add": "Añadir" +"inspector.context.enable": "Activar" +"inspector.context.disable": "Desactivar" +"inspector.context.remove": "Quitar" +"inspector.context.rename": "Cambiar nombre" diff --git a/assets/i18n/fr.yaml b/assets/i18n/fr.yaml new file mode 100644 index 000000000..2f88e3741 --- /dev/null +++ b/assets/i18n/fr.yaml @@ -0,0 +1,433 @@ +# Oak Video Editor UI strings — Français (fr) +# Format: "i18n.key": "Translated text". Add new languages as new files named .yaml. +"menu.file": "Fichier(F)" +"menu.edit": "Édition(E)" +"menu.view": "Affichage(A)" +"menu.playback": "Lecture(L)" +"menu.sequence": "Séquence(S)" +"menu.window": "Fenêtre(F)" +"menu.tools": "Outils(O)" +"menu.help": "Aide(A)" +"menu.file.new": "Nouveau" +"menu.file.new_project": "Nouveau projet…" +"menu.file.new_sequence": "Nouvelle séquence…" +"menu.file.new_folder": "Nouveau dossier" +"menu.file.open_project": "Ouvrir un fichier projet…" +"menu.file.open_library": "Ouvrir depuis la bibliothèque…" +"menu.file.open_recent": "Fichiers récents" +"menu.file.clear_recent": "Effacer la liste des fichiers récents" +"menu.file.project_manager": "Gestionnaire de projets…" +"menu.file.import_footage": "Importer des médias…" +"menu.file.export_project": "Exporter un fichier projet…" +"menu.file.save_as": "Enregistrer sous…" +"menu.file.revert": "Restaurer" +"menu.file.export_media": "Exporter les médias…" +"menu.file.project_properties": "Propriétés du projet…" +"menu.file.close": "Fermer le projet" +"menu.file.quit": "Quitter" +"menu.edit.undo": "Annuler" +"menu.edit.redo": "Rétablir" +"menu.edit.cut": "Couper" +"menu.edit.copy": "Copier" +"menu.edit.paste": "Coller" +"menu.edit.paste_insert": "Coller en insertion" +"menu.edit.duplicate": "Dupliquer" +"menu.edit.rename": "Renommer" +"menu.edit.delete": "Supprimer" +"menu.edit.ripple_delete": "Suppression en ondulation" +"menu.edit.split": "Fractionner au curseur" +"menu.edit.speed_duration": "Vitesse/Durée…" +"menu.edit.default_transition": "Définir comme transition par défaut" +"menu.edit.link_unlink": "Lier/Dissocier" +"menu.edit.enable_disable": "Activer/Désactiver" +"menu.edit.nest": "Imbriquer" +"menu.edit.select_all": "Tout sélectionner" +"menu.edit.deselect_all": "Désélectionner tout" +"menu.edit.insert": "Insérer" +"menu.edit.overwrite": "Écraser" +"menu.edit.ripple_to_in": "Ondulation jusqu'à l'entrée" +"menu.edit.ripple_to_out": "Ondulation jusqu'à la sortie" +"menu.edit.edit_to_in": "Modifier jusqu'à l'entrée" +"menu.edit.edit_to_out": "Modifier jusqu'à la sortie" +"menu.edit.nudge_left": "Décaler à gauche" +"menu.edit.nudge_right": "Décaler à droite" +"menu.edit.move_in_to_playhead": "Déplacer le point d'entrée au curseur" +"menu.edit.move_out_to_playhead": "Déplacer le point de sortie au curseur" +"menu.edit.set_in_point": "Définir le point d'entrée" +"menu.edit.set_out_point": "Définir le point de sortie" +"menu.edit.reset_in": "Réinitialiser le point d'entrée" +"menu.edit.reset_out": "Réinitialiser le point de sortie" +"menu.edit.clear_in_out": "Effacer entrée/sortie" +"menu.edit.delete_in_out": "Supprimer de l'entrée à la sortie" +"menu.edit.ripple_delete_in_out": "Suppression en ondulation de l'entrée à la sortie" +"menu.edit.marker": "Ajouter un marqueur" +"menu.view.theme": "Thème" +"menu.view.theme.dark": "Olive Dark" +"menu.view.theme.light": "Olive Light" +"menu.view.language": "Langue" +"language.name": "Français" +"menu.view.zoom_in": "Zoom avant" +"menu.view.zoom_out": "Zoom arrière" +"menu.view.increase_track_height": "Augmenter la hauteur de piste" +"menu.view.decrease_track_height": "Réduire la hauteur de piste" +"menu.view.show_all": "Afficher toutes les pistes" +"menu.view.full_screen": "Plein écran" +"menu.view.full_screen_viewer": "Visionneuse plein écran" +"menu.view.preferences": "Préférences…" +"menu.playback.play_pause": "Lecture/Pause" +"menu.playback.play_in_to_out": "Lire de l'entrée à la sortie" +"menu.playback.loop": "Boucle" +"menu.playback.shuttle_left": "Navette à gauche" +"menu.playback.shuttle_stop": "Arrêter la navette" +"menu.playback.shuttle_right": "Navette à droite" +"menu.playback.prev_frame": "Image précédente" +"menu.playback.next_frame": "Image suivante" +"menu.playback.to_start": "Aller au début de la séquence" +"menu.playback.go_to_end": "Aller à la fin de la séquence" +"menu.playback.prev_cut": "Aller à la coupe précédente" +"menu.playback.next_cut": "Aller à la coupe suivante" +"menu.playback.go_to_in": "Aller au point d'entrée" +"menu.playback.go_to_out": "Aller au point de sortie" +"menu.sequence.add_video_track": "Ajouter une piste vidéo" +"menu.sequence.add_audio_track": "Ajouter une piste audio" +"menu.sequence.remove_track": "Supprimer la piste sélectionnée" +"menu.sequence.remove_marker": "Supprimer le marqueur" +"menu.sequence.set_workarea": "Définir la zone de travail" +"menu.sequence.clear_workarea": "Effacer la zone de travail" +"menu.sequence.cache": "Cache de rendu" +"menu.sequence.cache_in_out": "Cache de rendu de l'entrée à la sortie" +"menu.sequence.cache_clear": "Effacer le cache de rendu" +"menu.sequence.settings": "Paramètres de séquence…" +"menu.window.project": "Projet" +"menu.window.source_viewer": "Visionneuse source" +"menu.window.program_viewer": "Visionneuse du programme" +"menu.window.node_editor": "Éditeur de nœuds" +"menu.window.inspector": "Inspecteur" +"menu.window.history": "Historique" +"menu.window.timeline": "Montage" +"menu.window.effect_library": "Bibliothèque d'effets" +"menu.window.multicam": "Multicam" +"menu.window.maximize_panel": "Agrandir le panneau" +"menu.window.reset_layout": "Réinitialiser la disposition" +"menu.tools.pointer": "Pointeur" +"menu.tools.track_select": "Sélection de piste" +"menu.tools.edit": "Modifier" +"menu.tools.ripple": "Ondulation" +"menu.tools.rolling": "Roulage" +"menu.tools.razor_tool": "Rasoir" +"menu.tools.slip": "Glissement" +"menu.tools.slide": "Coulissement" +"menu.tools.hand": "Main" +"menu.tools.zoom_tool": "Zoom" +"menu.tools.transition": "Transition" +"menu.tools.add": "Ajouter" +"menu.tools.record": "Enregistrer" +"menu.tools.addable.empty": "Clip vide" +"menu.tools.addable.bars": "Barres de couleur" +"menu.tools.addable.shape": "Forme" +"menu.tools.addable.solid": "Couleur unie" +"menu.tools.addable.title": "Titre" +"menu.tools.addable.tone": "Tonalité de test" +"menu.tools.addable.subtitle": "Sous-titre" +"menu.tools.snapping": "Magnétisme" +"menu.tools.use_proxy": "Utiliser les médias proxy" +"menu.tools.proxy_settings": "Paramètres de proxy…" +"menu.help.action_search": "Rechercher des actions…" +"menu.help.feedback": "Envoyer un retour…" +"menu.help.about": "À propos d'Oak…" +"shortcut.space": "Espace" +"panel.project": "Projet" +"panel.source_viewer": "Visionneuse source" +"panel.program_viewer": "Visionneuse du programme" +"panel.node_editor": "Éditeur de nœuds" +"panel.inspector": "Inspecteur" +"panel.history": "Historique" +"panel.timeline": "Montage" +"panel.effect_library": "Bibliothèque d'effets" +"panel.multicam": "Multicam" +"multicam.no_multicam": "Aucun clip multicaméra détecté" +"multicam.switch_1": "Passer à la caméra 1" +"multicam.switch_2": "Passer à la caméra 2" +"multicam.switch_3": "Passer à la caméra 3" +"multicam.switch_4": "Passer à la caméra 4" +"multicam.switch_5": "Passer à la caméra 5" +"multicam.switch_6": "Passer à la caméra 6" +"multicam.switch_7": "Passer à la caméra 7" +"multicam.switch_8": "Passer à la caméra 8" +"multicam.switch_9": "Passer à la caméra 9" +"effect_library.hint": "Double-cliquer pour ajouter au clip sélectionné" +"effect_library.group.builtin": "Intégré" +"status.ready": "Prêt" +"status.cache": "Cache : activé" +"status.proxy.on": "Proxy : activé" +"status.proxy.off": "Proxy : désactivé" +"status.storage.written": "Bibliothèque : enregistrée" +"status.storage.unbound": "Bibliothèque : désactivée" +"status.storage.error": "Bibliothèque : échec de l'écriture" +"status.untitled": "Projet sans titre" +"status.backend": "Moteur :" +"manager.title": "Gestionnaire de projets" +"manager.new": "Nouveau projet" +"manager.new.default_name": "Projet sans titre" +"manager.open": "Ouvrir" +"manager.rename": "Renommer…" +"manager.rename.title": "Renommer le projet" +"manager.rename.label": "Nouveau nom" +"manager.duplicate": "Dupliquer" +"manager.delete": "Supprimer" +"manager.delete.title": "Supprimer le projet" +"manager.delete.confirm": "Supprimer le projet « {name} » de la bibliothèque ? Cette action est irréversible." +"manager.import": "Importer…" +"manager.import.title": "Importer un projet" +"manager.export": "Exporter…" +"manager.export.title": "Exporter un projet" +"manager.col.name": "Nom" +"manager.col.modified": "Modifié" +"manager.col.duration": "Durée" +"manager.col.tracks": "Pistes" +"manager.col.clips": "Clips" +"manager.col.footage": "Médias" +"manager.empty": "Aucun projet dans la bibliothèque pour le moment." +"timeline.tool.select": "Sélection" +"timeline.tool.razor": "Rasoir" +"timeline.tool.ripple": "Ondulation" +"timeline.tool.slip": "Glissement" +"timeline.tool.roll": "Roulage" +"timeline.tool.zoom": "Zoom" +"timeline.tool.slide": "Coulissement" +"timeline.tool.track_select": "Sélection de piste" +"timeline.zoom": "Zoom" +"timeline.zoom_in": "Zoom avant" +"timeline.zoom_out": "Zoom arrière" +"timeline.track_height": "Hauteur de piste" +"timeline.snap": "Magnétisme" +"timeline.add_video_track": "Ajouter une piste vidéo" +"timeline.add_audio_track": "Ajouter une piste audio" +"bin.footage": "Médias" +"bin.music": "Musique" +"history.command": "Commande" +"history.empty": "Aucun historique" +"history.jump_here": "Aller à cette étape" +"node.fit": "Ajuster" +"viewer.picture": "Image" +"viewer.scopes": "Oscilloscopes" +"scope.histogram": "Histogramme" +"scope.waveform": "Forme d'onde" +"scope.vectorscope": "Vecteurscope" +"viewer.in_point": "Définir le point d'entrée" +"viewer.step_back": "Image précédente" +"viewer.play": "Lecture" +"viewer.pause": "Pause" +"viewer.step_forward": "Image suivante" +"viewer.out_point": "Définir le point de sortie" +"viewer.clear_range": "Effacer la plage entrée/sortie" +"explorer.tree": "Arborescence" +"explorer.icons": "Icônes" +"viewer.safe_frames": "Cadres de sécurité" +"viewer.zoom": "Zoom" +"viewer.no_frame_source": "Aucune source d'image" +"effect_stack.empty": "Aucune sélection" +"effect_stack.add": "+ Ajouter un effet" +"inspector.params": "Aucun paramètre modifiable" +"inspector.add_effect": "Ajouter un effet" +"inspector.badge.openfx": "OpenFX" +"ofx.progress.title": "Progression du plugin OpenFX" +"ofx.color.hex": "Hexadécimal" +"ofx.color.ok": "Valider" +"ofx.color.cancel": "Annuler" +"ofx.color.invalid": "Couleur hexadécimale invalide" +"dialog.cancel": "Annuler" +"dialog.close": "Fermer" +"file.open.title": "Ouvrir le projet" +"file.import_footage.title": "Importer des médias" +"preferences.title": "Préférences" +"preferences.section.general": "Général" +"preferences.section.render": "Rendu" +"preferences.section.cache": "Cache" +"preferences.section.proxy": "Proxy" +"preferences.section.color": "Couleur" +"preferences.color.enable": "Afficher la gestion des couleurs (ICC du moniteur)" +"preferences.color.custom": "Profil ICC personnalisé" +"preferences.color.browse": "Parcourir…" +"preferences.color.restart_hint": "Un changement de mode prend effet après un redémarrage." +"preferences.section.project": "Projet" +"preferences.section.audio": "Audio" +"preferences.backend": "Moteur de rendu" +"preferences.hwdecode.enable": "Décodage vidéo accéléré par le matériel (VideoToolbox / VA-API / NVDEC / D3D11VA)" +"preferences.backend.placeholder": "Sélectionner un moteur…" +"preferences.language": "Langue" +"preferences.language.placeholder": "Sélectionner une langue…" +"preferences.theme": "Thème" +"preferences.theme.dark": "Olive Dark" +"preferences.theme.light": "Olive Light" +"preferences.cache.dir": "Dossier du cache disque" +"preferences.cache.browse": "Parcourir…" +"preferences.proxy.enable": "Utiliser les médias proxy" +"preferences.proxy.resolution": "Résolution du proxy" +"preferences.proxy.full": "Pleine résolution" +"preferences.snapshot.interval": "Intervalle d'enregistrement automatique (instantané), en secondes" +"preferences.transition.default": "Durée par défaut des transitions, en secondes" +"preferences.audio.output": "Périphérique de sortie audio" +"preferences.audio.output.placeholder": "Sélectionner un périphérique de sortie…" +"preferences.audio.input": "Périphérique d'entrée audio" +"preferences.audio.input.placeholder": "Sélectionner un périphérique d'entrée…" +"preferences.audio.default": "Par défaut du système" +"preferences.hint": "Le moteur de rendu s'applique au processus de rendu au prochain lancement ; tous les autres réglages prennent effet immédiatement et sont enregistrés à la fermeture." +"preferences.tab.general": "Général" +"preferences.tab.keyboard": "Clavier" +"preferences.section.keyboard": "Clavier" +"preferences.keyboard.search": "Rechercher une action ou un raccourci" +"preferences.keyboard.action": "Action" +"preferences.keyboard.shortcut": "Raccourci" +"preferences.keyboard.click_to_edit": "Cliquer pour définir le raccourci…" +"preferences.keyboard.capturing": "Appuyer sur les touches… (Échap pour annuler)" +"preferences.keyboard.unbound": "Aucun" +"preferences.keyboard.import": "Importer…" +"preferences.keyboard.export": "Exporter…" +"preferences.keyboard.reset_selected": "Réinitialiser la sélection" +"preferences.keyboard.reset_all": "Tout réinitialiser" +"preferences.keyboard.reset_all.confirm": "Voulez-vous vraiment réinitialiser tous les raccourcis clavier à leurs valeurs par défaut ?" +"preferences.keyboard.conflict": "Le raccourci est déjà affecté à {action} ; l'affectation a été déplacée." +"preferences.keyboard.cleared": "Raccourci effacé (action désaffectée)." +"preferences.keyboard.reset": "Raccourci réinitialisé à sa valeur par défaut." +"preferences.keyboard.reset_all_done": "Tous les raccourcis ont été réinitialisés à leurs valeurs par défaut." +"preferences.keyboard.imported": "Raccourcis importés avec succès." +"preferences.keyboard.exported": "Raccourcis exportés avec succès." +"preferences.keyboard.import_failed": "Impossible d'ouvrir le fichier en lecture." +"preferences.keyboard.export_failed": "Impossible d'ouvrir le fichier en écriture." +"actionsearch.search_placeholder": "Rechercher une action…" +"actionsearch.empty": "Aucune action correspondante" +"actionsearch.no_actions": "Aucune action disponible" +"export.title": "Exporter la séquence" +"export.format": "Format" +"export.format.placeholder": "Sélectionner un format…" +"export.path": "Chemin de sortie" +"export.run": "Exporter" +"export.hint": "La séquence est exportée via le chemin d'exportation oaktask ; la progression s'affiche dans la boîte de dialogue." +"export.progress.title": "Exportation" +"export.progress.label": "Rendu des images…" +"proxydialog.title": "Paramètres de proxy" +"proxydialog.footage_group": "Médias" +"proxydialog.no_footage": "Aucun média dans le projet" +"proxydialog.custom": "Utiliser des paramètres personnalisés pour les médias" +"proxydialog.custom_suffix": " (paramètres personnalisés)" +"proxydialog.global": "Paramètres de proxy globaux" +"proxydialog.resolution": "Résolution du proxy" +"proxydialog.resolution.custom": "Taille personnalisée" +"proxydialog.resolution.half": "1/2 de la source" +"proxydialog.resolution.quarter": "1/4 de la source" +"proxydialog.resolution.eighth": "1/8 de la source" +"proxydialog.width": "Largeur du proxy" +"proxydialog.height": "Hauteur du proxy" +"proxydialog.crf": "CRF du proxy" +"proxydialog.preset": "Présélection du proxy" +"proxydialog.include_audio": "Inclure l'audio dans les proxies" +"proxydialog.ffmpeg": "Exécutable ffmpeg" +"proxydialog.generate": "Générer les proxies" +"proxydialog.delete": "Supprimer les proxies" +"proxydialog.close": "Fermer" +"proxydialog.state.missing": "Manquant" +"proxydialog.state.generating": "Génération" +"proxydialog.state.ready": "Prêt" +"proxydialog.state.failed": "Échec" +"menu.color.label": "Étiquette de couleur" +"menu.color.red": "Rouge" +"menu.color.maroon": "Bordeaux" +"menu.color.orange": "Orange" +"menu.color.brown": "Marron" +"menu.color.yellow": "Jaune" +"menu.color.oak": "Chêne" +"menu.color.lime": "Citron vert" +"menu.color.green": "Vert" +"menu.color.cyan": "Cyan" +"menu.color.teal": "Sarcelle" +"menu.color.blue": "Bleu" +"menu.color.navy": "Bleu marine" +"menu.color.pink": "Rose" +"menu.color.purple": "Violet" +"menu.color.silver": "Argent" +"menu.color.gray": "Gris" +"menu.context.properties": "Propriétés" +"timeline.context.sync_source_time": "Synchroniser par l'heure de la source" +"timeline.context.sync_waveform": "Synchroniser par la forme d'onde" +"timeline.context.sync_waveform_speed": "Synchroniser par la forme d'onde (ajuster la vitesse)" +"timeline.context.cache": "Cache" +"timeline.context.auto_cache": "Cache automatique" +"timeline.context.cache_all": "Tout mettre en cache" +"timeline.context.cache_in_out": "Mettre en cache entrée/sortie" +"timeline.context.cache_discard": "Jeter" +"timeline.context.proxy": "Proxy" +"timeline.context.generate_proxy": "Générer le proxy" +"timeline.context.use_proxy": "Utiliser le proxy" +"timeline.context.reveal_proxy": "Révéler le proxy" +"timeline.context.delete_proxy": "Supprimer le proxy" +"timeline.context.reveal_in_footage_viewer": "Révéler dans la visionneuse de médias" +"timeline.context.reveal_in_project": "Révéler dans le projet" +"timeline.context.multicam": "Multicaméra" +"timeline.context.use_audio_time_units": "Utiliser les unités de temps audio" +"timeline.context.show_thumbnails": "Afficher les vignettes" +"timeline.context.thumbnails_off": "Désactivé" +"timeline.context.thumbnails_at_in_points": "Uniquement aux points d'entrée" +"timeline.context.thumbnails_on": "Activé" +"timeline.context.show_waveforms": "Afficher les formes d'onde" +"timeline.context.add_video_track": "Ajouter une piste vidéo" +"timeline.context.add_audio_track": "Ajouter une piste audio" +"timeline.context.delete_track": "Supprimer" +"timeline.context.delete_all_empty": "Supprimer toutes les pistes vides" +"timeline.context.timecode_drop_frame": "Timecode drop frame" +"timeline.context.timecode_non_drop_frame": "Timecode non-drop frame" +"timeline.context.timecode_seconds": "Secondes" +"timeline.context.timecode_frames": "Images" +"timeline.context.timecode_milliseconds": "Millisecondes" +"node.category.output": "Sortie" +"node.category.effect": "Effet" +"node.category.generator": "Générateur" +"node.category.input": "Entrée" +"node.category.math": "Maths" +"node.category.color": "Couleur" +"node.category.distort": "Déformation" +"node.category.filter": "Filtre" +"node.category.keying": "Incrustation" +"node.category.openfx": "OpenFX" +"node.category.group": "Groupe" +"project.context.new": "Nouveau" +"project.context.reveal_in_finder": "Révéler dans le Finder" +"project.context.replace_footage": "Remplacer les médias" +"project.context.rename": "Renommer" +"project.context.delete": "Supprimer" +"project.context.open_in_new_tab": "Ouvrir dans un nouvel onglet" +"project.context.open_in_new_window": "Ouvrir dans une nouvelle fenêtre" +"viewer.context.zoom": "Zoom" +"viewer.context.zoom_fit": "Ajuster" +"viewer.context.full_screen": "Plein écran" +"viewer.context.playback_resolution": "Résolution de lecture" +"viewer.context.res_full": "Pleine" +"viewer.context.res_half": "1/2" +"viewer.context.res_quarter": "1/4" +"viewer.context.res_eighth": "1/8" +"viewer.context.safe_margins": "Marges de sécurité" +"viewer.context.safe_off": "Désactivé" +"viewer.context.safe_on": "Activé" +"viewer.context.safe_custom": "Format d'image personnalisé" +"viewer.context.stop_on_last": "Arrêter la lecture sur la dernière image" +"viewer.context.audio_waveform": "Forme d'onde audio" +"viewer.context.wf_automatic": "Afficher/masquer automatiquement" +"viewer.context.wf_only": "Afficher uniquement la forme d'onde" +"viewer.context.wf_both": "Afficher la visionneuse et la forme d'onde" +"viewer.context.show_fps": "Afficher les FPS" +"viewer.context.save_frame": "Enregistrer l'image comme fichier" +"node.context.group": "Grouper" +"node.context.ungroup": "Dégrouper" +"node.context.open_in_viewer": "Ouvrir dans la visionneuse" +"node.context.show_in_param_editor": "Afficher dans l'éditeur de paramètres" +"node.context.smooth_edges": "Bords lissés" +"node.context.direction": "Direction" +"node.context.dir_top_bottom": "Du haut vers le bas" +"node.context.dir_bottom_top": "Du bas vers le haut" +"node.context.dir_left_right": "De gauche à droite" +"node.context.dir_right_left": "De droite à gauche" +"node.context.add": "Ajouter" +"inspector.context.enable": "Activer" +"inspector.context.disable": "Désactiver" +"inspector.context.remove": "Retirer" +"inspector.context.rename": "Renommer" diff --git a/assets/i18n/ja.yaml b/assets/i18n/ja.yaml new file mode 100644 index 000000000..932accd7c --- /dev/null +++ b/assets/i18n/ja.yaml @@ -0,0 +1,433 @@ +# Oak Video Editor UI strings — 日本語 (ja) +# Format: "i18n.key": "Translated text". Add new languages as new files named .yaml. +"menu.file": "ファイル(F)" +"menu.edit": "編集(E)" +"menu.view": "表示(V)" +"menu.playback": "再生(P)" +"menu.sequence": "シーケンス(S)" +"menu.window": "ウィンドウ(W)" +"menu.tools": "ツール(T)" +"menu.help": "ヘルプ(H)" +"menu.file.new": "新規" +"menu.file.new_project": "新規プロジェクト…" +"menu.file.new_sequence": "新規シーケンス…" +"menu.file.new_folder": "新規フォルダー" +"menu.file.open_project": "プロジェクトファイルを開く…" +"menu.file.open_library": "ライブラリから開く…" +"menu.file.open_recent": "最近使ったファイル" +"menu.file.clear_recent": "最近使ったファイルの一覧をクリア" +"menu.file.project_manager": "プロジェクトマネージャー…" +"menu.file.import_footage": "素材を読み込み…" +"menu.file.export_project": "プロジェクトファイルを書き出し…" +"menu.file.save_as": "別名で保存…" +"menu.file.revert": "保存済みに戻す" +"menu.file.export_media": "メディアを書き出し…" +"menu.file.project_properties": "プロジェクトのプロパティ…" +"menu.file.close": "プロジェクトを閉じる" +"menu.file.quit": "終了" +"menu.edit.undo": "元に戻す" +"menu.edit.redo": "やり直す" +"menu.edit.cut": "切り取り" +"menu.edit.copy": "コピー" +"menu.edit.paste": "貼り付け" +"menu.edit.paste_insert": "挿入として貼り付け" +"menu.edit.duplicate": "複製" +"menu.edit.rename": "名前を変更" +"menu.edit.delete": "削除" +"menu.edit.ripple_delete": "リップル削除" +"menu.edit.split": "再生ヘッドで分割" +"menu.edit.speed_duration": "速度/長さ…" +"menu.edit.default_transition": "既定のトランジションに設定" +"menu.edit.link_unlink": "リンク/リンク解除" +"menu.edit.enable_disable": "有効/無効" +"menu.edit.nest": "ネスト" +"menu.edit.select_all": "すべて選択" +"menu.edit.deselect_all": "選択を解除" +"menu.edit.insert": "挿入" +"menu.edit.overwrite": "上書き" +"menu.edit.ripple_to_in": "イン点までリップル" +"menu.edit.ripple_to_out": "アウト点までリップル" +"menu.edit.edit_to_in": "イン点まで編集" +"menu.edit.edit_to_out": "アウト点まで編集" +"menu.edit.nudge_left": "左へ微調整" +"menu.edit.nudge_right": "右へ微調整" +"menu.edit.move_in_to_playhead": "イン点を再生ヘッドへ移動" +"menu.edit.move_out_to_playhead": "アウト点を再生ヘッドへ移動" +"menu.edit.set_in_point": "イン点を設定" +"menu.edit.set_out_point": "アウト点を設定" +"menu.edit.reset_in": "イン点をリセット" +"menu.edit.reset_out": "アウト点をリセット" +"menu.edit.clear_in_out": "イン/アウトをクリア" +"menu.edit.delete_in_out": "インからアウトまで削除" +"menu.edit.ripple_delete_in_out": "インからアウトまでリップル削除" +"menu.edit.marker": "マーカーを追加" +"menu.view.theme": "テーマ" +"menu.view.theme.dark": "Olive Dark" +"menu.view.theme.light": "Olive Light" +"menu.view.language": "言語" +"language.name": "日本語" +"menu.view.zoom_in": "ズームイン" +"menu.view.zoom_out": "ズームアウト" +"menu.view.increase_track_height": "トラックの高さを増やす" +"menu.view.decrease_track_height": "トラックの高さを減らす" +"menu.view.show_all": "すべてのトラックを表示" +"menu.view.full_screen": "フルスクリーン" +"menu.view.full_screen_viewer": "ビューアをフルスクリーン表示" +"menu.view.preferences": "環境設定…" +"menu.playback.play_pause": "再生/一時停止" +"menu.playback.play_in_to_out": "インからアウトまで再生" +"menu.playback.loop": "ループ" +"menu.playback.shuttle_left": "シャトル左" +"menu.playback.shuttle_stop": "シャトル停止" +"menu.playback.shuttle_right": "シャトル右" +"menu.playback.prev_frame": "前のフレーム" +"menu.playback.next_frame": "次のフレーム" +"menu.playback.to_start": "シーケンスの先頭へ移動" +"menu.playback.go_to_end": "シーケンスの末尾へ移動" +"menu.playback.prev_cut": "前のカットへ移動" +"menu.playback.next_cut": "次のカットへ移動" +"menu.playback.go_to_in": "イン点へ移動" +"menu.playback.go_to_out": "アウト点へ移動" +"menu.sequence.add_video_track": "ビデオトラックを追加" +"menu.sequence.add_audio_track": "オーディオトラックを追加" +"menu.sequence.remove_track": "選択したトラックを削除" +"menu.sequence.remove_marker": "マーカーを削除" +"menu.sequence.set_workarea": "作業領域を設定" +"menu.sequence.clear_workarea": "作業領域をクリア" +"menu.sequence.cache": "レンダーキャッシュ" +"menu.sequence.cache_in_out": "インからアウトまでをレンダーキャッシュ" +"menu.sequence.cache_clear": "レンダーキャッシュをクリア" +"menu.sequence.settings": "シーケンス設定…" +"menu.window.project": "プロジェクト" +"menu.window.source_viewer": "ソースビューア" +"menu.window.program_viewer": "プログラムビューア" +"menu.window.node_editor": "ノードエディター" +"menu.window.inspector": "インスペクター" +"menu.window.history": "履歴" +"menu.window.timeline": "タイムライン" +"menu.window.effect_library": "エフェクトライブラリ" +"menu.window.multicam": "マルチカム" +"menu.window.maximize_panel": "パネルを最大化" +"menu.window.reset_layout": "レイアウトをリセット" +"menu.tools.pointer": "ポインター" +"menu.tools.track_select": "トラック選択" +"menu.tools.edit": "編集" +"menu.tools.ripple": "リップル" +"menu.tools.rolling": "ローリング" +"menu.tools.razor_tool": "カミソリ" +"menu.tools.slip": "スリップ" +"menu.tools.slide": "スライド" +"menu.tools.hand": "手のひら" +"menu.tools.zoom_tool": "ズーム" +"menu.tools.transition": "トランジション" +"menu.tools.add": "追加" +"menu.tools.record": "録画" +"menu.tools.addable.empty": "空のクリップ" +"menu.tools.addable.bars": "カラーバー" +"menu.tools.addable.shape": "シェイプ" +"menu.tools.addable.solid": "単色" +"menu.tools.addable.title": "タイトル" +"menu.tools.addable.tone": "テストトーン" +"menu.tools.addable.subtitle": "字幕" +"menu.tools.snapping": "スナップ" +"menu.tools.use_proxy": "プロキシメディアを使用" +"menu.tools.proxy_settings": "プロキシ設定…" +"menu.help.action_search": "アクションを検索…" +"menu.help.feedback": "フィードバックを送信…" +"menu.help.about": "Oak について…" +"shortcut.space": "スペース" +"panel.project": "プロジェクト" +"panel.source_viewer": "ソースビューア" +"panel.program_viewer": "プログラムビューア" +"panel.node_editor": "ノードエディター" +"panel.inspector": "インスペクター" +"panel.history": "履歴" +"panel.timeline": "タイムライン" +"panel.effect_library": "エフェクトライブラリ" +"panel.multicam": "マルチカム" +"multicam.no_multicam": "マルチカメラクリップが検出されませんでした" +"multicam.switch_1": "カメラ1に切り替え" +"multicam.switch_2": "カメラ2に切り替え" +"multicam.switch_3": "カメラ3に切り替え" +"multicam.switch_4": "カメラ4に切り替え" +"multicam.switch_5": "カメラ5に切り替え" +"multicam.switch_6": "カメラ6に切り替え" +"multicam.switch_7": "カメラ7に切り替え" +"multicam.switch_8": "カメラ8に切り替え" +"multicam.switch_9": "カメラ9に切り替え" +"effect_library.hint": "ダブルクリックで選択中のクリップに追加します" +"effect_library.group.builtin": "内蔵" +"status.ready": "準備完了" +"status.cache": "キャッシュ:有効" +"status.proxy.on": "プロキシ:オン" +"status.proxy.off": "プロキシ:オフ" +"status.storage.written": "ライブラリ:保存済み" +"status.storage.unbound": "ライブラリ:オフ" +"status.storage.error": "ライブラリ:書き込みエラー" +"status.untitled": "無題のプロジェクト" +"status.backend": "エンジン:" +"manager.title": "プロジェクトマネージャー" +"manager.new": "新規プロジェクト" +"manager.new.default_name": "無題のプロジェクト" +"manager.open": "開く" +"manager.rename": "名前を変更…" +"manager.rename.title": "プロジェクトの名前を変更" +"manager.rename.label": "新しい名前" +"manager.duplicate": "複製" +"manager.delete": "削除" +"manager.delete.title": "プロジェクトを削除" +"manager.delete.confirm": "ライブラリからプロジェクト「{name}」を削除しますか?この操作は元に戻せません。" +"manager.import": "読み込み…" +"manager.import.title": "プロジェクトを読み込み" +"manager.export": "書き出し…" +"manager.export.title": "プロジェクトを書き出し" +"manager.col.name": "名前" +"manager.col.modified": "更新日時" +"manager.col.duration": "長さ" +"manager.col.tracks": "トラック" +"manager.col.clips": "クリップ" +"manager.col.footage": "素材" +"manager.empty": "ライブラリにプロジェクトはまだありません。" +"timeline.tool.select": "選択" +"timeline.tool.razor": "カミソリ" +"timeline.tool.ripple": "リップル" +"timeline.tool.slip": "スリップ" +"timeline.tool.roll": "ローリング" +"timeline.tool.zoom": "ズーム" +"timeline.tool.slide": "スライド" +"timeline.tool.track_select": "トラック選択" +"timeline.zoom": "ズーム" +"timeline.zoom_in": "ズームイン" +"timeline.zoom_out": "ズームアウト" +"timeline.track_height": "トラックの高さ" +"timeline.snap": "スナップ" +"timeline.add_video_track": "ビデオトラックを追加" +"timeline.add_audio_track": "オーディオトラックを追加" +"bin.footage": "素材" +"bin.music": "音楽" +"history.command": "コマンド" +"history.empty": "履歴なし" +"history.jump_here": "このステップに移動" +"node.fit": "フィット" +"viewer.picture": "画像" +"viewer.scopes": "スコープ" +"scope.histogram": "ヒストグラム" +"scope.waveform": "ウェーブフォーム" +"scope.vectorscope": "ベクトルスコープ" +"viewer.in_point": "イン点を設定" +"viewer.step_back": "前のフレーム" +"viewer.play": "再生" +"viewer.pause": "一時停止" +"viewer.step_forward": "次のフレーム" +"viewer.out_point": "アウト点を設定" +"viewer.clear_range": "イン/アウト範囲をクリア" +"explorer.tree": "ツリー" +"explorer.icons": "アイコン" +"viewer.safe_frames": "セーフエリア" +"viewer.zoom": "ズーム" +"viewer.no_frame_source": "フレームソースがありません" +"effect_stack.empty": "選択なし" +"effect_stack.add": "+ エフェクトを追加" +"inspector.params": "編集可能なパラメーターはありません" +"inspector.add_effect": "エフェクトを追加" +"inspector.badge.openfx": "OpenFX" +"ofx.progress.title": "OpenFX プラグインの進捗" +"ofx.color.hex": "16進" +"ofx.color.ok": "決定" +"ofx.color.cancel": "キャンセル" +"ofx.color.invalid": "無効な16進カラーです" +"dialog.cancel": "キャンセル" +"dialog.close": "閉じる" +"file.open.title": "プロジェクトを開く" +"file.import_footage.title": "素材を読み込み" +"preferences.title": "環境設定" +"preferences.section.general": "一般" +"preferences.section.render": "レンダリング" +"preferences.section.cache": "キャッシュ" +"preferences.section.proxy": "プロキシ" +"preferences.section.color": "カラー" +"preferences.color.enable": "ディスプレイのカラーマネジメントを有効にする(ディスプレイICC)" +"preferences.color.custom": "カスタムICCプロファイル" +"preferences.color.browse": "参照…" +"preferences.color.restart_hint": "モードの変更は再起動後に有効になります。" +"preferences.section.project": "プロジェクト" +"preferences.section.audio": "オーディオ" +"preferences.backend": "レンダラーバックエンド" +"preferences.hwdecode.enable": "ハードウェアアクセラレーションによるビデオデコード(VideoToolbox / VA-API / NVDEC / D3D11VA)" +"preferences.backend.placeholder": "バックエンドを選択…" +"preferences.language": "言語" +"preferences.language.placeholder": "言語を選択…" +"preferences.theme": "テーマ" +"preferences.theme.dark": "Olive Dark" +"preferences.theme.light": "Olive Light" +"preferences.cache.dir": "ディスクキャッシュの保存先" +"preferences.cache.browse": "参照…" +"preferences.proxy.enable": "プロキシメディアを使用" +"preferences.proxy.resolution": "プロキシ解像度" +"preferences.proxy.full": "フル解像度" +"preferences.snapshot.interval": "自動保存(スナップショット)の間隔(秒)" +"preferences.transition.default": "既定のトランジションの長さ(秒)" +"preferences.audio.output": "オーディオ出力デバイス" +"preferences.audio.output.placeholder": "出力デバイスを選択…" +"preferences.audio.input": "オーディオ入力デバイス" +"preferences.audio.input.placeholder": "入力デバイスを選択…" +"preferences.audio.default": "システムの既定" +"preferences.hint": "レンダラーバックエンドは次回起動時にレンダーワーカーへ適用されます。その他の設定はすぐに有効になり、終了時に保存されます。" +"preferences.tab.general": "一般" +"preferences.tab.keyboard": "キーボード" +"preferences.section.keyboard": "キーボード" +"preferences.keyboard.search": "アクションまたはショートカットを検索" +"preferences.keyboard.action": "アクション" +"preferences.keyboard.shortcut": "ショートカット" +"preferences.keyboard.click_to_edit": "クリックしてショートカットを設定…" +"preferences.keyboard.capturing": "キーを押してください…(Escでキャンセル)" +"preferences.keyboard.unbound": "なし" +"preferences.keyboard.import": "読み込み…" +"preferences.keyboard.export": "書き出し…" +"preferences.keyboard.reset_selected": "選択項目をリセット" +"preferences.keyboard.reset_all": "すべてリセット" +"preferences.keyboard.reset_all.confirm": "すべてのキーボードショートカットを既定にリセットしますか?" +"preferences.keyboard.conflict": "ショートカットは既に{action}に割り当てられています。割り当ては移動されました。" +"preferences.keyboard.cleared": "ショートカットをクリアしました(アクションは未割り当て)。" +"preferences.keyboard.reset": "ショートカットを既定にリセットしました。" +"preferences.keyboard.reset_all_done": "すべてのショートカットを既定にリセットしました。" +"preferences.keyboard.imported": "ショートカットを読み込みました。" +"preferences.keyboard.exported": "ショートカットを書き出しました。" +"preferences.keyboard.import_failed": "ファイルを読み取り用に開けませんでした。" +"preferences.keyboard.export_failed": "ファイルを書き込み用に開けませんでした。" +"actionsearch.search_placeholder": "アクションを検索…" +"actionsearch.empty": "一致するアクションがありません" +"actionsearch.no_actions": "利用可能なアクションがありません" +"export.title": "シーケンスを書き出し" +"export.format": "フォーマット" +"export.format.placeholder": "フォーマットを選択…" +"export.path": "出力先" +"export.run": "書き出し" +"export.hint": "シーケンスはoaktaskの書き出しパスを通じて書き出されます。進捗はダイアログに表示されます。" +"export.progress.title": "書き出し中" +"export.progress.label": "フレームをレンダリング中…" +"proxydialog.title": "プロキシ設定" +"proxydialog.footage_group": "素材" +"proxydialog.no_footage": "プロジェクトに素材がありません" +"proxydialog.custom": "素材にカスタム設定を使用" +"proxydialog.custom_suffix": " (カスタム設定)" +"proxydialog.global": "グローバルプロキシ設定" +"proxydialog.resolution": "プロキシ解像度" +"proxydialog.resolution.custom": "カスタムサイズ" +"proxydialog.resolution.half": "ソースの1/2" +"proxydialog.resolution.quarter": "ソースの1/4" +"proxydialog.resolution.eighth": "ソースの1/8" +"proxydialog.width": "プロキシ幅" +"proxydialog.height": "プロキシ高さ" +"proxydialog.crf": "プロキシCRF" +"proxydialog.preset": "プロキシプリセット" +"proxydialog.include_audio": "プロキシにオーディオを含める" +"proxydialog.ffmpeg": "ffmpeg 実行ファイル" +"proxydialog.generate": "プロキシを生成" +"proxydialog.delete": "プロキシを削除" +"proxydialog.close": "閉じる" +"proxydialog.state.missing": "見つかりません" +"proxydialog.state.generating": "生成中" +"proxydialog.state.ready": "準備完了" +"proxydialog.state.failed": "失敗" +"menu.color.label": "カラーラベル" +"menu.color.red": "赤" +"menu.color.maroon": "えび茶" +"menu.color.orange": "オレンジ" +"menu.color.brown": "茶" +"menu.color.yellow": "黄" +"menu.color.oak": "オーク" +"menu.color.lime": "ライム" +"menu.color.green": "緑" +"menu.color.cyan": "シアン" +"menu.color.teal": "ティール" +"menu.color.blue": "青" +"menu.color.navy": "紺" +"menu.color.pink": "ピンク" +"menu.color.purple": "紫" +"menu.color.silver": "銀" +"menu.color.gray": "灰" +"menu.context.properties": "プロパティ" +"timeline.context.sync_source_time": "ソース時間で同期" +"timeline.context.sync_waveform": "ウェーブフォームで同期" +"timeline.context.sync_waveform_speed": "ウェーブフォームで同期(速度調整)" +"timeline.context.cache": "キャッシュ" +"timeline.context.auto_cache": "自動キャッシュ" +"timeline.context.cache_all": "すべてキャッシュ" +"timeline.context.cache_in_out": "イン/アウトをキャッシュ" +"timeline.context.cache_discard": "破棄" +"timeline.context.proxy": "プロキシ" +"timeline.context.generate_proxy": "プロキシを生成" +"timeline.context.use_proxy": "プロキシを使用" +"timeline.context.reveal_proxy": "プロキシを表示" +"timeline.context.delete_proxy": "プロキシを削除" +"timeline.context.reveal_in_footage_viewer": "素材ビューアに表示" +"timeline.context.reveal_in_project": "プロジェクトに表示" +"timeline.context.multicam": "マルチカム" +"timeline.context.use_audio_time_units": "オーディオ時間単位を使用" +"timeline.context.show_thumbnails": "サムネイルを表示" +"timeline.context.thumbnails_off": "無効" +"timeline.context.thumbnails_at_in_points": "イン点のみ" +"timeline.context.thumbnails_on": "有効" +"timeline.context.show_waveforms": "ウェーブフォームを表示" +"timeline.context.add_video_track": "ビデオトラックを追加" +"timeline.context.add_audio_track": "オーディオトラックを追加" +"timeline.context.delete_track": "削除" +"timeline.context.delete_all_empty": "空のトラックをすべて削除" +"timeline.context.timecode_drop_frame": "ドロップフレーム" +"timeline.context.timecode_non_drop_frame": "ノンドロップフレーム" +"timeline.context.timecode_seconds": "秒" +"timeline.context.timecode_frames": "フレーム" +"timeline.context.timecode_milliseconds": "ミリ秒" +"node.category.output": "出力" +"node.category.effect": "エフェクト" +"node.category.generator": "ジェネレーター" +"node.category.input": "入力" +"node.category.math": "数学" +"node.category.color": "カラー" +"node.category.distort": "ディストーション" +"node.category.filter": "フィルター" +"node.category.keying": "キーイング" +"node.category.openfx": "OpenFX" +"node.category.group": "グループ" +"project.context.new": "新規" +"project.context.reveal_in_finder": "Finderに表示" +"project.context.replace_footage": "素材を置き換え" +"project.context.rename": "名前を変更" +"project.context.delete": "削除" +"project.context.open_in_new_tab": "新しいタブで開く" +"project.context.open_in_new_window": "新しいウィンドウで開く" +"viewer.context.zoom": "ズーム" +"viewer.context.zoom_fit": "フィット" +"viewer.context.full_screen": "フルスクリーン" +"viewer.context.playback_resolution": "再生解像度" +"viewer.context.res_full": "フル" +"viewer.context.res_half": "1/2" +"viewer.context.res_quarter": "1/4" +"viewer.context.res_eighth": "1/8" +"viewer.context.safe_margins": "セーフマージン" +"viewer.context.safe_off": "オフ" +"viewer.context.safe_on": "オン" +"viewer.context.safe_custom": "カスタムアスペクト比" +"viewer.context.stop_on_last": "最後のフレームで再生を停止" +"viewer.context.audio_waveform": "オーディオウェーブフォーム" +"viewer.context.wf_automatic": "自動的に表示/非表示" +"viewer.context.wf_only": "ウェーブフォームのみ表示" +"viewer.context.wf_both": "ビューアとウェーブフォームの両方を表示" +"viewer.context.show_fps": "FPSを表示" +"viewer.context.save_frame": "フレームを画像として保存" +"node.context.group": "グループ化" +"node.context.ungroup": "グループ化を解除" +"node.context.open_in_viewer": "ビューアで開く" +"node.context.show_in_param_editor": "パラメーターエディターに表示" +"node.context.smooth_edges": "エッジを滑らかに" +"node.context.direction": "方向" +"node.context.dir_top_bottom": "上から下" +"node.context.dir_bottom_top": "下から上" +"node.context.dir_left_right": "左から右" +"node.context.dir_right_left": "右から左" +"node.context.add": "追加" +"inspector.context.enable": "有効にする" +"inspector.context.disable": "無効にする" +"inspector.context.remove": "削除" +"inspector.context.rename": "名前を変更" diff --git a/assets/i18n/pt.yaml b/assets/i18n/pt.yaml new file mode 100644 index 000000000..b42b57c22 --- /dev/null +++ b/assets/i18n/pt.yaml @@ -0,0 +1,433 @@ +# Oak Video Editor UI strings — Português (pt) +# Format: "i18n.key": "Translated text". Add new languages as new files named .yaml. +"menu.file": "Arquivo(A)" +"menu.edit": "Editar(E)" +"menu.view": "Ver(V)" +"menu.playback": "Reprodução(R)" +"menu.sequence": "Sequência(S)" +"menu.window": "Janela(J)" +"menu.tools": "Ferramentas(F)" +"menu.help": "Ajuda(A)" +"menu.file.new": "Novo" +"menu.file.new_project": "Novo projeto…" +"menu.file.new_sequence": "Nova sequência…" +"menu.file.new_folder": "Nova pasta" +"menu.file.open_project": "Abrir arquivo de projeto…" +"menu.file.open_library": "Abrir da biblioteca…" +"menu.file.open_recent": "Abrir recentes" +"menu.file.clear_recent": "Limpar lista de recentes" +"menu.file.project_manager": "Gerenciador de projetos…" +"menu.file.import_footage": "Importar mídia…" +"menu.file.export_project": "Exportar arquivo de projeto…" +"menu.file.save_as": "Salvar como…" +"menu.file.revert": "Reverter" +"menu.file.export_media": "Exportar mídia…" +"menu.file.project_properties": "Propriedades do projeto…" +"menu.file.close": "Fechar projeto" +"menu.file.quit": "Sair" +"menu.edit.undo": "Desfazer" +"menu.edit.redo": "Refazer" +"menu.edit.cut": "Recortar" +"menu.edit.copy": "Copiar" +"menu.edit.paste": "Colar" +"menu.edit.paste_insert": "Colar como inserção" +"menu.edit.duplicate": "Duplicar" +"menu.edit.rename": "Renomear" +"menu.edit.delete": "Excluir" +"menu.edit.ripple_delete": "Exclusão em cadeia" +"menu.edit.split": "Dividir no cursor" +"menu.edit.speed_duration": "Velocidade/Duração…" +"menu.edit.default_transition": "Definir como transição padrão" +"menu.edit.link_unlink": "Vincular/Desvincular" +"menu.edit.enable_disable": "Ativar/Desativar" +"menu.edit.nest": "Aninhar" +"menu.edit.select_all": "Selecionar tudo" +"menu.edit.deselect_all": "Desmarcar tudo" +"menu.edit.insert": "Inserir" +"menu.edit.overwrite": "Sobrescrever" +"menu.edit.ripple_to_in": "Ondulação até a entrada" +"menu.edit.ripple_to_out": "Ondulação até a saída" +"menu.edit.edit_to_in": "Editar até a entrada" +"menu.edit.edit_to_out": "Editar até a saída" +"menu.edit.nudge_left": "Deslocar para a esquerda" +"menu.edit.nudge_right": "Deslocar para a direita" +"menu.edit.move_in_to_playhead": "Mover ponto de entrada para o cursor" +"menu.edit.move_out_to_playhead": "Mover ponto de saída para o cursor" +"menu.edit.set_in_point": "Definir ponto de entrada" +"menu.edit.set_out_point": "Definir ponto de saída" +"menu.edit.reset_in": "Redefinir ponto de entrada" +"menu.edit.reset_out": "Redefinir ponto de saída" +"menu.edit.clear_in_out": "Limpar entrada/saída" +"menu.edit.delete_in_out": "Excluir da entrada à saída" +"menu.edit.ripple_delete_in_out": "Exclusão em cadeia da entrada à saída" +"menu.edit.marker": "Adicionar marcador" +"menu.view.theme": "Tema" +"menu.view.theme.dark": "Olive Dark" +"menu.view.theme.light": "Olive Light" +"menu.view.language": "Idioma" +"language.name": "Português" +"menu.view.zoom_in": "Aproximar" +"menu.view.zoom_out": "Afastar" +"menu.view.increase_track_height": "Aumentar altura da trilha" +"menu.view.decrease_track_height": "Diminuir altura da trilha" +"menu.view.show_all": "Mostrar todas as trilhas" +"menu.view.full_screen": "Tela cheia" +"menu.view.full_screen_viewer": "Visualizador em tela cheia" +"menu.view.preferences": "Preferências…" +"menu.playback.play_pause": "Reproduzir/Pausar" +"menu.playback.play_in_to_out": "Reproduzir da entrada à saída" +"menu.playback.loop": "Loop" +"menu.playback.shuttle_left": "Shuttle à esquerda" +"menu.playback.shuttle_stop": "Parar shuttle" +"menu.playback.shuttle_right": "Shuttle à direita" +"menu.playback.prev_frame": "Quadro anterior" +"menu.playback.next_frame": "Próximo quadro" +"menu.playback.to_start": "Ir para o início da sequência" +"menu.playback.go_to_end": "Ir para o fim da sequência" +"menu.playback.prev_cut": "Ir para o corte anterior" +"menu.playback.next_cut": "Ir para o próximo corte" +"menu.playback.go_to_in": "Ir para o ponto de entrada" +"menu.playback.go_to_out": "Ir para o ponto de saída" +"menu.sequence.add_video_track": "Adicionar trilha de vídeo" +"menu.sequence.add_audio_track": "Adicionar trilha de áudio" +"menu.sequence.remove_track": "Remover trilha selecionada" +"menu.sequence.remove_marker": "Remover marcador" +"menu.sequence.set_workarea": "Definir área de trabalho" +"menu.sequence.clear_workarea": "Limpar área de trabalho" +"menu.sequence.cache": "Cache de renderização" +"menu.sequence.cache_in_out": "Cache de renderização da entrada à saída" +"menu.sequence.cache_clear": "Limpar cache de renderização" +"menu.sequence.settings": "Configurações da sequência…" +"menu.window.project": "Projeto" +"menu.window.source_viewer": "Visualizador de origem" +"menu.window.program_viewer": "Visualizador de programa" +"menu.window.node_editor": "Editor de nós" +"menu.window.inspector": "Inspetor" +"menu.window.history": "Histórico" +"menu.window.timeline": "Linha do tempo" +"menu.window.effect_library": "Biblioteca de efeitos" +"menu.window.multicam": "Multicâmera" +"menu.window.maximize_panel": "Maximizar painel" +"menu.window.reset_layout": "Redefinir layout" +"menu.tools.pointer": "Ponteiro" +"menu.tools.track_select": "Seleção de trilha" +"menu.tools.edit": "Edição" +"menu.tools.ripple": "Ondulação" +"menu.tools.rolling": "Rolagem" +"menu.tools.razor_tool": "Navalha" +"menu.tools.slip": "Deslizamento" +"menu.tools.slide": "Deslocamento" +"menu.tools.hand": "Mão" +"menu.tools.zoom_tool": "Zoom" +"menu.tools.transition": "Transição" +"menu.tools.add": "Adicionar" +"menu.tools.record": "Gravar" +"menu.tools.addable.empty": "Clipe vazio" +"menu.tools.addable.bars": "Barras de cores" +"menu.tools.addable.shape": "Forma" +"menu.tools.addable.solid": "Cor sólida" +"menu.tools.addable.title": "Título" +"menu.tools.addable.tone": "Tom de teste" +"menu.tools.addable.subtitle": "Legenda" +"menu.tools.snapping": "Ajuste" +"menu.tools.use_proxy": "Usar mídia proxy" +"menu.tools.proxy_settings": "Configurações de proxy…" +"menu.help.action_search": "Pesquisar ações…" +"menu.help.feedback": "Enviar comentários…" +"menu.help.about": "Sobre o Oak…" +"shortcut.space": "Espaço" +"panel.project": "Projeto" +"panel.source_viewer": "Visualizador de origem" +"panel.program_viewer": "Visualizador de programa" +"panel.node_editor": "Editor de nós" +"panel.inspector": "Inspetor" +"panel.history": "Histórico" +"panel.timeline": "Linha do tempo" +"panel.effect_library": "Biblioteca de efeitos" +"panel.multicam": "Multicâmera" +"multicam.no_multicam": "Nenhum clipe multicâmera detectado" +"multicam.switch_1": "Alternar para a câmera 1" +"multicam.switch_2": "Alternar para a câmera 2" +"multicam.switch_3": "Alternar para a câmera 3" +"multicam.switch_4": "Alternar para a câmera 4" +"multicam.switch_5": "Alternar para a câmera 5" +"multicam.switch_6": "Alternar para a câmera 6" +"multicam.switch_7": "Alternar para a câmera 7" +"multicam.switch_8": "Alternar para a câmera 8" +"multicam.switch_9": "Alternar para a câmera 9" +"effect_library.hint": "Clique duas vezes para adicionar ao clipe selecionado" +"effect_library.group.builtin": "Integrados" +"status.ready": "Pronto" +"status.cache": "Cache: ativado" +"status.proxy.on": "Proxy: ativado" +"status.proxy.off": "Proxy: desativado" +"status.storage.written": "Biblioteca: salva" +"status.storage.unbound": "Biblioteca: desativada" +"status.storage.error": "Biblioteca: falha ao gravar" +"status.untitled": "Projeto sem título" +"status.backend": "Mecanismo:" +"manager.title": "Gerenciador de projetos" +"manager.new": "Novo projeto" +"manager.new.default_name": "Projeto sem título" +"manager.open": "Abrir" +"manager.rename": "Renomear…" +"manager.rename.title": "Renomear projeto" +"manager.rename.label": "Novo nome" +"manager.duplicate": "Duplicar" +"manager.delete": "Excluir" +"manager.delete.title": "Excluir projeto" +"manager.delete.confirm": "Excluir o projeto \"{name}\" da biblioteca? Esta ação não pode ser desfeita." +"manager.import": "Importar…" +"manager.import.title": "Importar projeto" +"manager.export": "Exportar…" +"manager.export.title": "Exportar projeto" +"manager.col.name": "Nome" +"manager.col.modified": "Modificado" +"manager.col.duration": "Duração" +"manager.col.tracks": "Trilhas" +"manager.col.clips": "Clipes" +"manager.col.footage": "Mídia" +"manager.empty": "Ainda não há projetos na biblioteca." +"timeline.tool.select": "Seleção" +"timeline.tool.razor": "Navalha" +"timeline.tool.ripple": "Ondulação" +"timeline.tool.slip": "Deslizamento" +"timeline.tool.roll": "Rolagem" +"timeline.tool.zoom": "Zoom" +"timeline.tool.slide": "Deslocamento" +"timeline.tool.track_select": "Seleção de trilha" +"timeline.zoom": "Zoom" +"timeline.zoom_in": "Aproximar" +"timeline.zoom_out": "Afastar" +"timeline.track_height": "Altura da trilha" +"timeline.snap": "Ajuste" +"timeline.add_video_track": "Adicionar trilha de vídeo" +"timeline.add_audio_track": "Adicionar trilha de áudio" +"bin.footage": "Mídia" +"bin.music": "Música" +"history.command": "Comando" +"history.empty": "Sem histórico" +"history.jump_here": "Ir para esta etapa" +"node.fit": "Ajustar" +"viewer.picture": "Imagem" +"viewer.scopes": "Escopos" +"scope.histogram": "Histograma" +"scope.waveform": "Forma de onda" +"scope.vectorscope": "Vectorscópio" +"viewer.in_point": "Definir ponto de entrada" +"viewer.step_back": "Quadro anterior" +"viewer.play": "Reproduzir" +"viewer.pause": "Pausar" +"viewer.step_forward": "Próximo quadro" +"viewer.out_point": "Definir ponto de saída" +"viewer.clear_range": "Limpar intervalo entrada/saída" +"explorer.tree": "Árvore" +"explorer.icons": "Ícones" +"viewer.safe_frames": "Margens seguras" +"viewer.zoom": "Zoom" +"viewer.no_frame_source": "Nenhuma fonte de quadro" +"effect_stack.empty": "Nenhuma seleção" +"effect_stack.add": "+ Adicionar efeito" +"inspector.params": "Nenhum parâmetro editável" +"inspector.add_effect": "Adicionar efeito" +"inspector.badge.openfx": "OpenFX" +"ofx.progress.title": "Progresso do plugin OpenFX" +"ofx.color.hex": "Hexadecimal" +"ofx.color.ok": "Confirmar" +"ofx.color.cancel": "Cancelar" +"ofx.color.invalid": "Cor hexadecimal inválida" +"dialog.cancel": "Cancelar" +"dialog.close": "Fechar" +"file.open.title": "Abrir projeto" +"file.import_footage.title": "Importar mídia" +"preferences.title": "Preferências" +"preferences.section.general": "Geral" +"preferences.section.render": "Renderização" +"preferences.section.cache": "Cache" +"preferences.section.proxy": "Proxy" +"preferences.section.color": "Cor" +"preferences.color.enable": "Exibir gerenciamento de cores (ICC da tela)" +"preferences.color.custom": "Perfil ICC personalizado" +"preferences.color.browse": "Procurar…" +"preferences.color.restart_hint": "A alteração do modo entra em vigor após reiniciar." +"preferences.section.project": "Projeto" +"preferences.section.audio": "Áudio" +"preferences.backend": "Backend do renderizador" +"preferences.hwdecode.enable": "Decodificação de vídeo acelerada por hardware (VideoToolbox / VA-API / NVDEC / D3D11VA)" +"preferences.backend.placeholder": "Selecionar um backend…" +"preferences.language": "Idioma" +"preferences.language.placeholder": "Selecionar um idioma…" +"preferences.theme": "Tema" +"preferences.theme.dark": "Olive Dark" +"preferences.theme.light": "Olive Light" +"preferences.cache.dir": "Diretório do cache em disco" +"preferences.cache.browse": "Procurar…" +"preferences.proxy.enable": "Usar mídia proxy" +"preferences.proxy.resolution": "Resolução do proxy" +"preferences.proxy.full": "Resolução completa" +"preferences.snapshot.interval": "Intervalo de salvamento automático (snapshot), em segundos" +"preferences.transition.default": "Duração padrão das transições, em segundos" +"preferences.audio.output": "Dispositivo de saída de áudio" +"preferences.audio.output.placeholder": "Selecionar um dispositivo de saída…" +"preferences.audio.input": "Dispositivo de entrada de áudio" +"preferences.audio.input.placeholder": "Selecionar um dispositivo de entrada…" +"preferences.audio.default": "Padrão do sistema" +"preferences.hint": "O backend do renderizador é aplicado ao worker de renderização no próximo início; as demais configurações entram em vigor imediatamente e são salvas ao sair." +"preferences.tab.general": "Geral" +"preferences.tab.keyboard": "Teclado" +"preferences.section.keyboard": "Teclado" +"preferences.keyboard.search": "Pesquisar ação ou atalho" +"preferences.keyboard.action": "Ação" +"preferences.keyboard.shortcut": "Atalho" +"preferences.keyboard.click_to_edit": "Clique para definir o atalho…" +"preferences.keyboard.capturing": "Pressione as teclas… (Esc para cancelar)" +"preferences.keyboard.unbound": "Nenhum" +"preferences.keyboard.import": "Importar…" +"preferences.keyboard.export": "Exportar…" +"preferences.keyboard.reset_selected": "Redefinir selecionados" +"preferences.keyboard.reset_all": "Redefinir tudo" +"preferences.keyboard.reset_all.confirm": "Tem certeza de que deseja redefinir todos os atalhos de teclado para os padrões?" +"preferences.keyboard.conflict": "O atalho já está atribuído a {action}; a atribuição foi movida." +"preferences.keyboard.cleared": "Atalho limpo (ação sem atribuição)." +"preferences.keyboard.reset": "Atalho redefinido para o padrão." +"preferences.keyboard.reset_all_done": "Todos os atalhos foram redefinidos para os padrões." +"preferences.keyboard.imported": "Atalhos importados com sucesso." +"preferences.keyboard.exported": "Atalhos exportados com sucesso." +"preferences.keyboard.import_failed": "Não foi possível abrir o arquivo para leitura." +"preferences.keyboard.export_failed": "Não foi possível abrir o arquivo para gravação." +"actionsearch.search_placeholder": "Pesquisar ação…" +"actionsearch.empty": "Nenhuma ação correspondente" +"actionsearch.no_actions": "Nenhuma ação disponível" +"export.title": "Exportar sequência" +"export.format": "Formato" +"export.format.placeholder": "Selecionar um formato…" +"export.path": "Caminho de saída" +"export.run": "Exportar" +"export.hint": "A sequência é exportada pelo caminho de exportação do oaktask; o progresso é exibido no diálogo." +"export.progress.title": "Exportando" +"export.progress.label": "Renderizando quadros…" +"proxydialog.title": "Configurações de proxy" +"proxydialog.footage_group": "Mídia" +"proxydialog.no_footage": "Nenhuma mídia no projeto" +"proxydialog.custom": "Usar configurações personalizadas para a mídia" +"proxydialog.custom_suffix": " (configurações personalizadas)" +"proxydialog.global": "Configurações globais de proxy" +"proxydialog.resolution": "Resolução do proxy" +"proxydialog.resolution.custom": "Tamanho personalizado" +"proxydialog.resolution.half": "1/2 da origem" +"proxydialog.resolution.quarter": "1/4 da origem" +"proxydialog.resolution.eighth": "1/8 da origem" +"proxydialog.width": "Largura do proxy" +"proxydialog.height": "Altura do proxy" +"proxydialog.crf": "CRF do proxy" +"proxydialog.preset": "Predefinição do proxy" +"proxydialog.include_audio": "Incluir áudio nos proxies" +"proxydialog.ffmpeg": "Executável do ffmpeg" +"proxydialog.generate": "Gerar proxies" +"proxydialog.delete": "Excluir proxies" +"proxydialog.close": "Fechar" +"proxydialog.state.missing": "Ausente" +"proxydialog.state.generating": "Gerando" +"proxydialog.state.ready": "Pronto" +"proxydialog.state.failed": "Falha" +"menu.color.label": "Rótulo de cor" +"menu.color.red": "Vermelho" +"menu.color.maroon": "Castanho" +"menu.color.orange": "Laranja" +"menu.color.brown": "Marrom" +"menu.color.yellow": "Amarelo" +"menu.color.oak": "Carvalho" +"menu.color.lime": "Lima" +"menu.color.green": "Verde" +"menu.color.cyan": "Ciano" +"menu.color.teal": "Verde-azulado" +"menu.color.blue": "Azul" +"menu.color.navy": "Azul-marinho" +"menu.color.pink": "Rosa" +"menu.color.purple": "Roxo" +"menu.color.silver": "Prata" +"menu.color.gray": "Cinza" +"menu.context.properties": "Propriedades" +"timeline.context.sync_source_time": "Sincronizar pelo tempo da origem" +"timeline.context.sync_waveform": "Sincronizar pela forma de onda" +"timeline.context.sync_waveform_speed": "Sincronizar pela forma de onda (ajustar velocidade)" +"timeline.context.cache": "Cache" +"timeline.context.auto_cache": "Cache automático" +"timeline.context.cache_all": "Armazenar tudo em cache" +"timeline.context.cache_in_out": "Armazenar entrada/saída em cache" +"timeline.context.cache_discard": "Descartar" +"timeline.context.proxy": "Proxy" +"timeline.context.generate_proxy": "Gerar proxy" +"timeline.context.use_proxy": "Usar proxy" +"timeline.context.reveal_proxy": "Revelar proxy" +"timeline.context.delete_proxy": "Excluir proxy" +"timeline.context.reveal_in_footage_viewer": "Revelar no visualizador de mídia" +"timeline.context.reveal_in_project": "Revelar no projeto" +"timeline.context.multicam": "Multicâmera" +"timeline.context.use_audio_time_units": "Usar unidades de tempo de áudio" +"timeline.context.show_thumbnails": "Mostrar miniaturas" +"timeline.context.thumbnails_off": "Desativadas" +"timeline.context.thumbnails_at_in_points": "Somente nos pontos de entrada" +"timeline.context.thumbnails_on": "Ativadas" +"timeline.context.show_waveforms": "Mostrar formas de onda" +"timeline.context.add_video_track": "Adicionar trilha de vídeo" +"timeline.context.add_audio_track": "Adicionar trilha de áudio" +"timeline.context.delete_track": "Excluir" +"timeline.context.delete_all_empty": "Excluir todas as trilhas vazias" +"timeline.context.timecode_drop_frame": "Drop-frame" +"timeline.context.timecode_non_drop_frame": "Non-drop-frame" +"timeline.context.timecode_seconds": "Segundos" +"timeline.context.timecode_frames": "Quadros" +"timeline.context.timecode_milliseconds": "Milissegundos" +"node.category.output": "Saída" +"node.category.effect": "Efeito" +"node.category.generator": "Gerador" +"node.category.input": "Entrada" +"node.category.math": "Matemática" +"node.category.color": "Cor" +"node.category.distort": "Distorção" +"node.category.filter": "Filtro" +"node.category.keying": "Chroma key" +"node.category.openfx": "OpenFX" +"node.category.group": "Grupo" +"project.context.new": "Novo" +"project.context.reveal_in_finder": "Revelar no Finder" +"project.context.replace_footage": "Substituir mídia" +"project.context.rename": "Renomear" +"project.context.delete": "Excluir" +"project.context.open_in_new_tab": "Abrir em nova guia" +"project.context.open_in_new_window": "Abrir em nova janela" +"viewer.context.zoom": "Zoom" +"viewer.context.zoom_fit": "Ajustar" +"viewer.context.full_screen": "Tela cheia" +"viewer.context.playback_resolution": "Resolução de reprodução" +"viewer.context.res_full": "Completa" +"viewer.context.res_half": "1/2" +"viewer.context.res_quarter": "1/4" +"viewer.context.res_eighth": "1/8" +"viewer.context.safe_margins": "Margens seguras" +"viewer.context.safe_off": "Desativado" +"viewer.context.safe_on": "Ativado" +"viewer.context.safe_custom": "Proporção personalizada" +"viewer.context.stop_on_last": "Parar reprodução no último quadro" +"viewer.context.audio_waveform": "Forma de onda de áudio" +"viewer.context.wf_automatic": "Mostrar/ocultar automaticamente" +"viewer.context.wf_only": "Mostrar somente a forma de onda" +"viewer.context.wf_both": "Mostrar visualizador e forma de onda" +"viewer.context.show_fps": "Mostrar FPS" +"viewer.context.save_frame": "Salvar quadro como imagem" +"node.context.group": "Agrupar" +"node.context.ungroup": "Desagrupar" +"node.context.open_in_viewer": "Abrir no visualizador" +"node.context.show_in_param_editor": "Mostrar no editor de parâmetros" +"node.context.smooth_edges": "Suavizar bordas" +"node.context.direction": "Direção" +"node.context.dir_top_bottom": "De cima para baixo" +"node.context.dir_bottom_top": "De baixo para cima" +"node.context.dir_left_right": "Da esquerda para a direita" +"node.context.dir_right_left": "Da direita para a esquerda" +"node.context.add": "Adicionar" +"inspector.context.enable": "Ativar" +"inspector.context.disable": "Desativar" +"inspector.context.remove": "Remover" +"inspector.context.rename": "Renomear" diff --git a/assets/i18n/ru.yaml b/assets/i18n/ru.yaml new file mode 100644 index 000000000..cc9a27f8c --- /dev/null +++ b/assets/i18n/ru.yaml @@ -0,0 +1,433 @@ +# Oak Video Editor UI strings — Русский (ru) +# Format: "i18n.key": "Translated text". Add new languages as new files named .yaml. +"menu.file": "Файл(Ф)" +"menu.edit": "Правка(П)" +"menu.view": "Вид(В)" +"menu.playback": "Воспроизведение(В)" +"menu.sequence": "Последовательность(П)" +"menu.window": "Окно(О)" +"menu.tools": "Инструменты(И)" +"menu.help": "Справка(С)" +"menu.file.new": "Создать" +"menu.file.new_project": "Новый проект…" +"menu.file.new_sequence": "Новая последовательность…" +"menu.file.new_folder": "Новая папка" +"menu.file.open_project": "Открыть файл проекта…" +"menu.file.open_library": "Открыть из библиотеки…" +"menu.file.open_recent": "Недавние файлы" +"menu.file.clear_recent": "Очистить список недавних файлов" +"menu.file.project_manager": "Диспетчер проектов…" +"menu.file.import_footage": "Импортировать медиафайлы…" +"menu.file.export_project": "Экспортировать файл проекта…" +"menu.file.save_as": "Сохранить как…" +"menu.file.revert": "Восстановить" +"menu.file.export_media": "Экспортировать медиафайлы…" +"menu.file.project_properties": "Свойства проекта…" +"menu.file.close": "Закрыть проект" +"menu.file.quit": "Выход" +"menu.edit.undo": "Отменить" +"menu.edit.redo": "Повторить" +"menu.edit.cut": "Вырезать" +"menu.edit.copy": "Копировать" +"menu.edit.paste": "Вставить" +"menu.edit.paste_insert": "Вставить вставкой" +"menu.edit.duplicate": "Дублировать" +"menu.edit.rename": "Переименовать" +"menu.edit.delete": "Удалить" +"menu.edit.ripple_delete": "Удалить со сдвигом" +"menu.edit.split": "Разделить в позиции курсора" +"menu.edit.speed_duration": "Скорость/Длительность…" +"menu.edit.default_transition": "Назначить переходом по умолчанию" +"menu.edit.link_unlink": "Связать/Отменить связь" +"menu.edit.enable_disable": "Включить/Отключить" +"menu.edit.nest": "Вложить" +"menu.edit.select_all": "Выделить всё" +"menu.edit.deselect_all": "Снять выделение" +"menu.edit.insert": "Вставка" +"menu.edit.overwrite": "Перезаписать" +"menu.edit.ripple_to_in": "Со сдвигом до точки входа" +"menu.edit.ripple_to_out": "Со сдвигом до точки выхода" +"menu.edit.edit_to_in": "Обрезать до точки входа" +"menu.edit.edit_to_out": "Обрезать до точки выхода" +"menu.edit.nudge_left": "Сдвинуть влево" +"menu.edit.nudge_right": "Сдвинуть вправо" +"menu.edit.move_in_to_playhead": "Переместить точку входа к курсору" +"menu.edit.move_out_to_playhead": "Переместить точку выхода к курсору" +"menu.edit.set_in_point": "Задать точку входа" +"menu.edit.set_out_point": "Задать точку выхода" +"menu.edit.reset_in": "Сбросить точку входа" +"menu.edit.reset_out": "Сбросить точку выхода" +"menu.edit.clear_in_out": "Очистить вход/выход" +"menu.edit.delete_in_out": "Удалить от входа до выхода" +"menu.edit.ripple_delete_in_out": "Удалить со сдвигом от входа до выхода" +"menu.edit.marker": "Добавить маркер" +"menu.view.theme": "Тема" +"menu.view.theme.dark": "Olive Dark" +"menu.view.theme.light": "Olive Light" +"menu.view.language": "Язык" +"language.name": "Русский" +"menu.view.zoom_in": "Увеличить" +"menu.view.zoom_out": "Уменьшить" +"menu.view.increase_track_height": "Увеличить высоту дорожки" +"menu.view.decrease_track_height": "Уменьшить высоту дорожки" +"menu.view.show_all": "Показать все дорожки" +"menu.view.full_screen": "Во весь экран" +"menu.view.full_screen_viewer": "Просмотр на весь экран" +"menu.view.preferences": "Настройки…" +"menu.playback.play_pause": "Воспроизведение/Пауза" +"menu.playback.play_in_to_out": "Воспроизвести от входа до выхода" +"menu.playback.loop": "Зациклить" +"menu.playback.shuttle_left": "Шаттл влево" +"menu.playback.shuttle_stop": "Остановить шаттл" +"menu.playback.shuttle_right": "Шаттл вправо" +"menu.playback.prev_frame": "Предыдущий кадр" +"menu.playback.next_frame": "Следующий кадр" +"menu.playback.to_start": "Перейти в начало последовательности" +"menu.playback.go_to_end": "Перейти в конец последовательности" +"menu.playback.prev_cut": "Перейти к предыдущей склейке" +"menu.playback.next_cut": "Перейти к следующей склейке" +"menu.playback.go_to_in": "Перейти к точке входа" +"menu.playback.go_to_out": "Перейти к точке выхода" +"menu.sequence.add_video_track": "Добавить видеодорожку" +"menu.sequence.add_audio_track": "Добавить аудиодорожку" +"menu.sequence.remove_track": "Удалить выбранную дорожку" +"menu.sequence.remove_marker": "Удалить маркер" +"menu.sequence.set_workarea": "Задать рабочую область" +"menu.sequence.clear_workarea": "Очистить рабочую область" +"menu.sequence.cache": "Кэш рендеринга" +"menu.sequence.cache_in_out": "Кэшировать от входа до выхода" +"menu.sequence.cache_clear": "Очистить кэш рендеринга" +"menu.sequence.settings": "Параметры последовательности…" +"menu.window.project": "Проект" +"menu.window.source_viewer": "Монитор источника" +"menu.window.program_viewer": "Монитор программы" +"menu.window.node_editor": "Редактор узлов" +"menu.window.inspector": "Инспектор" +"menu.window.history": "История" +"menu.window.timeline": "Таймлайн" +"menu.window.effect_library": "Библиотека эффектов" +"menu.window.multicam": "Мультикамера" +"menu.window.maximize_panel": "Развернуть панель" +"menu.window.reset_layout": "Сбросить расположение" +"menu.tools.pointer": "Указатель" +"menu.tools.track_select": "Выбор дорожки" +"menu.tools.edit": "Редактирование" +"menu.tools.ripple": "Сдвиг" +"menu.tools.rolling": "Накат" +"menu.tools.razor_tool": "Бритва" +"menu.tools.slip": "Скольжение" +"menu.tools.slide": "Перемещение" +"menu.tools.hand": "Рука" +"menu.tools.zoom_tool": "Масштаб" +"menu.tools.transition": "Переход" +"menu.tools.add": "Добавить" +"menu.tools.record": "Запись" +"menu.tools.addable.empty": "Пустой клип" +"menu.tools.addable.bars": "Цветные полосы" +"menu.tools.addable.shape": "Фигура" +"menu.tools.addable.solid": "Сплошной цвет" +"menu.tools.addable.title": "Титры" +"menu.tools.addable.tone": "Тестовый тон" +"menu.tools.addable.subtitle": "Субтитры" +"menu.tools.snapping": "Привязка" +"menu.tools.use_proxy": "Использовать прокси-медиа" +"menu.tools.proxy_settings": "Параметры прокси…" +"menu.help.action_search": "Поиск действий…" +"menu.help.feedback": "Отправить отзыв…" +"menu.help.about": "О программе Oak…" +"shortcut.space": "Пробел" +"panel.project": "Проект" +"panel.source_viewer": "Монитор источника" +"panel.program_viewer": "Монитор программы" +"panel.node_editor": "Редактор узлов" +"panel.inspector": "Инспектор" +"panel.history": "История" +"panel.timeline": "Таймлайн" +"panel.effect_library": "Библиотека эффектов" +"panel.multicam": "Мультикамера" +"multicam.no_multicam": "Не обнаружен клип мультикамеры" +"multicam.switch_1": "Переключиться на камеру 1" +"multicam.switch_2": "Переключиться на камеру 2" +"multicam.switch_3": "Переключиться на камеру 3" +"multicam.switch_4": "Переключиться на камеру 4" +"multicam.switch_5": "Переключиться на камеру 5" +"multicam.switch_6": "Переключиться на камеру 6" +"multicam.switch_7": "Переключиться на камеру 7" +"multicam.switch_8": "Переключиться на камеру 8" +"multicam.switch_9": "Переключиться на камеру 9" +"effect_library.hint": "Двойной щелчок добавляет эффект к выбранному клипу" +"effect_library.group.builtin": "Встроенные" +"status.ready": "Готово" +"status.cache": "Кэш: включён" +"status.proxy.on": "Прокси: вкл" +"status.proxy.off": "Прокси: выкл" +"status.storage.written": "Библиотека: сохранена" +"status.storage.unbound": "Библиотека: выключена" +"status.storage.error": "Библиотека: ошибка записи" +"status.untitled": "Проект без названия" +"status.backend": "Движок:" +"manager.title": "Диспетчер проектов" +"manager.new": "Новый проект" +"manager.new.default_name": "Проект без названия" +"manager.open": "Открыть" +"manager.rename": "Переименовать…" +"manager.rename.title": "Переименовать проект" +"manager.rename.label": "Новое имя" +"manager.duplicate": "Дублировать" +"manager.delete": "Удалить" +"manager.delete.title": "Удалить проект" +"manager.delete.confirm": "Удалить проект «{name}» из библиотеки? Это действие нельзя отменить." +"manager.import": "Импортировать…" +"manager.import.title": "Импорт проекта" +"manager.export": "Экспортировать…" +"manager.export.title": "Экспорт проекта" +"manager.col.name": "Имя" +"manager.col.modified": "Изменён" +"manager.col.duration": "Длительность" +"manager.col.tracks": "Дорожки" +"manager.col.clips": "Клипы" +"manager.col.footage": "Медиафайлы" +"manager.empty": "В библиотеке пока нет проектов." +"timeline.tool.select": "Выбор" +"timeline.tool.razor": "Бритва" +"timeline.tool.ripple": "Сдвиг" +"timeline.tool.slip": "Скольжение" +"timeline.tool.roll": "Накат" +"timeline.tool.zoom": "Масштаб" +"timeline.tool.slide": "Перемещение" +"timeline.tool.track_select": "Выбор дорожки" +"timeline.zoom": "Масштаб" +"timeline.zoom_in": "Увеличить" +"timeline.zoom_out": "Уменьшить" +"timeline.track_height": "Высота дорожки" +"timeline.snap": "Привязка" +"timeline.add_video_track": "Добавить видеодорожку" +"timeline.add_audio_track": "Добавить аудиодорожку" +"bin.footage": "Медиафайлы" +"bin.music": "Музыка" +"history.command": "Команда" +"history.empty": "История пуста" +"history.jump_here": "Перейти к этому шагу" +"node.fit": "Вписать" +"viewer.picture": "Изображение" +"viewer.scopes": "Осциллографы" +"scope.histogram": "Гистограмма" +"scope.waveform": "Осциллограмма" +"scope.vectorscope": "Вектороскоп" +"viewer.in_point": "Задать точку входа" +"viewer.step_back": "Предыдущий кадр" +"viewer.play": "Воспроизвести" +"viewer.pause": "Пауза" +"viewer.step_forward": "Следующий кадр" +"viewer.out_point": "Задать точку выхода" +"viewer.clear_range": "Очистить диапазон входа/выхода" +"explorer.tree": "Дерево" +"explorer.icons": "Значки" +"viewer.safe_frames": "Безопасные зоны" +"viewer.zoom": "Масштаб" +"viewer.no_frame_source": "Нет источника кадров" +"effect_stack.empty": "Ничего не выбрано" +"effect_stack.add": "+ Добавить эффект" +"inspector.params": "Нет редактируемых параметров" +"inspector.add_effect": "Добавить эффект" +"inspector.badge.openfx": "OpenFX" +"ofx.progress.title": "Выполнение плагина OpenFX" +"ofx.color.hex": "Шестнадцатеричный" +"ofx.color.ok": "ОК" +"ofx.color.cancel": "Отмена" +"ofx.color.invalid": "Недопустимый шестнадцатеричный цвет" +"dialog.cancel": "Отмена" +"dialog.close": "Закрыть" +"file.open.title": "Открыть проект" +"file.import_footage.title": "Импорт медиафайлов" +"preferences.title": "Настройки" +"preferences.section.general": "Общие" +"preferences.section.render": "Рендеринг" +"preferences.section.cache": "Кэш" +"preferences.section.proxy": "Прокси" +"preferences.section.color": "Цвет" +"preferences.color.enable": "Управление цветом отображения (ICC дисплея)" +"preferences.color.custom": "Пользовательский профиль ICC" +"preferences.color.browse": "Обзор…" +"preferences.color.restart_hint": "Изменение режима вступит в силу после перезапуска." +"preferences.section.project": "Проект" +"preferences.section.audio": "Аудио" +"preferences.backend": "Бэкенд рендеринга" +"preferences.hwdecode.enable": "Аппаратное декодирование видео (VideoToolbox / VA-API / NVDEC / D3D11VA)" +"preferences.backend.placeholder": "Выберите бэкенд…" +"preferences.language": "Язык" +"preferences.language.placeholder": "Выберите язык…" +"preferences.theme": "Тема" +"preferences.theme.dark": "Olive Dark" +"preferences.theme.light": "Olive Light" +"preferences.cache.dir": "Каталог дискового кэша" +"preferences.cache.browse": "Обзор…" +"preferences.proxy.enable": "Использовать прокси-медиа" +"preferences.proxy.resolution": "Разрешение прокси" +"preferences.proxy.full": "Полное разрешение" +"preferences.snapshot.interval": "Интервал автосохранения (снимков), в секундах" +"preferences.transition.default": "Длительность перехода по умолчанию, в секундах" +"preferences.audio.output": "Устройство вывода звука" +"preferences.audio.output.placeholder": "Выберите устройство вывода…" +"preferences.audio.input": "Устройство ввода звука" +"preferences.audio.input.placeholder": "Выберите устройство ввода…" +"preferences.audio.default": "Системное значение по умолчанию" +"preferences.hint": "Бэкенд рендеринга применяется к рабочему процессу рендеринга при следующем запуске; все остальные настройки вступают в силу немедленно и сохраняются при выходе." +"preferences.tab.general": "Общие" +"preferences.tab.keyboard": "Клавиатура" +"preferences.section.keyboard": "Клавиатура" +"preferences.keyboard.search": "Поиск действия или сочетания клавиш" +"preferences.keyboard.action": "Действие" +"preferences.keyboard.shortcut": "Сочетание клавиш" +"preferences.keyboard.click_to_edit": "Нажмите, чтобы задать сочетание…" +"preferences.keyboard.capturing": "Нажмите клавиши… (Esc — отмена)" +"preferences.keyboard.unbound": "Нет" +"preferences.keyboard.import": "Импортировать…" +"preferences.keyboard.export": "Экспортировать…" +"preferences.keyboard.reset_selected": "Сбросить выбранное" +"preferences.keyboard.reset_all": "Сбросить всё" +"preferences.keyboard.reset_all.confirm": "Сбросить все сочетания клавиш к значениям по умолчанию?" +"preferences.keyboard.conflict": "Сочетание уже назначено для {action}; привязка перемещена." +"preferences.keyboard.cleared": "Сочетание очищено (действие без привязки)." +"preferences.keyboard.reset": "Сочетание сброшено к значению по умолчанию." +"preferences.keyboard.reset_all_done": "Все сочетания клавиш сброшены к значениям по умолчанию." +"preferences.keyboard.imported": "Сочетания импортированы." +"preferences.keyboard.exported": "Сочетания экспортированы." +"preferences.keyboard.import_failed": "Не удалось открыть файл для чтения." +"preferences.keyboard.export_failed": "Не удалось открыть файл для записи." +"actionsearch.search_placeholder": "Поиск действия…" +"actionsearch.empty": "Нет подходящих действий" +"actionsearch.no_actions": "Нет доступных действий" +"export.title": "Экспорт последовательности" +"export.format": "Формат" +"export.format.placeholder": "Выберите формат…" +"export.path": "Путь вывода" +"export.run": "Экспортировать" +"export.hint": "Последовательность экспортируется через путь экспорта oaktask; ход выполнения отображается в диалоговом окне." +"export.progress.title": "Экспорт" +"export.progress.label": "Рендеринг кадров…" +"proxydialog.title": "Параметры прокси" +"proxydialog.footage_group": "Медиафайлы" +"proxydialog.no_footage": "В проекте нет медиафайлов" +"proxydialog.custom": "Использовать свои настройки для медиафайлов" +"proxydialog.custom_suffix": " (свои настройки)" +"proxydialog.global": "Общие параметры прокси" +"proxydialog.resolution": "Разрешение прокси" +"proxydialog.resolution.custom": "Свой размер" +"proxydialog.resolution.half": "1/2 исходного" +"proxydialog.resolution.quarter": "1/4 исходного" +"proxydialog.resolution.eighth": "1/8 исходного" +"proxydialog.width": "Ширина прокси" +"proxydialog.height": "Высота прокси" +"proxydialog.crf": "CRF прокси" +"proxydialog.preset": "Пресет прокси" +"proxydialog.include_audio": "Включать звук в прокси" +"proxydialog.ffmpeg": "Исполняемый файл ffmpeg" +"proxydialog.generate": "Создать прокси" +"proxydialog.delete": "Удалить прокси" +"proxydialog.close": "Закрыть" +"proxydialog.state.missing": "Отсутствует" +"proxydialog.state.generating": "Создание" +"proxydialog.state.ready": "Готово" +"proxydialog.state.failed": "Сбой" +"menu.color.label": "Цветовая метка" +"menu.color.red": "Красный" +"menu.color.maroon": "Бордовый" +"menu.color.orange": "Оранжевый" +"menu.color.brown": "Коричневый" +"menu.color.yellow": "Жёлтый" +"menu.color.oak": "Дуб" +"menu.color.lime": "Лаймовый" +"menu.color.green": "Зелёный" +"menu.color.cyan": "Голубой" +"menu.color.teal": "Бирюзовый" +"menu.color.blue": "Синий" +"menu.color.navy": "Тёмно-синий" +"menu.color.pink": "Розовый" +"menu.color.purple": "Фиолетовый" +"menu.color.silver": "Серебристый" +"menu.color.gray": "Серый" +"menu.context.properties": "Свойства" +"timeline.context.sync_source_time": "Синхронизировать по времени источника" +"timeline.context.sync_waveform": "Синхронизировать по осциллограмме" +"timeline.context.sync_waveform_speed": "Синхронизировать по осциллограмме (с изменением скорости)" +"timeline.context.cache": "Кэш" +"timeline.context.auto_cache": "Автокэш" +"timeline.context.cache_all": "Кэшировать всё" +"timeline.context.cache_in_out": "Кэшировать вход/выход" +"timeline.context.cache_discard": "Отбросить" +"timeline.context.proxy": "Прокси" +"timeline.context.generate_proxy": "Создать прокси" +"timeline.context.use_proxy": "Использовать прокси" +"timeline.context.reveal_proxy": "Показать прокси" +"timeline.context.delete_proxy": "Удалить прокси" +"timeline.context.reveal_in_footage_viewer": "Показать в окне просмотра медиафайлов" +"timeline.context.reveal_in_project": "Показать в проекте" +"timeline.context.multicam": "Мультикамера" +"timeline.context.use_audio_time_units": "Использовать единицы времени для аудио" +"timeline.context.show_thumbnails": "Показывать миниатюры" +"timeline.context.thumbnails_off": "Отключено" +"timeline.context.thumbnails_at_in_points": "Только в точках входа" +"timeline.context.thumbnails_on": "Включено" +"timeline.context.show_waveforms": "Показывать осциллограммы" +"timeline.context.add_video_track": "Добавить видеодорожку" +"timeline.context.add_audio_track": "Добавить аудиодорожку" +"timeline.context.delete_track": "Удалить" +"timeline.context.delete_all_empty": "Удалить все пустые дорожки" +"timeline.context.timecode_drop_frame": "С пропуском кадров" +"timeline.context.timecode_non_drop_frame": "Без пропуска кадров" +"timeline.context.timecode_seconds": "Секунды" +"timeline.context.timecode_frames": "Кадры" +"timeline.context.timecode_milliseconds": "Миллисекунды" +"node.category.output": "Выход" +"node.category.effect": "Эффект" +"node.category.generator": "Генератор" +"node.category.input": "Вход" +"node.category.math": "Математика" +"node.category.color": "Цвет" +"node.category.distort": "Искажение" +"node.category.filter": "Фильтр" +"node.category.keying": "Ключевание" +"node.category.openfx": "OpenFX" +"node.category.group": "Группа" +"project.context.new": "Создать" +"project.context.reveal_in_finder": "Показать в Finder" +"project.context.replace_footage": "Заменить медиафайлы" +"project.context.rename": "Переименовать" +"project.context.delete": "Удалить" +"project.context.open_in_new_tab": "Открыть в новой вкладке" +"project.context.open_in_new_window": "Открыть в новом окне" +"viewer.context.zoom": "Масштаб" +"viewer.context.zoom_fit": "Вписать" +"viewer.context.full_screen": "Во весь экран" +"viewer.context.playback_resolution": "Разрешение воспроизведения" +"viewer.context.res_full": "Полное" +"viewer.context.res_half": "1/2" +"viewer.context.res_quarter": "1/4" +"viewer.context.res_eighth": "1/8" +"viewer.context.safe_margins": "Безопасные поля" +"viewer.context.safe_off": "Выкл" +"viewer.context.safe_on": "Вкл" +"viewer.context.safe_custom": "Свой формат" +"viewer.context.stop_on_last": "Останавливать воспроизведение на последнем кадре" +"viewer.context.audio_waveform": "Осциллограмма звука" +"viewer.context.wf_automatic": "Автоматически показывать/скрывать" +"viewer.context.wf_only": "Показывать только осциллограмму" +"viewer.context.wf_both": "Показывать и изображение, и осциллограмму" +"viewer.context.show_fps": "Показывать FPS" +"viewer.context.save_frame": "Сохранить кадр как изображение" +"node.context.group": "Сгруппировать" +"node.context.ungroup": "Разгруппировать" +"node.context.open_in_viewer": "Открыть в окне просмотра" +"node.context.show_in_param_editor": "Показать в редакторе параметров" +"node.context.smooth_edges": "Сгладить края" +"node.context.direction": "Направление" +"node.context.dir_top_bottom": "Сверху вниз" +"node.context.dir_bottom_top": "Снизу вверх" +"node.context.dir_left_right": "Слева направо" +"node.context.dir_right_left": "Справа налево" +"node.context.add": "Добавить" +"inspector.context.enable": "Включить" +"inspector.context.disable": "Отключить" +"inspector.context.remove": "Удалить" +"inspector.context.rename": "Переименовать" diff --git a/assets/i18n/zh-CN.yaml b/assets/i18n/zh-CN.yaml index d294f21c4..6c350bbe1 100644 --- a/assets/i18n/zh-CN.yaml +++ b/assets/i18n/zh-CN.yaml @@ -65,8 +65,7 @@ "menu.view.theme.dark": "Olive Dark" "menu.view.theme.light": "Olive Light" "menu.view.language": "语言" -"menu.view.language.en": "English" -"menu.view.language.zh": "简体中文" +"language.name": "简体中文" "menu.view.zoom_in": "放大" "menu.view.zoom_out": "缩小" "menu.view.increase_track_height": "增加轨道高度" diff --git a/src/actions.rs b/src/actions.rs index 5fc0b752e..d8684e9fb 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -162,8 +162,11 @@ define_actions! { FullScreenViewer { cpp: "fullscreenviewer", i18n: "menu.view.full_screen_viewer", keys: [], route: FocusedPanel, menu_id: 1044 }; ThemeDark { cpp: "themedark", i18n: "menu.view.theme.dark", keys: [], route: Global, menu_id: 301 }; ThemeLight { cpp: "themelight", i18n: "menu.view.theme.light", keys: [], route: Global, menu_id: 302 }; - LangZh { cpp: "langzh", i18n: "menu.view.language.zh", keys: [], route: Global, menu_id: 303 }; - LangEn { cpp: "langen", i18n: "menu.view.language.en", keys: [], route: Global, menu_id: 304 }; + // Language switching is deliberately NOT a registry action: the + // language menu is built from the discovered packs (menu ids in the + // LANG_ITEM_BASE range, see menus::shared), so a new language is a new + // YAML file, not a code change. The C++ `langzh`/`langen` action slots + // (menu ids 303/304) stay reserved. // --- Playback ----------------------------------------------------------- GoToStart { cpp: "gotostart", i18n: "menu.playback.to_start", keys: ["home"], route: FocusedPanel, menu_id: 404 }; @@ -820,8 +823,8 @@ mod tests { /// is meant to drive the menus). #[test] fn every_action_appears_in_the_menu_tree() { - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); - crate::i18n::set_language(crate::i18n::Language::EnUs); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code("en-US"); fn collect(menu: &gpui_widgets::menu::Menu, out: &mut Vec) { for item in &menu.items { @@ -866,18 +869,22 @@ mod tests { #[test] fn every_i18n_key_exists_in_both_languages() { for entry in REGISTRY { - for language in [crate::i18n::Language::EnUs, crate::i18n::Language::ZhCN] { - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); - crate::i18n::set_language(language); + for code in ["en-US", "zh-CN"] { + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code(code); let value = crate::i18n::tr(entry.i18n_key); assert_ne!( value, entry.i18n_key, - "i18n key {} ({language:?}) is missing for action {}", + "i18n key {} ({code}) is missing for action {}", entry.i18n_key, entry.cpp_id ); assert!(!value.is_empty()); } } + // The loop leaves zh-CN active; restore the default so lock-free + // tests building English menus are not fooled mid-assert. + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code("en-US"); } /// The shortcut display formatter renders the documented labels for a diff --git a/src/app.rs b/src/app.rs index fa00f870e..455ded477 100644 --- a/src/app.rs +++ b/src/app.rs @@ -95,8 +95,6 @@ pub(crate) mod menu_ids { pub const RIPPLE_DELETE: usize = ActionId::RippleDelete.menu_id(); pub const THEME_DARK: usize = ActionId::ThemeDark.menu_id(); - pub const LANG_ZH: usize = ActionId::LangZh.menu_id(); - pub const LANG_EN: usize = ActionId::LangEn.menu_id(); pub const PREFERENCES: usize = ActionId::Preferences.menu_id(); pub const FOCUS_PROJECT: usize = ActionId::FocusProject.menu_id(); @@ -759,10 +757,20 @@ impl OakApp { } } - /// Routes a menu click: resolves the item id to its registry action and - /// dispatches it through the same path the keyboard shortcuts use, so a - /// menu click and a key press can never diverge. + /// Routes a menu click: the dynamic language items (one per discovered + /// pack, above the registry id range) switch the language directly; + /// everything else resolves to its registry action and dispatches + /// through the same path the keyboard shortcuts use, so a menu click + /// and a key press can never diverge. fn on_menu(&mut self, item: usize, cx: &mut Context) { + if let Some(index) = crate::menus::shared::language_item_index(item) { + let languages = crate::i18n::available_languages(); + if let Some(code) = languages.get(index) { + let code = code.clone(); + self.switch_language(&code, cx); + } + return; + } let Some(entry) = crate::actions::entry_for_menu_id(item) else { println!("[menu] unknown item {item}"); return; @@ -910,8 +918,6 @@ impl OakApp { println!("[view] full screen: {} (placeholder)", self.full_screen); self.rebuild_menu_bar(cx); } - A::LangZh => self.switch_language(crate::i18n::Language::ZhCN, cx), - A::LangEn => self.switch_language(crate::i18n::Language::EnUs, cx), A::Preferences => self.open_preferences(cx), // --- Playback (the program monitor) ---------------------------- A::PlayPause => { @@ -1338,8 +1344,8 @@ impl OakApp { /// Switches the UI language live: updates the [`i18n`] global, rebuilds /// the menu bar (so the menu labels and the language checkmark move /// immediately), and repaints the whole shell. - fn switch_language(&mut self, language: crate::i18n::Language, cx: &mut Context) { - crate::i18n::set_language(language); + fn switch_language(&mut self, code: &str, cx: &mut Context) { + crate::i18n::set_language_code(code); self.rebuild_menu_bar(cx); cx.notify(); } @@ -2362,12 +2368,6 @@ fn make_menus(state: MenuState) -> Vec { menu_item(A::ThemeLight).with_checked(!state.dark), ]); - let language = crate::i18n::language(); - let language_submenu = Menu::new(vec![ - menu_item(A::LangZh).with_checked(language == crate::i18n::Language::ZhCN), - menu_item(A::LangEn).with_checked(language == crate::i18n::Language::EnUs), - ]); - // 工具: the mutually exclusive tool group in the registry's order, the // add tool growing its addable-items submenu, then snapping + proxy. let mut tools: Vec = Vec::new(); @@ -2478,7 +2478,7 @@ fn make_menus(state: MenuState) -> Vec { tr("menu.view"), Menu::new(vec![ menu_item(A::ThemeDark).with_submenu(theme_submenu), - menu_item(A::LangZh).with_submenu(language_submenu), + crate::menus::shared::language_menu(), menu_item(A::ZoomIn).separated(), menu_item(A::ZoomOut), menu_item(A::IncreaseTrackHeight), @@ -2751,7 +2751,7 @@ mod tests { fn language_menu_tracks_the_active_language() { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let view_entry = |dark: bool| -> MenuBarEntry { make_menus(MenuState::new(dark)) @@ -2766,46 +2766,54 @@ mod tests { .menu .items .iter() - .find(|item| item.id == menu_ids::LANG_ZH) + .find(|item| item.label == crate::i18n::tr("menu.view.language")) .map(|item| item.submenu.clone().map(|m| *m).unwrap_or_default()) .expect("语言/Language submenu exists") }; - crate::i18n::set_language(crate::i18n::Language::EnUs); + crate::i18n::set_language_code("en-US"); + let languages = crate::i18n::available_languages(); + let en_index = languages.iter().position(|c| c == "en-US").unwrap(); + let zh_index = languages.iter().position(|c| c == "zh-CN").unwrap(); let submenu = language_item(&view_entry(true)); + assert_eq!( + submenu.items.len(), + languages.len(), + "one menu item per discovered language pack" + ); let zh = submenu .items .iter() - .find(|i| i.id == menu_ids::LANG_ZH) + .find(|i| i.id == crate::menus::shared::LANG_ITEM_BASE + zh_index) .expect("zh item"); let en = submenu .items .iter() - .find(|i| i.id == menu_ids::LANG_EN) + .find(|i| i.id == crate::menus::shared::LANG_ITEM_BASE + en_index) .expect("en item"); - assert_eq!(zh.label, "简体中文"); - assert_eq!(en.label, "English"); + assert_eq!(zh.label, "简体中文 (zh-CN)"); + assert_eq!(en.label, "English (en-US)"); assert_eq!(zh.checked, Some(false)); assert_eq!(en.checked, Some(true), "en-US is active → checked"); - crate::i18n::set_language(crate::i18n::Language::ZhCN); + crate::i18n::set_language_code("zh-CN"); let submenu = language_item(&view_entry(true)); let zh = submenu .items .iter() - .find(|i| i.id == menu_ids::LANG_ZH) + .find(|i| i.id == crate::menus::shared::LANG_ITEM_BASE + zh_index) .expect("zh item"); let en = submenu .items .iter() - .find(|i| i.id == menu_ids::LANG_EN) + .find(|i| i.id == crate::menus::shared::LANG_ITEM_BASE + en_index) .expect("en item"); assert_eq!(zh.checked, Some(true), "zh-CN is active → checked"); assert_eq!(en.checked, Some(false)); // The menu titles themselves are localized. assert_eq!(view_entry(true).title, "视图(V)"); - crate::i18n::set_language(crate::i18n::Language::EnUs); + crate::i18n::set_language_code("en-US"); assert_eq!(view_entry(true).title, "View(V)"); } @@ -2814,7 +2822,7 @@ mod tests { fn theme_menu_checkmark_follows_dark_flag() { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let dark_item = |dark: bool| -> gpui_widgets::menu::MenuItem { let entries = make_menus(MenuState::new(dark)); @@ -2845,8 +2853,8 @@ mod tests { #[test] fn window_menu_lists_all_panels_and_checks_the_open_ones() { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); - crate::i18n::set_language(crate::i18n::Language::EnUs); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code("en-US"); let mask = panel_bit(INSPECTOR) | panel_bit(TIMELINE); let mut state = MenuState::new(true); @@ -2878,7 +2886,7 @@ mod tests { #[gpui::test] async fn window_menu_toggle_closes_and_reopens_the_inspector(cx: &mut TestAppContext) { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (_window, root) = mock_shell(cx); // The checkmark shown in the 窗口 menu for the inspector, derived from @@ -2932,7 +2940,7 @@ mod tests { cx: &mut TestAppContext, ) { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (window, root) = mock_shell(cx); let menu_bar_before = cx.read(|app| root.read(app).menu_bar.entity_id()); @@ -2965,7 +2973,7 @@ mod tests { fn file_and_edit_menus_cover_the_project_lifecycle() { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let entry = |title: &str| -> MenuBarEntry { make_menus_for_test() @@ -2974,7 +2982,7 @@ mod tests { .expect("menu exists") }; - crate::i18n::set_language(crate::i18n::Language::EnUs); + crate::i18n::set_language_code("en-US"); let file = entry("File(F)"); for id in [ menu_ids::NEW_PROJECT, @@ -3005,13 +3013,15 @@ mod tests { } // The same ids exist in the zh-CN menu bar. - crate::i18n::set_language(crate::i18n::Language::ZhCN); + crate::i18n::set_language_code("zh-CN"); let file = entry("文件(F)"); assert!(file .menu .items .iter() .any(|item| item.id == menu_ids::EXPORT_PROJECT)); + // Restore the default language for the next (lock-free) test. + crate::i18n::set_language_code("en-US"); } /// Opening 视图 → Preferences… must not crash: the dialog content and the @@ -3021,8 +3031,8 @@ mod tests { async fn preferences_dialog_opens_without_crashing(cx: &mut TestAppContext) { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); - crate::i18n::set_language(crate::i18n::Language::EnUs); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code("en-US"); cx.update(|cx| cx.init_colors()); let window = cx.open_window(size(px(1600.0), px(900.0)), |window, cx| { @@ -3058,8 +3068,8 @@ mod tests { fn every_shortcut_maps_to_a_menu_item() { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); - crate::i18n::set_language(crate::i18n::Language::EnUs); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code("en-US"); fn collect(menu: &Menu, out: &mut Vec) { for item in &menu.items { @@ -3104,8 +3114,8 @@ mod tests { fn menu_shortcut_labels_match_the_table() { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); - crate::i18n::set_language(crate::i18n::Language::EnUs); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code("en-US"); fn walk(menu: &Menu) -> Vec<(usize, Option)> { let mut out = Vec::new(); @@ -3140,7 +3150,7 @@ mod tests { async fn space_toggles_program_playback(cx: &mut TestAppContext) { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (window, root) = mock_shell(cx); let playing = cx.read(|app| root.read(app).program_clock.read(app).is_playing()); @@ -3172,7 +3182,7 @@ mod tests { async fn keymap_defaults_dispatch_their_actions(cx: &mut TestAppContext) { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (window, root) = mock_shell(cx); let key = |keystroke: &str| gpui::Keystroke::parse(keystroke).unwrap(); @@ -3285,7 +3295,7 @@ mod tests { async fn edit_shortcuts_dispatch_to_the_engine(cx: &mut TestAppContext) { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (window, root) = mock_shell(cx); // Move the playhead inside the first clip (the → shortcut steps one @@ -3341,7 +3351,7 @@ mod tests { async fn shortcuts_are_suppressed_while_a_modal_is_open(cx: &mut TestAppContext) { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (window, root) = mock_shell(cx); cx.update(|app| root.update(app, |app, cx| app.on_menu(menu_ids::PREFERENCES, cx))); @@ -3404,7 +3414,7 @@ mod tests { #[gpui::test] async fn action_search_opens_from_the_slash_key(cx: &mut TestAppContext) { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _lang = crate::i18n::lang_test_lock().lock().unwrap(); + let _lang = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (window, root) = mock_shell(cx); // A leftover real shortcuts file must not shadow the default `/` // binding under test. @@ -3443,7 +3453,7 @@ mod tests { cx: &mut TestAppContext, ) { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _lang = crate::i18n::lang_test_lock().lock().unwrap(); + let _lang = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (window, root) = mock_shell(cx); crate::actions::reset_all_custom_shortcuts(); cx.update(|app| root.update(app, |app, cx| app.rebind_keys(cx))); @@ -3481,7 +3491,7 @@ mod tests { cx: &mut TestAppContext, ) { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _lang = crate::i18n::lang_test_lock().lock().unwrap(); + let _lang = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (window, root) = mock_shell(cx); crate::actions::reset_all_custom_shortcuts(); cx.update(|app| root.update(app, |app, cx| app.rebind_keys(cx))); @@ -3515,7 +3525,7 @@ mod tests { #[gpui::test] async fn keyboard_tab_capture_assigns_a_shortcut(cx: &mut TestAppContext) { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _lang = crate::i18n::lang_test_lock().lock().unwrap(); + let _lang = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (window, root) = mock_shell(cx); crate::actions::reset_all_custom_shortcuts(); cx.update(|app| root.update(app, |app, cx| app.on_menu(menu_ids::PREFERENCES, cx))); @@ -3569,7 +3579,7 @@ mod tests { #[gpui::test] async fn keyboard_tab_capture_escape_cancels_without_closing(cx: &mut TestAppContext) { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _lang = crate::i18n::lang_test_lock().lock().unwrap(); + let _lang = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (window, root) = mock_shell(cx); crate::actions::reset_all_custom_shortcuts(); cx.update(|app| root.update(app, |app, cx| app.on_menu(menu_ids::PREFERENCES, cx))); @@ -3611,7 +3621,7 @@ mod tests { #[gpui::test] async fn custom_shortcut_overrides_are_live_after_rebind(cx: &mut TestAppContext) { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _lang = crate::i18n::lang_test_lock().lock().unwrap(); + let _lang = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (window, root) = mock_shell(cx); crate::actions::reset_all_custom_shortcuts(); @@ -3645,8 +3655,8 @@ mod tests { /// mock engine records it, so the async round trip is observable. #[gpui::test] async fn import_footage_prompts_and_routes_the_picked_path(cx: &mut TestAppContext) { - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); - crate::i18n::set_language(crate::i18n::Language::EnUs); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code("en-US"); cx.update(|cx| cx.init_colors()); let window = cx.open_window(size(px(1600.0), px(900.0)), |window, cx| { @@ -3694,7 +3704,7 @@ mod tests { /// delivers it — the OS drag entering the window, then the release. #[gpui::test] async fn dropping_files_onto_the_project_browser_imports_them(cx: &mut TestAppContext) { - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (window, root) = mock_shell(cx); let mut cx = VisualTestContext::from_window(window.into(), cx); @@ -3743,7 +3753,7 @@ mod tests { /// correct parameters. #[gpui::test] async fn dragging_footage_onto_the_timeline_places_a_clip(cx: &mut TestAppContext) { - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (window, root) = mock_shell(cx); let mut cx = VisualTestContext::from_window(window.into(), cx); @@ -3822,7 +3832,7 @@ mod tests { /// grid the same entry (plus folder children) appears as icons. #[gpui::test] async fn explorer_views_list_root_level_footage(cx: &mut TestAppContext) { - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (window, _root) = mock_shell(cx); let mut cx = VisualTestContext::from_window(window.into(), cx); @@ -3905,7 +3915,7 @@ mod tests { gpui::WindowHandle>, Entity>, ) { - crate::i18n::set_language(crate::i18n::Language::EnUs); + crate::i18n::set_language_code("en-US"); cx.update(|cx| cx.init_colors()); let window = cx.open_window(size(px(1600.0), px(900.0)), |window, cx| { OakApp::::new(window, None, cx) @@ -3941,7 +3951,7 @@ mod tests { /// / 打开 route) drives the engine's library open and closes the dialog. #[gpui::test] async fn manager_lists_and_opens(cx: &mut TestAppContext) { - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (_window, root) = mock_shell(cx); let content = open_manager(cx, &root); let rows = manager_rows(cx, &content); @@ -3971,7 +3981,7 @@ mod tests { /// and its sub-dialogs. #[gpui::test] async fn manager_create_rename_duplicate_delete(cx: &mut TestAppContext) { - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (_window, root) = mock_shell(cx); let content = open_manager(cx, &root); @@ -4080,7 +4090,7 @@ mod tests { async fn manager_import_and_export_route_through_the_platform_dialogs( cx: &mut TestAppContext, ) { - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); let (_window, root) = mock_shell(cx); let content = open_manager(cx, &root); diff --git a/src/dialogs.rs b/src/dialogs.rs index 86917e26b..df0cb6132 100644 --- a/src/dialogs.rs +++ b/src/dialogs.rs @@ -148,26 +148,34 @@ impl PreferencesContent { }); // --- 常规 General: language + theme -------------------------------- - let language_options = vec![ - ComboBoxOption::new(0, "English (en-US)"), - ComboBoxOption::new(1, "简体中文 (zh-CN)"), - ]; + // Data-driven: one option per discovered language pack, labelled + // with the pack's own `language.name` — a newly dropped-in pack + // appears here with no code change. + let languages = i18n::available_languages(); + let language_options: Vec<_> = languages + .iter() + .enumerate() + .map(|(index, code)| { + ComboBoxOption::new( + index, + format!("{} ({code})", i18n::pack_native_name(code)), + ) + }) + .collect(); let language = cx.new(|cx| { ComboBox::new(2, language_options, window, cx) .with_placeholder(i18n::tr("preferences.language.placeholder")) }); - let language_selected = match crate::i18n::language() { - crate::i18n::Language::EnUs => 0, - crate::i18n::Language::ZhCN => 1, - }; - cx.subscribe(&language, |_this, _combo, event: &ComboBoxEvent, cx| { + let language_selected = languages + .iter() + .position(|code| *code == i18n::language_code()) + .unwrap_or(0); + cx.subscribe(&language, move |_this, _combo, event: &ComboBoxEvent, cx| { if let ComboBoxEvent::Selected { value, .. } = event { - let language = match *value { - 1 => crate::i18n::Language::ZhCN, - _ => crate::i18n::Language::EnUs, - }; - crate::i18n::set_language(language); - cx.emit(PreferencesEvent::LanguageChanged); + if let Some(code) = languages.get(*value) { + crate::i18n::set_language_code(code); + cx.emit(PreferencesEvent::LanguageChanged); + } } }) .detach(); @@ -2308,8 +2316,8 @@ mod tests { #[test] fn search_filter_matches_label_or_path() { - let _guard = crate::i18n::lang_test_lock().lock().unwrap(); - crate::i18n::set_language(crate::i18n::Language::EnUs); + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code("en-US"); assert!(search_filter_matches(ActionId::NewProject, "File", "new")); assert!(search_filter_matches(ActionId::NewProject, "File", "file > new")); assert!(!search_filter_matches(ActionId::NewProject, "File", "undo")); @@ -2329,8 +2337,8 @@ mod tests { #[test] fn keyboard_rows_cover_the_menu_bar() { let _guard = crate::actions::shortcuts_test_lock().lock().unwrap(); - let _lang = crate::i18n::lang_test_lock().lock().unwrap(); - crate::i18n::set_language(crate::i18n::Language::EnUs); + let _lang = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code("en-US"); let rows = crate::app::menu_action_paths(); assert!(!rows.is_empty()); // Every listed action resolves to a registry entry with a menu item. diff --git a/src/i18n.rs b/src/i18n.rs index 5734a970d..b995c2509 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -14,21 +14,20 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! Localization: UI strings live in YAML language packs under -//! `assets/i18n/` (one `.yaml` per language, e.g. `en.yaml`, -//! `zh-CN.yaml`), loaded at runtime and shipped inside the app bundle. -//! Users can add or override languages by dropping extra `.yaml` files -//! into the user pack directory (`~/.oak/i18n/`) — no rebuild needed. +//! Localization: UI strings live in YAML language packs discovered at +//! runtime (one `.yaml` per language — the file stem IS the +//! language code, e.g. `en-US.yaml`, `zh-CN.yaml`). Nothing is compiled in: adding a language means +//! dropping one more `.yaml` into any searched directory — no rebuild, +//! no code change. Each pack carries its own display name in the +//! `language.name` key (the endonym, e.g. "Français"), which the +//! language pickers show. //! //! # Lookup order //! //! 1. The user pack directory (`~/.oak/i18n/`); -//! 2. The bundled packs (next to the executable, or the app bundle's -//! `Resources/i18n/`); -//! 3. The dev checkout's `assets/i18n/` (running from the repo); -//! 4. The compiled-in English/Chinese tables (the same YAML embedded -//! with `include_str!`) — the last-resort fallback so a broken -//! install still renders English. +//! 2. The bundled packs (next to the executable, the app bundle's +//! `Resources/i18n/`, or the system install's `/../share/oak/i18n/`); +//! 3. The dev checkout's `assets/i18n/` (running from the repo). //! //! Later sources override earlier ones per key. `tr` falls back from //! the active language to `en-US` and then to the key itself, so a @@ -36,148 +35,80 @@ //! //! # The language setting //! -//! The active language code is a process-global string (an atomic-free +//! The active language code is a process-global string (behind an //! `RwLock` — any thread can read it). At startup [`init`] loads the //! persisted value from the config key `Language`; unknown codes fall -//! back to `en-US`. [`set_language`] flips the global and persists. +//! back to `en-US`. [`set_language_code`] flips the global and persists. use std::collections::HashMap; -use std::sync::atomic::{AtomicU8, Ordering}; use std::sync::{OnceLock, RwLock}; -/// The languages shipped with the app (compiled-in packs). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Language { - /// English (United States). - EnUs, - /// Simplified Chinese. - ZhCN, -} - -impl Language { - /// The stable config/pack-file code, e.g. `"en-US"`. - pub fn code(self) -> &'static str { - match self { - Language::EnUs => "en-US", - Language::ZhCN => "zh-CN", - } - } - - /// Parses a stored config value into a built-in language. Empty or - /// unknown values fall back to en-US (a custom pack's code is kept - /// verbatim by [`set_language_code`]). - fn from_code(code: &str) -> Self { - let code = code.trim().to_ascii_lowercase(); - if code.starts_with("zh") { - Language::ZhCN - } else { - Language::EnUs - } - } -} - -/// The built-in (compiled-in) packs, parsed from the same YAML files the -/// bundle ships: the YAML is the single source of truth, embedded so a -/// missing/corrupt install still renders English. -const BUILTIN_PACKS: &[(&str, &str)] = &[ - ("en-US", include_str!("../assets/i18n/en.yaml")), - ("zh-CN", include_str!("../assets/i18n/zh-CN.yaml")), -]; +/// The fallback language code: the reference pack every other pack is +/// key-compared against, the config default, and `tr`'s last resort +/// before the raw key. +pub const FALLBACK_CODE: &str = "en-US"; /// One language's key → value table (values are leaked once at load — /// the tables live for the whole process). type Table = HashMap; -/// All loaded packs: language code → table. +/// All discovered packs: language code → table. static TABLES: OnceLock>> = OnceLock::new(); -/// The active language code (0/1 fast path for the two built-ins; a -/// custom pack's code lives in [`CURRENT_CUSTOM`]). -static CURRENT: AtomicU8 = AtomicU8::new(0); -static CURRENT_CUSTOM: RwLock> = RwLock::new(None); +/// The active language code. Empty means "not yet initialized" and reads +/// as [`FALLBACK_CODE`]. +static CURRENT_CODE: RwLock = RwLock::new(String::new()); -/// The active language (the built-in view of the current code; custom -/// packs report the nearest built-in). -pub fn language() -> Language { - if CURRENT_CUSTOM - .read() - .unwrap_or_else(|e| e.into_inner()) - .is_some() - { - return match CURRENT.load(Ordering::Relaxed) { - 1 => Language::ZhCN, - _ => Language::EnUs, - }; - } - match CURRENT.load(Ordering::Relaxed) { - 1 => Language::ZhCN, - _ => Language::EnUs, - } -} - -/// The active language code (`en-US`, `zh-CN`, or a pack's code). +/// The active language code (a discovered pack's code, or `en-US`). pub fn language_code() -> String { - if let Some(custom) = CURRENT_CUSTOM - .read() - .unwrap_or_else(|e| e.into_inner()) - .clone() - { - return custom; + let code = CURRENT_CODE.read().unwrap_or_else(|e| e.into_inner()); + if code.is_empty() { + FALLBACK_CODE.to_string() + } else { + code.clone() } - language().code().to_string() } -/// Switches the active language live (built-in) and persists the choice. -pub fn set_language(language: Language) { - CURRENT.store( - match language { - Language::EnUs => 0, - Language::ZhCN => 1, - }, - Ordering::Relaxed, - ); - CURRENT_CUSTOM - .write() - .unwrap_or_else(|e| e.into_inner()) - .take(); - persist_language(language.code()); - sync_widgets(); -} - -/// Switches the active language to any loaded pack's code (a custom -/// language pack). Unknown codes fall back to en-US. +/// Switches the active language to any discovered pack's code and +/// persists the choice. Unknown codes fall back to en-US. pub fn set_language_code(code: &str) { - if code == Language::EnUs.code() || code == Language::ZhCN.code() { - set_language(Language::from_code(code)); - return; - } let exists = tables() .read() .unwrap_or_else(|e| e.into_inner()) .contains_key(code); - if !exists { - set_language(Language::EnUs); - return; - } - *CURRENT_CUSTOM - .write() - .unwrap_or_else(|e| e.into_inner()) = Some(code.to_string()); + let code = if exists { code } else { FALLBACK_CODE }; + *CURRENT_CODE.write().unwrap_or_else(|e| e.into_inner()) = code.to_string(); persist_language(code); sync_widgets(); } -/// The language codes with a loaded pack, built-ins first. +/// A pack's own display name — its `language.name` value (the endonym, +/// e.g. "Français") — which the language pickers show, so a newly +/// dropped-in pack needs no code change. Falls back to the code. +pub fn pack_native_name(code: &str) -> String { + tables() + .read() + .unwrap_or_else(|e| e.into_inner()) + .get(code) + .and_then(|t| t.get("language.name")) + .map(|s| s.to_string()) + .unwrap_or_else(|| code.to_string()) +} + +/// The language codes with a discovered pack: en-US first, zh-CN +/// second, the rest alphabetically by code. pub fn available_languages() -> Vec { let tables = tables().read().unwrap_or_else(|e| e.into_inner()); - let mut codes = vec![Language::EnUs.code().to_string(), Language::ZhCN.code().to_string()]; - let mut extra: Vec = tables - .keys() - .filter(|code| !codes.contains(code)) - .cloned() - .collect(); - extra.sort(); - codes.extend(extra); - codes + let mut codes: Vec = tables.keys().cloned().collect(); + codes.sort(); + let mut out = Vec::with_capacity(codes.len()); + for preferred in [FALLBACK_CODE, "zh-CN"] { + if let Some(pos) = codes.iter().position(|c| c == preferred) { + out.push(codes.remove(pos)); + } + } + out.extend(codes); + out } /// Loads the persisted language from the config. Called once at startup. @@ -204,7 +135,7 @@ pub fn tr(key: &'static str) -> &'static str { if let Some(value) = tables.get(¤t).and_then(|t| t.get(key)) { return value; } - if let Some(value) = tables.get(Language::EnUs.code()).and_then(|t| t.get(key)) { + if let Some(value) = tables.get(FALLBACK_CODE).and_then(|t| t.get(key)) { return value; } key @@ -248,9 +179,8 @@ fn tables() -> &'static RwLock> { TABLES.get_or_init(|| RwLock::new(load_packs())) } -/// The directories searched for packs, lowest precedence first: the -/// compiled-in tables are the base; each directory's packs override the -/// previous source per key. +/// The directories searched for packs, lowest precedence first; each +/// directory's packs override the previous source per key. fn pack_dirs() -> Vec { let mut dirs = Vec::new(); // Dev checkout (running from the repo root or a subdir). @@ -259,9 +189,11 @@ fn pack_dirs() -> Vec { if let Some(dir) = exe.parent() { // Portable layout: /i18n. dirs.push(dir.join("i18n")); - // macOS bundle: /../Resources/i18n. if let Some(contents) = dir.parent() { + // macOS bundle: /../Resources/i18n. dirs.push(contents.join("Resources/i18n")); + // System install (deb/rpm/pkg): /../share/oak/i18n. + dirs.push(contents.join("share/oak/i18n")); } } } @@ -289,14 +221,11 @@ fn parse_pack(code: &str, source: &str) -> Table { table } -/// Loads the built-in packs and every pack file found in [`pack_dirs`] -/// (later sources override earlier ones per key; a file named -/// `.yaml` registers or extends language ``). +/// Loads every pack file found in [`pack_dirs`] (later sources override +/// earlier ones per key; a file named `.yaml` registers or extends +/// language ``). fn load_packs() -> HashMap { let mut packs: HashMap = HashMap::new(); - for (code, source) in BUILTIN_PACKS { - packs.insert(code.to_string(), parse_pack(code, source)); - } for dir in pack_dirs() { let Ok(read_dir) = std::fs::read_dir(&dir) else { continue; @@ -324,6 +253,12 @@ fn load_packs() -> HashMap { .extend(table); } } + if packs.is_empty() { + eprintln!( + "[i18n] no language packs found in {:?}; UI strings degrade to raw keys", + pack_dirs() + ); + } packs } @@ -338,18 +273,35 @@ fn reload_for_test() { mod tests { use super::*; - /// The two compiled-in packs parse and cover the same key set. + /// Every shipped pack (the YAML files in `assets/i18n/`) parses and + /// covers exactly the en-US key set, and carries its `language.name` + /// display name. User packs from `$HOME/.oak/i18n` are deliberately + /// not held to the key set (a partial override pack is legitimate). #[test] - fn every_key_exists_in_both_languages() { + fn every_shipped_pack_matches_en_keys() { let tables = tables().read().unwrap_or_else(|e| e.into_inner()); let en = tables.get("en-US").expect("en-US pack"); - let zh = tables.get("zh-CN").expect("zh-CN pack"); - assert!(!en.is_empty() && !zh.is_empty(), "packs parse to non-empty tables"); - for key in en.keys() { - assert!(zh.contains_key(key), "key {key} missing from the zh-CN pack"); - } - for key in zh.keys() { - assert!(en.contains_key(key), "key {key} missing from the en-US pack"); + assert!(!en.is_empty(), "en-US pack parses to a non-empty table"); + for entry in std::fs::read_dir("assets/i18n").expect("dev checkout packs") { + let path = entry.unwrap().path(); + if path.extension().and_then(|e| e.to_str()) != Some("yaml") { + continue; + } + let code = path.file_stem().unwrap().to_str().unwrap().to_string(); + let table = tables + .get(&code) + .unwrap_or_else(|| panic!("{code} pack")); + assert!(!table.is_empty(), "{code} pack parses to a non-empty table"); + assert!( + table.contains_key("language.name"), + "{code} pack has no language.name display name" + ); + for key in en.keys() { + assert!(table.contains_key(key), "key {key} missing from the {code} pack"); + } + for key in table.keys() { + assert!(en.contains_key(key), "key {key} missing from the en-US pack"); + } } } @@ -391,18 +343,21 @@ mod tests { #[test] fn tr_falls_back_to_english_then_the_key() { let _guard = crate::i18n::test_lock(); - set_language(Language::ZhCN); + set_language_code("zh-CN"); assert_eq!(tr("menu.file"), "文件(F)"); - set_language(Language::EnUs); + set_language_code("en-US"); assert_eq!(tr("menu.file"), "File(F)"); assert_eq!(tr("no.such.key"), "no.such.key"); } /// A custom pack on disk overrides built-in values and registers a - /// new language. + /// new language. The active language is pinned to en-US: the user + /// pack overrides the built-in en-US table, so the assertions below + /// must not inherit another test's active language. #[test] fn user_pack_overrides_and_extends() { let _guard = crate::i18n::test_lock(); + set_language_code("en-US"); let dir = std::env::temp_dir().join(format!("oak_i18n_pack_{}", std::process::id())); let home = dir.join("home"); let pack_dir = home.join(".oak/i18n"); @@ -419,6 +374,8 @@ mod tests { unsafe { std::env::remove_var("HOME") }; reload_for_test(); let _ = std::fs::remove_dir_all(&dir); + // Leave the process language at the default for the next test. + set_language_code("en-US"); } } diff --git a/src/menus/shared.rs b/src/menus/shared.rs index 1caa77878..35eacee64 100644 --- a/src/menus/shared.rs +++ b/src/menus/shared.rs @@ -43,6 +43,41 @@ pub const COLOR_LABEL_BASE: usize = LOCAL_ID_BASE; /// gray). pub const COLOR_LABEL_COUNT: usize = 16; +/// The first id reserved for the dynamic language items: one per +/// discovered language pack, `LANG_ITEM_BASE + index` into +/// [`crate::i18n::available_languages`]. Language switching is +/// data-driven (a new pack is a new YAML file, not a new registry +/// action), so these ids live above the registry range like the color +/// labels. +pub const LANG_ITEM_BASE: usize = COLOR_LABEL_BASE + COLOR_LABEL_COUNT; + +/// The reserved language-item id range (far more than any plausible +/// pack count). +pub const LANG_ITEM_COUNT: usize = 32; + +/// Maps a menu item id in the language range back to its index into +/// [`crate::i18n::available_languages`]. +pub fn language_item_index(item: usize) -> Option { + (item >= LANG_ITEM_BASE && item < LANG_ITEM_BASE + LANG_ITEM_COUNT) + .then(|| item - LANG_ITEM_BASE) +} + +/// Builds the language submenu: one item per discovered pack, labelled +/// with the pack's own `language.name` endonym plus its code, the active +/// pack checkmarked. +pub fn language_menu() -> MenuItem { + let current = crate::i18n::language_code(); + let items = crate::i18n::available_languages() + .iter() + .enumerate() + .map(|(index, code)| { + let label = format!("{} ({code})", crate::i18n::pack_native_name(code)); + MenuItem::new(LANG_ITEM_BASE + index, label).with_checked(*code == current) + }) + .collect(); + MenuItem::new(0, crate::i18n::tr("menu.view.language")).with_submenu(Menu::new(items)) +} + /// One menu item straight from the registry: id and label come from the /// entry, the shortcut annotation from /// [`display_shortcut`](crate::actions::display_shortcut) — the same recipe @@ -375,6 +410,10 @@ mod tests { /// defaults each radio group to its first entry. #[test] fn viewer_menu_offers_every_zoom_level() { + // The label lookups below race with tests that flip the process + // language: pin en-US under the shared lock. + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code("en-US"); let menu = viewer_menu(1); let zoom = menu .items @@ -394,6 +433,10 @@ mod tests { /// (default) entry only. #[test] fn viewer_menu_radio_groups_default_to_the_first_entry() { + // The label lookups below race with tests that flip the process + // language: pin en-US under the shared lock. + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code("en-US"); let menu = viewer_menu(1); for label_key in [ "viewer.context.playback_resolution", @@ -417,6 +460,9 @@ mod tests { /// The resolution radio follows the current playback divider. #[test] fn viewer_menu_resolution_radio_follows_the_divider() { + // Label lookup: pin en-US under the shared language lock. + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code("en-US"); let menu = viewer_menu(4); let item = menu .items diff --git a/src/panels/mod.rs b/src/panels/mod.rs index 0346d0995..038f05b48 100644 --- a/src/panels/mod.rs +++ b/src/panels/mod.rs @@ -78,15 +78,17 @@ pub(crate) fn chip(colors: &gpui::colors::Colors, label: impl gpui::IntoElement) } /// The viewer panel title, per the design: `<面板>·<素材/序列名>` in zh-CN, -/// ` · ` in en-US — no redundant "Source"/"Program" placeholder -/// suffix. The panel key is the localized panel name; `name` is the media or -/// sequence name (data, not translated). +/// ` · ` everywhere else — no redundant "Source"/"Program" +/// placeholder suffix. The panel key is the localized panel name; `name` is +/// the media or sequence name (data, not translated). pub(crate) fn viewer_title(panel_key: &'static str, name: &str) -> String { if name.is_empty() { return crate::i18n::tr(panel_key).to_owned(); } - match crate::i18n::language() { - crate::i18n::Language::ZhCN => format!("{}·{}", crate::i18n::tr(panel_key), name), - crate::i18n::Language::EnUs => format!("{} · {}", crate::i18n::tr(panel_key), name), + // CJK locales read better without the spaced separator. + if crate::i18n::language_code() == "zh-CN" { + format!("{}·{}", crate::i18n::tr(panel_key), name) + } else { + format!("{} · {}", crate::i18n::tr(panel_key), name) } } diff --git a/src/panels/node_editor.rs b/src/panels/node_editor.rs index 7f36376d6..aa6f98a26 100644 --- a/src/panels/node_editor.rs +++ b/src/panels/node_editor.rs @@ -574,6 +574,10 @@ mod tests { /// labels, the reveal entries and properties. #[test] fn node_menu_carries_grouping_and_reveals() { + // The color-label lookup below matches on a localized label: pin + // en-US under the shared language lock. + let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code("en-US"); let menu = node_menu(); let ids: Vec = menu.items.iter().map(|item| item.id).collect(); assert!(ids.contains(&LOCAL_GROUP));