Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ OpenScreen is a free, open-source screen recorder and video editor (Electron + R
- Test (e2e): `npm run test:e2e` (Playwright)
- Lint: `npm run lint` (Biome 2.4)
- Format: `npm run format` (Biome, tabs, double quotes, 100-col)
- i18n check: `npm run i18n:check` (validates the 13 locale files)
- i18n check: `npm run i18n:check` (validates the 15 locale folders)

**Use npm, not bun/pnpm/yarn/Deno.** Not a style preference. Node native modules are rebuilt against Electron's ABI by electron-builder + `@electron/rebuild`, which resolve the tree through `package-lock.json`. Another package manager writes a different lockfile, so that rebuild breaks. `packageManager` + `engines` in `package.json` pin the versions; CI installs with `npm ci`. Note what this does *not* cover: the standalone Swift (macOS) and C++ (Windows) capture helpers are separate executables, built by `npm run build:native:<platform>` and only *copied* into the package as `extraResources` — `build:win` even passes `--config.npmRebuild=false`. Nothing in a normal build compiles them.

Expand Down Expand Up @@ -165,6 +165,6 @@ The one rule to know before you merge anything: **there is one release branch pe

- **Native capture is platform-fragile**: macOS uses ScreenCaptureKit (Swift), Windows uses WGC (C++/Win32). CI runs on Linux only — manual smoke test on real macOS/Windows is required for native changes.
- **Pixi.js v8** is the rendering engine. Filters come from `pixi-filters` and `@pixi/filter-drop-shadow`. GSAP + `motion` for animation.
- **i18n**: 13 locales in `src/i18n/locales/<locale>/` (e.g. `src/i18n/locales/en/settings.json`). The `i18n:check` script validates them — run it after touching translation files.
- **i18n**: 15 locales in `src/i18n/locales/<locale>/` (e.g. `src/i18n/locales/en/settings.json`). The `i18n:check` script validates them — run it after touching translation files.
- **Build pipeline**: `npm run build` is full electron-builder. For iterating on renderer only, use `npm run build-vite` (Vite + tsc, no packaging).
- **Product constraints**: the project is free forever and explicitly "not production-grade". Don't add paywalls, premium tiers, or logic that gates a feature on who the user is, and don't add upsell language to the README or UI copy. This is a hard constraint, not a judgement call. (A flag that hides an unfinished capture backend is fine — it gates on readiness, not on the user.)
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ A raw capture goes in and comes out ready to post — on X, Reddit, YouTube, a d
- Timeline snapping guides and an audio waveform to make trimming easier.
- Customizable keyboard shortcuts.
- Export to MP4 or GIF in multiple aspect ratios and resolutions, rendered and encoded on the GPU (Metal on macOS, D3D11 on Windows, Vulkan on Linux) with an automatic CPU fallback.
- Languages supported: Arabic, English, Spanish, French, Italian, Japanese, Korean, Portuguese (Brazil), Russian, Turkish, Vietnamese, Simplified Chinese, and Traditional Chinese.
- Languages supported: Arabic, Czech, English, Spanish, French, German, Italian, Japanese, Korean, Portuguese (Brazil), Russian, Turkish, Vietnamese, Simplified Chinese, and Traditional Chinese.

## Command-line interface (headless)

Expand Down
3 changes: 2 additions & 1 deletion electron-builder.json5
Original file line number Diff line number Diff line change
Expand Up @@ -405,13 +405,14 @@
// Mirrors SUPPORTED_LOCALES in src/i18n/config.ts. This list is what the Store
// shows as "supported languages" on the product page and what lets the listing
// surface in each language's Store search — declaring only en-US/fr-FR advertised
// 2 of the 14 languages the app actually ships. Bare tags (ar, es, ...) match every
// 2 of the 15 languages the app actually ships. Bare tags (ar, es, ...) match every
// region of that language, which is what the renderer's locale resolution does too.
"languages": [
"en-US",
"fr-FR",
"ar",
"cs",
"de",
"es",
"it",
"ja-JP",
Expand Down
5 changes: 5 additions & 0 deletions electron/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import commonAr from "../src/i18n/locales/ar/common.json";
import dialogsAr from "../src/i18n/locales/ar/dialogs.json";
import commonCs from "../src/i18n/locales/cs/common.json";
import dialogsCs from "../src/i18n/locales/cs/dialogs.json";
import commonDe from "../src/i18n/locales/de/common.json";
import dialogsDe from "../src/i18n/locales/de/dialogs.json";
import commonEn from "../src/i18n/locales/en/common.json";
import dialogsEn from "../src/i18n/locales/en/dialogs.json";
import commonEs from "../src/i18n/locales/es/common.json";
Expand Down Expand Up @@ -34,6 +36,7 @@ type Locale =
| "en"
| "ar"
| "cs"
| "de"
| "es"
| "fr"
| "it"
Expand All @@ -52,6 +55,7 @@ const messages: Record<Locale, Record<Namespace, MessageMap>> = {
en: { common: commonEn, dialogs: dialogsEn },
ar: { common: commonAr, dialogs: dialogsAr },
cs: { common: commonCs, dialogs: dialogsCs },
de: { common: commonDe, dialogs: dialogsDe },
es: { common: commonEs, dialogs: dialogsEs },
fr: { common: commonFr, dialogs: dialogsFr },
it: { common: commonIt, dialogs: dialogsIt },
Expand All @@ -72,6 +76,7 @@ export function setMainLocale(locale: string) {
locale === "en" ||
locale === "ar" ||
locale === "cs" ||
locale === "de" ||
locale === "es" ||
locale === "fr" ||
locale === "it" ||
Expand Down
8 changes: 8 additions & 0 deletions src/components/ai-edition/RightPanes.i18n.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ describe("right-rail panes are localized", () => {
expect(screen.getByText("Masquer si inactif")).toBeInTheDocument();
});

it("renders the cursor pane in German", () => {
renderIn("de", <CursorPane />);
expect(screen.getByRole("heading", { name: "Cursor" })).toBeInTheDocument();
expect(screen.getByText("Glättung")).toBeInTheDocument();
expect(screen.getByText("Klick-Federung")).toBeInTheDocument();
expect(screen.getByText("Bei Inaktivität ausblenden")).toBeInTheDocument();
});

it("falls back to English when the locale is English", () => {
renderIn("en", <VideoEffectsPane />);
expect(screen.getByRole("heading", { name: "Composition" })).toBeInTheDocument();
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/__tests__/tutorialHelpTranslations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { type Locale, SUPPORTED_LOCALES } from "@/i18n/config";
import arDialogs from "@/i18n/locales/ar/dialogs.json";
import csDialogs from "@/i18n/locales/cs/dialogs.json";
import deDialogs from "@/i18n/locales/de/dialogs.json";
import enDialogs from "@/i18n/locales/en/dialogs.json";
import esDialogs from "@/i18n/locales/es/dialogs.json";
import frDialogs from "@/i18n/locales/fr/dialogs.json";
Expand Down Expand Up @@ -44,6 +45,7 @@ const dialogsByLocale = {
en: enDialogs,
ar: arDialogs,
cs: csDialogs,
de: deDialogs,
es: esDialogs,
fr: frDialogs,
it: itDialogs,
Expand Down
1 change: 1 addition & 0 deletions src/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const SUPPORTED_LOCALES = [
"en",
"ar",
"cs",
"de",
"es",
"fr",
"it",
Expand Down
74 changes: 74 additions & 0 deletions src/i18n/locales/de/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"actions": {
"cancel": "Abbrechen",
"save": "Speichern",
"delete": "Löschen",
"close": "Schließen",
"share": "Teilen",
"done": "Fertig",
"open": "Öffnen",
"checkForUpdates": "Nach Updates suchen",
"updateSettings": "Update-Einstellungen",
"updateModeNotify": "Benachrichtigen, wenn ein Update verfügbar ist",
"updateModeDownload": "Updates automatisch herunterladen",
"updateModeDownloadAndInstall": "Updates automatisch herunterladen und installieren",
"viewRelease": "Release ansehen",
"downloadUpdate": "Update herunterladen",
"restartNow": "Jetzt neu starten",
"upload": "Hochladen",
"export": "Exportieren",
"showInFolder": "Im Ordner anzeigen",
"file": "Datei",
"edit": "Bearbeiten",
"view": "Darstellung",
"window": "Fenster",
"help": "Hilfe",
"quit": "Beenden",
"stopRecording": "Aufnahme stoppen",
"undo": "Rückgängig",
"redo": "Wiederholen",
"cut": "Ausschneiden",
"copy": "Kopieren",
"paste": "Einfügen",
"selectAll": "Alles auswählen",
"minimize": "Minimieren",
"reload": "Neu laden",
"forceReload": "Neu laden erzwingen",
"toggleDevTools": "Entwicklertools ein-/ausblenden",
"actualSize": "Originalgröße",
"zoomIn": "Vergrößern",
"zoomOut": "Verkleinern",
"toggleFullScreen": "Vollbild ein-/ausschalten",
"recordingStatus": "Aufnahme: {{source}}",
"about": "Über OpenScreen",
"services": "Dienste",
"hide": "OpenScreen ausblenden",
"hideOthers": "Andere ausblenden",
"unhide": "Alle einblenden",
"saveDiagnostics": "Diagnosedaten speichern"
},
"updates": {
"available": "OpenScreen {{latestVersion}} ist verfügbar. Du verwendest {{currentVersion}}.",
"current": "OpenScreen ist auf dem neuesten Stand ({{currentVersion}}).",
"readyToInstall": "OpenScreen {{latestVersion}} wurde heruntergeladen. Starte neu, um die Installation abzuschließen.",
"downloaded": "OpenScreen {{latestVersion}} wurde heruntergeladen.",
"blockedRecording": "Beende deine Aufnahme, bevor du das Update installierst.",
"blockedLocation": "Verschiebe OpenScreen in deinen Programme-Ordner, bevor du aktualisierst.",
"failed": "Nach Updates konnte nicht gesucht werden.",
"downloadFailed": "Das Update konnte nicht heruntergeladen werden."
},
"units": {
"pixelsPerSecond": "{{value}} px/s",
"pixels": "{{value}} px"
},
"locale": {
"name": "Deutsch",
"short": "DE"
},
"recordingSource": {
"window": "Fenster",
"screen": "Bildschirm",
"virtual": "Virtueller Bildschirm",
"unknown": "Bildschirmaufnahme"
}
}
92 changes: 92 additions & 0 deletions src/i18n/locales/de/dialogs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"export": {
"complete": "Export abgeschlossen",
"yourFormatReady": "{{format}} ist fertig",
"showInFolder": "Im Ordner anzeigen",
"finalizingVideo": "Video-Export wird abgeschlossen…",
"compilingGifProgress": "GIF wird erstellt… {{progress}} %",
"compilingGifWait": "GIF wird erstellt… Das kann eine Weile dauern",
"takeMoment": "Das kann einen Moment dauern…",
"failed": "Export fehlgeschlagen",
"tryAgain": "Bitte versuche es erneut",
"finalizingVideoTitle": "Video wird abgeschlossen",
"compilingGif": "GIF wird erstellt",
"exportingFormat": "{{format}} wird exportiert",
"compiling": "Wird erstellt",
"renderingFrames": "Bilder werden gerendert",
"processing": "Wird verarbeitet…",
"finalizing": "Wird abgeschlossen…",
"compilingStatus": "Wird erstellt…",
"status": "Status",
"format": "Format",
"frames": "Bilder",
"cancelExport": "Export abbrechen",
"savedSuccessfully": "{{format}} erfolgreich gespeichert!"
},
"tutorial": {
"triggerLabel": "So funktioniert Trimmen",
"title": "So funktioniert Trimmen",
"description": "Verstehen, wie du unerwünschte Teile aus deinem Video herausschneidest.",
"explanationBefore": "Mit dem Trim-Werkzeug legst du die Abschnitte fest, die du",
"remove": "entfernen",
"explanationMiddle": " möchtest — alles, was von einem roten Trim-Bereich",
"covered": "abgedeckt",
"explanationAfter": "ist, wird beim Export herausgeschnitten.",
"visualExample": "Visuelles Beispiel",
"removed": "ENTFERNT",
"kept": "Behalten",
"part1": "Teil 1",
"part2": "Teil 2",
"part3": "Teil 3",
"finalVideo": "Fertiges Video",
"step1Title": "1. Trim hinzufügen",
"step1DescriptionBefore": "Drücke ",
"step1DescriptionAfter": " oder klicke auf das Scheren-Symbol, um einen Abschnitt zum Entfernen zu markieren.",
"step2Title": "2. Anpassen",
"step2Description": "Ziehe die Ränder des roten Bereichs so, dass er genau das abdeckt, was du herausschneiden willst."
},
"unsavedChanges": {
"title": "Nicht gespeicherte Änderungen",
"message": "Du hast nicht gespeicherte Änderungen.",
"detail": "Möchtest du dein Projekt speichern, bevor du schließt?",
"saveAndClose": "Speichern & schließen",
"discardAndClose": "Verwerfen & schließen",
"detailNewProject": "Möchtest du dein Projekt speichern, bevor du ein neues anlegst?",
"saveAndNewProject": "Speichern & neues Projekt",
"discardAndNewProject": "Verwerfen & neues Projekt",
"detailLoadProject": "Möchtest du dein Projekt speichern, bevor du ein anderes lädst?",
"saveAndLoadProject": "Speichern & Projekt laden",
"discardAndLoadProject": "Verwerfen & Projekt laden",
"newProject": "Neues Projekt",
"importVideo": "Videodatei importieren…",
"loadProject": "Projekt laden…",
"saveProject": "Projekt speichern…",
"saveProjectAs": "Projekt speichern unter…",
"modal": {
"closeTitle": "Änderungen vor dem Schließen speichern?",
"closeBody": "Dein Projekt hat nicht gespeicherte Änderungen. Speichere sie jetzt, verwirf sie oder bleib im Editor.",
"newTitle": "Änderungen speichern, bevor du ein neues Projekt anlegst?",
"openTitle": "Änderungen speichern, bevor du ein anderes Projekt öffnest?",
"recordTitle": "Änderungen speichern, bevor du eine neue Aufnahme startest?",
"sharedBody": "Das aktuelle Projekt hat nicht gespeicherte Änderungen. Speichere sie zuerst, verwirf sie oder brich ab.",
"notSavedYet": "Das aktuelle Projekt wurde noch nicht auf der Festplatte gespeichert.",
"discard": "Verwerfen",
"saveAndContinue": "Speichern & fortfahren",
"saving": "Wird gespeichert…"
}
},
"fileDialogs": {
"saveGif": "Exportiertes GIF speichern",
"saveVideo": "Exportiertes Video speichern",
"selectVideo": "Videodatei auswählen",
"selectAudio": "Audiodatei auswählen",
"saveProject": "OpenScreen-Projekt speichern",
"openProject": "OpenScreen-Projekt öffnen",
"gifImage": "GIF-Bild",
"mp4Video": "MP4-Video",
"videoFiles": "Videodateien",
"audioFiles": "Audiodateien",
"openscreenProject": "OpenScreen-Projekt",
"allFiles": "Alle Dateien"
}
}
Loading
Loading