diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81a7c35d..c44737b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,8 +83,22 @@ jobs: - name: Install FFmpeg timeout-minutes: 6 run: | - sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends ffmpeg + # Ubuntu's own package is 6.1.1, and the Docker image ships 8.1.2. + # Testing against 6.1.1 meant CI never exercised the FFmpeg users + # actually get, and the two differ in ways that matter: 6.1.1 accepts + # an 80-channel amerge where 8.1 stops at 64, and 6.1.1 cannot demux + # multitrack FLV at all. Pin the same 8.1 line the image uses. + set -euo pipefail + cd /tmp + # -c rather than a heredoc: inside a YAML block the heredoc body and + # its terminator are indented, which Python reads as a bad indent and + # bash does not accept as a delimiter. + python3 -c 'import urllib.request; urllib.request.urlretrieve("https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n8.1-latest-linux64-gpl-8.1.tar.xz", "/tmp/ff.tar.xz")' + mkdir -p ffbuild && tar xf ff.tar.xz --strip-components=1 -C ffbuild + sudo install -m 0755 ffbuild/bin/ffmpeg /usr/local/bin/ffmpeg + sudo install -m 0755 ffbuild/bin/ffprobe /usr/local/bin/ffprobe + # Fail loudly rather than silently testing a build without SRT. + ffmpeg -hide_banner -protocols | tr ' ' '\n' | grep -qx srt ffmpeg -hide_banner -version | head -1 # The gate is "gofmt named no files", and it has to rest on gofmt having @@ -196,7 +210,24 @@ jobs: - name: Install FFmpeg (Linux) timeout-minutes: 6 if: runner.os == 'Linux' - run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends ffmpeg + run: | + # Ubuntu's own package is 6.1.1, and the Docker image ships 8.1.2. + # Testing against 6.1.1 meant CI never exercised the FFmpeg users + # actually get, and the two differ in ways that matter: 6.1.1 accepts + # an 80-channel amerge where 8.1 stops at 64, and 6.1.1 cannot demux + # multitrack FLV at all. Pin the same 8.1 line the image uses. + set -euo pipefail + cd /tmp + # -c rather than a heredoc: inside a YAML block the heredoc body and + # its terminator are indented, which Python reads as a bad indent and + # bash does not accept as a delimiter. + python3 -c 'import urllib.request; urllib.request.urlretrieve("https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n8.1-latest-linux64-gpl-8.1.tar.xz", "/tmp/ff.tar.xz")' + mkdir -p ffbuild && tar xf ff.tar.xz --strip-components=1 -C ffbuild + sudo install -m 0755 ffbuild/bin/ffmpeg /usr/local/bin/ffmpeg + sudo install -m 0755 ffbuild/bin/ffprobe /usr/local/bin/ffprobe + # Fail loudly rather than silently testing a build without SRT. + ffmpeg -hide_banner -protocols | tr ' ' '\n' | grep -qx srt + ffmpeg -hide_banner -version | head -1 - name: Install FFmpeg (macOS) if: runner.os == 'macOS' run: brew install ffmpeg @@ -328,6 +359,11 @@ jobs: - run: npx --no-install tsc -b --noEmit - run: npm run lint - run: npm run build + # Unit tests for the pure logic the browser suite cannot enumerate -- + # platform link construction has five platforms times several missing-field + # cases, and driving each through a real browser would cost minutes to + # assert what a millisecond of vitest does. + - run: npm test # A vulnerable direct dependency should fail the build, not sit in a # report nobody opens. --audit-level=high so a low-severity transitive # advisory does not block a hotfix. diff --git a/.gitignore b/.gitignore index dd51cdcc..fa07d7cf 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,14 @@ server.key # not part of them. Regenerated freely and often half-finished, and a deck is # not something a reader of this repository needs in order to run polyemesis. presentation/ + +# Playwright's own trace/video output from the capture harness. 18 MB of +# .webm per run, regenerated every time, and it lands INSIDE docs/media +# because that is where the harness writes — so it is one `git add -A` away +# from history unless it is named here. +docs/media/.playwright/ + +# The built site. `/dist/` above is root-anchored and does not cover it, so +# without this line web/dist ships a second copy of every screenshot and the +# 1.4 MB tour video that web/public already holds. +web/dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 41549d38..71da4705 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,69 @@ its first tagged release. ## [Unreleased] +### Added + +- **Chat search.** `GET /api/v1/chat/search?q=` matches a message's text or its + author's name across the retained scrollback, newest first, optionally scoped + to one platform. The pane gains a search box that replaces the live timeline + while a query is active — results come from the database and are frozen at the + moment of the query, so letting live messages append underneath them would + present two different things as one list. Search is where an operator can + wrongly conclude something did not happen, so the retention caveat renders on + an empty result, not only on a full one. +- **Right-click and double-click moderation on a chat message.** Timeouts, delete + and the user card, two seconds from reading a bad line to acting on it. + Double-click is the same menu for pointers with no secondary button. It adds no + capability the user card lacked; a permanent ban deliberately does *not* fire + from the menu but opens the card, which confirms — the one irreversible action + reachable from a right-click should not be a single click on a menu that + appeared under the cursor. +- **Links out to the platform.** Twitch gets its real moderator viewer card in a + separate window. YouTube, Kick and Facebook get a profile or channel link and + say so, because none of them publishes a per-viewer chat history at any URL — + a uniform "Open on " label would promise a moderator the same thing + everywhere and deliver it only on Twitch. +- **Per-setting help.** An `(i)` popover beside a setting's label explains what it + actually changes — 2.5× your round-trip time for SRT latency, why the free-space + floor is the only limit that accounts for files polyemesis did not write. Click + rather than hover, so it works on touch and under a screen reader, and the body + is a catalogue key, so the explanation is translated too. + +### Changed + +- **The application is translated.** Every page under `src/pages` now reads from + the catalogue; a sweep for hard-coded prose returns nothing. The catalogue grew + from 135 keys to 1,098 and all fifteen languages are complete. + + Previously only the nav shell and three widgets were extracted, so an operator + who chose Deutsch got a German sidebar and an English application. The 135 keys + that existed were complete in every language, which is exactly why the gap was + easy to miss: the coverage looked like 100%. + + Extraction found prose in four shapes, each invisible to the check written for + the one before it — JSX text, string props and toasts; ternaries such as + `{busy ? "Pushing…" : "Push to platforms"}`; object-literal properties built + from template literals; and eight module-scope tables. The last of those could + never have called `useT()` at all, since hooks do not run at module scope, so + each now holds a `TranslationKey` and is translated where it is rendered — a + `Record` cannot hold a sentence, which the compiler enforces. +- **One word per locale for "rendition".** Ten of the fifteen catalogues disagreed + with themselves across `nav.renditions`, `rend.title`, `sources.renditions` and + `dash.renditions`. In Polish the sidebar read "Opcje jakości" while the page it + opened was titled "Warianty", so following the link appeared to land elsewhere. + Japanese, Korean and Dutch additionally used a term meaning "rendering + settings"; a rendition is one video variant at a given size and bitrate, not a + settings screen, and they now say so. +- `vitest` runs in CI alongside `tsc` and `oxlint`, covering the pure logic the + browser suite cannot practically enumerate — platform link construction across + five platforms, and the translation catalogues themselves. + +### Fixed + +- Chat's `(i)` help buttons no longer all announce as "More information"; each + names the setting it explains, so a screen-reader user can tell a dozen of them + apart. + ## [0.3.0] — 2026-08-05 A minor bump rather than a patch, and deliberately: `facebook.backupIngest` diff --git a/README.md b/README.md index ef963b4f..0dc95195 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,10 @@ hot. One upload, one video encode, different audio per platform. encode feeds every destination that selects it, ref-counted so an unused tier costs nothing — and it re-encodes **video only**, so audio still routes per destination. [→ RENDITIONS.md](docs/RENDITIONS.md) -- **SRT multitrack ingest** (up to 6 AAC tracks), addressed by token on a single - port, with RTMP as a single-track fallback. [→ OBS.md](docs/OBS.md) +- **SRT multitrack ingest** (up to 32 AAC tracks; six is what OBS sends), addressed by token on a single + port, with RTMP as a single-track fallback — also one port, addressed by + stream key, and equally unlimited in how many sources it carries. + [→ OBS.md](docs/OBS.md) - **Live audio meters** for every channel of every track — how you verify the clean track really is clean, *before* going live. - **Unlimited destinations**: RTMP(S), SRT, or local file, each independently @@ -86,9 +88,8 @@ roadmap. | | | |---|---| -| **No multitrack over RTMP.** | RTMP carries one stereo pair. Enhanced RTMP multitrack from OBS 30.2+ is **not** supported, and the `enhancedRtmp` config key is an inert placeholder. Multitrack means SRT. | -| **One RTMP source, maximum.** | polyemesis has no RTMP server of its own — it uses `ffmpeg -listen 1`, which cannot demultiplex by path. Any number of sources can share the SRT port; only one can use RTMP. [Why](docs/DESIGN-ONE-PORT-ONLY.md#rtmp) | -| **No per-source ports.** | Every push source is addressed by its token on one SRT port. Giving one programme its own port for firewall, NIC or QoS purposes is not something polyemesis does. [Why](docs/DESIGN-ONE-PORT-ONLY.md#what-it-costs) | +| **RTMP multitrack is not the operated path.** | Classic RTMP carries one stereo pair. Enhanced RTMP (OBS 30.2+) carries more and works on FFmpeg 7.1+ — verified end to end — but not on FFmpeg 6.1.1, and not yet confirmed with OBS publishing. SRT is what is operated. See `docs/notes/enhanced-rtmp-multitrack.md`. | +| **No per-source ports.** | Every push source is addressed by its token on one SRT port, or its stream key on one RTMP port. Giving one programme its own port for firewall, NIC or QoS purposes is not something polyemesis does. [Why](docs/DESIGN-ONE-PORT-ONLY.md#what-it-costs) | | **One user, no roles.** | No multi-user model, no per-destination permissions. Access to the UI is full control of the server's streaming — and, through file destinations and expert mode, meaningful control of the machine. | | **Instagram Live cannot work.** | There is no Live broadcast API, and Live Producer's RTMP path was removed for most accounts. It is listed and marked unsupported rather than shipped as a preset that quietly never connects. | | **Your source video is your problem.** | Renditions can re-encode down, but polyemesis will not rescue a 4K60 ingest on a box that cannot software-encode it in realtime. | diff --git a/SECURITY.md b/SECURITY.md index 24675f26..1a794347 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -103,10 +103,13 @@ These are design decisions, not oversights. Read them as operating instructions. need it, it is guarded by a confirmation showing exactly what will be spliced in, and it is still a way to make FFmpeg do things. Treat the ability to reach it as equivalent to shell access. -- **RTMP ingest is only as protected as its stream key.** SRT is authenticated - by construction — the publish token *is* the address, so a publisher that - cannot present a valid one is refused. RTMP has no such routing: it is gated - by the stream key alone, and it serves at most one source. +- **RTMP ingest is only as protected as its stream key.** Both protocols are now + authenticated by construction — the publish token *is* the address, over SRT as + `streamid` and over RTMP as the stream key in the URL path, so a publisher that + cannot present a valid one is refused. The difference that remains is the wire: + SRT can additionally encrypt with a passphrase, RTMP cannot unless you put it + behind RTMPS. Subscribing to the RTMP listener is restricted to loopback, so a + publish credential never doubles as a viewing one. - **Nothing is encrypted on the wire unless you say so.** Plain HTTP means the password and session cookie cross the network in clear text. The server warns about this at startup; the warning is not decorative. diff --git a/cmd/polyemesis/main.go b/cmd/polyemesis/main.go index 59a09c10..57313f82 100644 --- a/cmd/polyemesis/main.go +++ b/cmd/polyemesis/main.go @@ -569,7 +569,14 @@ func reportStartup(log *slog.Logger, cfg config.Config, provider *tlsx.Provider, fmt.Printf("\n polyemesis %s\n", version) fmt.Printf(" web ui %s://%s\n", scheme, shown) - fmt.Printf(" ingest %s (port %d)\n", settings.Ingest.Mode, ingestPort(settings)) + // An install that has not chosen yet prints that, rather than an empty mode + // beside a port number — which reads as "srt on 6000" to anyone skimming and + // is the one impression this must not give. + if settings.Ingest.Mode == db.IngestUnset { + fmt.Printf(" ingest not chosen yet — pick SRT, RTMP or pull in the web UI\n") + } else { + fmt.Printf(" ingest %s (port %d)\n", settings.Ingest.Mode, ingestPort(settings)) + } fmt.Printf(" data dir %s\n", cfg.DataDir) fmt.Printf(" ffmpeg %s\n", tools.Version) reportTLS(cfg, provider, shown) diff --git a/docker-compose.yml b/docker-compose.yml index 20db6aa1..26eecd43 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,8 +32,10 @@ services: - "6000:6000/udp" # SRT ingest — UDP, not TCP. ONE port for every # source; they are told apart by publish token, # so adding a source never changes this file. - - "1935:1935" # RTMP ingest (fallback, single stereo pair, - # and serves at most one source) + - "1935:1935" # RTMP ingest. One port, any number of sources, + # told apart by stream key — same as SRT above. + # Classic RTMP carries one stereo pair; Enhanced + # RTMP carries more on FFmpeg 7.1+. # ACME ONLY. Uncomment together with the config mount below if you want # polyemesis to obtain its own Let's Encrypt certificate: HTTP-01 diff --git a/docs/API.md b/docs/API.md index 76988049..3b2e1005 100644 --- a/docs/API.md +++ b/docs/API.md @@ -370,7 +370,7 @@ told so. The schedule still saves and still runs. | `GET` | `/oauth/{platform}/start`, `/callback` | | `GET` | `/metadata`, `/metadata/broadcast-window` | | `POST` | `/metadata/push`, `GET` `/metadata/push/{id}` | -| `GET` | `/chat`, `/chat/messages`, `/chat/users` | +| `GET` | `/chat`, `/chat/messages`, `/chat/search`, `/chat/users` | | `POST` | `/chat/send` | | `DELETE` | `/chat/messages` | | `POST` | `/chat/messages/hide` | @@ -391,6 +391,21 @@ Its mod card is a web-app feature backed by internal endpoints. The trade is depth for breadth: shallower than Twitch's card, and it works across all four platforms at once. +`/chat/search?q=` finds a message again, matching on its text **or its author's +name**, newest first — the one read here that is not chronological, because a +result list answers "where did that comment go" and burying the likeliest answer +at the bottom would be perverse. `platform=` narrows it to one tab and `limit=` +bounds the page. + +It searches the database and never the Hub's in-memory ring, which holds only +what the current process has seen; "find the comment from earlier" is precisely +the question a process-lifetime buffer cannot answer. The same caveat as +`/chat/users` applies and applies harder: the response carries `retentionNote` +and `truncated` because search is the one place an operator can conclude +something did *not* happen. **An empty result means "not in the scrollback we +kept", never "never said"** — so render the note alongside no-results, not only +alongside a full page. + `DELETE /chat/messages` removes one message on the platform. `POST /chat/messages/hide` is Facebook's reversible hide where the platform offers it, and a local-only hide everywhere else — the pane stops showing it, the platform diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1dd08667..cb3bc045 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -17,10 +17,16 @@ any number of destinations select and that touches video only. See §3. │ │ SRT (mpegts): 1 video + up to 6 AAC tracks │ or RTMP (1 video + 1 AAC track) + │ + │ Both land on ONE shared listener per protocol, addressed by the + │ publish token (SRT) or the stream key (RTMP). RTMP's is a Go + │ pub/sub server: the encoder publishes, the ingest ffmpeg below + │ subscribes to the same port over loopback. ▼ ┌───────────────────────────────────────────┐ │ ingest ffmpeg (supervised) │ │ -i srt://0.0.0.0:P?mode=listener │ +│ or -i rtmp://127.0.0.1:1935/live/KEY │ │ -map 0 -c copy -f mpegts │ │ udp://127.0.0.1:HUB_IN │ all tracks preserved, no decode └───────────────────────────────────────────┘ @@ -93,25 +99,53 @@ slow subscriber stall the hub, and `fifo_size`/`overrun_nonfatal=1` on the consumer side. A future `relay.Mode = "srt"` can swap the transport without touching anything above it. -### Why FFmpeg's RTMP listener, not yutopp/go-rtmp - -RTMP is the *fallback* ingest (single audio track), so the bar is "robust and -cheap", not "feature-rich". - -- go-rtmp hands us FLV tags. To reach the relay we would then have to write an - FLV→MPEG-TS muxer plus AVC/AAC bitstream handling (AVCDecoderConfigurationRecord - → Annex-B, ADTS framing) by hand. That is a large, subtle surface area for zero - user-visible gain. -- `ffmpeg -listen 1 -i rtmp://0.0.0.0:1935/live/KEY` demuxes FLV and emits - MPEG-TS with `-c copy`, which is **the exact same code path as SRT ingest** — - one `IngestArgs` builder, one supervisor, one relay entry point. -- The listener serves one publisher then exits; the supervisor already respawns - processes with backoff, so "wait for next publisher" is free. -- Stream-key auth comes from the URL path, which ffmpeg matches against the - publisher's `app/playpath`. - -The product has exactly one publisher (the streamer), so single-publisher -semantics are correct, not a limitation. +### Why a Go RTMP server (`internal/rtmpserver`), not `ffmpeg -listen 1` + +RTMP is the *fallback* ingest (one audio track on classic RTMP — see +`notes/enhanced-rtmp-multitrack.md` for what changes on FFmpeg 7.1+), and for a +long time the bar was "robust and cheap": `ffmpeg -listen 1` demuxed FLV and +emitted MPEG-TS with `-c copy`, which was the exact same code path as SRT +ingest, and the supervisor's respawn-with-backoff made "wait for the next +publisher" free. + +What that could never do is **demultiplex by path**. `-listen 1` is a +single-connection receiver, so an install could carry exactly one RTMP source +while SRT carried as many as you liked. That asymmetry was an artifact of the +tool, not a decision — and the rule it produced ("how many programmes you can +run depends on which protocol your encoder speaks") is not one anybody would +choose. It is now gone; the full argument, including what it cost to close, is +in [DESIGN-ONE-PORT-ONLY.md](DESIGN-ONE-PORT-ONLY.md#rtmp). + +The shape that replaced it: + +- `internal/rtmpserver` wraps `bluenviron/gortmplib` and adds only the part that + is ours, which is the same part `srtserver` adds over `gosrt`: a constant-time + `Lookup` from stream key to source, one publisher slot per (source, role), and + a relay. It is not a protocol implementation. +- **One port, both directions.** The encoder publishes to + `rtmp://host:1935/live/KEY`; the ingest ffmpeg subscribes to + `rtmp://127.0.0.1:1935/live/KEY` on that same listener. No per-source ports, + not even loopback ones — which is exactly what the earlier relay-outward draft + would have reintroduced. +- **Messages, never frames.** gortmplib's `Reader` hands over decoded access + units; using it would put a muxer in the critical path of every frame. This + forwards RTMP *messages*, so the bytes reaching FFmpeg are the bytes the + encoder sent, `-map 0 -c copy` is untouched downstream, and Enhanced RTMP + multitrack rides through without the package knowing what a track is. +- The one exception is stream **setup** — `onMetaData` and the codec sequence + headers — which are cached per stream key and replayed to each new subscriber. + A subscriber that joins after them cannot decode anything, and the order of + "encoder connects" and "FFmpeg connects" genuinely varies. That is a type + switch, not a decode. +- **Subscribing is loopback-only.** A stream key is a publish credential; if it + also authorised playback, every ingest key would quietly become a viewing key. + Viewer playback is `playout/`'s job, behind authentication. + +`yutopp/go-rtmp`, the dependency measured and rejected when this section was +first written, is still rejected — see +[DEPENDENCIES.md](DEPENDENCIES.md#githubcombluenvirongortmplib--the-third-protocol-dependency) +for why gortmplib is a different question rather than the same one answered +differently. --- @@ -533,8 +567,11 @@ Worth stating because the TLS mode is easy to mistake for the whole story: - SRT ingest carries its own AES encryption via a passphrase (`db.SRTSettings.Passphrase`, 10–79 characters, enforced in `Settings.Validate`), rendered into both the listener URL and the - copy-pasteable encoder URL. RTMP ingest has no equivalent — it is - authenticated by the stream key in the path and is otherwise in the clear. + copy-pasteable encoder URL. RTMP ingest has no equivalent: it is authenticated + by the stream key in the path — matched in constant time by + `internal/rtmpserver`, the same contract `srtserver` applies to tokens — but + the connection carries no encryption, so the key and the media both cross the + network in the clear. - Destination URLs are passed to FFmpeg verbatim, and `db.Destination.Validate` accepts `rtmps://` as well as `rtmp://`, so a destination can be TLS-wrapped independently of how the UI is served. @@ -582,6 +619,10 @@ internal/ -- ingest and viewer-facing edges srtserver/ the one-port SRT ingest: one listener serving every source, demultiplexed by the publish token + rtmpserver/ the same shape for RTMP: one listener serving every source, + demultiplexed by the stream key in the publish URL. Publish and + subscribe on that one port -- encoders push in, this install's + own FFmpeg pulls back out over loopback playout/ the viewer-facing origin: packages the relay into public HLS (optionally DASH) and counts who is watching meters/ the measurement tier: what a destination is ACTUALLY sending, diff --git a/docs/AUDIO-ROUTING.md b/docs/AUDIO-ROUTING.md index 5cf96a2f..021bf75d 100644 --- a/docs/AUDIO-ROUTING.md +++ b/docs/AUDIO-ROUTING.md @@ -2,7 +2,8 @@ This is the feature the rest of the product exists to serve. -One encoder sends up to six audio tracks. Every destination gets its own mix of +One encoder sends up to 32 audio tracks — six is what OBS sends, and what most +setups use. Every destination gets its own mix of them, compiled into its own FFmpeg `-filter_complex` graph. YouTube can receive the clean mix while your archive keeps everything and your Discord restream keeps the mic hot — from one upload and one video encode. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 116f10c1..54c810bd 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -101,15 +101,21 @@ cannot validate is an excellent way to lock yourself out of your own tool. ### `enhancedRtmp` was removed -It is no longer a key. Enhanced RTMP / multitrack FLV ingest (OBS 30.2+) is not -implemented and RTMP ingest is single-track either way, so the key never did -anything — it was declared as a placeholder and kept on the belief that config -files already carrying it would otherwise fail to parse. +It is no longer a key, and it does not need reinstating. Enhanced RTMP / +multitrack FLV ingest (OBS 30.2+) turns out to work on **FFmpeg 7.1+** — +verified end to end — and not on FFmpeg 6.1.1, which is Ubuntu 24.04's stock +build. Either way there is nothing for a flag to switch: where it works, it +works by default, because the tracks arrive through the existing ingest command +unchanged. It confirms rather than undermines the removal — the key was declared +as a placeholder and kept on the belief that config files already carrying it +would otherwise fail to parse. That belief was wrong: config loading ignores unrecognised keys, so the declaration was buying nothing while presenting a knob an operator could set and watch have no effect. **A config file that still names `enhancedRtmp` loads -exactly as before.** For multitrack ingest today, use SRT. +exactly as before.** For multitrack ingest that is actually operated, use SRT: +Enhanced RTMP's version dependency is real, and it has not been confirmed with +OBS itself as the publisher. See `notes/enhanced-rtmp-multitrack.md`. ## The data directory diff --git a/docs/DEPENDENCIES.md b/docs/DEPENDENCIES.md index b70161d4..58c5db36 100644 --- a/docs/DEPENDENCIES.md +++ b/docs/DEPENDENCIES.md @@ -36,6 +36,7 @@ Ten, deliberately. Each one earns its place below. | Module | Version | Used by | | --- | --- | --- | +| `github.com/bluenviron/gortmplib` | v1.0.0 | `internal/rtmpserver` | | `github.com/datarhei/gosrt` | v0.11.0 | `internal/srtserver` | | `github.com/eclipse/paho.golang` | v0.23.0 | `internal/mqtt` | | `github.com/go-chi/chi/v5` | v5.3.1 | `internal/api` | @@ -61,11 +62,26 @@ rejected — see [DESIGN-ONE-PORT-ONLY.md](DESIGN-ONE-PORT-ONLY.md#rtmp). go-rtmp would have pulled seven further modules including `logrus` (a second logging framework in a binary that uses `log/slog`) plus `pkg/errors` and `mapstructure`, both dead -upstream — and `ffmpeg -listen 1` was already a complete answer for RTMP. gosrt -had no such alternative: FFmpeg's SRT support is a client and a +upstream. gosrt had no such alternative: FFmpeg's SRT support is a client and a single-connection listener, and neither can demultiplex many publishers by `streamid` on one port. +The same reasoning eventually applied to RTMP, and it is why +`github.com/bluenviron/gortmplib` joined the tree on 2026-08-06: three +transitive modules against go-rtmp's seven, at v1.0.0 rather than v0.0.7, from +the people who maintain gortsplib and MediaMTX. Re-measured, not assumed — +see `notes/multi-source-rtmp.md`. + +> **The RTMP half of that comparison was retired on 2026-08-06.** It read "and +> `ffmpeg -listen 1` was already a complete answer for RTMP", and that was the +> claim that failed: `-listen 1` is *also* a single-connection listener, so it +> could not demultiplex many publishers by `app/streamkey` on one port either — +> the identical objection, which nobody had noticed applied to both. The rule +> survives unchanged and was in fact what settled the question; only the belief +> that FFmpeg satisfied it for RTMP was wrong. See +> [gortmplib](#githubcombluenvirongortmplib--the-third-protocol-dependency) +> below. + Pure Go, MIT, and `CGO_ENABLED=0` clean. > One licence detail worth knowing, recorded in @@ -103,6 +119,49 @@ of our code: `paho.mqtt.golang` — the older sibling — was not taken: it is in maintenance mode and speaks 3.1.1, which is the wrong direction for a new integration. +### `github.com/bluenviron/gortmplib` — the third protocol dependency + +Added for `internal/rtmpserver`, so that how many sources an install can carry +does not depend on which protocol the encoder speaks. + +It clears gosrt's bar for exactly the reason gosrt did, applied to the protocol +nobody had thought to apply it to. FFmpeg's RTMP support is a client and +`-listen 1`, a single-connection listener — the same shape as its SRT support, +and the same failure: neither can demultiplex many publishers by `app/streamkey` +on one port. The earlier "FFmpeg was already a complete answer for RTMP" was +true only of the single-source case, which is the case that was the problem. + +The dependency that was rejected is not this one. That measurement is worth +keeping side by side, because the difference is the whole justification: + +| | net-new modules | version | provenance | +|---|---|---|---| +| `datarhei/gosrt` | 1 | v0.11.0 | datarhei | +| **`bluenviron/gortmplib`** | **3** — `abema/go-mp4`, `bluenviron/mediacommon/v2`, `google/uuid`* | v1.0.0 | gortsplib / MediaMTX maintainers | +| `yutopp/go-rtmp` (rejected) | 7 — `logrus`, `pkg/errors`, `mapstructure`, … | v0.0.7 | unchanged since 2021 | + +\* `google/uuid` was already in the tree via sqlite, so the true net-new count +is two. + +Three modules is a real cost and not a free one. What makes it payable rather +than a reversal of the earlier decision: + +- **None of it is dead upstream**, which was the specific objection to go-rtmp's + tree — `mapstructure` pinned at a 2021 release, `pkg/errors` archived. +- **No second logging framework.** go-rtmp brings `logrus` into a binary that + uses `log/slog`. +- **v1.0.0, not v0.0.7**, from the people who maintain gortsplib and MediaMTX — + the same provenance argument that justified gosrt. +- All three are MIT and `CGO_ENABLED=0` clean. + +**Only the `Conn` seam is used, never `Reader`.** gortmplib will hand over +decoded access units if you ask it to; taking that would put a muxer in the +critical path of every frame and make a class of bug ours that currently belongs +to FFmpeg. `internal/rtmpserver` forwards RTMP *messages*, which is why +`-map 0 -c copy` downstream is untouched and Enhanced RTMP multitrack works +without the package knowing what a track is. That restraint is the reason this +is a ~540-line package and not a media stack. + ### `modernc.org/sqlite` — and why not `mattn/go-sqlite3` This is the single most consequential dependency in the project, and the one @@ -222,9 +281,13 @@ bundling is aggregation in an image, and does not change the analysis above. ## Indirect Go dependencies -All indirect modules come from exactly two roots: `gopsutil` and -`modernc.org/sqlite`. There are no other transitive sources, which is a -property worth preserving. +Every indirect module traces to one of a small, countable set of roots: +`gopsutil`, `modernc.org/sqlite`, `golang.org/x/crypto`, `gosrt` (one module, +`benburkert/openpgp`) and `gortmplib` (`abema/go-mp4`, +`bluenviron/mediacommon/v2`, and `google/uuid`, which sqlite already pulled). +There are no other transitive sources. Keeping that list short enough to write +down is the property worth preserving; `go mod why -m` should always have a +one-line answer. They are kept current on a plain `go get -u ./... && go mod tidy`, gated on the full verification below. As of the last review all indirect modules resolve to @@ -232,10 +295,13 @@ their latest published versions. A handful of modules appear in `go.sum` but not in `go.mod` — `google/pprof`, `golang.org/x/mod`, `golang.org/x/tools`, `golang.org/x/xerrors`, -`check.v1`, `modernc.org/cc/v4`, `modernc.org/gc/v3`. These are build- and -test-time dependencies of modernc's own code generation. They are not in the -binary. `go list -u -m all` will report some of them as outdated forever; that -is expected and is not a finding. +`check.v1`, `modernc.org/cc/v4`, `modernc.org/gc/v3`, and from the gortmplib +side `orcaman/writerseeker`, `sunfish-shogi/bufseekio`, `asticode/go-astits`, +`asticode/go-astikit` and `stretchr/testify`. These are build- and test-time +dependencies of other modules' own suites and code generation. They are not in +the binary — `go.mod`'s indirect block is the list that is, which is why the +net-new count above is three and not thirteen. `go list -u -m all` will report +some of them as outdated forever; that is expected and is not a finding. ## Frontend dependencies diff --git a/docs/DESIGN-ONE-PORT-INGEST.md b/docs/DESIGN-ONE-PORT-INGEST.md index ee16afc1..a2b6d2ba 100644 --- a/docs/DESIGN-ONE-PORT-INGEST.md +++ b/docs/DESIGN-ONE-PORT-INGEST.md @@ -5,9 +5,11 @@ > a shared token-addressed port *alongside* per-source ports, with the shared > path off by default. That position was reversed: token-addressed SRT is now > the only SRT path, per-source ports are gone entirely, and the failover backup -> is reached at `.backup` on the same port. The comparison with Core -> below still holds and is why this page is kept — read the successor first for -> what actually shipped. +> is reached at `.backup` on the same port. RTMP followed on 2026-08-06 — +> one port, addressed by stream key, any number of sources — so the "Not doing +> (yet)" section below is now fully spent. The comparison with Core below still +> holds and is why this page is kept — read the successor first for what +> actually shipped. Inspired by datarhei Core, not copied from it. Core proves the shape works — one SRT port, many programmes, demultiplexed at accept time. This document is @@ -140,6 +142,14 @@ a real SRT publisher and every SRT proof so far has had to run in Docker. so a port each costs little, and a Go RTMP server is a separate dependency decision — Core uses a joy4 fork; `mediamtx` is the better-maintained MIT option today. + + > **Done, 2026-08-06.** Deferred twice — here, then again in + > DESIGN-ONE-PORT-ONLY, which replaced "a port each" with "one port, one + > source". Both are now superseded: RTMP is one port serving any number of + > sources, addressed by the stream key. The instinct in this bullet was right + > about *where* the answer would come from — the dependency turned out to be + > `bluenviron/gortmplib`, from the MediaMTX maintainers, rather than a joy4 + > fork. See [DESIGN-ONE-PORT-ONLY.md](DESIGN-ONE-PORT-ONLY.md#rtmp). - **Replacing per-port addressing.** Both work at once: the shared port demultiplexes by token, and a source may still hold a dedicated port. Core is one-port only; per-port remains genuinely useful for firewall rules, separate diff --git a/docs/DESIGN-ONE-PORT-ONLY.md b/docs/DESIGN-ONE-PORT-ONLY.md index 4f36c3dc..c06c28f3 100644 --- a/docs/DESIGN-ONE-PORT-ONLY.md +++ b/docs/DESIGN-ONE-PORT-ONLY.md @@ -10,7 +10,7 @@ costs. | | Before | After | |---|---|---| | **SRT** | a UDP port per source; a shared token-addressed port as an opt-in extra, **off by default** | **one** UDP port, token-addressed, always, no alternative | -| **RTMP** | a TCP port per source | one TCP port, serving at most **one** source | +| **RTMP** | a TCP port per source | one TCP port, serving at most **one** source — *superseded 2026-08-06, see [RTMP](#rtmp)* | | **Pull** | dials out, binds nothing | unchanged | | **Port fields per source** | SRT port, RTMP port, with conflict detection and auto-move | **gone** | @@ -72,6 +72,14 @@ That line is now gone. SECURITY.md instead says SRT is authenticated by construction, and the caveat that remains is RTMP's — which has no token routing and is protected by its stream key alone. +> **Outcome, 2026-08-06.** The RTMP half of that caveat no longer holds either. +> RTMP is now routed by stream key on the shared listener, matched in constant +> time against every source's key, so a publisher presenting nothing or +> something unrecognised is refused rather than landing on whichever source +> happened to hold the socket. What separates the two protocols now is +> encryption, not authentication: SRT's passphrase is real AES, an RTMP stream +> key is a string comparison over a cleartext connection. See [RTMP](#rtmp). + **One published port, forever.** `docker-compose.yml` stops being something you edit when you add a programme. @@ -91,6 +99,14 @@ actually want — not the port-per-source model being removed here. ## RTMP +> **Superseded, 2026-08-06.** RTMP now has the same shape as SRT — one port, +> addressed by a key, unlimited sources — and the one-source limit is gone. +> `internal/rtmpserver` is the listener that closed it. +> +> The original argument is kept below rather than deleted, because two of its +> three premises were sound and are still the reason this was not done earlier. +> What actually moved is recorded in [What changed](#what-changed) after it. + RTMP keeps its single well-known port and serves **at most one source**. This is not one-port routing; it is the absence of it. RTMP's URL carries @@ -113,6 +129,94 @@ So: **at most one source may use RTMP ingest**, enforced with an error that says why and points at SRT. A second RTMP source is refused rather than accepted and silently starved, which is what would happen if two of them tried to bind 1935. +### What changed + +Three things, in the order they mattered. + +**The principle was wrong, not just the implementation.** The clearest way to +see it: *how many programmes you can run should not depend on which protocol +your encoder speaks.* Unlimited over SRT and exactly one over RTMP was never a +position anyone argued for — it was what `ffmpeg -listen 1` happened to permit, +back-filled with a justification. "Nobody has asked" is a fine reason to defer +work; it is not a reason to call the resulting limit a design. + +**The dependency was re-measured, and it is a different dependency.** The +rejection was of `yutopp/go-rtmp`, and that rejection still stands — it has not +moved since. What exists now did not exist in this form when the decision was +taken: + +| | transitive deps | version | provenance | +|---|---|---|---| +| `github.com/datarhei/gosrt` (the bar) | **1** | v0.11.0 | datarhei | +| **`github.com/bluenviron/gortmplib`** | **3** — `abema/go-mp4`, `bluenviron/mediacommon/v2`, `google/uuid` | **v1.0.0** | gortsplib / MediaMTX maintainers | +| `github.com/yutopp/go-rtmp` | 7 — `logrus`, `pkg/errors`, `mapstructure`, … | v0.0.7 | unchanged | + +Three modules against gosrt's one is a real cost, not a free one. But it is a +different order of magnitude from seven, none of it is dead upstream, there is +no second logging framework, and the provenance is the same argument that +justified gosrt. All three are MIT and `CGO_ENABLED=0` clean. + +**"Writing one is the largest single piece of work in the repository" was +answering the wrong question.** Nothing here implements RTMP. `internal/rtmpserver` +wraps gortmplib and adds only the part that is ours — the same part +`internal/srtserver` adds over gosrt: a constant-time `Lookup` from key to +source, one publisher slot per (source, role), and a relay. It is ~540 lines +plus tests, not a protocol stack. + +The premise that has moved *least* is the one about tracks. **RTMP still carries +one stereo pair on classic FLV.** Enhanced RTMP multitrack does work on FFmpeg +7.1+ — verified end to end — but not on 6.1.1, and it has not been confirmed +with OBS publishing. Multi-source RTMP does not change that and was never going +to. It closes a different gap: **encoders that cannot speak SRT at all.** +Hardware encoders and older appliances, where the install previously supported +exactly one of them. If your encoder can do SRT, SRT is still strictly better +and was already unlimited. + +### The shape it took + +One port, both directions. Encoders publish to the listener and this install's +own FFmpeg subscribes to it, on that same port: + +``` +encoder --publish--> rtmp://host:1935/live/ +ffmpeg --play-----> rtmp://127.0.0.1:1935/live/ +``` + +The first implementation relayed each publisher *outward* to a per-source FFmpeg +listening on its own loopback port. That works, and it is what `ffmpeg -listen 1` +forced before any of this existed, but it would have reintroduced per-source +ports — the exact surface this document removed — for no gain. Publish/subscribe +on one port is how RTMP servers are actually built: datarhei Core runs its +internal RTMP server this way, and so does everything else. + +Three consequences worth stating, because each is a decision rather than a +detail: + +- **Media is never parsed.** gortmplib also offers a `Reader` that hands over + decoded access units. Using it would put a muxer in the critical path of every + frame and make a whole class of bug ours that currently belongs to FFmpeg. + This forwards RTMP *messages*, so the bytes reaching FFmpeg are the bytes the + encoder sent and `-c copy` downstream is untouched. +- **Subscribers are loopback-only.** A stream key is a *publish* credential — it + is what sits in an operator's OBS settings. If it also authorised playback, + every ingest key would quietly become a viewing key for anyone who learned it. + Playback for viewers is the playout page's job, behind authentication. +- **Setup messages are cached and replayed.** A subscriber that arrives after + the metadata and codec sequence headers have gone past has a byte stream it + cannot interpret. They are remembered per stream key and replayed on + subscribe, which is what makes the order of "encoder connects" and "FFmpeg + connects" not matter — and it does vary, since an encoder reconnecting + mid-session is routine. This is the only message inspection in the package: a + type switch, not a decode. + +The failover standby gets a slot of its own on the same listener, keyed on +(source, role) exactly as SRT's is. Keying on the source alone would let the +primary and the standby evict each other, which is the failover feature failing +in the one situation it exists for. + +The full record, including the options rejected, is in +[notes/multi-source-rtmp.md](notes/multi-source-rtmp.md). + ## The failover backup needs an address too Easy to miss, and it falls out of the same change. The backup ingest is a @@ -148,6 +252,9 @@ has gone away. ## The rule this leaves - Every push source is addressed by its token on one SRT port. -- One source, at most, may additionally be reached over RTMP. +- Every RTMP source is addressed by its stream key on one RTMP port. Any number + of them, the same as SRT. *(Updated 2026-08-06; this line previously read + "one source, at most, may additionally be reached over RTMP" — see + [RTMP](#rtmp).)* - Pull sources dial out and bind nothing. - There are no per-source ports anywhere. diff --git a/docs/FAQ.md b/docs/FAQ.md index 270c3bb2..7d6a201f 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -40,6 +40,11 @@ You can use RTMP, but **RTMP carries one stereo pair**. If you only have one audio track there is nothing to route, and the main reason to use polyemesis disappears. +What RTMP is *not* short of is sources: one port carries any number of them, +each addressed by its own stream key, exactly as SRT sources are addressed by +their token. (Before 2026-08-06 an install could run exactly one RTMP source — +if you have read that anywhere, it is out of date.) + For multitrack you need SRT. Check with: ```sh @@ -50,10 +55,39 @@ Homebrew's FFmpeg on macOS has no SRT. Use Docker. ## What about Enhanced RTMP / multitrack FLV from OBS 30.2+? -Not implemented, and RTMP ingest is single-track either way. An `enhancedRtmp` -config key used to be declared for it and has been removed, because a key that -accepts a value and changes nothing is worse than no key. A config file that -still carries it keeps loading; the key is simply ignored. Use SRT. +It works on a new enough FFmpeg. Multitrack FLV demuxing landed in FFmpeg 7.1, +and from there the tracks arrive through polyemesis's existing ingest command +unchanged — verified end to end: a destination configured for tracks 1 and 3 +received exactly those two and neither of the others. It does **not** work on +FFmpeg 6.1.1, which is Ubuntu 24.04's stock build: that refuses with *"at most +one audio stream is supported in flv"*, and the extra tracks are lost with no +error at either end. + +**OBS does not currently send it.** That run has now been done — +`scripts/acceptance-obs-multitrack.sh`, with OBS headless in Docker — and the +answer was not the expected one. OBS 30.2.3, configured with three audio tracks +and `StreamMultiTrackAudioMixes=7` against a custom RTMP server, publishes a +single legacy-tagged audio track: capturing its bytes and walking the FLV tag +headers gives `0xaf legacy ×3541` and no `0x95` multitrack tag at all. + +The reason is a gate in OBS's service catalogue rather than anything about the +muxer. `rtmp-services.so` tests `supports_additional_audio_track`, and no service +in its `services.json` declares it — 0 of 91 — so the capability is unreachable +for every service, custom RTMP included. + +The earlier claim here was read from OBS's `flv-mux.c`, which does implement the +format correctly. What was not checked is whether anything reaches that code. + +So: polyemesis accepts multitrack from a publisher that sends it (FFmpeg ≥ 7.1 +does), and OBS's connect, handshake and stream are accepted and probed +correctly — but if you need multiple audio tracks out of OBS today, use SRT. +SRT remains the operated path. + +See `notes/enhanced-rtmp-multitrack.md`. + +The `enhancedRtmp` config key is still gone, and still does not need to come +back: this needs no flag. A config file that names it keeps loading; the key is +ignored. ## Can I run horizontal and vertical at once? diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 02cec8d6..860a3cae 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -255,6 +255,13 @@ Open and set an admin password on the first-run screen. | `1935/tcp` | RTMP ingest, the fallback for encoders that cannot do SRT | | `80/tcp` | **ACME only**, and shipped commented out — see below | +Both ingest ports are published **once, for the whole install.** Adding a second +programme never means publishing another port or restarting the container: every +source arrives on the same listener and is told apart by its publish token (SRT) +or stream key (RTMP). That is the point of +[DESIGN-ONE-PORT-ONLY.md](DESIGN-ONE-PORT-ONLY.md) — editing +`docker-compose.yml` to add a source was a real cost it removed. + **Port 80 and Let's Encrypt.** If you want polyemesis to obtain its own certificate (`tls.mode: acme`, or `auto` resolving to it), HTTP-01 validation must reach `http:///.well-known/acme-challenge/…` on port 80 from the @@ -639,14 +646,16 @@ tls mode=… hostname=… |---|---|---| | 8080 | TCP | always — web UI and API. Configurable via `addr`. | | 6000 | **UDP** | SRT ingest. The default; changeable in *Settings → Ingest*. | -| 1935 | TCP | RTMP ingest, only if you use the fallback | +| 1935 | TCP | RTMP ingest, only if you use the fallback. One port however many RTMP sources you run. | | 80 | TCP | only for `tls.mode: acme` (HTTP-01 validation), plus the HTTP→HTTPS redirect whenever polyemesis terminates TLS | | 443 | TCP | only if you set `addr` to `:443` rather than serving TLS on 8080 | -The SRT ingest listener binds `0.0.0.0` regardless of `addr`, so restricting -`addr` to loopback for a reverse-proxy deployment does not restrict ingest. Set -an SRT passphrase in *Settings → Ingest*: without one your stream crosses the -network in the clear. +The ingest listeners bind `0.0.0.0` regardless of `addr`, so restricting `addr` +to loopback for a reverse-proxy deployment does not restrict ingest. Set an SRT +passphrase in *Settings → Ingest*: without one your stream crosses the network +in the clear. RTMP has no equivalent — its stream key authenticates the +publisher but encrypts nothing, so the RTMP port is the one to keep off the +public internet if you have the choice. --- diff --git a/docs/MODULES.md b/docs/MODULES.md index 84e255d5..e156fd3f 100644 --- a/docs/MODULES.md +++ b/docs/MODULES.md @@ -26,6 +26,7 @@ hard or is the Go project's own code. | `golang.org/x/sys` | v0.47.0 | BSD-3-Clause | Process groups, disk stats, Windows job objects | | `github.com/shirou/gopsutil/v4` | v4.26.6 | BSD-3-Clause | Host CPU, memory and disk for the monitoring page | | `github.com/datarhei/gosrt` | v0.11.0 | MIT | Pure-Go SRT. Powers one-port token-addressed ingest | +| `github.com/bluenviron/gortmplib` | v1.0.0 | MIT | Pure-Go RTMP. Powers one-port key-addressed ingest — message level only, never decoded media | | `github.com/eclipse/paho.golang` | v0.23.0 | EPL-2.0 (dual EDL-1.0) | MQTT 5 client for retained telemetry | | `gopkg.in/yaml.v3` | v3.0.1 | Apache-2.0 (dual MIT) | Reads `config.yaml` | @@ -41,7 +42,9 @@ the rest are build-time or test-only for their parent. | `modernc.org/memory` | v1.11.0 | BSD-3-Clause | sqlite | ✅ | | `github.com/remyoudompheng/bigfft` | 20230129 | BSD-3-Clause | mathutil | ✅ | | `github.com/dustin/go-humanize` | v1.0.1 | MIT | sqlite | ✅ | -| `github.com/google/uuid` | v1.6.0 | BSD-3-Clause | sqlite | ✅ | +| `github.com/google/uuid` | v1.6.0 | BSD-3-Clause | sqlite, gortmplib | ✅ | +| `github.com/abema/go-mp4` | v1.7.1 | MIT | gortmplib | ✅ | +| `github.com/bluenviron/mediacommon/v2` | v2.9.2 | MIT | gortmplib | ✅ | | `github.com/ncruces/go-strftime` | v1.0.0 | MIT | sqlite | ✅ | | `github.com/mattn/go-isatty` | v0.0.24 | MIT | libc | ✅ | | `github.com/benburkert/openpgp` | 20160410 | see note | gosrt (AES key-wrap) | ✅ | diff --git a/docs/OBS.md b/docs/OBS.md index 6ad4af03..28a63e0a 100644 --- a/docs/OBS.md +++ b/docs/OBS.md @@ -3,6 +3,14 @@ polyemesis accepts a stream from anything that can push MPEG-TS over SRT or a single track over RTMP. OBS is what most people use, so it gets its own page. +> **On Enhanced RTMP.** OBS 30.2+ can send several audio tracks over RTMP using +> the Enhanced RTMP multitrack format. FFmpeg gained multitrack FLV demuxing in +> late 2024, so a new enough build carries those tracks through polyemesis's +> existing ingest command unchanged — verified up to six tracks on FFmpeg 8.1. +> Ubuntu 24.04's stock FFmpeg 6.1.1 cannot: it refuses with *"at most one audio +> stream is supported in flv"*. See `notes/enhanced-rtmp-multitrack.md`; a +> confirmation run against real OBS is still outstanding. + Only one of these configurations unlocks per-destination audio routing: **multitrack over SRT**. The others work, and are documented here so that choosing one is deliberate rather than accidental. @@ -94,32 +102,59 @@ claim. ## Standard RTMP (single track) -For encoders that cannot do SRT. RTMP carries one audio track by protocol, so +For encoders that cannot do SRT. *Classic* RTMP carries one audio track, so per-destination *track* routing does not apply — though gain, matrix panning and 5.1 downmix still do. -In polyemesis: `Settings → Ingest → Mode: RTMP`. Note the app name and stream -key. In OBS: `Settings → Stream`, Service **Custom...**: +In polyemesis: open the source under **Sources** and set `Ingest → Mode: RTMP`. +It gets its own app name and stream key. In OBS: `Settings → Stream`, Service +**Custom...**: | Field | Value | |---|---| | Server | `rtmp://YOUR_SERVER:1935/live` | -| Stream Key | the key from polyemesis Settings | +| Stream Key | that source's stream key, from **Sources** | Then press **Start Streaming**. -**At most one source may use RTMP ingest.** polyemesis has no RTMP server of its -own — it uses `ffmpeg -listen 1`, a single-connection receiver that cannot -demultiplex by path — so a second RTMP source is refused with an error that says -so, rather than accepted and silently starved. Any number of sources can share -the SRT port. +> **The stream key is the address, not an extra.** One RTMP port serves every +> source and they are told apart by the key in the publish URL, exactly as SRT +> tells them apart by token — so a publisher presenting an unrecognised key is +> refused rather than landing on whichever source happens to be there. Copy the +> Server and Stream Key from **Sources** rather than assembling them by hand: +> each source has its own pair, and pasting one source's key under another +> source's name is the mistake this shape makes easy to notice and easy to fix. + +**Any number of sources may use RTMP ingest**, the same as SRT. A second RTMP +encoder is an ordinary second source; it needs no extra port and no +configuration beyond its own stream key. See +[DESIGN-ONE-PORT-ONLY.md](DESIGN-ONE-PORT-ONLY.md#rtmp) for how that came to be +true — until 2026-08-06 an install could carry exactly one RTMP source. + +What RTMP still does not give you is per-destination *track* routing, which is +a limit of the format rather than of the listener. If your encoder can speak +SRT, use SRT. --- -## Enhanced RTMP multitrack (not implemented) +## Enhanced RTMP multitrack (works on FFmpeg 7.1+, unconfirmed with OBS) + +OBS 30.2+ can send multiple audio tracks over Enhanced RTMP/FLV. + +It works on a new enough FFmpeg. Multitrack FLV demuxing landed in FFmpeg 7.1, +and from there the tracks arrive through polyemesis's existing ingest command +unchanged — verified end to end: a destination configured for tracks 1 and 3 +received exactly those two and neither of the others. It does **not** work on +FFmpeg 6.1.1, which is Ubuntu 24.04's stock build: that refuses with *"at most +one audio stream is supported in flv"*, and the extra tracks are lost with no +error at either end. + +What has not been done is a run with OBS itself as the publisher — the testing +used FFmpeg. OBS writes the same `MULTITRACKTYPE_ONE_TRACK` format (read from +its `flv-mux.c`), so the wire format is not in question, but the handshake and +metadata path are unconfirmed. SRT remains the operated path. -OBS 30.2+ can send multiple audio tracks over Enhanced RTMP/FLV. **polyemesis -does not support this.** RTMP ingest is single-track. +See `notes/enhanced-rtmp-multitrack.md`. `config.yaml` used to declare an `enhancedRtmp` key. It has been removed: it was an inert placeholder that read as a switch, and it was kept on the belief diff --git a/docs/PLATFORMS.md b/docs/PLATFORMS.md index e876e01b..4ebd9f05 100644 --- a/docs/PLATFORMS.md +++ b/docs/PLATFORMS.md @@ -36,7 +36,7 @@ The same matrix is rendered in `Settings → Platform credentials` and served fr | **X (Twitter) Live** | Not possible | By hand | Not possible | Not possible | Not possible | Not possible | Not possible | | **Rumble** | Unverified | By hand | Unverified | Unverified | Unverified | Unverified | Unverified | | **DLive** | Unverified | By hand | Unverified | Unverified | Unverified | Unverified | Unverified | -| **Instagram Live** | Not possible | **Not possible** | Not possible | Not possible | Not possible | Not possible | Not possible | +| **Instagram Live** | Not possible | Not possible | Not possible | Not possible | Not possible | Not possible | Not possible | | *Everything else* | — | By hand | — | — | — | — | — | | Term | Means | @@ -128,9 +128,18 @@ third-party live-video ingest endpoint; access to what *is* documented is credit-based and paid. Create the source in X's own producer tooling and copy the URL and key across. -**Instagram — polyemesis cannot stream here.** Instagram's platform covers -messaging, content publishing and comments. There is no Live broadcast API, and -Live Producer's RTMP path was removed for most accounts. It is listed, and +**Instagram — polyemesis cannot AUTOMATE it, but it will push to it.** The +distinction matters and this page used to collapse it. Instagram's platform +covers messaging, content publishing and comments: there is no Live broadcast +API, so nothing here can create a broadcast, fetch a key, read chat or report +viewers. That is what "unsupported" means, and it is why the preset exists. + +What it does NOT mean is that the bytes are refused. `TierUnsupported` feeds +`UnsupportedPresets()`, which only tells the destination picker to mark the row; +nothing in the streaming path consults it. If your account is one of the few +that still has Live Producer, take its server URL and key, create a **Generic +RTMPS** destination, and it streams like any other — with its own per-destination +audio mix. The key changes every broadcast, so it is manual each time. It is listed, and marked unsupported in the destination picker, rather than shipped as a preset that quietly never connects — a destination that fails silently looks exactly like a bug in polyemesis, and there is nothing to fix. If your account is one of diff --git a/docs/RESEARCH-COMPETITIVE.md b/docs/RESEARCH-COMPETITIVE.md index e3c8bed3..b2d329f5 100644 --- a/docs/RESEARCH-COMPETITIVE.md +++ b/docs/RESEARCH-COMPETITIVE.md @@ -58,7 +58,7 @@ instead. | Video grid / multiple inputs | 1 (closed) | **GAP** | | | Deinterlacing | 1 (closed) | **have** | Was built-but-unreachable; the control landed 2026-07-28. See the correction below | | MQTT (core tracker) | — | **have** | Retained telemetry with Home Assistant discovery, alongside alert webhooks | -| Single token per RTMP endpoint (core) | — | **GAP** | Falls out of multi-source | +| Single token per RTMP endpoint (core) | — | **have** | Closed 2026-08-06 by `internal/rtmpserver`: one RTMP port, each source addressed by its own stream key, any number of them | Verified against the code, not assumed. Two initial readings were wrong and are corrected here: the only `overlay=` in the tree is inside the blurred-pad @@ -185,6 +185,10 @@ anyway and retrofitting a key is worse than designing it in: reach the port can publish to any source" stops being acceptable. This is the Core tracker's "Single Token per RTMP Endpoint", and it is nearly free while the schema is already being changed. + + > **Done.** SRT on 2026-07-30 (the publish token *is* the address), RTMP on + > 2026-08-06 (the stream key is). Both matched in constant time against every + > source's key, so an unrecognised publisher is refused rather than guessed at. - **Source-scoped everything** — destinations, renditions, recordings, clips, transcripts, chat. The migration is the expensive part; doing it once is much cheaper than doing it per-feature later. diff --git a/docs/TESTING.md b/docs/TESTING.md index cd04d2f2..6fad1244 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -39,7 +39,8 @@ Homebrew's `ffmpeg` bottle currently has no SRT. Options: - Use the **direct-to-relay** method (§6) — full multitrack routing, no SRT. polyemesis starts and warns rather than refusing, so you can always reach -Settings and switch the ingest to RTMP. +Settings and switch a source's ingest to RTMP. Any number of sources can be on +RTMP at once, so an SRT-less FFmpeg no longer limits you to testing one. --- @@ -86,7 +87,8 @@ Notes on the flags that matter: GOP boundaries. Want six tracks instead of three? Add more `-f lavfi -i "sine=frequency=..."` -inputs and matching `-map N:a` entries, up to six. +inputs and matching `-map N:a` entries. The ceiling is `routing.MaxTracks` (32), +though six is what OBS sends and what the fixtures assume. **Verify polyemesis saw it.** On the Dashboard, "Audio tracks" should read `3` and the video line should read `h264 1280×720 @ 30.00fps`. On the Audio meters @@ -205,18 +207,60 @@ the copyrighted-music track really is absent from the YouTube feed. RTMP carries one audio track by protocol, so this exercises ingest, relay, supervision and destinations but not multitrack routing. -Set Settings → Ingest → Mode to **RTMP**, note the app and stream key, then: +Open the source under **Sources**, set `Ingest → Mode: RTMP`, and copy its app +and stream key — they are per-source, so use that source's key rather than a +fixed one: ```bash +KEY= + ffmpeg -hide_banner -re \ -f lavfi -i "testsrc2=size=1280x720:rate=30" \ -f lavfi -i "sine=frequency=440:sample_rate=48000" \ -map 0:v -map 1:a \ -c:v libx264 -preset ultrafast -tune zerolatency -g 60 -b:v 2500k \ -c:a aac -b:a 128k \ - -f flv "rtmp://127.0.0.1:1935/live/stream" + -f flv "rtmp://127.0.0.1:1935/live/$KEY" +``` + +### 5a. Two RTMP sources at once + +The check worth adding, because until 2026-08-06 it was impossible: one RTMP +port carrying two independent programmes, told apart by their keys. If this +fails, the one-port RTMP listener has regressed to the `ffmpeg -listen 1` +behaviour it replaced — one publisher holding the socket and the second silently +receiving nothing. + +Make a **second** source, also RTMP, and run both publishers together. Give them +different tones so the meters distinguish them without guesswork: + +```bash +KEY_A= +KEY_B= + +for pair in "440:$KEY_A" "880:$KEY_B"; do + freq=${pair%%:*}; key=${pair#*:} + ffmpeg -hide_banner -re \ + -f lavfi -i "testsrc2=size=1280x720:rate=30" \ + -f lavfi -i "sine=frequency=$freq:sample_rate=48000" \ + -map 0:v -map 1:a \ + -c:v libx264 -preset ultrafast -tune zerolatency -g 60 -b:v 2500k \ + -c:a aac -b:a 128k \ + -f flv "rtmp://127.0.0.1:1935/live/$key" & +done +wait ``` +Both sources should go live and stay live. Two further things to confirm, each +guarding a decision rather than a line of code: + +- **Stopping one does not disturb the other.** They are separate publisher slots + on one listener; if killing publisher A drops B, the slots are keyed wrongly. +- **A wrong key is refused, not misrouted.** Publish to + `rtmp://127.0.0.1:1935/live/nonsense` and it should be dropped with a log line + that names no source. Landing on whichever source happened to be there would + make the stream key decorative rather than the address. + --- ## 6. Direct-to-relay (full multitrack, no SRT needed) @@ -316,6 +360,31 @@ go test -v ./internal/routing/ # pan strings, 5.1 downmix, validation go test -v ./internal/ffmpeg/ # ingest/destination/recorder command builders ``` +The browser has a unit tier too, for the pure logic Playwright cannot practically +enumerate: + +```bash +cd ui && npm test # vitest, scoped to src/**/*.test.ts +``` + +Two things live there, both chosen because a browser test would be the wrong +tool rather than merely a slower one: + +- **`lib/platformLinks.test.ts`** — five platforms times several missing-field + cases. Driving each through a real browser would cost minutes to assert what + takes a millisecond here. +- **`lib/i18n.test.ts`** — the translation catalogues. It checks the properties a + reviewer *cannot* check by reading: that no locale corrupts a `{placeholder}`, + defines a key English lacks, leaves an English function word inside a + non-Latin string, or splices a Latin word into a native one. It deliberately + does **not** judge whether a translation is good; no test can. Incompleteness + is not an error either — `lib/i18n.ts` falls back to English per key — so the + suite prints a coverage table rather than failing a lagging locale. + +Note that `e2e/` is Playwright's and is excluded from vitest in +`vitest.config.ts`; handed to vitest those specs fail on importing +`@playwright/test` rather than on anything real. + --- ## 10. Acceptance suites diff --git a/docs/TLS.md b/docs/TLS.md index 7bc39b9b..1d60d443 100644 --- a/docs/TLS.md +++ b/docs/TLS.md @@ -453,9 +453,12 @@ TLS on the web UI is not the whole story: and SRT encrypts the stream with AES. The dashboard renders the exact `srt://…?passphrase=…` URL to paste into OBS. Without one, your stream — including anything on screen — crosses the network unencrypted. -- **RTMP ingest has no equivalent.** RTMP is authenticated by the stream key in - the URL and is otherwise in the clear. It is the fallback for encoders that - cannot do SRT; prefer SRT where you have the choice. +- **RTMP ingest has no equivalent.** RTMP *is* authenticated — the stream key in + the URL is matched in constant time against every source's key, so an + unrecognised publisher is refused rather than guessed at — but that is a + string comparison, not encryption. The stream and the key itself both cross + the network in the clear. It is the fallback for encoders that cannot do SRT; + prefer SRT where you have the choice. - **Destinations can be `rtmps://`.** RTMP destination URLs accept both `rtmp://` and `rtmps://` and the URL is handed to FFmpeg verbatim, so where a platform publishes an RTMPS ingest address, paste that one. SRT destinations diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 1d2b0997..fb7ec986 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -61,13 +61,22 @@ pgrep -af ffmpeg 1. **Is the port published?** In Docker, SRT is UDP: `-p 6000:6000/udp`. Missing `/udp` is the single most common cause. 2. **Does the mode match?** An SRT listener will not accept an RTMP publisher. -3. **Is a firewall in the way?** SRT is UDP and is often dropped by default. -4. **Does your FFmpeg have SRT?** `ffmpeg -protocols | grep -x srt`. Homebrew's +3. **Is the address right for *that* source?** Both listeners are shared, so the + token (SRT) or the stream key (RTMP) is what picks the source out — a valid + URL with the wrong key reaches the server and is refused, it does not fall + through to whichever source happens to be there. On RTMP the log line is + `rtmp publish refused` and it deliberately names **no source**: "no such + key", "that source is disabled" and "that source has no engine" are three + different facts, and telling them apart is what would let someone enumerate + which keys exist. Copy the URL and key from **Sources** rather than retyping + them. +4. **Is a firewall in the way?** SRT is UDP and is often dropped by default. +5. **Does your FFmpeg have SRT?** `ffmpeg -protocols | grep -x srt`. Homebrew's does not. Without it, the ingest cannot listen and the log says `Protocol not found`. -5. **Are you on macOS with a bare `:port`?** See directly below — this one looks +6. **Are you on macOS with a bare `:port`?** See directly below — this one looks exactly like a firewall and is not one. -6. **Is the link lossy?** A handshake can fail on loss that an established +7. **Is the link lossy?** A handshake can fail on loss that an established stream would shrug off, and it fails with a bare `I/O error` that names nothing. See [It connects on some attempts and not others](#it-connects-on-some-attempts-and-not-others). diff --git a/docs/media/01-dashboard.png b/docs/media/01-dashboard.png index 3160ad0d..81183c67 100644 Binary files a/docs/media/01-dashboard.png and b/docs/media/01-dashboard.png differ diff --git a/docs/media/02-routing.png b/docs/media/02-routing.png index d5ada155..5f4d2d24 100644 Binary files a/docs/media/02-routing.png and b/docs/media/02-routing.png differ diff --git a/docs/media/03-routing-filtergraph.png b/docs/media/03-routing-filtergraph.png index 3b6a518a..788120ff 100644 Binary files a/docs/media/03-routing-filtergraph.png and b/docs/media/03-routing-filtergraph.png differ diff --git a/docs/media/04-meters.png b/docs/media/04-meters.png index 1b6b5213..b6e2f3bd 100644 Binary files a/docs/media/04-meters.png and b/docs/media/04-meters.png differ diff --git a/docs/media/05-sources.png b/docs/media/05-sources.png index bdfd36a4..6160ed29 100644 Binary files a/docs/media/05-sources.png and b/docs/media/05-sources.png differ diff --git a/docs/media/06-renditions.png b/docs/media/06-renditions.png index 1131f09f..56f61574 100644 Binary files a/docs/media/06-renditions.png and b/docs/media/06-renditions.png differ diff --git a/docs/media/07-monitoring.png b/docs/media/07-monitoring.png index e612586a..1cb3855a 100644 Binary files a/docs/media/07-monitoring.png and b/docs/media/07-monitoring.png differ diff --git a/docs/media/08-mix-matrix.png b/docs/media/08-mix-matrix.png index 9c28ed16..b9f321cf 100644 Binary files a/docs/media/08-mix-matrix.png and b/docs/media/08-mix-matrix.png differ diff --git a/docs/media/09-chat.png b/docs/media/09-chat.png index 040287a7..9ffb905f 100644 Binary files a/docs/media/09-chat.png and b/docs/media/09-chat.png differ diff --git a/docs/media/10-playout.png b/docs/media/10-playout.png index 9df7011f..8550c50c 100644 Binary files a/docs/media/10-playout.png and b/docs/media/10-playout.png differ diff --git a/docs/media/11-library.png b/docs/media/11-library.png index 1a21da7c..dc3d65d4 100644 Binary files a/docs/media/11-library.png and b/docs/media/11-library.png differ diff --git a/docs/media/12-jobs.png b/docs/media/12-jobs.png index 18fd27c0..3606fd3a 100644 Binary files a/docs/media/12-jobs.png and b/docs/media/12-jobs.png differ diff --git a/docs/media/13-automation.png b/docs/media/13-automation.png index 0be2868b..bece7a04 100644 Binary files a/docs/media/13-automation.png and b/docs/media/13-automation.png differ diff --git a/docs/media/14-clips.png b/docs/media/14-clips.png index de7448ec..60f2bed1 100644 Binary files a/docs/media/14-clips.png and b/docs/media/14-clips.png differ diff --git a/docs/media/15-recordings.png b/docs/media/15-recordings.png index 507b7924..d2676b56 100644 Binary files a/docs/media/15-recordings.png and b/docs/media/15-recordings.png differ diff --git a/docs/media/16-settings.png b/docs/media/16-settings.png index 05f7eed5..9a284f4c 100644 Binary files a/docs/media/16-settings.png and b/docs/media/16-settings.png differ diff --git a/docs/media/17-settings-full.png b/docs/media/17-settings-full.png index 2f7a48d4..a9f89b0f 100644 Binary files a/docs/media/17-settings-full.png and b/docs/media/17-settings-full.png differ diff --git a/docs/media/tour.mp4 b/docs/media/tour.mp4 index d57a2cbe..aad9fbc7 100644 Binary files a/docs/media/tour.mp4 and b/docs/media/tour.mp4 differ diff --git a/docs/media/tour.webm b/docs/media/tour.webm index cfd43040..1eb4533f 100644 Binary files a/docs/media/tour.webm and b/docs/media/tour.webm differ diff --git a/docs/notes/copy-review-2026-08-06.md b/docs/notes/copy-review-2026-08-06.md new file mode 100644 index 00000000..fa8e6605 --- /dev/null +++ b/docs/notes/copy-review-2026-08-06.md @@ -0,0 +1,116 @@ +# Copy review of the marketing site — three reviewers, 2026-08-06 + +Opus, codex and agy each read `web/src` in full and checked every factual claim +against the repository. Below is what they found, what was verified, what was +fixed, and — as importantly — what one of them got wrong. + +Fifteen accuracy defects were confirmed and fixed. The single worst one was +introduced by me an hour before the review ran. + +--- + +## The one that mattered most + +**The "Proof, not a diagram" block misdescribed its own screenshot.** + +`LoudnessBars.astro` was written from the hero's *illustrative* arrangement +rather than from `scripts/seed_demo.go`, which is the code that generates the +capture sitting beside it — while its own comment claimed it carried "the same +numbers as the screenshot beside this". + +| | shipped | truth (`seed_demo.go:136-140`) | +|---|---|---| +| YouTube | `01 + 03` | tracks 0,1,2 — the **full** mix | +| Twitch | `02 + 03` | tracks 0,2 | +| third destination | "Archive", "all six" | **"Podcast — mic only"**, track 0 | + +Four compounding problems: + +1. "Archive" does not exist in the capture. +2. "all six" was wrong twice: the source has **three** tracks, and that + destination receives one of them. +3. YouTube was labelled as receiving track 01 — the full mix *including + copyrighted music*. The same page argues at length that YouTube must get the + clean mix or you take a strike. The block captioned "Proof" showed the + product doing the exact thing the product exists to prevent. +4. The arithmetic was impossible. "all six" was shown **6.3 LU quieter** than a + two-track destination. A superset cannot be quieter than its subset, and that + is the first thing a broadcast engineer checks. + +Under the true sets — 3 tracks / 2 tracks / 1 track → −15.6 / −17.0 / −21.9 — +the ordering is exactly right. The defect was entirely in the labels. + +Also corrected here: the target tick was drawn at −23 LUFS and captioned as "the +EBU R128 target". −23 is EBU R128 *broadcast* delivery; `docs/AUDIO-ROUTING.md:52` +says `loudnorm` defaults to **−16**, "the figure the streaming platforms expect". +Marking −23 put all three bars on the loud side of a line nobody in this +workflow aims at. + +--- + +## Accuracy defects fixed + +| # | Defect | Evidence | +|---|---|---| +| 1 | Loudness proof block (above) | `scripts/seed_demo.go:136-140` | +| 2 | "One six-track source" — the capture uses three | `capture-media.sh:134-136`; the page's own `alt` text already said three | +| 3 | Install command **cannot run**: `--tls auto` invalid, `\| sh` fails, no root | `install.sh:1101` takes `off\|selfsigned\|acme`; `#!/usr/bin/env bash` + `${__answer,,}`; `install.sh:226` | +| 4 | OBS steps described `Settings → Stream`, which has no multi-track selector | `docs/OBS.md:50-67` requires Output → Advanced → Recording → Custom Output (FFmpeg) | +| 5 | "Moderation across all four: delete, ban, timeout" | `internal/chat/facebook.go` implements `Delete` and `Hide` only; `Ban` is YouTube/Twitch/Kick | +| 6 | "One send box that fans out" | No `Send` on Facebook; `PLATFORMS.md:34` marks it Unverified | +| 7 | "it is short" about the installer | `scripts/install.sh` is **1,164 lines** | +| 8 | TLS description was `config.yaml`'s `tls.mode: auto`, not the installer's | `install.sh:1033-1035` defaults to selfsigned; acme needs both flags | +| 9 | First-run steps omitted the ingest-mode choice | `internal/db/settings.go` — `IngestUnset`; no URL exists until chosen | +| 10 | `

` said six tracks, four lines above a spec strip saying 32 | `internal/routing/profile.go` — `MaxTracks = 32` | +| 11 | "Video is copied, never re-encoded" | Contradicted by the site's own next card: renditions "re-encode video only" | +| 12 | `/docs` claimed links "match the release you are running" | Every link targets `blob/main` | +| 13 | FFmpeg dependency line said RTMP is unconditionally single-track | Enhanced RTMP works on FFmpeg 7.1+, verified end to end | +| 14 | RTMP fallback card contradicted the corrected RTMP row two screens down | Same page | +| 15 | Features `

` "happens to route audio" vs subhead "the reason it exists" | Self-contradiction | + +## Documentation drift fixed + +`README.md:89`, `docs/FAQ.md:51` and `docs/OBS.md:127` all still stated Enhanced +RTMP multitrack was "not supported" — and `OBS.md` **contradicted itself within +one page**, carrying a callout near the top saying a new enough FFmpeg carries it +and a section 115 lines down saying it does not. All three now carry the verified +account: works on FFmpeg 7.1+, does not on 6.1.1, unconfirmed with OBS as +publisher. + +`docs/notes/enhanced-rtmp-multitrack.md` had itself drifted — it cited +`MaxTracks` as "a global cap of 6" at a line number that had moved. + +--- + +## What was rejected, and why + +**Instagram.** Codex called "Instagram Live cannot work" too absolute, arguing +`PLATFORMS.md` documents rare accounts that retain a Live Producer RTMP endpoint +and could use a Generic RTMPS destination. It does not: `PLATFORMS.md:39` reads +"Not possible" in every column, and `:131` says plainly "polyemesis cannot stream +here". The site matches its source. Left unchanged. + +**Agy's OBS fix.** Agy correctly flagged the missing ingest-mode step, but its +proposed replacement kept `Settings → Stream → Custom, service SRT` — preserving +defect #4, which the other two caught. A fix that carries the bug forward is +worth noticing when weighing reviewers against each other. + +**"every channel of every track".** Opus flagged this as over-claiming now that +`MaxMeterChannels = 64` is reachable at 32 tracks. True, but it cannot bite a +six-track OBS user, and the same phrasing is in `README.md`. Noted, not changed. + +**Deliberately kept:** the dBFS-vs-LUFS derivation note in `MixMatrix.astro` +(the most credibility-building paragraph on the site), six tracks as the working +*illustration* throughout, the "what it does not do" list, the comparison page's +instruction to go use Restreamer, and the structured data's absence of +`aggregateRating`. + +--- + +## Still open + +`web/public/shots/04-meters.png` — the image under "Proof, not a diagram" — has +**"Ingest Offline"** in its header bar. `features.astro:4-8` states the dashboard +capture was deliberately withheld for exactly this defect; the policy was simply +never applied to the one image carrying the site's central proof. Needs a +recapture, not a copy change. diff --git a/docs/notes/enhanced-rtmp-multitrack.md b/docs/notes/enhanced-rtmp-multitrack.md new file mode 100644 index 00000000..69c392d4 --- /dev/null +++ b/docs/notes/enhanced-rtmp-multitrack.md @@ -0,0 +1,233 @@ +# Accepting Enhanced RTMP multitrack from OBS + +**Status: the documented blocker is stale, but only on a new enough FFmpeg.** +Every doc in this repo says RTMP ingest is single-track "by protocol". The +protocol part is no longer true — FFmpeg merged multitrack FLV demuxing (Timo +Rothenpieler, late 2024) and the existing ingest command carries multiple audio +tracks over RTMP unchanged. + +**It is an FFmpeg version dependency, not a free win.** Verified on the OVH +deployment, which runs Ubuntu 24.04's stock FFmpeg 6.1.1: + +``` +$ ffmpeg ... -map 1:a -map 2:a -f flv out.flv +[flv @ ...] at most one audio stream is supported in flv +``` + +6.1.1 can neither write nor read multitrack FLV. So this is not the pure +documentation change the effort table below first suggested: it needs a minimum +FFmpeg version, and a capability probe so an operator on an older build is told +why their extra tracks vanished rather than silently receiving one. The bundled +Docker image pins a new enough FFmpeg; binary-mode installs on Ubuntu 24.04 do +not. + +Note the direction of travel is not uniformly "newer is more permissive": the +same 6.1.1 accepts an 80-channel `amerge` where 8.1.2 refuses past 64, because +the 7.0 channel-layout rework introduced a hard cap. Capping at 64 is therefore +correct on both. + +## What was verified + +FFmpeg 8.1.2, publishing into the exact argv `IngestArgs` builds +(`internal/ffmpeg/build.go:304`): + +``` +ffmpeg -listen 1 -fflags +genpts -i rtmp://…/live/test \ + -map 0 -c copy -f mpegts -flush_packets 1 +``` + +> That argv is what `IngestArgs` built at the time. Since 2026-08-06 the ingest +> ffmpeg *subscribes* to `internal/rtmpserver` instead — +> `-i rtmp://127.0.0.1:1935/live/`, no `-listen 1`. The finding is +> unaffected: the listener relays RTMP messages without decoding them, so the +> tracks reach this same `-map 0 -c copy` in the same order. Anyone re-running +> the verification should use the current argv rather than the one recorded +> here. + +**Six audio tracks — `MaxTracks` — arrive intact, in the published order, on +every run.** Stream *count* cannot detect a reordering, so each track carried a +distinct tone (300/500/700/1100/1300/1700 Hz) and was identified on arrival by +its content via a Goertzel detector rather than by its index: + +| publisher shape | runs | probe result | track order | +|---|---|---|---| +| AAC, track 0 legacy `0xaf` | 5 | `1v + 6a` | exact match, every run | +| Opus, track 0 ExHeader `0x91` | 3 | `1v + 6a` | exact match, every run | + +The harness is mutation-verified: republishing with the tracks deliberately +shuffled `[3,0,5,1,4,2]` made it report exactly that permutation, so it is +reading content and not stream position. + +Those probe results come from polyemesis's own `ProbeArgs` +(`internal/ffmpeg/build.go:1074`), not an ad-hoc ffprobe invocation. + +The harness is `scripts/verify_ertmp_multitrack.go`: + +```sh +go run scripts/verify_ertmp_multitrack.go -runs 5 # both codecs +go run scripts/verify_ertmp_multitrack.go -runs 1 -shuffle # prove it can fail +``` + +It checks FFmpeg's own multitrack conformance, not polyemesis's ingest path — +that stopped being `ffmpeg -listen 1` when the listener became +`internal/rtmpserver`. The shipped path is covered by +`TestEnhancedRTMPMultitrackSurvivesTheSharedListenerInOrder`. + +### It is genuinely E-RTMP v2, and genuinely the mode OBS uses + +Tag inspection of the published stream: + +| audio tag header | meaning | +|---|---| +| `0xaf` | legacy `SoundFormat=10` (AAC) — track 0, AAC case | +| `0x91` | `ExHeader`, `AudioPacketType=1`, fourCC `Opus` — track 0, Opus case | +| `0x95` | `ExHeader`, `AudioPacketType=5` **Multitrack** — tracks 1..5 | + +And OBS's own muxer (`plugins/obs-outputs/flv-mux.c`) writes audio this way: + +```c +s_w8(&s, AUDIO_HEADER_EX | (is_multitrack ? AUDIO_PACKETTYPE_MULTITRACK : type)); +if (is_multitrack) { + s_w8(&s, MULTITRACKTYPE_ONE_TRACK | type); + s_wa4cc(&s, codec_id); + s_w8(&s, (uint8_t)idx); // trackId +} else { + s_wa4cc(&s, codec_id); +} +``` + +Two things follow. First, OBS uses **`MULTITRACKTYPE_ONE_TRACK`** — of the three +modes E-RTMP v2 defines, it is the one exercised above. Second, OBS writes +track 0 as `AUDIO_HEADER_EX | type` (ExHeader + fourCC) rather than as a legacy +tag; that is why the Opus run matters, since Opus has no legacy FLV +`SoundFormat` and forces FFmpeg down the same ExHeader shape. Both track-0 +encodings were verified. + +Track discovery is already protocol-agnostic: `ProbeArgs` probes +`RelayInputURL(…)`, i.e. the MPEG-TS relay *after* ingest. Nothing in routing +keys off `IngestRTMP`, and `MaxTracks` (`internal/routing/profile.go`) is a +global cap of 32, not an SRT one. + +So the pipeline from the relay onward needs no work at all. + +## What actually remains + +### 1. A confirmation run against real OBS — DONE, and it found something else + +`scripts/acceptance-obs-multitrack.sh` runs OBS headless in Docker and publishes +into a real polyemesis. Two results, and the second was not expected. + +**Confirmed:** OBS's RTMP `connect`/handshake is accepted by the shared +listener, its stream key is admitted, and what it sends is probed and decodable. +That was the part FFmpeg-as-publisher could not stand in for. + +**Disproved:** OBS 30.2.3 does not send multitrack audio over RTMP. Three audio +tracks configured, each on its own mixer, `StreamMultiTrackAudioMixes=7`, custom +RTMP service — and the captured wire bytes are `0xaf legacy ×3541` with no +`0x95` multitrack tag anywhere. + +The gate is `supports_additional_audio_track`, tested in `rtmp-services.so`. +No service in `services.json` declares it (0 of 91), so it is unreachable for +every service including custom RTMP. Note the singular in the name: even where +it is enabled, it appears to buy one *additional* track, not six. + +This section previously said the wire format "is no longer in question — it was +read out of OBS's source". That was true and beside the point: the muxer +implements multitrack correctly, and nothing reaches it. Reading an +implementation tells you what the code would do, not whether it runs. + +### 2. Track identity — largely answered + +The original worry was that `ffprobe` exposes no `trackId`, only ordering, while +polyemesis routes by index. Eight runs across two codecs all preserved order +exactly, and OBS assigns `trackId = idx` in publish order. + +But every one of those runs published a **contiguous 0..5**, and the spec is +explicit that this is not something a receiver may assume: + +> Additional variants … SHOULD use distinct positive trackId values (1, 2, 3, …). +> These values are identifiers only and **do not imply any inherent ordering**, +> priority, or quality ranking. + +So the open case is a **sparse or reordered track set** — a publisher sending +trackIds 0, 2 and 5, or reconnecting mid-session with a different selection +enabled. Position and trackId diverge there, and since polyemesis routes by +index, that is precisely where audio would reach the wrong destination while +every screen still looks correct. + +Worth noting the risk is asymmetric: too few tracks is visible immediately, +whereas a silently shifted mapping is not visible at all until someone listens +to the wrong platform's output. That argues for reconciling against `trackId` +rather than trusting arrival order, even though arrival order held in testing. + +### 3. Codec breadth + +E-RTMP v2 permits Opus, AC-3, E-AC-3 and FLAC on audio tracks. Opus was +confirmed above to survive `-c copy` into MPEG-TS and probe correctly, so +*transport* is fine — but the downstream mixing path assumes AAC-shaped input. If OBS can be configured to send anything other +than AAC, that path needs either support or an explicit refusal — refusal being +consistent with how Opus-on-RTMP is already handled for destinations. + +### 4. Documentation and copy + +Six documents assert RTMP is single-track and would become wrong: + +- `docs/FAQ.md:51` — "What about Enhanced RTMP / multitrack FLV from OBS 30.2+?" +- `docs/CONFIGURATION.md:104` — the `enhancedRtmp` removal note +- `docs/ARCHITECTURE.md:98`, `docs/DESIGN-ONE-PORT-INGEST.md:139`, + `docs/DESIGN-ONE-PORT-ONLY.md:108`, `docs/INSTALL.md:206` +- `internal/db/settings.go:23` — the `IngestRTMP` doc comment +- `internal/config/config.go:50` + +Plus one UI string, `set.modeRtmp` = "RTMP — single track", and its 15 +translations. + +Note the `enhancedRtmp` config key does **not** need reinstating. It was removed +because it changed nothing; if this works it still changes nothing, because it +works by default with no flag. + +## What does *not* change + +**Multitrack and multi-source are independent problems**, and nothing in this +note touches the second one. That separation is the point and it still holds: +Enhanced RTMP is about how many audio tracks ride one connection, not about how +many connections the listener can tell apart. + +> **Correction, 2026-08-06.** This section previously read "**RTMP remains one +> source**", and gave as the reason that closing the limit would need either a +> Go RTMP server or the rejected `yutopp/go-rtmp` dependency. It needed the +> former, and got it: `internal/rtmpserver` wraps `bluenviron/gortmplib` — a +> different dependency from the rejected one — and `checkRTMPExclusive` is gone. +> RTMP is now one port, addressed by stream key, any number of sources. See +> [multi-source-rtmp.md](multi-source-rtmp.md) and +> `DESIGN-ONE-PORT-ONLY.md#rtmp`. +> +> Nothing about the multitrack findings below or above depends on that. The +> tracks ride through the new listener untouched, because it relays RTMP +> messages and never decodes them — the same reason they rode through +> `ffmpeg -listen 1` untouched. The FFmpeg version dependency is unchanged: +> 7.1+ verified, 6.1.1 refuses. OBS-as-publisher is now run and confirmed for +> connect/handshake/probe — and disproved for multitrack, which OBS 30.2.3 does +> not send at all. See section 1 under "What actually remains". + +The reasoning in `DESIGN-ONE-PORT-ONLY.md` — "no amount of RTMP work enables +per-destination multitrack routing" — is the part that has aged. It is now +enabled by FFmpeg, for free, without any RTMP work at all. That argument was +also the load-bearing reason multi-source RTMP was deferred, which is how the +two ended up connected after all. + +## Rough shape of the effort + +| | | +|---|---| +| Minimum-FFmpeg check + capability probe | half a day — **newly required, see above** | +| ~~Confirmation run against real OBS~~ | **done** — `scripts/acceptance-obs-multitrack.sh` | +| Reconnect-with-different-tracks test | half a day (order itself is verified) | +| Codec guard | half a day | +| Docs + UI string + 15 locales | half a day | +| Multi-source RTMP | out of scope here — done separately on 2026-08-06, see [multi-source-rtmp.md](multi-source-rtmp.md) | + +On the evidence gathered the ingest path already does this — on a new enough +FFmpeg. What turns it from a documentation change into a small piece of work is +the version dependency: the deployment this was tested against cannot do it at +all, and would need to say so rather than quietly dropping tracks. diff --git a/docs/notes/multi-source-rtmp.md b/docs/notes/multi-source-rtmp.md new file mode 100644 index 00000000..b02539f4 --- /dev/null +++ b/docs/notes/multi-source-rtmp.md @@ -0,0 +1,248 @@ +# Multi-source RTMP: the design record + +> **Built, 2026-08-06.** This started as a note asking what multi-source RTMP +> would take. It is kept as the record of the decision because it holds the +> measurements the choice rested on and the options that were rejected — +> `DESIGN-ONE-PORT-ONLY.md#rtmp` is the shorter statement of what shipped and is +> the one to read first. +> +> What shipped: `internal/rtmpserver`, one TCP port, sources addressed by the +> stream key in the publish URL, no per-source ports of any kind, media never +> decoded. `checkRTMPExclusive` is gone. +> +> Sections below are left in their pre-decision tense where that is what makes +> the reasoning legible; the outcome of each is marked. + +## What was actually in the way + +Nothing about RTMP. The URL carries `app/streamkey` and every RTMP server on +earth routes on it. polyemesis just did not have an RTMP server — it used +`ffmpeg -listen 1`, a single-connection receiver that cannot demultiplex by +path. A second RTMP source was refused at the form (`checkRTMPExclusive` in +`internal/db/sources.go`) rather than accepted and silently starved, which is +what would have happened if two of them tried to bind 1935. + +That refusal was the right call for as long as the limit existed — a source that +saves cleanly, reports itself configured and silently receives nothing is the +worst of the available failures. It is the limit that was wrong, not the guard. + +## The dependency argument, re-measured + +The original rejection was about `yutopp/go-rtmp` pulling seven modules. That is +still true, and it has not moved since: + +| | transitive deps | version | notes | +|---|---|---|---| +| `github.com/datarhei/gosrt` (in use today) | **1** | v0.11.0 | the bar | +| **`github.com/bluenviron/gortmplib`** | **3** — `abema/go-mp4`, `bluenviron/mediacommon/v2`, `google/uuid` | **v1.0.0** | MediaMTX/gortsplib team | +| `github.com/yutopp/go-rtmp` | 7 — `logrus`, `pkg/errors`, `mapstructure`, … | v0.0.7 | unchanged; `mapstructure` pinned at a 2021 release | + +**gortmplib did not exist in this form when the decision was made.** It is v1.0.0 +rather than v0.0.7, carries no second logging framework, no `pkg/errors`, no +`mapstructure`, and comes from the people who maintain gortsplib and MediaMTX — +i.e. the same provenance argument that justified `gosrt`. + +Three deps against `gosrt`'s one is a real cost, not a free one. But it is a +different order of magnitude from seven, and the "dead upstream" objection no +longer applies to anything in the tree. + +## The shape that fits this codebase + +`internal/srtserver` is the precedent, and it is worth being precise about what +it does: it is **not** a from-scratch protocol implementation. It wraps `gosrt` +and adds the part that is ours — a `Lookup func(token string) (Target, bool)` +for auth, one-publisher-per-key enforcement, and piping the accepted connection +to a relay sink. About 1,300 lines including tests. + +An RTMP equivalent lands in the same place, and gortmplib exposes exactly the +right seam: + +```go +type ServerConn struct { + RW io.ReadWriter + URL *url.URL // filled by Accept — this is app/streamkey + Publish bool + // ... +} + +type Conn interface { + Read() (message.Message, error) + Write(msg message.Message) error + // ... +} +``` + +`URL` after `Accept()` is the demux key, which is the entire feature. `Conn` then +works at **RTMP message level** — not raw bytes, but not decoded media either. + +That distinction decides the whole design. + +### Option A — routing front-end, FFmpeg still ingests (recommended) + +Accept the connection, read the URL, look up the source by stream key exactly as +`srtserver` looks up by token, then forward the RTMP messages to a per-source +`ffmpeg` process. polyemesis never parses a frame; FFmpeg keeps doing every bit +of media work it does today, and `-map 0 -c copy` is untouched. + +This preserves the architecture's central rule — *accept, do not decode, +republish* — and it is the reason to prefer gortmplib's `Conn` over its `Reader`. + +- ~600–900 lines plus tests, closely modelled on `internal/srtserver` +- One new dependency tree (3 modules) +- Roughly a week, including the port/listener plumbing and the settings work to + remove `checkRTMPExclusive` + +> **Outcome.** This is what was built. The package came in at ~540 lines plus +> ~380 of tests, and the dependency tree measured as predicted: three modules in +> `go.mod`'s indirect block, of which `google/uuid` was already present via +> sqlite. The one thing the estimate got wrong is described under *Correction — +> one port, both directions* below: "forward to a per-source ffmpeg" turned out +> to be a per-source port by another name, and the pub/sub shape removed it. + +### Option B — full ingest in Go + +Use gortmplib's `Reader` with its `OnDataH264` / `OnDataMPEG4Audio` callbacks, +then re-mux to MPEG-TS in Go and feed the relay directly. + +This is the wrong trade. Those callbacks hand you parsed access units, so a +muxer written here lands in the critical path of every RTMP frame — a class of +bug the project currently does not own at all, in exchange for removing one +FFmpeg process. Several weeks, and permanently ours to maintain. + +## What has NOT changed + +The strongest argument in the original rejection was never about dependencies: + +> **RTMP carries one stereo pair.** No amount of RTMP work enables +> per-destination multitrack routing, which is the reason this product exists. + +That is now **partly** false, and only partly. Enhanced RTMP multitrack does work +on FFmpeg 7.1+ — verified end to end, a destination configured for tracks 1 and 3 +received exactly those two tones. So multi-source RTMP would no longer be limited +to single-track programmes on a current FFmpeg. + +But it still gives nothing SRT does not already give, to anyone who can use SRT. +The honest case for this work is narrow and specific: **encoders that cannot do +SRT at all**, where an install supported exactly one of them. Hardware encoders, +older appliances, and anything speaking only RTMP. If you have two of those, +this is the only thing that unblocks you; if you have OBS, SRT is strictly +better and was already unlimited. + +## Decision (2026-08-06): build it + +The requirement is that **how many sources you can run must not depend on which +protocol your encoder speaks**. That is the right principle, and the current +asymmetry — unlimited over SRT, exactly one over RTMP — is an implementation +artifact of `ffmpeg -listen 1`, not a design position anyone chose. + +### Rejected: a TCP port per RTMP source + +The cheap version. It is also the surface `DESIGN-ONE-PORT-ONLY.md` deliberately +removed: allocation, conflict detection, auto-move-on-collision, a port field per +source in the UI, and validation in two places — "several of them *were* wrong at +some point". Reintroducing that for RTMP undoes a good decision to save a week. + +### Chosen: gortmplib as a routing front-end + +RTMP gets the same shape SRT already has — one port, addressed by a key, +unlimited sources: + +``` +srt://host:6000?streamid= # today +rtmp://host:1935/live/ # same model, same one port +``` + +`ServerConn.URL` is filled by `Accept()`, so the path IS the demux key. The +package mirrors `internal/srtserver` almost line for line: + +| `internal/srtserver` | `internal/rtmpserver` | +|---|---| +| `Lookup func(token string) (Target, bool)` | `Lookup func(streamKey string) (Target, bool)`, same constant-time contract | +| `PublisherKey{SourceID, Backup}` | identical — the failover standby is a second slot, not a contender | +| `handlePublish` pipes bytes to `Target.Sink` | relays `message.Message` to the source's FFmpeg | + +**Media is never parsed.** Both `ServerConn` and `Client` implement +`Conn{Read() (message.Message, error); Write(message.Message) error}`, so the +relay is message-level: accept the publisher, open a `Client` publishing to that +source's FFmpeg, and pump. gortmplib's `Reader`/`OnDataH264` path is explicitly +NOT used — it hands over decoded access units and would put a muxer in the +critical path of every frame. + +**Correction — one port, both directions.** The first implementation relayed each +publisher *outward* to a per-source FFmpeg on its own loopback port. That works, +but it is not how RTMP servers are built and the extra port was unnecessary: +datarhei Core runs its internal RTMP server as a publish/subscribe hub on a +single port, with FFmpeg pulling `rtmp://127.0.0.1:1935/` back out of the +same listener the encoder pushed into. Every other RTMP server does the same. + +So the server serves both roles: + +``` +encoder --publish--> rtmp://host:1935/live/ +ffmpeg --play-----> rtmp://127.0.0.1:1935/live/ +``` + +No per-source ports at all — not even loopback ones. + +Two consequences fall out of it: + +- **Subscribers are loopback-only.** A stream key is a *publish* credential; if + it also authorised playback, every ingest key would silently become a viewing + key. Playback for viewers is the playout page's job, behind authentication. +- **Setup messages are cached and replayed.** A subscriber joining after the + metadata and codec sequence headers have gone past cannot decode anything. + They are remembered per stream and replayed on subscribe, which is what makes + the order of "encoder connects" and "FFmpeg connects" not matter — and it does + vary, since an encoder reconnecting mid-session is routine. This is the only + message inspection in the package: a type switch, not a decode. + +### Work + +1. `internal/rtmpserver` — accept, demux by key, constant-time lookup, + one-publisher-per-slot with takeover, message relay. ~600 lines + tests. +2. Manager wiring, mirroring `manager.go`'s shared SRT listener. +3. Delete `checkRTMPExclusive` and its tests; RTMP sources become ordinary. +4. Publish URL for RTMP sources becomes per-source (`/live/`), so the + Sources page offers one the way it does for SRT. +5. Docs: `DESIGN-ONE-PORT-ONLY.md`'s RTMP section is superseded. + +Enhanced RTMP needs nothing extra — it is the same connection, and the tracks +ride through the relay untouched on FFmpeg 7.1+. + +## What this does and does not give you + +Worth being exact, because "multi-source RTMP" invites a reading it does not +support. + +**It gives you:** any number of RTMP sources on one install, on one port, told +apart by stream key, each an ordinary source with its own destinations, +renditions, recordings and routing. No per-source ports, no `docker-compose.yml` +edit per programme, no refusal at the form. + +**It does not give you multitrack over classic RTMP.** That is a property of FLV +and no amount of listener work changes it. Enhanced RTMP multitrack does work on +**FFmpeg 7.1+** — verified end to end, a destination configured for tracks 1 and +3 received exactly those two tones — and does **not** work on **FFmpeg 6.1.1**, +which is Ubuntu 24.04's stock build. It has **not** been confirmed with OBS +itself as the publisher; the verification used a synthetic FFmpeg publisher. See +[enhanced-rtmp-multitrack.md](enhanced-rtmp-multitrack.md). + +**It does not make RTMP the recommended path.** If your encoder can speak SRT, +SRT is still strictly better: more tracks, encryption that is real rather than a +string comparison, and it was already unlimited. The audience for this work is +encoders that cannot speak SRT at all — hardware encoders, older appliances — +where an install previously supported exactly one. + +## Why it was built when it was + +The original recommendation was "Option A, if and when the second RTMP encoder +is real rather than hypothetical", on the grounds that the case was narrow. + +That deferral was overturned by a better statement of the requirement: +**how many programmes you can run must not depend on which protocol your encoder +speaks.** Framed that way it is not a feature request waiting on demand, it is a +rule the product was breaking. The asymmetry existed because of what +`ffmpeg -listen 1` permitted, and "nobody has asked" was never evidence that the +shape was right — only that nobody had hit it yet. Waiting for the second +hardware encoder to show up would have meant fixing it under pressure, in front +of somebody whose install already could not do what they needed. diff --git a/docs/notes/video-treatment-ui.md b/docs/notes/video-treatment-ui.md new file mode 100644 index 00000000..48473cc2 --- /dev/null +++ b/docs/notes/video-treatment-ui.md @@ -0,0 +1,190 @@ +# Redesigning how a destination's video treatment is configured + +Three independent reviews — codex, agy and opus — plus a survey of datarhei +Restreamer, OBS, Livepeer Studio, Wowza, MediaLive, Ant Media, Oryx/SRS, +MediaMTX, Owncast and Jellyfin. Every claim below that touches this repo was +checked against the code. + +## What is actually wrong + +Not aesthetics. Three structural problems: + +1. **The cheap path and the expensive path are peers in one `` with two radio cards. Whole card is the hit target. + +``` +Video treatment ─────────────────────────────────────────────────── + + ● Copy the source video Recommended + -c:v copy — the source video exactly as your encoder sent it. + No encode, no process, no CPU. + + ○ Use a shared video encode + Changes the picture once and shares it between destinations. + + ┌────────────────────────────────────────────────────────────┐ + │ 1920×1080 · 60 fps · H.264 · 6000 kbps │ + │ "1080p60 for Twitch and Kick" │ + │ Feeds 2 destinations · already encoding │ + │ This destination joins the running encode. No new encode │ + │ starts. [Change] │ + └────────────────────────────────────────────────────────────┘ + + [Create a shared encode…] [Manage shared encodes ↗] +``` + +Spec first, name second. This fixes "named by the user, therefore +unrecognisable" without removing names. + +The consequence line is computed, not static, and has four states: + +| situation | line | +|---|---| +| joining a running encode | `This destination joins the running encode. No new encode starts.` | +| joining an idle encode | `Starts one shared encode when an enabled destination uses it.` | +| leaving, others remain | `Twitch and Kick stay on this encode. Nothing else changes.` | +| leaving, last one out | `Stops the "720p30 backup" encode — no other enabled destination is on it.` | + +The last two are the genuinely novel part: **nothing surveyed tells an operator +what happens to the encode they are leaving.** MediaLive's documented +remediation is literally "Make a note of the video encode, in case you need to +refer to it again." It is the same arithmetic as the join case, run backwards, +and it is free to compute. + +### Progressive disclosure + +When Copy is selected, everything below collapses. This is not invention — +Restreamer already does it (`FilterSelect` renders only `if (coder !== 'copy')`), +and it is the asymmetry made structural: the free path has nothing to configure. + +### Duplicate + +Add a third action on each encode row, seeded from the tier it copies. +Operators want "the same tier but 4500 kbps for the constrained uplink", and +without it they either edit the shared tier — silently changing another +platform's picture — or retype eight fields. MediaLive offers exactly this +(`Share the existing settings` vs `Clone the existing settings`). + +Do **not** copy MediaLive's modal. Selecting an existing tier should stay +frictionless; a dialog taxes the good case. And the risk the modal guards +against is already covered — `RenditionsPage.tsx:1753` warns *"Saving restarts +this encode, and with it the N enabled destinations above. Their audio routing +is untouched."* + +The Duplicate consequence line must say a duplicate is **a second encode, not a +free variation**. + +## Explicitly rejected + +**datarhei's `allowCopy` capability filter.** Restreamer suppresses the +passthrough option entirely when the source codec is not in the destination's +accepted list, driven by per-service capability metadata. Elegant, and wrong +here: `docs/PLATFORMS.md` and `db.PresetDisclaimer` take a deliberate position +against asserting platform ceilings — *"Being confidently wrong about one of +these numbers breaks a live stream."* A hard filter is that assertion, and it +would hide the cheap option on a guess. + +If the effect is wanted, take **OBS's** version: keep incompatible entries +visible and annotate them (`CodecCompat.Incompatible="(Incompatible with %1)"`). +Advisory and reversible. + +**Cost hints derived from platform limits.** If a hint is wanted, derive it from +the source-to-tier delta `sourceNotes()` (`RenditionsPage.tsx:326`) already +computes — "re-encoding 3840×2160 → 1920×1080" — never from a limits table. + +## Naming + +Leave "rendition" alone in the model and the docs. Unify only the four +phrasings of the *free* state, which currently reads as four different things: + +- `"passthrough · copy"` (RenditionsPage) +- `"passthrough"` (DestinationDialog's sentinel) +- `play.passthrough: "Ingest (passthrough)"` (i18n) +- the untranslated `""` in the dialog's own select + +Wowza is the warning here: it ships `Encode` in XML, "Preset" in the Manager and +"Stream Name Group" in the docs, for one concept. + +## Ranked + +| # | Change | Cost | +|---|---|---| +| 1 | Stop discarding `RenditionView` usage data | one line | +| 2 | Two radio cards replacing the ` + listing "passthrough" alongside every encode with two radio cards, on the + argument that copying video is nearly free and a shared encode is the most + expensive thing on the page — so they are not the same kind of choice and a + dropdown was wrong to say they were. + + What is asserted here is the part unit tests cannot reach: that the cards are + actually wired to the picker, that the picker COLLAPSES under Copy, and that + the consequence line is fed the real usage numbers from the server rather + than a plausible-looking constant. The phrasing and the arithmetic are + covered exhaustively (and mutation-tested) in + src/lib/rendition-consequence.test.ts; duplicating that here would be slow + and would not add a guard. + + The fourth consequence state — "N other enabled destinations stay on this + encode" — is the reason this file exists. It needs two destinations sharing + one encode, which is a fixture no unit test can stand up, and it shipped + without anyone confirming N was right. + =========================================================================== */ + +async function signIn(page: Page) { + await page.goto("/"); + await expect(page.locator("nav")).toBeVisible(); +} + +/** Same in-page fetch the rest of the suite uses, so the call carries the + * session cookie and the CSRF token. */ +async function api( + page: Page, + method: string, + path: string, + body?: unknown, +): Promise { + return page.evaluate( + async ({ m, p, b }) => { + const match = document.cookie.match(/(?:^|;\s*)polyemesis_csrf=([^;]+)/); + const csrf = match ? decodeURIComponent(match[1]) : ""; + const res = await fetch(p, { + method: m, + credentials: "same-origin", + headers: { + ...(b === undefined ? {} : { "Content-Type": "application/json" }), + ...(csrf ? { "X-CSRF-Token": csrf } : {}), + }, + body: b === undefined ? undefined : JSON.stringify(b), + }); + if (!res.ok) + throw new Error( + `${m} ${p} failed (${res.status}): ${await res.text()}`, + ); + return res.status === 204 ? null : await res.json(); + }, + { m: method, p: path, b: body }, + ); +} + +interface Rendition { + id: number; + name: string; +} +interface Destination { + id: number; + name: string; + renditionId?: number | null; +} + +async function makeRendition(page: Page, name: string): Promise { + const { rendition } = await api<{ rendition: Rendition }>( + page, + "POST", + "/api/v1/renditions", + { + name, + width: 1920, + height: 1080, + fps: 60, + videoBitrate: 6000, + encoder: "libx264", + preset: "veryfast", + gopSeconds: 2, + overlay: {}, + text: {}, + note: "", + }, + ); + return rendition; +} + +async function makeDestination( + page: Page, + name: string, + extra: Record = {}, +): Promise { + const { destination } = await api<{ destination: Destination }>( + page, + "POST", + "/api/v1/destinations", + { + name, + kind: "rtmp", + platform: "custom", + url: "rtmp://example.invalid/live/", + streamKey: "e2e-key", + audioBitrate: 160, + ...extra, + }, + ); + return destination; +} + +/** Opens the edit dialog for a destination by name, the way an operator does. */ +async function openEditor(page: Page, name: string) { + await page.goto("/"); + await page.getByRole("button", { name: `Actions for ${name}` }).click(); + // Exact: the same menu carries "Edit routing", a link to a different page. + await page.getByRole("menuitem", { name: "Edit", exact: true }).click(); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + return dialog; +} + +/** The encode picker, by test id. + * + * Not getByRole("combobox"): the dialog carries several, and .first() picked + * the platform selector — so the option click below waited on a menu that had + * never opened and the test died on a 30s timeout rather than an assertion. */ +function picker(dialog: ReturnType) { + return dialog.getByTestId("rendition-picker"); +} + +/** Choose a named encode explicitly. + * + * Clicking the "shared encode" card seeds the FIRST rendition in the list, + * which is whichever one the database hands back first — not necessarily the + * one this test created. Asserting on the seed is how "the chosen encode is + * what gets saved" failed with `Expected: 6, Received: 2` while the feature + * was working perfectly. */ +async function chooseEncode( + page: Page, + dialog: ReturnType, + name: string, +) { + await picker(dialog).click(); + await page.getByRole("option", { name: new RegExp(name) }).click(); +} + +test.describe("choosing a video treatment", () => { + test("the encode picker is collapsed under Copy and revealed by the encode card", async ({ + page, + }) => { + await signIn(page); + const rendition = await makeRendition(page, "e2e collapse tier"); + const dest = await makeDestination(page, "e2e collapse"); + + try { + const dialog = await openEditor(page, "e2e collapse"); + + // The group exists and is named. role="radio" outside a radiogroup is + // announced as an isolated control with no alternatives, which is why + // this is asserted rather than assumed. + const group = dialog.getByRole("radiogroup", { name: "Video treatment" }); + await expect(group).toBeVisible(); + + const copy = group.getByRole("radio", { name: /Copy the source video/ }); + const shared = group.getByRole("radio", { + name: /Use a shared video encode/, + }); + + // A new destination has no rendition, so Copy is the state. + await expect(copy).toHaveAttribute("aria-checked", "true"); + await expect(shared).toHaveAttribute("aria-checked", "false"); + + // THE PROGRESSIVE DISCLOSURE: the free path has nothing to configure, so + // nothing about encodes is on screen at all. If the picker rendered here + // it would imply Copy has settings. + await expect(picker(dialog)).toHaveCount(0); + + await shared.click(); + await expect(shared).toHaveAttribute("aria-checked", "true"); + await expect(copy).toHaveAttribute("aria-checked", "false"); + + // Revealed, and seeded with a real encode rather than left empty — an + // empty required picker behind a radio the operator just chose is a dead + // end they have to discover by saving. + await expect(picker(dialog)).toBeVisible(); + await expect(picker(dialog)).toContainText("1920×1080"); + + // And back: the picker collapses again. + await copy.click(); + await expect(picker(dialog)).toHaveCount(0); + } finally { + await api(page, "DELETE", `/api/v1/destinations/${dest.id}`); + await api(page, "DELETE", `/api/v1/renditions/${rendition.id}`); + } + }); + + test("the join consequence reports whether an encode is already running", async ({ + page, + }) => { + await signIn(page); + const idle = await makeRendition(page, "e2e idle tier"); + const busy = await makeRendition(page, "e2e busy tier"); + + // One ENABLED destination on `busy`, so it is genuinely encoding. Without + // this the two branches are indistinguishable and the line could be a + // constant. + const holder = await makeDestination(page, "e2e holder", { + renditionId: busy.id, + enabled: true, + }); + const dest = await makeDestination(page, "e2e joiner"); + + try { + const dialog = await openEditor(page, "e2e joiner"); + await dialog + .getByRole("radio", { name: /Use a shared video encode/ }) + .click(); + + // Pick the idle tier: joining it will START an encode. This is the + // expensive branch and the one worth naming. + await chooseEncode(page, dialog, "e2e idle tier"); + await expect( + dialog.getByText( + "Starts one shared encode when an enabled destination uses it.", + ), + ).toBeVisible(); + + // Pick the busy tier: joining costs nothing. + await chooseEncode(page, dialog, "e2e busy tier"); + await expect(dialog.getByText(/already encoding/)).toBeVisible(); + await expect(dialog.getByText(/no new encode starts/)).toBeVisible(); + } finally { + await api(page, "DELETE", `/api/v1/destinations/${dest.id}`); + await api(page, "DELETE", `/api/v1/destinations/${holder.id}`); + await api(page, "DELETE", `/api/v1/renditions/${idle.id}`); + await api(page, "DELETE", `/api/v1/renditions/${busy.id}`); + } + }); + + test("leaving an encode says whether it stops or keeps running for others", async ({ + page, + }) => { + await signIn(page); + const shared = await makeRendition(page, "e2e shared tier"); + const solo = await makeRendition(page, "e2e solo tier"); + + // Two enabled destinations on `shared`, one on `solo`. That is the whole + // point: the same action produces opposite consequences depending on who + // else is on the encode, and nothing surveyed in the field tells an + // operator either one. + const a = await makeDestination(page, "e2e leaver a", { + renditionId: shared.id, + enabled: true, + }); + const b = await makeDestination(page, "e2e leaver b", { + renditionId: shared.id, + enabled: true, + }); + const lone = await makeDestination(page, "e2e lone", { + renditionId: solo.id, + enabled: true, + }); + + try { + // ---- others remain ---- + let dialog = await openEditor(page, "e2e leaver a"); + await dialog + .getByRole("radio", { name: /Copy the source video/ }) + .click(); + // Exactly one other enabled destination is on it, so the encode survives. + await expect( + dialog.getByText( + "1 other enabled destination stay on “e2e shared tier”. Nothing else changes.", + ), + ).toBeVisible(); + await expect(dialog.getByText(/Stops the/)).toHaveCount(0); + await page.keyboard.press("Escape"); + await expect(dialog).toBeHidden(); + + // ---- last one out ---- + dialog = await openEditor(page, "e2e lone"); + await dialog + .getByRole("radio", { name: /Copy the source video/ }) + .click(); + await expect( + dialog.getByText( + "Stops the “e2e solo tier” encode — no other enabled destination is on it.", + ), + ).toBeVisible(); + } finally { + for (const d of [a, b, lone]) + await api(page, "DELETE", `/api/v1/destinations/${d.id}`); + await api(page, "DELETE", `/api/v1/renditions/${shared.id}`); + await api(page, "DELETE", `/api/v1/renditions/${solo.id}`); + } + }); + + test("the chosen encode is what gets saved", async ({ page }) => { + // The cards can be wired to the picker and the picker to nothing. This is + // the end of the chain: what the operator selected is what the server + // stores, and what comes back when they reopen. + await signIn(page); + const rendition = await makeRendition(page, "e2e persisted tier"); + const dest = await makeDestination(page, "e2e persist"); + + try { + const dialog = await openEditor(page, "e2e persist"); + await dialog + .getByRole("radio", { name: /Use a shared video encode/ }) + .click(); + await chooseEncode(page, dialog, "e2e persisted tier"); + await dialog.getByRole("button", { name: /save/i }).click(); + await expect(dialog).toBeHidden(); + + const { destination: after } = await api<{ destination: Destination }>( + page, + "GET", + `/api/v1/destinations/${dest.id}`, + ); + expect(after.renditionId).toBe(rendition.id); + + // And it round-trips back into the dialog rather than reopening on Copy, + // which would quietly discard the choice on the next unrelated edit. + const reopened = await openEditor(page, "e2e persist"); + await expect( + reopened.getByRole("radio", { name: /Use a shared video encode/ }), + ).toHaveAttribute("aria-checked", "true"); + } finally { + await api(page, "DELETE", `/api/v1/destinations/${dest.id}`); + await api(page, "DELETE", `/api/v1/renditions/${rendition.id}`); + } + }); +}); diff --git a/ui/package-lock.json b/ui/package-lock.json index 36ac7c9b..1d86608a 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -13,6 +13,7 @@ "@radix-ui/react-dialog": "^1.1.23", "@radix-ui/react-dropdown-menu": "^2.1.24", "@radix-ui/react-label": "^2.1.15", + "@radix-ui/react-popover": "^1.1.23", "@radix-ui/react-progress": "^1.1.16", "@radix-ui/react-select": "^2.3.7", "@radix-ui/react-separator": "^1.1.15", @@ -44,7 +45,8 @@ "tailwindcss": "^4.3.3", "tw-animate-css": "^1.4.0", "typescript": "~7.0.2", - "vite": "^8.2.0" + "vite": "^8.2.0", + "vitest": "^4.1.10" } }, "node_modules/@emnapi/core": { @@ -950,6 +952,43 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.23.tgz", + "integrity": "sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", @@ -1636,9 +1675,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1656,9 +1692,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1676,9 +1709,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1696,9 +1726,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1716,9 +1743,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1736,9 +1760,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2412,6 +2433,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -2475,6 +2507,20 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.13.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", @@ -2877,6 +2923,119 @@ } } }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", @@ -2889,6 +3048,26 @@ "node": ">=10" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -2910,6 +3089,13 @@ "node": ">=6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie-es": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", @@ -3080,6 +3266,13 @@ "node": ">=10.13.0" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-toolkit": { "version": "1.50.0", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", @@ -3091,12 +3284,32 @@ "tests/types" ] }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -3480,6 +3693,20 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/oxlint": { "version": "1.76.0", "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.76.0.tgz", @@ -3529,6 +3756,13 @@ } } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3857,6 +4091,13 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -3877,6 +4118,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", @@ -3914,6 +4169,23 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -3931,6 +4203,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -4140,6 +4422,113 @@ "optional": true } } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/ui/package.json b/ui/package.json index bbf619b8..ada75cc3 100644 --- a/ui/package.json +++ b/ui/package.json @@ -7,7 +7,9 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "oxlint", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@radix-ui/react-accordion": "^1.2.20", @@ -15,6 +17,7 @@ "@radix-ui/react-dialog": "^1.1.23", "@radix-ui/react-dropdown-menu": "^2.1.24", "@radix-ui/react-label": "^2.1.15", + "@radix-ui/react-popover": "^1.1.23", "@radix-ui/react-progress": "^1.1.16", "@radix-ui/react-select": "^2.3.7", "@radix-ui/react-separator": "^1.1.15", @@ -46,7 +49,8 @@ "tailwindcss": "^4.3.3", "tw-animate-css": "^1.4.0", "typescript": "~7.0.2", - "vite": "^8.2.0" + "vite": "^8.2.0", + "vitest": "^4.1.10" }, "description": "Web UI for polyemesis, embedded into the Go binary at build time." } diff --git a/ui/src/components/AppLayout.tsx b/ui/src/components/AppLayout.tsx index 9347fad5..637b5401 100644 --- a/ui/src/components/AppLayout.tsx +++ b/ui/src/components/AppLayout.tsx @@ -39,7 +39,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { StatusDot } from "@/components/signature/StatusDot"; -import { useLiveData } from "@/hooks/useLiveData"; +import { useIngestLive, useLiveData } from "@/hooks/useLiveData"; import { useNavCollapsed } from "@/hooks/useNavCollapsed"; import { toneForState } from "@/lib/signal"; import { duration, kbps } from "@/lib/format"; @@ -100,7 +100,21 @@ export function AppLayout({ useEffect(() => setMobileOpen(false), [location.pathname]); const ingest = status?.ingest; - const ingestTone = toneForState(ingest?.state); + // BYTES ARRIVING, not a process running. + // + // useIngestLive is the app's one definition of "a broadcast is going out", + // and its own comment says `status.ingest.state === "running"` is not it. + // This header was the last place still asking the wrong question, and for SRT + // the answer was not merely imprecise but inverted: engine.reconcileIngest + // returns early for SRT on purpose — srtserver delivers datagrams straight + // into the hub, and a second thing on that socket would crash-loop — so + // `ingest` is null, `stateLabel(undefined)` is "Offline", and every healthy + // SRT install said its ingest was down in the most prominent status in the + // chrome. A committed screenshot showed it beside three live destinations. + // + // The process is still consulted for the bitrate below, where it exists. + const ingestLive = useIngestLive(); + const ingestTone = ingestLive ? "live" : toneForState(ingest?.state); const liveCount = status?.destinations.filter((d) => d.process?.state === "running").length ?? 0; @@ -129,9 +143,15 @@ export function AppLayout({ {t("chrome.ingest")} + {/* The bitrate when there is a process to read it from, and the + state otherwise. An SRT source is live without a child, so it + has no bitrate to show here — saying "Running" is the honest + answer rather than printing 0 kbps at a stream that is fine. */} {ingest?.state === "running" ? kbps(ingest.progress?.bitrateKbps ?? 0) - : stateLabel(ingest?.state)} + : ingestLive + ? stateLabel("running") + : stateLabel(ingest?.state)} @@ -318,7 +338,24 @@ export function AppLayout({ -
+ {/* `relative` is load-bearing, not decoration. + + The shell is h-dvh, so nothing should ever scroll the document — + only this pane and the sidebar. An absolutely positioned descendant + with no positioned ancestor resolves against the initial containing + block instead, which is the html element, so it extends + documentElement.scrollHeight without touching body's. That produces + a second scrollbar down the right-hand side next to this pane's own, + and measuring body tells you nothing because body is still exactly + the viewport height. + + The Dashboard's aria-live did precisely this: Tailwind's + .sr-only is `position: absolute` with no top/left, so it sat at its + static position ~951px down and stretched the document to 952px + against a 900px viewport. Making this pane a containing block keeps + that — and anything else a page renders — inside the scroller where + it belongs. */} +
diff --git a/ui/src/components/ChatMessageMenu.tsx b/ui/src/components/ChatMessageMenu.tsx new file mode 100644 index 00000000..2a9946e3 --- /dev/null +++ b/ui/src/components/ChatMessageMenu.tsx @@ -0,0 +1,212 @@ +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { Ban, ExternalLink, Timer, Trash2, UserSearch } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { api } from "@/lib/api"; +import { capabilityFor, supportOf } from "@/lib/capabilities"; +import { TIMEOUTS } from "@/lib/chat"; +import { openPlatformLink, platformLinkFor, platformNoun } from "@/lib/platformLinks"; +import type { ChatMessage } from "@/lib/types"; + +/* =========================================================================== + Right-click (or double-click) on a message. + + This adds no capability. Everything here can already be done from the user + card, and the card remains the place for anything that needs to be read + before it is decided — history, ban state, the retention caveat. What this + buys is the two-second path for the case that does not need reading: a line + scrolls past, it is obviously bad, and the moderator already knows what to do + about it. + + So the menu is deliberately the SHORT list. Timeouts, a ban, delete the + message, open the card, open the platform. Anything longer would turn a + shortcut back into a form. + + Actions that the platform cannot perform are shown DISABLED with the reason, + never hidden — the same rule the card follows, and for the same reason: a + missing button reads as a broken tool, not as an unsupported platform. + =========================================================================== */ + +export interface MenuAnchor { + x: number; + y: number; +} + +export function ChatMessageMenu({ + message, + anchor, + onClose, + onOpenCard, + onDelete, +}: { + message: ChatMessage; + /** Viewport coordinates of the click that opened this. */ + anchor: MenuAnchor; + onClose: () => void; + onOpenCard: (m: ChatMessage) => void; + onDelete?: (m: ChatMessage) => void; +}) { + // Radix positions against a trigger element, and the trigger here is a point + // rather than a control. A zero-size fixed div at the cursor is the standard + // shape for that: it anchors correctly, collision-flips near screen edges for + // free, and there is nothing to see or tab to. + const [open, setOpen] = useState(true); + + useEffect(() => { + if (!open) onClose(); + }, [open, onClose]); + + const authorId = message.author.id?.trim() ?? ""; + const caps = capabilityFor(message.platform); + const canModerate = supportOf(caps, "moderation") === "yes"; + const link = platformLinkFor(message); + + // The two reasons an action is unavailable are different and the operator + // needs to tell them apart: the platform has no such API, versus this + // particular message arrived without a user id to address. + const modReason = !canModerate + ? `${platformNoun(message.platform)} publishes no moderation API that polyemesis can call` + : !authorId + ? `${platformNoun(message.platform)} sent no user id with this message, so there is nobody to address` + : ""; + + const run = async (what: string, fn: () => Promise<{ detail?: string } | void>) => { + try { + const res = await fn(); + // The server's sentence wins. It knows the difference between "timed out + // on Twitch" and "Kick rounded this to the nearest minute". + const detail = res && "detail" in res ? res.detail : ""; + toast.success(detail || `${what} — ${message.author.name}`); + } catch (err) { + toast.error(err instanceof Error ? err.message : `Could not ${what.toLowerCase()}.`); + } + }; + + const close = () => setOpen(false); + + return ( + + {/* Radix measures the TRIGGER to place the content, so the cursor point + has to be the trigger itself — a marker rendered as a sibling is not + measured and the menu lands wherever the layout happens to put it. + A zero-size fixed element gives Radix a real rect at the cursor and + gets edge collision-flipping for free. */} + + + + e.preventDefault()} + > + + {message.author.name} + + on {platformNoun(message.platform)} + + + + + { + close(); + onOpenCard(message); + }} + disabled={!authorId} + title={authorId ? "Everything they have said, and every action" : modReason} + > + + View history & all actions + + + {link && ( + { + close(); + openPlatformLink(link); + }} + title={link.caveat} + > + + {link.label} + + )} + + + + {TIMEOUTS.map((t) => ( + { + close(); + void run(`Timed out for ${t.label}`, () => + api.banChatUser({ + platform: message.platform, + account: message.account, + userId: authorId, + seconds: t.seconds, + }), + ); + }} + > + + Time out {t.label} + + ))} + + + + {/* A permanent ban is the one irreversible action reachable from a + right-click, so it does not fire from the menu. It opens the card, + which asks again — the confirmation lives in one place rather than + being re-implemented here with different wording. */} + { + close(); + onOpenCard(message); + }} + > + + Ban permanently… + + + {onDelete && ( + { + close(); + void onDelete(message); + }} + > + + Delete message + + )} + + + ); +} diff --git a/ui/src/components/ChatPanel.tsx b/ui/src/components/ChatPanel.tsx index 5e6b2783..ab117795 100644 --- a/ui/src/components/ChatPanel.tsx +++ b/ui/src/components/ChatPanel.tsx @@ -5,9 +5,11 @@ import { Filter, Loader2, MessagesSquare, + Search, Send, Shield, Trash2, + X, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -27,7 +29,9 @@ import { splitMessage, } from "@/lib/chat"; import { messageKey, useChatFeed } from "@/hooks/useChatFeed"; +import { useChatSearch, type ChatSearchState } from "@/hooks/useChatSearch"; import { ChatUserCard } from "@/components/ChatUserCard"; +import { ChatMessageMenu, type MenuAnchor } from "@/components/ChatMessageMenu"; import type { ChatLimit, ChatMessage, @@ -97,12 +101,15 @@ function MessageRow({ m, onDelete, onOpenUser, + onMenu, compact, }: { m: ChatMessage; onDelete?: (m: ChatMessage) => void; /** Open the moderator's user card for whoever said this. */ onOpenUser?: (m: ChatMessage) => void; + /** Open the quick-action menu at a point. */ + onMenu?: (m: ChatMessage, at: { x: number; y: number }) => void; compact?: boolean; }) { const accent = accentFor(m.platform); @@ -119,8 +126,24 @@ function MessageRow({ } }; + // Right-click is the moderator's reflex from every other chat tool, and + // double-click is the same menu for anyone whose pointer has no right button + // — a trackpad without secondary-click configured, or a touch device. + // + // Text selection is deliberately NOT sacrificed for this: the browser's own + // selection survives because neither handler fires on a drag, and copying a + // line to quote it elsewhere is something moderators do constantly. + const openMenu = onMenu + ? (e: React.MouseEvent) => { + e.preventDefault(); + onMenu(m, { x: e.clientX, y: e.clientY }); + } + : undefined; + return (
void; onOpenUser?: (m: ChatMessage) => void; + onMenu?: (m: ChatMessage, at: { x: number; y: number }) => void; compact?: boolean; empty?: React.ReactNode; }) { @@ -260,6 +285,7 @@ export function ChatTimeline({ m={m} onDelete={onDelete} onOpenUser={onOpenUser} + onMenu={onMenu} compact={compact} /> ))} @@ -551,6 +577,121 @@ export function ChatStatusList({ ); } +// -------------------------------------------------------------------- search + +/** The search box. Narrow on purpose: one field, one clear button. */ +export function ChatSearchBox({ + search, + className, +}: { + search: ChatSearchState; + className?: string; +}) { + return ( +
+ + search.setQuery(e.target.value)} + // Escape is the way out of a search box everywhere else, and a + // moderator who has to find and click a small × while chat scrolls has + // been given a worse tool than they had before. + onKeyDown={(e) => { + if (e.key === "Escape") { + e.stopPropagation(); + search.clear(); + } + }} + placeholder="Search chat history…" + aria-label="Search chat history" + className="min-w-0 flex-1 bg-transparent text-[12px] outline-none placeholder:text-subtle-foreground" + /> + {search.loading && } + {search.query && ( + + )} +
+ ); +} + +/** Search results, in place of the live timeline. + * + * Replacing the timeline rather than filtering it is the honest rendering: + * these messages come from the database and are frozen at the moment of the + * query, so letting live messages continue to append underneath them would + * present two different things as one list. */ +export function ChatSearchResults({ + search, + onDelete, + onOpenUser, + onMenu, +}: { + search: ChatSearchState; + onDelete?: (m: ChatMessage) => void; + onOpenUser?: (m: ChatMessage) => void; + onMenu?: (m: ChatMessage, at: { x: number; y: number }) => void; +}) { + return ( +
+
+ + {search.loading + ? "Searching…" + : `${search.results.length} match${search.results.length === 1 ? "" : "es"}`} + {search.truncated && " (showing the newest)"} + + newest first +
+ +
+ {search.error ? ( +

{search.error}

+ ) : search.results.length === 0 && !search.loading ? ( + // The caveat matters most exactly here. An empty result is the one + // outcome an operator can misread as evidence, and "no matches" on + // its own invites "then they never said it" — which is a conclusion + // about a purged table, not about a person. +
+

No matches in the retained scrollback.

+ {search.note && ( +

{search.note}

+ )} +
+ ) : ( + search.results.map((m) => ( + + )) + )} +
+ + {search.results.length > 0 && search.note && ( +

+ {search.note} +

+ )} +
+ ); +} + // -------------------------------------------------------------- the panel /** The compact pane, sized to sit beside the dashboard preview. @@ -587,6 +728,9 @@ export function ChatPanel({ // just an id keeps the platform and account with it, which the card needs to // address a moderation call and cannot re-derive. const [card, setCard] = useState(null); + // Same reasoning for the quick menu, plus where it was opened. + const [menu, setMenu] = useState<{ m: ChatMessage; at: MenuAnchor } | null>(null); + const search = useChatSearch(); const del = useCallback( async (m: ChatMessage) => { @@ -613,6 +757,15 @@ export function ChatPanel({ onOpenChange={(o) => !o && setCard(null)} /> )} + {menu && ( + setMenu(null)} + onOpenCard={setCard} + onDelete={del} + /> + )}
Chat @@ -631,15 +784,27 @@ export function ChatPanel({
- - } - /> + + + {search.active ? ( + setMenu({ m, at })} + /> + ) : ( + setMenu({ m, at })} + compact + empty={ + + } + /> + )} {showComposer && ( diff --git a/ui/src/components/ChatUserCard.tsx b/ui/src/components/ChatUserCard.tsx index 80f5eda7..b5a106b0 100644 --- a/ui/src/components/ChatUserCard.tsx +++ b/ui/src/components/ChatUserCard.tsx @@ -11,7 +11,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { api } from "@/lib/api"; -import { accentFor } from "@/lib/chat"; +import { accentFor, TIMEOUTS } from "@/lib/chat"; import { capabilityFor, supportOf } from "@/lib/capabilities"; import { clockTime } from "@/lib/format"; import { cn } from "@/lib/utils"; @@ -38,16 +38,6 @@ import type { ChatMessage, ChatPlatform, ChatUserCard as CardData } from "@/lib/ ask again. =========================================================================== */ -/** Timeout choices. Seconds on the wire, always — the server converts for Kick, - * which counts in minutes. The list stops at a day because past that a timeout - * is a ban with extra steps, and Kick refuses beyond seven days anyway. */ -const TIMEOUTS: { label: string; seconds: number }[] = [ - { label: "1 min", seconds: 60 }, - { label: "10 min", seconds: 600 }, - { label: "1 hour", seconds: 3600 }, - { label: "1 day", seconds: 86400 }, -]; - export function ChatUserCard({ platform, account, diff --git a/ui/src/components/DestinationCard.tsx b/ui/src/components/DestinationCard.tsx index 9d06c089..d90bf266 100644 --- a/ui/src/components/DestinationCard.tsx +++ b/ui/src/components/DestinationCard.tsx @@ -25,8 +25,9 @@ import { StatusDot } from "@/components/signature/StatusDot"; import { TrackSummary } from "@/components/signature/TrackRows"; import { Stat } from "@/components/signature/Stat"; import { duration, kbps } from "@/lib/format"; -import { labelForState, toneBadge, toneForState } from "@/lib/signal"; +import { toneBadge, toneForState } from "@/lib/signal"; import type { DestStatus } from "@/lib/types"; +import { useT, useStateLabel } from "@/lib/i18n"; const PLATFORM_LABEL: Record = { youtube: "YouTube", @@ -70,6 +71,8 @@ export function DestinationCard({ canMoveLater: boolean; busy?: boolean; }) { + const t = useT(); + const stateLabel = useStateLabel(); const state = dest.process?.state; const tone = dest.enabled ? toneForState(state) : "idle"; const running = state === "running"; @@ -82,7 +85,12 @@ export function DestinationCard({ // would send someone looking for a fault in the wrong place. const video = dest.renditionId ? dest.renditionName || `rendition ${dest.renditionId}` - : "passthrough · copy"; + // One name for the free state, everywhere. It read four different ways + // across the UI — "passthrough · copy" here, "Passthrough — source, copied" + // in the dialog, "Ingest (passthrough)" in playout — for one concept, which + // is how Wowza ended up shipping "Encode", "Preset" and "Stream Name Group" + // for a single thing. + : "source, copied"; return ( @@ -101,7 +109,7 @@ export function DestinationCard({
- {dest.enabled ? labelForState(state) : "Stopped"} + {dest.enabled ? stateLabel(state) : t("state.stopped")} @@ -234,9 +242,16 @@ export function DestinationCard({ {dest.backupProcess && (
backup feed: - - {dest.backupProcess.state} - + {/* Through toneForState, like every other state in the app. The + two hand-written classes this replaced were `text-ok` and + `text-warn`: there is no --color-ok token, so Tailwind never + emitted the first one and a *healthy* backup rendered in plain + body text while an unhealthy one was amber — the two states + were told apart only by the presence of colour, and the good + one was the invisible half. The raw `state` string was also + escaping the catalogue untranslated. */} + + {stateLabel(dest.backupProcess.state)}
)} {dest.backupError && ( diff --git a/ui/src/components/DestinationDialog.tsx b/ui/src/components/DestinationDialog.tsx index f5b52109..3323e6fc 100644 --- a/ui/src/components/DestinationDialog.tsx +++ b/ui/src/components/DestinationDialog.tsx @@ -10,6 +10,8 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Badge } from "@/components/ui/badge"; +import { StatusDot } from "@/components/signature/StatusDot"; +import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -22,6 +24,7 @@ import { } from "@/components/ui/select"; import { api } from "@/lib/api"; import { useT } from "@/lib/i18n"; +import { computeLeaving, joinConsequence, leaveConsequence } from "@/lib/rendition-consequence"; import { Switch } from "@/components/ui/switch"; // The capability matrix this dialog renders inline. Data, not a component, and // shared with the settings page — see lib/capabilities.ts. @@ -45,6 +48,8 @@ import type { Platform, PlatformAccount, Rendition, + PlatformPresetInfo, + RenditionView, } from "@/lib/types"; /** The transport a preset's ingest is reached over. Finer-grained than DestKind @@ -589,16 +594,43 @@ export function DestinationDialog({ open, onOpenChange, destination, onSaved }: const [accountId, setAccountId] = useState("none"); const [accounts, setAccounts] = useState([]); const [renditionId, setRenditionId] = useState(PASSTHROUGH); - const [renditions, setRenditions] = useState([]); + // RenditionView, not Rendition. The view carries `destinations` and + // `enabledDestinations`, and this used to strip them off one line after they + // arrived — throwing away the only data that can tell an operator whether + // picking an encode starts a new one or joins a running one. That fact is + // the entire argument for renditions existing, and the UI could not state it. + const [renditions, setRenditions] = useState([]); + const [guidance, setGuidance] = useState([]); const [busy, setBusy] = useState(false); + // The variant form: an operator wanting "this tier but 4500 kbps for the + // constrained uplink". Deliberately NOT a per-destination override of the + // shared encode — there is no such thing, and there must not be: editing the + // shared tier would silently change the picture every other destination on it + // receives. A variant is a SECOND encode, and the UI says so before you make + // one. + const [variantOpen, setVariantOpen] = useState(false); + const [variantBitrate, setVariantBitrate] = useState(""); + const [variantWidth, setVariantWidth] = useState(""); + const [variantHeight, setVariantHeight] = useState(""); + const [variantFps, setVariantFps] = useState(""); + const [variantErr, setVariantErr] = useState(""); useEffect(() => { if (!open) return; api.listAccounts().then(setAccounts).catch(() => setAccounts([])); api .listRenditions() - .then((rows) => setRenditions(rows.map((r) => r.rendition))) + .then(setRenditions) .catch(() => setRenditions([])); + // Fetched, not mirrored. The UI keeps its own preset list so the picker + // renders before any request resolves, but the researched numbers carry a + // source and a date and a second copy of them here would drift silently — + // which is exactly what happened when they were added to the Go catalogue + // and nothing surfaced them. + api + .platformPresets() + .then((r) => setGuidance(r.presets)) + .catch(() => setGuidance([])); setPickerOpen(false); setQuery(""); @@ -676,10 +708,50 @@ export function DestinationDialog({ open, onOpenChange, destination, onSaved }: return acct?.accountRef?.startsWith("page:") ?? false; }, [accounts, accountId]); - const selectedRendition = useMemo( - () => renditions.find((r) => String(r.id) === renditionId) ?? null, + /** What this destination's platform says it wants. Null when the operator has + * not picked a preset, or when the platform publishes nothing usable — which + * is a real answer for TikTok and PeerTube and is shown as silence rather + * than as an invented number. */ + const platformVideo = useMemo( + () => guidance.find((g) => g.id === presetId)?.video ?? null, + [guidance, presetId], + ); + + const selectedView = useMemo( + () => renditions.find((v) => String(v.rendition.id) === renditionId) ?? null, [renditions, renditionId], ); + const selectedRendition = selectedView?.rendition ?? null; + + /** What the encode this destination is LEAVING will do once it has gone. + * + * Nothing in the field tells an operator this — MediaLive's documented + * remediation is "make a note of the video encode, in case you need to refer + * to it again". It is the same arithmetic as joining, run backwards, so it + * costs nothing to say. */ + useEffect(() => { + if (!variantOpen || !selectedRendition) return; + setVariantBitrate(String(selectedRendition.videoBitrate)); + setVariantWidth(selectedRendition.width ? String(selectedRendition.width) : ""); + setVariantHeight(selectedRendition.height ? String(selectedRendition.height) : ""); + setVariantFps(selectedRendition.fps ? String(selectedRendition.fps) : ""); + setVariantErr(""); + }, [variantOpen, selectedRendition]); + + const leaving = useMemo( + () => + computeLeaving(destination?.renditionId, renditionId, destination?.enabled === true, (id) => { + const was = renditions.find((v) => v.rendition.id === id); + return was + ? { + name: was.rendition.name, + destinations: was.destinations, + enabledDestinations: was.enabledDestinations, + } + : null; + }), + [destination, renditionId, renditions], + ); // Thirty entries is past the point where scanning works, so the list is // searchable over everything an operator might type: the name, the id, the @@ -813,17 +885,17 @@ export function DestinationDialog({ open, onOpenChange, destination, onSaved }:
- + setName(e.target.value)} - placeholder="YouTube — main channel" + placeholder={t("dest.namePlaceholder")} />
- + + )} + {variantErr && ( +

{variantErr}

+ )} + + + )} +
)} -
+ + {/* What the platform asks for, with its provenance attached. + Shown under BOTH cards rather than only under the encode one: + an operator on passthrough needs to know their 4K source is + above what Kick will take just as much as one who is already + encoding — arguably more, since nothing is going to reshape it + for them. */} + {platformVideo && ( +
+ + + {guidance.find((g) => g.id === presetId)?.name ?? "This platform"} publishes: + + + {[ + platformVideo.width && platformVideo.height + ? `${platformVideo.width}×${platformVideo.height}` + : null, + platformVideo.fps ? `${platformVideo.fps} fps` : null, + platformVideo.kbpsMin + ? platformVideo.kbpsMax && platformVideo.kbpsMax !== platformVideo.kbpsMin + ? `${platformVideo.kbpsMin}–${platformVideo.kbpsMax} kbps` + : `${platformVideo.kbpsMin} kbps` + : null, + platformVideo.gopSeconds ? `${platformVideo.gopSeconds}s keyframes` : null, + ] + .filter(Boolean) + .join(" · ")} + + + {platformVideo.note && ( +

+ {platformVideo.note} +

+ )} + {/* The disclaimer this catalogue has always carried, applied to + numbers rather than to hostnames. A figure whose provenance + is not on screen is indistinguishable from a guess. */} +

+ Starting point, not a rule —{" "} + + published by the platform + + , read {platformVideo.checked}. Verify before you go live. +

+
+ )} + + {leaving && ( + + {leaveConsequence(leaving)} + + )} +
- +
- + @@ -1226,10 +1568,10 @@ export function DestinationDialog({ open, onOpenChange, destination, onSaved }: - Leave as it is on YouTube - Private - Unlisted - Public + {t("dest.leaveOnYouTube")} + {t("dest.private")} + {t("dest.unlisted")} + {t("dest.public")} @@ -1241,7 +1583,7 @@ export function DestinationDialog({ open, onOpenChange, destination, onSaved }:
- + @@ -1333,11 +1675,11 @@ export function DestinationDialog({ open, onOpenChange, destination, onSaved }: - Leave as it is on Facebook - Only me - Friends - Friends of friends - Public + {t("dest.leaveOnFacebook")} + {t("dest.onlyMe")} + {t("dest.friends")} + {t("dest.friendsOfFriends")} + {t("dest.public")} @@ -1494,7 +1836,7 @@ export function DestinationDialog({ open, onOpenChange, destination, onSaved }:
- +
- +
- These are a pair: FFmpeg applies the packet cap only once the queue - has grown past the byte threshold, so a threshold on its own does nothing and will - be refused. Raise them if a destination reports interleave errors — the audio - path here has variable latency, because loudness normalisation reads ahead. + {t("dest.muxQueuePair")}
-

Reconnecting

+

{t("dest.reconnecting")}

- +
- + + + + + + {title && ( +

+ {t(title)} +

+ )} +

{t(body)}

+
+ + ); +} + +/** A label with its hint attached, for the common case. + * + * Exists so the pairing is one element rather than a flex wrapper repeated at + * every field — several hundred times across the settings pages, which is + * several hundred chances to get the spacing subtly different. */ +export function LabelWithHint({ + label, + body, + title, + htmlFor, + className, +}: { + label: TranslationKey; + body: TranslationKey; + title?: TranslationKey; + htmlFor?: string; + className?: string; +}) { + const t = useT(); + return ( + + + + + ); +} diff --git a/ui/src/components/ui/popover.tsx b/ui/src/components/ui/popover.tsx new file mode 100644 index 00000000..e60258da --- /dev/null +++ b/ui/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +import * as React from "react"; +import * as PopoverPrimitive from "@radix-ui/react-popover"; +import { cn } from "@/lib/utils"; + +const Popover = PopoverPrimitive.Root; +const PopoverTrigger = PopoverPrimitive.Trigger; +const PopoverAnchor = PopoverPrimitive.Anchor; + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "start", sideOffset = 6, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; diff --git a/ui/src/hooks/useChatSearch.ts b/ui/src/hooks/useChatSearch.ts new file mode 100644 index 00000000..06671fdf --- /dev/null +++ b/ui/src/hooks/useChatSearch.ts @@ -0,0 +1,101 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { api } from "@/lib/api"; +import type { ChatMessage, ChatPlatform } from "@/lib/types"; + +/* Searching the retained scrollback. + * + * Server-side, always. The pane holds one session's messages and the whole + * point of the search box is to reach the comment that scrolled away — a + * client-side filter over what is already on screen answers a question the + * operator did not ask. + * + * Two things this hook is careful about, both of which make the difference + * between a search box and a frustrating one: + * + * - It debounces, so typing "moderator" is one request rather than nine. + * - It drops stale responses. Requests can land out of order, and a slow + * reply for "mod" arriving after the fast one for "moderator" would leave + * the box saying one thing and the list showing another. */ + +/** Long enough to swallow a burst of typing, short enough that the results feel + * attached to the keystrokes. */ +const DEBOUNCE_MS = 250; + +/** Below this a search matches so much that the result set is noise. */ +const MIN_QUERY = 2; + +export interface ChatSearchState { + query: string; + setQuery: (q: string) => void; + /** True once the query is long enough to have been searched for. Drives + * whether the UI shows results INSTEAD of the live timeline. */ + active: boolean; + results: ChatMessage[]; + loading: boolean; + error: string; + /** More matches exist beyond the limit. */ + truncated: boolean; + /** Why the history only goes back so far. Render it, including on an empty + * result — "nothing found" must not be read as "never said". */ + note: string; + clear: () => void; +} + +export function useChatSearch(platform?: ChatPlatform): ChatSearchState { + const [query, setQuery] = useState(""); + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [truncated, setTruncated] = useState(false); + const [note, setNote] = useState(""); + + const trimmed = query.trim(); + const active = trimmed.length >= MIN_QUERY; + + // Identifies the in-flight request. Anything that comes back with a stale + // token is discarded rather than rendered. + const seq = useRef(0); + + useEffect(() => { + if (!active) { + seq.current++; // cancel whatever is in flight + setResults([]); + setLoading(false); + setError(""); + setTruncated(false); + return; + } + + const token = ++seq.current; + setLoading(true); + const timer = setTimeout(() => { + api + .chatSearch({ q: trimmed, platform, limit: 200 }) + .then((r) => { + if (seq.current !== token) return; + setResults(r.messages ?? []); + setTruncated(Boolean(r.truncated)); + setNote(r.retentionNote ?? ""); + setError(""); + }) + .catch((err: unknown) => { + if (seq.current !== token) return; + setResults([]); + setError(err instanceof Error ? err.message : "Search failed."); + }) + .finally(() => { + if (seq.current === token) setLoading(false); + }); + }, DEBOUNCE_MS); + + return () => clearTimeout(timer); + }, [trimmed, active, platform]); + + const clear = useCallback(() => setQuery(""), []); + + return useMemo( + () => ({ query, setQuery, active, results, loading, error, truncated, note, clear }), + [query, active, results, loading, error, truncated, note, clear], + ); +} diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts index bede4295..58821427 100644 --- a/ui/src/lib/api.ts +++ b/ui/src/lib/api.ts @@ -7,6 +7,7 @@ import type { ChatModerationResult, ChatOverview, ChatPlatform, + ChatSearchResult, ChatSendResponse, ChatSettings, ChatUserCard, @@ -46,6 +47,7 @@ import type { RenditionBounds, RenditionDeleted, RenditionPreset, + PlatformPresetInfo, RenditionView, RoutingProfile, RoutingResult, @@ -348,6 +350,11 @@ export const api = { // A rendition is one shared video encode several destinations can select, so // N destinations wanting 1080p60 cost one encode rather than N. A destination // with no rendition is passthrough, which is the default and costs nothing. + /** The server's platform catalogue, including researched encoder guidance. + * Fetched rather than mirrored: the numbers carry a source and a date, and + * a second copy of them in the UI would drift silently. */ + platformPresets: () => + get<{ presets: PlatformPresetInfo[]; disclaimer: string }>("/platforms/presets"), listRenditions: () => get("/renditions"), getRendition: (id: number) => get(`/renditions/${id}`), createRendition: (r: Partial) => @@ -647,6 +654,18 @@ export const api = { "/chat/messages" + (qs ? `?${qs}` : ""), ); }, + /** Find a message again, by its text or by who said it. + * + * Server-side and against the database, not the pane: the timeline holds one + * session's worth of messages and "where did that comment go" is exactly the + * question it cannot answer. Bounded by the chat retention setting, which is + * why the response carries a note saying so. */ + chatSearch: (opts: { q: string; platform?: ChatPlatform; limit?: number }) => { + const p = new URLSearchParams({ q: opts.q }); + if (opts.platform) p.set("platform", opts.platform); + if (opts.limit) p.set("limit", String(opts.limit)); + return get(`/chat/search?${p.toString()}`); + }, /** Fan-out. Answers 200 even when every platform failed: the per-platform * verdicts are the answer, and a status code cannot say "Twitch took it and * YouTube did not". */ diff --git a/ui/src/lib/chat.ts b/ui/src/lib/chat.ts index 42f3f0c7..6fa77490 100644 --- a/ui/src/lib/chat.ts +++ b/ui/src/lib/chat.ts @@ -70,6 +70,20 @@ const ACCENT: Record< /** Unknown platforms fall back rather than crash: the server may know about a * platform this build does not. */ +/** Timeout choices. Seconds on the wire, always — the server converts for Kick, + * which counts in minutes. The list stops at a day because past that a timeout + * is a ban with extra steps, and Kick refuses beyond seven days anyway. + * + * Here rather than beside either consumer: the user card and the right-click + * menu both offer these durations, and two copies would drift into a menu + * whose "1 hour" is not the card's. */ +export const TIMEOUTS: { label: string; seconds: number }[] = [ + { label: "1 min", seconds: 60 }, + { label: "10 min", seconds: 600 }, + { label: "1 hour", seconds: 3600 }, + { label: "1 day", seconds: 86400 }, +]; + export function accentFor(p: ChatPlatform) { return ACCENT[p] ?? ACCENT.custom; } diff --git a/ui/src/lib/i18n.test.ts b/ui/src/lib/i18n.test.ts new file mode 100644 index 00000000..ca7bb4e2 --- /dev/null +++ b/ui/src/lib/i18n.test.ts @@ -0,0 +1,212 @@ +import { describe, expect, it } from "vitest"; + +import en from "./i18n/en.json"; +import de from "./i18n/de.json"; +import es from "./i18n/es.json"; +import fr from "./i18n/fr.json"; +import id from "./i18n/id.json"; +// `it` is vitest's test function, so Italian is aliased. +import itIT from "./i18n/it.json"; +import ja from "./i18n/ja.json"; +import ko from "./i18n/ko.json"; +import nl from "./i18n/nl.json"; +import pl from "./i18n/pl.json"; +import ptBR from "./i18n/pt-BR.json"; +import ru from "./i18n/ru.json"; +import tr from "./i18n/tr.json"; +import uk from "./i18n/uk.json"; +import zhHans from "./i18n/zh-Hans.json"; + +/* Mechanical guards on the catalogues. + * + * These check the properties a human reviewer cannot check by reading, and + * deliberately do NOT check whether a translation is good — no test can. What + * they catch is the class of mistake that turns a translated string into a + * broken one: + * + * - A translated PLACEHOLDER. `{count}` rendered as `{anzahl}` stops + * substituting and the operator sees a literal brace on screen. This is the + * single most common machine-translation failure and it is invisible until + * someone runs that locale. + * - A key English does not define. The type system already rejects this via + * NoStrayKeys, but only for a catalogue that is imported and type-checked; + * this asserts it against the JSON itself. + * - An empty string, which renders as nothing at all rather than falling back + * to English — worse than an untranslated string, because it is invisible. + * + * Incompleteness is NOT an error. lib/i18n.ts falls back to English per key, so + * a locale that is behind degrades to readable English; the coverage report at + * the end of this file makes it visible without failing the build. */ + +const CATALOGUES: Record> = { + de, es, fr, id, it: itIT, ja, ko, nl, pl, "pt-BR": ptBR, ru, tr, uk, "zh-Hans": zhHans, +}; + +const english = en as Record; + +/** The `{name}` tokens in a string, which lib/i18n.ts substitutes at render. */ +function placeholders(s: string): string[] { + return (s.match(/\{(\w+)\}/g) ?? []).sort(); +} + +describe("translation catalogues", () => { + it.each(Object.keys(CATALOGUES))("%s defines no key English lacks", (lang) => { + const stray = Object.keys(CATALOGUES[lang]).filter((k) => !(k in english)); + // A key renamed in en.json leaves a dead entry behind here, and the only + // symptom is a string that silently stops translating. + expect(stray, `${lang} has keys absent from en.json`).toEqual([]); + }); + + it.each(Object.keys(CATALOGUES))("%s preserves every placeholder", (lang) => { + const broken: string[] = []; + for (const [key, value] of Object.entries(CATALOGUES[lang])) { + const want = placeholders(english[key] ?? ""); + const got = placeholders(value); + if (want.join() !== got.join()) { + broken.push(`${key}: expected ${want.join(",") || "none"}, got ${got.join(",") || "none"}`); + } + } + expect(broken, `${lang} corrupted placeholders`).toEqual([]); + }); + + it.each(Object.keys(CATALOGUES))("%s has no empty strings", (lang) => { + const empty = Object.entries(CATALOGUES[lang]) + .filter(([, v]) => v.trim() === "") + .map(([k]) => k); + // An empty value beats the English fallback and renders nothing, so a label + // simply vanishes. + expect(empty, `${lang} has empty values that would render as blank`).toEqual([]); + }); + + /* Catches a source string half-translated in place. + * + * Editing a long paragraph in a non-Latin script, it is genuinely easy to + * leave an English connective behind — "バリエーションalso一緒に" reads as a + * typo to anyone who does not know the source, and no reviewer of the target + * language would recognise it as a leftover. Restricted to the scripts where + * a stray Latin word is unambiguous; a Latin-script locale legitimately + * contains these letters. Technical terms stay in English by design, so the + * check looks only for English FUNCTION words, which never survive a real + * translation. */ + const NON_LATIN = ["ru", "uk", "ja", "ko", "zh-Hans"]; + const FUNCTION_WORDS = new Set([ + "also", "and", "the", "with", "that", "this", "from", "when", "which", + "then", "but", "for", "not", "are", "its", "you", "your", "have", "will", + ]); + + it.each(NON_LATIN)("%s contains no leftover English function words", (lang) => { + const leaked: string[] = []; + for (const [key, value] of Object.entries(CATALOGUES[lang])) { + for (const word of value.match(/[A-Za-z]{3,}/g) ?? []) { + if (FUNCTION_WORDS.has(word.toLowerCase())) leaked.push(`${key}: ${word}`); + } + } + expect(leaked, `${lang} has untranslated fragments`).toEqual([]); + }); + + /* Catches text from the WRONG target language. + * + * Writing fifteen locales in sequence, a character can carry over from the + * one before — a Hangul syllable landing in the Japanese catalogue reads as + * a rare kanji to anyone who does not know Korean, and the Latin-word check + * above cannot see it because nothing Latin is involved. Each script is only + * asserted absent where it could never legitimately appear. */ + const FOREIGN_SCRIPTS: Record = { + ja: [["Hangul", /[가-힯]/], ["Cyrillic", /[Ѐ-ӿ]/]], + ko: [["Kana", /[぀-ヿ]/], ["Cyrillic", /[Ѐ-ӿ]/]], + "zh-Hans": [["Hangul", /[가-힯]/], ["Kana", /[぀-ヿ]/]], + ru: [["Hangul", /[가-힯]/], ["CJK", /[一-鿿]/]], + uk: [["Hangul", /[가-힯]/], ["CJK", /[一-鿿]/]], + }; + + it.each(Object.keys(FOREIGN_SCRIPTS))("%s contains no foreign script", (lang) => { + const found: string[] = []; + for (const [key, value] of Object.entries(CATALOGUES[lang])) { + for (const [script, pattern] of FOREIGN_SCRIPTS[lang]) { + const hit = pattern.exec(value); + if (hit) found.push(`${key}: ${script} ${JSON.stringify(hit[0])}`); + } + } + expect(found, `${lang} contains characters from another language`).toEqual([]); + }); + + /* Catches an English word spliced into the middle of a non-Latin word. + * + * The real defect this is named for: 応answerしない, where "answer" landed + * inside 応答しない. The function-word check above cannot see it — "answer" + * is a content word — and no amount of proofreading the English side would + * reveal it either. + * + * KOREAN IS EXCLUDED, and that is not an oversight. Korean particles attach + * directly to Latin words as a matter of correct orthography — URL이, + * polyemesis가, FLAC은 are all right — so this check produces nothing but + * false positives there. The other four scripts separate Latin runs with + * spaces or punctuation, so a glued one is genuinely a typo. */ + const GLUE_CHECKED = ["ja", "zh-Hans", "ru", "uk"]; + const GLUED = /(?:[぀-ヿ一-鿿Ѐ-ӿ][A-Za-z]{2,})|(?:[A-Za-z]{2,}[぀-ヿ一-鿿Ѐ-ӿ])/; + + it.each(GLUE_CHECKED)("%s has no Latin word glued into a native word", (lang) => { + const glued: string[] = []; + for (const [key, value] of Object.entries(CATALOGUES[lang])) { + const hit = GLUED.exec(value); + if (hit) glued.push(`${key}: ${JSON.stringify(hit[0])}`); + } + expect(glued, `${lang} has a spliced Latin word`).toEqual([]); + }); + + it("English defines no empty strings either", () => { + const empty = Object.entries(english).filter(([, v]) => v.trim() === "").map(([k]) => k); + expect(empty).toEqual([]); + }); + + // Not an assertion about completeness — just makes the gap visible, so a + // locale quietly falling behind is noticed rather than discovered by a user. + it("reports coverage per locale", () => { + const total = Object.keys(english).length; + const rows = Object.entries(CATALOGUES) + .map(([lang, cat]) => { + const have = Object.keys(cat).filter((k) => k in english).length; + return { lang, have, pct: Math.round((have / total) * 100) }; + }) + .sort((a, b) => a.have - b.have); + // eslint-disable-next-line no-console + console.log( + `\n i18n coverage (${total} keys):\n` + + rows.map((r) => ` ${r.lang.padEnd(8)} ${String(r.have).padStart(4)} ${r.pct}%`).join("\n"), + ); + expect(total).toBeGreaterThan(0); + }); + + // A RATCHET, not a completeness rule. + // + // The reporting test above is deliberately non-enforcing, and its reasoning + // holds: lib/i18n.ts falls back per key, so a locale that is behind degrades + // to readable English rather than breaking. Nothing here changes that. + // + // What it did not survive is the case it was written for. Every catalogue was + // at 100%, then 45 keys were added for the destination dialog and translated + // nowhere -- and the whole suite stayed green while fourteen locales silently + // rendered English. "Visible in a console.log nobody reads during a passing + // run" is not visible. + // + // So the level is pinned instead. Falling behind is still allowed in the + // sense that it does not break the app; it is just no longer something that + // can happen without anyone agreeing to it. Adding a key means translating it + // or deliberately lowering this floor, which is a reviewable act rather than + // an oversight. + it("every locale stays fully translated", () => { + const behind = Object.entries(CATALOGUES) + .map(([lang, cat]) => ({ + lang, + missing: Object.keys(english).filter((k) => !(k in cat) || cat[k].trim() === ""), + })) + .filter((r) => r.missing.length > 0) + .map((r) => `${r.lang}: ${r.missing.length} missing (${r.missing.slice(0, 5).join(", ")}…)`); + + expect( + behind, + "a key was added to en.json without translations. Translate it in every " + + "locale, or lower this floor on purpose — but do not let it happen silently.", + ).toEqual([]); + }); +}); diff --git a/ui/src/lib/i18n/de.json b/ui/src/lib/i18n/de.json index 5c69bd6e..667323fa 100644 --- a/ui/src/lib/i18n/de.json +++ b/ui/src/lib/i18n/de.json @@ -1,26 +1,227 @@ { - "nav.dashboard": "Übersicht", - "nav.sources": "Quellen", - "nav.meters": "Audiopegel", - "nav.routing": "Routing", - "nav.renditions": "Varianten", - "nav.playout": "Playout", - "nav.library": "Bibliothek", - "nav.clips": "Clips", - "nav.chat": "Chat", - "nav.jobs": "Aufgaben", - "nav.automation": "Automatisierung", - "nav.recordings": "Aufnahmen", - "nav.monitoring": "Überwachung", - "nav.settings": "Einstellungen", - "chrome.toggleNav": "Navigation umschalten", + "auth.accountCreated": "Administratorkonto erstellt.", + "auth.confirmPassword": "Kennwort bestätigen", + "auth.createAccount": "Konto erstellen", + "auth.firstRun": "Ersteinrichtung", + "auth.firstRunDesc": "Wählen Sie die Administrator-Zugangsdaten für diesen Server. Es gibt keine Kontowiederherstellung, bewahren Sie sie also sicher auf.", + "auth.password": "Kennwort", + "auth.passwordMismatch": "Die Kennwörter stimmen nicht überein.", + "auth.signIn": "Anmelden", + "auth.signInDesc": "Geben Sie Ihre Administrator-Zugangsdaten ein.", + "auth.somethingWentWrong": "Etwas ist schiefgegangen.", + "auth.tokenCreated": "API-Token erstellt", + "auth.tokenRevoked": "API-Token widerrufen", + "auth.username": "Benutzername", + "auto.action": "Aktion", + "auto.adminPasswordChanged": "Administratorkennwort geändert", + "auto.chosenWeekdays": "Ausgewählte Wochentage", + "auto.coalesced": "Zusammengefasst", + "auto.coalescingWindow": "Zusammenfassungsfenster (Sekunden)", + "auto.couldNotChangeTheRule": "Die Regel konnte nicht geändert werden.", + "auto.couldNotChangeTheSchedule": "Der Zeitplan konnte nicht geändert werden.", + "auto.couldNotDeleteTheRule": "Die Regel konnte nicht gelöscht werden.", + "auto.couldNotDeleteTheSchedule": "Der Zeitplan konnte nicht gelöscht werden.", + "auto.couldNotLoadAutomationSettings": "Die Automatisierungseinstellungen konnten nicht geladen werden.", + "auto.couldNotSaveTheRule": "Die Regel konnte nicht gespeichert werden.", + "auto.couldNotSaveTheSchedule": "Der Zeitplan konnte nicht gespeichert werden.", + "auto.createRule": "Regel anlegen", + "auto.createSchedule": "Zeitplan anlegen", + "auto.days": "Tage", + "auto.delete": "Löschen", + "auto.deleteRule": "Regel löschen", + "auto.deleteRuleDesc": "Die Regel löst nicht mehr aus. Ihr Webhook-Endpunkt bleibt unberührt, und Sie können die Regel neu anlegen.", + "auto.deleteRuleTitle": "Diese Alarmregel löschen?", + "auto.deleteSchedule": "Zeitplan löschen", + "auto.deleteScheduleDesc": "Der Zeitplan läuft nicht mehr. Was er bereits gestartet hat, bleibt davon unberührt.", + "auto.deleteScheduleTitle": "Diesen Zeitplan löschen?", + "auto.delivery": "Zustellung", + "auto.destinationFallingBehindRealtime": "Ziel fällt hinter die Echtzeit zurück", + "auto.destinationKeepingUpAgain": "Ziel hält wieder Schritt", + "auto.destinationRecovered": "Ziel wiederhergestellt", + "auto.destinationWentDown": "Ziel ist ausgefallen", + "auto.destinations": "Ziele", + "auto.dropped": "Verworfen", + "auto.edit": "Bearbeiten", + "auto.editRule": "Alarmregel bearbeiten", + "auto.editSchedule": "Zeitplan bearbeiten", + "auto.endpoint": "Endpunkt", + "auto.evAudioClipping": "Audioübersteuerung", + "auto.evClipCaptured": "Clip aufgenommen", + "auto.evDiskLow": "Speicherplatz wird knapp", + "auto.evDiskRecovered": "Speicherplatz wieder verfügbar", + "auto.evIngestLost": "Eingang verloren", + "auto.evIngestRecovered": "Eingang wiederhergestellt", + "auto.evSettingsChanged": "Einstellungen geändert", + "auto.evSignedIn": "Angemeldet", + "auto.events": "Ereignisse", + "auto.everyDay": "Täglich", + "auto.failed": "Fehlgeschlagen", + "auto.failoverSwitchedSource": "Ausfallsicherung hat die Quelle gewechselt", + "auto.fired": "ausgelöst", + "auto.firesAt": "Löst aus um", + "auto.lastDelivery": "Letzte Zustellung", + "auto.lastSweep": "Letzter Durchlauf", + "auto.latenessAllowance": "Verspätungstoleranz (Sekunden)", + "auto.loudnessBackInCompliance": "Lautheit wieder konform", + "auto.loudnessOutOfCompliance": "Lautheit nicht konform", + "auto.minGap": "Mindestabstand zwischen Nachrichten (Sekunden)", + "auto.minSeverity": "Mindestschweregrad", + "auto.missedNote": "Ein Termin, der verpasst wurde, weil der Server ausgefallen war, wird als erledigt markiert und übersprungen, statt verspätet auszulösen — ein Stream, der sich vier Stunden zu spät selbst startet, ist schlimmer als einer, der nie gestartet ist.", + "auto.name": "Name", + "auto.newRule": "Neue Alarmregel", + "auto.newSchedule": "Neuer Zeitplan", + "auto.next": "Nächste", + "auto.nothingFired": "Noch nichts ausgelöst. Der Planer läuft alle 20 Sekunden durch.", + "auto.once": "Einmalig", + "auto.payloadFormat": "Payload-Format", + "auto.pending": "Ausstehend", + "auto.repeatedFailedSignIns": "Wiederholt fehlgeschlagene Anmeldungen", + "auto.repeats": "Wiederholung", + "auto.retries": "Wiederholungen", + "auto.rule": "Regel", + "auto.ruleCreated": "Alarmregel angelegt. Senden Sie ihr einen Test.", + "auto.ruleDeleted": "Alarmregel gelöscht.", + "auto.rulePlaceholder": "Discord — Sendungsalarme", + "auto.ruleSaved": "Alarmregel gespeichert.", + "auto.saveRule": "Regel speichern", + "auto.saveSchedule": "Zeitplan speichern", + "auto.schedule": "Zeitplan", + "auto.scheduleCreated": "Zeitplan angelegt.", + "auto.scheduleDeleted": "Zeitplan gelöscht.", + "auto.schedulePlaceholder": "Sendung unter der Woche", + "auto.scheduleSaved": "Zeitplan gespeichert.", + "auto.schedules": "Zeitpläne", + "auto.secretMasked": "Maskiert dargestellt, weil das Geheimnis im Pfad steht. Lassen Sie ihn unverändert, um den aktuellen Endpunkt zu behalten, oder fügen Sie eine neue URL ein, um ihn zu ersetzen.", + "auto.sendTest": "Testalarm senden", + "auto.sendTestTitle": "Jetzt einen Testalarm senden", + "auto.sent": "Gesendet", + "auto.skipped": "übersprungen", + "auto.startDestinations": "Ziele starten", + "auto.startPlaylist": "Die Playlist starten", + "auto.stopDestinations": "Ziele stoppen", + "auto.stopPlaylist": "Die Playlist stoppen", + "auto.subscribedTo": "Abonniert", + "auto.subtitle": "Webhooks, die Ihnen melden, dass etwas kaputtgegangen ist, und Zeitpläne, die die Sendung ohne Sie fahren.", + "auto.theEndpointDidNotAccept": "Der Endpunkt hat die Testnachricht nicht angenommen.", + "auto.time": "Zeit", + "auto.timeZone": "Zeitzone", + "auto.title": "Automatisierung", + "auto.tzPlaceholder": "UTC", + "auto.webhookHint": "Discord und Slack stellen beide einen unter den Kanaleinstellungen bereit.", + "auto.webhookPlaceholder": "https://discord.com/api/webhooks/…", + "auto.webhookRules": "Webhook-Regeln", + "auto.webhookUrl": "Webhook-URL", + "auto.weekFri": "Fr", + "auto.weekMon": "Mo", + "auto.weekSat": "Sa", + "auto.weekSun": "So", + "auto.weekThu": "Do", + "auto.weekTue": "Di", + "auto.weekWed": "Mi", + "auto.when": "Wann", + "chatpage.allFiltered": "Jede Plattform mit Nachrichten ist ausgefiltert.", + "chatpage.couldNotDeleteThatMessage": "Diese Nachricht konnte nicht gelöscht werden.", + "chatpage.deleteNote": "Löschen entfernt die Nachricht auf der Plattform, von der sie stammt. Eine Plattform, auf der polyemesis nicht löschen kann, sagt das und nennt, wo es stattdessen geht — die Aktion wird nie auf Verdacht ausgeblendet.", + "chatpage.droppedTitle": "Live angezeigt, aber die Speicherung kam nicht hinterher und der Verlauf hat sie verloren.", + "chatpage.dupTitle": "Dieselbe Nachricht zweimal gesehen — unser eigenes Echo oder eine Wiederholung der Plattform.", + "chatpage.noAccount": "Chat läuft, aber es ist kein Plattformkonto verbunden. Verbinden Sie eines unter Einstellungen → Plattform-Zugangsdaten.", + "chatpage.notRunning": "Auf diesem Server läuft kein Chat. Der Verlauf unter der Zeitleiste ist das, was zuvor gespeichert wurde, und das Sendefeld bleibt deaktiviert, bis eine Plattform verbunden ist.", + "chatpage.received": "Empfangen", + "chatpage.reload": "Neu laden", + "chatpage.stored": "Gespeichert", + "chatpage.storedTitle": "Nichts ist verbunden; dies ist der gespeicherte Verlauf.", + "chatpage.subtitle": "Jede Plattform in einer Zeitleiste, mit einem Sendefeld.", + "chatpage.timeline": "Zeitleiste", + "chatpage.title": "Chat", "chrome.ingest": "Ingest", + "chrome.language": "Sprache", "chrome.liveCount": "{count} live", + "chrome.signOut": "Abmelden", "chrome.socketOffline": "offline", "chrome.socketOfflineHint": "Live-Aktualisierung getrennt, Verbindung wird wiederhergestellt", - "chrome.signOut": "Abmelden", - "chrome.language": "Sprache", + "chrome.toggleNav": "Navigation umschalten", + "clipedit.back33": "33 ms zurück", + "clipedit.clickLine": "Klicken Sie auf eine Zeile, um dorthin zu springen. Markieren Sie mehrere, und In- und Out-Punkt wandern zu den hervorgehobenen Wörtern.", + "clipedit.clipThisLine": "Clip aus dieser Zeile", + "clipedit.clipThisLineTitle": "In- und Out-Punkt um diese Zeile setzen", + "clipedit.copied": "Kopiert", + "clipedit.copiesEveryPacketSecondsBit": "Kopiert jedes Paket. Sekundenschnell, bitgenau, und der Anfang rastet auf den nächstgelegenen Keyframe zurück.", + "clipedit.couldNotOpenThatRecording": "Diese Aufnahme konnte nicht geöffnet werden.", + "clipedit.couldNotQueueTheExport": "Der Export konnte nicht eingereiht werden.", + "clipedit.cut": "Schnitt", + "clipedit.downloadClip": "Den Clip herunterladen", + "clipedit.exportQueued": "Export eingereiht. Er läuft, sobald die Maschine die Rechenzeit erübrigen kann.", + "clipedit.exports": "Exporte", + "clipedit.fast": "Schnell", + "clipedit.filterTranscript": "Das Transkript filtern", + "clipedit.findSaid": "Finden, was gesagt wurde", + "clipedit.forward33": "33 ms vor", + "clipedit.in": "In", + "clipedit.keepAllTracks": "Jede Audiospur behalten", + "clipedit.keepsEveryAudioTrackIn": "Behält jede Audiospur in jedem Codec. Das, was der Rekorder schreibt.", + "clipedit.markIn": "I — In-Punkt an der Abspielposition setzen", + "clipedit.markOut": "O — Out-Punkt an der Abspielposition setzen", + "clipedit.name": "Name", + "clipedit.noKeyframes": "Die Keyframe-Positionen konnten nicht gelesen werden, niemand kann also sagen, wo dieser Schnitt wirklich beginnt. Er funktioniert trotzdem — FFmpegs eigene Suche landet auf einem Keyframe —, aber der Anfang kann bis zu einen GOP zu früh liegen, und diese Seite kann Ihnen nicht zeigen, um wie viel.", + "clipedit.noTranscript": "Für diese Aufnahme wurde noch nichts transkribiert. Jedes Mikrofon wurde auf einer eigenen Spur aufgenommen; die Transkription liefert Ihnen daher Sprechernamen ohne Diarisierungsmodell — und danach können Sie nach dem Gesagten schneiden.", + "clipedit.nothingExported": "Noch nichts exportiert.", + "clipedit.out": "Out", + "clipedit.pickAtLeastOneAudio": "Wählen Sie mindestens eine Audiospur oder nehmen Sie alle.", + "clipedit.playFaster": "L — abspielen, erneut für schneller", + "clipedit.precise": "Genau", + "clipedit.queueNote": "Der Export wird eingereiht und nicht hier ausgeführt: Er tritt hinter den Live-Stream zurück, ein Schnitt mitten in der Sendung wartet also, statt dem Stream die Rechenzeit zu stehlen, die er braucht.", + "clipedit.reEncodesOnlyTheLeading": "Kodiert nur den führenden Teil-GOP neu, damit der Anfang exakt ist. Der Rest wird weiterhin kopiert.", + "clipedit.reencoded": "Neu kodiert", + "clipedit.shortcuts": "J / K / L steuern die Wiedergabe, I und O setzen In- und Out-Punkt, Pfeiltasten verschieben um 33 ms und Umschalt-Pfeiltasten um eine Sekunde. Der Feinschritt entspricht einem Bild bei 30 fps; die tatsächliche Bildrate dieser Aufnahme ist nicht indexiert, deshalb wird der Schritt in Millisekunden benannt statt geraten.", + "clipedit.shuttleBack": "J — rückwärts spulen", + "clipedit.startMoved": "Anfang verschoben", + "clipedit.starts": "Beginnt", + "clipedit.stop": "K — stoppen", + "clipedit.thatCutCouldNotBe": "Dieser Schnitt konnte nicht geplant werden.", + "clipedit.thatIsNotARecording": "Das ist keine Aufnahme-ID.", + "clipedit.thatRecordingIsNotAvailable": "Diese Aufnahme ist nicht verfügbar.", + "clipedit.title": "Clip-Editor", + "clipedit.whatASocialPlatformWill": "Das, was eine soziale Plattform akzeptiert. Weniger Codecs überleben es.", + "clipedit.zoomIn": "Vergrößern", + "clipedit.zoomOut": "Verkleinern", + "clips.bitrate": "Bitrate", + "clips.buffer": "Puffer", + "clips.bufferLiveNote": "Der Pufferschalter ist ein Live-Umschalter, keine gespeicherte Einstellung: Nach einem Neustart ist er wieder aus.", + "clips.captured": "Aufgenommen", + "clips.ceiling": "Obergrenze", + "clips.clipsKept": "Behaltene Clips", + "clips.couldNotCaptureAClip": "Es konnte kein Clip aufgenommen werden.", + "clips.couldNotChangeTheClip": "Der Clip-Puffer konnte nicht geändert werden.", + "clips.couldNotDeleteTheClip": "Der Clip konnte nicht gelöscht werden.", + "clips.couldNotLoadClips": "Clips konnten nicht geladen werden.", + "clips.covers": "Umfasst", + "clips.customLength": "Eigene Cliplänge in Sekunden", + "clips.delete": "Löschen", + "clips.deleteDescription": "Ein Clip ist meist genau das, was Sie behalten wollten. Dies entfernt die Datei; die Aufnahme, aus der er stammt, bleibt unberührt.", + "clips.deleteTitle": "Diesen Clip löschen?", + "clips.deleted": "Clip gelöscht.", + "clips.download": "Herunterladen", + "clips.everythingHeld": "Alles, was der Puffer hält", + "clips.file": "Datei", + "clips.held": "Vorgehalten", + "clips.inMemory": "Im Arbeitsspeicher", + "clips.keepBuffer": "Rollenden Puffer vorhalten", + "clips.keyframes": "Keyframes", + "clips.length": "Länge", + "clips.none": "keins", + "clips.off": "aus", + "clips.onDisk": "Auf der Festplatte", + "clips.seen": "gesehen", + "clips.settings": "Einstellungen", + "clips.size": "Größe", + "clips.subtitle": "Ein rollender Puffer dessen, was gerade hinausging. Nehmen Sie es im Nachhinein auf — nichts muss vorher bereitgestellt werden.", + "clips.theClipDoesNotStart": "Der Clip beginnt nicht auf einem Keyframe.", + "clips.title": "Clips", + "clips.waitingForStream": "wartet auf Stream", + "clips.window": "Fenster", + "clips.windowSeconds": "Fenster (Sekunden)", "common.add": "Hinzufügen", + "common.apply": "Übernehmen", "common.cancel": "Abbrechen", "common.close": "Schließen", "common.confirm": "Bestätigen", @@ -28,110 +229,921 @@ "common.copy": "Kopieren", "common.delete": "Löschen", "common.disabled": "Deaktiviert", + "common.discard": "Verwerfen", "common.edit": "Bearbeiten", "common.enabled": "Aktiviert", "common.loading": "Wird geladen…", + "common.moreInfo": "Weitere Informationen", "common.refresh": "Aktualisieren", "common.restart": "Neu starten", "common.retry": "Erneut versuchen", "common.save": "Speichern", "common.start": "Starten", "common.stop": "Stoppen", - "state.live": "Live", - "state.reconnecting": "Verbindet neu", - "state.starting": "Startet", - "state.failed": "Fehlgeschlagen", - "state.stopped": "Gestoppt", - "state.offline": "Offline", - "hooks.title": "Lifecycle-Webhooks", - "hooks.new": "Neuer Webhook", - "hooks.empty": "Keine Webhooks. Legen Sie einen an, und polyemesis sendet einen signierten JSON-Body per POST, sobald der Stream startet oder stoppt oder ein Ziel online oder offline geht — eine Zustellung pro Übergang, in der richtigen Reihenfolge, damit ein Skript darauf reagieren kann.", - "hooks.colWebhook": "Webhook", - "hooks.colEndpoint": "Endpunkt", - "hooks.colTriggers": "Abonniert", - "hooks.colEnabled": "An", - "hooks.signed": "signiert", - "hooks.unsigned": "UNSIGNIERT", + "dash.audioTracks": "Audiospuren", + "dash.bitrate": "Bitrate", + "dash.broadcastSettings": "Sendungseinstellungen", + "dash.categoryPlaceholder": "Musik", + "dash.clipboardUnavailable": "Die Zwischenablage ist auf diesem Origin nicht verfügbar.", + "dash.connectPrompt": "Verbinden Sie unter Einstellungen → Plattform-Zugangsdaten ein YouTube-, Twitch-, Facebook- oder Kick-Konto, um Titel, Beschreibung und Kategorie Ihres Streams auf allen Plattformen gleichzeitig zu setzen. Jede akzeptiert eine andere Teilmenge — der Editor nennt welche und überträgt, was er kann.", + "dash.copyIngestUrl": "Eingangs-URL kopieren", + "dash.couldNotLoadTheDestination": "Das Ziel konnte nicht geladen werden.", + "dash.couldNotPushTheMetadata": "Die Metadaten konnten nicht übertragen werden.", + "dash.couldNotReorderTheDestinations": "Die Ziele konnten nicht neu geordnet werden.", + "dash.deleteDest": "Ziel löschen", + "dash.deleteDestDescription": "Dies entfernt das Ziel und sein Routing-Profil. Die Plattform bleibt unberührt, und Sie können es wieder hinzufügen — das dafür eingerichtete Audiorouting ist jedoch weg.", + "dash.descriptionPlaceholder": "Worum es in diesem Stream geht.", + "dash.destDeleted": "Ziel gelöscht.", + "dash.disabled": "deaktiviert", + "dash.everyConnectedAccount": "jedes verbundene Konto", + "dash.idle": "im Leerlauf", + "dash.ingestUrlCopied": "Eingangs-URL kopiert.", + "dash.keyRefreshed": "Streamschlüssel von der Plattform aktualisiert.", + "dash.leaveUnchanged": "Unverändert lassen", + "dash.leaveUnchangedNote": "„Unverändert lassen“ sendet überhaupt nichts, die Plattform behält also, was sie hat. Die Wahl von An oder Aus schreibt diesen Wert.", + "dash.metaCategory": "Kategorie", + "dash.metaDescription": "Beschreibung", + "dash.metaTags": "Tags", + "dash.metaTitle": "Titel", + "dash.noDestinations": "Noch keine Ziele. Fügen Sie eines hinzu und wählen Sie dann, welche Audiospuren es erhält.", + "dash.off": "Aus", + "dash.pipeline": "Verarbeitungskette", + "dash.pushToPlatforms": "An die Plattformen übertragen", + "dash.pushing": "Wird übertragen…", + "dash.reconnects": "Neuverbindungen", + "dash.relaySubscribers": "Relay-Abonnenten", + "dash.renditions": "Varianten", + "dash.scheduledStart": "Geplanter Start", + "dash.subtitle": "Einmal empfangen, mit gemeinsamem Video und Audio pro Ziel verteilen.", + "dash.tagsPlaceholder": "live, house, dj set", + "dash.theConnectedAccount": "das verbundene Konto", + "dash.title": "Übersicht", + "dash.titlePlaceholder": "Freitagabend-Set", + "dash.uptime": "Laufzeit", + "dash.youtubeStopsAcceptingTheseOnce": "YouTube nimmt diese nicht mehr an, sobald eine Übertragung „created“ oder „ready“ verlässt.", + "dest.audience": "Zielgruppe", + "dest.audioBitrate": "Audiobitrate", + "dest.audioCodec": "Audio-Codec", + "dest.channels": "Kanäle", + "dest.compliance": "Compliance", + "dest.connectedAccount": "Verbundenes Konto", + "dest.contentLabels": "Inhaltskennzeichnungen", + "dest.copySource": "Quellvideo kopieren", + "dest.createVariant": "Diese Variante erstellen", + "dest.customiseSummary": "Für dieses Ziel anpassen", + "dest.fbBackupCost": "Verdoppelt die Upload-Bandbreite dieses Ziels.", + "dest.fbBackupHelp": "Sendet eine zweite Kopie dieses Streams an Facebooks Backup-Ingest, damit eine abgebrochene Verbindung nicht die Übertragung abbricht.", + "dest.fbBackupLabel": "Backup-Feed veröffentlichen", + "dest.fbBackupReconnect": "Beim Einschalten verbindet sich der Stream einmal neu – aktivieren Sie es also, bevor Sie live gehen.", + "dest.fbBoxIntro": "Wird mit demselben Erstellungsaufruf wie die Zielgruppe gesendet, es gilt also dieselbe Regel: einmalig angewendet, wenn die Übertragung startet, und vollständig unangetastet gelassen, wenn leer.", + "dest.fbBoxTitle": "Crossposting und Spendenbutton für Facebook", + "dest.fbCrosspostAdd": "Seite hinzufügen", + "dest.fbCrosspostAlsoPost": "Auch posten", + "dest.fbCrosspostHelp": "Die numerische Seiten-ID aus Facebooks eigener Konsole (Seiteneinstellungen oder graph.facebook.com/me/accounts, während Sie als Seite angemeldet sind) – hier gibt es keine Suche, fügen Sie also die ID ein und nicht einen Namen oder eine URL. „Auch posten“ veröffentlicht als diese Seite, statt die Übertragung nur mit ihr zu teilen; ausgeschaltet passiert nur das leisere Teilen.", + "dest.fbCrosspostLabel": "Crossposting auf Seiten", + "dest.fbCrosspostPageId": "Seiten-ID", + "dest.fbCrosspostRemove": "Diese Seite entfernen", + "dest.fbDonateHelp": "Ebenfalls eine undurchsichtige ID aus Facebooks Spendenaktions-Konsole, kein Name einer Organisation zum Suchen. Leer lassen, um gar keinen Spendenbutton anzuhängen.", + "dest.fbDonateLabel": "Spenden-Button: Organisations-ID", + "dest.fbDonatePlaceholder": "Organisations-ID", + "dest.fillFromPlatform": "Mit den Angaben dieser Plattform füllen", + "dest.friends": "Freunde", + "dest.friendsOfFriends": "Freunde von Freunden", + "dest.giveUpAfter": "Aufgeben nach (Versuchen)", + "dest.leaveOnFacebook": "Auf Facebook unverändert lassen", + "dest.leaveOnYouTube": "Auf YouTube unverändert lassen", + "dest.madeForKids": "Für Kinder gemacht (COPPA)", + "dest.muxQueue": "Muxing-Warteschlange (Pakete)", + "dest.muxQueuePair": "Diese beiden gehören zusammen: FFmpeg wendet die Paketgrenze erst an, wenn die Warteschlange den Byte-Schwellwert überschritten hat. Ein Schwellwert allein bewirkt daher nichts und wird abgelehnt. Erhöhe beide, wenn ein Ziel Interleave-Fehler meldet — der Audiopfad hier hat variable Latenz, weil die Lautheitsnormalisierung vorausliest.", + "dest.name": "Name", + "dest.namePlaceholder": "YouTube — Hauptkanal", + "dest.notLinked": "Nicht verknüpft", + "dest.onlyMe": "Nur ich", + "dest.platform": "Plattform", + "dest.private": "Privat", + "dest.public": "Öffentlich", + "dest.reconnecting": "Wiederverbinden", + "dest.retryAfter": "Erneut versuchen nach (Sekunden)", + "dest.sharedEncodeAria": "Gemeinsame Videokodierung", + "dest.socketTimeout": "Socket-Timeout (Sekunden)", + "dest.streamKeyLabel": "Stream-Schlüssel", + "dest.transport": "Transport", + "dest.transportFile": "Lokale Datei", + "dest.transportRtmp": "RTMP / RTMPS", + "dest.unlisted": "Nicht gelistet", + "dest.urlFromDashboard": "URL aus dem Dashboard", + "dest.useSharedEncode": "Gemeinsame Videokodierung verwenden", + "dest.varBitrate": "Videobitrate (kbps)", + "dest.varFps": "Bildrate", + "dest.varHeight": "Höhe", + "dest.varSourceHint": "Quelle", + "dest.varWidth": "Breite", + "dest.variantWarning": "Startet eine zweite Kodierung aus deiner Quelle, ausgehend von „{name}“. Das ist keine kostenlose Variante und ändert nichts daran, was die anderen Ziele dieser Kodierung erhalten. Lass ein Feld leer, um den Wert der Quelle zu behalten.", + "dest.videoTreatment": "Videoverarbeitung", + "dest.visibility": "Sichtbarkeit", + "hooks.allTriggers": "alles", "hooks.attempts": "{count} Versuche", + "hooks.attemptsLabel": "Versuche", "hooks.attemptsOne": "{count} Versuch", - "hooks.timeout": "{seconds}s Zeitlimit", - "hooks.allTriggers": "alles", - "hooks.triggerCount": "{count} Auslöser", - "hooks.triggerCountOne": "{count} Auslöser", - "hooks.enableAria": "{name} aktivieren", + "hooks.colEnabled": "An", + "hooks.colEndpoint": "Endpunkt", + "hooks.colTriggers": "Abonniert", + "hooks.colWebhook": "Webhook", + "hooks.create": "Anlegen", + "hooks.created": "Webhook angelegt.", + "hooks.deleteBody": "Er stellt sofort nichts mehr zu. Der Endpunkt bleibt unberührt, und Sie können den Hook neu anlegen — sein Signaturschlüssel lässt sich jedoch nicht wiederherstellen.", + "hooks.deleteConfirm": "Webhook löschen", + "hooks.deleteFailed": "Der Hook konnte nicht gelöscht werden.", + "hooks.deleteTitle": "Diesen Webhook löschen?", + "hooks.deleted": "Webhook gelöscht.", "hooks.deliveries": "Letzte Zustellungen", - "hooks.sendTest": "Testzustellung senden", - "hooks.sendTestTitle": "Jetzt eine Testzustellung senden", - "hooks.secretTitle": "Signaturschlüssel — jetzt kopieren, er kann nicht erneut angezeigt werden", - "hooks.secretCopied": "Ich habe ihn kopiert", - "hooks.httpStatus": "HTTP {status}", - "hooks.noResponse": "keine Antwort", + "hooks.deliveriesFailed": "Die letzten Zustellungen konnten nicht gelesen werden.", + "hooks.deliveryTitle": "Zustellung", + "hooks.dialogIntro": "Ein signierter POST pro Übergang, in der richtigen Reihenfolge. Anders als bei einer Warnung wird nichts zusammengefasst oder entprellt — ein Skript erhält jede Flanke.", "hooks.durationMs": "{ms} ms", + "hooks.editTitle": "Webhook bearbeiten", + "hooks.empty": "Keine Webhooks. Legen Sie einen an, und polyemesis sendet einen signierten JSON-Body per POST, sobald der Stream startet oder stoppt oder ein Ziel online oder offline geht — eine Zustellung pro Übergang, in der richtigen Reihenfolge, damit ein Skript darauf reagieren kann.", + "hooks.enableAria": "{name} aktivieren", + "hooks.httpStatus": "HTTP {status}", + "hooks.loadFailed": "Webhooks konnten nicht geladen werden.", + "hooks.name": "Name", + "hooks.new": "Neuer Webhook", + "hooks.newTitle": "Neuer Webhook", "hooks.noDeliveries": "Noch nichts zugestellt. Zustellungen erscheinen hier, sobald Übergänge stattfinden.", - "hooks.deliveryTitle": "Zustellung", - "hooks.statSent": "Gesendet", - "hooks.statFailed": "Fehlgeschlagen", + "hooks.noResponse": "keine Antwort", + "hooks.payloadNote": "Payload {version}. Signatur in {header}, über Zeitstempel und Body.", + "hooks.retriesHint": "Wiederholungen blockieren die eigene Warteschlange dieses Endpunkts, denn die Reihenfolge ist das Versprechen. Ein 4xx wird nie wiederholt — ein Endpunkt, der die Anfrage für falsch erklärt, wird das wieder tun.", + "hooks.saveFailed": "Der Hook konnte nicht gespeichert werden.", + "hooks.secretCopied": "Ich habe ihn kopiert", + "hooks.secretTitle": "Signaturschlüssel — jetzt kopieren, er kann nicht erneut angezeigt werden", + "hooks.sendTest": "Testzustellung senden", + "hooks.sendTestTitle": "Jetzt eine Testzustellung senden", + "hooks.signed": "signiert", + "hooks.statDropped": "Verworfen", "hooks.statEndpoints": "Endpunkte", + "hooks.statFailed": "Fehlgeschlagen", "hooks.statQueued": "In Warteschlange", - "hooks.statDropped": "Verworfen", "hooks.statRetries": "Wiederholungen", - "hooks.payloadNote": "Payload {version}. Signatur in {header}, über Zeitstempel und Body.", - "hooks.deleteTitle": "Diesen Webhook löschen?", - "hooks.deleteBody": "Er stellt sofort nichts mehr zu. Der Endpunkt bleibt unberührt, und Sie können den Hook neu anlegen — sein Signaturschlüssel lässt sich jedoch nicht wiederherstellen.", - "hooks.deleteConfirm": "Webhook löschen", - "hooks.editTitle": "Webhook bearbeiten", - "hooks.newTitle": "Neuer Webhook", - "hooks.dialogIntro": "Ein signierter POST pro Übergang, in der richtigen Reihenfolge. Anders als bei einer Warnung wird nichts zusammengefasst oder entprellt — ein Skript erhält jede Flanke.", - "hooks.name": "Name", - "hooks.url": "Endpunkt-URL", - "hooks.urlMasked": "Die gespeicherte URL wird nie vollständig angezeigt. Lassen Sie den maskierten Wert stehen, um sie zu behalten, oder geben Sie eine neue URL ein, um sie zu ersetzen.", - "hooks.triggers": "Auslöser", - "hooks.triggersNoneHint": "Nichts ausgewählt bedeutet jeder Auslöser, auch solche, die eine künftige Version hinzufügt.", + "hooks.statSent": "Gesendet", + "hooks.testFailed": "Der Endpunkt hat die Testzustellung nicht angenommen.", + "hooks.timeout": "{seconds}s Zeitlimit", "hooks.timeoutLabel": "Zeitlimit (s)", - "hooks.attemptsLabel": "Versuche", - "hooks.retriesHint": "Wiederholungen blockieren die eigene Warteschlange dieses Endpunkts, denn die Reihenfolge ist das Versprechen. Ein 4xx wird nie wiederholt — ein Endpunkt, der die Anfrage für falsch erklärt, wird das wieder tun.", - "hooks.create": "Anlegen", - "hooks.triggerIngestPublished": "Stream gestartet", - "hooks.triggerIngestDisconnected": "Stream gestoppt", - "hooks.triggerDestinationUp": "Ziel live", - "hooks.triggerDestinationDown": "Ziel gestoppt", - "hooks.loadFailed": "Webhooks konnten nicht geladen werden.", + "hooks.title": "Lifecycle-Webhooks", "hooks.toggleFailed": "Der Hook konnte nicht geändert werden.", - "hooks.deleted": "Webhook gelöscht.", - "hooks.deleteFailed": "Der Hook konnte nicht gelöscht werden.", - "hooks.testFailed": "Der Endpunkt hat die Testzustellung nicht angenommen.", - "hooks.deliveriesFailed": "Die letzten Zustellungen konnten nicht gelesen werden.", + "hooks.triggerCount": "{count} Auslöser", + "hooks.triggerCountOne": "{count} Auslöser", + "hooks.triggerDestinationDown": "Ziel gestoppt", + "hooks.triggerDestinationUp": "Ziel live", + "hooks.triggerIngestDisconnected": "Stream gestoppt", + "hooks.triggerIngestPublished": "Stream gestartet", + "hooks.triggers": "Auslöser", + "hooks.triggersNoneHint": "Nichts ausgewählt bedeutet jeder Auslöser, auch solche, die eine künftige Version hinzufügt.", + "hooks.unsigned": "UNSIGNIERT", "hooks.updated": "Webhook aktualisiert.", - "hooks.created": "Webhook angelegt.", - "hooks.saveFailed": "Der Hook konnte nicht gespeichert werden.", - "playlist.title": "Playlist", + "hooks.url": "Endpunkt-URL", + "hooks.urlMasked": "Die gespeicherte URL wird nie vollständig angezeigt. Lassen Sie den maskierten Wert stehen, um sie zu behalten, oder geben Sie eine neue URL ein, um sie zu ersetzen.", + "jobs.alwaysKeepNewest": "Neueste immer behalten", + "jobs.automaticModel": "Automatisch (anhand dieser Hardware gewählt)", + "jobs.backToDefault": "Zurück zum Standardmodus für diese Art", + "jobs.batteryFloor": "Akku-Untergrenze %", + "jobs.broadcastGoing": "Eine Sendung läuft; zurückgestellte Arbeit tritt hinter sie zurück.", + "jobs.cancel": "Abbrechen", + "jobs.cannotPause": "Diese Arten sollten pausiert werden, können es aber nicht, und laufen deshalb zu Ende.", + "jobs.completed": "Abgeschlossen", + "jobs.concurrency": "Parallelität", + "jobs.couldNotReadTheJob": "Die Aufgabenwarteschlange konnte nicht gelesen werden.", + "jobs.couldNotSaveThePolicy": "Die Regeln konnten nicht gespeichert werden.", + "jobs.cpuCeiling": "CPU-Obergrenze %", + "jobs.cpuResume": "CPU-Fortsetzungsgrenze %", + "jobs.defaultMode": "Standardmodus", + "jobs.defaultModel": "Standardmodell", + "jobs.deferred": "Zurückgestellt", + "jobs.failed": "Fehlgeschlagen", + "jobs.finished": "Beendet", + "jobs.global": "Global", + "jobs.gpuGateNote": "Nur Arten, die als GPU-nutzend markiert sind, werden hierdurch begrenzt.", + "jobs.history": "Verlauf", + "jobs.historyRetention": "Aufbewahrung des Verlaufs", + "jobs.holdDeferredWorkBackOn": "Aufgeschobene Arbeit auf einem entladenden Laptop unterhalb dieses Werts zurückhalten. 0 deaktiviert dies.", + "jobs.hostCpuLoad": "CPU-Last des Hosts", + "jobs.howManyJobsMayRun": "Wie viele Aufgaben gleichzeitig laufen dürfen. Eine, denn ein zweiter Transcode erkauft einen Durchsatz, nach dem niemand gefragt hat, mit einem Risiko, das niemand akzeptiert hat.", + "jobs.inQueue": "In der Warteschlange", + "jobs.job": "Aufgabe", + "jobs.keepForDays": "Behalten für Tage", + "jobs.machineSafety": "Maschinenschutz. Dies ist die eine Schranke, an die sich auch Echtzeitarbeit hält.", + "jobs.niceLevel": "Nice-Stufe", + "jobs.noFinished": "Noch keine beendeten Aufgaben.", + "jobs.noModels": "Im Modellverzeichnis wurden keine Modelldateien gefunden. „Automatisch“ lädt das gewählte Modell herunter; ein hier benanntes Modell funktioniert erst, wenn diese Datei vorhanden ist.", + "jobs.nothingNewStartsAboveThis": "Oberhalb dieses Werts startet nichts Neues. 0 deaktiviert die Sperre.", + "jobs.nothingQueued": "Nichts in der Warteschlange. Die Nachbearbeitung wird aus der Bibliothek eingereicht.", + "jobs.nothingWasOldEnoughTo": "Nichts war alt genug zum Bereinigen. Senken Sie zuerst die Aufbewahrung unter Regeln.", + "jobs.pauseAll": "Alle pausieren", + "jobs.policy": "Regeln", + "jobs.policySaved": "Ressourcenregeln gespeichert.", + "jobs.progress": "Fortschritt", + "jobs.queuePaused": "Warteschlange pausiert.", + "jobs.queueResumed": "Warteschlange fortgesetzt.", + "jobs.queued": "In Warteschlange", + "jobs.recording": "Aufnahme", + "jobs.releasedItWillStartAs": "Freigegeben. Es startet, sobald ein Platz frei ist.", + "jobs.removeFromHistory": "Aus dem Verlauf entfernen", + "jobs.removeWindow": "Fenster entfernen", + "jobs.resumeAll": "Alle fortsetzen", + "jobs.retry": "Erneut versuchen", + "jobs.retryTitle": "Mit frischem Versuchskontingent neu scharfschalten", + "jobs.runNow": "Jetzt ausführen", + "jobs.runNowTitle": "Diese jetzt ausführen und die Ressourcenregeln übergehen", + "jobs.running": "Läuft", + "jobs.savedTheConcurrencyChangeTakes": "Gespeichert. Die Änderung der Parallelität wird beim Neustart des Servers wirksam — die Warteschlange legt das beim Start fest.", + "jobs.state": "Zustand", + "jobs.stopsEverythingRealtimeIncludedA": "Stoppt alles, Echtzeit eingeschlossen: Eine CPU, die drosselt, hat den Stream bereits zu verschlechtern begonnen. 0 deaktiviert dies.", + "jobs.subtitle": "Hintergrundarbeit: Transkription, Proxys, Miniaturansichten und Archivkomprimierung.", + "jobs.thatDidNotWork": "Das hat nicht funktioniert.", + "jobs.thermalCeiling": "Temperaturobergrenze °C", + "jobs.thisManyFinishedJobsSurvive": "So viele beendete Aufgaben bleiben unabhängig von ihrem Alter erhalten.", + "jobs.title": "Aufgaben", + "jobs.transcription": "Transkription", + "jobs.unsavedChanges": "Nicht gespeicherte Änderungen.", + "jobs.whereTheCeilingReleasesThe": "Wo die Obergrenze wieder freigibt. Der Abstand ist die Hysterese, die ein Schwingen der Sperre verhindert.", + "jobs.whisperMissing": "whisper.cpp ist hier nicht installiert, dies hat also noch keine Wirkung. Die Einstellung lohnt sich dennoch: Die Wahl gilt, sobald es installiert ist.", + "jobs.why": "Warum", + "jobs.windowEnd": "Fensterende", + "jobs.windowStart": "Fensterbeginn", + "jobs.yieldNote": "Schwere Arbeit wird eingereiht und tritt hinter den Live-Stream zurück. Ein verlorenes Bild ist unwiederbringlich; ein Transkript, das eine Stunde später kommt, kostet nichts.", + "lib.anySpeaker": "Beliebiger Sprecher", + "lib.anyTag": "Beliebiger Tag", + "lib.autoGrouped": "Automatisch gruppiert. Durch Bearbeiten übernehmen Sie sie.", + "lib.autoGroupedNote": "Diese Sitzung wurde automatisch gruppiert. Durch Speichern übernehmen Sie sie, und der Gruppierungslauf schreibt sie nicht mehr um.", + "lib.bestMatch": "Beste Übereinstimmung", + "lib.clearSearch": "Suche zurücksetzen", + "lib.clip": "Clip", + "lib.clipTitle": "Einen Clip aus dieser Aufnahme schneiden", + "lib.collapseSession": "Sitzung einklappen", + "lib.couldNotLoadTheLibrary": "Die Bibliothek konnte nicht geladen werden.", + "lib.couldNotLoadTheSession": "Die Segmente der Sitzung konnten nicht geladen werden.", + "lib.couldNotQueueTheProxy": "Der Proxy konnte nicht eingereiht werden.", + "lib.couldNotRegroup": "Neugruppierung nicht möglich.", + "lib.couldNotSaveTheSession": "Die Sitzung konnte nicht gespeichert werden.", + "lib.delete": "Löschen", + "lib.deleteDescription": "Die Datei wird zusammen mit ihren Proxys, Miniaturansichten und dem Transkript von der Festplatte entfernt. Dies kann nicht rückgängig gemacht werden.", + "lib.deleteTitle": "Diese Aufnahme löschen?", + "lib.downloadMaster": "Master herunterladen", + "lib.downloadMasterTitle": "Den Mehrspur-Master herunterladen", + "lib.editSession": "Sitzung bearbeiten", + "lib.expandSession": "Sitzung ausklappen", + "lib.from": "Von", + "lib.generateProxy": "Proxy erzeugen", + "lib.generateProxyTitle": "Den im Browser abspielbaren Proxy erzeugen", + "lib.mostRecent": "Neueste zuerst", + "lib.noProxyYet": "Noch kein Proxy — öffnen, um einen zu erzeugen", + "lib.noSegments": "Hier gibt es keine Segmente.", + "lib.noSessionMatch": "Keine Sitzung passt zu diesen Filtern.", + "lib.nothingRecorded": "Noch nichts aufgenommen. Sitzungen erscheinen, sobald der Recorder Segmente schreibt.", + "lib.oldestFirst": "Älteste zuerst", + "lib.playProxy": "Den Proxy abspielen", + "lib.proxyAlreadyQueued": "Ein Proxy ist bereits eingereiht.", + "lib.proxyQueued": "Proxy eingereiht.", + "lib.recordingDeleted": "Aufnahme gelöscht.", + "lib.regroupTitle": "Führt die Gruppierung erneut aus. Additiv und idempotent: Sie führt eine von Hand getrennte Gruppierung nie wieder zusammen.", + "lib.searchPlaceholder": "Alles Gesagte über alle Aufnahmen hinweg durchsuchen…", + "lib.searchTranscripts": "Transkripte durchsuchen", + "lib.searchable": "Durchsuchbar", + "lib.searchableOnly": "Segmente mit Transkript, also durchsuchbar", + "lib.session": "Sitzung", + "lib.sessionDescription": "Beschreibung", + "lib.sessionSaved": "Sitzung gespeichert.", + "lib.sessionTags": "Tags", + "lib.sessionTitle": "Titel", + "lib.subtitle": "Jede Sendung, durchsuchbar. Jedes Mikrofon wurde auf einer eigenen Spur aufgenommen, das Transkript weiß also bereits, wer es gesagt hat.", + "lib.tag": "Tag", + "lib.tagsPlaceholder": "komma, getrennt", + "lib.title": "Bibliothek", + "lib.transcribe": "Transkribieren", + "lib.transcribeEachTrackOnIts": "Jede Spur einzeln transkribieren — genau das liefert die Sprecherzuordnung", + "lib.ungrouped": "Nicht gruppierte Segmente", + "meters.deviation": "Abweichung", + "meters.idle": "im Leerlauf", + "meters.integrated": "Integriert", + "meters.loudness": "Lautheitskonformität", + "meters.metering": "misst", + "meters.momentary": "Momentan", + "meters.monitorOff": "Der Monitor ist aus. Schalten Sie ihn ein, um zu messen, was jedes Ziel liefert.", + "meters.noSignal": "kein Signal auf dieser Spur", + "meters.nothingToMeasure": "Noch nichts zu messen. Jedes laufende Ziel erhält seinen eigenen EBU R128-Analysator für die Mischung, die es empfängt.", + "meters.range": "Bereich", + "meters.shortTerm": "Kurzzeitig", + "meters.subtitle": "Jeder Kanal jeder Eingangsspur, direkt vom Relay.", + "meters.title": "Audiopegel", + "meters.truePeak": "True Peak", + "meters.waitingForStream": "wartet auf Stream", + "mon.allProcesses": "Alle Prozesse", + "mon.bitrateChart": "Eingangsbitrate — letzte 30 Minuten", + "mon.chooseDestination": "Ziel wählen…", + "mon.clearLog": "Protokoll leeren", + "mon.clearedThisDestinationIsBack": "Zurückgesetzt. Dieses Ziel läuft wieder mit dem generierten Befehl.", + "mon.dryRun": "Trockenlauf", + "mon.dryRunInvalid": "FFmpeg hat ein Argument abgelehnt", + "mon.dryRunOk": "FFmpeg hat es akzeptiert", + "mon.extraInput": "Zusätzliche Eingangsargumente", + "mon.extraOutput": "Zusätzliche Ausgangsargumente", + "mon.ffmpegLog": "FFmpeg-Protokoll", + "mon.fromRunningProcess": "aus dem laufenden Prozess", + "mon.fullCommand": "Vollständig aufgelöster Befehl", + "mon.hostCpu": "Host-CPU", + "mon.ingestLoss": "Eingangsverlust", + "mon.memory": "Speicher", + "mon.nothingProvenEitherWay": "Nichts in die eine oder andere Richtung bewiesen", + "mon.pid": "PID", + "mon.processes": "Prozesse", + "mon.rebuilt": "neu aufgebaut", + "mon.relayDrops": "Relay-Verluste", + "mon.relayIn": "Relay ein", + "mon.resolveHint": "Lösen Sie den Befehl auf, um vor dem Übernehmen genau zu sehen, was ausgeführt wird.", + "mon.restarts": "Neustarts", + "mon.subtitle": "Prozesszustand, Host-Ressourcen und das laufende FFmpeg-Protokoll.", + "mon.title": "Überwachung", + "mon.uptime": "Laufzeit", + "nav.automation": "Automatisierung", + "nav.chat": "Chat", + "nav.clips": "Clips", + "nav.dashboard": "Übersicht", + "nav.jobs": "Aufgaben", + "nav.library": "Bibliothek", + "nav.meters": "Audiopegel", + "nav.monitoring": "Überwachung", + "nav.playout": "Playout", + "nav.recordings": "Aufnahmen", + "nav.renditions": "Varianten", + "nav.routing": "Routing", + "nav.settings": "Einstellungen", + "nav.sources": "Quellen", + "play.acrossEveryRungTheBackstop": "Über alle Stufen hinweg. Die Rückfallsicherung, die verhindert, dass ein langer Lauf die Festplatte füllt.", + "play.allowCrossOrigin": "Cross-Origin erlauben", + "play.allowCrossOriginPlayback": "Cross-Origin-Wiedergabe erlauben", + "play.audioBitrate": "Audiobitrate (kbps)", + "play.audioTrack": "Audiospur", + "play.basicPasswordNote": "Player, die keinem Link folgen können (eine Set-Top-Box, ein CDN), können dasselbe Geheimnis stattdessen als HTTP-Basic-Kennwort senden.", + "play.boundsTheViewerTableViewers": "Begrenzt die Zuschauertabelle. Zuschauer darüber hinaus werden bedient, nur nicht gezählt.", + "play.carriesThePlaybackTokenAnyone": "Enthält das Wiedergabetoken. Jeder mit dieser URL kann zusehen.", + "play.clipboardUnavailable": "Die Zwischenablage ist nicht verfügbar; markieren und von Hand kopieren", + "play.couldNotLoadPlayout": "Playout konnte nicht geladen werden", + "play.disabled": "Deaktiviert", + "play.diskCap": "Speicherobergrenze (MB)", + "play.dvrWindow": "DVR-Fenster (s)", + "play.embedCode": "Einbettungscode", + "play.enable": "Playout aktivieren", + "play.enabled": "Aktiviert", + "play.forObsACdnOr": "Für OBS, ein CDN oder jeden Player, der HLS spricht.", + "play.format": "Format", + "play.hls": "HLS", + "play.hlsDash": "HLS und DASH", + "play.hlsPlaylist": "HLS-Playlist", + "play.idleTimeout": "Leerlaufzeit der Zuschauer (s)", + "play.linkReset": "Neuer Wiedergabelink erzeugt. Der vorherige funktioniert nicht mehr.", + "play.linksProtected": "Diese Links funktionieren nur für einen angemeldeten Administrator, bis Sie „Öffentlich“ einschalten.", + "play.linksPublic": "Diese Links funktionieren ohne Konto.", + "play.liveWindow": "Live-Fenster (Segmente)", + "play.metaDescription": "Beschreibung", + "play.metaTitle": "Titel", + "play.mustExceedOneSegmentOr": "Muss ein Segment überschreiten, sonst fallen Zuschauer zwischen zwei Abfragen kurz heraus.", + "play.name": "Name", + "play.noCredentialNote": "Es ist keinerlei Zugangsberechtigung erforderlich. Nur für einen Stream angebracht, den das gesamte Internet sehen soll.", + "play.noRungs": "Noch keine Stufen. Fügen Sie eine hinzu, um überhaupt etwas zu veröffentlichen.", + "play.onDisk": "Auf der Festplatte", + "play.oneStereoAacTrackPer": "Eine Stereo-AAC-Spur pro Stufe.", + "play.packaging": "Paketierung", + "play.passthrough": "Eingang (Durchleitung)", + "play.peak": "Spitze", + "play.playerPage": "Player-Seite", + "play.posterNote": "Wird auf der öffentlichen Seite über dem Video angezeigt. Das Posterbild ist ein aktueller Keyframe und wird automatisch erneuert.", + "play.private": "Privat.", + "play.protectedNote": "Die Wiedergabe ist standardmäßig geschützt. Sie zu öffnen ist ein eigener, bewusster Schritt.", + "play.protection": "Schutz", + "play.public": "Öffentlich", + "play.resetFailed": "Zurücksetzen nicht möglich", + "play.restartNote": "Eine Änderung an einem dieser Werte startet die Muxer neu, was die Zuschauer für einige Sekunden unterbricht.", + "play.rungNote": "Jede Stufe kopiert das Video ihrer Variante Bit für Bit; eine weitere kostet also einen Muxer und niemals eine zweite Videokodierung. Nur die Audiospur unterscheidet sich je Stufe; das Routing pro Ziel bleibt unberührt.", + "play.segmentLength": "Segmentlänge (s)", + "play.serveWithoutSession": "Zuschauer ohne Sitzung bedienen", + "play.sessionCap": "Sitzungsobergrenze", + "play.sessions": "Sitzungen", + "play.share": "Teilen", + "play.shorterIsLowerLatencyAnd": "Kürzer bedeutet weniger Latenz und mehr Bewegung in der Playlist.", + "play.subtitle": "Den Stream direkt an die Zuschauer ausliefern, als öffentliches HLS.", + "play.threeIsTheMinimumA": "Drei ist das Minimum, mit dem ein Player puffern kann.", + "play.title": "Playout", + "play.unlisted": "Nicht gelistet.", + "play.untitled": "Stream ohne Titel", + "play.videoSource": "Videoquelle", + "play.watchingNow": "Sehen gerade zu", + "play.whoCanWatch": "Wer zusehen darf", + "player.copied": "Kopiert", + "player.copy": "Kopieren", + "player.loading": "Wird geladen…", + "player.missingToken": "Dem aufgerufenen Link fehlt sein Wiedergabetoken, oder das Token wurde geändert. Bitten Sie die Person, die ihn geteilt hat, um einen aktuellen Link.", + "player.notLive": "Derzeit nicht live", + "player.viewer": "Zuschauer", + "player.viewers": "Zuschauer", + "player.willStart": "Diese Seite beginnt von selbst zu spielen, sobald die Übertragung startet.", + "playlist.choose": "Upload auswählen", + "playlist.chooseAria": "Upload zum Hinzufügen auswählen", "playlist.empty": "Noch nichts in der Warteschlange.", + "playlist.hint": "Hochgeladen unter Media. Ein Eintrag dieser Playlist, dessen Upload später verloren geht, wird oben als aufmerksamkeitsbedürftig angezeigt, statt beim nächsten Failover stillschweigend zu scheitern.", "playlist.itemsAria": "Playlist-Einträge", - "playlist.stateReady": "Bereit", - "playlist.stateTranscoding": "Wird transkodiert", - "playlist.stateAttention": "Benötigt Aufmerksamkeit", - "playlist.stateUnsaved": "Noch nicht gespeichert", "playlist.missingUpload": "Dieser Upload ist nicht mehr auffindbar.", - "playlist.moveUp": "{name} nach oben verschieben", "playlist.moveDown": "{name} nach unten verschieben", - "playlist.remove": "{name} entfernen", - "playlist.choose": "Upload auswählen", - "playlist.chooseAria": "Upload zum Hinzufügen auswählen", + "playlist.moveUp": "{name} nach oben verschieben", "playlist.noUploads": "Keine Uploads verfügbar", - "playlist.hint": "Hochgeladen unter Media. Ein Eintrag dieser Playlist, dessen Upload später verloren geht, wird oben als aufmerksamkeitsbedürftig angezeigt, statt beim nächsten Failover stillschweigend zu scheitern.", - "dest.fbBoxTitle": "Crossposting und Spendenbutton für Facebook", - "dest.fbBoxIntro": "Wird mit demselben Erstellungsaufruf wie die Zielgruppe gesendet, es gilt also dieselbe Regel: einmalig angewendet, wenn die Übertragung startet, und vollständig unangetastet gelassen, wenn leer.", - "dest.fbBackupLabel": "Backup-Feed veröffentlichen", - "dest.fbBackupHelp": "Sendet eine zweite Kopie dieses Streams an Facebooks Backup-Ingest, damit eine abgebrochene Verbindung nicht die Übertragung abbricht.", - "dest.fbBackupCost": "Verdoppelt die Upload-Bandbreite dieses Ziels.", - "dest.fbBackupReconnect": "Beim Einschalten verbindet sich der Stream einmal neu – aktivieren Sie es also, bevor Sie live gehen.", - "dest.fbCrosspostLabel": "Crossposting auf Seiten", - "dest.fbCrosspostPageId": "Seiten-ID", - "dest.fbCrosspostAlsoPost": "Auch posten", - "dest.fbCrosspostRemove": "Diese Seite entfernen", - "dest.fbCrosspostAdd": "Seite hinzufügen", - "dest.fbCrosspostHelp": "Die numerische Seiten-ID aus Facebooks eigener Konsole (Seiteneinstellungen oder graph.facebook.com/me/accounts, während Sie als Seite angemeldet sind) – hier gibt es keine Suche, fügen Sie also die ID ein und nicht einen Namen oder eine URL. „Auch posten“ veröffentlicht als diese Seite, statt die Übertragung nur mit ihr zu teilen; ausgeschaltet passiert nur das leisere Teilen.", - "dest.fbDonateLabel": "Spenden-Button: Organisations-ID", - "dest.fbDonatePlaceholder": "Organisations-ID", - "dest.fbDonateHelp": "Ebenfalls eine undurchsichtige ID aus Facebooks Spendenaktions-Konsole, kein Name einer Organisation zum Suchen. Leer lassen, um gar keinen Spendenbutton anzuhängen." + "playlist.remove": "{name} entfernen", + "playlist.stateAttention": "Benötigt Aufmerksamkeit", + "playlist.stateReady": "Bereit", + "playlist.stateTranscoding": "Wird transkodiert", + "playlist.stateUnsaved": "Noch nicht gespeichert", + "playlist.title": "Playlist", + "rec.delete": "Löschen", + "rec.deleteDescription": "Die Datei wird zusammen mit ihren Proxys, Miniaturansichten und dem Transkript von der Festplatte entfernt. Dies kann nicht rückgängig gemacht werden.", + "rec.deleteFailed": "Die Aufnahme konnte nicht gelöscht werden.", + "rec.deleteTitle": "Diese Aufnahme löschen?", + "rec.deleted": "Aufnahme gelöscht.", + "rec.disabled": "deaktiviert", + "rec.disk": "Speicher", + "rec.diskRecordings": "Aufnahmen", + "rec.download": "Herunterladen", + "rec.downloadNamed": "{name} herunterladen", + "rec.duration": "Dauer", + "rec.empty": "Noch keine Aufnahmen. Aktivieren Sie rechts die Aufnahme und starten Sie einen Stream.", + "rec.enabled": "Aktiviert", + "rec.file": "Datei", + "rec.free": "Frei", + "rec.haltedDefault": "Die Aufnahme ist angehalten: Das Volume liegt unter der Mindestgrenze für freien Speicher.", + "rec.haltedRestarts": "Der Recorder startet von selbst neu, sobald Speicher frei wird.", + "rec.help.enabled": "Schreibt jeden Eingang während des Streamens auf die Festplatte und hält dabei alle Audiospuren getrennt und unkodiert. Die Aufnahme ist unabhängig davon, was Sie an Plattformen senden: Ausschalten wirkt sich nicht auf Ihre Live-Ausgabe aus, Einschalten kostet Speicherplatz statt Upload-Bandbreite.", + "rec.help.maxAgeHours": "Löscht Aufnahmen, die älter als diese Stundenzahl sind, unabhängig von der Gesamtgröße. Nützlich, wenn Sie ohnehin nur die letzten ein bis zwei Tage brauchen. 0 behält alles, bis eine andere Grenze es entfernt.", + "rec.help.maxGb": "Eine Obergrenze für die Gesamtgröße der Aufnahmen, die polyemesis behält. Wird sie überschritten, werden die ältesten gelöscht, bis die Summe wieder passt. 0 bedeutet keine Größenbegrenzung — dann begrenzen nur das Alter oder die Untergrenze für freien Speicher das Wachstum.", + "rec.help.minFreeGb": "Ein harter Stopp: Die Aufnahme hält an, wenn auf dem Volume weniger als dieser freie Speicher bleibt, und läuft von selbst weiter, sobald Platz frei wird. Dies ist die einzige Grenze, die auch Dateien berücksichtigt, die polyemesis nicht geschrieben hat — und damit die Einstellung, die eine gemeinsam genutzte Festplatte tatsächlich vor dem Volllaufen schützt.", + "rec.help.segmentSeconds": "Wie lang jede Datei ist, bevor der Recorder eine neue beginnt. Kürzere Segmente begrenzen, was ein Absturz kosten kann, und erleichtern das Verschieben einzelner Dateien, hinterlassen aber mehr davon. Schnitte liegen auf einem Keyframe, die tatsächliche Länge schwankt daher um etwa eine Sekunde.", + "rec.help.stemFiles": "Schreibt zusätzlich jede Audiospur als eigene Datei neben dem Master. Nützlich, wenn Sie später schneiden und Moderation, Gäste und Spielton auf getrennten Kanälen brauchen. Es vervielfacht das Schreibvolumen einer Sitzung etwa um die Anzahl der Spuren — deshalb ist es standardmäßig aus.", + "rec.help.stemFormat": "Beide Optionen sind verlustfrei, keine kostet also Qualität. FLAC ist etwa halb so groß und die richtige Wahl, sofern nicht ein Werkzeug in Ihrem Arbeitsablauf es nicht lesen kann; WAV öffnet sich überall. Spuren mit mehr als 8 Kanälen fallen automatisch auf WAV zurück.", + "rec.hideStems": "Stems ausblenden", + "rec.keepForever": "0 = dauerhaft behalten.", + "rec.loadFailed": "Aufnahmen konnten nicht geladen werden.", + "rec.maxAgeHours": "Löschen nach (Stunden)", + "rec.maxGb": "Höchstens behalten (GB)", + "rec.minFreeGb": "Stoppen unter freiem Speicher (GB)", + "rec.minFreeNote": "Die Grenzen oben begrenzen nur, was polyemesis geschrieben hat; alles andere auf dem Volume kann es trotzdem füllen. 0 = keine Untergrenze.", + "rec.noSizeLimit": "0 = keine Größenbegrenzung.", + "rec.recording": "nimmt auf", + "rec.retentionNote": "Die Bereinigung läuft alle 30 Sekunden: älteste zuerst, nach Alter und dann nach Gesamtgröße. Das gerade geschriebene Segment wird nie gelöscht.", + "rec.saveFailed": "Die Einstellungen konnten nicht gespeichert werden.", + "rec.savePolicy": "Aufbewahrungsregeln speichern", + "rec.saved": "Aufnahmeeinstellungen gespeichert.", + "rec.segmentSeconds": "Segmentlänge (Sekunden)", + "rec.segments": "Segmente", + "rec.settingsTitle": "Aufnahme", + "rec.showStems": "Stems anzeigen", + "rec.size": "Größe", + "rec.started": "Gestartet", + "rec.stemFiles": "Stem-Dateien pro Spur", + "rec.stemFlac": "FLAC — verlustfrei, etwa halb so groß", + "rec.stemFormat": "Stem-Format", + "rec.stemNote": "Wird aus demselben Prozess wie der Master geschrieben, sodass jeder Stem samplegenau zu den anderen bleibt. Spuren mit mehr als 8 Kanälen fallen von selbst auf WAV zurück.", + "rec.stemWav": "WAV — verlustfrei, überall abspielbar", + "rec.stems": "Stems", + "rec.stemsTitle": "{count} Stem-Dateien pro Spur, samplegenau zum Master ausgerichtet", + "rec.stemsUsage": "Stems ({count} Dateien)", + "rec.subtitle": "Das vollständige Mehrspur-Archiv: jede Audiospur des Eingangs erhalten, unkodiert.", + "rec.title": "Aufnahmen", + "rec.tracks": "Spuren", + "rec.tracksTitle": "{count} Audiospuren erhalten", + "rec.used": "Belegt", + "rec.volume": "Volume", + "rend.aspectBlurpad": "Unscharfe Füllung", + "rend.aspectBlurpadHint": "Füllt den Rest mit einer unscharfen, formatfüllend beschnittenen Kopie des Bildes selbst. Das ist die Konvention, auf die sich vertikale Feeds geeinigt haben.", + "rend.aspectCrop": "Formatfüllend beschneiden", + "rend.aspectCropHint": "Beschneidet mittig auf die Zielform und skaliert dann. Motive behalten ihre Bildschirmgröße; die Ränder des Bildes sind weg.", + "rend.aspectHandling": "Umgang mit dem Seitenverhältnis", + "rend.aspectPad": "Letterbox", + "rend.aspectPadHint": "Skaliert das gesamte Bild passend und füllt den Rest mit einer einfarbigen Fläche. Nichts geht verloren, aber eine 16:9-Quelle auf einer 9:16-Fläche besteht überwiegend aus Balken.", + "rend.aspectStretch": "Passend dehnen", + "rend.aspectStretchHint": "Skaliert auf die Zielgröße und lässt das Bild verzerren, wenn die Quelle nicht passt. Das, was jede Variante tat, bevor es die anderen Modi gab.", + "rend.boxColour": "Boxfarbe", + "rend.boxOpacity": "Boxdeckkraft (%)", + "rend.builtInFont": "Eingebauter Standard", + "rend.burnedText": "Eingebrannter Text", + "rend.chooseHardware": "Wählen Sie einen Hardware-Encoder oder eine kleinere Ausgabe.", + "rend.colourBlack": "black", + "rend.colourWhite": "white", + "rend.costBeyond": "Dies in Echtzeit per Software zu kodieren übersteigt die meisten Maschinen", + "rend.costBeyondDetail": "{label} ergibt {mpps} für eine einzelne Kodierung. x264 verliert in dieser Größe meist das Rennen gegen die Echtzeit, und eine Variante, die zurückfällt, verliert Bilder für jedes Ziel, das sie liest — was Sie mitten im Stream herausfinden.{machine} {escape}", + "rend.costBusy": "Rechnen Sie damit, dass mehrere Kerne ausgelastet sind", + "rend.costBusyDetail": "{label} ergibt {mpps} auf {encoder}, das ist eine schwere Echtzeitkodierung.{machine} Beobachten Sie die Geschwindigkeit des Encoders, sobald er live ist: unter 1,0× fällt er zurück, und die Ziele darunter beginnen, Bilder zu verlieren. {escape}", + "rend.costMachineCores": " Diese Maschine meldet {cores} logische Kerne.", + "rend.costModest": "Bescheidene Softwarekodierung", + "rend.costModestDetail": "{label} ergibt {mpps} und liegt in Reichweite der meisten Maschinen.{machine} Es bleibt eine echte Kodierung — geteilt von jedem Ziel, das sie auswählt, aber nicht kostenlos.", + "rend.costNoNumbers": "Die Kosten lassen sich noch nicht abschätzen", + "rend.costNoNumbersDetail": "Nichts hat die Quelle geprüft, „Quelle beibehalten“ hat also keine Zahlen hinter sich. Starten Sie den Eingang, dann füllt sich dies.", + "rend.costOffloaded": "Ausgelagert an {encoder}", + "rend.costOffloadedDetail": "{label} ({mpps}) läuft auf dem Festfunktions-Encoder der GPU und kostet daher wenig CPU. Qualität und Ratensteuerung unterscheiden sich je nach Treiber, und jeder Chip hat seine eigene Obergrenze — prüfen Sie die Ausgabe vor einer Sendung, auf die es ankommt.", + "rend.create": "Anlegen", + "rend.created": "Variante angelegt. Wählen Sie sie an einem Ziel aus, um die Kodierung zu starten.", + "rend.custom": "Eigene…", + "rend.deintAll": "Jedes Bild", + "rend.deintAllHint": "Für Quellen, die zeilenverschränkt sind, es aber nicht angeben. Viele SDI-Bridges und Capture-Karten kennzeichnen alles als progressiv, unabhängig davon, was sie bekommen haben, und auf diesen ist „nur zeilenverschränkte Bilder“ ein Leerlauf, der wie eine defekte Einstellung aussieht.", + "rend.deintAuto": "Nur zeilenverschränkte Bilder", + "rend.deintAutoHint": "Behandelt nur Bilder, die die Quelle als zeilenverschränkt gekennzeichnet hat, progressive Bilder laufen also unverändert durch. Die richtige Wahl für alles Gemischte.", + "rend.deintOff": "Aus", + "rend.deintOffHint": "Richtig für die progressiven Quellen, die fast jeder hat. Deinterlacing macht sie ohne Gewinn weicher.", + "rend.deinterlace": "Deinterlacing", + "rend.deleteFailed": "Die Variante konnte nicht gelöscht werden.", + "rend.deleteFallbackMany": "{count} Ziele fallen auf Durchleitung zurück und erhalten das Quellvideo unverändert. Prüfen Sie, ob die Quelle noch zu dem passt, was die jeweilige Plattform akzeptiert.", + "rend.deleteFallbackOne": "{count} Ziel fällt auf Durchleitung zurück und erhält das Quellvideo unverändert. Prüfen Sie, ob die Quelle noch zu dem passt, was die Plattform akzeptiert.", + "rend.deleteNoDestinations": "Nichts wählt diese Variante aus, das Löschen ändert also kein Ziel.", + "rend.deleteRenditionTitle": "{name} löschen?", + "rend.editRendition": "Variante bearbeiten", + "rend.empty": "Keine Varianten. Jedes Ziel läuft auf Durchleitung, und das ist genau richtig, bis eines von ihnen die Quelle nicht annehmen kann — etwa ein 4K60-Eingang, den Twitch, Kick oder X nicht akzeptieren. Legen Sie eine Variante an und richten Sie diese Ziele darauf aus.", + "rend.encoder": "Encoder", + "rend.encoderPreset": "Encoder-Preset", + "rend.encoderRelatedFailed": "{name} wurde nicht angeboten, weil ein verwandter Encoder hier fehlgeschlagen ist.", + "rend.encoderTestFailed": "{name} ist auf dieser Maschine bei einer Testkodierung mit einem Bild fehlgeschlagen.", + "rend.encoderTestOk": "{name} hat auf dieser Maschine ein Testbild kodiert.", + "rend.evenNote": "0 auf einer Achse übernimmt den Wert der Quelle und erhält das Seitenverhältnis — geben Sie nur die Höhe an, um zu skalieren. Beide müssen gerade Zahlen sein: H.264 und HEVC haben keine Chroma-Ebene ungerader Größe.", + "rend.font": "Schriftart", + "rend.frameRate": "Bildrate", + "rend.gop": "GOP", + "rend.gpuUnopenable": "Eine GPU ist vorhanden, aber dieser Prozess kann sie nicht öffnen", + "rend.hardwareFallback": "Hardware", + "rend.hardwareScanFound": "Was der Hardware-Scan gefunden hat", + "rend.height": "Höhe", + "rend.hevcWarning": "Dieser Encoder erzeugt HEVC. Die meisten RTMP-Ziele akzeptieren nur H.264 — wählen Sie ihn nur für ein Ziel, das Ihnen HEVC zugesagt hat.", + "rend.keyframeInterval": "Keyframe-Intervall", + "rend.letterboxColour": "Letterbox-Farbe", + "rend.live": "Live", + "rend.loadFailed": "Die Varianten konnten nicht geladen werden.", + "rend.marginX": "Rand X (%)", + "rend.marginY": "Rand Y (%)", + "rend.name": "Name", + "rend.namePlaceholder": "1080p60 für Twitch und Kick", + "rend.needsBothAWidthAnd": "Benötigt sowohl eine Breite als auch eine Höhe — mit einer freien Achse wahrt die Skalierung das Seitenverhältnis bereits.", + "rend.needsBothAxes": "Benötigt sowohl eine Breite als auch eine Höhe", + "rend.newRendition": "Neue Variante", + "rend.noEncoder": "Dieses FFmpeg hat keinen nutzbaren Video-Encoder gemeldet.", + "rend.noHardwareEscape": "Auf dieser Maschine funktioniert kein Hardware-Encoder, es gibt also nichts, worauf sich dies verlagern ließe — der einzige Hebel hier ist eine kleinere Ausgabe. Erkennen Sie die Hardware erneut, wenn Sie seither einen Treiber installiert oder eine GPU in den Container durchgereicht haben.", + "rend.notProbed": "Auf dieser Maschine wurde noch nichts testweise kodiert, diese Liste zeigt also, was der FFmpeg-Build enthält, und nicht, was die Hardware kann. Führen Sie die Erkennung erneut aus, um es herauszufinden.", + "rend.notRunning": "Läuft nicht", + "rend.note": "Notiz", + "rend.noteDuplicateFps": "Die Quelle läuft mit {srcFps} fps, {fps} fps dupliziert daher nur Bilder.", + "rend.notePlaceholder": "Wofür diese Stufe gedacht ist.", + "rend.noteUpscale": "Die Quelle ist {width}×{height}. Hochskalieren kostet CPU und bringt keine Details.", + "rend.opacity": "Deckkraft (%)", + "rend.percentNote": "Alles ist ein Prozentsatz des Bildes, dieselben Einstellungen sind daher auf einer 16:9-Stufe genauso richtig wie auf einer 9:16-Stufe. Ränder werden auf einer zentrierten Achse ignoriert.", + "rend.preset": "Preset", + "rend.presetPlaceholder": "veryfast", + "rend.presetStartingPoint": "Ausgangspunkt — prüfen Sie die aktuellen Grenzwerte bei der Plattform.", + "rend.redetectFailed": "Die Hardware konnte nicht erneut erkannt werden.", + "rend.redetectTitle": "Den GPU-Scan erneut ausführen und mit jedem Encoder ein Testbild kodieren. Dauert einige Sekunden.", + "rend.redetectedNoHardware": "Hardware erneut erkannt. Kein Hardware-Encoder hat auf dieser Maschine eine Testkodierung bestanden.", + "rend.resolution": "Auflösung", + "rend.restartEncode": "Diese Kodierung und die Ziele, die sie lesen, neu starten", + "rend.restartFailed": "Die Kodierung konnte nicht neu gestartet werden.", + "rend.restarting": "{name} startet neu. Die zugehörigen Ziele verbinden sich mit ihr neu.", + "rend.save": "Speichern", + "rend.saveFailed": "Die Variante konnte nicht gespeichert werden.", + "rend.seedNote": "Diese füllen nur das Formular vor — ändern Sie vor dem Speichern, was Sie möchten. Die Obergrenzen der Plattformen ändern sich und hängen vom Partnerstatus ab.", + "rend.signalError": "Fehler", + "rend.signalIdle": "Inaktiv", + "rend.sizePct": "Größe (% der Höhe)", + "rend.softwareEncoderFallback": "einem Software-Encoder", + "rend.speed": "Geschwindigkeit", + "rend.subtitle": "Eine gemeinsame Videokodierung, viele Ziele. Audio wird hier nie angefasst.", + "rend.target": "Zielwert", + "rend.textColour": "Textfarbe", + "rend.textPlaceholder": "MEIN SENDER", + "rend.title": "Varianten", + "rend.unused": "Noch kein Ziel wählt dies aus, es wird also nichts kodiert. Wählen Sie es an einem Ziel aus, um die Kodierung zu starten.", + "rend.uptime": "Laufzeit", + "rend.videoBitrate": "Videobitrate", + "rend.waitingSnapshot": "Warten auf die erste Live-Momentaufnahme.", + "rend.watermarkImage": "Wasserzeichenbild", + "rend.watermarkPlaceholder": "overlays/logo.png", + "rend.width": "Breite", + "rend.widthPct": "Breite (% des Bildes)", + "route.attack": "Attack", + "route.audio": "Audio", + "route.audioAndVideoGoOut": "Audio und Video gehen gemeinsam hinaus.", + "route.audioLaterThanVideo": "Audio später als Video", + "route.audioRouting": "Audiorouting", + "route.audioRoutingDesc": "Wählen Sie genau, welche Eingangsspuren jedes Ziel erhält.", + "route.autoLimitWhen2Tracks": "Automatisch (begrenzen ab 2 Spuren)", + "route.commentaryLanguage": "Kommentarsprache", + "route.couldNotApplyThePreset": "Das Preset konnte nicht angewendet werden.", + "route.couldNotCompileThisRouting": "Dieses Routing konnte nicht kompiliert werden.", + "route.couldNotLoadDestinations": "Ziele konnten nicht geladen werden.", + "route.couldNotSaveTheRouting": "Das Routing konnte nicht gespeichert werden.", + "route.couldNotSaveTrackRoles": "Die Spurrollen konnten nicht gespeichert werden.", + "route.custom": "Eigene…", + "route.delayDirection": "Verzögerungsrichtung", + "route.destinations": "Ziele", + "route.duckingNote": "Senkt eine Gruppe von Spuren ab, sobald eine andere spricht — das Mikrofon über dem Musikbett. Aus bedeutet, dass jede ausgewählte Spur unverändert mit ihrer eigenen Verstärkung summiert wird.", + "route.duckingTriggerNote": "Ein Auslöser muss nicht in der Mischung dieses Ziels liegen — ein Mikrofon kann die Musik hier absenken, während nur die Musik gesendet wird.", + "route.enableDucking": "Ducking aktivieren", + "route.everyTrackReaches": "Jede ausgewählte Spur erreicht dieses Ziel, Musik eingeschlossen.", + "route.excludeMusic": "Musik ausschließen", + "route.keepMusic": "Musik behalten", + "route.keepMusicOut": "Musik aus diesem Ziel heraushalten", + "route.labelTracks": "Spuren beschriften", + "route.langPlaceholder": "lang — z. B. es", + "route.limiter04DbfsCeiling": "Limiter (Grenze −0,4 dBFS)", + "route.localOnlyNote": "Dieser Server speichert Spurrollen noch nicht, diese bleiben also in diesem Browser. Alles andere auf der Seite wird normal gespeichert.", + "route.loudnessTarget": "Lautheitsziel", + "route.lufsBroadcast": "EBU R128 Rundfunk", + "route.measuredOverTheWholeProgramme": "Gemessen über die gesamte Sendung, nicht von Moment zu Moment: Leise Passagen bleiben leise. Das sind die Werte, auf die die Normalisierer der Plattformen selbst zielen; einen davon zu treffen ist das, was verhindert, dass die Plattform Sie leiser dreht.", + "route.mixMatrix": "Mischmatrix", + "route.musicPolicy": "Musikregel", + "route.noTarget": "Kein Zielwert (Pegel unverändert lassen)", + "route.normalizeLoudnorm": "Lautheit (EBU R128)", + "route.normalizeOff": "Aus", + "route.off": "aus", + "route.offset": "Versatz", + "route.on": "an", + "route.platformDefault": "Plattformstandard", + "route.podcastSpokenWord": "Podcast / Sprache", + "route.presetApplied": "Preset angewendet. Prüfen Sie es und speichern Sie dann.", + "route.range": "Bereich", + "route.ratio": "Ratio", + "route.release": "Release", + "route.result": "Ergebnis", + "route.role51": "5.1", + "route.roleClean": "clean", + "route.roleMic": "Mikro", + "route.roleMusic": "Musik", + "route.rolesNote": "Rollen beschreiben den Eingang, nicht dieses Ziel, und werden für sich gespeichert. Es wird nichts gefiltert, bis ein Ziel weiter unten darum bittet.", + "route.simple": "Einfach", + "route.target": "Zielwert", + "route.targetRole": "Ziel", + "route.thisDestinationSPlatform": "Die Plattform dieses Ziels", + "route.threshold": "Schwelle", + "route.title": "Routing", + "route.triggerRole": "Auslöser", + "route.truePeak": "True Peak", + "route.video": "Video", + "route.videoLaterThanAudio": "Video später als Audio", + "route.withoutATargetTheMix": "Ohne Ziel geht die Mischung mit dem Pegel hinaus, den der Eingang geliefert hat; angewendet wird nur der Übersteuerungsschutz oben.", + "route.youtubeTwitchSpotify": "YouTube, Twitch, Spotify", + "set.acceptSelfSigned": "Selbstsigniertes Zertifikat akzeptieren", + "set.accountDisconnected": "Konto getrennt.", + "set.adminPassword": "Administratorkennwort", + "set.alertDelivery": "Alarmzustellung", + "set.alertDeliveryDesc": "Wie hartnäckig ein Webhook, der nicht antwortet, verfolgt wird, bevor der Alarm aufgegeben wird.", + "set.apiTokens": "API-Token", + "set.apiTokensDesc": "Langlebige Zugangsdaten für Skripte, Prometheus und alles andere, was keine Sitzung halten kann. Senden Sie eines als Authorization: Bearer pmk_…. Ein Token kann alles, was der Administrator kann, außer Token zu verwalten — das bleibt hinter dem Kennwort, weshalb ein Widerruf endgültig ist.", + "set.app": "App", + "set.attemptsBackoff": "Die Wartezeit zwischen den Versuchen verlängert sich automatisch bis zu einer Obergrenze von 30 Sekunden, sodass das obere Ende dieses Bereichs bereits mehrere Minuten Verfolgung eines toten Endpunkts bedeutet. Die Begrenzung ist der Sinn der Sache — endloses Wiederholen macht aus einem einzigen toten Webhook eine Warteschlange, die nie leer wird.", + "set.attemptsNote": "Einschließlich des ersten Versuchs. 1 bedeutet: keine Wiederholung.", + "set.attemptsRetryable": "Nur Fehler, bei denen sich ein erneuter Versuch lohnt, werden wiederholt. Ein 404 von einem Webhook, den jemand gelöscht hat, ist endgültig, und vier weitere Versuche verzögern nur jeden dahinter wartenden Alarm; ein 502 von einem Reverse-Proxy ist genau das, wofür diese Versuche da sind.", + "set.audioMeters": "Audiopegel", + "set.brokerPasswordCleared": "Broker-Kennwort gelöscht.", + "set.brokerPlainNote": "mqtt:// ist unverschlüsselt. MQTT sendet Benutzername und Kennwort in seinem CONNECT-Paket, sodass beide im Klartext über das Netz gehen — in einem vertrauenswürdigen LAN in Ordnung, was der übliche Home-Assistant-Fall ist, und nicht in Ordnung über alles, was Sie nicht kontrollieren. Verwenden Sie mqtts://, wenn der Broker es anbietet.", + "set.brokerUrl": "Broker-URL", + "set.brokerUrlNote": "mqtt://, mqtts://, ws:// oder wss://. Ein Kennwort in der URL wird abgelehnt: Eine URL landet in Logzeilen und in der ps-Ausgabe, und das lässt sich nicht zurücknehmen. Tragen Sie es in das Feld unten ein, wo es verschlüsselt gespeichert wird.", + "set.brokerUrlPlaceholder": "mqtts://broker.local:8883", + "set.chatBothApply": "Beide gelten, und die großzügigere Regel gewinnt: Eine Nachricht verschwindet, wenn sie älter als die Stundenzahl ist und außerhalb der neuesten N liegt. Ein aktiver Kanal behält so weniger Zeit als ein ruhiger bei denselben Einstellungen.", + "set.chatBufferNote": "„Beim Verbinden senden“ ist eine andere Art von Zahl als die beiden anderen. Jene begrenzen, was auf der Festplatte bleibt; diese ist ein Puffer im Arbeitsspeicher, der pro verbundenem Dashboard belegt wird.", + "set.chatHistory": "Chatverlauf", + "set.chatHistoryDesc": "Wie weit der Chat-Verlauf zurückreicht — und damit, wie viel ein Moderator sieht, wenn er die Karte einer Person öffnet.", + "set.chatHours": "Aufbewahren für (Stunden)", + "set.chatKeep": "Immer behalten (Nachrichten)", + "set.chatKeepNote": "Eine Untergrenze, unabhängig vom Alter.", + "set.chatNotOnlyDisk": "Das ist nicht nur eine Speichereinstellung. Ein Klick auf einen Namen im Chat zeigt, was diese Person zuvor gesagt hat, und dieser Verlauf stammt von hier — keine Plattform bietet dafür eine API, polyemesis kann also nur zeigen, was es aufbewahrt hat. Ist dieser Wert zu klein, wirkt jemand, der seit einer Woche auffällt, als hätte er nie etwas gesagt.", + "set.chatSendNote": "Verlauf ohne Abfrage.", + "set.chatSendOnConnect": "Beim Verbinden senden (Nachrichten)", + "set.chatSendOnConnectShort": "Beim Verbinden senden", + "set.chatSmall": "Chat ist klein. Ein Kanal mit durchschnittlich zehn Nachrichten pro Sekunde speichert etwa 7 MB pro Stunde; die meisten liegen weit darunter.", + "set.clientId": "Client-ID", + "set.clientIdNote": "Muss auf dem Broker eindeutig sein. Eine Kollision ist die häufigste Ursache einer unerklärlichen Reconnect-Schleife: Der Broker trennt bei jedem Verbinden die ältere Sitzung, und beide Clients verbinden sich endlos neu.", + "set.clientIdPlaceholder": "vom Instanznamen abgeleitet", + "set.clientSecret": "Client-Secret", + "set.comingBack": "Rückkehr", + "set.confirmPassword": "Neues Kennwort bestätigen", + "set.connectedAccounts": "Verbundene Konten", + "set.copied": "Kopiert", + "set.copy": "Kopieren", + "set.couldNotChangeThePassword": "Das Kennwort konnte nicht geändert werden.", + "set.couldNotClearThePassword": "Das Kennwort konnte nicht gelöscht werden.", + "set.couldNotCreateTheToken": "Das Token konnte nicht erstellt werden.", + "set.couldNotReCheckThe": "Die Zugangsdaten konnten nicht erneut geprüft werden.", + "set.couldNotReadTheTls": "Der TLS-Status konnte nicht gelesen werden.", + "set.couldNotRevokeTheToken": "Das Token konnte nicht widerrufen werden.", + "set.couldNotSaveTheCredentials": "Die Zugangsdaten konnten nicht gespeichert werden.", + "set.couldNotSaveTheMqtt": "Die MQTT-Einstellungen konnten nicht gespeichert werden.", + "set.credsRemoved": "Zugangsdaten entfernt.", + "set.currentPassword": "Aktuelles Kennwort", + "set.dataDirectory": "Datenverzeichnis", + "set.deliveryAttempts": "Zustellversuche", + "set.dialsOnly": "Der Server wählt ausschließlich: {schemes}.", + "set.disconnect": "Trennen", + "set.disconnectDesc": "Ziele, die es verwenden, behalten ihre Einstellungen, aktualisieren aber ihren Streamschlüssel nicht mehr. Sie können dasselbe Konto danach wieder verbinden.", + "set.disconnectTitle": "Dieses Konto trennen?", + "set.drawtextMissing": "Dieser Build hat keinen drawtext-Filter, Text-Overlays sind daher nicht verfügbar. Er benötigt ein mit libfreetype gebautes FFmpeg.", + "set.drawtextOk": "Dieser Build kann Text auf Video zeichnen.", + "set.enabled": "Aktiviert", + "set.encoderUrl": "Encoder-URL", + "set.expires": "Läuft ab", + "set.failover": "Ausfallsicherung", + "set.failoverDesc": "Ein Ersatzeingang und eine Wartekarte für den Fall, dass die Live-Quelle nichts mehr liefert. Standardmäßig aus: In diesem Zustand ist die Verarbeitungskette Byte für Byte dieselbe wie vor Einführung dieser Funktion.", + "set.failoverRemuxNote": "Das Einschalten fügt einen Remux-Schritt zwischen dem Eingang und allem darunter ein. Günstig, aber nicht kostenlos — deshalb ist es eine Entscheidung und keine Voreinstellung.", + "set.fileSizeMb": "Dateigröße (MB)", + "set.filesKept": "Aufbewahrte Dateien", + "set.goingLive": "Livegang", + "set.goingLiveDesc": "Was passiert, wenn mehrere Ziele gleichzeitig starten.", + "set.graceNote": "Wie lange die Live-Quelle nichts liefern darf, bevor die Stufe wechselt. Absichtlich länger als ein Reconnect — ein Encoder, der eine RTMP-Verbindung wieder aufbaut, ist Normalbetrieb und kein Ausfall.", + "set.gracePeriod": "Karenzzeit (Sekunden)", + "set.haDiscovery": "Home-Assistant-Erkennung", + "set.height": "Höhe", + "set.ingest": "Eingang", + "set.ingestDesc": "SRT überträgt bis zu sechs Audiospuren. RTMP überträgt eine und existiert als Rückfallebene für Encoder, die kein SRT beherrschen. „Abrufen“ wählt eine Quelle an, statt auf eine zu warten.", + "set.ingestUrlCopied": "Eingangs-URL kopiert.", + "set.instance": "Instanz", + "set.intervalNote": "Unveränderter Zustand wird nicht erneut veröffentlicht, ein kurzes Intervall kostet also einen Vergleich statt einer Nachricht. Das Keep-Alive begrenzt, wie lange eine tote Verbindung unbemerkt bleibt — und damit, wie schnell der Broker den Abonnenten mitteilt, dass diese Instanz weg ist.", + "set.issuer": "Aussteller", + "set.kbps": "kbps", + "set.keepAlive": "Keep-Alive (s)", + "set.latencyMs": "Latenz (ms)", + "set.listeners": "Listener", + "set.listenersDesc": "Wo sich der Server bindet, für die gesamte Installation. Ein SRT-Port bedient jede Quelle — sie werden anhand ihres Publish-Tokens unterschieden, sodass eine weitere Quelle nie einen weiteren offenen Port bedeutet. RTMP kennt kein solches Routing, sein Port bedient daher eine Quelle.", + "set.logsBoundNote": "Das Logverzeichnis ist durch das Produkt dieser beiden Werte begrenzt, sodass die Speicherung niemals die Festplatte füllen kann.", + "set.logsDesc": "Erfasstes FFmpeg-stderr wird immer im Arbeitsspeicher gehalten, doch der stirbt mit dem Server — und verliert genau die Zeilen, die erklären, warum er gestorben ist. Die Speicherung spiegelt es in eine rotierende Datei im Datenverzeichnis.", + "set.metersDesc": "Ein Beiprozess misst jeden Kanal jeder Spur. Kürzere Intervalle aktualisieren schneller und kosten mehr CPU.", + "set.mode": "Modus", + "set.modePull": "Abrufen — Kamera, Feed oder Datei anwählen", + "set.modeRtmp": "RTMP — eine Spur", + "set.modeSrt": "SRT — mehrspurig (empfohlen)", + "set.mqtt": "MQTT-Telemetrie", + "set.mqtt5Only": "Nur MQTT 5.0.", + "set.mqtt5OnlyNote": "Ein auf 3.1.1 festgelegter Broker stellt überhaupt keine Verbindung her — das ist kein eingeschränkter Modus. Mosquitto 2.x, EMQX und das Home-Assistant-Add-on sprechen alle 5.0.", + "set.mqttDesc": "Veröffentlicht Zustände als retained Messages an einen Broker, sodass ein Verbraucher, der bei einer Änderung nicht verbunden war, trotzdem die aktuelle Antwort erhält.", + "set.multiAccountNote": "Verbinden Sie dieselbe Plattform mehrfach, um auf mehrere Kanäle zu streamen — jedes Konto wird ein eigenes Ziel.", + "set.name": "Name", + "set.newPassword": "Neues Kennwort", + "set.no": "nein", + "set.noEncryption": "Nichts verschlüsselt diese Verbindung. Kennwörter und Sitzungscookies gehen im Klartext über das Netz — setzen Sie tls.mode: auto oder binden Sie hinter einem Proxy an 127.0.0.1.", + "set.noLibsrt": "Dieser FFmpeg-Build hat keine SRT-Unterstützung. Installieren Sie einen mit --enable-libsrt konfigurierten Build oder verwenden Sie RTMP.", + "set.noTokens": "Noch keine Token.", + "set.passphrase": "Passphrase", + "set.passphraseNote": "Aktiviert AES-Verschlüsselung. Leer lassen für keine. SRT lehnt Passphrasen außerhalb von 10–79 Zeichen ab.", + "set.passphrasePlaceholder": "optional — 10 bis 79 Zeichen", + "set.password": "Kennwort", + "set.passwordChanged": "Kennwort geändert.", + "set.passwordMismatch": "Die neuen Kennwörter stimmen nicht überein.", + "set.passwordNote": "Das Kennwort wird von keiner API zurückgegeben, dieses Feld bleibt also leer, auch wenn eines gesetzt ist. Lassen Sie es leer, um das gespeicherte Kennwort zu behalten; löschen Sie es mit der Schaltfläche unten.", + "set.platform": "Plattform", + "set.platformsDesc": "Plattformen unterscheiden sich darin, wie viel davon polyemesis automatisieren kann, und dieser Unterschied ist vorher wissenswert statt erst nach einer Stunde. Das Streamen selbst funktioniert überall gleich — Audiorouting pro Ziel, Varianten, Reconnect und Pegelanzeigen hängen von keiner der Spalten unten ab.", + "set.playLoop": "Eine Schleife abspielen, wenn nichts live ist", + "set.playLoopNote": "Ein zweiter Kandidat neben der Wartekarte: Dateien laufen statt eines Standbilds. Beides kann aktiv sein — welche Variante gewinnt, steht in der Stufenreihenfolge im Failover-Entwurf.", + "set.pointObs": "Richten Sie OBS auf diese Adresse.", + "set.preview": "Vorschau", + "set.previewDesc": "Der HLS-Monitor des Dashboards. Dies ist die einzige Stelle, an der polyemesis Video neu kodiert, und sie läuft über ein eigenes Relay-Abonnement, kann ein Ziel also niemals beeinträchtigen.", + "set.previewIdleNote": "Der Encoder startet, wenn das Dashboard die Playlist anfordert, und stoppt so lange nach der letzten Anfrage wieder, sodass ein geschlossenes Dashboard keine CPU kostet. Diese Änderung startet nie eine Vorschau neu, die gerade jemand ansieht.", + "set.processLogs": "Prozessprotokolle", + "set.proxyTerminatesTls": "Ein Reverse-Proxy vor polyemesis terminiert TLS, dieser Server liefert daher absichtlich einfaches HTTP aus.", + "set.publishInterval": "Veröffentlichungsintervall (s)", + "set.reconnectCap": "Obergrenze für Reconnect-Wartezeit (s)", + "set.reconnectCapNote": "0 verwendet den Standardwert. Nur HTTP- und HLS-Quellen.", + "set.redirectNote": "Muss exakt übereinstimmen, einschließlich Schema und Port. Wenn Sie polyemesis über einen Reverse-Proxy erreichen, verwenden Sie diese öffentliche URL.", + "set.redirectUri": "Freizugebende Redirect-URI", + "set.removeCreds": "Zugangsdaten entfernen", + "set.removeCredsDesc": "Jedes über diese App verbundene Konto aktualisiert sein Token nicht mehr und muss neu verbunden werden. Das Client-Secret lässt sich von hier nicht wiederherstellen — Sie würden bei der Plattform eine neue App anlegen.", + "set.removeCredsTitle": "Diese Entwickler-Zugangsdaten entfernen?", + "set.returnAfter": "Zurück nach (Sekunden stabil)", + "set.returnAutomatic": "Automatisch — zurück, sobald die primäre Quelle stabil ist", + "set.returnManual": "Manuell — auf dem Ersatz bleiben, bis ich umschalte", + "set.returnManualNote": "Manuell ist der Standard, weil eine automatische Rückkehr flattern kann: Eine primäre Quelle, die sich schubweise erholt, würde das Programm mitten in der Sendung hin und her schalten.", + "set.rtmpPort": "RTMP-Port", + "set.rtspNote": "TCP, sofern die Kamera nicht anders kann. RTSP über UDP durch NAT wirkt verbunden und liefert nichts.", + "set.rtspTransport": "RTSP-Transport", + "set.saveAndDrop": "Speichern und den Live-Stream trennen", + "set.saveIngest": "Eingangseinstellungen speichern", + "set.segmentSec": "Segment (s)", + "set.settingsSavedAffectedProcessesHave": "Einstellungen gespeichert. Die betroffenen Prozesse wurden neu gestartet.", + "set.showSlate": "Wartekarte zeigen, wenn nichts live ist", + "set.slateColour": "Farbe der Wartekarte", + "set.slateColourNote": "Ein Wort, denn der Wert landet in einem Filtergraphen, in dem ein Komma das Argument beenden würde. Wird nur verwendet, wenn kein Bild gesetzt ist.", + "set.slateColourPlaceholder": "black", + "set.slateImage": "Bild der Wartekarte", + "set.slateImageNote": "Ein Pfad innerhalb des Datenverzeichnisses. Lassen Sie ihn leer, um stattdessen eine einfarbige Fläche zu zeichnen — genau das ist die Rückfallebene, weil eine Farbe keine Datei hat, die sich nicht öffnen lässt.", + "set.slateImagePlaceholder": "slate/holding.png", + "set.sourceUrl": "Quell-URL", + "set.sourceUrlNote": "Eines von {schemes}. Eine file://-Quelle ist ein Pfad RELATIV zum Datenverzeichnis und darf kein „..“ enthalten — sie ist dort genauso eingeschlossen wie ein Datei-Upload.", + "set.sourceUrlPlaceholder": "rtsp://camera.local/stream1", + "set.srtPort": "SRT-Port", + "set.stagger": "Verbindungen staffeln (ms)", + "set.staggerNote": "0 ist aus. Livegang bedeutet sonst, dass jedes Ziel im selben Moment eine Verbindung öffnet, TLS aushandelt und Audio zu kodieren beginnt — auf einer kleinen Maschine ist das der Moment, in dem am ehesten Bilder verloren gehen, und es ist genau der Moment, den Sie beobachten, weil Sie gerade live gegangen sind. Ein paar hundert Millisekunden genügen.", + "set.staggerReconnectNote": "Ein Reconnect wird nie verzögert. Ein Ziel, das um 3 Uhr nachts abbricht, kommt sofort zurück, statt sich hinter bereits gesunden Prozessen anzustellen.", + "set.stepByStep": "Schritt-für-Schritt-Einrichtung", + "set.stopAfterIdle": "Stoppen nach Leerlauf (s)", + "set.streamKey": "Streamschlüssel", + "set.streamKeyNote": "Publisher müssen genau diesen Pfad verwenden, damit ein offener Port keine offene Tür ist.", + "set.subject": "Antragsteller", + "set.synthesiseNote": "Jede große Plattform lehnt einen Stream ohne Audiospur ab. Ist dies aktiv, erhält ein reiner Videoeingang eine stille Stereospur und seine Ziele starten; ist es aus, verweigern sie mit „routing profile selects no audio“. Es greift ausschließlich bei einem Eingang, der mit null Spuren ERKANNT wurde, kann also nie Audio verdrängen, das Sie tatsächlich senden.", + "set.synthesiseSilent": "Stille Spur erzeugen", + "set.syntheticAudio": "Synthetisches Audio", + "set.syntheticDesc": "Was passiert, wenn der Eingang Video und überhaupt kein Audio führt.", + "set.tabIngest": "Eingang", + "set.tabPipeline": "Verarbeitungskette", + "set.tabPlatforms": "Plattform-Zugangsdaten", + "set.tabSecurity": "Sicherheit", + "set.title": "Einstellungen", + "set.tlsAcme": "Let's Encrypt", + "set.tlsAuto": "Automatisch", + "set.tlsDesc": "Wird in config.yaml konfiguriert, nicht hier — TLS muss stimmen, bevor der Server zu lauschen beginnt. Diese Karte berichtet, was dabei herauskam.", + "set.tlsManual": "Manuelles Zertifikat", + "set.tlsOff": "Nicht hier terminiert", + "set.tlsSelfsigned": "Selbstsigniert", + "set.tokenHashNote": "polyemesis speichert nur einen Hash. Wenn Sie ihn verlieren, widerrufen Sie dieses Token und erstellen ein neues.", + "set.tokenNamePlaceholder": "prometheus", + "set.tokenRevoked": "{name} widerrufen.", + "set.topicPrefix": "Topic-Präfix", + "set.topicPrefixNote": "Trennzeichen im Präfix bleiben erhalten, home/av bedeutet also zwei Ebenen. Ein Präfix, das mit $ beginnt, wird abgelehnt: Broker reservieren diese, und ein Abonnent, der # verwendet, empfängt sie nie — die Telemetrie würde erfolgreich veröffentlicht und wäre genau in der Ansicht unsichtbar, mit der Sie Fehler suchen würden.", + "set.transportSecurity": "Transportsicherheit", + "set.updateInterval": "Aktualisierungsintervall (ms)", + "set.username": "Benutzername", + "set.validFor": "Gültig für", + "set.whatPlatformsDo": "Was jede Plattform kann", + "set.whyDevApp": "Warum Sie eine eigene Entwickler-App brauchen", + "set.whyDevAppDesc": "polyemesis kann keine OAuth-Client-Secrets ausliefern — jeder mit der Binärdatei hätte sie, und die Plattformen würden sie widerrufen. Sie registrieren einmal eine kostenlose Entwickler-App, fügen Client-ID und Secret hier ein, und polyemesis holt dann Ihre Streamschlüssel für Sie.", + "set.writeLogs": "Protokolle auf die Festplatte schreiben", + "set.yes": "ja", + "sources.addDescription": "Ein zweites Programm mit eigenem Eingang, eigenen Zielen und Varianten — etwa eine vertikale Leinwand neben einer horizontalen. Es startet auf den Standardports, die Sie ändern sollten, damit sich beide nicht in die Quere kommen.", + "sources.addFailed": "Die Quelle konnte nicht hinzugefügt werden", + "sources.addSource": "Quelle hinzufügen", + "sources.addTitle": "Quelle hinzufügen", + "sources.added": "„{name}“ hinzugefügt. Legen Sie unten die Eingangsports fest.", + "sources.app": "App", + "sources.applyAndDrop": "Übernehmen und Stream trennen", + "sources.copied": "{what} kopiert", + "sources.copyFailed": "{what} konnte nicht kopiert werden", + "sources.copyTokenAria": "Token kopieren", + "sources.copyUrlAria": "{proto}-URL kopieren", + "sources.default": "Standard", + "sources.defaultDescription": "Anfragen ohne Angabe einer Quelle wirken auf diese.", + "sources.deleteAria": "{name} löschen", + "sources.deleteConfirm": "Quelle löschen", + "sources.deleteDescription": "Die zugehörigen Ziele und Varianten werden mitgelöscht — sie beschreiben, wohin dieses Programm geht, und sind ohne es bedeutungslos. Aufnahmen bleiben erhalten: Die Dateien liegen weiterhin auf der Festplatte und sind abspielbar, sie sind nur keiner Quelle mehr zugeordnet.", + "sources.deleteFailed": "Die Quelle konnte nicht gelöscht werden", + "sources.deleteThis": "Diese Quelle löschen", + "sources.deleteTitle": "„{name}“ löschen?", + "sources.deleted": "„{name}“ und zugehörige Ziele gelöscht", + "sources.destinations": "Ziele", + "sources.dirtyIdle": "Nicht gespeicherte Eingangsänderungen. Beim Übernehmen wird der Eingang dieser Quelle neu gestartet.", + "sources.dirtyPublishing": "Ein Encoder sendet an diese Quelle. Beim Übernehmen wird der Eingang neu gestartet und alle Zuschauer werden getrennt.", + "sources.enableAria": "{name} aktivieren", + "sources.help.app": "Der RTMP-Anwendungspfad — der Teil der URL zwischen Host und Streamschlüssel. Die meisten Encoder erwarten „live“. Ändern Sie ihn nur, wenn Sie einen Grund dafür haben.", + "sources.help.ingest": "Wie Ihr Encoder diese Quelle erreicht. SRT übersteht instabile Verbindungen, indem verlorene Pakete erneut gesendet werden, und ist die richtige Standardwahl über das öffentliche Internet. RTMP ist älter und wird überall unterstützt, verschlechtert sich aber auf einer instabilen Leitung stark. Bei „Abrufen“ holt polyemesis den Stream von einer URL, die Sie angeben, statt auf eine Einspeisung zu warten.", + "sources.help.latencyMs": "Wie lange SRT auf ein verspätetes oder erneut gesendetes Paket wartet, bevor es aufgegeben wird. Höhere Werte überbrücken schlechtere Verbindungen auf Kosten der Verzögerung: Etwa das 2,5-Fache Ihrer Paketumlaufzeit ist ein guter Ausgangspunkt, bei 40 ms RTT also rund 100 ms. Zu niedrig, und ein kurzer Aussetzer wird zu einem sichtbaren Bildfehler.", + "sources.help.passphrase": "Verschlüsselt die SRT-Verbindung, und heute ist genau dies das, was Fremde von diesem Eingang fernhält. Zwischen 10 und 79 Zeichen. Bleibt das Feld leer, kann jeder senden, der den Port erreicht.", + "sources.help.streamKey": "Das Geheimnis, das ein Encoder sendet, um über RTMP zu übertragen. Dies ist die Zugangsberechtigung für diesen Eingang, behandeln Sie sie also wie ein Passwort: Wer sie besitzt, kann an Ihre Ziele senden.", + "sources.help.token": "Identifiziert diese Quelle, wenn sich mehrere einen Eingangsport teilen. Ob es zusätzlich den Zugang schützt, hängt von Ihrem Eingangsmodus ab — der Hinweis darunter sagt, was gerade zutrifft. Beim Erneuern wird ein neues Token ausgestellt und das alte bleibt fünf Minuten gültig, damit ein laufender Encoder nicht mitten in der Übertragung getrennt wird.", + "sources.ingest": "Eingang", + "sources.lastSourceTitle": "Die letzte Quelle kann nicht gelöscht werden — eine Installation braucht einen Eingang", + "sources.latencyMs": "Latenz (ms)", + "sources.loadFailed": "Quellen konnten nicht geladen werden", + "sources.loading": "Quellen werden geladen…", + "sources.loss": "Verlust", + "sources.modePull": "Abrufen", + "sources.name": "Name", + "sources.namePlaceholder": "Vertikal", + "sources.noEngine": "Für diese Quelle läuft keine Engine. Die übliche Ursache ist ein Eingangsport, der bereits von einer anderen Quelle oder einem anderen Prozess belegt ist.", + "sources.notRunning": "läuft nicht", + "sources.ownDescription": "Eigene Ziele, Varianten und Aufnahmen.", + "sources.passphrase": "Passphrase", + "sources.passphrasePlaceholder": "10–79 Zeichen", + "sources.publishing": "sendet", + "sources.renditions": "Varianten", + "sources.retrans": "Neuübertr.", + "sources.rotate": "Erneuern", + "sources.rotateConfirm": "Token erneuern", + "sources.rotateFailed": "Das Token konnte nicht erneuert werden", + "sources.rotateIdle": "Ein neues Token wird sofort ausgestellt. Das alte bleibt fünf Minuten gültig, damit ein Encoder, der es bereits verwendet, nicht getrennt wird.", + "sources.rotateTitle": "Token für „{name}“ erneuern?", + "sources.rotateWhilePublishing": "Ein Encoder sendet gerade an diese Quelle. Das alte Token bleibt fünf Minuten gültig, die Verbindung wird also nicht sofort getrennt — es muss aber vor Ablauf dieser Frist aktualisiert werden.", + "sources.rtt": "RTT", + "sources.running": "läuft", + "sources.saveFailed": "Speichern nicht möglich", + "sources.streamKey": "Streamschlüssel", + "sources.subtitle": "Je ein eingehendes Programm. Ziele und Varianten gehören zu einer Quelle.", + "sources.title": "Quellen", + "sources.token": "Token", + "sources.tokenEnforcedDetailRtmp": "Er ist der Streamschlüssel: senden Sie an die obige URL mit diesem Schlüssel. Beim Rotieren wird ein neuer erzeugt und der alte bleibt fünf Minuten gültig, sodass Sie ohne Unterbrechung wechseln können.", + "sources.tokenEnforcedDetailSrt": "Tragen Sie ihn als SRT-streamid im Encoder ein, um auf dem gemeinsamen Port zu diese Quelle zu senden. Beim Rotieren wird ein neuer erzeugt und der alte bleibt fünf Minuten gültig, sodass Sie ohne Unterbrechung wechseln können.", + "sources.tokenIsCredential": "Dieses Token ist die Zugangsberechtigung.", + "sources.tokenIssued": "Neues Token ausgestellt", + "sources.tokenNotEnforced": "Dieses Token wird gespeichert, aber derzeit nicht erzwungen.", + "sources.tokenNotEnforcedRtmp": "Quellen werden über den Port getrennt gehalten. Was diesen Eingang tatsächlich schützt, ist der Streamschlüssel oben. Aktivieren Sie in den Einstellungen den Ein-Port-Eingang, damit das Token zur Zugangsberechtigung wird.", + "sources.tokenNotEnforcedSrt": "Quellen werden über den Port getrennt gehalten. Was diesen Eingang tatsächlich schützt, ist die SRT-Passphrase oben. Aktivieren Sie in den Einstellungen den Ein-Port-Eingang, damit das Token zur Zugangsberechtigung wird.", + "state.disabled": "Deaktiviert", + "state.failed": "Fehlgeschlagen", + "state.live": "Live", + "state.offline": "Offline", + "state.reconnecting": "Verbindet neu", + "state.starting": "Startet", + "state.stopped": "Gestoppt" } diff --git a/ui/src/lib/i18n/en.json b/ui/src/lib/i18n/en.json index 2df93c11..801d9bc0 100644 --- a/ui/src/lib/i18n/en.json +++ b/ui/src/lib/i18n/en.json @@ -1,26 +1,227 @@ { - "nav.dashboard": "Dashboard", - "nav.sources": "Sources", - "nav.meters": "Audio meters", - "nav.routing": "Routing", - "nav.renditions": "Renditions", - "nav.playout": "Playout", - "nav.library": "Library", - "nav.clips": "Clips", - "nav.chat": "Chat", - "nav.jobs": "Jobs", - "nav.automation": "Automation", - "nav.recordings": "Recordings", - "nav.monitoring": "Monitoring", - "nav.settings": "Settings", - "chrome.toggleNav": "Toggle navigation", + "auth.accountCreated": "Admin account created.", + "auth.confirmPassword": "Confirm password", + "auth.createAccount": "Create account", + "auth.firstRun": "First-run setup", + "auth.firstRunDesc": "Choose the administrator credentials for this server. There is no account recovery, so store them somewhere safe.", + "auth.password": "Password", + "auth.passwordMismatch": "The passwords do not match.", + "auth.signIn": "Sign in", + "auth.signInDesc": "Enter your administrator credentials.", + "auth.somethingWentWrong": "Something went wrong.", + "auth.tokenCreated": "API token created", + "auth.tokenRevoked": "API token revoked", + "auth.username": "Username", + "auto.action": "Action", + "auto.adminPasswordChanged": "Admin password changed", + "auto.chosenWeekdays": "Chosen weekdays", + "auto.coalesced": "Coalesced", + "auto.coalescingWindow": "Coalescing window (seconds)", + "auto.couldNotChangeTheRule": "Could not change the rule.", + "auto.couldNotChangeTheSchedule": "Could not change the schedule.", + "auto.couldNotDeleteTheRule": "Could not delete the rule.", + "auto.couldNotDeleteTheSchedule": "Could not delete the schedule.", + "auto.couldNotLoadAutomationSettings": "Could not load automation settings.", + "auto.couldNotSaveTheRule": "Could not save the rule.", + "auto.couldNotSaveTheSchedule": "Could not save the schedule.", + "auto.createRule": "Create rule", + "auto.createSchedule": "Create schedule", + "auto.days": "Days", + "auto.delete": "Delete", + "auto.deleteRule": "Delete rule", + "auto.deleteRuleDesc": "The rule stops firing. Its webhook endpoint is untouched, and you can recreate the rule.", + "auto.deleteRuleTitle": "Delete this alert rule?", + "auto.deleteSchedule": "Delete schedule", + "auto.deleteScheduleDesc": "The schedule stops running. Anything it already started is unaffected.", + "auto.deleteScheduleTitle": "Delete this schedule?", + "auto.delivery": "Delivery", + "auto.destinationFallingBehindRealtime": "Destination falling behind realtime", + "auto.destinationKeepingUpAgain": "Destination keeping up again", + "auto.destinationRecovered": "Destination recovered", + "auto.destinationWentDown": "Destination went down", + "auto.destinations": "Destinations", + "auto.dropped": "Dropped", + "auto.edit": "Edit", + "auto.editRule": "Edit alert rule", + "auto.editSchedule": "Edit schedule", + "auto.endpoint": "Endpoint", + "auto.evAudioClipping": "Audio clipping", + "auto.evClipCaptured": "Clip captured", + "auto.evDiskLow": "Disk running low", + "auto.evDiskRecovered": "Disk recovered", + "auto.evIngestLost": "Ingest lost", + "auto.evIngestRecovered": "Ingest recovered", + "auto.evSettingsChanged": "Settings changed", + "auto.evSignedIn": "Signed in", + "auto.events": "Events", + "auto.everyDay": "Every day", + "auto.failed": "Failed", + "auto.failoverSwitchedSource": "Failover switched source", + "auto.fired": "fired", + "auto.firesAt": "Fires at", + "auto.lastDelivery": "Last delivery", + "auto.lastSweep": "Last sweep", + "auto.latenessAllowance": "Lateness allowance (seconds)", + "auto.loudnessBackInCompliance": "Loudness back in compliance", + "auto.loudnessOutOfCompliance": "Loudness out of compliance", + "auto.minGap": "Minimum gap between messages (seconds)", + "auto.minSeverity": "Minimum severity", + "auto.missedNote": "An occurrence missed because the server was down is marked handled and skipped rather than fired late — a stream that starts itself four hours late is worse than one that never started.", + "auto.name": "Name", + "auto.newRule": "New alert rule", + "auto.newSchedule": "New schedule", + "auto.next": "Next", + "auto.nothingFired": "Nothing has fired yet. The scheduler sweeps every 20 seconds.", + "auto.once": "Once", + "auto.payloadFormat": "Payload format", + "auto.pending": "Pending", + "auto.repeatedFailedSignIns": "Repeated failed sign-ins", + "auto.repeats": "Repeats", + "auto.retries": "Retries", + "auto.rule": "Rule", + "auto.ruleCreated": "Alert rule created. Send it a test.", + "auto.ruleDeleted": "Alert rule deleted.", + "auto.rulePlaceholder": "Discord — show alerts", + "auto.ruleSaved": "Alert rule saved.", + "auto.saveRule": "Save rule", + "auto.saveSchedule": "Save schedule", + "auto.schedule": "Schedule", + "auto.scheduleCreated": "Schedule created.", + "auto.scheduleDeleted": "Schedule deleted.", + "auto.schedulePlaceholder": "Weeknight show", + "auto.scheduleSaved": "Schedule saved.", + "auto.schedules": "Schedules", + "auto.secretMasked": "Shown masked because the secret lives in the path. Leave it alone to keep the current endpoint, or paste a new URL to replace it.", + "auto.sendTest": "Send test alert", + "auto.sendTestTitle": "Send a test alert now", + "auto.sent": "Sent", + "auto.skipped": "skipped", + "auto.startDestinations": "Start destinations", + "auto.startPlaylist": "Start the playlist", + "auto.stopDestinations": "Stop destinations", + "auto.stopPlaylist": "Stop the playlist", + "auto.subscribedTo": "Subscribed to", + "auto.subtitle": "Webhooks that tell you something broke, and schedules that run the show without you.", + "auto.theEndpointDidNotAccept": "The endpoint did not accept the test message.", + "auto.time": "Time", + "auto.timeZone": "Time zone", + "auto.title": "Automation", + "auto.tzPlaceholder": "UTC", + "auto.webhookHint": "Discord and Slack both give you one under channel settings.", + "auto.webhookPlaceholder": "https://discord.com/api/webhooks/…", + "auto.webhookRules": "Webhook rules", + "auto.webhookUrl": "Webhook URL", + "auto.weekFri": "Fri", + "auto.weekMon": "Mon", + "auto.weekSat": "Sat", + "auto.weekSun": "Sun", + "auto.weekThu": "Thu", + "auto.weekTue": "Tue", + "auto.weekWed": "Wed", + "auto.when": "When", + "chatpage.allFiltered": "Every platform with messages is filtered out.", + "chatpage.couldNotDeleteThatMessage": "Could not delete that message.", + "chatpage.deleteNote": "Deleting removes the message on the platform that issued it. A platform polyemesis cannot delete on says so and names where it can be done instead — the action is never hidden on a guess.", + "chatpage.droppedTitle": "Shown live, but persistence fell behind and the scrollback lost them.", + "chatpage.dupTitle": "The same message seen twice — our own echo, or a platform replaying it.", + "chatpage.noAccount": "Chat is running, but no platform account is attached. Connect one under Settings → Platform credentials.", + "chatpage.notRunning": "No chat is running on this server. The scrollback below the timeline is whatever was stored before, and the send box is disabled until a platform is attached.", + "chatpage.received": "Received", + "chatpage.reload": "Reload", + "chatpage.stored": "Stored", + "chatpage.storedTitle": "Nothing is attached; this is the stored scrollback.", + "chatpage.subtitle": "Every platform in one timeline, with one send box.", + "chatpage.timeline": "Timeline", + "chatpage.title": "Chat", "chrome.ingest": "Ingest", + "chrome.language": "Language", "chrome.liveCount": "{count} live", + "chrome.signOut": "Sign out", "chrome.socketOffline": "offline", "chrome.socketOfflineHint": "Live updates are disconnected; retrying", - "chrome.signOut": "Sign out", - "chrome.language": "Language", + "chrome.toggleNav": "Toggle navigation", + "clipedit.back33": "Back 33 ms", + "clipedit.clickLine": "Click a line to jump there. Select across several and the in and out points move to the words you highlighted.", + "clipedit.clipThisLine": "Clip this line", + "clipedit.clipThisLineTitle": "Set the in and out points around this line", + "clipedit.copied": "Copied", + "clipedit.copiesEveryPacketSecondsBit": "Copies every packet. Seconds, bit-exact, and the start snaps back to the nearest keyframe.", + "clipedit.couldNotOpenThatRecording": "Could not open that recording.", + "clipedit.couldNotQueueTheExport": "Could not queue the export.", + "clipedit.cut": "Cut", + "clipedit.downloadClip": "Download the clip", + "clipedit.exportQueued": "Export queued. It runs when the machine can spare the cycles.", + "clipedit.exports": "Exports", + "clipedit.fast": "Fast", + "clipedit.filterTranscript": "Filter the transcript", + "clipedit.findSaid": "Find what was said", + "clipedit.forward33": "Forward 33 ms", + "clipedit.in": "In", + "clipedit.keepAllTracks": "Keep every audio track", + "clipedit.keepsEveryAudioTrackIn": "Keeps every audio track in any codec. What the recorder writes.", + "clipedit.markIn": "I — mark in at the playhead", + "clipedit.markOut": "O — mark out at the playhead", + "clipedit.name": "Name", + "clipedit.noKeyframes": "The keyframe positions could not be read, so nobody can say where this cut will really start. It will still work — FFmpeg's own seek lands on a keyframe — but the start may be up to one GOP early and this page cannot show you by how much.", + "clipedit.noTranscript": "Nothing has been transcribed for this recording yet. Each microphone was recorded on its own track, so transcribing it gives you speaker names without a diarization model — and then you can clip by what was said.", + "clipedit.nothingExported": "Nothing exported yet.", + "clipedit.out": "Out", + "clipedit.pickAtLeastOneAudio": "Pick at least one audio track, or take all of them.", + "clipedit.playFaster": "L — play, again for faster", + "clipedit.precise": "Precise", + "clipedit.queueNote": "The export is queued, not run here: it yields to the live stream, so a cut taken mid-broadcast waits rather than stealing the cycles the stream needs.", + "clipedit.reEncodesOnlyTheLeading": "Re-encodes only the leading part-GOP so the start is exact. The rest is still copied.", + "clipedit.reencoded": "Re-encoded", + "clipedit.shortcuts": "J / K / L shuttle, I and O mark in and out, arrows nudge 33 ms and shift-arrows a second. The fine step is one frame at 30 fps; this recording's real rate is not indexed, so it is named in milliseconds rather than guessed at.", + "clipedit.shuttleBack": "J — shuttle backwards", + "clipedit.startMoved": "Start moved", + "clipedit.starts": "Starts", + "clipedit.stop": "K — stop", + "clipedit.thatCutCouldNotBe": "That cut could not be planned.", + "clipedit.thatIsNotARecording": "That is not a recording id.", + "clipedit.thatRecordingIsNotAvailable": "That recording is not available.", + "clipedit.title": "Clip editor", + "clipedit.whatASocialPlatformWill": "What a social platform will accept. Fewer codecs survive it.", + "clipedit.zoomIn": "Zoom in", + "clipedit.zoomOut": "Zoom out", + "clips.bitrate": "Bitrate", + "clips.buffer": "Buffer", + "clips.bufferLiveNote": "The buffer switch is a live toggle, not a saved setting: it is off again after a restart.", + "clips.captured": "Captured", + "clips.ceiling": "Ceiling", + "clips.clipsKept": "Clips kept", + "clips.couldNotCaptureAClip": "Could not capture a clip.", + "clips.couldNotChangeTheClip": "Could not change the clip buffer.", + "clips.couldNotDeleteTheClip": "Could not delete the clip.", + "clips.couldNotLoadClips": "Could not load clips.", + "clips.covers": "Covers", + "clips.customLength": "Custom clip length in seconds", + "clips.delete": "Delete", + "clips.deleteDescription": "A clip is usually the artifact you meant to keep. This removes the file; the recording it came from is untouched.", + "clips.deleteTitle": "Delete this clip?", + "clips.deleted": "Clip deleted.", + "clips.download": "Download", + "clips.everythingHeld": "Everything the buffer is holding", + "clips.file": "File", + "clips.held": "Held", + "clips.inMemory": "In memory", + "clips.keepBuffer": "Keep a rolling buffer", + "clips.keyframes": "Keyframes", + "clips.length": "Length", + "clips.none": "none", + "clips.off": "off", + "clips.onDisk": "On disk", + "clips.seen": "seen", + "clips.settings": "Settings", + "clips.size": "Size", + "clips.subtitle": "A rolling buffer of what just went out. Capture it after the fact — nothing has to be armed in advance.", + "clips.theClipDoesNotStart": "The clip does not start on a keyframe.", + "clips.title": "Clips", + "clips.waitingForStream": "waiting for stream", + "clips.window": "Window", + "clips.windowSeconds": "Window (seconds)", "common.add": "Add", + "common.apply": "Apply", "common.cancel": "Cancel", "common.close": "Close", "common.confirm": "Confirm", @@ -28,110 +229,921 @@ "common.copy": "Copy", "common.delete": "Delete", "common.disabled": "Disabled", + "common.discard": "Discard", "common.edit": "Edit", "common.enabled": "Enabled", "common.loading": "Loading…", + "common.moreInfo": "More information", "common.refresh": "Refresh", "common.restart": "Restart", "common.retry": "Retry", "common.save": "Save", "common.start": "Start", "common.stop": "Stop", - "state.live": "Live", - "state.reconnecting": "Reconnecting", - "state.starting": "Starting", - "state.failed": "Failed", - "state.stopped": "Stopped", - "state.offline": "Offline", - "hooks.title": "Lifecycle webhooks", - "hooks.new": "New webhook", - "hooks.empty": "No webhooks. Add one and polyemesis will POST a signed JSON body the moment the stream starts or stops, or a destination goes up or down — one delivery per transition, in order, for a script to act on.", - "hooks.colWebhook": "Webhook", - "hooks.colEndpoint": "Endpoint", - "hooks.colTriggers": "Subscribed to", - "hooks.colEnabled": "On", - "hooks.signed": "signed", - "hooks.unsigned": "UNSIGNED", + "dash.audioTracks": "Audio tracks", + "dash.bitrate": "Bitrate", + "dash.broadcastSettings": "Broadcast settings", + "dash.categoryPlaceholder": "Music", + "dash.clipboardUnavailable": "Clipboard is unavailable on this origin.", + "dash.connectPrompt": "Connect a YouTube, Twitch, Facebook or Kick account in Settings → Platform credentials to set your stream title, description and category on every platform at once. Each one accepts a different subset — the composer names which, and pushes what it can.", + "dash.copyIngestUrl": "Copy ingest URL", + "dash.couldNotLoadTheDestination": "Could not load the destination.", + "dash.couldNotPushTheMetadata": "Could not push the metadata.", + "dash.couldNotReorderTheDestinations": "Could not reorder the destinations.", + "dash.deleteDest": "Delete destination", + "dash.deleteDestDescription": "This removes the destination and its routing profile. The platform is untouched, and you can add it back — but the audio routing you set up for it is gone.", + "dash.descriptionPlaceholder": "What this stream is.", + "dash.destDeleted": "Destination deleted.", + "dash.disabled": "disabled", + "dash.everyConnectedAccount": "every connected account", + "dash.idle": "idle", + "dash.ingestUrlCopied": "Ingest URL copied.", + "dash.keyRefreshed": "Stream key refreshed from the platform.", + "dash.leaveUnchanged": "Leave unchanged", + "dash.leaveUnchangedNote": "“Leave unchanged” sends nothing at all, so the platform keeps what it has. Choosing On or Off writes that value.", + "dash.metaCategory": "Category", + "dash.metaDescription": "Description", + "dash.metaTags": "Tags", + "dash.metaTitle": "Title", + "dash.noDestinations": "No destinations yet. Add one, then choose which audio tracks it receives.", + "dash.off": "Off", + "dash.pipeline": "Pipeline", + "dash.pushToPlatforms": "Push to platforms", + "dash.pushing": "Pushing…", + "dash.reconnects": "Reconnects", + "dash.relaySubscribers": "Relay subscribers", + "dash.renditions": "Renditions", + "dash.scheduledStart": "Scheduled start", + "dash.subtitle": "Ingest once, fan out with shared video and per-destination audio.", + "dash.tagsPlaceholder": "live, house, dj set", + "dash.theConnectedAccount": "the connected account", + "dash.title": "Dashboard", + "dash.titlePlaceholder": "Friday night set", + "dash.uptime": "Uptime", + "dash.youtubeStopsAcceptingTheseOnce": "YouTube stops accepting these once a broadcast leaves “created” or “ready”.", + "dest.audience": "Audience", + "dest.audioBitrate": "Audio bitrate", + "dest.audioCodec": "Audio codec", + "dest.channels": "Channels", + "dest.compliance": "Compliance", + "dest.connectedAccount": "Connected account", + "dest.contentLabels": "Content labels", + "dest.copySource": "Copy the source video", + "dest.createVariant": "Create this variant", + "dest.customiseSummary": "Customise for this destination", + "dest.fbBackupCost": "Doubles this destination’s upload bandwidth.", + "dest.fbBackupHelp": "Sends a second copy of this stream to Facebook’s backup ingest, so a dropped connection does not drop the broadcast.", + "dest.fbBackupLabel": "Publish a backup feed", + "dest.fbBackupReconnect": "Turning it on reconnects the stream once — so enable it before you go live.", + "dest.fbBoxIntro": "Sent on the same create call as Audience, so the same rule applies: applied once when the broadcast starts, and left alone entirely if empty.", + "dest.fbBoxTitle": "Facebook crossposting & donate button", + "dest.fbCrosspostAdd": "Add Page", + "dest.fbCrosspostAlsoPost": "Also post", + "dest.fbCrosspostHelp": "The numeric Page ID from Facebook’s own console (Page settings, or graph.facebook.com/me/accounts while signed in as the Page) — there is no lookup here, so paste the id rather than a name or URL. “Also post” publishes as that Page rather than only sharing the broadcast to it; left off, only the quieter share happens.", + "dest.fbCrosspostLabel": "Crosspost to Pages", + "dest.fbCrosspostPageId": "Page ID", + "dest.fbCrosspostRemove": "Remove this Page", + "dest.fbDonateHelp": "Also an opaque id from Facebook’s fundraisers console, not a charity name to search for. Leave blank to attach no donate button at all.", + "dest.fbDonateLabel": "Donate button charity ID", + "dest.fbDonatePlaceholder": "Charity ID", + "dest.fillFromPlatform": "Fill from what this platform publishes", + "dest.friends": "Friends", + "dest.friendsOfFriends": "Friends of friends", + "dest.giveUpAfter": "Give up after (retries)", + "dest.leaveOnFacebook": "Leave as it is on Facebook", + "dest.leaveOnYouTube": "Leave as it is on YouTube", + "dest.madeForKids": "Made for kids (COPPA)", + "dest.muxQueue": "Muxing queue (packets)", + "dest.muxQueuePair": "These are a pair: FFmpeg applies the packet cap only once the queue has grown past the byte threshold, so a threshold on its own does nothing and will be refused. Raise them if a destination reports interleave errors — the audio path here has variable latency, because loudness normalisation reads ahead.", + "dest.name": "Name", + "dest.namePlaceholder": "YouTube — main channel", + "dest.notLinked": "Not linked", + "dest.onlyMe": "Only me", + "dest.platform": "Platform", + "dest.private": "Private", + "dest.public": "Public", + "dest.reconnecting": "Reconnecting", + "dest.retryAfter": "Retry after (seconds)", + "dest.sharedEncodeAria": "Shared video encode", + "dest.socketTimeout": "Socket timeout (seconds)", + "dest.streamKeyLabel": "Stream key", + "dest.transport": "Transport", + "dest.transportFile": "Local file", + "dest.transportRtmp": "RTMP / RTMPS", + "dest.unlisted": "Unlisted", + "dest.urlFromDashboard": "URL from dashboard", + "dest.useSharedEncode": "Use a shared video encode", + "dest.varBitrate": "Video bitrate (kbps)", + "dest.varFps": "Frame rate", + "dest.varHeight": "Height", + "dest.varSourceHint": "source", + "dest.varWidth": "Width", + "dest.variantWarning": "Starts a second encode from your source, seeded from “{name}”. It is not a free variation and it does not change what the other destinations on that encode receive. Leave a field blank to keep the source's.", + "dest.videoTreatment": "Video treatment", + "dest.visibility": "Visibility", + "hooks.allTriggers": "everything", "hooks.attempts": "{count} attempts", + "hooks.attemptsLabel": "Attempts", "hooks.attemptsOne": "{count} attempt", - "hooks.timeout": "{seconds}s timeout", - "hooks.allTriggers": "everything", - "hooks.triggerCount": "{count} triggers", - "hooks.triggerCountOne": "{count} trigger", - "hooks.enableAria": "Enable {name}", + "hooks.colEnabled": "On", + "hooks.colEndpoint": "Endpoint", + "hooks.colTriggers": "Subscribed to", + "hooks.colWebhook": "Webhook", + "hooks.create": "Create", + "hooks.created": "Webhook created.", + "hooks.deleteBody": "It stops delivering immediately. The endpoint is untouched, and you can recreate the hook — but its signing key cannot be recovered.", + "hooks.deleteConfirm": "Delete webhook", + "hooks.deleteFailed": "Could not delete the hook.", + "hooks.deleteTitle": "Delete this webhook?", + "hooks.deleted": "Webhook deleted.", "hooks.deliveries": "Recent deliveries", - "hooks.sendTest": "Send test delivery", - "hooks.sendTestTitle": "Send a test delivery now", - "hooks.secretTitle": "Signing key — copy it now, it cannot be shown again", - "hooks.secretCopied": "I have copied it", - "hooks.httpStatus": "HTTP {status}", - "hooks.noResponse": "no response", + "hooks.deliveriesFailed": "Could not read recent deliveries.", + "hooks.deliveryTitle": "Delivery", + "hooks.dialogIntro": "One signed POST per transition, in order. Unlike an alert, nothing is coalesced or debounced — a script gets every edge.", "hooks.durationMs": "{ms} ms", + "hooks.editTitle": "Edit webhook", + "hooks.empty": "No webhooks. Add one and polyemesis will POST a signed JSON body the moment the stream starts or stops, or a destination goes up or down — one delivery per transition, in order, for a script to act on.", + "hooks.enableAria": "Enable {name}", + "hooks.httpStatus": "HTTP {status}", + "hooks.loadFailed": "Could not load webhooks.", + "hooks.name": "Name", + "hooks.new": "New webhook", + "hooks.newTitle": "New webhook", "hooks.noDeliveries": "Nothing delivered yet. Deliveries appear here as transitions happen.", - "hooks.deliveryTitle": "Delivery", - "hooks.statSent": "Sent", - "hooks.statFailed": "Failed", + "hooks.noResponse": "no response", + "hooks.payloadNote": "Payload {version}. Signature in {header}, over the timestamp and body.", + "hooks.retriesHint": "Retries block this endpoint’s own queue, because ordering is the promise. A 4xx is never retried — an endpoint saying the request is wrong will say it again.", + "hooks.saveFailed": "Could not save the hook.", + "hooks.secretCopied": "I have copied it", + "hooks.secretTitle": "Signing key — copy it now, it cannot be shown again", + "hooks.sendTest": "Send test delivery", + "hooks.sendTestTitle": "Send a test delivery now", + "hooks.signed": "signed", + "hooks.statDropped": "Dropped", "hooks.statEndpoints": "Endpoints", + "hooks.statFailed": "Failed", "hooks.statQueued": "Queued", - "hooks.statDropped": "Dropped", "hooks.statRetries": "Retries", - "hooks.payloadNote": "Payload {version}. Signature in {header}, over the timestamp and body.", - "hooks.deleteTitle": "Delete this webhook?", - "hooks.deleteBody": "It stops delivering immediately. The endpoint is untouched, and you can recreate the hook — but its signing key cannot be recovered.", - "hooks.deleteConfirm": "Delete webhook", - "hooks.editTitle": "Edit webhook", - "hooks.newTitle": "New webhook", - "hooks.dialogIntro": "One signed POST per transition, in order. Unlike an alert, nothing is coalesced or debounced — a script gets every edge.", - "hooks.name": "Name", - "hooks.url": "Endpoint URL", - "hooks.urlMasked": "The stored URL is never shown in full. Leave the masked value to keep it, or type a new URL to replace it.", - "hooks.triggers": "Triggers", - "hooks.triggersNoneHint": "None selected means every trigger, including ones added by a future release.", + "hooks.statSent": "Sent", + "hooks.testFailed": "The endpoint did not accept the test delivery.", + "hooks.timeout": "{seconds}s timeout", "hooks.timeoutLabel": "Timeout (s)", - "hooks.attemptsLabel": "Attempts", - "hooks.retriesHint": "Retries block this endpoint’s own queue, because ordering is the promise. A 4xx is never retried — an endpoint saying the request is wrong will say it again.", - "hooks.create": "Create", - "hooks.triggerIngestPublished": "Stream started", - "hooks.triggerIngestDisconnected": "Stream stopped", - "hooks.triggerDestinationUp": "Destination live", - "hooks.triggerDestinationDown": "Destination stopped", - "hooks.loadFailed": "Could not load webhooks.", + "hooks.title": "Lifecycle webhooks", "hooks.toggleFailed": "Could not change the hook.", - "hooks.deleted": "Webhook deleted.", - "hooks.deleteFailed": "Could not delete the hook.", - "hooks.testFailed": "The endpoint did not accept the test delivery.", - "hooks.deliveriesFailed": "Could not read recent deliveries.", + "hooks.triggerCount": "{count} triggers", + "hooks.triggerCountOne": "{count} trigger", + "hooks.triggerDestinationDown": "Destination stopped", + "hooks.triggerDestinationUp": "Destination live", + "hooks.triggerIngestDisconnected": "Stream stopped", + "hooks.triggerIngestPublished": "Stream started", + "hooks.triggers": "Triggers", + "hooks.triggersNoneHint": "None selected means every trigger, including ones added by a future release.", + "hooks.unsigned": "UNSIGNED", "hooks.updated": "Webhook updated.", - "hooks.created": "Webhook created.", - "hooks.saveFailed": "Could not save the hook.", - "playlist.title": "Playlist", + "hooks.url": "Endpoint URL", + "hooks.urlMasked": "The stored URL is never shown in full. Leave the masked value to keep it, or type a new URL to replace it.", + "jobs.alwaysKeepNewest": "Always keep newest", + "jobs.automaticModel": "Automatic (chosen from this hardware)", + "jobs.backToDefault": "Go back to the default mode for this kind", + "jobs.batteryFloor": "Battery floor %", + "jobs.broadcastGoing": "A broadcast is going out; deferred work yields to it.", + "jobs.cancel": "Cancel", + "jobs.cannotPause": "These kinds should be paused but cannot be, so they are finishing instead.", + "jobs.completed": "Completed", + "jobs.concurrency": "Concurrency", + "jobs.couldNotReadTheJob": "Could not read the job queue.", + "jobs.couldNotSaveThePolicy": "Could not save the policy.", + "jobs.cpuCeiling": "CPU ceiling %", + "jobs.cpuResume": "CPU resume %", + "jobs.defaultMode": "Default mode", + "jobs.defaultModel": "Default model", + "jobs.deferred": "Deferred", + "jobs.failed": "Failed", + "jobs.finished": "Finished", + "jobs.global": "Global", + "jobs.gpuGateNote": "Only kinds marked as using the GPU are gated by this.", + "jobs.history": "History", + "jobs.historyRetention": "History retention", + "jobs.holdDeferredWorkBackOn": "Hold deferred work back on a discharging laptop below this. 0 disables it.", + "jobs.hostCpuLoad": "Host CPU load", + "jobs.howManyJobsMayRun": "How many jobs may run at once. One, because a second transcode buys throughput nobody asked for at a risk nobody accepted.", + "jobs.inQueue": "In the queue", + "jobs.job": "Job", + "jobs.keepForDays": "Keep for days", + "jobs.machineSafety": "Machine safety. This is the one gate realtime work obeys too.", + "jobs.niceLevel": "Nice level", + "jobs.noFinished": "No finished jobs yet.", + "jobs.noModels": "No model files were found in the models directory. Automatic will download the one it picks; naming a specific model here only works once that file exists.", + "jobs.nothingNewStartsAboveThis": "Nothing new starts above this. 0 disables the gate.", + "jobs.nothingQueued": "Nothing queued. Post-production is submitted from the library.", + "jobs.nothingWasOldEnoughTo": "Nothing was old enough to purge. Lower the retention in Policy first.", + "jobs.pauseAll": "Pause all", + "jobs.policy": "Policy", + "jobs.policySaved": "Resource policy saved.", + "jobs.progress": "Progress", + "jobs.queuePaused": "Queue paused.", + "jobs.queueResumed": "Queue resumed.", + "jobs.queued": "Queued", + "jobs.recording": "Recording", + "jobs.releasedItWillStartAs": "Released. It will start as soon as a slot is free.", + "jobs.removeFromHistory": "Remove from history", + "jobs.removeWindow": "Remove window", + "jobs.resumeAll": "Resume all", + "jobs.retry": "Retry", + "jobs.retryTitle": "Re-arm with a fresh attempt budget", + "jobs.runNow": "Run now", + "jobs.runNowTitle": "Run this one now, ignoring the resource policy", + "jobs.running": "Running", + "jobs.savedTheConcurrencyChangeTakes": "Saved. The concurrency change takes effect when the server restarts — the queue fixes that when it starts.", + "jobs.state": "State", + "jobs.stopsEverythingRealtimeIncludedA": "Stops everything, realtime included: a CPU that is throttling has already begun degrading the stream. 0 disables it.", + "jobs.subtitle": "Background work: transcription, proxies, thumbnails and archive compression.", + "jobs.thatDidNotWork": "That did not work.", + "jobs.thermalCeiling": "Thermal ceiling °C", + "jobs.thisManyFinishedJobsSurvive": "This many finished jobs survive whatever their age.", + "jobs.title": "Jobs", + "jobs.transcription": "Transcription", + "jobs.unsavedChanges": "Unsaved changes.", + "jobs.whereTheCeilingReleasesThe": "Where the ceiling releases. The gap is the hysteresis that stops the gate oscillating.", + "jobs.whisperMissing": "whisper.cpp is not installed here, so this has no effect yet. It is still worth setting: the choice applies as soon as it is.", + "jobs.why": "Why", + "jobs.windowEnd": "Window end", + "jobs.windowStart": "Window start", + "jobs.yieldNote": "Heavy work is queued and yields to the live stream. A dropped frame is unrecoverable; a transcript arriving an hour later costs nothing.", + "lib.anySpeaker": "Any speaker", + "lib.anyTag": "Any tag", + "lib.autoGrouped": "Grouped automatically. Editing it makes it yours.", + "lib.autoGroupedNote": "This session was grouped automatically. Saving makes it yours, and the grouping pass will stop rewriting it.", + "lib.bestMatch": "Best match", + "lib.clearSearch": "Clear search", + "lib.clip": "Clip", + "lib.clipTitle": "Cut a clip from this recording", + "lib.collapseSession": "Collapse session", + "lib.couldNotLoadTheLibrary": "Could not load the library.", + "lib.couldNotLoadTheSession": "Could not load the session's segments.", + "lib.couldNotQueueTheProxy": "Could not queue the proxy.", + "lib.couldNotRegroup": "Could not regroup.", + "lib.couldNotSaveTheSession": "Could not save the session.", + "lib.delete": "Delete", + "lib.deleteDescription": "The file is removed from disk, along with its proxies, thumbnails and transcript. This cannot be undone.", + "lib.deleteTitle": "Delete this recording?", + "lib.downloadMaster": "Download master", + "lib.downloadMasterTitle": "Download the multitrack master", + "lib.editSession": "Edit session", + "lib.expandSession": "Expand session", + "lib.from": "From", + "lib.generateProxy": "Generate proxy", + "lib.generateProxyTitle": "Generate the browser-playable proxy", + "lib.mostRecent": "Most recent", + "lib.noProxyYet": "No proxy yet — open to generate one", + "lib.noSegments": "No segments here.", + "lib.noSessionMatch": "No session matches those filters.", + "lib.nothingRecorded": "Nothing recorded yet. Sessions appear as the recorder writes segments.", + "lib.oldestFirst": "Oldest first", + "lib.playProxy": "Play the proxy", + "lib.proxyAlreadyQueued": "A proxy is already queued.", + "lib.proxyQueued": "Proxy queued.", + "lib.recordingDeleted": "Recording deleted.", + "lib.regroupTitle": "Re-runs the grouping. Additive and idempotent: it never merges a grouping you split by hand.", + "lib.searchPlaceholder": "Search everything anyone said, across every recording…", + "lib.searchTranscripts": "Search transcripts", + "lib.searchable": "Searchable", + "lib.searchableOnly": "Segments with a transcript, so they are searchable", + "lib.session": "Session", + "lib.sessionDescription": "Description", + "lib.sessionSaved": "Session saved.", + "lib.sessionTags": "Tags", + "lib.sessionTitle": "Title", + "lib.subtitle": "Every broadcast, searchable. Each microphone was recorded on its own track, so the transcript already knows who said it.", + "lib.tag": "Tag", + "lib.tagsPlaceholder": "comma, separated", + "lib.title": "Library", + "lib.transcribe": "Transcribe", + "lib.transcribeEachTrackOnIts": "Transcribe each track on its own — that is what gives speaker attribution", + "lib.ungrouped": "Ungrouped segments", + "meters.deviation": "Deviation", + "meters.idle": "idle", + "meters.integrated": "Integrated", + "meters.loudness": "Loudness compliance", + "meters.metering": "metering", + "meters.momentary": "Momentary", + "meters.monitorOff": "The monitor is off. Turn it on to measure what each destination is delivering.", + "meters.noSignal": "no signal on this track", + "meters.nothingToMeasure": "Nothing to measure yet. Each running destination gets its own EBU R128 analyser on the mix it receives.", + "meters.range": "Range", + "meters.shortTerm": "Short term", + "meters.subtitle": "Every channel of every ingest track, straight from the relay.", + "meters.title": "Audio meters", + "meters.truePeak": "True peak", + "meters.waitingForStream": "waiting for stream", + "mon.allProcesses": "All processes", + "mon.bitrateChart": "Ingest bitrate — last 30 minutes", + "mon.chooseDestination": "Choose a destination…", + "mon.clearLog": "Clear log", + "mon.clearedThisDestinationIsBack": "Cleared. This destination is back on the generated command.", + "mon.dryRun": "Dry run", + "mon.dryRunInvalid": "FFmpeg rejected an argument", + "mon.dryRunOk": "FFmpeg accepted it", + "mon.extraInput": "Extra input arguments", + "mon.extraOutput": "Extra output arguments", + "mon.ffmpegLog": "FFmpeg log", + "mon.fromRunningProcess": "from the running process", + "mon.fullCommand": "Full resolved command", + "mon.hostCpu": "Host CPU", + "mon.ingestLoss": "Ingest loss", + "mon.memory": "Memory", + "mon.nothingProvenEitherWay": "Nothing proven either way", + "mon.pid": "PID", + "mon.processes": "Processes", + "mon.rebuilt": "rebuilt", + "mon.relayDrops": "Relay drops", + "mon.relayIn": "Relay in", + "mon.resolveHint": "Resolve the command to see exactly what will run before applying.", + "mon.restarts": "Restarts", + "mon.subtitle": "Process health, host resources, and the live FFmpeg log.", + "mon.title": "Monitoring", + "mon.uptime": "Uptime", + "nav.automation": "Automation", + "nav.chat": "Chat", + "nav.clips": "Clips", + "nav.dashboard": "Dashboard", + "nav.jobs": "Jobs", + "nav.library": "Library", + "nav.meters": "Audio meters", + "nav.monitoring": "Monitoring", + "nav.playout": "Playout", + "nav.recordings": "Recordings", + "nav.renditions": "Renditions", + "nav.routing": "Routing", + "nav.settings": "Settings", + "nav.sources": "Sources", + "play.acrossEveryRungTheBackstop": "Across every rung. The backstop that keeps a long run from filling the disk.", + "play.allowCrossOrigin": "Allow cross-origin", + "play.allowCrossOriginPlayback": "Allow cross-origin playback", + "play.audioBitrate": "Audio bitrate (kbps)", + "play.audioTrack": "Audio track", + "play.basicPasswordNote": "Players that cannot follow a link (a set-top box, a CDN) can send the same secret as an HTTP basic password instead.", + "play.boundsTheViewerTableViewers": "Bounds the viewer table. Viewers past it are served, just not counted.", + "play.carriesThePlaybackTokenAnyone": "Carries the playback token. Anyone with this URL can watch.", + "play.clipboardUnavailable": "Clipboard is unavailable; select and copy by hand", + "play.couldNotLoadPlayout": "Could not load playout", + "play.disabled": "Disabled", + "play.diskCap": "Disk cap (MB)", + "play.dvrWindow": "DVR window (s)", + "play.embedCode": "Embed code", + "play.enable": "Enable playout", + "play.enabled": "Enabled", + "play.forObsACdnOr": "For OBS, a CDN, or any player that speaks HLS.", + "play.format": "Format", + "play.hls": "HLS", + "play.hlsDash": "HLS and DASH", + "play.hlsPlaylist": "HLS playlist", + "play.idleTimeout": "Viewer idle timeout (s)", + "play.linkReset": "New playback link generated. The previous one no longer works.", + "play.linksProtected": "These links only work for a signed-in administrator until you turn on Public.", + "play.linksPublic": "These links work without an account.", + "play.liveWindow": "Live window (segments)", + "play.metaDescription": "Description", + "play.metaTitle": "Title", + "play.mustExceedOneSegmentOr": "Must exceed one segment, or viewers flicker out between polls.", + "play.name": "Name", + "play.noCredentialNote": "No credential of any kind is required. Only appropriate for a stream you intend the whole internet to see.", + "play.noRungs": "No rungs yet. Add one to publish anything.", + "play.onDisk": "On disk", + "play.oneStereoAacTrackPer": "One stereo AAC track per rung.", + "play.packaging": "Packaging", + "play.passthrough": "Ingest (passthrough)", + "play.peak": "Peak", + "play.playerPage": "Player page", + "play.posterNote": "Shown above the video on the public page. The poster is a recent keyframe, refreshed automatically.", + "play.private": "Private.", + "play.protectedNote": "Playback is protected by default. Opening it is a separate, deliberate step.", + "play.protection": "Protection", + "play.public": "Public", + "play.resetFailed": "Could not reset", + "play.restartNote": "Changing any of these restarts the muxers, which interrupts viewers for a few seconds.", + "play.rungNote": "Each rung copies its rendition's video bit-for-bit, so adding one costs a muxer and never a second video encode. Only the audio track differs per rung; per-destination routing is untouched.", + "play.segmentLength": "Segment length (s)", + "play.serveWithoutSession": "Serve viewers without a session", + "play.sessionCap": "Session cap", + "play.sessions": "Sessions", + "play.share": "Share", + "play.shorterIsLowerLatencyAnd": "Shorter is lower latency and more playlist churn.", + "play.subtitle": "Serve the stream to viewers directly, as public HLS.", + "play.threeIsTheMinimumA": "Three is the minimum a player can buffer on.", + "play.title": "Playout", + "play.unlisted": "Unlisted.", + "play.untitled": "Untitled stream", + "play.videoSource": "Video source", + "play.watchingNow": "Watching now", + "play.whoCanWatch": "Who can watch", + "player.copied": "Copied", + "player.copy": "Copy", + "player.loading": "Loading…", + "player.missingToken": "The link you followed is missing its playback token, or the token has been changed. Ask whoever shared it for a current link.", + "player.notLive": "Not live right now", + "player.viewer": "viewer", + "player.viewers": "viewers", + "player.willStart": "This page will start playing on its own when the broadcast begins.", + "playlist.choose": "Choose an upload", + "playlist.chooseAria": "Choose an upload to add", "playlist.empty": "Nothing queued yet.", + "playlist.hint": "Uploaded in Media. An item this playlist names that later loses its upload shows as needing attention above, rather than failing silently the next time failover reaches it.", "playlist.itemsAria": "Playlist items", - "playlist.stateReady": "Ready", - "playlist.stateTranscoding": "Transcoding", - "playlist.stateAttention": "Needs attention", - "playlist.stateUnsaved": "Not saved yet", "playlist.missingUpload": "This upload can no longer be found.", - "playlist.moveUp": "Move {name} up", "playlist.moveDown": "Move {name} down", - "playlist.remove": "Remove {name}", - "playlist.choose": "Choose an upload", - "playlist.chooseAria": "Choose an upload to add", + "playlist.moveUp": "Move {name} up", "playlist.noUploads": "No uploads available", - "playlist.hint": "Uploaded in Media. An item this playlist names that later loses its upload shows as needing attention above, rather than failing silently the next time failover reaches it.", - "dest.fbBoxTitle": "Facebook crossposting & donate button", - "dest.fbBoxIntro": "Sent on the same create call as Audience, so the same rule applies: applied once when the broadcast starts, and left alone entirely if empty.", - "dest.fbBackupLabel": "Publish a backup feed", - "dest.fbBackupHelp": "Sends a second copy of this stream to Facebook’s backup ingest, so a dropped connection does not drop the broadcast.", - "dest.fbBackupCost": "Doubles this destination’s upload bandwidth.", - "dest.fbBackupReconnect": "Turning it on reconnects the stream once — so enable it before you go live.", - "dest.fbCrosspostLabel": "Crosspost to Pages", - "dest.fbCrosspostPageId": "Page ID", - "dest.fbCrosspostAlsoPost": "Also post", - "dest.fbCrosspostRemove": "Remove this Page", - "dest.fbCrosspostAdd": "Add Page", - "dest.fbCrosspostHelp": "The numeric Page ID from Facebook’s own console (Page settings, or graph.facebook.com/me/accounts while signed in as the Page) — there is no lookup here, so paste the id rather than a name or URL. “Also post” publishes as that Page rather than only sharing the broadcast to it; left off, only the quieter share happens.", - "dest.fbDonateLabel": "Donate button charity ID", - "dest.fbDonatePlaceholder": "Charity ID", - "dest.fbDonateHelp": "Also an opaque id from Facebook’s fundraisers console, not a charity name to search for. Leave blank to attach no donate button at all." + "playlist.remove": "Remove {name}", + "playlist.stateAttention": "Needs attention", + "playlist.stateReady": "Ready", + "playlist.stateTranscoding": "Transcoding", + "playlist.stateUnsaved": "Not saved yet", + "playlist.title": "Playlist", + "rec.delete": "Delete", + "rec.deleteDescription": "The file is removed from disk, along with its proxies, thumbnails and transcript. This cannot be undone.", + "rec.deleteFailed": "Could not delete the recording.", + "rec.deleteTitle": "Delete this recording?", + "rec.deleted": "Recording deleted.", + "rec.disabled": "disabled", + "rec.disk": "Disk", + "rec.diskRecordings": "Recordings", + "rec.download": "Download", + "rec.downloadNamed": "Download {name}", + "rec.duration": "Duration", + "rec.empty": "No recordings yet. Enable recording on the right and start a stream.", + "rec.enabled": "Enabled", + "rec.file": "File", + "rec.free": "Free", + "rec.haltedDefault": "Recording is halted: the volume is below the free-space floor.", + "rec.haltedRestarts": "The recorder restarts by itself once space is freed.", + "rec.help.enabled": "Writes every ingest to disk as it streams, keeping all audio tracks separate and unencoded. Recording is independent of what you send to platforms: turning it off does not affect your live output, and turning it on costs disk rather than upload bandwidth.", + "rec.help.maxAgeHours": "Deletes recordings older than this many hours, whatever the total size. Useful when you only ever need the last day or two. Set 0 to keep everything until some other limit removes it.", + "rec.help.maxGb": "A ceiling on the total size of recordings polyemesis keeps. Once past it, the oldest are deleted until the total fits again. Set 0 for no size limit — in which case age, or the free-space floor, is the only thing bounding growth.", + "rec.help.minFreeGb": "A hard stop: recording halts when the volume drops below this much free space and resumes on its own once space is freed. This is the one limit that accounts for files polyemesis did not write, so it is the setting that actually protects a shared disk from filling up.", + "rec.help.segmentSeconds": "How long each file is before the recorder starts a new one. Shorter segments limit what a crash can cost you and make individual files easier to move, but leave more of them to manage. Cuts land on a keyframe, so the real length varies by a second or so.", + "rec.help.stemFiles": "Also writes each audio track as its own file alongside the master. Useful when you edit afterwards and want the host, the guests and the game audio on separate channels. It multiplies what a session writes by roughly the number of tracks, which is why it is off by default.", + "rec.help.stemFormat": "Both options are lossless, so neither costs quality. FLAC is about half the size and is what you want unless a tool in your workflow cannot read it; WAV opens in anything. Tracks wider than 8 channels fall back to WAV automatically.", + "rec.hideStems": "Hide stems", + "rec.keepForever": "0 = keep forever.", + "rec.loadFailed": "Could not load recordings.", + "rec.maxAgeHours": "Delete after (hours)", + "rec.maxGb": "Keep at most (GB)", + "rec.minFreeGb": "Stop below free space (GB)", + "rec.minFreeNote": "The caps above only bound what polyemesis wrote; anything else on the volume can still fill it. 0 = no floor.", + "rec.noSizeLimit": "0 = no size limit.", + "rec.recording": "recording", + "rec.retentionNote": "Retention runs every 30 seconds: oldest first, by age then by total size. The segment currently being written is never deleted.", + "rec.saveFailed": "Could not save the settings.", + "rec.savePolicy": "Save retention policy", + "rec.saved": "Recording settings saved.", + "rec.segmentSeconds": "Segment length (seconds)", + "rec.segments": "Segments", + "rec.settingsTitle": "Recording", + "rec.showStems": "Show stems", + "rec.size": "Size", + "rec.started": "Started", + "rec.stemFiles": "Per-track stem files", + "rec.stemFlac": "FLAC — lossless, about half the size", + "rec.stemFormat": "Stem format", + "rec.stemNote": "Written from the same process as the master, so every stem stays sample-aligned with the others. Tracks wider than 8 channels fall back to WAV on their own.", + "rec.stemWav": "WAV — lossless, opens anywhere", + "rec.stems": "Stems", + "rec.stemsTitle": "{count} per-track stem files, sample-aligned with the master", + "rec.stemsUsage": "Stems ({count} files)", + "rec.subtitle": "The full multitrack archive: every ingest audio track preserved, unencoded.", + "rec.title": "Recordings", + "rec.tracks": "Tracks", + "rec.tracksTitle": "{count} audio tracks preserved", + "rec.used": "Used", + "rec.volume": "Volume", + "rend.aspectBlurpad": "Blurred fill", + "rend.aspectBlurpadHint": "Fills the remainder with a blurred, cropped-to-fill copy of the frame itself. This is the convention vertical feeds have settled on.", + "rend.aspectCrop": "Crop to fill", + "rend.aspectCropHint": "Centre-crops to the target shape, then scales. Subjects keep their on-screen size; the edges of the frame are gone.", + "rend.aspectHandling": "Aspect handling", + "rend.aspectPad": "Letterbox", + "rend.aspectPadHint": "Scales the whole frame to fit and fills the rest with a flat colour. Nothing is lost, but a 16:9 source on a 9:16 canvas is mostly bars.", + "rend.aspectStretch": "Stretch to fit", + "rend.aspectStretchHint": "Scales to the target size and lets the picture distort if the source disagrees. What every rendition did before the other modes existed.", + "rend.boxColour": "Box colour", + "rend.boxOpacity": "Box opacity (%)", + "rend.builtInFont": "Built-in default", + "rend.burnedText": "Burned-in text", + "rend.chooseHardware": "Choose a hardware encoder, or a smaller output.", + "rend.colourBlack": "black", + "rend.colourWhite": "white", + "rend.costBeyond": "Software encoding this in real time is beyond most machines", + "rend.costBeyondDetail": "{label} is {mpps} for a single encode. x264 at this size usually loses the race with real time, and a rendition that falls behind drops frames for every destination reading it — which you find out mid-stream.{machine} {escape}", + "rend.costBusy": "Expect several cores to be busy", + "rend.costBusyDetail": "{label} is {mpps} on {encoder}, which is a heavy real-time encode.{machine} Watch the encoder's speed once it is live: below 1.0× it is falling behind and the destinations under it will start dropping frames. {escape}", + "rend.costMachineCores": " This machine reports {cores} logical cores.", + "rend.costModest": "Modest software encode", + "rend.costModestDetail": "{label} is {mpps}, within reach of most machines.{machine} It is still one real encode — shared by every destination that selects it, but not free.", + "rend.costNoNumbers": "Cost cannot be estimated yet", + "rend.costNoNumbersDetail": "Nothing has probed the source, so “keep source” has no numbers behind it. Start the ingest and this fills in.", + "rend.costOffloaded": "Offloaded to {encoder}", + "rend.costOffloadedDetail": "{label} ({mpps}) runs on the GPU's fixed-function encoder, so it costs little CPU. Quality and rate control vary by driver, and every chip has a ceiling of its own — check the output before a show that matters.", + "rend.create": "Create", + "rend.created": "Rendition created. Select it on a destination to start the encode.", + "rend.custom": "Custom…", + "rend.deintAll": "Every frame", + "rend.deintAllHint": "For sources that are interlaced but do not say so. Plenty of SDI bridges and capture cards flag everything progressive regardless of what they were fed, and on those “only interlaced” is a no-op that looks like a broken setting.", + "rend.deintAuto": "Only interlaced frames", + "rend.deintAutoHint": "Touches only frames the source flagged as interlaced, so progressive frames pass through untouched. The right choice for anything mixed.", + "rend.deintOff": "Off", + "rend.deintOffHint": "Right for the progressive sources almost everyone has. Deinterlacing one softens it for no gain.", + "rend.deinterlace": "Deinterlace", + "rend.deleteFailed": "Could not delete the rendition.", + "rend.deleteFallbackMany": "{count} destinations fall back to passthrough and will be sent the source video unchanged. Check the source still fits what each platform accepts.", + "rend.deleteFallbackOne": "{count} destination falls back to passthrough and will be sent the source video unchanged. Check the source still fits what each platform accepts.", + "rend.deleteNoDestinations": "Nothing selects this rendition, so deleting it changes no destination.", + "rend.deleteRenditionTitle": "Delete {name}?", + "rend.editRendition": "Edit rendition", + "rend.empty": "No renditions. Every destination is on passthrough, which is exactly right until one of them cannot take the source — a 4K60 ingest that Twitch, Kick or X will not accept, say. Add a rendition and point those destinations at it.", + "rend.encoder": "Encoder", + "rend.encoderPreset": "Encoder preset", + "rend.encoderRelatedFailed": "{name} was not offered because a related encoder failed here.", + "rend.encoderTestFailed": "{name} failed a one-frame test encode on this machine.", + "rend.encoderTestOk": "{name} encoded a test frame on this machine.", + "rend.evenNote": "0 on an axis keeps the source's and preserves the aspect ratio — set height alone to rescale. Both must be even numbers: H.264 and HEVC have no odd-sized chroma plane.", + "rend.font": "Font", + "rend.frameRate": "Frame rate", + "rend.gop": "GOP", + "rend.gpuUnopenable": "A GPU is present but this process cannot open it", + "rend.hardwareFallback": "hardware", + "rend.hardwareScanFound": "What the hardware scan found", + "rend.height": "Height", + "rend.hevcWarning": "This encoder produces HEVC. Most RTMP destinations accept H.264 only — pick it only for a destination that has told you it takes HEVC.", + "rend.keyframeInterval": "Keyframe interval", + "rend.letterboxColour": "Letterbox colour", + "rend.live": "Live", + "rend.loadFailed": "Could not load the renditions.", + "rend.marginX": "Margin X (%)", + "rend.marginY": "Margin Y (%)", + "rend.name": "Name", + "rend.namePlaceholder": "1080p60 for Twitch and Kick", + "rend.needsBothAWidthAnd": "Needs both a width and a height — with one axis free the scale already preserves the aspect ratio.", + "rend.needsBothAxes": "Needs both a width and a height", + "rend.newRendition": "New rendition", + "rend.noEncoder": "This FFmpeg reported no usable video encoder.", + "rend.noHardwareEscape": "No hardware encoder works on this machine, so there is nothing to move this onto — the only lever here is a smaller output. Re-detect hardware if you have since installed a driver or passed a GPU into the container.", + "rend.notProbed": "Nothing has been test-encoded on this machine yet, so this list is what the FFmpeg build contains rather than what the hardware can do. Re-detect to find out.", + "rend.notRunning": "Not running", + "rend.note": "Note", + "rend.noteDuplicateFps": "The source runs at {srcFps} fps, so {fps} fps only duplicates frames.", + "rend.notePlaceholder": "What this tier is for.", + "rend.noteUpscale": "The source is {width}×{height}. Scaling up costs CPU and adds no detail.", + "rend.opacity": "Opacity (%)", + "rend.percentNote": "Everything is a percentage of the frame, so the same settings are correct on a 16:9 tier and a 9:16 one. Margins are ignored on a centred axis.", + "rend.preset": "Preset", + "rend.presetPlaceholder": "veryfast", + "rend.presetStartingPoint": "Starting point — verify current limits with the platform.", + "rend.redetectFailed": "Could not re-detect the hardware.", + "rend.redetectTitle": "Re-run the GPU scan and encode one test frame with each encoder. Takes a few seconds.", + "rend.redetectedNoHardware": "Hardware re-detected. No hardware encoder passed a test encode on this machine.", + "rend.resolution": "Resolution", + "rend.restartEncode": "Restart this encode and the destinations reading it", + "rend.restartFailed": "Could not restart the encode.", + "rend.restarting": "{name} is restarting. Its destinations reconnect with it.", + "rend.save": "Save", + "rend.saveFailed": "Could not save the rendition.", + "rend.seedNote": "These seed the form and nothing more — edit anything before saving. Platform ceilings change and differ by partner status.", + "rend.signalError": "Error", + "rend.signalIdle": "Idle", + "rend.sizePct": "Size (% of height)", + "rend.softwareEncoderFallback": "a software encoder", + "rend.speed": "Speed", + "rend.subtitle": "One shared video encode, many destinations. Audio is never touched here.", + "rend.target": "Target", + "rend.textColour": "Text colour", + "rend.textPlaceholder": "MY STATION", + "rend.title": "Renditions", + "rend.unused": "No destination selects this yet, so nothing is being encoded. Pick it on a destination to start the encode.", + "rend.uptime": "Uptime", + "rend.videoBitrate": "Video bitrate", + "rend.waitingSnapshot": "Waiting for the first live snapshot.", + "rend.watermarkImage": "Watermark image", + "rend.watermarkPlaceholder": "overlays/logo.png", + "rend.width": "Width", + "rend.widthPct": "Width (% of frame)", + "route.attack": "Attack", + "route.audio": "audio", + "route.audioAndVideoGoOut": "Audio and video go out together.", + "route.audioLaterThanVideo": "Audio later than video", + "route.audioRouting": "Audio routing", + "route.audioRoutingDesc": "Choose exactly which ingest tracks each destination receives.", + "route.autoLimitWhen2Tracks": "Auto (limit when 2+ tracks)", + "route.commentaryLanguage": "Commentary language", + "route.couldNotApplyThePreset": "Could not apply the preset.", + "route.couldNotCompileThisRouting": "Could not compile this routing.", + "route.couldNotLoadDestinations": "Could not load destinations.", + "route.couldNotSaveTheRouting": "Could not save the routing.", + "route.couldNotSaveTrackRoles": "Could not save track roles.", + "route.custom": "Custom…", + "route.delayDirection": "Delay direction", + "route.destinations": "Destinations", + "route.duckingNote": "Pull one group of tracks down whenever another is speaking — the mic over the music bed. Off means every selected track is summed at its own gain, unchanged.", + "route.duckingTriggerNote": "A trigger does not have to be in this destination’s mix — a mic can duck the music here while only the music is sent.", + "route.enableDucking": "Enable ducking", + "route.everyTrackReaches": "Every selected track reaches this destination, music included.", + "route.excludeMusic": "Exclude music", + "route.keepMusic": "Keep music", + "route.keepMusicOut": "Keep music out of this destination", + "route.labelTracks": "Label tracks", + "route.langPlaceholder": "lang — e.g. es", + "route.limiter04DbfsCeiling": "Limiter (−0.4 dBFS ceiling)", + "route.localOnlyNote": "This server does not store track roles yet, so these stay in this browser. Everything else on the page saves normally.", + "route.loudnessTarget": "Loudness target", + "route.lufsBroadcast": "EBU R128 broadcast", + "route.measuredOverTheWholeProgramme": "Measured over the whole programme, not moment to moment: quiet passages stay quiet. These are the numbers the platforms' own normalizers aim at, so hitting one is what stops the platform turning you down.", + "route.mixMatrix": "Mix matrix", + "route.musicPolicy": "Music policy", + "route.noTarget": "No target (leave levels alone)", + "route.normalizeLoudnorm": "Loudness (EBU R128)", + "route.normalizeOff": "Off", + "route.off": "off", + "route.offset": "Offset", + "route.on": "on", + "route.platformDefault": "Platform default", + "route.podcastSpokenWord": "Podcast / spoken word", + "route.presetApplied": "Preset applied. Review it, then save.", + "route.range": "Range", + "route.ratio": "Ratio", + "route.release": "Release", + "route.result": "Result", + "route.role51": "5.1", + "route.roleClean": "clean", + "route.roleMic": "mic", + "route.roleMusic": "music", + "route.rolesNote": "Roles describe the ingest, not this destination, and save on their own. Nothing is filtered until a destination below asks for it.", + "route.simple": "Simple", + "route.target": "Target", + "route.targetRole": "target", + "route.thisDestinationSPlatform": "This destination's platform", + "route.threshold": "Threshold", + "route.title": "Routing", + "route.triggerRole": "trigger", + "route.truePeak": "True peak", + "route.video": "video", + "route.videoLaterThanAudio": "Video later than audio", + "route.withoutATargetTheMix": "Without a target the mix goes out at whatever level the ingest produced, with only the clip protection above applied.", + "route.youtubeTwitchSpotify": "YouTube, Twitch, Spotify", + "set.acceptSelfSigned": "Accept a self-signed certificate", + "set.accountDisconnected": "Account disconnected.", + "set.adminPassword": "Admin password", + "set.alertDelivery": "Alert delivery", + "set.alertDeliveryDesc": "How hard a webhook that is not answering gets chased, before the alert is given up on.", + "set.apiTokens": "API tokens", + "set.apiTokensDesc": "Long-lived credentials for scripts, Prometheus and anything else that cannot hold a session. Send one as Authorization: Bearer pmk_…. A token can do everything the admin can, except manage tokens — that stays behind the password, so revoking one is final.", + "set.app": "App", + "set.attemptsBackoff": "The wait between attempts backs off automatically to a 30 second ceiling, so the top of this range is already several minutes of chasing one dead endpoint. Bounded is the point — retrying forever turns a single dead webhook into a queue that never drains.", + "set.attemptsNote": "Including the first try. 1 means never retry.", + "set.attemptsRetryable": "Only failures worth retrying are retried. A 404 from a webhook somebody deleted is permanent, and trying it again four times only delays every alert queued behind it; a 502 from a reverse proxy is exactly what these attempts are for.", + "set.audioMeters": "Audio meters", + "set.brokerPasswordCleared": "Broker password cleared.", + "set.brokerPlainNote": "mqtt:// is unencrypted. MQTT sends the username and password in its CONNECT packet, so both cross the network in the clear — fine on a trusted LAN, which is the usual Home Assistant case, and not fine over anything you do not control. Use mqtts:// if the broker offers it.", + "set.brokerUrl": "Broker URL", + "set.brokerUrlNote": "mqtt://, mqtts://, ws:// or wss://. A password in the URL is refused: a URL reaches log lines and ps output, and there is no taking it back. Put it in the field below, where it is encrypted at rest.", + "set.brokerUrlPlaceholder": "mqtts://broker.local:8883", + "set.chatBothApply": "Both apply and the more generous one wins: a message goes when it is older than the hours and outside the newest N. So a busy channel keeps less time than a quiet one at the same settings.", + "set.chatBufferNote": "Send on connect is a different kind of number from the other two. Those bound what is kept on disk; this one is a buffer held in memory, allocated per connected dashboard.", + "set.chatHistory": "Chat history", + "set.chatHistoryDesc": "How far back the chat scrollback goes — and with it, how much a moderator can see when they open somebody's card.", + "set.chatHours": "Keep for (hours)", + "set.chatKeep": "Always keep (messages)", + "set.chatKeepNote": "A floor, whatever their age.", + "set.chatNotOnlyDisk": "This is not only a disk setting. Clicking a name in chat opens what that person has said before, and that history comes from here — no platform publishes an API for it, so polyemesis can only show what it kept. Set this too short and somebody who has been trouble for a week reads as though they have never said anything.", + "set.chatSendNote": "Scrollback without a query.", + "set.chatSendOnConnect": "Send on connect (messages)", + "set.chatSendOnConnectShort": "Send on connect", + "set.chatSmall": "Chat is small. A channel averaging ten messages a second stores roughly 7 MB an hour; most are nowhere near that.", + "set.clientId": "Client ID", + "set.clientIdNote": "Must be unique on the broker. A collision is the number-one cause of an unexplained reconnect loop: the broker disconnects the older session on every connect, and both clients reconnect forever.", + "set.clientIdPlaceholder": "derived from the instance name", + "set.clientSecret": "Client secret", + "set.comingBack": "Coming back", + "set.confirmPassword": "Confirm new password", + "set.connectedAccounts": "Connected accounts", + "set.copied": "Copied", + "set.copy": "Copy", + "set.couldNotChangeThePassword": "Could not change the password.", + "set.couldNotClearThePassword": "Could not clear the password.", + "set.couldNotCreateTheToken": "Could not create the token.", + "set.couldNotReCheckThe": "Could not re-check the credentials.", + "set.couldNotReadTheTls": "Could not read the TLS status.", + "set.couldNotRevokeTheToken": "Could not revoke the token.", + "set.couldNotSaveTheCredentials": "Could not save the credentials.", + "set.couldNotSaveTheMqtt": "Could not save the MQTT settings.", + "set.credsRemoved": "Credentials removed.", + "set.currentPassword": "Current password", + "set.dataDirectory": "Data directory", + "set.deliveryAttempts": "Delivery attempts", + "set.dialsOnly": "The server dials only: {schemes}.", + "set.disconnect": "Disconnect", + "set.disconnectDesc": "Destinations using it keep their settings but stop refreshing their stream key. You can reconnect the same account afterwards.", + "set.disconnectTitle": "Disconnect this account?", + "set.drawtextMissing": "This build has no drawtext filter, so text overlays are unavailable. It needs an FFmpeg built with libfreetype.", + "set.drawtextOk": "This build can draw text on video.", + "set.enabled": "Enabled", + "set.encoderUrl": "Encoder URL", + "set.expires": "Expires", + "set.failover": "Failover", + "set.failoverDesc": "A standby input and a holding card, for when the live source stops delivering. Off by default: with it off the pipeline is byte-for-byte what it was before this existed.", + "set.failoverRemuxNote": "Turning this on adds one remux hop between the ingest and everything below it. Cheap, but not free, which is why it is a decision rather than a default.", + "set.fileSizeMb": "File size (MB)", + "set.filesKept": "Files kept", + "set.goingLive": "Going live", + "set.goingLiveDesc": "What happens when several destinations start at once.", + "set.graceNote": "How long the live source may deliver nothing before the tier switches. Longer than a reconnect on purpose — an encoder re-establishing an RTMP connection is normal operation, not a failure.", + "set.gracePeriod": "Grace period (seconds)", + "set.haDiscovery": "Home Assistant discovery", + "set.height": "Height", + "set.ingest": "Ingest", + "set.ingestDesc": "SRT carries up to six audio tracks. RTMP carries one, and exists as a fallback for encoders that cannot do SRT. Pull dials a source instead of waiting for one.", + "set.ingestUrlCopied": "Ingest URL copied.", + "set.instance": "Instance", + "set.intervalNote": "Unchanged state is not republished, so a short interval costs a comparison rather than a message. The keep-alive is what bounds how long a dead link goes unnoticed — and therefore how quickly the broker tells subscribers this instance is gone.", + "set.issuer": "Issuer", + "set.kbps": "kbps", + "set.keepAlive": "Keep-alive (s)", + "set.latencyMs": "Latency (ms)", + "set.listeners": "Listeners", + "set.listenersDesc": "Where the server binds, for the whole install. One SRT port serves every source — they are told apart by their publish token, so adding a source never means publishing another port. RTMP has no such routing, so its port serves one source.", + "set.logsBoundNote": "The log directory is bounded by the product of these two, so persistence can never be the thing that fills the disk.", + "set.logsDesc": "Captured FFmpeg stderr is always kept in memory, but that dies with the server — which loses exactly the lines explaining why it died. Persisting mirrors it to a rotating file under the data directory.", + "set.metersDesc": "One sidecar process meters every channel of every track. Lower intervals update faster and cost more CPU.", + "set.mode": "Mode", + "set.modePull": "Pull — dial a camera, feed or file", + "set.modeRtmp": "RTMP — single track", + "set.modeSrt": "SRT — multitrack (recommended)", + "set.mqtt": "MQTT telemetry", + "set.mqtt5Only": "MQTT 5.0 only.", + "set.mqtt5OnlyNote": "A broker pinned to 3.1.1 will not complete a connection at all — it is not a degraded mode. Mosquitto 2.x, EMQX and the Home Assistant add-on all speak 5.0.", + "set.mqttDesc": "Publishes state to a broker as retained messages, so a consumer that was not connected when something changed still gets the current answer.", + "set.multiAccountNote": "Connect the same platform more than once to stream to multiple channels — each account becomes its own destination.", + "set.name": "Name", + "set.newPassword": "New password", + "set.no": "no", + "set.noEncryption": "Nothing is encrypting this connection. Passwords and session cookies cross the network in plaintext — set tls.mode: auto, or bind to 127.0.0.1 behind a proxy.", + "set.noLibsrt": "This FFmpeg build has no SRT support. Install one configured with --enable-libsrt, or use RTMP.", + "set.noTokens": "No tokens yet.", + "set.passphrase": "Passphrase", + "set.passphraseNote": "Enables AES encryption. Leave blank for none. SRT rejects passphrases outside 10–79 characters.", + "set.passphrasePlaceholder": "optional — 10 to 79 characters", + "set.password": "Password", + "set.passwordChanged": "Password changed.", + "set.passwordMismatch": "The new passwords do not match.", + "set.passwordNote": "The password is never returned by any API, so this box is blank even when one is set. Leave it empty to keep the stored password; clear it with the button below.", + "set.platform": "Platform", + "set.platformsDesc": "Platforms differ in how much of this polyemesis can automate, and the difference is worth knowing before you start rather than an hour in. Streaming itself works the same everywhere — per-destination audio routing, renditions, reconnect and meters do not depend on any of the columns below.", + "set.playLoop": "Play a loop when nothing is live", + "set.playLoopNote": "A second candidate beside the slate: files play instead of a still image. Both can be on -- see the tier order in the Failover design for which wins.", + "set.pointObs": "Point OBS at this address.", + "set.preview": "Preview", + "set.previewDesc": "The dashboard's HLS monitor. This is the only place polyemesis re-encodes video, and it runs on its own relay subscription so it can never affect a destination.", + "set.previewIdleNote": "The encoder starts when the dashboard asks for the playlist and stops again this long after the last request, so a closed dashboard costs no CPU. Changing this never restarts a preview someone is watching.", + "set.processLogs": "Process logs", + "set.proxyTerminatesTls": "A reverse proxy in front of polyemesis terminates TLS, so this server serves plain HTTP on purpose.", + "set.publishInterval": "Publish interval (s)", + "set.reconnectCap": "Reconnect backoff cap (s)", + "set.reconnectCapNote": "0 uses the default. HTTP and HLS sources only.", + "set.redirectNote": "Must match exactly, including scheme and port. If you reach polyemesis through a reverse proxy, use that public URL.", + "set.redirectUri": "Redirect URI to whitelist", + "set.removeCreds": "Remove credentials", + "set.removeCredsDesc": "Every account connected through this app stops refreshing its token and will need reconnecting. The client secret is not recoverable from here — you would create a new app at the platform.", + "set.removeCredsTitle": "Remove these developer credentials?", + "set.returnAfter": "Return after (seconds steady)", + "set.returnAutomatic": "Automatic — return once the primary is steady", + "set.returnManual": "Manual — stay on the backup until I switch", + "set.returnManualNote": "Manual is the default because an automatic return can flap: a primary that is recovering in bursts would move the programme back and forth mid-broadcast.", + "set.rtmpPort": "RTMP port", + "set.rtspNote": "TCP unless the camera cannot. RTSP over UDP through NAT looks connected and delivers nothing.", + "set.rtspTransport": "RTSP transport", + "set.saveAndDrop": "Save and drop the live stream", + "set.saveIngest": "Save ingest settings", + "set.segmentSec": "Segment (s)", + "set.settingsSavedAffectedProcessesHave": "Settings saved. Affected processes have been restarted.", + "set.showSlate": "Show a slate when nothing is live", + "set.slateColour": "Slate colour", + "set.slateColourNote": "One word, because it lands on a filter graph where a comma would end the argument. Used only when no image is set.", + "set.slateColourPlaceholder": "black", + "set.slateImage": "Slate image", + "set.slateImageNote": "A path inside the data directory. Leave it empty to paint a flat colour instead — that is the fallback precisely because a colour has no file that can fail to open.", + "set.slateImagePlaceholder": "slate/holding.png", + "set.sourceUrl": "Source URL", + "set.sourceUrlNote": "One of {schemes}. A file:// source is a path RELATIVE to the data directory and may not contain \"..\" — it is confined there the same way a file upload is.", + "set.sourceUrlPlaceholder": "rtsp://camera.local/stream1", + "set.srtPort": "SRT port", + "set.stagger": "Stagger connections (ms)", + "set.staggerNote": "0 is off. Going live otherwise means every destination opening a connection, negotiating TLS and starting to encode audio in the same tick — on a small box that is the moment most likely to drop frames, and it is the exact moment you are watching, because it is when you went live. A few hundred milliseconds is plenty.", + "set.staggerReconnectNote": "It never delays a reconnect. A destination that drops at 3am comes back immediately rather than waiting its turn behind processes that are already healthy.", + "set.stepByStep": "Step-by-step setup", + "set.stopAfterIdle": "Stop after idle (s)", + "set.streamKey": "Stream key", + "set.streamKeyNote": "Publishers must use this exact path, so an open port is not an open door.", + "set.subject": "Subject", + "set.synthesiseNote": "Every major platform rejects a stream with no audio track. With this on, a video-only ingest gains one silent stereo track and its destinations start; with it off they refuse with \"routing profile selects no audio\". It only ever applies to an ingest that PROBED with zero tracks, so it can never displace audio you are actually sending.", + "set.synthesiseSilent": "Synthesise a silent track", + "set.syntheticAudio": "Synthetic audio", + "set.syntheticDesc": "What happens when the ingest carries video and no audio at all.", + "set.tabIngest": "Ingest", + "set.tabPipeline": "Pipeline", + "set.tabPlatforms": "Platform credentials", + "set.tabSecurity": "Security", + "set.title": "Settings", + "set.tlsAcme": "Let's Encrypt", + "set.tlsAuto": "Auto", + "set.tlsDesc": "Configured in config.yaml, not here — TLS must be right before the server starts listening. This card reports what that produced.", + "set.tlsManual": "Manual certificate", + "set.tlsOff": "Not terminated here", + "set.tlsSelfsigned": "Self-signed", + "set.tokenHashNote": "polyemesis stores only a hash. If you lose it, revoke this token and create another.", + "set.tokenNamePlaceholder": "prometheus", + "set.tokenRevoked": "Revoked {name}.", + "set.topicPrefix": "Topic prefix", + "set.topicPrefixNote": "Separators in the prefix are preserved, so home/av means two levels. A prefix starting with $ is refused: brokers reserve those, and a subscriber using # never receives them — the telemetry would publish successfully and be invisible in exactly the view you would debug with.", + "set.transportSecurity": "Transport security", + "set.updateInterval": "Update interval (ms)", + "set.username": "Username", + "set.validFor": "Valid for", + "set.whatPlatformsDo": "What each platform can do", + "set.whyDevApp": "Why you need your own developer app", + "set.whyDevAppDesc": "polyemesis cannot ship OAuth client secrets — anyone with the binary would have them, and the platforms would revoke them. You register a free developer app once, paste the client ID and secret here, and polyemesis then fetches your stream keys for you.", + "set.writeLogs": "Write logs to disk", + "set.yes": "yes", + "sources.addDescription": "A second programme with its own ingest, destinations and renditions — a vertical canvas alongside a horizontal one, say. It starts on the default ports, which you will want to change so the two do not clash.", + "sources.addFailed": "Could not add the source", + "sources.addSource": "Add source", + "sources.addTitle": "Add a source", + "sources.added": "Added “{name}”. Set its ingest ports below.", + "sources.app": "App", + "sources.applyAndDrop": "Apply and drop the stream", + "sources.copied": "{what} copied", + "sources.copyFailed": "Could not copy the {what}", + "sources.copyTokenAria": "Copy the token", + "sources.copyUrlAria": "Copy the {proto} URL", + "sources.default": "default", + "sources.defaultDescription": "Requests that do not name a source act on this one.", + "sources.deleteAria": "Delete {name}", + "sources.deleteConfirm": "Delete source", + "sources.deleteDescription": "Its destinations and renditions go with it — they describe where this programme goes and mean nothing without it. Recordings are kept: the files are still on disk and still playable, they just stop being attributed to a source.", + "sources.deleteFailed": "Could not delete the source", + "sources.deleteThis": "Delete this source", + "sources.deleteTitle": "Delete “{name}”?", + "sources.deleted": "Deleted “{name}” and its destinations", + "sources.destinations": "Destinations", + "sources.dirtyIdle": "Unsaved ingest changes. Applying restarts this source's ingest.", + "sources.dirtyPublishing": "An encoder is publishing to this source. Applying restarts its ingest and drops everyone watching.", + "sources.enableAria": "Enable {name}", + "sources.help.app": "The RTMP application path — the part of the URL between the host and the stream key. Most encoders expect “live”. Change it only if you have a reason to.", + "sources.help.ingest": "How your encoder reaches this source. SRT survives a lossy connection by retransmitting what went missing, and is the right default over the public internet. RTMP is older and universally supported, but it degrades badly on an unstable uplink. Pull makes polyemesis fetch from a URL you provide instead of waiting to be published to.", + "sources.help.latencyMs": "How long SRT will wait for a late or retransmitted packet before giving up on it. Higher values ride out worse connections at the cost of delay: roughly 2.5× your round-trip time is a good starting point, so a 40 ms RTT wants about 100 ms here. Too low and a brief hiccup becomes a visible glitch.", + "sources.help.passphrase": "Encrypts the SRT connection, and today this is what actually keeps strangers out of this ingest. Between 10 and 79 characters. Leave it empty and anyone who can reach the port can publish to it.", + "sources.help.streamKey": "The secret an encoder sends to publish over RTMP. This is the credential that gates this ingest, so treat it like a password: anyone holding it can broadcast to your destinations.", + "sources.help.token": "Identifies this source when several share one ingest port. Whether it also gates access depends on your ingest mode — the note underneath says which is true right now. Rotating issues a new token and keeps the old one valid for five minutes so a live encoder is not cut off mid-broadcast.", + "sources.ingest": "Ingest", + "sources.lastSourceTitle": "The last source cannot be deleted — an install needs one ingest", + "sources.latencyMs": "Latency (ms)", + "sources.loadFailed": "Could not load sources", + "sources.loading": "Loading sources…", + "sources.loss": "loss", + "sources.modePull": "Pull", + "sources.name": "Name", + "sources.namePlaceholder": "Vertical", + "sources.noEngine": "No engine is running for this source. The usual cause is an ingest port already in use by another source or another process.", + "sources.notRunning": "not running", + "sources.ownDescription": "Its own destinations, renditions and recordings.", + "sources.passphrase": "Passphrase", + "sources.passphrasePlaceholder": "10–79 chars", + "sources.publishing": "publishing", + "sources.renditions": "Renditions", + "sources.retrans": "retrans", + "sources.rotate": "Rotate", + "sources.rotateConfirm": "Rotate token", + "sources.rotateFailed": "Could not rotate the token", + "sources.rotateIdle": "A new token is issued immediately. The old one keeps working for five minutes so an encoder already using it is not cut off.", + "sources.rotateTitle": "Rotate the token for “{name}”?", + "sources.rotateWhilePublishing": "An encoder is publishing to this source right now. The old token keeps working for five minutes, so it will not be cut off — but it must be updated before that window closes.", + "sources.rtt": "RTT", + "sources.running": "running", + "sources.saveFailed": "Could not save", + "sources.streamKey": "Stream key", + "sources.subtitle": "One ingested programme each. Destinations and renditions belong to a source.", + "sources.title": "Sources", + "sources.token": "token", + "sources.tokenEnforcedDetailRtmp": "It is the stream key: publish to the URL above with this as the key. Rotating issues a new one and keeps the old working for five minutes, so you can move across without dropping a live stream.", + "sources.tokenEnforcedDetailSrt": "Put it in your encoder's SRT streamid to publish to this source on the shared port. Rotating issues a new one and keeps the old working for five minutes, so you can move across without dropping a live stream.", + "sources.tokenIsCredential": "This token is the credential.", + "sources.tokenIssued": "New token issued", + "sources.tokenNotEnforced": "This token is stored but not enforced right now.", + "sources.tokenNotEnforcedRtmp": "Sources are kept apart by port. What actually protects this ingest is the stream key above. Turn on one-port ingest in Settings to make the token the credential.", + "sources.tokenNotEnforcedSrt": "Sources are kept apart by port. What actually protects this ingest is the SRT passphrase above. Turn on one-port ingest in Settings to make the token the credential.", + "state.disabled": "Disabled", + "state.failed": "Failed", + "state.live": "Live", + "state.offline": "Offline", + "state.reconnecting": "Reconnecting", + "state.starting": "Starting", + "state.stopped": "Stopped" } diff --git a/ui/src/lib/i18n/es.json b/ui/src/lib/i18n/es.json index 782e7d58..eca439bb 100644 --- a/ui/src/lib/i18n/es.json +++ b/ui/src/lib/i18n/es.json @@ -1,26 +1,227 @@ { - "nav.dashboard": "Panel de control", - "nav.sources": "Fuentes", - "nav.meters": "Medidores de audio", - "nav.routing": "Routing", - "nav.renditions": "Versiones", - "nav.playout": "Playout", - "nav.library": "Biblioteca", - "nav.clips": "Fragmentos", - "nav.chat": "Chat", - "nav.jobs": "Tareas", - "nav.automation": "Automatización", - "nav.recordings": "Grabaciones", - "nav.monitoring": "Monitorización", - "nav.settings": "Ajustes", - "chrome.toggleNav": "Alternar navegación", + "auth.accountCreated": "Cuenta de administrador creada.", + "auth.confirmPassword": "Confirmar contraseña", + "auth.createAccount": "Crear cuenta", + "auth.firstRun": "Configuración inicial", + "auth.firstRunDesc": "Elija las credenciales de administrador de este servidor. No hay recuperación de cuenta, así que guárdelas en un lugar seguro.", + "auth.password": "Contraseña", + "auth.passwordMismatch": "Las contraseñas no coinciden.", + "auth.signIn": "Iniciar sesión", + "auth.signInDesc": "Introduzca sus credenciales de administrador.", + "auth.somethingWentWrong": "Algo ha salido mal.", + "auth.tokenCreated": "Token de API creado", + "auth.tokenRevoked": "Token de API revocado", + "auth.username": "Usuario", + "auto.action": "Acción", + "auto.adminPasswordChanged": "Contraseña de administrador cambiada", + "auto.chosenWeekdays": "Días de la semana elegidos", + "auto.coalesced": "Agrupadas", + "auto.coalescingWindow": "Ventana de agrupación (segundos)", + "auto.couldNotChangeTheRule": "No se pudo cambiar la regla.", + "auto.couldNotChangeTheSchedule": "No se pudo cambiar la programación.", + "auto.couldNotDeleteTheRule": "No se pudo eliminar la regla.", + "auto.couldNotDeleteTheSchedule": "No se pudo eliminar la programación.", + "auto.couldNotLoadAutomationSettings": "No se pudieron cargar los ajustes de automatización.", + "auto.couldNotSaveTheRule": "No se pudo guardar la regla.", + "auto.couldNotSaveTheSchedule": "No se pudo guardar la programación.", + "auto.createRule": "Crear regla", + "auto.createSchedule": "Crear programación", + "auto.days": "Días", + "auto.delete": "Eliminar", + "auto.deleteRule": "Eliminar regla", + "auto.deleteRuleDesc": "La regla deja de dispararse. Su endpoint de webhook queda intacto y puede volver a crear la regla.", + "auto.deleteRuleTitle": "¿Eliminar esta regla de alerta?", + "auto.deleteSchedule": "Eliminar programación", + "auto.deleteScheduleDesc": "La programación deja de ejecutarse. Lo que ya haya iniciado no se ve afectado.", + "auto.deleteScheduleTitle": "¿Eliminar esta programación?", + "auto.delivery": "Entrega", + "auto.destinationFallingBehindRealtime": "El destino se queda por detrás del tiempo real", + "auto.destinationKeepingUpAgain": "El destino vuelve a seguir el ritmo", + "auto.destinationRecovered": "Destino recuperado", + "auto.destinationWentDown": "El destino ha caído", + "auto.destinations": "Destinos", + "auto.dropped": "Descartadas", + "auto.edit": "Editar", + "auto.editRule": "Editar regla de alerta", + "auto.editSchedule": "Editar programación", + "auto.endpoint": "Endpoint", + "auto.evAudioClipping": "Saturación de audio", + "auto.evClipCaptured": "Clip capturado", + "auto.evDiskLow": "Queda poco espacio en disco", + "auto.evDiskRecovered": "Disco recuperado", + "auto.evIngestLost": "Entrada perdida", + "auto.evIngestRecovered": "Entrada recuperada", + "auto.evSettingsChanged": "Ajustes cambiados", + "auto.evSignedIn": "Sesión iniciada", + "auto.events": "Eventos", + "auto.everyDay": "Todos los días", + "auto.failed": "Fallidas", + "auto.failoverSwitchedSource": "La conmutación por error cambió de fuente", + "auto.fired": "disparadas", + "auto.firesAt": "Se dispara a las", + "auto.lastDelivery": "Última entrega", + "auto.lastSweep": "Última pasada", + "auto.latenessAllowance": "Margen de retraso (segundos)", + "auto.loudnessBackInCompliance": "Sonoridad de nuevo dentro de la norma", + "auto.loudnessOutOfCompliance": "Sonoridad fuera de la norma", + "auto.minGap": "Intervalo mínimo entre mensajes (segundos)", + "auto.minSeverity": "Gravedad mínima", + "auto.missedNote": "Una ocurrencia perdida porque el servidor estaba caído se marca como atendida y se omite en lugar de dispararse tarde: una transmisión que se inicia sola con cuatro horas de retraso es peor que una que nunca se inició.", + "auto.name": "Nombre", + "auto.newRule": "Nueva regla de alerta", + "auto.newSchedule": "Nueva programación", + "auto.next": "Siguiente", + "auto.nothingFired": "Todavía no se ha disparado nada. El planificador hace una pasada cada 20 segundos.", + "auto.once": "Una vez", + "auto.payloadFormat": "Formato del payload", + "auto.pending": "Pendiente", + "auto.repeatedFailedSignIns": "Inicios de sesión fallidos repetidos", + "auto.repeats": "Repetición", + "auto.retries": "Reintentos", + "auto.rule": "Regla", + "auto.ruleCreated": "Regla de alerta creada. Envíele una prueba.", + "auto.ruleDeleted": "Regla de alerta eliminada.", + "auto.rulePlaceholder": "Discord — alertas del programa", + "auto.ruleSaved": "Regla de alerta guardada.", + "auto.saveRule": "Guardar regla", + "auto.saveSchedule": "Guardar programación", + "auto.schedule": "Programación", + "auto.scheduleCreated": "Programación creada.", + "auto.scheduleDeleted": "Programación eliminada.", + "auto.schedulePlaceholder": "Programa entre semana", + "auto.scheduleSaved": "Programación guardada.", + "auto.schedules": "Programaciones", + "auto.secretMasked": "Se muestra enmascarada porque el secreto vive en la ruta. Déjela como está para conservar el endpoint actual, o pegue una URL nueva para reemplazarlo.", + "auto.sendTest": "Enviar alerta de prueba", + "auto.sendTestTitle": "Enviar ahora una alerta de prueba", + "auto.sent": "Enviadas", + "auto.skipped": "omitidas", + "auto.startDestinations": "Iniciar los destinos", + "auto.startPlaylist": "Iniciar la lista de reproducción", + "auto.stopDestinations": "Detener los destinos", + "auto.stopPlaylist": "Detener la lista de reproducción", + "auto.subscribedTo": "Suscrito a", + "auto.subtitle": "Webhooks que le avisan de que algo se ha roto, y programaciones que sacan el programa sin usted.", + "auto.theEndpointDidNotAccept": "El endpoint no ha aceptado el mensaje de prueba.", + "auto.time": "Hora", + "auto.timeZone": "Zona horaria", + "auto.title": "Automatización", + "auto.tzPlaceholder": "UTC", + "auto.webhookHint": "Discord y Slack le dan uno en los ajustes del canal.", + "auto.webhookPlaceholder": "https://discord.com/api/webhooks/…", + "auto.webhookRules": "Reglas de webhook", + "auto.webhookUrl": "URL del webhook", + "auto.weekFri": "Vie", + "auto.weekMon": "Lun", + "auto.weekSat": "Sáb", + "auto.weekSun": "Dom", + "auto.weekThu": "Jue", + "auto.weekTue": "Mar", + "auto.weekWed": "Mié", + "auto.when": "Cuándo", + "chatpage.allFiltered": "Todas las plataformas con mensajes están filtradas.", + "chatpage.couldNotDeleteThatMessage": "No se pudo eliminar ese mensaje.", + "chatpage.deleteNote": "Eliminar borra el mensaje en la plataforma que lo emitió. Una plataforma en la que polyemesis no puede borrar lo dice y señala dónde sí puede hacerse: la acción nunca se oculta por suposición.", + "chatpage.droppedTitle": "Se mostraron en directo, pero la persistencia se quedó atrás y el historial los perdió.", + "chatpage.dupTitle": "El mismo mensaje visto dos veces: nuestro propio eco, o la plataforma repitiéndolo.", + "chatpage.noAccount": "El chat está funcionando, pero no hay ninguna cuenta de plataforma conectada. Conecte una en Ajustes → Credenciales de plataforma.", + "chatpage.notRunning": "No hay chat funcionando en este servidor. El historial bajo la línea de tiempo es lo que se almacenó antes, y el cuadro de envío queda desactivado hasta que se conecte una plataforma.", + "chatpage.received": "Recibidos", + "chatpage.reload": "Recargar", + "chatpage.stored": "Almacenados", + "chatpage.storedTitle": "No hay nada conectado; este es el historial almacenado.", + "chatpage.subtitle": "Todas las plataformas en una línea de tiempo, con un solo cuadro de envío.", + "chatpage.timeline": "Línea de tiempo", + "chatpage.title": "Chat", "chrome.ingest": "Ingest", + "chrome.language": "Idioma", "chrome.liveCount": "{count} en vivo", + "chrome.signOut": "Cerrar sesión", "chrome.socketOffline": "desconectado", "chrome.socketOfflineHint": "Las actualizaciones en vivo están desconectadas; reintentando", - "chrome.signOut": "Cerrar sesión", - "chrome.language": "Idioma", - "common.add": "Agregar", + "chrome.toggleNav": "Alternar navegación", + "clipedit.back33": "33 ms atrás", + "clipedit.clickLine": "Haga clic en una línea para saltar allí. Seleccione varias y los puntos de entrada y salida se mueven a las palabras que haya resaltado.", + "clipedit.clipThisLine": "Clip de esta línea", + "clipedit.clipThisLineTitle": "Fijar los puntos de entrada y salida alrededor de esta línea", + "clipedit.copied": "Copiado", + "clipedit.copiesEveryPacketSecondsBit": "Copia todos los paquetes. Cuestión de segundos, exacto bit a bit, y el inicio retrocede hasta el fotograma clave más cercano.", + "clipedit.couldNotOpenThatRecording": "No se pudo abrir esa grabación.", + "clipedit.couldNotQueueTheExport": "No se pudo poner la exportación en cola.", + "clipedit.cut": "Corte", + "clipedit.downloadClip": "Descargar el clip", + "clipedit.exportQueued": "Exportación en cola. Se ejecuta cuando la máquina pueda dedicarle ciclos.", + "clipedit.exports": "Exportaciones", + "clipedit.fast": "Rápido", + "clipedit.filterTranscript": "Filtrar la transcripción", + "clipedit.findSaid": "Buscar lo que se dijo", + "clipedit.forward33": "33 ms adelante", + "clipedit.in": "Entrada", + "clipedit.keepAllTracks": "Conservar todas las pistas de audio", + "clipedit.keepsEveryAudioTrackIn": "Conserva todas las pistas de audio en cualquier códec. Lo que escribe el grabador.", + "clipedit.markIn": "I — marcar la entrada en el cabezal", + "clipedit.markOut": "O — marcar la salida en el cabezal", + "clipedit.name": "Nombre", + "clipedit.noKeyframes": "No se han podido leer las posiciones de los fotogramas clave, así que nadie puede decir dónde empezará realmente este corte. Funcionará igualmente — la propia búsqueda de FFmpeg cae en un fotograma clave —, pero el inicio puede adelantarse hasta un GOP y esta página no puede mostrarle cuánto.", + "clipedit.noTranscript": "Todavía no se ha transcrito nada de esta grabación. Cada micrófono se grabó en su propia pista, así que transcribirla le da los nombres de quien habla sin ningún modelo de diarización, y después puede cortar por lo que se dijo.", + "clipedit.nothingExported": "Todavía no se ha exportado nada.", + "clipedit.out": "Salida", + "clipedit.pickAtLeastOneAudio": "Elija al menos una pista de audio, o llévelas todas.", + "clipedit.playFaster": "L — reproducir, de nuevo para más rápido", + "clipedit.precise": "Preciso", + "clipedit.queueNote": "La exportación se pone en cola, no se ejecuta aquí: cede ante la transmisión en directo, así que un corte tomado en mitad de la emisión espera en lugar de robar los ciclos que la transmisión necesita.", + "clipedit.reEncodesOnlyTheLeading": "Recodifica solo el GOP parcial inicial para que el inicio sea exacto. El resto se sigue copiando.", + "clipedit.reencoded": "Recodificado", + "clipedit.shortcuts": "J / K / L mueven la reproducción, I y O marcan la entrada y la salida, las flechas desplazan 33 ms y con mayúsculas un segundo. El paso fino equivale a un fotograma a 30 fps; la velocidad real de esta grabación no está indexada, así que se nombra en milisegundos en lugar de adivinarla.", + "clipedit.shuttleBack": "J — retroceder", + "clipedit.startMoved": "Inicio desplazado", + "clipedit.starts": "Empieza", + "clipedit.stop": "K — detener", + "clipedit.thatCutCouldNotBe": "No se pudo planificar ese corte.", + "clipedit.thatIsNotARecording": "Ese no es un id de grabación.", + "clipedit.thatRecordingIsNotAvailable": "Esa grabación no está disponible.", + "clipedit.title": "Editor de clips", + "clipedit.whatASocialPlatformWill": "Lo que acepta una plataforma social. Sobreviven menos códecs.", + "clipedit.zoomIn": "Acercar", + "clipedit.zoomOut": "Alejar", + "clips.bitrate": "Tasa de bits", + "clips.buffer": "Búfer", + "clips.bufferLiveNote": "El interruptor del búfer es un conmutador en vivo, no un ajuste guardado: vuelve a estar apagado tras un reinicio.", + "clips.captured": "Capturado", + "clips.ceiling": "Techo", + "clips.clipsKept": "Clips conservados", + "clips.couldNotCaptureAClip": "No se pudo capturar un clip.", + "clips.couldNotChangeTheClip": "No se pudo cambiar el búfer de clips.", + "clips.couldNotDeleteTheClip": "No se pudo eliminar el clip.", + "clips.couldNotLoadClips": "No se pudieron cargar los clips.", + "clips.covers": "Abarca", + "clips.customLength": "Duración personalizada del clip en segundos", + "clips.delete": "Eliminar", + "clips.deleteDescription": "Un clip suele ser justo el material que quería conservar. Esto elimina el archivo; la grabación de la que salió queda intacta.", + "clips.deleteTitle": "¿Eliminar este clip?", + "clips.deleted": "Clip eliminado.", + "clips.download": "Descargar", + "clips.everythingHeld": "Todo lo que el búfer está guardando", + "clips.file": "Archivo", + "clips.held": "Retenido", + "clips.inMemory": "En memoria", + "clips.keepBuffer": "Mantener un búfer rodante", + "clips.keyframes": "Fotogramas clave", + "clips.length": "Duración", + "clips.none": "ninguno", + "clips.off": "apagado", + "clips.onDisk": "En disco", + "clips.seen": "detectado", + "clips.settings": "Ajustes", + "clips.size": "Tamaño", + "clips.subtitle": "Un búfer rodante de lo que acaba de salir. Captúrelo después del hecho: no hay que preparar nada por adelantado.", + "clips.theClipDoesNotStart": "El clip no empieza en un fotograma clave.", + "clips.title": "Clips", + "clips.waitingForStream": "esperando la transmisión", + "clips.window": "Ventana", + "clips.windowSeconds": "Ventana (segundos)", + "common.add": "Añadir", + "common.apply": "Aplicar", "common.cancel": "Cancelar", "common.close": "Cerrar", "common.confirm": "Confirmar", @@ -28,110 +229,921 @@ "common.copy": "Copiar", "common.delete": "Eliminar", "common.disabled": "Desactivado", + "common.discard": "Descartar", "common.edit": "Editar", "common.enabled": "Activado", "common.loading": "Cargando…", + "common.moreInfo": "Más información", "common.refresh": "Actualizar", "common.restart": "Reiniciar", "common.retry": "Reintentar", "common.save": "Guardar", "common.start": "Iniciar", "common.stop": "Detener", - "state.live": "En vivo", - "state.reconnecting": "Reconectando", - "state.starting": "Iniciando", - "state.failed": "Fallido", - "state.stopped": "Detenido", - "state.offline": "Sin conexión", - "hooks.title": "Webhooks de ciclo de vida", - "hooks.new": "Nuevo webhook", - "hooks.empty": "No hay webhooks. Añada uno y polyemesis enviará por POST un cuerpo JSON firmado en cuanto el stream empiece o termine, o un destino se active o se caiga: una entrega por transición, en orden, para que un script actúe.", - "hooks.colWebhook": "Webhook", - "hooks.colEndpoint": "Endpoint", - "hooks.colTriggers": "Suscrito a", - "hooks.colEnabled": "Act.", - "hooks.signed": "firmado", - "hooks.unsigned": "SIN FIRMAR", + "dash.audioTracks": "Pistas de audio", + "dash.bitrate": "Tasa de bits", + "dash.broadcastSettings": "Ajustes de la emisión", + "dash.categoryPlaceholder": "Música", + "dash.clipboardUnavailable": "El portapapeles no está disponible en este origen.", + "dash.connectPrompt": "Conecte una cuenta de YouTube, Twitch, Facebook o Kick en Ajustes → Credenciales de plataforma para fijar el título, la descripción y la categoría de su transmisión en todas las plataformas a la vez. Cada una acepta un subconjunto distinto: el compositor indica cuál y envía lo que puede.", + "dash.copyIngestUrl": "Copiar la URL de entrada", + "dash.couldNotLoadTheDestination": "No se pudo cargar el destino.", + "dash.couldNotPushTheMetadata": "No se pudieron enviar los metadatos.", + "dash.couldNotReorderTheDestinations": "No se pudieron reordenar los destinos.", + "dash.deleteDest": "Eliminar destino", + "dash.deleteDestDescription": "Esto elimina el destino y su perfil de enrutado. La plataforma queda intacta y puede volver a añadirlo, pero el enrutado de audio que configuró para él desaparece.", + "dash.descriptionPlaceholder": "De qué va esta transmisión.", + "dash.destDeleted": "Destino eliminado.", + "dash.disabled": "desactivado", + "dash.everyConnectedAccount": "todas las cuentas conectadas", + "dash.idle": "inactivo", + "dash.ingestUrlCopied": "URL de entrada copiada.", + "dash.keyRefreshed": "Clave de transmisión renovada desde la plataforma.", + "dash.leaveUnchanged": "Dejar sin cambios", + "dash.leaveUnchangedNote": "«Dejar sin cambios» no envía nada en absoluto, así que la plataforma conserva lo que tiene. Elegir Activado o Desactivado escribe ese valor.", + "dash.metaCategory": "Categoría", + "dash.metaDescription": "Descripción", + "dash.metaTags": "Etiquetas", + "dash.metaTitle": "Título", + "dash.noDestinations": "Todavía no hay destinos. Añada uno y elija después qué pistas de audio recibe.", + "dash.off": "Desactivado", + "dash.pipeline": "Cadena de proceso", + "dash.pushToPlatforms": "Enviar a las plataformas", + "dash.pushing": "Enviando…", + "dash.reconnects": "Reconexiones", + "dash.relaySubscribers": "Suscriptores del relé", + "dash.renditions": "Versiones", + "dash.scheduledStart": "Inicio programado", + "dash.subtitle": "Reciba una vez y reparta con vídeo compartido y audio por destino.", + "dash.tagsPlaceholder": "live, house, dj set", + "dash.theConnectedAccount": "la cuenta conectada", + "dash.title": "Panel de control", + "dash.titlePlaceholder": "Sesión del viernes por la noche", + "dash.uptime": "Tiempo activo", + "dash.youtubeStopsAcceptingTheseOnce": "YouTube deja de aceptarlos en cuanto una transmisión sale de «created» o «ready».", + "dest.audience": "Audiencia", + "dest.audioBitrate": "Bitrate de audio", + "dest.audioCodec": "Códec de audio", + "dest.channels": "Canales", + "dest.compliance": "Cumplimiento", + "dest.connectedAccount": "Cuenta conectada", + "dest.contentLabels": "Etiquetas de contenido", + "dest.copySource": "Copiar el vídeo de origen", + "dest.createVariant": "Crear esta variante", + "dest.customiseSummary": "Personalizar para este destino", + "dest.fbBackupCost": "Duplica el ancho de banda de subida de este destino.", + "dest.fbBackupHelp": "Envía una segunda copia de esta transmisión a la entrada de respaldo de Facebook, para que una conexión caída no tumbe la transmisión.", + "dest.fbBackupLabel": "Publicar un feed de respaldo", + "dest.fbBackupReconnect": "Activarlo reconecta la transmisión una vez, así que actívalo antes de salir en directo.", + "dest.fbBoxIntro": "Se envía en la misma llamada de creación que Audiencia, así que se aplica la misma regla: se aplica una vez cuando comienza la transmisión, y se deja intacto por completo si está vacío.", + "dest.fbBoxTitle": "Publicación cruzada y botón de donación de Facebook", + "dest.fbCrosspostAdd": "Añadir Página", + "dest.fbCrosspostAlsoPost": "Publicar también", + "dest.fbCrosspostHelp": "El ID numérico de la Página desde la propia consola de Facebook (ajustes de la Página, o graph.facebook.com/me/accounts con la sesión iniciada como la Página): aquí no hay ninguna búsqueda, así que pega el id en lugar de un nombre o una URL. «Publicar también» publica como esa Página en lugar de solo compartir la transmisión con ella; si se deja desactivado, solo ocurre el compartido más discreto.", + "dest.fbCrosspostLabel": "Publicación cruzada en Páginas", + "dest.fbCrosspostPageId": "ID de la Página", + "dest.fbCrosspostRemove": "Quitar esta Página", + "dest.fbDonateHelp": "También es un id opaco de la consola de recaudaciones de Facebook, no un nombre de organización que buscar. Déjalo en blanco para no adjuntar ningún botón de donación.", + "dest.fbDonateLabel": "ID de la organización del botón de donación", + "dest.fbDonatePlaceholder": "ID de la organización", + "dest.fillFromPlatform": "Rellenar con lo que publica esta plataforma", + "dest.friends": "Amigos", + "dest.friendsOfFriends": "Amigos de amigos", + "dest.giveUpAfter": "Rendirse tras (reintentos)", + "dest.leaveOnFacebook": "Dejarlo como está en Facebook", + "dest.leaveOnYouTube": "Dejarlo como está en YouTube", + "dest.madeForKids": "Hecho para niños (COPPA)", + "dest.muxQueue": "Cola de multiplexado (paquetes)", + "dest.muxQueuePair": "Estos dos van juntos: FFmpeg aplica el límite de paquetes solo cuando la cola supera el umbral de bytes, así que un umbral por sí solo no hace nada y será rechazado. Súbelos si un destino informa de errores de interleave: la ruta de audio aquí tiene latencia variable, porque la normalización de sonoridad lee por adelantado.", + "dest.name": "Nombre", + "dest.namePlaceholder": "YouTube — canal principal", + "dest.notLinked": "Sin vincular", + "dest.onlyMe": "Solo yo", + "dest.platform": "Plataforma", + "dest.private": "Privado", + "dest.public": "Público", + "dest.reconnecting": "Reconexión", + "dest.retryAfter": "Reintentar tras (segundos)", + "dest.sharedEncodeAria": "Codificación de vídeo compartida", + "dest.socketTimeout": "Tiempo de espera del socket (segundos)", + "dest.streamKeyLabel": "Clave de transmisión", + "dest.transport": "Transporte", + "dest.transportFile": "Archivo local", + "dest.transportRtmp": "RTMP / RTMPS", + "dest.unlisted": "Oculto", + "dest.urlFromDashboard": "URL del panel", + "dest.useSharedEncode": "Usar una codificación de vídeo compartida", + "dest.varBitrate": "Bitrate de vídeo (kbps)", + "dest.varFps": "Velocidad de fotogramas", + "dest.varHeight": "Alto", + "dest.varSourceHint": "origen", + "dest.varWidth": "Ancho", + "dest.variantWarning": "Inicia una segunda codificación desde tu origen, partiendo de «{name}». No es una variación gratuita y no cambia lo que reciben los demás destinos de esa codificación. Deja un campo en blanco para conservar el valor del origen.", + "dest.videoTreatment": "Tratamiento de vídeo", + "dest.visibility": "Visibilidad", + "hooks.allTriggers": "todo", "hooks.attempts": "{count} intentos", + "hooks.attemptsLabel": "Intentos", "hooks.attemptsOne": "{count} intento", - "hooks.timeout": "{seconds}s de espera", - "hooks.allTriggers": "todo", - "hooks.triggerCount": "{count} disparadores", - "hooks.triggerCountOne": "{count} disparador", - "hooks.enableAria": "Activar {name}", + "hooks.colEnabled": "Act.", + "hooks.colEndpoint": "Endpoint", + "hooks.colTriggers": "Suscrito a", + "hooks.colWebhook": "Webhook", + "hooks.create": "Crear", + "hooks.created": "Webhook creado.", + "hooks.deleteBody": "Deja de entregar de inmediato. El endpoint no se toca y puede volver a crear el hook, pero su clave de firma no se puede recuperar.", + "hooks.deleteConfirm": "Eliminar webhook", + "hooks.deleteFailed": "No se ha podido eliminar el hook.", + "hooks.deleteTitle": "¿Eliminar este webhook?", + "hooks.deleted": "Webhook eliminado.", "hooks.deliveries": "Entregas recientes", - "hooks.sendTest": "Enviar entrega de prueba", - "hooks.sendTestTitle": "Enviar ahora una entrega de prueba", - "hooks.secretTitle": "Clave de firma: cópiela ahora, no se puede volver a mostrar", - "hooks.secretCopied": "Ya la he copiado", - "hooks.httpStatus": "HTTP {status}", - "hooks.noResponse": "sin respuesta", + "hooks.deliveriesFailed": "No se han podido leer las entregas recientes.", + "hooks.deliveryTitle": "Entrega", + "hooks.dialogIntro": "Un POST firmado por transición, en orden. A diferencia de una alerta, nada se agrupa ni se filtra por rebotes: un script recibe todos los flancos.", "hooks.durationMs": "{ms} ms", + "hooks.editTitle": "Editar webhook", + "hooks.empty": "No hay webhooks. Añada uno y polyemesis enviará por POST un cuerpo JSON firmado en cuanto el stream empiece o termine, o un destino se active o se caiga: una entrega por transición, en orden, para que un script actúe.", + "hooks.enableAria": "Activar {name}", + "hooks.httpStatus": "HTTP {status}", + "hooks.loadFailed": "No se han podido cargar los webhooks.", + "hooks.name": "Nombre", + "hooks.new": "Nuevo webhook", + "hooks.newTitle": "Nuevo webhook", "hooks.noDeliveries": "Todavía no se ha entregado nada. Las entregas aparecen aquí a medida que ocurren las transiciones.", - "hooks.deliveryTitle": "Entrega", - "hooks.statSent": "Enviadas", - "hooks.statFailed": "Fallidas", + "hooks.noResponse": "sin respuesta", + "hooks.payloadNote": "Payload {version}. Firma en {header}, sobre la marca de tiempo y el cuerpo.", + "hooks.retriesHint": "Los reintentos bloquean la cola propia de este endpoint, porque el orden es la promesa. Un 4xx nunca se reintenta: un endpoint que dice que la petición está mal lo volverá a decir.", + "hooks.saveFailed": "No se ha podido guardar el hook.", + "hooks.secretCopied": "Ya la he copiado", + "hooks.secretTitle": "Clave de firma: cópiela ahora, no se puede volver a mostrar", + "hooks.sendTest": "Enviar entrega de prueba", + "hooks.sendTestTitle": "Enviar ahora una entrega de prueba", + "hooks.signed": "firmado", + "hooks.statDropped": "Descartadas", "hooks.statEndpoints": "Endpoints", + "hooks.statFailed": "Fallidas", "hooks.statQueued": "En cola", - "hooks.statDropped": "Descartadas", "hooks.statRetries": "Reintentos", - "hooks.payloadNote": "Payload {version}. Firma en {header}, sobre la marca de tiempo y el cuerpo.", - "hooks.deleteTitle": "¿Eliminar este webhook?", - "hooks.deleteBody": "Deja de entregar de inmediato. El endpoint no se toca y puede volver a crear el hook, pero su clave de firma no se puede recuperar.", - "hooks.deleteConfirm": "Eliminar webhook", - "hooks.editTitle": "Editar webhook", - "hooks.newTitle": "Nuevo webhook", - "hooks.dialogIntro": "Un POST firmado por transición, en orden. A diferencia de una alerta, nada se agrupa ni se filtra por rebotes: un script recibe todos los flancos.", - "hooks.name": "Nombre", - "hooks.url": "URL del endpoint", - "hooks.urlMasked": "La URL guardada nunca se muestra completa. Deje el valor enmascarado para conservarla, o escriba una URL nueva para reemplazarla.", - "hooks.triggers": "Disparadores", - "hooks.triggersNoneHint": "No seleccionar ninguno significa todos los disparadores, incluidos los que añada una versión futura.", + "hooks.statSent": "Enviadas", + "hooks.testFailed": "El endpoint no ha aceptado la entrega de prueba.", + "hooks.timeout": "{seconds}s de espera", "hooks.timeoutLabel": "Tiempo de espera (s)", - "hooks.attemptsLabel": "Intentos", - "hooks.retriesHint": "Los reintentos bloquean la cola propia de este endpoint, porque el orden es la promesa. Un 4xx nunca se reintenta: un endpoint que dice que la petición está mal lo volverá a decir.", - "hooks.create": "Crear", - "hooks.triggerIngestPublished": "Stream iniciado", - "hooks.triggerIngestDisconnected": "Stream detenido", - "hooks.triggerDestinationUp": "Destino en directo", - "hooks.triggerDestinationDown": "Destino detenido", - "hooks.loadFailed": "No se han podido cargar los webhooks.", + "hooks.title": "Webhooks de ciclo de vida", "hooks.toggleFailed": "No se ha podido cambiar el hook.", - "hooks.deleted": "Webhook eliminado.", - "hooks.deleteFailed": "No se ha podido eliminar el hook.", - "hooks.testFailed": "El endpoint no ha aceptado la entrega de prueba.", - "hooks.deliveriesFailed": "No se han podido leer las entregas recientes.", + "hooks.triggerCount": "{count} disparadores", + "hooks.triggerCountOne": "{count} disparador", + "hooks.triggerDestinationDown": "Destino detenido", + "hooks.triggerDestinationUp": "Destino en directo", + "hooks.triggerIngestDisconnected": "Stream detenido", + "hooks.triggerIngestPublished": "Stream iniciado", + "hooks.triggers": "Disparadores", + "hooks.triggersNoneHint": "No seleccionar ninguno significa todos los disparadores, incluidos los que añada una versión futura.", + "hooks.unsigned": "SIN FIRMAR", "hooks.updated": "Webhook actualizado.", - "hooks.created": "Webhook creado.", - "hooks.saveFailed": "No se ha podido guardar el hook.", - "playlist.title": "Lista de reproducción", + "hooks.url": "URL del endpoint", + "hooks.urlMasked": "La URL guardada nunca se muestra completa. Deje el valor enmascarado para conservarla, o escriba una URL nueva para reemplazarla.", + "jobs.alwaysKeepNewest": "Conservar siempre las más recientes", + "jobs.automaticModel": "Automático (elegido según este hardware)", + "jobs.backToDefault": "Volver al modo predeterminado de este tipo", + "jobs.batteryFloor": "Mínimo de batería %", + "jobs.broadcastGoing": "Hay una emisión saliendo; el trabajo aplazado le cede el paso.", + "jobs.cancel": "Cancelar", + "jobs.cannotPause": "Estos tipos deberían pausarse pero no pueden, así que están terminando en su lugar.", + "jobs.completed": "Completadas", + "jobs.concurrency": "Concurrencia", + "jobs.couldNotReadTheJob": "No se pudo leer la cola de tareas.", + "jobs.couldNotSaveThePolicy": "No se pudo guardar la política.", + "jobs.cpuCeiling": "Techo de CPU %", + "jobs.cpuResume": "Reanudación de CPU %", + "jobs.defaultMode": "Modo predeterminado", + "jobs.defaultModel": "Modelo predeterminado", + "jobs.deferred": "Aplazadas", + "jobs.failed": "Fallidas", + "jobs.finished": "Terminadas", + "jobs.global": "Global", + "jobs.gpuGateNote": "Esto solo limita los tipos marcados como usuarios de la GPU.", + "jobs.history": "Historial", + "jobs.historyRetention": "Retención del historial", + "jobs.holdDeferredWorkBackOn": "Retiene el trabajo aplazado en un portátil que se está descargando por debajo de esto. 0 lo desactiva.", + "jobs.hostCpuLoad": "Carga de CPU del host", + "jobs.howManyJobsMayRun": "Cuántas tareas pueden ejecutarse a la vez. Una, porque una segunda transcodificación compra un rendimiento que nadie ha pedido a un riesgo que nadie ha aceptado.", + "jobs.inQueue": "En la cola", + "jobs.job": "Tarea", + "jobs.keepForDays": "Conservar durante días", + "jobs.machineSafety": "Seguridad de la máquina. Esta es la única barrera que el trabajo en tiempo real también respeta.", + "jobs.niceLevel": "Nivel nice", + "jobs.noFinished": "Todavía no hay tareas terminadas.", + "jobs.noModels": "No se han encontrado archivos de modelo en el directorio de modelos. «Automático» descargará el que elija; nombrar aquí un modelo concreto solo funciona una vez que ese archivo existe.", + "jobs.nothingNewStartsAboveThis": "Por encima de esto no se inicia nada nuevo. 0 desactiva la barrera.", + "jobs.nothingQueued": "No hay nada en cola. La posproducción se envía desde la biblioteca.", + "jobs.nothingWasOldEnoughTo": "Nada era lo bastante antiguo para purgarlo. Baje antes la retención en Política.", + "jobs.pauseAll": "Pausar todo", + "jobs.policy": "Política", + "jobs.policySaved": "Política de recursos guardada.", + "jobs.progress": "Progreso", + "jobs.queuePaused": "Cola pausada.", + "jobs.queueResumed": "Cola reanudada.", + "jobs.queued": "En cola", + "jobs.recording": "Grabación", + "jobs.releasedItWillStartAs": "Liberada. Empezará en cuanto haya un hueco libre.", + "jobs.removeFromHistory": "Quitar del historial", + "jobs.removeWindow": "Quitar ventana", + "jobs.resumeAll": "Reanudar todo", + "jobs.retry": "Reintentar", + "jobs.retryTitle": "Rearmar con un presupuesto de intentos nuevo", + "jobs.runNow": "Ejecutar ahora", + "jobs.runNowTitle": "Ejecutar esta ahora, ignorando la política de recursos", + "jobs.running": "En ejecución", + "jobs.savedTheConcurrencyChangeTakes": "Guardado. El cambio de concurrencia surte efecto cuando el servidor se reinicia: la cola lo fija al arrancar.", + "jobs.state": "Estado", + "jobs.stopsEverythingRealtimeIncludedA": "Detiene todo, incluido el tiempo real: una CPU que se está limitando ya ha empezado a degradar la transmisión. 0 lo desactiva.", + "jobs.subtitle": "Trabajo en segundo plano: transcripción, proxies, miniaturas y compresión del archivo.", + "jobs.thatDidNotWork": "Eso no ha funcionado.", + "jobs.thermalCeiling": "Techo térmico °C", + "jobs.thisManyFinishedJobsSurvive": "Este número de tareas terminadas sobrevive sea cual sea su antigüedad.", + "jobs.title": "Tareas", + "jobs.transcription": "Transcripción", + "jobs.unsavedChanges": "Cambios sin guardar.", + "jobs.whereTheCeilingReleasesThe": "Dónde se libera el techo. La diferencia es la histéresis que evita que la barrera oscile.", + "jobs.whisperMissing": "whisper.cpp no está instalado aquí, así que esto todavía no tiene efecto. Merece la pena configurarlo igualmente: la elección se aplica en cuanto lo esté.", + "jobs.why": "Motivo", + "jobs.windowEnd": "Fin de la ventana", + "jobs.windowStart": "Inicio de la ventana", + "jobs.yieldNote": "El trabajo pesado se pone en cola y cede ante la transmisión en directo. Un fotograma perdido es irrecuperable; una transcripción que llega una hora más tarde no cuesta nada.", + "lib.anySpeaker": "Cualquier hablante", + "lib.anyTag": "Cualquier etiqueta", + "lib.autoGrouped": "Agrupada automáticamente. Editarla la hace suya.", + "lib.autoGroupedNote": "Esta sesión se agrupó automáticamente. Guardarla la hace suya, y la pasada de agrupación dejará de reescribirla.", + "lib.bestMatch": "Mejor coincidencia", + "lib.clearSearch": "Limpiar la búsqueda", + "lib.clip": "Clip", + "lib.clipTitle": "Cortar un clip de esta grabación", + "lib.collapseSession": "Contraer la sesión", + "lib.couldNotLoadTheLibrary": "No se pudo cargar la biblioteca.", + "lib.couldNotLoadTheSession": "No se pudieron cargar los segmentos de la sesión.", + "lib.couldNotQueueTheProxy": "No se pudo poner el proxy en cola.", + "lib.couldNotRegroup": "No se pudo reagrupar.", + "lib.couldNotSaveTheSession": "No se pudo guardar la sesión.", + "lib.delete": "Eliminar", + "lib.deleteDescription": "El archivo se elimina del disco, junto con sus proxies, miniaturas y transcripción. Esto no se puede deshacer.", + "lib.deleteTitle": "¿Eliminar esta grabación?", + "lib.downloadMaster": "Descargar el máster", + "lib.downloadMasterTitle": "Descargar el máster multipista", + "lib.editSession": "Editar la sesión", + "lib.expandSession": "Expandir la sesión", + "lib.from": "De", + "lib.generateProxy": "Generar proxy", + "lib.generateProxyTitle": "Generar el proxy reproducible en el navegador", + "lib.mostRecent": "Más recientes primero", + "lib.noProxyYet": "Todavía no hay proxy — ábrala para generar uno", + "lib.noSegments": "Aquí no hay segmentos.", + "lib.noSessionMatch": "Ninguna sesión coincide con esos filtros.", + "lib.nothingRecorded": "Todavía no se ha grabado nada. Las sesiones aparecen a medida que el grabador escribe segmentos.", + "lib.oldestFirst": "Más antiguas primero", + "lib.playProxy": "Reproducir el proxy", + "lib.proxyAlreadyQueued": "Ya hay un proxy en cola.", + "lib.proxyQueued": "Proxy en cola.", + "lib.recordingDeleted": "Grabación eliminada.", + "lib.regroupTitle": "Vuelve a ejecutar la agrupación. Aditiva e idempotente: nunca vuelve a unir una agrupación que usted separó a mano.", + "lib.searchPlaceholder": "Busque todo lo que dijo cualquiera, en todas las grabaciones…", + "lib.searchTranscripts": "Buscar en las transcripciones", + "lib.searchable": "Buscable", + "lib.searchableOnly": "Segmentos con transcripción, y por tanto buscables", + "lib.session": "Sesión", + "lib.sessionDescription": "Descripción", + "lib.sessionSaved": "Sesión guardada.", + "lib.sessionTags": "Etiquetas", + "lib.sessionTitle": "Título", + "lib.subtitle": "Todas las emisiones, buscables. Cada micrófono se grabó en su propia pista, así que la transcripción ya sabe quién lo dijo.", + "lib.tag": "Etiqueta", + "lib.tagsPlaceholder": "separadas, por, comas", + "lib.title": "Biblioteca", + "lib.transcribe": "Transcribir", + "lib.transcribeEachTrackOnIts": "Transcribir cada pista por separado: eso es lo que da la atribución de quien habla", + "lib.ungrouped": "Segmentos sin agrupar", + "meters.deviation": "Desviación", + "meters.idle": "inactivo", + "meters.integrated": "Integrada", + "meters.loudness": "Cumplimiento de sonoridad", + "meters.metering": "midiendo", + "meters.momentary": "Momentánea", + "meters.monitorOff": "El monitor está apagado. Enciéndalo para medir lo que entrega cada destino.", + "meters.noSignal": "sin señal en esta pista", + "meters.nothingToMeasure": "Todavía no hay nada que medir. Cada destino en ejecución recibe su propio analizador EBU R128 sobre la mezcla que recibe.", + "meters.range": "Rango", + "meters.shortTerm": "Corto plazo", + "meters.subtitle": "Cada canal de cada pista de entrada, directamente desde el relé.", + "meters.title": "Medidores de audio", + "meters.truePeak": "Pico real", + "meters.waitingForStream": "esperando la transmisión", + "mon.allProcesses": "Todos los procesos", + "mon.bitrateChart": "Tasa de bits de entrada — últimos 30 minutos", + "mon.chooseDestination": "Elija un destino…", + "mon.clearLog": "Borrar registro", + "mon.clearedThisDestinationIsBack": "Borrado. Este destino vuelve a usar el comando generado.", + "mon.dryRun": "Simulación", + "mon.dryRunInvalid": "FFmpeg ha rechazado un argumento", + "mon.dryRunOk": "FFmpeg lo ha aceptado", + "mon.extraInput": "Argumentos de entrada adicionales", + "mon.extraOutput": "Argumentos de salida adicionales", + "mon.ffmpegLog": "Registro de FFmpeg", + "mon.fromRunningProcess": "del proceso en ejecución", + "mon.fullCommand": "Comando resuelto completo", + "mon.hostCpu": "CPU del host", + "mon.ingestLoss": "Pérdida de entrada", + "mon.memory": "Memoria", + "mon.nothingProvenEitherWay": "Nada demostrado en ningún sentido", + "mon.pid": "PID", + "mon.processes": "Procesos", + "mon.rebuilt": "reconstruido", + "mon.relayDrops": "Descartes del relé", + "mon.relayIn": "Entrada al relé", + "mon.resolveHint": "Resuelva el comando para ver exactamente qué se ejecutará antes de aplicar.", + "mon.restarts": "Reinicios", + "mon.subtitle": "Salud de los procesos, recursos del host y el registro de FFmpeg en vivo.", + "mon.title": "Monitorización", + "mon.uptime": "Tiempo activo", + "nav.automation": "Automatización", + "nav.chat": "Chat", + "nav.clips": "Fragmentos", + "nav.dashboard": "Panel de control", + "nav.jobs": "Tareas", + "nav.library": "Biblioteca", + "nav.meters": "Medidores de audio", + "nav.monitoring": "Monitorización", + "nav.playout": "Playout", + "nav.recordings": "Grabaciones", + "nav.renditions": "Versiones", + "nav.routing": "Routing", + "nav.settings": "Ajustes", + "nav.sources": "Fuentes", + "play.acrossEveryRungTheBackstop": "En todos los escalones. La salvaguarda que impide que una emisión larga llene el disco.", + "play.allowCrossOrigin": "Permitir cross-origin", + "play.allowCrossOriginPlayback": "Permitir la reproducción cross-origin", + "play.audioBitrate": "Tasa de bits de audio (kbps)", + "play.audioTrack": "Pista de audio", + "play.basicPasswordNote": "Los reproductores que no pueden seguir un enlace (un decodificador, un CDN) pueden enviar el mismo secreto como contraseña básica HTTP en su lugar.", + "play.boundsTheViewerTableViewers": "Limita la tabla de espectadores. Los espectadores que la superan se sirven, solo que no se cuentan.", + "play.carriesThePlaybackTokenAnyone": "Lleva el token de reproducción. Cualquiera con esta URL puede ver.", + "play.clipboardUnavailable": "El portapapeles no está disponible; seleccione y copie a mano", + "play.couldNotLoadPlayout": "No se pudo cargar el playout", + "play.disabled": "Desactivado", + "play.diskCap": "Techo de disco (MB)", + "play.dvrWindow": "Ventana DVR (s)", + "play.embedCode": "Código para incrustar", + "play.enable": "Activar el playout", + "play.enabled": "Activado", + "play.forObsACdnOr": "Para OBS, un CDN o cualquier reproductor que hable HLS.", + "play.format": "Formato", + "play.hls": "HLS", + "play.hlsDash": "HLS y DASH", + "play.hlsPlaylist": "Lista de reproducción HLS", + "play.idleTimeout": "Tiempo de inactividad del espectador (s)", + "play.linkReset": "Se ha generado un enlace de reproducción nuevo. El anterior ya no funciona.", + "play.linksProtected": "Estos enlaces solo funcionan para un administrador con la sesión iniciada hasta que active Público.", + "play.linksPublic": "Estos enlaces funcionan sin cuenta.", + "play.liveWindow": "Ventana en directo (segmentos)", + "play.metaDescription": "Descripción", + "play.metaTitle": "Título", + "play.mustExceedOneSegmentOr": "Debe superar un segmento, o los espectadores parpadearán y desaparecerán entre sondeos.", + "play.name": "Nombre", + "play.noCredentialNote": "No se exige ninguna credencial de ningún tipo. Solo es apropiado para una transmisión que quiera que vea todo internet.", + "play.noRungs": "Todavía no hay escalones. Añada uno para publicar algo.", + "play.onDisk": "En disco", + "play.oneStereoAacTrackPer": "Una pista AAC estéreo por escalón.", + "play.packaging": "Empaquetado", + "play.passthrough": "Entrada (paso directo)", + "play.peak": "Pico", + "play.playerPage": "Página del reproductor", + "play.posterNote": "Se muestra sobre el vídeo en la página pública. El póster es un fotograma clave reciente y se actualiza automáticamente.", + "play.private": "Privado.", + "play.protectedNote": "La reproducción está protegida de forma predeterminada. Abrirla es un paso aparte y deliberado.", + "play.protection": "Protección", + "play.public": "Público", + "play.resetFailed": "No se pudo restablecer", + "play.restartNote": "Cambiar cualquiera de estos reinicia los multiplexores, lo que interrumpe a los espectadores durante unos segundos.", + "play.rungNote": "Cada escalón copia el vídeo de su versión bit a bit, así que añadir uno cuesta un multiplexor y nunca una segunda codificación de vídeo. Solo la pista de audio cambia por escalón; el enrutado por destino queda intacto.", + "play.segmentLength": "Duración del segmento (s)", + "play.serveWithoutSession": "Servir a los espectadores sin sesión", + "play.sessionCap": "Techo de sesiones", + "play.sessions": "Sesiones", + "play.share": "Compartir", + "play.shorterIsLowerLatencyAnd": "Más corto es menos latencia y más rotación de la lista de reproducción.", + "play.subtitle": "Sirva la transmisión directamente a los espectadores, como HLS público.", + "play.threeIsTheMinimumA": "Tres es el mínimo con el que un reproductor puede almacenar en búfer.", + "play.title": "Playout", + "play.unlisted": "No listado.", + "play.untitled": "Transmisión sin título", + "play.videoSource": "Fuente de vídeo", + "play.watchingNow": "Viendo ahora", + "play.whoCanWatch": "Quién puede ver", + "player.copied": "Copiado", + "player.copy": "Copiar", + "player.loading": "Cargando…", + "player.missingToken": "Al enlace que ha seguido le falta su token de reproducción, o el token ha cambiado. Pida un enlace actual a quien se lo compartió.", + "player.notLive": "No hay emisión en directo ahora mismo", + "player.viewer": "espectador", + "player.viewers": "espectadores", + "player.willStart": "Esta página empezará a reproducir por sí sola cuando comience la emisión.", + "playlist.choose": "Elija una subida", + "playlist.chooseAria": "Elija una subida para añadir", "playlist.empty": "Todavía no hay nada en cola.", + "playlist.hint": "Se suben en Media. Un elemento de esta lista que después pierda su archivo aparece arriba como que necesita atención, en lugar de fallar en silencio la próxima vez que el failover llegue a él.", "playlist.itemsAria": "Elementos de la lista", - "playlist.stateReady": "Listo", - "playlist.stateTranscoding": "Transcodificando", - "playlist.stateAttention": "Necesita atención", - "playlist.stateUnsaved": "Sin guardar todavía", "playlist.missingUpload": "Esta subida ya no se encuentra.", - "playlist.moveUp": "Subir {name}", "playlist.moveDown": "Bajar {name}", - "playlist.remove": "Quitar {name}", - "playlist.choose": "Elija una subida", - "playlist.chooseAria": "Elija una subida para añadir", + "playlist.moveUp": "Subir {name}", "playlist.noUploads": "No hay subidas disponibles", - "playlist.hint": "Se suben en Media. Un elemento de esta lista que después pierda su archivo aparece arriba como que necesita atención, en lugar de fallar en silencio la próxima vez que el failover llegue a él.", - "dest.fbBoxTitle": "Publicación cruzada y botón de donación de Facebook", - "dest.fbBoxIntro": "Se envía en la misma llamada de creación que Audiencia, así que se aplica la misma regla: se aplica una vez cuando comienza la transmisión, y se deja intacto por completo si está vacío.", - "dest.fbBackupLabel": "Publicar un feed de respaldo", - "dest.fbBackupHelp": "Envía una segunda copia de esta transmisión a la entrada de respaldo de Facebook, para que una conexión caída no tumbe la transmisión.", - "dest.fbBackupCost": "Duplica el ancho de banda de subida de este destino.", - "dest.fbBackupReconnect": "Activarlo reconecta la transmisión una vez, así que actívalo antes de salir en directo.", - "dest.fbCrosspostLabel": "Publicación cruzada en Páginas", - "dest.fbCrosspostPageId": "ID de la Página", - "dest.fbCrosspostAlsoPost": "Publicar también", - "dest.fbCrosspostRemove": "Quitar esta Página", - "dest.fbCrosspostAdd": "Añadir Página", - "dest.fbCrosspostHelp": "El ID numérico de la Página desde la propia consola de Facebook (ajustes de la Página, o graph.facebook.com/me/accounts con la sesión iniciada como la Página): aquí no hay ninguna búsqueda, así que pega el id en lugar de un nombre o una URL. «Publicar también» publica como esa Página en lugar de solo compartir la transmisión con ella; si se deja desactivado, solo ocurre el compartido más discreto.", - "dest.fbDonateLabel": "ID de la organización del botón de donación", - "dest.fbDonatePlaceholder": "ID de la organización", - "dest.fbDonateHelp": "También es un id opaco de la consola de recaudaciones de Facebook, no un nombre de organización que buscar. Déjalo en blanco para no adjuntar ningún botón de donación." + "playlist.remove": "Quitar {name}", + "playlist.stateAttention": "Necesita atención", + "playlist.stateReady": "Listo", + "playlist.stateTranscoding": "Transcodificando", + "playlist.stateUnsaved": "Sin guardar todavía", + "playlist.title": "Lista de reproducción", + "rec.delete": "Eliminar", + "rec.deleteDescription": "El archivo se elimina del disco, junto con sus proxies, miniaturas y transcripción. Esto no se puede deshacer.", + "rec.deleteFailed": "No se pudo eliminar la grabación.", + "rec.deleteTitle": "¿Eliminar esta grabación?", + "rec.deleted": "Grabación eliminada.", + "rec.disabled": "desactivado", + "rec.disk": "Disco", + "rec.diskRecordings": "Grabaciones", + "rec.download": "Descargar", + "rec.downloadNamed": "Descargar {name}", + "rec.duration": "Duración", + "rec.empty": "Todavía no hay grabaciones. Active la grabación a la derecha e inicie una transmisión.", + "rec.enabled": "Activado", + "rec.file": "Archivo", + "rec.free": "Libre", + "rec.haltedDefault": "La grabación está detenida: el volumen está por debajo del mínimo de espacio libre.", + "rec.haltedRestarts": "El grabador se reinicia solo en cuanto se libere espacio.", + "rec.help.enabled": "Escribe en disco cada entrada mientras se transmite, manteniendo todas las pistas de audio separadas y sin codificar. La grabación es independiente de lo que envía a las plataformas: desactivarla no afecta a su salida en directo, y activarla cuesta disco en lugar de ancho de banda de subida.", + "rec.help.maxAgeHours": "Elimina las grabaciones con más horas de antigüedad que este valor, sea cual sea el tamaño total. Útil cuando solo necesita el último día o dos. Ponga 0 para conservarlo todo hasta que otro límite lo elimine.", + "rec.help.maxGb": "Un techo para el tamaño total de las grabaciones que polyemesis conserva. Al superarlo, se eliminan las más antiguas hasta que el total vuelva a caber. Ponga 0 para no tener límite de tamaño; en ese caso, la edad o el mínimo de espacio libre serán lo único que acote el crecimiento.", + "rec.help.minFreeGb": "Un tope absoluto: la grabación se detiene cuando el volumen baja de este espacio libre y se reanuda sola cuando se libera espacio. Es el único límite que tiene en cuenta archivos que polyemesis no escribió, así que es el ajuste que realmente protege un disco compartido de llenarse.", + "rec.help.segmentSeconds": "Cuánto dura cada archivo antes de que el grabador empiece otro. Los segmentos más cortos limitan lo que puede costarle un fallo y facilitan mover archivos sueltos, pero dejan más que gestionar. Los cortes caen en un fotograma clave, así que la duración real varía en torno a un segundo.", + "rec.help.stemFiles": "Escribe además cada pista de audio como archivo propio junto al máster. Útil cuando edita después y quiere la voz principal, los invitados y el audio del juego en canales separados. Multiplica lo que escribe una sesión por aproximadamente el número de pistas, y por eso está desactivado de forma predeterminada.", + "rec.help.stemFormat": "Ambas opciones son sin pérdida, así que ninguna cuesta calidad. FLAC ocupa aproximadamente la mitad y es lo que conviene salvo que alguna herramienta de su flujo de trabajo no pueda leerlo; WAV se abre en cualquier cosa. Las pistas de más de 8 canales pasan a WAV automáticamente.", + "rec.hideStems": "Ocultar pistas separadas", + "rec.keepForever": "0 = conservar para siempre.", + "rec.loadFailed": "No se pudieron cargar las grabaciones.", + "rec.maxAgeHours": "Eliminar después de (horas)", + "rec.maxGb": "Conservar como máximo (GB)", + "rec.minFreeGb": "Detener por debajo de espacio libre (GB)", + "rec.minFreeNote": "Los límites de arriba solo acotan lo que escribió polyemesis; cualquier otra cosa en el volumen puede llenarlo igualmente. 0 = sin mínimo.", + "rec.noSizeLimit": "0 = sin límite de tamaño.", + "rec.recording": "grabando", + "rec.retentionNote": "La retención se ejecuta cada 30 segundos: primero las más antiguas, por edad y luego por tamaño total. El segmento que se está escribiendo nunca se elimina.", + "rec.saveFailed": "No se pudieron guardar los ajustes.", + "rec.savePolicy": "Guardar política de retención", + "rec.saved": "Ajustes de grabación guardados.", + "rec.segmentSeconds": "Duración del segmento (segundos)", + "rec.segments": "Segmentos", + "rec.settingsTitle": "Grabación", + "rec.showStems": "Mostrar pistas separadas", + "rec.size": "Tamaño", + "rec.started": "Iniciada", + "rec.stemFiles": "Archivos de pista separada", + "rec.stemFlac": "FLAC — sin pérdida, aproximadamente la mitad de tamaño", + "rec.stemFormat": "Formato de pista separada", + "rec.stemNote": "Se escribe desde el mismo proceso que el máster, por lo que cada pista separada permanece alineada por muestra con las demás. Las pistas de más de 8 canales pasan a WAV por su cuenta.", + "rec.stemWav": "WAV — sin pérdida, se abre en cualquier sitio", + "rec.stems": "Pistas separadas", + "rec.stemsTitle": "{count} archivos de pista separada, alineados por muestra con el máster", + "rec.stemsUsage": "Pistas separadas ({count} archivos)", + "rec.subtitle": "El archivo multipista completo: cada pista de audio de entrada conservada, sin codificar.", + "rec.title": "Grabaciones", + "rec.tracks": "Pistas", + "rec.tracksTitle": "{count} pistas de audio conservadas", + "rec.used": "Usado", + "rec.volume": "Volumen", + "rend.aspectBlurpad": "Relleno desenfocado", + "rend.aspectBlurpadHint": "Rellena el resto con una copia desenfocada del propio fotograma, recortada para llenar. Es la convención que han adoptado los feeds verticales.", + "rend.aspectCrop": "Recortar para llenar", + "rend.aspectCropHint": "Recorta desde el centro a la forma de destino y luego escala. Los sujetos conservan su tamaño en pantalla; los bordes del fotograma desaparecen.", + "rend.aspectHandling": "Tratamiento de la relación de aspecto", + "rend.aspectPad": "Letterbox", + "rend.aspectPadHint": "Escala el fotograma entero para que quepa y rellena el resto con un color plano. No se pierde nada, pero una fuente 16:9 en un lienzo 9:16 es sobre todo barras.", + "rend.aspectStretch": "Estirar para ajustar", + "rend.aspectStretchHint": "Escala al tamaño de destino y deja que la imagen se deforme si la fuente no coincide. Lo que hacía toda versión antes de que existieran los demás modos.", + "rend.boxColour": "Color del recuadro", + "rend.boxOpacity": "Opacidad del recuadro (%)", + "rend.builtInFont": "Predeterminada integrada", + "rend.burnedText": "Texto incrustado", + "rend.chooseHardware": "Elija un codificador por hardware, o una salida más pequeña.", + "rend.colourBlack": "black", + "rend.colourWhite": "white", + "rend.costBeyond": "Codificar esto por software en tiempo real está fuera del alcance de la mayoría de las máquinas", + "rend.costBeyondDetail": "{label} son {mpps} para una sola codificación. x264 a este tamaño suele perder la carrera contra el tiempo real, y una versión que se queda atrás pierde fotogramas para todos los destinos que la leen, algo que descubre en mitad de la transmisión.{machine} {escape}", + "rend.costBusy": "Cuente con que varios núcleos estarán ocupados", + "rend.costBusyDetail": "{label} son {mpps} en {encoder}, lo que es una codificación en tiempo real pesada.{machine} Vigile la velocidad del codificador en cuanto esté en directo: por debajo de 1,0× se está quedando atrás y los destinos que dependen de él empezarán a perder fotogramas. {escape}", + "rend.costMachineCores": " Esta máquina informa de {cores} núcleos lógicos.", + "rend.costModest": "Codificación por software modesta", + "rend.costModestDetail": "{label} son {mpps}, al alcance de la mayoría de las máquinas.{machine} Sigue siendo una codificación real: compartida por todos los destinos que la seleccionan, pero no gratuita.", + "rend.costNoNumbers": "Todavía no se puede estimar el coste", + "rend.costNoNumbersDetail": "Nada ha sondeado la fuente, así que «conservar la fuente» no tiene números detrás. Inicie la entrada y esto se rellenará.", + "rend.costOffloaded": "Descargado en {encoder}", + "rend.costOffloadedDetail": "{label} ({mpps}) se ejecuta en el codificador de función fija de la GPU, así que cuesta poca CPU. La calidad y el control de tasa varían según el controlador, y cada chip tiene su propio techo: compruebe la salida antes de un programa que importe.", + "rend.create": "Crear", + "rend.created": "Versión creada. Selecciónela en un destino para iniciar la codificación.", + "rend.custom": "Personalizado…", + "rend.deintAll": "Todos los fotogramas", + "rend.deintAllHint": "Para fuentes que están entrelazadas pero no lo declaran. Muchos puentes SDI y tarjetas de captura marcan todo como progresivo sea cual sea lo que reciben, y en esos «solo fotogramas entrelazados» no hace nada y parece un ajuste roto.", + "rend.deintAuto": "Solo fotogramas entrelazados", + "rend.deintAutoHint": "Solo toca los fotogramas que la fuente marcó como entrelazados, así que los progresivos pasan intactos. La elección correcta para cualquier material mixto.", + "rend.deintOff": "Desactivado", + "rend.deintOffHint": "Lo correcto para las fuentes progresivas que casi todo el mundo tiene. Desentrelazar una la suaviza sin ninguna ganancia.", + "rend.deinterlace": "Desentrelazar", + "rend.deleteFailed": "No se pudo eliminar la versión.", + "rend.deleteFallbackMany": "{count} destinos vuelven al paso directo y recibirán el vídeo de la fuente sin cambios. Compruebe que la fuente sigue encajando con lo que acepta cada plataforma.", + "rend.deleteFallbackOne": "{count} destino vuelve al paso directo y recibirá el vídeo de la fuente sin cambios. Compruebe que la fuente sigue encajando con lo que acepta la plataforma.", + "rend.deleteNoDestinations": "Nada selecciona esta versión, así que eliminarla no cambia ningún destino.", + "rend.deleteRenditionTitle": "¿Eliminar {name}?", + "rend.editRendition": "Editar versión", + "rend.empty": "No hay versiones. Todos los destinos están en paso directo, que es exactamente lo correcto hasta que alguno no pueda aceptar la fuente: por ejemplo, una entrada 4K60 que Twitch, Kick o X no admiten. Añada una versión y apunte esos destinos a ella.", + "rend.encoder": "Codificador", + "rend.encoderPreset": "Preset del codificador", + "rend.encoderRelatedFailed": "{name} no se ha ofrecido porque un codificador relacionado ha fallado aquí.", + "rend.encoderTestFailed": "{name} ha fallado una codificación de prueba de un fotograma en esta máquina.", + "rend.encoderTestOk": "{name} ha codificado un fotograma de prueba en esta máquina.", + "rend.evenNote": "Un 0 en un eje conserva el de la fuente y mantiene la relación de aspecto: fije solo la altura para reescalar. Ambos deben ser números pares: H.264 y HEVC no tienen un plano de croma de tamaño impar.", + "rend.font": "Tipografía", + "rend.frameRate": "Velocidad de fotogramas", + "rend.gop": "GOP", + "rend.gpuUnopenable": "Hay una GPU presente pero este proceso no puede abrirla", + "rend.hardwareFallback": "hardware", + "rend.hardwareScanFound": "Lo que encontró el análisis del hardware", + "rend.height": "Altura", + "rend.hevcWarning": "Este codificador produce HEVC. La mayoría de los destinos RTMP solo aceptan H.264: elíjalo únicamente para un destino que le haya confirmado que admite HEVC.", + "rend.keyframeInterval": "Intervalo de fotogramas clave", + "rend.letterboxColour": "Color del letterbox", + "rend.live": "En vivo", + "rend.loadFailed": "No se pudieron cargar las versiones.", + "rend.marginX": "Margen X (%)", + "rend.marginY": "Margen Y (%)", + "rend.name": "Nombre", + "rend.namePlaceholder": "1080p60 para Twitch y Kick", + "rend.needsBothAWidthAnd": "Necesita tanto una anchura como una altura: con un eje libre, el escalado ya conserva la relación de aspecto.", + "rend.needsBothAxes": "Necesita tanto una anchura como una altura", + "rend.newRendition": "Nueva versión", + "rend.noEncoder": "Este FFmpeg no ha informado de ningún codificador de vídeo utilizable.", + "rend.noHardwareEscape": "En esta máquina no funciona ningún codificador por hardware, así que no hay adónde trasladar esto: la única palanca aquí es una salida más pequeña. Vuelva a detectar el hardware si desde entonces ha instalado un controlador o ha pasado una GPU al contenedor.", + "rend.notProbed": "Todavía no se ha codificado nada de prueba en esta máquina, así que esta lista es lo que contiene la compilación de FFmpeg y no lo que puede hacer el hardware. Vuelva a detectar para averiguarlo.", + "rend.notRunning": "Detenida", + "rend.note": "Nota", + "rend.noteDuplicateFps": "La fuente va a {srcFps} fps, así que {fps} fps solo duplica fotogramas.", + "rend.notePlaceholder": "Para qué sirve este nivel.", + "rend.noteUpscale": "La fuente es {width}×{height}. Escalar hacia arriba cuesta CPU y no añade detalle.", + "rend.opacity": "Opacidad (%)", + "rend.percentNote": "Todo es un porcentaje del fotograma, así que los mismos ajustes son correctos en un nivel 16:9 y en uno 9:16. Los márgenes se ignoran en un eje centrado.", + "rend.preset": "Preset", + "rend.presetPlaceholder": "veryfast", + "rend.presetStartingPoint": "Punto de partida: verifique los límites actuales con la plataforma.", + "rend.redetectFailed": "No se pudo volver a detectar el hardware.", + "rend.redetectTitle": "Vuelve a ejecutar el análisis de la GPU y codifica un fotograma de prueba con cada codificador. Tarda unos segundos.", + "rend.redetectedNoHardware": "Hardware detectado de nuevo. Ningún codificador por hardware ha superado una codificación de prueba en esta máquina.", + "rend.resolution": "Resolución", + "rend.restartEncode": "Reiniciar esta codificación y los destinos que la leen", + "rend.restartFailed": "No se pudo reiniciar la codificación.", + "rend.restarting": "{name} se está reiniciando. Sus destinos se reconectan con ella.", + "rend.save": "Guardar", + "rend.saveFailed": "No se pudo guardar la versión.", + "rend.seedNote": "Estos solo rellenan el formulario y nada más: edite lo que quiera antes de guardar. Los techos de las plataformas cambian y difieren según el estado de socio.", + "rend.signalError": "Error", + "rend.signalIdle": "Inactivo", + "rend.sizePct": "Tamaño (% de la altura)", + "rend.softwareEncoderFallback": "un codificador por software", + "rend.speed": "Velocidad", + "rend.subtitle": "Una codificación de vídeo compartida, muchos destinos. El audio nunca se toca aquí.", + "rend.target": "Objetivo", + "rend.textColour": "Color del texto", + "rend.textPlaceholder": "MI EMISORA", + "rend.title": "Versiones", + "rend.unused": "Todavía no hay ningún destino que seleccione esto, así que no se está codificando nada. Elíjalo en un destino para iniciar la codificación.", + "rend.uptime": "Tiempo activo", + "rend.videoBitrate": "Tasa de bits de vídeo", + "rend.waitingSnapshot": "Esperando la primera instantánea en directo.", + "rend.watermarkImage": "Imagen de marca de agua", + "rend.watermarkPlaceholder": "overlays/logo.png", + "rend.width": "Anchura", + "rend.widthPct": "Anchura (% del fotograma)", + "route.attack": "Ataque", + "route.audio": "audio", + "route.audioAndVideoGoOut": "El audio y el vídeo salen juntos.", + "route.audioLaterThanVideo": "Audio después del vídeo", + "route.audioRouting": "Enrutado de audio", + "route.audioRoutingDesc": "Elija exactamente qué pistas de la entrada recibe cada destino.", + "route.autoLimitWhen2Tracks": "Automático (limitar con 2 o más pistas)", + "route.commentaryLanguage": "Idioma del comentario", + "route.couldNotApplyThePreset": "No se pudo aplicar el preset.", + "route.couldNotCompileThisRouting": "No se pudo compilar este enrutado.", + "route.couldNotLoadDestinations": "No se pudieron cargar los destinos.", + "route.couldNotSaveTheRouting": "No se pudo guardar el enrutado.", + "route.couldNotSaveTrackRoles": "No se pudieron guardar los roles de las pistas.", + "route.custom": "Personalizado…", + "route.delayDirection": "Dirección del retardo", + "route.destinations": "Destinos", + "route.duckingNote": "Baja un grupo de pistas siempre que otro esté hablando: el micrófono sobre la base musical. Desactivado significa que cada pista seleccionada se suma con su propia ganancia, sin cambios.", + "route.duckingTriggerNote": "Un disparador no tiene por qué estar en la mezcla de este destino: un micrófono puede bajar la música aquí mientras solo se envía la música.", + "route.enableDucking": "Activar el ducking", + "route.everyTrackReaches": "Todas las pistas seleccionadas llegan a este destino, música incluida.", + "route.excludeMusic": "Excluir la música", + "route.keepMusic": "Conservar la música", + "route.keepMusicOut": "Mantener la música fuera de este destino", + "route.labelTracks": "Etiquetar pistas", + "route.langPlaceholder": "lang — p. ej. es", + "route.limiter04DbfsCeiling": "Limitador (techo de −0,4 dBFS)", + "route.localOnlyNote": "Este servidor todavía no almacena los roles de pista, así que estos se quedan en este navegador. Todo lo demás de la página se guarda con normalidad.", + "route.loudnessTarget": "Objetivo de sonoridad", + "route.lufsBroadcast": "EBU R128 emisión", + "route.measuredOverTheWholeProgramme": "Se mide sobre el programa entero, no momento a momento: los pasajes silenciosos siguen siendo silenciosos. Son las cifras a las que apuntan los propios normalizadores de las plataformas, así que alcanzar una es lo que impide que la plataforma le baje el volumen.", + "route.mixMatrix": "Matriz de mezcla", + "route.musicPolicy": "Política de música", + "route.noTarget": "Sin objetivo (dejar los niveles como están)", + "route.normalizeLoudnorm": "Sonoridad (EBU R128)", + "route.normalizeOff": "Desactivado", + "route.off": "desactivado", + "route.offset": "Desfase", + "route.on": "activado", + "route.platformDefault": "Predeterminado de la plataforma", + "route.podcastSpokenWord": "Pódcast / palabra hablada", + "route.presetApplied": "Preset aplicado. Revíselo y guarde después.", + "route.range": "Rango", + "route.ratio": "Ratio", + "route.release": "Liberación", + "route.result": "Resultado", + "route.role51": "5.1", + "route.roleClean": "clean", + "route.roleMic": "micro", + "route.roleMusic": "música", + "route.rolesNote": "Los roles describen la entrada, no este destino, y se guardan por su cuenta. No se filtra nada hasta que un destino de abajo lo pida.", + "route.simple": "Simple", + "route.target": "Objetivo", + "route.targetRole": "objetivo", + "route.thisDestinationSPlatform": "La plataforma de este destino", + "route.threshold": "Umbral", + "route.title": "Routing", + "route.triggerRole": "disparador", + "route.truePeak": "Pico real", + "route.video": "vídeo", + "route.videoLaterThanAudio": "Vídeo después del audio", + "route.withoutATargetTheMix": "Sin un objetivo, la mezcla sale al nivel que haya producido la entrada, con solo la protección contra saturación de arriba aplicada.", + "route.youtubeTwitchSpotify": "YouTube, Twitch, Spotify", + "set.acceptSelfSigned": "Aceptar un certificado autofirmado", + "set.accountDisconnected": "Cuenta desconectada.", + "set.adminPassword": "Contraseña de administrador", + "set.alertDelivery": "Entrega de alertas", + "set.alertDeliveryDesc": "Con cuánta insistencia se persigue un webhook que no responde antes de dar la alerta por perdida.", + "set.apiTokens": "Tokens de API", + "set.apiTokensDesc": "Credenciales de larga duración para scripts, Prometheus y cualquier otra cosa que no pueda mantener una sesión. Envíe uno como Authorization: Bearer pmk_…. Un token puede hacer todo lo que hace el administrador, salvo gestionar tokens: eso queda detrás de la contraseña, por lo que revocar uno es definitivo.", + "set.app": "App", + "set.attemptsBackoff": "La espera entre intentos se alarga automáticamente hasta un techo de 30 segundos, así que el extremo superior de este rango ya son varios minutos persiguiendo un extremo muerto. El límite es justamente el punto: reintentar sin fin convierte un único webhook muerto en una cola que nunca se vacía.", + "set.attemptsNote": "Incluido el primer intento. 1 significa no reintentar nunca.", + "set.attemptsRetryable": "Solo se reintentan los fallos que merece la pena reintentar. Un 404 de un webhook que alguien borró es permanente, y probarlo cuatro veces más solo retrasa todas las alertas que esperan detrás; un 502 de un proxy inverso es exactamente para lo que existen estos intentos.", + "set.audioMeters": "Medidores de audio", + "set.brokerPasswordCleared": "Contraseña del bróker borrada.", + "set.brokerPlainNote": "mqtt:// no está cifrado. MQTT envía el usuario y la contraseña en su paquete CONNECT, así que ambos cruzan la red en claro: bien en una LAN de confianza, que es el caso habitual de Home Assistant, y mal en cualquier cosa que usted no controle. Use mqtts:// si el bróker lo ofrece.", + "set.brokerUrl": "URL del bróker", + "set.brokerUrlNote": "mqtt://, mqtts://, ws:// o wss://. Una contraseña en la URL se rechaza: una URL acaba en líneas de registro y en la salida de ps, y eso no tiene vuelta atrás. Póngala en el campo de abajo, donde se cifra en reposo.", + "set.brokerUrlPlaceholder": "mqtts://broker.local:8883", + "set.chatBothApply": "Ambos se aplican y gana el más generoso: un mensaje desaparece cuando es más antiguo que las horas y queda fuera de los N más recientes. Así, un canal concurrido conserva menos tiempo que uno tranquilo con los mismos ajustes.", + "set.chatBufferNote": "«Enviar al conectar» es un número de otra naturaleza que los otros dos. Aquellos acotan lo que se guarda en disco; este es un búfer en memoria, reservado por cada panel conectado.", + "set.chatHistory": "Historial de chat", + "set.chatHistoryDesc": "Hasta dónde llega el historial del chat y, con él, cuánto puede ver un moderador al abrir la ficha de alguien.", + "set.chatHours": "Conservar durante (horas)", + "set.chatKeep": "Conservar siempre (mensajes)", + "set.chatKeepNote": "Un mínimo, sea cual sea su antigüedad.", + "set.chatNotOnlyDisk": "Esto no es solo un ajuste de disco. Al hacer clic en un nombre del chat se abre lo que esa persona ha dicho antes, y ese historial sale de aquí: ninguna plataforma publica una API para ello, así que polyemesis solo puede mostrar lo que conservó. Si lo deja demasiado corto, alguien que lleva una semana dando problemas parecerá no haber dicho nunca nada.", + "set.chatSendNote": "Historial sin consulta.", + "set.chatSendOnConnect": "Enviar al conectar (mensajes)", + "set.chatSendOnConnectShort": "Enviar al conectar", + "set.chatSmall": "El chat ocupa poco. Un canal con una media de diez mensajes por segundo almacena unos 7 MB por hora; la mayoría no se acercan a eso.", + "set.clientId": "ID de cliente", + "set.clientIdNote": "Debe ser único en el bróker. Una colisión es la causa número uno de un bucle de reconexión inexplicable: el bróker desconecta la sesión más antigua en cada conexión, y ambos clientes se reconectan indefinidamente.", + "set.clientIdPlaceholder": "derivado del nombre de la instancia", + "set.clientSecret": "Secreto de cliente", + "set.comingBack": "Regreso", + "set.confirmPassword": "Confirmar la nueva contraseña", + "set.connectedAccounts": "Cuentas conectadas", + "set.copied": "Copiado", + "set.copy": "Copiar", + "set.couldNotChangeThePassword": "No se pudo cambiar la contraseña.", + "set.couldNotClearThePassword": "No se pudo borrar la contraseña.", + "set.couldNotCreateTheToken": "No se pudo crear el token.", + "set.couldNotReCheckThe": "No se pudieron volver a comprobar las credenciales.", + "set.couldNotReadTheTls": "No se pudo leer el estado de TLS.", + "set.couldNotRevokeTheToken": "No se pudo revocar el token.", + "set.couldNotSaveTheCredentials": "No se pudieron guardar las credenciales.", + "set.couldNotSaveTheMqtt": "No se pudieron guardar los ajustes de MQTT.", + "set.credsRemoved": "Credenciales eliminadas.", + "set.currentPassword": "Contraseña actual", + "set.dataDirectory": "Directorio de datos", + "set.deliveryAttempts": "Intentos de entrega", + "set.dialsOnly": "El servidor solo marca: {schemes}.", + "set.disconnect": "Desconectar", + "set.disconnectDesc": "Los destinos que la usan conservan sus ajustes pero dejan de renovar su clave de transmisión. Puede volver a conectar la misma cuenta después.", + "set.disconnectTitle": "¿Desconectar esta cuenta?", + "set.drawtextMissing": "Esta compilación no tiene el filtro drawtext, así que las superposiciones de texto no están disponibles. Necesita un FFmpeg compilado con libfreetype.", + "set.drawtextOk": "Esta compilación puede dibujar texto sobre el vídeo.", + "set.enabled": "Activado", + "set.encoderUrl": "URL del codificador", + "set.expires": "Caduca", + "set.failover": "Conmutación por error", + "set.failoverDesc": "Una entrada de reserva y una tarjeta de espera para cuando la fuente en directo deja de entregar. Desactivada por defecto: con ella apagada, la cadena es byte a byte la que era antes de que esto existiera.", + "set.failoverRemuxNote": "Activarla añade un paso de remultiplexado entre la entrada y todo lo que hay debajo. Barato, pero no gratis, y por eso es una decisión y no un valor por defecto.", + "set.fileSizeMb": "Tamaño de archivo (MB)", + "set.filesKept": "Archivos conservados", + "set.goingLive": "Salir en directo", + "set.goingLiveDesc": "Qué ocurre cuando varios destinos arrancan a la vez.", + "set.graceNote": "Cuánto tiempo puede la fuente en directo no entregar nada antes de que cambie el nivel. Más largo que una reconexión a propósito: un codificador restableciendo una conexión RTMP es funcionamiento normal, no un fallo.", + "set.gracePeriod": "Periodo de gracia (segundos)", + "set.haDiscovery": "Detección de Home Assistant", + "set.height": "Altura", + "set.ingest": "Entrada", + "set.ingestDesc": "SRT transporta hasta seis pistas de audio. RTMP transporta una y existe como alternativa para codificadores que no admiten SRT. La extracción marca una fuente en lugar de esperar a que le publiquen.", + "set.ingestUrlCopied": "URL de entrada copiada.", + "set.instance": "Instancia", + "set.intervalNote": "El estado sin cambios no se vuelve a publicar, así que un intervalo corto cuesta una comparación en lugar de un mensaje. El keep-alive es lo que acota cuánto tiempo pasa desapercibido un enlace muerto y, por tanto, con qué rapidez el bróker avisa a los suscriptores de que esta instancia ya no está.", + "set.issuer": "Emisor", + "set.kbps": "kbps", + "set.keepAlive": "Keep-alive (s)", + "set.latencyMs": "Latencia (ms)", + "set.listeners": "Escuchas", + "set.listenersDesc": "Dónde se enlaza el servidor, para toda la instalación. Un único puerto SRT sirve a todas las fuentes: se distinguen por su token de publicación, así que añadir una fuente nunca implica publicar otro puerto. RTMP no tiene ese enrutado, de modo que su puerto sirve a una sola fuente.", + "set.logsBoundNote": "El directorio de registros está acotado por el producto de estos dos valores, así que la persistencia nunca puede ser lo que llene el disco.", + "set.logsDesc": "La salida de error de FFmpeg siempre se conserva en memoria, pero esa memoria muere con el servidor, y con ella justo las líneas que explican por qué murió. Persistir la refleja en un archivo rotatorio dentro del directorio de datos.", + "set.metersDesc": "Un proceso auxiliar mide cada canal de cada pista. Los intervalos más cortos se actualizan antes y cuestan más CPU.", + "set.mode": "Modo", + "set.modePull": "Extracción — marcar una cámara, señal o archivo", + "set.modeRtmp": "RTMP — una sola pista", + "set.modeSrt": "SRT — multipista (recomendado)", + "set.mqtt": "Telemetría MQTT", + "set.mqtt5Only": "Solo MQTT 5.0.", + "set.mqtt5OnlyNote": "Un bróker fijado a 3.1.1 no llegará a completar una conexión: no es un modo degradado. Mosquitto 2.x, EMQX y el complemento de Home Assistant hablan todos 5.0.", + "set.mqttDesc": "Publica el estado en un bróker como mensajes retenidos, de modo que un consumidor que no estuviera conectado cuando algo cambió obtenga igualmente la respuesta actual.", + "set.multiAccountNote": "Conecte la misma plataforma más de una vez para transmitir a varios canales: cada cuenta se convierte en su propio destino.", + "set.name": "Nombre", + "set.newPassword": "Nueva contraseña", + "set.no": "no", + "set.noEncryption": "Nada está cifrando esta conexión. Las contraseñas y las cookies de sesión cruzan la red en texto claro: ponga tls.mode: auto, o enlace a 127.0.0.1 detrás de un proxy.", + "set.noLibsrt": "Esta compilación de FFmpeg no admite SRT. Instale una configurada con --enable-libsrt, o use RTMP.", + "set.noTokens": "Todavía no hay tokens.", + "set.passphrase": "Frase de contraseña", + "set.passphraseNote": "Habilita el cifrado AES. Déjelo en blanco para ninguno. SRT rechaza frases fuera del rango de 10 a 79 caracteres.", + "set.passphrasePlaceholder": "opcional — de 10 a 79 caracteres", + "set.password": "Contraseña", + "set.passwordChanged": "Contraseña cambiada.", + "set.passwordMismatch": "Las nuevas contraseñas no coinciden.", + "set.passwordNote": "Ninguna API devuelve la contraseña, así que esta casilla aparece vacía incluso cuando hay una establecida. Déjela vacía para conservar la contraseña guardada; bórrela con el botón de abajo.", + "set.platform": "Plataforma", + "set.platformsDesc": "Las plataformas difieren en cuánto de esto puede automatizar polyemesis, y esa diferencia conviene conocerla antes de empezar y no una hora después. La transmisión en sí funciona igual en todas: el enrutado de audio por destino, las versiones, la reconexión y los medidores no dependen de ninguna de las columnas de abajo.", + "set.playLoop": "Reproducir un bucle cuando no hay nada en directo", + "set.playLoopNote": "Un segundo candidato junto a la tarjeta de espera: se reproducen archivos en lugar de una imagen fija. Ambos pueden estar activos; consulte el orden de niveles en el diseño de conmutación por error para saber cuál gana.", + "set.pointObs": "Apunte OBS a esta dirección.", + "set.preview": "Previsualización", + "set.previewDesc": "El monitor HLS del panel. Es el único punto donde polyemesis recodifica vídeo, y funciona sobre su propia suscripción al relé, de modo que nunca puede afectar a un destino.", + "set.previewIdleNote": "El codificador arranca cuando el panel pide la lista de reproducción y se detiene este tiempo después de la última petición, así que un panel cerrado no cuesta CPU. Cambiar esto nunca reinicia una previsualización que alguien esté viendo.", + "set.processLogs": "Registros de procesos", + "set.proxyTerminatesTls": "Un proxy inverso delante de polyemesis termina TLS, así que este servidor sirve HTTP en claro a propósito.", + "set.publishInterval": "Intervalo de publicación (s)", + "set.reconnectCap": "Techo de espera de reconexión (s)", + "set.reconnectCapNote": "0 usa el valor por defecto. Solo fuentes HTTP y HLS.", + "set.redirectNote": "Debe coincidir exactamente, incluidos el esquema y el puerto. Si accede a polyemesis a través de un proxy inverso, use esa URL pública.", + "set.redirectUri": "URI de redirección que autorizar", + "set.removeCreds": "Eliminar credenciales", + "set.removeCredsDesc": "Todas las cuentas conectadas mediante esta aplicación dejarán de renovar su token y habrá que volver a conectarlas. El secreto de cliente no se puede recuperar desde aquí: tendría que crear una aplicación nueva en la plataforma.", + "set.removeCredsTitle": "¿Eliminar estas credenciales de desarrollador?", + "set.returnAfter": "Volver tras (segundos estable)", + "set.returnAutomatic": "Automático — volver cuando la principal esté estable", + "set.returnManual": "Manual — permanecer en la de reserva hasta que yo cambie", + "set.returnManualNote": "Manual es el valor por defecto porque un regreso automático puede oscilar: una fuente principal que se recupera a rachas movería el programa de un lado a otro en mitad de la emisión.", + "set.rtmpPort": "Puerto RTMP", + "set.rtspNote": "TCP salvo que la cámara no pueda. RTSP sobre UDP a través de NAT parece conectado y no entrega nada.", + "set.rtspTransport": "Transporte RTSP", + "set.saveAndDrop": "Guardar y cortar la transmisión en directo", + "set.saveIngest": "Guardar los ajustes de entrada", + "set.segmentSec": "Segmento (s)", + "set.settingsSavedAffectedProcessesHave": "Ajustes guardados. Los procesos afectados se han reiniciado.", + "set.showSlate": "Mostrar una tarjeta cuando no hay nada en directo", + "set.slateColour": "Color de la tarjeta", + "set.slateColourNote": "Una sola palabra, porque acaba en un grafo de filtros donde una coma terminaría el argumento. Solo se usa cuando no hay imagen establecida.", + "set.slateColourPlaceholder": "black", + "set.slateImage": "Imagen de la tarjeta", + "set.slateImageNote": "Una ruta dentro del directorio de datos. Déjela vacía para pintar un color plano en su lugar: esa es precisamente la alternativa, porque un color no tiene ningún archivo que pueda fallar al abrirse.", + "set.slateImagePlaceholder": "slate/holding.png", + "set.sourceUrl": "URL de origen", + "set.sourceUrlNote": "Uno de {schemes}. Una fuente file:// es una ruta RELATIVA al directorio de datos y no puede contener «..»: queda confinada allí igual que un archivo subido.", + "set.sourceUrlPlaceholder": "rtsp://camera.local/stream1", + "set.srtPort": "Puerto SRT", + "set.stagger": "Escalonar conexiones (ms)", + "set.staggerNote": "0 lo desactiva. De lo contrario, salir en directo significa que todos los destinos abren una conexión, negocian TLS y empiezan a codificar audio en el mismo instante: en una máquina pequeña ese es el momento con más probabilidad de perder fotogramas, y es justo el momento que usted está mirando, porque acaba de salir en directo. Con unos cientos de milisegundos basta.", + "set.staggerReconnectNote": "Nunca retrasa una reconexión. Un destino que se cae a las 3 de la madrugada vuelve de inmediato en lugar de esperar su turno detrás de procesos que ya están sanos.", + "set.stepByStep": "Configuración paso a paso", + "set.stopAfterIdle": "Detener tras inactividad (s)", + "set.streamKey": "Clave de transmisión", + "set.streamKeyNote": "Los publicadores deben usar exactamente esta ruta, de modo que un puerto abierto no sea una puerta abierta.", + "set.subject": "Sujeto", + "set.synthesiseNote": "Todas las plataformas importantes rechazan una transmisión sin pista de audio. Con esto activado, una entrada solo de vídeo gana una pista estéreo silenciosa y sus destinos arrancan; con ello desactivado, se niegan con «routing profile selects no audio». Solo se aplica a una entrada que se DETECTÓ con cero pistas, así que nunca puede desplazar audio que usted esté enviando de verdad.", + "set.synthesiseSilent": "Sintetizar una pista silenciosa", + "set.syntheticAudio": "Audio sintético", + "set.syntheticDesc": "Qué ocurre cuando la entrada trae vídeo y ningún audio en absoluto.", + "set.tabIngest": "Entrada", + "set.tabPipeline": "Cadena de proceso", + "set.tabPlatforms": "Credenciales de plataforma", + "set.tabSecurity": "Seguridad", + "set.title": "Ajustes", + "set.tlsAcme": "Let's Encrypt", + "set.tlsAuto": "Automático", + "set.tlsDesc": "Se configura en config.yaml, no aquí: TLS debe estar bien antes de que el servidor empiece a escuchar. Esta ficha informa de lo que se obtuvo.", + "set.tlsManual": "Certificado manual", + "set.tlsOff": "No se termina aquí", + "set.tlsSelfsigned": "Autofirmado", + "set.tokenHashNote": "polyemesis solo guarda un hash. Si lo pierde, revoque este token y cree otro.", + "set.tokenNamePlaceholder": "prometheus", + "set.tokenRevoked": "{name} revocado.", + "set.topicPrefix": "Prefijo de tema", + "set.topicPrefixNote": "Los separadores del prefijo se conservan, así que home/av significa dos niveles. Un prefijo que empiece por $ se rechaza: los brókeres los reservan, y un suscriptor que use # nunca los recibe; la telemetría se publicaría correctamente y sería invisible justo en la vista con la que usted depuraría.", + "set.transportSecurity": "Seguridad del transporte", + "set.updateInterval": "Intervalo de actualización (ms)", + "set.username": "Usuario", + "set.validFor": "Válido para", + "set.whatPlatformsDo": "Qué puede hacer cada plataforma", + "set.whyDevApp": "Por qué necesita su propia aplicación de desarrollador", + "set.whyDevAppDesc": "polyemesis no puede distribuir secretos de cliente de OAuth: cualquiera con el binario los tendría y las plataformas los revocarían. Usted registra una vez una aplicación de desarrollador gratuita, pega aquí el ID y el secreto de cliente, y polyemesis obtiene entonces sus claves de transmisión por usted.", + "set.writeLogs": "Escribir registros en disco", + "set.yes": "sí", + "sources.addDescription": "Un segundo programa con su propia entrada, destinos y versiones — por ejemplo, un lienzo vertical junto a uno horizontal. Empieza en los puertos predeterminados, que conviene cambiar para que ambos no entren en conflicto.", + "sources.addFailed": "No se pudo añadir la fuente", + "sources.addSource": "Añadir fuente", + "sources.addTitle": "Añadir una fuente", + "sources.added": "«{name}» añadida. Configure sus puertos de entrada abajo.", + "sources.app": "App", + "sources.applyAndDrop": "Aplicar y cortar la transmisión", + "sources.copied": "{what} copiado", + "sources.copyFailed": "No se pudo copiar {what}", + "sources.copyTokenAria": "Copiar el token", + "sources.copyUrlAria": "Copiar la URL de {proto}", + "sources.default": "predeterminada", + "sources.defaultDescription": "Las solicitudes que no nombran una fuente actúan sobre esta.", + "sources.deleteAria": "Eliminar {name}", + "sources.deleteConfirm": "Eliminar fuente", + "sources.deleteDescription": "Sus destinos y versiones se van con ella: describen adónde va este programa y no significan nada sin él. Las grabaciones se conservan: los archivos siguen en el disco y se pueden reproducir, solo dejan de atribuirse a una fuente.", + "sources.deleteFailed": "No se pudo eliminar la fuente", + "sources.deleteThis": "Eliminar esta fuente", + "sources.deleteTitle": "¿Eliminar «{name}»?", + "sources.deleted": "«{name}» y sus destinos eliminados", + "sources.destinations": "Destinos", + "sources.dirtyIdle": "Cambios de entrada sin guardar. Aplicar reinicia la entrada de esta fuente.", + "sources.dirtyPublishing": "Un codificador está publicando en esta fuente. Aplicar reinicia su entrada y desconecta a todos los que están viendo.", + "sources.enableAria": "Activar {name}", + "sources.help.app": "La ruta de aplicación RTMP: la parte de la URL entre el host y la clave de transmisión. La mayoría de los codificadores esperan «live». Cámbiela solo si tiene un motivo.", + "sources.help.ingest": "Cómo llega su codificador a esta fuente. SRT sobrevive a una conexión con pérdidas retransmitiendo lo que falta, y es la opción predeterminada correcta por internet público. RTMP es más antiguo y goza de soporte universal, pero se degrada mucho en un enlace inestable. Con extracción, polyemesis obtiene la señal de una URL que usted indique en lugar de esperar a que le publiquen.", + "sources.help.latencyMs": "Cuánto esperará SRT un paquete tardío o retransmitido antes de descartarlo. Los valores más altos aguantan peores conexiones a costa de retardo: aproximadamente 2,5 veces su tiempo de ida y vuelta es un buen punto de partida, así que 40 ms de RTT piden unos 100 ms aquí. Demasiado bajo y un pequeño tropiezo se convierte en un fallo visible.", + "sources.help.passphrase": "Cifra la conexión SRT y hoy es lo que realmente mantiene a los desconocidos fuera de esta entrada. Entre 10 y 79 caracteres. Si lo deja vacío, cualquiera que alcance el puerto puede publicar en él.", + "sources.help.streamKey": "El secreto que envía un codificador para publicar por RTMP. Es la credencial que protege esta entrada, así que trátela como una contraseña: quien la tenga puede emitir a sus destinos.", + "sources.help.token": "Identifica esta fuente cuando varias comparten un puerto de entrada. Que además controle el acceso depende de su modo de entrada; la nota de abajo dice qué es cierto ahora mismo. Al rotar se emite un token nuevo y el antiguo sigue siendo válido cinco minutos para que un codificador en directo no se corte a mitad de la emisión.", + "sources.ingest": "Entrada", + "sources.lastSourceTitle": "La última fuente no se puede eliminar: una instalación necesita una entrada", + "sources.latencyMs": "Latencia (ms)", + "sources.loadFailed": "No se pudieron cargar las fuentes", + "sources.loading": "Cargando fuentes…", + "sources.loss": "pérdida", + "sources.modePull": "Extracción", + "sources.name": "Nombre", + "sources.namePlaceholder": "Vertical", + "sources.noEngine": "No hay ningún motor en ejecución para esta fuente. La causa habitual es un puerto de entrada ya ocupado por otra fuente u otro proceso.", + "sources.notRunning": "detenida", + "sources.ownDescription": "Sus propios destinos, versiones y grabaciones.", + "sources.passphrase": "Frase de contraseña", + "sources.passphrasePlaceholder": "10–79 caracteres", + "sources.publishing": "publicando", + "sources.renditions": "Versiones", + "sources.retrans": "retransm.", + "sources.rotate": "Rotar", + "sources.rotateConfirm": "Rotar token", + "sources.rotateFailed": "No se pudo rotar el token", + "sources.rotateIdle": "Se emite un token nuevo de inmediato. El antiguo sigue funcionando durante cinco minutos para que un codificador que ya lo usa no se corte.", + "sources.rotateTitle": "¿Rotar el token de «{name}»?", + "sources.rotateWhilePublishing": "Un codificador está publicando en esta fuente ahora mismo. El token antiguo sigue funcionando durante cinco minutos, así que no se cortará, pero debe actualizarse antes de que se cierre esa ventana.", + "sources.rtt": "RTT", + "sources.running": "en ejecución", + "sources.saveFailed": "No se pudo guardar", + "sources.streamKey": "Clave de transmisión", + "sources.subtitle": "Un programa entrante cada una. Los destinos y las versiones pertenecen a una fuente.", + "sources.title": "Fuentes", + "sources.token": "token", + "sources.tokenEnforcedDetailRtmp": "Es la clave de emisión: publique en la URL de arriba usándolo como clave. Al rotarlo se emite uno nuevo y el anterior sigue funcionando cinco minutos, así puede cambiar sin cortar la emisión.", + "sources.tokenEnforcedDetailSrt": "Póngalo en el streamid SRT de su codificador para publicar en esta fuente por el puerto compartido. Al rotarlo se emite uno nuevo y el anterior sigue funcionando cinco minutos, así puede cambiar sin cortar la emisión.", + "sources.tokenIsCredential": "Este token es la credencial.", + "sources.tokenIssued": "Nuevo token emitido", + "sources.tokenNotEnforced": "Este token se almacena pero ahora mismo no se aplica.", + "sources.tokenNotEnforcedRtmp": "Las fuentes se mantienen separadas por puerto. Lo que realmente protege esta entrada es la clave de transmisión de arriba. Active la entrada de un solo puerto en Ajustes para que el token sea la credencial.", + "sources.tokenNotEnforcedSrt": "Las fuentes se mantienen separadas por puerto. Lo que realmente protege esta entrada es la frase de contraseña SRT de arriba. Active la entrada de un solo puerto en Ajustes para que el token sea la credencial.", + "state.disabled": "Desactivado", + "state.failed": "Fallido", + "state.live": "En vivo", + "state.offline": "Sin conexión", + "state.reconnecting": "Reconectando", + "state.starting": "Iniciando", + "state.stopped": "Detenido" } diff --git a/ui/src/lib/i18n/fr.json b/ui/src/lib/i18n/fr.json index 05a00c57..0e056eca 100644 --- a/ui/src/lib/i18n/fr.json +++ b/ui/src/lib/i18n/fr.json @@ -1,26 +1,227 @@ { - "nav.dashboard": "Tableau de bord", - "nav.sources": "Sources", - "nav.meters": "Indicateurs audio", - "nav.routing": "Routing", - "nav.renditions": "Versions", - "nav.playout": "Playout", - "nav.library": "Bibliothèque", - "nav.clips": "Extraits", - "nav.chat": "Chat", - "nav.jobs": "Tâches", - "nav.automation": "Automatisation", - "nav.recordings": "Enregistrements", - "nav.monitoring": "Surveillance", - "nav.settings": "Paramètres", - "chrome.toggleNav": "Afficher/masquer la navigation", + "auth.accountCreated": "Compte administrateur créé.", + "auth.confirmPassword": "Confirmer le mot de passe", + "auth.createAccount": "Créer le compte", + "auth.firstRun": "Configuration initiale", + "auth.firstRunDesc": "Choisissez les identifiants administrateur de ce serveur. Il n'existe aucune récupération de compte, conservez-les donc en lieu sûr.", + "auth.password": "Mot de passe", + "auth.passwordMismatch": "Les mots de passe ne correspondent pas.", + "auth.signIn": "Se connecter", + "auth.signInDesc": "Saisissez vos identifiants administrateur.", + "auth.somethingWentWrong": "Une erreur est survenue.", + "auth.tokenCreated": "Jeton d'API créé", + "auth.tokenRevoked": "Jeton d'API révoqué", + "auth.username": "Nom d'utilisateur", + "auto.action": "Action", + "auto.adminPasswordChanged": "Mot de passe administrateur modifié", + "auto.chosenWeekdays": "Jours de la semaine choisis", + "auto.coalesced": "Regroupées", + "auto.coalescingWindow": "Fenêtre de regroupement (secondes)", + "auto.couldNotChangeTheRule": "Impossible de modifier la règle.", + "auto.couldNotChangeTheSchedule": "Impossible de modifier la programmation.", + "auto.couldNotDeleteTheRule": "Impossible de supprimer la règle.", + "auto.couldNotDeleteTheSchedule": "Impossible de supprimer la programmation.", + "auto.couldNotLoadAutomationSettings": "Impossible de charger les paramètres d'automatisation.", + "auto.couldNotSaveTheRule": "Impossible d'enregistrer la règle.", + "auto.couldNotSaveTheSchedule": "Impossible d'enregistrer la programmation.", + "auto.createRule": "Créer la règle", + "auto.createSchedule": "Créer la programmation", + "auto.days": "Jours", + "auto.delete": "Supprimer", + "auto.deleteRule": "Supprimer la règle", + "auto.deleteRuleDesc": "La règle cesse de se déclencher. Son point de terminaison webhook n'est pas touché, et vous pouvez recréer la règle.", + "auto.deleteRuleTitle": "Supprimer cette règle d'alerte ?", + "auto.deleteSchedule": "Supprimer la programmation", + "auto.deleteScheduleDesc": "La programmation cesse de s'exécuter. Ce qu'elle a déjà démarré n'est pas affecté.", + "auto.deleteScheduleTitle": "Supprimer cette programmation ?", + "auto.delivery": "Remise", + "auto.destinationFallingBehindRealtime": "Destination en retard sur le temps réel", + "auto.destinationKeepingUpAgain": "La destination suit de nouveau", + "auto.destinationRecovered": "Destination rétablie", + "auto.destinationWentDown": "Destination tombée", + "auto.destinations": "Destinations", + "auto.dropped": "Abandonnées", + "auto.edit": "Modifier", + "auto.editRule": "Modifier la règle d'alerte", + "auto.editSchedule": "Modifier la programmation", + "auto.endpoint": "Point de terminaison", + "auto.evAudioClipping": "Écrêtage audio", + "auto.evClipCaptured": "Extrait capturé", + "auto.evDiskLow": "Espace disque faible", + "auto.evDiskRecovered": "Espace disque rétabli", + "auto.evIngestLost": "Entrée perdue", + "auto.evIngestRecovered": "Entrée rétablie", + "auto.evSettingsChanged": "Paramètres modifiés", + "auto.evSignedIn": "Connexion effectuée", + "auto.events": "Événements", + "auto.everyDay": "Tous les jours", + "auto.failed": "Échouées", + "auto.failoverSwitchedSource": "La bascule a changé de source", + "auto.fired": "déclenchées", + "auto.firesAt": "Se déclenche à", + "auto.lastDelivery": "Dernière remise", + "auto.lastSweep": "Dernier balayage", + "auto.latenessAllowance": "Retard toléré (secondes)", + "auto.loudnessBackInCompliance": "Niveau sonore de nouveau conforme", + "auto.loudnessOutOfCompliance": "Niveau sonore non conforme", + "auto.minGap": "Écart minimal entre messages (secondes)", + "auto.minSeverity": "Gravité minimale", + "auto.missedNote": "Une occurrence manquée parce que le serveur était arrêté est marquée comme traitée et ignorée plutôt que déclenchée en retard — un flux qui démarre de lui-même avec quatre heures de retard est pire qu'un flux qui n'a jamais démarré.", + "auto.name": "Nom", + "auto.newRule": "Nouvelle règle d'alerte", + "auto.newSchedule": "Nouvelle programmation", + "auto.next": "Prochaine", + "auto.nothingFired": "Rien ne s'est encore déclenché. Le planificateur effectue un balayage toutes les 20 secondes.", + "auto.once": "Une fois", + "auto.payloadFormat": "Format du payload", + "auto.pending": "En attente", + "auto.repeatedFailedSignIns": "Échecs de connexion répétés", + "auto.repeats": "Répétition", + "auto.retries": "Réessais", + "auto.rule": "Règle", + "auto.ruleCreated": "Règle d'alerte créée. Envoyez-lui un test.", + "auto.ruleDeleted": "Règle d'alerte supprimée.", + "auto.rulePlaceholder": "Discord — alertes de l'émission", + "auto.ruleSaved": "Règle d'alerte enregistrée.", + "auto.saveRule": "Enregistrer la règle", + "auto.saveSchedule": "Enregistrer la programmation", + "auto.schedule": "Programmation", + "auto.scheduleCreated": "Programmation créée.", + "auto.scheduleDeleted": "Programmation supprimée.", + "auto.schedulePlaceholder": "Émission en semaine", + "auto.scheduleSaved": "Programmation enregistrée.", + "auto.schedules": "Programmations", + "auto.secretMasked": "Affichée masquée car le secret se trouve dans le chemin. Laissez-la telle quelle pour conserver le point de terminaison actuel, ou collez une nouvelle URL pour la remplacer.", + "auto.sendTest": "Envoyer une alerte de test", + "auto.sendTestTitle": "Envoyer une alerte de test maintenant", + "auto.sent": "Envoyées", + "auto.skipped": "ignorées", + "auto.startDestinations": "Démarrer les destinations", + "auto.startPlaylist": "Démarrer la playlist", + "auto.stopDestinations": "Arrêter les destinations", + "auto.stopPlaylist": "Arrêter la playlist", + "auto.subscribedTo": "Abonné à", + "auto.subtitle": "Des webhooks qui vous préviennent quand quelque chose casse, et des programmations qui font tourner l'émission sans vous.", + "auto.theEndpointDidNotAccept": "Le point de terminaison n'a pas accepté le message de test.", + "auto.time": "Heure", + "auto.timeZone": "Fuseau horaire", + "auto.title": "Automatisation", + "auto.tzPlaceholder": "UTC", + "auto.webhookHint": "Discord et Slack en fournissent un dans les paramètres du salon.", + "auto.webhookPlaceholder": "https://discord.com/api/webhooks/…", + "auto.webhookRules": "Règles de webhook", + "auto.webhookUrl": "URL du webhook", + "auto.weekFri": "Ven", + "auto.weekMon": "Lun", + "auto.weekSat": "Sam", + "auto.weekSun": "Dim", + "auto.weekThu": "Jeu", + "auto.weekTue": "Mar", + "auto.weekWed": "Mer", + "auto.when": "Quand", + "chatpage.allFiltered": "Toutes les plateformes ayant des messages sont filtrées.", + "chatpage.couldNotDeleteThatMessage": "Impossible de supprimer ce message.", + "chatpage.deleteNote": "Supprimer retire le message sur la plateforme qui l'a émis. Une plateforme sur laquelle polyemesis ne peut pas supprimer le dit et indique où le faire à la place — l'action n'est jamais masquée sur une supposition.", + "chatpage.droppedTitle": "Affichés en direct, mais la persistance a pris du retard et l'historique les a perdus.", + "chatpage.dupTitle": "Le même message vu deux fois — notre propre écho, ou la plateforme qui le rejoue.", + "chatpage.noAccount": "Le chat fonctionne, mais aucun compte de plateforme n'est rattaché. Rattachez-en un dans Paramètres → Identifiants de plateforme.", + "chatpage.notRunning": "Aucun chat ne fonctionne sur ce serveur. L'historique sous la chronologie est ce qui avait été stocké auparavant, et la zone d'envoi reste désactivée tant qu'aucune plateforme n'est rattachée.", + "chatpage.received": "Reçus", + "chatpage.reload": "Recharger", + "chatpage.stored": "Stockés", + "chatpage.storedTitle": "Rien n'est rattaché ; ceci est l'historique stocké.", + "chatpage.subtitle": "Toutes les plateformes dans une seule chronologie, avec une seule zone d'envoi.", + "chatpage.timeline": "Chronologie", + "chatpage.title": "Chat", "chrome.ingest": "Ingest", + "chrome.language": "Langue", "chrome.liveCount": "{count} en direct", + "chrome.signOut": "Déconnexion", "chrome.socketOffline": "Hors ligne", "chrome.socketOfflineHint": "Les mises à jour en direct sont déconnectées ; nouvelle tentative", - "chrome.signOut": "Déconnexion", - "chrome.language": "Langue", + "chrome.toggleNav": "Afficher/masquer la navigation", + "clipedit.back33": "Reculer de 33 ms", + "clipedit.clickLine": "Cliquez sur une ligne pour vous y rendre. Sélectionnez-en plusieurs et les points d'entrée et de sortie se placent sur les mots que vous avez surlignés.", + "clipedit.clipThisLine": "Extraire cette ligne", + "clipedit.clipThisLineTitle": "Placer les points d'entrée et de sortie autour de cette ligne", + "clipedit.copied": "Copié", + "clipedit.copiesEveryPacketSecondsBit": "Copie chaque paquet. Quelques secondes, exact au bit près, et le début se cale sur l'image clé la plus proche.", + "clipedit.couldNotOpenThatRecording": "Impossible d'ouvrir cet enregistrement.", + "clipedit.couldNotQueueTheExport": "Impossible de mettre l'export en file.", + "clipedit.cut": "Coupe", + "clipedit.downloadClip": "Télécharger l'extrait", + "clipedit.exportQueued": "Export mis en file. Il s'exécutera quand la machine pourra y consacrer des cycles.", + "clipedit.exports": "Exports", + "clipedit.fast": "Rapide", + "clipedit.filterTranscript": "Filtrer la transcription", + "clipedit.findSaid": "Retrouver ce qui a été dit", + "clipedit.forward33": "Avancer de 33 ms", + "clipedit.in": "Entrée", + "clipedit.keepAllTracks": "Conserver toutes les pistes audio", + "clipedit.keepsEveryAudioTrackIn": "Conserve toutes les pistes audio dans n'importe quel codec. Ce qu'écrit l'enregistreur.", + "clipedit.markIn": "I — marquer l'entrée à la tête de lecture", + "clipedit.markOut": "O — marquer la sortie à la tête de lecture", + "clipedit.name": "Nom", + "clipedit.noKeyframes": "Les positions des images clés n'ont pas pu être lues, personne ne peut donc dire où cette coupe commencera réellement. Cela fonctionnera quand même — la recherche de FFmpeg tombe elle-même sur une image clé — mais le début peut être en avance d'un GOP au maximum, et cette page ne peut pas vous dire de combien.", + "clipedit.noTranscript": "Rien n'a encore été transcrit pour cet enregistrement. Chaque microphone a été enregistré sur sa propre piste, la transcription vous donne donc les noms des intervenants sans modèle de diarisation — et vous pouvez ensuite extraire à partir de ce qui a été dit.", + "clipedit.nothingExported": "Rien n'a encore été exporté.", + "clipedit.out": "Sortie", + "clipedit.pickAtLeastOneAudio": "Choisissez au moins une piste audio, ou prenez-les toutes.", + "clipedit.playFaster": "L — lire, à nouveau pour accélérer", + "clipedit.precise": "Précis", + "clipedit.queueNote": "L'export est mis en file, pas exécuté ici : il cède la place au flux en direct, si bien qu'une coupe prise en pleine diffusion attend au lieu de voler les cycles dont le flux a besoin.", + "clipedit.reEncodesOnlyTheLeading": "Ré-encode uniquement le GOP partiel de tête pour que le début soit exact. Le reste est toujours copié.", + "clipedit.reencoded": "Réencodé", + "clipedit.shortcuts": "J / K / L pour le défilement, I et O marquent l'entrée et la sortie, les flèches décalent de 33 ms et maj+flèches d'une seconde. Le pas fin vaut une image à 30 fps ; la cadence réelle de cet enregistrement n'est pas indexée, il est donc exprimé en millisecondes plutôt que deviné.", + "clipedit.shuttleBack": "J — défiler en arrière", + "clipedit.startMoved": "Début décalé", + "clipedit.starts": "Commence", + "clipedit.stop": "K — arrêter", + "clipedit.thatCutCouldNotBe": "Impossible de planifier cette coupe.", + "clipedit.thatIsNotARecording": "Ceci n'est pas un id d'enregistrement.", + "clipedit.thatRecordingIsNotAvailable": "Cet enregistrement n'est pas disponible.", + "clipedit.title": "Éditeur d'extraits", + "clipedit.whatASocialPlatformWill": "Ce qu'une plateforme sociale accepte. Moins de codecs y survivent.", + "clipedit.zoomIn": "Zoom avant", + "clipedit.zoomOut": "Zoom arrière", + "clips.bitrate": "Débit", + "clips.buffer": "Tampon", + "clips.bufferLiveNote": "L'interrupteur du tampon est une bascule à chaud, pas un réglage enregistré : il est de nouveau désactivé après un redémarrage.", + "clips.captured": "Capturé", + "clips.ceiling": "Plafond", + "clips.clipsKept": "Extraits conservés", + "clips.couldNotCaptureAClip": "Impossible de capturer un extrait.", + "clips.couldNotChangeTheClip": "Impossible de modifier le tampon d'extraits.", + "clips.couldNotDeleteTheClip": "Impossible de supprimer l'extrait.", + "clips.couldNotLoadClips": "Impossible de charger les extraits.", + "clips.covers": "Couvre", + "clips.customLength": "Durée d'extrait personnalisée en secondes", + "clips.delete": "Supprimer", + "clips.deleteDescription": "Un extrait est généralement précisément ce que vous vouliez garder. Ceci supprime le fichier ; l'enregistrement dont il provient reste intact.", + "clips.deleteTitle": "Supprimer cet extrait ?", + "clips.deleted": "Extrait supprimé.", + "clips.download": "Télécharger", + "clips.everythingHeld": "Tout ce que le tampon retient", + "clips.file": "Fichier", + "clips.held": "Retenu", + "clips.inMemory": "En mémoire", + "clips.keepBuffer": "Conserver un tampon glissant", + "clips.keyframes": "Images clés", + "clips.length": "Durée", + "clips.none": "aucun", + "clips.off": "désactivé", + "clips.onDisk": "Sur le disque", + "clips.seen": "vu", + "clips.settings": "Paramètres", + "clips.size": "Taille", + "clips.subtitle": "Un tampon glissant de ce qui vient d'être diffusé. Capturez-le après coup — rien n'a besoin d'être armé à l'avance.", + "clips.theClipDoesNotStart": "L'extrait ne commence pas sur une image clé.", + "clips.title": "Extraits", + "clips.waitingForStream": "en attente du flux", + "clips.window": "Fenêtre", + "clips.windowSeconds": "Fenêtre (secondes)", "common.add": "Ajouter", + "common.apply": "Appliquer", "common.cancel": "Annuler", "common.close": "Fermer", "common.confirm": "Confirmer", @@ -28,110 +229,921 @@ "common.copy": "Copier", "common.delete": "Supprimer", "common.disabled": "Désactivé", + "common.discard": "Abandonner", "common.edit": "Modifier", "common.enabled": "Activé", "common.loading": "Chargement…", + "common.moreInfo": "Plus d'informations", "common.refresh": "Actualiser", "common.restart": "Redémarrer", "common.retry": "Réessayer", "common.save": "Enregistrer", "common.start": "Démarrer", "common.stop": "Arrêter", - "state.live": "En direct", - "state.reconnecting": "Tentative de reconnexion", - "state.starting": "Démarrage", - "state.failed": "Échec", - "state.stopped": "Arrêté", - "state.offline": "Hors ligne", - "hooks.title": "Webhooks de cycle de vie", - "hooks.new": "Nouveau webhook", - "hooks.empty": "Aucun webhook. Ajoutez-en un et polyemesis enverra en POST un corps JSON signé dès que le flux démarre ou s’arrête, ou qu’une destination monte ou tombe — une livraison par transition, dans l’ordre, pour qu’un script puisse agir.", - "hooks.colWebhook": "Webhook", - "hooks.colEndpoint": "Point de terminaison", - "hooks.colTriggers": "Abonné à", - "hooks.colEnabled": "Actif", - "hooks.signed": "signé", - "hooks.unsigned": "NON SIGNÉ", + "dash.audioTracks": "Pistes audio", + "dash.bitrate": "Débit", + "dash.broadcastSettings": "Paramètres de diffusion", + "dash.categoryPlaceholder": "Musique", + "dash.clipboardUnavailable": "Le presse-papiers n'est pas disponible sur cette origine.", + "dash.connectPrompt": "Connectez un compte YouTube, Twitch, Facebook ou Kick dans Paramètres → Identifiants de plateforme pour définir le titre, la description et la catégorie de votre flux sur toutes les plateformes à la fois. Chacune n'en accepte qu'un sous-ensemble différent — le composeur indique lequel, et envoie ce qu'il peut.", + "dash.copyIngestUrl": "Copier l'URL d'entrée", + "dash.couldNotLoadTheDestination": "Impossible de charger la destination.", + "dash.couldNotPushTheMetadata": "Impossible d'envoyer les métadonnées.", + "dash.couldNotReorderTheDestinations": "Impossible de réordonner les destinations.", + "dash.deleteDest": "Supprimer la destination", + "dash.deleteDestDescription": "Ceci supprime la destination et son profil de routage. La plateforme n'est pas touchée, et vous pouvez la rajouter — mais le routage audio que vous aviez mis en place pour elle est perdu.", + "dash.descriptionPlaceholder": "Ce qu'est ce flux.", + "dash.destDeleted": "Destination supprimée.", + "dash.disabled": "désactivée", + "dash.everyConnectedAccount": "tous les comptes connectés", + "dash.idle": "inactif", + "dash.ingestUrlCopied": "URL d'entrée copiée.", + "dash.keyRefreshed": "Clé de flux actualisée depuis la plateforme.", + "dash.leaveUnchanged": "Laisser inchangé", + "dash.leaveUnchangedNote": "« Laisser inchangé » n'envoie strictement rien, la plateforme conserve donc ce qu'elle a. Choisir Activé ou Désactivé écrit cette valeur.", + "dash.metaCategory": "Catégorie", + "dash.metaDescription": "Description", + "dash.metaTags": "Étiquettes", + "dash.metaTitle": "Titre", + "dash.noDestinations": "Aucune destination pour l'instant. Ajoutez-en une, puis choisissez les pistes audio qu'elle reçoit.", + "dash.off": "Désactivé", + "dash.pipeline": "Chaîne de traitement", + "dash.pushToPlatforms": "Envoyer aux plateformes", + "dash.pushing": "Envoi…", + "dash.reconnects": "Reconnexions", + "dash.relaySubscribers": "Abonnés au relais", + "dash.renditions": "Versions", + "dash.scheduledStart": "Début programmé", + "dash.subtitle": "Une seule entrée, diffusée en éventail avec une vidéo partagée et un audio propre à chaque destination.", + "dash.tagsPlaceholder": "live, house, dj set", + "dash.theConnectedAccount": "le compte connecté", + "dash.title": "Tableau de bord", + "dash.titlePlaceholder": "Set du vendredi soir", + "dash.uptime": "Durée de fonctionnement", + "dash.youtubeStopsAcceptingTheseOnce": "YouTube cesse de les accepter dès qu'une diffusion quitte « created » ou « ready ».", + "dest.audience": "Audience", + "dest.audioBitrate": "Débit audio", + "dest.audioCodec": "Codec audio", + "dest.channels": "Canaux", + "dest.compliance": "Conformité", + "dest.connectedAccount": "Compte connecté", + "dest.contentLabels": "Étiquettes de contenu", + "dest.copySource": "Copier la vidéo source", + "dest.createVariant": "Créer cette variante", + "dest.customiseSummary": "Personnaliser pour cette destination", + "dest.fbBackupCost": "Double la bande passante d’envoi de cette destination.", + "dest.fbBackupHelp": "Envoie une seconde copie de ce flux vers l’entrée de secours de Facebook, afin qu’une connexion perdue n’interrompe pas la diffusion.", + "dest.fbBackupLabel": "Publier un flux de secours", + "dest.fbBackupReconnect": "L’activer reconnecte le flux une fois — activez-le donc avant de passer en direct.", + "dest.fbBoxIntro": "Envoyé lors du même appel de création que l’Audience, donc la même règle s’applique : appliqué une fois au démarrage de la diffusion, et laissé totalement intact si vide.", + "dest.fbBoxTitle": "Publication croisée et bouton de don Facebook", + "dest.fbCrosspostAdd": "Ajouter une Page", + "dest.fbCrosspostAlsoPost": "Publier aussi", + "dest.fbCrosspostHelp": "L’ID numérique de la Page depuis la console de Facebook (paramètres de la Page, ou graph.facebook.com/me/accounts en étant connecté en tant que Page) — il n’y a pas de recherche ici, collez donc l’id plutôt qu’un nom ou une URL. « Publier aussi » publie en tant que cette Page au lieu de seulement lui partager la diffusion ; désactivé, seul le partage discret a lieu.", + "dest.fbCrosspostLabel": "Publication croisée sur des Pages", + "dest.fbCrosspostPageId": "ID de la Page", + "dest.fbCrosspostRemove": "Retirer cette Page", + "dest.fbDonateHelp": "Également un id opaque issu de la console des collectes de Facebook, pas un nom d’association à rechercher. Laissez vide pour n’attacher aucun bouton de don.", + "dest.fbDonateLabel": "ID de l’association du bouton de don", + "dest.fbDonatePlaceholder": "ID de l’association", + "dest.fillFromPlatform": "Remplir avec ce que publie cette plateforme", + "dest.friends": "Amis", + "dest.friendsOfFriends": "Amis d’amis", + "dest.giveUpAfter": "Abandonner après (tentatives)", + "dest.leaveOnFacebook": "Laisser tel quel sur Facebook", + "dest.leaveOnYouTube": "Laisser tel quel sur YouTube", + "dest.madeForKids": "Conçu pour les enfants (COPPA)", + "dest.muxQueue": "File de multiplexage (paquets)", + "dest.muxQueuePair": "Ces deux réglages vont de pair : FFmpeg n’applique le plafond de paquets qu’une fois la file au-delà du seuil en octets, donc un seuil seul ne fait rien et sera refusé. Augmentez-les si une destination signale des erreurs d’entrelacement — le chemin audio a ici une latence variable, car la normalisation sonore lit en avance.", + "dest.name": "Nom", + "dest.namePlaceholder": "YouTube — chaîne principale", + "dest.notLinked": "Non lié", + "dest.onlyMe": "Moi uniquement", + "dest.platform": "Plateforme", + "dest.private": "Privé", + "dest.public": "Public", + "dest.reconnecting": "Reconnexion", + "dest.retryAfter": "Réessayer après (secondes)", + "dest.sharedEncodeAria": "Encodage vidéo partagé", + "dest.socketTimeout": "Délai du socket (secondes)", + "dest.streamKeyLabel": "Clé de flux", + "dest.transport": "Transport", + "dest.transportFile": "Fichier local", + "dest.transportRtmp": "RTMP / RTMPS", + "dest.unlisted": "Non répertorié", + "dest.urlFromDashboard": "URL depuis le tableau de bord", + "dest.useSharedEncode": "Utiliser un encodage vidéo partagé", + "dest.varBitrate": "Débit vidéo (kbps)", + "dest.varFps": "Fréquence d’images", + "dest.varHeight": "Hauteur", + "dest.varSourceHint": "source", + "dest.varWidth": "Largeur", + "dest.variantWarning": "Lance un second encodage à partir de votre source, initialisé depuis « {name} ». Ce n’est pas une variation gratuite et cela ne change rien à ce que reçoivent les autres destinations de cet encodage. Laissez un champ vide pour conserver la valeur de la source.", + "dest.videoTreatment": "Traitement vidéo", + "dest.visibility": "Visibilité", + "hooks.allTriggers": "tout", "hooks.attempts": "{count} tentatives", + "hooks.attemptsLabel": "Tentatives", "hooks.attemptsOne": "{count} tentative", - "hooks.timeout": "délai de {seconds}s", - "hooks.allTriggers": "tout", - "hooks.triggerCount": "{count} déclencheurs", - "hooks.triggerCountOne": "{count} déclencheur", - "hooks.enableAria": "Activer {name}", + "hooks.colEnabled": "Actif", + "hooks.colEndpoint": "Point de terminaison", + "hooks.colTriggers": "Abonné à", + "hooks.colWebhook": "Webhook", + "hooks.create": "Créer", + "hooks.created": "Webhook créé.", + "hooks.deleteBody": "Il cesse immédiatement de livrer. Le point de terminaison n’est pas touché et vous pouvez recréer le hook — mais sa clé de signature est irrécupérable.", + "hooks.deleteConfirm": "Supprimer le webhook", + "hooks.deleteFailed": "Impossible de supprimer le hook.", + "hooks.deleteTitle": "Supprimer ce webhook ?", + "hooks.deleted": "Webhook supprimé.", "hooks.deliveries": "Livraisons récentes", - "hooks.sendTest": "Envoyer une livraison de test", - "hooks.sendTestTitle": "Envoyer une livraison de test maintenant", - "hooks.secretTitle": "Clé de signature — copiez-la maintenant, elle ne peut plus être affichée", - "hooks.secretCopied": "Je l’ai copiée", - "hooks.httpStatus": "HTTP {status}", - "hooks.noResponse": "aucune réponse", + "hooks.deliveriesFailed": "Impossible de lire les livraisons récentes.", + "hooks.deliveryTitle": "Livraison", + "hooks.dialogIntro": "Un POST signé par transition, dans l’ordre. Contrairement à une alerte, rien n’est regroupé ni anti-rebondi — un script reçoit chaque front.", "hooks.durationMs": "{ms} ms", + "hooks.editTitle": "Modifier le webhook", + "hooks.empty": "Aucun webhook. Ajoutez-en un et polyemesis enverra en POST un corps JSON signé dès que le flux démarre ou s’arrête, ou qu’une destination monte ou tombe — une livraison par transition, dans l’ordre, pour qu’un script puisse agir.", + "hooks.enableAria": "Activer {name}", + "hooks.httpStatus": "HTTP {status}", + "hooks.loadFailed": "Impossible de charger les webhooks.", + "hooks.name": "Nom", + "hooks.new": "Nouveau webhook", + "hooks.newTitle": "Nouveau webhook", "hooks.noDeliveries": "Rien de livré pour l’instant. Les livraisons apparaissent ici au fil des transitions.", - "hooks.deliveryTitle": "Livraison", - "hooks.statSent": "Envoyées", - "hooks.statFailed": "Échouées", + "hooks.noResponse": "aucune réponse", + "hooks.payloadNote": "Payload {version}. Signature dans {header}, sur l’horodatage et le corps.", + "hooks.retriesHint": "Les réessais bloquent la file propre à ce point de terminaison, car l’ordre est la promesse. Un 4xx n’est jamais réessayé — un point de terminaison qui dit que la requête est mauvaise le redira.", + "hooks.saveFailed": "Impossible d’enregistrer le hook.", + "hooks.secretCopied": "Je l’ai copiée", + "hooks.secretTitle": "Clé de signature — copiez-la maintenant, elle ne peut plus être affichée", + "hooks.sendTest": "Envoyer une livraison de test", + "hooks.sendTestTitle": "Envoyer une livraison de test maintenant", + "hooks.signed": "signé", + "hooks.statDropped": "Abandonnées", "hooks.statEndpoints": "Points de terminaison", + "hooks.statFailed": "Échouées", "hooks.statQueued": "En file", - "hooks.statDropped": "Abandonnées", "hooks.statRetries": "Réessais", - "hooks.payloadNote": "Payload {version}. Signature dans {header}, sur l’horodatage et le corps.", - "hooks.deleteTitle": "Supprimer ce webhook ?", - "hooks.deleteBody": "Il cesse immédiatement de livrer. Le point de terminaison n’est pas touché et vous pouvez recréer le hook — mais sa clé de signature est irrécupérable.", - "hooks.deleteConfirm": "Supprimer le webhook", - "hooks.editTitle": "Modifier le webhook", - "hooks.newTitle": "Nouveau webhook", - "hooks.dialogIntro": "Un POST signé par transition, dans l’ordre. Contrairement à une alerte, rien n’est regroupé ni anti-rebondi — un script reçoit chaque front.", - "hooks.name": "Nom", - "hooks.url": "URL du point de terminaison", - "hooks.urlMasked": "L’URL enregistrée n’est jamais affichée en entier. Laissez la valeur masquée pour la conserver, ou saisissez une nouvelle URL pour la remplacer.", - "hooks.triggers": "Déclencheurs", - "hooks.triggersNoneHint": "Aucun sélectionné signifie tous les déclencheurs, y compris ceux ajoutés par une version future.", + "hooks.statSent": "Envoyées", + "hooks.testFailed": "Le point de terminaison n’a pas accepté la livraison de test.", + "hooks.timeout": "délai de {seconds}s", "hooks.timeoutLabel": "Délai (s)", - "hooks.attemptsLabel": "Tentatives", - "hooks.retriesHint": "Les réessais bloquent la file propre à ce point de terminaison, car l’ordre est la promesse. Un 4xx n’est jamais réessayé — un point de terminaison qui dit que la requête est mauvaise le redira.", - "hooks.create": "Créer", - "hooks.triggerIngestPublished": "Flux démarré", - "hooks.triggerIngestDisconnected": "Flux arrêté", - "hooks.triggerDestinationUp": "Destination en direct", - "hooks.triggerDestinationDown": "Destination arrêtée", - "hooks.loadFailed": "Impossible de charger les webhooks.", + "hooks.title": "Webhooks de cycle de vie", "hooks.toggleFailed": "Impossible de modifier le hook.", - "hooks.deleted": "Webhook supprimé.", - "hooks.deleteFailed": "Impossible de supprimer le hook.", - "hooks.testFailed": "Le point de terminaison n’a pas accepté la livraison de test.", - "hooks.deliveriesFailed": "Impossible de lire les livraisons récentes.", + "hooks.triggerCount": "{count} déclencheurs", + "hooks.triggerCountOne": "{count} déclencheur", + "hooks.triggerDestinationDown": "Destination arrêtée", + "hooks.triggerDestinationUp": "Destination en direct", + "hooks.triggerIngestDisconnected": "Flux arrêté", + "hooks.triggerIngestPublished": "Flux démarré", + "hooks.triggers": "Déclencheurs", + "hooks.triggersNoneHint": "Aucun sélectionné signifie tous les déclencheurs, y compris ceux ajoutés par une version future.", + "hooks.unsigned": "NON SIGNÉ", "hooks.updated": "Webhook mis à jour.", - "hooks.created": "Webhook créé.", - "hooks.saveFailed": "Impossible d’enregistrer le hook.", - "playlist.title": "Playlist", + "hooks.url": "URL du point de terminaison", + "hooks.urlMasked": "L’URL enregistrée n’est jamais affichée en entier. Laissez la valeur masquée pour la conserver, ou saisissez une nouvelle URL pour la remplacer.", + "jobs.alwaysKeepNewest": "Toujours conserver les plus récentes", + "jobs.automaticModel": "Automatique (choisi selon ce matériel)", + "jobs.backToDefault": "Revenir au mode par défaut pour ce type", + "jobs.batteryFloor": "Plancher de batterie %", + "jobs.broadcastGoing": "Une diffusion est en cours ; le travail différé lui cède la place.", + "jobs.cancel": "Annuler", + "jobs.cannotPause": "Ces types devraient être suspendus mais ne peuvent pas l'être, ils terminent donc à la place.", + "jobs.completed": "Achevées", + "jobs.concurrency": "Concurrence", + "jobs.couldNotReadTheJob": "Impossible de lire la file des tâches.", + "jobs.couldNotSaveThePolicy": "Impossible d'enregistrer la politique.", + "jobs.cpuCeiling": "Plafond CPU %", + "jobs.cpuResume": "Reprise CPU %", + "jobs.defaultMode": "Mode par défaut", + "jobs.defaultModel": "Modèle par défaut", + "jobs.deferred": "Différées", + "jobs.failed": "Échouées", + "jobs.finished": "Terminées", + "jobs.global": "Global", + "jobs.gpuGateNote": "Seuls les types marqués comme utilisant le GPU sont soumis à cette limite.", + "jobs.history": "Historique", + "jobs.historyRetention": "Conservation de l'historique", + "jobs.holdDeferredWorkBackOn": "Retient le travail différé sur un portable en décharge en dessous de cette valeur. 0 le désactive.", + "jobs.hostCpuLoad": "Charge CPU de l'hôte", + "jobs.howManyJobsMayRun": "Combien de tâches peuvent s'exécuter en même temps. Une, parce qu'un second transcodage achète un débit que personne n'a demandé au prix d'un risque que personne n'a accepté.", + "jobs.inQueue": "Dans la file", + "jobs.job": "Tâche", + "jobs.keepForDays": "Conserver pendant (jours)", + "jobs.machineSafety": "Sécurité de la machine. C'est la seule limite à laquelle le travail temps réel obéit également.", + "jobs.niceLevel": "Niveau nice", + "jobs.noFinished": "Aucune tâche terminée pour l'instant.", + "jobs.noModels": "Aucun fichier de modèle n'a été trouvé dans le répertoire des modèles. Le mode automatique téléchargera celui qu'il choisit ; nommer ici un modèle précis ne fonctionne qu'une fois ce fichier présent.", + "jobs.nothingNewStartsAboveThis": "Au-dessus de cette valeur, rien de nouveau ne démarre. 0 désactive le verrou.", + "jobs.nothingQueued": "Rien en file. La post-production se soumet depuis la bibliothèque.", + "jobs.nothingWasOldEnoughTo": "Rien n'était assez ancien pour être purgé. Baissez d'abord la conservation dans Politique.", + "jobs.pauseAll": "Tout suspendre", + "jobs.policy": "Politique", + "jobs.policySaved": "Politique de ressources enregistrée.", + "jobs.progress": "Progression", + "jobs.queuePaused": "File suspendue.", + "jobs.queueResumed": "File reprise.", + "jobs.queued": "En file", + "jobs.recording": "Enregistrement", + "jobs.releasedItWillStartAs": "Libérée. Elle démarrera dès qu'un emplacement sera libre.", + "jobs.removeFromHistory": "Retirer de l'historique", + "jobs.removeWindow": "Supprimer la fenêtre", + "jobs.resumeAll": "Tout reprendre", + "jobs.retry": "Réessayer", + "jobs.retryTitle": "Réarmer avec un nouveau budget de tentatives", + "jobs.runNow": "Exécuter maintenant", + "jobs.runNowTitle": "Exécuter celle-ci maintenant, en ignorant la politique de ressources", + "jobs.running": "En cours", + "jobs.savedTheConcurrencyChangeTakes": "Enregistré. Le changement de concurrence prend effet au redémarrage du serveur — la file le fixe à son démarrage.", + "jobs.state": "État", + "jobs.stopsEverythingRealtimeIncludedA": "Arrête tout, temps réel compris : un CPU qui se bride a déjà commencé à dégrader le flux. 0 le désactive.", + "jobs.subtitle": "Travail en arrière-plan : transcription, proxys, vignettes et compression de l'archive.", + "jobs.thatDidNotWork": "Cela n'a pas fonctionné.", + "jobs.thermalCeiling": "Plafond thermique °C", + "jobs.thisManyFinishedJobsSurvive": "Ce nombre de tâches terminées est conservé quel que soit leur âge.", + "jobs.title": "Tâches", + "jobs.transcription": "Transcription", + "jobs.unsavedChanges": "Modifications non enregistrées.", + "jobs.whereTheCeilingReleasesThe": "Où le plafond relâche. L'écart est l'hystérésis qui empêche le verrou d'osciller.", + "jobs.whisperMissing": "whisper.cpp n'est pas installé ici, ceci n'a donc encore aucun effet. Il vaut tout de même la peine de le régler : le choix s'applique dès qu'il le sera.", + "jobs.why": "Pourquoi", + "jobs.windowEnd": "Fin de la fenêtre", + "jobs.windowStart": "Début de la fenêtre", + "jobs.yieldNote": "Le travail lourd est mis en file et cède la place au flux en direct. Une image perdue est irrécupérable ; une transcription qui arrive une heure plus tard ne coûte rien.", + "lib.anySpeaker": "Tout intervenant", + "lib.anyTag": "Toute étiquette", + "lib.autoGrouped": "Regroupée automatiquement. La modifier vous l'approprie.", + "lib.autoGroupedNote": "Cette session a été regroupée automatiquement. L'enregistrer vous l'approprie, et la passe de regroupement cessera de la réécrire.", + "lib.bestMatch": "Meilleure correspondance", + "lib.clearSearch": "Effacer la recherche", + "lib.clip": "Extraire", + "lib.clipTitle": "Couper un extrait de cet enregistrement", + "lib.collapseSession": "Réduire la session", + "lib.couldNotLoadTheLibrary": "Impossible de charger la bibliothèque.", + "lib.couldNotLoadTheSession": "Impossible de charger les segments de la session.", + "lib.couldNotQueueTheProxy": "Impossible de mettre le proxy en file.", + "lib.couldNotRegroup": "Impossible de regrouper.", + "lib.couldNotSaveTheSession": "Impossible d'enregistrer la session.", + "lib.delete": "Supprimer", + "lib.deleteDescription": "Le fichier est retiré du disque, avec ses proxys, ses vignettes et sa transcription. Cette action est irréversible.", + "lib.deleteTitle": "Supprimer cet enregistrement ?", + "lib.downloadMaster": "Télécharger le master", + "lib.downloadMasterTitle": "Télécharger le master multipiste", + "lib.editSession": "Modifier la session", + "lib.expandSession": "Développer la session", + "lib.from": "De", + "lib.generateProxy": "Générer le proxy", + "lib.generateProxyTitle": "Générer le proxy lisible dans le navigateur", + "lib.mostRecent": "Les plus récents", + "lib.noProxyYet": "Pas encore de proxy — ouvrez pour en générer un", + "lib.noSegments": "Aucun segment ici.", + "lib.noSessionMatch": "Aucune session ne correspond à ces filtres.", + "lib.nothingRecorded": "Rien d'enregistré pour l'instant. Les sessions apparaissent au fur et à mesure que l'enregistreur écrit des segments.", + "lib.oldestFirst": "Les plus anciens d'abord", + "lib.playProxy": "Lire le proxy", + "lib.proxyAlreadyQueued": "Un proxy est déjà en file.", + "lib.proxyQueued": "Proxy mis en file.", + "lib.recordingDeleted": "Enregistrement supprimé.", + "lib.regroupTitle": "Relance le regroupement. Additif et idempotent : il ne fusionne jamais un regroupement que vous avez séparé à la main.", + "lib.searchPlaceholder": "Rechercher tout ce qui a été dit, dans tous les enregistrements…", + "lib.searchTranscripts": "Rechercher dans les transcriptions", + "lib.searchable": "Consultable", + "lib.searchableOnly": "Segments dotés d'une transcription, donc consultables", + "lib.session": "Session", + "lib.sessionDescription": "Description", + "lib.sessionSaved": "Session enregistrée.", + "lib.sessionTags": "Étiquettes", + "lib.sessionTitle": "Titre", + "lib.subtitle": "Toutes vos diffusions, consultables. Chaque microphone a été enregistré sur sa propre piste, la transcription sait donc déjà qui a parlé.", + "lib.tag": "Étiquette", + "lib.tagsPlaceholder": "séparées, par virgules", + "lib.title": "Bibliothèque", + "lib.transcribe": "Transcrire", + "lib.transcribeEachTrackOnIts": "Transcrire chaque piste séparément — c'est ce qui donne l'attribution des intervenants", + "lib.ungrouped": "Segments non regroupés", + "meters.deviation": "Écart", + "meters.idle": "inactif", + "meters.integrated": "Intégrée", + "meters.loudness": "Conformité sonore", + "meters.metering": "mesure en cours", + "meters.momentary": "Momentanée", + "meters.monitorOff": "Le moniteur est arrêté. Activez-le pour mesurer ce que chaque destination délivre.", + "meters.noSignal": "aucun signal sur cette piste", + "meters.nothingToMeasure": "Rien à mesurer pour l'instant. Chaque destination en cours reçoit son propre analyseur EBU R128 sur le mixage qu'elle reçoit.", + "meters.range": "Plage", + "meters.shortTerm": "Court terme", + "meters.subtitle": "Chaque canal de chaque piste d'entrée, directement depuis le relais.", + "meters.title": "Vumètres audio", + "meters.truePeak": "Crête réelle", + "meters.waitingForStream": "en attente du flux", + "mon.allProcesses": "Tous les processus", + "mon.bitrateChart": "Débit d'entrée — 30 dernières minutes", + "mon.chooseDestination": "Choisir une destination…", + "mon.clearLog": "Vider le journal", + "mon.clearedThisDestinationIsBack": "Effacé. Cette destination utilise de nouveau la commande générée.", + "mon.dryRun": "Essai à blanc", + "mon.dryRunInvalid": "FFmpeg a rejeté un argument", + "mon.dryRunOk": "FFmpeg l'a accepté", + "mon.extraInput": "Arguments d'entrée supplémentaires", + "mon.extraOutput": "Arguments de sortie supplémentaires", + "mon.ffmpegLog": "Journal FFmpeg", + "mon.fromRunningProcess": "depuis le processus en cours", + "mon.fullCommand": "Commande entièrement résolue", + "mon.hostCpu": "Processeur de l'hôte", + "mon.ingestLoss": "Perte à l'entrée", + "mon.memory": "Mémoire", + "mon.nothingProvenEitherWay": "Rien de prouvé dans un sens ou dans l'autre", + "mon.pid": "PID", + "mon.processes": "Processus", + "mon.rebuilt": "reconstruite", + "mon.relayDrops": "Pertes du relais", + "mon.relayIn": "Entrée du relais", + "mon.resolveHint": "Résolvez la commande pour voir exactement ce qui sera exécuté avant d'appliquer.", + "mon.restarts": "Redémarrages", + "mon.subtitle": "Santé des processus, ressources de l'hôte et le journal FFmpeg en direct.", + "mon.title": "Supervision", + "mon.uptime": "Durée de fonctionnement", + "nav.automation": "Automatisation", + "nav.chat": "Chat", + "nav.clips": "Extraits", + "nav.dashboard": "Tableau de bord", + "nav.jobs": "Tâches", + "nav.library": "Bibliothèque", + "nav.meters": "Indicateurs audio", + "nav.monitoring": "Surveillance", + "nav.playout": "Playout", + "nav.recordings": "Enregistrements", + "nav.renditions": "Versions", + "nav.routing": "Routing", + "nav.settings": "Paramètres", + "nav.sources": "Sources", + "play.acrossEveryRungTheBackstop": "Sur tous les échelons. Le garde-fou qui empêche une longue diffusion de remplir le disque.", + "play.allowCrossOrigin": "Autoriser le cross-origin", + "play.allowCrossOriginPlayback": "Autoriser la lecture cross-origin", + "play.audioBitrate": "Débit audio (kbps)", + "play.audioTrack": "Piste audio", + "play.basicPasswordNote": "Les lecteurs incapables de suivre un lien (un décodeur, un CDN) peuvent envoyer le même secret sous forme de mot de passe HTTP basic à la place.", + "play.boundsTheViewerTableViewers": "Borne la table des spectateurs. Les spectateurs au-delà sont servis, simplement pas comptés.", + "play.carriesThePlaybackTokenAnyone": "Contient le jeton de lecture. Toute personne disposant de cette URL peut regarder.", + "play.clipboardUnavailable": "Le presse-papiers n'est pas disponible ; sélectionnez et copiez à la main", + "play.couldNotLoadPlayout": "Impossible de charger le playout", + "play.disabled": "Désactivé", + "play.diskCap": "Plafond disque (Mo)", + "play.dvrWindow": "Fenêtre DVR (s)", + "play.embedCode": "Code d'intégration", + "play.enable": "Activer le playout", + "play.enabled": "Activé", + "play.forObsACdnOr": "Pour OBS, un CDN, ou tout lecteur qui parle HLS.", + "play.format": "Format", + "play.hls": "HLS", + "play.hlsDash": "HLS et DASH", + "play.hlsPlaylist": "Playlist HLS", + "play.idleTimeout": "Délai d'inactivité spectateur (s)", + "play.linkReset": "Nouveau lien de lecture généré. Le précédent ne fonctionne plus.", + "play.linksProtected": "Ces liens ne fonctionnent que pour un administrateur connecté tant que vous n'avez pas activé Public.", + "play.linksPublic": "Ces liens fonctionnent sans compte.", + "play.liveWindow": "Fenêtre en direct (segments)", + "play.metaDescription": "Description", + "play.metaTitle": "Titre", + "play.mustExceedOneSegmentOr": "Doit dépasser un segment, sinon les spectateurs disparaissent par intermittence entre deux relevés.", + "play.name": "Nom", + "play.noCredentialNote": "Aucun identifiant d'aucune sorte n'est requis. À réserver à un flux que vous destinez à l'internet tout entier.", + "play.noRungs": "Aucun échelon pour l'instant. Ajoutez-en un pour publier quoi que ce soit.", + "play.onDisk": "Sur le disque", + "play.oneStereoAacTrackPer": "Une piste AAC stéréo par échelon.", + "play.packaging": "Conditionnement", + "play.passthrough": "Entrée (transmission directe)", + "play.peak": "Crête", + "play.playerPage": "Page du lecteur", + "play.posterNote": "Affichée au-dessus de la vidéo sur la page publique. L'affiche est une image clé récente, actualisée automatiquement.", + "play.private": "Privé.", + "play.protectedNote": "La lecture est protégée par défaut. L'ouvrir est une étape distincte et délibérée.", + "play.protection": "Protection", + "play.public": "Public", + "play.resetFailed": "Impossible de réinitialiser", + "play.restartNote": "Modifier l'un de ces réglages redémarre les multiplexeurs, ce qui interrompt les spectateurs pendant quelques secondes.", + "play.rungNote": "Chaque échelon copie la vidéo de sa version bit à bit : en ajouter un coûte un multiplexeur et jamais un second encodage vidéo. Seule la piste audio diffère d'un échelon à l'autre ; le routage par destination n'est pas touché.", + "play.segmentLength": "Durée d'un segment (s)", + "play.serveWithoutSession": "Servir les spectateurs sans session", + "play.sessionCap": "Plafond de sessions", + "play.sessions": "Sessions", + "play.share": "Partager", + "play.shorterIsLowerLatencyAnd": "Plus court signifie moins de latence et davantage de remaniement de la playlist.", + "play.subtitle": "Servir le flux directement aux spectateurs, en HLS public.", + "play.threeIsTheMinimumA": "Trois est le minimum sur lequel un lecteur peut se constituer un tampon.", + "play.title": "Playout", + "play.unlisted": "Non répertorié.", + "play.untitled": "Flux sans titre", + "play.videoSource": "Source vidéo", + "play.watchingNow": "Spectateurs actuels", + "play.whoCanWatch": "Qui peut regarder", + "player.copied": "Copié", + "player.copy": "Copier", + "player.loading": "Chargement…", + "player.missingToken": "Le lien que vous avez suivi n'a pas de jeton de lecture, ou le jeton a été modifié. Demandez un lien à jour à la personne qui vous l'a partagé.", + "player.notLive": "Pas de direct en ce moment", + "player.viewer": "spectateur", + "player.viewers": "spectateurs", + "player.willStart": "Cette page se mettra à lire d'elle-même dès le début de la diffusion.", + "playlist.choose": "Choisir un fichier", + "playlist.chooseAria": "Choisir un fichier à ajouter", "playlist.empty": "Rien en file pour l’instant.", + "playlist.hint": "Envoyés dans Media. Un élément nommé par cette playlist qui perd ensuite son fichier apparaît ci-dessus comme nécessitant une action, au lieu d’échouer en silence au prochain basculement.", "playlist.itemsAria": "Éléments de la playlist", - "playlist.stateReady": "Prêt", - "playlist.stateTranscoding": "Transcodage", - "playlist.stateAttention": "Nécessite une action", - "playlist.stateUnsaved": "Pas encore enregistré", "playlist.missingUpload": "Ce fichier envoyé est introuvable.", - "playlist.moveUp": "Monter {name}", "playlist.moveDown": "Descendre {name}", - "playlist.remove": "Retirer {name}", - "playlist.choose": "Choisir un fichier", - "playlist.chooseAria": "Choisir un fichier à ajouter", + "playlist.moveUp": "Monter {name}", "playlist.noUploads": "Aucun fichier disponible", - "playlist.hint": "Envoyés dans Media. Un élément nommé par cette playlist qui perd ensuite son fichier apparaît ci-dessus comme nécessitant une action, au lieu d’échouer en silence au prochain basculement.", - "dest.fbBoxTitle": "Publication croisée et bouton de don Facebook", - "dest.fbBoxIntro": "Envoyé lors du même appel de création que l’Audience, donc la même règle s’applique : appliqué une fois au démarrage de la diffusion, et laissé totalement intact si vide.", - "dest.fbBackupLabel": "Publier un flux de secours", - "dest.fbBackupHelp": "Envoie une seconde copie de ce flux vers l’entrée de secours de Facebook, afin qu’une connexion perdue n’interrompe pas la diffusion.", - "dest.fbBackupCost": "Double la bande passante d’envoi de cette destination.", - "dest.fbBackupReconnect": "L’activer reconnecte le flux une fois — activez-le donc avant de passer en direct.", - "dest.fbCrosspostLabel": "Publication croisée sur des Pages", - "dest.fbCrosspostPageId": "ID de la Page", - "dest.fbCrosspostAlsoPost": "Publier aussi", - "dest.fbCrosspostRemove": "Retirer cette Page", - "dest.fbCrosspostAdd": "Ajouter une Page", - "dest.fbCrosspostHelp": "L’ID numérique de la Page depuis la console de Facebook (paramètres de la Page, ou graph.facebook.com/me/accounts en étant connecté en tant que Page) — il n’y a pas de recherche ici, collez donc l’id plutôt qu’un nom ou une URL. « Publier aussi » publie en tant que cette Page au lieu de seulement lui partager la diffusion ; désactivé, seul le partage discret a lieu.", - "dest.fbDonateLabel": "ID de l’association du bouton de don", - "dest.fbDonatePlaceholder": "ID de l’association", - "dest.fbDonateHelp": "Également un id opaque issu de la console des collectes de Facebook, pas un nom d’association à rechercher. Laissez vide pour n’attacher aucun bouton de don." + "playlist.remove": "Retirer {name}", + "playlist.stateAttention": "Nécessite une action", + "playlist.stateReady": "Prêt", + "playlist.stateTranscoding": "Transcodage", + "playlist.stateUnsaved": "Pas encore enregistré", + "playlist.title": "Playlist", + "rec.delete": "Supprimer", + "rec.deleteDescription": "Le fichier est retiré du disque, avec ses proxys, ses vignettes et sa transcription. Cette action est irréversible.", + "rec.deleteFailed": "Impossible de supprimer l'enregistrement.", + "rec.deleteTitle": "Supprimer cet enregistrement ?", + "rec.deleted": "Enregistrement supprimé.", + "rec.disabled": "désactivé", + "rec.disk": "Disque", + "rec.diskRecordings": "Enregistrements", + "rec.download": "Télécharger", + "rec.downloadNamed": "Télécharger {name}", + "rec.duration": "Durée", + "rec.empty": "Aucun enregistrement pour l'instant. Activez l'enregistrement à droite et lancez un flux.", + "rec.enabled": "Activé", + "rec.file": "Fichier", + "rec.free": "Libre", + "rec.haltedDefault": "L'enregistrement est arrêté : le volume est sous le seuil d'espace libre.", + "rec.haltedRestarts": "L'enregistreur redémarre de lui-même dès que de l'espace est libéré.", + "rec.help.enabled": "Écrit chaque entrée sur le disque pendant la diffusion, en gardant toutes les pistes audio séparées et non encodées. L'enregistrement est indépendant de ce que vous envoyez aux plateformes : le désactiver n'affecte pas votre sortie en direct, et l'activer coûte du disque plutôt que de la bande passante montante.", + "rec.help.maxAgeHours": "Supprime les enregistrements plus anciens que ce nombre d'heures, quelle que soit la taille totale. Utile quand vous n'avez besoin que du dernier jour ou deux. Mettez 0 pour tout conserver jusqu'à ce qu'une autre limite l'efface.", + "rec.help.maxGb": "Un plafond sur la taille totale des enregistrements que polyemesis conserve. Une fois dépassé, les plus anciens sont supprimés jusqu'à ce que le total rentre à nouveau. Mettez 0 pour aucune limite de taille — auquel cas l'âge, ou le seuil d'espace libre, est la seule chose qui borne la croissance.", + "rec.help.minFreeGb": "Un arrêt net : l'enregistrement s'interrompt lorsque le volume passe sous cet espace libre et reprend de lui-même une fois de la place libérée. C'est la seule limite qui tienne compte des fichiers que polyemesis n'a pas écrits, donc le réglage qui protège réellement un disque partagé du remplissage.", + "rec.help.segmentSeconds": "La durée de chaque fichier avant que l'enregistreur n'en commence un nouveau. Des segments plus courts limitent ce qu'un plantage peut vous coûter et facilitent le déplacement des fichiers, mais en laissent davantage à gérer. Les coupes tombent sur une image clé, la durée réelle varie donc d'une seconde environ.", + "rec.help.stemFiles": "Écrit en plus chaque piste audio dans son propre fichier à côté du master. Utile si vous montez ensuite et voulez l'animateur, les invités et le son du jeu sur des canaux distincts. Cela multiplie ce qu'une session écrit par environ le nombre de pistes, d'où sa désactivation par défaut.", + "rec.help.stemFormat": "Les deux options sont sans perte, aucune ne coûte donc en qualité. FLAC occupe environ moitié moins et convient sauf si un outil de votre chaîne ne sait pas le lire ; WAV s'ouvre dans n'importe quoi. Les pistes de plus de 8 canaux basculent automatiquement en WAV.", + "rec.hideStems": "Masquer les pistes séparées", + "rec.keepForever": "0 = conserver indéfiniment.", + "rec.loadFailed": "Impossible de charger les enregistrements.", + "rec.maxAgeHours": "Supprimer après (heures)", + "rec.maxGb": "Conserver au plus (Go)", + "rec.minFreeGb": "Arrêter en dessous d'un espace libre de (Go)", + "rec.minFreeNote": "Les plafonds ci-dessus ne limitent que ce que polyemesis a écrit ; tout le reste sur le volume peut quand même le remplir. 0 = aucun seuil.", + "rec.noSizeLimit": "0 = aucune limite de taille.", + "rec.recording": "enregistre", + "rec.retentionNote": "La conservation s'exécute toutes les 30 secondes : les plus anciens d'abord, par âge puis par taille totale. Le segment en cours d'écriture n'est jamais supprimé.", + "rec.saveFailed": "Impossible d'enregistrer les paramètres.", + "rec.savePolicy": "Enregistrer la politique de conservation", + "rec.saved": "Paramètres d'enregistrement enregistrés.", + "rec.segmentSeconds": "Durée d'un segment (secondes)", + "rec.segments": "Segments", + "rec.settingsTitle": "Enregistrement", + "rec.showStems": "Afficher les pistes séparées", + "rec.size": "Taille", + "rec.started": "Débuté", + "rec.stemFiles": "Fichiers de piste séparée", + "rec.stemFlac": "FLAC — sans perte, environ deux fois plus léger", + "rec.stemFormat": "Format des pistes séparées", + "rec.stemNote": "Écrit depuis le même processus que le master, si bien que chaque piste séparée reste alignée à l'échantillon près sur les autres. Les pistes de plus de 8 canaux basculent d'elles-mêmes en WAV.", + "rec.stemWav": "WAV — sans perte, s'ouvre partout", + "rec.stems": "Pistes séparées", + "rec.stemsTitle": "{count} fichiers de piste séparée, alignés à l'échantillon près sur le master", + "rec.stemsUsage": "Pistes séparées ({count} fichiers)", + "rec.subtitle": "L'archive multipiste complète : chaque piste audio d'entrée conservée, non encodée.", + "rec.title": "Enregistrements", + "rec.tracks": "Pistes", + "rec.tracksTitle": "{count} pistes audio conservées", + "rec.used": "Utilisé", + "rec.volume": "Volume", + "rend.aspectBlurpad": "Remplissage flou", + "rend.aspectBlurpadHint": "Remplit le reste avec une copie floue de l'image elle-même, recadrée pour remplir. C'est la convention qu'ont adoptée les flux verticaux.", + "rend.aspectCrop": "Recadrer pour remplir", + "rend.aspectCropHint": "Recadre au centre à la forme cible, puis met à l'échelle. Les sujets gardent leur taille à l'écran ; les bords de l'image disparaissent.", + "rend.aspectHandling": "Gestion du format d'image", + "rend.aspectPad": "Bandes", + "rend.aspectPadHint": "Met à l'échelle l'image entière pour qu'elle tienne et remplit le reste avec une couleur unie. Rien n'est perdu, mais une source 16:9 sur un cadre 9:16 n'est presque que des bandes.", + "rend.aspectStretch": "Étirer pour remplir", + "rend.aspectStretchHint": "Met à l'échelle à la taille cible et laisse l'image se déformer si la source ne correspond pas. Ce que faisait chaque version avant que les autres modes n'existent.", + "rend.boxColour": "Couleur du cadre", + "rend.boxOpacity": "Opacité du cadre (%)", + "rend.builtInFont": "Police intégrée par défaut", + "rend.burnedText": "Texte incrusté", + "rend.chooseHardware": "Choisissez un encodeur matériel, ou une sortie plus petite.", + "rend.colourBlack": "black", + "rend.colourWhite": "white", + "rend.costBeyond": "Encoder cela en logiciel en temps réel dépasse la plupart des machines", + "rend.costBeyondDetail": "{label} représente {mpps} pour un seul encodage. x264 à cette taille perd généralement la course contre le temps réel, et une version qui prend du retard perd des images pour toutes les destinations qui la lisent — ce que vous découvrez en pleine diffusion.{machine} {escape}", + "rend.costBusy": "Attendez-vous à ce que plusieurs cœurs soient occupés", + "rend.costBusyDetail": "{label} représente {mpps} sur {encoder}, ce qui constitue un encodage temps réel lourd.{machine} Surveillez la vitesse de l'encodeur une fois en direct : en dessous de 1,0×, il prend du retard et les destinations qui en dépendent commenceront à perdre des images. {escape}", + "rend.costMachineCores": " Cette machine déclare {cores} cœurs logiques.", + "rend.costModest": "Encodage logiciel modeste", + "rend.costModestDetail": "{label} représente {mpps}, à la portée de la plupart des machines.{machine} Cela reste un encodage réel — partagé par toutes les destinations qui le sélectionnent, mais pas gratuit.", + "rend.costNoNumbers": "Le coût ne peut pas encore être estimé", + "rend.costNoNumbersDetail": "Rien n'a sondé la source, « conserver la source » n'a donc aucun chiffre derrière lui. Démarrez l'entrée et ceci se remplira.", + "rend.costOffloaded": "Déchargé sur {encoder}", + "rend.costOffloadedDetail": "{label} ({mpps}) s'exécute sur l'encodeur à fonction fixe du GPU, il coûte donc peu de CPU. La qualité et le contrôle de débit varient selon le pilote, et chaque puce a son propre plafond — vérifiez la sortie avant une émission qui compte.", + "rend.create": "Créer", + "rend.created": "Version créée. Sélectionnez-la sur une destination pour démarrer l'encodage.", + "rend.custom": "Personnalisé…", + "rend.deintAll": "Toutes les images", + "rend.deintAllHint": "Pour les sources entrelacées qui ne le déclarent pas. Beaucoup de ponts SDI et de cartes d'acquisition marquent tout comme progressif quoi qu'on leur envoie, et sur ceux-là « uniquement les images entrelacées » ne fait rien et ressemble à un réglage cassé.", + "rend.deintAuto": "Uniquement les images entrelacées", + "rend.deintAutoHint": "Ne touche que les images que la source a marquées comme entrelacées : les images progressives passent intactes. Le bon choix pour tout contenu mixte.", + "rend.deintOff": "Désactivé", + "rend.deintOffHint": "Le bon choix pour les sources progressives que presque tout le monde a. En désentrelacer une l'adoucit sans aucun gain.", + "rend.deinterlace": "Désentrelacer", + "rend.deleteFailed": "Impossible de supprimer la version.", + "rend.deleteFallbackMany": "{count} destinations repassent en transmission directe et recevront la vidéo source telle quelle. Vérifiez que la source correspond toujours à ce que chaque plateforme accepte.", + "rend.deleteFallbackOne": "{count} destination repasse en transmission directe et recevra la vidéo source telle quelle. Vérifiez que la source correspond toujours à ce que la plateforme accepte.", + "rend.deleteNoDestinations": "Rien ne sélectionne cette version : la supprimer ne change aucune destination.", + "rend.deleteRenditionTitle": "Supprimer {name} ?", + "rend.editRendition": "Modifier la version", + "rend.empty": "Aucune version. Chaque destination est en transmission directe, ce qui est exactement ce qu'il faut jusqu'à ce que l'une d'elles ne puisse pas accepter la source — une entrée 4K60 que Twitch, Kick ou X refusent, par exemple. Ajoutez une version et pointez ces destinations dessus.", + "rend.encoder": "Encodeur", + "rend.encoderPreset": "Préréglage de l'encodeur", + "rend.encoderRelatedFailed": "{name} n'a pas été proposé parce qu'un encodeur apparenté a échoué ici.", + "rend.encoderTestFailed": "{name} a échoué à un encodage de test d'une image sur cette machine.", + "rend.encoderTestOk": "{name} a encodé une image de test sur cette machine.", + "rend.evenNote": "0 sur un axe conserve celui de la source et préserve le format d'image — ne définissez que la hauteur pour redimensionner. Les deux doivent être des nombres pairs : H.264 et HEVC n'ont pas de plan de chrominance de taille impaire.", + "rend.font": "Police", + "rend.frameRate": "Cadence d'images", + "rend.gop": "GOP", + "rend.gpuUnopenable": "Un GPU est présent mais ce processus ne peut pas l'ouvrir", + "rend.hardwareFallback": "matériel", + "rend.hardwareScanFound": "Ce que l'analyse du matériel a trouvé", + "rend.height": "Hauteur", + "rend.hevcWarning": "Cet encodeur produit du HEVC. La plupart des destinations RTMP n'acceptent que H.264 — ne le choisissez que pour une destination qui vous a indiqué accepter le HEVC.", + "rend.keyframeInterval": "Intervalle entre images clés", + "rend.letterboxColour": "Couleur des bandes", + "rend.live": "En direct", + "rend.loadFailed": "Impossible de charger les versions.", + "rend.marginX": "Marge X (%)", + "rend.marginY": "Marge Y (%)", + "rend.name": "Nom", + "rend.namePlaceholder": "1080p60 pour Twitch et Kick", + "rend.needsBothAWidthAnd": "Nécessite à la fois une largeur et une hauteur — avec un axe libre, la mise à l'échelle préserve déjà le rapport d'aspect.", + "rend.needsBothAxes": "Nécessite à la fois une largeur et une hauteur", + "rend.newRendition": "Nouvelle version", + "rend.noEncoder": "Ce FFmpeg n'a signalé aucun encodeur vidéo utilisable.", + "rend.noHardwareEscape": "Aucun encodeur matériel ne fonctionne sur cette machine, il n'y a donc rien vers quoi déplacer cela — le seul levier ici est une sortie plus petite. Relancez la détection du matériel si vous avez depuis installé un pilote ou passé un GPU dans le conteneur.", + "rend.notProbed": "Rien n'a encore été encodé à titre de test sur cette machine, cette liste correspond donc à ce que contient la version de FFmpeg plutôt qu'à ce dont le matériel est capable. Relancez la détection pour le savoir.", + "rend.notRunning": "Arrêtée", + "rend.note": "Note", + "rend.noteDuplicateFps": "La source tourne à {srcFps} fps, donc {fps} fps ne fait que dupliquer des images.", + "rend.notePlaceholder": "À quoi sert ce palier.", + "rend.noteUpscale": "La source est en {width}×{height}. Agrandir coûte du CPU et n'ajoute aucun détail.", + "rend.opacity": "Opacité (%)", + "rend.percentNote": "Tout est exprimé en pourcentage de l'image, si bien que les mêmes réglages conviennent à un palier 16:9 comme à un palier 9:16. Les marges sont ignorées sur un axe centré.", + "rend.preset": "Préréglage", + "rend.presetPlaceholder": "veryfast", + "rend.presetStartingPoint": "Point de départ — vérifiez les limites actuelles auprès de la plateforme.", + "rend.redetectFailed": "Impossible de relancer la détection du matériel.", + "rend.redetectTitle": "Relancer l'analyse du GPU et encoder une image de test avec chaque encodeur. Prend quelques secondes.", + "rend.redetectedNoHardware": "Matériel redétecté. Aucun encodeur matériel n'a réussi un encodage de test sur cette machine.", + "rend.resolution": "Résolution", + "rend.restartEncode": "Redémarrer cet encodage et les destinations qui le lisent", + "rend.restartFailed": "Impossible de redémarrer l'encodage.", + "rend.restarting": "{name} redémarre. Ses destinations se reconnectent avec elle.", + "rend.save": "Enregistrer", + "rend.saveFailed": "Impossible d'enregistrer la version.", + "rend.seedNote": "Ceci ne fait que préremplir le formulaire — modifiez ce que vous voulez avant d'enregistrer. Les plafonds des plateformes changent et diffèrent selon le statut de partenaire.", + "rend.signalError": "Erreur", + "rend.signalIdle": "Inactif", + "rend.sizePct": "Taille (% de la hauteur)", + "rend.softwareEncoderFallback": "un encodeur logiciel", + "rend.speed": "Vitesse", + "rend.subtitle": "Un seul encodage vidéo partagé, de nombreuses destinations. L'audio n'est jamais touché ici.", + "rend.target": "Cible", + "rend.textColour": "Couleur du texte", + "rend.textPlaceholder": "MA STATION", + "rend.title": "Versions", + "rend.unused": "Aucune destination ne la sélectionne pour l'instant, rien n'est donc encodé. Choisissez-la sur une destination pour démarrer l'encodage.", + "rend.uptime": "Durée de fonctionnement", + "rend.videoBitrate": "Débit vidéo", + "rend.waitingSnapshot": "En attente du premier instantané en direct.", + "rend.watermarkImage": "Image de filigrane", + "rend.watermarkPlaceholder": "overlays/logo.png", + "rend.width": "Largeur", + "rend.widthPct": "Largeur (% de l'image)", + "route.attack": "Attaque", + "route.audio": "audio", + "route.audioAndVideoGoOut": "L'audio et la vidéo partent ensemble.", + "route.audioLaterThanVideo": "Audio après la vidéo", + "route.audioRouting": "Routage audio", + "route.audioRoutingDesc": "Choisissez exactement quelles pistes d'entrée chaque destination reçoit.", + "route.autoLimitWhen2Tracks": "Automatique (limiter à partir de 2 pistes)", + "route.commentaryLanguage": "Langue du commentaire", + "route.couldNotApplyThePreset": "Impossible d'appliquer le préréglage.", + "route.couldNotCompileThisRouting": "Impossible de compiler ce routage.", + "route.couldNotLoadDestinations": "Impossible de charger les destinations.", + "route.couldNotSaveTheRouting": "Impossible d'enregistrer le routage.", + "route.couldNotSaveTrackRoles": "Impossible d'enregistrer les rôles des pistes.", + "route.custom": "Personnalisé…", + "route.delayDirection": "Sens du décalage", + "route.destinations": "Destinations", + "route.duckingNote": "Baisse un groupe de pistes dès qu'un autre parle — le micro par-dessus le fond musical. Désactivé, chaque piste sélectionnée est sommée à son propre gain, inchangée.", + "route.duckingTriggerNote": "Un déclencheur n'a pas besoin de figurer dans le mixage de cette destination — un micro peut atténuer la musique ici alors que seule la musique est envoyée.", + "route.enableDucking": "Activer l'atténuation automatique", + "route.everyTrackReaches": "Toutes les pistes sélectionnées atteignent cette destination, musique comprise.", + "route.excludeMusic": "Exclure la musique", + "route.keepMusic": "Conserver la musique", + "route.keepMusicOut": "Empêcher la musique d'atteindre cette destination", + "route.labelTracks": "Étiqueter les pistes", + "route.langPlaceholder": "lang — p. ex. es", + "route.limiter04DbfsCeiling": "Limiteur (plafond −0,4 dBFS)", + "route.localOnlyNote": "Ce serveur ne stocke pas encore les rôles des pistes, ceux-ci restent donc dans ce navigateur. Tout le reste de la page s'enregistre normalement.", + "route.loudnessTarget": "Cible de niveau sonore", + "route.lufsBroadcast": "EBU R128 diffusion", + "route.measuredOverTheWholeProgramme": "Mesuré sur l'ensemble du programme, et non instant par instant : les passages calmes restent calmes. Ce sont les valeurs que visent les normaliseurs des plateformes elles-mêmes, et en atteindre une est ce qui empêche la plateforme de vous baisser.", + "route.mixMatrix": "Matrice de mixage", + "route.musicPolicy": "Politique musicale", + "route.noTarget": "Aucune cible (ne pas toucher aux niveaux)", + "route.normalizeLoudnorm": "Niveau sonore (EBU R128)", + "route.normalizeOff": "Désactivé", + "route.off": "désactivé", + "route.offset": "Décalage", + "route.on": "activé", + "route.platformDefault": "Valeur par défaut de la plateforme", + "route.podcastSpokenWord": "Podcast / parole", + "route.presetApplied": "Préréglage appliqué. Vérifiez-le, puis enregistrez.", + "route.range": "Plage", + "route.ratio": "Ratio", + "route.release": "Relâchement", + "route.result": "Résultat", + "route.role51": "5.1", + "route.roleClean": "propre", + "route.roleMic": "micro", + "route.roleMusic": "musique", + "route.rolesNote": "Les rôles décrivent l'entrée, pas cette destination, et s'enregistrent séparément. Rien n'est filtré tant qu'une destination ci-dessous ne le demande pas.", + "route.simple": "Simple", + "route.target": "Cible", + "route.targetRole": "cible", + "route.thisDestinationSPlatform": "La plateforme de cette destination", + "route.threshold": "Seuil", + "route.title": "Routing", + "route.triggerRole": "déclencheur", + "route.truePeak": "Crête réelle", + "route.video": "vidéo", + "route.videoLaterThanAudio": "Vidéo après l'audio", + "route.withoutATargetTheMix": "Sans cible, le mixage sort au niveau produit par l'entrée, avec seulement la protection contre l'écrêtage ci-dessus appliquée.", + "route.youtubeTwitchSpotify": "YouTube, Twitch, Spotify", + "set.acceptSelfSigned": "Accepter un certificat auto-signé", + "set.accountDisconnected": "Compte déconnecté.", + "set.adminPassword": "Mot de passe administrateur", + "set.alertDelivery": "Remise des alertes", + "set.alertDeliveryDesc": "Avec quelle insistance un webhook qui ne répond pas est relancé, avant que l'alerte soit abandonnée.", + "set.apiTokens": "Jetons d'API", + "set.apiTokensDesc": "Identifiants de longue durée pour les scripts, Prometheus et tout ce qui ne peut pas tenir une session. Envoyez-en un via Authorization: Bearer pmk_…. Un jeton peut tout ce que peut l'administrateur, sauf gérer les jetons — cela reste derrière le mot de passe, donc en révoquer un est définitif.", + "set.app": "App", + "set.attemptsBackoff": "L'attente entre les tentatives s'allonge automatiquement jusqu'à un plafond de 30 secondes, si bien que le haut de cette plage représente déjà plusieurs minutes de relance d'un point de terminaison mort. La borne est justement l'objectif : réessayer indéfiniment transforme un seul webhook mort en une file qui ne se vide jamais.", + "set.attemptsNote": "Première tentative comprise. 1 signifie aucun réessai.", + "set.attemptsRetryable": "Seuls les échecs qui valent la peine d'être réessayés le sont. Un 404 d'un webhook que quelqu'un a supprimé est définitif, et le relancer quatre fois ne fait que retarder toutes les alertes qui attendent derrière ; un 502 d'un proxy inverse est exactement ce à quoi ces tentatives servent.", + "set.audioMeters": "Vumètres audio", + "set.brokerPasswordCleared": "Mot de passe du courtier effacé.", + "set.brokerPlainNote": "mqtt:// n'est pas chiffré. MQTT envoie le nom d'utilisateur et le mot de passe dans son paquet CONNECT, si bien que les deux traversent le réseau en clair — acceptable sur un réseau local de confiance, ce qui est le cas habituel avec Home Assistant, et inacceptable sur tout ce que vous ne maîtrisez pas. Utilisez mqtts:// si le courtier le propose.", + "set.brokerUrl": "URL du courtier", + "set.brokerUrlNote": "mqtt://, mqtts://, ws:// ou wss://. Un mot de passe dans l'URL est refusé : une URL se retrouve dans les journaux et dans la sortie de ps, et cela ne se rattrape pas. Mettez-le dans le champ ci-dessous, où il est chiffré au repos.", + "set.brokerUrlPlaceholder": "mqtts://broker.local:8883", + "set.chatBothApply": "Les deux s'appliquent et la règle la plus généreuse l'emporte : un message part lorsqu'il est plus ancien que le nombre d'heures et hors des N plus récents. Un salon actif conserve donc moins de temps qu'un salon calme à réglages identiques.", + "set.chatBufferNote": "« Envoyer à la connexion » est un nombre d'une autre nature que les deux autres. Ceux-ci bornent ce qui est gardé sur le disque ; celui-ci est un tampon en mémoire, alloué pour chaque tableau de bord connecté.", + "set.chatHistory": "Historique du chat", + "set.chatHistoryDesc": "Jusqu'où remonte l'historique du chat — et donc ce qu'un modérateur peut voir lorsqu'il ouvre la fiche de quelqu'un.", + "set.chatHours": "Conserver pendant (heures)", + "set.chatKeep": "Toujours conserver (messages)", + "set.chatKeepNote": "Un plancher, quel que soit leur âge.", + "set.chatNotOnlyDisk": "Ce n'est pas seulement un réglage de disque. Cliquer sur un nom dans le chat ouvre ce que cette personne a dit auparavant, et cet historique vient d'ici — aucune plateforme ne publie d'API pour cela, polyemesis ne peut donc montrer que ce qu'il a gardé. Réglez-le trop court et quelqu'un qui pose problème depuis une semaine paraîtra n'avoir jamais rien dit.", + "set.chatSendNote": "Historique sans requête.", + "set.chatSendOnConnect": "Envoyer à la connexion (messages)", + "set.chatSendOnConnectShort": "Envoyer à la connexion", + "set.chatSmall": "Le chat est peu volumineux. Un salon à dix messages par seconde en moyenne stocke environ 7 Mo par heure ; la plupart en sont très loin.", + "set.clientId": "Identifiant client", + "set.clientIdNote": "Doit être unique sur le courtier. Une collision est la cause numéro un d'une boucle de reconnexion inexpliquée : le courtier déconnecte la session la plus ancienne à chaque connexion, et les deux clients se reconnectent sans fin.", + "set.clientIdPlaceholder": "dérivé du nom de l'instance", + "set.clientSecret": "Secret client", + "set.comingBack": "Retour", + "set.confirmPassword": "Confirmer le nouveau mot de passe", + "set.connectedAccounts": "Comptes connectés", + "set.copied": "Copié", + "set.copy": "Copier", + "set.couldNotChangeThePassword": "Impossible de modifier le mot de passe.", + "set.couldNotClearThePassword": "Impossible d'effacer le mot de passe.", + "set.couldNotCreateTheToken": "Impossible de créer le jeton.", + "set.couldNotReCheckThe": "Impossible de revérifier les identifiants.", + "set.couldNotReadTheTls": "Impossible de lire l'état TLS.", + "set.couldNotRevokeTheToken": "Impossible de révoquer le jeton.", + "set.couldNotSaveTheCredentials": "Impossible d'enregistrer les identifiants.", + "set.couldNotSaveTheMqtt": "Impossible d'enregistrer les paramètres MQTT.", + "set.credsRemoved": "Identifiants supprimés.", + "set.currentPassword": "Mot de passe actuel", + "set.dataDirectory": "Répertoire de données", + "set.deliveryAttempts": "Tentatives de remise", + "set.dialsOnly": "Le serveur ne compose que : {schemes}.", + "set.disconnect": "Déconnecter", + "set.disconnectDesc": "Les destinations qui l'utilisent conservent leurs réglages mais cessent de renouveler leur clé de flux. Vous pourrez reconnecter le même compte ensuite.", + "set.disconnectTitle": "Déconnecter ce compte ?", + "set.drawtextMissing": "Cette version ne dispose pas du filtre drawtext, les incrustations de texte sont donc indisponibles. Il faut un FFmpeg compilé avec libfreetype.", + "set.drawtextOk": "Cette version sait incruster du texte sur la vidéo.", + "set.enabled": "Activé", + "set.encoderUrl": "URL de l'encodeur", + "set.expires": "Expire", + "set.failover": "Bascule", + "set.failoverDesc": "Une entrée de secours et une carte d'attente, pour quand la source en direct cesse de livrer. Désactivée par défaut : dans cet état, la chaîne est octet pour octet celle d'avant l'existence de cette fonction.", + "set.failoverRemuxNote": "L'activer ajoute une étape de remultiplexage entre l'entrée et tout ce qui suit. Peu coûteux, mais pas gratuit, et c'est pourquoi il s'agit d'une décision et non d'un réglage par défaut.", + "set.fileSizeMb": "Taille de fichier (Mo)", + "set.filesKept": "Fichiers conservés", + "set.goingLive": "Passage en direct", + "set.goingLiveDesc": "Ce qui se passe quand plusieurs destinations démarrent en même temps.", + "set.graceNote": "Combien de temps la source en direct peut ne rien livrer avant que le niveau ne bascule. Volontairement plus long qu'une reconnexion : un encodeur qui rétablit une connexion RTMP relève du fonctionnement normal, pas d'une panne.", + "set.gracePeriod": "Délai de grâce (secondes)", + "set.haDiscovery": "Découverte Home Assistant", + "set.height": "Hauteur", + "set.ingest": "Entrée", + "set.ingestDesc": "SRT transporte jusqu'à six pistes audio. RTMP en transporte une et existe comme solution de repli pour les encodeurs incapables de faire du SRT. La récupération compose une source au lieu d'en attendre une.", + "set.ingestUrlCopied": "URL d'entrée copiée.", + "set.instance": "Instance", + "set.intervalNote": "Un état inchangé n'est pas republié, un intervalle court coûte donc une comparaison plutôt qu'un message. Le keep-alive borne la durée pendant laquelle un lien mort passe inaperçu — et donc la rapidité avec laquelle le courtier annonce aux abonnés que cette instance a disparu.", + "set.issuer": "Émetteur", + "set.kbps": "kbps", + "set.keepAlive": "Keep-alive (s)", + "set.latencyMs": "Latence (ms)", + "set.listeners": "Écouteurs", + "set.listenersDesc": "Où le serveur s'attache, pour toute l'installation. Un seul port SRT dessert toutes les sources — elles se distinguent par leur jeton de publication, ajouter une source n'implique donc jamais d'exposer un port de plus. RTMP n'a pas ce routage, son port dessert donc une seule source.", + "set.logsBoundNote": "Le répertoire des journaux est borné par le produit de ces deux valeurs, la persistance ne peut donc jamais être ce qui remplit le disque.", + "set.logsDesc": "La sortie d'erreur de FFmpeg est toujours conservée en mémoire, mais celle-ci meurt avec le serveur — ce qui fait perdre précisément les lignes expliquant pourquoi il est mort. La persistance la recopie dans un fichier tournant sous le répertoire de données.", + "set.metersDesc": "Un processus auxiliaire mesure chaque canal de chaque piste. Des intervalles plus courts rafraîchissent plus vite et coûtent plus de CPU.", + "set.mode": "Mode", + "set.modePull": "Récupération — composer une caméra, un flux ou un fichier", + "set.modeRtmp": "RTMP — une seule piste", + "set.modeSrt": "SRT — multipiste (recommandé)", + "set.mqtt": "Télémétrie MQTT", + "set.mqtt5Only": "MQTT 5.0 uniquement.", + "set.mqtt5OnlyNote": "Un courtier figé en 3.1.1 n'établira aucune connexion : ce n'est pas un mode dégradé. Mosquitto 2.x, EMQX et le module complémentaire Home Assistant parlent tous 5.0.", + "set.mqttDesc": "Publie l'état auprès d'un courtier sous forme de messages retenus, de sorte qu'un consommateur non connecté au moment d'un changement obtienne tout de même la réponse actuelle.", + "set.multiAccountNote": "Connectez la même plateforme plusieurs fois pour diffuser vers plusieurs chaînes — chaque compte devient sa propre destination.", + "set.name": "Nom", + "set.newPassword": "Nouveau mot de passe", + "set.no": "non", + "set.noEncryption": "Rien ne chiffre cette connexion. Les mots de passe et les cookies de session traversent le réseau en clair — définissez tls.mode: auto, ou attachez-vous à 127.0.0.1 derrière un proxy.", + "set.noLibsrt": "Cette version de FFmpeg ne prend pas en charge SRT. Installez-en une configurée avec --enable-libsrt, ou utilisez RTMP.", + "set.noTokens": "Aucun jeton pour l'instant.", + "set.passphrase": "Phrase secrète", + "set.passphraseNote": "Active le chiffrement AES. Laissez vide pour aucun. SRT refuse les phrases secrètes hors de 10 à 79 caractères.", + "set.passphrasePlaceholder": "facultatif — de 10 à 79 caractères", + "set.password": "Mot de passe", + "set.passwordChanged": "Mot de passe modifié.", + "set.passwordMismatch": "Les nouveaux mots de passe ne correspondent pas.", + "set.passwordNote": "Le mot de passe n'est renvoyé par aucune API, ce champ est donc vide même lorsqu'il en existe un. Laissez-le vide pour conserver le mot de passe enregistré ; effacez-le avec le bouton ci-dessous.", + "set.platform": "Plateforme", + "set.platformsDesc": "Les plateformes diffèrent par ce que polyemesis peut en automatiser, et cette différence mérite d'être connue avant de commencer plutôt qu'une heure plus tard. La diffusion elle-même fonctionne partout de la même façon — le routage audio par destination, les versions, la reconnexion et les vumètres ne dépendent d'aucune des colonnes ci-dessous.", + "set.playLoop": "Lire une boucle quand rien n'est en direct", + "set.playLoopNote": "Un second candidat à côté de la carte d'attente : des fichiers sont lus au lieu d'une image fixe. Les deux peuvent être actifs — voyez l'ordre des niveaux dans la conception de la bascule pour savoir lequel l'emporte.", + "set.pointObs": "Pointez OBS vers cette adresse.", + "set.preview": "Aperçu", + "set.previewDesc": "Le moniteur HLS du tableau de bord. C'est le seul endroit où polyemesis réencode de la vidéo, et il fonctionne sur son propre abonnement au relais, il ne peut donc jamais affecter une destination.", + "set.previewIdleNote": "L'encodeur démarre lorsque le tableau de bord demande la playlist et s'arrête ce délai après la dernière requête, un tableau de bord fermé ne coûte donc aucun CPU. Modifier ce réglage ne redémarre jamais un aperçu que quelqu'un est en train de regarder.", + "set.processLogs": "Journaux des processus", + "set.proxyTerminatesTls": "Un proxy inverse placé devant polyemesis termine TLS, ce serveur sert donc du HTTP en clair à dessein.", + "set.publishInterval": "Intervalle de publication (s)", + "set.reconnectCap": "Plafond du délai de reconnexion (s)", + "set.reconnectCapNote": "0 utilise la valeur par défaut. Sources HTTP et HLS uniquement.", + "set.redirectNote": "Doit correspondre exactement, schéma et port compris. Si vous atteignez polyemesis via un proxy inverse, utilisez cette URL publique.", + "set.redirectUri": "URI de redirection à autoriser", + "set.removeCreds": "Supprimer les identifiants", + "set.removeCredsDesc": "Tous les comptes connectés via cette application cessent de renouveler leur jeton et devront être reconnectés. Le secret client n'est pas récupérable depuis ici — vous devriez créer une nouvelle application sur la plateforme.", + "set.removeCredsTitle": "Supprimer ces identifiants de développeur ?", + "set.returnAfter": "Revenir après (secondes stables)", + "set.returnAutomatic": "Automatique — revenir dès que la source principale est stable", + "set.returnManual": "Manuel — rester sur la source de secours jusqu'à ce que je bascule", + "set.returnManualNote": "Le mode manuel est la valeur par défaut car un retour automatique peut osciller : une source principale qui se rétablit par à-coups ferait aller et venir le programme en pleine diffusion.", + "set.rtmpPort": "Port RTMP", + "set.rtspNote": "TCP, sauf si la caméra ne le peut pas. RTSP sur UDP à travers du NAT paraît connecté et ne livre rien.", + "set.rtspTransport": "Transport RTSP", + "set.saveAndDrop": "Enregistrer et couper le flux en direct", + "set.saveIngest": "Enregistrer les paramètres d'entrée", + "set.segmentSec": "Segment (s)", + "set.settingsSavedAffectedProcessesHave": "Paramètres enregistrés. Les processus concernés ont été redémarrés.", + "set.showSlate": "Afficher une carte quand rien n'est en direct", + "set.slateColour": "Couleur de la carte", + "set.slateColourNote": "Un seul mot, car la valeur atterrit dans un graphe de filtres où une virgule terminerait l'argument. Utilisée uniquement lorsqu'aucune image n'est définie.", + "set.slateColourPlaceholder": "black", + "set.slateImage": "Image de la carte", + "set.slateImageNote": "Un chemin à l'intérieur du répertoire de données. Laissez-le vide pour peindre une couleur unie à la place — c'est précisément la solution de repli, car une couleur n'a aucun fichier susceptible de ne pas s'ouvrir.", + "set.slateImagePlaceholder": "slate/holding.png", + "set.sourceUrl": "URL de la source", + "set.sourceUrlNote": "L'un de {schemes}. Une source file:// est un chemin RELATIF au répertoire de données et ne peut pas contenir « .. » — elle y est confinée exactement comme un fichier téléversé.", + "set.sourceUrlPlaceholder": "rtsp://camera.local/stream1", + "set.srtPort": "Port SRT", + "set.stagger": "Échelonner les connexions (ms)", + "set.staggerNote": "0 désactive. Sinon, passer en direct signifie que chaque destination ouvre une connexion, négocie TLS et commence à encoder l'audio dans le même instant — sur une petite machine, c'est le moment le plus propice à la perte d'images, et c'est exactement celui que vous regardez, puisque vous venez de passer en direct. Quelques centaines de millisecondes suffisent.", + "set.staggerReconnectNote": "Cela ne retarde jamais une reconnexion. Une destination qui tombe à 3 heures du matin revient immédiatement au lieu d'attendre son tour derrière des processus déjà sains.", + "set.stepByStep": "Configuration pas à pas", + "set.stopAfterIdle": "Arrêter après inactivité (s)", + "set.streamKey": "Clé de flux", + "set.streamKeyNote": "Les diffuseurs doivent utiliser exactement ce chemin, afin qu'un port ouvert ne soit pas une porte ouverte.", + "set.subject": "Sujet", + "set.synthesiseNote": "Toutes les grandes plateformes refusent un flux sans piste audio. Avec cette option, une entrée uniquement vidéo reçoit une piste stéréo silencieuse et ses destinations démarrent ; sans elle, elles refusent avec « routing profile selects no audio ». Cela ne s'applique qu'à une entrée DÉTECTÉE avec zéro piste, et ne peut donc jamais évincer de l'audio que vous envoyez réellement.", + "set.synthesiseSilent": "Synthétiser une piste silencieuse", + "set.syntheticAudio": "Audio synthétique", + "set.syntheticDesc": "Ce qui se passe quand l'entrée transporte de la vidéo et aucun audio.", + "set.tabIngest": "Entrée", + "set.tabPipeline": "Chaîne de traitement", + "set.tabPlatforms": "Identifiants de plateforme", + "set.tabSecurity": "Sécurité", + "set.title": "Paramètres", + "set.tlsAcme": "Let's Encrypt", + "set.tlsAuto": "Automatique", + "set.tlsDesc": "Se configure dans config.yaml, pas ici — TLS doit être correct avant que le serveur ne se mette à écouter. Cette carte rapporte le résultat obtenu.", + "set.tlsManual": "Certificat manuel", + "set.tlsOff": "Non terminé ici", + "set.tlsSelfsigned": "Auto-signé", + "set.tokenHashNote": "polyemesis ne stocke qu'une empreinte. Si vous le perdez, révoquez ce jeton et créez-en un autre.", + "set.tokenNamePlaceholder": "prometheus", + "set.tokenRevoked": "{name} révoqué.", + "set.topicPrefix": "Préfixe de sujet", + "set.topicPrefixNote": "Les séparateurs du préfixe sont conservés, home/av signifie donc deux niveaux. Un préfixe commençant par $ est refusé : les courtiers les réservent, et un abonné utilisant # ne les reçoit jamais — la télémétrie serait publiée avec succès et invisible précisément dans la vue avec laquelle vous chercheriez la panne.", + "set.transportSecurity": "Sécurité du transport", + "set.updateInterval": "Intervalle de rafraîchissement (ms)", + "set.username": "Nom d'utilisateur", + "set.validFor": "Valable pour", + "set.whatPlatformsDo": "Ce que chaque plateforme sait faire", + "set.whyDevApp": "Pourquoi il vous faut votre propre application de développeur", + "set.whyDevAppDesc": "polyemesis ne peut pas livrer de secrets client OAuth — quiconque possède le binaire les aurait, et les plateformes les révoqueraient. Vous enregistrez une fois une application de développeur gratuite, collez ici l'identifiant et le secret client, et polyemesis récupère alors vos clés de flux pour vous.", + "set.writeLogs": "Écrire les journaux sur le disque", + "set.yes": "oui", + "sources.addDescription": "Un second programme avec sa propre entrée, ses destinations et ses versions — un canevas vertical à côté d'un horizontal, par exemple. Il démarre sur les ports par défaut, que vous voudrez modifier pour que les deux n'entrent pas en conflit.", + "sources.addFailed": "Impossible d'ajouter la source", + "sources.addSource": "Ajouter une source", + "sources.addTitle": "Ajouter une source", + "sources.added": "« {name} » ajoutée. Définissez ses ports d'entrée ci-dessous.", + "sources.app": "App", + "sources.applyAndDrop": "Appliquer et couper le flux", + "sources.copied": "{what} copié", + "sources.copyFailed": "Impossible de copier {what}", + "sources.copyTokenAria": "Copier le jeton", + "sources.copyUrlAria": "Copier l'URL {proto}", + "sources.default": "par défaut", + "sources.defaultDescription": "Les requêtes qui ne nomment aucune source agissent sur celle-ci.", + "sources.deleteAria": "Supprimer {name}", + "sources.deleteConfirm": "Supprimer la source", + "sources.deleteDescription": "Ses destinations et ses versions disparaissent avec elle : elles décrivent où va ce programme et n'ont aucun sens sans lui. Les enregistrements sont conservés : les fichiers restent sur le disque et restent lisibles, ils cessent simplement d'être rattachés à une source.", + "sources.deleteFailed": "Impossible de supprimer la source", + "sources.deleteThis": "Supprimer cette source", + "sources.deleteTitle": "Supprimer « {name} » ?", + "sources.deleted": "« {name} » et ses destinations supprimées", + "sources.destinations": "Destinations", + "sources.dirtyIdle": "Modifications d'entrée non enregistrées. Appliquer redémarre l'entrée de cette source.", + "sources.dirtyPublishing": "Un encodeur publie sur cette source. Appliquer redémarre son entrée et déconnecte tous les spectateurs.", + "sources.enableAria": "Activer {name}", + "sources.help.app": "Le chemin d'application RTMP — la partie de l'URL entre l'hôte et la clé de flux. La plupart des encodeurs attendent « live ». Ne le modifiez que si vous avez une raison de le faire.", + "sources.help.ingest": "Comment votre encodeur atteint cette source. SRT résiste à une connexion avec pertes en retransmettant ce qui manque, et constitue le bon choix par défaut sur l'internet public. RTMP est plus ancien et universellement pris en charge, mais il se dégrade fortement sur une liaison instable. Avec la récupération, polyemesis va chercher le flux à une URL que vous fournissez au lieu d'attendre qu'on lui publie quelque chose.", + "sources.help.latencyMs": "Combien de temps SRT attendra un paquet tardif ou retransmis avant d'y renoncer. Des valeurs élevées absorbent de moins bonnes connexions au prix d'un délai : environ 2,5 fois votre temps d'aller-retour est un bon point de départ, donc 40 ms de RTT appellent environ 100 ms ici. Trop bas, et un bref accroc devient un défaut visible.", + "sources.help.passphrase": "Chiffre la connexion SRT, et c'est aujourd'hui ce qui empêche réellement les inconnus d'accéder à cette entrée. Entre 10 et 79 caractères. Laissez le champ vide et quiconque atteint le port peut y publier.", + "sources.help.streamKey": "Le secret qu'un encodeur envoie pour publier en RTMP. C'est l'identifiant qui protège cette entrée : traitez-le comme un mot de passe, car quiconque le détient peut diffuser vers vos destinations.", + "sources.help.token": "Identifie cette source lorsque plusieurs partagent un même port d'entrée. Qu'il contrôle aussi l'accès dépend de votre mode d'entrée — la note en dessous indique ce qui est vrai actuellement. Le renouvellement émet un nouveau jeton et garde l'ancien valable cinq minutes afin qu'un encodeur en direct ne soit pas coupé en pleine diffusion.", + "sources.ingest": "Entrée", + "sources.lastSourceTitle": "La dernière source ne peut pas être supprimée — une installation a besoin d'une entrée", + "sources.latencyMs": "Latence (ms)", + "sources.loadFailed": "Impossible de charger les sources", + "sources.loading": "Chargement des sources…", + "sources.loss": "perte", + "sources.modePull": "Récupération", + "sources.name": "Nom", + "sources.namePlaceholder": "Vertical", + "sources.noEngine": "Aucun moteur ne fonctionne pour cette source. La cause habituelle est un port d'entrée déjà utilisé par une autre source ou un autre processus.", + "sources.notRunning": "arrêtée", + "sources.ownDescription": "Ses propres destinations, versions et enregistrements.", + "sources.passphrase": "Phrase secrète", + "sources.passphrasePlaceholder": "10–79 caractères", + "sources.publishing": "publication", + "sources.renditions": "Versions", + "sources.retrans": "retransm.", + "sources.rotate": "Renouveler", + "sources.rotateConfirm": "Renouveler le jeton", + "sources.rotateFailed": "Impossible de renouveler le jeton", + "sources.rotateIdle": "Un nouveau jeton est émis immédiatement. L'ancien reste valable cinq minutes afin qu'un encodeur qui l'utilise déjà ne soit pas coupé.", + "sources.rotateTitle": "Renouveler le jeton de « {name} » ?", + "sources.rotateWhilePublishing": "Un encodeur publie sur cette source en ce moment. L'ancien jeton reste valable cinq minutes, la connexion ne sera donc pas coupée — mais il doit être mis à jour avant la fin de ce délai.", + "sources.rtt": "RTT", + "sources.running": "en cours", + "sources.saveFailed": "Impossible d'enregistrer", + "sources.streamKey": "Clé de flux", + "sources.subtitle": "Un programme entrant chacune. Les destinations et les versions appartiennent à une source.", + "sources.title": "Sources", + "sources.token": "jeton", + "sources.tokenEnforcedDetailRtmp": "C'est la clé de flux : publiez vers l'URL ci-dessus en l'utilisant comme clé. La rotation en émet un nouveau et conserve l'ancien pendant cinq minutes, pour changer sans couper le direct.", + "sources.tokenEnforcedDetailSrt": "Placez-le dans le streamid SRT de votre encodeur pour publier vers cette source sur le port partagé. La rotation en émet un nouveau et conserve l'ancien pendant cinq minutes, pour changer sans couper le direct.", + "sources.tokenIsCredential": "Ce jeton est l'identifiant d'accès.", + "sources.tokenIssued": "Nouveau jeton émis", + "sources.tokenNotEnforced": "Ce jeton est enregistré mais n'est pas appliqué actuellement.", + "sources.tokenNotEnforcedRtmp": "Les sources sont séparées par port. Ce qui protège réellement cette entrée, c'est la clé de flux ci-dessus. Activez l'entrée à port unique dans les Paramètres pour que le jeton devienne l'identifiant d'accès.", + "sources.tokenNotEnforcedSrt": "Les sources sont séparées par port. Ce qui protège réellement cette entrée, c'est la phrase secrète SRT ci-dessus. Activez l'entrée à port unique dans les Paramètres pour que le jeton devienne l'identifiant d'accès.", + "state.disabled": "Désactivé", + "state.failed": "Échec", + "state.live": "En direct", + "state.offline": "Hors ligne", + "state.reconnecting": "Tentative de reconnexion", + "state.starting": "Démarrage", + "state.stopped": "Arrêté" } diff --git a/ui/src/lib/i18n/id.json b/ui/src/lib/i18n/id.json index 7ef85242..c3ec2208 100644 --- a/ui/src/lib/i18n/id.json +++ b/ui/src/lib/i18n/id.json @@ -1,137 +1,1149 @@ { - "nav.dashboard": "Dasbor", - "nav.sources": "Sumber", - "nav.meters": "Meter audio", - "nav.routing": "Routing", - "nav.renditions": "Versi", - "nav.playout": "Playout", - "nav.library": "Perpustakaan", - "nav.clips": "Klip", - "nav.chat": "Obrolan", - "nav.jobs": "Tugas", - "nav.automation": "Otomatisasi", - "nav.recordings": "Rekaman", - "nav.monitoring": "Pemantauan", - "nav.settings": "Pengaturan", - "chrome.toggleNav": "Beralih navigasi", + "auth.accountCreated": "Akun administrator dibuat.", + "auth.confirmPassword": "Konfirmasi kata sandi", + "auth.createAccount": "Buat akun", + "auth.firstRun": "Penyiapan pertama", + "auth.firstRunDesc": "Pilih kredensial administrator untuk server ini. Tidak ada pemulihan akun, jadi simpan di tempat yang aman.", + "auth.password": "Kata sandi", + "auth.passwordMismatch": "Kata sandi tidak cocok.", + "auth.signIn": "Masuk", + "auth.signInDesc": "Masukkan kredensial administrator Anda.", + "auth.somethingWentWrong": "Terjadi kesalahan.", + "auth.tokenCreated": "Token API dibuat", + "auth.tokenRevoked": "Token API dicabut", + "auth.username": "Nama pengguna", + "auto.action": "Aksi", + "auto.adminPasswordChanged": "Kata sandi administrator diubah", + "auto.chosenWeekdays": "Hari terpilih", + "auto.coalesced": "Digabungkan", + "auto.coalescingWindow": "Jendela penggabungan (detik)", + "auto.couldNotChangeTheRule": "Tidak dapat mengubah aturan.", + "auto.couldNotChangeTheSchedule": "Tidak dapat mengubah jadwal.", + "auto.couldNotDeleteTheRule": "Tidak dapat menghapus aturan.", + "auto.couldNotDeleteTheSchedule": "Tidak dapat menghapus jadwal.", + "auto.couldNotLoadAutomationSettings": "Tidak dapat memuat pengaturan otomatisasi.", + "auto.couldNotSaveTheRule": "Tidak dapat menyimpan aturan.", + "auto.couldNotSaveTheSchedule": "Tidak dapat menyimpan jadwal.", + "auto.createRule": "Buat aturan", + "auto.createSchedule": "Buat jadwal", + "auto.days": "Hari", + "auto.delete": "Hapus", + "auto.deleteRule": "Hapus aturan", + "auto.deleteRuleDesc": "Aturan berhenti dipicu. Endpoint webhook-nya tidak diubah, dan Anda bisa membuat ulang aturan itu.", + "auto.deleteRuleTitle": "Hapus aturan peringatan ini?", + "auto.deleteSchedule": "Hapus jadwal", + "auto.deleteScheduleDesc": "Jadwal berhenti berjalan. Apa pun yang sudah dimulainya tidak terpengaruh.", + "auto.deleteScheduleTitle": "Hapus jadwal ini?", + "auto.delivery": "Pengiriman", + "auto.destinationFallingBehindRealtime": "Tujuan tertinggal dari waktu nyata", + "auto.destinationKeepingUpAgain": "Tujuan kembali mengimbangi", + "auto.destinationRecovered": "Tujuan pulih", + "auto.destinationWentDown": "Tujuan terputus", + "auto.destinations": "Tujuan", + "auto.dropped": "Dibuang", + "auto.edit": "Edit", + "auto.editRule": "Edit aturan peringatan", + "auto.editSchedule": "Edit jadwal", + "auto.endpoint": "Endpoint", + "auto.evAudioClipping": "Clipping audio", + "auto.evClipCaptured": "Klip ditangkap", + "auto.evDiskLow": "Disk menipis", + "auto.evDiskRecovered": "Disk pulih", + "auto.evIngestLost": "Masukan hilang", + "auto.evIngestRecovered": "Masukan pulih", + "auto.evSettingsChanged": "Pengaturan diubah", + "auto.evSignedIn": "Berhasil masuk", + "auto.events": "Peristiwa", + "auto.everyDay": "Setiap hari", + "auto.failed": "Gagal", + "auto.failoverSwitchedSource": "Failover mengganti sumber", + "auto.fired": "terpicu", + "auto.firesAt": "Dipicu pada", + "auto.lastDelivery": "Pengiriman terakhir", + "auto.lastSweep": "Sapuan terakhir", + "auto.latenessAllowance": "Toleransi keterlambatan (detik)", + "auto.loudnessBackInCompliance": "Kenyaringan kembali patuh", + "auto.loudnessOutOfCompliance": "Kenyaringan keluar dari kepatuhan", + "auto.minGap": "Jeda minimum antar pesan (detik)", + "auto.minSeverity": "Tingkat keparahan minimum", + "auto.missedNote": "Kejadian yang terlewat karena server mati ditandai sudah ditangani dan dilewati, bukan dipicu terlambat — siaran yang menyalakan dirinya sendiri empat jam terlambat lebih buruk daripada yang tidak pernah dimulai.", + "auto.name": "Nama", + "auto.newRule": "Aturan peringatan baru", + "auto.newSchedule": "Jadwal baru", + "auto.next": "Berikutnya", + "auto.nothingFired": "Belum ada yang terpicu. Penjadwal menyapu setiap 20 detik.", + "auto.once": "Sekali", + "auto.payloadFormat": "Format payload", + "auto.pending": "Menunggu", + "auto.repeatedFailedSignIns": "Kegagalan masuk berulang", + "auto.repeats": "Pengulangan", + "auto.retries": "Percobaan ulang", + "auto.rule": "Aturan", + "auto.ruleCreated": "Aturan peringatan dibuat. Kirimkan sebuah uji coba.", + "auto.ruleDeleted": "Aturan peringatan dihapus.", + "auto.rulePlaceholder": "Discord — peringatan acara", + "auto.ruleSaved": "Aturan peringatan disimpan.", + "auto.saveRule": "Simpan aturan", + "auto.saveSchedule": "Simpan jadwal", + "auto.schedule": "Jadwal", + "auto.scheduleCreated": "Jadwal dibuat.", + "auto.scheduleDeleted": "Jadwal dihapus.", + "auto.schedulePlaceholder": "Acara malam hari kerja", + "auto.scheduleSaved": "Jadwal disimpan.", + "auto.schedules": "Jadwal", + "auto.secretMasked": "Ditampilkan tersamar karena rahasianya berada di dalam path. Biarkan apa adanya untuk mempertahankan endpoint saat ini, atau tempelkan URL baru untuk menggantinya.", + "auto.sendTest": "Kirim peringatan uji", + "auto.sendTestTitle": "Kirim peringatan uji sekarang", + "auto.sent": "Terkirim", + "auto.skipped": "terlewat", + "auto.startDestinations": "Mulai tujuan", + "auto.startPlaylist": "Mulai playlist", + "auto.stopDestinations": "Hentikan tujuan", + "auto.stopPlaylist": "Hentikan playlist", + "auto.subscribedTo": "Berlangganan", + "auto.subtitle": "Webhook yang memberi tahu Anda ada yang rusak, dan jadwal yang menjalankan acara tanpa Anda.", + "auto.theEndpointDidNotAccept": "Endpoint tidak menerima pesan uji itu.", + "auto.time": "Waktu", + "auto.timeZone": "Zona waktu", + "auto.title": "Otomatisasi", + "auto.tzPlaceholder": "UTC", + "auto.webhookHint": "Discord dan Slack sama-sama menyediakannya di pengaturan kanal.", + "auto.webhookPlaceholder": "https://discord.com/api/webhooks/…", + "auto.webhookRules": "Aturan webhook", + "auto.webhookUrl": "URL webhook", + "auto.weekFri": "Jum", + "auto.weekMon": "Sen", + "auto.weekSat": "Sab", + "auto.weekSun": "Min", + "auto.weekThu": "Kam", + "auto.weekTue": "Sel", + "auto.weekWed": "Rab", + "auto.when": "Kapan", + "chatpage.allFiltered": "Semua platform yang punya pesan tersaring keluar.", + "chatpage.couldNotDeleteThatMessage": "Tidak dapat menghapus pesan itu.", + "chatpage.deleteNote": "Menghapus akan membuang pesan itu di platform yang menerbitkannya. Platform yang tidak bisa dihapus oleh polyemesis akan mengatakannya dan menyebutkan di mana hal itu bisa dilakukan — tindakan ini tidak pernah disembunyikan berdasarkan dugaan.", + "chatpage.droppedTitle": "Ditampilkan langsung, tetapi penyimpanan tertinggal dan riwayat kehilangannya.", + "chatpage.dupTitle": "Pesan yang sama terlihat dua kali — gema kita sendiri, atau platform yang memutarnya ulang.", + "chatpage.noAccount": "Obrolan berjalan, tetapi tidak ada akun platform yang terhubung. Hubungkan satu di Pengaturan → Kredensial platform.", + "chatpage.notRunning": "Tidak ada obrolan yang berjalan di server ini. Riwayat di bawah lini masa adalah apa yang tersimpan sebelumnya, dan kotak kirim tetap nonaktif sampai sebuah platform terhubung.", + "chatpage.received": "Diterima", + "chatpage.reload": "Muat ulang", + "chatpage.stored": "Tersimpan", + "chatpage.storedTitle": "Tidak ada yang terhubung; ini adalah riwayat tersimpan.", + "chatpage.subtitle": "Semua platform dalam satu lini masa, dengan satu kotak kirim.", + "chatpage.timeline": "Lini masa", + "chatpage.title": "Obrolan", "chrome.ingest": "Ingest", + "chrome.language": "Bahasa", "chrome.liveCount": "{count} aktif", + "chrome.signOut": "Keluar", "chrome.socketOffline": "offline", "chrome.socketOfflineHint": "Pembaruan langsung terputus; mencoba lagi", - "chrome.signOut": "Keluar", - "chrome.language": "Bahasa", + "chrome.toggleNav": "Beralih navigasi", + "clipedit.back33": "Mundur 33 ms", + "clipedit.clickLine": "Klik sebuah baris untuk melompat ke sana. Pilih beberapa baris sekaligus dan titik masuk serta titik keluar berpindah ke kata yang Anda sorot.", + "clipedit.clipThisLine": "Klip baris ini", + "clipedit.clipThisLineTitle": "Setel titik masuk dan keluar di sekitar baris ini", + "clipedit.copied": "Disalin", + "clipedit.copiesEveryPacketSecondsBit": "Menyalin setiap paket. Hitungan detik, sama persis bit demi bit, dan awalnya melompat mundur ke keyframe terdekat.", + "clipedit.couldNotOpenThatRecording": "Tidak dapat membuka rekaman itu.", + "clipedit.couldNotQueueTheExport": "Tidak dapat mengantrekan ekspornya.", + "clipedit.cut": "Potongan", + "clipedit.downloadClip": "Unduh klipnya", + "clipedit.exportQueued": "Ekspor diantrekan. Berjalan saat mesin punya sisa tenaga.", + "clipedit.exports": "Ekspor", + "clipedit.fast": "Cepat", + "clipedit.filterTranscript": "Saring transkrip", + "clipedit.findSaid": "Cari apa yang diucapkan", + "clipedit.forward33": "Maju 33 ms", + "clipedit.in": "Masuk", + "clipedit.keepAllTracks": "Pertahankan setiap trek audio", + "clipedit.keepsEveryAudioTrackIn": "Mempertahankan setiap trek audio dalam codec apa pun. Persis yang ditulis perekam.", + "clipedit.markIn": "I — tandai titik masuk di playhead", + "clipedit.markOut": "O — tandai titik keluar di playhead", + "clipedit.name": "Nama", + "clipedit.noKeyframes": "Posisi keyframe tidak dapat dibaca, jadi tidak ada yang bisa memastikan di mana potongan ini benar-benar dimulai. Potongan tetap berfungsi — pencarian FFmpeg sendiri mendarat pada keyframe — tetapi awalnya bisa lebih cepat hingga satu GOP dan halaman ini tidak dapat menunjukkan seberapa jauh.", + "clipedit.noTranscript": "Belum ada yang ditranskrip untuk rekaman ini. Setiap mikrofon direkam pada treknya sendiri, jadi mentranskripnya memberi Anda nama pembicara tanpa model diarisasi — lalu Anda bisa mengeklip berdasarkan apa yang diucapkan.", + "clipedit.nothingExported": "Belum ada yang diekspor.", + "clipedit.out": "Keluar", + "clipedit.pickAtLeastOneAudio": "Pilih setidaknya satu trek audio, atau ambil semuanya.", + "clipedit.playFaster": "L — putar, tekan lagi agar lebih cepat", + "clipedit.precise": "Presisi", + "clipedit.queueNote": "Ekspor diantrekan, bukan dijalankan di sini: ia mengalah pada siaran langsung, jadi potongan yang diambil di tengah siaran menunggu alih-alih merampas tenaga yang dibutuhkan siaran.", + "clipedit.reEncodesOnlyTheLeading": "Hanya mengodekan ulang GOP awal yang tidak utuh sehingga awalnya tepat. Sisanya tetap disalin.", + "clipedit.reencoded": "Dikodekan ulang", + "clipedit.shortcuts": "J / K / L untuk shuttle, I dan O menandai titik masuk dan keluar, tombol panah menggeser 33 ms dan shift-panah satu detik. Langkah halusnya adalah satu bingkai pada 30 fps; laju sebenarnya rekaman ini tidak terindeks, jadi dinamai dalam milidetik alih-alih ditebak.", + "clipedit.shuttleBack": "J — shuttle mundur", + "clipedit.startMoved": "Pergeseran awal", + "clipedit.starts": "Mulai", + "clipedit.stop": "K — berhenti", + "clipedit.thatCutCouldNotBe": "Potongan itu tidak dapat direncanakan.", + "clipedit.thatIsNotARecording": "Itu bukan id rekaman.", + "clipedit.thatRecordingIsNotAvailable": "Rekaman itu tidak tersedia.", + "clipedit.title": "Editor klip", + "clipedit.whatASocialPlatformWill": "Yang akan diterima platform sosial. Lebih sedikit codec yang selamat melewatinya.", + "clipedit.zoomIn": "Perbesar", + "clipedit.zoomOut": "Perkecil", + "clips.bitrate": "Bitrate", + "clips.buffer": "Penyangga", + "clips.bufferLiveNote": "Sakelar penyangga adalah tombol langsung, bukan pengaturan tersimpan: setelah dimulai ulang ia kembali nonaktif.", + "clips.captured": "Ditangkap", + "clips.ceiling": "Batas atas", + "clips.clipsKept": "Klip disimpan", + "clips.couldNotCaptureAClip": "Tidak dapat menangkap klip.", + "clips.couldNotChangeTheClip": "Tidak dapat mengubah penyangga klip.", + "clips.couldNotDeleteTheClip": "Tidak dapat menghapus klip.", + "clips.couldNotLoadClips": "Tidak dapat memuat klip.", + "clips.covers": "Mencakup", + "clips.customLength": "Durasi klip khusus dalam detik", + "clips.delete": "Hapus", + "clips.deleteDescription": "Klip biasanya justru materi yang ingin Anda simpan. Ini menghapus berkasnya; rekaman asalnya tidak tersentuh.", + "clips.deleteTitle": "Hapus klip ini?", + "clips.deleted": "Klip dihapus.", + "clips.download": "Unduh", + "clips.everythingHeld": "Semua yang sedang ditahan penyangga", + "clips.file": "Berkas", + "clips.held": "Ditahan", + "clips.inMemory": "Di memori", + "clips.keepBuffer": "Pertahankan penyangga bergulir", + "clips.keyframes": "Bingkai kunci", + "clips.length": "Durasi", + "clips.none": "tidak ada", + "clips.off": "mati", + "clips.onDisk": "Di disk", + "clips.seen": "terlihat", + "clips.settings": "Pengaturan", + "clips.size": "Ukuran", + "clips.subtitle": "Penyangga bergulir dari apa yang baru saja tayang. Tangkap setelah kejadian — tidak ada yang perlu disiapkan lebih dulu.", + "clips.theClipDoesNotStart": "Klip itu tidak dimulai pada bingkai kunci.", + "clips.title": "Klip", + "clips.waitingForStream": "menunggu siaran", + "clips.window": "Jendela", + "clips.windowSeconds": "Jendela (detik)", "common.add": "Tambah", - "common.cancel": "Batalkan", + "common.apply": "Terapkan", + "common.cancel": "Batal", "common.close": "Tutup", "common.confirm": "Konfirmasi", "common.copied": "Disalin", "common.copy": "Salin", "common.delete": "Hapus", "common.disabled": "Dinonaktifkan", + "common.discard": "Buang", "common.edit": "Edit", "common.enabled": "Diaktifkan", "common.loading": "Memuat…", + "common.moreInfo": "Informasi selengkapnya", "common.refresh": "Segarkan", "common.restart": "Mulai Ulang", "common.retry": "Coba Lagi", "common.save": "Simpan", "common.start": "Mulai", "common.stop": "Berhenti", - "state.live": "Langsung", - "state.reconnecting": "Menghubungkan Kembali", - "state.starting": "Memulai", - "state.failed": "Gagal", - "state.stopped": "Dihentikan", - "state.offline": "Luring", - "hooks.title": "Webhook siklus hidup", - "hooks.new": "Webhook baru", - "hooks.empty": "Belum ada webhook. Tambahkan satu dan polyemesis akan mem-POST body JSON bertanda tangan begitu stream mulai atau berhenti, atau sebuah tujuan naik atau turun — satu pengiriman per transisi, berurutan, agar bisa ditindaklanjuti oleh skrip.", - "hooks.colWebhook": "Webhook", - "hooks.colEndpoint": "Endpoint", - "hooks.colTriggers": "Berlangganan", - "hooks.colEnabled": "Aktif", - "hooks.signed": "bertanda tangan", - "hooks.unsigned": "TANPA TANDA TANGAN", + "dash.audioTracks": "Trek audio", + "dash.bitrate": "Bitrate", + "dash.broadcastSettings": "Pengaturan siaran", + "dash.categoryPlaceholder": "Musik", + "dash.clipboardUnavailable": "Papan klip tidak tersedia pada origin ini.", + "dash.connectPrompt": "Hubungkan akun YouTube, Twitch, Facebook, atau Kick di Pengaturan → Kredensial platform untuk menyetel judul, deskripsi, dan kategori siaran Anda di semua platform sekaligus. Masing-masing menerima bagian yang berbeda — penyusunnya menyebutkan yang mana, dan mengirim apa yang bisa.", + "dash.copyIngestUrl": "Salin URL masukan", + "dash.couldNotLoadTheDestination": "Tidak dapat memuat tujuan.", + "dash.couldNotPushTheMetadata": "Tidak dapat mengirim metadata.", + "dash.couldNotReorderTheDestinations": "Tidak dapat mengurutkan ulang tujuan.", + "dash.deleteDest": "Hapus tujuan", + "dash.deleteDestDescription": "Ini menghapus tujuan beserta profil routing-nya. Platformnya tidak diubah, dan Anda bisa menambahkannya kembali — tetapi routing audio yang Anda siapkan untuknya hilang.", + "dash.descriptionPlaceholder": "Tentang apa siaran ini.", + "dash.destDeleted": "Tujuan dihapus.", + "dash.disabled": "dinonaktifkan", + "dash.everyConnectedAccount": "setiap akun yang terhubung", + "dash.idle": "diam", + "dash.ingestUrlCopied": "URL masukan disalin.", + "dash.keyRefreshed": "Kunci siaran disegarkan dari platform.", + "dash.leaveUnchanged": "Biarkan tidak berubah", + "dash.leaveUnchangedNote": "“Biarkan tidak berubah” sama sekali tidak mengirim apa pun, jadi platform mempertahankan nilai yang sudah ada. Memilih Aktif atau Mati akan menuliskan nilai itu.", + "dash.metaCategory": "Kategori", + "dash.metaDescription": "Deskripsi", + "dash.metaTags": "Tag", + "dash.metaTitle": "Judul", + "dash.noDestinations": "Belum ada tujuan. Tambahkan satu, lalu pilih trek audio mana yang diterimanya.", + "dash.off": "Mati", + "dash.pipeline": "Pipeline", + "dash.pushToPlatforms": "Kirim ke platform", + "dash.pushing": "Mengirim…", + "dash.reconnects": "Penyambungan ulang", + "dash.relaySubscribers": "Pelanggan relai", + "dash.renditions": "Varian", + "dash.scheduledStart": "Mulai terjadwal", + "dash.subtitle": "Sekali masuk, disebar dengan video bersama dan audio per tujuan.", + "dash.tagsPlaceholder": "live, house, dj set", + "dash.theConnectedAccount": "akun yang terhubung", + "dash.title": "Panel", + "dash.titlePlaceholder": "Set Jumat malam", + "dash.uptime": "Waktu aktif", + "dash.youtubeStopsAcceptingTheseOnce": "YouTube berhenti menerima ini begitu sebuah siaran keluar dari “created” atau “ready”.", + "dest.audience": "Audiens", + "dest.audioBitrate": "Bitrate audio", + "dest.audioCodec": "Codec audio", + "dest.channels": "Kanal", + "dest.compliance": "Kepatuhan", + "dest.connectedAccount": "Akun terhubung", + "dest.contentLabels": "Label konten", + "dest.copySource": "Salin video sumber", + "dest.createVariant": "Buat varian ini", + "dest.customiseSummary": "Sesuaikan untuk tujuan ini", + "dest.fbBackupCost": "Menggandakan bandwidth unggah tujuan ini.", + "dest.fbBackupHelp": "Mengirim salinan kedua dari siaran ini ke ingest cadangan Facebook, sehingga koneksi yang putus tidak menjatuhkan siarannya.", + "dest.fbBackupLabel": "Terbitkan umpan cadangan", + "dest.fbBackupReconnect": "Mengaktifkannya menyambungkan ulang siaran satu kali — jadi aktifkan sebelum Anda mulai siaran langsung.", + "dest.fbBoxIntro": "Dikirim pada panggilan pembuatan yang sama dengan Audiens, jadi aturan yang sama berlaku: diterapkan sekali saat siaran dimulai, dan dibiarkan sepenuhnya jika kosong.", + "dest.fbBoxTitle": "Lintas-posting dan tombol donasi Facebook", + "dest.fbCrosspostAdd": "Tambah Halaman", + "dest.fbCrosspostAlsoPost": "Posting juga", + "dest.fbCrosspostHelp": "ID Halaman numerik dari konsol Facebook sendiri (pengaturan Halaman, atau graph.facebook.com/me/accounts saat masuk sebagai Halaman) — tidak ada pencarian di sini, jadi tempelkan id-nya, bukan nama atau URL. “Posting juga” menerbitkan sebagai Halaman tersebut alih-alih hanya membagikan siaran kepadanya; jika dimatikan, hanya pembagian yang lebih senyap yang terjadi.", + "dest.fbCrosspostLabel": "Lintas-posting ke Halaman", + "dest.fbCrosspostPageId": "ID Halaman", + "dest.fbCrosspostRemove": "Hapus Halaman ini", + "dest.fbDonateHelp": "Ini juga id buram dari konsol penggalangan dana Facebook, bukan nama lembaga amal untuk dicari. Kosongkan agar tidak ada tombol donasi yang dilampirkan sama sekali.", + "dest.fbDonateLabel": "ID lembaga amal tombol donasi", + "dest.fbDonatePlaceholder": "ID lembaga amal", + "dest.fillFromPlatform": "Isi dengan yang dipublikasikan platform ini", + "dest.friends": "Teman", + "dest.friendsOfFriends": "Teman dari teman", + "dest.giveUpAfter": "Menyerah setelah (percobaan)", + "dest.leaveOnFacebook": "Biarkan seperti di Facebook", + "dest.leaveOnYouTube": "Biarkan seperti di YouTube", + "dest.madeForKids": "Dibuat untuk anak-anak (COPPA)", + "dest.muxQueue": "Antrean muxing (paket)", + "dest.muxQueuePair": "Keduanya sepasang: FFmpeg menerapkan batas paket hanya setelah antrean melewati ambang byte, jadi ambang saja tidak melakukan apa pun dan akan ditolak. Naikkan keduanya jika sebuah tujuan melaporkan galat interleave — jalur audio di sini memiliki latensi yang bervariasi, karena normalisasi kenyaringan membaca lebih dulu.", + "dest.name": "Nama", + "dest.namePlaceholder": "YouTube — kanal utama", + "dest.notLinked": "Tidak tertaut", + "dest.onlyMe": "Hanya saya", + "dest.platform": "Platform", + "dest.private": "Pribadi", + "dest.public": "Publik", + "dest.reconnecting": "Menyambung ulang", + "dest.retryAfter": "Coba lagi setelah (detik)", + "dest.sharedEncodeAria": "Enkode video bersama", + "dest.socketTimeout": "Batas waktu soket (detik)", + "dest.streamKeyLabel": "Kunci stream", + "dest.transport": "Transport", + "dest.transportFile": "Berkas lokal", + "dest.transportRtmp": "RTMP / RTMPS", + "dest.unlisted": "Tidak terdaftar", + "dest.urlFromDashboard": "URL dari dasbor", + "dest.useSharedEncode": "Gunakan enkode video bersama", + "dest.varBitrate": "Bitrate video (kbps)", + "dest.varFps": "Laju bingkai", + "dest.varHeight": "Tinggi", + "dest.varSourceHint": "sumber", + "dest.varWidth": "Lebar", + "dest.variantWarning": "Memulai enkode kedua dari sumber Anda, berdasarkan “{name}”. Ini bukan variasi gratis dan tidak mengubah apa yang diterima tujuan lain pada enkode tersebut. Kosongkan sebuah kolom untuk mempertahankan nilai sumber.", + "dest.videoTreatment": "Perlakuan video", + "dest.visibility": "Visibilitas", + "hooks.allTriggers": "semuanya", "hooks.attempts": "{count} percobaan", + "hooks.attemptsLabel": "Percobaan", "hooks.attemptsOne": "{count} percobaan", - "hooks.timeout": "batas waktu {seconds}d", - "hooks.allTriggers": "semuanya", - "hooks.triggerCount": "{count} pemicu", - "hooks.triggerCountOne": "{count} pemicu", - "hooks.enableAria": "Aktifkan {name}", + "hooks.colEnabled": "Aktif", + "hooks.colEndpoint": "Endpoint", + "hooks.colTriggers": "Berlangganan", + "hooks.colWebhook": "Webhook", + "hooks.create": "Buat", + "hooks.created": "Webhook dibuat.", + "hooks.deleteBody": "Pengiriman berhenti seketika. Endpoint tidak tersentuh dan hook bisa dibuat ulang — tetapi kunci penanda tangannya tidak dapat dipulihkan.", + "hooks.deleteConfirm": "Hapus webhook", + "hooks.deleteFailed": "Tidak dapat menghapus hook.", + "hooks.deleteTitle": "Hapus webhook ini?", + "hooks.deleted": "Webhook dihapus.", "hooks.deliveries": "Pengiriman terbaru", - "hooks.sendTest": "Kirim pengiriman uji", - "hooks.sendTestTitle": "Kirim pengiriman uji sekarang", - "hooks.secretTitle": "Kunci penanda tangan — salin sekarang, kunci ini tidak bisa ditampilkan lagi", - "hooks.secretCopied": "Sudah saya salin", - "hooks.httpStatus": "HTTP {status}", - "hooks.noResponse": "tanpa respons", + "hooks.deliveriesFailed": "Tidak dapat membaca pengiriman terbaru.", + "hooks.deliveryTitle": "Pengiriman", + "hooks.dialogIntro": "Satu POST bertanda tangan per transisi, berurutan. Tidak seperti peringatan, tidak ada yang digabung atau ditahan — skrip menerima setiap perubahan.", "hooks.durationMs": "{ms} md", + "hooks.editTitle": "Edit webhook", + "hooks.empty": "Belum ada webhook. Tambahkan satu dan polyemesis akan mem-POST body JSON bertanda tangan begitu stream mulai atau berhenti, atau sebuah tujuan naik atau turun — satu pengiriman per transisi, berurutan, agar bisa ditindaklanjuti oleh skrip.", + "hooks.enableAria": "Aktifkan {name}", + "hooks.httpStatus": "HTTP {status}", + "hooks.loadFailed": "Tidak dapat memuat webhook.", + "hooks.name": "Nama", + "hooks.new": "Webhook baru", + "hooks.newTitle": "Webhook baru", "hooks.noDeliveries": "Belum ada yang dikirim. Pengiriman muncul di sini seiring terjadinya transisi.", - "hooks.deliveryTitle": "Pengiriman", - "hooks.statSent": "Terkirim", - "hooks.statFailed": "Gagal", + "hooks.noResponse": "tanpa respons", + "hooks.payloadNote": "Payload {version}. Tanda tangan di {header}, atas timestamp dan body.", + "hooks.retriesHint": "Percobaan ulang memblokir antrean milik endpoint ini sendiri, karena urutan adalah janjinya. 4xx tidak pernah diulang — endpoint yang bilang permintaannya salah akan bilang begitu lagi.", + "hooks.saveFailed": "Tidak dapat menyimpan hook.", + "hooks.secretCopied": "Sudah saya salin", + "hooks.secretTitle": "Kunci penanda tangan — salin sekarang, kunci ini tidak bisa ditampilkan lagi", + "hooks.sendTest": "Kirim pengiriman uji", + "hooks.sendTestTitle": "Kirim pengiriman uji sekarang", + "hooks.signed": "bertanda tangan", + "hooks.statDropped": "Dibuang", "hooks.statEndpoints": "Endpoint", + "hooks.statFailed": "Gagal", "hooks.statQueued": "Antre", - "hooks.statDropped": "Dibuang", "hooks.statRetries": "Percobaan ulang", - "hooks.payloadNote": "Payload {version}. Tanda tangan di {header}, atas timestamp dan body.", - "hooks.deleteTitle": "Hapus webhook ini?", - "hooks.deleteBody": "Pengiriman berhenti seketika. Endpoint tidak tersentuh dan hook bisa dibuat ulang — tetapi kunci penanda tangannya tidak dapat dipulihkan.", - "hooks.deleteConfirm": "Hapus webhook", - "hooks.editTitle": "Edit webhook", - "hooks.newTitle": "Webhook baru", - "hooks.dialogIntro": "Satu POST bertanda tangan per transisi, berurutan. Tidak seperti peringatan, tidak ada yang digabung atau ditahan — skrip menerima setiap perubahan.", - "hooks.name": "Nama", - "hooks.url": "URL endpoint", - "hooks.urlMasked": "URL yang tersimpan tidak pernah ditampilkan utuh. Biarkan nilai tersamar untuk mempertahankannya, atau ketik URL baru untuk menggantinya.", - "hooks.triggers": "Pemicu", - "hooks.triggersNoneHint": "Tidak memilih apa pun berarti semua pemicu, termasuk yang ditambahkan rilis mendatang.", + "hooks.statSent": "Terkirim", + "hooks.testFailed": "Endpoint tidak menerima pengiriman uji.", + "hooks.timeout": "batas waktu {seconds}d", "hooks.timeoutLabel": "Batas waktu (d)", - "hooks.attemptsLabel": "Percobaan", - "hooks.retriesHint": "Percobaan ulang memblokir antrean milik endpoint ini sendiri, karena urutan adalah janjinya. 4xx tidak pernah diulang — endpoint yang bilang permintaannya salah akan bilang begitu lagi.", - "hooks.create": "Buat", - "hooks.triggerIngestPublished": "Stream dimulai", - "hooks.triggerIngestDisconnected": "Stream berhenti", - "hooks.triggerDestinationUp": "Tujuan live", - "hooks.triggerDestinationDown": "Tujuan berhenti", - "hooks.loadFailed": "Tidak dapat memuat webhook.", + "hooks.title": "Webhook siklus hidup", "hooks.toggleFailed": "Tidak dapat mengubah hook.", - "hooks.deleted": "Webhook dihapus.", - "hooks.deleteFailed": "Tidak dapat menghapus hook.", - "hooks.testFailed": "Endpoint tidak menerima pengiriman uji.", - "hooks.deliveriesFailed": "Tidak dapat membaca pengiriman terbaru.", + "hooks.triggerCount": "{count} pemicu", + "hooks.triggerCountOne": "{count} pemicu", + "hooks.triggerDestinationDown": "Tujuan berhenti", + "hooks.triggerDestinationUp": "Tujuan live", + "hooks.triggerIngestDisconnected": "Stream berhenti", + "hooks.triggerIngestPublished": "Stream dimulai", + "hooks.triggers": "Pemicu", + "hooks.triggersNoneHint": "Tidak memilih apa pun berarti semua pemicu, termasuk yang ditambahkan rilis mendatang.", + "hooks.unsigned": "TANPA TANDA TANGAN", "hooks.updated": "Webhook diperbarui.", - "hooks.created": "Webhook dibuat.", - "hooks.saveFailed": "Tidak dapat menyimpan hook.", - "playlist.title": "Playlist", + "hooks.url": "URL endpoint", + "hooks.urlMasked": "URL yang tersimpan tidak pernah ditampilkan utuh. Biarkan nilai tersamar untuk mempertahankannya, atau ketik URL baru untuk menggantinya.", + "jobs.alwaysKeepNewest": "Selalu simpan yang terbaru", + "jobs.automaticModel": "Otomatis (dipilih dari perangkat keras ini)", + "jobs.backToDefault": "Kembali ke mode bawaan untuk jenis ini", + "jobs.batteryFloor": "Batas bawah baterai %", + "jobs.broadcastGoing": "Sedang ada siaran; pekerjaan yang ditunda mengalah padanya.", + "jobs.cancel": "Batal", + "jobs.cannotPause": "Jenis ini seharusnya dijeda tetapi tidak bisa, jadi dibiarkan selesai.", + "jobs.completed": "Selesai", + "jobs.concurrency": "Konkurensi", + "jobs.couldNotReadTheJob": "Tidak dapat membaca antrean tugas.", + "jobs.couldNotSaveThePolicy": "Tidak dapat menyimpan kebijakan.", + "jobs.cpuCeiling": "Langit-langit CPU %", + "jobs.cpuResume": "CPU lanjut kembali %", + "jobs.defaultMode": "Mode bawaan", + "jobs.defaultModel": "Model bawaan", + "jobs.deferred": "Ditunda", + "jobs.failed": "Gagal", + "jobs.finished": "Rampung", + "jobs.global": "Global", + "jobs.gpuGateNote": "Hanya jenis yang ditandai memakai GPU yang dibatasi oleh ini.", + "jobs.history": "Riwayat", + "jobs.historyRetention": "Penyimpanan riwayat", + "jobs.holdDeferredWorkBackOn": "Tahan pekerjaan yang ditunda pada laptop yang sedang memakai baterai di bawah nilai ini. 0 menonaktifkannya.", + "jobs.hostCpuLoad": "Beban CPU host", + "jobs.howManyJobsMayRun": "Berapa banyak tugas yang boleh berjalan sekaligus. Satu, karena transcode kedua membeli throughput yang tidak diminta siapa pun dengan risiko yang tidak disetujui siapa pun.", + "jobs.inQueue": "Di antrean", + "jobs.job": "Tugas", + "jobs.keepForDays": "Simpan berapa hari", + "jobs.machineSafety": "Keamanan mesin. Ini satu-satunya gerbang yang dipatuhi pekerjaan realtime juga.", + "jobs.niceLevel": "Level nice", + "jobs.noFinished": "Belum ada tugas yang rampung.", + "jobs.noModels": "Tidak ada berkas model yang ditemukan di direktori model. Mode otomatis akan mengunduh model yang dipilihnya; menyebut model tertentu di sini baru berfungsi setelah berkas itu ada.", + "jobs.nothingNewStartsAboveThis": "Tidak ada yang baru dimulai di atas nilai ini. 0 menonaktifkan gerbangnya.", + "jobs.nothingQueued": "Tidak ada yang diantrekan. Pascaproduksi dikirim dari perpustakaan.", + "jobs.nothingWasOldEnoughTo": "Tidak ada yang cukup tua untuk dibersihkan. Turunkan dulu masa penyimpanan di Kebijakan.", + "jobs.pauseAll": "Jeda semua", + "jobs.policy": "Kebijakan", + "jobs.policySaved": "Kebijakan sumber daya disimpan.", + "jobs.progress": "Kemajuan", + "jobs.queuePaused": "Antrean dijeda.", + "jobs.queueResumed": "Antrean dilanjutkan.", + "jobs.queued": "Antre", + "jobs.recording": "Rekaman", + "jobs.releasedItWillStartAs": "Dilepaskan. Akan dimulai begitu ada slot kosong.", + "jobs.removeFromHistory": "Hapus dari riwayat", + "jobs.removeWindow": "Hapus jendela", + "jobs.resumeAll": "Lanjutkan semua", + "jobs.retry": "Coba lagi", + "jobs.retryTitle": "Siapkan ulang dengan jatah percobaan baru", + "jobs.runNow": "Jalankan sekarang", + "jobs.runNowTitle": "Jalankan yang ini sekarang, abaikan kebijakan sumber daya", + "jobs.running": "Berjalan", + "jobs.savedTheConcurrencyChangeTakes": "Disimpan. Perubahan konkurensi baru berlaku setelah server dimulai ulang — antrean menetapkan nilai itu saat ia mulai.", + "jobs.state": "Status", + "jobs.stopsEverythingRealtimeIncludedA": "Menghentikan semuanya, termasuk pekerjaan realtime: CPU yang sedang throttling sudah mulai menurunkan mutu siarannya. 0 menonaktifkannya.", + "jobs.subtitle": "Pekerjaan latar: transkripsi, proxy, gambar mini, dan kompresi arsip.", + "jobs.thatDidNotWork": "Itu tidak berhasil.", + "jobs.thermalCeiling": "Langit-langit suhu °C", + "jobs.thisManyFinishedJobsSurvive": "Sebanyak ini tugas yang rampung tetap disimpan berapa pun usianya.", + "jobs.title": "Tugas", + "jobs.transcription": "Transkripsi", + "jobs.unsavedChanges": "Ada perubahan yang belum disimpan.", + "jobs.whereTheCeilingReleasesThe": "Di titik mana langit-langitnya melepas. Selisihnya adalah histeresis yang mencegah gerbangnya berayun-ayun.", + "jobs.whisperMissing": "whisper.cpp tidak terpasang di sini, jadi ini belum berpengaruh. Tetap layak disetel: pilihannya berlaku begitu whisper.cpp terpasang.", + "jobs.why": "Alasan", + "jobs.windowEnd": "Akhir jendela", + "jobs.windowStart": "Awal jendela", + "jobs.yieldNote": "Pekerjaan berat diantrekan dan mengalah pada siaran langsung. Bingkai yang jatuh tidak bisa dipulihkan; transkrip yang datang sejam kemudian tidak merugikan apa pun.", + "lib.anySpeaker": "Pembicara mana pun", + "lib.anyTag": "Tag mana pun", + "lib.autoGrouped": "Dikelompokkan otomatis. Mengeditnya menjadikannya milik Anda.", + "lib.autoGroupedNote": "Sesi ini dikelompokkan secara otomatis. Menyimpannya menjadikannya milik Anda, dan proses pengelompokan berhenti menulis ulangnya.", + "lib.bestMatch": "Paling cocok", + "lib.clearSearch": "Bersihkan pencarian", + "lib.clip": "Klip", + "lib.clipTitle": "Potong sebuah klip dari rekaman ini", + "lib.collapseSession": "Ciutkan sesi", + "lib.couldNotLoadTheLibrary": "Tidak dapat memuat perpustakaan.", + "lib.couldNotLoadTheSession": "Tidak dapat memuat segmen sesi itu.", + "lib.couldNotQueueTheProxy": "Tidak dapat mengantrekan proxy.", + "lib.couldNotRegroup": "Tidak dapat mengelompokkan ulang.", + "lib.couldNotSaveTheSession": "Tidak dapat menyimpan sesi.", + "lib.delete": "Hapus", + "lib.deleteDescription": "Berkas dihapus dari disk, beserta proxy, gambar mini, dan transkripnya. Ini tidak dapat dibatalkan.", + "lib.deleteTitle": "Hapus rekaman ini?", + "lib.downloadMaster": "Unduh master", + "lib.downloadMasterTitle": "Unduh master multitrek", + "lib.editSession": "Edit sesi", + "lib.expandSession": "Bentangkan sesi", + "lib.from": "Dari", + "lib.generateProxy": "Buat proxy", + "lib.generateProxyTitle": "Buat proxy yang bisa diputar di peramban", + "lib.mostRecent": "Paling baru", + "lib.noProxyYet": "Belum ada proxy — buka untuk membuatnya", + "lib.noSegments": "Tidak ada segmen di sini.", + "lib.noSessionMatch": "Tidak ada sesi yang cocok dengan filter itu.", + "lib.nothingRecorded": "Belum ada yang direkam. Sesi muncul saat perekam menulis segmen.", + "lib.oldestFirst": "Terlama dulu", + "lib.playProxy": "Putar proxy", + "lib.proxyAlreadyQueued": "Sudah ada proxy dalam antrean.", + "lib.proxyQueued": "Proxy diantrekan.", + "lib.recordingDeleted": "Rekaman dihapus.", + "lib.regroupTitle": "Menjalankan ulang pengelompokan. Bersifat menambah dan idempoten: tidak pernah menggabungkan pengelompokan yang Anda pisahkan sendiri.", + "lib.searchPlaceholder": "Cari semua yang pernah diucapkan siapa pun, di seluruh rekaman…", + "lib.searchTranscripts": "Cari transkrip", + "lib.searchable": "Bisa dicari", + "lib.searchableOnly": "Segmen yang punya transkrip, sehingga bisa dicari", + "lib.session": "Sesi", + "lib.sessionDescription": "Deskripsi", + "lib.sessionSaved": "Sesi disimpan.", + "lib.sessionTags": "Tag", + "lib.sessionTitle": "Judul", + "lib.subtitle": "Setiap siaran, bisa dicari. Setiap mikrofon direkam pada treknya sendiri, jadi transkripnya sudah tahu siapa yang mengucapkannya.", + "lib.tag": "Tag", + "lib.tagsPlaceholder": "koma, dipisah", + "lib.title": "Perpustakaan", + "lib.transcribe": "Transkripsikan", + "lib.transcribeEachTrackOnIts": "Transkripsikan setiap trek secara terpisah — itulah yang memberi atribusi pembicara", + "lib.ungrouped": "Segmen tak terkelompok", + "meters.deviation": "Simpangan", + "meters.idle": "diam", + "meters.integrated": "Terintegrasi", + "meters.loudness": "Kepatuhan loudness", + "meters.metering": "mengukur", + "meters.momentary": "Sesaat", + "meters.monitorOff": "Monitornya mati. Nyalakan untuk mengukur apa yang dikirim setiap tujuan.", + "meters.noSignal": "tidak ada sinyal di trek ini", + "meters.nothingToMeasure": "Belum ada yang bisa diukur. Setiap tujuan yang berjalan mendapat penganalisis EBU R128 sendiri pada campuran yang diterimanya.", + "meters.range": "Rentang", + "meters.shortTerm": "Jangka pendek", + "meters.subtitle": "Setiap kanal dari setiap trek masukan, langsung dari relai.", + "meters.title": "Meteran audio", + "meters.truePeak": "Puncak sebenarnya", + "meters.waitingForStream": "menunggu siaran", + "mon.allProcesses": "Semua proses", + "mon.bitrateChart": "Bitrate masukan — 30 menit terakhir", + "mon.chooseDestination": "Pilih sebuah tujuan…", + "mon.clearLog": "Bersihkan log", + "mon.clearedThisDestinationIsBack": "Dibersihkan. Tujuan ini kembali memakai perintah yang dihasilkan.", + "mon.dryRun": "Uji jalan", + "mon.dryRunInvalid": "FFmpeg menolak sebuah argumen", + "mon.dryRunOk": "FFmpeg menerimanya", + "mon.extraInput": "Argumen masukan tambahan", + "mon.extraOutput": "Argumen keluaran tambahan", + "mon.ffmpegLog": "Log FFmpeg", + "mon.fromRunningProcess": "dari proses yang berjalan", + "mon.fullCommand": "Perintah terurai lengkap", + "mon.hostCpu": "CPU host", + "mon.ingestLoss": "Kehilangan masukan", + "mon.memory": "Memori", + "mon.nothingProvenEitherWay": "Tidak ada yang terbukti ke arah mana pun", + "mon.pid": "PID", + "mon.processes": "Proses", + "mon.rebuilt": "dibangun ulang", + "mon.relayDrops": "Buangan relai", + "mon.relayIn": "Masuk ke relai", + "mon.resolveHint": "Uraikan perintahnya untuk melihat persis apa yang akan dijalankan sebelum menerapkan.", + "mon.restarts": "Mulai ulang", + "mon.subtitle": "Kesehatan proses, sumber daya host, dan log FFmpeg langsung.", + "mon.title": "Pemantauan", + "mon.uptime": "Waktu aktif", + "nav.automation": "Otomatisasi", + "nav.chat": "Obrolan", + "nav.clips": "Klip", + "nav.dashboard": "Dasbor", + "nav.jobs": "Tugas", + "nav.library": "Perpustakaan", + "nav.meters": "Meter audio", + "nav.monitoring": "Pemantauan", + "nav.playout": "Playout", + "nav.recordings": "Rekaman", + "nav.renditions": "Varian", + "nav.routing": "Routing", + "nav.settings": "Pengaturan", + "nav.sources": "Sumber", + "play.acrossEveryRungTheBackstop": "Mencakup setiap anak tangga. Pengaman terakhir yang mencegah siaran panjang memenuhi disk.", + "play.allowCrossOrigin": "Izinkan lintas-origin", + "play.allowCrossOriginPlayback": "Izinkan pemutaran lintas-origin", + "play.audioBitrate": "Bitrate audio (kbps)", + "play.audioTrack": "Trek audio", + "play.basicPasswordNote": "Pemutar yang tidak bisa mengikuti tautan (set-top box, CDN) dapat mengirim rahasia yang sama sebagai kata sandi HTTP basic.", + "play.boundsTheViewerTableViewers": "Membatasi tabel penonton. Penonton di luar batas itu tetap dilayani, hanya saja tidak dihitung.", + "play.carriesThePlaybackTokenAnyone": "Membawa token pemutaran. Siapa pun yang punya URL ini bisa menonton.", + "play.clipboardUnavailable": "Papan klip tidak tersedia; pilih dan salin secara manual", + "play.couldNotLoadPlayout": "Tidak dapat memuat Playout", + "play.disabled": "Dinonaktifkan", + "play.diskCap": "Batas disk (MB)", + "play.dvrWindow": "Jendela DVR (dtk)", + "play.embedCode": "Kode sematan", + "play.enable": "Aktifkan Playout", + "play.enabled": "Diaktifkan", + "play.forObsACdnOr": "Untuk OBS, sebuah CDN, atau pemutar apa pun yang berbicara HLS.", + "play.format": "Format", + "play.hls": "HLS", + "play.hlsDash": "HLS dan DASH", + "play.hlsPlaylist": "Playlist HLS", + "play.idleTimeout": "Batas waktu diam penonton (dtk)", + "play.linkReset": "Tautan pemutaran baru dibuat. Yang sebelumnya tidak berfungsi lagi.", + "play.linksProtected": "Tautan ini hanya berfungsi bagi administrator yang sudah masuk sampai Anda mengaktifkan Publik.", + "play.linksPublic": "Tautan ini berfungsi tanpa akun.", + "play.liveWindow": "Jendela langsung (segmen)", + "play.metaDescription": "Deskripsi", + "play.metaTitle": "Judul", + "play.mustExceedOneSegmentOr": "Harus melebihi satu segmen, atau penonton berkedip hilang di antara dua polling.", + "play.name": "Nama", + "play.noCredentialNote": "Tidak diperlukan kredensial apa pun. Hanya cocok untuk siaran yang memang Anda ingin dilihat seluruh internet.", + "play.noRungs": "Belum ada anak tangga. Tambahkan satu agar ada yang bisa dipublikasikan.", + "play.onDisk": "Di disk", + "play.oneStereoAacTrackPer": "Satu trek AAC stereo per anak tangga.", + "play.packaging": "Pengemasan", + "play.passthrough": "Masukan (passthrough)", + "play.peak": "Puncak", + "play.playerPage": "Halaman pemutar", + "play.posterNote": "Ditampilkan di atas video pada halaman publik. Posternya adalah keyframe terbaru, disegarkan otomatis.", + "play.private": "Privat.", + "play.protectedNote": "Pemutaran dilindungi secara bawaan. Membukanya adalah langkah tersendiri yang disengaja.", + "play.protection": "Perlindungan", + "play.public": "Publik", + "play.resetFailed": "Tidak dapat mereset", + "play.restartNote": "Mengubah salah satu dari ini akan memulai ulang muxer, yang memutus penonton selama beberapa detik.", + "play.rungNote": "Setiap anak tangga menyalin video variannya bit demi bit, jadi menambah satu hanya menghabiskan satu muxer dan tidak pernah pengodean video kedua. Hanya trek audio yang berbeda per anak tangga; routing per tujuan tidak diubah.", + "play.segmentLength": "Panjang segmen (dtk)", + "play.serveWithoutSession": "Layani penonton tanpa sesi", + "play.sessionCap": "Batas sesi", + "play.sessions": "Sesi", + "play.share": "Bagikan", + "play.shorterIsLowerLatencyAnd": "Makin pendek, makin rendah latensinya dan makin sering playlist berubah.", + "play.subtitle": "Sajikan siaran langsung ke penonton, sebagai HLS publik.", + "play.threeIsTheMinimumA": "Tiga adalah jumlah minimum yang bisa dijadikan penyangga oleh pemutar.", + "play.title": "Playout", + "play.unlisted": "Tidak terdaftar.", + "play.untitled": "Siaran tanpa judul", + "play.videoSource": "Sumber video", + "play.watchingNow": "Sedang menonton", + "play.whoCanWatch": "Siapa yang boleh menonton", + "player.copied": "Disalin", + "player.copy": "Salin", + "player.loading": "Memuat…", + "player.missingToken": "Tautan yang Anda buka tidak memuat token pemutaran, atau tokennya telah diubah. Mintalah tautan terbaru kepada orang yang membagikannya.", + "player.notLive": "Sedang tidak siaran langsung", + "player.viewer": "penonton", + "player.viewers": "penonton", + "player.willStart": "Halaman ini akan mulai memutar dengan sendirinya ketika siaran dimulai.", + "playlist.choose": "Pilih unggahan", + "playlist.chooseAria": "Pilih unggahan untuk ditambahkan", "playlist.empty": "Belum ada yang diantrekan.", + "playlist.hint": "Diunggah di Media. Item yang disebut playlist ini dan kemudian kehilangan unggahannya akan tampil di atas sebagai perlu perhatian, alih-alih gagal diam-diam saat failover berikutnya mencapainya.", "playlist.itemsAria": "Item playlist", - "playlist.stateReady": "Siap", - "playlist.stateTranscoding": "Transcoding", - "playlist.stateAttention": "Perlu perhatian", - "playlist.stateUnsaved": "Belum disimpan", "playlist.missingUpload": "Unggahan ini tidak dapat ditemukan lagi.", - "playlist.moveUp": "Naikkan {name}", "playlist.moveDown": "Turunkan {name}", - "playlist.remove": "Hapus {name}", - "playlist.choose": "Pilih unggahan", - "playlist.chooseAria": "Pilih unggahan untuk ditambahkan", + "playlist.moveUp": "Naikkan {name}", "playlist.noUploads": "Tidak ada unggahan tersedia", - "playlist.hint": "Diunggah di Media. Item yang disebut playlist ini dan kemudian kehilangan unggahannya akan tampil di atas sebagai perlu perhatian, alih-alih gagal diam-diam saat failover berikutnya mencapainya.", - "dest.fbBoxTitle": "Lintas-posting dan tombol donasi Facebook", - "dest.fbBoxIntro": "Dikirim pada panggilan pembuatan yang sama dengan Audiens, jadi aturan yang sama berlaku: diterapkan sekali saat siaran dimulai, dan dibiarkan sepenuhnya jika kosong.", - "dest.fbBackupLabel": "Terbitkan umpan cadangan", - "dest.fbBackupHelp": "Mengirim salinan kedua dari siaran ini ke ingest cadangan Facebook, sehingga koneksi yang putus tidak menjatuhkan siarannya.", - "dest.fbBackupCost": "Menggandakan bandwidth unggah tujuan ini.", - "dest.fbBackupReconnect": "Mengaktifkannya menyambungkan ulang siaran satu kali — jadi aktifkan sebelum Anda mulai siaran langsung.", - "dest.fbCrosspostLabel": "Lintas-posting ke Halaman", - "dest.fbCrosspostPageId": "ID Halaman", - "dest.fbCrosspostAlsoPost": "Posting juga", - "dest.fbCrosspostRemove": "Hapus Halaman ini", - "dest.fbCrosspostAdd": "Tambah Halaman", - "dest.fbCrosspostHelp": "ID Halaman numerik dari konsol Facebook sendiri (pengaturan Halaman, atau graph.facebook.com/me/accounts saat masuk sebagai Halaman) — tidak ada pencarian di sini, jadi tempelkan id-nya, bukan nama atau URL. “Posting juga” menerbitkan sebagai Halaman tersebut alih-alih hanya membagikan siaran kepadanya; jika dimatikan, hanya pembagian yang lebih senyap yang terjadi.", - "dest.fbDonateLabel": "ID lembaga amal tombol donasi", - "dest.fbDonatePlaceholder": "ID lembaga amal", - "dest.fbDonateHelp": "Ini juga id buram dari konsol penggalangan dana Facebook, bukan nama lembaga amal untuk dicari. Kosongkan agar tidak ada tombol donasi yang dilampirkan sama sekali." + "playlist.remove": "Hapus {name}", + "playlist.stateAttention": "Perlu perhatian", + "playlist.stateReady": "Siap", + "playlist.stateTranscoding": "Transcoding", + "playlist.stateUnsaved": "Belum disimpan", + "playlist.title": "Playlist", + "rec.delete": "Hapus", + "rec.deleteDescription": "Berkas dihapus dari disk, beserta proxy, gambar mini, dan transkripnya. Tindakan ini tidak dapat dibatalkan.", + "rec.deleteFailed": "Tidak dapat menghapus rekaman.", + "rec.deleteTitle": "Hapus rekaman ini?", + "rec.deleted": "Rekaman dihapus.", + "rec.disabled": "nonaktif", + "rec.disk": "Disk", + "rec.diskRecordings": "Rekaman", + "rec.download": "Unduh", + "rec.downloadNamed": "Unduh {name}", + "rec.duration": "Durasi", + "rec.empty": "Belum ada rekaman. Aktifkan perekaman di sebelah kanan lalu mulai siaran.", + "rec.enabled": "Aktif", + "rec.file": "Berkas", + "rec.free": "Bebas", + "rec.haltedDefault": "Perekaman dihentikan: volume berada di bawah batas bawah ruang kosong.", + "rec.haltedRestarts": "Perekam akan mulai lagi dengan sendirinya begitu ruang tersedia.", + "rec.help.enabled": "Menulis setiap masukan ke disk sembari disiarkan, menjaga semua trek audio tetap terpisah dan tanpa dikodekan ulang. Perekaman terlepas dari apa yang Anda kirim ke platform: mematikannya tidak memengaruhi keluaran langsung, dan menyalakannya menghabiskan disk alih-alih lebar pita unggah.", + "rec.help.maxAgeHours": "Menghapus rekaman yang lebih tua dari sekian jam ini, berapa pun ukuran totalnya. Berguna bila Anda hanya perlu satu atau dua hari terakhir. Isi 0 untuk menyimpan semuanya sampai batas lain menghapusnya.", + "rec.help.maxGb": "Batas atas ukuran total rekaman yang disimpan polyemesis. Setelah terlampaui, yang terlama dihapus sampai totalnya muat lagi. Isi 0 untuk tanpa batas ukuran — dalam hal itu hanya usia, atau batas bawah ruang kosong, yang membatasi pertumbuhan.", + "rec.help.minFreeGb": "Rem darurat: perekaman berhenti ketika ruang kosong volume turun di bawah nilai ini dan berlanjut sendiri begitu ruang tersedia. Ini satu-satunya batas yang memperhitungkan berkas yang bukan tulisan polyemesis, jadi inilah pengaturan yang benar-benar melindungi disk bersama dari penuh.", + "rec.help.segmentSeconds": "Berapa lama tiap berkas sebelum perekam memulai yang baru. Segmen lebih pendek membatasi kerugian akibat kegagalan dan memudahkan memindahkan berkas satuan, tetapi menyisakan lebih banyak berkas untuk dikelola. Pemotongan jatuh pada bingkai kunci, jadi panjang sebenarnya bervariasi sekitar satu detik.", + "rec.help.stemFiles": "Juga menulis setiap trek audio sebagai berkas tersendiri di samping berkas utama. Berguna bila Anda menyunting setelahnya dan ingin pembawa acara, tamu, serta audio permainan pada kanal terpisah. Ini melipatgandakan volume tulis satu sesi kira-kira sebanyak jumlah trek, itulah sebabnya bawaannya nonaktif.", + "rec.help.stemFormat": "Keduanya tanpa kehilangan kualitas, jadi tidak ada yang mengorbankan mutu. FLAC berukuran sekitar separuh dan itulah yang Anda inginkan kecuali ada perkakas dalam alur kerja Anda yang tidak bisa membacanya; WAV terbuka di mana pun. Trek lebih dari 8 kanal otomatis beralih ke WAV.", + "rec.hideStems": "Sembunyikan trek terpisah", + "rec.keepForever": "0 = simpan selamanya.", + "rec.loadFailed": "Tidak dapat memuat rekaman.", + "rec.maxAgeHours": "Hapus setelah (jam)", + "rec.maxGb": "Simpan paling banyak (GB)", + "rec.minFreeGb": "Berhenti di bawah ruang kosong (GB)", + "rec.minFreeNote": "Batas di atas hanya membatasi apa yang ditulis polyemesis; apa pun yang lain di volume tetap bisa memenuhinya. 0 = tanpa batas bawah.", + "rec.noSizeLimit": "0 = tanpa batas ukuran.", + "rec.recording": "merekam", + "rec.retentionNote": "Pembersihan berjalan setiap 30 detik: yang terlama dulu, berdasarkan usia lalu ukuran total. Segmen yang sedang ditulis tidak pernah dihapus.", + "rec.saveFailed": "Tidak dapat menyimpan pengaturan.", + "rec.savePolicy": "Simpan kebijakan penyimpanan", + "rec.saved": "Pengaturan perekaman disimpan.", + "rec.segmentSeconds": "Panjang segmen (detik)", + "rec.segments": "Segmen", + "rec.settingsTitle": "Perekaman", + "rec.showStems": "Tampilkan trek terpisah", + "rec.size": "Ukuran", + "rec.started": "Dimulai", + "rec.stemFiles": "Berkas trek terpisah", + "rec.stemFlac": "FLAC — tanpa kehilangan kualitas, sekitar separuh ukuran", + "rec.stemFormat": "Format trek terpisah", + "rec.stemNote": "Ditulis dari proses yang sama dengan berkas utama, sehingga setiap trek terpisah tetap selaras per sampel dengan yang lain. Trek lebih dari 8 kanal beralih sendiri ke WAV.", + "rec.stemWav": "WAV — tanpa kehilangan kualitas, terbuka di mana saja", + "rec.stems": "Trek terpisah", + "rec.stemsTitle": "{count} berkas trek terpisah, selaras per sampel dengan berkas utama", + "rec.stemsUsage": "Trek terpisah ({count} berkas)", + "rec.subtitle": "Arsip multitrek lengkap: setiap trek audio masukan disimpan tanpa dikodekan ulang.", + "rec.title": "Rekaman", + "rec.tracks": "Trek", + "rec.tracksTitle": "{count} trek audio disimpan", + "rec.used": "Terpakai", + "rec.volume": "Volume", + "rend.aspectBlurpad": "Isian buram", + "rend.aspectBlurpadHint": "Mengisi sisanya dengan salinan bingkai itu sendiri yang dipangkas hingga penuh lalu diburamkan. Inilah konvensi yang sudah disepakati oleh siaran vertikal.", + "rend.aspectCrop": "Pangkas hingga penuh", + "rend.aspectCropHint": "Memangkas dari tengah ke bentuk target, lalu menskalakan. Subjek mempertahankan ukurannya di layar; tepi bingkainya hilang.", + "rend.aspectHandling": "Penanganan rasio aspek", + "rend.aspectPad": "Letterbox", + "rend.aspectPadHint": "Menskalakan seluruh bingkai agar muat dan mengisi sisanya dengan warna polos. Tidak ada yang hilang, tetapi sumber 16:9 pada kanvas 9:16 sebagian besar hanya berupa bilah.", + "rend.aspectStretch": "Regangkan agar muat", + "rend.aspectStretchHint": "Menskalakan ke ukuran target dan membiarkan gambarnya melar bila sumbernya tidak sepadan. Inilah yang dilakukan setiap varian sebelum mode-mode lain ada.", + "rend.boxColour": "Warna kotak", + "rend.boxOpacity": "Opasitas kotak (%)", + "rend.builtInFont": "Bawaan internal", + "rend.burnedText": "Teks tertanam", + "rend.chooseHardware": "Pilih encoder perangkat keras, atau keluaran yang lebih kecil.", + "rend.colourBlack": "black", + "rend.colourWhite": "white", + "rend.costBeyond": "Mengodekan ini dengan perangkat lunak secara waktu nyata di luar kemampuan sebagian besar mesin", + "rend.costBeyondDetail": "{label} adalah {mpps} untuk satu pengodean. x264 pada ukuran ini biasanya kalah berpacu dengan waktu nyata, dan varian yang tertinggal menjatuhkan bingkai bagi setiap tujuan yang membacanya — dan itu baru Anda ketahui di tengah siaran.{machine} {escape}", + "rend.costBusy": "Perkirakan beberapa inti akan sibuk", + "rend.costBusyDetail": "{label} adalah {mpps} pada {encoder}, yaitu pengodean waktu nyata yang berat.{machine} Perhatikan kecepatan encoder begitu tayang: di bawah 1,0× berarti ia tertinggal dan tujuan di bawahnya akan mulai menjatuhkan bingkai. {escape}", + "rend.costMachineCores": " Mesin ini melaporkan {cores} inti logis.", + "rend.costModest": "Pengodean perangkat lunak yang sedang", + "rend.costModestDetail": "{label} adalah {mpps}, terjangkau oleh sebagian besar mesin.{machine} Ini tetap satu pengodean nyata — dipakai bersama oleh setiap tujuan yang memilihnya, tetapi tidak gratis.", + "rend.costNoNumbers": "Biaya belum bisa diperkirakan", + "rend.costNoNumbersDetail": "Belum ada yang menyelidik sumbernya, jadi “pertahankan sumber” tidak punya angka di belakangnya. Mulai masukannya dan bagian ini akan terisi.", + "rend.costOffloaded": "Dialihkan ke {encoder}", + "rend.costOffloadedDetail": "{label} ({mpps}) berjalan pada encoder fungsi-tetap GPU, jadi hanya memakan sedikit CPU. Kualitas dan kendali laju berbeda-beda menurut driver, dan tiap chip punya langit-langitnya sendiri — periksa keluarannya sebelum acara yang penting.", + "rend.create": "Buat", + "rend.created": "Varian dibuat. Pilih pada sebuah tujuan untuk memulai pengodean.", + "rend.custom": "Khusus…", + "rend.deintAll": "Setiap bingkai", + "rend.deintAllHint": "Untuk sumber yang interlaced tetapi tidak menyatakannya. Banyak jembatan SDI dan kartu tangkap menandai semuanya sebagai progresif tanpa peduli apa yang mereka terima, dan pada perangkat itu “hanya bingkai interlaced” tidak melakukan apa-apa sehingga tampak seperti pengaturan yang rusak.", + "rend.deintAuto": "Hanya bingkai interlaced", + "rend.deintAutoHint": "Hanya menyentuh bingkai yang ditandai interlaced oleh sumbernya, sehingga bingkai progresif lewat tanpa disentuh. Pilihan yang tepat untuk materi campuran apa pun.", + "rend.deintOff": "Mati", + "rend.deintOffHint": "Tepat untuk sumber progresif yang dimiliki hampir semua orang. Melakukan deinterlace padanya hanya melembutkan gambarnya tanpa manfaat.", + "rend.deinterlace": "Deinterlace", + "rend.deleteFailed": "Tidak dapat menghapus varian.", + "rend.deleteFallbackMany": "{count} tujuan kembali ke passthrough dan akan dikirimi video sumber tanpa perubahan. Periksa apakah sumbernya masih sesuai dengan yang diterima tiap platform.", + "rend.deleteFallbackOne": "{count} tujuan kembali ke passthrough dan akan dikirimi video sumber tanpa perubahan. Periksa apakah sumbernya masih sesuai dengan yang diterima tiap platform.", + "rend.deleteNoDestinations": "Tidak ada yang memilih varian ini, jadi menghapusnya tidak mengubah tujuan mana pun.", + "rend.deleteRenditionTitle": "Hapus {name}?", + "rend.editRendition": "Edit varian", + "rend.empty": "Belum ada varian. Setiap tujuan memakai passthrough, dan itu memang tepat sampai salah satunya tidak sanggup menerima sumbernya — misalnya masukan 4K60 yang tidak diterima Twitch, Kick, atau X. Tambahkan sebuah varian dan arahkan tujuan-tujuan itu ke sana.", + "rend.encoder": "Encoder", + "rend.encoderPreset": "Preset encoder", + "rend.encoderRelatedFailed": "{name} tidak ditawarkan karena sebuah encoder terkait gagal di sini.", + "rend.encoderTestFailed": "{name} gagal pada uji pengodean satu bingkai di mesin ini.", + "rend.encoderTestOk": "{name} berhasil mengodekan satu bingkai uji di mesin ini.", + "rend.evenNote": "Nilai 0 pada satu sumbu mempertahankan nilai sumber dan menjaga rasio aspek — setel tingginya saja untuk menskalakan ulang. Keduanya harus bilangan genap: H.264 dan HEVC tidak punya bidang krominansi berukuran ganjil.", + "rend.font": "Font", + "rend.frameRate": "Laju bingkai", + "rend.gop": "GOP", + "rend.gpuUnopenable": "GPU tersedia tetapi proses ini tidak dapat membukanya", + "rend.hardwareFallback": "perangkat keras", + "rend.hardwareScanFound": "Hasil pemindaian perangkat keras", + "rend.height": "Tinggi", + "rend.hevcWarning": "Encoder ini menghasilkan HEVC. Sebagian besar tujuan RTMP hanya menerima H.264 — pilih hanya untuk tujuan yang sudah menyatakan menerima HEVC.", + "rend.keyframeInterval": "Interval keyframe", + "rend.letterboxColour": "Warna letterbox", + "rend.live": "Langsung", + "rend.loadFailed": "Tidak dapat memuat varian.", + "rend.marginX": "Margin X (%)", + "rend.marginY": "Margin Y (%)", + "rend.name": "Nama", + "rend.namePlaceholder": "1080p60 untuk Twitch dan Kick", + "rend.needsBothAWidthAnd": "Perlu lebar dan tinggi sekaligus — dengan satu sumbu dibiarkan bebas, penskalaannya sudah menjaga rasio aspek.", + "rend.needsBothAxes": "Perlu lebar dan tinggi sekaligus", + "rend.newRendition": "Varian baru", + "rend.noEncoder": "FFmpeg ini melaporkan tidak ada encoder video yang bisa dipakai.", + "rend.noHardwareEscape": "Tidak ada encoder perangkat keras yang bekerja di mesin ini, jadi tidak ada tempat untuk memindahkannya — satu-satunya tuas di sini adalah keluaran yang lebih kecil. Deteksi ulang perangkat keras jika sejak itu Anda memasang driver atau meneruskan GPU ke dalam kontainer.", + "rend.notProbed": "Belum ada yang diuji-kodekan di mesin ini, jadi daftar ini adalah isi build FFmpeg, bukan kemampuan perangkat kerasnya. Deteksi ulang untuk mengetahuinya.", + "rend.notRunning": "Tidak berjalan", + "rend.note": "Catatan", + "rend.noteDuplicateFps": "Sumbernya berjalan pada {srcFps} fps, jadi {fps} fps hanya menggandakan bingkai.", + "rend.notePlaceholder": "Untuk apa tingkat ini.", + "rend.noteUpscale": "Sumbernya {width}×{height}. Memperbesar memakan CPU dan tidak menambah detail.", + "rend.opacity": "Opasitas (%)", + "rend.percentNote": "Semuanya adalah persentase dari bingkai, jadi pengaturan yang sama tepat pada tingkat 16:9 maupun 9:16. Margin diabaikan pada sumbu yang dipusatkan.", + "rend.preset": "Preset", + "rend.presetPlaceholder": "veryfast", + "rend.presetStartingPoint": "Titik awal — pastikan batas terkini pada platformnya.", + "rend.redetectFailed": "Tidak dapat mendeteksi ulang perangkat keras.", + "rend.redetectTitle": "Jalankan ulang pemindaian GPU dan kodekan satu bingkai uji dengan tiap encoder. Perlu beberapa detik.", + "rend.redetectedNoHardware": "Perangkat keras dideteksi ulang. Tidak ada encoder perangkat keras yang lolos uji pengodean di mesin ini.", + "rend.resolution": "Resolusi", + "rend.restartEncode": "Mulai ulang pengodean ini dan tujuan yang membacanya", + "rend.restartFailed": "Tidak dapat memulai ulang pengodean.", + "rend.restarting": "{name} sedang dimulai ulang. Tujuan-tujuannya menyambung kembali bersamanya.", + "rend.save": "Simpan", + "rend.saveFailed": "Tidak dapat menyimpan varian.", + "rend.seedNote": "Ini hanya mengisi awal formulir dan tidak lebih — ubah apa pun sebelum menyimpan. Langit-langit platform berubah dan berbeda menurut status kemitraan.", + "rend.signalError": "Galat", + "rend.signalIdle": "Menganggur", + "rend.sizePct": "Ukuran (% dari tinggi)", + "rend.softwareEncoderFallback": "sebuah encoder perangkat lunak", + "rend.speed": "Kecepatan", + "rend.subtitle": "Satu pengodean video bersama, banyak tujuan. Audio tidak pernah disentuh di sini.", + "rend.target": "Target", + "rend.textColour": "Warna teks", + "rend.textPlaceholder": "STASIUN SAYA", + "rend.title": "Varian", + "rend.unused": "Belum ada tujuan yang memilihnya, jadi tidak ada yang dikodekan. Pilih pada sebuah tujuan untuk memulai pengodean.", + "rend.uptime": "Waktu aktif", + "rend.videoBitrate": "Bitrate video", + "rend.waitingSnapshot": "Menunggu cuplikan langsung pertama.", + "rend.watermarkImage": "Gambar tanda air", + "rend.watermarkPlaceholder": "overlays/logo.png", + "rend.width": "Lebar", + "rend.widthPct": "Lebar (% dari bingkai)", + "route.attack": "Attack", + "route.audio": "audio", + "route.audioAndVideoGoOut": "Audio dan video keluar bersamaan.", + "route.audioLaterThanVideo": "Audio lebih lambat dari video", + "route.audioRouting": "Routing audio", + "route.audioRoutingDesc": "Pilih dengan tepat trek masukan mana yang diterima setiap tujuan.", + "route.autoLimitWhen2Tracks": "Otomatis (batasi bila 2+ trek)", + "route.commentaryLanguage": "Bahasa komentar", + "route.couldNotApplyThePreset": "Tidak dapat menerapkan preset.", + "route.couldNotCompileThisRouting": "Tidak dapat mengompilasi routing ini.", + "route.couldNotLoadDestinations": "Tidak dapat memuat tujuan.", + "route.couldNotSaveTheRouting": "Tidak dapat menyimpan routing.", + "route.couldNotSaveTrackRoles": "Tidak dapat menyimpan peran trek.", + "route.custom": "Khusus…", + "route.delayDirection": "Arah tunda", + "route.destinations": "Tujuan", + "route.duckingNote": "Turunkan satu kelompok trek setiap kali kelompok lain sedang berbicara — mikrofon di atas alas musik. Mati berarti setiap trek terpilih dijumlahkan pada gain-nya sendiri, tanpa perubahan.", + "route.duckingTriggerNote": "Pemicu tidak harus ada di dalam campuran tujuan ini — sebuah mikrofon bisa menekan musik di sini sementara yang dikirim hanya musiknya.", + "route.enableDucking": "Aktifkan ducking", + "route.everyTrackReaches": "Setiap trek terpilih sampai ke tujuan ini, termasuk musik.", + "route.excludeMusic": "Kecualikan musik", + "route.keepMusic": "Pertahankan musik", + "route.keepMusicOut": "Jangan kirim musik ke tujuan ini", + "route.labelTracks": "Beri label trek", + "route.langPlaceholder": "lang — mis. es", + "route.limiter04DbfsCeiling": "Limiter (batas atas −0,4 dBFS)", + "route.localOnlyNote": "Server ini belum menyimpan peran trek, jadi semuanya tetap di peramban ini. Hal lain di halaman ini tersimpan seperti biasa.", + "route.loudnessTarget": "Target kenyaringan", + "route.lufsBroadcast": "Siaran EBU R128", + "route.measuredOverTheWholeProgramme": "Diukur sepanjang keseluruhan acara, bukan dari saat ke saat: bagian yang lirih tetap lirih. Angka-angka inilah yang dibidik oleh normalizer milik platform sendiri, jadi mencapai salah satunya yang mencegah platform menurunkan volume Anda.", + "route.mixMatrix": "Matriks campuran", + "route.musicPolicy": "Kebijakan musik", + "route.noTarget": "Tanpa target (biarkan levelnya)", + "route.normalizeLoudnorm": "Kenyaringan (EBU R128)", + "route.normalizeOff": "Mati", + "route.off": "mati", + "route.offset": "Offset", + "route.on": "aktif", + "route.platformDefault": "Bawaan platform", + "route.podcastSpokenWord": "Podcast / ucapan", + "route.presetApplied": "Preset diterapkan. Periksa, lalu simpan.", + "route.range": "Rentang", + "route.ratio": "Rasio", + "route.release": "Release", + "route.result": "Hasil", + "route.role51": "5.1", + "route.roleClean": "bersih", + "route.roleMic": "mik", + "route.roleMusic": "musik", + "route.rolesNote": "Peran menjelaskan masukan, bukan tujuan ini, dan tersimpan sendiri. Tidak ada yang disaring sampai sebuah tujuan di bawah memintanya.", + "route.simple": "Sederhana", + "route.target": "Target", + "route.targetRole": "target", + "route.thisDestinationSPlatform": "Platform tujuan ini", + "route.threshold": "Ambang", + "route.title": "Routing", + "route.triggerRole": "pemicu", + "route.truePeak": "Puncak sebenarnya", + "route.video": "video", + "route.videoLaterThanAudio": "Video lebih lambat dari audio", + "route.withoutATargetTheMix": "Tanpa target, campurannya keluar pada level apa pun yang dihasilkan masukan, hanya dengan perlindungan clipping di atas yang diterapkan.", + "route.youtubeTwitchSpotify": "YouTube, Twitch, Spotify", + "set.acceptSelfSigned": "Terima sertifikat yang ditandatangani sendiri", + "set.accountDisconnected": "Akun diputuskan.", + "set.adminPassword": "Kata sandi administrator", + "set.alertDelivery": "Pengiriman peringatan", + "set.alertDeliveryDesc": "Seberapa gigih sebuah webhook yang tidak menjawab dikejar, sebelum peringatan itu dilepaskan.", + "set.apiTokens": "Token API", + "set.apiTokensDesc": "Kredensial berumur panjang untuk skrip, Prometheus, dan apa pun yang tidak dapat memelihara sesi. Kirimkan sebagai Authorization: Bearer pmk_…. Sebuah token bisa melakukan semua yang bisa dilakukan administrator, kecuali mengelola token — itu tetap berada di balik kata sandi, sehingga mencabut sebuah token bersifat final.", + "set.app": "Aplikasi", + "set.attemptsBackoff": "Jeda antar percobaan otomatis memanjang hingga langit-langit 30 detik, jadi ujung atas rentang ini sudah berarti beberapa menit mengejar satu endpoint mati. Adanya batas itulah intinya: mencoba tanpa henti mengubah satu webhook mati menjadi antrean yang tak pernah surut.", + "set.attemptsNote": "Termasuk percobaan pertama. 1 berarti tidak pernah mengulang.", + "set.attemptsRetryable": "Hanya kegagalan yang layak diulang yang diulang. Kode 404 dari webhook yang sudah dihapus seseorang bersifat permanen, dan mencobanya empat kali lagi hanya menunda setiap peringatan yang mengantre di belakangnya; kode 502 dari proxy terbalik justru itulah alasan percobaan ini ada.", + "set.audioMeters": "Meteran audio", + "set.brokerPasswordCleared": "Kata sandi broker dihapus.", + "set.brokerPlainNote": "mqtt:// tidak terenkripsi. MQTT mengirim nama pengguna dan kata sandi di dalam paket CONNECT-nya, jadi keduanya melintasi jaringan dalam bentuk terbuka — tidak masalah di LAN tepercaya, yang merupakan kasus Home Assistant yang lazim, dan tidak pantas melalui apa pun yang tidak Anda kendalikan. Gunakan mqtts:// bila broker menyediakannya.", + "set.brokerUrl": "URL broker", + "set.brokerUrlNote": "mqtt://, mqtts://, ws:// atau wss://. Kata sandi di dalam URL ditolak: URL berakhir di baris log dan keluaran ps, dan itu tidak bisa ditarik kembali. Masukkan di kolom di bawah, tempat ia dienkripsi saat disimpan.", + "set.brokerUrlPlaceholder": "mqtts://broker.local:8883", + "set.chatBothApply": "Keduanya berlaku dan yang lebih longgar menang: sebuah pesan lenyap ketika lebih tua dari jumlah jam sekaligus berada di luar N terbaru. Maka kanal yang ramai menyimpan rentang waktu lebih pendek daripada kanal sepi pada pengaturan yang sama.", + "set.chatBufferNote": "“Kirim saat terhubung” adalah angka yang berbeda sifatnya dari dua yang lain. Keduanya membatasi apa yang disimpan di disk; yang ini adalah penyangga di memori, dialokasikan untuk setiap panel yang terhubung.", + "set.chatHistory": "Riwayat obrolan", + "set.chatHistoryDesc": "Seberapa jauh riwayat obrolan menjangkau ke belakang — dan dengan itu, seberapa banyak yang bisa dilihat moderator saat membuka kartu seseorang.", + "set.chatHours": "Simpan selama (jam)", + "set.chatKeep": "Selalu simpan (pesan)", + "set.chatKeepNote": "Sebuah batas bawah, berapa pun usianya.", + "set.chatNotOnlyDisk": "Ini bukan sekadar pengaturan disk. Mengeklik sebuah nama di obrolan membuka apa yang pernah dikatakan orang itu, dan riwayat tersebut berasal dari sini — tidak ada platform yang menerbitkan API untuk itu, jadi polyemesis hanya dapat menampilkan apa yang disimpannya. Setel terlalu pendek dan seseorang yang sudah seminggu bermasalah akan terbaca seolah tak pernah mengucapkan apa pun.", + "set.chatSendNote": "Riwayat tanpa kueri.", + "set.chatSendOnConnect": "Kirim saat terhubung (pesan)", + "set.chatSendOnConnectShort": "Kirim saat terhubung", + "set.chatSmall": "Obrolan berukuran kecil. Kanal dengan rata-rata sepuluh pesan per detik menyimpan kira-kira 7 MB per jam; sebagian besar jauh di bawah itu.", + "set.clientId": "ID klien", + "set.clientIdNote": "Harus unik di broker. Tabrakan adalah penyebab nomor satu dari lingkaran sambung-ulang yang tak terjelaskan: broker memutus sesi yang lebih lama pada setiap penyambungan, dan kedua klien menyambung ulang selamanya.", + "set.clientIdPlaceholder": "diturunkan dari nama instans", + "set.clientSecret": "Client secret", + "set.comingBack": "Kembali", + "set.confirmPassword": "Konfirmasi kata sandi baru", + "set.connectedAccounts": "Akun terhubung", + "set.copied": "Disalin", + "set.copy": "Salin", + "set.couldNotChangeThePassword": "Tidak dapat mengubah kata sandi.", + "set.couldNotClearThePassword": "Tidak dapat menghapus kata sandi.", + "set.couldNotCreateTheToken": "Tidak dapat membuat token.", + "set.couldNotReCheckThe": "Tidak dapat memeriksa ulang kredensial.", + "set.couldNotReadTheTls": "Tidak dapat membaca status TLS.", + "set.couldNotRevokeTheToken": "Tidak dapat mencabut token.", + "set.couldNotSaveTheCredentials": "Tidak dapat menyimpan kredensial.", + "set.couldNotSaveTheMqtt": "Tidak dapat menyimpan pengaturan MQTT.", + "set.credsRemoved": "Kredensial dihapus.", + "set.currentPassword": "Kata sandi saat ini", + "set.dataDirectory": "Direktori data", + "set.deliveryAttempts": "Percobaan pengiriman", + "set.dialsOnly": "Server hanya menghubungi: {schemes}.", + "set.disconnect": "Putuskan", + "set.disconnectDesc": "Tujuan yang memakainya tetap menyimpan pengaturannya tetapi berhenti memperbarui kunci siarannya. Anda dapat menghubungkan kembali akun yang sama setelahnya.", + "set.disconnectTitle": "Putuskan akun ini?", + "set.drawtextMissing": "Build ini tidak punya filter drawtext, jadi overlay teks tidak tersedia. Diperlukan FFmpeg yang dibangun dengan libfreetype.", + "set.drawtextOk": "Build ini bisa menggambar teks pada video.", + "set.enabled": "Aktif", + "set.encoderUrl": "URL encoder", + "set.expires": "Kedaluwarsa", + "set.failover": "Failover", + "set.failoverDesc": "Masukan cadangan dan kartu tunggu, untuk saat sumber langsung berhenti mengirim. Nonaktif secara bawaan: dalam keadaan nonaktif, alur pemrosesan sama persis bita demi bita dengan sebelum fitur ini ada.", + "set.failoverRemuxNote": "Mengaktifkannya menambah satu tahap remux antara masukan dan segala sesuatu di bawahnya. Murah, tetapi tidak gratis, dan itulah sebabnya ini sebuah keputusan dan bukan nilai bawaan.", + "set.fileSizeMb": "Ukuran berkas (MB)", + "set.filesKept": "Berkas yang disimpan", + "set.goingLive": "Mulai siaran", + "set.goingLiveDesc": "Apa yang terjadi ketika beberapa tujuan mulai bersamaan.", + "set.graceNote": "Berapa lama sumber langsung boleh tidak mengirim apa pun sebelum tingkatan beralih. Sengaja lebih panjang daripada satu penyambungan ulang — encoder yang membangun kembali koneksi RTMP adalah operasi normal, bukan kegagalan.", + "set.gracePeriod": "Masa tenggang (detik)", + "set.haDiscovery": "Penemuan Home Assistant", + "set.height": "Tinggi", + "set.ingest": "Masukan", + "set.ingestDesc": "SRT membawa hingga enam trek audio. RTMP membawa satu, dan ada sebagai cadangan bagi encoder yang tidak bisa SRT. Mode tarik menghubungi sumber alih-alih menunggu dikirimi.", + "set.ingestUrlCopied": "URL masukan disalin.", + "set.instance": "Instans", + "set.intervalNote": "Keadaan yang tidak berubah tidak diterbitkan ulang, jadi interval pendek berbiaya satu perbandingan alih-alih satu pesan. Keep-alive-lah yang membatasi berapa lama tautan mati luput dari perhatian — dan karenanya seberapa cepat broker memberi tahu pelanggan bahwa instans ini sudah lenyap.", + "set.issuer": "Penerbit", + "set.kbps": "kbps", + "set.keepAlive": "Keep-alive (d)", + "set.latencyMs": "Latensi (ms)", + "set.listeners": "Pendengar", + "set.listenersDesc": "Tempat server mengikat diri, untuk seluruh instalasi. Satu porta SRT melayani setiap sumber — mereka dibedakan oleh token publikasinya, jadi menambah sumber tidak pernah berarti membuka porta lain. RTMP tidak punya perutean semacam itu, sehingga portanya melayani satu sumber.", + "set.logsBoundNote": "Direktori log dibatasi oleh hasil kali kedua nilai ini, jadi penyimpanan log tidak akan pernah menjadi penyebab disk penuh.", + "set.logsDesc": "stderr FFmpeg yang ditangkap selalu disimpan di memori, tetapi memori itu mati bersama server — sehingga hilang justru baris-baris yang menjelaskan mengapa ia mati. Menyimpannya secara permanen akan mencerminkannya ke berkas bergilir di bawah direktori data.", + "set.metersDesc": "Satu proses pendamping mengukur setiap kanal dari setiap trek. Interval lebih pendek memperbarui lebih cepat dan menghabiskan lebih banyak CPU.", + "set.mode": "Mode", + "set.modePull": "Tarik — hubungi kamera, umpan atau berkas", + "set.modeRtmp": "RTMP — trek tunggal", + "set.modeSrt": "SRT — multitrek (disarankan)", + "set.mqtt": "Telemetri MQTT", + "set.mqtt5Only": "Hanya MQTT 5.0.", + "set.mqtt5OnlyNote": "Broker yang dipatok pada 3.1.1 tidak akan menyelesaikan koneksi sama sekali — ini bukan mode dengan kemampuan berkurang. Mosquitto 2.x, EMQX, dan add-on Home Assistant semuanya berbicara 5.0.", + "set.mqttDesc": "Menerbitkan keadaan ke broker sebagai pesan retained, sehingga konsumen yang tidak terhubung saat sesuatu berubah tetap memperoleh jawaban terkini.", + "set.multiAccountNote": "Hubungkan platform yang sama lebih dari sekali untuk menyiarkan ke beberapa kanal — setiap akun menjadi tujuannya sendiri.", + "set.name": "Nama", + "set.newPassword": "Kata sandi baru", + "set.no": "tidak", + "set.noEncryption": "Tidak ada yang mengenkripsi koneksi ini. Kata sandi dan cookie sesi melintasi jaringan dalam teks polos — setel tls.mode: auto, atau ikat ke 127.0.0.1 di belakang proxy.", + "set.noLibsrt": "Build FFmpeg ini tidak mendukung SRT. Pasang build yang dikonfigurasi dengan --enable-libsrt, atau gunakan RTMP.", + "set.noTokens": "Belum ada token.", + "set.passphrase": "Frasa sandi", + "set.passphraseNote": "Mengaktifkan enkripsi AES. Biarkan kosong untuk tanpa enkripsi. SRT menolak frasa sandi di luar 10–79 karakter.", + "set.passphrasePlaceholder": "opsional — 10 hingga 79 karakter", + "set.password": "Kata sandi", + "set.passwordChanged": "Kata sandi diubah.", + "set.passwordMismatch": "Kata sandi baru tidak cocok.", + "set.passwordNote": "Kata sandi tidak pernah dikembalikan oleh API mana pun, jadi kotak ini kosong bahkan ketika sudah ada yang disetel. Biarkan kosong untuk mempertahankan kata sandi tersimpan; hapus dengan tombol di bawah.", + "set.platform": "Platform", + "set.platformsDesc": "Platform berbeda-beda dalam seberapa banyak hal ini yang bisa diotomatiskan polyemesis, dan perbedaan itu layak diketahui sebelum mulai alih-alih satu jam kemudian. Penyiarannya sendiri bekerja sama di mana pun — perutean audio per tujuan, varian, penyambungan ulang, dan meteran tidak bergantung pada kolom mana pun di bawah.", + "set.playLoop": "Putar sebuah lup saat tidak ada yang disiarkan", + "set.playLoopNote": "Kandidat kedua di samping kartu tunggu: berkas diputar alih-alih gambar diam. Keduanya bisa aktif — lihat urutan tingkatan dalam rancangan failover untuk mengetahui mana yang menang.", + "set.pointObs": "Arahkan OBS ke alamat ini.", + "set.preview": "Pratinjau", + "set.previewDesc": "Monitor HLS pada panel. Ini satu-satunya tempat polyemesis mengodekan ulang video, dan ia berjalan pada langganan relainya sendiri sehingga tidak pernah bisa memengaruhi sebuah tujuan.", + "set.previewIdleNote": "Encoder mulai ketika panel meminta playlist dan berhenti lagi setelah selang waktu ini sejak permintaan terakhir, sehingga panel yang tertutup tidak memakan CPU. Mengubah nilai ini tidak pernah memulai ulang pratinjau yang sedang ditonton seseorang.", + "set.processLogs": "Log proses", + "set.proxyTerminatesTls": "Proxy terbalik di depan polyemesis mengakhiri TLS, jadi server ini sengaja menyajikan HTTP biasa.", + "set.publishInterval": "Interval penerbitan (d)", + "set.reconnectCap": "Batas atas jeda sambung ulang (d)", + "set.reconnectCapNote": "0 memakai nilai bawaan. Hanya sumber HTTP dan HLS.", + "set.redirectNote": "Harus cocok persis, termasuk skema dan porta. Jika Anda mencapai polyemesis lewat proxy terbalik, gunakan URL publik itu.", + "set.redirectUri": "URI pengalihan yang perlu diizinkan", + "set.removeCreds": "Hapus kredensial", + "set.removeCredsDesc": "Setiap akun yang terhubung melalui aplikasi ini berhenti memperbarui tokennya dan perlu dihubungkan ulang. Client secret tidak dapat dipulihkan dari sini — Anda harus membuat aplikasi baru di platform tersebut.", + "set.removeCredsTitle": "Hapus kredensial pengembang ini?", + "set.returnAfter": "Kembali setelah (detik stabil)", + "set.returnAutomatic": "Otomatis — kembali begitu sumber utama stabil", + "set.returnManual": "Manual — tetap di cadangan sampai saya beralih", + "set.returnManualNote": "Manual menjadi bawaan karena kembali secara otomatis bisa berayun-ayun: sumber utama yang pulih tersendat-sendat akan memindahkan program bolak-balik di tengah siaran.", + "set.rtmpPort": "Porta RTMP", + "set.rtspNote": "Pakai TCP kecuali kamera tidak mampu. RTSP melalui UDP menembus NAT tampak terhubung dan tidak mengirimkan apa pun.", + "set.rtspTransport": "Transport RTSP", + "set.saveAndDrop": "Simpan dan putuskan siaran langsung", + "set.saveIngest": "Simpan pengaturan masukan", + "set.segmentSec": "Segmen (d)", + "set.settingsSavedAffectedProcessesHave": "Pengaturan disimpan. Proses yang terpengaruh telah dimulai ulang.", + "set.showSlate": "Tampilkan kartu tunggu saat tidak ada yang disiarkan", + "set.slateColour": "Warna kartu tunggu", + "set.slateColourNote": "Satu kata saja, karena nilainya masuk ke graf filter tempat sebuah koma akan mengakhiri argumen. Dipakai hanya bila tidak ada gambar yang disetel.", + "set.slateColourPlaceholder": "black", + "set.slateImage": "Gambar kartu tunggu", + "set.slateImageNote": "Sebuah jalur di dalam direktori data. Biarkan kosong untuk mengecat warna polos sebagai gantinya — justru itulah alasan ia menjadi cadangan, karena sebuah warna tidak punya berkas yang bisa gagal dibuka.", + "set.slateImagePlaceholder": "slate/holding.png", + "set.sourceUrl": "URL sumber", + "set.sourceUrlNote": "Salah satu dari {schemes}. Sumber file:// adalah jalur RELATIF terhadap direktori data dan tidak boleh memuat “..” — ia dikurung di sana dengan cara yang sama seperti berkas yang diunggah.", + "set.sourceUrlPlaceholder": "rtsp://camera.local/stream1", + "set.srtPort": "Porta SRT", + "set.stagger": "Renggangkan koneksi (ms)", + "set.staggerNote": "0 berarti mati. Kalau tidak, mulai siaran berarti setiap tujuan membuka koneksi, merundingkan TLS, dan mulai mengodekan audio pada detak yang sama — pada mesin kecil itulah saat paling mungkin kehilangan bingkai, dan justru saat itulah Anda sedang memperhatikan, karena Anda baru saja mulai siaran. Beberapa ratus milidetik sudah lebih dari cukup.", + "set.staggerReconnectNote": "Ini tidak pernah menunda penyambungan ulang. Tujuan yang putus pukul 3 pagi kembali seketika alih-alih menunggu giliran di belakang proses yang sudah sehat.", + "set.stepByStep": "Penyiapan langkah demi langkah", + "set.stopAfterIdle": "Berhenti setelah menganggur (d)", + "set.streamKey": "Kunci siaran", + "set.streamKeyNote": "Pihak yang menyiarkan harus memakai jalur ini persis, sehingga porta yang terbuka bukan berarti pintu yang terbuka.", + "set.subject": "Subjek", + "set.synthesiseNote": "Setiap platform besar menolak siaran tanpa trek audio. Dengan ini aktif, masukan yang hanya berisi video memperoleh satu trek stereo senyap dan tujuan-tujuannya mulai berjalan; dengan ini mati, mereka menolak dengan “routing profile selects no audio”. Ini hanya berlaku pada masukan yang TERDETEKSI dengan nol trek, jadi tidak akan pernah menggeser audio yang sungguh-sungguh Anda kirim.", + "set.synthesiseSilent": "Sintesiskan trek senyap", + "set.syntheticAudio": "Audio sintetis", + "set.syntheticDesc": "Apa yang terjadi ketika masukan membawa video dan sama sekali tanpa audio.", + "set.tabIngest": "Masukan", + "set.tabPipeline": "Alur pemrosesan", + "set.tabPlatforms": "Kredensial platform", + "set.tabSecurity": "Keamanan", + "set.title": "Pengaturan", + "set.tlsAcme": "Let's Encrypt", + "set.tlsAuto": "Otomatis", + "set.tlsDesc": "Dikonfigurasi di config.yaml, bukan di sini — TLS harus benar sebelum server mulai mendengarkan. Kartu ini melaporkan hasilnya.", + "set.tlsManual": "Sertifikat manual", + "set.tlsOff": "Tidak diterminasi di sini", + "set.tlsSelfsigned": "Ditandatangani sendiri", + "set.tokenHashNote": "polyemesis hanya menyimpan hash. Jika Anda kehilangannya, cabut token ini dan buat yang baru.", + "set.tokenNamePlaceholder": "prometheus", + "set.tokenRevoked": "{name} dicabut.", + "set.topicPrefix": "Awalan topik", + "set.topicPrefixNote": "Pemisah di dalam awalan dipertahankan, jadi home/av berarti dua tingkat. Awalan yang dimulai dengan $ ditolak: broker mencadangkannya, dan pelanggan yang memakai # tidak pernah menerimanya — telemetri akan terbit dengan sukses dan tak terlihat justru pada tampilan yang akan Anda pakai untuk menelusuri masalah.", + "set.transportSecurity": "Keamanan transport", + "set.updateInterval": "Interval pembaruan (ms)", + "set.username": "Nama pengguna", + "set.validFor": "Berlaku untuk", + "set.whatPlatformsDo": "Apa yang bisa dilakukan tiap platform", + "set.whyDevApp": "Mengapa Anda perlu aplikasi pengembang sendiri", + "set.whyDevAppDesc": "polyemesis tidak dapat menyertakan client secret OAuth — siapa pun yang memiliki binernya akan mendapatkannya, dan platform akan mencabutnya. Anda mendaftarkan aplikasi pengembang gratis satu kali, menempelkan client ID dan secret di sini, lalu polyemesis mengambilkan kunci siaran Anda.", + "set.writeLogs": "Tulis log ke disk", + "set.yes": "ya", + "sources.addDescription": "Program kedua dengan masukan, tujuan, dan varian sendiri — misalnya kanvas vertikal di samping yang horizontal. Program ini dimulai pada porta bawaan, yang sebaiknya Anda ubah agar keduanya tidak bentrok.", + "sources.addFailed": "Tidak dapat menambahkan sumber", + "sources.addSource": "Tambah sumber", + "sources.addTitle": "Tambah sumber", + "sources.added": "“{name}” ditambahkan. Atur porta masukannya di bawah.", + "sources.app": "Aplikasi", + "sources.applyAndDrop": "Terapkan dan putuskan siaran", + "sources.copied": "{what} disalin", + "sources.copyFailed": "Tidak dapat menyalin {what}", + "sources.copyTokenAria": "Salin token", + "sources.copyUrlAria": "Salin URL {proto}", + "sources.default": "bawaan", + "sources.defaultDescription": "Permintaan yang tidak menyebut sumber akan berlaku pada yang ini.", + "sources.deleteAria": "Hapus {name}", + "sources.deleteConfirm": "Hapus sumber", + "sources.deleteDescription": "Tujuan dan variannya ikut terhapus — semuanya menjelaskan ke mana program ini dikirim dan tidak berarti apa-apa tanpanya. Rekaman tetap disimpan: berkasnya masih ada di disk dan tetap bisa diputar, hanya saja tidak lagi dikaitkan dengan sebuah sumber.", + "sources.deleteFailed": "Tidak dapat menghapus sumber", + "sources.deleteThis": "Hapus sumber ini", + "sources.deleteTitle": "Hapus “{name}”?", + "sources.deleted": "“{name}” dan tujuannya dihapus", + "sources.destinations": "Tujuan", + "sources.dirtyIdle": "Ada perubahan masukan yang belum disimpan. Menerapkan akan memulai ulang masukan sumber ini.", + "sources.dirtyPublishing": "Sebuah encoder sedang mengirim ke sumber ini. Menerapkan akan memulai ulang masukannya dan memutus semua yang sedang menonton.", + "sources.enableAria": "Aktifkan {name}", + "sources.help.app": "Jalur aplikasi RTMP — bagian URL antara host dan kunci siaran. Sebagian besar encoder mengharapkan “live”. Ubah hanya jika Anda punya alasan.", + "sources.help.ingest": "Bagaimana encoder Anda mencapai sumber ini. SRT bertahan pada koneksi yang banyak kehilangan paket dengan mengirim ulang bagian yang hilang, dan merupakan pilihan bawaan yang tepat melalui internet publik. RTMP lebih tua dan didukung di mana-mana, tetapi kualitasnya turun drastis pada sambungan yang tidak stabil. Dengan mode tarik, polyemesis mengambil sendiri siaran dari URL yang Anda berikan alih-alih menunggu dikirimi.", + "sources.help.latencyMs": "Berapa lama SRT menunggu paket yang terlambat atau dikirim ulang sebelum menyerah. Nilai lebih tinggi bertahan pada koneksi yang lebih buruk dengan konsekuensi penundaan: sekitar 2,5× waktu pulang-pergi adalah titik awal yang baik, jadi RTT 40 ms membutuhkan sekitar 100 ms di sini. Terlalu rendah, dan gangguan sesaat menjadi cacat yang terlihat.", + "sources.help.passphrase": "Mengenkripsi koneksi SRT, dan saat ini inilah yang benar-benar menjaga orang asing keluar dari masukan ini. Antara 10 dan 79 karakter. Biarkan kosong dan siapa pun yang dapat menjangkau porta tersebut bisa menyiarkan ke dalamnya.", + "sources.help.streamKey": "Rahasia yang dikirim encoder untuk menyiarkan lewat RTMP. Inilah kredensial yang menjaga masukan ini, jadi perlakukan seperti kata sandi: siapa pun yang memilikinya dapat menyiarkan ke tujuan Anda.", + "sources.help.token": "Mengidentifikasi sumber ini ketika beberapa sumber berbagi satu porta masukan. Apakah token juga mengatur akses bergantung pada mode masukan Anda — catatan di bawah menyatakan mana yang berlaku saat ini. Penggantian menerbitkan token baru dan menjaga yang lama berlaku lima menit agar encoder yang sedang siaran tidak terputus di tengah jalan.", + "sources.ingest": "Masukan", + "sources.lastSourceTitle": "Sumber terakhir tidak dapat dihapus — satu instalasi membutuhkan satu masukan", + "sources.latencyMs": "Latensi (ms)", + "sources.loadFailed": "Tidak dapat memuat sumber", + "sources.loading": "Memuat sumber…", + "sources.loss": "kehilangan", + "sources.modePull": "Tarik", + "sources.name": "Nama", + "sources.namePlaceholder": "Vertikal", + "sources.noEngine": "Tidak ada mesin yang berjalan untuk sumber ini. Penyebab yang lazim adalah porta masukan yang sudah dipakai oleh sumber lain atau proses lain.", + "sources.notRunning": "tidak berjalan", + "sources.ownDescription": "Tujuan, varian, dan rekaman miliknya sendiri.", + "sources.passphrase": "Frasa sandi", + "sources.passphrasePlaceholder": "10–79 karakter", + "sources.publishing": "menyiarkan", + "sources.renditions": "Varian", + "sources.retrans": "kirim ulang", + "sources.rotate": "Ganti", + "sources.rotateConfirm": "Ganti token", + "sources.rotateFailed": "Tidak dapat mengganti token", + "sources.rotateIdle": "Token baru diterbitkan segera. Token lama tetap berfungsi selama lima menit agar encoder yang sudah memakainya tidak terputus.", + "sources.rotateTitle": "Ganti token untuk “{name}”?", + "sources.rotateWhilePublishing": "Sebuah encoder sedang mengirim ke sumber ini saat ini. Token lama masih berfungsi selama lima menit, jadi tidak akan langsung terputus — tetapi harus diperbarui sebelum tenggang itu berakhir.", + "sources.rtt": "RTT", + "sources.running": "berjalan", + "sources.saveFailed": "Tidak dapat menyimpan", + "sources.streamKey": "Kunci siaran", + "sources.subtitle": "Masing-masing satu program masuk. Tujuan dan varian dimiliki oleh sebuah sumber.", + "sources.title": "Sumber", + "sources.token": "token", + "sources.tokenEnforcedDetailRtmp": "Ini adalah kunci siaran: siarkan ke URL di atas dengan ini sebagai kuncinya. Rotasi menerbitkan yang baru dan menjaga yang lama tetap berfungsi lima menit, sehingga Anda bisa berpindah tanpa memutus siaran.", + "sources.tokenEnforcedDetailSrt": "Masukkan ke streamid SRT encoder Anda untuk menyiarkan ke sumber ini lewat port bersama. Rotasi menerbitkan yang baru dan menjaga yang lama tetap berfungsi lima menit, sehingga Anda bisa berpindah tanpa memutus siaran.", + "sources.tokenIsCredential": "Token ini adalah kredensialnya.", + "sources.tokenIssued": "Token baru diterbitkan", + "sources.tokenNotEnforced": "Token ini disimpan tetapi saat ini tidak diberlakukan.", + "sources.tokenNotEnforcedRtmp": "Sumber dipisahkan berdasarkan porta. Yang benar-benar melindungi masukan ini adalah kunci siaran di atas. Aktifkan masukan porta tunggal di Pengaturan agar token menjadi kredensialnya.", + "sources.tokenNotEnforcedSrt": "Sumber dipisahkan berdasarkan porta. Yang benar-benar melindungi masukan ini adalah frasa sandi SRT di atas. Aktifkan masukan porta tunggal di Pengaturan agar token menjadi kredensialnya.", + "state.disabled": "Dinonaktifkan", + "state.failed": "Gagal", + "state.live": "Langsung", + "state.offline": "Luring", + "state.reconnecting": "Menghubungkan Kembali", + "state.starting": "Memulai", + "state.stopped": "Dihentikan" } diff --git a/ui/src/lib/i18n/it.json b/ui/src/lib/i18n/it.json index 7bdc8c9c..fbfad98a 100644 --- a/ui/src/lib/i18n/it.json +++ b/ui/src/lib/i18n/it.json @@ -1,26 +1,227 @@ { - "nav.dashboard": "Pannello di controllo", - "nav.sources": "Sorgenti", - "nav.meters": "Misuratori audio", - "nav.routing": "Routing", - "nav.renditions": "Versioni", - "nav.playout": "Playout", - "nav.library": "Libreria", - "nav.clips": "Clip", - "nav.chat": "Chat", - "nav.jobs": "Attività", - "nav.automation": "Automazione", - "nav.recordings": "Registrazioni", - "nav.monitoring": "Monitoraggio", - "nav.settings": "Impostazioni", - "chrome.toggleNav": "Attiva/disattiva navigazione", + "auth.accountCreated": "Account amministratore creato.", + "auth.confirmPassword": "Conferma password", + "auth.createAccount": "Crea account", + "auth.firstRun": "Configurazione iniziale", + "auth.firstRunDesc": "Scegli le credenziali dell'amministratore per questo server. Non esiste alcun recupero dell'account, quindi conservale in un posto sicuro.", + "auth.password": "Password", + "auth.passwordMismatch": "Le password non coincidono.", + "auth.signIn": "Accedi", + "auth.signInDesc": "Inserisci le tue credenziali di amministratore.", + "auth.somethingWentWrong": "Qualcosa è andato storto.", + "auth.tokenCreated": "Token API creato", + "auth.tokenRevoked": "Token API revocato", + "auth.username": "Nome utente", + "auto.action": "Azione", + "auto.adminPasswordChanged": "Password amministratore modificata", + "auto.chosenWeekdays": "Giorni scelti", + "auto.coalesced": "Raggruppati", + "auto.coalescingWindow": "Finestra di raggruppamento (secondi)", + "auto.couldNotChangeTheRule": "Impossibile modificare la regola.", + "auto.couldNotChangeTheSchedule": "Impossibile modificare la pianificazione.", + "auto.couldNotDeleteTheRule": "Impossibile eliminare la regola.", + "auto.couldNotDeleteTheSchedule": "Impossibile eliminare la pianificazione.", + "auto.couldNotLoadAutomationSettings": "Impossibile caricare le impostazioni di automazione.", + "auto.couldNotSaveTheRule": "Impossibile salvare la regola.", + "auto.couldNotSaveTheSchedule": "Impossibile salvare la pianificazione.", + "auto.createRule": "Crea regola", + "auto.createSchedule": "Crea pianificazione", + "auto.days": "Giorni", + "auto.delete": "Elimina", + "auto.deleteRule": "Elimina regola", + "auto.deleteRuleDesc": "La regola smette di scattare. Il suo endpoint webhook resta intatto e puoi ricreare la regola.", + "auto.deleteRuleTitle": "Eliminare questa regola di avviso?", + "auto.deleteSchedule": "Elimina pianificazione", + "auto.deleteScheduleDesc": "La pianificazione smette di essere eseguita. Ciò che ha già avviato non ne risente.", + "auto.deleteScheduleTitle": "Eliminare questa pianificazione?", + "auto.delivery": "Consegna", + "auto.destinationFallingBehindRealtime": "Destinazione in ritardo rispetto al tempo reale", + "auto.destinationKeepingUpAgain": "Destinazione di nuovo al passo", + "auto.destinationRecovered": "Destinazione ripristinata", + "auto.destinationWentDown": "Destinazione caduta", + "auto.destinations": "Destinazioni", + "auto.dropped": "Scartati", + "auto.edit": "Modifica", + "auto.editRule": "Modifica regola di avviso", + "auto.editSchedule": "Modifica pianificazione", + "auto.endpoint": "Endpoint", + "auto.evAudioClipping": "Clipping audio", + "auto.evClipCaptured": "Clip catturato", + "auto.evDiskLow": "Disco in esaurimento", + "auto.evDiskRecovered": "Disco ripristinato", + "auto.evIngestLost": "Ingresso perso", + "auto.evIngestRecovered": "Ingresso ripristinato", + "auto.evSettingsChanged": "Impostazioni modificate", + "auto.evSignedIn": "Accesso effettuato", + "auto.events": "Eventi", + "auto.everyDay": "Ogni giorno", + "auto.failed": "Fallite", + "auto.failoverSwitchedSource": "Il failover ha cambiato sorgente", + "auto.fired": "scattate", + "auto.firesAt": "Scatta alle", + "auto.lastDelivery": "Ultima consegna", + "auto.lastSweep": "Ultima scansione", + "auto.latenessAllowance": "Tolleranza di ritardo (secondi)", + "auto.loudnessBackInCompliance": "Loudness di nuovo conforme", + "auto.loudnessOutOfCompliance": "Loudness non conforme", + "auto.minGap": "Intervallo minimo tra i messaggi (secondi)", + "auto.minSeverity": "Gravità minima", + "auto.missedNote": "Un'occorrenza mancata perché il server era spento viene contrassegnata come gestita e saltata anziché scattare in ritardo — uno stream che parte da solo con quattro ore di ritardo è peggio di uno che non è mai partito.", + "auto.name": "Nome", + "auto.newRule": "Nuova regola di avviso", + "auto.newSchedule": "Nuova pianificazione", + "auto.next": "Prossima", + "auto.nothingFired": "Non è ancora scattato nulla. Lo scheduler esegue una scansione ogni 20 secondi.", + "auto.once": "Una volta", + "auto.payloadFormat": "Formato del payload", + "auto.pending": "In attesa", + "auto.repeatedFailedSignIns": "Accessi falliti ripetuti", + "auto.repeats": "Ripetizione", + "auto.retries": "Ritentativi", + "auto.rule": "Regola", + "auto.ruleCreated": "Regola di avviso creata. Inviale una prova.", + "auto.ruleDeleted": "Regola di avviso eliminata.", + "auto.rulePlaceholder": "Discord — avvisi dello show", + "auto.ruleSaved": "Regola di avviso salvata.", + "auto.saveRule": "Salva regola", + "auto.saveSchedule": "Salva pianificazione", + "auto.schedule": "Pianificazione", + "auto.scheduleCreated": "Pianificazione creata.", + "auto.scheduleDeleted": "Pianificazione eliminata.", + "auto.schedulePlaceholder": "Show infrasettimanale", + "auto.scheduleSaved": "Pianificazione salvata.", + "auto.schedules": "Pianificazioni", + "auto.secretMasked": "Mostrato mascherato perché il segreto si trova nel percorso. Lascialo com'è per mantenere l'endpoint attuale, oppure incolla un nuovo URL per sostituirlo.", + "auto.sendTest": "Invia avviso di prova", + "auto.sendTestTitle": "Invia subito un avviso di prova", + "auto.sent": "Inviate", + "auto.skipped": "saltate", + "auto.startDestinations": "Avvia le destinazioni", + "auto.startPlaylist": "Avvia la playlist", + "auto.stopDestinations": "Arresta le destinazioni", + "auto.stopPlaylist": "Arresta la playlist", + "auto.subscribedTo": "Iscritto a", + "auto.subtitle": "Webhook che ti avvisano quando qualcosa si rompe, e pianificazioni che mandano avanti lo show senza di te.", + "auto.theEndpointDidNotAccept": "L'endpoint non ha accettato il messaggio di prova.", + "auto.time": "Ora", + "auto.timeZone": "Fuso orario", + "auto.title": "Automazione", + "auto.tzPlaceholder": "UTC", + "auto.webhookHint": "Sia Discord che Slack te ne forniscono uno nelle impostazioni del canale.", + "auto.webhookPlaceholder": "https://discord.com/api/webhooks/…", + "auto.webhookRules": "Regole webhook", + "auto.webhookUrl": "URL del webhook", + "auto.weekFri": "Ven", + "auto.weekMon": "Lun", + "auto.weekSat": "Sab", + "auto.weekSun": "Dom", + "auto.weekThu": "Gio", + "auto.weekTue": "Mar", + "auto.weekWed": "Mer", + "auto.when": "Quando", + "chatpage.allFiltered": "Ogni piattaforma con messaggi è esclusa dai filtri.", + "chatpage.couldNotDeleteThatMessage": "Impossibile eliminare quel messaggio.", + "chatpage.deleteNote": "L'eliminazione rimuove il messaggio sulla piattaforma che lo ha emesso. Una piattaforma su cui polyemesis non può eliminare lo dichiara e indica dove farlo invece — l'azione non viene mai nascosta per supposizione.", + "chatpage.droppedTitle": "Mostrati in diretta, ma la memorizzazione è rimasta indietro e la cronologia li ha persi.", + "chatpage.dupTitle": "Lo stesso messaggio visto due volte — la nostra eco, o la piattaforma che lo ripropone.", + "chatpage.noAccount": "La chat è attiva, ma non è collegato alcun account di piattaforma. Collegane uno in Impostazioni → Credenziali piattaforma.", + "chatpage.notRunning": "Su questo server non è attiva alcuna chat. La cronologia sotto la timeline è quanto era stato memorizzato in precedenza, e la casella di invio resta disattivata finché non viene collegata una piattaforma.", + "chatpage.received": "Ricevuti", + "chatpage.reload": "Ricarica", + "chatpage.stored": "Memorizzati", + "chatpage.storedTitle": "Non è collegato nulla; questa è la cronologia memorizzata.", + "chatpage.subtitle": "Ogni piattaforma in un'unica cronologia, con una sola casella di invio.", + "chatpage.timeline": "Cronologia", + "chatpage.title": "Chat", "chrome.ingest": "Ingest", + "chrome.language": "Lingua", "chrome.liveCount": "{count} in diretta", + "chrome.signOut": "Disconnetti", "chrome.socketOffline": "Offline", "chrome.socketOfflineHint": "Aggiornamenti in diretta disconnessi; riprova", - "chrome.signOut": "Disconnetti", - "chrome.language": "Lingua", + "chrome.toggleNav": "Attiva/disattiva navigazione", + "clipedit.back33": "Indietro di 33 ms", + "clipedit.clickLine": "Clicca una riga per saltare lì. Selezionane più di una e i punti di in e out si spostano sulle parole che hai evidenziato.", + "clipedit.clipThisLine": "Ritaglia questa riga", + "clipedit.clipThisLineTitle": "Imposta i punti di in e out attorno a questa riga", + "clipedit.copied": "Copiato", + "clipedit.copiesEveryPacketSecondsBit": "Copia ogni pacchetto. Secondi, bit per bit, e l'inizio scatta indietro al keyframe più vicino.", + "clipedit.couldNotOpenThatRecording": "Impossibile aprire quella registrazione.", + "clipedit.couldNotQueueTheExport": "Impossibile mettere in coda l'esportazione.", + "clipedit.cut": "Taglio", + "clipedit.downloadClip": "Scarica il clip", + "clipedit.exportQueued": "Esportazione in coda. Verrà eseguita quando la macchina potrà permettersi i cicli.", + "clipedit.exports": "Esportazioni", + "clipedit.fast": "Veloce", + "clipedit.filterTranscript": "Filtra la trascrizione", + "clipedit.findSaid": "Trova ciò che è stato detto", + "clipedit.forward33": "Avanti di 33 ms", + "clipedit.in": "In", + "clipedit.keepAllTracks": "Mantieni tutte le tracce audio", + "clipedit.keepsEveryAudioTrackIn": "Mantiene tutte le tracce audio in qualsiasi codec. Ciò che scrive il registratore.", + "clipedit.markIn": "I — segna il punto di in alla testina di riproduzione", + "clipedit.markOut": "O — segna il punto di out alla testina di riproduzione", + "clipedit.name": "Nome", + "clipedit.noKeyframes": "Non è stato possibile leggere le posizioni dei keyframe, quindi nessuno può dire dove comincerà davvero questo taglio. Funzionerà comunque — la ricerca di FFmpeg atterra su un keyframe — ma l'inizio potrebbe anticipare fino a un GOP e questa pagina non può dirti di quanto.", + "clipedit.noTranscript": "Non è ancora stato trascritto nulla per questa registrazione. Ogni microfono è stato registrato su una traccia propria, quindi trascriverla ti dà i nomi di chi parla senza un modello di diarizzazione — e poi puoi ritagliare in base a ciò che è stato detto.", + "clipedit.nothingExported": "Ancora nulla di esportato.", + "clipedit.out": "Out", + "clipedit.pickAtLeastOneAudio": "Scegli almeno una traccia audio, oppure prendile tutte.", + "clipedit.playFaster": "L — riproduci, di nuovo per accelerare", + "clipedit.precise": "Preciso", + "clipedit.queueNote": "L'esportazione è in coda, non eseguita qui: cede il passo allo stream in diretta, così un taglio preso durante la trasmissione attende anziché rubare i cicli di cui lo stream ha bisogno.", + "clipedit.reEncodesOnlyTheLeading": "Ricodifica solo il GOP parziale iniziale, così l'inizio è esatto. Il resto viene comunque copiato.", + "clipedit.reencoded": "Ricodificato", + "clipedit.shortcuts": "J / K / L per lo shuttle, I e O segnano in e out, le frecce spostano di 33 ms e shift-frecce di un secondo. Il passo fine è un fotogramma a 30 fps; la frequenza reale di questa registrazione non è indicizzata, quindi è espresso in millisecondi anziché tirato a indovinare.", + "clipedit.shuttleBack": "J — shuttle all'indietro", + "clipedit.startMoved": "Inizio spostato", + "clipedit.starts": "Inizia", + "clipedit.stop": "K — ferma", + "clipedit.thatCutCouldNotBe": "Impossibile pianificare quel taglio.", + "clipedit.thatIsNotARecording": "Questo non è un id di registrazione.", + "clipedit.thatRecordingIsNotAvailable": "Quella registrazione non è disponibile.", + "clipedit.title": "Editor dei clip", + "clipedit.whatASocialPlatformWill": "Ciò che una piattaforma social accetta. Meno codec sopravvivono.", + "clipedit.zoomIn": "Ingrandisci", + "clipedit.zoomOut": "Riduci", + "clips.bitrate": "Bitrate", + "clips.buffer": "Buffer", + "clips.bufferLiveNote": "L'interruttore del buffer è un comando a caldo, non un'impostazione salvata: dopo un riavvio è di nuovo spento.", + "clips.captured": "Catturato", + "clips.ceiling": "Tetto", + "clips.clipsKept": "Clip conservati", + "clips.couldNotCaptureAClip": "Impossibile catturare un clip.", + "clips.couldNotChangeTheClip": "Impossibile modificare il buffer dei clip.", + "clips.couldNotDeleteTheClip": "Impossibile eliminare il clip.", + "clips.couldNotLoadClips": "Impossibile caricare i clip.", + "clips.covers": "Copre", + "clips.customLength": "Durata personalizzata del clip in secondi", + "clips.delete": "Elimina", + "clips.deleteDescription": "Un clip è di solito proprio ciò che volevi conservare. Questo rimuove il file; la registrazione da cui proviene resta intatta.", + "clips.deleteTitle": "Eliminare questo clip?", + "clips.deleted": "Clip eliminato.", + "clips.download": "Scarica", + "clips.everythingHeld": "Tutto ciò che il buffer sta trattenendo", + "clips.file": "File", + "clips.held": "Trattenuto", + "clips.inMemory": "In memoria", + "clips.keepBuffer": "Mantieni un buffer scorrevole", + "clips.keyframes": "Keyframe", + "clips.length": "Durata", + "clips.none": "nessuno", + "clips.off": "spento", + "clips.onDisk": "Su disco", + "clips.seen": "visto", + "clips.settings": "Impostazioni", + "clips.size": "Dimensione", + "clips.subtitle": "Un buffer scorrevole di ciò che è appena andato in onda. Catturalo a posteriori — non serve predisporre nulla in anticipo.", + "clips.theClipDoesNotStart": "Il clip non inizia su un keyframe.", + "clips.title": "Clip", + "clips.waitingForStream": "in attesa dello stream", + "clips.window": "Finestra", + "clips.windowSeconds": "Finestra (secondi)", "common.add": "Aggiungi", + "common.apply": "Applica", "common.cancel": "Annulla", "common.close": "Chiudi", "common.confirm": "Conferma", @@ -28,110 +229,921 @@ "common.copy": "Copia", "common.delete": "Elimina", "common.disabled": "Disabilitato", + "common.discard": "Scarta", "common.edit": "Modifica", "common.enabled": "Abilitato", "common.loading": "Caricamento…", + "common.moreInfo": "Maggiori informazioni", "common.refresh": "Aggiorna", "common.restart": "Riavvia", "common.retry": "Riprova", "common.save": "Salva", "common.start": "Avvia", "common.stop": "Arresta", - "state.live": "In diretta", - "state.reconnecting": "Riconnessione in corso", - "state.starting": "Avvio", - "state.failed": "Errore", - "state.stopped": "Interrotto", - "state.offline": "Offline", - "hooks.title": "Webhook di ciclo di vita", - "hooks.new": "Nuovo webhook", - "hooks.empty": "Nessun webhook. Aggiungine uno e polyemesis invierà in POST un corpo JSON firmato non appena lo stream parte o si ferma, o una destinazione va su o giù — una consegna per transizione, in ordine, perché uno script possa agire.", - "hooks.colWebhook": "Webhook", - "hooks.colEndpoint": "Endpoint", - "hooks.colTriggers": "Iscritto a", - "hooks.colEnabled": "On", - "hooks.signed": "firmato", - "hooks.unsigned": "NON FIRMATO", + "dash.audioTracks": "Tracce audio", + "dash.bitrate": "Bitrate", + "dash.broadcastSettings": "Impostazioni della diretta", + "dash.categoryPlaceholder": "Musica", + "dash.clipboardUnavailable": "Gli appunti non sono disponibili su questa origine.", + "dash.connectPrompt": "Collega un account YouTube, Twitch, Facebook o Kick in Impostazioni → Credenziali piattaforma per impostare titolo, descrizione e categoria dello stream su tutte le piattaforme in una volta sola. Ognuna ne accetta un sottoinsieme diverso — il compositore indica quale, e invia ciò che può.", + "dash.copyIngestUrl": "Copia l'URL di ingresso", + "dash.couldNotLoadTheDestination": "Impossibile caricare la destinazione.", + "dash.couldNotPushTheMetadata": "Impossibile inviare i metadati.", + "dash.couldNotReorderTheDestinations": "Impossibile riordinare le destinazioni.", + "dash.deleteDest": "Elimina destinazione", + "dash.deleteDestDescription": "Questo rimuove la destinazione e il suo profilo di routing. La piattaforma resta intatta e puoi riaggiungerla — ma il routing audio che avevi impostato per essa è perduto.", + "dash.descriptionPlaceholder": "Di cosa parla questo stream.", + "dash.destDeleted": "Destinazione eliminata.", + "dash.disabled": "disattivato", + "dash.everyConnectedAccount": "ogni account collegato", + "dash.idle": "inattivo", + "dash.ingestUrlCopied": "URL di ingresso copiato.", + "dash.keyRefreshed": "Chiave di stream aggiornata dalla piattaforma.", + "dash.leaveUnchanged": "Lascia invariato", + "dash.leaveUnchangedNote": "«Lascia invariato» non invia proprio nulla, quindi la piattaforma mantiene ciò che ha. Scegliere On o Off scrive quel valore.", + "dash.metaCategory": "Categoria", + "dash.metaDescription": "Descrizione", + "dash.metaTags": "Tag", + "dash.metaTitle": "Titolo", + "dash.noDestinations": "Ancora nessuna destinazione. Aggiungine una, poi scegli quali tracce audio riceve.", + "dash.off": "Off", + "dash.pipeline": "Catena di elaborazione", + "dash.pushToPlatforms": "Invia alle piattaforme", + "dash.pushing": "Invio…", + "dash.reconnects": "Riconnessioni", + "dash.relaySubscribers": "Sottoscrittori del relay", + "dash.renditions": "Versioni", + "dash.scheduledStart": "Inizio programmato", + "dash.subtitle": "Un solo ingresso, distribuito con video condiviso e audio per destinazione.", + "dash.tagsPlaceholder": "live, house, dj set", + "dash.theConnectedAccount": "l'account collegato", + "dash.title": "Pannello di controllo", + "dash.titlePlaceholder": "Set del venerdì sera", + "dash.uptime": "Tempo di attività", + "dash.youtubeStopsAcceptingTheseOnce": "YouTube smette di accettarli una volta che una diretta esce da «created» o «ready».", + "dest.audience": "Pubblico", + "dest.audioBitrate": "Bitrate audio", + "dest.audioCodec": "Codec audio", + "dest.channels": "Canali", + "dest.compliance": "Conformità", + "dest.connectedAccount": "Account collegato", + "dest.contentLabels": "Etichette dei contenuti", + "dest.copySource": "Copia il video sorgente", + "dest.createVariant": "Crea questa variante", + "dest.customiseSummary": "Personalizza per questa destinazione", + "dest.fbBackupCost": "Raddoppia la banda in upload di questa destinazione.", + "dest.fbBackupHelp": "Invia una seconda copia di questo stream all’ingresso di riserva di Facebook, così una connessione caduta non fa cadere la diretta.", + "dest.fbBackupLabel": "Pubblica un feed di riserva", + "dest.fbBackupReconnect": "Attivarlo riconnette lo stream una volta — quindi abilitalo prima di andare in diretta.", + "dest.fbBoxIntro": "Inviato nella stessa chiamata di creazione del Pubblico, quindi vale la stessa regola: applicato una volta quando la diretta inizia, e lasciato del tutto intatto se vuoto.", + "dest.fbBoxTitle": "Pubblicazione incrociata e pulsante di donazione di Facebook", + "dest.fbCrosspostAdd": "Aggiungi Pagina", + "dest.fbCrosspostAlsoPost": "Pubblica anche", + "dest.fbCrosspostHelp": "L’ID numerico della Pagina dalla console di Facebook (impostazioni della Pagina, oppure graph.facebook.com/me/accounts mentre sei connesso come Pagina) — qui non c’è alcuna ricerca, quindi incolla l’id anziché un nome o un URL. «Pubblica anche» pubblica come quella Pagina invece di limitarsi a condividere con essa la diretta; se disattivato, avviene solo la condivisione più discreta.", + "dest.fbCrosspostLabel": "Pubblicazione incrociata sulle Pagine", + "dest.fbCrosspostPageId": "ID della Pagina", + "dest.fbCrosspostRemove": "Rimuovi questa Pagina", + "dest.fbDonateHelp": "Anche questo è un id opaco dalla console delle raccolte fondi di Facebook, non un nome di ente da cercare. Lascialo vuoto per non allegare alcun pulsante di donazione.", + "dest.fbDonateLabel": "ID dell’ente del pulsante di donazione", + "dest.fbDonatePlaceholder": "ID dell’ente", + "dest.fillFromPlatform": "Compila con ciò che pubblica questa piattaforma", + "dest.friends": "Amici", + "dest.friendsOfFriends": "Amici di amici", + "dest.giveUpAfter": "Rinuncia dopo (tentativi)", + "dest.leaveOnFacebook": "Lascia com’è su Facebook", + "dest.leaveOnYouTube": "Lascia com’è su YouTube", + "dest.madeForKids": "Realizzato per bambini (COPPA)", + "dest.muxQueue": "Coda di muxing (pacchetti)", + "dest.muxQueuePair": "Questi due vanno insieme: FFmpeg applica il limite di pacchetti solo quando la coda supera la soglia in byte, quindi una soglia da sola non fa nulla e verrà rifiutata. Alzali entrambi se una destinazione segnala errori di interleave — il percorso audio qui ha latenza variabile, perché la normalizzazione del volume legge in anticipo.", + "dest.name": "Nome", + "dest.namePlaceholder": "YouTube — canale principale", + "dest.notLinked": "Non collegato", + "dest.onlyMe": "Solo io", + "dest.platform": "Piattaforma", + "dest.private": "Privato", + "dest.public": "Pubblico", + "dest.reconnecting": "Riconnessione", + "dest.retryAfter": "Riprova dopo (secondi)", + "dest.sharedEncodeAria": "Codifica video condivisa", + "dest.socketTimeout": "Timeout del socket (secondi)", + "dest.streamKeyLabel": "Chiave di streaming", + "dest.transport": "Trasporto", + "dest.transportFile": "File locale", + "dest.transportRtmp": "RTMP / RTMPS", + "dest.unlisted": "Non in elenco", + "dest.urlFromDashboard": "URL dalla dashboard", + "dest.useSharedEncode": "Usa una codifica video condivisa", + "dest.varBitrate": "Bitrate video (kbps)", + "dest.varFps": "Frequenza fotogrammi", + "dest.varHeight": "Altezza", + "dest.varSourceHint": "sorgente", + "dest.varWidth": "Larghezza", + "dest.variantWarning": "Avvia una seconda codifica dalla tua sorgente, partendo da «{name}». Non è una variante gratuita e non cambia ciò che ricevono le altre destinazioni su quella codifica. Lascia un campo vuoto per mantenere il valore della sorgente.", + "dest.videoTreatment": "Trattamento video", + "dest.visibility": "Visibilità", + "hooks.allTriggers": "tutto", "hooks.attempts": "{count} tentativi", + "hooks.attemptsLabel": "Tentativi", "hooks.attemptsOne": "{count} tentativo", - "hooks.timeout": "timeout di {seconds}s", - "hooks.allTriggers": "tutto", - "hooks.triggerCount": "{count} trigger", - "hooks.triggerCountOne": "{count} trigger", - "hooks.enableAria": "Abilita {name}", + "hooks.colEnabled": "On", + "hooks.colEndpoint": "Endpoint", + "hooks.colTriggers": "Iscritto a", + "hooks.colWebhook": "Webhook", + "hooks.create": "Crea", + "hooks.created": "Webhook creato.", + "hooks.deleteBody": "Smette di consegnare immediatamente. L’endpoint resta intatto e puoi ricreare l’hook — ma la sua chiave di firma non è recuperabile.", + "hooks.deleteConfirm": "Elimina webhook", + "hooks.deleteFailed": "Impossibile eliminare l’hook.", + "hooks.deleteTitle": "Eliminare questo webhook?", + "hooks.deleted": "Webhook eliminato.", "hooks.deliveries": "Consegne recenti", - "hooks.sendTest": "Invia consegna di prova", - "hooks.sendTestTitle": "Invia subito una consegna di prova", - "hooks.secretTitle": "Chiave di firma — copiala ora, non può essere mostrata di nuovo", - "hooks.secretCopied": "L’ho copiata", - "hooks.httpStatus": "HTTP {status}", - "hooks.noResponse": "nessuna risposta", + "hooks.deliveriesFailed": "Impossibile leggere le consegne recenti.", + "hooks.deliveryTitle": "Consegna", + "hooks.dialogIntro": "Un POST firmato per transizione, in ordine. A differenza di un avviso, nulla viene accorpato o filtrato — uno script riceve ogni fronte.", "hooks.durationMs": "{ms} ms", + "hooks.editTitle": "Modifica webhook", + "hooks.empty": "Nessun webhook. Aggiungine uno e polyemesis invierà in POST un corpo JSON firmato non appena lo stream parte o si ferma, o una destinazione va su o giù — una consegna per transizione, in ordine, perché uno script possa agire.", + "hooks.enableAria": "Abilita {name}", + "hooks.httpStatus": "HTTP {status}", + "hooks.loadFailed": "Impossibile caricare i webhook.", + "hooks.name": "Nome", + "hooks.new": "Nuovo webhook", + "hooks.newTitle": "Nuovo webhook", "hooks.noDeliveries": "Ancora nulla di consegnato. Le consegne compaiono qui man mano che avvengono le transizioni.", - "hooks.deliveryTitle": "Consegna", - "hooks.statSent": "Inviate", - "hooks.statFailed": "Fallite", + "hooks.noResponse": "nessuna risposta", + "hooks.payloadNote": "Payload {version}. Firma in {header}, su timestamp e corpo.", + "hooks.retriesHint": "I ritentativi bloccano la coda propria di questo endpoint, perché l’ordine è la promessa. Un 4xx non viene mai ritentato — un endpoint che dice che la richiesta è sbagliata lo dirà di nuovo.", + "hooks.saveFailed": "Impossibile salvare l’hook.", + "hooks.secretCopied": "L’ho copiata", + "hooks.secretTitle": "Chiave di firma — copiala ora, non può essere mostrata di nuovo", + "hooks.sendTest": "Invia consegna di prova", + "hooks.sendTestTitle": "Invia subito una consegna di prova", + "hooks.signed": "firmato", + "hooks.statDropped": "Scartate", "hooks.statEndpoints": "Endpoint", + "hooks.statFailed": "Fallite", "hooks.statQueued": "In coda", - "hooks.statDropped": "Scartate", "hooks.statRetries": "Ritentativi", - "hooks.payloadNote": "Payload {version}. Firma in {header}, su timestamp e corpo.", - "hooks.deleteTitle": "Eliminare questo webhook?", - "hooks.deleteBody": "Smette di consegnare immediatamente. L’endpoint resta intatto e puoi ricreare l’hook — ma la sua chiave di firma non è recuperabile.", - "hooks.deleteConfirm": "Elimina webhook", - "hooks.editTitle": "Modifica webhook", - "hooks.newTitle": "Nuovo webhook", - "hooks.dialogIntro": "Un POST firmato per transizione, in ordine. A differenza di un avviso, nulla viene accorpato o filtrato — uno script riceve ogni fronte.", - "hooks.name": "Nome", - "hooks.url": "URL dell’endpoint", - "hooks.urlMasked": "L’URL memorizzato non viene mai mostrato per intero. Lascia il valore mascherato per conservarlo, oppure digita un nuovo URL per sostituirlo.", - "hooks.triggers": "Trigger", - "hooks.triggersNoneHint": "Nessuno selezionato significa tutti i trigger, compresi quelli aggiunti da una versione futura.", + "hooks.statSent": "Inviate", + "hooks.testFailed": "L’endpoint non ha accettato la consegna di prova.", + "hooks.timeout": "timeout di {seconds}s", "hooks.timeoutLabel": "Timeout (s)", - "hooks.attemptsLabel": "Tentativi", - "hooks.retriesHint": "I ritentativi bloccano la coda propria di questo endpoint, perché l’ordine è la promessa. Un 4xx non viene mai ritentato — un endpoint che dice che la richiesta è sbagliata lo dirà di nuovo.", - "hooks.create": "Crea", - "hooks.triggerIngestPublished": "Stream avviato", - "hooks.triggerIngestDisconnected": "Stream fermato", - "hooks.triggerDestinationUp": "Destinazione live", - "hooks.triggerDestinationDown": "Destinazione ferma", - "hooks.loadFailed": "Impossibile caricare i webhook.", + "hooks.title": "Webhook di ciclo di vita", "hooks.toggleFailed": "Impossibile modificare l’hook.", - "hooks.deleted": "Webhook eliminato.", - "hooks.deleteFailed": "Impossibile eliminare l’hook.", - "hooks.testFailed": "L’endpoint non ha accettato la consegna di prova.", - "hooks.deliveriesFailed": "Impossibile leggere le consegne recenti.", + "hooks.triggerCount": "{count} trigger", + "hooks.triggerCountOne": "{count} trigger", + "hooks.triggerDestinationDown": "Destinazione ferma", + "hooks.triggerDestinationUp": "Destinazione live", + "hooks.triggerIngestDisconnected": "Stream fermato", + "hooks.triggerIngestPublished": "Stream avviato", + "hooks.triggers": "Trigger", + "hooks.triggersNoneHint": "Nessuno selezionato significa tutti i trigger, compresi quelli aggiunti da una versione futura.", + "hooks.unsigned": "NON FIRMATO", "hooks.updated": "Webhook aggiornato.", - "hooks.created": "Webhook creato.", - "hooks.saveFailed": "Impossibile salvare l’hook.", - "playlist.title": "Playlist", + "hooks.url": "URL dell’endpoint", + "hooks.urlMasked": "L’URL memorizzato non viene mai mostrato per intero. Lascia il valore mascherato per conservarlo, oppure digita un nuovo URL per sostituirlo.", + "jobs.alwaysKeepNewest": "Conserva sempre le più recenti", + "jobs.automaticModel": "Automatico (scelto in base a questo hardware)", + "jobs.backToDefault": "Torna alla modalità predefinita per questo tipo", + "jobs.batteryFloor": "Soglia minima batteria %", + "jobs.broadcastGoing": "È in corso una diretta; il lavoro differito le cede il passo.", + "jobs.cancel": "Annulla", + "jobs.cannotPause": "Questi tipi dovrebbero essere messi in pausa ma non possono esserlo, quindi stanno terminando.", + "jobs.completed": "Completate", + "jobs.concurrency": "Concorrenza", + "jobs.couldNotReadTheJob": "Impossibile leggere la coda delle attività.", + "jobs.couldNotSaveThePolicy": "Impossibile salvare il criterio.", + "jobs.cpuCeiling": "Tetto CPU %", + "jobs.cpuResume": "Ripresa CPU %", + "jobs.defaultMode": "Modalità predefinita", + "jobs.defaultModel": "Modello predefinito", + "jobs.deferred": "Differite", + "jobs.failed": "Fallite", + "jobs.finished": "Terminate", + "jobs.global": "Globale", + "jobs.gpuGateNote": "Solo i tipi contrassegnati come utilizzatori della GPU sono limitati da questo.", + "jobs.history": "Cronologia", + "jobs.historyRetention": "Conservazione della cronologia", + "jobs.holdDeferredWorkBackOn": "Trattieni il lavoro differito su un portatile in scarica al di sotto di questo valore. 0 lo disattiva.", + "jobs.hostCpuLoad": "Carico CPU dell'host", + "jobs.howManyJobsMayRun": "Quante attività possono essere eseguite insieme. Una, perché una seconda transcodifica compra un throughput che nessuno ha chiesto a un rischio che nessuno ha accettato.", + "jobs.inQueue": "In coda", + "jobs.job": "Attività", + "jobs.keepForDays": "Conserva per giorni", + "jobs.machineSafety": "Sicurezza della macchina. È l'unico limite a cui obbedisce anche il lavoro in tempo reale.", + "jobs.niceLevel": "Livello nice", + "jobs.noFinished": "Ancora nessuna attività terminata.", + "jobs.noModels": "Nessun file di modello è stato trovato nella directory dei modelli. Automatico scaricherà quello che sceglie; indicare qui un modello specifico funziona solo una volta che quel file esiste.", + "jobs.nothingNewStartsAboveThis": "Al di sopra di questo valore non parte nulla di nuovo. 0 disattiva il blocco.", + "jobs.nothingQueued": "Nulla in coda. La post-produzione si invia dalla libreria.", + "jobs.nothingWasOldEnoughTo": "Nulla era abbastanza vecchio da eliminare. Abbassa prima la conservazione in Criterio.", + "jobs.pauseAll": "Metti tutto in pausa", + "jobs.policy": "Criterio", + "jobs.policySaved": "Criterio delle risorse salvato.", + "jobs.progress": "Avanzamento", + "jobs.queuePaused": "Coda in pausa.", + "jobs.queueResumed": "Coda ripresa.", + "jobs.queued": "In coda", + "jobs.recording": "Registrazione", + "jobs.releasedItWillStartAs": "Rilasciata. Partirà non appena si libera uno slot.", + "jobs.removeFromHistory": "Rimuovi dalla cronologia", + "jobs.removeWindow": "Rimuovi finestra", + "jobs.resumeAll": "Riprendi tutto", + "jobs.retry": "Riprova", + "jobs.retryTitle": "Riarma con un nuovo budget di tentativi", + "jobs.runNow": "Esegui ora", + "jobs.runNowTitle": "Esegui subito questa, ignorando il criterio delle risorse", + "jobs.running": "In esecuzione", + "jobs.savedTheConcurrencyChangeTakes": "Salvato. La modifica della concorrenza ha effetto al riavvio del server — la coda lo risolve all'avvio.", + "jobs.state": "Stato", + "jobs.stopsEverythingRealtimeIncludedA": "Ferma tutto, tempo reale incluso: una CPU in throttling ha già iniziato a degradare lo stream. 0 lo disattiva.", + "jobs.subtitle": "Lavoro in background: trascrizione, proxy, miniature e compressione dell'archivio.", + "jobs.thatDidNotWork": "Non ha funzionato.", + "jobs.thermalCeiling": "Tetto termico °C", + "jobs.thisManyFinishedJobsSurvive": "Questo numero di attività terminate sopravvive a prescindere dalla loro età.", + "jobs.title": "Attività", + "jobs.transcription": "Trascrizione", + "jobs.unsavedChanges": "Modifiche non salvate.", + "jobs.whereTheCeilingReleasesThe": "Dove il tetto rilascia. Il divario è l'isteresi che impedisce al blocco di oscillare.", + "jobs.whisperMissing": "whisper.cpp non è installato qui, quindi questo non ha ancora effetto. Vale comunque la pena impostarlo: la scelta si applica non appena lo sarà.", + "jobs.why": "Perché", + "jobs.windowEnd": "Fine della finestra", + "jobs.windowStart": "Inizio della finestra", + "jobs.yieldNote": "Il lavoro pesante viene messo in coda e cede il passo allo stream in diretta. Un fotogramma perso è irrecuperabile; una trascrizione che arriva un'ora dopo non costa nulla.", + "lib.anySpeaker": "Qualsiasi parlante", + "lib.anyTag": "Qualsiasi tag", + "lib.autoGrouped": "Raggruppata automaticamente. Modificarla la rende tua.", + "lib.autoGroupedNote": "Questa sessione è stata raggruppata automaticamente. Salvarla la rende tua, e il passaggio di raggruppamento smetterà di riscriverla.", + "lib.bestMatch": "Corrispondenza migliore", + "lib.clearSearch": "Svuota la ricerca", + "lib.clip": "Ritaglia", + "lib.clipTitle": "Ritaglia un clip da questa registrazione", + "lib.collapseSession": "Comprimi la sessione", + "lib.couldNotLoadTheLibrary": "Impossibile caricare la libreria.", + "lib.couldNotLoadTheSession": "Impossibile caricare i segmenti della sessione.", + "lib.couldNotQueueTheProxy": "Impossibile mettere in coda il proxy.", + "lib.couldNotRegroup": "Impossibile raggruppare di nuovo.", + "lib.couldNotSaveTheSession": "Impossibile salvare la sessione.", + "lib.delete": "Elimina", + "lib.deleteDescription": "Il file viene rimosso dal disco, insieme ai suoi proxy, alle miniature e alla trascrizione. L'operazione non è reversibile.", + "lib.deleteTitle": "Eliminare questa registrazione?", + "lib.downloadMaster": "Scarica il master", + "lib.downloadMasterTitle": "Scarica il master multitraccia", + "lib.editSession": "Modifica sessione", + "lib.expandSession": "Espandi la sessione", + "lib.from": "Da", + "lib.generateProxy": "Genera proxy", + "lib.generateProxyTitle": "Genera il proxy riproducibile nel browser", + "lib.mostRecent": "Più recenti", + "lib.noProxyYet": "Ancora nessun proxy — apri per generarne uno", + "lib.noSegments": "Nessun segmento qui.", + "lib.noSessionMatch": "Nessuna sessione corrisponde a quei filtri.", + "lib.nothingRecorded": "Ancora nulla di registrato. Le sessioni compaiono man mano che il registratore scrive i segmenti.", + "lib.oldestFirst": "Prima i più vecchi", + "lib.playProxy": "Riproduci il proxy", + "lib.proxyAlreadyQueued": "Un proxy è già in coda.", + "lib.proxyQueued": "Proxy in coda.", + "lib.recordingDeleted": "Registrazione eliminata.", + "lib.regroupTitle": "Riesegue il raggruppamento. Additivo e idempotente: non unisce mai un raggruppamento che hai diviso a mano.", + "lib.searchPlaceholder": "Cerca tutto ciò che qualcuno ha detto, in ogni registrazione…", + "lib.searchTranscripts": "Cerca nelle trascrizioni", + "lib.searchable": "Ricercabile", + "lib.searchableOnly": "Segmenti con una trascrizione, quindi ricercabili", + "lib.session": "Sessione", + "lib.sessionDescription": "Descrizione", + "lib.sessionSaved": "Sessione salvata.", + "lib.sessionTags": "Tag", + "lib.sessionTitle": "Titolo", + "lib.subtitle": "Ogni diretta, ricercabile. Ogni microfono è stato registrato su una traccia propria, quindi la trascrizione sa già chi ha parlato.", + "lib.tag": "Tag", + "lib.tagsPlaceholder": "separati, da virgole", + "lib.title": "Libreria", + "lib.transcribe": "Trascrivi", + "lib.transcribeEachTrackOnIts": "Trascrivi ogni traccia separatamente — è questo che dà l'attribuzione dei parlanti", + "lib.ungrouped": "Segmenti non raggruppati", + "meters.deviation": "Scostamento", + "meters.idle": "inattivo", + "meters.integrated": "Integrata", + "meters.loudness": "Conformità di loudness", + "meters.metering": "misurazione", + "meters.momentary": "Momentanea", + "meters.monitorOff": "Il monitor è spento. Accendilo per misurare ciò che ogni destinazione sta consegnando.", + "meters.noSignal": "nessun segnale su questa traccia", + "meters.nothingToMeasure": "Ancora nulla da misurare. Ogni destinazione in esecuzione ottiene il proprio analizzatore EBU R128 sul mix che riceve.", + "meters.range": "Intervallo", + "meters.shortTerm": "Breve termine", + "meters.subtitle": "Ogni canale di ogni traccia in ingresso, direttamente dal relay.", + "meters.title": "Misuratori audio", + "meters.truePeak": "Picco reale", + "meters.waitingForStream": "in attesa dello stream", + "mon.allProcesses": "Tutti i processi", + "mon.bitrateChart": "Bitrate in ingresso — ultimi 30 minuti", + "mon.chooseDestination": "Scegli una destinazione…", + "mon.clearLog": "Svuota il log", + "mon.clearedThisDestinationIsBack": "Azzerato. Questa destinazione torna al comando generato.", + "mon.dryRun": "Prova a vuoto", + "mon.dryRunInvalid": "FFmpeg ha rifiutato un argomento", + "mon.dryRunOk": "FFmpeg l'ha accettato", + "mon.extraInput": "Argomenti di ingresso aggiuntivi", + "mon.extraOutput": "Argomenti di uscita aggiuntivi", + "mon.ffmpegLog": "Log FFmpeg", + "mon.fromRunningProcess": "dal processo in esecuzione", + "mon.fullCommand": "Comando risolto completo", + "mon.hostCpu": "CPU dell'host", + "mon.ingestLoss": "Perdita in ingresso", + "mon.memory": "Memoria", + "mon.nothingProvenEitherWay": "Nulla dimostrato in un senso o nell'altro", + "mon.pid": "PID", + "mon.processes": "Processi", + "mon.rebuilt": "ricostruito", + "mon.relayDrops": "Scarti del relay", + "mon.relayIn": "Ingresso relay", + "mon.resolveHint": "Risolvi il comando per vedere esattamente cosa verrà eseguito prima di applicare.", + "mon.restarts": "Riavvii", + "mon.subtitle": "Salute dei processi, risorse dell'host e il log FFmpeg in tempo reale.", + "mon.title": "Monitoraggio", + "mon.uptime": "Tempo di attività", + "nav.automation": "Automazione", + "nav.chat": "Chat", + "nav.clips": "Clip", + "nav.dashboard": "Pannello di controllo", + "nav.jobs": "Attività", + "nav.library": "Libreria", + "nav.meters": "Misuratori audio", + "nav.monitoring": "Monitoraggio", + "nav.playout": "Playout", + "nav.recordings": "Registrazioni", + "nav.renditions": "Versioni", + "nav.routing": "Routing", + "nav.settings": "Impostazioni", + "nav.sources": "Sorgenti", + "play.acrossEveryRungTheBackstop": "Su tutti i gradini. La protezione che impedisce a una lunga sessione di riempire il disco.", + "play.allowCrossOrigin": "Consenti cross-origin", + "play.allowCrossOriginPlayback": "Consenti la riproduzione cross-origin", + "play.audioBitrate": "Bitrate audio (kbps)", + "play.audioTrack": "Traccia audio", + "play.basicPasswordNote": "I player che non possono seguire un link (un decoder, una CDN) possono invece inviare lo stesso segreto come password HTTP basic.", + "play.boundsTheViewerTableViewers": "Limita la tabella degli spettatori. Gli spettatori oltre il limite vengono serviti, semplicemente non contati.", + "play.carriesThePlaybackTokenAnyone": "Contiene il token di riproduzione. Chiunque abbia questo URL può guardare.", + "play.clipboardUnavailable": "Gli appunti non sono disponibili; seleziona e copia a mano", + "play.couldNotLoadPlayout": "Impossibile caricare il playout", + "play.disabled": "Disabilitato", + "play.diskCap": "Tetto del disco (MB)", + "play.dvrWindow": "Finestra DVR (s)", + "play.embedCode": "Codice di incorporamento", + "play.enable": "Abilita il playout", + "play.enabled": "Abilitato", + "play.forObsACdnOr": "Per OBS, una CDN o qualsiasi player che parli HLS.", + "play.format": "Formato", + "play.hls": "HLS", + "play.hlsDash": "HLS e DASH", + "play.hlsPlaylist": "Playlist HLS", + "play.idleTimeout": "Timeout di inattività dello spettatore (s)", + "play.linkReset": "Nuovo link di riproduzione generato. Il precedente non funziona più.", + "play.linksProtected": "Questi link funzionano solo per un amministratore connesso finché non attivi Pubblico.", + "play.linksPublic": "Questi link funzionano senza un account.", + "play.liveWindow": "Finestra live (segmenti)", + "play.metaDescription": "Descrizione", + "play.metaTitle": "Titolo", + "play.mustExceedOneSegmentOr": "Deve superare un segmento, altrimenti gli spettatori scompaiono tra un polling e l'altro.", + "play.name": "Nome", + "play.noCredentialNote": "Non è richiesta alcuna credenziale di alcun tipo. Adatto solo a uno stream che intendi far vedere a tutta internet.", + "play.noRungs": "Ancora nessun gradino. Aggiungine uno per pubblicare qualcosa.", + "play.onDisk": "Su disco", + "play.oneStereoAacTrackPer": "Una traccia AAC stereo per gradino.", + "play.packaging": "Pacchettizzazione", + "play.passthrough": "Ingresso (passthrough)", + "play.peak": "Picco", + "play.playerPage": "Pagina del player", + "play.posterNote": "Mostrato sopra il video nella pagina pubblica. Il poster è un keyframe recente, aggiornato automaticamente.", + "play.private": "Privato.", + "play.protectedNote": "La riproduzione è protetta per impostazione predefinita. Aprirla è un passo separato e deliberato.", + "play.protection": "Protezione", + "play.public": "Pubblico", + "play.resetFailed": "Impossibile reimpostare", + "play.restartNote": "Modificare uno qualsiasi di questi riavvia i muxer, il che interrompe gli spettatori per qualche secondo.", + "play.rungNote": "Ogni gradino copia il video della sua versione bit per bit, quindi aggiungerne uno costa un muxer e mai una seconda codifica video. Solo la traccia audio cambia da un gradino all'altro; il routing per destinazione resta intatto.", + "play.segmentLength": "Durata del segmento (s)", + "play.serveWithoutSession": "Servi gli spettatori senza una sessione", + "play.sessionCap": "Tetto delle sessioni", + "play.sessions": "Sessioni", + "play.share": "Condividi", + "play.shorterIsLowerLatencyAnd": "Più corto significa meno latenza e più aggiornamenti della playlist.", + "play.subtitle": "Servi lo stream direttamente agli spettatori, come HLS pubblico.", + "play.threeIsTheMinimumA": "Tre è il minimo su cui un player può bufferizzare.", + "play.title": "Playout", + "play.unlisted": "Non elencato.", + "play.untitled": "Stream senza titolo", + "play.videoSource": "Sorgente video", + "play.watchingNow": "Stanno guardando ora", + "play.whoCanWatch": "Chi può guardare", + "player.copied": "Copiato", + "player.copy": "Copia", + "player.loading": "Caricamento…", + "player.missingToken": "Al link che hai seguito manca il token di riproduzione, oppure il token è stato cambiato. Chiedi un link aggiornato a chi te lo ha condiviso.", + "player.notLive": "Nessuna diretta al momento", + "player.viewer": "spettatore", + "player.viewers": "spettatori", + "player.willStart": "Questa pagina inizierà a riprodurre da sola quando la trasmissione comincerà.", + "playlist.choose": "Scegli un caricamento", + "playlist.chooseAria": "Scegli un caricamento da aggiungere", "playlist.empty": "Ancora nulla in coda.", + "playlist.hint": "Caricati in Media. Un elemento nominato da questa playlist che poi perde il suo file compare sopra come da controllare, invece di fallire in silenzio la prossima volta che il failover lo raggiunge.", "playlist.itemsAria": "Elementi della playlist", - "playlist.stateReady": "Pronto", - "playlist.stateTranscoding": "Transcodifica", - "playlist.stateAttention": "Richiede attenzione", - "playlist.stateUnsaved": "Non ancora salvato", "playlist.missingUpload": "Questo caricamento non è più reperibile.", - "playlist.moveUp": "Sposta {name} su", "playlist.moveDown": "Sposta {name} giù", - "playlist.remove": "Rimuovi {name}", - "playlist.choose": "Scegli un caricamento", - "playlist.chooseAria": "Scegli un caricamento da aggiungere", + "playlist.moveUp": "Sposta {name} su", "playlist.noUploads": "Nessun caricamento disponibile", - "playlist.hint": "Caricati in Media. Un elemento nominato da questa playlist che poi perde il suo file compare sopra come da controllare, invece di fallire in silenzio la prossima volta che il failover lo raggiunge.", - "dest.fbBoxTitle": "Pubblicazione incrociata e pulsante di donazione di Facebook", - "dest.fbBoxIntro": "Inviato nella stessa chiamata di creazione del Pubblico, quindi vale la stessa regola: applicato una volta quando la diretta inizia, e lasciato del tutto intatto se vuoto.", - "dest.fbBackupLabel": "Pubblica un feed di riserva", - "dest.fbBackupHelp": "Invia una seconda copia di questo stream all’ingresso di riserva di Facebook, così una connessione caduta non fa cadere la diretta.", - "dest.fbBackupCost": "Raddoppia la banda in upload di questa destinazione.", - "dest.fbBackupReconnect": "Attivarlo riconnette lo stream una volta — quindi abilitalo prima di andare in diretta.", - "dest.fbCrosspostLabel": "Pubblicazione incrociata sulle Pagine", - "dest.fbCrosspostPageId": "ID della Pagina", - "dest.fbCrosspostAlsoPost": "Pubblica anche", - "dest.fbCrosspostRemove": "Rimuovi questa Pagina", - "dest.fbCrosspostAdd": "Aggiungi Pagina", - "dest.fbCrosspostHelp": "L’ID numerico della Pagina dalla console di Facebook (impostazioni della Pagina, oppure graph.facebook.com/me/accounts mentre sei connesso come Pagina) — qui non c’è alcuna ricerca, quindi incolla l’id anziché un nome o un URL. «Pubblica anche» pubblica come quella Pagina invece di limitarsi a condividere con essa la diretta; se disattivato, avviene solo la condivisione più discreta.", - "dest.fbDonateLabel": "ID dell’ente del pulsante di donazione", - "dest.fbDonatePlaceholder": "ID dell’ente", - "dest.fbDonateHelp": "Anche questo è un id opaco dalla console delle raccolte fondi di Facebook, non un nome di ente da cercare. Lascialo vuoto per non allegare alcun pulsante di donazione." + "playlist.remove": "Rimuovi {name}", + "playlist.stateAttention": "Richiede attenzione", + "playlist.stateReady": "Pronto", + "playlist.stateTranscoding": "Transcodifica", + "playlist.stateUnsaved": "Non ancora salvato", + "playlist.title": "Playlist", + "rec.delete": "Elimina", + "rec.deleteDescription": "Il file viene rimosso dal disco, insieme ai suoi proxy, alle miniature e alla trascrizione. L'operazione non può essere annullata.", + "rec.deleteFailed": "Impossibile eliminare la registrazione.", + "rec.deleteTitle": "Eliminare questa registrazione?", + "rec.deleted": "Registrazione eliminata.", + "rec.disabled": "disattivato", + "rec.disk": "Disco", + "rec.diskRecordings": "Registrazioni", + "rec.download": "Scarica", + "rec.downloadNamed": "Scarica {name}", + "rec.duration": "Durata", + "rec.empty": "Ancora nessuna registrazione. Attiva la registrazione a destra e avvia uno stream.", + "rec.enabled": "Attivata", + "rec.file": "File", + "rec.free": "Libero", + "rec.haltedDefault": "La registrazione è ferma: il volume è sotto la soglia di spazio libero.", + "rec.haltedRestarts": "Il registratore riparte da solo non appena si libera spazio.", + "rec.help.enabled": "Scrive su disco ogni ingresso mentre viene trasmesso, mantenendo tutte le tracce audio separate e non codificate. La registrazione è indipendente da ciò che invii alle piattaforme: disattivarla non incide sull'uscita in diretta, e attivarla costa spazio su disco anziché banda in upload.", + "rec.help.maxAgeHours": "Elimina le registrazioni più vecchie di questo numero di ore, indipendentemente dalla dimensione totale. Utile quando ti serve solo l'ultimo giorno o due. Imposta 0 per conservare tutto finché un altro limite non lo rimuove.", + "rec.help.maxGb": "Un tetto alla dimensione totale delle registrazioni che polyemesis conserva. Una volta superato, le più vecchie vengono eliminate finché il totale non rientra. Imposta 0 per nessun limite di dimensione: in tal caso solo l'età, o la soglia di spazio libero, limita la crescita.", + "rec.help.minFreeGb": "Uno stop netto: la registrazione si interrompe quando il volume scende sotto questo spazio libero e riprende da sola una volta liberato spazio. È l'unico limite che tiene conto dei file che polyemesis non ha scritto, quindi è l'impostazione che protegge davvero un disco condiviso dal riempirsi.", + "rec.help.segmentSeconds": "Quanto dura ogni file prima che il registratore ne inizi uno nuovo. Segmenti più brevi limitano ciò che un crash può costarti e rendono più facile spostare i singoli file, ma ne lasciano di più da gestire. I tagli cadono su un keyframe, quindi la durata reale varia di circa un secondo.", + "rec.help.stemFiles": "Scrive inoltre ogni traccia audio come file a sé accanto al master. Utile se monti in seguito e vuoi conduttore, ospiti e audio di gioco su canali separati. Moltiplica ciò che una sessione scrive all'incirca per il numero di tracce, ed è per questo che è disattivato per impostazione predefinita.", + "rec.help.stemFormat": "Entrambe le opzioni sono senza perdita, quindi nessuna costa qualità. FLAC occupa circa la metà ed è la scelta giusta a meno che uno strumento del tuo flusso di lavoro non riesca a leggerlo; WAV si apre con qualsiasi cosa. Le tracce con più di 8 canali passano automaticamente a WAV.", + "rec.hideStems": "Nascondi tracce separate", + "rec.keepForever": "0 = conserva per sempre.", + "rec.loadFailed": "Impossibile caricare le registrazioni.", + "rec.maxAgeHours": "Elimina dopo (ore)", + "rec.maxGb": "Conserva al massimo (GB)", + "rec.minFreeGb": "Ferma sotto lo spazio libero (GB)", + "rec.minFreeNote": "I limiti qui sopra vincolano solo ciò che ha scritto polyemesis; qualsiasi altra cosa sul volume può comunque riempirlo. 0 = nessuna soglia.", + "rec.noSizeLimit": "0 = nessun limite di dimensione.", + "rec.recording": "in registrazione", + "rec.retentionNote": "La conservazione viene eseguita ogni 30 secondi: prima le più vecchie, per età e poi per dimensione totale. Il segmento in corso di scrittura non viene mai eliminato.", + "rec.saveFailed": "Impossibile salvare le impostazioni.", + "rec.savePolicy": "Salva criterio di conservazione", + "rec.saved": "Impostazioni di registrazione salvate.", + "rec.segmentSeconds": "Durata del segmento (secondi)", + "rec.segments": "Segmenti", + "rec.settingsTitle": "Registrazione", + "rec.showStems": "Mostra tracce separate", + "rec.size": "Dimensione", + "rec.started": "Avviata", + "rec.stemFiles": "File di traccia separata", + "rec.stemFlac": "FLAC — senza perdita, circa la metà della dimensione", + "rec.stemFormat": "Formato tracce separate", + "rec.stemNote": "Scritto dallo stesso processo del master, così ogni traccia separata resta allineata al campione con le altre. Le tracce con più di 8 canali passano da sole a WAV.", + "rec.stemWav": "WAV — senza perdita, si apre ovunque", + "rec.stems": "Tracce separate", + "rec.stemsTitle": "{count} file di traccia separata, allineati al campione con il master", + "rec.stemsUsage": "Tracce separate ({count} file)", + "rec.subtitle": "L'archivio multitraccia completo: ogni traccia audio in ingresso conservata, non codificata.", + "rec.title": "Registrazioni", + "rec.tracks": "Tracce", + "rec.tracksTitle": "{count} tracce audio conservate", + "rec.used": "Usato", + "rec.volume": "Volume", + "rend.aspectBlurpad": "Riempimento sfocato", + "rend.aspectBlurpadHint": "Riempie lo spazio restante con una copia sfocata del fotogramma stesso, ritagliata per riempire. È la convenzione su cui si sono assestati i feed verticali.", + "rend.aspectCrop": "Ritaglia per riempire", + "rend.aspectCropHint": "Ritaglia al centro secondo la forma di destinazione, poi scala. I soggetti mantengono la loro dimensione sullo schermo; i bordi del fotogramma spariscono.", + "rend.aspectHandling": "Gestione delle proporzioni", + "rend.aspectPad": "Letterbox", + "rend.aspectPadHint": "Scala l'intero fotogramma per farlo entrare e riempie il resto con un colore pieno. Non si perde nulla, ma una sorgente 16:9 su una tela 9:16 è per lo più barre.", + "rend.aspectStretch": "Allunga per adattare", + "rend.aspectStretchHint": "Scala alla dimensione di destinazione e lascia distorcere l'immagine se la sorgente non concorda. È ciò che faceva ogni versione prima che esistessero le altre modalità.", + "rend.boxColour": "Colore del riquadro", + "rend.boxOpacity": "Opacità del riquadro (%)", + "rend.builtInFont": "Predefinito incorporato", + "rend.burnedText": "Testo impresso", + "rend.chooseHardware": "Scegli un encoder hardware, o un'uscita più piccola.", + "rend.colourBlack": "black", + "rend.colourWhite": "white", + "rend.costBeyond": "Codificarlo via software in tempo reale è oltre le possibilità della maggior parte delle macchine", + "rend.costBeyondDetail": "{label} è {mpps} per una singola codifica. x264 a questa dimensione di solito perde la corsa con il tempo reale, e una versione che resta indietro perde fotogrammi per ogni destinazione che la legge — e te ne accorgi a stream in corso.{machine} {escape}", + "rend.costBusy": "Aspettati diversi core occupati", + "rend.costBusyDetail": "{label} è {mpps} su {encoder}, una codifica in tempo reale pesante.{machine} Tieni d'occhio la velocità dell'encoder una volta in diretta: sotto 1,0× sta restando indietro e le destinazioni che dipendono da esso inizieranno a perdere fotogrammi. {escape}", + "rend.costMachineCores": " Questa macchina riporta {cores} core logici.", + "rend.costModest": "Codifica software modesta", + "rend.costModestDetail": "{label} è {mpps}, alla portata della maggior parte delle macchine.{machine} Resta comunque una codifica reale — condivisa da ogni destinazione che la seleziona, ma non gratuita.", + "rend.costNoNumbers": "Il costo non può ancora essere stimato", + "rend.costNoNumbersDetail": "Nulla ha sondato la sorgente, quindi «mantieni sorgente» non ha numeri alle spalle. Avvia l'ingresso e questo si riempirà.", + "rend.costOffloaded": "Scaricato su {encoder}", + "rend.costOffloadedDetail": "{label} ({mpps}) gira sull'encoder a funzione fissa della GPU, quindi costa poca CPU. Qualità e controllo del bitrate variano a seconda del driver, e ogni chip ha un tetto proprio — controlla l'uscita prima di uno show che conta.", + "rend.create": "Crea", + "rend.created": "Versione creata. Selezionala su una destinazione per avviare la codifica.", + "rend.custom": "Personalizzato…", + "rend.deintAll": "Ogni fotogramma", + "rend.deintAllHint": "Per le sorgenti interlacciate che non lo dichiarano. Molti bridge SDI e schede di acquisizione contrassegnano tutto come progressivo a prescindere da ciò che ricevono, e su quelli «solo i fotogrammi interlacciati» non fa nulla e sembra un'impostazione guasta.", + "rend.deintAuto": "Solo i fotogrammi interlacciati", + "rend.deintAutoHint": "Tocca solo i fotogrammi che la sorgente ha contrassegnato come interlacciati, così quelli progressivi passano intatti. La scelta giusta per qualsiasi contenuto misto.", + "rend.deintOff": "Off", + "rend.deintOffHint": "Giusto per le sorgenti progressive che ha quasi chiunque. Deinterlacciarne una la ammorbidisce senza alcun guadagno.", + "rend.deinterlace": "Deinterlaccia", + "rend.deleteFailed": "Impossibile eliminare la versione.", + "rend.deleteFallbackMany": "{count} destinazioni ripiegano su passthrough e riceveranno il video della sorgente invariato. Verifica che la sorgente rientri ancora in ciò che ogni piattaforma accetta.", + "rend.deleteFallbackOne": "{count} destinazione ripiega su passthrough e riceverà il video della sorgente invariato. Verifica che la sorgente rientri ancora in ciò che ogni piattaforma accetta.", + "rend.deleteNoDestinations": "Nessuno seleziona questa versione, quindi eliminarla non cambia alcuna destinazione.", + "rend.deleteRenditionTitle": "Eliminare {name}?", + "rend.editRendition": "Modifica versione", + "rend.empty": "Nessuna versione. Ogni destinazione è in passthrough, il che va benissimo finché una di esse non riesce a prendere la sorgente — per esempio un ingresso 4K60 che Twitch, Kick o X non accettano. Aggiungi una versione e punta quelle destinazioni su di essa.", + "rend.encoder": "Encoder", + "rend.encoderPreset": "Preset dell'encoder", + "rend.encoderRelatedFailed": "{name} non è stato proposto perché un encoder correlato ha fallito qui.", + "rend.encoderTestFailed": "{name} non ha superato una codifica di prova di un fotogramma su questa macchina.", + "rend.encoderTestOk": "{name} ha codificato un fotogramma di prova su questa macchina.", + "rend.evenNote": "0 su un asse mantiene quello della sorgente e preserva le proporzioni — imposta solo l'altezza per ridimensionare. Entrambi devono essere numeri pari: H.264 e HEVC non hanno un piano di crominanza di dimensione dispari.", + "rend.font": "Font", + "rend.frameRate": "Frequenza dei fotogrammi", + "rend.gop": "GOP", + "rend.gpuUnopenable": "È presente una GPU ma questo processo non riesce ad aprirla", + "rend.hardwareFallback": "hardware", + "rend.hardwareScanFound": "Cosa ha trovato la scansione dell'hardware", + "rend.height": "Altezza", + "rend.hevcWarning": "Questo encoder produce HEVC. La maggior parte delle destinazioni RTMP accetta solo H.264 — sceglilo solo per una destinazione che ti ha detto di accettare HEVC.", + "rend.keyframeInterval": "Intervallo tra i keyframe", + "rend.letterboxColour": "Colore del letterbox", + "rend.live": "In diretta", + "rend.loadFailed": "Impossibile caricare le versioni.", + "rend.marginX": "Margine X (%)", + "rend.marginY": "Margine Y (%)", + "rend.name": "Nome", + "rend.namePlaceholder": "1080p60 per Twitch e Kick", + "rend.needsBothAWidthAnd": "Servono sia una larghezza sia un'altezza — con un asse libero la scala preserva già le proporzioni.", + "rend.needsBothAxes": "Servono sia una larghezza sia un'altezza", + "rend.newRendition": "Nuova versione", + "rend.noEncoder": "Questo FFmpeg non ha riportato alcun encoder video utilizzabile.", + "rend.noHardwareEscape": "Nessun encoder hardware funziona su questa macchina, quindi non c'è nulla su cui spostarlo — l'unica leva qui è un'uscita più piccola. Rileva di nuovo l'hardware se nel frattempo hai installato un driver o passato una GPU nel container.", + "rend.notProbed": "Su questa macchina non è ancora stata fatta alcuna codifica di prova, quindi questo elenco è ciò che contiene la build di FFmpeg anziché ciò che l'hardware sa fare. Rileva di nuovo per scoprirlo.", + "rend.notRunning": "Non in esecuzione", + "rend.note": "Nota", + "rend.noteDuplicateFps": "La sorgente gira a {srcFps} fps, quindi {fps} fps duplica soltanto i fotogrammi.", + "rend.notePlaceholder": "A cosa serve questo livello.", + "rend.noteUpscale": "La sorgente è {width}×{height}. Ingrandire costa CPU e non aggiunge dettaglio.", + "rend.opacity": "Opacità (%)", + "rend.percentNote": "Tutto è una percentuale del fotogramma, quindi le stesse impostazioni sono corrette su un livello 16:9 e su uno 9:16. I margini sono ignorati su un asse centrato.", + "rend.preset": "Preset", + "rend.presetPlaceholder": "veryfast", + "rend.presetStartingPoint": "Punto di partenza — verifica i limiti attuali con la piattaforma.", + "rend.redetectFailed": "Impossibile rilevare di nuovo l'hardware.", + "rend.redetectTitle": "Riesegue la scansione della GPU e codifica un fotogramma di prova con ogni encoder. Richiede qualche secondo.", + "rend.redetectedNoHardware": "Hardware rilevato di nuovo. Nessun encoder hardware ha superato una codifica di prova su questa macchina.", + "rend.resolution": "Risoluzione", + "rend.restartEncode": "Riavvia questa codifica e le destinazioni che la leggono", + "rend.restartFailed": "Impossibile riavviare la codifica.", + "rend.restarting": "{name} si sta riavviando. Le sue destinazioni si riconnettono con essa.", + "rend.save": "Salva", + "rend.saveFailed": "Impossibile salvare la versione.", + "rend.seedNote": "Questi riempiono il modulo e nulla più — modifica ciò che vuoi prima di salvare. I tetti delle piattaforme cambiano e variano in base allo stato di partner.", + "rend.signalError": "Errore", + "rend.signalIdle": "Inattivo", + "rend.sizePct": "Dimensione (% dell'altezza)", + "rend.softwareEncoderFallback": "un encoder software", + "rend.speed": "Velocità", + "rend.subtitle": "Una sola codifica video condivisa, molte destinazioni. L'audio non viene mai toccato qui.", + "rend.target": "Obiettivo", + "rend.textColour": "Colore del testo", + "rend.textPlaceholder": "LA MIA EMITTENTE", + "rend.title": "Versioni", + "rend.unused": "Nessuna destinazione la seleziona ancora, quindi non si sta codificando nulla. Sceglila su una destinazione per avviare la codifica.", + "rend.uptime": "Tempo di attività", + "rend.videoBitrate": "Bitrate video", + "rend.waitingSnapshot": "In attesa della prima istantanea dal vivo.", + "rend.watermarkImage": "Immagine della filigrana", + "rend.watermarkPlaceholder": "overlays/logo.png", + "rend.width": "Larghezza", + "rend.widthPct": "Larghezza (% del fotogramma)", + "route.attack": "Attacco", + "route.audio": "audio", + "route.audioAndVideoGoOut": "Audio e video escono insieme.", + "route.audioLaterThanVideo": "Audio dopo il video", + "route.audioRouting": "Routing audio", + "route.audioRoutingDesc": "Scegli esattamente quali tracce in ingresso riceve ogni destinazione.", + "route.autoLimitWhen2Tracks": "Auto (limita con 2+ tracce)", + "route.commentaryLanguage": "Lingua del commento", + "route.couldNotApplyThePreset": "Impossibile applicare il preset.", + "route.couldNotCompileThisRouting": "Impossibile compilare questo routing.", + "route.couldNotLoadDestinations": "Impossibile caricare le destinazioni.", + "route.couldNotSaveTheRouting": "Impossibile salvare il routing.", + "route.couldNotSaveTrackRoles": "Impossibile salvare i ruoli delle tracce.", + "route.custom": "Personalizzato…", + "route.delayDirection": "Direzione del ritardo", + "route.destinations": "Destinazioni", + "route.duckingNote": "Abbassa un gruppo di tracce ogni volta che un altro sta parlando — il microfono sopra il tappeto musicale. Off significa che ogni traccia selezionata viene sommata al proprio guadagno, invariata.", + "route.duckingTriggerNote": "Un trigger non deve necessariamente far parte del mix di questa destinazione — un microfono può abbassare la musica qui mentre viene inviata solo la musica.", + "route.enableDucking": "Abilita il ducking", + "route.everyTrackReaches": "Ogni traccia selezionata raggiunge questa destinazione, musica compresa.", + "route.excludeMusic": "Escludi la musica", + "route.keepMusic": "Mantieni la musica", + "route.keepMusicOut": "Tieni la musica fuori da questa destinazione", + "route.labelTracks": "Etichetta le tracce", + "route.langPlaceholder": "lingua — ad es. es", + "route.limiter04DbfsCeiling": "Limiter (tetto −0,4 dBFS)", + "route.localOnlyNote": "Questo server non memorizza ancora i ruoli delle tracce, quindi questi restano in questo browser. Tutto il resto della pagina si salva normalmente.", + "route.loudnessTarget": "Obiettivo di loudness", + "route.lufsBroadcast": "EBU R128 broadcast", + "route.measuredOverTheWholeProgramme": "Misurato sull'intero programma, non momento per momento: i passaggi quieti restano quieti. Sono i numeri a cui puntano i normalizzatori delle piattaforme stesse, quindi centrarne uno è ciò che impedisce alla piattaforma di abbassarti il volume.", + "route.mixMatrix": "Matrice del mix", + "route.musicPolicy": "Criterio per la musica", + "route.noTarget": "Nessun obiettivo (lascia stare i livelli)", + "route.normalizeLoudnorm": "Loudness (EBU R128)", + "route.normalizeOff": "Off", + "route.off": "off", + "route.offset": "Offset", + "route.on": "on", + "route.platformDefault": "Predefinito della piattaforma", + "route.podcastSpokenWord": "Podcast / parlato", + "route.presetApplied": "Preset applicato. Rivedilo, poi salva.", + "route.range": "Intervallo", + "route.ratio": "Rapporto", + "route.release": "Rilascio", + "route.result": "Risultato", + "route.role51": "5.1", + "route.roleClean": "pulita", + "route.roleMic": "mic", + "route.roleMusic": "musica", + "route.rolesNote": "I ruoli descrivono l'ingresso, non questa destinazione, e si salvano per conto proprio. Nulla viene filtrato finché una destinazione più sotto non lo richiede.", + "route.simple": "Semplice", + "route.target": "Obiettivo", + "route.targetRole": "obiettivo", + "route.thisDestinationSPlatform": "La piattaforma di questa destinazione", + "route.threshold": "Soglia", + "route.title": "Routing", + "route.triggerRole": "trigger", + "route.truePeak": "Picco reale", + "route.video": "video", + "route.videoLaterThanAudio": "Video dopo l'audio", + "route.withoutATargetTheMix": "Senza un obiettivo il mix esce al livello prodotto dall'ingresso, con applicata solo la protezione dal clipping qui sopra.", + "route.youtubeTwitchSpotify": "YouTube, Twitch, Spotify", + "set.acceptSelfSigned": "Accetta un certificato autofirmato", + "set.accountDisconnected": "Account disconnesso.", + "set.adminPassword": "Password amministratore", + "set.alertDelivery": "Recapito degli avvisi", + "set.alertDeliveryDesc": "Con quanta insistenza viene inseguito un webhook che non risponde, prima di rinunciare all'avviso.", + "set.apiTokens": "Token API", + "set.apiTokensDesc": "Credenziali di lunga durata per script, Prometheus e qualsiasi altra cosa non possa mantenere una sessione. Inviane uno come Authorization: Bearer pmk_…. Un token può fare tutto ciò che fa l'amministratore, tranne gestire i token — quello resta dietro la password, quindi revocarne uno è definitivo.", + "set.app": "App", + "set.attemptsBackoff": "L'attesa tra i tentativi si allunga automaticamente fino a un tetto di 30 secondi, quindi la parte alta di questo intervallo equivale già a diversi minuti passati a inseguire un endpoint morto. Il limite è proprio il punto: riprovare all'infinito trasforma un singolo webhook morto in una coda che non si svuota mai.", + "set.attemptsNote": "Primo tentativo incluso. 1 significa nessun nuovo tentativo.", + "set.attemptsRetryable": "Vengono ritentati solo gli errori per cui vale la pena riprovare. Un 404 da un webhook che qualcuno ha cancellato è definitivo, e riprovarlo altre quattro volte ritarda soltanto ogni avviso in coda dietro di esso; un 502 da un proxy inverso è esattamente ciò per cui questi tentativi esistono.", + "set.audioMeters": "Misuratori audio", + "set.brokerPasswordCleared": "Password del broker cancellata.", + "set.brokerPlainNote": "mqtt:// non è cifrato. MQTT invia nome utente e password nel proprio pacchetto CONNECT, quindi entrambi attraversano la rete in chiaro — accettabile su una LAN fidata, che è il caso tipico di Home Assistant, e non accettabile su qualsiasi cosa non controlli tu. Usa mqtts:// se il broker lo offre.", + "set.brokerUrl": "URL del broker", + "set.brokerUrlNote": "mqtt://, mqtts://, ws:// o wss://. Una password nell'URL viene rifiutata: un URL finisce nelle righe di log e nell'output di ps, e non si torna indietro. Inseriscila nel campo qui sotto, dove viene cifrata a riposo.", + "set.brokerUrlPlaceholder": "mqtts://broker.local:8883", + "set.chatBothApply": "Valgono entrambi e vince il più generoso: un messaggio se ne va quando è più vecchio delle ore e fuori dagli N più recenti. Così un canale affollato conserva meno tempo di uno tranquillo a parità di impostazioni.", + "set.chatBufferNote": "«Invia alla connessione» è un numero di natura diversa dagli altri due. Quelli limitano ciò che resta su disco; questo è un buffer in memoria, allocato per ogni pannello connesso.", + "set.chatHistory": "Cronologia chat", + "set.chatHistoryDesc": "Quanto indietro arriva la cronologia della chat — e con essa quanto può vedere un moderatore quando apre la scheda di qualcuno.", + "set.chatHours": "Conserva per (ore)", + "set.chatKeep": "Conserva sempre (messaggi)", + "set.chatKeepNote": "Un minimo, qualunque sia la loro età.", + "set.chatNotOnlyDisk": "Non è solo un'impostazione di disco. Cliccare un nome in chat apre ciò che quella persona ha detto in precedenza, e quella cronologia viene da qui — nessuna piattaforma pubblica un'API per questo, quindi polyemesis può mostrare solo ciò che ha conservato. Impostalo troppo breve e chi crea problemi da una settimana sembrerà non aver mai detto nulla.", + "set.chatSendNote": "Cronologia senza query.", + "set.chatSendOnConnect": "Invia alla connessione (messaggi)", + "set.chatSendOnConnectShort": "Invia alla connessione", + "set.chatSmall": "La chat occupa poco. Un canale con una media di dieci messaggi al secondo memorizza circa 7 MB all'ora; la maggior parte è ben lontana da quella cifra.", + "set.clientId": "ID client", + "set.clientIdNote": "Deve essere unico sul broker. Una collisione è la causa numero uno di un ciclo di riconnessione inspiegabile: il broker disconnette la sessione più vecchia a ogni connessione, ed entrambi i client si riconnettono all'infinito.", + "set.clientIdPlaceholder": "derivato dal nome dell'istanza", + "set.clientSecret": "Client secret", + "set.comingBack": "Ritorno", + "set.confirmPassword": "Conferma la nuova password", + "set.connectedAccounts": "Account collegati", + "set.copied": "Copiato", + "set.copy": "Copia", + "set.couldNotChangeThePassword": "Impossibile modificare la password.", + "set.couldNotClearThePassword": "Impossibile cancellare la password.", + "set.couldNotCreateTheToken": "Impossibile creare il token.", + "set.couldNotReCheckThe": "Impossibile ricontrollare le credenziali.", + "set.couldNotReadTheTls": "Impossibile leggere lo stato TLS.", + "set.couldNotRevokeTheToken": "Impossibile revocare il token.", + "set.couldNotSaveTheCredentials": "Impossibile salvare le credenziali.", + "set.couldNotSaveTheMqtt": "Impossibile salvare le impostazioni MQTT.", + "set.credsRemoved": "Credenziali rimosse.", + "set.currentPassword": "Password attuale", + "set.dataDirectory": "Directory dei dati", + "set.deliveryAttempts": "Tentativi di recapito", + "set.dialsOnly": "Il server compone soltanto: {schemes}.", + "set.disconnect": "Disconnetti", + "set.disconnectDesc": "Le destinazioni che lo usano mantengono le impostazioni ma smettono di rinnovare la propria chiave di stream. Potrai ricollegare lo stesso account in seguito.", + "set.disconnectTitle": "Disconnettere questo account?", + "set.drawtextMissing": "Questa build non ha il filtro drawtext, quindi le sovrimpressioni di testo non sono disponibili. Serve un FFmpeg compilato con libfreetype.", + "set.drawtextOk": "Questa build può disegnare testo sul video.", + "set.enabled": "Attivo", + "set.encoderUrl": "URL dell'encoder", + "set.expires": "Scade", + "set.failover": "Failover", + "set.failoverDesc": "Un ingresso di riserva e una schermata di attesa, per quando la sorgente in diretta smette di consegnare. Disattivato per impostazione predefinita: da spento, la catena è byte per byte quella di prima che questo esistesse.", + "set.failoverRemuxNote": "Attivarlo aggiunge un passaggio di rimultiplexing tra l'ingresso e tutto ciò che sta sotto. Poco costoso, ma non gratuito, ed è per questo che è una decisione e non un valore predefinito.", + "set.fileSizeMb": "Dimensione file (MB)", + "set.filesKept": "File conservati", + "set.goingLive": "Andare in diretta", + "set.goingLiveDesc": "Cosa succede quando più destinazioni partono insieme.", + "set.graceNote": "Per quanto tempo la sorgente in diretta può non consegnare nulla prima che il livello cambi. Volutamente più lungo di una riconnessione: un encoder che ristabilisce una connessione RTMP è funzionamento normale, non un guasto.", + "set.gracePeriod": "Periodo di tolleranza (secondi)", + "set.haDiscovery": "Rilevamento Home Assistant", + "set.height": "Altezza", + "set.ingest": "Ingresso", + "set.ingestDesc": "SRT trasporta fino a sei tracce audio. RTMP ne trasporta una ed esiste come ripiego per gli encoder che non supportano SRT. Il prelievo compone una sorgente invece di aspettarne una.", + "set.ingestUrlCopied": "URL di ingresso copiato.", + "set.instance": "Istanza", + "set.intervalNote": "Uno stato invariato non viene ripubblicato, quindi un intervallo breve costa un confronto anziché un messaggio. Il keep-alive è ciò che limita per quanto tempo un collegamento morto passa inosservato — e quindi con quanta rapidità il broker comunica agli iscritti che questa istanza non c'è più.", + "set.issuer": "Emittente", + "set.kbps": "kbps", + "set.keepAlive": "Keep-alive (s)", + "set.latencyMs": "Latenza (ms)", + "set.listeners": "Listener", + "set.listenersDesc": "Dove si lega il server, per l'intera installazione. Una sola porta SRT serve ogni sorgente — si distinguono dal token di pubblicazione, quindi aggiungere una sorgente non significa mai esporre un'altra porta. RTMP non ha questo instradamento, perciò la sua porta serve una sola sorgente.", + "set.logsBoundNote": "La directory dei log è limitata dal prodotto di questi due valori, quindi la persistenza non può mai essere ciò che riempie il disco.", + "set.logsDesc": "Lo stderr catturato di FFmpeg è sempre tenuto in memoria, ma quella muore con il server — perdendo esattamente le righe che spiegano perché è morto. La persistenza lo replica in un file a rotazione dentro la directory dei dati.", + "set.metersDesc": "Un processo affiancato misura ogni canale di ogni traccia. Intervalli più bassi aggiornano più in fretta e costano più CPU.", + "set.mode": "Modalità", + "set.modePull": "Prelievo — componi una telecamera, un flusso o un file", + "set.modeRtmp": "RTMP — traccia singola", + "set.modeSrt": "SRT — multitraccia (consigliato)", + "set.mqtt": "Telemetria MQTT", + "set.mqtt5Only": "Solo MQTT 5.0.", + "set.mqtt5OnlyNote": "Un broker fissato al 3.1.1 non completerà affatto una connessione: non è una modalità ridotta. Mosquitto 2.x, EMQX e il componente aggiuntivo di Home Assistant parlano tutti 5.0.", + "set.mqttDesc": "Pubblica lo stato su un broker come messaggi retained, così che un consumatore non connesso al momento di un cambiamento riceva comunque la risposta attuale.", + "set.multiAccountNote": "Collega la stessa piattaforma più di una volta per trasmettere su più canali — ogni account diventa una destinazione a sé.", + "set.name": "Nome", + "set.newPassword": "Nuova password", + "set.no": "no", + "set.noEncryption": "Nulla sta cifrando questa connessione. Password e cookie di sessione attraversano la rete in chiaro — imposta tls.mode: auto, oppure vincola a 127.0.0.1 dietro un proxy.", + "set.noLibsrt": "Questa build di FFmpeg non supporta SRT. Installane una configurata con --enable-libsrt, oppure usa RTMP.", + "set.noTokens": "Ancora nessun token.", + "set.passphrase": "Passphrase", + "set.passphraseNote": "Abilita la cifratura AES. Lascia vuoto per nessuna. SRT rifiuta passphrase fuori dai 10–79 caratteri.", + "set.passphrasePlaceholder": "facoltativa — da 10 a 79 caratteri", + "set.password": "Password", + "set.passwordChanged": "Password modificata.", + "set.passwordMismatch": "Le nuove password non coincidono.", + "set.passwordNote": "La password non viene restituita da nessuna API, quindi questo campo è vuoto anche quando ne è impostata una. Lascialo vuoto per mantenere la password memorizzata; cancellala con il pulsante qui sotto.", + "set.platform": "Piattaforma", + "set.platformsDesc": "Le piattaforme differiscono per quanto di tutto questo polyemesis riesce ad automatizzare, e vale la pena conoscere la differenza prima di iniziare anziché un'ora dopo. La trasmissione in sé funziona ovunque allo stesso modo — instradamento audio per destinazione, versioni, riconnessione e misuratori non dipendono da nessuna delle colonne qui sotto.", + "set.playLoop": "Riproduci un loop quando nulla è in diretta", + "set.playLoopNote": "Un secondo candidato accanto alla schermata di attesa: vengono riprodotti file invece di un'immagine fissa. Possono essere attivi entrambi — vedi l'ordine dei livelli nella progettazione del failover per sapere quale vince.", + "set.pointObs": "Punta OBS a questo indirizzo.", + "set.preview": "Anteprima", + "set.previewDesc": "Il monitor HLS del pannello. È l'unico punto in cui polyemesis ricodifica il video, e gira su una propria sottoscrizione al relay, quindi non può mai influire su una destinazione.", + "set.previewIdleNote": "L'encoder parte quando il pannello richiede la playlist e si ferma di nuovo dopo questo tempo dall'ultima richiesta, così un pannello chiuso non costa CPU. Modificare questo valore non riavvia mai un'anteprima che qualcuno sta guardando.", + "set.processLogs": "Log dei processi", + "set.proxyTerminatesTls": "Un reverse proxy davanti a polyemesis termina il TLS, quindi questo server serve HTTP in chiaro di proposito.", + "set.publishInterval": "Intervallo di pubblicazione (s)", + "set.reconnectCap": "Tetto dell'attesa di riconnessione (s)", + "set.reconnectCapNote": "0 usa il valore predefinito. Solo sorgenti HTTP e HLS.", + "set.redirectNote": "Deve corrispondere esattamente, schema e porta compresi. Se raggiungi polyemesis tramite un proxy inverso, usa quell'URL pubblico.", + "set.redirectUri": "URI di reindirizzamento da autorizzare", + "set.removeCreds": "Rimuovi credenziali", + "set.removeCredsDesc": "Ogni account collegato tramite questa app smette di rinnovare il proprio token e dovrà essere ricollegato. Il client secret non è recuperabile da qui — dovresti creare una nuova app sulla piattaforma.", + "set.removeCredsTitle": "Rimuovere queste credenziali di sviluppatore?", + "set.returnAfter": "Torna dopo (secondi stabile)", + "set.returnAutomatic": "Automatico — torna quando la primaria è stabile", + "set.returnManual": "Manuale — resta sulla riserva finché non cambio io", + "set.returnManualNote": "Manuale è l'impostazione predefinita perché un ritorno automatico può oscillare: una primaria che si riprende a sbalzi sposterebbe il programma avanti e indietro in piena trasmissione.", + "set.rtmpPort": "Porta RTMP", + "set.rtspNote": "TCP, a meno che la telecamera non possa. RTSP su UDP attraverso il NAT sembra connesso e non consegna nulla.", + "set.rtspTransport": "Trasporto RTSP", + "set.saveAndDrop": "Salva e interrompi lo stream in diretta", + "set.saveIngest": "Salva le impostazioni di ingresso", + "set.segmentSec": "Segmento (s)", + "set.settingsSavedAffectedProcessesHave": "Impostazioni salvate. I processi interessati sono stati riavviati.", + "set.showSlate": "Mostra una schermata quando nulla è in diretta", + "set.slateColour": "Colore della schermata", + "set.slateColourNote": "Una sola parola, perché il valore finisce in un filter graph dove una virgola chiuderebbe l'argomento. Usato solo quando non è impostata alcuna immagine.", + "set.slateColourPlaceholder": "black", + "set.slateImage": "Immagine della schermata", + "set.slateImageNote": "Un percorso all'interno della directory dei dati. Lascialo vuoto per dipingere invece un colore pieno — è proprio questo il ripiego, perché un colore non ha alcun file che possa non aprirsi.", + "set.slateImagePlaceholder": "slate/holding.png", + "set.sourceUrl": "URL della sorgente", + "set.sourceUrlNote": "Uno tra {schemes}. Una sorgente file:// è un percorso RELATIVO alla directory dei dati e non può contenere «..» — vi è confinata esattamente come un file caricato.", + "set.sourceUrlPlaceholder": "rtsp://camera.local/stream1", + "set.srtPort": "Porta SRT", + "set.stagger": "Scagliona le connessioni (ms)", + "set.staggerNote": "0 disattiva. Altrimenti andare in diretta significa che ogni destinazione apre una connessione, negozia TLS e inizia a codificare audio nello stesso istante — su una macchina piccola è il momento in cui è più probabile perdere fotogrammi, ed è esattamente il momento che stai guardando, perché sei appena andato in diretta. Qualche centinaio di millisecondi basta.", + "set.staggerReconnectNote": "Non ritarda mai una riconnessione. Una destinazione che cade alle 3 di notte torna subito, invece di aspettare il proprio turno dietro processi già sani.", + "set.stepByStep": "Configurazione passo passo", + "set.stopAfterIdle": "Ferma dopo inattività (s)", + "set.streamKey": "Chiave di stream", + "set.streamKeyNote": "Chi pubblica deve usare esattamente questo percorso, così una porta aperta non è una porta spalancata.", + "set.subject": "Soggetto", + "set.synthesiseNote": "Ogni grande piattaforma rifiuta uno stream senza traccia audio. Con questa opzione attiva, un ingresso di solo video ottiene una traccia stereo silenziosa e le sue destinazioni partono; con essa disattiva, rifiutano con «routing profile selects no audio». Si applica soltanto a un ingresso RILEVATO con zero tracce, quindi non può mai scalzare audio che stai davvero inviando.", + "set.synthesiseSilent": "Sintetizza una traccia silenziosa", + "set.syntheticAudio": "Audio sintetico", + "set.syntheticDesc": "Cosa succede quando l'ingresso porta video e nessun audio.", + "set.tabIngest": "Ingresso", + "set.tabPipeline": "Catena di elaborazione", + "set.tabPlatforms": "Credenziali piattaforma", + "set.tabSecurity": "Sicurezza", + "set.title": "Impostazioni", + "set.tlsAcme": "Let's Encrypt", + "set.tlsAuto": "Auto", + "set.tlsDesc": "Si configura in config.yaml, non qui — il TLS deve essere corretto prima che il server si metta in ascolto. Questa scheda riporta il risultato ottenuto.", + "set.tlsManual": "Certificato manuale", + "set.tlsOff": "Non terminato qui", + "set.tlsSelfsigned": "Autofirmato", + "set.tokenHashNote": "polyemesis memorizza solo un hash. Se lo perdi, revoca questo token e creane un altro.", + "set.tokenNamePlaceholder": "prometheus", + "set.tokenRevoked": "{name} revocato.", + "set.topicPrefix": "Prefisso del topic", + "set.topicPrefixNote": "I separatori nel prefisso vengono conservati, quindi home/av significa due livelli. Un prefisso che inizia con $ viene rifiutato: i broker li riservano, e un iscritto che usa # non li riceve mai — la telemetria verrebbe pubblicata con successo e sarebbe invisibile proprio nella vista con cui andresti a indagare.", + "set.transportSecurity": "Sicurezza del trasporto", + "set.updateInterval": "Intervallo di aggiornamento (ms)", + "set.username": "Nome utente", + "set.validFor": "Valido per", + "set.whatPlatformsDo": "Cosa può fare ogni piattaforma", + "set.whyDevApp": "Perché ti serve una tua app da sviluppatore", + "set.whyDevAppDesc": "polyemesis non può distribuire client secret OAuth — chiunque abbia il binario li avrebbe, e le piattaforme li revocherebbero. Registri una volta un'app da sviluppatore gratuita, incolli qui client ID e secret, e polyemesis recupera poi le tue chiavi di stream al posto tuo.", + "set.writeLogs": "Scrivi i log su disco", + "set.yes": "sì", + "sources.addDescription": "Un secondo programma con il proprio ingresso, le proprie destinazioni e versioni — per esempio una tela verticale accanto a una orizzontale. Parte sulle porte predefinite, che converrà cambiare perché le due non entrino in conflitto.", + "sources.addFailed": "Impossibile aggiungere la sorgente", + "sources.addSource": "Aggiungi sorgente", + "sources.addTitle": "Aggiungi una sorgente", + "sources.added": "«{name}» aggiunta. Imposta le sue porte di ingresso qui sotto.", + "sources.app": "App", + "sources.applyAndDrop": "Applica e interrompi lo stream", + "sources.copied": "{what} copiato", + "sources.copyFailed": "Impossibile copiare {what}", + "sources.copyTokenAria": "Copia il token", + "sources.copyUrlAria": "Copia l'URL {proto}", + "sources.default": "predefinita", + "sources.defaultDescription": "Le richieste che non indicano una sorgente agiscono su questa.", + "sources.deleteAria": "Elimina {name}", + "sources.deleteConfirm": "Elimina sorgente", + "sources.deleteDescription": "Le sue destinazioni e versioni se ne vanno con lei: descrivono dove va questo programma e senza di esso non significano nulla. Le registrazioni vengono conservate: i file restano sul disco e riproducibili, semplicemente smettono di essere attribuiti a una sorgente.", + "sources.deleteFailed": "Impossibile eliminare la sorgente", + "sources.deleteThis": "Elimina questa sorgente", + "sources.deleteTitle": "Eliminare «{name}»?", + "sources.deleted": "«{name}» e le sue destinazioni eliminate", + "sources.destinations": "Destinazioni", + "sources.dirtyIdle": "Modifiche all'ingresso non salvate. Applicare riavvia l'ingresso di questa sorgente.", + "sources.dirtyPublishing": "Un encoder sta pubblicando su questa sorgente. Applicare riavvia il suo ingresso e disconnette tutti gli spettatori.", + "sources.enableAria": "Abilita {name}", + "sources.help.app": "Il percorso applicativo RTMP — la parte dell'URL tra l'host e la chiave di stream. La maggior parte degli encoder si aspetta «live». Cambialo solo se hai un motivo.", + "sources.help.ingest": "Come il tuo encoder raggiunge questa sorgente. SRT sopravvive a una connessione con perdite ritrasmettendo ciò che è andato perso ed è la scelta predefinita giusta su internet pubblica. RTMP è più vecchio e supportato ovunque, ma si degrada molto su un collegamento instabile. Con il prelievo, polyemesis va a prendere il flusso da un URL che indichi tu invece di aspettare che qualcuno pubblichi.", + "sources.help.latencyMs": "Quanto a lungo SRT attenderà un pacchetto in ritardo o ritrasmesso prima di rinunciarvi. Valori più alti reggono connessioni peggiori al prezzo di un ritardo: circa 2,5 volte il tempo di andata e ritorno è un buon punto di partenza, quindi 40 ms di RTT chiedono circa 100 ms qui. Troppo basso e un breve intoppo diventa un difetto visibile.", + "sources.help.passphrase": "Cifra la connessione SRT e oggi è ciò che tiene davvero gli estranei fuori da questo ingresso. Tra 10 e 79 caratteri. Se lo lasci vuoto, chiunque raggiunga la porta può pubblicarvi.", + "sources.help.streamKey": "Il segreto che un encoder invia per pubblicare via RTMP. È la credenziale che protegge questo ingresso, quindi trattala come una password: chiunque la possieda può trasmettere alle tue destinazioni.", + "sources.help.token": "Identifica questa sorgente quando più sorgenti condividono una porta di ingresso. Se controlli anche l'accesso dipende dalla modalità di ingresso — la nota qui sotto dice cosa è vero adesso. La rigenerazione emette un nuovo token e mantiene il vecchio valido per cinque minuti, così un encoder in diretta non viene interrotto a metà trasmissione.", + "sources.ingest": "Ingresso", + "sources.lastSourceTitle": "L'ultima sorgente non può essere eliminata: un'installazione ha bisogno di un ingresso", + "sources.latencyMs": "Latenza (ms)", + "sources.loadFailed": "Impossibile caricare le sorgenti", + "sources.loading": "Caricamento sorgenti…", + "sources.loss": "perdita", + "sources.modePull": "Prelievo", + "sources.name": "Nome", + "sources.namePlaceholder": "Verticale", + "sources.noEngine": "Nessun motore è in esecuzione per questa sorgente. La causa abituale è una porta di ingresso già occupata da un'altra sorgente o da un altro processo.", + "sources.notRunning": "non in esecuzione", + "sources.ownDescription": "Destinazioni, versioni e registrazioni proprie.", + "sources.passphrase": "Passphrase", + "sources.passphrasePlaceholder": "10–79 caratteri", + "sources.publishing": "in pubblicazione", + "sources.renditions": "Versioni", + "sources.retrans": "ritrasm.", + "sources.rotate": "Rigenera", + "sources.rotateConfirm": "Rigenera token", + "sources.rotateFailed": "Impossibile rigenerare il token", + "sources.rotateIdle": "Viene emesso subito un nuovo token. Il vecchio resta valido per cinque minuti, così un encoder che lo sta già usando non viene interrotto.", + "sources.rotateTitle": "Rigenerare il token di «{name}»?", + "sources.rotateWhilePublishing": "Un encoder sta pubblicando su questa sorgente in questo momento. Il vecchio token resta valido per cinque minuti, quindi non verrà interrotto — ma va aggiornato prima che quella finestra si chiuda.", + "sources.rtt": "RTT", + "sources.running": "in esecuzione", + "sources.saveFailed": "Impossibile salvare", + "sources.streamKey": "Chiave di stream", + "sources.subtitle": "Un programma in ingresso ciascuna. Destinazioni e versioni appartengono a una sorgente.", + "sources.title": "Sorgenti", + "sources.token": "token", + "sources.tokenEnforcedDetailRtmp": "È la chiave di streaming: pubblica sull'URL qui sopra usandola come chiave. La rotazione ne emette una nuova e mantiene la vecchia per cinque minuti, così puoi cambiare senza interrompere la diretta.", + "sources.tokenEnforcedDetailSrt": "Inseriscilo nello streamid SRT del tuo encoder per pubblicare su questa sorgente sulla porta condivisa. La rotazione ne emette uno nuovo e mantiene il vecchio per cinque minuti, così puoi cambiare senza interrompere la diretta.", + "sources.tokenIsCredential": "Questo token è la credenziale.", + "sources.tokenIssued": "Nuovo token emesso", + "sources.tokenNotEnforced": "Questo token è memorizzato ma al momento non viene applicato.", + "sources.tokenNotEnforcedRtmp": "Le sorgenti sono tenute separate dalla porta. Ciò che protegge davvero questo ingresso è la chiave di stream qui sopra. Attiva l'ingresso a porta singola nelle Impostazioni per rendere il token la credenziale.", + "sources.tokenNotEnforcedSrt": "Le sorgenti sono tenute separate dalla porta. Ciò che protegge davvero questo ingresso è la passphrase SRT qui sopra. Attiva l'ingresso a porta singola nelle Impostazioni per rendere il token la credenziale.", + "state.disabled": "Disattivato", + "state.failed": "Errore", + "state.live": "In diretta", + "state.offline": "Offline", + "state.reconnecting": "Riconnessione in corso", + "state.starting": "Avvio", + "state.stopped": "Interrotto" } diff --git a/ui/src/lib/i18n/ja.json b/ui/src/lib/i18n/ja.json index 43a0fc7d..b2c9c67e 100644 --- a/ui/src/lib/i18n/ja.json +++ b/ui/src/lib/i18n/ja.json @@ -1,26 +1,227 @@ { - "nav.dashboard": "ダッシュボード", - "nav.sources": "ソース", - "nav.meters": "オーディオレベル", - "nav.routing": "Routing", - "nav.renditions": "レンダリング設定", - "nav.playout": "Playout", - "nav.library": "ライブラリ", - "nav.clips": "クリップ", - "nav.chat": "チャット", - "nav.jobs": "ジョブ", - "nav.automation": "自動化", - "nav.recordings": "録画", - "nav.monitoring": "モニタリング", - "nav.settings": "設定", - "chrome.toggleNav": "ナビゲーションの表示/非表示を切り替え", + "auth.accountCreated": "管理者アカウントを作成しました。", + "auth.confirmPassword": "パスワードの確認", + "auth.createAccount": "アカウントを作成", + "auth.firstRun": "初回セットアップ", + "auth.firstRunDesc": "このサーバーの管理者資格情報を設定してください。アカウントの復旧手段はないため、安全な場所に保管してください。", + "auth.password": "パスワード", + "auth.passwordMismatch": "パスワードが一致しません。", + "auth.signIn": "サインイン", + "auth.signInDesc": "管理者の資格情報を入力してください。", + "auth.somethingWentWrong": "問題が発生しました。", + "auth.tokenCreated": "API トークンを作成しました", + "auth.tokenRevoked": "API トークンを失効しました", + "auth.username": "ユーザー名", + "auto.action": "アクション", + "auto.adminPasswordChanged": "管理者パスワードを変更", + "auto.chosenWeekdays": "選択した曜日", + "auto.coalesced": "まとめ済み", + "auto.coalescingWindow": "まとめる時間枠(秒)", + "auto.couldNotChangeTheRule": "ルールを変更できませんでした。", + "auto.couldNotChangeTheSchedule": "スケジュールを変更できませんでした。", + "auto.couldNotDeleteTheRule": "ルールを削除できませんでした。", + "auto.couldNotDeleteTheSchedule": "スケジュールを削除できませんでした。", + "auto.couldNotLoadAutomationSettings": "自動化の設定を読み込めませんでした。", + "auto.couldNotSaveTheRule": "ルールを保存できませんでした。", + "auto.couldNotSaveTheSchedule": "スケジュールを保存できませんでした。", + "auto.createRule": "ルールを作成", + "auto.createSchedule": "スケジュールを作成", + "auto.days": "曜日", + "auto.delete": "削除", + "auto.deleteRule": "ルールを削除", + "auto.deleteRuleDesc": "ルールは発火しなくなります。Webhook エンドポイントはそのまま残り、ルールは作り直せます。", + "auto.deleteRuleTitle": "このアラートルールを削除しますか?", + "auto.deleteSchedule": "スケジュールを削除", + "auto.deleteScheduleDesc": "スケジュールは実行されなくなります。すでに開始したものには影響しません。", + "auto.deleteScheduleTitle": "このスケジュールを削除しますか?", + "auto.delivery": "配送", + "auto.destinationFallingBehindRealtime": "配信先がリアルタイムから遅れている", + "auto.destinationKeepingUpAgain": "配信先が再び追いついた", + "auto.destinationRecovered": "配信先が復旧した", + "auto.destinationWentDown": "配信先が停止した", + "auto.destinations": "配信先", + "auto.dropped": "破棄", + "auto.edit": "編集", + "auto.editRule": "アラートルールを編集", + "auto.editSchedule": "スケジュールを編集", + "auto.endpoint": "エンドポイント", + "auto.evAudioClipping": "音声のクリッピング", + "auto.evClipCaptured": "クリップを取得", + "auto.evDiskLow": "ディスク残量が少ない", + "auto.evDiskRecovered": "ディスクが回復した", + "auto.evIngestLost": "入力が失われた", + "auto.evIngestRecovered": "入力が復旧した", + "auto.evSettingsChanged": "設定が変更された", + "auto.evSignedIn": "サインインした", + "auto.events": "イベント", + "auto.everyDay": "毎日", + "auto.failed": "失敗", + "auto.failoverSwitchedSource": "フェイルオーバーがソースを切り替えた", + "auto.fired": "発火", + "auto.firesAt": "発火時刻", + "auto.lastDelivery": "最終配送", + "auto.lastSweep": "最終スイープ", + "auto.latenessAllowance": "遅延の許容時間(秒)", + "auto.loudnessBackInCompliance": "ラウドネスが適合に戻った", + "auto.loudnessOutOfCompliance": "ラウドネスが適合外", + "auto.minGap": "メッセージ間の最小間隔(秒)", + "auto.minSeverity": "最小の重大度", + "auto.missedNote": "サーバーが停止していて逃した実行は、遅れて発火させるのではなく処理済みとして記録し、スキップします。4 時間遅れで勝手に始まる配信は、まったく始まらない配信よりも厄介だからです。", + "auto.name": "名前", + "auto.newRule": "新しいアラートルール", + "auto.newSchedule": "新しいスケジュール", + "auto.next": "次回", + "auto.nothingFired": "まだ何も発火していません。スケジューラーは 20 秒ごとにスイープします。", + "auto.once": "1 回のみ", + "auto.payloadFormat": "ペイロード形式", + "auto.pending": "保留中", + "auto.repeatedFailedSignIns": "サインイン失敗の連続", + "auto.repeats": "繰り返し", + "auto.retries": "再試行", + "auto.rule": "ルール", + "auto.ruleCreated": "アラートルールを作成しました。テストを送信してください。", + "auto.ruleDeleted": "アラートルールを削除しました。", + "auto.rulePlaceholder": "Discord — 番組アラート", + "auto.ruleSaved": "アラートルールを保存しました。", + "auto.saveRule": "ルールを保存", + "auto.saveSchedule": "スケジュールを保存", + "auto.schedule": "スケジュール", + "auto.scheduleCreated": "スケジュールを作成しました。", + "auto.scheduleDeleted": "スケジュールを削除しました。", + "auto.schedulePlaceholder": "平日夜の番組", + "auto.scheduleSaved": "スケジュールを保存しました。", + "auto.schedules": "スケジュール", + "auto.secretMasked": "シークレットがパスに含まれるため、マスクして表示しています。現在のエンドポイントを維持するならそのままに、置き換えるなら新しい URL を貼り付けてください。", + "auto.sendTest": "テストアラートを送信", + "auto.sendTestTitle": "いますぐテストアラートを送信します", + "auto.sent": "送信", + "auto.skipped": "スキップ", + "auto.startDestinations": "配信先を開始", + "auto.startPlaylist": "プレイリストを開始", + "auto.stopDestinations": "配信先を停止", + "auto.stopPlaylist": "プレイリストを停止", + "auto.subscribedTo": "購読対象", + "auto.subtitle": "何かが壊れたことを知らせる Webhook と、あなたがいなくても番組を進めるスケジュール。", + "auto.theEndpointDidNotAccept": "エンドポイントがテストメッセージを受け付けませんでした。", + "auto.time": "時刻", + "auto.timeZone": "タイムゾーン", + "auto.title": "自動化", + "auto.tzPlaceholder": "UTC", + "auto.webhookHint": "Discord も Slack も、チャンネル設定から取得できます。", + "auto.webhookPlaceholder": "https://discord.com/api/webhooks/…", + "auto.webhookRules": "Webhook ルール", + "auto.webhookUrl": "Webhook URL", + "auto.weekFri": "金", + "auto.weekMon": "月", + "auto.weekSat": "土", + "auto.weekSun": "日", + "auto.weekThu": "木", + "auto.weekTue": "火", + "auto.weekWed": "水", + "auto.when": "条件", + "chatpage.allFiltered": "メッセージのあるプラットフォームがすべて絞り込みで除外されています。", + "chatpage.couldNotDeleteThatMessage": "そのメッセージを削除できませんでした。", + "chatpage.deleteNote": "削除すると、そのメッセージを発行したプラットフォーム上から取り除かれます。polyemesis が削除できないプラットフォームはその旨を表示し、代わりにどこで実行できるかを示します。操作が推測で隠されることはありません。", + "chatpage.droppedTitle": "ライブでは表示されましたが、保存が追いつかず履歴から失われました。", + "chatpage.dupTitle": "同じメッセージを二度受け取りました。自分のエコーか、プラットフォーム側の再送です。", + "chatpage.noAccount": "チャットは動作していますが、プラットフォームのアカウントが接続されていません。設定 → プラットフォーム認証情報から接続してください。", + "chatpage.notRunning": "このサーバーではチャットが動作していません。タイムラインの下の履歴は以前保存されたもので、プラットフォームが接続されるまで送信欄は無効のままです。", + "chatpage.received": "受信", + "chatpage.reload": "再読み込み", + "chatpage.stored": "保存済み", + "chatpage.storedTitle": "何も接続されていません。これは保存された履歴です。", + "chatpage.subtitle": "すべてのプラットフォームを一つのタイムラインに、送信欄も一つ。", + "chatpage.timeline": "タイムライン", + "chatpage.title": "チャット", "chrome.ingest": "Ingest", + "chrome.language": "言語", "chrome.liveCount": "{count} 件ライブ中", + "chrome.signOut": "サインアウト", "chrome.socketOffline": "オフライン", "chrome.socketOfflineHint": "ライブ更新が切断されました。再試行します。", - "chrome.signOut": "サインアウト", - "chrome.language": "言語", + "chrome.toggleNav": "ナビゲーションの表示/非表示を切り替え", + "clipedit.back33": "33 ms 戻る", + "clipedit.clickLine": "行をクリックするとその位置へ移動します。複数行にまたがって選択すると、イン点とアウト点が選択した語に合わせて移動します。", + "clipedit.clipThisLine": "この行を切り出す", + "clipedit.clipThisLineTitle": "この行を囲むようにイン点とアウト点を設定します", + "clipedit.copied": "コピーしました", + "clipedit.copiesEveryPacketSecondsBit": "すべてのパケットをコピーします。数秒で終わり、ビット単位で同一で、開始位置は直前のキーフレームまで戻ります。", + "clipedit.couldNotOpenThatRecording": "その録画を開けませんでした。", + "clipedit.couldNotQueueTheExport": "書き出しをキューに入れられませんでした。", + "clipedit.cut": "カット", + "clipedit.downloadClip": "クリップをダウンロード", + "clipedit.exportQueued": "書き出しをキューに入れました。マシンに余裕ができたときに実行されます。", + "clipedit.exports": "書き出し", + "clipedit.fast": "高速", + "clipedit.filterTranscript": "文字起こしを絞り込む", + "clipedit.findSaid": "発言を検索", + "clipedit.forward33": "33 ms 進む", + "clipedit.in": "イン点", + "clipedit.keepAllTracks": "すべての音声トラックを保持", + "clipedit.keepsEveryAudioTrackIn": "どのコーデックでも、すべての音声トラックを保持します。レコーダーが書き出すものそのままです。", + "clipedit.markIn": "I — 再生ヘッドにイン点を設定", + "clipedit.markOut": "O — 再生ヘッドにアウト点を設定", + "clipedit.name": "名前", + "clipedit.noKeyframes": "キーフレームの位置を読み取れなかったため、このカットが実際にどこから始まるかは誰にも分かりません。動作自体は問題ありません(FFmpeg 自身のシークはキーフレーム上に着地します)が、開始点は最大で 1 GOP 分早くなる可能性があり、その差をこのページで示すことはできません。", + "clipedit.noTranscript": "この録画はまだ文字起こしされていません。マイクごとに個別のトラックへ収録されているため、文字起こしすると話者分離モデルなしで話者名が得られ、発言内容から切り出せるようになります。", + "clipedit.nothingExported": "まだ何も書き出していません。", + "clipedit.out": "アウト点", + "clipedit.pickAtLeastOneAudio": "音声トラックを少なくとも 1 つ選ぶか、すべてを選んでください。", + "clipedit.playFaster": "L — 再生、もう一度押すと早送り", + "clipedit.precise": "高精度", + "clipedit.queueNote": "書き出しはここで実行されるのではなくキューに入ります。ライブ配信を優先するため、放送中に取ったカットは、配信が必要とする処理能力を奪わずに待機します。", + "clipedit.reEncodesOnlyTheLeading": "先頭の不完全な GOP だけを再エンコードして開始位置を正確にします。残りはそのままコピーされます。", + "clipedit.reencoded": "再エンコード", + "clipedit.shortcuts": "J / K / L でシャトル、I と O でイン点とアウト点、矢印キーで 33 ms、Shift + 矢印キーで 1 秒動かします。細かい方のステップは 30 fps における 1 フレームです。この録画の実際のフレームレートは索引化されていないため、推測せずミリ秒で表記しています。", + "clipedit.shuttleBack": "J — 逆方向にシャトル", + "clipedit.startMoved": "開始位置の移動", + "clipedit.starts": "開始", + "clipedit.stop": "K — 停止", + "clipedit.thatCutCouldNotBe": "そのカットを計画できませんでした。", + "clipedit.thatIsNotARecording": "録画 ID ではありません。", + "clipedit.thatRecordingIsNotAvailable": "その録画は利用できません。", + "clipedit.title": "クリップエディター", + "clipedit.whatASocialPlatformWill": "ソーシャルプラットフォームが受け付ける形式です。通過できるコーデックは少なくなります。", + "clipedit.zoomIn": "拡大", + "clipedit.zoomOut": "縮小", + "clips.bitrate": "ビットレート", + "clips.buffer": "バッファ", + "clips.bufferLiveNote": "バッファのスイッチは即時切り替えであり、保存される設定ではありません。再起動するとまたオフになります。", + "clips.captured": "取得日時", + "clips.ceiling": "上限", + "clips.clipsKept": "保存クリップ数", + "clips.couldNotCaptureAClip": "クリップを取得できませんでした。", + "clips.couldNotChangeTheClip": "クリップバッファを変更できませんでした。", + "clips.couldNotDeleteTheClip": "クリップを削除できませんでした。", + "clips.couldNotLoadClips": "クリップを読み込めませんでした。", + "clips.covers": "対象範囲", + "clips.customLength": "クリップの長さ(秒)を指定", + "clips.delete": "削除", + "clips.deleteDescription": "クリップはたいてい、まさに残したかった素材です。これはファイルを削除しますが、元になった録画はそのまま残ります。", + "clips.deleteTitle": "このクリップを削除しますか?", + "clips.deleted": "クリップを削除しました。", + "clips.download": "ダウンロード", + "clips.everythingHeld": "バッファが保持している全体", + "clips.file": "ファイル", + "clips.held": "保持中", + "clips.inMemory": "メモリ上", + "clips.keepBuffer": "ローリングバッファを保持する", + "clips.keyframes": "キーフレーム", + "clips.length": "長さ", + "clips.none": "なし", + "clips.off": "オフ", + "clips.onDisk": "ディスク上", + "clips.seen": "検出済み", + "clips.settings": "設定", + "clips.size": "サイズ", + "clips.subtitle": "いま配信された分のローリングバッファです。あとから切り出せるので、事前の準備は要りません。", + "clips.theClipDoesNotStart": "クリップがキーフレームから始まっていません。", + "clips.title": "クリップ", + "clips.waitingForStream": "配信待ち", + "clips.window": "ウィンドウ", + "clips.windowSeconds": "ウィンドウ(秒)", "common.add": "追加", + "common.apply": "適用", "common.cancel": "キャンセル", "common.close": "閉じる", "common.confirm": "確認", @@ -28,110 +229,921 @@ "common.copy": "コピー", "common.delete": "削除", "common.disabled": "無効", + "common.discard": "破棄", "common.edit": "編集", "common.enabled": "有効", "common.loading": "ローディング…", + "common.moreInfo": "詳細情報", "common.refresh": "更新", "common.restart": "再起動", "common.retry": "再試行", "common.save": "保存", "common.start": "開始", "common.stop": "停止", - "state.live": "ライブ", - "state.reconnecting": "再接続中", - "state.starting": "開始中", - "state.failed": "エラー", - "state.stopped": "停止しました", - "state.offline": "オフライン", - "hooks.title": "ライフサイクル Webhook", - "hooks.new": "新しい Webhook", - "hooks.empty": "Webhook がありません。追加すると、配信が開始または停止した瞬間、あるいは配信先が起動または停止した瞬間に、polyemesis が署名付き JSON ボディを POST します。遷移ごとに 1 回、順序どおりに届くので、スクリプトで処理できます。", - "hooks.colWebhook": "Webhook", - "hooks.colEndpoint": "エンドポイント", - "hooks.colTriggers": "購読対象", - "hooks.colEnabled": "有効", - "hooks.signed": "署名あり", - "hooks.unsigned": "署名なし", + "dash.audioTracks": "音声トラック", + "dash.bitrate": "ビットレート", + "dash.broadcastSettings": "配信設定", + "dash.categoryPlaceholder": "音楽", + "dash.clipboardUnavailable": "このオリジンではクリップボードを利用できません。", + "dash.connectPrompt": "設定 → プラットフォーム認証情報で YouTube、Twitch、Facebook、Kick のアカウントを接続すると、配信のタイトル、説明、カテゴリーをすべてのプラットフォームへ一度に設定できます。受け付ける項目はプラットフォームごとに異なります。作成画面がどれを受け付けるかを示し、送れるものを送ります。", + "dash.copyIngestUrl": "入力 URL をコピー", + "dash.couldNotLoadTheDestination": "配信先を読み込めませんでした。", + "dash.couldNotPushTheMetadata": "メタデータを送信できませんでした。", + "dash.couldNotReorderTheDestinations": "配信先の並び順を変更できませんでした。", + "dash.deleteDest": "配信先を削除", + "dash.deleteDestDescription": "配信先とそのルーティングプロファイルを削除します。プラットフォーム側はそのままで、後から追加し直せますが、その配信先のために設定した音声ルーティングは失われます。", + "dash.descriptionPlaceholder": "この配信の内容。", + "dash.destDeleted": "配信先を削除しました。", + "dash.disabled": "無効", + "dash.everyConnectedAccount": "接続済みのすべてのアカウント", + "dash.idle": "待機中", + "dash.ingestUrlCopied": "入力 URL をコピーしました。", + "dash.keyRefreshed": "プラットフォームからストリームキーを更新しました。", + "dash.leaveUnchanged": "変更しない", + "dash.leaveUnchangedNote": "「変更しない」は何も送らないため、プラットフォームは現在の値を保ちます。オンまたはオフを選ぶと、その値が書き込まれます。", + "dash.metaCategory": "カテゴリー", + "dash.metaDescription": "説明", + "dash.metaTags": "タグ", + "dash.metaTitle": "タイトル", + "dash.noDestinations": "配信先がまだありません。追加してから、受け取る音声トラックを選んでください。", + "dash.off": "オフ", + "dash.pipeline": "パイプライン", + "dash.pushToPlatforms": "プラットフォームへ送信", + "dash.pushing": "送信中…", + "dash.reconnects": "再接続回数", + "dash.relaySubscribers": "リレーの購読数", + "dash.renditions": "バリエーション", + "dash.scheduledStart": "開始予定時刻", + "dash.subtitle": "一度受け取り、共有の映像と配信先ごとの音声で送り分けます。", + "dash.tagsPlaceholder": "live, house, dj set", + "dash.theConnectedAccount": "接続済みのアカウント", + "dash.title": "ダッシュボード", + "dash.titlePlaceholder": "金曜夜のセット", + "dash.uptime": "稼働時間", + "dash.youtubeStopsAcceptingTheseOnce": "放送が「created」または「ready」を離れると、YouTube はこれらを受け付けなくなります。", + "dest.audience": "公開対象", + "dest.audioBitrate": "音声ビットレート", + "dest.audioCodec": "音声コーデック", + "dest.channels": "チャンネル数", + "dest.compliance": "コンプライアンス", + "dest.connectedAccount": "連携アカウント", + "dest.contentLabels": "コンテンツラベル", + "dest.copySource": "ソース映像をコピー", + "dest.createVariant": "このバリアントを作成", + "dest.customiseSummary": "この配信先だけ調整する", + "dest.fbBackupCost": "この配信先のアップロード帯域が 2 倍になります。", + "dest.fbBackupHelp": "このストリームの 2 つ目のコピーを Facebook のバックアップ受信先へ送るため、接続が切れても配信は切れません。", + "dest.fbBackupLabel": "バックアップ配信を公開する", + "dest.fbBackupReconnect": "オンにするとストリームが一度再接続されます。ライブ開始前に有効にしてください。", + "dest.fbBoxIntro": "「視聴者」と同じ作成呼び出しで送信されるため、同じ規則が適用されます。配信開始時に一度だけ適用され、空の場合はまったく手を触れません。", + "dest.fbBoxTitle": "Facebook のクロス投稿と寄付ボタン", + "dest.fbCrosspostAdd": "ページを追加", + "dest.fbCrosspostAlsoPost": "投稿も行う", + "dest.fbCrosspostHelp": "Facebook 自身のコンソールにある数値のページ ID(ページの設定、またはページとしてログインした状態での graph.facebook.com/me/accounts)です。ここに検索はないので、名前や URL ではなく id を貼り付けてください。「投稿も行う」は、配信をそのページに共有するだけでなく、そのページとして投稿します。オフのままなら、控えめな共有だけが行われます。", + "dest.fbCrosspostLabel": "ページへのクロス投稿", + "dest.fbCrosspostPageId": "ページ ID", + "dest.fbCrosspostRemove": "このページを削除", + "dest.fbDonateHelp": "これも Facebook の募金コンソールから得られる不透明な id であり、検索するための団体名ではありません。寄付ボタンをまったく付けない場合は空欄のままにしてください。", + "dest.fbDonateLabel": "寄付ボタンの慈善団体 ID", + "dest.fbDonatePlaceholder": "慈善団体 ID", + "dest.fillFromPlatform": "このプラットフォームの公表値を入力", + "dest.friends": "友達", + "dest.friendsOfFriends": "友達の友達", + "dest.giveUpAfter": "中止するまでの再試行回数", + "dest.leaveOnFacebook": "Facebook の設定のままにする", + "dest.leaveOnYouTube": "YouTube の設定のままにする", + "dest.madeForKids": "子ども向け (COPPA)", + "dest.muxQueue": "多重化キュー (パケット)", + "dest.muxQueuePair": "この2つは対で働きます。FFmpeg はキューがバイト閾値を超えてから初めてパケット上限を適用するため、閾値だけでは何も起こらず拒否されます。配信先がインターリーブエラーを報告する場合は両方を上げてください。ここでの音声経路はラウドネス正規化が先読みするため、遅延が一定ではありません。", + "dest.name": "名前", + "dest.namePlaceholder": "YouTube — メインチャンネル", + "dest.notLinked": "未連携", + "dest.onlyMe": "自分のみ", + "dest.platform": "プラットフォーム", + "dest.private": "非公開", + "dest.public": "公開", + "dest.reconnecting": "再接続", + "dest.retryAfter": "再試行までの待ち時間 (秒)", + "dest.sharedEncodeAria": "共有映像エンコード", + "dest.socketTimeout": "ソケットタイムアウト (秒)", + "dest.streamKeyLabel": "ストリームキー", + "dest.transport": "トランスポート", + "dest.transportFile": "ローカルファイル", + "dest.transportRtmp": "RTMP / RTMPS", + "dest.unlisted": "限定公開", + "dest.urlFromDashboard": "ダッシュボードの URL", + "dest.useSharedEncode": "共有エンコードを使う", + "dest.varBitrate": "映像ビットレート (kbps)", + "dest.varFps": "フレームレート", + "dest.varHeight": "高さ", + "dest.varSourceHint": "ソース", + "dest.varWidth": "幅", + "dest.variantWarning": "「{name}」を元に、ソースから2本目のエンコードを開始します。これは無償の派生ではなく、そのエンコードを使う他の配信先が受け取る映像は変わりません。ソースの値を保つには空欄のままにしてください。", + "dest.videoTreatment": "映像の処理", + "dest.visibility": "公開範囲", + "hooks.allTriggers": "すべて", "hooks.attempts": "{count} 回試行", + "hooks.attemptsLabel": "試行回数", "hooks.attemptsOne": "{count} 回試行", - "hooks.timeout": "タイムアウト {seconds} 秒", - "hooks.allTriggers": "すべて", - "hooks.triggerCount": "{count} 個のトリガー", - "hooks.triggerCountOne": "{count} 個のトリガー", - "hooks.enableAria": "{name} を有効にする", + "hooks.colEnabled": "有効", + "hooks.colEndpoint": "エンドポイント", + "hooks.colTriggers": "購読対象", + "hooks.colWebhook": "Webhook", + "hooks.create": "作成", + "hooks.created": "Webhook を作成しました。", + "hooks.deleteBody": "ただちに配送を停止します。エンドポイントはそのままで、フックは作り直せますが、その署名キーは復元できません。", + "hooks.deleteConfirm": "Webhook を削除", + "hooks.deleteFailed": "フックを削除できませんでした。", + "hooks.deleteTitle": "この Webhook を削除しますか?", + "hooks.deleted": "Webhook を削除しました。", "hooks.deliveries": "最近の配送", - "hooks.sendTest": "テスト配送を送信", - "hooks.sendTestTitle": "今すぐテスト配送を送信", - "hooks.secretTitle": "署名キー — 今すぐコピーしてください。二度と表示できません", - "hooks.secretCopied": "コピーしました", - "hooks.httpStatus": "HTTP {status}", - "hooks.noResponse": "応答なし", + "hooks.deliveriesFailed": "最近の配送を読み取れませんでした。", + "hooks.deliveryTitle": "配送", + "hooks.dialogIntro": "遷移ごとに署名付き POST を 1 回、順序どおりに送ります。アラートと違い、まとめたり抑制したりはしません。スクリプトはすべてのエッジを受け取ります。", "hooks.durationMs": "{ms} ミリ秒", + "hooks.editTitle": "Webhook を編集", + "hooks.empty": "Webhook がありません。追加すると、配信が開始または停止した瞬間、あるいは配信先が起動または停止した瞬間に、polyemesis が署名付き JSON ボディを POST します。遷移ごとに 1 回、順序どおりに届くので、スクリプトで処理できます。", + "hooks.enableAria": "{name} を有効にする", + "hooks.httpStatus": "HTTP {status}", + "hooks.loadFailed": "Webhook を読み込めませんでした。", + "hooks.name": "名前", + "hooks.new": "新しい Webhook", + "hooks.newTitle": "新しい Webhook", "hooks.noDeliveries": "まだ何も配送されていません。遷移が起きるたびに、ここに配送が表示されます。", - "hooks.deliveryTitle": "配送", - "hooks.statSent": "送信", - "hooks.statFailed": "失敗", + "hooks.noResponse": "応答なし", + "hooks.payloadNote": "ペイロード {version}。署名は {header}、タイムスタンプとボディに対して計算されます。", + "hooks.retriesHint": "順序が約束なので、再試行はこのエンドポイント自身のキューを止めます。4xx は決して再試行しません。リクエストが誤りだと答えたエンドポイントは、また同じことを答えます。", + "hooks.saveFailed": "フックを保存できませんでした。", + "hooks.secretCopied": "コピーしました", + "hooks.secretTitle": "署名キー — 今すぐコピーしてください。二度と表示できません", + "hooks.sendTest": "テスト配送を送信", + "hooks.sendTestTitle": "今すぐテスト配送を送信", + "hooks.signed": "署名あり", + "hooks.statDropped": "破棄", "hooks.statEndpoints": "エンドポイント", + "hooks.statFailed": "失敗", "hooks.statQueued": "キュー", - "hooks.statDropped": "破棄", "hooks.statRetries": "再試行", - "hooks.payloadNote": "ペイロード {version}。署名は {header}、タイムスタンプとボディに対して計算されます。", - "hooks.deleteTitle": "この Webhook を削除しますか?", - "hooks.deleteBody": "ただちに配送を停止します。エンドポイントはそのままで、フックは作り直せますが、その署名キーは復元できません。", - "hooks.deleteConfirm": "Webhook を削除", - "hooks.editTitle": "Webhook を編集", - "hooks.newTitle": "新しい Webhook", - "hooks.dialogIntro": "遷移ごとに署名付き POST を 1 回、順序どおりに送ります。アラートと違い、まとめたり抑制したりはしません。スクリプトはすべてのエッジを受け取ります。", - "hooks.name": "名前", - "hooks.url": "エンドポイント URL", - "hooks.urlMasked": "保存された URL が全文表示されることはありません。そのまま残せば維持され、新しい URL を入力すると置き換わります。", - "hooks.triggers": "トリガー", - "hooks.triggersNoneHint": "何も選ばない場合はすべてのトリガーが対象になります。将来のリリースで追加されるものも含みます。", + "hooks.statSent": "送信", + "hooks.testFailed": "エンドポイントがテスト配送を受け付けませんでした。", + "hooks.timeout": "タイムアウト {seconds} 秒", "hooks.timeoutLabel": "タイムアウト(秒)", - "hooks.attemptsLabel": "試行回数", - "hooks.retriesHint": "順序が約束なので、再試行はこのエンドポイント自身のキューを止めます。4xx は決して再試行しません。リクエストが誤りだと答えたエンドポイントは、また同じことを答えます。", - "hooks.create": "作成", - "hooks.triggerIngestPublished": "配信開始", - "hooks.triggerIngestDisconnected": "配信停止", - "hooks.triggerDestinationUp": "配信先がライブ", - "hooks.triggerDestinationDown": "配信先が停止", - "hooks.loadFailed": "Webhook を読み込めませんでした。", + "hooks.title": "ライフサイクル Webhook", "hooks.toggleFailed": "フックを変更できませんでした。", - "hooks.deleted": "Webhook を削除しました。", - "hooks.deleteFailed": "フックを削除できませんでした。", - "hooks.testFailed": "エンドポイントがテスト配送を受け付けませんでした。", - "hooks.deliveriesFailed": "最近の配送を読み取れませんでした。", + "hooks.triggerCount": "{count} 個のトリガー", + "hooks.triggerCountOne": "{count} 個のトリガー", + "hooks.triggerDestinationDown": "配信先が停止", + "hooks.triggerDestinationUp": "配信先がライブ", + "hooks.triggerIngestDisconnected": "配信停止", + "hooks.triggerIngestPublished": "配信開始", + "hooks.triggers": "トリガー", + "hooks.triggersNoneHint": "何も選ばない場合はすべてのトリガーが対象になります。将来のリリースで追加されるものも含みます。", + "hooks.unsigned": "署名なし", "hooks.updated": "Webhook を更新しました。", - "hooks.created": "Webhook を作成しました。", - "hooks.saveFailed": "フックを保存できませんでした。", - "playlist.title": "プレイリスト", + "hooks.url": "エンドポイント URL", + "hooks.urlMasked": "保存された URL が全文表示されることはありません。そのまま残せば維持され、新しい URL を入力すると置き換わります。", + "jobs.alwaysKeepNewest": "最新のものは常に保持", + "jobs.automaticModel": "自動(このハードウェアから選択)", + "jobs.backToDefault": "この種別の既定モードに戻します", + "jobs.batteryFloor": "バッテリー下限 %", + "jobs.broadcastGoing": "配信中です。後回しの処理はこれに道を譲ります。", + "jobs.cancel": "キャンセル", + "jobs.cannotPause": "これらの種別は一時停止すべきですが、それができないため、そのまま完了まで実行されます。", + "jobs.completed": "完了", + "jobs.concurrency": "同時実行数", + "jobs.couldNotReadTheJob": "ジョブキューを読み取れませんでした。", + "jobs.couldNotSaveThePolicy": "ポリシーを保存できませんでした。", + "jobs.cpuCeiling": "CPU 上限 %", + "jobs.cpuResume": "CPU 再開 %", + "jobs.defaultMode": "既定モード", + "jobs.defaultModel": "既定モデル", + "jobs.deferred": "保留", + "jobs.failed": "失敗", + "jobs.finished": "完了済み", + "jobs.global": "全体", + "jobs.gpuGateNote": "GPU を使用すると指定された種別だけが、これによって制限されます。", + "jobs.history": "履歴", + "jobs.historyRetention": "履歴の保持", + "jobs.holdDeferredWorkBackOn": "放電中のノートパソコンでこの値を下回ったら、後回しにできる処理を保留します。0 で無効になります。", + "jobs.hostCpuLoad": "ホスト CPU 負荷", + "jobs.howManyJobsMayRun": "同時に実行できるジョブ数です。1 なのは、2 本目のトランスコードが、誰も求めていないスループットを、誰も引き受けていないリスクと引き換えに得るだけだからです。", + "jobs.inQueue": "キューの中", + "jobs.job": "ジョブ", + "jobs.keepForDays": "保持日数", + "jobs.machineSafety": "マシンの保護。リアルタイム処理も従う唯一のゲートです。", + "jobs.niceLevel": "nice 値", + "jobs.noFinished": "完了したジョブはまだありません。", + "jobs.noModels": "models ディレクトリにモデルファイルが見つかりませんでした。自動を選ぶと、選ばれたモデルをダウンロードします。ここで特定のモデルを指定できるのは、そのファイルが存在してからです。", + "jobs.nothingNewStartsAboveThis": "この値を超えると新しい処理は開始しません。0 でゲートを無効にします。", + "jobs.nothingQueued": "キューに何もありません。ポストプロダクションはライブラリから投入します。", + "jobs.nothingWasOldEnoughTo": "消去できるほど古いものはありませんでした。先にポリシーで保持期間を短くしてください。", + "jobs.pauseAll": "すべて一時停止", + "jobs.policy": "ポリシー", + "jobs.policySaved": "リソースポリシーを保存しました。", + "jobs.progress": "進捗", + "jobs.queuePaused": "キューを一時停止しました。", + "jobs.queueResumed": "キューを再開しました。", + "jobs.queued": "キュー", + "jobs.recording": "録画中", + "jobs.releasedItWillStartAs": "解放しました。スロットが空き次第、開始します。", + "jobs.removeFromHistory": "履歴から削除", + "jobs.removeWindow": "時間帯を削除", + "jobs.resumeAll": "すべて再開", + "jobs.retry": "再試行", + "jobs.retryTitle": "試行回数の枠を新たにして再投入します", + "jobs.runNow": "いますぐ実行", + "jobs.runNowTitle": "リソースポリシーを無視して、これをいますぐ実行します", + "jobs.running": "実行中", + "jobs.savedTheConcurrencyChangeTakes": "保存しました。同時実行数の変更はサーバーの再起動時に反映されます。キューは起動時にその値を固定します。", + "jobs.state": "状態", + "jobs.stopsEverythingRealtimeIncludedA": "リアルタイム処理も含めてすべてを停止します。サーマルスロットリング中の CPU は、すでにストリームを劣化させ始めています。0 で無効になります。", + "jobs.subtitle": "バックグラウンド処理。文字起こし、プロキシ、サムネイル、アーカイブ圧縮。", + "jobs.thatDidNotWork": "うまくいきませんでした。", + "jobs.thermalCeiling": "温度上限 °C", + "jobs.thisManyFinishedJobsSurvive": "この数だけの完了ジョブは、経過時間にかかわらず残ります。", + "jobs.title": "ジョブ", + "jobs.transcription": "文字起こし", + "jobs.unsavedChanges": "未保存の変更があります。", + "jobs.whereTheCeilingReleasesThe": "上限が解除される値です。その差が、ゲートの発振を止めるヒステリシスになります。", + "jobs.whisperMissing": "ここには whisper.cpp がインストールされていないため、この設定はまだ効果がありません。それでも設定しておく価値はあります。インストールされ次第、選択が適用されます。", + "jobs.why": "理由", + "jobs.windowEnd": "時間帯の終了", + "jobs.windowStart": "時間帯の開始", + "jobs.yieldNote": "重い処理はキューに入り、ライブ配信に道を譲ります。落ちたフレームは取り戻せませんが、文字起こしが 1 時間遅れて届いても損はありません。", + "lib.anySpeaker": "すべての話者", + "lib.anyTag": "すべてのタグ", + "lib.autoGrouped": "自動でグループ化されました。編集すると自分のものになります。", + "lib.autoGroupedNote": "このセッションは自動でグループ化されました。保存すると自分のものになり、グループ化処理が書き換えることはなくなります。", + "lib.bestMatch": "関連度順", + "lib.clearSearch": "検索条件を消去", + "lib.clip": "クリップ", + "lib.clipTitle": "この録画からクリップを切り出します", + "lib.collapseSession": "セッションを折りたたむ", + "lib.couldNotLoadTheLibrary": "ライブラリを読み込めませんでした。", + "lib.couldNotLoadTheSession": "セッションのセグメントを読み込めませんでした。", + "lib.couldNotQueueTheProxy": "プロキシをキューに入れられませんでした。", + "lib.couldNotRegroup": "グループ化し直せませんでした。", + "lib.couldNotSaveTheSession": "セッションを保存できませんでした。", + "lib.delete": "削除", + "lib.deleteDescription": "ファイルはプロキシ・サムネイル・文字起こしとともにディスクから削除されます。この操作は取り消せません。", + "lib.deleteTitle": "この録画を削除しますか?", + "lib.downloadMaster": "マスターをダウンロード", + "lib.downloadMasterTitle": "マルチトラックのマスターをダウンロードします", + "lib.editSession": "セッションを編集", + "lib.expandSession": "セッションを展開", + "lib.from": "出典", + "lib.generateProxy": "プロキシを生成", + "lib.generateProxyTitle": "ブラウザーで再生できるプロキシを生成します", + "lib.mostRecent": "新しい順", + "lib.noProxyYet": "プロキシがまだありません — 開くと生成します", + "lib.noSegments": "ここにはセグメントがありません。", + "lib.noSessionMatch": "その条件に一致するセッションはありません。", + "lib.nothingRecorded": "まだ何も録画されていません。レコーダーがセグメントを書き込むとセッションが現れます。", + "lib.oldestFirst": "古い順", + "lib.playProxy": "プロキシを再生します", + "lib.proxyAlreadyQueued": "プロキシはすでにキューに入っています。", + "lib.proxyQueued": "プロキシをキューに入れました。", + "lib.recordingDeleted": "録画を削除しました。", + "lib.regroupTitle": "グループ化をやり直します。加算的かつ冪等で、手作業で分けたグループを結合することはありません。", + "lib.searchPlaceholder": "すべての録画から、誰の発言でも検索…", + "lib.searchTranscripts": "文字起こしを検索", + "lib.searchable": "検索可能", + "lib.searchableOnly": "文字起こしがあり検索できるセグメント", + "lib.session": "セッション", + "lib.sessionDescription": "説明", + "lib.sessionSaved": "セッションを保存しました。", + "lib.sessionTags": "タグ", + "lib.sessionTitle": "タイトル", + "lib.subtitle": "すべての放送を検索できます。マイクごとに個別のトラックへ収録されているため、文字起こしには誰の発言かがすでに分かっています。", + "lib.tag": "タグ", + "lib.tagsPlaceholder": "カンマ, 区切り", + "lib.title": "ライブラリ", + "lib.transcribe": "文字起こし", + "lib.transcribeEachTrackOnIts": "各トラックを個別に文字起こしします。話者の特定ができるのはこのためです", + "lib.ungrouped": "未グループのセグメント", + "meters.deviation": "偏差", + "meters.idle": "待機中", + "meters.integrated": "インテグレーテッド", + "meters.loudness": "ラウドネス適合", + "meters.metering": "測定中", + "meters.momentary": "モーメンタリー", + "meters.monitorOff": "モニターがオフです。オンにすると、各配信先が送出している内容を測定できます。", + "meters.noSignal": "このトラックに信号がありません", + "meters.nothingToMeasure": "まだ測定するものがありません。実行中の配信先ごとに、受け取るミックスに対する専用の EBU R128 アナライザーが割り当てられます。", + "meters.range": "レンジ", + "meters.shortTerm": "ショートターム", + "meters.subtitle": "入力の全トラックの全チャンネルを、リレーから直接。", + "meters.title": "音声メーター", + "meters.truePeak": "トゥルーピーク", + "meters.waitingForStream": "配信待ち", + "mon.allProcesses": "すべてのプロセス", + "mon.bitrateChart": "入力ビットレート — 直近 30 分", + "mon.chooseDestination": "配信先を選択…", + "mon.clearLog": "ログを消去", + "mon.clearedThisDestinationIsBack": "解除しました。この配信先は生成されたコマンドに戻ります。", + "mon.dryRun": "ドライラン", + "mon.dryRunInvalid": "FFmpeg が引数を拒否しました", + "mon.dryRunOk": "FFmpeg が受け付けました", + "mon.extraInput": "追加の入力引数", + "mon.extraOutput": "追加の出力引数", + "mon.ffmpegLog": "FFmpeg ログ", + "mon.fromRunningProcess": "実行中のプロセスから", + "mon.fullCommand": "展開後の完全なコマンド", + "mon.hostCpu": "ホスト CPU", + "mon.ingestLoss": "入力の損失", + "mon.memory": "メモリ", + "mon.nothingProvenEitherWay": "どちらとも判定できず", + "mon.pid": "PID", + "mon.processes": "プロセス", + "mon.rebuilt": "再構築", + "mon.relayDrops": "リレー破棄", + "mon.relayIn": "リレー入力", + "mon.resolveHint": "適用する前に何が実行されるのかを正確に確認するため、コマンドを展開してください。", + "mon.restarts": "再起動回数", + "mon.subtitle": "プロセスの状態、ホストのリソース、そして FFmpeg のライブログ。", + "mon.title": "モニタリング", + "mon.uptime": "稼働時間", + "nav.automation": "自動化", + "nav.chat": "チャット", + "nav.clips": "クリップ", + "nav.dashboard": "ダッシュボード", + "nav.jobs": "ジョブ", + "nav.library": "ライブラリ", + "nav.meters": "オーディオレベル", + "nav.monitoring": "モニタリング", + "nav.playout": "Playout", + "nav.recordings": "録画", + "nav.renditions": "バリエーション", + "nav.routing": "Routing", + "nav.settings": "設定", + "nav.sources": "ソース", + "play.acrossEveryRungTheBackstop": "すべての段の合計です。長時間の配信でディスクが埋まるのを防ぐ歯止めです。", + "play.allowCrossOrigin": "クロスオリジンを許可", + "play.allowCrossOriginPlayback": "クロスオリジン再生を許可", + "play.audioBitrate": "音声ビットレート(kbps)", + "play.audioTrack": "音声トラック", + "play.basicPasswordNote": "リンクをたどれないプレイヤー(セットトップボックスや CDN など)は、代わりに同じシークレットを HTTP ベーシック認証のパスワードとして送信できます。", + "play.boundsTheViewerTableViewers": "視聴者テーブルの上限です。これを超えた視聴者にも配信はされ、集計されないだけです。", + "play.carriesThePlaybackTokenAnyone": "再生トークンを含みます。この URL を持つ人は誰でも視聴できます。", + "play.clipboardUnavailable": "クリップボードを利用できません。手動で選択してコピーしてください", + "play.couldNotLoadPlayout": "Playout を読み込めませんでした", + "play.disabled": "無効", + "play.diskCap": "ディスク上限(MB)", + "play.dvrWindow": "DVR ウィンドウ(秒)", + "play.embedCode": "埋め込みコード", + "play.enable": "Playout を有効にする", + "play.enabled": "有効", + "play.forObsACdnOr": "OBS、CDN、あるいは HLS を扱えるプレーヤー向けです。", + "play.format": "形式", + "play.hls": "HLS", + "play.hlsDash": "HLS と DASH", + "play.hlsPlaylist": "HLS プレイリスト", + "play.idleTimeout": "視聴者のアイドルタイムアウト(秒)", + "play.linkReset": "新しい再生リンクを生成しました。以前のリンクは使えなくなります。", + "play.linksProtected": "公開をオンにするまで、これらのリンクはサインイン済みの管理者にのみ有効です。", + "play.linksPublic": "これらのリンクはアカウントなしで利用できます。", + "play.liveWindow": "ライブウィンドウ(セグメント数)", + "play.metaDescription": "説明", + "play.metaTitle": "タイトル", + "play.mustExceedOneSegmentOr": "1 セグメントより長くする必要があります。そうしないと、ポーリングの合間に視聴者が消えます。", + "play.name": "名前", + "play.noCredentialNote": "いかなる資格情報も必要ありません。インターネット全体に見せるつもりの配信にのみ適しています。", + "play.noRungs": "段がまだありません。何かを配信するには 1 つ追加してください。", + "play.onDisk": "ディスク上", + "play.oneStereoAacTrackPer": "1 段につきステレオの AAC トラックが 1 本です。", + "play.packaging": "パッケージング", + "play.passthrough": "入力(パススルー)", + "play.peak": "ピーク", + "play.playerPage": "プレイヤーページ", + "play.posterNote": "公開ページで映像の上に表示されます。ポスターは最近のキーフレームで、自動的に更新されます。", + "play.private": "非公開。", + "play.protectedNote": "再生は既定で保護されています。公開するのは別個の、意図的な操作です。", + "play.protection": "保護", + "play.public": "公開", + "play.resetFailed": "リセットできませんでした", + "play.restartNote": "これらのいずれかを変更するとマルチプレクサーが再起動し、視聴者は数秒間中断されます。", + "play.rungNote": "各段は対応するバリエーションの映像をビット単位でそのままコピーするため、追加してもかかるのはマルチプレクサー 1 つ分だけで、2 つ目の映像エンコードは発生しません。段ごとに異なるのは音声トラックだけで、配信先ごとのルーティングには影響しません。", + "play.segmentLength": "セグメント長(秒)", + "play.serveWithoutSession": "セッションなしで視聴者に配信する", + "play.sessionCap": "セッション数の上限", + "play.sessions": "セッション", + "play.share": "共有", + "play.shorterIsLowerLatencyAnd": "短いほど低遅延になり、プレイリストの更新は増えます。", + "play.subtitle": "配信を公開 HLS として、視聴者へ直接届けます。", + "play.threeIsTheMinimumA": "3 はプレーヤーがバッファリングできる最小値です。", + "play.title": "Playout", + "play.unlisted": "限定公開。", + "play.untitled": "無題の配信", + "play.videoSource": "映像ソース", + "play.watchingNow": "現在の視聴者数", + "play.whoCanWatch": "視聴できる人", + "player.copied": "コピーしました", + "player.copy": "コピー", + "player.loading": "読み込み中…", + "player.missingToken": "開いたリンクに再生トークンがないか、トークンが変更されています。共有した相手に最新のリンクを尋ねてください。", + "player.notLive": "現在配信していません", + "player.viewer": "視聴者", + "player.viewers": "視聴者", + "player.willStart": "配信が始まると、このページは自動的に再生を開始します。", + "playlist.choose": "アップロードを選択", + "playlist.chooseAria": "追加するアップロードを選択", "playlist.empty": "まだ何も入っていません。", + "playlist.hint": "Media でアップロードします。このプレイリストが指す項目が後でアップロードを失った場合、次のフェイルオーバーで黙って失敗するのではなく、上に「要確認」として表示されます。", "playlist.itemsAria": "プレイリストの項目", - "playlist.stateReady": "準備完了", - "playlist.stateTranscoding": "変換中", - "playlist.stateAttention": "要確認", - "playlist.stateUnsaved": "未保存", "playlist.missingUpload": "このアップロードはもう見つかりません。", - "playlist.moveUp": "{name} を上へ移動", "playlist.moveDown": "{name} を下へ移動", - "playlist.remove": "{name} を削除", - "playlist.choose": "アップロードを選択", - "playlist.chooseAria": "追加するアップロードを選択", + "playlist.moveUp": "{name} を上へ移動", "playlist.noUploads": "利用できるアップロードがありません", - "playlist.hint": "Media でアップロードします。このプレイリストが指す項目が後でアップロードを失った場合、次のフェイルオーバーで黙って失敗するのではなく、上に「要確認」として表示されます。", - "dest.fbBoxTitle": "Facebook のクロス投稿と寄付ボタン", - "dest.fbBoxIntro": "「視聴者」と同じ作成呼び出しで送信されるため、同じ規則が適用されます。配信開始時に一度だけ適用され、空の場合はまったく手を触れません。", - "dest.fbBackupLabel": "バックアップ配信を公開する", - "dest.fbBackupHelp": "このストリームの 2 つ目のコピーを Facebook のバックアップ受信先へ送るため、接続が切れても配信は切れません。", - "dest.fbBackupCost": "この配信先のアップロード帯域が 2 倍になります。", - "dest.fbBackupReconnect": "オンにするとストリームが一度再接続されます。ライブ開始前に有効にしてください。", - "dest.fbCrosspostLabel": "ページへのクロス投稿", - "dest.fbCrosspostPageId": "ページ ID", - "dest.fbCrosspostAlsoPost": "投稿も行う", - "dest.fbCrosspostRemove": "このページを削除", - "dest.fbCrosspostAdd": "ページを追加", - "dest.fbCrosspostHelp": "Facebook 自身のコンソールにある数値のページ ID(ページの設定、またはページとしてログインした状態での graph.facebook.com/me/accounts)です。ここに検索はないので、名前や URL ではなく id を貼り付けてください。「投稿も行う」は、配信をそのページに共有するだけでなく、そのページとして投稿します。オフのままなら、控えめな共有だけが行われます。", - "dest.fbDonateLabel": "寄付ボタンの慈善団体 ID", - "dest.fbDonatePlaceholder": "慈善団体 ID", - "dest.fbDonateHelp": "これも Facebook の募金コンソールから得られる不透明な id であり、検索するための団体名ではありません。寄付ボタンをまったく付けない場合は空欄のままにしてください。" + "playlist.remove": "{name} を削除", + "playlist.stateAttention": "要確認", + "playlist.stateReady": "準備完了", + "playlist.stateTranscoding": "変換中", + "playlist.stateUnsaved": "未保存", + "playlist.title": "プレイリスト", + "rec.delete": "削除", + "rec.deleteDescription": "ファイルはプロキシ・サムネイル・文字起こしとともにディスクから削除されます。この操作は取り消せません。", + "rec.deleteFailed": "録画を削除できませんでした。", + "rec.deleteTitle": "この録画を削除しますか?", + "rec.deleted": "録画を削除しました。", + "rec.disabled": "無効", + "rec.disk": "ディスク", + "rec.diskRecordings": "録画", + "rec.download": "ダウンロード", + "rec.downloadNamed": "{name} をダウンロード", + "rec.duration": "長さ", + "rec.empty": "録画はまだありません。右側で録画を有効にしてストリームを開始してください。", + "rec.enabled": "有効", + "rec.file": "ファイル", + "rec.free": "空き", + "rec.haltedDefault": "録画は停止しています。ボリュームの空き容量が下限を下回っています。", + "rec.haltedRestarts": "空き容量が確保されればレコーダーは自動的に再開します。", + "rec.help.enabled": "配信しながら各入力をディスクに書き込み、すべての音声トラックを分離したまま未エンコードで保持します。録画はプラットフォームへの送出とは独立しています。オフにしてもライブ出力には影響せず、オンにしてもコストは上りの帯域ではなくディスク容量です。", + "rec.help.maxAgeHours": "合計サイズにかかわらず、この時間より古い録画を削除します。直近 1〜2 日分だけあればよい場合に便利です。0 にすると、他の制限が削除するまですべて保持します。", + "rec.help.maxGb": "polyemesis が保持する録画の合計サイズの上限です。超えると、合計が収まるまで古いものから削除されます。0 にするとサイズ制限はなくなり、その場合は経過時間か空き容量の下限だけが増加を抑えます。", + "rec.help.minFreeGb": "確実な停止装置です。ボリュームの空き容量がこれを下回ると録画が停止し、空きができれば自動的に再開します。polyemesis が書き込んでいないファイルも考慮する唯一の制限であり、共有ディスクが満杯になるのを実際に防ぐ設定です。", + "rec.help.segmentSeconds": "レコーダーが次のファイルを開始するまでの 1 ファイルの長さです。短くするとクラッシュ時の損失を抑えられ、個々のファイルも扱いやすくなりますが、ファイル数は増えます。切れ目はキーフレーム上に置かれるため、実際の長さは 1 秒ほど前後します。", + "rec.help.stemFiles": "マスターとは別に、各音声トラックを個別のファイルとしても書き出します。後で編集する際に、進行役・ゲスト・ゲーム音声を別チャンネルで扱いたい場合に便利です。1 セッションの書き込み量がおおよそトラック数の分だけ増えるため、既定ではオフです。", + "rec.help.stemFormat": "どちらもロスレスなので音質は変わりません。FLAC はサイズが約半分で、ワークフロー中のツールが読めない場合を除けばこちらが適しています。WAV はどんな環境でも開けます。8 チャンネルを超えるトラックは自動的に WAV になります。", + "rec.hideStems": "ステムを隠す", + "rec.keepForever": "0 = 無期限に保持。", + "rec.loadFailed": "録画を読み込めませんでした。", + "rec.maxAgeHours": "削除するまでの時間(時間)", + "rec.maxGb": "保持する上限(GB)", + "rec.minFreeGb": "この空き容量を下回ったら停止(GB)", + "rec.minFreeNote": "上の上限は polyemesis が書き込んだ分だけを制限します。ボリューム上の他のファイルは依然として容量を埋める可能性があります。0 = 下限なし。", + "rec.noSizeLimit": "0 = サイズ制限なし。", + "rec.recording": "録画中", + "rec.retentionNote": "保持処理は 30 秒ごとに実行されます。古いものから順に、まず経過時間、次に合計サイズで判断します。現在書き込み中のセグメントが削除されることはありません。", + "rec.saveFailed": "設定を保存できませんでした。", + "rec.savePolicy": "保持ポリシーを保存", + "rec.saved": "録画設定を保存しました。", + "rec.segmentSeconds": "セグメント長(秒)", + "rec.segments": "セグメント", + "rec.settingsTitle": "録画", + "rec.showStems": "ステムを表示", + "rec.size": "サイズ", + "rec.started": "開始", + "rec.stemFiles": "トラックごとのステムファイル", + "rec.stemFlac": "FLAC — ロスレス、サイズは約半分", + "rec.stemFormat": "ステム形式", + "rec.stemNote": "マスターと同じプロセスから書き出されるため、各ステムは互いにサンプル単位で同期したままです。8 チャンネルを超えるトラックは自動的に WAV になります。", + "rec.stemWav": "WAV — ロスレス、どこでも開ける", + "rec.stems": "ステム", + "rec.stemsTitle": "トラックごとのステムファイル {count} 個。マスターとサンプル単位で同期しています", + "rec.stemsUsage": "ステム({count} ファイル)", + "rec.subtitle": "完全なマルチトラックアーカイブ。入力のすべての音声トラックを未エンコードのまま保持します。", + "rec.title": "録画", + "rec.tracks": "トラック", + "rec.tracksTitle": "{count} 本の音声トラックを保持", + "rec.used": "使用中", + "rec.volume": "ボリューム", + "rend.aspectBlurpad": "ぼかしフィル", + "rend.aspectBlurpadHint": "残りの部分を、フレーム自体を切り抜いてぼかしたコピーで埋めます。縦型の配信で定着した作法です。", + "rend.aspectCrop": "切り抜いて埋める", + "rend.aspectCropHint": "目標の形に中央で切り抜いてから拡大縮小します。被写体の画面上の大きさは保たれますが、フレームの端は失われます。", + "rend.aspectHandling": "アスペクト比の扱い", + "rend.aspectPad": "レターボックス", + "rend.aspectPadHint": "フレーム全体を収まるように拡大縮小し、残りを単色で埋めます。失われるものはありませんが、9:16 のキャンバスに 16:9 のソースを置くとほとんどが帯になります。", + "rend.aspectStretch": "引き伸ばして合わせる", + "rend.aspectStretchHint": "目標サイズに拡大縮小し、ソースの縦横比が合わなければ絵は歪みます。他のモードができる前は、すべてのバリエーションがこの動作でした。", + "rend.boxColour": "ボックスの色", + "rend.boxOpacity": "ボックスの不透明度(%)", + "rend.builtInFont": "内蔵の既定フォント", + "rend.burnedText": "焼き込みテキスト", + "rend.chooseHardware": "ハードウェアエンコーダーを選ぶか、出力を小さくしてください。", + "rend.colourBlack": "black", + "rend.colourWhite": "white", + "rend.costBeyond": "これをソフトウェアでリアルタイムにエンコードするのは、ほとんどのマシンには荷が重すぎます", + "rend.costBeyondDetail": "{label} は 1 回のエンコードで {mpps} です。このサイズの x264 はたいていリアルタイムに追いつけず、遅れたバリエーションはそれを読むすべての配信先でフレームを落とします。しかもそれが分かるのは配信の最中です。{machine} {escape}", + "rend.costBusy": "複数のコアが占有されると考えてください", + "rend.costBusyDetail": "{label} は {encoder} で {mpps} であり、リアルタイムとしては重いエンコードです。{machine} ライブ開始後はエンコーダーの速度を確認してください。1.0× を下回っていれば遅れており、その下にある配信先はフレームを落とし始めます。 {escape}", + "rend.costMachineCores": " このマシンは {cores} 個の論理コアを報告しています。", + "rend.costModest": "軽めのソフトウェアエンコード", + "rend.costModestDetail": "{label} は {mpps} で、ほとんどのマシンで扱えます。{machine} それでも実際のエンコードが 1 つ走ります。選択したすべての配信先で共有されますが、無料ではありません。", + "rend.costNoNumbers": "コストはまだ見積もれません", + "rend.costNoNumbersDetail": "ソースをまだ解析していないため、「ソースを維持」には裏付けとなる数値がありません。入力を開始すると、ここが埋まります。", + "rend.costOffloaded": "{encoder} にオフロード", + "rend.costOffloadedDetail": "{label}({mpps})は GPU の固定機能エンコーダー上で動作するため、CPU 消費はわずかです。画質とレート制御はドライバーによって異なり、チップごとに固有の上限があります。大事な番組の前には出力を確認してください。", + "rend.create": "作成", + "rend.created": "バリエーションを作成しました。配信先で選択するとエンコードが始まります。", + "rend.custom": "カスタム…", + "rend.deintAll": "すべてのフレーム", + "rend.deintAllHint": "インターレースなのにそう申告しないソース向けです。多くの SDI ブリッジやキャプチャーカードは、何を入力されても一律に順次走査と申告するため、そうした機器では「インターレースのフレームのみ」は何も起こらず、設定が壊れているように見えます。", + "rend.deintAuto": "インターレースのフレームのみ", + "rend.deintAutoHint": "ソースがインターレースと申告したフレームだけを処理するので、順次走査のフレームはそのまま通過します。混在した素材にはこれが正解です。", + "rend.deintOff": "オフ", + "rend.deintOffHint": "ほとんどの人が使っている順次走査のソースにはこれが正解です。順次走査にインターレース解除をかけても、得るものなく眠い画になるだけです。", + "rend.deinterlace": "インターレース解除", + "rend.deleteFailed": "バリエーションを削除できませんでした。", + "rend.deleteFallbackMany": "{count} 件の配信先がパススルーに戻り、ソースの映像がそのまま送られます。ソースが各プラットフォームの受け入れ条件に今も合っているか確認してください。", + "rend.deleteFallbackOne": "{count} 件の配信先がパススルーに戻り、ソースの映像がそのまま送られます。ソースが各プラットフォームの受け入れ条件に今も合っているか確認してください。", + "rend.deleteNoDestinations": "このバリエーションはどこからも選ばれていないため、削除しても配信先は変わりません。", + "rend.deleteRenditionTitle": "{name} を削除しますか?", + "rend.editRendition": "バリエーションを編集", + "rend.empty": "バリエーションがありません。すべての配信先がパススルーであり、どこかがソースを受け取れなくなるまではそれが正解です。たとえば Twitch、Kick、X が受け付けない 4K60 の入力などです。バリエーションを追加し、それらの配信先をそこへ向けてください。", + "rend.encoder": "エンコーダー", + "rend.encoderPreset": "エンコーダープリセット", + "rend.encoderRelatedFailed": "関連するエンコーダーがこの環境で失敗したため、{name} は候補に出ていません。", + "rend.encoderTestFailed": "{name} はこのマシンでの 1 フレームのテストエンコードに失敗しました。", + "rend.encoderTestOk": "{name} はこのマシンでテストフレームをエンコードできました。", + "rend.evenNote": "いずれかの軸を 0 にするとソースの値を維持し、アスペクト比も保たれます。高さだけを指定すれば拡大縮小できます。どちらも偶数である必要があります。H.264 と HEVC には奇数サイズの色差プレーンが存在しないためです。", + "rend.font": "フォント", + "rend.frameRate": "フレームレート", + "rend.gop": "GOP", + "rend.gpuUnopenable": "GPU は存在しますが、このプロセスからは開けません", + "rend.hardwareFallback": "ハードウェア", + "rend.hardwareScanFound": "ハードウェアスキャンの結果", + "rend.height": "高さ", + "rend.hevcWarning": "このエンコーダーは HEVC を出力します。ほとんどの RTMP 配信先は H.264 のみを受け付けます。HEVC を受け付けると明示された配信先にのみ選んでください。", + "rend.keyframeInterval": "キーフレーム間隔", + "rend.letterboxColour": "レターボックスの色", + "rend.live": "ライブ", + "rend.loadFailed": "バリエーションを読み込めませんでした。", + "rend.marginX": "余白 X(%)", + "rend.marginY": "余白 Y(%)", + "rend.name": "名前", + "rend.namePlaceholder": "Twitch と Kick 向けの 1080p60", + "rend.needsBothAWidthAnd": "幅と高さの両方が必要です。片方の軸が自由なら、拡大縮小はそもそも縦横比を保ちます。", + "rend.needsBothAxes": "幅と高さの両方が必要です", + "rend.newRendition": "新しいバリエーション", + "rend.noEncoder": "この FFmpeg は使用可能な映像エンコーダーを報告しませんでした。", + "rend.noHardwareEscape": "このマシンでは動作するハードウェアエンコーダーがないため、移し先がありません。ここで動かせるのは出力サイズだけです。その後ドライバーを導入したり、コンテナーに GPU を渡したりした場合は、ハードウェアを再検出してください。", + "rend.notProbed": "このマシンではまだテストエンコードを行っていないため、この一覧はハードウェアの能力ではなく FFmpeg ビルドに含まれるものを示しています。確かめるには再検出してください。", + "rend.notRunning": "停止中", + "rend.note": "メモ", + "rend.noteDuplicateFps": "ソースは {srcFps} fps なので、{fps} fps はフレームを複製するだけです。", + "rend.notePlaceholder": "この階層の用途。", + "rend.noteUpscale": "ソースは {width}×{height} です。拡大しても CPU を消費するだけで、ディテールは増えません。", + "rend.opacity": "不透明度(%)", + "rend.percentNote": "すべてフレームに対する割合なので、16:9 の階層でも 9:16 の階層でも同じ設定がそのまま通用します。中央揃えの軸では余白は無視されます。", + "rend.preset": "プリセット", + "rend.presetPlaceholder": "veryfast", + "rend.presetStartingPoint": "出発点です。現在の上限はプラットフォーム側で確認してください。", + "rend.redetectFailed": "ハードウェアを再検出できませんでした。", + "rend.redetectTitle": "GPU スキャンを再実行し、各エンコーダーでテストフレームを 1 枚エンコードします。数秒かかります。", + "rend.redetectedNoHardware": "ハードウェアを再検出しました。このマシンでテストエンコードに成功したハードウェアエンコーダーはありませんでした。", + "rend.resolution": "解像度", + "rend.restartEncode": "このエンコードと、それを読んでいる配信先を再起動します", + "rend.restartFailed": "エンコードを再起動できませんでした。", + "rend.restarting": "{name} を再起動しています。その配信先も一緒に再接続します。", + "rend.save": "保存", + "rend.saveFailed": "バリエーションを保存できませんでした。", + "rend.seedNote": "これらはフォームの初期値を入れるだけです。保存前に自由に編集してください。プラットフォームの上限は変わりますし、パートナー資格によっても異なります。", + "rend.signalError": "エラー", + "rend.signalIdle": "アイドル", + "rend.sizePct": "サイズ(高さに対する %)", + "rend.softwareEncoderFallback": "ソフトウェアエンコーダー", + "rend.speed": "速度", + "rend.subtitle": "共有される映像エンコードは 1 つ、配信先は多数。ここで音声に手を加えることはありません。", + "rend.target": "ターゲット", + "rend.textColour": "文字色", + "rend.textPlaceholder": "MY STATION", + "rend.title": "バリエーション", + "rend.unused": "これを選択している配信先がまだないため、エンコードは行われていません。配信先で選ぶとエンコードが始まります。", + "rend.uptime": "稼働時間", + "rend.videoBitrate": "映像ビットレート", + "rend.waitingSnapshot": "最初のライブスナップショットを待っています。", + "rend.watermarkImage": "ウォーターマーク画像", + "rend.watermarkPlaceholder": "overlays/logo.png", + "rend.width": "幅", + "rend.widthPct": "幅(フレームに対する %)", + "route.attack": "アタック", + "route.audio": "音声", + "route.audioAndVideoGoOut": "音声と映像が一緒に出ます。", + "route.audioLaterThanVideo": "音声が映像より遅い", + "route.audioRouting": "音声ルーティング", + "route.audioRoutingDesc": "各配信先が受け取る入力トラックを正確に選びます。", + "route.autoLimitWhen2Tracks": "自動(2 トラック以上で制限)", + "route.commentaryLanguage": "実況の言語", + "route.couldNotApplyThePreset": "プリセットを適用できませんでした。", + "route.couldNotCompileThisRouting": "このルーティングをコンパイルできませんでした。", + "route.couldNotLoadDestinations": "配信先を読み込めませんでした。", + "route.couldNotSaveTheRouting": "ルーティングを保存できませんでした。", + "route.couldNotSaveTrackRoles": "トラックの役割を保存できませんでした。", + "route.custom": "カスタム…", + "route.delayDirection": "遅延の方向", + "route.destinations": "配信先", + "route.duckingNote": "別のグループが話しているあいだ、片方のトラック群のレベルを下げます。音楽ベッドの上にマイクを重ねる場合などです。オフの場合、選択した各トラックはそれぞれのゲインのまま合成されます。", + "route.duckingTriggerNote": "トリガーがこの配信先のミックスに含まれている必要はありません。音楽だけを送りながら、マイクでここの音楽を下げることもできます。", + "route.enableDucking": "ダッキングを有効にする", + "route.everyTrackReaches": "選択したすべてのトラックが、音楽も含めてこの配信先に届きます。", + "route.excludeMusic": "音楽を除外", + "route.keepMusic": "音楽を残す", + "route.keepMusicOut": "この配信先に音楽を送らない", + "route.labelTracks": "トラックにラベルを付ける", + "route.langPlaceholder": "lang — 例: es", + "route.limiter04DbfsCeiling": "リミッター(−0.4 dBFS 上限)", + "route.localOnlyNote": "このサーバーはまだトラックの役割を保存しないため、これらはこのブラウザーに留まります。ページ上のそれ以外の項目は通常どおり保存されます。", + "route.loudnessTarget": "ラウドネスターゲット", + "route.lufsBroadcast": "EBU R128 放送", + "route.measuredOverTheWholeProgramme": "瞬間ごとではなく番組全体で測定します。静かな箇所は静かなままです。これらは各プラットフォーム自身のノーマライザーが目標とする数値であり、どれかに合わせることがプラットフォームに音量を下げられずに済む方法です。", + "route.mixMatrix": "ミックスマトリクス", + "route.musicPolicy": "音楽の扱い", + "route.noTarget": "ターゲットなし(レベルはそのまま)", + "route.normalizeLoudnorm": "ラウドネス(EBU R128)", + "route.normalizeOff": "オフ", + "route.off": "オフ", + "route.offset": "オフセット", + "route.on": "オン", + "route.platformDefault": "プラットフォームの既定", + "route.podcastSpokenWord": "ポッドキャスト / トーク", + "route.presetApplied": "プリセットを適用しました。内容を確認して保存してください。", + "route.range": "レンジ", + "route.ratio": "レシオ", + "route.release": "リリース", + "route.result": "結果", + "route.role51": "5.1", + "route.roleClean": "クリーン", + "route.roleMic": "マイク", + "route.roleMusic": "音楽", + "route.rolesNote": "役割は配信先ではなく入力を説明するもので、単独で保存されます。下にある配信先が要求するまで、何もフィルタリングされません。", + "route.simple": "シンプル", + "route.target": "ターゲット", + "route.targetRole": "ターゲット", + "route.thisDestinationSPlatform": "この配信先のプラットフォーム", + "route.threshold": "スレッショルド", + "route.title": "Routing", + "route.triggerRole": "トリガー", + "route.truePeak": "トゥルーピーク", + "route.video": "映像", + "route.videoLaterThanAudio": "映像が音声より遅い", + "route.withoutATargetTheMix": "ターゲットを設定しない場合、ミックスは入力が出したレベルのまま送出され、上のクリップ保護だけが適用されます。", + "route.youtubeTwitchSpotify": "YouTube, Twitch, Spotify", + "set.acceptSelfSigned": "自己署名証明書を受け入れる", + "set.accountDisconnected": "アカウントの接続を解除しました。", + "set.adminPassword": "管理者パスワード", + "set.alertDelivery": "アラートの配信", + "set.alertDeliveryDesc": "応答しないウェブフックをどこまで追いかけてから、アラートをあきらめるか。", + "set.apiTokens": "API トークン", + "set.apiTokensDesc": "スクリプトや Prometheus など、セッションを保持できないものに向けた長期有効の認証情報です。Authorization: Bearer pmk_… として送信してください。トークンは管理者と同じことをすべて実行できますが、トークンの管理だけはできません。それはパスワードの背後に残るため、失効させると元に戻せません。", + "set.app": "アプリ", + "set.attemptsBackoff": "試行間の待ち時間は自動的に伸び、上限は 30 秒です。したがってこの範囲の上端は、応答しないエンドポイントを数分間追いかけ続けることを意味します。上限があること自体が要点です。無限に再試行すると、たった 1 つの死んだウェブフックが決して掃けないキューに変わります。", + "set.attemptsNote": "最初の試行を含みます。1 は再試行しないという意味です。", + "set.attemptsRetryable": "再試行する価値のある失敗だけが再試行されます。誰かが削除したウェブフックから返る 404 は恒久的で、さらに 4 回試しても後ろに並んだアラートをすべて遅らせるだけです。リバースプロキシから返る 502 こそ、この試行が存在する理由です。", + "set.audioMeters": "音声メーター", + "set.brokerPasswordCleared": "ブローカーのパスワードを消去しました。", + "set.brokerPlainNote": "mqtt:// は暗号化されません。MQTT はユーザー名とパスワードを CONNECT パケットで送るため、どちらも平文でネットワークを通ります。信頼できる LAN 内なら問題なく、これは Home Assistant の一般的な構成です。ただし自分の管理下にないものを経由する場合は不適切です。ブローカーが対応しているなら mqtts:// を使ってください。", + "set.brokerUrl": "ブローカー URL", + "set.brokerUrlNote": "mqtt://、mqtts://、ws:// または wss://。URL 内のパスワードは拒否されます。URL はログ行や ps の出力に残り、後から取り消せません。下の欄に入力してください。そこでは保存時に暗号化されます。", + "set.brokerUrlPlaceholder": "mqtts://broker.local:8883", + "set.chatBothApply": "両方が適用され、寛容なほうが優先されます。メッセージは、指定時間より古く、かつ最新 N 件の外に出たときに削除されます。したがって同じ設定でも、にぎやかなチャンネルは静かなチャンネルより短い期間しか保持しません。", + "set.chatBufferNote": "「接続時に送信」は他の 2 つとは性質の違う数値です。ほかの 2 つはディスクに残す量を制限しますが、これはメモリ上のバッファで、接続中のダッシュボードごとに確保されます。", + "set.chatHistory": "チャット履歴", + "set.chatHistoryDesc": "チャット履歴がどこまでさかのぼれるか、そしてそれに伴い、モデレーターが誰かのカードを開いたときにどれだけ見られるか。", + "set.chatHours": "保持期間(時間)", + "set.chatKeep": "常に保持(メッセージ数)", + "set.chatKeepNote": "古さにかかわらず下限として残します。", + "set.chatNotOnlyDisk": "これはディスクだけの設定ではありません。チャットで名前をクリックするとその人が以前話した内容が開きますが、その履歴はここから来ます。どのプラットフォームもそのための API を公開していないため、polyemesis は保持していたものしか表示できません。短くしすぎると、一週間ずっと問題を起こしている人物が、何も発言していないかのように見えます。", + "set.chatSendNote": "クエリなしの履歴です。", + "set.chatSendOnConnect": "接続時に送信(メッセージ数)", + "set.chatSendOnConnectShort": "接続時に送信", + "set.chatSmall": "チャットの容量はわずかです。毎秒平均 10 件のチャンネルで 1 時間あたりおよそ 7 MB。ほとんどはそれよりはるかに少なくなります。", + "set.clientId": "クライアント ID", + "set.clientIdNote": "ブローカー上で一意である必要があります。衝突は、原因不明の再接続ループの最大の要因です。ブローカーは接続のたびに古いセッションを切断し、両方のクライアントが延々と再接続し続けます。", + "set.clientIdPlaceholder": "インスタンス名から導出", + "set.clientSecret": "クライアントシークレット", + "set.comingBack": "復帰", + "set.confirmPassword": "新しいパスワードの確認", + "set.connectedAccounts": "接続済みアカウント", + "set.copied": "コピーしました", + "set.copy": "コピー", + "set.couldNotChangeThePassword": "パスワードを変更できませんでした。", + "set.couldNotClearThePassword": "パスワードを消去できませんでした。", + "set.couldNotCreateTheToken": "トークンを作成できませんでした。", + "set.couldNotReCheckThe": "資格情報を再確認できませんでした。", + "set.couldNotReadTheTls": "TLS の状態を読み取れませんでした。", + "set.couldNotRevokeTheToken": "トークンを失効できませんでした。", + "set.couldNotSaveTheCredentials": "資格情報を保存できませんでした。", + "set.couldNotSaveTheMqtt": "MQTT の設定を保存できませんでした。", + "set.credsRemoved": "認証情報を削除しました。", + "set.currentPassword": "現在のパスワード", + "set.dataDirectory": "データディレクトリ", + "set.deliveryAttempts": "配信試行回数", + "set.dialsOnly": "サーバーが接続するのは次のみです:{schemes}。", + "set.disconnect": "接続を解除", + "set.disconnectDesc": "これを使用している配信先は設定を保持しますが、ストリームキーの更新を停止します。同じアカウントは後から再接続できます。", + "set.disconnectTitle": "このアカウントの接続を解除しますか?", + "set.drawtextMissing": "このビルドには drawtext フィルターがないため、テキストオーバーレイを利用できません。libfreetype 付きでビルドされた FFmpeg が必要です。", + "set.drawtextOk": "このビルドは映像に文字を描画できます。", + "set.enabled": "有効", + "set.encoderUrl": "エンコーダー URL", + "set.expires": "有効期限", + "set.failover": "フェイルオーバー", + "set.failoverDesc": "ライブソースが配信を止めたときのための予備入力と待機カードです。既定では無効で、無効のあいだパイプラインはこの機能が存在する前とバイト単位で同一です。", + "set.failoverRemuxNote": "これを有効にすると、入力とその下のすべての間にリマックス処理が 1 段追加されます。安価ですが無料ではありません。だからこそ既定値ではなく、選択すべき設定になっています。", + "set.fileSizeMb": "ファイルサイズ (MB)", + "set.filesKept": "保持ファイル数", + "set.goingLive": "配信開始時", + "set.goingLiveDesc": "複数の配信先が同時に開始したときに何が起きるか。", + "set.graceNote": "階層が切り替わるまでに、ライブソースが何も送らないでいられる時間です。再接続よりも意図的に長くしてあります。RTMP 接続を張り直しているエンコーダーは通常動作であり、障害ではありません。", + "set.gracePeriod": "猶予時間(秒)", + "set.haDiscovery": "Home Assistant ディスカバリー", + "set.height": "高さ", + "set.ingest": "入力", + "set.ingestDesc": "SRT は最大 6 本の音声トラックを運びます。RTMP は 1 本を運び、SRT を扱えないエンコーダー向けの代替として存在します。プルは、配信を待つのではなく自分からソースへ接続します。", + "set.ingestUrlCopied": "入力 URL をコピーしました。", + "set.instance": "インスタンス", + "set.intervalNote": "変化のない状態は再送されないため、間隔を短くしてもコストはメッセージではなく比較処理です。キープアライブは、切れたリンクがどれだけ気づかれずに残るか、ひいてはブローカーが購読者にこのインスタンスの消失を伝えるまでの速さを決めます。", + "set.issuer": "発行者", + "set.kbps": "kbps", + "set.keepAlive": "キープアライブ (s)", + "set.latencyMs": "レイテンシ (ms)", + "set.listeners": "リスナー", + "set.listenersDesc": "サーバーがバインドする場所で、インストール全体に適用されます。1 つの SRT ポートがすべてのソースに対応します。ソースは公開トークンで区別されるため、ソースを追加してもポートを増やす必要はありません。RTMP にはそうしたルーティングがないため、そのポートは 1 つのソースだけを扱います。", + "set.logsBoundNote": "ログディレクトリはこの 2 つの積で上限が決まるため、ログの保存がディスクを埋め尽くすことはありません。", + "set.logsDesc": "取得した FFmpeg の標準エラー出力は常にメモリ上に保持されますが、それはサーバーとともに失われます。つまり、なぜ落ちたのかを説明する行がまさに失われます。永続化すると、データディレクトリ配下のローテーションファイルに複製されます。", + "set.metersDesc": "補助プロセスが 1 つ、すべてのトラックのすべてのチャンネルを計測します。間隔を短くすると更新は速くなり、CPU 消費は増えます。", + "set.mode": "モード", + "set.modePull": "プル — カメラ・配信・ファイルへ接続", + "set.modeRtmp": "RTMP — 単一トラック", + "set.modeSrt": "SRT — マルチトラック(推奨)", + "set.mqtt": "MQTT テレメトリ", + "set.mqtt5Only": "MQTT 5.0 のみ。", + "set.mqtt5OnlyNote": "3.1.1 に固定されたブローカーは接続をまったく確立できません。機能が制限されるモードではありません。Mosquitto 2.x、EMQX、Home Assistant のアドオンはいずれも 5.0 に対応しています。", + "set.mqttDesc": "状態を retained メッセージとしてブローカーに公開します。そのため、変更時に接続していなかった購読者でも現在の値を受け取れます。", + "set.multiAccountNote": "同じプラットフォームを複数回接続すると、複数のチャンネルへ配信できます。アカウントごとに独立した配信先になります。", + "set.name": "名前", + "set.newPassword": "新しいパスワード", + "set.no": "いいえ", + "set.noEncryption": "この接続は暗号化されていません。パスワードとセッションクッキーが平文でネットワークを流れます。tls.mode: auto を設定するか、プロキシの背後で 127.0.0.1 にバインドしてください。", + "set.noLibsrt": "この FFmpeg ビルドは SRT に対応していません。--enable-libsrt を付けて構成したビルドを導入するか、RTMP を使用してください。", + "set.noTokens": "トークンはまだありません。", + "set.passphrase": "パスフレーズ", + "set.passphraseNote": "AES 暗号化を有効にします。使用しない場合は空欄のままにしてください。SRT は 10〜79 文字の範囲外のパスフレーズを拒否します。", + "set.passphrasePlaceholder": "省略可 — 10〜79 文字", + "set.password": "パスワード", + "set.passwordChanged": "パスワードを変更しました。", + "set.passwordMismatch": "新しいパスワードが一致しません。", + "set.passwordNote": "パスワードはどの API からも返されないため、設定済みでもこの欄は空になります。保存済みのパスワードを保つには空のままにしてください。消去するには下のボタンを使います。", + "set.platform": "プラットフォーム", + "set.platformsDesc": "polyemesis がどこまで自動化できるかはプラットフォームによって異なり、その違いは 1 時間経ってからではなく始める前に知っておく価値があります。配信そのものはどこでも同じように動作します。配信先ごとの音声ルーティング、バリエーション、再接続、メーターは、下のどの列にも依存しません。", + "set.playLoop": "何も配信していないときにループを再生する", + "set.playLoopNote": "待機カードと並ぶもう一つの候補で、静止画の代わりにファイルを再生します。両方を有効にすることもできます。どちらが優先されるかはフェイルオーバー設計の階層順を参照してください。", + "set.pointObs": "OBS をこのアドレスに向けてください。", + "set.preview": "プレビュー", + "set.previewDesc": "ダッシュボードの HLS モニターです。polyemesis が映像を再エンコードするのはここだけで、独自のリレー購読上で動作するため、配信先に影響を与えることはありません。", + "set.previewIdleNote": "エンコーダーはダッシュボードがプレイリストを要求したときに起動し、最後の要求からこの時間が経つと再び停止します。そのためダッシュボードを閉じていれば CPU は消費しません。この値を変更しても、誰かが視聴中のプレビューが再起動されることはありません。", + "set.processLogs": "プロセスログ", + "set.proxyTerminatesTls": "polyemesis の前段にあるリバースプロキシが TLS を終端するため、このサーバーは意図的に平文の HTTP を提供します。", + "set.publishInterval": "公開間隔 (s)", + "set.reconnectCap": "再接続待機の上限 (s)", + "set.reconnectCapNote": "0 で既定値を使用します。HTTP および HLS ソースのみ。", + "set.redirectNote": "スキームとポートを含め、完全に一致している必要があります。リバースプロキシ経由で polyemesis にアクセスしている場合は、その公開 URL を使ってください。", + "set.redirectUri": "許可するリダイレクト URI", + "set.removeCreds": "認証情報を削除", + "set.removeCredsDesc": "このアプリ経由で接続しているすべてのアカウントがトークンの更新を停止し、再接続が必要になります。クライアントシークレットはここからは復元できず、プラットフォーム側で新しいアプリを作成することになります。", + "set.removeCredsTitle": "この開発者認証情報を削除しますか?", + "set.returnAfter": "復帰までの時間(安定した秒数)", + "set.returnAutomatic": "自動 — プライマリが安定したら戻る", + "set.returnManual": "手動 — 自分で切り替えるまでバックアップのまま", + "set.returnManualNote": "既定が手動なのは、自動復帰が行ったり来たりしかねないためです。断続的に回復するプライマリは、放送の途中で番組を何度も往復させてしまいます。", + "set.rtmpPort": "RTMP ポート", + "set.rtspNote": "カメラが対応できない場合を除き TCP を使用してください。NAT 越しの UDP による RTSP は、接続しているように見えて何も届きません。", + "set.rtspTransport": "RTSP トランスポート", + "set.saveAndDrop": "保存してライブ配信を切断", + "set.saveIngest": "入力設定を保存", + "set.segmentSec": "セグメント (s)", + "set.settingsSavedAffectedProcessesHave": "設定を保存しました。影響を受けるプロセスを再起動しました。", + "set.showSlate": "何も配信していないときに待機カードを表示する", + "set.slateColour": "待機カードの色", + "set.slateColourNote": "1 語で指定します。値はフィルターグラフに渡され、そこではカンマが引数の終わりになるためです。画像が設定されていない場合にのみ使用されます。", + "set.slateColourPlaceholder": "black", + "set.slateImage": "待機カードの画像", + "set.slateImageNote": "データディレクトリ内のパスです。空のままにすると代わりに単色で塗りつぶします。色には開けないファイルが存在しないからこそ、これが代替手段になっています。", + "set.slateImagePlaceholder": "slate/holding.png", + "set.sourceUrl": "ソース URL", + "set.sourceUrlNote": "{schemes} のいずれかです。file:// のソースはデータディレクトリからの相対パスであり、「..」を含められません。アップロードされたファイルと同じ方法でそこに閉じ込められます。", + "set.sourceUrlPlaceholder": "rtsp://camera.local/stream1", + "set.srtPort": "SRT ポート", + "set.stagger": "接続をずらす (ms)", + "set.staggerNote": "0 で無効です。そうしない場合、配信開始とはすべての配信先が同じ瞬間に接続を開き、TLS をネゴシエートし、音声のエンコードを始めることを意味します。小さなマシンではフレーム落ちが最も起きやすい瞬間であり、しかも配信を始めた直後なので、あなたが最も注視している瞬間でもあります。数百ミリ秒あれば十分です。", + "set.staggerReconnectNote": "再接続が遅らされることはありません。深夜 3 時に切断された配信先は、すでに正常な処理の後ろで順番を待つことなく、直ちに復帰します。", + "set.stepByStep": "手順ごとのセットアップ", + "set.stopAfterIdle": "アイドル後に停止 (s)", + "set.streamKey": "ストリームキー", + "set.streamKeyNote": "配信側はこのパスを正確に使う必要があります。そうすることで、開いているポートが開いた扉にはなりません。", + "set.subject": "サブジェクト", + "set.synthesiseNote": "主要なプラットフォームはいずれも音声トラックのないストリームを拒否します。これを有効にすると、映像のみの入力に無音のステレオトラックが 1 本追加され、配信先が開始します。無効の場合は「routing profile selects no audio」で拒否されます。適用されるのはトラック数ゼロと判定された入力に限られるため、実際に送っている音声を押しのけることはありません。", + "set.synthesiseSilent": "無音トラックを生成する", + "set.syntheticAudio": "合成音声", + "set.syntheticDesc": "入力に映像があり音声がまったくない場合の動作。", + "set.tabIngest": "入力", + "set.tabPipeline": "パイプライン", + "set.tabPlatforms": "プラットフォーム認証情報", + "set.tabSecurity": "セキュリティ", + "set.title": "設定", + "set.tlsAcme": "Let's Encrypt", + "set.tlsAuto": "自動", + "set.tlsDesc": "ここではなく config.yaml で設定します。サーバーが待ち受けを開始する前に TLS が正しくなっている必要があるためです。このカードはその結果を報告します。", + "set.tlsManual": "手動の証明書", + "set.tlsOff": "ここでは終端しない", + "set.tlsSelfsigned": "自己署名", + "set.tokenHashNote": "polyemesis はハッシュのみを保存します。紛失した場合は、このトークンを失効させて新しく作成してください。", + "set.tokenNamePlaceholder": "prometheus", + "set.tokenRevoked": "{name} を失効しました。", + "set.topicPrefix": "トピックの接頭辞", + "set.topicPrefixNote": "接頭辞の区切り文字はそのまま保たれるため、home/av は 2 階層を意味します。$ で始まる接頭辞は拒否されます。ブローカーがそれを予約しており、# を使う購読者には決して届かないためです。テレメトリは正常に公開されたうえで、まさに調査に使うビューからだけ見えなくなります。", + "set.transportSecurity": "トランスポートセキュリティ", + "set.updateInterval": "更新間隔 (ms)", + "set.username": "ユーザー名", + "set.validFor": "有効期間", + "set.whatPlatformsDo": "各プラットフォームでできること", + "set.whyDevApp": "自分の開発者アプリが必要な理由", + "set.whyDevAppDesc": "polyemesis は OAuth のクライアントシークレットを同梱できません。バイナリを持つ人全員がそれを手にすることになり、プラットフォーム側が失効させてしまうためです。無料の開発者アプリを一度登録し、クライアント ID とシークレットをここに貼り付ければ、あとは polyemesis があなたに代わってストリームキーを取得します。", + "set.writeLogs": "ログをディスクに書き込む", + "set.yes": "はい", + "sources.addDescription": "独自の入力・配信先・バリエーションを持つ 2 つ目の番組です。たとえば横位置のキャンバスと並べる縦位置のキャンバスなど。既定のポートで開始するため、両者が競合しないようポートを変更してください。", + "sources.addFailed": "ソースを追加できませんでした", + "sources.addSource": "ソースを追加", + "sources.addTitle": "ソースを追加", + "sources.added": "「{name}」を追加しました。下で入力ポートを設定してください。", + "sources.app": "アプリ", + "sources.applyAndDrop": "適用してストリームを切断", + "sources.copied": "{what} をコピーしました", + "sources.copyFailed": "{what} をコピーできませんでした", + "sources.copyTokenAria": "トークンをコピー", + "sources.copyUrlAria": "{proto} の URL をコピー", + "sources.default": "既定", + "sources.defaultDescription": "ソースを指定しないリクエストはこのソースに対して実行されます。", + "sources.deleteAria": "{name} を削除", + "sources.deleteConfirm": "ソースを削除", + "sources.deleteDescription": "配信先とバリエーションも一緒に削除されます。これらはこの番組の送り先を表すものであり、番組なしでは意味を持ちません。録画は保持されます。ファイルはディスク上に残り再生も可能で、ソースへの紐付けがなくなるだけです。", + "sources.deleteFailed": "ソースを削除できませんでした", + "sources.deleteThis": "このソースを削除", + "sources.deleteTitle": "「{name}」を削除しますか?", + "sources.deleted": "「{name}」とその配信先を削除しました", + "sources.destinations": "配信先", + "sources.dirtyIdle": "入力設定に未保存の変更があります。適用するとこのソースの入力が再起動します。", + "sources.dirtyPublishing": "このソースへエンコーダーが配信中です。適用すると入力が再起動し、視聴者全員の接続が切れます。", + "sources.enableAria": "{name} を有効にする", + "sources.help.app": "RTMP のアプリケーションパス、つまり URL のホストとストリームキーの間の部分です。ほとんどのエンコーダーは「live」を想定します。理由がある場合のみ変更してください。", + "sources.help.ingest": "エンコーダーがこのソースへ到達する方法です。SRT は失われたパケットを再送することで不安定な回線に耐えるため、公衆インターネット経由では既定として適切です。RTMP は古くから広く対応されていますが、不安定な回線では品質が大きく低下します。プルでは、配信されるのを待つ代わりに polyemesis が指定した URL から取得します。", + "sources.help.latencyMs": "SRT が遅延パケットや再送パケットを諦めるまで待つ時間です。値を大きくすると遅延と引き換えに悪い回線に耐えられます。往復時間の約 2.5 倍が目安で、RTT が 40 ms ならここは約 100 ms です。小さすぎると、わずかな乱れが目に見える不具合になります。", + "sources.help.passphrase": "SRT 接続を暗号化します。現状、この入力から部外者を締め出しているのは実質これです。10〜79 文字。空のままにすると、ポートに到達できる人は誰でも配信できます。", + "sources.help.streamKey": "エンコーダーが RTMP で配信するために送る秘密の文字列です。この入力を保護する認証情報なので、パスワードと同様に扱ってください。これを持つ人は誰でもあなたの配信先へ送出できます。", + "sources.help.token": "複数のソースが 1 つの入力ポートを共有する際に、このソースを識別します。アクセス制御も兼ねるかどうかは入力モードによります。現在どちらであるかは下の注記が示します。更新すると新しいトークンが発行され、古いものも 5 分間有効なままなので、配信中のエンコーダーが途中で切断されることはありません。", + "sources.ingest": "入力", + "sources.lastSourceTitle": "最後のソースは削除できません。インストールには入力が 1 つ必要です", + "sources.latencyMs": "レイテンシ (ms)", + "sources.loadFailed": "ソースを読み込めませんでした", + "sources.loading": "ソースを読み込み中…", + "sources.loss": "損失", + "sources.modePull": "プル", + "sources.name": "名前", + "sources.namePlaceholder": "縦位置", + "sources.noEngine": "このソースのエンジンが実行されていません。通常の原因は、入力ポートが他のソースまたは他のプロセスによってすでに使用されていることです。", + "sources.notRunning": "停止中", + "sources.ownDescription": "独自の配信先・バリエーション・録画。", + "sources.passphrase": "パスフレーズ", + "sources.passphrasePlaceholder": "10〜79 文字", + "sources.publishing": "配信中", + "sources.renditions": "バリエーション", + "sources.retrans": "再送", + "sources.rotate": "更新", + "sources.rotateConfirm": "トークンを更新", + "sources.rotateFailed": "トークンを更新できませんでした", + "sources.rotateIdle": "新しいトークンが直ちに発行されます。すでに使用中のエンコーダーが切断されないよう、古いトークンも 5 分間有効です。", + "sources.rotateTitle": "「{name}」のトークンを更新しますか?", + "sources.rotateWhilePublishing": "現在このソースへエンコーダーが配信中です。古いトークンは 5 分間有効なため接続はすぐには切れませんが、その期限までに更新する必要があります。", + "sources.rtt": "RTT", + "sources.running": "実行中", + "sources.saveFailed": "保存できませんでした", + "sources.streamKey": "ストリームキー", + "sources.subtitle": "それぞれが 1 つの入力番組です。配信先とバリエーションはソースに属します。", + "sources.title": "ソース", + "sources.token": "トークン", + "sources.tokenEnforcedDetailRtmp": "これがストリームキーです。上記の URL にこれをキーとして配信してください。ローテーションすると新しいものが発行され、古いものも5分間有効なので、配信を止めずに切り替えられます。", + "sources.tokenEnforcedDetailSrt": "共有ポート経由でこのソースに配信するには、エンコーダーの SRT streamid に入力してください。ローテーションすると新しいものが発行され、古いものも5分間有効なので、配信を止めずに切り替えられます。", + "sources.tokenIsCredential": "このトークンが認証情報です。", + "sources.tokenIssued": "新しいトークンを発行しました", + "sources.tokenNotEnforced": "このトークンは保存されていますが、現在は強制されていません。", + "sources.tokenNotEnforcedRtmp": "ソースはポートによって分離されています。この入力を実際に保護しているのは上のストリームキーです。トークンを認証情報にするには、設定で単一ポート入力を有効にしてください。", + "sources.tokenNotEnforcedSrt": "ソースはポートによって分離されています。この入力を実際に保護しているのは上の SRT パスフレーズです。トークンを認証情報にするには、設定で単一ポート入力を有効にしてください。", + "state.disabled": "無効", + "state.failed": "エラー", + "state.live": "ライブ", + "state.offline": "オフライン", + "state.reconnecting": "再接続中", + "state.starting": "開始中", + "state.stopped": "停止しました" } diff --git a/ui/src/lib/i18n/ko.json b/ui/src/lib/i18n/ko.json index e6dbe39a..fd71be35 100644 --- a/ui/src/lib/i18n/ko.json +++ b/ui/src/lib/i18n/ko.json @@ -1,26 +1,227 @@ { - "nav.dashboard": "대시보드", - "nav.sources": "소스", - "nav.meters": "오디오 레벨 표시기", - "nav.routing": "Routing", - "nav.renditions": "렌더링 설정", - "nav.playout": "Playout", - "nav.library": "라이브러리", - "nav.clips": "클립", - "nav.chat": "채팅", - "nav.jobs": "작업", - "nav.automation": "자동화", - "nav.recordings": "녹화", - "nav.monitoring": "모니터링", - "nav.settings": "설정", - "chrome.toggleNav": "탐색 메뉴 열기/닫기", + "auth.accountCreated": "관리자 계정을 만들었습니다.", + "auth.confirmPassword": "비밀번호 확인", + "auth.createAccount": "계정 만들기", + "auth.firstRun": "최초 실행 설정", + "auth.firstRunDesc": "이 서버의 관리자 자격 증명을 정하세요. 계정 복구 수단이 없으므로 안전한 곳에 보관하세요.", + "auth.password": "비밀번호", + "auth.passwordMismatch": "비밀번호가 일치하지 않습니다.", + "auth.signIn": "로그인", + "auth.signInDesc": "관리자 자격 증명을 입력하세요.", + "auth.somethingWentWrong": "문제가 발생했습니다.", + "auth.tokenCreated": "API 토큰을 만들었습니다", + "auth.tokenRevoked": "API 토큰을 취소했습니다", + "auth.username": "사용자 이름", + "auto.action": "동작", + "auto.adminPasswordChanged": "관리자 비밀번호 변경됨", + "auto.chosenWeekdays": "선택한 요일", + "auto.coalesced": "병합됨", + "auto.coalescingWindow": "병합 구간(초)", + "auto.couldNotChangeTheRule": "규칙을 변경할 수 없습니다.", + "auto.couldNotChangeTheSchedule": "일정을 변경할 수 없습니다.", + "auto.couldNotDeleteTheRule": "규칙을 삭제할 수 없습니다.", + "auto.couldNotDeleteTheSchedule": "일정을 삭제할 수 없습니다.", + "auto.couldNotLoadAutomationSettings": "자동화 설정을 불러올 수 없습니다.", + "auto.couldNotSaveTheRule": "규칙을 저장할 수 없습니다.", + "auto.couldNotSaveTheSchedule": "일정을 저장할 수 없습니다.", + "auto.createRule": "규칙 만들기", + "auto.createSchedule": "일정 만들기", + "auto.days": "요일", + "auto.delete": "삭제", + "auto.deleteRule": "규칙 삭제", + "auto.deleteRuleDesc": "규칙이 더 이상 발동하지 않습니다. 웹훅 엔드포인트는 그대로 남으며, 규칙은 다시 만들 수 있습니다.", + "auto.deleteRuleTitle": "이 알림 규칙을 삭제할까요?", + "auto.deleteSchedule": "일정 삭제", + "auto.deleteScheduleDesc": "일정이 더 이상 실행되지 않습니다. 이미 시작한 것에는 영향이 없습니다.", + "auto.deleteScheduleTitle": "이 일정을 삭제할까요?", + "auto.delivery": "전달", + "auto.destinationFallingBehindRealtime": "대상이 실시간보다 뒤처짐", + "auto.destinationKeepingUpAgain": "대상이 다시 따라잡음", + "auto.destinationRecovered": "대상 복구됨", + "auto.destinationWentDown": "대상이 중단됨", + "auto.destinations": "대상", + "auto.dropped": "버림", + "auto.edit": "편집", + "auto.editRule": "알림 규칙 편집", + "auto.editSchedule": "일정 편집", + "auto.endpoint": "엔드포인트", + "auto.evAudioClipping": "오디오 클리핑", + "auto.evClipCaptured": "클립 캡처됨", + "auto.evDiskLow": "디스크 공간 부족", + "auto.evDiskRecovered": "디스크 복구됨", + "auto.evIngestLost": "입력 끊김", + "auto.evIngestRecovered": "입력 복구됨", + "auto.evSettingsChanged": "설정 변경됨", + "auto.evSignedIn": "로그인됨", + "auto.events": "이벤트", + "auto.everyDay": "매일", + "auto.failed": "실패", + "auto.failoverSwitchedSource": "장애 조치가 소스를 전환함", + "auto.fired": "발동", + "auto.firesAt": "발동 시각", + "auto.lastDelivery": "마지막 전달", + "auto.lastSweep": "마지막 훑기", + "auto.latenessAllowance": "지각 허용 시간(초)", + "auto.loudnessBackInCompliance": "라우드니스가 다시 준수 범위로", + "auto.loudnessOutOfCompliance": "라우드니스 준수 범위 이탈", + "auto.minGap": "메시지 사이 최소 간격(초)", + "auto.minSeverity": "최소 심각도", + "auto.missedNote": "서버가 멈춰 있어 놓친 실행은 늦게 발동시키지 않고 처리됨으로 표시한 뒤 건너뜁니다. 네 시간 늦게 저절로 시작되는 방송은 아예 시작되지 않은 방송보다 나쁘기 때문입니다.", + "auto.name": "이름", + "auto.newRule": "새 알림 규칙", + "auto.newSchedule": "새 일정", + "auto.next": "다음", + "auto.nothingFired": "아직 발동한 것이 없습니다. 스케줄러는 20초마다 훑습니다.", + "auto.once": "한 번", + "auto.payloadFormat": "페이로드 형식", + "auto.pending": "대기 중", + "auto.repeatedFailedSignIns": "반복된 로그인 실패", + "auto.repeats": "반복", + "auto.retries": "재시도", + "auto.rule": "규칙", + "auto.ruleCreated": "알림 규칙을 만들었습니다. 테스트를 보내 보세요.", + "auto.ruleDeleted": "알림 규칙을 삭제했습니다.", + "auto.rulePlaceholder": "Discord — 방송 알림", + "auto.ruleSaved": "알림 규칙을 저장했습니다.", + "auto.saveRule": "규칙 저장", + "auto.saveSchedule": "일정 저장", + "auto.schedule": "일정", + "auto.scheduleCreated": "일정을 만들었습니다.", + "auto.scheduleDeleted": "일정을 삭제했습니다.", + "auto.schedulePlaceholder": "평일 저녁 방송", + "auto.scheduleSaved": "일정을 저장했습니다.", + "auto.schedules": "일정", + "auto.secretMasked": "비밀 값이 경로에 들어 있으므로 가려서 표시합니다. 현재 엔드포인트를 유지하려면 그대로 두고, 바꾸려면 새 URL을 붙여 넣으세요.", + "auto.sendTest": "테스트 알림 보내기", + "auto.sendTestTitle": "지금 테스트 알림을 보냅니다", + "auto.sent": "보냄", + "auto.skipped": "건너뜀", + "auto.startDestinations": "대상 시작", + "auto.startPlaylist": "재생목록 시작", + "auto.stopDestinations": "대상 중지", + "auto.stopPlaylist": "재생목록 중지", + "auto.subscribedTo": "구독 대상", + "auto.subtitle": "무언가 고장 났음을 알려 주는 웹훅과, 사용자 없이도 방송을 진행하는 일정.", + "auto.theEndpointDidNotAccept": "엔드포인트가 테스트 메시지를 받지 않았습니다.", + "auto.time": "시각", + "auto.timeZone": "시간대", + "auto.title": "자동화", + "auto.tzPlaceholder": "UTC", + "auto.webhookHint": "Discord와 Slack 모두 채널 설정에서 하나 발급해 줍니다.", + "auto.webhookPlaceholder": "https://discord.com/api/webhooks/…", + "auto.webhookRules": "웹훅 규칙", + "auto.webhookUrl": "웹훅 URL", + "auto.weekFri": "금", + "auto.weekMon": "월", + "auto.weekSat": "토", + "auto.weekSun": "일", + "auto.weekThu": "목", + "auto.weekTue": "화", + "auto.weekWed": "수", + "auto.when": "조건", + "chatpage.allFiltered": "메시지가 있는 모든 플랫폼이 필터에서 제외되었습니다.", + "chatpage.couldNotDeleteThatMessage": "해당 메시지를 삭제할 수 없습니다.", + "chatpage.deleteNote": "삭제하면 그 메시지를 발행한 플랫폼에서 제거됩니다. polyemesis가 삭제할 수 없는 플랫폼은 그렇다고 알리고 대신 어디서 할 수 있는지 알려줍니다. 이 동작이 추측으로 숨겨지는 일은 없습니다.", + "chatpage.droppedTitle": "실시간으로는 표시되었지만 저장이 뒤처져 기록에서 사라졌습니다.", + "chatpage.dupTitle": "같은 메시지를 두 번 받았습니다 — 우리 쪽 에코이거나 플랫폼이 다시 보낸 것입니다.", + "chatpage.noAccount": "채팅은 실행 중이지만 연결된 플랫폼 계정이 없습니다. 설정 → 플랫폼 자격 증명에서 하나를 연결하세요.", + "chatpage.notRunning": "이 서버에서는 채팅이 실행되고 있지 않습니다. 타임라인 아래의 기록은 이전에 저장된 것이며, 플랫폼이 연결될 때까지 전송창은 비활성 상태로 남습니다.", + "chatpage.received": "수신", + "chatpage.reload": "새로 고침", + "chatpage.stored": "저장됨", + "chatpage.storedTitle": "연결된 것이 없습니다. 이것은 저장된 기록입니다.", + "chatpage.subtitle": "모든 플랫폼을 하나의 타임라인에, 전송창도 하나로.", + "chatpage.timeline": "타임라인", + "chatpage.title": "채팅", "chrome.ingest": "Ingest", + "chrome.language": "언어", "chrome.liveCount": "{count}개의 라이브 방송", + "chrome.signOut": "로그아웃", "chrome.socketOffline": "오프라인", "chrome.socketOfflineHint": "실시간 업데이트 연결 끊김, 재시도 중", - "chrome.signOut": "로그아웃", - "chrome.language": "언어", + "chrome.toggleNav": "탐색 메뉴 열기/닫기", + "clipedit.back33": "33ms 뒤로", + "clipedit.clickLine": "줄을 클릭하면 그 지점으로 이동합니다. 여러 줄에 걸쳐 선택하면 인점과 아웃점이 선택한 단어에 맞춰 이동합니다.", + "clipedit.clipThisLine": "이 줄을 잘라내기", + "clipedit.clipThisLineTitle": "이 줄을 감싸도록 인점과 아웃점을 설정합니다", + "clipedit.copied": "복사됨", + "clipedit.copiesEveryPacketSecondsBit": "모든 패킷을 복사합니다. 몇 초면 끝나고 비트 단위로 동일하며, 시작 지점은 가장 가까운 키프레임으로 되돌아갑니다.", + "clipedit.couldNotOpenThatRecording": "해당 녹화를 열 수 없습니다.", + "clipedit.couldNotQueueTheExport": "내보내기를 대기열에 넣을 수 없습니다.", + "clipedit.cut": "컷", + "clipedit.downloadClip": "클립 다운로드", + "clipedit.exportQueued": "내보내기를 대기열에 넣었습니다. 장비에 여유가 생기면 실행됩니다.", + "clipedit.exports": "내보내기", + "clipedit.fast": "빠름", + "clipedit.filterTranscript": "자막 걸러내기", + "clipedit.findSaid": "발언 찾기", + "clipedit.forward33": "33ms 앞으로", + "clipedit.in": "인점", + "clipedit.keepAllTracks": "모든 오디오 트랙 유지", + "clipedit.keepsEveryAudioTrackIn": "코덱을 가리지 않고 모든 오디오 트랙을 유지합니다. 레코더가 기록하는 그대로입니다.", + "clipedit.markIn": "I — 재생 헤드에 인점 표시", + "clipedit.markOut": "O — 재생 헤드에 아웃점 표시", + "clipedit.name": "이름", + "clipedit.noKeyframes": "키프레임 위치를 읽을 수 없어 이 컷이 실제로 어디에서 시작될지는 아무도 알 수 없습니다. 동작 자체는 문제없지만(FFmpeg의 탐색은 키프레임에 맞춰집니다), 시작점이 최대 1 GOP 앞당겨질 수 있고 그 차이를 이 페이지에서 보여 줄 수는 없습니다.", + "clipedit.noTranscript": "이 녹화는 아직 자막이 만들어지지 않았습니다. 마이크마다 자체 트랙에 녹음되었으므로, 자막을 만들면 화자 분리 모델 없이도 화자 이름을 얻고 발언 내용으로 잘라낼 수 있습니다.", + "clipedit.nothingExported": "아직 내보낸 것이 없습니다.", + "clipedit.out": "아웃점", + "clipedit.pickAtLeastOneAudio": "오디오 트랙을 하나 이상 고르거나 전부 선택하세요.", + "clipedit.playFaster": "L — 재생, 한 번 더 누르면 빨리", + "clipedit.precise": "정밀", + "clipedit.queueNote": "내보내기는 여기서 실행되지 않고 대기열에 들어갑니다. 라이브 스트림에 양보하므로, 방송 도중에 딴 컷은 스트림이 필요로 하는 자원을 빼앗지 않고 기다립니다.", + "clipedit.reEncodesOnlyTheLeading": "시작 지점을 정확히 맞추기 위해 앞쪽의 불완전한 GOP만 다시 인코딩합니다. 나머지는 그대로 복사됩니다.", + "clipedit.reencoded": "다시 인코딩됨", + "clipedit.shortcuts": "J / K / L로 셔틀, I와 O로 인점과 아웃점, 화살표 키는 33ms, Shift+화살표 키는 1초씩 움직입니다. 미세 단계는 30fps에서 한 프레임입니다. 이 녹화의 실제 프레임 레이트는 색인되어 있지 않으므로 추측하지 않고 밀리초로 표기합니다.", + "clipedit.shuttleBack": "J — 뒤로 셔틀", + "clipedit.startMoved": "시작점 이동", + "clipedit.starts": "시작", + "clipedit.stop": "K — 정지", + "clipedit.thatCutCouldNotBe": "해당 컷을 계획할 수 없습니다.", + "clipedit.thatIsNotARecording": "녹화 ID가 아닙니다.", + "clipedit.thatRecordingIsNotAvailable": "해당 녹화를 사용할 수 없습니다.", + "clipedit.title": "클립 편집기", + "clipedit.whatASocialPlatformWill": "소셜 플랫폼이 받아 주는 형식입니다. 통과하는 코덱은 더 적습니다.", + "clipedit.zoomIn": "확대", + "clipedit.zoomOut": "축소", + "clips.bitrate": "비트레이트", + "clips.buffer": "버퍼", + "clips.bufferLiveNote": "버퍼 스위치는 즉시 적용되는 토글이며 저장되는 설정이 아닙니다. 재시작하면 다시 꺼집니다.", + "clips.captured": "캡처 시각", + "clips.ceiling": "상한", + "clips.clipsKept": "보관 클립 수", + "clips.couldNotCaptureAClip": "클립을 캡처할 수 없습니다.", + "clips.couldNotChangeTheClip": "클립 버퍼를 변경할 수 없습니다.", + "clips.couldNotDeleteTheClip": "클립을 삭제할 수 없습니다.", + "clips.couldNotLoadClips": "클립을 불러올 수 없습니다.", + "clips.covers": "포함 구간", + "clips.customLength": "클립 길이를 초 단위로 지정", + "clips.delete": "삭제", + "clips.deleteDescription": "클립은 대개 남기려던 바로 그 결과물입니다. 이 작업은 파일을 지우며, 클립을 만들어 낸 녹화는 그대로 남습니다.", + "clips.deleteTitle": "이 클립을 삭제할까요?", + "clips.deleted": "클립을 삭제했습니다.", + "clips.download": "다운로드", + "clips.everythingHeld": "버퍼가 담고 있는 전체", + "clips.file": "파일", + "clips.held": "보관 중", + "clips.inMemory": "메모리", + "clips.keepBuffer": "롤링 버퍼 유지", + "clips.keyframes": "키프레임", + "clips.length": "길이", + "clips.none": "없음", + "clips.off": "꺼짐", + "clips.onDisk": "디스크", + "clips.seen": "확인됨", + "clips.settings": "설정", + "clips.size": "크기", + "clips.subtitle": "방금 나간 내용을 담은 롤링 버퍼입니다. 지나간 뒤에 잘라내면 되므로 미리 준비해 둘 것이 없습니다.", + "clips.theClipDoesNotStart": "클립이 키프레임에서 시작하지 않습니다.", + "clips.title": "클립", + "clips.waitingForStream": "스트림 대기 중", + "clips.window": "구간", + "clips.windowSeconds": "구간(초)", "common.add": "추가", + "common.apply": "적용", "common.cancel": "취소", "common.close": "닫기", "common.confirm": "확인", @@ -28,110 +229,921 @@ "common.copy": "복사", "common.delete": "삭제", "common.disabled": "비활성화됨", + "common.discard": "버리기", "common.edit": "편집", "common.enabled": "활성화됨", "common.loading": "로딩 중…", + "common.moreInfo": "자세히 보기", "common.refresh": "새로 고침", "common.restart": "다시 시작", "common.retry": "재시도", "common.save": "저장", "common.start": "시작", "common.stop": "중지", - "state.live": "라이브", - "state.reconnecting": "다시 연결 중", - "state.starting": "시작 중", - "state.failed": "실패", - "state.stopped": "중지됨", - "state.offline": "오프라인", - "hooks.title": "수명 주기 웹훅", - "hooks.new": "새 웹훅", - "hooks.empty": "웹훅이 없습니다. 하나 추가하면 스트림이 시작되거나 멈추는 순간, 또는 대상이 살아나거나 죽는 순간에 polyemesis가 서명된 JSON 본문을 POST합니다. 전환마다 한 번씩, 순서대로 전달되므로 스크립트가 처리할 수 있습니다.", - "hooks.colWebhook": "웹훅", - "hooks.colEndpoint": "엔드포인트", - "hooks.colTriggers": "구독 대상", - "hooks.colEnabled": "켜기", - "hooks.signed": "서명됨", - "hooks.unsigned": "서명 없음", + "dash.audioTracks": "오디오 트랙", + "dash.bitrate": "비트레이트", + "dash.broadcastSettings": "방송 설정", + "dash.categoryPlaceholder": "음악", + "dash.clipboardUnavailable": "이 출처에서는 클립보드를 사용할 수 없습니다.", + "dash.connectPrompt": "설정 → 플랫폼 자격 증명에서 YouTube, Twitch, Facebook, Kick 계정을 연결하면 방송 제목, 설명, 카테고리를 모든 플랫폼에 한 번에 설정할 수 있습니다. 받아들이는 항목은 플랫폼마다 다릅니다. 작성 화면이 어느 것을 받는지 알려 주고, 보낼 수 있는 것을 보냅니다.", + "dash.copyIngestUrl": "입력 URL 복사", + "dash.couldNotLoadTheDestination": "대상을 불러올 수 없습니다.", + "dash.couldNotPushTheMetadata": "메타데이터를 전송할 수 없습니다.", + "dash.couldNotReorderTheDestinations": "대상 순서를 변경할 수 없습니다.", + "dash.deleteDest": "대상 삭제", + "dash.deleteDestDescription": "대상과 해당 라우팅 프로필을 제거합니다. 플랫폼 쪽은 그대로이며 다시 추가할 수 있지만, 그 대상을 위해 설정한 오디오 라우팅은 사라집니다.", + "dash.descriptionPlaceholder": "이 방송이 무엇인지.", + "dash.destDeleted": "대상을 삭제했습니다.", + "dash.disabled": "비활성화됨", + "dash.everyConnectedAccount": "연결된 모든 계정", + "dash.idle": "대기 중", + "dash.ingestUrlCopied": "입력 URL을 복사했습니다.", + "dash.keyRefreshed": "플랫폼에서 스트림 키를 갱신했습니다.", + "dash.leaveUnchanged": "변경하지 않음", + "dash.leaveUnchangedNote": "‘변경하지 않음’은 아무것도 보내지 않으므로 플랫폼이 가진 값을 그대로 둡니다. 켜기나 끄기를 고르면 그 값을 씁니다.", + "dash.metaCategory": "카테고리", + "dash.metaDescription": "설명", + "dash.metaTags": "태그", + "dash.metaTitle": "제목", + "dash.noDestinations": "아직 대상이 없습니다. 하나 추가한 뒤 어떤 오디오 트랙을 받을지 고르세요.", + "dash.off": "끄기", + "dash.pipeline": "파이프라인", + "dash.pushToPlatforms": "플랫폼으로 전송", + "dash.pushing": "전송 중…", + "dash.reconnects": "재연결 횟수", + "dash.relaySubscribers": "릴레이 구독자", + "dash.renditions": "변형", + "dash.scheduledStart": "예정된 시작", + "dash.subtitle": "한 번 받아, 공유 영상과 대상별 오디오로 나눠 보냅니다.", + "dash.tagsPlaceholder": "live, house, dj set", + "dash.theConnectedAccount": "연결된 계정", + "dash.title": "대시보드", + "dash.titlePlaceholder": "금요일 밤 세트", + "dash.uptime": "가동 시간", + "dash.youtubeStopsAcceptingTheseOnce": "방송이 “created” 또는 “ready” 상태를 벗어나면 YouTube는 이를 더 이상 받지 않습니다.", + "dest.audience": "대상", + "dest.audioBitrate": "오디오 비트레이트", + "dest.audioCodec": "오디오 코덱", + "dest.channels": "채널", + "dest.compliance": "규정 준수", + "dest.connectedAccount": "연결된 계정", + "dest.contentLabels": "콘텐츠 라벨", + "dest.copySource": "소스 영상 복사", + "dest.createVariant": "이 변형 만들기", + "dest.customiseSummary": "이 대상에 맞게 조정", + "dest.fbBackupCost": "이 대상의 업로드 대역폭이 두 배가 됩니다.", + "dest.fbBackupHelp": "이 스트림의 두 번째 사본을 Facebook의 백업 수신 지점으로 보내, 연결이 끊겨도 방송이 끊기지 않게 합니다.", + "dest.fbBackupLabel": "백업 피드 게시", + "dest.fbBackupReconnect": "켜면 스트림이 한 번 재연결되므로, 라이브 시작 전에 활성화하세요.", + "dest.fbBoxIntro": "‘시청자’와 같은 생성 호출로 전송되므로 같은 규칙이 적용됩니다. 방송이 시작될 때 한 번 적용되며, 비어 있으면 전혀 건드리지 않습니다.", + "dest.fbBoxTitle": "Facebook 크로스 게시 및 기부 버튼", + "dest.fbCrosspostAdd": "페이지 추가", + "dest.fbCrosspostAlsoPost": "게시도 하기", + "dest.fbCrosspostHelp": "Facebook 자체 콘솔에 있는 숫자 페이지 ID입니다(페이지 설정, 또는 페이지로 로그인한 상태의 graph.facebook.com/me/accounts). 여기에는 검색이 없으니 이름이나 URL이 아니라 id를 붙여 넣으세요. ‘게시도 하기’는 방송을 그 페이지에 공유하기만 하는 대신 그 페이지로 게시합니다. 꺼두면 조용한 공유만 일어납니다.", + "dest.fbCrosspostLabel": "페이지로 크로스 게시", + "dest.fbCrosspostPageId": "페이지 ID", + "dest.fbCrosspostRemove": "이 페이지 제거", + "dest.fbDonateHelp": "이것도 Facebook 모금 콘솔에서 얻는 불투명한 id이며, 검색할 단체 이름이 아닙니다. 기부 버튼을 전혀 붙이지 않으려면 비워 두세요.", + "dest.fbDonateLabel": "기부 버튼 자선단체 ID", + "dest.fbDonatePlaceholder": "자선단체 ID", + "dest.fillFromPlatform": "이 플랫폼이 공개한 값으로 채우기", + "dest.friends": "친구", + "dest.friendsOfFriends": "친구의 친구", + "dest.giveUpAfter": "포기까지 재시도 횟수", + "dest.leaveOnFacebook": "Facebook 설정 그대로 두기", + "dest.leaveOnYouTube": "YouTube 설정 그대로 두기", + "dest.madeForKids": "아동용 (COPPA)", + "dest.muxQueue": "먹싱 대기열 (패킷)", + "dest.muxQueuePair": "이 둘은 한 쌍입니다. FFmpeg는 대기열이 바이트 임계값을 넘은 뒤에야 패킷 상한을 적용하므로, 임계값만으로는 아무 효과가 없고 거부됩니다. 대상이 인터리브 오류를 보고하면 둘 다 올리세요. 여기의 오디오 경로는 라우드니스 정규화가 미리 읽기 때문에 지연이 일정하지 않습니다.", + "dest.name": "이름", + "dest.namePlaceholder": "YouTube — 기본 채널", + "dest.notLinked": "연결 안 됨", + "dest.onlyMe": "나만", + "dest.platform": "플랫폼", + "dest.private": "비공개", + "dest.public": "공개", + "dest.reconnecting": "재연결", + "dest.retryAfter": "재시도 간격 (초)", + "dest.sharedEncodeAria": "공유 영상 인코드", + "dest.socketTimeout": "소켓 시간 초과 (초)", + "dest.streamKeyLabel": "스트림 키", + "dest.transport": "전송 방식", + "dest.transportFile": "로컬 파일", + "dest.transportRtmp": "RTMP / RTMPS", + "dest.unlisted": "일부 공개", + "dest.urlFromDashboard": "대시보드의 URL", + "dest.useSharedEncode": "공유 영상 인코드 사용", + "dest.varBitrate": "영상 비트레이트 (kbps)", + "dest.varFps": "프레임 레이트", + "dest.varHeight": "높이", + "dest.varSourceHint": "소스", + "dest.varWidth": "너비", + "dest.variantWarning": "“{name}”을(를) 기준으로 소스에서 두 번째 인코드를 시작합니다. 무료 변형이 아니며, 해당 인코드를 쓰는 다른 대상이 받는 영상은 바뀌지 않습니다. 소스 값을 유지하려면 칸을 비워 두세요.", + "dest.videoTreatment": "영상 처리", + "dest.visibility": "공개 범위", + "hooks.allTriggers": "전체", "hooks.attempts": "{count}회 시도", + "hooks.attemptsLabel": "시도 횟수", "hooks.attemptsOne": "{count}회 시도", - "hooks.timeout": "{seconds}초 제한", - "hooks.allTriggers": "전체", - "hooks.triggerCount": "트리거 {count}개", - "hooks.triggerCountOne": "트리거 {count}개", - "hooks.enableAria": "{name} 사용", + "hooks.colEnabled": "켜기", + "hooks.colEndpoint": "엔드포인트", + "hooks.colTriggers": "구독 대상", + "hooks.colWebhook": "웹훅", + "hooks.create": "만들기", + "hooks.created": "웹훅을 만들었습니다.", + "hooks.deleteBody": "즉시 전달을 멈춥니다. 엔드포인트는 그대로이고 훅은 다시 만들 수 있지만, 그 서명 키는 복구할 수 없습니다.", + "hooks.deleteConfirm": "웹훅 삭제", + "hooks.deleteFailed": "훅을 삭제하지 못했습니다.", + "hooks.deleteTitle": "이 웹훅을 삭제할까요?", + "hooks.deleted": "웹훅을 삭제했습니다.", "hooks.deliveries": "최근 전달", - "hooks.sendTest": "테스트 전달 보내기", - "hooks.sendTestTitle": "지금 테스트 전달 보내기", - "hooks.secretTitle": "서명 키 — 지금 복사하세요. 다시 표시할 수 없습니다", - "hooks.secretCopied": "복사했습니다", - "hooks.httpStatus": "HTTP {status}", - "hooks.noResponse": "응답 없음", + "hooks.deliveriesFailed": "최근 전달을 읽지 못했습니다.", + "hooks.deliveryTitle": "전달", + "hooks.dialogIntro": "전환마다 서명된 POST 한 번, 순서대로 보냅니다. 알림과 달리 병합하거나 디바운스하지 않으므로 스크립트는 모든 에지를 받습니다.", "hooks.durationMs": "{ms} ms", + "hooks.editTitle": "웹훅 편집", + "hooks.empty": "웹훅이 없습니다. 하나 추가하면 스트림이 시작되거나 멈추는 순간, 또는 대상이 살아나거나 죽는 순간에 polyemesis가 서명된 JSON 본문을 POST합니다. 전환마다 한 번씩, 순서대로 전달되므로 스크립트가 처리할 수 있습니다.", + "hooks.enableAria": "{name} 사용", + "hooks.httpStatus": "HTTP {status}", + "hooks.loadFailed": "웹훅을 불러오지 못했습니다.", + "hooks.name": "이름", + "hooks.new": "새 웹훅", + "hooks.newTitle": "새 웹훅", "hooks.noDeliveries": "아직 전달된 것이 없습니다. 전환이 일어나면 여기에 표시됩니다.", - "hooks.deliveryTitle": "전달", - "hooks.statSent": "보냄", - "hooks.statFailed": "실패", + "hooks.noResponse": "응답 없음", + "hooks.payloadNote": "페이로드 {version}. 서명은 {header}에 있으며 타임스탬프와 본문을 대상으로 합니다.", + "hooks.retriesHint": "순서가 약속이므로 재시도는 이 엔드포인트 자체의 큐를 막습니다. 4xx는 절대 재시도하지 않습니다. 요청이 잘못되었다고 말한 엔드포인트는 다시 그렇게 말합니다.", + "hooks.saveFailed": "훅을 저장하지 못했습니다.", + "hooks.secretCopied": "복사했습니다", + "hooks.secretTitle": "서명 키 — 지금 복사하세요. 다시 표시할 수 없습니다", + "hooks.sendTest": "테스트 전달 보내기", + "hooks.sendTestTitle": "지금 테스트 전달 보내기", + "hooks.signed": "서명됨", + "hooks.statDropped": "버림", "hooks.statEndpoints": "엔드포인트", + "hooks.statFailed": "실패", "hooks.statQueued": "대기", - "hooks.statDropped": "버림", "hooks.statRetries": "재시도", - "hooks.payloadNote": "페이로드 {version}. 서명은 {header}에 있으며 타임스탬프와 본문을 대상으로 합니다.", - "hooks.deleteTitle": "이 웹훅을 삭제할까요?", - "hooks.deleteBody": "즉시 전달을 멈춥니다. 엔드포인트는 그대로이고 훅은 다시 만들 수 있지만, 그 서명 키는 복구할 수 없습니다.", - "hooks.deleteConfirm": "웹훅 삭제", - "hooks.editTitle": "웹훅 편집", - "hooks.newTitle": "새 웹훅", - "hooks.dialogIntro": "전환마다 서명된 POST 한 번, 순서대로 보냅니다. 알림과 달리 병합하거나 디바운스하지 않으므로 스크립트는 모든 에지를 받습니다.", - "hooks.name": "이름", - "hooks.url": "엔드포인트 URL", - "hooks.urlMasked": "저장된 URL은 전체가 표시되지 않습니다. 마스킹된 값을 그대로 두면 유지되고, 새 URL을 입력하면 교체됩니다.", - "hooks.triggers": "트리거", - "hooks.triggersNoneHint": "아무것도 선택하지 않으면 모든 트리거가 대상이며, 향후 릴리스에서 추가되는 것도 포함됩니다.", + "hooks.statSent": "보냄", + "hooks.testFailed": "엔드포인트가 테스트 전달을 받지 않았습니다.", + "hooks.timeout": "{seconds}초 제한", "hooks.timeoutLabel": "제한 시간(초)", - "hooks.attemptsLabel": "시도 횟수", - "hooks.retriesHint": "순서가 약속이므로 재시도는 이 엔드포인트 자체의 큐를 막습니다. 4xx는 절대 재시도하지 않습니다. 요청이 잘못되었다고 말한 엔드포인트는 다시 그렇게 말합니다.", - "hooks.create": "만들기", - "hooks.triggerIngestPublished": "스트림 시작됨", - "hooks.triggerIngestDisconnected": "스트림 중지됨", - "hooks.triggerDestinationUp": "대상 라이브", - "hooks.triggerDestinationDown": "대상 중지됨", - "hooks.loadFailed": "웹훅을 불러오지 못했습니다.", + "hooks.title": "수명 주기 웹훅", "hooks.toggleFailed": "훅을 변경하지 못했습니다.", - "hooks.deleted": "웹훅을 삭제했습니다.", - "hooks.deleteFailed": "훅을 삭제하지 못했습니다.", - "hooks.testFailed": "엔드포인트가 테스트 전달을 받지 않았습니다.", - "hooks.deliveriesFailed": "최근 전달을 읽지 못했습니다.", + "hooks.triggerCount": "트리거 {count}개", + "hooks.triggerCountOne": "트리거 {count}개", + "hooks.triggerDestinationDown": "대상 중지됨", + "hooks.triggerDestinationUp": "대상 라이브", + "hooks.triggerIngestDisconnected": "스트림 중지됨", + "hooks.triggerIngestPublished": "스트림 시작됨", + "hooks.triggers": "트리거", + "hooks.triggersNoneHint": "아무것도 선택하지 않으면 모든 트리거가 대상이며, 향후 릴리스에서 추가되는 것도 포함됩니다.", + "hooks.unsigned": "서명 없음", "hooks.updated": "웹훅을 업데이트했습니다.", - "hooks.created": "웹훅을 만들었습니다.", - "hooks.saveFailed": "훅을 저장하지 못했습니다.", - "playlist.title": "재생목록", + "hooks.url": "엔드포인트 URL", + "hooks.urlMasked": "저장된 URL은 전체가 표시되지 않습니다. 마스킹된 값을 그대로 두면 유지되고, 새 URL을 입력하면 교체됩니다.", + "jobs.alwaysKeepNewest": "최신 항목은 항상 보관", + "jobs.automaticModel": "자동(이 하드웨어에 맞춰 선택)", + "jobs.backToDefault": "이 종류의 기본 모드로 되돌립니다", + "jobs.batteryFloor": "배터리 하한 %", + "jobs.broadcastGoing": "방송이 나가는 중입니다. 미뤄 둔 작업은 방송에 양보합니다.", + "jobs.cancel": "취소", + "jobs.cannotPause": "이 종류들은 일시 중지해야 하지만 그럴 수 없어 대신 끝까지 실행됩니다.", + "jobs.completed": "완료", + "jobs.concurrency": "동시 실행 수", + "jobs.couldNotReadTheJob": "작업 대기열을 읽을 수 없습니다.", + "jobs.couldNotSaveThePolicy": "정책을 저장할 수 없습니다.", + "jobs.cpuCeiling": "CPU 상한 %", + "jobs.cpuResume": "CPU 재개 %", + "jobs.defaultMode": "기본 모드", + "jobs.defaultModel": "기본 모델", + "jobs.deferred": "미룸", + "jobs.failed": "실패", + "jobs.finished": "끝남", + "jobs.global": "전역", + "jobs.gpuGateNote": "GPU를 쓴다고 표시된 종류만 이 값의 제한을 받습니다.", + "jobs.history": "기록", + "jobs.historyRetention": "기록 보관", + "jobs.holdDeferredWorkBackOn": "방전 중인 노트북에서 이 값 아래로 내려가면 미룰 수 있는 작업을 보류합니다. 0이면 사용하지 않습니다.", + "jobs.hostCpuLoad": "호스트 CPU 부하", + "jobs.howManyJobsMayRun": "동시에 실행할 수 있는 작업 수입니다. 1인 이유는, 두 번째 트랜스코드가 아무도 요구하지 않은 처리량을 아무도 감수하기로 하지 않은 위험과 맞바꿔 얻을 뿐이기 때문입니다.", + "jobs.inQueue": "대기열", + "jobs.job": "작업", + "jobs.keepForDays": "보관 일수", + "jobs.machineSafety": "장비 보호. 실시간 작업도 따르는 유일한 관문입니다.", + "jobs.niceLevel": "nice 값", + "jobs.noFinished": "아직 끝난 작업이 없습니다.", + "jobs.noModels": "models 디렉터리에서 모델 파일을 찾지 못했습니다. 자동을 고르면 선택한 모델을 내려받습니다. 여기에서 특정 모델을 지정하는 것은 그 파일이 있어야만 동작합니다.", + "jobs.nothingNewStartsAboveThis": "이 값을 넘으면 새 작업이 시작되지 않습니다. 0이면 게이트를 사용하지 않습니다.", + "jobs.nothingQueued": "대기열이 비어 있습니다. 후반 작업은 라이브러리에서 제출합니다.", + "jobs.nothingWasOldEnoughTo": "삭제할 만큼 오래된 항목이 없습니다. 먼저 정책에서 보관 기간을 줄이세요.", + "jobs.pauseAll": "전체 일시 중지", + "jobs.policy": "정책", + "jobs.policySaved": "자원 정책을 저장했습니다.", + "jobs.progress": "진행률", + "jobs.queuePaused": "대기열을 일시 중지했습니다.", + "jobs.queueResumed": "대기열을 재개했습니다.", + "jobs.queued": "대기", + "jobs.recording": "녹화 중", + "jobs.releasedItWillStartAs": "해제했습니다. 슬롯이 비는 대로 시작합니다.", + "jobs.removeFromHistory": "기록에서 제거", + "jobs.removeWindow": "구간 제거", + "jobs.resumeAll": "전체 재개", + "jobs.retry": "재시도", + "jobs.retryTitle": "시도 횟수를 새로 부여해 다시 준비합니다", + "jobs.runNow": "지금 실행", + "jobs.runNowTitle": "자원 정책을 무시하고 이 작업을 지금 실행합니다", + "jobs.running": "실행 중", + "jobs.savedTheConcurrencyChangeTakes": "저장했습니다. 동시 실행 수 변경은 서버를 다시 시작할 때 적용됩니다. 대기열이 시작 시점에 그 값을 고정합니다.", + "jobs.state": "상태", + "jobs.stopsEverythingRealtimeIncludedA": "실시간 처리를 포함해 전부 중지합니다. 스로틀링 중인 CPU는 이미 스트림을 저하시키기 시작한 상태입니다. 0이면 사용하지 않습니다.", + "jobs.subtitle": "백그라운드 작업: 자막 생성, 프록시, 썸네일, 아카이브 압축.", + "jobs.thatDidNotWork": "실행하지 못했습니다.", + "jobs.thermalCeiling": "온도 상한 °C", + "jobs.thisManyFinishedJobsSurvive": "이 개수만큼의 완료된 작업은 오래되었더라도 남습니다.", + "jobs.title": "작업", + "jobs.transcription": "자막 생성", + "jobs.unsavedChanges": "저장하지 않은 변경이 있습니다.", + "jobs.whereTheCeilingReleasesThe": "상한이 해제되는 지점입니다. 그 차이가 게이트의 진동을 막는 히스테리시스입니다.", + "jobs.whisperMissing": "여기에는 whisper.cpp가 설치되어 있지 않아 이 설정은 아직 효과가 없습니다. 그래도 설정해 둘 가치가 있습니다. 설치되는 즉시 선택이 적용됩니다.", + "jobs.why": "이유", + "jobs.windowEnd": "구간 종료", + "jobs.windowStart": "구간 시작", + "jobs.yieldNote": "무거운 작업은 대기열에 들어가 라이브 스트림에 양보합니다. 떨어진 프레임은 되돌릴 수 없지만, 자막이 한 시간 늦게 도착해도 손해는 없습니다.", + "lib.anySpeaker": "모든 화자", + "lib.anyTag": "모든 태그", + "lib.autoGrouped": "자동으로 묶였습니다. 편집하면 사용자의 것이 됩니다.", + "lib.autoGroupedNote": "이 세션은 자동으로 묶였습니다. 저장하면 사용자의 것이 되고, 묶기 처리가 더 이상 덮어쓰지 않습니다.", + "lib.bestMatch": "관련도순", + "lib.clearSearch": "검색어 지우기", + "lib.clip": "클립", + "lib.clipTitle": "이 녹화에서 클립을 잘라냅니다", + "lib.collapseSession": "세션 접기", + "lib.couldNotLoadTheLibrary": "라이브러리를 불러올 수 없습니다.", + "lib.couldNotLoadTheSession": "세션의 세그먼트를 불러올 수 없습니다.", + "lib.couldNotQueueTheProxy": "프록시를 대기열에 넣을 수 없습니다.", + "lib.couldNotRegroup": "다시 묶을 수 없습니다.", + "lib.couldNotSaveTheSession": "세션을 저장할 수 없습니다.", + "lib.delete": "삭제", + "lib.deleteDescription": "파일이 프록시, 썸네일, 자막과 함께 디스크에서 제거됩니다. 이 작업은 되돌릴 수 없습니다.", + "lib.deleteTitle": "이 녹화를 삭제할까요?", + "lib.downloadMaster": "마스터 다운로드", + "lib.downloadMasterTitle": "멀티트랙 마스터를 다운로드합니다", + "lib.editSession": "세션 편집", + "lib.expandSession": "세션 펼치기", + "lib.from": "출처", + "lib.generateProxy": "프록시 생성", + "lib.generateProxyTitle": "브라우저에서 재생할 수 있는 프록시를 생성합니다", + "lib.mostRecent": "최신순", + "lib.noProxyYet": "아직 프록시가 없습니다 — 열면 생성합니다", + "lib.noSegments": "여기에는 세그먼트가 없습니다.", + "lib.noSessionMatch": "그 필터에 맞는 세션이 없습니다.", + "lib.nothingRecorded": "아직 녹화된 것이 없습니다. 레코더가 세그먼트를 기록하면 세션이 나타납니다.", + "lib.oldestFirst": "오래된순", + "lib.playProxy": "프록시를 재생합니다", + "lib.proxyAlreadyQueued": "프록시가 이미 대기열에 있습니다.", + "lib.proxyQueued": "프록시를 대기열에 넣었습니다.", + "lib.recordingDeleted": "녹화를 삭제했습니다.", + "lib.regroupTitle": "묶기를 다시 실행합니다. 더하기만 하고 멱등이며, 손으로 나눈 묶음을 합치지 않습니다.", + "lib.searchPlaceholder": "모든 녹화에서 누구의 발언이든 검색…", + "lib.searchTranscripts": "자막 검색", + "lib.searchable": "검색 가능", + "lib.searchableOnly": "자막이 있어 검색할 수 있는 세그먼트", + "lib.session": "세션", + "lib.sessionDescription": "설명", + "lib.sessionSaved": "세션을 저장했습니다.", + "lib.sessionTags": "태그", + "lib.sessionTitle": "제목", + "lib.subtitle": "모든 방송을 검색할 수 있습니다. 마이크마다 자체 트랙에 녹음되었으므로 자막은 누가 말했는지 이미 알고 있습니다.", + "lib.tag": "태그", + "lib.tagsPlaceholder": "쉼표, 구분", + "lib.title": "라이브러리", + "lib.transcribe": "자막 만들기", + "lib.transcribeEachTrackOnIts": "각 트랙을 개별로 전사합니다 — 화자 구분이 가능해지는 이유가 바로 이것입니다", + "lib.ungrouped": "묶이지 않은 세그먼트", + "meters.deviation": "편차", + "meters.idle": "대기 중", + "meters.integrated": "통합", + "meters.loudness": "라우드니스 준수", + "meters.metering": "측정 중", + "meters.momentary": "순간", + "meters.monitorOff": "모니터가 꺼져 있습니다. 켜면 각 대상이 내보내는 내용을 측정할 수 있습니다.", + "meters.noSignal": "이 트랙에 신호 없음", + "meters.nothingToMeasure": "아직 측정할 것이 없습니다. 실행 중인 대상마다 자신이 받는 믹스에 대한 전용 EBU R128 분석기가 붙습니다.", + "meters.range": "범위", + "meters.shortTerm": "단기", + "meters.subtitle": "모든 입력 트랙의 모든 채널을 릴레이에서 곧바로.", + "meters.title": "오디오 미터", + "meters.truePeak": "트루 피크", + "meters.waitingForStream": "스트림 대기 중", + "mon.allProcesses": "모든 프로세스", + "mon.bitrateChart": "입력 비트레이트 — 최근 30분", + "mon.chooseDestination": "대상을 선택하세요…", + "mon.clearLog": "로그 비우기", + "mon.clearedThisDestinationIsBack": "해제했습니다. 이 대상은 생성된 명령으로 되돌아갑니다.", + "mon.dryRun": "모의 실행", + "mon.dryRunInvalid": "FFmpeg가 인자를 거부함", + "mon.dryRunOk": "FFmpeg가 명령을 받아들임", + "mon.extraInput": "추가 입력 인자", + "mon.extraOutput": "추가 출력 인자", + "mon.ffmpegLog": "FFmpeg 로그", + "mon.fromRunningProcess": "실행 중인 프로세스에서", + "mon.fullCommand": "완전히 펼친 명령", + "mon.hostCpu": "호스트 CPU", + "mon.ingestLoss": "입력 손실", + "mon.memory": "메모리", + "mon.nothingProvenEitherWay": "어느 쪽도 확인되지 않음", + "mon.pid": "PID", + "mon.processes": "프로세스", + "mon.rebuilt": "다시 구성됨", + "mon.relayDrops": "릴레이 폐기", + "mon.relayIn": "릴레이 수신", + "mon.resolveHint": "적용하기 전에 무엇이 실행될지 정확히 보려면 명령을 펼쳐 보세요.", + "mon.restarts": "재시작 횟수", + "mon.subtitle": "프로세스 상태, 호스트 자원, 그리고 실시간 FFmpeg 로그.", + "mon.title": "모니터링", + "mon.uptime": "가동 시간", + "nav.automation": "자동화", + "nav.chat": "채팅", + "nav.clips": "클립", + "nav.dashboard": "대시보드", + "nav.jobs": "작업", + "nav.library": "라이브러리", + "nav.meters": "오디오 레벨 표시기", + "nav.monitoring": "모니터링", + "nav.playout": "Playout", + "nav.recordings": "녹화", + "nav.renditions": "변형", + "nav.routing": "Routing", + "nav.settings": "설정", + "nav.sources": "소스", + "play.acrossEveryRungTheBackstop": "모든 단을 합친 값입니다. 장시간 방송이 디스크를 채우는 것을 막는 안전장치입니다.", + "play.allowCrossOrigin": "교차 출처 허용", + "play.allowCrossOriginPlayback": "교차 출처 재생 허용", + "play.audioBitrate": "오디오 비트레이트(kbps)", + "play.audioTrack": "오디오 트랙", + "play.basicPasswordNote": "링크를 따라갈 수 없는 재생기(셋톱박스, CDN)는 대신 같은 비밀 값을 HTTP 기본 인증 비밀번호로 보낼 수 있습니다.", + "play.boundsTheViewerTableViewers": "시청자 테이블의 상한입니다. 이를 넘는 시청자에게도 스트림은 제공되며, 집계에만 포함되지 않습니다.", + "play.carriesThePlaybackTokenAnyone": "재생 토큰을 포함합니다. 이 URL을 가진 사람은 누구나 볼 수 있습니다.", + "play.clipboardUnavailable": "클립보드를 사용할 수 없습니다. 직접 선택해서 복사하세요", + "play.couldNotLoadPlayout": "Playout을 불러올 수 없습니다", + "play.disabled": "비활성화됨", + "play.diskCap": "디스크 상한(MB)", + "play.dvrWindow": "DVR 구간(초)", + "play.embedCode": "임베드 코드", + "play.enable": "Playout 활성화", + "play.enabled": "활성화됨", + "play.forObsACdnOr": "OBS, CDN, 또는 HLS를 지원하는 모든 플레이어용입니다.", + "play.format": "형식", + "play.hls": "HLS", + "play.hlsDash": "HLS 및 DASH", + "play.hlsPlaylist": "HLS 재생목록", + "play.idleTimeout": "시청자 유휴 제한 시간(초)", + "play.linkReset": "새 재생 링크를 만들었습니다. 이전 링크는 더 이상 동작하지 않습니다.", + "play.linksProtected": "공개를 켤 때까지 이 링크들은 로그인한 관리자에게만 동작합니다.", + "play.linksPublic": "이 링크들은 계정 없이도 동작합니다.", + "play.liveWindow": "라이브 구간(세그먼트)", + "play.metaDescription": "설명", + "play.metaTitle": "제목", + "play.mustExceedOneSegmentOr": "세그먼트 하나보다 길어야 합니다. 그렇지 않으면 폴링 사이에 시청자가 사라집니다.", + "play.name": "이름", + "play.noCredentialNote": "어떤 자격 증명도 필요하지 않습니다. 인터넷 전체에 보여 줄 작정인 방송에만 적합합니다.", + "play.noRungs": "아직 단이 없습니다. 무엇이든 게시하려면 하나 추가하세요.", + "play.onDisk": "디스크", + "play.oneStereoAacTrackPer": "단마다 스테레오 AAC 트랙 하나입니다.", + "play.packaging": "패키징", + "play.passthrough": "입력(패스스루)", + "play.peak": "최고치", + "play.playerPage": "재생기 페이지", + "play.posterNote": "공개 페이지에서 영상 위에 표시됩니다. 포스터는 최근 키프레임이며 자동으로 갱신됩니다.", + "play.private": "비공개.", + "play.protectedNote": "재생은 기본적으로 보호됩니다. 이를 여는 것은 별개의 의도적인 단계입니다.", + "play.protection": "보호", + "play.public": "공개", + "play.resetFailed": "초기화할 수 없습니다", + "play.restartNote": "이 가운데 무엇이든 바꾸면 멀티플렉서가 재시작되어 시청자는 몇 초 동안 끊깁니다.", + "play.rungNote": "각 단은 해당 변형의 영상을 비트 단위로 그대로 복사하므로, 하나 추가해도 드는 비용은 멀티플렉서 하나뿐이고 두 번째 영상 인코딩은 발생하지 않습니다. 단마다 다른 것은 오디오 트랙뿐이며, 대상별 라우팅에는 영향이 없습니다.", + "play.segmentLength": "세그먼트 길이(초)", + "play.serveWithoutSession": "세션 없이 시청자에게 제공", + "play.sessionCap": "세션 상한", + "play.sessions": "세션", + "play.share": "공유", + "play.shorterIsLowerLatencyAnd": "짧을수록 지연이 줄고 재생목록 갱신은 잦아집니다.", + "play.subtitle": "스트림을 공개 HLS로 시청자에게 곧바로 제공합니다.", + "play.threeIsTheMinimumA": "3은 플레이어가 버퍼링할 수 있는 최솟값입니다.", + "play.title": "Playout", + "play.unlisted": "일부 공개.", + "play.untitled": "제목 없는 방송", + "play.videoSource": "영상 소스", + "play.watchingNow": "지금 보는 사람", + "play.whoCanWatch": "볼 수 있는 사람", + "player.copied": "복사됨", + "player.copy": "복사", + "player.loading": "불러오는 중…", + "player.missingToken": "따라간 링크에 재생 토큰이 없거나 토큰이 변경되었습니다. 링크를 공유한 사람에게 최신 링크를 요청하세요.", + "player.notLive": "지금은 방송 중이 아닙니다", + "player.viewer": "시청자", + "player.viewers": "시청자", + "player.willStart": "방송이 시작되면 이 페이지가 알아서 재생을 시작합니다.", + "playlist.choose": "업로드 선택", + "playlist.chooseAria": "추가할 업로드 선택", "playlist.empty": "아직 대기 중인 항목이 없습니다.", + "playlist.hint": "Media에서 업로드합니다. 이 재생목록이 가리키는 항목이 나중에 업로드를 잃으면, 다음 장애 조치 때 조용히 실패하는 대신 위에 확인 필요로 표시됩니다.", "playlist.itemsAria": "재생목록 항목", - "playlist.stateReady": "준비됨", - "playlist.stateTranscoding": "변환 중", - "playlist.stateAttention": "확인 필요", - "playlist.stateUnsaved": "아직 저장되지 않음", "playlist.missingUpload": "이 업로드를 더 이상 찾을 수 없습니다.", - "playlist.moveUp": "{name} 위로 이동", "playlist.moveDown": "{name} 아래로 이동", - "playlist.remove": "{name} 제거", - "playlist.choose": "업로드 선택", - "playlist.chooseAria": "추가할 업로드 선택", + "playlist.moveUp": "{name} 위로 이동", "playlist.noUploads": "사용 가능한 업로드가 없습니다", - "playlist.hint": "Media에서 업로드합니다. 이 재생목록이 가리키는 항목이 나중에 업로드를 잃으면, 다음 장애 조치 때 조용히 실패하는 대신 위에 확인 필요로 표시됩니다.", - "dest.fbBoxTitle": "Facebook 크로스 게시 및 기부 버튼", - "dest.fbBoxIntro": "‘시청자’와 같은 생성 호출로 전송되므로 같은 규칙이 적용됩니다. 방송이 시작될 때 한 번 적용되며, 비어 있으면 전혀 건드리지 않습니다.", - "dest.fbBackupLabel": "백업 피드 게시", - "dest.fbBackupHelp": "이 스트림의 두 번째 사본을 Facebook의 백업 수신 지점으로 보내, 연결이 끊겨도 방송이 끊기지 않게 합니다.", - "dest.fbBackupCost": "이 대상의 업로드 대역폭이 두 배가 됩니다.", - "dest.fbBackupReconnect": "켜면 스트림이 한 번 재연결되므로, 라이브 시작 전에 활성화하세요.", - "dest.fbCrosspostLabel": "페이지로 크로스 게시", - "dest.fbCrosspostPageId": "페이지 ID", - "dest.fbCrosspostAlsoPost": "게시도 하기", - "dest.fbCrosspostRemove": "이 페이지 제거", - "dest.fbCrosspostAdd": "페이지 추가", - "dest.fbCrosspostHelp": "Facebook 자체 콘솔에 있는 숫자 페이지 ID입니다(페이지 설정, 또는 페이지로 로그인한 상태의 graph.facebook.com/me/accounts). 여기에는 검색이 없으니 이름이나 URL이 아니라 id를 붙여 넣으세요. ‘게시도 하기’는 방송을 그 페이지에 공유하기만 하는 대신 그 페이지로 게시합니다. 꺼두면 조용한 공유만 일어납니다.", - "dest.fbDonateLabel": "기부 버튼 자선단체 ID", - "dest.fbDonatePlaceholder": "자선단체 ID", - "dest.fbDonateHelp": "이것도 Facebook 모금 콘솔에서 얻는 불투명한 id이며, 검색할 단체 이름이 아닙니다. 기부 버튼을 전혀 붙이지 않으려면 비워 두세요." + "playlist.remove": "{name} 제거", + "playlist.stateAttention": "확인 필요", + "playlist.stateReady": "준비됨", + "playlist.stateTranscoding": "변환 중", + "playlist.stateUnsaved": "아직 저장되지 않음", + "playlist.title": "재생목록", + "rec.delete": "삭제", + "rec.deleteDescription": "파일이 프록시, 썸네일, 자막과 함께 디스크에서 제거됩니다. 이 작업은 되돌릴 수 없습니다.", + "rec.deleteFailed": "녹화를 삭제할 수 없습니다.", + "rec.deleteTitle": "이 녹화를 삭제할까요?", + "rec.deleted": "녹화를 삭제했습니다.", + "rec.disabled": "비활성", + "rec.disk": "디스크", + "rec.diskRecordings": "녹화", + "rec.download": "다운로드", + "rec.downloadNamed": "{name} 다운로드", + "rec.duration": "길이", + "rec.empty": "아직 녹화가 없습니다. 오른쪽에서 녹화를 켜고 스트림을 시작하세요.", + "rec.enabled": "활성화", + "rec.file": "파일", + "rec.free": "여유", + "rec.haltedDefault": "녹화가 중지되었습니다: 볼륨의 여유 공간이 하한 아래입니다.", + "rec.haltedRestarts": "공간이 확보되면 레코더가 스스로 다시 시작합니다.", + "rec.help.enabled": "스트리밍하는 동안 각 입력을 디스크에 기록하며, 모든 오디오 트랙을 분리된 상태로 인코딩 없이 유지합니다. 녹화는 플랫폼으로 보내는 것과 무관합니다. 끄더라도 라이브 출력에는 영향이 없고, 켜면 업로드 대역폭이 아니라 디스크를 소모합니다.", + "rec.help.maxAgeHours": "전체 크기와 상관없이 이 시간보다 오래된 녹화를 삭제합니다. 최근 하루이틀치만 필요할 때 유용합니다. 0으로 두면 다른 제한이 지울 때까지 모두 보관합니다.", + "rec.help.maxGb": "polyemesis가 보관하는 녹화 전체 크기의 상한입니다. 초과하면 총량이 다시 맞을 때까지 오래된 것부터 삭제됩니다. 0으로 두면 크기 제한이 없으며, 그 경우 나이 또는 여유 공간 하한만이 증가를 제한합니다.", + "rec.help.minFreeGb": "확실한 정지선입니다. 볼륨의 여유 공간이 이 값 아래로 떨어지면 녹화가 멈추고, 공간이 확보되면 스스로 재개합니다. polyemesis가 기록하지 않은 파일까지 계산에 넣는 유일한 제한이므로, 공유 디스크가 가득 차는 것을 실제로 막아 주는 설정입니다.", + "rec.help.segmentSeconds": "레코더가 새 파일을 시작하기 전까지 한 파일의 길이입니다. 세그먼트가 짧을수록 충돌 시 손실이 줄고 개별 파일을 옮기기 쉽지만, 관리할 파일은 많아집니다. 자르는 지점은 키프레임에 맞춰지므로 실제 길이는 1초 정도 달라집니다.", + "rec.help.stemFiles": "마스터와 함께 각 오디오 트랙을 별도 파일로도 기록합니다. 나중에 편집하면서 진행자, 게스트, 게임 사운드를 개별 채널로 다루고 싶을 때 유용합니다. 한 세션이 기록하는 용량이 대략 트랙 수만큼 늘어나므로 기본값은 꺼짐입니다.", + "rec.help.stemFormat": "두 형식 모두 무손실이므로 음질 손해는 없습니다. FLAC은 크기가 약 절반이라 작업 흐름의 어떤 도구가 읽지 못하는 경우가 아니라면 적합합니다. WAV는 어디서든 열립니다. 8채널을 넘는 트랙은 자동으로 WAV가 됩니다.", + "rec.hideStems": "스템 숨기기", + "rec.keepForever": "0 = 영구 보관.", + "rec.loadFailed": "녹화를 불러올 수 없습니다.", + "rec.maxAgeHours": "삭제까지 시간(시간)", + "rec.maxGb": "최대 보관 용량(GB)", + "rec.minFreeGb": "여유 공간이 이 값 미만이면 중지(GB)", + "rec.minFreeNote": "위 상한은 polyemesis가 기록한 것만 제한합니다. 볼륨의 다른 파일은 여전히 공간을 채울 수 있습니다. 0 = 하한 없음.", + "rec.noSizeLimit": "0 = 크기 제한 없음.", + "rec.recording": "녹화 중", + "rec.retentionNote": "보관 정리는 30초마다 실행되며, 오래된 것부터 먼저 나이 순으로, 그다음 전체 크기 순으로 처리합니다. 현재 기록 중인 세그먼트는 절대 삭제되지 않습니다.", + "rec.saveFailed": "설정을 저장할 수 없습니다.", + "rec.savePolicy": "보관 정책 저장", + "rec.saved": "녹화 설정을 저장했습니다.", + "rec.segmentSeconds": "세그먼트 길이(초)", + "rec.segments": "세그먼트", + "rec.settingsTitle": "녹화", + "rec.showStems": "스템 보기", + "rec.size": "크기", + "rec.started": "시작", + "rec.stemFiles": "트랙별 스템 파일", + "rec.stemFlac": "FLAC — 무손실, 크기는 약 절반", + "rec.stemFormat": "스템 형식", + "rec.stemNote": "마스터와 같은 프로세스에서 기록되므로 각 스템은 서로 샘플 단위로 정렬된 상태를 유지합니다. 8채널을 넘는 트랙은 자동으로 WAV로 전환됩니다.", + "rec.stemWav": "WAV — 무손실, 어디서나 열림", + "rec.stems": "스템", + "rec.stemsTitle": "트랙별 스템 파일 {count}개, 마스터와 샘플 단위로 정렬됨", + "rec.stemsUsage": "스템 (파일 {count}개)", + "rec.subtitle": "완전한 멀티트랙 아카이브: 모든 입력 오디오 트랙을 인코딩 없이 보존합니다.", + "rec.title": "녹화", + "rec.tracks": "트랙", + "rec.tracksTitle": "오디오 트랙 {count}개 보존됨", + "rec.used": "사용됨", + "rec.volume": "볼륨", + "rend.aspectBlurpad": "블러 채움", + "rend.aspectBlurpadHint": "남는 영역을 프레임 자체를 잘라 채운 흐릿한 사본으로 메웁니다. 세로형 방송에서 자리 잡은 관행입니다.", + "rend.aspectCrop": "잘라서 채우기", + "rend.aspectCropHint": "목표 비율에 맞춰 가운데를 잘라낸 뒤 크기를 조정합니다. 피사체의 화면상 크기는 유지되지만 프레임 가장자리는 사라집니다.", + "rend.aspectHandling": "화면비 처리", + "rend.aspectPad": "레터박스", + "rend.aspectPadHint": "프레임 전체를 맞춰 축소한 뒤 남는 영역을 단색으로 채웁니다. 잃는 것은 없지만, 9:16 캔버스에 올린 16:9 소스는 대부분이 여백 띠가 됩니다.", + "rend.aspectStretch": "늘려서 맞추기", + "rend.aspectStretchHint": "목표 크기로 조정하며, 소스 비율이 맞지 않으면 화면이 왜곡됩니다. 다른 모드가 생기기 전에는 모든 변형이 이렇게 동작했습니다.", + "rend.boxColour": "상자 색상", + "rend.boxOpacity": "상자 불투명도(%)", + "rend.builtInFont": "내장 기본값", + "rend.burnedText": "새겨 넣은 텍스트", + "rend.chooseHardware": "하드웨어 인코더를 고르거나, 출력을 더 작게 하세요.", + "rend.colourBlack": "black", + "rend.colourWhite": "white", + "rend.costBeyond": "이것을 소프트웨어로 실시간 인코딩하는 것은 대부분의 장비에 무리입니다", + "rend.costBeyondDetail": "{label}은 인코딩 한 번에 {mpps}입니다. 이 크기의 x264는 대개 실시간을 따라가지 못하며, 뒤처진 변형은 그것을 읽는 모든 대상에서 프레임을 떨어뜨립니다. 그 사실은 방송 도중에야 알게 됩니다.{machine} {escape}", + "rend.costBusy": "코어 여러 개가 바빠질 것으로 보세요", + "rend.costBusyDetail": "{label}은 {encoder}에서 {mpps}이며, 실시간으로는 무거운 인코딩입니다.{machine} 라이브가 시작되면 인코더의 속도를 지켜보세요. 1.0× 아래면 뒤처지고 있다는 뜻이고, 그 아래의 대상들이 프레임을 떨어뜨리기 시작합니다. {escape}", + "rend.costMachineCores": " 이 장비는 논리 코어 {cores}개를 보고합니다.", + "rend.costModest": "부담이 적은 소프트웨어 인코딩", + "rend.costModestDetail": "{label}은 {mpps}이며 대부분의 장비가 감당할 수 있습니다.{machine} 그래도 실제 인코딩 하나가 돌아갑니다. 이를 선택한 모든 대상이 공유하지만 공짜는 아닙니다.", + "rend.costNoNumbers": "아직 비용을 추정할 수 없습니다", + "rend.costNoNumbersDetail": "소스를 아직 분석하지 않아 ‘소스 유지’를 뒷받침할 수치가 없습니다. 입력을 시작하면 이 부분이 채워집니다.", + "rend.costOffloaded": "{encoder}로 오프로드", + "rend.costOffloadedDetail": "{label}({mpps})은 GPU의 고정 기능 인코더에서 동작하므로 CPU를 거의 쓰지 않습니다. 품질과 레이트 제어는 드라이버마다 다르고 칩마다 고유한 상한이 있으니, 중요한 방송 전에는 출력을 확인하세요.", + "rend.create": "만들기", + "rend.created": "변형을 만들었습니다. 대상에서 선택하면 인코딩이 시작됩니다.", + "rend.custom": "사용자 지정…", + "rend.deintAll": "모든 프레임", + "rend.deintAllHint": "인터레이스인데도 그렇게 알리지 않는 소스를 위한 설정입니다. 많은 SDI 브리지와 캡처 카드는 무엇을 받았든 전부 프로그레시브로 표시하므로, 그런 장비에서는 “인터레이스 프레임만”이 아무 동작도 하지 않아 설정이 고장 난 것처럼 보입니다.", + "rend.deintAuto": "인터레이스 프레임만", + "rend.deintAutoHint": "소스가 인터레이스로 표시한 프레임만 처리하므로 프로그레시브 프레임은 그대로 통과합니다. 혼재된 소재에는 이 선택이 맞습니다.", + "rend.deintOff": "끄기", + "rend.deintOffHint": "거의 모두가 쓰는 프로그레시브 소스에는 이 설정이 맞습니다. 프로그레시브에 디인터레이스를 걸면 이득 없이 화면만 뭉개집니다.", + "rend.deinterlace": "디인터레이스", + "rend.deleteFailed": "변형을 삭제할 수 없습니다.", + "rend.deleteFallbackMany": "{count}개 대상이 패스스루로 돌아가 소스 영상이 그대로 전송됩니다. 소스가 각 플랫폼의 허용 조건에 여전히 맞는지 확인하세요.", + "rend.deleteFallbackOne": "{count}개 대상이 패스스루로 돌아가 소스 영상이 그대로 전송됩니다. 소스가 각 플랫폼의 허용 조건에 여전히 맞는지 확인하세요.", + "rend.deleteNoDestinations": "이 변형을 선택한 곳이 없으므로 삭제해도 바뀌는 대상이 없습니다.", + "rend.deleteRenditionTitle": "{name}을(를) 삭제할까요?", + "rend.editRendition": "변형 편집", + "rend.empty": "변형이 없습니다. 모든 대상이 패스스루이며, 어느 하나가 소스를 받지 못하게 되기 전까지는 그것이 정답입니다. 예를 들어 Twitch, Kick, X가 받아 주지 않는 4K60 입력이 그렇습니다. 변형을 추가하고 그 대상들을 그쪽으로 향하게 하세요.", + "rend.encoder": "인코더", + "rend.encoderPreset": "인코더 프리셋", + "rend.encoderRelatedFailed": "관련 인코더가 이 환경에서 실패했기 때문에 {name}은(는) 제공되지 않았습니다.", + "rend.encoderTestFailed": "{name}은(는) 이 머신에서 1프레임 테스트 인코딩에 실패했습니다.", + "rend.encoderTestOk": "{name}은(는) 이 머신에서 테스트 프레임을 인코딩했습니다.", + "rend.evenNote": "한 축을 0으로 두면 소스의 값을 유지하고 화면비도 보존합니다. 높이만 지정하면 크기를 조정할 수 있습니다. 둘 다 짝수여야 합니다. H.264와 HEVC에는 홀수 크기의 색차 평면이 없기 때문입니다.", + "rend.font": "글꼴", + "rend.frameRate": "프레임 레이트", + "rend.gop": "GOP", + "rend.gpuUnopenable": "GPU는 있지만 이 프로세스가 열 수 없습니다", + "rend.hardwareFallback": "하드웨어", + "rend.hardwareScanFound": "하드웨어 검사 결과", + "rend.height": "높이", + "rend.hevcWarning": "이 인코더는 HEVC를 출력합니다. 대부분의 RTMP 대상은 H.264만 받습니다. HEVC를 받는다고 밝힌 대상에만 선택하세요.", + "rend.keyframeInterval": "키프레임 간격", + "rend.letterboxColour": "레터박스 색상", + "rend.live": "라이브", + "rend.loadFailed": "변형을 불러올 수 없습니다.", + "rend.marginX": "여백 X(%)", + "rend.marginY": "여백 Y(%)", + "rend.name": "이름", + "rend.namePlaceholder": "Twitch와 Kick용 1080p60", + "rend.needsBothAWidthAnd": "너비와 높이가 모두 필요합니다 — 한 축이 비어 있으면 크기 조정이 이미 화면비를 유지합니다.", + "rend.needsBothAxes": "너비와 높이가 모두 필요합니다", + "rend.newRendition": "새 변형", + "rend.noEncoder": "이 FFmpeg는 사용할 수 있는 영상 인코더를 보고하지 않았습니다.", + "rend.noHardwareEscape": "이 장비에서는 동작하는 하드웨어 인코더가 없어 옮길 곳이 없습니다. 여기서 움직일 수 있는 것은 출력 크기뿐입니다. 그 뒤에 드라이버를 설치했거나 컨테이너에 GPU를 넘겨주었다면 하드웨어를 다시 검사하세요.", + "rend.notProbed": "이 장비에서는 아직 시험 인코딩을 하지 않았으므로, 이 목록은 하드웨어가 할 수 있는 것이 아니라 FFmpeg 빌드에 들어 있는 것입니다. 확인하려면 다시 검사하세요.", + "rend.notRunning": "실행 중 아님", + "rend.note": "메모", + "rend.noteDuplicateFps": "소스가 {srcFps} fps로 동작하므로 {fps} fps는 프레임을 복제할 뿐입니다.", + "rend.notePlaceholder": "이 계층의 용도.", + "rend.noteUpscale": "소스는 {width}×{height}입니다. 확대하면 CPU만 쓰고 디테일은 늘지 않습니다.", + "rend.opacity": "불투명도(%)", + "rend.percentNote": "모두 프레임에 대한 비율이므로 16:9 계층과 9:16 계층에서 같은 설정이 그대로 맞습니다. 가운데 정렬된 축에서는 여백이 무시됩니다.", + "rend.preset": "프리셋", + "rend.presetPlaceholder": "veryfast", + "rend.presetStartingPoint": "출발점입니다 — 현재 제한은 플랫폼에서 확인하세요.", + "rend.redetectFailed": "하드웨어를 다시 감지할 수 없습니다.", + "rend.redetectTitle": "GPU 검사를 다시 실행하고 인코더마다 시험 프레임 하나를 인코딩합니다. 몇 초 걸립니다.", + "rend.redetectedNoHardware": "하드웨어를 다시 감지했습니다. 이 머신에서는 테스트 인코딩을 통과한 하드웨어 인코더가 없습니다.", + "rend.resolution": "해상도", + "rend.restartEncode": "이 인코딩과 이를 읽는 대상들을 다시 시작합니다", + "rend.restartFailed": "인코딩을 다시 시작할 수 없습니다.", + "rend.restarting": "{name}을(를) 다시 시작하는 중입니다. 해당 대상들도 함께 다시 연결됩니다.", + "rend.save": "저장", + "rend.saveFailed": "변형을 저장할 수 없습니다.", + "rend.seedNote": "이들은 양식에 초깃값을 넣을 뿐입니다. 저장하기 전에 무엇이든 수정하세요. 플랫폼의 상한은 바뀌며 파트너 등급에 따라 다릅니다.", + "rend.signalError": "오류", + "rend.signalIdle": "유휴", + "rend.sizePct": "크기(높이 대비 %)", + "rend.softwareEncoderFallback": "소프트웨어 인코더", + "rend.speed": "속도", + "rend.subtitle": "공유되는 영상 인코딩 하나에 대상은 여럿. 여기서 오디오를 건드리는 일은 없습니다.", + "rend.target": "목표", + "rend.textColour": "글자 색상", + "rend.textPlaceholder": "MY STATION", + "rend.title": "변형", + "rend.unused": "아직 이를 선택한 대상이 없어 인코딩이 진행되지 않습니다. 대상에서 고르면 인코딩이 시작됩니다.", + "rend.uptime": "가동 시간", + "rend.videoBitrate": "영상 비트레이트", + "rend.waitingSnapshot": "첫 라이브 스냅숏을 기다리는 중입니다.", + "rend.watermarkImage": "워터마크 이미지", + "rend.watermarkPlaceholder": "overlays/logo.png", + "rend.width": "너비", + "rend.widthPct": "너비(프레임 대비 %)", + "route.attack": "어택", + "route.audio": "오디오", + "route.audioAndVideoGoOut": "오디오와 비디오가 함께 나갑니다.", + "route.audioLaterThanVideo": "오디오가 비디오보다 늦음", + "route.audioRouting": "오디오 라우팅", + "route.audioRoutingDesc": "각 대상이 받을 입력 트랙을 정확히 고릅니다.", + "route.autoLimitWhen2Tracks": "자동(2트랙 이상일 때 제한)", + "route.commentaryLanguage": "해설 언어", + "route.couldNotApplyThePreset": "프리셋을 적용할 수 없습니다.", + "route.couldNotCompileThisRouting": "이 라우팅을 컴파일할 수 없습니다.", + "route.couldNotLoadDestinations": "대상을 불러올 수 없습니다.", + "route.couldNotSaveTheRouting": "라우팅을 저장할 수 없습니다.", + "route.couldNotSaveTrackRoles": "트랙 역할을 저장할 수 없습니다.", + "route.custom": "사용자 지정…", + "route.delayDirection": "지연 방향", + "route.destinations": "대상", + "route.duckingNote": "다른 그룹이 말하는 동안 한 트랙 묶음의 레벨을 내립니다. 음악 베드 위의 마이크가 그런 경우입니다. 끄면 선택한 각 트랙이 자기 게인 그대로 합쳐집니다.", + "route.duckingTriggerNote": "트리거가 이 대상의 믹스에 들어 있을 필요는 없습니다. 음악만 보내면서도 마이크로 여기의 음악을 낮출 수 있습니다.", + "route.enableDucking": "더킹 활성화", + "route.everyTrackReaches": "선택한 모든 트랙이 음악을 포함해 이 대상에 도달합니다.", + "route.excludeMusic": "음악 제외", + "route.keepMusic": "음악 유지", + "route.keepMusicOut": "이 대상에는 음악을 보내지 않기", + "route.labelTracks": "트랙에 이름표 달기", + "route.langPlaceholder": "lang — 예: es", + "route.limiter04DbfsCeiling": "리미터(−0.4 dBFS 한계)", + "route.localOnlyNote": "이 서버는 아직 트랙 역할을 저장하지 않으므로 이 값들은 이 브라우저에만 남습니다. 페이지의 다른 항목은 평소대로 저장됩니다.", + "route.loudnessTarget": "라우드니스 목표", + "route.lufsBroadcast": "EBU R128 방송", + "route.measuredOverTheWholeProgramme": "순간순간이 아니라 프로그램 전체를 기준으로 측정하므로 조용한 대목은 조용하게 남습니다. 이 수치들은 각 플랫폼의 자체 노멀라이저가 목표로 삼는 값이며, 그중 하나에 맞추는 것이 플랫폼이 음량을 낮추지 않게 하는 방법입니다.", + "route.mixMatrix": "믹스 행렬", + "route.musicPolicy": "음악 정책", + "route.noTarget": "목표 없음(레벨은 그대로)", + "route.normalizeLoudnorm": "라우드니스(EBU R128)", + "route.normalizeOff": "끄기", + "route.off": "꺼짐", + "route.offset": "오프셋", + "route.on": "켜짐", + "route.platformDefault": "플랫폼 기본값", + "route.podcastSpokenWord": "팟캐스트 / 토크", + "route.presetApplied": "프리셋을 적용했습니다. 내용을 확인한 뒤 저장하세요.", + "route.range": "범위", + "route.ratio": "비율", + "route.release": "릴리스", + "route.result": "결과", + "route.role51": "5.1", + "route.roleClean": "클린", + "route.roleMic": "마이크", + "route.roleMusic": "음악", + "route.rolesNote": "역할은 이 대상이 아니라 입력을 설명하며 별도로 저장됩니다. 아래의 대상이 요청하기 전까지는 아무것도 걸러지지 않습니다.", + "route.simple": "간단", + "route.target": "타깃", + "route.targetRole": "타깃", + "route.thisDestinationSPlatform": "이 대상의 플랫폼", + "route.threshold": "임계값", + "route.title": "Routing", + "route.triggerRole": "트리거", + "route.truePeak": "트루 피크", + "route.video": "영상", + "route.videoLaterThanAudio": "비디오가 오디오보다 늦음", + "route.withoutATargetTheMix": "목표값이 없으면 믹스는 입력이 만들어 낸 레벨 그대로 나가며, 위의 클리핑 보호만 적용됩니다.", + "route.youtubeTwitchSpotify": "YouTube, Twitch, Spotify", + "set.acceptSelfSigned": "자체 서명 인증서 허용", + "set.accountDisconnected": "계정 연결을 끊었습니다.", + "set.adminPassword": "관리자 비밀번호", + "set.alertDelivery": "알림 전달", + "set.alertDeliveryDesc": "응답하지 않는 웹훅을 어디까지 재시도한 뒤 알림을 포기할지.", + "set.apiTokens": "API 토큰", + "set.apiTokensDesc": "스크립트, Prometheus 등 세션을 유지할 수 없는 대상을 위한 장기 자격 증명입니다. Authorization: Bearer pmk_… 형태로 보내세요. 토큰은 관리자가 할 수 있는 모든 일을 할 수 있지만 토큰 관리만은 예외이며, 그 권한은 비밀번호 뒤에 남습니다. 따라서 토큰을 철회하면 되돌릴 수 없습니다.", + "set.app": "앱", + "set.attemptsBackoff": "시도 사이의 대기 시간은 자동으로 늘어나 30초 상한에 이릅니다. 따라서 이 범위의 위쪽 끝은 이미 죽은 엔드포인트를 몇 분 동안 쫓는다는 뜻입니다. 상한이 있다는 것 자체가 핵심입니다. 끝없이 재시도하면 죽은 웹훅 하나가 결코 비워지지 않는 대기열로 변합니다.", + "set.attemptsNote": "첫 시도를 포함합니다. 1은 재시도하지 않음을 뜻합니다.", + "set.attemptsRetryable": "재시도할 가치가 있는 실패만 재시도합니다. 누군가 삭제한 웹훅의 404는 영구적이며, 네 번 더 시도해도 뒤에 밀린 알림을 지연시킬 뿐입니다. 리버스 프록시의 502야말로 이 시도가 존재하는 이유입니다.", + "set.audioMeters": "오디오 미터", + "set.brokerPasswordCleared": "브로커 비밀번호를 지웠습니다.", + "set.brokerPlainNote": "mqtt://는 암호화되지 않습니다. MQTT는 사용자 이름과 비밀번호를 CONNECT 패킷에 담아 보내므로 둘 다 평문으로 네트워크를 지납니다. 신뢰할 수 있는 LAN에서는 괜찮으며 이것이 일반적인 Home Assistant 구성입니다. 다만 통제할 수 없는 경로에서는 적절하지 않습니다. 브로커가 지원한다면 mqtts://를 사용하세요.", + "set.brokerUrl": "브로커 URL", + "set.brokerUrlNote": "mqtt://, mqtts://, ws:// 또는 wss://. URL에 담긴 비밀번호는 거부됩니다. URL은 로그 줄과 ps 출력에 남고, 그건 되돌릴 수 없습니다. 아래 칸에 입력하세요. 그곳에서는 저장 시 암호화됩니다.", + "set.brokerUrlPlaceholder": "mqtts://broker.local:8883", + "set.chatBothApply": "둘 다 적용되며 더 너그러운 쪽이 이깁니다. 메시지는 지정한 시간보다 오래되고 동시에 최신 N개 밖으로 밀려났을 때 사라집니다. 그래서 같은 설정이라도 붐비는 채널은 조용한 채널보다 짧은 기간만 보관합니다.", + "set.chatBufferNote": "“연결 시 전송”은 나머지 둘과 성격이 다른 숫자입니다. 그 둘은 디스크에 남기는 양을 제한하지만, 이것은 메모리에 두는 버퍼이며 연결된 대시보드마다 할당됩니다.", + "set.chatHistory": "채팅 기록", + "set.chatHistoryDesc": "채팅 기록이 얼마나 거슬러 올라가는지, 그리고 그에 따라 모더레이터가 누군가의 카드를 열었을 때 얼마나 볼 수 있는지.", + "set.chatHours": "보관 기간(시간)", + "set.chatKeep": "항상 보관(메시지 수)", + "set.chatKeepNote": "오래된 정도와 무관한 하한입니다.", + "set.chatNotOnlyDisk": "이것은 디스크만의 설정이 아닙니다. 채팅에서 이름을 클릭하면 그 사람이 이전에 한 말이 열리는데, 그 기록은 여기에서 옵니다. 어떤 플랫폼도 이를 위한 API를 공개하지 않으므로 polyemesis는 보관해 둔 것만 보여줄 수 있습니다. 너무 짧게 두면 일주일 내내 문제를 일으킨 사람이 아무 말도 하지 않은 것처럼 보입니다.", + "set.chatSendNote": "질의 없는 기록입니다.", + "set.chatSendOnConnect": "연결 시 전송(메시지 수)", + "set.chatSendOnConnectShort": "연결 시 전송", + "set.chatSmall": "채팅은 용량이 작습니다. 초당 평균 열 개의 메시지가 오가는 채널이 시간당 대략 7MB를 저장하며, 대부분은 그보다 훨씬 적습니다.", + "set.clientId": "클라이언트 ID", + "set.clientIdNote": "브로커에서 고유해야 합니다. 충돌은 설명되지 않는 재연결 루프의 첫 번째 원인입니다. 브로커는 연결할 때마다 더 오래된 세션을 끊고, 두 클라이언트는 끝없이 재연결합니다.", + "set.clientIdPlaceholder": "인스턴스 이름에서 파생", + "set.clientSecret": "클라이언트 시크릿", + "set.comingBack": "복귀", + "set.confirmPassword": "새 비밀번호 확인", + "set.connectedAccounts": "연결된 계정", + "set.copied": "복사됨", + "set.copy": "복사", + "set.couldNotChangeThePassword": "비밀번호를 변경할 수 없습니다.", + "set.couldNotClearThePassword": "비밀번호를 지울 수 없습니다.", + "set.couldNotCreateTheToken": "토큰을 만들 수 없습니다.", + "set.couldNotReCheckThe": "자격 증명을 다시 확인할 수 없습니다.", + "set.couldNotReadTheTls": "TLS 상태를 읽을 수 없습니다.", + "set.couldNotRevokeTheToken": "토큰을 취소할 수 없습니다.", + "set.couldNotSaveTheCredentials": "자격 증명을 저장할 수 없습니다.", + "set.couldNotSaveTheMqtt": "MQTT 설정을 저장할 수 없습니다.", + "set.credsRemoved": "자격 증명을 삭제했습니다.", + "set.currentPassword": "현재 비밀번호", + "set.dataDirectory": "데이터 디렉터리", + "set.deliveryAttempts": "전달 시도 횟수", + "set.dialsOnly": "서버가 연결하는 대상은 다음뿐입니다: {schemes}.", + "set.disconnect": "연결 끊기", + "set.disconnectDesc": "이를 사용하는 대상은 설정을 유지하지만 스트림 키 갱신을 멈춥니다. 같은 계정은 나중에 다시 연결할 수 있습니다.", + "set.disconnectTitle": "이 계정의 연결을 끊을까요?", + "set.drawtextMissing": "이 빌드에는 drawtext 필터가 없어 텍스트 오버레이를 쓸 수 없습니다. libfreetype과 함께 빌드된 FFmpeg가 필요합니다.", + "set.drawtextOk": "이 빌드는 영상에 텍스트를 그릴 수 있습니다.", + "set.enabled": "활성화", + "set.encoderUrl": "인코더 URL", + "set.expires": "만료", + "set.failover": "장애 조치", + "set.failoverDesc": "라이브 소스가 전송을 멈췄을 때를 위한 예비 입력과 대기 화면입니다. 기본값은 꺼짐이며, 꺼져 있는 동안 파이프라인은 이 기능이 생기기 전과 바이트 단위로 동일합니다.", + "set.failoverRemuxNote": "이를 켜면 입력과 그 아래 모든 것 사이에 리먹스 단계가 하나 추가됩니다. 저렴하지만 공짜는 아니며, 그래서 기본값이 아니라 선택 사항입니다.", + "set.fileSizeMb": "파일 크기(MB)", + "set.filesKept": "보관 파일 수", + "set.goingLive": "방송 시작", + "set.goingLiveDesc": "여러 대상이 동시에 시작할 때 벌어지는 일.", + "set.graceNote": "계층이 전환되기 전까지 라이브 소스가 아무것도 보내지 않아도 되는 시간입니다. 재연결보다 일부러 길게 잡았습니다. RTMP 연결을 다시 맺는 인코더는 정상 동작이지 장애가 아닙니다.", + "set.gracePeriod": "유예 시간(초)", + "set.haDiscovery": "Home Assistant 검색", + "set.height": "높이", + "set.ingest": "입력", + "set.ingestDesc": "SRT는 최대 여섯 개의 오디오 트랙을 운반합니다. RTMP는 하나를 운반하며 SRT를 지원하지 않는 인코더를 위한 대체 수단으로 존재합니다. 가져오기는 송출을 기다리는 대신 소스로 직접 연결합니다.", + "set.ingestUrlCopied": "입력 URL을 복사했습니다.", + "set.instance": "인스턴스", + "set.intervalNote": "변경되지 않은 상태는 다시 게시되지 않으므로, 간격을 짧게 해도 드는 비용은 메시지가 아니라 비교 연산입니다. 킵얼라이브는 끊긴 연결이 얼마나 오래 눈에 띄지 않는지, 나아가 브로커가 구독자에게 이 인스턴스가 사라졌음을 얼마나 빨리 알리는지를 결정합니다.", + "set.issuer": "발급자", + "set.kbps": "kbps", + "set.keepAlive": "킵얼라이브(초)", + "set.latencyMs": "지연 시간(ms)", + "set.listeners": "리스너", + "set.listenersDesc": "설치 전체에 대해 서버가 바인딩하는 위치입니다. SRT 포트 하나가 모든 소스를 처리합니다. 소스는 게시 토큰으로 구분되므로 소스를 추가한다고 포트를 하나 더 열 필요가 없습니다. RTMP에는 그런 라우팅이 없어 해당 포트는 소스 하나만 처리합니다.", + "set.logsBoundNote": "로그 디렉터리는 이 두 값의 곱으로 제한되므로, 로그 보관이 디스크를 가득 채우는 원인이 될 수는 없습니다.", + "set.logsDesc": "수집한 FFmpeg의 표준 오류는 항상 메모리에 보관되지만, 그 메모리는 서버와 함께 사라집니다. 즉 왜 죽었는지 설명하는 바로 그 줄들이 사라집니다. 영구 저장을 켜면 데이터 디렉터리 아래의 순환 파일로 복제됩니다.", + "set.metersDesc": "보조 프로세스 하나가 모든 트랙의 모든 채널을 측정합니다. 간격이 짧을수록 갱신이 빨라지고 CPU를 더 씁니다.", + "set.mode": "모드", + "set.modePull": "가져오기 — 카메라, 피드 또는 파일에 연결", + "set.modeRtmp": "RTMP — 단일 트랙", + "set.modeSrt": "SRT — 멀티트랙(권장)", + "set.mqtt": "MQTT 텔레메트리", + "set.mqtt5Only": "MQTT 5.0 전용.", + "set.mqtt5OnlyNote": "3.1.1로 고정된 브로커는 연결 자체를 완료하지 못합니다. 기능이 축소된 모드가 아닙니다. Mosquitto 2.x, EMQX, Home Assistant 애드온은 모두 5.0을 지원합니다.", + "set.mqttDesc": "상태를 retained 메시지로 브로커에 게시하므로, 변경 시점에 연결되어 있지 않던 소비자도 현재 값을 받을 수 있습니다.", + "set.multiAccountNote": "같은 플랫폼을 여러 번 연결하면 여러 채널로 송출할 수 있습니다. 계정마다 별도의 대상이 됩니다.", + "set.name": "이름", + "set.newPassword": "새 비밀번호", + "set.no": "아니요", + "set.noEncryption": "이 연결은 아무것도 암호화하지 않습니다. 비밀번호와 세션 쿠키가 평문으로 네트워크를 지나갑니다. tls.mode: auto를 설정하거나, 프록시 뒤에서 127.0.0.1에 바인딩하세요.", + "set.noLibsrt": "이 FFmpeg 빌드에는 SRT 지원이 없습니다. --enable-libsrt로 구성한 빌드를 설치하거나 RTMP를 사용하세요.", + "set.noTokens": "아직 토큰이 없습니다.", + "set.passphrase": "암호문", + "set.passphraseNote": "AES 암호화를 켭니다. 사용하지 않으려면 비워 두세요. SRT는 10~79자 범위를 벗어난 암호문을 거부합니다.", + "set.passphrasePlaceholder": "선택 사항 — 10자에서 79자", + "set.password": "비밀번호", + "set.passwordChanged": "비밀번호를 변경했습니다.", + "set.passwordMismatch": "새 비밀번호가 일치하지 않습니다.", + "set.passwordNote": "비밀번호는 어떤 API도 반환하지 않으므로, 설정되어 있어도 이 칸은 비어 있습니다. 저장된 비밀번호를 유지하려면 비워 두고, 지우려면 아래 버튼을 사용하세요.", + "set.platform": "플랫폼", + "set.platformsDesc": "polyemesis가 이 가운데 얼마나 자동화할 수 있는지는 플랫폼마다 다르며, 그 차이는 한 시간 뒤가 아니라 시작하기 전에 알아 두는 편이 좋습니다. 송출 자체는 어디서나 똑같이 동작합니다. 대상별 오디오 라우팅, 변형, 재연결, 미터는 아래 어떤 열에도 의존하지 않습니다.", + "set.playLoop": "아무것도 방송하지 않을 때 루프 재생", + "set.playLoopNote": "대기 화면과 나란한 두 번째 후보로, 정지 이미지 대신 파일을 재생합니다. 둘 다 켤 수 있으며, 어느 쪽이 우선하는지는 장애 조치 설계의 계층 순서를 참고하세요.", + "set.pointObs": "OBS를 이 주소로 향하게 하세요.", + "set.preview": "미리 보기", + "set.previewDesc": "대시보드의 HLS 모니터입니다. polyemesis가 영상을 다시 인코딩하는 유일한 지점이며, 자체 릴레이 구독 위에서 동작하므로 대상에 영향을 줄 수 없습니다.", + "set.previewIdleNote": "인코더는 대시보드가 재생목록을 요청할 때 시작하고 마지막 요청 이후 이 시간이 지나면 다시 멈추므로, 대시보드를 닫아 두면 CPU를 쓰지 않습니다. 이 값을 바꿔도 누군가 보고 있는 미리 보기가 재시작되지는 않습니다.", + "set.processLogs": "프로세스 로그", + "set.proxyTerminatesTls": "polyemesis 앞의 리버스 프록시가 TLS를 종단하므로, 이 서버는 의도적으로 평문 HTTP를 제공합니다.", + "set.publishInterval": "게시 간격(초)", + "set.reconnectCap": "재연결 대기 상한(초)", + "set.reconnectCapNote": "0은 기본값을 사용합니다. HTTP 및 HLS 소스만 해당합니다.", + "set.redirectNote": "스킴과 포트를 포함해 정확히 일치해야 합니다. 리버스 프록시를 통해 polyemesis에 접속한다면 그 공개 URL을 사용하세요.", + "set.redirectUri": "허용할 리디렉션 URI", + "set.removeCreds": "자격 증명 삭제", + "set.removeCredsDesc": "이 앱을 통해 연결된 모든 계정이 토큰 갱신을 멈추고 다시 연결해야 합니다. 클라이언트 시크릿은 여기서 복구할 수 없으며, 플랫폼에서 새 앱을 만들어야 합니다.", + "set.removeCredsTitle": "이 개발자 자격 증명을 삭제할까요?", + "set.returnAfter": "복귀까지(안정 상태 초)", + "set.returnAutomatic": "자동 — 주 소스가 안정되면 복귀", + "set.returnManual": "수동 — 내가 전환할 때까지 예비에 머무름", + "set.returnManualNote": "기본값이 수동인 이유는 자동 복귀가 요동칠 수 있기 때문입니다. 띄엄띄엄 회복하는 주 소스는 방송 도중 프로그램을 이리저리 옮겨 놓게 됩니다.", + "set.rtmpPort": "RTMP 포트", + "set.rtspNote": "카메라가 달리 할 수 없는 경우가 아니라면 TCP를 쓰세요. NAT을 지나는 UDP 기반 RTSP는 연결된 것처럼 보이면서 아무것도 전달하지 않습니다.", + "set.rtspTransport": "RTSP 전송", + "set.saveAndDrop": "저장하고 라이브 스트림 끊기", + "set.saveIngest": "입력 설정 저장", + "set.segmentSec": "세그먼트(초)", + "set.settingsSavedAffectedProcessesHave": "설정을 저장했습니다. 영향을 받는 프로세스를 다시 시작했습니다.", + "set.showSlate": "아무것도 방송하지 않을 때 대기 화면 표시", + "set.slateColour": "대기 화면 색상", + "set.slateColourNote": "한 단어로 적으세요. 값이 필터 그래프로 전달되는데 거기서는 쉼표가 인수를 끝내기 때문입니다. 이미지가 설정되지 않은 경우에만 사용됩니다.", + "set.slateColourPlaceholder": "black", + "set.slateImage": "대기 화면 이미지", + "set.slateImageNote": "데이터 디렉터리 안의 경로입니다. 비워 두면 대신 단색으로 채웁니다. 색에는 열리지 않을 파일이 없기 때문에 바로 그것이 대체 수단이 됩니다.", + "set.slateImagePlaceholder": "slate/holding.png", + "set.sourceUrl": "소스 URL", + "set.sourceUrlNote": "{schemes} 중 하나입니다. file:// 소스는 데이터 디렉터리를 기준으로 한 상대 경로이며 “..”를 포함할 수 없습니다. 업로드된 파일과 똑같은 방식으로 그 안에 갇힙니다.", + "set.sourceUrlPlaceholder": "rtsp://camera.local/stream1", + "set.srtPort": "SRT 포트", + "set.stagger": "연결 시차 두기(ms)", + "set.staggerNote": "0이면 끕니다. 그러지 않으면 방송 시작이란 모든 대상이 같은 순간에 연결을 열고 TLS를 협상하며 오디오 인코딩을 시작한다는 뜻입니다. 작은 장비에서는 프레임이 가장 떨어지기 쉬운 순간이고, 방금 방송을 시작했으니 바로 여러분이 지켜보고 있는 순간이기도 합니다. 수백 밀리초면 충분합니다.", + "set.staggerReconnectNote": "재연결이 지연되는 일은 없습니다. 새벽 3시에 끊긴 대상은 이미 정상인 프로세스들 뒤에서 차례를 기다리지 않고 즉시 돌아옵니다.", + "set.stepByStep": "단계별 설정", + "set.stopAfterIdle": "유휴 후 중지(초)", + "set.streamKey": "스트림 키", + "set.streamKeyNote": "송출하는 쪽은 정확히 이 경로를 사용해야 합니다. 그래야 열린 포트가 열린 문이 되지 않습니다.", + "set.subject": "주체", + "set.synthesiseNote": "주요 플랫폼은 모두 오디오 트랙이 없는 스트림을 거부합니다. 이 옵션을 켜면 영상만 있는 입력에 무음 스테레오 트랙 하나가 추가되어 대상이 시작됩니다. 꺼져 있으면 “routing profile selects no audio”로 거부합니다. 트랙이 0개로 감지된 입력에만 적용되므로, 실제로 보내고 있는 오디오를 밀어내는 일은 결코 없습니다.", + "set.synthesiseSilent": "무음 트랙 합성", + "set.syntheticAudio": "합성 오디오", + "set.syntheticDesc": "입력에 영상만 있고 오디오가 전혀 없을 때의 동작.", + "set.tabIngest": "입력", + "set.tabPipeline": "파이프라인", + "set.tabPlatforms": "플랫폼 자격 증명", + "set.tabSecurity": "보안", + "set.title": "설정", + "set.tlsAcme": "Let's Encrypt", + "set.tlsAuto": "자동", + "set.tlsDesc": "여기가 아니라 config.yaml에서 설정합니다. 서버가 수신을 시작하기 전에 TLS가 올바르게 되어 있어야 하기 때문입니다. 이 카드는 그 결과를 보고합니다.", + "set.tlsManual": "수동 인증서", + "set.tlsOff": "여기서 종료하지 않음", + "set.tlsSelfsigned": "자체 서명", + "set.tokenHashNote": "polyemesis는 해시만 저장합니다. 분실했다면 이 토큰을 철회하고 새로 만드세요.", + "set.tokenNamePlaceholder": "prometheus", + "set.tokenRevoked": "{name}을(를) 취소했습니다.", + "set.topicPrefix": "토픽 접두사", + "set.topicPrefixNote": "접두사의 구분자는 그대로 유지되므로 home/av는 두 단계를 뜻합니다. $로 시작하는 접두사는 거부됩니다. 브로커가 그것을 예약해 두었고, #를 쓰는 구독자에게는 결코 도달하지 않기 때문입니다. 텔레메트리는 정상적으로 게시되면서, 정작 문제를 살펴볼 바로 그 화면에서만 보이지 않게 됩니다.", + "set.transportSecurity": "전송 보안", + "set.updateInterval": "갱신 간격(ms)", + "set.username": "사용자 이름", + "set.validFor": "유효 대상", + "set.whatPlatformsDo": "플랫폼별로 가능한 작업", + "set.whyDevApp": "자체 개발자 앱이 필요한 이유", + "set.whyDevAppDesc": "polyemesis는 OAuth 클라이언트 시크릿을 함께 배포할 수 없습니다. 바이너리를 가진 사람 누구나 그것을 손에 넣게 되고, 플랫폼은 이를 철회할 것이기 때문입니다. 무료 개발자 앱을 한 번 등록하고 클라이언트 ID와 시크릿을 여기에 붙여 넣으면, 이후 polyemesis가 여러분 대신 스트림 키를 가져옵니다.", + "set.writeLogs": "로그를 디스크에 기록", + "set.yes": "예", + "sources.addDescription": "자체 입력, 대상, 변형을 갖는 두 번째 프로그램입니다. 예를 들어 가로 캔버스 옆의 세로 캔버스가 있습니다. 기본 포트에서 시작하므로, 둘이 충돌하지 않도록 포트를 변경하는 것이 좋습니다.", + "sources.addFailed": "소스를 추가할 수 없습니다", + "sources.addSource": "소스 추가", + "sources.addTitle": "소스 추가", + "sources.added": "“{name}”을(를) 추가했습니다. 아래에서 입력 포트를 설정하세요.", + "sources.app": "앱", + "sources.applyAndDrop": "적용하고 스트림 끊기", + "sources.copied": "{what} 복사됨", + "sources.copyFailed": "{what}을(를) 복사할 수 없습니다", + "sources.copyTokenAria": "토큰 복사", + "sources.copyUrlAria": "{proto} URL 복사", + "sources.default": "기본", + "sources.defaultDescription": "소스를 지정하지 않은 요청은 이 소스에 적용됩니다.", + "sources.deleteAria": "{name} 삭제", + "sources.deleteConfirm": "소스 삭제", + "sources.deleteDescription": "해당 대상과 변형도 함께 삭제됩니다. 이들은 이 프로그램이 어디로 가는지를 설명하며 프로그램 없이는 의미가 없습니다. 녹화는 유지됩니다. 파일은 디스크에 남아 있고 재생도 가능하며, 단지 소스에 귀속되지 않게 될 뿐입니다.", + "sources.deleteFailed": "소스를 삭제할 수 없습니다", + "sources.deleteThis": "이 소스 삭제", + "sources.deleteTitle": "“{name}”을(를) 삭제할까요?", + "sources.deleted": "“{name}”과(와) 해당 대상을 삭제했습니다", + "sources.destinations": "대상", + "sources.dirtyIdle": "저장되지 않은 입력 변경 사항이 있습니다. 적용하면 이 소스의 입력이 재시작됩니다.", + "sources.dirtyPublishing": "이 소스로 인코더가 송출 중입니다. 적용하면 입력이 재시작되고 시청 중인 모든 사람의 연결이 끊깁니다.", + "sources.enableAria": "{name} 활성화", + "sources.help.app": "RTMP 애플리케이션 경로 — URL에서 호스트와 스트림 키 사이의 부분입니다. 대부분의 인코더는 “live”를 기대합니다. 이유가 있을 때만 변경하세요.", + "sources.help.ingest": "인코더가 이 소스에 도달하는 방식입니다. SRT는 유실된 패킷을 재전송하여 손실이 있는 연결에서도 견디므로, 공용 인터넷에서는 적절한 기본값입니다. RTMP는 더 오래되었고 어디서나 지원되지만 불안정한 회선에서는 품질이 크게 떨어집니다. 가져오기를 선택하면 송출을 기다리는 대신 polyemesis가 지정한 URL에서 직접 가져옵니다.", + "sources.help.latencyMs": "SRT가 늦거나 재전송된 패킷을 포기하기까지 기다리는 시간입니다. 값이 클수록 지연을 대가로 나쁜 연결을 견딥니다. 왕복 시간의 약 2.5배가 좋은 출발점이므로 RTT가 40ms라면 여기는 약 100ms가 적당합니다. 너무 낮으면 짧은 끊김이 눈에 보이는 결함이 됩니다.", + "sources.help.passphrase": "SRT 연결을 암호화하며, 현재 이 입력에 외부인이 접근하지 못하게 막는 것은 사실상 이것입니다. 10자에서 79자 사이입니다. 비워 두면 포트에 도달할 수 있는 누구나 송출할 수 있습니다.", + "sources.help.streamKey": "인코더가 RTMP로 송출하기 위해 보내는 비밀 값입니다. 이 입력을 지키는 자격 증명이므로 비밀번호처럼 다루세요. 이를 가진 사람은 누구나 여러분의 대상으로 방송할 수 있습니다.", + "sources.help.token": "여러 소스가 하나의 입력 포트를 공유할 때 이 소스를 식별합니다. 접근까지 통제하는지는 입력 모드에 따라 다르며, 아래 설명이 현재 어느 쪽인지 알려줍니다. 교체하면 새 토큰이 발급되고 이전 토큰은 5분간 유효하므로, 방송 중인 인코더가 중간에 끊기지 않습니다.", + "sources.ingest": "입력", + "sources.lastSourceTitle": "마지막 소스는 삭제할 수 없습니다 — 설치에는 입력이 하나 필요합니다", + "sources.latencyMs": "지연 시간 (ms)", + "sources.loadFailed": "소스를 불러올 수 없습니다", + "sources.loading": "소스를 불러오는 중…", + "sources.loss": "손실", + "sources.modePull": "가져오기", + "sources.name": "이름", + "sources.namePlaceholder": "세로", + "sources.noEngine": "이 소스에 대해 실행 중인 엔진이 없습니다. 흔한 원인은 다른 소스나 다른 프로세스가 이미 사용 중인 입력 포트입니다.", + "sources.notRunning": "실행 중 아님", + "sources.ownDescription": "자체 대상, 변형, 녹화.", + "sources.passphrase": "암호문", + "sources.passphrasePlaceholder": "10–79자", + "sources.publishing": "송출 중", + "sources.renditions": "변형", + "sources.retrans": "재전송", + "sources.rotate": "교체", + "sources.rotateConfirm": "토큰 교체", + "sources.rotateFailed": "토큰을 교체할 수 없습니다", + "sources.rotateIdle": "새 토큰이 즉시 발급됩니다. 이미 사용 중인 인코더가 끊기지 않도록 이전 토큰도 5분 동안 유지됩니다.", + "sources.rotateTitle": "“{name}”의 토큰을 교체할까요?", + "sources.rotateWhilePublishing": "지금 이 소스로 인코더가 송출 중입니다. 이전 토큰은 5분 동안 계속 작동하므로 연결이 끊기지는 않지만, 그 시간이 지나기 전에 반드시 갱신해야 합니다.", + "sources.rtt": "RTT", + "sources.running": "실행 중", + "sources.saveFailed": "저장할 수 없습니다", + "sources.streamKey": "스트림 키", + "sources.subtitle": "각각 하나의 수신 프로그램입니다. 대상과 변형은 소스에 속합니다.", + "sources.title": "소스", + "sources.token": "토큰", + "sources.tokenEnforcedDetailRtmp": "스트림 키입니다. 위 URL에 이 값을 키로 사용해 송출하세요. 교체하면 새 키가 발급되고 이전 키도 5분간 유효하므로 방송을 끊지 않고 전환할 수 있습니다.", + "sources.tokenEnforcedDetailSrt": "공유 포트로 이 소스에 송출하려면 인코더의 SRT streamid에 입력하세요. 교체하면 새 토큰이 발급되고 이전 토큰도 5분간 유효하므로 방송을 끊지 않고 전환할 수 있습니다.", + "sources.tokenIsCredential": "이 토큰이 자격 증명입니다.", + "sources.tokenIssued": "새 토큰이 발급되었습니다", + "sources.tokenNotEnforced": "이 토큰은 저장되어 있지만 현재 적용되지는 않습니다.", + "sources.tokenNotEnforcedRtmp": "소스는 포트로 구분됩니다. 이 입력을 실제로 보호하는 것은 위의 스트림 키입니다. 토큰을 자격 증명으로 만들려면 설정에서 단일 포트 입력을 켜세요.", + "sources.tokenNotEnforcedSrt": "소스는 포트로 구분됩니다. 이 입력을 실제로 보호하는 것은 위의 SRT 암호문입니다. 토큰을 자격 증명으로 만들려면 설정에서 단일 포트 입력을 켜세요.", + "state.disabled": "비활성화됨", + "state.failed": "실패", + "state.live": "라이브", + "state.offline": "오프라인", + "state.reconnecting": "다시 연결 중", + "state.starting": "시작 중", + "state.stopped": "중지됨" } diff --git a/ui/src/lib/i18n/nl.json b/ui/src/lib/i18n/nl.json index 2222f3e5..7ca883e1 100644 --- a/ui/src/lib/i18n/nl.json +++ b/ui/src/lib/i18n/nl.json @@ -1,26 +1,227 @@ { - "nav.dashboard": "Dashboard", - "nav.sources": "Bronnen", - "nav.meters": "Audiometers", - "nav.routing": "Routing", - "nav.renditions": "Renderings", - "nav.playout": "Playout", - "nav.library": "Bibliotheek", - "nav.clips": "Fragmenten", - "nav.chat": "Chat", - "nav.jobs": "Taken", - "nav.automation": "Automatisering", - "nav.recordings": "Opnames", - "nav.monitoring": "Monitoring", - "nav.settings": "Instellingen", - "chrome.toggleNav": "Navigatie in- en uitschakelen", + "auth.accountCreated": "Beheerdersaccount aangemaakt.", + "auth.confirmPassword": "Wachtwoord bevestigen", + "auth.createAccount": "Account aanmaken", + "auth.firstRun": "Eerste installatie", + "auth.firstRunDesc": "Kies de beheerdersgegevens voor deze server. Er is geen accountherstel, bewaar ze dus ergens veilig.", + "auth.password": "Wachtwoord", + "auth.passwordMismatch": "De wachtwoorden komen niet overeen.", + "auth.signIn": "Aanmelden", + "auth.signInDesc": "Voer je beheerdersgegevens in.", + "auth.somethingWentWrong": "Er is iets misgegaan.", + "auth.tokenCreated": "API-token aangemaakt", + "auth.tokenRevoked": "API-token ingetrokken", + "auth.username": "Gebruikersnaam", + "auto.action": "Actie", + "auto.adminPasswordChanged": "Beheerderswachtwoord gewijzigd", + "auto.chosenWeekdays": "Gekozen weekdagen", + "auto.coalesced": "Samengevoegd", + "auto.coalescingWindow": "Samenvoegvenster (seconden)", + "auto.couldNotChangeTheRule": "Kon de regel niet wijzigen.", + "auto.couldNotChangeTheSchedule": "Kon de planning niet wijzigen.", + "auto.couldNotDeleteTheRule": "Kon de regel niet verwijderen.", + "auto.couldNotDeleteTheSchedule": "Kon de planning niet verwijderen.", + "auto.couldNotLoadAutomationSettings": "Kon de automatiseringsinstellingen niet laden.", + "auto.couldNotSaveTheRule": "Kon de regel niet opslaan.", + "auto.couldNotSaveTheSchedule": "Kon de planning niet opslaan.", + "auto.createRule": "Regel aanmaken", + "auto.createSchedule": "Planning aanmaken", + "auto.days": "Dagen", + "auto.delete": "Verwijderen", + "auto.deleteRule": "Regel verwijderen", + "auto.deleteRuleDesc": "De regel wordt niet meer geactiveerd. Het webhook-endpoint blijft onaangeroerd en je kunt de regel opnieuw aanmaken.", + "auto.deleteRuleTitle": "Deze meldingsregel verwijderen?", + "auto.deleteSchedule": "Planning verwijderen", + "auto.deleteScheduleDesc": "De planning wordt niet meer uitgevoerd. Wat ze al gestart heeft, blijft ongemoeid.", + "auto.deleteScheduleTitle": "Deze planning verwijderen?", + "auto.delivery": "Levering", + "auto.destinationFallingBehindRealtime": "Bestemming raakt achter op realtime", + "auto.destinationKeepingUpAgain": "Bestemming loopt weer bij", + "auto.destinationRecovered": "Bestemming hersteld", + "auto.destinationWentDown": "Bestemming viel uit", + "auto.destinations": "Bestemmingen", + "auto.dropped": "Verworpen", + "auto.edit": "Bewerken", + "auto.editRule": "Meldingsregel bewerken", + "auto.editSchedule": "Planning bewerken", + "auto.endpoint": "Endpoint", + "auto.evAudioClipping": "Audio clipt", + "auto.evClipCaptured": "Clip vastgelegd", + "auto.evDiskLow": "Schijf raakt vol", + "auto.evDiskRecovered": "Schijf hersteld", + "auto.evIngestLost": "Ingang verloren", + "auto.evIngestRecovered": "Ingang hersteld", + "auto.evSettingsChanged": "Instellingen gewijzigd", + "auto.evSignedIn": "Aangemeld", + "auto.events": "Gebeurtenissen", + "auto.everyDay": "Elke dag", + "auto.failed": "Mislukt", + "auto.failoverSwitchedSource": "Failover wisselde van bron", + "auto.fired": "geactiveerd", + "auto.firesAt": "Wordt geactiveerd om", + "auto.lastDelivery": "Laatste levering", + "auto.lastSweep": "Laatste ronde", + "auto.latenessAllowance": "Toegestane vertraging (seconden)", + "auto.loudnessBackInCompliance": "Loudness weer binnen de norm", + "auto.loudnessOutOfCompliance": "Loudness buiten de norm", + "auto.minGap": "Minimale tussenpoos tussen berichten (seconden)", + "auto.minSeverity": "Minimale ernst", + "auto.missedNote": "Een uitvoering die is gemist omdat de server uit stond wordt als afgehandeld gemarkeerd en overgeslagen in plaats van te laat geactiveerd — een stream die zichzelf vier uur te laat start is erger dan een die nooit is gestart.", + "auto.name": "Naam", + "auto.newRule": "Nieuwe meldingsregel", + "auto.newSchedule": "Nieuwe planning", + "auto.next": "Volgende", + "auto.nothingFired": "Er is nog niets geactiveerd. De planner doet elke 20 seconden een ronde.", + "auto.once": "Eenmalig", + "auto.payloadFormat": "Payload-formaat", + "auto.pending": "In afwachting", + "auto.repeatedFailedSignIns": "Herhaalde mislukte aanmeldingen", + "auto.repeats": "Herhaling", + "auto.retries": "Nieuwe pogingen", + "auto.rule": "Regel", + "auto.ruleCreated": "Meldingsregel aangemaakt. Stuur er een test naartoe.", + "auto.ruleDeleted": "Meldingsregel verwijderd.", + "auto.rulePlaceholder": "Discord — showmeldingen", + "auto.ruleSaved": "Meldingsregel opgeslagen.", + "auto.saveRule": "Regel opslaan", + "auto.saveSchedule": "Planning opslaan", + "auto.schedule": "Planning", + "auto.scheduleCreated": "Planning aangemaakt.", + "auto.scheduleDeleted": "Planning verwijderd.", + "auto.schedulePlaceholder": "Doordeweekse show", + "auto.scheduleSaved": "Planning opgeslagen.", + "auto.schedules": "Planningen", + "auto.secretMasked": "Gemaskeerd getoond omdat het geheim in het pad staat. Laat het staan om het huidige endpoint te behouden, of plak een nieuwe URL om het te vervangen.", + "auto.sendTest": "Testmelding sturen", + "auto.sendTestTitle": "Nu een testmelding sturen", + "auto.sent": "Verstuurd", + "auto.skipped": "overgeslagen", + "auto.startDestinations": "Bestemmingen starten", + "auto.startPlaylist": "De afspeellijst starten", + "auto.stopDestinations": "Bestemmingen stoppen", + "auto.stopPlaylist": "De afspeellijst stoppen", + "auto.subscribedTo": "Geabonneerd op", + "auto.subtitle": "Webhooks die je vertellen dat er iets stuk is, en planningen die de show zonder jou draaien.", + "auto.theEndpointDidNotAccept": "Het endpoint accepteerde het testbericht niet.", + "auto.time": "Tijd", + "auto.timeZone": "Tijdzone", + "auto.title": "Automatisering", + "auto.tzPlaceholder": "UTC", + "auto.webhookHint": "Zowel Discord als Slack geeft je er een bij de kanaalinstellingen.", + "auto.webhookPlaceholder": "https://discord.com/api/webhooks/…", + "auto.webhookRules": "Webhook-regels", + "auto.webhookUrl": "Webhook-URL", + "auto.weekFri": "Vr", + "auto.weekMon": "Ma", + "auto.weekSat": "Za", + "auto.weekSun": "Zo", + "auto.weekThu": "Do", + "auto.weekTue": "Di", + "auto.weekWed": "Wo", + "auto.when": "Wanneer", + "chatpage.allFiltered": "Elk platform met berichten is uitgefilterd.", + "chatpage.couldNotDeleteThatMessage": "Kon dat bericht niet verwijderen.", + "chatpage.deleteNote": "Verwijderen haalt het bericht weg op het platform dat het uitgaf. Een platform waarop polyemesis niet kan verwijderen zegt dat en noemt waar het wel kan — de actie wordt nooit op een gok verborgen.", + "chatpage.droppedTitle": "Live getoond, maar het opslaan raakte achter en de geschiedenis is ze kwijt.", + "chatpage.dupTitle": "Hetzelfde bericht twee keer gezien — onze eigen echo, of het platform dat het opnieuw afspeelt.", + "chatpage.noAccount": "Chat draait, maar er is geen platformaccount gekoppeld. Koppel er een onder Instellingen → Platformgegevens.", + "chatpage.notRunning": "Op deze server draait geen chat. De geschiedenis onder de tijdlijn is wat eerder is opgeslagen, en het verzendvak blijft uitgeschakeld tot er een platform is gekoppeld.", + "chatpage.received": "Ontvangen", + "chatpage.reload": "Opnieuw laden", + "chatpage.stored": "Opgeslagen", + "chatpage.storedTitle": "Er is niets gekoppeld; dit is de opgeslagen geschiedenis.", + "chatpage.subtitle": "Elk platform in één tijdlijn, met één verzendvak.", + "chatpage.timeline": "Tijdlijn", + "chatpage.title": "Chat", "chrome.ingest": "Ingest", + "chrome.language": "Taal", "chrome.liveCount": "{count} live", + "chrome.signOut": "Afmelden", "chrome.socketOffline": "offline", "chrome.socketOfflineHint": "Live updates zijn verbroken; opnieuw proberen", - "chrome.signOut": "Afmelden", - "chrome.language": "Taal", + "chrome.toggleNav": "Navigatie in- en uitschakelen", + "clipedit.back33": "33 ms terug", + "clipedit.clickLine": "Klik op een regel om daarheen te springen. Selecteer er meerdere en de in- en out-punten verspringen naar de woorden die je hebt gemarkeerd.", + "clipedit.clipThisLine": "Deze regel clippen", + "clipedit.clipThisLineTitle": "Zet de in- en out-punten rond deze regel", + "clipedit.copied": "Gekopieerd", + "clipedit.copiesEveryPacketSecondsBit": "Kopieert elk pakket. Seconden, bit-exact, en het begin springt terug naar het dichtstbijzijnde keyframe.", + "clipedit.couldNotOpenThatRecording": "Kon die opname niet openen.", + "clipedit.couldNotQueueTheExport": "Kon de export niet in de wachtrij zetten.", + "clipedit.cut": "Knip", + "clipedit.downloadClip": "De clip downloaden", + "clipedit.exportQueued": "Export in de wachtrij. Hij draait wanneer de machine de cycli kan missen.", + "clipedit.exports": "Exports", + "clipedit.fast": "Snel", + "clipedit.filterTranscript": "De transcriptie filteren", + "clipedit.findSaid": "Vind wat er gezegd is", + "clipedit.forward33": "33 ms vooruit", + "clipedit.in": "In", + "clipedit.keepAllTracks": "Elke audiotrack behouden", + "clipedit.keepsEveryAudioTrackIn": "Behoudt elke audiotrack in elke codec. Wat de recorder wegschrijft.", + "clipedit.markIn": "I — in-punt zetten op de afspeelkop", + "clipedit.markOut": "O — out-punt zetten op de afspeelkop", + "clipedit.name": "Naam", + "clipedit.noKeyframes": "De keyframeposities konden niet worden gelezen, dus niemand kan zeggen waar deze knip echt begint. Hij werkt nog steeds — de seek van FFmpeg zelf landt op een keyframe — maar het begin kan tot een GOP te vroeg liggen en deze pagina kan je niet laten zien hoeveel.", + "clipedit.noTranscript": "Voor deze opname is nog niets getranscribeerd. Elke microfoon is op een eigen track opgenomen, dus transcriberen levert je sprekersnamen op zonder diarisatiemodel — en daarna kun je clippen op wat er gezegd is.", + "clipedit.nothingExported": "Nog niets geëxporteerd.", + "clipedit.out": "Out", + "clipedit.pickAtLeastOneAudio": "Kies minstens één audiotrack, of neem ze allemaal.", + "clipedit.playFaster": "L — afspelen, nogmaals voor sneller", + "clipedit.precise": "Precies", + "clipedit.queueNote": "De export staat in de wachtrij en draait niet hier: hij wijkt voor de live stream, zodat een knip die midden in een uitzending wordt genomen wacht in plaats van de cycli te stelen die de stream nodig heeft.", + "clipedit.reEncodesOnlyTheLeading": "Hercodeert alleen de eerste deel-GOP zodat het begin exact is. De rest wordt nog steeds gekopieerd.", + "clipedit.reencoded": "Hergecodeerd", + "clipedit.shortcuts": "J / K / L voor shuttle, I en O zetten in en out, pijltjes verschuiven 33 ms en shift-pijltjes een seconde. De fijne stap is één frame bij 30 fps; de echte beeldsnelheid van deze opname is niet geïndexeerd, dus hij heet in milliseconden in plaats van dat ernaar geraden wordt.", + "clipedit.shuttleBack": "J — shuttle achteruit", + "clipedit.startMoved": "Start verplaatst", + "clipedit.starts": "Begint", + "clipedit.stop": "K — stoppen", + "clipedit.thatCutCouldNotBe": "Die knip kon niet worden gepland.", + "clipedit.thatIsNotARecording": "Dat is geen opname-id.", + "clipedit.thatRecordingIsNotAvailable": "Die opname is niet beschikbaar.", + "clipedit.title": "Clipeditor", + "clipedit.whatASocialPlatformWill": "Wat een sociaal platform accepteert. Minder codecs overleven het.", + "clipedit.zoomIn": "Inzoomen", + "clipedit.zoomOut": "Uitzoomen", + "clips.bitrate": "Bitrate", + "clips.buffer": "Buffer", + "clips.bufferLiveNote": "De bufferschakelaar is een live schakelaar, geen opgeslagen instelling: na een herstart staat hij weer uit.", + "clips.captured": "Vastgelegd", + "clips.ceiling": "Plafond", + "clips.clipsKept": "Bewaarde clips", + "clips.couldNotCaptureAClip": "Kon geen clip vastleggen.", + "clips.couldNotChangeTheClip": "Kon de clipbuffer niet wijzigen.", + "clips.couldNotDeleteTheClip": "Kon de clip niet verwijderen.", + "clips.couldNotLoadClips": "Kon de clips niet laden.", + "clips.covers": "Beslaat", + "clips.customLength": "Eigen cliplengte in seconden", + "clips.delete": "Verwijderen", + "clips.deleteDescription": "Een clip is meestal precies het materiaal dat je wilde bewaren. Dit verwijdert het bestand; de opname waaruit het komt blijft ongemoeid.", + "clips.deleteTitle": "Deze clip verwijderen?", + "clips.deleted": "Clip verwijderd.", + "clips.download": "Downloaden", + "clips.everythingHeld": "Alles wat de buffer vasthoudt", + "clips.file": "Bestand", + "clips.held": "Vastgehouden", + "clips.inMemory": "In geheugen", + "clips.keepBuffer": "Een rollende buffer aanhouden", + "clips.keyframes": "Keyframes", + "clips.length": "Duur", + "clips.none": "geen", + "clips.off": "uit", + "clips.onDisk": "Op schijf", + "clips.seen": "gezien", + "clips.settings": "Instellingen", + "clips.size": "Grootte", + "clips.subtitle": "Een rollende buffer van wat er net is uitgegaan. Leg het achteraf vast — er hoeft niets vooraf klaargezet te worden.", + "clips.theClipDoesNotStart": "De clip begint niet op een keyframe.", + "clips.title": "Clips", + "clips.waitingForStream": "wachten op stream", + "clips.window": "Venster", + "clips.windowSeconds": "Venster (seconden)", "common.add": "Toevoegen", + "common.apply": "Toepassen", "common.cancel": "Annuleren", "common.close": "Sluiten", "common.confirm": "Bevestigen", @@ -28,110 +229,921 @@ "common.copy": "Kopieer", "common.delete": "Verwijderen", "common.disabled": "Uitgeschakeld", + "common.discard": "Verwerpen", "common.edit": "Bewerken", "common.enabled": "Ingeschakeld", "common.loading": "Wordt geladen…", + "common.moreInfo": "Meer informatie", "common.refresh": "Vernieuwen", "common.restart": "Herstarten", "common.retry": "Opnieuw proberen", "common.save": "Opslaan", "common.start": "Start", "common.stop": "Stoppen", - "state.live": "Live", - "state.reconnecting": "Opnieuw verbinden", - "state.starting": "Starten", - "state.failed": "Mislukt", - "state.stopped": "Gestopt", - "state.offline": "Niet online", - "hooks.title": "Lifecycle-webhooks", - "hooks.new": "Nieuwe webhook", - "hooks.empty": "Geen webhooks. Voeg er een toe en polyemesis POST een ondertekende JSON-body zodra de stream start of stopt, of een bestemming online of offline gaat — één levering per overgang, op volgorde, zodat een script erop kan reageren.", - "hooks.colWebhook": "Webhook", - "hooks.colEndpoint": "Endpoint", - "hooks.colTriggers": "Geabonneerd op", - "hooks.colEnabled": "Aan", - "hooks.signed": "ondertekend", - "hooks.unsigned": "NIET ONDERTEKEND", + "dash.audioTracks": "Audiotracks", + "dash.bitrate": "Bitrate", + "dash.broadcastSettings": "Uitzendinstellingen", + "dash.categoryPlaceholder": "Muziek", + "dash.clipboardUnavailable": "Het klembord is niet beschikbaar op deze origin.", + "dash.connectPrompt": "Verbind een YouTube-, Twitch-, Facebook- of Kick-account in Instellingen → Platformgegevens om je streamtitel, -beschrijving en -categorie op elk platform tegelijk in te stellen. Elk platform accepteert een andere deelverzameling — de composer benoemt welke, en stuurt wat hij kan.", + "dash.copyIngestUrl": "Ingangs-URL kopiëren", + "dash.couldNotLoadTheDestination": "Kon de bestemming niet laden.", + "dash.couldNotPushTheMetadata": "Kon de metadata niet versturen.", + "dash.couldNotReorderTheDestinations": "Kon de bestemmingen niet herordenen.", + "dash.deleteDest": "Bestemming verwijderen", + "dash.deleteDestDescription": "Dit verwijdert de bestemming en haar routingprofiel. Het platform blijft ongemoeid en je kunt haar weer toevoegen — maar de audiorouting die je ervoor had opgezet is weg.", + "dash.descriptionPlaceholder": "Waar deze stream over gaat.", + "dash.destDeleted": "Bestemming verwijderd.", + "dash.disabled": "uitgeschakeld", + "dash.everyConnectedAccount": "elk verbonden account", + "dash.idle": "inactief", + "dash.ingestUrlCopied": "Ingangs-URL gekopieerd.", + "dash.keyRefreshed": "Streamsleutel vernieuwd vanaf het platform.", + "dash.leaveUnchanged": "Ongewijzigd laten", + "dash.leaveUnchangedNote": "“Ongewijzigd laten” stuurt helemaal niets, dus het platform houdt wat het heeft. Aan of Uit kiezen schrijft die waarde.", + "dash.metaCategory": "Categorie", + "dash.metaDescription": "Beschrijving", + "dash.metaTags": "Tags", + "dash.metaTitle": "Titel", + "dash.noDestinations": "Nog geen bestemmingen. Voeg er een toe en kies daarna welke audiotracks hij ontvangt.", + "dash.off": "Uit", + "dash.pipeline": "Verwerkingsketen", + "dash.pushToPlatforms": "Naar platforms sturen", + "dash.pushing": "Versturen…", + "dash.reconnects": "Herverbindingen", + "dash.relaySubscribers": "Relay-abonnees", + "dash.renditions": "Varianten", + "dash.scheduledStart": "Geplande start", + "dash.subtitle": "Eén keer ingesten, uitwaaieren met gedeelde video en audio per bestemming.", + "dash.tagsPlaceholder": "live, house, dj set", + "dash.theConnectedAccount": "het verbonden account", + "dash.title": "Dashboard", + "dash.titlePlaceholder": "Vrijdagavondset", + "dash.uptime": "Actieve tijd", + "dash.youtubeStopsAcceptingTheseOnce": "YouTube accepteert deze niet meer zodra een uitzending “created” of “ready” verlaat.", + "dest.audience": "Doelgroep", + "dest.audioBitrate": "Audiobitrate", + "dest.audioCodec": "Audiocodec", + "dest.channels": "Kanalen", + "dest.compliance": "Compliance", + "dest.connectedAccount": "Gekoppeld account", + "dest.contentLabels": "Inhoudslabels", + "dest.copySource": "De bronvideo kopiëren", + "dest.createVariant": "Deze variant maken", + "dest.customiseSummary": "Aanpassen voor deze bestemming", + "dest.fbBackupCost": "Verdubbelt de uploadbandbreedte van deze bestemming.", + "dest.fbBackupHelp": "Stuurt een tweede kopie van deze stream naar Facebooks reserve-ingest, zodat een weggevallen verbinding de uitzending niet meesleurt.", + "dest.fbBackupLabel": "Een reservefeed publiceren", + "dest.fbBackupReconnect": "Inschakelen verbindt de stream één keer opnieuw — schakel het dus in voordat je live gaat.", + "dest.fbBoxIntro": "Wordt in dezelfde aanmaak-aanroep verstuurd als Publiek, dus dezelfde regel geldt: eenmalig toegepast wanneer de uitzending start, en volledig met rust gelaten als het leeg is.", + "dest.fbBoxTitle": "Cross-posten en donatieknop van Facebook", + "dest.fbCrosspostAdd": "Pagina toevoegen", + "dest.fbCrosspostAlsoPost": "Ook plaatsen", + "dest.fbCrosspostHelp": "De numerieke Pagina-ID uit Facebooks eigen console (Pagina-instellingen, of graph.facebook.com/me/accounts terwijl je als de Pagina bent ingelogd) — er is hier geen zoekfunctie, plak dus de id in plaats van een naam of URL. ‘Ook plaatsen’ publiceert als die Pagina in plaats van de uitzending er alleen mee te delen; uitgeschakeld gebeurt alleen het stillere delen.", + "dest.fbCrosspostLabel": "Cross-posten naar Pagina’s", + "dest.fbCrosspostPageId": "Pagina-ID", + "dest.fbCrosspostRemove": "Deze Pagina verwijderen", + "dest.fbDonateHelp": "Ook een ondoorzichtige id uit Facebooks console voor inzamelingsacties, geen naam van een goed doel om op te zoeken. Laat leeg om helemaal geen donatieknop toe te voegen.", + "dest.fbDonateLabel": "Goede doel-ID voor de donatieknop", + "dest.fbDonatePlaceholder": "Goede doel-ID", + "dest.fillFromPlatform": "Vullen met wat dit platform publiceert", + "dest.friends": "Vrienden", + "dest.friendsOfFriends": "Vrienden van vrienden", + "dest.giveUpAfter": "Opgeven na (pogingen)", + "dest.leaveOnFacebook": "Laten zoals het op Facebook staat", + "dest.leaveOnYouTube": "Laten zoals het op YouTube staat", + "dest.madeForKids": "Gemaakt voor kinderen (COPPA)", + "dest.muxQueue": "Muxwachtrij (pakketten)", + "dest.muxQueuePair": "Deze twee horen bij elkaar: FFmpeg past de pakketlimiet pas toe zodra de wachtrij voorbij de bytedrempel is gegroeid, dus een drempel alleen doet niets en wordt geweigerd. Verhoog ze als een bestemming interleave-fouten meldt — het audiopad heeft hier een variabele latentie, omdat de luidheidsnormalisatie vooruitleest.", + "dest.name": "Naam", + "dest.namePlaceholder": "YouTube — hoofdkanaal", + "dest.notLinked": "Niet gekoppeld", + "dest.onlyMe": "Alleen ik", + "dest.platform": "Platform", + "dest.private": "Privé", + "dest.public": "Openbaar", + "dest.reconnecting": "Opnieuw verbinden", + "dest.retryAfter": "Opnieuw proberen na (seconden)", + "dest.sharedEncodeAria": "Gedeelde video-encode", + "dest.socketTimeout": "Socket-time-out (seconden)", + "dest.streamKeyLabel": "Streamsleutel", + "dest.transport": "Transport", + "dest.transportFile": "Lokaal bestand", + "dest.transportRtmp": "RTMP / RTMPS", + "dest.unlisted": "Verborgen", + "dest.urlFromDashboard": "URL uit het dashboard", + "dest.useSharedEncode": "Een gedeelde video-encode gebruiken", + "dest.varBitrate": "Videobitrate (kbps)", + "dest.varFps": "Framerate", + "dest.varHeight": "Hoogte", + "dest.varSourceHint": "bron", + "dest.varWidth": "Breedte", + "dest.variantWarning": "Start een tweede encode vanuit je bron, met „{name}” als uitgangspunt. Dit is geen gratis variant en verandert niets aan wat de andere bestemmingen op die encode ontvangen. Laat een veld leeg om de waarde van de bron te behouden.", + "dest.videoTreatment": "Videobewerking", + "dest.visibility": "Zichtbaarheid", + "hooks.allTriggers": "alles", "hooks.attempts": "{count} pogingen", + "hooks.attemptsLabel": "Pogingen", "hooks.attemptsOne": "{count} poging", - "hooks.timeout": "{seconds}s time-out", - "hooks.allTriggers": "alles", - "hooks.triggerCount": "{count} triggers", - "hooks.triggerCountOne": "{count} trigger", - "hooks.enableAria": "{name} inschakelen", + "hooks.colEnabled": "Aan", + "hooks.colEndpoint": "Endpoint", + "hooks.colTriggers": "Geabonneerd op", + "hooks.colWebhook": "Webhook", + "hooks.create": "Aanmaken", + "hooks.created": "Webhook aangemaakt.", + "hooks.deleteBody": "Hij levert onmiddellijk niets meer. Het endpoint blijft onaangeroerd en je kunt de hook opnieuw aanmaken — maar zijn ondertekeningssleutel is niet te herstellen.", + "hooks.deleteConfirm": "Webhook verwijderen", + "hooks.deleteFailed": "Kon de hook niet verwijderen.", + "hooks.deleteTitle": "Deze webhook verwijderen?", + "hooks.deleted": "Webhook verwijderd.", "hooks.deliveries": "Recente leveringen", - "hooks.sendTest": "Testlevering sturen", - "hooks.sendTestTitle": "Nu een testlevering sturen", - "hooks.secretTitle": "Ondertekeningssleutel — kopieer hem nu, hij kan niet opnieuw worden getoond", - "hooks.secretCopied": "Ik heb hem gekopieerd", - "hooks.httpStatus": "HTTP {status}", - "hooks.noResponse": "geen antwoord", + "hooks.deliveriesFailed": "Kon de recente leveringen niet lezen.", + "hooks.deliveryTitle": "Levering", + "hooks.dialogIntro": "Eén ondertekende POST per overgang, op volgorde. Anders dan bij een waarschuwing wordt niets samengevoegd of ontdenderd — een script krijgt elke flank.", "hooks.durationMs": "{ms} ms", + "hooks.editTitle": "Webhook bewerken", + "hooks.empty": "Geen webhooks. Voeg er een toe en polyemesis POST een ondertekende JSON-body zodra de stream start of stopt, of een bestemming online of offline gaat — één levering per overgang, op volgorde, zodat een script erop kan reageren.", + "hooks.enableAria": "{name} inschakelen", + "hooks.httpStatus": "HTTP {status}", + "hooks.loadFailed": "Kon de webhooks niet laden.", + "hooks.name": "Naam", + "hooks.new": "Nieuwe webhook", + "hooks.newTitle": "Nieuwe webhook", "hooks.noDeliveries": "Nog niets geleverd. Leveringen verschijnen hier zodra er overgangen plaatsvinden.", - "hooks.deliveryTitle": "Levering", - "hooks.statSent": "Verstuurd", - "hooks.statFailed": "Mislukt", + "hooks.noResponse": "geen antwoord", + "hooks.payloadNote": "Payload {version}. Handtekening in {header}, over het tijdstempel en de body.", + "hooks.retriesHint": "Nieuwe pogingen blokkeren de eigen wachtrij van dit endpoint, want de volgorde is de belofte. Een 4xx wordt nooit opnieuw geprobeerd — een endpoint dat zegt dat het verzoek fout is, zegt dat opnieuw.", + "hooks.saveFailed": "Kon de hook niet opslaan.", + "hooks.secretCopied": "Ik heb hem gekopieerd", + "hooks.secretTitle": "Ondertekeningssleutel — kopieer hem nu, hij kan niet opnieuw worden getoond", + "hooks.sendTest": "Testlevering sturen", + "hooks.sendTestTitle": "Nu een testlevering sturen", + "hooks.signed": "ondertekend", + "hooks.statDropped": "Verworpen", "hooks.statEndpoints": "Endpoints", + "hooks.statFailed": "Mislukt", "hooks.statQueued": "In wachtrij", - "hooks.statDropped": "Verworpen", "hooks.statRetries": "Nieuwe pogingen", - "hooks.payloadNote": "Payload {version}. Handtekening in {header}, over het tijdstempel en de body.", - "hooks.deleteTitle": "Deze webhook verwijderen?", - "hooks.deleteBody": "Hij levert onmiddellijk niets meer. Het endpoint blijft onaangeroerd en je kunt de hook opnieuw aanmaken — maar zijn ondertekeningssleutel is niet te herstellen.", - "hooks.deleteConfirm": "Webhook verwijderen", - "hooks.editTitle": "Webhook bewerken", - "hooks.newTitle": "Nieuwe webhook", - "hooks.dialogIntro": "Eén ondertekende POST per overgang, op volgorde. Anders dan bij een waarschuwing wordt niets samengevoegd of ontdenderd — een script krijgt elke flank.", - "hooks.name": "Naam", - "hooks.url": "Endpoint-URL", - "hooks.urlMasked": "De opgeslagen URL wordt nooit volledig getoond. Laat de gemaskeerde waarde staan om hem te behouden, of typ een nieuwe URL om hem te vervangen.", - "hooks.triggers": "Triggers", - "hooks.triggersNoneHint": "Niets geselecteerd betekent elke trigger, ook de triggers die een toekomstige versie toevoegt.", + "hooks.statSent": "Verstuurd", + "hooks.testFailed": "Het endpoint heeft de testlevering niet geaccepteerd.", + "hooks.timeout": "{seconds}s time-out", "hooks.timeoutLabel": "Time-out (s)", - "hooks.attemptsLabel": "Pogingen", - "hooks.retriesHint": "Nieuwe pogingen blokkeren de eigen wachtrij van dit endpoint, want de volgorde is de belofte. Een 4xx wordt nooit opnieuw geprobeerd — een endpoint dat zegt dat het verzoek fout is, zegt dat opnieuw.", - "hooks.create": "Aanmaken", - "hooks.triggerIngestPublished": "Stream gestart", - "hooks.triggerIngestDisconnected": "Stream gestopt", - "hooks.triggerDestinationUp": "Bestemming live", - "hooks.triggerDestinationDown": "Bestemming gestopt", - "hooks.loadFailed": "Kon de webhooks niet laden.", + "hooks.title": "Lifecycle-webhooks", "hooks.toggleFailed": "Kon de hook niet wijzigen.", - "hooks.deleted": "Webhook verwijderd.", - "hooks.deleteFailed": "Kon de hook niet verwijderen.", - "hooks.testFailed": "Het endpoint heeft de testlevering niet geaccepteerd.", - "hooks.deliveriesFailed": "Kon de recente leveringen niet lezen.", + "hooks.triggerCount": "{count} triggers", + "hooks.triggerCountOne": "{count} trigger", + "hooks.triggerDestinationDown": "Bestemming gestopt", + "hooks.triggerDestinationUp": "Bestemming live", + "hooks.triggerIngestDisconnected": "Stream gestopt", + "hooks.triggerIngestPublished": "Stream gestart", + "hooks.triggers": "Triggers", + "hooks.triggersNoneHint": "Niets geselecteerd betekent elke trigger, ook de triggers die een toekomstige versie toevoegt.", + "hooks.unsigned": "NIET ONDERTEKEND", "hooks.updated": "Webhook bijgewerkt.", - "hooks.created": "Webhook aangemaakt.", - "hooks.saveFailed": "Kon de hook niet opslaan.", - "playlist.title": "Afspeellijst", + "hooks.url": "Endpoint-URL", + "hooks.urlMasked": "De opgeslagen URL wordt nooit volledig getoond. Laat de gemaskeerde waarde staan om hem te behouden, of typ een nieuwe URL om hem te vervangen.", + "jobs.alwaysKeepNewest": "Nieuwste altijd bewaren", + "jobs.automaticModel": "Automatisch (gekozen op basis van deze hardware)", + "jobs.backToDefault": "Terug naar de standaardmodus voor dit soort", + "jobs.batteryFloor": "Batterij-ondergrens %", + "jobs.broadcastGoing": "Er gaat een uitzending uit; uitgesteld werk wijkt daarvoor.", + "jobs.cancel": "Annuleren", + "jobs.cannotPause": "Deze soorten zouden gepauzeerd moeten worden maar dat kan niet, dus maken ze zich af.", + "jobs.completed": "Voltooid", + "jobs.concurrency": "Gelijktijdigheid", + "jobs.couldNotReadTheJob": "Kon de takenwachtrij niet lezen.", + "jobs.couldNotSaveThePolicy": "Kon het beleid niet opslaan.", + "jobs.cpuCeiling": "CPU-plafond %", + "jobs.cpuResume": "CPU-hervatting %", + "jobs.defaultMode": "Standaardmodus", + "jobs.defaultModel": "Standaardmodel", + "jobs.deferred": "Uitgesteld", + "jobs.failed": "Mislukt", + "jobs.finished": "Afgerond", + "jobs.global": "Globaal", + "jobs.gpuGateNote": "Alleen soorten die als GPU-gebruiker gemarkeerd zijn worden hierdoor beperkt.", + "jobs.history": "Geschiedenis", + "jobs.historyRetention": "Bewaartermijn geschiedenis", + "jobs.holdDeferredWorkBackOn": "Houd uitgesteld werk tegen op een ontladende laptop onder deze waarde. 0 schakelt het uit.", + "jobs.hostCpuLoad": "Host-CPU-belasting", + "jobs.howManyJobsMayRun": "Hoeveel taken tegelijk mogen draaien. Eén, want een tweede transcodering levert doorvoer op waar niemand om vroeg, tegen een risico dat niemand accepteerde.", + "jobs.inQueue": "In de wachtrij", + "jobs.job": "Taak", + "jobs.keepForDays": "Bewaren gedurende dagen", + "jobs.machineSafety": "Machineveiligheid. Dit is de enige grens waaraan ook realtime werk zich houdt.", + "jobs.niceLevel": "Nice-niveau", + "jobs.noFinished": "Nog geen afgeronde taken.", + "jobs.noModels": "Er zijn geen modelbestanden gevonden in de modellenmap. Automatisch downloadt het model dat het kiest; hier een specifiek model noemen werkt pas zodra dat bestand bestaat.", + "jobs.nothingNewStartsAboveThis": "Boven deze waarde start er niets nieuws. 0 schakelt de poort uit.", + "jobs.nothingQueued": "Niets in de wachtrij. Postproductie wordt vanuit de bibliotheek ingediend.", + "jobs.nothingWasOldEnoughTo": "Niets was oud genoeg om op te ruimen. Verlaag eerst de bewaartermijn bij Beleid.", + "jobs.pauseAll": "Alles pauzeren", + "jobs.policy": "Beleid", + "jobs.policySaved": "Resourcebeleid opgeslagen.", + "jobs.progress": "Voortgang", + "jobs.queuePaused": "Wachtrij gepauzeerd.", + "jobs.queueResumed": "Wachtrij hervat.", + "jobs.queued": "In wachtrij", + "jobs.recording": "Opname", + "jobs.releasedItWillStartAs": "Vrijgegeven. Hij start zodra er een plek vrij is.", + "jobs.removeFromHistory": "Uit de geschiedenis verwijderen", + "jobs.removeWindow": "Venster verwijderen", + "jobs.resumeAll": "Alles hervatten", + "jobs.retry": "Opnieuw proberen", + "jobs.retryTitle": "Opnieuw scherpstellen met een vers aantal pogingen", + "jobs.runNow": "Nu uitvoeren", + "jobs.runNowTitle": "Voer deze nu uit en negeer het resourcebeleid", + "jobs.running": "Actief", + "jobs.savedTheConcurrencyChangeTakes": "Opgeslagen. De wijziging in gelijktijdigheid gaat in wanneer de server herstart — de wachtrij lost dat op bij het opstarten.", + "jobs.state": "Status", + "jobs.stopsEverythingRealtimeIncludedA": "Stopt alles, realtime inbegrepen: een CPU die terugschakelt is de stream al aan het verslechteren. 0 schakelt het uit.", + "jobs.subtitle": "Achtergrondwerk: transcriptie, proxy's, miniaturen en archiefcompressie.", + "jobs.thatDidNotWork": "Dat werkte niet.", + "jobs.thermalCeiling": "Thermisch plafond °C", + "jobs.thisManyFinishedJobsSurvive": "Zoveel afgeronde taken blijven bewaard, ongeacht hun leeftijd.", + "jobs.title": "Taken", + "jobs.transcription": "Transcriptie", + "jobs.unsavedChanges": "Niet-opgeslagen wijzigingen.", + "jobs.whereTheCeilingReleasesThe": "Waar het plafond loslaat. Het gat is de hysterese die de poort niet laat oscilleren.", + "jobs.whisperMissing": "whisper.cpp is hier niet geïnstalleerd, dus dit heeft nog geen effect. Het is toch zinvol om in te stellen: de keuze geldt zodra dat wel zo is.", + "jobs.why": "Waarom", + "jobs.windowEnd": "Einde venster", + "jobs.windowStart": "Begin venster", + "jobs.yieldNote": "Zwaar werk wordt in de wachtrij gezet en wijkt voor de live stream. Een verloren frame is onherstelbaar; een transcriptie die een uur later komt kost niets.", + "lib.anySpeaker": "Elke spreker", + "lib.anyTag": "Elke tag", + "lib.autoGrouped": "Automatisch gegroepeerd. Als je het bewerkt wordt het van jou.", + "lib.autoGroupedNote": "Deze sessie is automatisch gegroepeerd. Opslaan maakt haar van jou, en de groepeerronde zal haar niet meer herschrijven.", + "lib.bestMatch": "Beste overeenkomst", + "lib.clearSearch": "Zoekopdracht wissen", + "lib.clip": "Clippen", + "lib.clipTitle": "Knip een clip uit deze opname", + "lib.collapseSession": "Sessie inklappen", + "lib.couldNotLoadTheLibrary": "Kon de bibliotheek niet laden.", + "lib.couldNotLoadTheSession": "Kon de segmenten van de sessie niet laden.", + "lib.couldNotQueueTheProxy": "Kon de proxy niet in de wachtrij zetten.", + "lib.couldNotRegroup": "Kon niet opnieuw groeperen.", + "lib.couldNotSaveTheSession": "Kon de sessie niet opslaan.", + "lib.delete": "Verwijderen", + "lib.deleteDescription": "Het bestand wordt van schijf verwijderd, samen met zijn proxy's, miniaturen en transcriptie. Dit kan niet ongedaan worden gemaakt.", + "lib.deleteTitle": "Deze opname verwijderen?", + "lib.downloadMaster": "Master downloaden", + "lib.downloadMasterTitle": "Download de meersporige master", + "lib.editSession": "Sessie bewerken", + "lib.expandSession": "Sessie uitklappen", + "lib.from": "Van", + "lib.generateProxy": "Proxy genereren", + "lib.generateProxyTitle": "Genereer de in de browser afspeelbare proxy", + "lib.mostRecent": "Meest recent", + "lib.noProxyYet": "Nog geen proxy — open om er een te genereren", + "lib.noSegments": "Hier geen segmenten.", + "lib.noSessionMatch": "Geen sessie voldoet aan die filters.", + "lib.nothingRecorded": "Nog niets opgenomen. Sessies verschijnen zodra de recorder segmenten wegschrijft.", + "lib.oldestFirst": "Oudste eerst", + "lib.playProxy": "De proxy afspelen", + "lib.proxyAlreadyQueued": "Er staat al een proxy in de wachtrij.", + "lib.proxyQueued": "Proxy in de wachtrij gezet.", + "lib.recordingDeleted": "Opname verwijderd.", + "lib.regroupTitle": "Voert de groepering opnieuw uit. Additief en idempotent: het voegt nooit een groepering samen die je met de hand hebt gesplitst.", + "lib.searchPlaceholder": "Doorzoek alles wat iemand gezegd heeft, in elke opname…", + "lib.searchTranscripts": "Transcripties doorzoeken", + "lib.searchable": "Doorzoekbaar", + "lib.searchableOnly": "Segmenten met een transcriptie, dus doorzoekbaar", + "lib.session": "Sessie", + "lib.sessionDescription": "Beschrijving", + "lib.sessionSaved": "Sessie opgeslagen.", + "lib.sessionTags": "Tags", + "lib.sessionTitle": "Titel", + "lib.subtitle": "Elke uitzending, doorzoekbaar. Elke microfoon is op een eigen track opgenomen, dus de transcriptie weet al wie het zei.", + "lib.tag": "Tag", + "lib.tagsPlaceholder": "komma, gescheiden", + "lib.title": "Bibliotheek", + "lib.transcribe": "Transcriberen", + "lib.transcribeEachTrackOnIts": "Transcribeer elke track apart — dat is wat sprekertoewijzing oplevert", + "lib.ungrouped": "Niet-gegroepeerde segmenten", + "meters.deviation": "Afwijking", + "meters.idle": "inactief", + "meters.integrated": "Geïntegreerd", + "meters.loudness": "Loudness-conformiteit", + "meters.metering": "meten", + "meters.momentary": "Momentaan", + "meters.monitorOff": "De monitor staat uit. Zet hem aan om te meten wat elke bestemming levert.", + "meters.noSignal": "geen signaal op deze track", + "meters.nothingToMeasure": "Nog niets te meten. Elke actieve bestemming krijgt zijn eigen EBU R128-analyser op de mix die hij ontvangt.", + "meters.range": "Bereik", + "meters.shortTerm": "Korte termijn", + "meters.subtitle": "Elk kanaal van elke ingangstrack, rechtstreeks vanaf de relay.", + "meters.title": "Audiometers", + "meters.truePeak": "True peak", + "meters.waitingForStream": "wachten op stream", + "mon.allProcesses": "Alle processen", + "mon.bitrateChart": "Ingangsbitrate — laatste 30 minuten", + "mon.chooseDestination": "Kies een bestemming…", + "mon.clearLog": "Logboek wissen", + "mon.clearedThisDestinationIsBack": "Gewist. Deze bestemming volgt weer het gegenereerde commando.", + "mon.dryRun": "Proefrun", + "mon.dryRunInvalid": "FFmpeg wees een argument af", + "mon.dryRunOk": "FFmpeg accepteerde het", + "mon.extraInput": "Extra ingangsargumenten", + "mon.extraOutput": "Extra uitgangsargumenten", + "mon.ffmpegLog": "FFmpeg-logboek", + "mon.fromRunningProcess": "van het actieve proces", + "mon.fullCommand": "Volledig opgelost commando", + "mon.hostCpu": "Host-CPU", + "mon.ingestLoss": "Ingangsverlies", + "mon.memory": "Geheugen", + "mon.nothingProvenEitherWay": "Niets bewezen, in geen van beide richtingen", + "mon.pid": "PID", + "mon.processes": "Processen", + "mon.rebuilt": "opnieuw opgebouwd", + "mon.relayDrops": "Relay-verlies", + "mon.relayIn": "Relay in", + "mon.resolveHint": "Los het commando op om precies te zien wat er wordt uitgevoerd voordat je toepast.", + "mon.restarts": "Herstarts", + "mon.subtitle": "Procesgezondheid, hostresources en het live FFmpeg-logboek.", + "mon.title": "Monitoring", + "mon.uptime": "Actieve tijd", + "nav.automation": "Automatisering", + "nav.chat": "Chat", + "nav.clips": "Fragmenten", + "nav.dashboard": "Dashboard", + "nav.jobs": "Taken", + "nav.library": "Bibliotheek", + "nav.meters": "Audiometers", + "nav.monitoring": "Monitoring", + "nav.playout": "Playout", + "nav.recordings": "Opnames", + "nav.renditions": "Varianten", + "nav.routing": "Routing", + "nav.settings": "Instellingen", + "nav.sources": "Bronnen", + "play.acrossEveryRungTheBackstop": "Over alle treden samen. De achtervang die voorkomt dat een lange sessie de schijf volzet.", + "play.allowCrossOrigin": "Cross-origin toestaan", + "play.allowCrossOriginPlayback": "Cross-origin afspelen toestaan", + "play.audioBitrate": "Audiobitrate (kbps)", + "play.audioTrack": "Audiotrack", + "play.basicPasswordNote": "Spelers die geen link kunnen volgen (een set-top box, een CDN) kunnen hetzelfde geheim in plaats daarvan als HTTP basic-wachtwoord sturen.", + "play.boundsTheViewerTableViewers": "Begrenst de kijkerstabel. Kijkers daarbuiten worden bediend, alleen niet geteld.", + "play.carriesThePlaybackTokenAnyone": "Bevat het afspeeltoken. Iedereen met deze URL kan kijken.", + "play.clipboardUnavailable": "Het klembord is niet beschikbaar; selecteer en kopieer met de hand", + "play.couldNotLoadPlayout": "Kon playout niet laden", + "play.disabled": "Uitgeschakeld", + "play.diskCap": "Schijfplafond (MB)", + "play.dvrWindow": "DVR-venster (s)", + "play.embedCode": "Insluitcode", + "play.enable": "Playout inschakelen", + "play.enabled": "Ingeschakeld", + "play.forObsACdnOr": "Voor OBS, een CDN of elke speler die HLS spreekt.", + "play.format": "Formaat", + "play.hls": "HLS", + "play.hlsDash": "HLS en DASH", + "play.hlsPlaylist": "HLS-afspeellijst", + "play.idleTimeout": "Time-out bij inactieve kijker (s)", + "play.linkReset": "Nieuwe afspeellink gegenereerd. De vorige werkt niet meer.", + "play.linksProtected": "Deze links werken alleen voor een aangemelde beheerder totdat je Openbaar inschakelt.", + "play.linksPublic": "Deze links werken zonder account.", + "play.liveWindow": "Live venster (segmenten)", + "play.metaDescription": "Beschrijving", + "play.metaTitle": "Titel", + "play.mustExceedOneSegmentOr": "Moet groter zijn dan één segment, anders vallen kijkers tussen twee pollmomenten weg.", + "play.name": "Naam", + "play.noCredentialNote": "Er is geen enkele toegangssleutel vereist. Alleen gepast voor een stream die je door het hele internet wilt laten zien.", + "play.noRungs": "Nog geen treden. Voeg er een toe om iets te publiceren.", + "play.onDisk": "Op schijf", + "play.oneStereoAacTrackPer": "Eén stereo-AAC-track per trede.", + "play.packaging": "Verpakking", + "play.passthrough": "Ingang (passthrough)", + "play.peak": "Piek", + "play.playerPage": "Spelerpagina", + "play.posterNote": "Getoond boven de video op de openbare pagina. De poster is een recent keyframe, dat automatisch wordt vernieuwd.", + "play.private": "Privé.", + "play.protectedNote": "Afspelen is standaard beschermd. Het openstellen is een aparte, bewuste stap.", + "play.protection": "Bescherming", + "play.public": "Openbaar", + "play.resetFailed": "Kon niet resetten", + "play.restartNote": "Als je hier iets van wijzigt herstarten de muxers, wat kijkers een paar seconden onderbreekt.", + "play.rungNote": "Elke trede kopieert de video van haar rendering bit voor bit, dus een trede toevoegen kost een muxer en nooit een tweede videocodering. Alleen de audiotrack verschilt per trede; de routing per bestemming blijft ongemoeid.", + "play.segmentLength": "Segmentlengte (s)", + "play.serveWithoutSession": "Kijkers bedienen zonder sessie", + "play.sessionCap": "Sessieplafond", + "play.sessions": "Sessies", + "play.share": "Delen", + "play.shorterIsLowerLatencyAnd": "Korter betekent minder latentie en meer verloop in de afspeellijst.", + "play.subtitle": "Lever de stream rechtstreeks aan kijkers, als openbare HLS.", + "play.threeIsTheMinimumA": "Drie is het minimum waarop een speler kan bufferen.", + "play.title": "Playout", + "play.unlisted": "Niet vermeld.", + "play.untitled": "Naamloze stream", + "play.videoSource": "Videobron", + "play.watchingNow": "Kijken nu", + "play.whoCanWatch": "Wie mag kijken", + "player.copied": "Gekopieerd", + "player.copy": "Kopiëren", + "player.loading": "Laden…", + "player.missingToken": "Aan de link die je hebt gevolgd ontbreekt het afspeeltoken, of het token is gewijzigd. Vraag degene die hem deelde om een actuele link.", + "player.notLive": "Op dit moment niet live", + "player.viewer": "kijker", + "player.viewers": "kijkers", + "player.willStart": "Deze pagina begint vanzelf af te spelen zodra de uitzending start.", + "playlist.choose": "Kies een upload", + "playlist.chooseAria": "Kies een upload om toe te voegen", "playlist.empty": "Nog niets in de wachtrij.", + "playlist.hint": "Geüpload bij Media. Een item dat deze afspeellijst noemt en later zijn upload kwijtraakt, verschijnt hierboven als aandacht vragend in plaats van stil te falen zodra de failover het weer nodig heeft.", "playlist.itemsAria": "Items in de afspeellijst", - "playlist.stateReady": "Klaar", - "playlist.stateTranscoding": "Transcoderen", - "playlist.stateAttention": "Vraagt aandacht", - "playlist.stateUnsaved": "Nog niet opgeslagen", "playlist.missingUpload": "Deze upload is niet meer te vinden.", - "playlist.moveUp": "{name} omhoog verplaatsen", "playlist.moveDown": "{name} omlaag verplaatsen", - "playlist.remove": "{name} verwijderen", - "playlist.choose": "Kies een upload", - "playlist.chooseAria": "Kies een upload om toe te voegen", + "playlist.moveUp": "{name} omhoog verplaatsen", "playlist.noUploads": "Geen uploads beschikbaar", - "playlist.hint": "Geüpload bij Media. Een item dat deze afspeellijst noemt en later zijn upload kwijtraakt, verschijnt hierboven als aandacht vragend in plaats van stil te falen zodra de failover het weer nodig heeft.", - "dest.fbBoxTitle": "Cross-posten en donatieknop van Facebook", - "dest.fbBoxIntro": "Wordt in dezelfde aanmaak-aanroep verstuurd als Publiek, dus dezelfde regel geldt: eenmalig toegepast wanneer de uitzending start, en volledig met rust gelaten als het leeg is.", - "dest.fbBackupLabel": "Een reservefeed publiceren", - "dest.fbBackupHelp": "Stuurt een tweede kopie van deze stream naar Facebooks reserve-ingest, zodat een weggevallen verbinding de uitzending niet meesleurt.", - "dest.fbBackupCost": "Verdubbelt de uploadbandbreedte van deze bestemming.", - "dest.fbBackupReconnect": "Inschakelen verbindt de stream één keer opnieuw — schakel het dus in voordat je live gaat.", - "dest.fbCrosspostLabel": "Cross-posten naar Pagina’s", - "dest.fbCrosspostPageId": "Pagina-ID", - "dest.fbCrosspostAlsoPost": "Ook plaatsen", - "dest.fbCrosspostRemove": "Deze Pagina verwijderen", - "dest.fbCrosspostAdd": "Pagina toevoegen", - "dest.fbCrosspostHelp": "De numerieke Pagina-ID uit Facebooks eigen console (Pagina-instellingen, of graph.facebook.com/me/accounts terwijl je als de Pagina bent ingelogd) — er is hier geen zoekfunctie, plak dus de id in plaats van een naam of URL. ‘Ook plaatsen’ publiceert als die Pagina in plaats van de uitzending er alleen mee te delen; uitgeschakeld gebeurt alleen het stillere delen.", - "dest.fbDonateLabel": "Goede doel-ID voor de donatieknop", - "dest.fbDonatePlaceholder": "Goede doel-ID", - "dest.fbDonateHelp": "Ook een ondoorzichtige id uit Facebooks console voor inzamelingsacties, geen naam van een goed doel om op te zoeken. Laat leeg om helemaal geen donatieknop toe te voegen." + "playlist.remove": "{name} verwijderen", + "playlist.stateAttention": "Vraagt aandacht", + "playlist.stateReady": "Klaar", + "playlist.stateTranscoding": "Transcoderen", + "playlist.stateUnsaved": "Nog niet opgeslagen", + "playlist.title": "Afspeellijst", + "rec.delete": "Verwijderen", + "rec.deleteDescription": "Het bestand wordt van schijf verwijderd, samen met de proxy's, miniaturen en transcriptie. Dit kan niet ongedaan worden gemaakt.", + "rec.deleteFailed": "Kon de opname niet verwijderen.", + "rec.deleteTitle": "Deze opname verwijderen?", + "rec.deleted": "Opname verwijderd.", + "rec.disabled": "uitgeschakeld", + "rec.disk": "Schijf", + "rec.diskRecordings": "Opnames", + "rec.download": "Downloaden", + "rec.downloadNamed": "{name} downloaden", + "rec.duration": "Duur", + "rec.empty": "Nog geen opnames. Schakel opnemen rechts in en start een stream.", + "rec.enabled": "Ingeschakeld", + "rec.file": "Bestand", + "rec.free": "Vrij", + "rec.haltedDefault": "De opname is gestopt: het volume zit onder de ondergrens voor vrije ruimte.", + "rec.haltedRestarts": "De recorder start vanzelf opnieuw zodra er ruimte vrijkomt.", + "rec.help.enabled": "Schrijft elke ingang tijdens het streamen naar schijf, waarbij alle audiotracks gescheiden en ongecodeerd blijven. Opnemen staat los van wat je naar platforms stuurt: uitschakelen heeft geen invloed op je live-uitvoer, en inschakelen kost schijfruimte in plaats van uploadbandbreedte.", + "rec.help.maxAgeHours": "Verwijdert opnames ouder dan dit aantal uren, ongeacht de totale grootte. Handig als je toch alleen de laatste dag of twee nodig hebt. Stel 0 in om alles te bewaren tot een andere limiet het verwijdert.", + "rec.help.maxGb": "Een plafond op de totale grootte van de opnames die polyemesis bewaart. Daarboven worden de oudste verwijderd tot het totaal weer past. Stel 0 in voor geen groottelimiet — dan begrenzen alleen de leeftijd of de ondergrens voor vrije ruimte de groei.", + "rec.help.minFreeGb": "Een harde stop: de opname stopt wanneer het volume onder deze vrije ruimte zakt en hervat vanzelf zodra er ruimte vrijkomt. Dit is de enige limiet die rekening houdt met bestanden die polyemesis niet heeft geschreven, en dus de instelling die een gedeelde schijf werkelijk behoedt voor vollopen.", + "rec.help.segmentSeconds": "Hoe lang elk bestand is voordat de recorder een nieuw bestand begint. Kortere segmenten beperken wat een crash je kan kosten en maken losse bestanden makkelijker te verplaatsen, maar laten er meer over om te beheren. Knippen gebeurt op een keyframe, dus de werkelijke lengte varieert ongeveer een seconde.", + "rec.help.stemFiles": "Schrijft daarnaast elke audiotrack als eigen bestand naast de master. Handig als je later monteert en de presentator, de gasten en het spelgeluid op aparte kanalen wilt. Het vermenigvuldigt wat een sessie schrijft met ongeveer het aantal tracks, en daarom staat het standaard uit.", + "rec.help.stemFormat": "Beide opties zijn verliesvrij, dus geen van beide kost kwaliteit. FLAC is ongeveer half zo groot en is wat je wilt, tenzij een programma in je workflow het niet kan lezen; WAV opent in alles. Tracks met meer dan 8 kanalen vallen automatisch terug op WAV.", + "rec.hideStems": "Losse tracks verbergen", + "rec.keepForever": "0 = voor altijd bewaren.", + "rec.loadFailed": "Kon de opnames niet laden.", + "rec.maxAgeHours": "Verwijderen na (uren)", + "rec.maxGb": "Maximaal bewaren (GB)", + "rec.minFreeGb": "Stoppen onder vrije ruimte (GB)", + "rec.minFreeNote": "De limieten hierboven begrenzen alleen wat polyemesis heeft geschreven; al het andere op het volume kan het alsnog vullen. 0 = geen ondergrens.", + "rec.noSizeLimit": "0 = geen groottelimiet.", + "rec.recording": "neemt op", + "rec.retentionNote": "Het opruimen draait elke 30 seconden: oudste eerst, op leeftijd en daarna op totale grootte. Het segment dat op dat moment wordt geschreven wordt nooit verwijderd.", + "rec.saveFailed": "Kon de instellingen niet opslaan.", + "rec.savePolicy": "Bewaarbeleid opslaan", + "rec.saved": "Opname-instellingen opgeslagen.", + "rec.segmentSeconds": "Segmentlengte (seconden)", + "rec.segments": "Segmenten", + "rec.settingsTitle": "Opname", + "rec.showStems": "Losse tracks tonen", + "rec.size": "Grootte", + "rec.started": "Gestart", + "rec.stemFiles": "Losse trackbestanden", + "rec.stemFlac": "FLAC — verliesvrij, ongeveer half zo groot", + "rec.stemFormat": "Formaat losse tracks", + "rec.stemNote": "Wordt vanuit hetzelfde proces als de master geschreven, zodat elke losse track sample-uitgelijnd blijft met de andere. Tracks met meer dan 8 kanalen vallen vanzelf terug op WAV.", + "rec.stemWav": "WAV — verliesvrij, opent overal", + "rec.stems": "Losse tracks", + "rec.stemsTitle": "{count} losse trackbestanden, sample-uitgelijnd met de master", + "rec.stemsUsage": "Losse tracks ({count} bestanden)", + "rec.subtitle": "Het volledige multitrack-archief: elke binnenkomende audiotrack bewaard, ongecodeerd.", + "rec.title": "Opnames", + "rec.tracks": "Tracks", + "rec.tracksTitle": "{count} audiotracks bewaard", + "rec.used": "Gebruikt", + "rec.volume": "Volume", + "rend.aspectBlurpad": "Wazige vulling", + "rend.aspectBlurpadHint": "Vult de rest met een wazige, tot vullend bijgesneden kopie van het beeld zelf. Dit is de conventie waar verticale feeds op zijn uitgekomen.", + "rend.aspectCrop": "Bijsnijden tot vullend", + "rend.aspectCropHint": "Snijdt vanuit het midden bij tot de doelvorm en schaalt daarna. Onderwerpen houden hun formaat op het scherm; de randen van het beeld zijn weg.", + "rend.aspectHandling": "Omgang met beeldverhouding", + "rend.aspectPad": "Letterbox", + "rend.aspectPadHint": "Schaalt het hele beeld passend en vult de rest met een egale kleur. Er gaat niets verloren, maar een 16:9-bron op een 9:16-canvas is voornamelijk balken.", + "rend.aspectStretch": "Uitrekken tot passend", + "rend.aspectStretchHint": "Schaalt naar het doelformaat en laat het beeld vervormen als de bron daar niet mee overeenkomt. Wat elke variant deed voordat de andere modi bestonden.", + "rend.boxColour": "Kleur van het kader", + "rend.boxOpacity": "Dekking van het kader (%)", + "rend.builtInFont": "Ingebouwde standaard", + "rend.burnedText": "Ingebrande tekst", + "rend.chooseHardware": "Kies een hardware-encoder, of een kleinere uitvoer.", + "rend.colourBlack": "black", + "rend.colourWhite": "white", + "rend.costBeyond": "Dit in realtime met software coderen gaat de meeste machines te boven", + "rend.costBeyondDetail": "{label} is {mpps} voor één enkele codering. x264 verliest op dit formaat meestal de race met realtime, en een rendering die achterop raakt verliest frames voor elke bestemming die haar leest — wat je midden in de stream ontdekt.{machine} {escape}", + "rend.costBusy": "Reken erop dat meerdere cores bezet zijn", + "rend.costBusyDetail": "{label} is {mpps} op {encoder}, wat een zware realtime codering is.{machine} Houd de snelheid van de encoder in de gaten zodra hij live is: onder 1,0× raakt hij achterop en gaan de bestemmingen eronder frames verliezen. {escape}", + "rend.costMachineCores": " Deze machine meldt {cores} logische cores.", + "rend.costModest": "Bescheiden softwarecodering", + "rend.costModestDetail": "{label} is {mpps}, binnen bereik van de meeste machines.{machine} Het blijft één echte codering — gedeeld door elke bestemming die haar selecteert, maar niet gratis.", + "rend.costNoNumbers": "De kosten zijn nog niet te schatten", + "rend.costNoNumbersDetail": "Niets heeft de bron gesondeerd, dus “bron behouden” heeft geen cijfers achter zich. Start de ingest en dit vult zich.", + "rend.costOffloaded": "Overgeheveld naar {encoder}", + "rend.costOffloadedDetail": "{label} ({mpps}) draait op de fixed-function encoder van de GPU en kost dus weinig CPU. Kwaliteit en rate control verschillen per driver, en elke chip heeft een eigen plafond — controleer de uitvoer vóór een show die ertoe doet.", + "rend.create": "Aanmaken", + "rend.created": "Rendering aangemaakt. Selecteer haar op een bestemming om de codering te starten.", + "rend.custom": "Aangepast…", + "rend.deintAll": "Elk frame", + "rend.deintAllHint": "Voor bronnen die interlaced zijn maar dat niet melden. Veel SDI-bruggen en capturekaarten markeren alles als progressief, ongeacht wat ze binnenkregen, en daarop doet “alleen interlaced frames” niets en lijkt het een kapotte instelling.", + "rend.deintAuto": "Alleen interlaced frames", + "rend.deintAutoHint": "Raakt alleen frames aan die de bron als interlaced heeft gemarkeerd, zodat progressieve frames onaangeroerd doorgaan. De juiste keuze voor alles wat gemengd is.", + "rend.deintOff": "Uit", + "rend.deintOffHint": "Juist voor de progressieve bronnen die bijna iedereen heeft. Er een deinterlacen maakt hem zachter zonder enige winst.", + "rend.deinterlace": "Deinterlacen", + "rend.deleteFailed": "Kon de variant niet verwijderen.", + "rend.deleteFallbackMany": "{count} bestemmingen vallen terug op passthrough en krijgen de bronvideo ongewijzigd toegestuurd. Controleer of de bron nog past bij wat elk platform accepteert.", + "rend.deleteFallbackOne": "{count} bestemming valt terug op passthrough en krijgt de bronvideo ongewijzigd toegestuurd. Controleer of de bron nog past bij wat elk platform accepteert.", + "rend.deleteNoDestinations": "Niets selecteert deze variant, dus verwijderen verandert geen enkele bestemming.", + "rend.deleteRenditionTitle": "{name} verwijderen?", + "rend.editRendition": "Rendering bewerken", + "rend.empty": "Geen renderingen. Elke bestemming staat op passthrough, wat precies goed is totdat een ervan de bron niet aankan — bijvoorbeeld een 4K60-ingest die Twitch, Kick of X niet accepteert. Voeg een rendering toe en richt die bestemmingen daarop.", + "rend.encoder": "Encoder", + "rend.encoderPreset": "Encoder-preset", + "rend.encoderRelatedFailed": "{name} werd niet aangeboden omdat een verwante encoder hier mislukte.", + "rend.encoderTestFailed": "{name} zakte voor een testcodering van één frame op deze machine.", + "rend.encoderTestOk": "{name} codeerde een testframe op deze machine.", + "rend.evenNote": "0 op een as behoudt die van de bron en bewaart de beeldverhouding — stel alleen de hoogte in om te herschalen. Beide moeten even getallen zijn: H.264 en HEVC hebben geen chromavlak met oneven afmetingen.", + "rend.font": "Lettertype", + "rend.frameRate": "Beeldsnelheid", + "rend.gop": "GOP", + "rend.gpuUnopenable": "Er is een GPU aanwezig maar dit proces kan hem niet openen", + "rend.hardwareFallback": "hardware", + "rend.hardwareScanFound": "Wat de hardwarescan heeft gevonden", + "rend.height": "Hoogte", + "rend.hevcWarning": "Deze encoder produceert HEVC. De meeste RTMP-bestemmingen accepteren alleen H.264 — kies hem alleen voor een bestemming die je heeft laten weten HEVC aan te nemen.", + "rend.keyframeInterval": "Keyframe-interval", + "rend.letterboxColour": "Letterbox-kleur", + "rend.live": "Live", + "rend.loadFailed": "Kon de varianten niet laden.", + "rend.marginX": "Marge X (%)", + "rend.marginY": "Marge Y (%)", + "rend.name": "Naam", + "rend.namePlaceholder": "1080p60 voor Twitch en Kick", + "rend.needsBothAWidthAnd": "Heeft zowel een breedte als een hoogte nodig — met één vrije as behoudt de schaling de beeldverhouding al.", + "rend.needsBothAxes": "Heeft zowel een breedte als een hoogte nodig", + "rend.newRendition": "Nieuwe rendering", + "rend.noEncoder": "Deze FFmpeg meldde geen bruikbare video-encoder.", + "rend.noHardwareEscape": "Geen enkele hardware-encoder werkt op deze machine, dus er is niets om dit naartoe te verplaatsen — de enige knop hier is een kleinere uitvoer. Detecteer de hardware opnieuw als je sindsdien een driver hebt geïnstalleerd of een GPU aan de container hebt doorgegeven.", + "rend.notProbed": "Op deze machine is nog niets testgecodeerd, dus deze lijst is wat de FFmpeg-build bevat en niet wat de hardware kan. Detecteer opnieuw om daarachter te komen.", + "rend.notRunning": "Niet actief", + "rend.note": "Notitie", + "rend.noteDuplicateFps": "De bron draait op {srcFps} fps, dus {fps} fps dupliceert alleen frames.", + "rend.notePlaceholder": "Waar dit niveau voor is.", + "rend.noteUpscale": "De bron is {width}×{height}. Opschalen kost CPU en voegt geen detail toe.", + "rend.opacity": "Dekking (%)", + "rend.percentNote": "Alles is een percentage van het beeld, dus dezelfde instellingen kloppen op een 16:9-niveau en op een 9:16-niveau. Marges worden genegeerd op een gecentreerde as.", + "rend.preset": "Preset", + "rend.presetPlaceholder": "veryfast", + "rend.presetStartingPoint": "Startpunt — controleer de huidige limieten bij het platform.", + "rend.redetectFailed": "Kon de hardware niet opnieuw detecteren.", + "rend.redetectTitle": "Voert de GPU-scan opnieuw uit en codeert één testframe met elke encoder. Duurt een paar seconden.", + "rend.redetectedNoHardware": "Hardware opnieuw gedetecteerd. Geen enkele hardware-encoder doorstond een testcodering op deze machine.", + "rend.resolution": "Resolutie", + "rend.restartEncode": "Herstart deze codering en de bestemmingen die haar lezen", + "rend.restartFailed": "Kon de codering niet herstarten.", + "rend.restarting": "{name} start opnieuw op. De bijbehorende bestemmingen verbinden mee opnieuw.", + "rend.save": "Opslaan", + "rend.saveFailed": "Kon de variant niet opslaan.", + "rend.seedNote": "Deze vullen het formulier alvast in en meer niet — pas gerust alles aan voordat je opslaat. Platformplafonds veranderen en verschillen per partnerstatus.", + "rend.signalError": "Fout", + "rend.signalIdle": "Inactief", + "rend.sizePct": "Grootte (% van de hoogte)", + "rend.softwareEncoderFallback": "een software-encoder", + "rend.speed": "Snelheid", + "rend.subtitle": "Eén gedeelde videocodering, veel bestemmingen. Audio wordt hier nooit aangeraakt.", + "rend.target": "Doel", + "rend.textColour": "Tekstkleur", + "rend.textPlaceholder": "MIJN ZENDER", + "rend.title": "Varianten", + "rend.unused": "Nog geen enkele bestemming selecteert deze, dus er wordt niets gecodeerd. Kies haar op een bestemming om de codering te starten.", + "rend.uptime": "Actieve tijd", + "rend.videoBitrate": "Videobitrate", + "rend.waitingSnapshot": "Wachten op de eerste live momentopname.", + "rend.watermarkImage": "Watermerkafbeelding", + "rend.watermarkPlaceholder": "overlays/logo.png", + "rend.width": "Breedte", + "rend.widthPct": "Breedte (% van het beeld)", + "route.attack": "Attack", + "route.audio": "audio", + "route.audioAndVideoGoOut": "Audio en video gaan samen uit.", + "route.audioLaterThanVideo": "Audio later dan video", + "route.audioRouting": "Audiorouting", + "route.audioRoutingDesc": "Kies precies welke ingangstracks elke bestemming ontvangt.", + "route.autoLimitWhen2Tracks": "Auto (limiteren bij 2+ tracks)", + "route.commentaryLanguage": "Taal van het commentaar", + "route.couldNotApplyThePreset": "Kon het preset niet toepassen.", + "route.couldNotCompileThisRouting": "Kon deze routing niet compileren.", + "route.couldNotLoadDestinations": "Kon de bestemmingen niet laden.", + "route.couldNotSaveTheRouting": "Kon de routing niet opslaan.", + "route.couldNotSaveTrackRoles": "Kon de trackrollen niet opslaan.", + "route.custom": "Aangepast…", + "route.delayDirection": "Richting van de vertraging", + "route.destinations": "Bestemmingen", + "route.duckingNote": "Haalt de ene groep tracks omlaag zodra een andere spreekt — de microfoon over het muziekbed. Uit betekent dat elke geselecteerde track op zijn eigen versterking wordt opgeteld, ongewijzigd.", + "route.duckingTriggerNote": "Een trigger hoeft niet in de mix van deze bestemming te zitten — een microfoon kan hier de muziek wegdrukken terwijl alleen de muziek wordt verstuurd.", + "route.enableDucking": "Ducking inschakelen", + "route.everyTrackReaches": "Elke geselecteerde track bereikt deze bestemming, muziek inbegrepen.", + "route.excludeMusic": "Muziek uitsluiten", + "route.keepMusic": "Muziek behouden", + "route.keepMusicOut": "Houd muziek buiten deze bestemming", + "route.labelTracks": "Tracks labelen", + "route.langPlaceholder": "taal — bijv. es", + "route.limiter04DbfsCeiling": "Limiter (plafond −0,4 dBFS)", + "route.localOnlyNote": "Deze server bewaart trackrollen nog niet, dus deze blijven in deze browser. Al het andere op de pagina wordt gewoon opgeslagen.", + "route.loudnessTarget": "Loudness-doel", + "route.lufsBroadcast": "EBU R128 broadcast", + "route.measuredOverTheWholeProgramme": "Gemeten over het hele programma, niet van moment tot moment: stille passages blijven stil. Dit zijn de getallen waar de normalisatoren van de platforms zelf op mikken, dus er een halen is wat voorkomt dat het platform je terugdraait.", + "route.mixMatrix": "Mixmatrix", + "route.musicPolicy": "Muziekbeleid", + "route.noTarget": "Geen doel (laat de niveaus met rust)", + "route.normalizeLoudnorm": "Loudness (EBU R128)", + "route.normalizeOff": "Uit", + "route.off": "uit", + "route.offset": "Offset", + "route.on": "aan", + "route.platformDefault": "Standaard van het platform", + "route.podcastSpokenWord": "Podcast / gesproken woord", + "route.presetApplied": "Preset toegepast. Controleer het en sla dan op.", + "route.range": "Bereik", + "route.ratio": "Ratio", + "route.release": "Release", + "route.result": "Resultaat", + "route.role51": "5.1", + "route.roleClean": "clean", + "route.roleMic": "mic", + "route.roleMusic": "muziek", + "route.rolesNote": "Rollen beschrijven de ingest, niet deze bestemming, en worden apart opgeslagen. Er wordt niets gefilterd totdat een bestemming hieronder erom vraagt.", + "route.simple": "Eenvoudig", + "route.target": "Doel", + "route.targetRole": "doel", + "route.thisDestinationSPlatform": "Het platform van deze bestemming", + "route.threshold": "Drempel", + "route.title": "Routing", + "route.triggerRole": "trigger", + "route.truePeak": "True peak", + "route.video": "video", + "route.videoLaterThanAudio": "Video later dan audio", + "route.withoutATargetTheMix": "Zonder doel gaat de mix eruit op het niveau dat de ingang produceerde, met alleen de clipbescherming hierboven toegepast.", + "route.youtubeTwitchSpotify": "YouTube, Twitch, Spotify", + "set.acceptSelfSigned": "Een zelfondertekend certificaat accepteren", + "set.accountDisconnected": "Account losgekoppeld.", + "set.adminPassword": "Beheerderswachtwoord", + "set.alertDelivery": "Bezorging van meldingen", + "set.alertDeliveryDesc": "Hoe hardnekkig een webhook die niet antwoordt wordt nagejaagd, voordat de melding wordt opgegeven.", + "set.apiTokens": "API-tokens", + "set.apiTokensDesc": "Langlevende inloggegevens voor scripts, Prometheus en al het andere dat geen sessie kan vasthouden. Stuur er een mee als Authorization: Bearer pmk_…. Een token kan alles wat de beheerder kan, behalve tokens beheren — dat blijft achter het wachtwoord, dus intrekken is definitief.", + "set.app": "App", + "set.attemptsBackoff": "De wachttijd tussen pogingen loopt automatisch op tot een plafond van 30 seconden, dus de bovenkant van dit bereik is al enkele minuten najagen van één dood eindpunt. Die begrenzing is precies het punt: eindeloos opnieuw proberen maakt van één dode webhook een wachtrij die nooit leegloopt.", + "set.attemptsNote": "Inclusief de eerste poging. 1 betekent nooit opnieuw proberen.", + "set.attemptsRetryable": "Alleen fouten die het opnieuw proberen waard zijn worden herhaald. Een 404 van een webhook die iemand verwijderd heeft is definitief, en hem nog vier keer proberen vertraagt alleen elke melding die erachter in de rij staat; een 502 van een reverse proxy is precies waar deze pogingen voor bedoeld zijn.", + "set.audioMeters": "Audiometers", + "set.brokerPasswordCleared": "Brokerwachtwoord gewist.", + "set.brokerPlainNote": "mqtt:// is onversleuteld. MQTT stuurt de gebruikersnaam en het wachtwoord mee in het CONNECT-pakket, dus beide gaan onversleuteld over het netwerk — prima op een vertrouwd LAN, wat het gebruikelijke Home Assistant-geval is, en niet prima over iets wat je niet beheert. Gebruik mqtts:// als de broker dat aanbiedt.", + "set.brokerUrl": "Broker-URL", + "set.brokerUrlNote": "mqtt://, mqtts://, ws:// of wss://. Een wachtwoord in de URL wordt geweigerd: een URL belandt in logregels en in de uitvoer van ps, en dat valt niet terug te draaien. Zet het in het veld hieronder, waar het versleuteld wordt opgeslagen.", + "set.brokerUrlPlaceholder": "mqtts://broker.local:8883", + "set.chatBothApply": "Beide gelden en de ruimhartigste wint: een bericht verdwijnt wanneer het ouder is dan het aantal uren en buiten de nieuwste N valt. Een druk kanaal bewaart daardoor minder tijd dan een rustig kanaal bij dezelfde instellingen.", + "set.chatBufferNote": "“Sturen bij verbinden” is een ander soort getal dan de andere twee. Die begrenzen wat op schijf blijft; dit is een buffer in het geheugen, per verbonden dashboard toegewezen.", + "set.chatHistory": "Chatgeschiedenis", + "set.chatHistoryDesc": "Hoe ver de chatgeschiedenis teruggaat — en daarmee hoeveel een moderator ziet als hij iemands kaart opent.", + "set.chatHours": "Bewaren gedurende (uren)", + "set.chatKeep": "Altijd bewaren (berichten)", + "set.chatKeepNote": "Een ondergrens, ongeacht hun leeftijd.", + "set.chatNotOnlyDisk": "Dit is niet alleen een schijfinstelling. Op een naam in de chat klikken toont wat die persoon eerder heeft gezegd, en die geschiedenis komt hiervandaan — geen enkel platform publiceert er een API voor, dus polyemesis kan alleen tonen wat het bewaard heeft. Zet je dit te kort, dan lijkt iemand die al een week problemen geeft nooit iets gezegd te hebben.", + "set.chatSendNote": "Geschiedenis zonder zoekopdracht.", + "set.chatSendOnConnect": "Sturen bij verbinden (berichten)", + "set.chatSendOnConnectShort": "Sturen bij verbinden", + "set.chatSmall": "Chat is klein. Een kanaal met gemiddeld tien berichten per seconde slaat ruwweg 7 MB per uur op; de meeste zitten daar ver onder.", + "set.clientId": "Client-ID", + "set.clientIdNote": "Moet uniek zijn op de broker. Een botsing is de belangrijkste oorzaak van een onverklaarbare herverbindingslus: de broker verbreekt bij elke verbinding de oudere sessie, en beide clients blijven eindeloos opnieuw verbinden.", + "set.clientIdPlaceholder": "afgeleid van de instantienaam", + "set.clientSecret": "Client secret", + "set.comingBack": "Terugkeren", + "set.confirmPassword": "Nieuw wachtwoord bevestigen", + "set.connectedAccounts": "Verbonden accounts", + "set.copied": "Gekopieerd", + "set.copy": "Kopiëren", + "set.couldNotChangeThePassword": "Kon het wachtwoord niet wijzigen.", + "set.couldNotClearThePassword": "Kon het wachtwoord niet wissen.", + "set.couldNotCreateTheToken": "Kon het token niet aanmaken.", + "set.couldNotReCheckThe": "Kon de inloggegevens niet opnieuw controleren.", + "set.couldNotReadTheTls": "Kon de TLS-status niet lezen.", + "set.couldNotRevokeTheToken": "Kon het token niet intrekken.", + "set.couldNotSaveTheCredentials": "Kon de inloggegevens niet opslaan.", + "set.couldNotSaveTheMqtt": "Kon de MQTT-instellingen niet opslaan.", + "set.credsRemoved": "Inloggegevens verwijderd.", + "set.currentPassword": "Huidig wachtwoord", + "set.dataDirectory": "Datamap", + "set.deliveryAttempts": "Bezorgpogingen", + "set.dialsOnly": "De server belt uitsluitend: {schemes}.", + "set.disconnect": "Loskoppelen", + "set.disconnectDesc": "Bestemmingen die het gebruiken behouden hun instellingen maar vernieuwen hun streamsleutel niet meer. Je kunt hetzelfde account daarna opnieuw verbinden.", + "set.disconnectTitle": "Dit account loskoppelen?", + "set.drawtextMissing": "Deze build heeft geen drawtext-filter, dus tekstoverlays zijn niet beschikbaar. Er is een FFmpeg nodig die met libfreetype is gebouwd.", + "set.drawtextOk": "Deze build kan tekst op video tekenen.", + "set.enabled": "Ingeschakeld", + "set.encoderUrl": "Encoder-URL", + "set.expires": "Verloopt", + "set.failover": "Failover", + "set.failoverDesc": "Een reserve-ingang en een wachtkaart, voor wanneer de live bron niets meer levert. Standaard uit: met deze functie uit is de keten byte voor byte wat hij was voordat dit bestond.", + "set.failoverRemuxNote": "Dit inschakelen voegt één remux-stap toe tussen de ingang en alles daaronder. Goedkoop, maar niet gratis, en daarom is het een beslissing en geen standaardwaarde.", + "set.fileSizeMb": "Bestandsgrootte (MB)", + "set.filesKept": "Bewaarde bestanden", + "set.goingLive": "Live gaan", + "set.goingLiveDesc": "Wat er gebeurt wanneer meerdere bestemmingen tegelijk starten.", + "set.graceNote": "Hoe lang de live bron niets mag leveren voordat het niveau omschakelt. Met opzet langer dan een herverbinding — een encoder die een RTMP-verbinding herstelt is normale werking, geen storing.", + "set.gracePeriod": "Respijtperiode (seconden)", + "set.haDiscovery": "Home Assistant-detectie", + "set.height": "Hoogte", + "set.ingest": "Ingang", + "set.ingestDesc": "SRT draagt tot zes audiotracks. RTMP draagt er één en bestaat als terugval voor encoders die geen SRT kunnen. Ophalen belt een bron in plaats van op er een te wachten.", + "set.ingestUrlCopied": "Ingangs-URL gekopieerd.", + "set.instance": "Instantie", + "set.intervalNote": "Ongewijzigde toestand wordt niet opnieuw gepubliceerd, dus een kort interval kost een vergelijking in plaats van een bericht. De keep-alive bepaalt hoe lang een dode verbinding onopgemerkt blijft — en dus hoe snel de broker abonnees vertelt dat deze instantie weg is.", + "set.issuer": "Uitgever", + "set.kbps": "kbps", + "set.keepAlive": "Keep-alive (s)", + "set.latencyMs": "Latentie (ms)", + "set.listeners": "Listeners", + "set.listenersDesc": "Waar de server zich bindt, voor de hele installatie. Eén SRT-poort bedient elke bron — ze worden onderscheiden door hun publicatietoken, dus een bron toevoegen betekent nooit nog een poort openzetten. RTMP kent zo'n routering niet, dus die poort bedient één bron.", + "set.logsBoundNote": "De logmap wordt begrensd door het product van deze twee waarden, dus het bewaren kan nooit datgene zijn wat de schijf vol laat lopen.", + "set.logsDesc": "De opgevangen stderr van FFmpeg wordt altijd in het geheugen bewaard, maar dat sterft met de server — waarmee precies de regels verdwijnen die verklaren waaróm hij stierf. Bewaren spiegelt het naar een roterend bestand in de datamap.", + "set.metersDesc": "Eén hulpproces meet elk kanaal van elke track. Kortere intervallen verversen sneller en kosten meer CPU.", + "set.mode": "Modus", + "set.modePull": "Ophalen — een camera, feed of bestand aanroepen", + "set.modeRtmp": "RTMP — één track", + "set.modeSrt": "SRT — meersporig (aanbevolen)", + "set.mqtt": "MQTT-telemetrie", + "set.mqtt5Only": "Alleen MQTT 5.0.", + "set.mqtt5OnlyNote": "Een broker die op 3.1.1 is vastgezet maakt helemaal geen verbinding — het is geen afgeslankte modus. Mosquitto 2.x, EMQX en de Home Assistant-add-on spreken alle 5.0.", + "set.mqttDesc": "Publiceert toestand naar een broker als retained messages, zodat een afnemer die niet verbonden was toen er iets veranderde alsnog het actuele antwoord krijgt.", + "set.multiAccountNote": "Verbind hetzelfde platform meer dan eens om naar meerdere kanalen te streamen — elk account wordt zijn eigen bestemming.", + "set.name": "Naam", + "set.newPassword": "Nieuw wachtwoord", + "set.no": "nee", + "set.noEncryption": "Niets versleutelt deze verbinding. Wachtwoorden en sessiecookies gaan onversleuteld over het netwerk — stel tls.mode: auto in, of bind aan 127.0.0.1 achter een proxy.", + "set.noLibsrt": "Deze FFmpeg-build heeft geen SRT-ondersteuning. Installeer er een die met --enable-libsrt is geconfigureerd, of gebruik RTMP.", + "set.noTokens": "Nog geen tokens.", + "set.passphrase": "Wachtwoordzin", + "set.passphraseNote": "Schakelt AES-versleuteling in. Laat leeg voor geen. SRT weigert wachtwoordzinnen buiten 10–79 tekens.", + "set.passphrasePlaceholder": "optioneel — 10 tot 79 tekens", + "set.password": "Wachtwoord", + "set.passwordChanged": "Wachtwoord gewijzigd.", + "set.passwordMismatch": "De nieuwe wachtwoorden komen niet overeen.", + "set.passwordNote": "Het wachtwoord wordt door geen enkele API teruggegeven, dus dit vak is leeg zelfs als er een is ingesteld. Laat het leeg om het opgeslagen wachtwoord te behouden; wis het met de knop hieronder.", + "set.platform": "Platform", + "set.platformsDesc": "Platforms verschillen in hoeveel hiervan polyemesis kan automatiseren, en dat verschil is het waard vooraf te weten in plaats van een uur later. Het streamen zelf werkt overal hetzelfde — audiorouting per bestemming, varianten, herverbinden en meters hangen van geen van de kolommen hieronder af.", + "set.playLoop": "Een lus afspelen wanneer er niets live is", + "set.playLoopNote": "Een tweede kandidaat naast de wachtkaart: er worden bestanden afgespeeld in plaats van een stilstaand beeld. Beide kunnen aanstaan — zie de niveauvolgorde in het failover-ontwerp voor welke wint.", + "set.pointObs": "Richt OBS op dit adres.", + "set.preview": "Voorbeeld", + "set.previewDesc": "De HLS-monitor van het dashboard. Dit is de enige plek waar polyemesis video hercodeert, en het draait op een eigen relay-abonnement, zodat het nooit een bestemming kan beïnvloeden.", + "set.previewIdleNote": "De encoder start wanneer het dashboard om de afspeellijst vraagt en stopt weer zo lang na het laatste verzoek, zodat een gesloten dashboard geen CPU kost. Dit wijzigen herstart nooit een voorbeeld waar iemand naar kijkt.", + "set.processLogs": "Proceslogboeken", + "set.proxyTerminatesTls": "Een reverse proxy vóór polyemesis termineert TLS, dus deze server levert bewust gewone HTTP.", + "set.publishInterval": "Publicatie-interval (s)", + "set.reconnectCap": "Bovengrens herverbindingswachttijd (s)", + "set.reconnectCapNote": "0 gebruikt de standaardwaarde. Alleen HTTP- en HLS-bronnen.", + "set.redirectNote": "Moet exact overeenkomen, inclusief schema en poort. Bereik je polyemesis via een reverse proxy, gebruik dan die publieke URL.", + "set.redirectUri": "Redirect-URI om toe te staan", + "set.removeCreds": "Inloggegevens verwijderen", + "set.removeCredsDesc": "Elk account dat via deze app is verbonden stopt met het vernieuwen van zijn token en moet opnieuw worden verbonden. Het client secret is hiervandaan niet te herstellen — je zou een nieuwe app bij het platform aanmaken.", + "set.removeCredsTitle": "Deze ontwikkelaarsgegevens verwijderen?", + "set.returnAfter": "Terugkeren na (seconden stabiel)", + "set.returnAutomatic": "Automatisch — terug zodra de primaire stabiel is", + "set.returnManual": "Handmatig — op de reserve blijven tot ik omschakel", + "set.returnManualNote": "Handmatig is de standaard omdat een automatische terugkeer kan flapperen: een primaire bron die met horten en stoten herstelt zou het programma midden in de uitzending heen en weer schuiven.", + "set.rtmpPort": "RTMP-poort", + "set.rtspNote": "TCP tenzij de camera niet anders kan. RTSP over UDP door NAT lijkt verbonden en levert niets.", + "set.rtspTransport": "RTSP-transport", + "set.saveAndDrop": "Opslaan en de live stream afbreken", + "set.saveIngest": "Ingangsinstellingen opslaan", + "set.segmentSec": "Segment (s)", + "set.settingsSavedAffectedProcessesHave": "Instellingen opgeslagen. De betrokken processen zijn herstart.", + "set.showSlate": "Een wachtkaart tonen wanneer er niets live is", + "set.slateColour": "Kleur van de wachtkaart", + "set.slateColourNote": "Eén woord, want de waarde belandt in een filtergraaf waar een komma het argument zou beëindigen. Wordt alleen gebruikt wanneer er geen afbeelding is ingesteld.", + "set.slateColourPlaceholder": "black", + "set.slateImage": "Afbeelding van de wachtkaart", + "set.slateImageNote": "Een pad binnen de datamap. Laat het leeg om in plaats daarvan een egale kleur te tonen — dat is juist de terugval, omdat een kleur geen bestand heeft dat niet geopend kan worden.", + "set.slateImagePlaceholder": "slate/holding.png", + "set.sourceUrl": "Bron-URL", + "set.sourceUrlNote": "Een van {schemes}. Een file://-bron is een pad RELATIEF ten opzichte van de datamap en mag geen “..” bevatten — het is daar op dezelfde manier opgesloten als een geüpload bestand.", + "set.sourceUrlPlaceholder": "rtsp://camera.local/stream1", + "set.srtPort": "SRT-poort", + "set.stagger": "Verbindingen spreiden (ms)", + "set.staggerNote": "0 is uit. Anders betekent live gaan dat elke bestemming op hetzelfde moment een verbinding opent, TLS onderhandelt en audio begint te coderen — op een kleine machine is dat het moment waarop je het meest waarschijnlijk beelden verliest, en het is precies het moment waar je naar kijkt, omdat je net live bent gegaan. Een paar honderd milliseconden is ruim voldoende.", + "set.staggerReconnectNote": "Het vertraagt nooit een herverbinding. Een bestemming die om 3 uur 's nachts wegvalt komt meteen terug in plaats van te wachten achter processen die al gezond zijn.", + "set.stepByStep": "Stapsgewijze installatie", + "set.stopAfterIdle": "Stoppen na inactiviteit (s)", + "set.streamKey": "Streamsleutel", + "set.streamKeyNote": "Publicerende partijen moeten exact dit pad gebruiken, zodat een open poort geen open deur is.", + "set.subject": "Onderwerp", + "set.synthesiseNote": "Elk groot platform weigert een stream zonder audiotrack. Staat dit aan, dan krijgt een ingang met alleen video één stille stereotrack en starten de bestemmingen; staat het uit, dan weigeren ze met “routing profile selects no audio”. Het geldt uitsluitend voor een ingang die met nul tracks is GEDETECTEERD, en kan dus nooit audio verdringen die je werkelijk verstuurt.", + "set.synthesiseSilent": "Een stille track synthetiseren", + "set.syntheticAudio": "Synthetische audio", + "set.syntheticDesc": "Wat er gebeurt wanneer de ingang video draagt en helemaal geen audio.", + "set.tabIngest": "Ingang", + "set.tabPipeline": "Verwerkingsketen", + "set.tabPlatforms": "Platformgegevens", + "set.tabSecurity": "Beveiliging", + "set.title": "Instellingen", + "set.tlsAcme": "Let's Encrypt", + "set.tlsAuto": "Auto", + "set.tlsDesc": "Wordt ingesteld in config.yaml, niet hier — TLS moet kloppen voordat de server begint te luisteren. Deze kaart rapporteert wat dat heeft opgeleverd.", + "set.tlsManual": "Handmatig certificaat", + "set.tlsOff": "Hier niet getermineerd", + "set.tlsSelfsigned": "Zelfondertekend", + "set.tokenHashNote": "polyemesis bewaart alleen een hash. Raak je hem kwijt, trek dit token dan in en maak een nieuw aan.", + "set.tokenNamePlaceholder": "prometheus", + "set.tokenRevoked": "{name} ingetrokken.", + "set.topicPrefix": "Topic-voorvoegsel", + "set.topicPrefixNote": "Scheidingstekens in het voorvoegsel blijven behouden, dus home/av betekent twee niveaus. Een voorvoegsel dat met $ begint wordt geweigerd: brokers reserveren die, en een abonnee die # gebruikt ontvangt ze nooit — de telemetrie zou met succes gepubliceerd worden en onzichtbaar zijn in precies de weergave waarmee je zou gaan zoeken.", + "set.transportSecurity": "Transportbeveiliging", + "set.updateInterval": "Vernieuwingsinterval (ms)", + "set.username": "Gebruikersnaam", + "set.validFor": "Geldig voor", + "set.whatPlatformsDo": "Wat elk platform kan", + "set.whyDevApp": "Waarom je een eigen ontwikkelaarsapp nodig hebt", + "set.whyDevAppDesc": "polyemesis kan geen OAuth-client secrets meeleveren — iedereen met het binaire bestand zou ze hebben, en de platforms zouden ze intrekken. Je registreert eenmalig een gratis ontwikkelaarsapp, plakt hier de client-ID en het secret, en polyemesis haalt daarna je streamsleutels voor je op.", + "set.writeLogs": "Logboeken naar schijf schrijven", + "set.yes": "ja", + "sources.addDescription": "Een tweede programma met een eigen ingang, eigen bestemmingen en varianten — bijvoorbeeld een verticaal canvas naast een horizontaal. Het start op de standaardpoorten, die je wilt wijzigen zodat de twee elkaar niet in de weg zitten.", + "sources.addFailed": "Kon de bron niet toevoegen", + "sources.addSource": "Bron toevoegen", + "sources.addTitle": "Een bron toevoegen", + "sources.added": "“{name}” toegevoegd. Stel hieronder de ingangspoorten in.", + "sources.app": "App", + "sources.applyAndDrop": "Toepassen en de stream afbreken", + "sources.copied": "{what} gekopieerd", + "sources.copyFailed": "Kon {what} niet kopiëren", + "sources.copyTokenAria": "Het token kopiëren", + "sources.copyUrlAria": "De {proto}-URL kopiëren", + "sources.default": "standaard", + "sources.defaultDescription": "Verzoeken die geen bron noemen werken op deze.", + "sources.deleteAria": "{name} verwijderen", + "sources.deleteConfirm": "Bron verwijderen", + "sources.deleteDescription": "De bestemmingen en varianten gaan mee — ze beschrijven waar dit programma heen gaat en betekenen niets zonder dat programma. Opnames blijven bewaard: de bestanden staan nog op schijf en zijn nog afspeelbaar, ze worden alleen niet langer aan een bron toegeschreven.", + "sources.deleteFailed": "Kon de bron niet verwijderen", + "sources.deleteThis": "Deze bron verwijderen", + "sources.deleteTitle": "“{name}” verwijderen?", + "sources.deleted": "“{name}” en de bijbehorende bestemmingen verwijderd", + "sources.destinations": "Bestemmingen", + "sources.dirtyIdle": "Niet-opgeslagen wijzigingen aan de ingang. Toepassen herstart de ingang van deze bron.", + "sources.dirtyPublishing": "Er publiceert een encoder naar deze bron. Toepassen herstart de ingang en verbreekt de verbinding met alle kijkers.", + "sources.enableAria": "{name} inschakelen", + "sources.help.app": "Het RTMP-applicatiepad — het deel van de URL tussen de host en de streamsleutel. De meeste encoders verwachten “live”. Wijzig het alleen als je daar een reden voor hebt.", + "sources.help.ingest": "Hoe je encoder deze bron bereikt. SRT overleeft een verbinding met pakketverlies door te hersturen wat verloren ging, en is de juiste standaardkeuze over het publieke internet. RTMP is ouder en wordt overal ondersteund, maar gaat sterk achteruit op een instabiele verbinding. Bij ophalen haalt polyemesis het signaal op van een URL die jij opgeeft, in plaats van te wachten tot er naar toe gepubliceerd wordt.", + "sources.help.latencyMs": "Hoe lang SRT wacht op een laat of hergestuurd pakket voordat het wordt opgegeven. Hogere waarden vangen slechtere verbindingen op ten koste van vertraging: ongeveer 2,5× je round-trip-tijd is een goed startpunt, dus 40 ms RTT vraagt hier om zo'n 100 ms. Te laag, en een korte hapering wordt een zichtbare storing.", + "sources.help.passphrase": "Versleutelt de SRT-verbinding, en dit is vandaag wat vreemden daadwerkelijk buiten deze ingang houdt. Tussen 10 en 79 tekens. Laat je het leeg, dan kan iedereen die de poort bereikt ernaartoe publiceren.", + "sources.help.streamKey": "Het geheim dat een encoder verstuurt om via RTMP te publiceren. Dit is de toegangssleutel die deze ingang beschermt, dus behandel hem als een wachtwoord: wie hem heeft, kan naar jouw bestemmingen uitzenden.", + "sources.help.token": "Identificeert deze bron wanneer er meerdere één ingangspoort delen. Of het ook de toegang bewaakt hangt af van je ingangsmodus — de notitie eronder zegt wat op dit moment waar is. Vernieuwen geeft een nieuw token uit en houdt het oude vijf minuten geldig, zodat een live encoder niet midden in een uitzending wordt afgebroken.", + "sources.ingest": "Ingang", + "sources.lastSourceTitle": "De laatste bron kan niet worden verwijderd — een installatie heeft één ingang nodig", + "sources.latencyMs": "Latentie (ms)", + "sources.loadFailed": "Kon de bronnen niet laden", + "sources.loading": "Bronnen laden…", + "sources.loss": "verlies", + "sources.modePull": "Ophalen", + "sources.name": "Naam", + "sources.namePlaceholder": "Verticaal", + "sources.noEngine": "Er draait geen engine voor deze bron. De gebruikelijke oorzaak is een ingangspoort die al in gebruik is door een andere bron of een ander proces.", + "sources.notRunning": "niet actief", + "sources.ownDescription": "Eigen bestemmingen, varianten en opnames.", + "sources.passphrase": "Wachtwoordzin", + "sources.passphrasePlaceholder": "10–79 tekens", + "sources.publishing": "publiceert", + "sources.renditions": "Varianten", + "sources.retrans": "hertransm.", + "sources.rotate": "Vernieuwen", + "sources.rotateConfirm": "Token vernieuwen", + "sources.rotateFailed": "Kon het token niet vernieuwen", + "sources.rotateIdle": "Er wordt onmiddellijk een nieuw token uitgegeven. Het oude blijft vijf minuten werken zodat een encoder die het al gebruikt niet wordt afgebroken.", + "sources.rotateTitle": "Token voor “{name}” vernieuwen?", + "sources.rotateWhilePublishing": "Er publiceert op dit moment een encoder naar deze bron. Het oude token blijft vijf minuten werken, dus de verbinding wordt niet verbroken — maar het moet vóór het einde van die periode bijgewerkt worden.", + "sources.rtt": "RTT", + "sources.running": "actief", + "sources.saveFailed": "Kon niet opslaan", + "sources.streamKey": "Streamsleutel", + "sources.subtitle": "Elk één binnenkomend programma. Bestemmingen en varianten horen bij een bron.", + "sources.title": "Bronnen", + "sources.token": "token", + "sources.tokenEnforcedDetailRtmp": "Het is de streamsleutel: publiceer naar de URL hierboven met deze als sleutel. Roteren geeft een nieuwe en houdt de oude vijf minuten geldig, zodat je kunt overstappen zonder de uitzending te onderbreken.", + "sources.tokenEnforcedDetailSrt": "Zet hem in de SRT-streamid van je encoder om naar deze bron op de gedeelde poort te publiceren. Roteren geeft een nieuwe en houdt de oude vijf minuten geldig, zodat je kunt overstappen zonder de uitzending te onderbreken.", + "sources.tokenIsCredential": "Dit token is de toegangssleutel.", + "sources.tokenIssued": "Nieuw token uitgegeven", + "sources.tokenNotEnforced": "Dit token wordt opgeslagen maar op dit moment niet afgedwongen.", + "sources.tokenNotEnforcedRtmp": "Bronnen worden per poort gescheiden gehouden. Wat deze ingang werkelijk beschermt is de streamsleutel hierboven. Schakel in Instellingen de ingang met één poort in om het token de toegangssleutel te maken.", + "sources.tokenNotEnforcedSrt": "Bronnen worden per poort gescheiden gehouden. Wat deze ingang werkelijk beschermt is de SRT-wachtwoordzin hierboven. Schakel in Instellingen de ingang met één poort in om het token de toegangssleutel te maken.", + "state.disabled": "Uitgeschakeld", + "state.failed": "Mislukt", + "state.live": "Live", + "state.offline": "Niet online", + "state.reconnecting": "Opnieuw verbinden", + "state.starting": "Starten", + "state.stopped": "Gestopt" } diff --git a/ui/src/lib/i18n/pl.json b/ui/src/lib/i18n/pl.json index 2d1d0a47..1019ce12 100644 --- a/ui/src/lib/i18n/pl.json +++ b/ui/src/lib/i18n/pl.json @@ -1,26 +1,227 @@ { - "nav.dashboard": "Panel sterowania", - "nav.sources": "Źródła", - "nav.meters": "Wskaźniki dźwięku", - "nav.routing": "Routing", - "nav.renditions": "Opcje jakości", - "nav.playout": "Playout", - "nav.library": "Biblioteka", - "nav.clips": "Fragmenty", - "nav.chat": "Czat", - "nav.jobs": "Zadania", - "nav.automation": "Automatyzacja", - "nav.recordings": "Nagrania", - "nav.monitoring": "Monitorowanie", - "nav.settings": "Ustawienia", - "chrome.toggleNav": "Pokaż/ukryj menu", + "auth.accountCreated": "Utworzono konto administratora.", + "auth.confirmPassword": "Potwierdź hasło", + "auth.createAccount": "Utwórz konto", + "auth.firstRun": "Konfiguracja początkowa", + "auth.firstRunDesc": "Wybierz dane logowania administratora dla tego serwera. Nie ma odzyskiwania konta, więc przechowuj je w bezpiecznym miejscu.", + "auth.password": "Hasło", + "auth.passwordMismatch": "Hasła nie są zgodne.", + "auth.signIn": "Zaloguj się", + "auth.signInDesc": "Podaj swoje dane logowania administratora.", + "auth.somethingWentWrong": "Coś poszło nie tak.", + "auth.tokenCreated": "Utworzono token API", + "auth.tokenRevoked": "Unieważniono token API", + "auth.username": "Nazwa użytkownika", + "auto.action": "Akcja", + "auto.adminPasswordChanged": "Zmieniono hasło administratora", + "auto.chosenWeekdays": "Wybrane dni tygodnia", + "auto.coalesced": "Połączone", + "auto.coalescingWindow": "Okno łączenia (sekundy)", + "auto.couldNotChangeTheRule": "Nie udało się zmienić reguły.", + "auto.couldNotChangeTheSchedule": "Nie udało się zmienić harmonogramu.", + "auto.couldNotDeleteTheRule": "Nie udało się usunąć reguły.", + "auto.couldNotDeleteTheSchedule": "Nie udało się usunąć harmonogramu.", + "auto.couldNotLoadAutomationSettings": "Nie udało się wczytać ustawień automatyzacji.", + "auto.couldNotSaveTheRule": "Nie udało się zapisać reguły.", + "auto.couldNotSaveTheSchedule": "Nie udało się zapisać harmonogramu.", + "auto.createRule": "Utwórz regułę", + "auto.createSchedule": "Utwórz harmonogram", + "auto.days": "Dni", + "auto.delete": "Usuń", + "auto.deleteRule": "Usuń regułę", + "auto.deleteRuleDesc": "Reguła przestaje się uruchamiać. Jej punkt końcowy webhooka pozostaje nietknięty, a regułę możesz utworzyć ponownie.", + "auto.deleteRuleTitle": "Usunąć tę regułę alertu?", + "auto.deleteSchedule": "Usuń harmonogram", + "auto.deleteScheduleDesc": "Harmonogram przestaje działać. To, co już uruchomił, pozostaje bez zmian.", + "auto.deleteScheduleTitle": "Usunąć ten harmonogram?", + "auto.delivery": "Dostarczanie", + "auto.destinationFallingBehindRealtime": "Cel zostaje w tyle za czasem rzeczywistym", + "auto.destinationKeepingUpAgain": "Cel znów nadąża", + "auto.destinationRecovered": "Cel przywrócony", + "auto.destinationWentDown": "Cel przestał działać", + "auto.destinations": "Cele", + "auto.dropped": "Odrzucone", + "auto.edit": "Edytuj", + "auto.editRule": "Edytuj regułę alertu", + "auto.editSchedule": "Edytuj harmonogram", + "auto.endpoint": "Punkt końcowy", + "auto.evAudioClipping": "Przesterowanie dźwięku", + "auto.evClipCaptured": "Przechwycono klip", + "auto.evDiskLow": "Kończy się miejsce na dysku", + "auto.evDiskRecovered": "Dysk przywrócony", + "auto.evIngestLost": "Utracono wejście", + "auto.evIngestRecovered": "Wejście przywrócone", + "auto.evSettingsChanged": "Zmieniono ustawienia", + "auto.evSignedIn": "Zalogowano", + "auto.events": "Zdarzenia", + "auto.everyDay": "Codziennie", + "auto.failed": "Niepowodzenie", + "auto.failoverSwitchedSource": "Przełączanie awaryjne zmieniło źródło", + "auto.fired": "uruchomione", + "auto.firesAt": "Uruchamia się o", + "auto.lastDelivery": "Ostatnie dostarczenie", + "auto.lastSweep": "Ostatni przegląd", + "auto.latenessAllowance": "Dopuszczalne opóźnienie (sekundy)", + "auto.loudnessBackInCompliance": "Głośność znów zgodna z normą", + "auto.loudnessOutOfCompliance": "Głośność niezgodna z normą", + "auto.minGap": "Minimalna przerwa między wiadomościami (sekundy)", + "auto.minSeverity": "Minimalna waga", + "auto.missedNote": "Wystąpienie pominięte z powodu wyłączonego serwera zostaje oznaczone jako obsłużone i pominięte, zamiast uruchomione z opóźnieniem — transmisja, która sama startuje cztery godziny za późno, jest gorsza niż taka, która nie wystartowała wcale.", + "auto.name": "Nazwa", + "auto.newRule": "Nowa reguła alertu", + "auto.newSchedule": "Nowy harmonogram", + "auto.next": "Następne", + "auto.nothingFired": "Nic się jeszcze nie uruchomiło. Harmonogram sprawdza zadania co 20 sekund.", + "auto.once": "Jednorazowo", + "auto.payloadFormat": "Format payloadu", + "auto.pending": "Oczekujące", + "auto.repeatedFailedSignIns": "Powtarzające się nieudane logowania", + "auto.repeats": "Powtarzanie", + "auto.retries": "Ponowne próby", + "auto.rule": "Reguła", + "auto.ruleCreated": "Reguła alertu utworzona. Wyślij do niej test.", + "auto.ruleDeleted": "Reguła alertu usunięta.", + "auto.rulePlaceholder": "Discord — alerty programu", + "auto.ruleSaved": "Reguła alertu zapisana.", + "auto.saveRule": "Zapisz regułę", + "auto.saveSchedule": "Zapisz harmonogram", + "auto.schedule": "Harmonogram", + "auto.scheduleCreated": "Harmonogram utworzony.", + "auto.scheduleDeleted": "Harmonogram usunięty.", + "auto.schedulePlaceholder": "Program w dni powszednie", + "auto.scheduleSaved": "Harmonogram zapisany.", + "auto.schedules": "Harmonogramy", + "auto.secretMasked": "Pokazywane w formie zamaskowanej, ponieważ sekret znajduje się w ścieżce. Zostaw je bez zmian, aby zachować bieżący punkt końcowy, albo wklej nowy adres URL, aby go zastąpić.", + "auto.sendTest": "Wyślij alert testowy", + "auto.sendTestTitle": "Wyślij teraz alert testowy", + "auto.sent": "Wysłane", + "auto.skipped": "pominięte", + "auto.startDestinations": "Uruchom cele", + "auto.startPlaylist": "Uruchom playlistę", + "auto.stopDestinations": "Zatrzymaj cele", + "auto.stopPlaylist": "Zatrzymaj playlistę", + "auto.subscribedTo": "Subskrybowane", + "auto.subtitle": "Webhooki, które informują, że coś się zepsuło, i harmonogramy, które prowadzą program bez ciebie.", + "auto.theEndpointDidNotAccept": "Punkt końcowy nie przyjął wiadomości testowej.", + "auto.time": "Godzina", + "auto.timeZone": "Strefa czasowa", + "auto.title": "Automatyzacja", + "auto.tzPlaceholder": "UTC", + "auto.webhookHint": "Discord i Slack udostępniają go w ustawieniach kanału.", + "auto.webhookPlaceholder": "https://discord.com/api/webhooks/…", + "auto.webhookRules": "Reguły webhooków", + "auto.webhookUrl": "Adres URL webhooka", + "auto.weekFri": "Pt", + "auto.weekMon": "Pon", + "auto.weekSat": "Sob", + "auto.weekSun": "Nd", + "auto.weekThu": "Czw", + "auto.weekTue": "Wt", + "auto.weekWed": "Śr", + "auto.when": "Kiedy", + "chatpage.allFiltered": "Wszystkie platformy z wiadomościami są odfiltrowane.", + "chatpage.couldNotDeleteThatMessage": "Nie udało się usunąć tej wiadomości.", + "chatpage.deleteNote": "Usunięcie kasuje wiadomość na platformie, która ją wydała. Platforma, na której polyemesis nie może usuwać, mówi o tym i wskazuje, gdzie da się to zrobić — akcja nigdy nie jest ukrywana na wyczucie.", + "chatpage.droppedTitle": "Pokazane na żywo, ale zapis nie nadążył i historia je utraciła.", + "chatpage.dupTitle": "Ta sama wiadomość widziana dwukrotnie — nasze własne echo albo powtórka z platformy.", + "chatpage.noAccount": "Czat działa, ale nie podłączono żadnego konta platformy. Podłącz je w Ustawienia → Poświadczenia platform.", + "chatpage.notRunning": "Na tym serwerze nie działa czat. Historia pod osią czasu to zapis sprzed, a pole wysyłania pozostaje wyłączone, dopóki nie zostanie podłączona platforma.", + "chatpage.received": "Odebrane", + "chatpage.reload": "Odśwież", + "chatpage.stored": "Zapisane", + "chatpage.storedTitle": "Nic nie jest podłączone; to zapisana historia.", + "chatpage.subtitle": "Wszystkie platformy na jednej osi czasu, z jednym polem wysyłania.", + "chatpage.timeline": "Oś czasu", + "chatpage.title": "Czat", "chrome.ingest": "Ingest", + "chrome.language": "Język", "chrome.liveCount": "{count} transmisji na żywo", + "chrome.signOut": "Wyloguj się", "chrome.socketOffline": "offline", "chrome.socketOfflineHint": "Aktualizacje na żywo zostały odłączone; ponawianie próby", - "chrome.signOut": "Wyloguj się", - "chrome.language": "Język", + "chrome.toggleNav": "Pokaż/ukryj menu", + "clipedit.back33": "Cofnij o 33 ms", + "clipedit.clickLine": "Kliknij wiersz, aby przejść w to miejsce. Zaznacz kilka, a punkt początkowy i końcowy przesuną się do zaznaczonych słów.", + "clipedit.clipThisLine": "Utnij klip z tego wiersza", + "clipedit.clipThisLineTitle": "Ustaw punkt początkowy i końcowy wokół tego wiersza", + "clipedit.copied": "Skopiowano", + "clipedit.copiesEveryPacketSecondsBit": "Kopiuje każdy pakiet. Sekundy, bit w bit, a początek cofa się do najbliższej klatki kluczowej.", + "clipedit.couldNotOpenThatRecording": "Nie udało się otworzyć tego nagrania.", + "clipedit.couldNotQueueTheExport": "Nie udało się dodać eksportu do kolejki.", + "clipedit.cut": "Cięcie", + "clipedit.downloadClip": "Pobierz klip", + "clipedit.exportQueued": "Eksport dodany do kolejki. Zostanie wykonany, gdy maszyna będzie miała wolne zasoby.", + "clipedit.exports": "Eksporty", + "clipedit.fast": "Szybko", + "clipedit.filterTranscript": "Filtruj transkrypcję", + "clipedit.findSaid": "Znajdź, co powiedziano", + "clipedit.forward33": "Do przodu o 33 ms", + "clipedit.in": "Początek", + "clipedit.keepAllTracks": "Zachowaj każdą ścieżkę dźwiękową", + "clipedit.keepsEveryAudioTrackIn": "Zachowuje każdą ścieżkę dźwiękową w dowolnym kodeku. To, co zapisuje rejestrator.", + "clipedit.markIn": "I — ustaw początek w pozycji głowicy", + "clipedit.markOut": "O — ustaw koniec w pozycji głowicy", + "clipedit.name": "Nazwa", + "clipedit.noKeyframes": "Nie udało się odczytać pozycji klatek kluczowych, więc nikt nie powie, gdzie naprawdę zacznie się to cięcie. Będzie działać — własne przewijanie FFmpega trafia na klatkę kluczową — ale początek może wypaść nawet o jeden GOP wcześniej, a ta strona nie pokaże ci o ile.", + "clipedit.noTranscript": "Dla tego nagrania nie sporządzono jeszcze transkrypcji. Każdy mikrofon był nagrywany na własnej ścieżce, więc transkrypcja daje ci nazwy mówców bez modelu diaryzacji — a potem możesz wycinać klipy według tego, co powiedziano.", + "clipedit.nothingExported": "Nic jeszcze nie wyeksportowano.", + "clipedit.out": "Koniec", + "clipedit.pickAtLeastOneAudio": "Wybierz co najmniej jedną ścieżkę dźwiękową albo weź wszystkie.", + "clipedit.playFaster": "L — odtwarzaj, ponownie dla przyspieszenia", + "clipedit.precise": "Precyzyjnie", + "clipedit.queueNote": "Eksport trafia do kolejki, a nie jest wykonywany tutaj: ustępuje transmisji na żywo, więc cięcie wykonane w trakcie emisji czeka, zamiast zabierać zasoby potrzebne transmisji.", + "clipedit.reEncodesOnlyTheLeading": "Przekodowuje tylko początkowy niepełny GOP, dzięki czemu start jest dokładny. Reszta nadal jest kopiowana.", + "clipedit.reencoded": "Przekodowany", + "clipedit.shortcuts": "J / K / L sterują przewijaniem, I i O ustawiają początek i koniec, strzałki przesuwają o 33 ms, a strzałki z Shiftem o sekundę. Drobny krok to jedna klatka przy 30 kl./s; rzeczywista częstotliwość tego nagrania nie jest zindeksowana, więc jest podana w milisekundach, zamiast zgadywana.", + "clipedit.shuttleBack": "J — przewijaj do tyłu", + "clipedit.startMoved": "Przesunięcie początku", + "clipedit.starts": "Zaczyna się", + "clipedit.stop": "K — zatrzymaj", + "clipedit.thatCutCouldNotBe": "Nie udało się zaplanować tego cięcia.", + "clipedit.thatIsNotARecording": "To nie jest identyfikator nagrania.", + "clipedit.thatRecordingIsNotAvailable": "To nagranie jest niedostępne.", + "clipedit.title": "Edytor klipów", + "clipedit.whatASocialPlatformWill": "To, co przyjmie platforma społecznościowa. Przetrwa mniej kodeków.", + "clipedit.zoomIn": "Powiększ", + "clipedit.zoomOut": "Pomniejsz", + "clips.bitrate": "Przepływność", + "clips.buffer": "Bufor", + "clips.bufferLiveNote": "Przełącznik bufora działa na żywo i nie jest zapisywanym ustawieniem: po ponownym uruchomieniu znów jest wyłączony.", + "clips.captured": "Przechwycono", + "clips.ceiling": "Pułap", + "clips.clipsKept": "Zachowane klipy", + "clips.couldNotCaptureAClip": "Nie udało się przechwycić klipu.", + "clips.couldNotChangeTheClip": "Nie udało się zmienić bufora klipów.", + "clips.couldNotDeleteTheClip": "Nie udało się usunąć klipu.", + "clips.couldNotLoadClips": "Nie udało się wczytać klipów.", + "clips.covers": "Obejmuje", + "clips.customLength": "Własna długość klipu w sekundach", + "clips.delete": "Usuń", + "clips.deleteDescription": "Klip to zwykle dokładnie ten materiał, który chciałeś zachować. To usuwa plik; nagranie, z którego pochodzi, pozostaje nietknięte.", + "clips.deleteTitle": "Usunąć ten klip?", + "clips.deleted": "Klip usunięty.", + "clips.download": "Pobierz", + "clips.everythingHeld": "Wszystko, co trzyma bufor", + "clips.file": "Plik", + "clips.held": "Przechowywane", + "clips.inMemory": "W pamięci", + "clips.keepBuffer": "Utrzymuj przesuwny bufor", + "clips.keyframes": "Klatki kluczowe", + "clips.length": "Długość", + "clips.none": "brak", + "clips.off": "wyłączone", + "clips.onDisk": "Na dysku", + "clips.seen": "wykryto", + "clips.settings": "Ustawienia", + "clips.size": "Rozmiar", + "clips.subtitle": "Przesuwny bufor tego, co właśnie poszło na antenę. Przechwyć to po fakcie — nic nie musi być przygotowane wcześniej.", + "clips.theClipDoesNotStart": "Klip nie zaczyna się na klatce kluczowej.", + "clips.title": "Klipy", + "clips.waitingForStream": "oczekiwanie na strumień", + "clips.window": "Okno", + "clips.windowSeconds": "Okno (sekundy)", "common.add": "Dodaj", + "common.apply": "Zastosuj", "common.cancel": "Anuluj", "common.close": "Zamknij", "common.confirm": "Zatwierdź", @@ -28,110 +229,921 @@ "common.copy": "Kopiuj", "common.delete": "Usuń", "common.disabled": "Wyłączone", + "common.discard": "Odrzuć", "common.edit": "Edytuj", "common.enabled": "Włączone", "common.loading": "Ładowanie…", + "common.moreInfo": "Więcej informacji", "common.refresh": "Odśwież", "common.restart": "Uruchom ponownie", "common.retry": "Spróbuj ponownie", "common.save": "Zapisz", "common.start": "Rozpocznij", "common.stop": "Zatrzymaj", - "state.live": "Na żywo", - "state.reconnecting": "Ponawianie połączenia", - "state.starting": "Uruchamianie", - "state.failed": "Niepowodzenie", - "state.stopped": "Zatrzymano", - "state.offline": "Poza siecią", - "hooks.title": "Webhooki cyklu życia", - "hooks.new": "Nowy webhook", - "hooks.empty": "Brak webhooków. Dodaj jeden, a polyemesis wyśle metodą POST podpisane ciało JSON w chwili, gdy stream się zacznie lub skończy albo cel wstanie lub padnie — jedna dostawa na przejście, w kolejności, żeby skrypt mógł zareagować.", - "hooks.colWebhook": "Webhook", - "hooks.colEndpoint": "Punkt końcowy", - "hooks.colTriggers": "Subskrybuje", - "hooks.colEnabled": "Wł.", - "hooks.signed": "podpisany", - "hooks.unsigned": "NIEPODPISANY", + "dash.audioTracks": "Ścieżki dźwiękowe", + "dash.bitrate": "Przepływność", + "dash.broadcastSettings": "Ustawienia transmisji", + "dash.categoryPlaceholder": "Muzyka", + "dash.clipboardUnavailable": "Schowek nie jest dostępny w tej domenie.", + "dash.connectPrompt": "Podłącz konto YouTube, Twitch, Facebook lub Kick w Ustawienia → Poświadczenia platform, aby ustawić tytuł, opis i kategorię transmisji na wszystkich platformach naraz. Każda przyjmuje inny podzbiór — formularz wskazuje który i wysyła to, co może.", + "dash.copyIngestUrl": "Kopiuj adres URL wejścia", + "dash.couldNotLoadTheDestination": "Nie udało się wczytać celu.", + "dash.couldNotPushTheMetadata": "Nie udało się wysłać metadanych.", + "dash.couldNotReorderTheDestinations": "Nie udało się zmienić kolejności celów.", + "dash.deleteDest": "Usuń cel", + "dash.deleteDestDescription": "To usuwa cel i jego profil routingu. Platforma pozostaje nietknięta i możesz dodać cel z powrotem — ale skonfigurowany dla niego routing dźwięku przepada.", + "dash.descriptionPlaceholder": "Czym jest ta transmisja.", + "dash.destDeleted": "Cel usunięty.", + "dash.disabled": "wyłączone", + "dash.everyConnectedAccount": "każde połączone konto", + "dash.idle": "bezczynne", + "dash.ingestUrlCopied": "Skopiowano adres URL wejścia.", + "dash.keyRefreshed": "Klucz strumienia odświeżony z platformy.", + "dash.leaveUnchanged": "Pozostaw bez zmian", + "dash.leaveUnchangedNote": "„Pozostaw bez zmian” nie wysyła niczego, więc platforma zachowuje to, co ma. Wybranie Włączone lub Wyłączone zapisuje tę wartość.", + "dash.metaCategory": "Kategoria", + "dash.metaDescription": "Opis", + "dash.metaTags": "Tagi", + "dash.metaTitle": "Tytuł", + "dash.noDestinations": "Brak celów. Dodaj jeden, a następnie wybierz, które ścieżki dźwiękowe otrzymuje.", + "dash.off": "Wyłączone", + "dash.pipeline": "Potok", + "dash.pushToPlatforms": "Wyślij na platformy", + "dash.pushing": "Wysyłanie…", + "dash.reconnects": "Ponowne połączenia", + "dash.relaySubscribers": "Subskrybenci przekaźnika", + "dash.renditions": "Warianty", + "dash.scheduledStart": "Zaplanowany start", + "dash.subtitle": "Jedno wejście, rozgałęzienie ze wspólnym obrazem i osobnym dźwiękiem dla każdego celu.", + "dash.tagsPlaceholder": "na żywo, house, set dj", + "dash.theConnectedAccount": "połączone konto", + "dash.title": "Panel sterowania", + "dash.titlePlaceholder": "Piątkowy set wieczorny", + "dash.uptime": "Czas działania", + "dash.youtubeStopsAcceptingTheseOnce": "YouTube przestaje je przyjmować, gdy transmisja opuści stan „created” lub „ready”.", + "dest.audience": "Odbiorcy", + "dest.audioBitrate": "Bitrate audio", + "dest.audioCodec": "Kodek audio", + "dest.channels": "Kanały", + "dest.compliance": "Zgodność", + "dest.connectedAccount": "Połączone konto", + "dest.contentLabels": "Etykiety treści", + "dest.copySource": "Kopiuj wideo źródłowe", + "dest.createVariant": "Utwórz ten wariant", + "dest.customiseSummary": "Dostosuj dla tego celu", + "dest.fbBackupCost": "Podwaja przepustowość wysyłania tego celu.", + "dest.fbBackupHelp": "Wysyła drugą kopię tego strumienia do zapasowego wejścia Facebooka, aby zerwane połączenie nie zerwało transmisji.", + "dest.fbBackupLabel": "Publikuj strumień zapasowy", + "dest.fbBackupReconnect": "Włączenie tej opcji raz ponownie łączy strumień — włącz ją więc, zanim wejdziesz na żywo.", + "dest.fbBoxIntro": "Wysyłane tym samym wywołaniem tworzenia co Odbiorcy, więc obowiązuje ta sama zasada: stosowane raz, gdy transmisja się rozpoczyna, i całkowicie pomijane, gdy jest puste.", + "dest.fbBoxTitle": "Publikowanie krzyżowe i przycisk darowizny na Facebooku", + "dest.fbCrosspostAdd": "Dodaj Stronę", + "dest.fbCrosspostAlsoPost": "Opublikuj także", + "dest.fbCrosspostHelp": "Numeryczne ID Strony z konsoli samego Facebooka (ustawienia Strony lub graph.facebook.com/me/accounts, gdy jesteś zalogowany jako Strona) — nie ma tu wyszukiwania, więc wklej id, a nie nazwę czy adres URL. „Opublikuj także” publikuje jako ta Strona, zamiast tylko udostępnić jej transmisję; wyłączone, dzieje się wyłącznie ciche udostępnienie.", + "dest.fbCrosspostLabel": "Publikowanie krzyżowe na Stronach", + "dest.fbCrosspostPageId": "ID Strony", + "dest.fbCrosspostRemove": "Usuń tę Stronę", + "dest.fbDonateHelp": "To również nieprzejrzyste id z konsoli zbiórek Facebooka, a nie nazwa organizacji do wyszukania. Zostaw puste, aby nie dołączać żadnego przycisku darowizny.", + "dest.fbDonateLabel": "ID organizacji dla przycisku darowizny", + "dest.fbDonatePlaceholder": "ID organizacji", + "dest.fillFromPlatform": "Wypełnij tym, co publikuje ta platforma", + "dest.friends": "Znajomi", + "dest.friendsOfFriends": "Znajomi znajomych", + "dest.giveUpAfter": "Poddaj się po (próbach)", + "dest.leaveOnFacebook": "Zostaw bez zmian na Facebooku", + "dest.leaveOnYouTube": "Zostaw bez zmian w YouTube", + "dest.madeForKids": "Stworzone dla dzieci (COPPA)", + "dest.muxQueue": "Kolejka muxowania (pakiety)", + "dest.muxQueuePair": "Te dwa ustawienia idą w parze: FFmpeg stosuje limit pakietów dopiero wtedy, gdy kolejka przekroczy próg w bajtach, więc sam próg nic nie daje i zostanie odrzucony. Zwiększ oba, jeśli cel zgłasza błędy przeplotu — ścieżka audio ma tu zmienne opóźnienie, ponieważ normalizacja głośności czyta z wyprzedzeniem.", + "dest.name": "Nazwa", + "dest.namePlaceholder": "YouTube — kanał główny", + "dest.notLinked": "Niepołączone", + "dest.onlyMe": "Tylko ja", + "dest.platform": "Platforma", + "dest.private": "Prywatny", + "dest.public": "Publiczny", + "dest.reconnecting": "Ponowne łączenie", + "dest.retryAfter": "Ponów po (sekundy)", + "dest.sharedEncodeAria": "Współdzielone kodowanie wideo", + "dest.socketTimeout": "Limit czasu gniazda (sekundy)", + "dest.streamKeyLabel": "Klucz strumienia", + "dest.transport": "Transport", + "dest.transportFile": "Plik lokalny", + "dest.transportRtmp": "RTMP / RTMPS", + "dest.unlisted": "Niepubliczny", + "dest.urlFromDashboard": "URL z panelu", + "dest.useSharedEncode": "Użyj współdzielonego kodowania wideo", + "dest.varBitrate": "Bitrate wideo (kbps)", + "dest.varFps": "Liczba klatek", + "dest.varHeight": "Wysokość", + "dest.varSourceHint": "źródło", + "dest.varWidth": "Szerokość", + "dest.variantWarning": "Uruchamia drugie kodowanie z Twojego źródła, na podstawie „{name}”. To nie jest darmowy wariant i nie zmienia tego, co otrzymują pozostałe cele tego kodowania. Zostaw pole puste, aby zachować wartość źródła.", + "dest.videoTreatment": "Obróbka wideo", + "dest.visibility": "Widoczność", + "hooks.allTriggers": "wszystko", "hooks.attempts": "{count} prób", + "hooks.attemptsLabel": "Próby", "hooks.attemptsOne": "{count} próba", - "hooks.timeout": "limit {seconds}s", - "hooks.allTriggers": "wszystko", - "hooks.triggerCount": "{count} wyzwalaczy", - "hooks.triggerCountOne": "{count} wyzwalacz", - "hooks.enableAria": "Włącz {name}", + "hooks.colEnabled": "Wł.", + "hooks.colEndpoint": "Punkt końcowy", + "hooks.colTriggers": "Subskrybuje", + "hooks.colWebhook": "Webhook", + "hooks.create": "Utwórz", + "hooks.created": "Webhook utworzony.", + "hooks.deleteBody": "Natychmiast przestaje dostarczać. Punkt końcowy pozostaje nietknięty i możesz utworzyć hook ponownie — ale jego klucza podpisującego nie da się odzyskać.", + "hooks.deleteConfirm": "Usuń webhook", + "hooks.deleteFailed": "Nie udało się usunąć hooka.", + "hooks.deleteTitle": "Usunąć ten webhook?", + "hooks.deleted": "Webhook usunięty.", "hooks.deliveries": "Ostatnie dostawy", - "hooks.sendTest": "Wyślij dostawę testową", - "hooks.sendTestTitle": "Wyślij teraz dostawę testową", - "hooks.secretTitle": "Klucz podpisujący — skopiuj go teraz, nie da się go pokazać ponownie", - "hooks.secretCopied": "Skopiowałem go", - "hooks.httpStatus": "HTTP {status}", - "hooks.noResponse": "brak odpowiedzi", + "hooks.deliveriesFailed": "Nie udało się odczytać ostatnich dostaw.", + "hooks.deliveryTitle": "Dostarczanie", + "hooks.dialogIntro": "Jeden podpisany POST na przejście, w kolejności. W odróżnieniu od alertu nic nie jest łączone ani wygładzane — skrypt dostaje każde zbocze.", "hooks.durationMs": "{ms} ms", + "hooks.editTitle": "Edytuj webhook", + "hooks.empty": "Brak webhooków. Dodaj jeden, a polyemesis wyśle metodą POST podpisane ciało JSON w chwili, gdy stream się zacznie lub skończy albo cel wstanie lub padnie — jedna dostawa na przejście, w kolejności, żeby skrypt mógł zareagować.", + "hooks.enableAria": "Włącz {name}", + "hooks.httpStatus": "HTTP {status}", + "hooks.loadFailed": "Nie udało się wczytać webhooków.", + "hooks.name": "Nazwa", + "hooks.new": "Nowy webhook", + "hooks.newTitle": "Nowy webhook", "hooks.noDeliveries": "Jeszcze nic nie dostarczono. Dostawy pojawią się tutaj wraz z kolejnymi przejściami.", - "hooks.deliveryTitle": "Dostarczanie", - "hooks.statSent": "Wysłane", - "hooks.statFailed": "Nieudane", + "hooks.noResponse": "brak odpowiedzi", + "hooks.payloadNote": "Payload {version}. Podpis w {header}, po znaczniku czasu i ciele.", + "hooks.retriesHint": "Ponowienia blokują własną kolejkę tego punktu końcowego, bo obietnicą jest kolejność. Kod 4xx nigdy nie jest ponawiany — punkt końcowy, który mówi, że żądanie jest złe, powie to znowu.", + "hooks.saveFailed": "Nie udało się zapisać hooka.", + "hooks.secretCopied": "Skopiowałem go", + "hooks.secretTitle": "Klucz podpisujący — skopiuj go teraz, nie da się go pokazać ponownie", + "hooks.sendTest": "Wyślij dostawę testową", + "hooks.sendTestTitle": "Wyślij teraz dostawę testową", + "hooks.signed": "podpisany", + "hooks.statDropped": "Odrzucone", "hooks.statEndpoints": "Punkty końcowe", + "hooks.statFailed": "Nieudane", "hooks.statQueued": "W kolejce", - "hooks.statDropped": "Odrzucone", "hooks.statRetries": "Ponowienia", - "hooks.payloadNote": "Payload {version}. Podpis w {header}, po znaczniku czasu i ciele.", - "hooks.deleteTitle": "Usunąć ten webhook?", - "hooks.deleteBody": "Natychmiast przestaje dostarczać. Punkt końcowy pozostaje nietknięty i możesz utworzyć hook ponownie — ale jego klucza podpisującego nie da się odzyskać.", - "hooks.deleteConfirm": "Usuń webhook", - "hooks.editTitle": "Edytuj webhook", - "hooks.newTitle": "Nowy webhook", - "hooks.dialogIntro": "Jeden podpisany POST na przejście, w kolejności. W odróżnieniu od alertu nic nie jest łączone ani wygładzane — skrypt dostaje każde zbocze.", - "hooks.name": "Nazwa", - "hooks.url": "URL punktu końcowego", - "hooks.urlMasked": "Zapisany URL nigdy nie jest pokazywany w całości. Zostaw zamaskowaną wartość, aby go zachować, albo wpisz nowy URL, aby go zastąpić.", - "hooks.triggers": "Wyzwalacze", - "hooks.triggersNoneHint": "Brak zaznaczenia oznacza każdy wyzwalacz, łącznie z tymi dodanymi przez przyszłe wydanie.", + "hooks.statSent": "Wysłane", + "hooks.testFailed": "Punkt końcowy nie przyjął dostawy testowej.", + "hooks.timeout": "limit {seconds}s", "hooks.timeoutLabel": "Limit czasu (s)", - "hooks.attemptsLabel": "Próby", - "hooks.retriesHint": "Ponowienia blokują własną kolejkę tego punktu końcowego, bo obietnicą jest kolejność. Kod 4xx nigdy nie jest ponawiany — punkt końcowy, który mówi, że żądanie jest złe, powie to znowu.", - "hooks.create": "Utwórz", - "hooks.triggerIngestPublished": "Stream wystartował", - "hooks.triggerIngestDisconnected": "Stream zatrzymany", - "hooks.triggerDestinationUp": "Cel na żywo", - "hooks.triggerDestinationDown": "Cel zatrzymany", - "hooks.loadFailed": "Nie udało się wczytać webhooków.", + "hooks.title": "Webhooki cyklu życia", "hooks.toggleFailed": "Nie udało się zmienić hooka.", - "hooks.deleted": "Webhook usunięty.", - "hooks.deleteFailed": "Nie udało się usunąć hooka.", - "hooks.testFailed": "Punkt końcowy nie przyjął dostawy testowej.", - "hooks.deliveriesFailed": "Nie udało się odczytać ostatnich dostaw.", + "hooks.triggerCount": "{count} wyzwalaczy", + "hooks.triggerCountOne": "{count} wyzwalacz", + "hooks.triggerDestinationDown": "Cel zatrzymany", + "hooks.triggerDestinationUp": "Cel na żywo", + "hooks.triggerIngestDisconnected": "Stream zatrzymany", + "hooks.triggerIngestPublished": "Stream wystartował", + "hooks.triggers": "Wyzwalacze", + "hooks.triggersNoneHint": "Brak zaznaczenia oznacza każdy wyzwalacz, łącznie z tymi dodanymi przez przyszłe wydanie.", + "hooks.unsigned": "NIEPODPISANY", "hooks.updated": "Webhook zaktualizowany.", - "hooks.created": "Webhook utworzony.", - "hooks.saveFailed": "Nie udało się zapisać hooka.", - "playlist.title": "Playlista", + "hooks.url": "URL punktu końcowego", + "hooks.urlMasked": "Zapisany URL nigdy nie jest pokazywany w całości. Zostaw zamaskowaną wartość, aby go zachować, albo wpisz nowy URL, aby go zastąpić.", + "jobs.alwaysKeepNewest": "Zawsze zachowuj najnowsze", + "jobs.automaticModel": "Automatycznie (wybrany na podstawie tego sprzętu)", + "jobs.backToDefault": "Wróć do domyślnego trybu dla tego rodzaju", + "jobs.batteryFloor": "Minimalny poziom baterii %", + "jobs.broadcastGoing": "Trwa transmisja; odłożona praca ustępuje jej miejsca.", + "jobs.cancel": "Anuluj", + "jobs.cannotPause": "Te rodzaje powinny zostać wstrzymane, ale nie mogą, więc zamiast tego są kończone.", + "jobs.completed": "Ukończone", + "jobs.concurrency": "Współbieżność", + "jobs.couldNotReadTheJob": "Nie udało się odczytać kolejki zadań.", + "jobs.couldNotSaveThePolicy": "Nie udało się zapisać zasad.", + "jobs.cpuCeiling": "Pułap CPU %", + "jobs.cpuResume": "Wznowienie przy CPU %", + "jobs.defaultMode": "Tryb domyślny", + "jobs.defaultModel": "Model domyślny", + "jobs.deferred": "Odłożone", + "jobs.failed": "Niepowodzenie", + "jobs.finished": "Zakończone", + "jobs.global": "Globalne", + "jobs.gpuGateNote": "Dotyczy to tylko rodzajów oznaczonych jako korzystające z GPU.", + "jobs.history": "Historia", + "jobs.historyRetention": "Przechowywanie historii", + "jobs.holdDeferredWorkBackOn": "Wstrzymuj odłożone zadania na rozładowującym się laptopie poniżej tej wartości. 0 to wyłącza.", + "jobs.hostCpuLoad": "Obciążenie CPU hosta", + "jobs.howManyJobsMayRun": "Ile zadań może działać naraz. Jedno, bo druga transkodacja daje przepustowość, o którą nikt nie prosił, przy ryzyku, na które nikt nie przystał.", + "jobs.inQueue": "W kolejce", + "jobs.job": "Zadanie", + "jobs.keepForDays": "Przechowuj przez (dni)", + "jobs.machineSafety": "Bezpieczeństwo maszyny. To jedyna bramka, której przestrzega także praca w czasie rzeczywistym.", + "jobs.niceLevel": "Poziom nice", + "jobs.noFinished": "Brak zakończonych zadań.", + "jobs.noModels": "W katalogu modeli nie znaleziono żadnych plików modeli. Tryb automatyczny pobierze ten, który wybierze; wskazanie tutaj konkretnego modelu zadziała dopiero wtedy, gdy ten plik będzie istniał.", + "jobs.nothingNewStartsAboveThis": "Powyżej tej wartości nic nowego się nie uruchamia. 0 wyłącza tę bramkę.", + "jobs.nothingQueued": "Nic w kolejce. Postprodukcję zleca się z biblioteki.", + "jobs.nothingWasOldEnoughTo": "Nic nie było na tyle stare, by je usunąć. Najpierw obniż czas przechowywania w Zasadach.", + "jobs.pauseAll": "Wstrzymaj wszystko", + "jobs.policy": "Zasady", + "jobs.policySaved": "Zasady zasobów zapisane.", + "jobs.progress": "Postęp", + "jobs.queuePaused": "Kolejka wstrzymana.", + "jobs.queueResumed": "Kolejka wznowiona.", + "jobs.queued": "W kolejce", + "jobs.recording": "Nagrywanie", + "jobs.releasedItWillStartAs": "Zwolnione. Ruszy, gdy tylko zwolni się miejsce.", + "jobs.removeFromHistory": "Usuń z historii", + "jobs.removeWindow": "Usuń okno", + "jobs.resumeAll": "Wznów wszystko", + "jobs.retry": "Spróbuj ponownie", + "jobs.retryTitle": "Uzbrój ponownie ze świeżym limitem prób", + "jobs.runNow": "Uruchom teraz", + "jobs.runNowTitle": "Uruchom to teraz, ignorując zasady zasobów", + "jobs.running": "W trakcie", + "jobs.savedTheConcurrencyChangeTakes": "Zapisano. Zmiana współbieżności zacznie obowiązywać po restarcie serwera — kolejka naprawia to przy starcie.", + "jobs.state": "Stan", + "jobs.stopsEverythingRealtimeIncludedA": "Zatrzymuje wszystko, łącznie z pracą w czasie rzeczywistym: procesor, który ogranicza takt, już zaczął pogarszać transmisję. 0 to wyłącza.", + "jobs.subtitle": "Praca w tle: transkrypcja, pliki proxy, miniatury i kompresja archiwum.", + "jobs.thatDidNotWork": "To nie zadziałało.", + "jobs.thermalCeiling": "Pułap temperatury °C", + "jobs.thisManyFinishedJobsSurvive": "Tyle zakończonych zadań pozostaje niezależnie od ich wieku.", + "jobs.title": "Zadania", + "jobs.transcription": "Transkrypcja", + "jobs.unsavedChanges": "Niezapisane zmiany.", + "jobs.whereTheCeilingReleasesThe": "Miejsce, w którym pułap zwalnia. Odstęp to histereza, która nie pozwala bramce oscylować.", + "jobs.whisperMissing": "whisper.cpp nie jest tu zainstalowany, więc na razie nie ma to efektu. Warto to jednak ustawić: wybór zacznie obowiązywać, gdy tylko zostanie zainstalowany.", + "jobs.why": "Dlaczego", + "jobs.windowEnd": "Koniec okna", + "jobs.windowStart": "Początek okna", + "jobs.yieldNote": "Ciężka praca trafia do kolejki i ustępuje transmisji na żywo. Zgubionej klatki nie da się odzyskać; transkrypcja, która przyjdzie godzinę później, nic nie kosztuje.", + "lib.anySpeaker": "Dowolny mówca", + "lib.anyTag": "Dowolny tag", + "lib.autoGrouped": "Zgrupowane automatycznie. Edycja czyni je twoim.", + "lib.autoGroupedNote": "Ta sesja została zgrupowana automatycznie. Zapisanie czyni ją twoją, a przebieg grupowania przestanie ją nadpisywać.", + "lib.bestMatch": "Najlepsze dopasowanie", + "lib.clearSearch": "Wyczyść wyszukiwanie", + "lib.clip": "Klip", + "lib.clipTitle": "Wytnij klip z tego nagrania", + "lib.collapseSession": "Zwiń sesję", + "lib.couldNotLoadTheLibrary": "Nie udało się wczytać biblioteki.", + "lib.couldNotLoadTheSession": "Nie udało się wczytać segmentów sesji.", + "lib.couldNotQueueTheProxy": "Nie udało się dodać pliku proxy do kolejki.", + "lib.couldNotRegroup": "Nie udało się przegrupować.", + "lib.couldNotSaveTheSession": "Nie udało się zapisać sesji.", + "lib.delete": "Usuń", + "lib.deleteDescription": "Plik zostanie usunięty z dysku wraz z plikami proxy, miniaturami i transkrypcją. Tej operacji nie można cofnąć.", + "lib.deleteTitle": "Usunąć to nagranie?", + "lib.downloadMaster": "Pobierz plik główny", + "lib.downloadMasterTitle": "Pobierz wielościeżkowy plik główny", + "lib.editSession": "Edytuj sesję", + "lib.expandSession": "Rozwiń sesję", + "lib.from": "Od", + "lib.generateProxy": "Wygeneruj proxy", + "lib.generateProxyTitle": "Wygeneruj plik proxy odtwarzalny w przeglądarce", + "lib.mostRecent": "Najnowsze", + "lib.noProxyYet": "Brak pliku proxy — otwórz, aby go wygenerować", + "lib.noSegments": "Brak segmentów.", + "lib.noSessionMatch": "Żadna sesja nie pasuje do tych filtrów.", + "lib.nothingRecorded": "Nic jeszcze nie nagrano. Sesje pojawiają się w miarę, jak rejestrator zapisuje segmenty.", + "lib.oldestFirst": "Najstarsze najpierw", + "lib.playProxy": "Odtwórz plik proxy", + "lib.proxyAlreadyQueued": "Plik proxy jest już w kolejce.", + "lib.proxyQueued": "Plik proxy dodany do kolejki.", + "lib.recordingDeleted": "Nagranie usunięte.", + "lib.regroupTitle": "Uruchamia grupowanie ponownie. Działa przyrostowo i idempotentnie: nigdy nie scala grupowania rozdzielonego ręcznie.", + "lib.searchPlaceholder": "Przeszukaj wszystko, co ktokolwiek powiedział, we wszystkich nagraniach…", + "lib.searchTranscripts": "Przeszukaj transkrypcje", + "lib.searchable": "Przeszukiwalne", + "lib.searchableOnly": "Segmenty z transkrypcją, dzięki czemu są przeszukiwalne", + "lib.session": "Sesja", + "lib.sessionDescription": "Opis", + "lib.sessionSaved": "Sesja zapisana.", + "lib.sessionTags": "Tagi", + "lib.sessionTitle": "Tytuł", + "lib.subtitle": "Każda transmisja, przeszukiwalna. Każdy mikrofon był nagrywany na własnej ścieżce, więc transkrypcja już wie, kto to powiedział.", + "lib.tag": "Tag", + "lib.tagsPlaceholder": "oddzielone, przecinkami", + "lib.title": "Biblioteka", + "lib.transcribe": "Transkrybuj", + "lib.transcribeEachTrackOnIts": "Transkrybuj każdą ścieżkę osobno — to właśnie daje przypisanie mówców", + "lib.ungrouped": "Niezgrupowane segmenty", + "meters.deviation": "Odchylenie", + "meters.idle": "bezczynne", + "meters.integrated": "Zintegrowana", + "meters.loudness": "Zgodność głośności", + "meters.metering": "pomiar", + "meters.momentary": "Chwilowa", + "meters.monitorOff": "Monitor jest wyłączony. Włącz go, aby zmierzyć, co dostarcza każdy cel.", + "meters.noSignal": "brak sygnału na tej ścieżce", + "meters.nothingToMeasure": "Nie ma jeszcze czego mierzyć. Każdy działający cel otrzymuje własny analizator EBU R128 dla miksu, który dostaje.", + "meters.range": "Zakres", + "meters.shortTerm": "Krótkookresowa", + "meters.subtitle": "Każdy kanał każdej ścieżki wejściowej, prosto z przekaźnika.", + "meters.title": "Mierniki dźwięku", + "meters.truePeak": "Szczyt rzeczywisty", + "meters.waitingForStream": "oczekiwanie na strumień", + "mon.allProcesses": "Wszystkie procesy", + "mon.bitrateChart": "Przepływność wejścia — ostatnie 30 minut", + "mon.chooseDestination": "Wybierz cel…", + "mon.clearLog": "Wyczyść dziennik", + "mon.clearedThisDestinationIsBack": "Wyczyszczono. Ten cel wraca do wygenerowanego polecenia.", + "mon.dryRun": "Próba na sucho", + "mon.dryRunInvalid": "FFmpeg odrzucił argument", + "mon.dryRunOk": "FFmpeg go przyjął", + "mon.extraInput": "Dodatkowe argumenty wejścia", + "mon.extraOutput": "Dodatkowe argumenty wyjścia", + "mon.ffmpegLog": "Dziennik FFmpega", + "mon.fromRunningProcess": "z działającego procesu", + "mon.fullCommand": "Pełne rozwiązane polecenie", + "mon.hostCpu": "Procesor hosta", + "mon.ingestLoss": "Straty na wejściu", + "mon.memory": "Pamięć", + "mon.nothingProvenEitherWay": "Nic nie zostało dowiedzione w żadną stronę", + "mon.pid": "PID", + "mon.processes": "Procesy", + "mon.rebuilt": "przebudowane", + "mon.relayDrops": "Odrzuty przekaźnika", + "mon.relayIn": "Wejście przekaźnika", + "mon.resolveHint": "Rozwiąż polecenie, aby przed zastosowaniem zobaczyć dokładnie, co zostanie uruchomione.", + "mon.restarts": "Ponowne uruchomienia", + "mon.subtitle": "Kondycja procesów, zasoby hosta i bieżący dziennik FFmpega.", + "mon.title": "Monitorowanie", + "mon.uptime": "Czas działania", + "nav.automation": "Automatyzacja", + "nav.chat": "Czat", + "nav.clips": "Fragmenty", + "nav.dashboard": "Panel sterowania", + "nav.jobs": "Zadania", + "nav.library": "Biblioteka", + "nav.meters": "Wskaźniki dźwięku", + "nav.monitoring": "Monitorowanie", + "nav.playout": "Playout", + "nav.recordings": "Nagrania", + "nav.renditions": "Warianty", + "nav.routing": "Routing", + "nav.settings": "Ustawienia", + "nav.sources": "Źródła", + "play.acrossEveryRungTheBackstop": "Na wszystkich szczeblach łącznie. Zabezpieczenie, które nie pozwala długiej transmisji zapełnić dysku.", + "play.allowCrossOrigin": "Zezwól na cross-origin", + "play.allowCrossOriginPlayback": "Zezwól na odtwarzanie cross-origin", + "play.audioBitrate": "Przepływność dźwięku (kbps)", + "play.audioTrack": "Ścieżka dźwiękowa", + "play.basicPasswordNote": "Odtwarzacze, które nie potrafią podążyć za linkiem (dekoder telewizyjny, CDN), mogą wysłać ten sam sekret jako hasło HTTP basic.", + "play.boundsTheViewerTableViewers": "Ogranicza tabelę widzów. Widzowie ponad limit są obsługiwani, tylko nie są liczeni.", + "play.carriesThePlaybackTokenAnyone": "Zawiera token odtwarzania. Każdy, kto ma ten adres URL, może oglądać.", + "play.clipboardUnavailable": "Schowek jest niedostępny; zaznacz i skopiuj ręcznie", + "play.couldNotLoadPlayout": "Nie udało się wczytać playoutu", + "play.disabled": "Wyłączone", + "play.diskCap": "Limit dysku (MB)", + "play.dvrWindow": "Okno DVR (s)", + "play.embedCode": "Kod osadzania", + "play.enable": "Włącz playout", + "play.enabled": "Włączone", + "play.forObsACdnOr": "Dla OBS, sieci CDN lub dowolnego odtwarzacza mówiącego po HLS.", + "play.format": "Format", + "play.hls": "HLS", + "play.hlsDash": "HLS i DASH", + "play.hlsPlaylist": "Playlista HLS", + "play.idleTimeout": "Limit bezczynności widza (s)", + "play.linkReset": "Wygenerowano nowy link odtwarzania. Poprzedni już nie działa.", + "play.linksProtected": "Te linki działają tylko dla zalogowanego administratora, dopóki nie włączysz opcji Publiczny.", + "play.linksPublic": "Te linki działają bez konta.", + "play.liveWindow": "Okno na żywo (segmenty)", + "play.metaDescription": "Opis", + "play.metaTitle": "Tytuł", + "play.mustExceedOneSegmentOr": "Musi przekraczać jeden segment, inaczej widzowie znikają między odpytaniami.", + "play.name": "Nazwa", + "play.noCredentialNote": "Nie są wymagane żadne dane uwierzytelniające. Odpowiednie tylko dla transmisji, którą ma zobaczyć cały internet.", + "play.noRungs": "Brak szczebli. Dodaj jeden, aby cokolwiek publikować.", + "play.onDisk": "Na dysku", + "play.oneStereoAacTrackPer": "Jedna ścieżka stereo AAC na szczebel.", + "play.packaging": "Pakowanie", + "play.passthrough": "Wejście (passthrough)", + "play.peak": "Szczyt", + "play.playerPage": "Strona odtwarzacza", + "play.posterNote": "Wyświetlany nad obrazem na stronie publicznej. Plakat to niedawna klatka kluczowa, odświeżana automatycznie.", + "play.private": "Prywatny.", + "play.protectedNote": "Odtwarzanie jest domyślnie chronione. Otwarcie go to osobny, świadomy krok.", + "play.protection": "Ochrona", + "play.public": "Publiczny", + "play.resetFailed": "Nie udało się zresetować", + "play.restartNote": "Zmiana którejkolwiek z tych opcji restartuje muksery, co na kilka sekund przerywa odbiór widzom.", + "play.rungNote": "Każdy szczebel kopiuje obraz swojego wariantu bit w bit, więc dodanie go kosztuje jeden mukser, a nigdy drugie kodowanie obrazu. Między szczeblami różni się tylko ścieżka dźwiękowa; routing dla poszczególnych celów pozostaje nietknięty.", + "play.segmentLength": "Długość segmentu (s)", + "play.serveWithoutSession": "Obsługuj widzów bez sesji", + "play.sessionCap": "Limit sesji", + "play.sessions": "Sesje", + "play.share": "Udostępnij", + "play.shorterIsLowerLatencyAnd": "Krócej to mniejsze opóźnienie i częstsze zmiany playlisty.", + "play.subtitle": "Dostarczaj strumień widzom bezpośrednio, jako publiczny HLS.", + "play.threeIsTheMinimumA": "Trzy to minimum, na którym odtwarzacz może buforować.", + "play.title": "Playout", + "play.unlisted": "Niepubliczny.", + "play.untitled": "Transmisja bez tytułu", + "play.videoSource": "Źródło obrazu", + "play.watchingNow": "Ogląda teraz", + "play.whoCanWatch": "Kto może oglądać", + "player.copied": "Skopiowano", + "player.copy": "Kopiuj", + "player.loading": "Wczytywanie…", + "player.missingToken": "W linku, który otwarto, brakuje tokenu odtwarzania albo token został zmieniony. Poproś osobę, która go udostępniła, o aktualny link.", + "player.notLive": "Obecnie brak transmisji na żywo", + "player.viewer": "widz", + "player.viewers": "widzów", + "player.willStart": "Ta strona sama zacznie odtwarzać, gdy transmisja się rozpocznie.", + "playlist.choose": "Wybierz plik", + "playlist.chooseAria": "Wybierz plik do dodania", "playlist.empty": "Nic jeszcze w kolejce.", + "playlist.hint": "Wgrywane w Media. Pozycja wskazana przez tę playlistę, która później straci swój plik, pojawi się powyżej jako wymagająca uwagi, zamiast po cichu zawieść przy następnym przełączeniu awaryjnym.", "playlist.itemsAria": "Pozycje playlisty", - "playlist.stateReady": "Gotowe", - "playlist.stateTranscoding": "Transkodowanie", - "playlist.stateAttention": "Wymaga uwagi", - "playlist.stateUnsaved": "Jeszcze niezapisane", "playlist.missingUpload": "Tego pliku już nie można znaleźć.", - "playlist.moveUp": "Przenieś {name} w górę", "playlist.moveDown": "Przenieś {name} w dół", - "playlist.remove": "Usuń {name}", - "playlist.choose": "Wybierz plik", - "playlist.chooseAria": "Wybierz plik do dodania", + "playlist.moveUp": "Przenieś {name} w górę", "playlist.noUploads": "Brak dostępnych plików", - "playlist.hint": "Wgrywane w Media. Pozycja wskazana przez tę playlistę, która później straci swój plik, pojawi się powyżej jako wymagająca uwagi, zamiast po cichu zawieść przy następnym przełączeniu awaryjnym.", - "dest.fbBoxTitle": "Publikowanie krzyżowe i przycisk darowizny na Facebooku", - "dest.fbBoxIntro": "Wysyłane tym samym wywołaniem tworzenia co Odbiorcy, więc obowiązuje ta sama zasada: stosowane raz, gdy transmisja się rozpoczyna, i całkowicie pomijane, gdy jest puste.", - "dest.fbBackupLabel": "Publikuj strumień zapasowy", - "dest.fbBackupHelp": "Wysyła drugą kopię tego strumienia do zapasowego wejścia Facebooka, aby zerwane połączenie nie zerwało transmisji.", - "dest.fbBackupCost": "Podwaja przepustowość wysyłania tego celu.", - "dest.fbBackupReconnect": "Włączenie tej opcji raz ponownie łączy strumień — włącz ją więc, zanim wejdziesz na żywo.", - "dest.fbCrosspostLabel": "Publikowanie krzyżowe na Stronach", - "dest.fbCrosspostPageId": "ID Strony", - "dest.fbCrosspostAlsoPost": "Opublikuj także", - "dest.fbCrosspostRemove": "Usuń tę Stronę", - "dest.fbCrosspostAdd": "Dodaj Stronę", - "dest.fbCrosspostHelp": "Numeryczne ID Strony z konsoli samego Facebooka (ustawienia Strony lub graph.facebook.com/me/accounts, gdy jesteś zalogowany jako Strona) — nie ma tu wyszukiwania, więc wklej id, a nie nazwę czy adres URL. „Opublikuj także” publikuje jako ta Strona, zamiast tylko udostępnić jej transmisję; wyłączone, dzieje się wyłącznie ciche udostępnienie.", - "dest.fbDonateLabel": "ID organizacji dla przycisku darowizny", - "dest.fbDonatePlaceholder": "ID organizacji", - "dest.fbDonateHelp": "To również nieprzejrzyste id z konsoli zbiórek Facebooka, a nie nazwa organizacji do wyszukania. Zostaw puste, aby nie dołączać żadnego przycisku darowizny." + "playlist.remove": "Usuń {name}", + "playlist.stateAttention": "Wymaga uwagi", + "playlist.stateReady": "Gotowe", + "playlist.stateTranscoding": "Transkodowanie", + "playlist.stateUnsaved": "Jeszcze niezapisane", + "playlist.title": "Playlista", + "rec.delete": "Usuń", + "rec.deleteDescription": "Plik zostanie usunięty z dysku wraz z proxy, miniaturami i transkrypcją. Tej operacji nie można cofnąć.", + "rec.deleteFailed": "Nie udało się usunąć nagrania.", + "rec.deleteTitle": "Usunąć to nagranie?", + "rec.deleted": "Nagranie usunięte.", + "rec.disabled": "wyłączone", + "rec.disk": "Dysk", + "rec.diskRecordings": "Nagrania", + "rec.download": "Pobierz", + "rec.downloadNamed": "Pobierz {name}", + "rec.duration": "Czas trwania", + "rec.empty": "Nie ma jeszcze nagrań. Włącz nagrywanie po prawej i rozpocznij transmisję.", + "rec.enabled": "Włączone", + "rec.file": "Plik", + "rec.free": "Wolne", + "rec.haltedDefault": "Nagrywanie jest wstrzymane: wolumen znajduje się poniżej progu wolnego miejsca.", + "rec.haltedRestarts": "Rejestrator wznowi się sam, gdy tylko zwolni się miejsce.", + "rec.help.enabled": "Zapisuje każde wejście na dysk w trakcie transmisji, zachowując wszystkie ścieżki dźwiękowe osobno i bez kodowania. Nagrywanie jest niezależne od tego, co wysyłasz na platformy: wyłączenie go nie wpływa na sygnał na żywo, a włączenie kosztuje miejsce na dysku, a nie pasmo wysyłania.", + "rec.help.maxAgeHours": "Usuwa nagrania starsze niż podana liczba godzin, niezależnie od łącznego rozmiaru. Przydatne, gdy i tak potrzebujesz tylko ostatniego dnia lub dwóch. Ustaw 0, aby zachować wszystko, dopóki nie usunie tego inny limit.", + "rec.help.maxGb": "Górna granica łącznego rozmiaru nagrań przechowywanych przez polyemesis. Po jej przekroczeniu najstarsze są usuwane, aż całość znów się zmieści. Ustaw 0, aby nie było limitu rozmiaru — wtedy wzrost ogranicza jedynie wiek albo próg wolnego miejsca.", + "rec.help.minFreeGb": "Twarde zatrzymanie: nagrywanie wstrzymuje się, gdy na wolumenie zostanie mniej wolnego miejsca niż podano, i wznawia się samo po jego zwolnieniu. To jedyny limit uwzględniający pliki, których polyemesis nie zapisał, więc to ustawienie realnie chroni współdzielony dysk przed zapełnieniem.", + "rec.help.segmentSeconds": "Jak długi jest każdy plik, zanim rejestrator zacznie kolejny. Krótsze segmenty ograniczają straty przy awarii i ułatwiają przenoszenie pojedynczych plików, ale zostawiają ich więcej do ogarnięcia. Cięcia wypadają na klatce kluczowej, więc rzeczywista długość waha się o około sekundę.", + "rec.help.stemFiles": "Dodatkowo zapisuje każdą ścieżkę dźwiękową jako osobny plik obok głównego. Przydatne, gdy montujesz później i chcesz mieć prowadzącego, gości i dźwięk gry na oddzielnych kanałach. Zwiększa objętość zapisu sesji mniej więcej o liczbę ścieżek, dlatego domyślnie jest wyłączone.", + "rec.help.stemFormat": "Obie opcje są bezstratne, więc żadna nie kosztuje jakości. FLAC zajmuje około połowy i jest właściwym wyborem, chyba że jakieś narzędzie w Twoim procesie nie potrafi go odczytać; WAV otwiera się wszędzie. Ścieżki powyżej 8 kanałów automatycznie przechodzą na WAV.", + "rec.hideStems": "Ukryj ścieżki osobne", + "rec.keepForever": "0 = przechowuj bezterminowo.", + "rec.loadFailed": "Nie udało się wczytać nagrań.", + "rec.maxAgeHours": "Usuwaj po (godzinach)", + "rec.maxGb": "Przechowuj najwyżej (GB)", + "rec.minFreeGb": "Zatrzymaj poniżej wolnego miejsca (GB)", + "rec.minFreeNote": "Powyższe limity ograniczają tylko to, co zapisał polyemesis; wszystko inne na wolumenie i tak może go zapełnić. 0 = brak progu.", + "rec.noSizeLimit": "0 = brak limitu rozmiaru.", + "rec.recording": "nagrywa", + "rec.retentionNote": "Porządkowanie uruchamia się co 30 sekund: najstarsze najpierw, według wieku, a następnie łącznego rozmiaru. Aktualnie zapisywany segment nigdy nie jest usuwany.", + "rec.saveFailed": "Nie udało się zapisać ustawień.", + "rec.savePolicy": "Zapisz zasady przechowywania", + "rec.saved": "Zapisano ustawienia nagrywania.", + "rec.segmentSeconds": "Długość segmentu (sekundy)", + "rec.segments": "Segmenty", + "rec.settingsTitle": "Nagrywanie", + "rec.showStems": "Pokaż ścieżki osobne", + "rec.size": "Rozmiar", + "rec.started": "Rozpoczęto", + "rec.stemFiles": "Pliki ścieżek osobnych", + "rec.stemFlac": "FLAC — bezstratny, około połowy rozmiaru", + "rec.stemFormat": "Format ścieżek osobnych", + "rec.stemNote": "Zapisywane z tego samego procesu co plik główny, dzięki czemu każda ścieżka osobna pozostaje zsynchronizowana próbkowo z pozostałymi. Ścieżki powyżej 8 kanałów samoczynnie przechodzą na WAV.", + "rec.stemWav": "WAV — bezstratny, otwiera się wszędzie", + "rec.stems": "Ścieżki osobne", + "rec.stemsTitle": "Pliki ścieżek osobnych: {count}, zsynchronizowane próbkowo z plikiem głównym", + "rec.stemsUsage": "Ścieżki osobne ({count} plików)", + "rec.subtitle": "Pełne archiwum wielościeżkowe: każda ścieżka dźwiękowa wejścia zachowana, bez kodowania.", + "rec.title": "Nagrania", + "rec.tracks": "Ścieżki", + "rec.tracksTitle": "Zachowane ścieżki dźwiękowe: {count}", + "rec.used": "Zajęte", + "rec.volume": "Wolumen", + "rend.aspectBlurpad": "Rozmyte wypełnienie", + "rend.aspectBlurpadHint": "Wypełnia resztę rozmytą kopią samego kadru, przyciętą tak, by wypełnić. To konwencja, na której osiadły pionowe przekazy.", + "rend.aspectCrop": "Przytnij do wypełnienia", + "rend.aspectCropHint": "Przycina od środka do docelowego kształtu, a potem skaluje. Obiekty zachowują swój rozmiar na ekranie; brzegi kadru znikają.", + "rend.aspectHandling": "Obsługa proporcji", + "rend.aspectPad": "Letterbox", + "rend.aspectPadHint": "Skaluje cały kadr tak, by się zmieścił, a resztę wypełnia jednolitym kolorem. Nic nie ginie, ale źródło 16:9 na płótnie 9:16 to głównie pasy.", + "rend.aspectStretch": "Rozciągnij do rozmiaru", + "rend.aspectStretchHint": "Skaluje do docelowego rozmiaru i pozwala obrazowi się zniekształcić, jeśli źródło ma inne proporcje. To, co robił każdy wariant, zanim powstały pozostałe tryby.", + "rend.boxColour": "Kolor pola", + "rend.boxOpacity": "Krycie pola (%)", + "rend.builtInFont": "Wbudowana domyślna", + "rend.burnedText": "Tekst wypalony w obrazie", + "rend.chooseHardware": "Wybierz enkoder sprzętowy albo mniejsze wyjście.", + "rend.colourBlack": "black", + "rend.colourWhite": "white", + "rend.costBeyond": "Kodowanie tego programowo w czasie rzeczywistym przekracza możliwości większości maszyn", + "rend.costBeyondDetail": "{label} to {mpps} na jedno kodowanie. x264 przy tym rozmiarze zwykle przegrywa wyścig z czasem rzeczywistym, a wariant, który zostaje w tyle, gubi klatki dla każdego celu, który go czyta — a dowiadujesz się o tym w trakcie transmisji.{machine} {escape}", + "rend.costBusy": "Spodziewaj się obciążenia kilku rdzeni", + "rend.costBusyDetail": "{label} to {mpps} z użyciem {encoder} — to ciężkie kodowanie w czasie rzeczywistym.{machine} Obserwuj prędkość enkodera po wejściu na żywo: poniżej 1,0× zostaje on w tyle, a cele pod nim zaczną gubić klatki. {escape}", + "rend.costMachineCores": " Ta maszyna zgłasza {cores} rdzeni logicznych.", + "rend.costModest": "Umiarkowane kodowanie programowe", + "rend.costModestDetail": "{label} to {mpps}, w zasięgu większości maszyn.{machine} To wciąż jedno rzeczywiste kodowanie — współdzielone przez każdy cel, który je wybierze, ale nie darmowe.", + "rend.costNoNumbers": "Nie można jeszcze oszacować kosztu", + "rend.costNoNumbersDetail": "Nic jeszcze nie zbadało źródła, więc za „zachowaj źródło” nie stoją żadne liczby. Uruchom wejście, a to się uzupełni.", + "rend.costOffloaded": "Przeniesione na {encoder}", + "rend.costOffloadedDetail": "{label} ({mpps}) działa na sprzętowym enkoderze GPU o stałej funkcji, więc kosztuje niewiele CPU. Jakość i kontrola przepływności zależą od sterownika, a każdy układ ma własny pułap — sprawdź wyjście przed ważnym programem.", + "rend.create": "Utwórz", + "rend.created": "Wariant utworzony. Wybierz go na celu, aby rozpocząć kodowanie.", + "rend.custom": "Własne…", + "rend.deintAll": "Każda klatka", + "rend.deintAllHint": "Dla źródeł, które mają przeplot, ale tego nie zgłaszają. Wiele mostków SDI i kart przechwytujących oznacza wszystko jako progresywne niezależnie od tego, co dostały, a na nich „tylko klatki z przeplotem” nic nie robi i wygląda na zepsute ustawienie.", + "rend.deintAuto": "Tylko klatki z przeplotem", + "rend.deintAutoHint": "Dotyka tylko klatek, które źródło oznaczyło jako z przeplotem, więc klatki progresywne przechodzą nietknięte. Właściwy wybór dla materiału mieszanego.", + "rend.deintOff": "Wyłączone", + "rend.deintOffHint": "Właściwe dla źródeł progresywnych, jakie ma niemal każdy. Usuwanie przeplotu z takiego źródła tylko je zmiękcza, bez żadnego zysku.", + "rend.deinterlace": "Usuń przeplot", + "rend.deleteFailed": "Nie udało się usunąć wariantu.", + "rend.deleteFallbackMany": "{count} celów przechodzi na passthrough i otrzyma niezmieniony obraz ze źródła. Sprawdź, czy źródło nadal mieści się w tym, co przyjmuje każda platforma.", + "rend.deleteFallbackOne": "{count} cel przechodzi na passthrough i otrzyma niezmieniony obraz ze źródła. Sprawdź, czy źródło nadal mieści się w tym, co przyjmuje każda platforma.", + "rend.deleteNoDestinations": "Nic nie wybiera tego wariantu, więc jego usunięcie nie zmieni żadnego celu.", + "rend.deleteRenditionTitle": "Usunąć {name}?", + "rend.editRendition": "Edytuj wariant", + "rend.empty": "Brak wariantów. Każdy cel działa w trybie passthrough, co jest dokładnie tym, czego trzeba, dopóki któryś z nich nie poradzi sobie ze źródłem — na przykład wejście 4K60, którego Twitch, Kick czy X nie przyjmie. Dodaj wariant i skieruj na niego te cele.", + "rend.encoder": "Enkoder", + "rend.encoderPreset": "Ustawienie wstępne enkodera", + "rend.encoderRelatedFailed": "Nie zaproponowano {name}, ponieważ powiązany enkoder tutaj zawiódł.", + "rend.encoderTestFailed": "{name} nie przeszedł testowego kodowania jednej klatki na tej maszynie.", + "rend.encoderTestOk": "{name} zakodował klatkę testową na tej maszynie.", + "rend.evenNote": "0 na osi zachowuje wartość źródła i utrzymuje proporcje obrazu — ustaw samą wysokość, aby przeskalować. Obie wartości muszą być parzyste: H.264 i HEVC nie mają płaszczyzny chrominancji o nieparzystym rozmiarze.", + "rend.font": "Czcionka", + "rend.frameRate": "Klatki na sekundę", + "rend.gop": "GOP", + "rend.gpuUnopenable": "GPU jest obecne, ale ten proces nie może go otworzyć", + "rend.hardwareFallback": "sprzęt", + "rend.hardwareScanFound": "Co wykrył skan sprzętu", + "rend.height": "Wysokość", + "rend.hevcWarning": "Ten enkoder produkuje HEVC. Większość celów RTMP przyjmuje wyłącznie H.264 — wybierz go tylko dla celu, który potwierdził, że przyjmuje HEVC.", + "rend.keyframeInterval": "Odstęp klatek kluczowych", + "rend.letterboxColour": "Kolor pasów letterbox", + "rend.live": "Na żywo", + "rend.loadFailed": "Nie udało się wczytać wariantów.", + "rend.marginX": "Margines X (%)", + "rend.marginY": "Margines Y (%)", + "rend.name": "Nazwa", + "rend.namePlaceholder": "1080p60 dla Twitcha i Kicka", + "rend.needsBothAWidthAnd": "Potrzebna jest i szerokość, i wysokość — przy jednej wolnej osi skalowanie i tak zachowuje proporcje.", + "rend.needsBothAxes": "Potrzebna jest i szerokość, i wysokość", + "rend.newRendition": "Nowy wariant", + "rend.noEncoder": "Ten FFmpeg nie zgłosił żadnego użytecznego enkodera obrazu.", + "rend.noHardwareEscape": "Na tej maszynie nie działa żaden enkoder sprzętowy, więc nie ma dokąd tego przenieść — jedyną dźwignią jest tu mniejsze wyjście. Wykryj sprzęt ponownie, jeśli od tego czasu zainstalowano sterownik lub przekazano GPU do kontenera.", + "rend.notProbed": "Na tej maszynie nie wykonano jeszcze żadnego kodowania testowego, więc ta lista zawiera to, co ma kompilacja FFmpega, a nie to, co potrafi sprzęt. Wykryj ponownie, aby się przekonać.", + "rend.notRunning": "Nie działa", + "rend.note": "Notatka", + "rend.noteDuplicateFps": "Źródło działa z {srcFps} kl./s, więc {fps} kl./s tylko powiela klatki.", + "rend.notePlaceholder": "Do czego służy ten poziom.", + "rend.noteUpscale": "Źródło ma {width}×{height}. Skalowanie w górę kosztuje CPU i nie dodaje szczegółów.", + "rend.opacity": "Krycie (%)", + "rend.percentNote": "Wszystko jest procentem kadru, więc te same ustawienia są poprawne dla poziomu 16:9 i 9:16. Marginesy są ignorowane na osi wyśrodkowanej.", + "rend.preset": "Ustawienie wstępne", + "rend.presetPlaceholder": "veryfast", + "rend.presetStartingPoint": "Punkt wyjścia — sprawdź aktualne limity u platformy.", + "rend.redetectFailed": "Nie udało się ponownie wykryć sprzętu.", + "rend.redetectTitle": "Ponów skan GPU i zakoduj jedną klatkę testową każdym enkoderem. Zajmuje kilka sekund.", + "rend.redetectedNoHardware": "Sprzęt wykryty ponownie. Żaden enkoder sprzętowy nie przeszedł testowego kodowania na tej maszynie.", + "rend.resolution": "Rozdzielczość", + "rend.restartEncode": "Zrestartuj to kodowanie i cele, które je czytają", + "rend.restartFailed": "Nie udało się zrestartować kodowania.", + "rend.restarting": "{name} restartuje się. Jego cele łączą się ponownie razem z nim.", + "rend.save": "Zapisz", + "rend.saveFailed": "Nie udało się zapisać wariantu.", + "rend.seedNote": "To tylko wypełnia formularz wartościami początkowymi — przed zapisaniem możesz zmienić wszystko. Limity platform się zmieniają i zależą od statusu partnera.", + "rend.signalError": "Błąd", + "rend.signalIdle": "Bezczynny", + "rend.sizePct": "Rozmiar (% wysokości)", + "rend.softwareEncoderFallback": "enkodera programowego", + "rend.speed": "Prędkość", + "rend.subtitle": "Jedno wspólne kodowanie obrazu, wiele celów. Dźwięk nigdy nie jest tu ruszany.", + "rend.target": "Wartość docelowa", + "rend.textColour": "Kolor tekstu", + "rend.textPlaceholder": "MOJA STACJA", + "rend.title": "Warianty", + "rend.unused": "Żaden cel jeszcze tego nie wybiera, więc nic nie jest kodowane. Wybierz to na celu, aby rozpocząć kodowanie.", + "rend.uptime": "Czas działania", + "rend.videoBitrate": "Przepływność obrazu", + "rend.waitingSnapshot": "Oczekiwanie na pierwszą migawkę na żywo.", + "rend.watermarkImage": "Obraz znaku wodnego", + "rend.watermarkPlaceholder": "overlays/logo.png", + "rend.width": "Szerokość", + "rend.widthPct": "Szerokość (% kadru)", + "route.attack": "Atak", + "route.audio": "dźwięk", + "route.audioAndVideoGoOut": "Dźwięk i obraz wychodzą razem.", + "route.audioLaterThanVideo": "Dźwięk później niż obraz", + "route.audioRouting": "Routing dźwięku", + "route.audioRoutingDesc": "Wybierz dokładnie, które ścieżki wejściowe otrzymuje każdy cel.", + "route.autoLimitWhen2Tracks": "Auto (limituj przy 2+ ścieżkach)", + "route.commentaryLanguage": "Język komentarza", + "route.couldNotApplyThePreset": "Nie udało się zastosować ustawienia wstępnego.", + "route.couldNotCompileThisRouting": "Nie udało się skompilować tego routingu.", + "route.couldNotLoadDestinations": "Nie udało się wczytać celów.", + "route.couldNotSaveTheRouting": "Nie udało się zapisać routingu.", + "route.couldNotSaveTrackRoles": "Nie udało się zapisać ról ścieżek.", + "route.custom": "Własne…", + "route.delayDirection": "Kierunek opóźnienia", + "route.destinations": "Cele", + "route.duckingNote": "Ścisza jedną grupę ścieżek, gdy mówi inna — mikrofon nad podkładem muzycznym. Wyłączone oznacza, że każda wybrana ścieżka jest sumowana z własnym wzmocnieniem, bez zmian.", + "route.duckingTriggerNote": "Wyzwalacz nie musi znajdować się w miksie tego celu — mikrofon może ściszać tu muzykę, mimo że wysyłana jest sama muzyka.", + "route.enableDucking": "Włącz ducking", + "route.everyTrackReaches": "Każda wybrana ścieżka trafia do tego celu, łącznie z muzyką.", + "route.excludeMusic": "Wyklucz muzykę", + "route.keepMusic": "Zachowaj muzykę", + "route.keepMusicOut": "Nie przepuszczaj muzyki do tego celu", + "route.labelTracks": "Oznacz ścieżki", + "route.langPlaceholder": "lang — np. es", + "route.limiter04DbfsCeiling": "Limiter (pułap −0,4 dBFS)", + "route.localOnlyNote": "Ten serwer nie przechowuje jeszcze ról ścieżek, więc pozostają one w tej przeglądarce. Wszystko inne na tej stronie zapisuje się normalnie.", + "route.loudnessTarget": "Docelowa głośność", + "route.lufsBroadcast": "Nadawanie EBU R128", + "route.measuredOverTheWholeProgramme": "Mierzone dla całego programu, a nie chwila po chwili: ciche fragmenty pozostają ciche. To wartości, w które celują własne normalizatory platform, więc trafienie w jedną z nich sprawia, że platforma sama cię nie ścisza.", + "route.mixMatrix": "Macierz miksu", + "route.musicPolicy": "Zasady dotyczące muzyki", + "route.noTarget": "Brak wartości docelowej (nie zmieniaj poziomów)", + "route.normalizeLoudnorm": "Głośność (EBU R128)", + "route.normalizeOff": "Wyłączone", + "route.off": "wyłączone", + "route.offset": "Przesunięcie", + "route.on": "włączone", + "route.platformDefault": "Domyślne dla platformy", + "route.podcastSpokenWord": "Podcast / mowa", + "route.presetApplied": "Zastosowano ustawienie wstępne. Sprawdź je, a potem zapisz.", + "route.range": "Zakres", + "route.ratio": "Proporcja", + "route.release": "Zwolnienie", + "route.result": "Wynik", + "route.role51": "5.1", + "route.roleClean": "czysty", + "route.roleMic": "mikrofon", + "route.roleMusic": "muzyka", + "route.rolesNote": "Role opisują wejście, a nie ten cel, i zapisują się osobno. Nic nie jest filtrowane, dopóki nie poprosi o to któryś z celów poniżej.", + "route.simple": "Proste", + "route.target": "Wartość docelowa", + "route.targetRole": "ściszany", + "route.thisDestinationSPlatform": "Platforma tego celu", + "route.threshold": "Próg", + "route.title": "Routing", + "route.triggerRole": "wyzwalacz", + "route.truePeak": "Szczyt rzeczywisty", + "route.video": "obraz", + "route.videoLaterThanAudio": "Obraz później niż dźwięk", + "route.withoutATargetTheMix": "Bez wartości docelowej miks wychodzi na poziomie, jaki dało wejście, z zastosowaną jedynie powyższą ochroną przed przesterowaniem.", + "route.youtubeTwitchSpotify": "YouTube, Twitch, Spotify", + "set.acceptSelfSigned": "Akceptuj certyfikat samopodpisany", + "set.accountDisconnected": "Konto odłączone.", + "set.adminPassword": "Hasło administratora", + "set.alertDelivery": "Dostarczanie alertów", + "set.alertDeliveryDesc": "Jak uporczywie ponawiany jest webhook, który nie odpowiada, zanim alert zostanie porzucony.", + "set.apiTokens": "Tokeny API", + "set.apiTokensDesc": "Długowieczne poświadczenia dla skryptów, Prometheusa i wszystkiego innego, co nie utrzyma sesji. Wysyłaj je jako Authorization: Bearer pmk_…. Token potrafi wszystko to co administrator, poza zarządzaniem tokenami — to pozostaje za hasłem, więc odwołanie tokenu jest ostateczne.", + "set.app": "Aplikacja", + "set.attemptsBackoff": "Odstęp między próbami rośnie automatycznie aż do pułapu 30 sekund, więc górny kraniec tego zakresu to już kilka minut ścigania martwego punktu końcowego. Ograniczenie jest tu sednem: ponawianie bez końca zamienia jeden martwy webhook w kolejkę, która nigdy się nie opróżnia.", + "set.attemptsNote": "Wliczając pierwszą próbę. 1 oznacza brak ponawiania.", + "set.attemptsRetryable": "Ponawiane są tylko błędy, które warto ponawiać. Odpowiedź 404 z webhooka, który ktoś usunął, jest trwała, a czterokrotne ponowienie tylko opóźni każdy alert czekający za nim w kolejce; odpowiedź 502 z odwrotnego proxy to dokładnie to, po co te próby istnieją.", + "set.audioMeters": "Mierniki dźwięku", + "set.brokerPasswordCleared": "Hasło brokera wyczyszczone.", + "set.brokerPlainNote": "mqtt:// nie jest szyfrowane. MQTT wysyła nazwę użytkownika i hasło w pakiecie CONNECT, więc oba przechodzą przez sieć otwartym tekstem — w zaufanej sieci lokalnej to w porządku, co jest typowym przypadkiem Home Assistanta, ale nie w porządku wszędzie tam, czego nie kontrolujesz. Użyj mqtts://, jeśli broker to oferuje.", + "set.brokerUrl": "Adres URL brokera", + "set.brokerUrlNote": "mqtt://, mqtts://, ws:// lub wss://. Hasło w adresie URL jest odrzucane: adres trafia do wierszy dziennika i do wyjścia ps, a tego nie da się cofnąć. Wpisz je w polu poniżej, gdzie jest szyfrowane w spoczynku.", + "set.brokerUrlPlaceholder": "mqtts://broker.local:8883", + "set.chatBothApply": "Obowiązują oba, a wygrywa bardziej hojne: wiadomość znika, gdy jest starsza niż liczba godzin i zarazem poza N najnowszymi. Ruchliwy kanał zachowuje więc krótszy okres niż spokojny przy tych samych ustawieniach.", + "set.chatBufferNote": "„Wyślij po połączeniu” to liczba innego rodzaju niż dwie pozostałe. Tamte ograniczają to, co zostaje na dysku; ta jest buforem w pamięci, przydzielanym na każdy podłączony panel.", + "set.chatHistory": "Historia czatu", + "set.chatHistoryDesc": "Jak daleko wstecz sięga historia czatu — a wraz z nią, ile widzi moderator, otwierając czyjąś kartę.", + "set.chatHours": "Przechowuj przez (godziny)", + "set.chatKeep": "Zawsze zachowuj (wiadomości)", + "set.chatKeepNote": "Dolna granica, niezależnie od wieku.", + "set.chatNotOnlyDisk": "To nie jest wyłącznie ustawienie dysku. Kliknięcie imienia na czacie otwiera to, co ta osoba mówiła wcześniej, a ta historia pochodzi stąd — żadna platforma nie udostępnia do tego API, więc polyemesis pokaże tylko to, co zachował. Ustaw zbyt krótko, a ktoś, kto sprawia kłopoty od tygodnia, będzie wyglądał, jakby nigdy nic nie powiedział.", + "set.chatSendNote": "Historia bez zapytania.", + "set.chatSendOnConnect": "Wyślij po połączeniu (wiadomości)", + "set.chatSendOnConnectShort": "Wyślij po połączeniu", + "set.chatSmall": "Czat zajmuje niewiele. Kanał ze średnią dziesięciu wiadomości na sekundę zapisuje mniej więcej 7 MB na godzinę; większość jest daleko poniżej tej wartości.", + "set.clientId": "Identyfikator klienta", + "set.clientIdNote": "Musi być unikalny na brokerze. Kolizja to przyczyna numer jeden niewyjaśnionej pętli ponownych połączeń: broker przy każdym połączeniu rozłącza starszą sesję, a obaj klienci łączą się w nieskończoność.", + "set.clientIdPlaceholder": "wywodzony z nazwy instancji", + "set.clientSecret": "Sekret klienta", + "set.comingBack": "Powrót", + "set.confirmPassword": "Potwierdź nowe hasło", + "set.connectedAccounts": "Połączone konta", + "set.copied": "Skopiowano", + "set.copy": "Kopiuj", + "set.couldNotChangeThePassword": "Nie udało się zmienić hasła.", + "set.couldNotClearThePassword": "Nie udało się wyczyścić hasła.", + "set.couldNotCreateTheToken": "Nie udało się utworzyć tokenu.", + "set.couldNotReCheckThe": "Nie udało się ponownie sprawdzić poświadczeń.", + "set.couldNotReadTheTls": "Nie udało się odczytać stanu TLS.", + "set.couldNotRevokeTheToken": "Nie udało się unieważnić tokenu.", + "set.couldNotSaveTheCredentials": "Nie udało się zapisać poświadczeń.", + "set.couldNotSaveTheMqtt": "Nie udało się zapisać ustawień MQTT.", + "set.credsRemoved": "Poświadczenia usunięte.", + "set.currentPassword": "Bieżące hasło", + "set.dataDirectory": "Katalog danych", + "set.deliveryAttempts": "Próby dostarczenia", + "set.dialsOnly": "Serwer łączy się wyłącznie z: {schemes}.", + "set.disconnect": "Odłącz", + "set.disconnectDesc": "Cele, które go używają, zachowują swoje ustawienia, ale przestają odświeżać klucz strumienia. To samo konto można później podłączyć ponownie.", + "set.disconnectTitle": "Odłączyć to konto?", + "set.drawtextMissing": "Ta kompilacja nie ma filtra drawtext, więc nakładki tekstowe są niedostępne. Wymaga FFmpega zbudowanego z libfreetype.", + "set.drawtextOk": "Ta kompilacja potrafi rysować tekst na obrazie.", + "set.enabled": "Włączone", + "set.encoderUrl": "Adres URL enkodera", + "set.expires": "Wygasa", + "set.failover": "Przełączanie awaryjne", + "set.failoverDesc": "Zapasowe wejście i plansza zastępcza na wypadek, gdy źródło na żywo przestanie dostarczać obraz. Domyślnie wyłączone: przy wyłączonym tor jest bajt w bajt taki, jaki był przed powstaniem tej funkcji.", + "set.failoverRemuxNote": "Włączenie dodaje jeden krok remultipleksowania między wejściem a wszystkim poniżej. Tanie, ale nie darmowe, i właśnie dlatego jest to decyzja, a nie wartość domyślna.", + "set.fileSizeMb": "Rozmiar pliku (MB)", + "set.filesKept": "Przechowywane pliki", + "set.goingLive": "Wejście na żywo", + "set.goingLiveDesc": "Co się dzieje, gdy kilka celów startuje jednocześnie.", + "set.graceNote": "Jak długo źródło na żywo może nic nie dostarczać, zanim zmieni się poziom. Celowo dłużej niż ponowne połączenie — enkoder odtwarzający połączenie RTMP to normalna praca, a nie awaria.", + "set.gracePeriod": "Okres tolerancji (sekundy)", + "set.haDiscovery": "Wykrywanie Home Assistant", + "set.height": "Wysokość", + "set.ingest": "Wejście", + "set.ingestDesc": "SRT przenosi do sześciu ścieżek dźwiękowych. RTMP przenosi jedną i istnieje jako rozwiązanie zapasowe dla enkoderów, które nie obsługują SRT. Pobieranie samo łączy się ze źródłem, zamiast na nie czekać.", + "set.ingestUrlCopied": "Skopiowano adres URL wejścia.", + "set.instance": "Instancja", + "set.intervalNote": "Niezmieniony stan nie jest publikowany ponownie, więc krótki interwał kosztuje porównanie zamiast wiadomości. Keep-alive ogranicza to, jak długo martwe łącze pozostaje niezauważone — a więc i to, jak szybko broker powiadomi subskrybentów, że tej instancji już nie ma.", + "set.issuer": "Wystawca", + "set.kbps": "kb/s", + "set.keepAlive": "Keep-alive (s)", + "set.latencyMs": "Opóźnienie (ms)", + "set.listeners": "Nasłuchy", + "set.listenersDesc": "Gdzie serwer nasłuchuje, dla całej instalacji. Jeden port SRT obsługuje każde źródło — rozróżnia się je po tokenie publikacji, więc dodanie źródła nigdy nie oznacza otwarcia kolejnego portu. RTMP nie ma takiego trasowania, więc jego port obsługuje jedno źródło.", + "set.logsBoundNote": "Katalog dzienników jest ograniczony iloczynem tych dwóch wartości, więc trwały zapis nigdy nie może zapełnić dysku.", + "set.logsDesc": "Przechwycone stderr FFmpega zawsze trafia do pamięci, ale ta ginie wraz z serwerem — co usuwa dokładnie te wiersze, które tłumaczą, dlaczego serwer padł. Trwały zapis odbija je do rotowanego pliku w katalogu danych.", + "set.metersDesc": "Jeden proces pomocniczy mierzy każdy kanał każdej ścieżki. Krótsze interwały odświeżają szybciej i zużywają więcej procesora.", + "set.mode": "Tryb", + "set.modePull": "Pobieranie — połącz się z kamerą, sygnałem lub plikiem", + "set.modeRtmp": "RTMP — pojedyncza ścieżka", + "set.modeSrt": "SRT — wielościeżkowy (zalecany)", + "set.mqtt": "Telemetria MQTT", + "set.mqtt5Only": "Tylko MQTT 5.0.", + "set.mqtt5OnlyNote": "Broker przypięty do 3.1.1 w ogóle nie nawiąże połączenia — to nie jest tryb ograniczony. Mosquitto 2.x, EMQX i dodatek Home Assistant mówią w 5.0.", + "set.mqttDesc": "Publikuje stan na brokerze jako wiadomości retained, dzięki czemu odbiorca, który nie był podłączony w chwili zmiany, i tak otrzyma aktualną odpowiedź.", + "set.multiAccountNote": "Podłącz tę samą platformę więcej niż raz, aby nadawać na kilka kanałów — każde konto staje się osobnym celem.", + "set.name": "Nazwa", + "set.newPassword": "Nowe hasło", + "set.no": "nie", + "set.noEncryption": "Nic nie szyfruje tego połączenia. Hasła i ciasteczka sesji przechodzą przez sieć jawnym tekstem — ustaw tls.mode: auto albo powiąż z 127.0.0.1 za serwerem proxy.", + "set.noLibsrt": "Ta kompilacja FFmpega nie obsługuje SRT. Zainstaluj skonfigurowaną z --enable-libsrt albo użyj RTMP.", + "set.noTokens": "Nie ma jeszcze tokenów.", + "set.passphrase": "Hasło dostępu", + "set.passphraseNote": "Włącza szyfrowanie AES. Zostaw puste, aby wyłączyć. SRT odrzuca hasła spoza zakresu 10–79 znaków.", + "set.passphrasePlaceholder": "opcjonalnie — od 10 do 79 znaków", + "set.password": "Hasło", + "set.passwordChanged": "Hasło zmienione.", + "set.passwordMismatch": "Nowe hasła nie są zgodne.", + "set.passwordNote": "Hasło nie jest zwracane przez żadne API, więc to pole jest puste nawet wtedy, gdy hasło jest ustawione. Zostaw je puste, aby zachować zapisane hasło; wyczyść je przyciskiem poniżej.", + "set.platform": "Platforma", + "set.platformsDesc": "Platformy różnią się tym, jak wiele z tego polyemesis potrafi zautomatyzować, i tę różnicę warto poznać przed startem, a nie godzinę później. Samo nadawanie działa wszędzie tak samo — trasowanie dźwięku na cel, warianty, ponowne łączenie i mierniki nie zależą od żadnej z poniższych kolumn.", + "set.playLoop": "Odtwarzaj pętlę, gdy nic nie idzie na żywo", + "set.playLoopNote": "Drugi kandydat obok planszy: zamiast nieruchomego obrazu odtwarzane są pliki. Oba mogą być włączone — kolejność poziomów i to, co wygrywa, opisuje projekt przełączania awaryjnego.", + "set.pointObs": "Skieruj OBS na ten adres.", + "set.preview": "Podgląd", + "set.previewDesc": "Monitor HLS panelu. To jedyne miejsce, w którym polyemesis ponownie koduje obraz, i działa na własnej subskrypcji przekaźnika, więc nigdy nie wpłynie na cel.", + "set.previewIdleNote": "Enkoder startuje, gdy panel poprosi o playlistę, i zatrzymuje się po tym czasie od ostatniego żądania, więc zamknięty panel nie kosztuje procesora. Zmiana tej wartości nigdy nie restartuje podglądu, który ktoś właśnie ogląda.", + "set.processLogs": "Dzienniki procesów", + "set.proxyTerminatesTls": "Odwrotne proxy przed polyemesis kończy TLS, więc ten serwer celowo obsługuje zwykły HTTP.", + "set.publishInterval": "Interwał publikacji (s)", + "set.reconnectCap": "Pułap odczekania przed ponowieniem (s)", + "set.reconnectCapNote": "0 używa wartości domyślnej. Tylko źródła HTTP i HLS.", + "set.redirectNote": "Musi zgadzać się dokładnie, łącznie ze schematem i portem. Jeśli docierasz do polyemesis przez odwrotne proxy, użyj tamtego publicznego adresu URL.", + "set.redirectUri": "Adres URI przekierowania do dodania", + "set.removeCreds": "Usuń poświadczenia", + "set.removeCredsDesc": "Każde konto podłączone przez tę aplikację przestanie odświeżać swój token i będzie wymagało ponownego podłączenia. Sekretu klienta nie da się stąd odzyskać — trzeba by utworzyć nową aplikację na platformie.", + "set.removeCredsTitle": "Usunąć te poświadczenia dewelopera?", + "set.returnAfter": "Wróć po (sekundach stabilności)", + "set.returnAutomatic": "Automatycznie — wróć, gdy źródło główne będzie stabilne", + "set.returnManual": "Ręcznie — pozostań na zapasowym, dopóki nie przełączę", + "set.returnManualNote": "Domyślnie jest tryb ręczny, ponieważ automatyczny powrót może migotać: źródło główne, które wraca do formy zrywami, przerzucałoby program tam i z powrotem w środku transmisji.", + "set.rtmpPort": "Port RTMP", + "set.rtspNote": "TCP, chyba że kamera inaczej nie potrafi. RTSP przez UDP poprzez NAT wygląda na połączony i nic nie dostarcza.", + "set.rtspTransport": "Transport RTSP", + "set.saveAndDrop": "Zapisz i przerwij transmisję na żywo", + "set.saveIngest": "Zapisz ustawienia wejścia", + "set.segmentSec": "Segment (s)", + "set.settingsSavedAffectedProcessesHave": "Ustawienia zapisane. Objęte procesy zostały zrestartowane.", + "set.showSlate": "Pokaż planszę, gdy nic nie idzie na żywo", + "set.slateColour": "Kolor planszy", + "set.slateColourNote": "Jedno słowo, bo wartość trafia do grafu filtrów, w którym przecinek zakończyłby argument. Używane tylko wtedy, gdy nie ustawiono obrazu.", + "set.slateColourPlaceholder": "black", + "set.slateImage": "Obraz planszy", + "set.slateImageNote": "Ścieżka wewnątrz katalogu danych. Zostaw puste, aby zamiast tego wypełnić jednolitym kolorem — to właśnie dlatego jest rozwiązaniem zapasowym, że kolor nie ma pliku, który mógłby się nie otworzyć.", + "set.slateImagePlaceholder": "slate/holding.png", + "set.sourceUrl": "Adres URL źródła", + "set.sourceUrlNote": "Jeden z: {schemes}. Źródło file:// to ścieżka WZGLĘDEM katalogu danych i nie może zawierać „..” — jest tam zamknięte tak samo jak przesłany plik.", + "set.sourceUrlPlaceholder": "rtsp://camera.local/stream1", + "set.srtPort": "Port SRT", + "set.stagger": "Rozłóż połączenia w czasie (ms)", + "set.staggerNote": "0 wyłącza. W przeciwnym razie wejście na żywo oznacza, że każdy cel otwiera połączenie, negocjuje TLS i zaczyna kodować dźwięk w tej samej chwili — na małej maszynie to moment, w którym najłatwiej pogubić klatki, i dokładnie ten moment, któremu się przyglądasz, bo właśnie wszedłeś na żywo. Kilkaset milisekund w zupełności wystarczy.", + "set.staggerReconnectNote": "Nigdy nie opóźnia ponownego połączenia. Cel, który odpadnie o 3 w nocy, wraca natychmiast, zamiast czekać w kolejce za procesami, które są już sprawne.", + "set.stepByStep": "Konfiguracja krok po kroku", + "set.stopAfterIdle": "Zatrzymaj po bezczynności (s)", + "set.streamKey": "Klucz strumienia", + "set.streamKeyNote": "Publikujący muszą użyć dokładnie tej ścieżki, żeby otwarty port nie był otwartymi drzwiami.", + "set.subject": "Podmiot", + "set.synthesiseNote": "Każda duża platforma odrzuca strumień bez ścieżki dźwiękowej. Przy włączonej opcji wejście zawierające sam obraz zyskuje jedną cichą ścieżkę stereo i jego cele ruszają; przy wyłączonej odmawiają z komunikatem „routing profile selects no audio”. Dotyczy to wyłącznie wejścia, które WYKRYTO z zerową liczbą ścieżek, więc nigdy nie wyprze dźwięku, który naprawdę wysyłasz.", + "set.synthesiseSilent": "Twórz cichą ścieżkę", + "set.syntheticAudio": "Dźwięk syntetyczny", + "set.syntheticDesc": "Co się dzieje, gdy wejście niesie obraz i zupełnie żadnego dźwięku.", + "set.tabIngest": "Wejście", + "set.tabPipeline": "Tor przetwarzania", + "set.tabPlatforms": "Poświadczenia platform", + "set.tabSecurity": "Bezpieczeństwo", + "set.title": "Ustawienia", + "set.tlsAcme": "Let's Encrypt", + "set.tlsAuto": "Auto", + "set.tlsDesc": "Konfigurowane w config.yaml, nie tutaj — TLS musi być poprawny, zanim serwer zacznie nasłuchiwać. Ta karta pokazuje, co z tego wyszło.", + "set.tlsManual": "Certyfikat ręczny", + "set.tlsOff": "Bez terminacji tutaj", + "set.tlsSelfsigned": "Samopodpisany", + "set.tokenHashNote": "polyemesis przechowuje wyłącznie skrót. Jeśli go zgubisz, odwołaj ten token i utwórz nowy.", + "set.tokenNamePlaceholder": "prometheus", + "set.tokenRevoked": "Unieważniono {name}.", + "set.topicPrefix": "Prefiks tematu", + "set.topicPrefixNote": "Separatory w prefiksie są zachowywane, więc home/av oznacza dwa poziomy. Prefiks zaczynający się od $ jest odrzucany: brokery je rezerwują, a subskrybent używający # nigdy ich nie otrzyma — telemetria opublikowałaby się pomyślnie i byłaby niewidoczna dokładnie w tym widoku, w którym szukałbyś przyczyny.", + "set.transportSecurity": "Bezpieczeństwo transportu", + "set.updateInterval": "Interwał odświeżania (ms)", + "set.username": "Nazwa użytkownika", + "set.validFor": "Ważny dla", + "set.whatPlatformsDo": "Co potrafi każda platforma", + "set.whyDevApp": "Dlaczego potrzebujesz własnej aplikacji dewelopera", + "set.whyDevAppDesc": "polyemesis nie może dostarczać sekretów klienta OAuth — miałby je każdy, kto ma plik binarny, a platformy by je odwołały. Rejestrujesz raz bezpłatną aplikację dewelopera, wklejasz tutaj identyfikator i sekret klienta, a polyemesis pobiera potem twoje klucze strumienia za ciebie.", + "set.writeLogs": "Zapisuj dzienniki na dysku", + "set.yes": "tak", + "sources.addDescription": "Drugi program z własnym wejściem, celami i wariantami — na przykład kanwa pionowa obok poziomej. Startuje na domyślnych portach, które warto zmienić, aby oba sobie nie kolidowały.", + "sources.addFailed": "Nie udało się dodać źródła", + "sources.addSource": "Dodaj źródło", + "sources.addTitle": "Dodaj źródło", + "sources.added": "Dodano „{name}”. Ustaw poniżej porty wejściowe.", + "sources.app": "Aplikacja", + "sources.applyAndDrop": "Zastosuj i przerwij strumień", + "sources.copied": "Skopiowano: {what}", + "sources.copyFailed": "Nie udało się skopiować: {what}", + "sources.copyTokenAria": "Skopiuj token", + "sources.copyUrlAria": "Skopiuj adres URL {proto}", + "sources.default": "domyślne", + "sources.defaultDescription": "Żądania, które nie wskazują źródła, działają na tym.", + "sources.deleteAria": "Usuń {name}", + "sources.deleteConfirm": "Usuń źródło", + "sources.deleteDescription": "Jego cele i warianty znikną razem z nim — opisują, dokąd trafia ten program, i bez niego nic nie znaczą. Nagrania zostają zachowane: pliki nadal są na dysku i można je odtwarzać, przestają jedynie być przypisane do źródła.", + "sources.deleteFailed": "Nie udało się usunąć źródła", + "sources.deleteThis": "Usuń to źródło", + "sources.deleteTitle": "Usunąć „{name}”?", + "sources.deleted": "Usunięto „{name}” wraz z jego celami", + "sources.destinations": "Cele", + "sources.dirtyIdle": "Niezapisane zmiany wejścia. Zastosowanie zrestartuje wejście tego źródła.", + "sources.dirtyPublishing": "Do tego źródła publikuje enkoder. Zastosowanie zrestartuje jego wejście i odłączy wszystkich oglądających.", + "sources.enableAria": "Włącz {name}", + "sources.help.app": "Ścieżka aplikacji RTMP — część adresu URL między hostem a kluczem strumienia. Większość enkoderów oczekuje „live”. Zmieniaj ją tylko wtedy, gdy masz ku temu powód.", + "sources.help.ingest": "Jak Twój enkoder dociera do tego źródła. SRT radzi sobie z zawodnym łączem, retransmitując utracone pakiety, i jest właściwym wyborem domyślnym w publicznym internecie. RTMP jest starszy i obsługiwany wszędzie, ale mocno traci jakość na niestabilnym łączu. Przy pobieraniu polyemesis sam pobiera sygnał z podanego przez Ciebie adresu URL, zamiast czekać, aż ktoś do niego opublikuje.", + "sources.help.latencyMs": "Jak długo SRT czeka na spóźniony lub retransmitowany pakiet, zanim z niego zrezygnuje. Wyższe wartości przetrwają gorsze łącza kosztem opóźnienia: dobrym punktem wyjścia jest około 2,5× czasu podróży w obie strony, więc przy RTT 40 ms warto ustawić około 100 ms. Zbyt niska wartość sprawia, że krótkie zakłócenie staje się widoczną usterką.", + "sources.help.passphrase": "Szyfruje połączenie SRT i dziś to właśnie ono realnie chroni to wejście przed obcymi. Od 10 do 79 znaków. Jeśli zostawisz je puste, publikować może każdy, kto dosięgnie portu.", + "sources.help.streamKey": "Sekret wysyłany przez enkoder w celu publikacji przez RTMP. To poświadczenie chroniące to wejście, więc traktuj je jak hasło: każdy, kto je ma, może nadawać do Twoich celów.", + "sources.help.token": "Identyfikuje to źródło, gdy kilka współdzieli jeden port wejściowy. To, czy dodatkowo chroni dostęp, zależy od trybu wejścia — notka poniżej mówi, co jest prawdą w tej chwili. Wymiana wydaje nowy token i utrzymuje stary przez pięć minut, aby działający enkoder nie został odcięty w trakcie transmisji.", + "sources.ingest": "Wejście", + "sources.lastSourceTitle": "Ostatniego źródła nie można usunąć — instalacja potrzebuje jednego wejścia", + "sources.latencyMs": "Opóźnienie (ms)", + "sources.loadFailed": "Nie udało się wczytać źródeł", + "sources.loading": "Wczytywanie źródeł…", + "sources.loss": "straty", + "sources.modePull": "Pobieranie", + "sources.name": "Nazwa", + "sources.namePlaceholder": "Pionowy", + "sources.noEngine": "Dla tego źródła nie działa żaden silnik. Zwykłą przyczyną jest port wejściowy zajęty już przez inne źródło lub inny proces.", + "sources.notRunning": "nie działa", + "sources.ownDescription": "Własne cele, warianty i nagrania.", + "sources.passphrase": "Hasło dostępu", + "sources.passphrasePlaceholder": "10–79 znaków", + "sources.publishing": "publikuje", + "sources.renditions": "Warianty", + "sources.retrans": "retransm.", + "sources.rotate": "Wymień", + "sources.rotateConfirm": "Wymień token", + "sources.rotateFailed": "Nie udało się wymienić tokenu", + "sources.rotateIdle": "Nowy token jest wydawany natychmiast. Stary działa jeszcze przez pięć minut, aby enkoder, który go już używa, nie został odłączony.", + "sources.rotateTitle": "Wymienić token dla „{name}”?", + "sources.rotateWhilePublishing": "Do tego źródła publikuje właśnie enkoder. Stary token działa jeszcze przez pięć minut, więc połączenie nie zostanie zerwane — ale trzeba go zaktualizować przed upływem tego czasu.", + "sources.rtt": "RTT", + "sources.running": "działa", + "sources.saveFailed": "Nie udało się zapisać", + "sources.streamKey": "Klucz strumienia", + "sources.subtitle": "Każde to jeden przyjmowany program. Cele i warianty należą do źródła.", + "sources.title": "Źródła", + "sources.token": "token", + "sources.tokenEnforcedDetailRtmp": "To jest klucz strumienia: publikuj pod powyższy adres, używając go jako klucza. Rotacja wydaje nowy i utrzymuje stary przez pięć minut, więc możesz się przełączyć bez przerywania transmisji.", + "sources.tokenEnforcedDetailSrt": "Wpisz go jako streamid SRT w koderze, aby publikować do tego źródła na wspólnym porcie. Rotacja wydaje nowy i utrzymuje stary przez pięć minut, więc możesz się przełączyć bez przerywania transmisji.", + "sources.tokenIsCredential": "Ten token jest poświadczeniem dostępu.", + "sources.tokenIssued": "Wydano nowy token", + "sources.tokenNotEnforced": "Ten token jest przechowywany, ale w tej chwili nie jest egzekwowany.", + "sources.tokenNotEnforcedRtmp": "Źródła są rozdzielane portem. To, co naprawdę chroni to wejście, to klucz strumienia powyżej. Włącz w Ustawieniach wejście jednoportowe, aby token stał się poświadczeniem.", + "sources.tokenNotEnforcedSrt": "Źródła są rozdzielane portem. To, co naprawdę chroni to wejście, to hasło dostępu SRT powyżej. Włącz w Ustawieniach wejście jednoportowe, aby token stał się poświadczeniem.", + "state.disabled": "Wyłączone", + "state.failed": "Niepowodzenie", + "state.live": "Na żywo", + "state.offline": "Poza siecią", + "state.reconnecting": "Ponawianie połączenia", + "state.starting": "Uruchamianie", + "state.stopped": "Zatrzymano" } diff --git a/ui/src/lib/i18n/pt-BR.json b/ui/src/lib/i18n/pt-BR.json index eaa298ae..41a5d16a 100644 --- a/ui/src/lib/i18n/pt-BR.json +++ b/ui/src/lib/i18n/pt-BR.json @@ -1,26 +1,227 @@ { - "nav.dashboard": "Painel", - "nav.sources": "Fontes", - "nav.meters": "Medidores de áudio", - "nav.routing": "Routing", - "nav.renditions": "Versões", - "nav.playout": "Playout", - "nav.library": "Biblioteca", - "nav.clips": "Clipes", - "nav.chat": "Chat", - "nav.jobs": "Tarefas", - "nav.automation": "Automação", - "nav.recordings": "Gravações", - "nav.monitoring": "Monitoramento", - "nav.settings": "Configurações", - "chrome.toggleNav": "Alternar navegação", + "auth.accountCreated": "Conta de administrador criada.", + "auth.confirmPassword": "Confirmar senha", + "auth.createAccount": "Criar conta", + "auth.firstRun": "Configuração inicial", + "auth.firstRunDesc": "Escolha as credenciais de administrador deste servidor. Não há recuperação de conta, então guarde-as em um lugar seguro.", + "auth.password": "Senha", + "auth.passwordMismatch": "As senhas não coincidem.", + "auth.signIn": "Entrar", + "auth.signInDesc": "Informe suas credenciais de administrador.", + "auth.somethingWentWrong": "Algo deu errado.", + "auth.tokenCreated": "Token de API criado", + "auth.tokenRevoked": "Token de API revogado", + "auth.username": "Usuário", + "auto.action": "Ação", + "auto.adminPasswordChanged": "Senha de administrador alterada", + "auto.chosenWeekdays": "Dias da semana escolhidos", + "auto.coalesced": "Agrupadas", + "auto.coalescingWindow": "Janela de agrupamento (segundos)", + "auto.couldNotChangeTheRule": "Não foi possível alterar a regra.", + "auto.couldNotChangeTheSchedule": "Não foi possível alterar o agendamento.", + "auto.couldNotDeleteTheRule": "Não foi possível excluir a regra.", + "auto.couldNotDeleteTheSchedule": "Não foi possível excluir o agendamento.", + "auto.couldNotLoadAutomationSettings": "Não foi possível carregar as configurações de automação.", + "auto.couldNotSaveTheRule": "Não foi possível salvar a regra.", + "auto.couldNotSaveTheSchedule": "Não foi possível salvar o agendamento.", + "auto.createRule": "Criar regra", + "auto.createSchedule": "Criar agendamento", + "auto.days": "Dias", + "auto.delete": "Excluir", + "auto.deleteRule": "Excluir regra", + "auto.deleteRuleDesc": "A regra para de disparar. O endpoint de webhook dela não é alterado, e você pode recriar a regra.", + "auto.deleteRuleTitle": "Excluir esta regra de alerta?", + "auto.deleteSchedule": "Excluir agendamento", + "auto.deleteScheduleDesc": "O agendamento para de rodar. O que ele já iniciou não é afetado.", + "auto.deleteScheduleTitle": "Excluir este agendamento?", + "auto.delivery": "Entrega", + "auto.destinationFallingBehindRealtime": "Destino ficando para trás do tempo real", + "auto.destinationKeepingUpAgain": "Destino acompanhando de novo", + "auto.destinationRecovered": "Destino recuperado", + "auto.destinationWentDown": "Destino caiu", + "auto.destinations": "Destinos", + "auto.dropped": "Descartadas", + "auto.edit": "Editar", + "auto.editRule": "Editar regra de alerta", + "auto.editSchedule": "Editar agendamento", + "auto.endpoint": "Endpoint", + "auto.evAudioClipping": "Clipping de áudio", + "auto.evClipCaptured": "Clipe capturado", + "auto.evDiskLow": "Espaço em disco acabando", + "auto.evDiskRecovered": "Disco recuperado", + "auto.evIngestLost": "Entrada perdida", + "auto.evIngestRecovered": "Entrada recuperada", + "auto.evSettingsChanged": "Configurações alteradas", + "auto.evSignedIn": "Sessão iniciada", + "auto.events": "Eventos", + "auto.everyDay": "Todos os dias", + "auto.failed": "Falhas", + "auto.failoverSwitchedSource": "O failover trocou de fonte", + "auto.fired": "disparadas", + "auto.firesAt": "Dispara às", + "auto.lastDelivery": "Última entrega", + "auto.lastSweep": "Última varredura", + "auto.latenessAllowance": "Atraso tolerado (segundos)", + "auto.loudnessBackInCompliance": "Loudness de volta à conformidade", + "auto.loudnessOutOfCompliance": "Loudness fora de conformidade", + "auto.minGap": "Intervalo mínimo entre mensagens (segundos)", + "auto.minSeverity": "Severidade mínima", + "auto.missedNote": "Uma ocorrência perdida porque o servidor estava fora do ar é marcada como tratada e ignorada, em vez de disparar atrasada — uma transmissão que começa sozinha quatro horas atrasada é pior do que uma que nunca começou.", + "auto.name": "Nome", + "auto.newRule": "Nova regra de alerta", + "auto.newSchedule": "Novo agendamento", + "auto.next": "Próxima", + "auto.nothingFired": "Nada disparou ainda. O agendador faz uma varredura a cada 20 segundos.", + "auto.once": "Uma vez", + "auto.payloadFormat": "Formato do payload", + "auto.pending": "Pendentes", + "auto.repeatedFailedSignIns": "Tentativas de acesso malsucedidas repetidas", + "auto.repeats": "Repete", + "auto.retries": "Novas tentativas", + "auto.rule": "Regra", + "auto.ruleCreated": "Regra de alerta criada. Envie um teste para ela.", + "auto.ruleDeleted": "Regra de alerta excluída.", + "auto.rulePlaceholder": "Discord — alertas do programa", + "auto.ruleSaved": "Regra de alerta salva.", + "auto.saveRule": "Salvar regra", + "auto.saveSchedule": "Salvar agendamento", + "auto.schedule": "Agendamento", + "auto.scheduleCreated": "Agendamento criado.", + "auto.scheduleDeleted": "Agendamento excluído.", + "auto.schedulePlaceholder": "Programa dos dias de semana", + "auto.scheduleSaved": "Agendamento salvo.", + "auto.schedules": "Agendamentos", + "auto.secretMasked": "Exibida mascarada porque o segredo fica no caminho. Deixe como está para manter o endpoint atual, ou cole uma nova URL para substituí-lo.", + "auto.sendTest": "Enviar alerta de teste", + "auto.sendTestTitle": "Enviar um alerta de teste agora", + "auto.sent": "Enviadas", + "auto.skipped": "ignoradas", + "auto.startDestinations": "Iniciar destinos", + "auto.startPlaylist": "Iniciar a playlist", + "auto.stopDestinations": "Parar destinos", + "auto.stopPlaylist": "Parar a playlist", + "auto.subscribedTo": "Inscrito em", + "auto.subtitle": "Webhooks que avisam quando algo quebrou, e agendamentos que tocam o programa sem você.", + "auto.theEndpointDidNotAccept": "O endpoint não aceitou a mensagem de teste.", + "auto.time": "Hora", + "auto.timeZone": "Fuso horário", + "auto.title": "Automação", + "auto.tzPlaceholder": "UTC", + "auto.webhookHint": "Discord e Slack fornecem um nas configurações do canal.", + "auto.webhookPlaceholder": "https://discord.com/api/webhooks/…", + "auto.webhookRules": "Regras de webhook", + "auto.webhookUrl": "URL do webhook", + "auto.weekFri": "Sex", + "auto.weekMon": "Seg", + "auto.weekSat": "Sáb", + "auto.weekSun": "Dom", + "auto.weekThu": "Qui", + "auto.weekTue": "Ter", + "auto.weekWed": "Qua", + "auto.when": "Quando", + "chatpage.allFiltered": "Todas as plataformas com mensagens estão filtradas.", + "chatpage.couldNotDeleteThatMessage": "Não foi possível excluir essa mensagem.", + "chatpage.deleteNote": "Excluir remove a mensagem na plataforma que a emitiu. Uma plataforma em que o polyemesis não consegue excluir diz isso e indica onde é possível fazê-lo — a ação nunca é ocultada por suposição.", + "chatpage.droppedTitle": "Exibidas ao vivo, mas a persistência ficou para trás e o histórico as perdeu.", + "chatpage.dupTitle": "A mesma mensagem vista duas vezes — nosso próprio eco, ou a plataforma repetindo.", + "chatpage.noAccount": "O chat está rodando, mas nenhuma conta de plataforma está conectada. Conecte uma em Configurações → Credenciais de plataforma.", + "chatpage.notRunning": "Nenhum chat está rodando neste servidor. O histórico abaixo da linha do tempo é o que foi armazenado antes, e a caixa de envio fica desativada até que uma plataforma seja conectada.", + "chatpage.received": "Recebidas", + "chatpage.reload": "Recarregar", + "chatpage.stored": "Armazenadas", + "chatpage.storedTitle": "Nada está conectado; este é o histórico armazenado.", + "chatpage.subtitle": "Todas as plataformas em uma linha do tempo, com uma única caixa de envio.", + "chatpage.timeline": "Linha do tempo", + "chatpage.title": "Chat", "chrome.ingest": "Ingest", + "chrome.language": "Idioma", "chrome.liveCount": "{count} ao vivo", + "chrome.signOut": "Sair", "chrome.socketOffline": "offline", "chrome.socketOfflineHint": "As atualizações ao vivo foram desconectadas; tentando novamente", - "chrome.signOut": "Sair", - "chrome.language": "Idioma", + "chrome.toggleNav": "Alternar navegação", + "clipedit.back33": "Voltar 33 ms", + "clipedit.clickLine": "Clique em uma linha para ir até ela. Selecione várias e os pontos de entrada e saída passam para as palavras que você destacou.", + "clipedit.clipThisLine": "Clipar esta linha", + "clipedit.clipThisLineTitle": "Definir os pontos de entrada e saída em torno desta linha", + "clipedit.copied": "Copiado", + "clipedit.copiesEveryPacketSecondsBit": "Copia todos os pacotes. Questão de segundos, exato bit a bit, e o início volta para o quadro-chave mais próximo.", + "clipedit.couldNotOpenThatRecording": "Não foi possível abrir essa gravação.", + "clipedit.couldNotQueueTheExport": "Não foi possível colocar a exportação na fila.", + "clipedit.cut": "Corte", + "clipedit.downloadClip": "Baixar o clipe", + "clipedit.exportQueued": "Exportação na fila. Ela roda quando a máquina puder ceder os ciclos.", + "clipedit.exports": "Exportações", + "clipedit.fast": "Rápido", + "clipedit.filterTranscript": "Filtrar a transcrição", + "clipedit.findSaid": "Encontrar o que foi dito", + "clipedit.forward33": "Avançar 33 ms", + "clipedit.in": "Entrada", + "clipedit.keepAllTracks": "Manter todas as faixas de áudio", + "clipedit.keepsEveryAudioTrackIn": "Mantém todas as faixas de áudio em qualquer codec. O que o gravador escreve.", + "clipedit.markIn": "I — marcar a entrada no cursor de reprodução", + "clipedit.markOut": "O — marcar a saída no cursor de reprodução", + "clipedit.name": "Nome", + "clipedit.noKeyframes": "Não foi possível ler as posições dos quadros-chave, então ninguém pode dizer onde este corte vai realmente começar. Ele ainda vai funcionar — a própria busca do FFmpeg cai em um quadro-chave — mas o início pode vir até um GOP antes, e esta página não tem como mostrar por quanto.", + "clipedit.noTranscript": "Nada foi transcrito para esta gravação ainda. Cada microfone foi gravado em sua própria faixa, então transcrever dá os nomes de quem falou sem um modelo de diarização — e aí você pode clipar pelo que foi dito.", + "clipedit.nothingExported": "Nada exportado ainda.", + "clipedit.out": "Saída", + "clipedit.pickAtLeastOneAudio": "Escolha ao menos uma faixa de áudio, ou leve todas.", + "clipedit.playFaster": "L — reproduzir, de novo para acelerar", + "clipedit.precise": "Preciso", + "clipedit.queueNote": "A exportação vai para a fila, não roda aqui: ela cede lugar à transmissão ao vivo, então um corte feito no meio da transmissão espera em vez de roubar os ciclos de que a transmissão precisa.", + "clipedit.reEncodesOnlyTheLeading": "Recodifica apenas o GOP parcial inicial para que o início seja exato. O resto continua sendo copiado.", + "clipedit.reencoded": "Recodificado", + "clipedit.shortcuts": "J / K / L fazem o shuttle, I e O marcam entrada e saída, as setas deslocam 33 ms e shift+setas um segundo. O passo fino é um quadro a 30 fps; a taxa real desta gravação não está indexada, então ele é indicado em milissegundos em vez de ser adivinhado.", + "clipedit.shuttleBack": "J — shuttle para trás", + "clipedit.startMoved": "Início deslocado", + "clipedit.starts": "Começa", + "clipedit.stop": "K — parar", + "clipedit.thatCutCouldNotBe": "Não foi possível planejar esse corte.", + "clipedit.thatIsNotARecording": "Isso não é um id de gravação.", + "clipedit.thatRecordingIsNotAvailable": "Essa gravação não está disponível.", + "clipedit.title": "Editor de clipes", + "clipedit.whatASocialPlatformWill": "O que uma plataforma social aceita. Menos codecs sobrevivem a isso.", + "clipedit.zoomIn": "Aumentar o zoom", + "clipedit.zoomOut": "Reduzir o zoom", + "clips.bitrate": "Taxa de bits", + "clips.buffer": "Buffer", + "clips.bufferLiveNote": "O interruptor do buffer é uma chave ao vivo, não uma configuração salva: ele volta a ficar desligado depois de um reinício.", + "clips.captured": "Capturado", + "clips.ceiling": "Teto", + "clips.clipsKept": "Clipes mantidos", + "clips.couldNotCaptureAClip": "Não foi possível capturar um clipe.", + "clips.couldNotChangeTheClip": "Não foi possível alterar o buffer de clipes.", + "clips.couldNotDeleteTheClip": "Não foi possível excluir o clipe.", + "clips.couldNotLoadClips": "Não foi possível carregar os clipes.", + "clips.covers": "Abrange", + "clips.customLength": "Duração personalizada do clipe em segundos", + "clips.delete": "Excluir", + "clips.deleteDescription": "Um clipe geralmente é justamente o material que você quis guardar. Isto remove o arquivo; a gravação de onde ele saiu fica intacta.", + "clips.deleteTitle": "Excluir este clipe?", + "clips.deleted": "Clipe excluído.", + "clips.download": "Baixar", + "clips.everythingHeld": "Tudo o que o buffer está guardando", + "clips.file": "Arquivo", + "clips.held": "Retido", + "clips.inMemory": "Na memória", + "clips.keepBuffer": "Manter um buffer rotativo", + "clips.keyframes": "Quadros-chave", + "clips.length": "Duração", + "clips.none": "nenhum", + "clips.off": "desligado", + "clips.onDisk": "Em disco", + "clips.seen": "visto", + "clips.settings": "Configurações", + "clips.size": "Tamanho", + "clips.subtitle": "Um buffer rotativo do que acabou de sair. Capture depois do fato — nada precisa ser preparado com antecedência.", + "clips.theClipDoesNotStart": "O clipe não começa em um quadro-chave.", + "clips.title": "Clipes", + "clips.waitingForStream": "aguardando a transmissão", + "clips.window": "Janela", + "clips.windowSeconds": "Janela (segundos)", "common.add": "Adicionar", + "common.apply": "Aplicar", "common.cancel": "Cancelar", "common.close": "Fechar", "common.confirm": "Confirmar", @@ -28,110 +229,921 @@ "common.copy": "Copiar", "common.delete": "Excluir", "common.disabled": "Desativado", + "common.discard": "Descartar", "common.edit": "Editar", "common.enabled": "Ativado", "common.loading": "Carregando…", + "common.moreInfo": "Mais informações", "common.refresh": "Atualizar", "common.restart": "Reiniciar", "common.retry": "Tentar novamente", "common.save": "Salvar", "common.start": "Iniciar", "common.stop": "Parar", - "state.live": "Ao vivo", - "state.reconnecting": "Reconectando", - "state.starting": "Iniciando", - "state.failed": "Falhou", - "state.stopped": "Parado", - "state.offline": "Offline", - "hooks.title": "Webhooks de ciclo de vida", - "hooks.new": "Novo webhook", - "hooks.empty": "Nenhum webhook. Adicione um e o polyemesis fará POST de um corpo JSON assinado assim que o stream começar ou parar, ou um destino subir ou cair — uma entrega por transição, em ordem, para um script agir.", - "hooks.colWebhook": "Webhook", - "hooks.colEndpoint": "Endpoint", - "hooks.colTriggers": "Inscrito em", - "hooks.colEnabled": "Lig.", - "hooks.signed": "assinado", - "hooks.unsigned": "SEM ASSINATURA", + "dash.audioTracks": "Faixas de áudio", + "dash.bitrate": "Taxa de bits", + "dash.broadcastSettings": "Configurações da transmissão", + "dash.categoryPlaceholder": "Música", + "dash.clipboardUnavailable": "A área de transferência não está disponível nesta origem.", + "dash.connectPrompt": "Conecte uma conta do YouTube, Twitch, Facebook ou Kick em Configurações → Credenciais de plataforma para definir o título, a descrição e a categoria da sua transmissão em todas as plataformas de uma vez. Cada uma aceita um subconjunto diferente — o compositor indica quais, e envia o que puder.", + "dash.copyIngestUrl": "Copiar a URL de entrada", + "dash.couldNotLoadTheDestination": "Não foi possível carregar o destino.", + "dash.couldNotPushTheMetadata": "Não foi possível enviar os metadados.", + "dash.couldNotReorderTheDestinations": "Não foi possível reordenar os destinos.", + "dash.deleteDest": "Excluir destino", + "dash.deleteDestDescription": "Isto remove o destino e o perfil de roteamento dele. A plataforma não é alterada, e você pode adicioná-lo de volta — mas o roteamento de áudio que você configurou para ele se perde.", + "dash.descriptionPlaceholder": "O que é esta transmissão.", + "dash.destDeleted": "Destino excluído.", + "dash.disabled": "desativado", + "dash.everyConnectedAccount": "todas as contas conectadas", + "dash.idle": "ocioso", + "dash.ingestUrlCopied": "URL de entrada copiada.", + "dash.keyRefreshed": "Chave de transmissão atualizada a partir da plataforma.", + "dash.leaveUnchanged": "Deixar sem alteração", + "dash.leaveUnchangedNote": "“Deixar sem alteração” não envia absolutamente nada, então a plataforma mantém o que tem. Escolher Ligado ou Desligado grava aquele valor.", + "dash.metaCategory": "Categoria", + "dash.metaDescription": "Descrição", + "dash.metaTags": "Tags", + "dash.metaTitle": "Título", + "dash.noDestinations": "Nenhum destino ainda. Adicione um e depois escolha quais faixas de áudio ele recebe.", + "dash.off": "Desligado", + "dash.pipeline": "Pipeline", + "dash.pushToPlatforms": "Enviar às plataformas", + "dash.pushing": "Enviando…", + "dash.reconnects": "Reconexões", + "dash.relaySubscribers": "Assinantes do relay", + "dash.renditions": "Versões", + "dash.scheduledStart": "Início agendado", + "dash.subtitle": "Uma entrada só, distribuída para muitos: vídeo compartilhado e áudio próprio por destino.", + "dash.tagsPlaceholder": "live, house, dj set", + "dash.theConnectedAccount": "a conta conectada", + "dash.title": "Painel", + "dash.titlePlaceholder": "Set de sexta à noite", + "dash.uptime": "Tempo ativo", + "dash.youtubeStopsAcceptingTheseOnce": "O YouTube deixa de aceitá-los assim que uma transmissão sai de “created” ou “ready”.", + "dest.audience": "Público", + "dest.audioBitrate": "Bitrate de áudio", + "dest.audioCodec": "Codec de áudio", + "dest.channels": "Canais", + "dest.compliance": "Conformidade", + "dest.connectedAccount": "Conta conectada", + "dest.contentLabels": "Rótulos de conteúdo", + "dest.copySource": "Copiar o vídeo de origem", + "dest.createVariant": "Criar esta variante", + "dest.customiseSummary": "Personalizar para este destino", + "dest.fbBackupCost": "Dobra a largura de banda de upload deste destino.", + "dest.fbBackupHelp": "Envia uma segunda cópia desta transmissão para a entrada reserva do Facebook, para que uma conexão perdida não derrube a transmissão.", + "dest.fbBackupLabel": "Publicar um feed reserva", + "dest.fbBackupReconnect": "Ativar isso reconecta a transmissão uma vez — então ative antes de entrar ao vivo.", + "dest.fbBoxIntro": "Enviado na mesma chamada de criação que Público, então vale a mesma regra: aplicado uma vez quando a transmissão começa, e deixado totalmente intacto se estiver vazio.", + "dest.fbBoxTitle": "Publicação cruzada e botão de doação do Facebook", + "dest.fbCrosspostAdd": "Adicionar Página", + "dest.fbCrosspostAlsoPost": "Publicar também", + "dest.fbCrosspostHelp": "O ID numérico da Página no próprio console do Facebook (configurações da Página, ou graph.facebook.com/me/accounts conectado como a Página) — não há busca aqui, então cole o id em vez de um nome ou URL. “Publicar também” publica como aquela Página em vez de apenas compartilhar a transmissão com ela; desligado, acontece só o compartilhamento mais discreto.", + "dest.fbCrosspostLabel": "Publicação cruzada em Páginas", + "dest.fbCrosspostPageId": "ID da Página", + "dest.fbCrosspostRemove": "Remover esta Página", + "dest.fbDonateHelp": "Também é um id opaco do console de arrecadações do Facebook, não um nome de instituição para pesquisar. Deixe em branco para não anexar botão de doação algum.", + "dest.fbDonateLabel": "ID da instituição do botão de doação", + "dest.fbDonatePlaceholder": "ID da instituição", + "dest.fillFromPlatform": "Preencher com o que esta plataforma publica", + "dest.friends": "Amigos", + "dest.friendsOfFriends": "Amigos de amigos", + "dest.giveUpAfter": "Desistir após (tentativas)", + "dest.leaveOnFacebook": "Deixar como está no Facebook", + "dest.leaveOnYouTube": "Deixar como está no YouTube", + "dest.madeForKids": "Feito para crianças (COPPA)", + "dest.muxQueue": "Fila de multiplexação (pacotes)", + "dest.muxQueuePair": "Estes dois andam juntos: o FFmpeg só aplica o limite de pacotes depois que a fila passa do limiar em bytes, então um limiar sozinho não faz nada e será recusado. Aumente ambos se um destino relatar erros de interleave — o caminho de áudio aqui tem latência variável, porque a normalização de loudness lê adiante.", + "dest.name": "Nome", + "dest.namePlaceholder": "YouTube — canal principal", + "dest.notLinked": "Não vinculada", + "dest.onlyMe": "Somente eu", + "dest.platform": "Plataforma", + "dest.private": "Privado", + "dest.public": "Público", + "dest.reconnecting": "Reconectando", + "dest.retryAfter": "Tentar novamente após (segundos)", + "dest.sharedEncodeAria": "Codificação de vídeo compartilhada", + "dest.socketTimeout": "Tempo limite do socket (segundos)", + "dest.streamKeyLabel": "Chave de transmissão", + "dest.transport": "Transporte", + "dest.transportFile": "Arquivo local", + "dest.transportRtmp": "RTMP / RTMPS", + "dest.unlisted": "Não listado", + "dest.urlFromDashboard": "URL do painel", + "dest.useSharedEncode": "Usar uma codificação de vídeo compartilhada", + "dest.varBitrate": "Bitrate de vídeo (kbps)", + "dest.varFps": "Taxa de quadros", + "dest.varHeight": "Altura", + "dest.varSourceHint": "origem", + "dest.varWidth": "Largura", + "dest.variantWarning": "Inicia uma segunda codificação a partir da sua origem, tendo “{name}” como base. Não é uma variação gratuita e não muda o que os outros destinos dessa codificação recebem. Deixe um campo em branco para manter o valor da origem.", + "dest.videoTreatment": "Tratamento de vídeo", + "dest.visibility": "Visibilidade", + "hooks.allTriggers": "tudo", "hooks.attempts": "{count} tentativas", + "hooks.attemptsLabel": "Tentativas", "hooks.attemptsOne": "{count} tentativa", - "hooks.timeout": "{seconds}s de tempo limite", - "hooks.allTriggers": "tudo", - "hooks.triggerCount": "{count} gatilhos", - "hooks.triggerCountOne": "{count} gatilho", - "hooks.enableAria": "Ativar {name}", + "hooks.colEnabled": "Lig.", + "hooks.colEndpoint": "Endpoint", + "hooks.colTriggers": "Inscrito em", + "hooks.colWebhook": "Webhook", + "hooks.create": "Criar", + "hooks.created": "Webhook criado.", + "hooks.deleteBody": "Ele para de entregar imediatamente. O endpoint não é alterado e você pode recriar o hook — mas a chave de assinatura dele não pode ser recuperada.", + "hooks.deleteConfirm": "Excluir webhook", + "hooks.deleteFailed": "Não foi possível excluir o hook.", + "hooks.deleteTitle": "Excluir este webhook?", + "hooks.deleted": "Webhook excluído.", "hooks.deliveries": "Entregas recentes", - "hooks.sendTest": "Enviar entrega de teste", - "hooks.sendTestTitle": "Enviar uma entrega de teste agora", - "hooks.secretTitle": "Chave de assinatura — copie agora, ela não pode ser exibida de novo", - "hooks.secretCopied": "Já copiei", - "hooks.httpStatus": "HTTP {status}", - "hooks.noResponse": "sem resposta", + "hooks.deliveriesFailed": "Não foi possível ler as entregas recentes.", + "hooks.deliveryTitle": "Entrega", + "hooks.dialogIntro": "Um POST assinado por transição, em ordem. Diferente de um alerta, nada é agrupado nem tem debounce — um script recebe todas as bordas.", "hooks.durationMs": "{ms} ms", + "hooks.editTitle": "Editar webhook", + "hooks.empty": "Nenhum webhook. Adicione um e o polyemesis fará POST de um corpo JSON assinado assim que o stream começar ou parar, ou um destino subir ou cair — uma entrega por transição, em ordem, para um script agir.", + "hooks.enableAria": "Ativar {name}", + "hooks.httpStatus": "HTTP {status}", + "hooks.loadFailed": "Não foi possível carregar os webhooks.", + "hooks.name": "Nome", + "hooks.new": "Novo webhook", + "hooks.newTitle": "Novo webhook", "hooks.noDeliveries": "Nada entregue ainda. As entregas aparecem aqui conforme as transições acontecem.", - "hooks.deliveryTitle": "Entrega", - "hooks.statSent": "Enviadas", - "hooks.statFailed": "Falhas", + "hooks.noResponse": "sem resposta", + "hooks.payloadNote": "Payload {version}. Assinatura em {header}, sobre o timestamp e o corpo.", + "hooks.retriesHint": "As novas tentativas bloqueiam a fila do próprio endpoint, porque a ordem é a promessa. Um 4xx nunca é repetido — um endpoint que diz que a requisição está errada vai dizer de novo.", + "hooks.saveFailed": "Não foi possível salvar o hook.", + "hooks.secretCopied": "Já copiei", + "hooks.secretTitle": "Chave de assinatura — copie agora, ela não pode ser exibida de novo", + "hooks.sendTest": "Enviar entrega de teste", + "hooks.sendTestTitle": "Enviar uma entrega de teste agora", + "hooks.signed": "assinado", + "hooks.statDropped": "Descartadas", "hooks.statEndpoints": "Endpoints", + "hooks.statFailed": "Falhas", "hooks.statQueued": "Na fila", - "hooks.statDropped": "Descartadas", "hooks.statRetries": "Novas tentativas", - "hooks.payloadNote": "Payload {version}. Assinatura em {header}, sobre o timestamp e o corpo.", - "hooks.deleteTitle": "Excluir este webhook?", - "hooks.deleteBody": "Ele para de entregar imediatamente. O endpoint não é alterado e você pode recriar o hook — mas a chave de assinatura dele não pode ser recuperada.", - "hooks.deleteConfirm": "Excluir webhook", - "hooks.editTitle": "Editar webhook", - "hooks.newTitle": "Novo webhook", - "hooks.dialogIntro": "Um POST assinado por transição, em ordem. Diferente de um alerta, nada é agrupado nem tem debounce — um script recebe todas as bordas.", - "hooks.name": "Nome", - "hooks.url": "URL do endpoint", - "hooks.urlMasked": "A URL armazenada nunca é mostrada por inteiro. Deixe o valor mascarado para mantê-la, ou digite uma nova URL para substituí-la.", - "hooks.triggers": "Gatilhos", - "hooks.triggersNoneHint": "Nenhum selecionado significa todos os gatilhos, inclusive os que uma versão futura adicionar.", + "hooks.statSent": "Enviadas", + "hooks.testFailed": "O endpoint não aceitou a entrega de teste.", + "hooks.timeout": "{seconds}s de tempo limite", "hooks.timeoutLabel": "Tempo limite (s)", - "hooks.attemptsLabel": "Tentativas", - "hooks.retriesHint": "As novas tentativas bloqueiam a fila do próprio endpoint, porque a ordem é a promessa. Um 4xx nunca é repetido — um endpoint que diz que a requisição está errada vai dizer de novo.", - "hooks.create": "Criar", - "hooks.triggerIngestPublished": "Stream iniciado", - "hooks.triggerIngestDisconnected": "Stream parado", - "hooks.triggerDestinationUp": "Destino ao vivo", - "hooks.triggerDestinationDown": "Destino parado", - "hooks.loadFailed": "Não foi possível carregar os webhooks.", + "hooks.title": "Webhooks de ciclo de vida", "hooks.toggleFailed": "Não foi possível alterar o hook.", - "hooks.deleted": "Webhook excluído.", - "hooks.deleteFailed": "Não foi possível excluir o hook.", - "hooks.testFailed": "O endpoint não aceitou a entrega de teste.", - "hooks.deliveriesFailed": "Não foi possível ler as entregas recentes.", + "hooks.triggerCount": "{count} gatilhos", + "hooks.triggerCountOne": "{count} gatilho", + "hooks.triggerDestinationDown": "Destino parado", + "hooks.triggerDestinationUp": "Destino ao vivo", + "hooks.triggerIngestDisconnected": "Stream parado", + "hooks.triggerIngestPublished": "Stream iniciado", + "hooks.triggers": "Gatilhos", + "hooks.triggersNoneHint": "Nenhum selecionado significa todos os gatilhos, inclusive os que uma versão futura adicionar.", + "hooks.unsigned": "SEM ASSINATURA", "hooks.updated": "Webhook atualizado.", - "hooks.created": "Webhook criado.", - "hooks.saveFailed": "Não foi possível salvar o hook.", - "playlist.title": "Playlist", + "hooks.url": "URL do endpoint", + "hooks.urlMasked": "A URL armazenada nunca é mostrada por inteiro. Deixe o valor mascarado para mantê-la, ou digite uma nova URL para substituí-la.", + "jobs.alwaysKeepNewest": "Sempre manter as mais recentes", + "jobs.automaticModel": "Automático (escolhido a partir deste hardware)", + "jobs.backToDefault": "Voltar ao modo padrão para este tipo", + "jobs.batteryFloor": "Piso de bateria %", + "jobs.broadcastGoing": "Há uma transmissão no ar; o trabalho adiado cede lugar a ela.", + "jobs.cancel": "Cancelar", + "jobs.cannotPause": "Estes tipos deveriam ser pausados, mas não podem, então estão terminando.", + "jobs.completed": "Concluídas", + "jobs.concurrency": "Concorrência", + "jobs.couldNotReadTheJob": "Não foi possível ler a fila de tarefas.", + "jobs.couldNotSaveThePolicy": "Não foi possível salvar a política.", + "jobs.cpuCeiling": "Teto de CPU %", + "jobs.cpuResume": "Retomada de CPU %", + "jobs.defaultMode": "Modo padrão", + "jobs.defaultModel": "Modelo padrão", + "jobs.deferred": "Adiadas", + "jobs.failed": "Falhas", + "jobs.finished": "Terminadas", + "jobs.global": "Global", + "jobs.gpuGateNote": "Só os tipos marcados como usuários da GPU são limitados por isto.", + "jobs.history": "Histórico", + "jobs.historyRetention": "Retenção do histórico", + "jobs.holdDeferredWorkBackOn": "Segura o trabalho adiado em um laptop descarregando abaixo deste valor. 0 desativa.", + "jobs.hostCpuLoad": "Carga de CPU do host", + "jobs.howManyJobsMayRun": "Quantas tarefas podem rodar ao mesmo tempo. Uma, porque uma segunda transcodificação compra uma vazão que ninguém pediu a um risco que ninguém aceitou.", + "jobs.inQueue": "Na fila", + "jobs.job": "Tarefa", + "jobs.keepForDays": "Manter por (dias)", + "jobs.machineSafety": "Segurança da máquina. Este é o único limite que o trabalho em tempo real também respeita.", + "jobs.niceLevel": "Nível nice", + "jobs.noFinished": "Nenhuma tarefa terminada ainda.", + "jobs.noModels": "Nenhum arquivo de modelo foi encontrado no diretório de modelos. O modo automático baixará o que escolher; indicar aqui um modelo específico só funciona depois que esse arquivo existir.", + "jobs.nothingNewStartsAboveThis": "Acima disto nada novo começa. 0 desativa a trava.", + "jobs.nothingQueued": "Nada na fila. A pós-produção é enviada a partir da biblioteca.", + "jobs.nothingWasOldEnoughTo": "Nada era antigo o bastante para ser purgado. Baixe antes a retenção em Política.", + "jobs.pauseAll": "Pausar tudo", + "jobs.policy": "Política", + "jobs.policySaved": "Política de recursos salva.", + "jobs.progress": "Progresso", + "jobs.queuePaused": "Fila pausada.", + "jobs.queueResumed": "Fila retomada.", + "jobs.queued": "Enfileirada", + "jobs.recording": "Gravação", + "jobs.releasedItWillStartAs": "Liberada. Vai começar assim que houver uma vaga livre.", + "jobs.removeFromHistory": "Remover do histórico", + "jobs.removeWindow": "Remover janela", + "jobs.resumeAll": "Retomar tudo", + "jobs.retry": "Tentar novamente", + "jobs.retryTitle": "Rearmar com um novo orçamento de tentativas", + "jobs.runNow": "Executar agora", + "jobs.runNowTitle": "Executar esta agora, ignorando a política de recursos", + "jobs.running": "Em execução", + "jobs.savedTheConcurrencyChangeTakes": "Salvo. A mudança de concorrência entra em vigor quando o servidor reinicia — a fila fixa isso ao iniciar.", + "jobs.state": "Estado", + "jobs.stopsEverythingRealtimeIncludedA": "Para tudo, inclusive o tempo real: uma CPU em throttling já começou a degradar a transmissão. 0 desativa.", + "jobs.subtitle": "Trabalho em segundo plano: transcrição, proxies, miniaturas e compressão do arquivo.", + "jobs.thatDidNotWork": "Isso não funcionou.", + "jobs.thermalCeiling": "Teto térmico °C", + "jobs.thisManyFinishedJobsSurvive": "Esta quantidade de tarefas terminadas sobrevive qualquer que seja a idade delas.", + "jobs.title": "Tarefas", + "jobs.transcription": "Transcrição", + "jobs.unsavedChanges": "Alterações não salvas.", + "jobs.whereTheCeilingReleasesThe": "Onde o teto libera. A diferença é a histerese que impede a trava de oscilar.", + "jobs.whisperMissing": "O whisper.cpp não está instalado aqui, então isto ainda não tem efeito. Ainda vale a pena configurar: a escolha passa a valer assim que ele estiver.", + "jobs.why": "Por quê", + "jobs.windowEnd": "Fim da janela", + "jobs.windowStart": "Início da janela", + "jobs.yieldNote": "O trabalho pesado vai para a fila e cede lugar à transmissão ao vivo. Um quadro perdido é irrecuperável; uma transcrição que chega uma hora depois não custa nada.", + "lib.anySpeaker": "Qualquer participante", + "lib.anyTag": "Qualquer tag", + "lib.autoGrouped": "Agrupada automaticamente. Editar torna-a sua.", + "lib.autoGroupedNote": "Esta sessão foi agrupada automaticamente. Salvar torna-a sua, e a passagem de agrupamento deixará de reescrevê-la.", + "lib.bestMatch": "Melhor correspondência", + "lib.clearSearch": "Limpar a busca", + "lib.clip": "Clipar", + "lib.clipTitle": "Cortar um clipe desta gravação", + "lib.collapseSession": "Recolher sessão", + "lib.couldNotLoadTheLibrary": "Não foi possível carregar a biblioteca.", + "lib.couldNotLoadTheSession": "Não foi possível carregar os segmentos da sessão.", + "lib.couldNotQueueTheProxy": "Não foi possível colocar o proxy na fila.", + "lib.couldNotRegroup": "Não foi possível reagrupar.", + "lib.couldNotSaveTheSession": "Não foi possível salvar a sessão.", + "lib.delete": "Excluir", + "lib.deleteDescription": "O arquivo é removido do disco, junto com seus proxies, miniaturas e transcrição. Isso não pode ser desfeito.", + "lib.deleteTitle": "Excluir esta gravação?", + "lib.downloadMaster": "Baixar o master", + "lib.downloadMasterTitle": "Baixar o master multipista", + "lib.editSession": "Editar sessão", + "lib.expandSession": "Expandir sessão", + "lib.from": "De", + "lib.generateProxy": "Gerar proxy", + "lib.generateProxyTitle": "Gerar o proxy reproduzível no navegador", + "lib.mostRecent": "Mais recentes", + "lib.noProxyYet": "Ainda sem proxy — abra para gerar um", + "lib.noSegments": "Nenhum segmento aqui.", + "lib.noSessionMatch": "Nenhuma sessão corresponde a esses filtros.", + "lib.nothingRecorded": "Nada gravado ainda. As sessões aparecem conforme o gravador escreve segmentos.", + "lib.oldestFirst": "Mais antigas primeiro", + "lib.playProxy": "Reproduzir o proxy", + "lib.proxyAlreadyQueued": "Já há um proxy na fila.", + "lib.proxyQueued": "Proxy na fila.", + "lib.recordingDeleted": "Gravação excluída.", + "lib.regroupTitle": "Roda o agrupamento de novo. Aditivo e idempotente: nunca junta um agrupamento que você separou à mão.", + "lib.searchPlaceholder": "Busque tudo o que alguém disse, em todas as gravações…", + "lib.searchTranscripts": "Buscar nas transcrições", + "lib.searchable": "Pesquisável", + "lib.searchableOnly": "Segmentos com transcrição, portanto pesquisáveis", + "lib.session": "Sessão", + "lib.sessionDescription": "Descrição", + "lib.sessionSaved": "Sessão salva.", + "lib.sessionTags": "Tags", + "lib.sessionTitle": "Título", + "lib.subtitle": "Todas as transmissões, pesquisáveis. Cada microfone foi gravado em sua própria faixa, então a transcrição já sabe quem falou.", + "lib.tag": "Tag", + "lib.tagsPlaceholder": "separadas, por vírgula", + "lib.title": "Biblioteca", + "lib.transcribe": "Transcrever", + "lib.transcribeEachTrackOnIts": "Transcrever cada faixa separadamente — é isso que dá a atribuição de quem falou", + "lib.ungrouped": "Segmentos não agrupados", + "meters.deviation": "Desvio", + "meters.idle": "ocioso", + "meters.integrated": "Integrada", + "meters.loudness": "Conformidade de loudness", + "meters.metering": "medindo", + "meters.momentary": "Momentânea", + "meters.monitorOff": "O monitor está desligado. Ligue-o para medir o que cada destino está entregando.", + "meters.noSignal": "sem sinal nesta faixa", + "meters.nothingToMeasure": "Nada a medir ainda. Cada destino em execução ganha seu próprio analisador EBU R128 sobre a mixagem que recebe.", + "meters.range": "Faixa", + "meters.shortTerm": "Curto prazo", + "meters.subtitle": "Cada canal de cada faixa de entrada, direto do relay.", + "meters.title": "Medidores de áudio", + "meters.truePeak": "Pico real", + "meters.waitingForStream": "aguardando a transmissão", + "mon.allProcesses": "Todos os processos", + "mon.bitrateChart": "Taxa de bits da entrada — últimos 30 minutos", + "mon.chooseDestination": "Escolha um destino…", + "mon.clearLog": "Limpar log", + "mon.clearedThisDestinationIsBack": "Limpo. Este destino voltou ao comando gerado.", + "mon.dryRun": "Execução simulada", + "mon.dryRunInvalid": "O FFmpeg rejeitou um argumento", + "mon.dryRunOk": "O FFmpeg aceitou", + "mon.extraInput": "Argumentos extras de entrada", + "mon.extraOutput": "Argumentos extras de saída", + "mon.ffmpegLog": "Log do FFmpeg", + "mon.fromRunningProcess": "do processo em execução", + "mon.fullCommand": "Comando resolvido completo", + "mon.hostCpu": "CPU do host", + "mon.ingestLoss": "Perda na entrada", + "mon.memory": "Memória", + "mon.nothingProvenEitherWay": "Nada provado em nenhum sentido", + "mon.pid": "PID", + "mon.processes": "Processos", + "mon.rebuilt": "reconstruído", + "mon.relayDrops": "Descartes do relay", + "mon.relayIn": "Entrada do relay", + "mon.resolveHint": "Resolva o comando para ver exatamente o que será executado antes de aplicar.", + "mon.restarts": "Reinícios", + "mon.subtitle": "Saúde dos processos, recursos do host e o log do FFmpeg ao vivo.", + "mon.title": "Monitoramento", + "mon.uptime": "Tempo ativo", + "nav.automation": "Automação", + "nav.chat": "Chat", + "nav.clips": "Clipes", + "nav.dashboard": "Painel", + "nav.jobs": "Tarefas", + "nav.library": "Biblioteca", + "nav.meters": "Medidores de áudio", + "nav.monitoring": "Monitoramento", + "nav.playout": "Playout", + "nav.recordings": "Gravações", + "nav.renditions": "Versões", + "nav.routing": "Routing", + "nav.settings": "Configurações", + "nav.sources": "Fontes", + "play.acrossEveryRungTheBackstop": "Em todos os degraus. A salvaguarda que impede uma transmissão longa de encher o disco.", + "play.allowCrossOrigin": "Permitir cross-origin", + "play.allowCrossOriginPlayback": "Permitir reprodução cross-origin", + "play.audioBitrate": "Taxa de bits do áudio (kbps)", + "play.audioTrack": "Faixa de áudio", + "play.basicPasswordNote": "Reprodutores que não conseguem seguir um link (um set-top box, um CDN) podem enviar o mesmo segredo como senha HTTP basic.", + "play.boundsTheViewerTableViewers": "Limita a tabela de espectadores. Espectadores além dela são atendidos, apenas não contados.", + "play.carriesThePlaybackTokenAnyone": "Carrega o token de reprodução. Qualquer pessoa com esta URL pode assistir.", + "play.clipboardUnavailable": "A área de transferência não está disponível; selecione e copie à mão", + "play.couldNotLoadPlayout": "Não foi possível carregar o playout", + "play.disabled": "Desativado", + "play.diskCap": "Teto de disco (MB)", + "play.dvrWindow": "Janela de DVR (s)", + "play.embedCode": "Código de incorporação", + "play.enable": "Ativar o playout", + "play.enabled": "Ativado", + "play.forObsACdnOr": "Para o OBS, um CDN, ou qualquer player que fale HLS.", + "play.format": "Formato", + "play.hls": "HLS", + "play.hlsDash": "HLS e DASH", + "play.hlsPlaylist": "Playlist HLS", + "play.idleTimeout": "Tempo limite de espectador ocioso (s)", + "play.linkReset": "Novo link de reprodução gerado. O anterior não funciona mais.", + "play.linksProtected": "Estes links só funcionam para um administrador conectado até você ativar Público.", + "play.linksPublic": "Estes links funcionam sem conta.", + "play.liveWindow": "Janela ao vivo (segmentos)", + "play.metaDescription": "Descrição", + "play.metaTitle": "Título", + "play.mustExceedOneSegmentOr": "Precisa passar de um segmento, ou os espectadores somem intermitentemente entre as sondagens.", + "play.name": "Nome", + "play.noCredentialNote": "Nenhuma credencial de qualquer tipo é exigida. Apropriado apenas para uma transmissão que você queira que a internet inteira veja.", + "play.noRungs": "Nenhum degrau ainda. Adicione um para publicar qualquer coisa.", + "play.onDisk": "Em disco", + "play.oneStereoAacTrackPer": "Uma faixa AAC estéreo por degrau.", + "play.packaging": "Empacotamento", + "play.passthrough": "Entrada (repasse direto)", + "play.peak": "Pico", + "play.playerPage": "Página do reprodutor", + "play.posterNote": "Exibida acima do vídeo na página pública. O pôster é um quadro-chave recente, atualizado automaticamente.", + "play.private": "Privado.", + "play.protectedNote": "A reprodução é protegida por padrão. Abri-la é um passo separado e deliberado.", + "play.protection": "Proteção", + "play.public": "Público", + "play.resetFailed": "Não foi possível redefinir", + "play.restartNote": "Alterar qualquer um destes reinicia os muxers, o que interrompe os espectadores por alguns segundos.", + "play.rungNote": "Cada degrau copia o vídeo da sua versão bit a bit, então acrescentar um custa um muxer e nunca uma segunda codificação de vídeo. Só a faixa de áudio muda de um degrau para outro; o roteamento por destino não é alterado.", + "play.segmentLength": "Duração do segmento (s)", + "play.serveWithoutSession": "Atender espectadores sem sessão", + "play.sessionCap": "Teto de sessões", + "play.sessions": "Sessões", + "play.share": "Compartilhar", + "play.shorterIsLowerLatencyAnd": "Mais curto significa menos latência e mais rotatividade na playlist.", + "play.subtitle": "Entregue a transmissão diretamente aos espectadores, como HLS público.", + "play.threeIsTheMinimumA": "Três é o mínimo com que um player consegue formar buffer.", + "play.title": "Playout", + "play.unlisted": "Não listado.", + "play.untitled": "Transmissão sem título", + "play.videoSource": "Fonte de vídeo", + "play.watchingNow": "Assistindo agora", + "play.whoCanWatch": "Quem pode assistir", + "player.copied": "Copiado", + "player.copy": "Copiar", + "player.loading": "Carregando…", + "player.missingToken": "O link que você seguiu está sem o token de reprodução, ou o token foi alterado. Peça um link atual a quem o compartilhou.", + "player.notLive": "Sem transmissão ao vivo no momento", + "player.viewer": "espectador", + "player.viewers": "espectadores", + "player.willStart": "Esta página começará a reproduzir sozinha quando a transmissão começar.", + "playlist.choose": "Escolha um upload", + "playlist.chooseAria": "Escolha um upload para adicionar", "playlist.empty": "Nada na fila ainda.", + "playlist.hint": "Enviados em Media. Um item nomeado por esta playlist que depois perca seu upload aparece acima como precisando de atenção, em vez de falhar em silêncio na próxima vez que o failover chegar nele.", "playlist.itemsAria": "Itens da playlist", - "playlist.stateReady": "Pronto", - "playlist.stateTranscoding": "Transcodificando", - "playlist.stateAttention": "Precisa de atenção", - "playlist.stateUnsaved": "Ainda não salvo", "playlist.missingUpload": "Este upload não pode mais ser encontrado.", - "playlist.moveUp": "Mover {name} para cima", "playlist.moveDown": "Mover {name} para baixo", - "playlist.remove": "Remover {name}", - "playlist.choose": "Escolha um upload", - "playlist.chooseAria": "Escolha um upload para adicionar", + "playlist.moveUp": "Mover {name} para cima", "playlist.noUploads": "Nenhum upload disponível", - "playlist.hint": "Enviados em Media. Um item nomeado por esta playlist que depois perca seu upload aparece acima como precisando de atenção, em vez de falhar em silêncio na próxima vez que o failover chegar nele.", - "dest.fbBoxTitle": "Publicação cruzada e botão de doação do Facebook", - "dest.fbBoxIntro": "Enviado na mesma chamada de criação que Público, então vale a mesma regra: aplicado uma vez quando a transmissão começa, e deixado totalmente intacto se estiver vazio.", - "dest.fbBackupLabel": "Publicar um feed reserva", - "dest.fbBackupHelp": "Envia uma segunda cópia desta transmissão para a entrada reserva do Facebook, para que uma conexão perdida não derrube a transmissão.", - "dest.fbBackupCost": "Dobra a largura de banda de upload deste destino.", - "dest.fbBackupReconnect": "Ativar isso reconecta a transmissão uma vez — então ative antes de entrar ao vivo.", - "dest.fbCrosspostLabel": "Publicação cruzada em Páginas", - "dest.fbCrosspostPageId": "ID da Página", - "dest.fbCrosspostAlsoPost": "Publicar também", - "dest.fbCrosspostRemove": "Remover esta Página", - "dest.fbCrosspostAdd": "Adicionar Página", - "dest.fbCrosspostHelp": "O ID numérico da Página no próprio console do Facebook (configurações da Página, ou graph.facebook.com/me/accounts conectado como a Página) — não há busca aqui, então cole o id em vez de um nome ou URL. “Publicar também” publica como aquela Página em vez de apenas compartilhar a transmissão com ela; desligado, acontece só o compartilhamento mais discreto.", - "dest.fbDonateLabel": "ID da instituição do botão de doação", - "dest.fbDonatePlaceholder": "ID da instituição", - "dest.fbDonateHelp": "Também é um id opaco do console de arrecadações do Facebook, não um nome de instituição para pesquisar. Deixe em branco para não anexar botão de doação algum." + "playlist.remove": "Remover {name}", + "playlist.stateAttention": "Precisa de atenção", + "playlist.stateReady": "Pronto", + "playlist.stateTranscoding": "Transcodificando", + "playlist.stateUnsaved": "Ainda não salvo", + "playlist.title": "Playlist", + "rec.delete": "Excluir", + "rec.deleteDescription": "O arquivo é removido do disco, junto com seus proxies, miniaturas e transcrição. Isso não pode ser desfeito.", + "rec.deleteFailed": "Não foi possível excluir a gravação.", + "rec.deleteTitle": "Excluir esta gravação?", + "rec.deleted": "Gravação excluída.", + "rec.disabled": "desativado", + "rec.disk": "Disco", + "rec.diskRecordings": "Gravações", + "rec.download": "Baixar", + "rec.downloadNamed": "Baixar {name}", + "rec.duration": "Duração", + "rec.empty": "Ainda não há gravações. Ative a gravação à direita e inicie uma transmissão.", + "rec.enabled": "Ativado", + "rec.file": "Arquivo", + "rec.free": "Livre", + "rec.haltedDefault": "A gravação está parada: o volume está abaixo do piso de espaço livre.", + "rec.haltedRestarts": "O gravador reinicia sozinho assim que houver espaço liberado.", + "rec.help.enabled": "Grava cada entrada em disco enquanto ela é transmitida, mantendo todas as faixas de áudio separadas e sem codificação. A gravação é independente do que você envia às plataformas: desligá-la não afeta sua saída ao vivo, e ligá-la custa disco em vez de banda de upload.", + "rec.help.maxAgeHours": "Exclui gravações com mais horas do que este valor, independentemente do tamanho total. Útil quando você só precisa do último dia ou dois. Use 0 para manter tudo até que outro limite remova.", + "rec.help.maxGb": "Um teto para o tamanho total das gravações que o polyemesis mantém. Ao ultrapassá-lo, as mais antigas são excluídas até o total caber novamente. Use 0 para não ter limite de tamanho — nesse caso, a idade ou o piso de espaço livre é a única coisa que limita o crescimento.", + "rec.help.minFreeGb": "Uma parada rígida: a gravação para quando o volume cai abaixo deste espaço livre e recomeça sozinha assim que houver espaço. É o único limite que considera arquivos que o polyemesis não gravou, então é a configuração que realmente protege um disco compartilhado de encher.", + "rec.help.segmentSeconds": "Quanto dura cada arquivo antes de o gravador começar outro. Segmentos mais curtos limitam o que uma falha pode custar e facilitam mover arquivos isolados, mas deixam mais deles para gerenciar. Os cortes caem em um quadro-chave, então a duração real varia cerca de um segundo.", + "rec.help.stemFiles": "Grava também cada faixa de áudio como um arquivo próprio ao lado do master. Útil quando você edita depois e quer o apresentador, os convidados e o áudio do jogo em canais separados. Multiplica o que uma sessão grava por aproximadamente o número de faixas, e por isso vem desligado.", + "rec.help.stemFormat": "Ambas as opções são sem perdas, então nenhuma custa qualidade. O FLAC ocupa cerca de metade e é o que você quer, a menos que alguma ferramenta do seu fluxo não consiga lê-lo; o WAV abre em qualquer coisa. Faixas com mais de 8 canais mudam para WAV automaticamente.", + "rec.hideStems": "Ocultar faixas separadas", + "rec.keepForever": "0 = manter para sempre.", + "rec.loadFailed": "Não foi possível carregar as gravações.", + "rec.maxAgeHours": "Excluir após (horas)", + "rec.maxGb": "Manter no máximo (GB)", + "rec.minFreeGb": "Parar abaixo do espaço livre (GB)", + "rec.minFreeNote": "Os limites acima só restringem o que o polyemesis gravou; qualquer outra coisa no volume ainda pode enchê-lo. 0 = sem piso.", + "rec.noSizeLimit": "0 = sem limite de tamanho.", + "rec.recording": "gravando", + "rec.retentionNote": "A retenção roda a cada 30 segundos: as mais antigas primeiro, por idade e depois por tamanho total. O segmento sendo gravado nunca é excluído.", + "rec.saveFailed": "Não foi possível salvar as configurações.", + "rec.savePolicy": "Salvar política de retenção", + "rec.saved": "Configurações de gravação salvas.", + "rec.segmentSeconds": "Duração do segmento (segundos)", + "rec.segments": "Segmentos", + "rec.settingsTitle": "Gravação", + "rec.showStems": "Mostrar faixas separadas", + "rec.size": "Tamanho", + "rec.started": "Iniciada", + "rec.stemFiles": "Arquivos de faixa separada", + "rec.stemFlac": "FLAC — sem perdas, cerca de metade do tamanho", + "rec.stemFormat": "Formato das faixas separadas", + "rec.stemNote": "Gravado pelo mesmo processo que o master, então cada faixa separada permanece alinhada por amostra com as demais. Faixas com mais de 8 canais mudam para WAV automaticamente.", + "rec.stemWav": "WAV — sem perdas, abre em qualquer lugar", + "rec.stems": "Faixas separadas", + "rec.stemsTitle": "{count} arquivos de faixa separada, alinhados por amostra com o master", + "rec.stemsUsage": "Faixas separadas ({count} arquivos)", + "rec.subtitle": "O arquivo multipista completo: cada faixa de áudio da entrada preservada, sem codificação.", + "rec.title": "Gravações", + "rec.tracks": "Faixas", + "rec.tracksTitle": "{count} faixas de áudio preservadas", + "rec.used": "Usado", + "rec.volume": "Volume", + "rend.aspectBlurpad": "Preenchimento desfocado", + "rend.aspectBlurpadHint": "Preenche o restante com uma cópia desfocada do próprio quadro, cortada para preencher. É a convenção que os feeds verticais adotaram.", + "rend.aspectCrop": "Cortar para preencher", + "rend.aspectCropHint": "Corta pelo centro até a forma de destino e depois escala. Os sujeitos mantêm o tamanho na tela; as bordas do quadro somem.", + "rend.aspectHandling": "Tratamento da proporção", + "rend.aspectPad": "Barras", + "rend.aspectPadHint": "Escala o quadro inteiro para caber e preenche o resto com uma cor sólida. Nada se perde, mas uma fonte 16:9 numa tela 9:16 é quase só barras.", + "rend.aspectStretch": "Esticar para caber", + "rend.aspectStretchHint": "Escala para o tamanho de destino e deixa a imagem distorcer se a fonte não bater. O que toda versão fazia antes de os outros modos existirem.", + "rend.boxColour": "Cor da caixa", + "rend.boxOpacity": "Opacidade da caixa (%)", + "rend.builtInFont": "Padrão integrado", + "rend.burnedText": "Texto gravado na imagem", + "rend.chooseHardware": "Escolha um codificador de hardware, ou uma saída menor.", + "rend.colourBlack": "black", + "rend.colourWhite": "white", + "rend.costBeyond": "Codificar isto por software em tempo real está além da maioria das máquinas", + "rend.costBeyondDetail": "{label} é {mpps} para uma única codificação. O x264 nesse tamanho normalmente perde a corrida contra o tempo real, e uma versão que fica para trás perde quadros para todos os destinos que a leem — coisa que você descobre no meio da transmissão.{machine} {escape}", + "rend.costBusy": "Espere vários núcleos ocupados", + "rend.costBusyDetail": "{label} é {mpps} em {encoder}, o que é uma codificação em tempo real pesada.{machine} Observe a velocidade do codificador assim que ele estiver no ar: abaixo de 1,0× ele está ficando para trás e os destinos abaixo dele começarão a perder quadros. {escape}", + "rend.costMachineCores": " Esta máquina informa {cores} núcleos lógicos.", + "rend.costModest": "Codificação por software modesta", + "rend.costModestDetail": "{label} é {mpps}, ao alcance da maioria das máquinas.{machine} Ainda assim é uma codificação real — compartilhada por todos os destinos que a selecionam, mas não é de graça.", + "rend.costNoNumbers": "O custo ainda não pode ser estimado", + "rend.costNoNumbersDetail": "Nada sondou a fonte, então “manter a fonte” não tem números por trás. Inicie a entrada e isto será preenchido.", + "rend.costOffloaded": "Repassado para {encoder}", + "rend.costOffloadedDetail": "{label} ({mpps}) roda no codificador de função fixa da GPU, então custa pouca CPU. Qualidade e controle de taxa variam conforme o driver, e cada chip tem um teto próprio — verifique a saída antes de um programa que importa.", + "rend.create": "Criar", + "rend.created": "Versão criada. Selecione-a em um destino para iniciar a codificação.", + "rend.custom": "Personalizado…", + "rend.deintAll": "Todos os quadros", + "rend.deintAllHint": "Para fontes que são entrelaçadas mas não dizem isso. Muitas pontes SDI e placas de captura marcam tudo como progressivo, seja lá o que receberam, e nessas “apenas quadros entrelaçados” não faz nada e parece um ajuste quebrado.", + "rend.deintAuto": "Apenas quadros entrelaçados", + "rend.deintAutoHint": "Toca apenas nos quadros que a fonte marcou como entrelaçados, então os progressivos passam intactos. A escolha certa para qualquer material misto.", + "rend.deintOff": "Desativado", + "rend.deintOffHint": "O certo para as fontes progressivas que quase todo mundo tem. Desentrelaçar uma só a deixa mais suave sem ganho nenhum.", + "rend.deinterlace": "Desentrelaçar", + "rend.deleteFailed": "Não foi possível excluir a versão.", + "rend.deleteFallbackMany": "{count} destinos voltam ao repasse direto e receberão o vídeo da fonte sem alteração. Verifique se a fonte ainda cabe no que cada plataforma aceita.", + "rend.deleteFallbackOne": "{count} destino volta ao repasse direto e receberá o vídeo da fonte sem alteração. Verifique se a fonte ainda cabe no que a plataforma aceita.", + "rend.deleteNoDestinations": "Nada seleciona esta versão, então excluí-la não muda destino nenhum.", + "rend.deleteRenditionTitle": "Excluir {name}?", + "rend.editRendition": "Editar versão", + "rend.empty": "Nenhuma versão. Todo destino está em repasse direto, o que é exatamente o certo até que um deles não consiga receber a fonte — uma entrada 4K60 que o Twitch, o Kick ou o X não aceitam, por exemplo. Adicione uma versão e aponte esses destinos para ela.", + "rend.encoder": "Codificador", + "rend.encoderPreset": "Preset do codificador", + "rend.encoderRelatedFailed": "{name} não foi oferecido porque um codificador relacionado falhou aqui.", + "rend.encoderTestFailed": "{name} falhou em uma codificação de teste de um quadro nesta máquina.", + "rend.encoderTestOk": "{name} codificou um quadro de teste nesta máquina.", + "rend.evenNote": "0 em um eixo mantém o valor da fonte e preserva a proporção — defina só a altura para redimensionar. Ambos precisam ser números pares: H.264 e HEVC não têm plano de croma de tamanho ímpar.", + "rend.font": "Fonte", + "rend.frameRate": "Taxa de quadros", + "rend.gop": "GOP", + "rend.gpuUnopenable": "Há uma GPU presente, mas este processo não consegue abri-la", + "rend.hardwareFallback": "hardware", + "rend.hardwareScanFound": "O que a varredura de hardware encontrou", + "rend.height": "Altura", + "rend.hevcWarning": "Este codificador produz HEVC. A maioria dos destinos RTMP aceita apenas H.264 — escolha-o só para um destino que tenha informado que aceita HEVC.", + "rend.keyframeInterval": "Intervalo entre quadros-chave", + "rend.letterboxColour": "Cor das barras", + "rend.live": "Ao vivo", + "rend.loadFailed": "Não foi possível carregar as versões.", + "rend.marginX": "Margem X (%)", + "rend.marginY": "Margem Y (%)", + "rend.name": "Nome", + "rend.namePlaceholder": "1080p60 para Twitch e Kick", + "rend.needsBothAWidthAnd": "Precisa de largura e de altura — com um eixo livre, o redimensionamento já preserva a proporção.", + "rend.needsBothAxes": "Precisa de largura e de altura", + "rend.newRendition": "Nova versão", + "rend.noEncoder": "Este FFmpeg não informou nenhum codificador de vídeo utilizável.", + "rend.noHardwareEscape": "Nenhum codificador de hardware funciona nesta máquina, então não há para onde mover isto — a única alavanca aqui é uma saída menor. Redetecte o hardware se, desde então, você instalou um driver ou passou uma GPU para o contêiner.", + "rend.notProbed": "Nada foi codificado como teste nesta máquina ainda, então esta lista é o que a compilação do FFmpeg contém, e não o que o hardware consegue fazer. Redetecte para descobrir.", + "rend.notRunning": "Parada", + "rend.note": "Nota", + "rend.noteDuplicateFps": "A fonte roda a {srcFps} fps, então {fps} fps só duplica quadros.", + "rend.notePlaceholder": "Para que serve este nível.", + "rend.noteUpscale": "A fonte é {width}×{height}. Ampliar custa CPU e não acrescenta detalhe.", + "rend.opacity": "Opacidade (%)", + "rend.percentNote": "Tudo é porcentagem do quadro, então os mesmos ajustes ficam certos em um nível 16:9 e em um 9:16. As margens são ignoradas em um eixo centralizado.", + "rend.preset": "Preset", + "rend.presetPlaceholder": "veryfast", + "rend.presetStartingPoint": "Ponto de partida — confira os limites atuais com a plataforma.", + "rend.redetectFailed": "Não foi possível redetectar o hardware.", + "rend.redetectTitle": "Rodar a varredura de GPU de novo e codificar um quadro de teste com cada codificador. Leva alguns segundos.", + "rend.redetectedNoHardware": "Hardware redetectado. Nenhum codificador de hardware passou em uma codificação de teste nesta máquina.", + "rend.resolution": "Resolução", + "rend.restartEncode": "Reiniciar esta codificação e os destinos que a leem", + "rend.restartFailed": "Não foi possível reiniciar a codificação.", + "rend.restarting": "{name} está reiniciando. Os destinos dela se reconectam junto.", + "rend.save": "Salvar", + "rend.saveFailed": "Não foi possível salvar a versão.", + "rend.seedNote": "Isto apenas preenche o formulário — edite o que quiser antes de salvar. Os tetos das plataformas mudam e variam conforme o status de parceiro.", + "rend.signalError": "Erro", + "rend.signalIdle": "Ocioso", + "rend.sizePct": "Tamanho (% da altura)", + "rend.softwareEncoderFallback": "um codificador de software", + "rend.speed": "Velocidade", + "rend.subtitle": "Uma codificação de vídeo compartilhada, muitos destinos. O áudio nunca é tocado aqui.", + "rend.target": "Alvo", + "rend.textColour": "Cor do texto", + "rend.textPlaceholder": "MINHA EMISSORA", + "rend.title": "Versões", + "rend.unused": "Nenhum destino seleciona isto ainda, então nada está sendo codificado. Escolha-a em um destino para iniciar a codificação.", + "rend.uptime": "Tempo ativo", + "rend.videoBitrate": "Taxa de bits do vídeo", + "rend.waitingSnapshot": "Aguardando o primeiro instantâneo ao vivo.", + "rend.watermarkImage": "Imagem de marca d'água", + "rend.watermarkPlaceholder": "overlays/logo.png", + "rend.width": "Largura", + "rend.widthPct": "Largura (% do quadro)", + "route.attack": "Ataque", + "route.audio": "áudio", + "route.audioAndVideoGoOut": "Áudio e vídeo saem juntos.", + "route.audioLaterThanVideo": "Áudio depois do vídeo", + "route.audioRouting": "Roteamento de áudio", + "route.audioRoutingDesc": "Escolha exatamente quais faixas da entrada cada destino recebe.", + "route.autoLimitWhen2Tracks": "Automático (limitar com 2 ou mais faixas)", + "route.commentaryLanguage": "Idioma do comentário", + "route.couldNotApplyThePreset": "Não foi possível aplicar o preset.", + "route.couldNotCompileThisRouting": "Não foi possível compilar este roteamento.", + "route.couldNotLoadDestinations": "Não foi possível carregar os destinos.", + "route.couldNotSaveTheRouting": "Não foi possível salvar o roteamento.", + "route.couldNotSaveTrackRoles": "Não foi possível salvar os papéis das faixas.", + "route.custom": "Personalizado…", + "route.delayDirection": "Direção do atraso", + "route.destinations": "Destinos", + "route.duckingNote": "Abaixa um grupo de faixas sempre que outro estiver falando — o microfone sobre a cama musical. Desligado significa que cada faixa selecionada é somada no próprio ganho, sem alteração.", + "route.duckingTriggerNote": "Um gatilho não precisa estar na mixagem deste destino — um microfone pode abaixar a música aqui enquanto só a música é enviada.", + "route.enableDucking": "Ativar ducking", + "route.everyTrackReaches": "Toda faixa selecionada chega a este destino, música inclusive.", + "route.excludeMusic": "Excluir música", + "route.keepMusic": "Manter música", + "route.keepMusicOut": "Manter a música fora deste destino", + "route.labelTracks": "Rotular faixas", + "route.langPlaceholder": "idioma — ex.: es", + "route.limiter04DbfsCeiling": "Limitador (teto de −0,4 dBFS)", + "route.localOnlyNote": "Este servidor ainda não armazena papéis de faixa, então eles ficam neste navegador. Todo o resto da página é salvo normalmente.", + "route.loudnessTarget": "Alvo de loudness", + "route.lufsBroadcast": "EBU R128 broadcast", + "route.measuredOverTheWholeProgramme": "Medido sobre o programa inteiro, não momento a momento: passagens silenciosas continuam silenciosas. Esses são os números que os próprios normalizadores das plataformas buscam, então acertar um é o que impede a plataforma de baixar seu volume.", + "route.mixMatrix": "Matriz de mixagem", + "route.musicPolicy": "Política de música", + "route.noTarget": "Sem alvo (não mexer nos níveis)", + "route.normalizeLoudnorm": "Loudness (EBU R128)", + "route.normalizeOff": "Desativado", + "route.off": "desligado", + "route.offset": "Deslocamento", + "route.on": "ligado", + "route.platformDefault": "Padrão da plataforma", + "route.podcastSpokenWord": "Podcast / palavra falada", + "route.presetApplied": "Preset aplicado. Revise e depois salve.", + "route.range": "Faixa", + "route.ratio": "Razão", + "route.release": "Liberação", + "route.result": "Resultado", + "route.role51": "5.1", + "route.roleClean": "limpo", + "route.roleMic": "microfone", + "route.roleMusic": "música", + "route.rolesNote": "Os papéis descrevem a entrada, não este destino, e são salvos por conta própria. Nada é filtrado até que um destino abaixo peça.", + "route.simple": "Simples", + "route.target": "Alvo", + "route.targetRole": "alvo", + "route.thisDestinationSPlatform": "A plataforma deste destino", + "route.threshold": "Limiar", + "route.title": "Routing", + "route.triggerRole": "gatilho", + "route.truePeak": "Pico real", + "route.video": "vídeo", + "route.videoLaterThanAudio": "Vídeo depois do áudio", + "route.withoutATargetTheMix": "Sem um alvo, a mistura sai no nível que a entrada produziu, com apenas a proteção contra clipping acima aplicada.", + "route.youtubeTwitchSpotify": "YouTube, Twitch, Spotify", + "set.acceptSelfSigned": "Aceitar um certificado autoassinado", + "set.accountDisconnected": "Conta desconectada.", + "set.adminPassword": "Senha de administrador", + "set.alertDelivery": "Entrega de alertas", + "set.alertDeliveryDesc": "Com que insistência um webhook que não responde é perseguido, antes de o alerta ser abandonado.", + "set.apiTokens": "Tokens de API", + "set.apiTokensDesc": "Credenciais de longa duração para scripts, Prometheus e qualquer outra coisa que não consiga manter uma sessão. Envie um como Authorization: Bearer pmk_…. Um token faz tudo o que o administrador faz, exceto gerenciar tokens — isso fica atrás da senha, então revogar um é definitivo.", + "set.app": "App", + "set.attemptsBackoff": "A espera entre tentativas aumenta automaticamente até um teto de 30 segundos, então o topo dessa faixa já são vários minutos perseguindo um endpoint morto. O limite é justamente o ponto: tentar para sempre transforma um único webhook morto em uma fila que nunca esvazia.", + "set.attemptsNote": "Incluindo a primeira tentativa. 1 significa nunca repetir.", + "set.attemptsRetryable": "Só são repetidas as falhas que valem a pena repetir. Um 404 de um webhook que alguém apagou é permanente, e tentar mais quatro vezes só atrasa todos os alertas na fila atrás dele; um 502 de um proxy reverso é exatamente para o que essas tentativas existem.", + "set.audioMeters": "Medidores de áudio", + "set.brokerPasswordCleared": "Senha do broker apagada.", + "set.brokerPlainNote": "mqtt:// não é criptografado. O MQTT envia usuário e senha no pacote CONNECT, então ambos atravessam a rede em texto claro — tudo bem numa LAN confiável, que é o caso usual do Home Assistant, e nada bem em qualquer coisa que você não controle. Use mqtts:// se o broker oferecer.", + "set.brokerUrl": "URL do broker", + "set.brokerUrlNote": "mqtt://, mqtts://, ws:// ou wss://. Uma senha na URL é recusada: a URL vai parar em linhas de log e na saída do ps, e isso não tem volta. Coloque-a no campo abaixo, onde fica criptografada em repouso.", + "set.brokerUrlPlaceholder": "mqtts://broker.local:8883", + "set.chatBothApply": "Ambos valem e o mais generoso vence: uma mensagem sai quando é mais antiga que as horas e está fora das N mais recentes. Assim, um canal movimentado guarda menos tempo que um tranquilo com os mesmos ajustes.", + "set.chatBufferNote": "“Enviar ao conectar” é um número de natureza diferente dos outros dois. Aqueles limitam o que fica em disco; este é um buffer em memória, alocado por painel conectado.", + "set.chatHistory": "Histórico do chat", + "set.chatHistoryDesc": "Até onde volta o histórico do chat — e, com ele, quanto um moderador consegue ver ao abrir a ficha de alguém.", + "set.chatHours": "Manter por (horas)", + "set.chatKeep": "Sempre manter (mensagens)", + "set.chatKeepNote": "Um piso, qualquer que seja a idade delas.", + "set.chatNotOnlyDisk": "Isto não é só um ajuste de disco. Clicar em um nome no chat abre o que aquela pessoa disse antes, e esse histórico vem daqui — nenhuma plataforma publica uma API para isso, então o polyemesis só pode mostrar o que guardou. Deixe curto demais e alguém que vem dando problema há uma semana parecerá nunca ter dito nada.", + "set.chatSendNote": "Histórico sem consulta.", + "set.chatSendOnConnect": "Enviar ao conectar (mensagens)", + "set.chatSendOnConnectShort": "Enviar ao conectar", + "set.chatSmall": "O chat é pequeno. Um canal com média de dez mensagens por segundo armazena cerca de 7 MB por hora; a maioria fica muito abaixo disso.", + "set.clientId": "ID do cliente", + "set.clientIdNote": "Precisa ser único no broker. Uma colisão é a causa número um de um laço de reconexão inexplicável: o broker desconecta a sessão mais antiga a cada conexão, e os dois clientes reconectam para sempre.", + "set.clientIdPlaceholder": "derivado do nome da instância", + "set.clientSecret": "Segredo do cliente", + "set.comingBack": "Retorno", + "set.confirmPassword": "Confirmar a nova senha", + "set.connectedAccounts": "Contas conectadas", + "set.copied": "Copiado", + "set.copy": "Copiar", + "set.couldNotChangeThePassword": "Não foi possível alterar a senha.", + "set.couldNotClearThePassword": "Não foi possível apagar a senha.", + "set.couldNotCreateTheToken": "Não foi possível criar o token.", + "set.couldNotReCheckThe": "Não foi possível verificar as credenciais novamente.", + "set.couldNotReadTheTls": "Não foi possível ler o estado do TLS.", + "set.couldNotRevokeTheToken": "Não foi possível revogar o token.", + "set.couldNotSaveTheCredentials": "Não foi possível salvar as credenciais.", + "set.couldNotSaveTheMqtt": "Não foi possível salvar as configurações de MQTT.", + "set.credsRemoved": "Credenciais removidas.", + "set.currentPassword": "Senha atual", + "set.dataDirectory": "Diretório de dados", + "set.deliveryAttempts": "Tentativas de entrega", + "set.dialsOnly": "O servidor só disca: {schemes}.", + "set.disconnect": "Desconectar", + "set.disconnectDesc": "Os destinos que a usam mantêm suas configurações, mas param de renovar a chave de transmissão. Você pode reconectar a mesma conta depois.", + "set.disconnectTitle": "Desconectar esta conta?", + "set.drawtextMissing": "Esta compilação não tem o filtro drawtext, então sobreposições de texto ficam indisponíveis. É preciso um FFmpeg compilado com libfreetype.", + "set.drawtextOk": "Esta compilação consegue desenhar texto sobre o vídeo.", + "set.enabled": "Ativado", + "set.encoderUrl": "URL do codificador", + "set.expires": "Expira", + "set.failover": "Failover", + "set.failoverDesc": "Uma entrada reserva e um cartão de espera, para quando a fonte ao vivo para de entregar. Desligado por padrão: com ele desligado, o pipeline é byte a byte o mesmo de antes de isto existir.", + "set.failoverRemuxNote": "Ligar isto acrescenta um passo de remultiplexação entre a entrada e tudo abaixo dela. Barato, mas não grátis, e é por isso que é uma decisão e não um padrão.", + "set.fileSizeMb": "Tamanho do arquivo (MB)", + "set.filesKept": "Arquivos mantidos", + "set.goingLive": "Entrando ao vivo", + "set.goingLiveDesc": "O que acontece quando vários destinos começam ao mesmo tempo.", + "set.graceNote": "Por quanto tempo a fonte ao vivo pode não entregar nada antes de o nível trocar. Mais longo que uma reconexão de propósito — um codificador restabelecendo uma conexão RTMP é operação normal, não uma falha.", + "set.gracePeriod": "Período de carência (segundos)", + "set.haDiscovery": "Descoberta do Home Assistant", + "set.height": "Altura", + "set.ingest": "Entrada", + "set.ingestDesc": "O SRT carrega até seis faixas de áudio. O RTMP carrega uma e existe como alternativa para codificadores que não fazem SRT. O modo puxar disca uma fonte em vez de esperar por uma.", + "set.ingestUrlCopied": "URL de entrada copiada.", + "set.instance": "Instância", + "set.intervalNote": "Estado inalterado não é republicado, então um intervalo curto custa uma comparação em vez de uma mensagem. O keep-alive é o que limita por quanto tempo um enlace morto passa despercebido — e, portanto, com que rapidez o broker avisa aos assinantes que esta instância se foi.", + "set.issuer": "Emissor", + "set.kbps": "kbps", + "set.keepAlive": "Keep-alive (s)", + "set.latencyMs": "Latência (ms)", + "set.listeners": "Escutas", + "set.listenersDesc": "Onde o servidor se vincula, para toda a instalação. Uma porta SRT atende todas as fontes — elas se distinguem pelo token de publicação, então acrescentar uma fonte nunca significa publicar outra porta. O RTMP não tem esse roteamento, então sua porta atende uma fonte.", + "set.logsBoundNote": "O diretório de logs é limitado pelo produto desses dois valores, então a persistência nunca pode ser o que lota o disco.", + "set.logsDesc": "O stderr capturado do FFmpeg é sempre mantido em memória, mas ela morre junto com o servidor — o que perde exatamente as linhas que explicam por que ele morreu. Persistir espelha isso em um arquivo rotativo dentro do diretório de dados.", + "set.metersDesc": "Um processo auxiliar mede cada canal de cada faixa. Intervalos menores atualizam mais rápido e custam mais CPU.", + "set.mode": "Modo", + "set.modePull": "Puxar — discar uma câmera, sinal ou arquivo", + "set.modeRtmp": "RTMP — faixa única", + "set.modeSrt": "SRT — multipista (recomendado)", + "set.mqtt": "Telemetria MQTT", + "set.mqtt5Only": "Somente MQTT 5.0.", + "set.mqtt5OnlyNote": "Um broker preso ao 3.1.1 não completará conexão alguma — não é um modo degradado. Mosquitto 2.x, EMQX e o complemento do Home Assistant falam 5.0.", + "set.mqttDesc": "Publica o estado em um broker como mensagens retidas, de modo que um consumidor que não estava conectado quando algo mudou ainda receba a resposta atual.", + "set.multiAccountNote": "Conecte a mesma plataforma mais de uma vez para transmitir a vários canais — cada conta vira seu próprio destino.", + "set.name": "Nome", + "set.newPassword": "Nova senha", + "set.no": "não", + "set.noEncryption": "Nada está criptografando esta conexão. Senhas e cookies de sessão atravessam a rede em texto claro — defina tls.mode: auto, ou vincule a 127.0.0.1 atrás de um proxy.", + "set.noLibsrt": "Esta compilação do FFmpeg não tem suporte a SRT. Instale uma configurada com --enable-libsrt, ou use RTMP.", + "set.noTokens": "Ainda não há tokens.", + "set.passphrase": "Senha SRT", + "set.passphraseNote": "Habilita a criptografia AES. Deixe em branco para nenhuma. O SRT recusa senhas fora de 10 a 79 caracteres.", + "set.passphrasePlaceholder": "opcional — de 10 a 79 caracteres", + "set.password": "Senha", + "set.passwordChanged": "Senha alterada.", + "set.passwordMismatch": "As novas senhas não coincidem.", + "set.passwordNote": "A senha nunca é devolvida por nenhuma API, então este campo aparece vazio mesmo quando há uma definida. Deixe vazio para manter a senha armazenada; limpe-a com o botão abaixo.", + "set.platform": "Plataforma", + "set.platformsDesc": "As plataformas diferem em quanto disso o polyemesis consegue automatizar, e vale conhecer essa diferença antes de começar, e não uma hora depois. A transmissão em si funciona igual em todas — roteamento de áudio por destino, variantes, reconexão e medidores não dependem de nenhuma das colunas abaixo.", + "set.playLoop": "Reproduzir um laço quando nada estiver ao vivo", + "set.playLoopNote": "Um segundo candidato ao lado do cartão de espera: arquivos tocam em vez de uma imagem fixa. Ambos podem estar ligados — veja a ordem dos níveis no projeto de failover para saber qual vence.", + "set.pointObs": "Aponte o OBS para este endereço.", + "set.preview": "Prévia", + "set.previewDesc": "O monitor HLS do painel. Este é o único lugar em que o polyemesis recodifica vídeo, e ele roda sobre a própria assinatura do relay, de modo que nunca pode afetar um destino.", + "set.previewIdleNote": "O codificador inicia quando o painel pede a playlist e para novamente este tempo após a última requisição, então um painel fechado não custa CPU. Alterar isto nunca reinicia uma prévia que alguém esteja assistindo.", + "set.processLogs": "Logs de processos", + "set.proxyTerminatesTls": "Um proxy reverso na frente do polyemesis termina o TLS, então este servidor serve HTTP puro de propósito.", + "set.publishInterval": "Intervalo de publicação (s)", + "set.reconnectCap": "Teto do recuo de reconexão (s)", + "set.reconnectCapNote": "0 usa o padrão. Apenas fontes HTTP e HLS.", + "set.redirectNote": "Precisa coincidir exatamente, incluindo esquema e porta. Se você acessa o polyemesis por um proxy reverso, use aquela URL pública.", + "set.redirectUri": "URI de redirecionamento a autorizar", + "set.removeCreds": "Remover credenciais", + "set.removeCredsDesc": "Toda conta conectada por este app para de renovar seu token e precisará ser reconectada. O segredo do cliente não é recuperável daqui — você teria de criar um novo app na plataforma.", + "set.removeCredsTitle": "Remover estas credenciais de desenvolvedor?", + "set.returnAfter": "Voltar após (segundos estável)", + "set.returnAutomatic": "Automático — voltar assim que a primária estiver estável", + "set.returnManual": "Manual — ficar na reserva até eu trocar", + "set.returnManualNote": "Manual é o padrão porque um retorno automático pode oscilar: uma primária que se recupera aos trancos levaria o programa de um lado para outro no meio da transmissão.", + "set.rtmpPort": "Porta RTMP", + "set.rtspNote": "TCP, a menos que a câmera não consiga. RTSP sobre UDP através de NAT parece conectado e não entrega nada.", + "set.rtspTransport": "Transporte RTSP", + "set.saveAndDrop": "Salvar e cortar a transmissão ao vivo", + "set.saveIngest": "Salvar as configurações de entrada", + "set.segmentSec": "Segmento (s)", + "set.settingsSavedAffectedProcessesHave": "Configurações salvas. Os processos afetados foram reiniciados.", + "set.showSlate": "Mostrar um cartão quando nada estiver ao vivo", + "set.slateColour": "Cor do cartão", + "set.slateColourNote": "Uma palavra só, porque o valor cai em um grafo de filtros onde uma vírgula encerraria o argumento. Usada apenas quando não há imagem definida.", + "set.slateColourPlaceholder": "black", + "set.slateImage": "Imagem do cartão", + "set.slateImageNote": "Um caminho dentro do diretório de dados. Deixe vazio para pintar uma cor sólida — essa é justamente a alternativa, porque uma cor não tem arquivo que possa falhar ao abrir.", + "set.slateImagePlaceholder": "slate/holding.png", + "set.sourceUrl": "URL de origem", + "set.sourceUrlNote": "Um de {schemes}. Uma fonte file:// é um caminho RELATIVO ao diretório de dados e não pode conter “..” — ela fica confinada ali do mesmo modo que um arquivo enviado.", + "set.sourceUrlPlaceholder": "rtsp://camera.local/stream1", + "set.srtPort": "Porta SRT", + "set.stagger": "Escalonar conexões (ms)", + "set.staggerNote": "0 desliga. Caso contrário, entrar ao vivo significa cada destino abrindo uma conexão, negociando TLS e começando a codificar áudio no mesmo instante — em uma máquina pequena esse é o momento mais propenso a perder quadros, e é exatamente o momento que você está observando, porque acabou de entrar ao vivo. Algumas centenas de milissegundos bastam.", + "set.staggerReconnectNote": "Nunca atrasa uma reconexão. Um destino que cai às 3 da manhã volta imediatamente, em vez de esperar sua vez atrás de processos que já estão saudáveis.", + "set.stepByStep": "Configuração passo a passo", + "set.stopAfterIdle": "Parar após ocioso (s)", + "set.streamKey": "Chave de transmissão", + "set.streamKeyNote": "Os publicadores precisam usar exatamente este caminho, para que uma porta aberta não seja uma porta destrancada.", + "set.subject": "Titular", + "set.synthesiseNote": "Toda plataforma importante rejeita uma transmissão sem faixa de áudio. Com isto ligado, uma entrada só de vídeo ganha uma faixa estéreo silenciosa e seus destinos iniciam; com isto desligado, eles recusam com “routing profile selects no audio”. Só se aplica a uma entrada DETECTADA com zero faixas, então nunca pode deslocar áudio que você esteja realmente enviando.", + "set.synthesiseSilent": "Sintetizar uma faixa silenciosa", + "set.syntheticAudio": "Áudio sintético", + "set.syntheticDesc": "O que acontece quando a entrada traz vídeo e nenhum áudio.", + "set.tabIngest": "Entrada", + "set.tabPipeline": "Pipeline", + "set.tabPlatforms": "Credenciais de plataforma", + "set.tabSecurity": "Segurança", + "set.title": "Configurações", + "set.tlsAcme": "Let's Encrypt", + "set.tlsAuto": "Automático", + "set.tlsDesc": "Configurado no config.yaml, não aqui — o TLS precisa estar certo antes de o servidor começar a escutar. Este cartão relata o que aquilo produziu.", + "set.tlsManual": "Certificado manual", + "set.tlsOff": "Não terminado aqui", + "set.tlsSelfsigned": "Autoassinado", + "set.tokenHashNote": "O polyemesis guarda apenas um hash. Se você perdê-lo, revogue este token e crie outro.", + "set.tokenNamePlaceholder": "prometheus", + "set.tokenRevoked": "{name} revogado.", + "set.topicPrefix": "Prefixo de tópico", + "set.topicPrefixNote": "Separadores no prefixo são preservados, então home/av significa dois níveis. Um prefixo começando com $ é recusado: os brokers os reservam, e um assinante usando # nunca os recebe — a telemetria seria publicada com sucesso e ficaria invisível exatamente na visão com que você iria depurar.", + "set.transportSecurity": "Segurança de transporte", + "set.updateInterval": "Intervalo de atualização (ms)", + "set.username": "Usuário", + "set.validFor": "Válido para", + "set.whatPlatformsDo": "O que cada plataforma pode fazer", + "set.whyDevApp": "Por que você precisa do seu próprio app de desenvolvedor", + "set.whyDevAppDesc": "O polyemesis não pode distribuir segredos de cliente OAuth — qualquer um com o binário os teria, e as plataformas os revogariam. Você registra um app de desenvolvedor gratuito uma vez, cola aqui o ID e o segredo do cliente, e então o polyemesis busca suas chaves de transmissão para você.", + "set.writeLogs": "Gravar logs em disco", + "set.yes": "sim", + "sources.addDescription": "Um segundo programa com sua própria entrada, destinos e variantes — uma tela vertical ao lado de uma horizontal, por exemplo. Ele começa nas portas padrão, que você vai querer alterar para que as duas não entrem em conflito.", + "sources.addFailed": "Não foi possível adicionar a fonte", + "sources.addSource": "Adicionar fonte", + "sources.addTitle": "Adicionar uma fonte", + "sources.added": "“{name}” adicionada. Defina suas portas de entrada abaixo.", + "sources.app": "App", + "sources.applyAndDrop": "Aplicar e cortar a transmissão", + "sources.copied": "{what} copiado", + "sources.copyFailed": "Não foi possível copiar {what}", + "sources.copyTokenAria": "Copiar o token", + "sources.copyUrlAria": "Copiar a URL {proto}", + "sources.default": "padrão", + "sources.defaultDescription": "Solicitações que não nomeiam uma fonte agem sobre esta.", + "sources.deleteAria": "Excluir {name}", + "sources.deleteConfirm": "Excluir fonte", + "sources.deleteDescription": "Seus destinos e variantes vão junto — eles descrevem para onde este programa vai e não significam nada sem ele. As gravações são mantidas: os arquivos continuam no disco e reproduzíveis, apenas deixam de ser atribuídos a uma fonte.", + "sources.deleteFailed": "Não foi possível excluir a fonte", + "sources.deleteThis": "Excluir esta fonte", + "sources.deleteTitle": "Excluir “{name}”?", + "sources.deleted": "“{name}” e seus destinos excluídos", + "sources.destinations": "Destinos", + "sources.dirtyIdle": "Alterações de entrada não salvas. Aplicar reinicia a entrada desta fonte.", + "sources.dirtyPublishing": "Um codificador está publicando nesta fonte. Aplicar reinicia a entrada e desconecta todos que estão assistindo.", + "sources.enableAria": "Ativar {name}", + "sources.help.app": "O caminho de aplicação RTMP — a parte da URL entre o host e a chave de transmissão. A maioria dos codificadores espera “live”. Altere apenas se tiver um motivo.", + "sources.help.ingest": "Como seu codificador alcança esta fonte. O SRT sobrevive a uma conexão com perdas retransmitindo o que faltou, e é o padrão certo pela internet pública. O RTMP é mais antigo e tem suporte universal, mas se degrada muito em um enlace instável. Com “Puxar”, o polyemesis busca o sinal em uma URL que você informar, em vez de esperar que publiquem para ele.", + "sources.help.latencyMs": "Quanto tempo o SRT esperará por um pacote atrasado ou retransmitido antes de desistir dele. Valores mais altos suportam conexões piores ao custo de atraso: cerca de 2,5× o seu tempo de ida e volta é um bom ponto de partida, então 40 ms de RTT pedem cerca de 100 ms aqui. Baixo demais, e um soluço breve vira uma falha visível.", + "sources.help.passphrase": "Criptografa a conexão SRT e, hoje, é isso que realmente mantém estranhos fora desta entrada. Entre 10 e 79 caracteres. Deixe em branco e qualquer um que alcance a porta poderá publicar nela.", + "sources.help.streamKey": "O segredo que um codificador envia para publicar por RTMP. Esta é a credencial que protege esta entrada, então trate-a como uma senha: quem a tiver pode transmitir para os seus destinos.", + "sources.help.token": "Identifica esta fonte quando várias compartilham uma porta de entrada. Se ele também controla o acesso depende do seu modo de entrada — a nota abaixo diz o que é verdade agora. Rotacionar emite um novo token e mantém o antigo válido por cinco minutos, para que um codificador ao vivo não seja cortado no meio da transmissão.", + "sources.ingest": "Entrada", + "sources.lastSourceTitle": "A última fonte não pode ser excluída — uma instalação precisa de uma entrada", + "sources.latencyMs": "Latência (ms)", + "sources.loadFailed": "Não foi possível carregar as fontes", + "sources.loading": "Carregando fontes…", + "sources.loss": "perda", + "sources.modePull": "Puxar", + "sources.name": "Nome", + "sources.namePlaceholder": "Vertical", + "sources.noEngine": "Nenhum mecanismo está em execução para esta fonte. A causa comum é uma porta de entrada já em uso por outra fonte ou outro processo.", + "sources.notRunning": "parada", + "sources.ownDescription": "Seus próprios destinos, variantes e gravações.", + "sources.passphrase": "Senha", + "sources.passphrasePlaceholder": "10–79 caracteres", + "sources.publishing": "publicando", + "sources.renditions": "Versões", + "sources.retrans": "retransm.", + "sources.rotate": "Rotacionar", + "sources.rotateConfirm": "Rotacionar token", + "sources.rotateFailed": "Não foi possível rotacionar o token", + "sources.rotateIdle": "Um novo token é emitido imediatamente. O antigo continua funcionando por cinco minutos para que um codificador que já o utiliza não seja cortado.", + "sources.rotateTitle": "Rotacionar o token de “{name}”?", + "sources.rotateWhilePublishing": "Um codificador está publicando nesta fonte agora. O token antigo continua funcionando por cinco minutos, então não será cortado — mas precisa ser atualizado antes que esse prazo termine.", + "sources.rtt": "RTT", + "sources.running": "em execução", + "sources.saveFailed": "Não foi possível salvar", + "sources.streamKey": "Chave de transmissão", + "sources.subtitle": "Um programa de entrada cada. Destinos e variantes pertencem a uma fonte.", + "sources.title": "Fontes", + "sources.token": "token", + "sources.tokenEnforcedDetailRtmp": "Ele é a chave de transmissão: publique na URL acima usando-o como chave. Girar emite um novo e mantém o antigo por cinco minutos, então você troca sem derrubar a transmissão.", + "sources.tokenEnforcedDetailSrt": "Coloque-o no streamid SRT do seu codificador para publicar nesta fonte pela porta compartilhada. Girar emite um novo e mantém o antigo por cinco minutos, então você troca sem derrubar a transmissão.", + "sources.tokenIsCredential": "Este token é a credencial.", + "sources.tokenIssued": "Novo token emitido", + "sources.tokenNotEnforced": "Este token é armazenado, mas não é exigido no momento.", + "sources.tokenNotEnforcedRtmp": "As fontes são separadas por porta. O que realmente protege esta entrada é a chave de transmissão acima. Ative a entrada de porta única em Configurações para tornar o token a credencial.", + "sources.tokenNotEnforcedSrt": "As fontes são separadas por porta. O que realmente protege esta entrada é a senha SRT acima. Ative a entrada de porta única em Configurações para tornar o token a credencial.", + "state.disabled": "Desativado", + "state.failed": "Falhou", + "state.live": "Ao vivo", + "state.offline": "Offline", + "state.reconnecting": "Reconectando", + "state.starting": "Iniciando", + "state.stopped": "Parado" } diff --git a/ui/src/lib/i18n/ru.json b/ui/src/lib/i18n/ru.json index f62dadec..6f23dc8d 100644 --- a/ui/src/lib/i18n/ru.json +++ b/ui/src/lib/i18n/ru.json @@ -1,137 +1,1149 @@ { - "nav.dashboard": "Панель управления", - "nav.sources": "Источники", - "nav.meters": "Индикаторы звука", - "nav.routing": "Routing", - "nav.renditions": "Варианты трансляции", - "nav.playout": "Playout", - "nav.library": "Библиотека", - "nav.clips": "Клипы", - "nav.chat": "Чат", - "nav.jobs": "Задачи", - "nav.automation": "Автоматизация", - "nav.recordings": "Записи", - "nav.monitoring": "Мониторинг", - "nav.settings": "Настройки", - "chrome.toggleNav": "Переключить навигацию", + "auth.accountCreated": "Учётная запись администратора создана.", + "auth.confirmPassword": "Подтвердите пароль", + "auth.createAccount": "Создать учётную запись", + "auth.firstRun": "Первоначальная настройка", + "auth.firstRunDesc": "Выберите учётные данные администратора для этого сервера. Восстановления учётной записи нет, поэтому храните их в надёжном месте.", + "auth.password": "Пароль", + "auth.passwordMismatch": "Пароли не совпадают.", + "auth.signIn": "Войти", + "auth.signInDesc": "Введите учётные данные администратора.", + "auth.somethingWentWrong": "Что-то пошло не так.", + "auth.tokenCreated": "Токен API создан", + "auth.tokenRevoked": "Токен API отозван", + "auth.username": "Имя пользователя", + "auto.action": "Действие", + "auto.adminPasswordChanged": "Пароль администратора изменён", + "auto.chosenWeekdays": "Выбранные дни недели", + "auto.coalesced": "Объединено", + "auto.coalescingWindow": "Окно объединения (секунды)", + "auto.couldNotChangeTheRule": "Не удалось изменить правило.", + "auto.couldNotChangeTheSchedule": "Не удалось изменить расписание.", + "auto.couldNotDeleteTheRule": "Не удалось удалить правило.", + "auto.couldNotDeleteTheSchedule": "Не удалось удалить расписание.", + "auto.couldNotLoadAutomationSettings": "Не удалось загрузить настройки автоматизации.", + "auto.couldNotSaveTheRule": "Не удалось сохранить правило.", + "auto.couldNotSaveTheSchedule": "Не удалось сохранить расписание.", + "auto.createRule": "Создать правило", + "auto.createSchedule": "Создать расписание", + "auto.days": "Дни", + "auto.delete": "Удалить", + "auto.deleteRule": "Удалить правило", + "auto.deleteRuleDesc": "Правило перестанет срабатывать. Его конечная точка webhook останется нетронутой, и правило можно создать заново.", + "auto.deleteRuleTitle": "Удалить это правило оповещения?", + "auto.deleteSchedule": "Удалить расписание", + "auto.deleteScheduleDesc": "Расписание перестанет выполняться. Всё, что оно уже запустило, останется без изменений.", + "auto.deleteScheduleTitle": "Удалить это расписание?", + "auto.delivery": "Доставка", + "auto.destinationFallingBehindRealtime": "Назначение отстаёт от реального времени", + "auto.destinationKeepingUpAgain": "Назначение снова успевает", + "auto.destinationRecovered": "Назначение восстановлено", + "auto.destinationWentDown": "Назначение упало", + "auto.destinations": "Назначения", + "auto.dropped": "Отброшено", + "auto.edit": "Редактировать", + "auto.editRule": "Изменить правило оповещения", + "auto.editSchedule": "Изменить расписание", + "auto.endpoint": "Конечная точка", + "auto.evAudioClipping": "Клиппинг звука", + "auto.evClipCaptured": "Клип записан", + "auto.evDiskLow": "Мало места на диске", + "auto.evDiskRecovered": "Место на диске восстановлено", + "auto.evIngestLost": "Вход потерян", + "auto.evIngestRecovered": "Вход восстановлен", + "auto.evSettingsChanged": "Настройки изменены", + "auto.evSignedIn": "Успешный вход", + "auto.events": "События", + "auto.everyDay": "Каждый день", + "auto.failed": "Ошибка", + "auto.failoverSwitchedSource": "Резервирование переключило источник", + "auto.fired": "сработало", + "auto.firesAt": "Срабатывает в", + "auto.lastDelivery": "Последняя доставка", + "auto.lastSweep": "Последний обход", + "auto.latenessAllowance": "Допустимое опоздание (секунды)", + "auto.loudnessBackInCompliance": "Громкость снова в норме", + "auto.loudnessOutOfCompliance": "Громкость вне нормы", + "auto.minGap": "Минимальный интервал между сообщениями (секунды)", + "auto.minSeverity": "Минимальная важность", + "auto.missedNote": "Срабатывание, пропущенное из-за остановки сервера, помечается как обработанное и пропускается, а не выполняется с опозданием: трансляция, которая сама начинается на четыре часа позже, хуже той, что не началась вовсе.", + "auto.name": "Название", + "auto.newRule": "Новое правило оповещения", + "auto.newSchedule": "Новое расписание", + "auto.next": "Следующее", + "auto.nothingFired": "Пока ничего не сработало. Планировщик выполняет обход каждые 20 секунд.", + "auto.once": "Однократно", + "auto.payloadFormat": "Формат полезной нагрузки", + "auto.pending": "В ожидании", + "auto.repeatedFailedSignIns": "Повторные неудачные входы", + "auto.repeats": "Повторение", + "auto.retries": "Повторные попытки", + "auto.rule": "Правило", + "auto.ruleCreated": "Правило оповещения создано. Отправьте на него тест.", + "auto.ruleDeleted": "Правило оповещения удалено.", + "auto.rulePlaceholder": "Discord — оповещения эфира", + "auto.ruleSaved": "Правило оповещения сохранено.", + "auto.saveRule": "Сохранить правило", + "auto.saveSchedule": "Сохранить расписание", + "auto.schedule": "Расписание", + "auto.scheduleCreated": "Расписание создано.", + "auto.scheduleDeleted": "Расписание удалено.", + "auto.schedulePlaceholder": "Будничное вечернее шоу", + "auto.scheduleSaved": "Расписание сохранено.", + "auto.schedules": "Расписания", + "auto.secretMasked": "Показано скрытым, потому что секрет находится в пути URL. Не трогайте это поле, чтобы сохранить текущую конечную точку, либо вставьте новый URL, чтобы заменить её.", + "auto.sendTest": "Отправить тестовое оповещение", + "auto.sendTestTitle": "Отправить тестовое оповещение сейчас", + "auto.sent": "Отправлено", + "auto.skipped": "пропущено", + "auto.startDestinations": "Запустить назначения", + "auto.startPlaylist": "Запустить плейлист", + "auto.stopDestinations": "Остановить назначения", + "auto.stopPlaylist": "Остановить плейлист", + "auto.subscribedTo": "Подписано на", + "auto.subtitle": "Webhook’и, которые сообщают о поломке, и расписания, которые ведут эфир без вас.", + "auto.theEndpointDidNotAccept": "Эндпоинт не принял тестовое сообщение.", + "auto.time": "Время", + "auto.timeZone": "Часовой пояс", + "auto.title": "Автоматизация", + "auto.tzPlaceholder": "UTC", + "auto.webhookHint": "И Discord, и Slack выдают его в настройках канала.", + "auto.webhookPlaceholder": "https://discord.com/api/webhooks/…", + "auto.webhookRules": "Правила webhook", + "auto.webhookUrl": "URL webhook", + "auto.weekFri": "Пт", + "auto.weekMon": "Пн", + "auto.weekSat": "Сб", + "auto.weekSun": "Вс", + "auto.weekThu": "Чт", + "auto.weekTue": "Вт", + "auto.weekWed": "Ср", + "auto.when": "Когда", + "chatpage.allFiltered": "Все платформы с сообщениями отфильтрованы.", + "chatpage.couldNotDeleteThatMessage": "Не удалось удалить это сообщение.", + "chatpage.deleteNote": "Удаление убирает сообщение на платформе, которая его выпустила. Платформа, на которой polyemesis удалять не умеет, сообщает об этом и указывает, где это можно сделать, — действие никогда не скрывается наугад.", + "chatpage.droppedTitle": "Показаны в эфире, но сохранение отстало, и история их потеряла.", + "chatpage.dupTitle": "Одно и то же сообщение увидено дважды — наше собственное эхо либо повтор с платформы.", + "chatpage.noAccount": "Чат работает, но не подключено ни одной учётной записи платформы. Подключите её в разделе Настройки → Учётные данные платформ.", + "chatpage.notRunning": "На этом сервере чат не запущен. История под лентой — это то, что было сохранено ранее, а поле отправки останется недоступным, пока не будет подключена платформа.", + "chatpage.received": "Получено", + "chatpage.reload": "Обновить", + "chatpage.stored": "Сохранено", + "chatpage.storedTitle": "Ничего не подключено; это сохранённая история.", + "chatpage.subtitle": "Все платформы в одной ленте, с одним полем отправки.", + "chatpage.timeline": "Лента", + "chatpage.title": "Чат", "chrome.ingest": "Ingest", + "chrome.language": "Язык", "chrome.liveCount": "{count} в прямом эфире", + "chrome.signOut": "Выйти", "chrome.socketOffline": "Офлайн", "chrome.socketOfflineHint": "Прямые обновления отключены; повторная попытка", - "chrome.signOut": "Выйти", - "chrome.language": "Язык", + "chrome.toggleNav": "Переключить навигацию", + "clipedit.back33": "Назад на 33 мс", + "clipedit.clickLine": "Щёлкните по строке, чтобы перейти к ней. Выделите несколько строк — точки входа и выхода перейдут к выделенным словам.", + "clipedit.clipThisLine": "Вырезать эту строку", + "clipedit.clipThisLineTitle": "Поставить точки входа и выхода вокруг этой строки", + "clipedit.copied": "Скопировано", + "clipedit.copiesEveryPacketSecondsBit": "Копирует каждый пакет. Секунды, бит в бит, а начало сдвигается назад к ближайшему ключевому кадру.", + "clipedit.couldNotOpenThatRecording": "Не удалось открыть эту запись.", + "clipedit.couldNotQueueTheExport": "Не удалось поставить экспорт в очередь.", + "clipedit.cut": "Резка", + "clipedit.downloadClip": "Скачать клип", + "clipedit.exportQueued": "Экспорт поставлен в очередь. Он выполнится, когда у машины появятся свободные ресурсы.", + "clipedit.exports": "Экспорты", + "clipedit.fast": "Быстро", + "clipedit.filterTranscript": "Фильтровать расшифровку", + "clipedit.findSaid": "Найти сказанное", + "clipedit.forward33": "Вперёд на 33 мс", + "clipedit.in": "Вход", + "clipedit.keepAllTracks": "Сохранить все звуковые дорожки", + "clipedit.keepsEveryAudioTrackIn": "Сохраняет все звуковые дорожки в любом кодеке. То, что пишет рекордер.", + "clipedit.markIn": "I — поставить точку входа на позиции воспроизведения", + "clipedit.markOut": "O — поставить точку выхода на позиции воспроизведения", + "clipedit.name": "Название", + "clipedit.noKeyframes": "Позиции ключевых кадров прочитать не удалось, поэтому никто не скажет, где на самом деле начнётся эта резка. Работать она будет — собственная перемотка FFmpeg попадает на ключевой кадр, — однако начало может оказаться раньше на величину до одного GOP, и эта страница не может показать, насколько именно.", + "clipedit.noTranscript": "Для этой записи пока ничего не расшифровано. Каждый микрофон записан на собственную дорожку, поэтому расшифровка даёт имена говорящих без модели диаризации — а затем можно нарезать клипы по сказанному.", + "clipedit.nothingExported": "Пока ничего не экспортировано.", + "clipedit.out": "Выход", + "clipedit.pickAtLeastOneAudio": "Выберите хотя бы одну звуковую дорожку или возьмите все.", + "clipedit.playFaster": "L — воспроизведение, повторно — быстрее", + "clipedit.precise": "Точно", + "clipedit.queueNote": "Экспорт ставится в очередь, а не выполняется здесь: он уступает живому потоку, поэтому резка, сделанная посреди эфира, ждёт, а не отбирает ресурсы, нужные потоку.", + "clipedit.reEncodesOnlyTheLeading": "Перекодирует только начальный неполный GOP, чтобы начало было точным. Остальное по-прежнему копируется.", + "clipedit.reencoded": "Перекодировано", + "clipedit.shortcuts": "J / K / L — перемотка, I и O ставят точки входа и выхода, стрелки сдвигают на 33 мс, а стрелки с shift — на секунду. Мелкий шаг равен одному кадру при 30 кадрах в секунду; реальная частота этой записи не проиндексирована, поэтому шаг назван в миллисекундах, а не угадан.", + "clipedit.shuttleBack": "J — перемотка назад", + "clipedit.startMoved": "Начало сдвинуто", + "clipedit.starts": "Начинается", + "clipedit.stop": "K — стоп", + "clipedit.thatCutCouldNotBe": "Не удалось спланировать эту нарезку.", + "clipedit.thatIsNotARecording": "Это не идентификатор записи.", + "clipedit.thatRecordingIsNotAvailable": "Эта запись недоступна.", + "clipedit.title": "Редактор клипов", + "clipedit.whatASocialPlatformWill": "То, что примет социальная платформа. Через него проходит меньше кодеков.", + "clipedit.zoomIn": "Приблизить", + "clipedit.zoomOut": "Отдалить", + "clips.bitrate": "Битрейт", + "clips.buffer": "Буфер", + "clips.bufferLiveNote": "Переключатель буфера действует немедленно и не сохраняется: после перезапуска он снова выключен.", + "clips.captured": "Захвачено", + "clips.ceiling": "Потолок", + "clips.clipsKept": "Хранимых клипов", + "clips.couldNotCaptureAClip": "Не удалось записать клип.", + "clips.couldNotChangeTheClip": "Не удалось изменить буфер клипов.", + "clips.couldNotDeleteTheClip": "Не удалось удалить клип.", + "clips.couldNotLoadClips": "Не удалось загрузить клипы.", + "clips.covers": "Охватывает", + "clips.customLength": "Своя длительность клипа в секундах", + "clips.delete": "Удалить", + "clips.deleteDescription": "Клип — обычно именно то, что вы хотели сохранить. Это удалит файл; запись, из которой он взят, останется нетронутой.", + "clips.deleteTitle": "Удалить этот клип?", + "clips.deleted": "Клип удалён.", + "clips.download": "Скачать", + "clips.everythingHeld": "Всё, что держит буфер", + "clips.file": "Файл", + "clips.held": "Удерживается", + "clips.inMemory": "В памяти", + "clips.keepBuffer": "Держать скользящий буфер", + "clips.keyframes": "Ключевые кадры", + "clips.length": "Длительность", + "clips.none": "нет", + "clips.off": "выкл", + "clips.onDisk": "На диске", + "clips.seen": "обнаружено", + "clips.settings": "Настройки", + "clips.size": "Размер", + "clips.subtitle": "Скользящий буфер того, что только что вышло в эфир. Захватывайте задним числом — ничего не нужно готовить заранее.", + "clips.theClipDoesNotStart": "Клип начинается не с ключевого кадра.", + "clips.title": "Клипы", + "clips.waitingForStream": "ожидание потока", + "clips.window": "Окно", + "clips.windowSeconds": "Окно (секунды)", "common.add": "Добавить", - "common.cancel": "Отменить", + "common.apply": "Применить", + "common.cancel": "Отмена", "common.close": "Закрыть", "common.confirm": "Подтвердить", "common.copied": "Скопировано", "common.copy": "Копировать", "common.delete": "Удалить", "common.disabled": "Отключено", + "common.discard": "Отменить изменения", "common.edit": "Редактировать", "common.enabled": "Включено", "common.loading": "Загрузка…", + "common.moreInfo": "Подробнее", "common.refresh": "Обновить", "common.restart": "Перезапустить", "common.retry": "Повторить", "common.save": "Сохранить", "common.start": "Начать", "common.stop": "Остановить", - "state.live": "Прямой эфир", - "state.reconnecting": "Повторное подключение", - "state.starting": "Запуск", - "state.failed": "Ошибка", - "state.stopped": "Остановено", - "state.offline": "Офлайн", - "hooks.title": "Webhook’и жизненного цикла", - "hooks.new": "Новый webhook", - "hooks.empty": "Webhook’ов нет. Добавьте один, и polyemesis отправит POST с подписанным JSON-телом в тот момент, когда поток начнётся или остановится либо назначение поднимется или упадёт — одна доставка на переход, по порядку, чтобы скрипт мог отреагировать.", - "hooks.colWebhook": "Webhook", - "hooks.colEndpoint": "Эндпоинт", - "hooks.colTriggers": "Подписан на", - "hooks.colEnabled": "Вкл.", - "hooks.signed": "подписан", - "hooks.unsigned": "БЕЗ ПОДПИСИ", + "dash.audioTracks": "Звуковые дорожки", + "dash.bitrate": "Битрейт", + "dash.broadcastSettings": "Настройки трансляции", + "dash.categoryPlaceholder": "Музыка", + "dash.clipboardUnavailable": "Буфер обмена недоступен на этом домене.", + "dash.connectPrompt": "Подключите аккаунт YouTube, Twitch, Facebook или Kick в разделе Настройки → Учётные данные платформ, чтобы задавать название, описание и категорию трансляции сразу на всех платформах. Каждая принимает свой набор полей — редактор указывает, какие именно, и отправляет то, что может.", + "dash.copyIngestUrl": "Копировать URL входа", + "dash.couldNotLoadTheDestination": "Не удалось загрузить назначение.", + "dash.couldNotPushTheMetadata": "Не удалось отправить метаданные.", + "dash.couldNotReorderTheDestinations": "Не удалось изменить порядок назначений.", + "dash.deleteDest": "Удалить назначение", + "dash.deleteDestDescription": "Это удалит назначение и его профиль маршрутизации. Платформа останется нетронутой, и назначение можно добавить снова — но настроенная для него маршрутизация звука будет потеряна.", + "dash.descriptionPlaceholder": "О чём эта трансляция.", + "dash.destDeleted": "Назначение удалено.", + "dash.disabled": "отключено", + "dash.everyConnectedAccount": "каждый подключённый аккаунт", + "dash.idle": "простой", + "dash.ingestUrlCopied": "URL входа скопирован.", + "dash.keyRefreshed": "Ключ трансляции обновлён с платформы.", + "dash.leaveUnchanged": "Оставить без изменений", + "dash.leaveUnchangedNote": "«Оставить без изменений» не отправляет вообще ничего, поэтому платформа сохраняет то, что у неё есть. Выбор «Вкл» или «Выкл» записывает это значение.", + "dash.metaCategory": "Категория", + "dash.metaDescription": "Описание", + "dash.metaTags": "Теги", + "dash.metaTitle": "Название", + "dash.noDestinations": "Назначений пока нет. Добавьте одно, затем выберите, какие звуковые дорожки оно получает.", + "dash.off": "Выкл", + "dash.pipeline": "Конвейер", + "dash.pushToPlatforms": "Отправить на платформы", + "dash.pushing": "Отправка…", + "dash.reconnects": "Переподключения", + "dash.relaySubscribers": "Подписчики ретранслятора", + "dash.renditions": "Варианты трансляции", + "dash.scheduledStart": "Запланированное начало", + "dash.subtitle": "Один вход, раздача многим: общее видео и свой звук для каждого назначения.", + "dash.tagsPlaceholder": "эфир, хаус, диджей-сет", + "dash.theConnectedAccount": "подключённый аккаунт", + "dash.title": "Панель управления", + "dash.titlePlaceholder": "Пятничный вечерний сет", + "dash.uptime": "Время работы", + "dash.youtubeStopsAcceptingTheseOnce": "YouTube перестаёт принимать это, как только трансляция выходит из состояния «created» или «ready».", + "dest.audience": "Аудитория", + "dest.audioBitrate": "Битрейт аудио", + "dest.audioCodec": "Аудиокодек", + "dest.channels": "Каналы", + "dest.compliance": "Соответствие требованиям", + "dest.connectedAccount": "Подключённый аккаунт", + "dest.contentLabels": "Метки контента", + "dest.copySource": "Копировать исходное видео", + "dest.createVariant": "Создать этот вариант", + "dest.customiseSummary": "Настроить для этого назначения", + "dest.fbBackupCost": "Удваивает исходящую полосу этого назначения.", + "dest.fbBackupHelp": "Отправляет вторую копию этого потока на резервный вход Facebook, чтобы оборвавшееся соединение не оборвало трансляцию.", + "dest.fbBackupLabel": "Публиковать резервный поток", + "dest.fbBackupReconnect": "Включение один раз переподключает поток — поэтому включайте его до выхода в эфир.", + "dest.fbBoxIntro": "Отправляется тем же вызовом создания, что и «Аудитория», поэтому действует то же правило: применяется один раз при старте трансляции и полностью игнорируется, если пусто.", + "dest.fbBoxTitle": "Кросспостинг и кнопка пожертвования в Facebook", + "dest.fbCrosspostAdd": "Добавить Страницу", + "dest.fbCrosspostAlsoPost": "Также опубликовать", + "dest.fbCrosspostHelp": "Числовой ID Страницы из собственной консоли Facebook (настройки Страницы или graph.facebook.com/me/accounts при входе от имени Страницы) — поиска здесь нет, поэтому вставляйте именно id, а не имя или URL. «Также опубликовать» публикует от имени этой Страницы, а не просто делится с ней трансляцией; если выключено, происходит только тихий репост.", + "dest.fbCrosspostLabel": "Кросспостинг на Страницы", + "dest.fbCrosspostPageId": "ID Страницы", + "dest.fbCrosspostRemove": "Убрать эту Страницу", + "dest.fbDonateHelp": "Это тоже непрозрачный id из консоли сборов Facebook, а не название организации для поиска. Оставьте пустым, чтобы не добавлять кнопку пожертвования вовсе.", + "dest.fbDonateLabel": "ID организации для кнопки пожертвования", + "dest.fbDonatePlaceholder": "ID организации", + "dest.fillFromPlatform": "Заполнить тем, что публикует эта платформа", + "dest.friends": "Друзья", + "dest.friendsOfFriends": "Друзья друзей", + "dest.giveUpAfter": "Сдаться после (попыток)", + "dest.leaveOnFacebook": "Оставить как есть в Facebook", + "dest.leaveOnYouTube": "Оставить как есть на YouTube", + "dest.madeForKids": "Создано для детей (COPPA)", + "dest.muxQueue": "Очередь мультиплексирования (пакеты)", + "dest.muxQueuePair": "Эти два параметра работают в паре: FFmpeg применяет лимит пакетов только после того, как очередь превысит порог в байтах, поэтому один лишь порог ничего не даёт и будет отклонён. Увеличьте оба, если назначение сообщает об ошибках чередования — звуковой тракт здесь имеет переменную задержку, потому что нормализация громкости читает с опережением.", + "dest.name": "Название", + "dest.namePlaceholder": "YouTube — основной канал", + "dest.notLinked": "Не привязан", + "dest.onlyMe": "Только я", + "dest.platform": "Платформа", + "dest.private": "Приватное", + "dest.public": "Публичное", + "dest.reconnecting": "Переподключение", + "dest.retryAfter": "Повтор через (секунды)", + "dest.sharedEncodeAria": "Общее кодирование видео", + "dest.socketTimeout": "Тайм-аут сокета (секунды)", + "dest.streamKeyLabel": "Ключ трансляции", + "dest.transport": "Транспорт", + "dest.transportFile": "Локальный файл", + "dest.transportRtmp": "RTMP / RTMPS", + "dest.unlisted": "По ссылке", + "dest.urlFromDashboard": "URL из панели", + "dest.useSharedEncode": "Использовать общее кодирование видео", + "dest.varBitrate": "Битрейт видео (кбит/с)", + "dest.varFps": "Частота кадров", + "dest.varHeight": "Высота", + "dest.varSourceHint": "источник", + "dest.varWidth": "Ширина", + "dest.variantWarning": "Запускает второе кодирование из вашего источника на основе «{name}». Это не бесплатный вариант, и он не меняет то, что получают другие назначения этого кодирования. Оставьте поле пустым, чтобы сохранить значение источника.", + "dest.videoTreatment": "Обработка видео", + "dest.visibility": "Видимость", + "hooks.allTriggers": "всё", "hooks.attempts": "попыток: {count}", + "hooks.attemptsLabel": "Попытки", "hooks.attemptsOne": "{count} попытка", - "hooks.timeout": "таймаут {seconds}с", - "hooks.allTriggers": "всё", - "hooks.triggerCount": "триггеров: {count}", - "hooks.triggerCountOne": "{count} триггер", - "hooks.enableAria": "Включить {name}", + "hooks.colEnabled": "Вкл.", + "hooks.colEndpoint": "Эндпоинт", + "hooks.colTriggers": "Подписан на", + "hooks.colWebhook": "Webhook", + "hooks.create": "Создать", + "hooks.created": "Webhook создан.", + "hooks.deleteBody": "Он немедленно перестаёт доставлять. Эндпоинт не затрагивается, и hook можно создать заново — но его ключ подписи восстановить нельзя.", + "hooks.deleteConfirm": "Удалить webhook", + "hooks.deleteFailed": "Не удалось удалить hook.", + "hooks.deleteTitle": "Удалить этот webhook?", + "hooks.deleted": "Webhook удалён.", "hooks.deliveries": "Последние доставки", - "hooks.sendTest": "Отправить тестовую доставку", - "hooks.sendTestTitle": "Отправить тестовую доставку сейчас", - "hooks.secretTitle": "Ключ подписи — скопируйте сейчас, показать его снова нельзя", - "hooks.secretCopied": "Я его скопировал", - "hooks.httpStatus": "HTTP {status}", - "hooks.noResponse": "нет ответа", + "hooks.deliveriesFailed": "Не удалось прочитать последние доставки.", + "hooks.deliveryTitle": "Доставка", + "hooks.dialogIntro": "Один подписанный POST на переход, по порядку. В отличие от оповещения ничего не объединяется и не подавляется — скрипт получает каждый фронт.", "hooks.durationMs": "{ms} мс", + "hooks.editTitle": "Изменить webhook", + "hooks.empty": "Webhook’ов нет. Добавьте один, и polyemesis отправит POST с подписанным JSON-телом в тот момент, когда поток начнётся или остановится либо назначение поднимется или упадёт — одна доставка на переход, по порядку, чтобы скрипт мог отреагировать.", + "hooks.enableAria": "Включить {name}", + "hooks.httpStatus": "HTTP {status}", + "hooks.loadFailed": "Не удалось загрузить webhook’и.", + "hooks.name": "Имя", + "hooks.new": "Новый webhook", + "hooks.newTitle": "Новый webhook", "hooks.noDeliveries": "Пока ничего не доставлено. Доставки появятся здесь по мере переходов.", - "hooks.deliveryTitle": "Доставка", - "hooks.statSent": "Отправлено", - "hooks.statFailed": "Ошибок", + "hooks.noResponse": "нет ответа", + "hooks.payloadNote": "Payload {version}. Подпись в {header}, по метке времени и телу.", + "hooks.retriesHint": "Повторы блокируют собственную очередь этого эндпоинта, потому что обещан порядок. Код 4xx не повторяется никогда — эндпоинт, сказавший, что запрос неверен, скажет это снова.", + "hooks.saveFailed": "Не удалось сохранить hook.", + "hooks.secretCopied": "Я его скопировал", + "hooks.secretTitle": "Ключ подписи — скопируйте сейчас, показать его снова нельзя", + "hooks.sendTest": "Отправить тестовую доставку", + "hooks.sendTestTitle": "Отправить тестовую доставку сейчас", + "hooks.signed": "подписан", + "hooks.statDropped": "Отброшено", "hooks.statEndpoints": "Эндпоинтов", + "hooks.statFailed": "Ошибок", "hooks.statQueued": "В очереди", - "hooks.statDropped": "Отброшено", "hooks.statRetries": "Повторов", - "hooks.payloadNote": "Payload {version}. Подпись в {header}, по метке времени и телу.", - "hooks.deleteTitle": "Удалить этот webhook?", - "hooks.deleteBody": "Он немедленно перестаёт доставлять. Эндпоинт не затрагивается, и hook можно создать заново — но его ключ подписи восстановить нельзя.", - "hooks.deleteConfirm": "Удалить webhook", - "hooks.editTitle": "Изменить webhook", - "hooks.newTitle": "Новый webhook", - "hooks.dialogIntro": "Один подписанный POST на переход, по порядку. В отличие от оповещения ничего не объединяется и не подавляется — скрипт получает каждый фронт.", - "hooks.name": "Имя", - "hooks.url": "URL эндпоинта", - "hooks.urlMasked": "Сохранённый URL никогда не показывается целиком. Оставьте замаскированное значение, чтобы сохранить его, или введите новый URL, чтобы заменить.", - "hooks.triggers": "Триггеры", - "hooks.triggersNoneHint": "Если не выбрано ничего, действуют все триггеры, включая добавленные будущим выпуском.", + "hooks.statSent": "Отправлено", + "hooks.testFailed": "Эндпоинт не принял тестовую доставку.", + "hooks.timeout": "таймаут {seconds}с", "hooks.timeoutLabel": "Таймаут (с)", - "hooks.attemptsLabel": "Попытки", - "hooks.retriesHint": "Повторы блокируют собственную очередь этого эндпоинта, потому что обещан порядок. Код 4xx не повторяется никогда — эндпоинт, сказавший, что запрос неверен, скажет это снова.", - "hooks.create": "Создать", - "hooks.triggerIngestPublished": "Поток начался", - "hooks.triggerIngestDisconnected": "Поток остановлен", - "hooks.triggerDestinationUp": "Назначение в эфире", - "hooks.triggerDestinationDown": "Назначение остановлено", - "hooks.loadFailed": "Не удалось загрузить webhook’и.", + "hooks.title": "Webhook’и жизненного цикла", "hooks.toggleFailed": "Не удалось изменить hook.", - "hooks.deleted": "Webhook удалён.", - "hooks.deleteFailed": "Не удалось удалить hook.", - "hooks.testFailed": "Эндпоинт не принял тестовую доставку.", - "hooks.deliveriesFailed": "Не удалось прочитать последние доставки.", + "hooks.triggerCount": "триггеров: {count}", + "hooks.triggerCountOne": "{count} триггер", + "hooks.triggerDestinationDown": "Назначение остановлено", + "hooks.triggerDestinationUp": "Назначение в эфире", + "hooks.triggerIngestDisconnected": "Поток остановлен", + "hooks.triggerIngestPublished": "Поток начался", + "hooks.triggers": "Триггеры", + "hooks.triggersNoneHint": "Если не выбрано ничего, действуют все триггеры, включая добавленные будущим выпуском.", + "hooks.unsigned": "БЕЗ ПОДПИСИ", "hooks.updated": "Webhook обновлён.", - "hooks.created": "Webhook создан.", - "hooks.saveFailed": "Не удалось сохранить hook.", - "playlist.title": "Плейлист", + "hooks.url": "URL эндпоинта", + "hooks.urlMasked": "Сохранённый URL никогда не показывается целиком. Оставьте замаскированное значение, чтобы сохранить его, или введите новый URL, чтобы заменить.", + "jobs.alwaysKeepNewest": "Всегда хранить самые новые", + "jobs.automaticModel": "Автоматически (по этому оборудованию)", + "jobs.backToDefault": "Вернуться к режиму по умолчанию для этого вида", + "jobs.batteryFloor": "Нижний порог заряда, %", + "jobs.broadcastGoing": "Идёт эфир; отложенная работа уступает ему.", + "jobs.cancel": "Отмена", + "jobs.cannotPause": "Эти виды следовало бы приостановить, но это невозможно, поэтому они доводятся до конца.", + "jobs.completed": "Завершено", + "jobs.concurrency": "Параллельность", + "jobs.couldNotReadTheJob": "Не удалось прочитать очередь задач.", + "jobs.couldNotSaveThePolicy": "Не удалось сохранить политику.", + "jobs.cpuCeiling": "Потолок CPU, %", + "jobs.cpuResume": "Возобновление при CPU, %", + "jobs.defaultMode": "Режим по умолчанию", + "jobs.defaultModel": "Модель по умолчанию", + "jobs.deferred": "Отложено", + "jobs.failed": "Ошибка", + "jobs.finished": "Завершённые", + "jobs.global": "Глобально", + "jobs.gpuGateNote": "Этим ограничиваются только виды, помеченные как использующие GPU.", + "jobs.history": "История", + "jobs.historyRetention": "Хранение истории", + "jobs.holdDeferredWorkBackOn": "Придерживать отложенные задачи на разряжающемся ноутбуке ниже этого значения. 0 отключает.", + "jobs.hostCpuLoad": "Загрузка CPU хоста", + "jobs.howManyJobsMayRun": "Сколько задач может выполняться одновременно. Одна — потому что второе перекодирование даёт производительность, которой никто не просил, ценой риска, на который никто не соглашался.", + "jobs.inQueue": "В очереди", + "jobs.job": "Задача", + "jobs.keepForDays": "Хранить дней", + "jobs.machineSafety": "Безопасность машины. Это единственный ограничитель, которому подчиняется и работа в реальном времени.", + "jobs.niceLevel": "Уровень nice", + "jobs.noFinished": "Завершённых задач пока нет.", + "jobs.noModels": "В каталоге моделей не найдено ни одного файла модели. Автоматический режим скачает выбранную им модель; указание конкретной модели здесь сработает только после появления соответствующего файла.", + "jobs.nothingNewStartsAboveThis": "Выше этого значения ничего нового не запускается. 0 отключает ограничение.", + "jobs.nothingQueued": "В очереди пусто. Постобработка отправляется из библиотеки.", + "jobs.nothingWasOldEnoughTo": "Ничего достаточно старого для очистки не нашлось. Сначала уменьшите срок хранения в «Политике».", + "jobs.pauseAll": "Приостановить все", + "jobs.policy": "Политика", + "jobs.policySaved": "Политика ресурсов сохранена.", + "jobs.progress": "Прогресс", + "jobs.queuePaused": "Очередь приостановлена.", + "jobs.queueResumed": "Очередь возобновлена.", + "jobs.queued": "В очереди", + "jobs.recording": "Запись", + "jobs.releasedItWillStartAs": "Освобождено. Задача запустится, как только освободится слот.", + "jobs.removeFromHistory": "Удалить из истории", + "jobs.removeWindow": "Удалить окно", + "jobs.resumeAll": "Возобновить все", + "jobs.retry": "Повторить", + "jobs.retryTitle": "Запустить заново со свежим запасом попыток", + "jobs.runNow": "Выполнить сейчас", + "jobs.runNowTitle": "Выполнить эту задачу сейчас, игнорируя политику ресурсов", + "jobs.running": "Выполняется", + "jobs.savedTheConcurrencyChangeTakes": "Сохранено. Изменение параллельности вступит в силу при перезапуске сервера — очередь фиксирует его при старте.", + "jobs.state": "Состояние", + "jobs.stopsEverythingRealtimeIncludedA": "Останавливает всё, включая реальное время: троттлящий CPU уже начал ухудшать поток. 0 отключает.", + "jobs.subtitle": "Фоновая работа: расшифровка, прокси, миниатюры и сжатие архива.", + "jobs.thatDidNotWork": "Не получилось.", + "jobs.thermalCeiling": "Тепловой потолок, °C", + "jobs.thisManyFinishedJobsSurvive": "Столько завершённых задач сохраняется независимо от их возраста.", + "jobs.title": "Задачи", + "jobs.transcription": "Расшифровка", + "jobs.unsavedChanges": "Несохранённые изменения.", + "jobs.whereTheCeilingReleasesThe": "Значение, при котором потолок отпускает. Разрыв между ними — это гистерезис, не дающий ограничению колебаться.", + "jobs.whisperMissing": "whisper.cpp здесь не установлен, поэтому пока это ни на что не влияет. Задать всё равно стоит: выбор вступит в силу сразу после установки.", + "jobs.why": "Причина", + "jobs.windowEnd": "Конец окна", + "jobs.windowStart": "Начало окна", + "jobs.yieldNote": "Тяжёлая работа ставится в очередь и уступает живому потоку. Потерянный кадр не восстановить; расшифровка, пришедшая часом позже, ничего не стоит.", + "lib.anySpeaker": "Любой говорящий", + "lib.anyTag": "Любой тег", + "lib.autoGrouped": "Сгруппировано автоматически. Правка делает группировку вашей.", + "lib.autoGroupedNote": "Эта сессия была сгруппирована автоматически. Сохранение делает её вашей, и проход группировки перестанет её переписывать.", + "lib.bestMatch": "Лучшее совпадение", + "lib.clearSearch": "Очистить поиск", + "lib.clip": "Клип", + "lib.clipTitle": "Вырезать клип из этой записи", + "lib.collapseSession": "Свернуть сессию", + "lib.couldNotLoadTheLibrary": "Не удалось загрузить библиотеку.", + "lib.couldNotLoadTheSession": "Не удалось загрузить сегменты сессии.", + "lib.couldNotQueueTheProxy": "Не удалось поставить прокси в очередь.", + "lib.couldNotRegroup": "Не удалось перегруппировать.", + "lib.couldNotSaveTheSession": "Не удалось сохранить сессию.", + "lib.delete": "Удалить", + "lib.deleteDescription": "Файл будет удалён с диска вместе с прокси, миниатюрами и расшифровкой. Это действие необратимо.", + "lib.deleteTitle": "Удалить эту запись?", + "lib.downloadMaster": "Скачать основной файл", + "lib.downloadMasterTitle": "Скачать многодорожечный основной файл", + "lib.editSession": "Изменить сессию", + "lib.expandSession": "Развернуть сессию", + "lib.from": "С", + "lib.generateProxy": "Создать прокси", + "lib.generateProxyTitle": "Создать прокси, воспроизводимый в браузере", + "lib.mostRecent": "Сначала новые", + "lib.noProxyYet": "Прокси пока нет — откройте, чтобы создать", + "lib.noSegments": "Здесь нет сегментов.", + "lib.noSessionMatch": "Ни одна сессия не подходит под эти фильтры.", + "lib.nothingRecorded": "Пока ничего не записано. Сессии появляются по мере того, как рекордер пишет сегменты.", + "lib.oldestFirst": "Сначала старые", + "lib.playProxy": "Воспроизвести прокси", + "lib.proxyAlreadyQueued": "Прокси уже в очереди.", + "lib.proxyQueued": "Прокси поставлен в очередь.", + "lib.recordingDeleted": "Запись удалена.", + "lib.regroupTitle": "Повторно выполняет группировку. Она аддитивна и идемпотентна: разделённую вручную группировку она никогда не объединит.", + "lib.searchPlaceholder": "Искать по всему сказанному во всех записях…", + "lib.searchTranscripts": "Поиск по расшифровкам", + "lib.searchable": "Доступные для поиска", + "lib.searchableOnly": "Сегменты с расшифровкой, доступные для поиска", + "lib.session": "Сессия", + "lib.sessionDescription": "Описание", + "lib.sessionSaved": "Сессия сохранена.", + "lib.sessionTags": "Теги", + "lib.sessionTitle": "Название", + "lib.subtitle": "Каждый эфир — с поиском. Каждый микрофон записан на собственную дорожку, поэтому расшифровка уже знает, кто это сказал.", + "lib.tag": "Тег", + "lib.tagsPlaceholder": "через, запятую", + "lib.title": "Библиотека", + "lib.transcribe": "Расшифровать", + "lib.transcribeEachTrackOnIts": "Расшифровывать каждую дорожку отдельно — именно это даёт привязку к говорящему", + "lib.ungrouped": "Несгруппированные сегменты", + "meters.deviation": "Отклонение", + "meters.idle": "простой", + "meters.integrated": "Интегральная", + "meters.loudness": "Соответствие громкости", + "meters.metering": "измерение", + "meters.momentary": "Мгновенная", + "meters.monitorOff": "Монитор выключен. Включите его, чтобы измерить, что отдаёт каждое назначение.", + "meters.noSignal": "нет сигнала на этой дорожке", + "meters.nothingToMeasure": "Пока нечего измерять. Каждое работающее назначение получает собственный анализатор EBU R128 для того микса, который оно получает.", + "meters.range": "Диапазон", + "meters.shortTerm": "Кратковременная", + "meters.subtitle": "Каждый канал каждой входной дорожки, прямо с ретранслятора.", + "meters.title": "Индикаторы звука", + "meters.truePeak": "Истинный пик", + "meters.waitingForStream": "ожидание потока", + "mon.allProcesses": "Все процессы", + "mon.bitrateChart": "Битрейт входа — последние 30 минут", + "mon.chooseDestination": "Выберите назначение…", + "mon.clearLog": "Очистить журнал", + "mon.clearedThisDestinationIsBack": "Сброшено. Это назначение снова использует сгенерированную команду.", + "mon.dryRun": "Пробный запуск", + "mon.dryRunInvalid": "FFmpeg отклонил аргумент", + "mon.dryRunOk": "FFmpeg принял команду", + "mon.extraInput": "Дополнительные аргументы входа", + "mon.extraOutput": "Дополнительные аргументы выхода", + "mon.ffmpegLog": "Журнал FFmpeg", + "mon.fromRunningProcess": "из работающего процесса", + "mon.fullCommand": "Полностью развёрнутая команда", + "mon.hostCpu": "Процессор хоста", + "mon.ingestLoss": "Потери на входе", + "mon.memory": "Память", + "mon.nothingProvenEitherWay": "Ничего не подтверждено", + "mon.pid": "PID", + "mon.processes": "Процессы", + "mon.rebuilt": "пересобрано", + "mon.relayDrops": "Потери ретранслятора", + "mon.relayIn": "Вход ретранслятора", + "mon.resolveHint": "Разверните команду, чтобы перед применением увидеть в точности, что будет выполнено.", + "mon.restarts": "Перезапуски", + "mon.subtitle": "Состояние процессов, ресурсы хоста и живой журнал FFmpeg.", + "mon.title": "Мониторинг", + "mon.uptime": "Время работы", + "nav.automation": "Автоматизация", + "nav.chat": "Чат", + "nav.clips": "Клипы", + "nav.dashboard": "Панель управления", + "nav.jobs": "Задачи", + "nav.library": "Библиотека", + "nav.meters": "Индикаторы звука", + "nav.monitoring": "Мониторинг", + "nav.playout": "Playout", + "nav.recordings": "Записи", + "nav.renditions": "Варианты трансляции", + "nav.routing": "Routing", + "nav.settings": "Настройки", + "nav.sources": "Источники", + "play.acrossEveryRungTheBackstop": "По всем ступеням. Ограничитель, не дающий долгой трансляции заполнить диск.", + "play.allowCrossOrigin": "Разрешить межсайтовые запросы", + "play.allowCrossOriginPlayback": "Разрешить межсайтовое воспроизведение", + "play.audioBitrate": "Битрейт звука (кбит/с)", + "play.audioTrack": "Звуковая дорожка", + "play.basicPasswordNote": "Плееры, которые не умеют переходить по ссылке (телеприставка, CDN), могут вместо этого отправлять тот же секрет как пароль HTTP basic.", + "play.boundsTheViewerTableViewers": "Ограничивает таблицу зрителей. Зрители сверх этого числа обслуживаются, просто не учитываются.", + "play.carriesThePlaybackTokenAnyone": "Содержит токен воспроизведения. Смотреть может любой, у кого есть этот URL.", + "play.clipboardUnavailable": "Буфер обмена недоступен; выделите и скопируйте вручную", + "play.couldNotLoadPlayout": "Не удалось загрузить Playout", + "play.disabled": "Отключено", + "play.diskCap": "Предел на диске (МБ)", + "play.dvrWindow": "Окно DVR (с)", + "play.embedCode": "Код для встраивания", + "play.enable": "Включить playout", + "play.enabled": "Включено", + "play.forObsACdnOr": "Для OBS, CDN или любого плеера, понимающего HLS.", + "play.format": "Формат", + "play.hls": "HLS", + "play.hlsDash": "HLS и DASH", + "play.hlsPlaylist": "Плейлист HLS", + "play.idleTimeout": "Тайм-аут простоя зрителя (с)", + "play.linkReset": "Создана новая ссылка воспроизведения. Предыдущая больше не работает.", + "play.linksProtected": "Эти ссылки работают только для вошедшего администратора, пока вы не включите «Публично».", + "play.linksPublic": "Эти ссылки работают без учётной записи.", + "play.liveWindow": "Живое окно (сегменты)", + "play.metaDescription": "Описание", + "play.metaTitle": "Название", + "play.mustExceedOneSegmentOr": "Должно превышать длину одного сегмента, иначе зрители будут пропадать между опросами.", + "play.name": "Название", + "play.noCredentialNote": "Никакие учётные данные не требуются. Подходит только для потока, который вы намерены показать всему интернету.", + "play.noRungs": "Ступеней пока нет. Добавьте хотя бы одну, чтобы что-то публиковать.", + "play.onDisk": "На диске", + "play.oneStereoAacTrackPer": "По одной стереодорожке AAC на ступень.", + "play.packaging": "Упаковка", + "play.passthrough": "Вход (без перекодирования)", + "play.peak": "Пик", + "play.playerPage": "Страница плеера", + "play.posterNote": "Показывается над видео на публичной странице. Постер — это недавний ключевой кадр, обновляется автоматически.", + "play.private": "Приватно.", + "play.protectedNote": "Воспроизведение по умолчанию защищено. Его открытие — отдельный осознанный шаг.", + "play.protection": "Защита", + "play.public": "Публично", + "play.resetFailed": "Не удалось сбросить", + "play.restartNote": "Изменение любого из этих параметров перезапускает мультиплексоры, что прерывает просмотр на несколько секунд.", + "play.rungNote": "Каждая ступень копирует видео своего варианта бит в бит, поэтому добавление ступени стоит одного мультиплексора и никогда — второго кодирования видео. От ступени к ступени отличается только звуковая дорожка; маршрутизация по назначениям остаётся нетронутой.", + "play.segmentLength": "Длина сегмента (с)", + "play.serveWithoutSession": "Обслуживать зрителей без сессии", + "play.sessionCap": "Предел числа сессий", + "play.sessions": "Сессии", + "play.share": "Поделиться", + "play.shorterIsLowerLatencyAnd": "Короче — меньше задержка и чаще обновляется плейлист.", + "play.subtitle": "Отдавайте поток зрителям напрямую, как публичный HLS.", + "play.threeIsTheMinimumA": "Три — минимум, на котором плеер может буферизовать.", + "play.title": "Playout", + "play.unlisted": "По ссылке.", + "play.untitled": "Поток без названия", + "play.videoSource": "Источник видео", + "play.watchingNow": "Смотрят сейчас", + "play.whoCanWatch": "Кто может смотреть", + "player.copied": "Скопировано", + "player.copy": "Копировать", + "player.loading": "Загрузка…", + "player.missingToken": "В открытой ссылке отсутствует токен воспроизведения либо токен был изменён. Попросите у того, кто её прислал, актуальную ссылку.", + "player.notLive": "Сейчас эфира нет", + "player.viewer": "зритель", + "player.viewers": "зрителей", + "player.willStart": "Эта страница начнёт воспроизведение сама, когда трансляция начнётся.", + "playlist.choose": "Выберите файл", + "playlist.chooseAria": "Выберите файл для добавления", "playlist.empty": "В очереди пока пусто.", + "playlist.hint": "Загружаются в разделе Media. Элемент, названный этим плейлистом, который позже потеряет свой файл, покажется выше как требующий внимания, вместо того чтобы молча отказать при следующем переключении.", "playlist.itemsAria": "Элементы плейлиста", - "playlist.stateReady": "Готово", - "playlist.stateTranscoding": "Перекодирование", - "playlist.stateAttention": "Требует внимания", - "playlist.stateUnsaved": "Ещё не сохранено", "playlist.missingUpload": "Этот файл больше не найден.", - "playlist.moveUp": "Переместить {name} вверх", "playlist.moveDown": "Переместить {name} вниз", - "playlist.remove": "Удалить {name}", - "playlist.choose": "Выберите файл", - "playlist.chooseAria": "Выберите файл для добавления", + "playlist.moveUp": "Переместить {name} вверх", "playlist.noUploads": "Нет доступных файлов", - "playlist.hint": "Загружаются в разделе Media. Элемент, названный этим плейлистом, который позже потеряет свой файл, покажется выше как требующий внимания, вместо того чтобы молча отказать при следующем переключении.", - "dest.fbBoxTitle": "Кросспостинг и кнопка пожертвования в Facebook", - "dest.fbBoxIntro": "Отправляется тем же вызовом создания, что и «Аудитория», поэтому действует то же правило: применяется один раз при старте трансляции и полностью игнорируется, если пусто.", - "dest.fbBackupLabel": "Публиковать резервный поток", - "dest.fbBackupHelp": "Отправляет вторую копию этого потока на резервный вход Facebook, чтобы оборвавшееся соединение не оборвало трансляцию.", - "dest.fbBackupCost": "Удваивает исходящую полосу этого назначения.", - "dest.fbBackupReconnect": "Включение один раз переподключает поток — поэтому включайте его до выхода в эфир.", - "dest.fbCrosspostLabel": "Кросспостинг на Страницы", - "dest.fbCrosspostPageId": "ID Страницы", - "dest.fbCrosspostAlsoPost": "Также опубликовать", - "dest.fbCrosspostRemove": "Убрать эту Страницу", - "dest.fbCrosspostAdd": "Добавить Страницу", - "dest.fbCrosspostHelp": "Числовой ID Страницы из собственной консоли Facebook (настройки Страницы или graph.facebook.com/me/accounts при входе от имени Страницы) — поиска здесь нет, поэтому вставляйте именно id, а не имя или URL. «Также опубликовать» публикует от имени этой Страницы, а не просто делится с ней трансляцией; если выключено, происходит только тихий репост.", - "dest.fbDonateLabel": "ID организации для кнопки пожертвования", - "dest.fbDonatePlaceholder": "ID организации", - "dest.fbDonateHelp": "Это тоже непрозрачный id из консоли сборов Facebook, а не название организации для поиска. Оставьте пустым, чтобы не добавлять кнопку пожертвования вовсе." + "playlist.remove": "Удалить {name}", + "playlist.stateAttention": "Требует внимания", + "playlist.stateReady": "Готово", + "playlist.stateTranscoding": "Перекодирование", + "playlist.stateUnsaved": "Ещё не сохранено", + "playlist.title": "Плейлист", + "rec.delete": "Удалить", + "rec.deleteDescription": "Файл будет удалён с диска вместе с прокси, миниатюрами и расшифровкой. Это действие необратимо.", + "rec.deleteFailed": "Не удалось удалить запись.", + "rec.deleteTitle": "Удалить эту запись?", + "rec.deleted": "Запись удалена.", + "rec.disabled": "отключено", + "rec.disk": "Диск", + "rec.diskRecordings": "Записи", + "rec.download": "Скачать", + "rec.downloadNamed": "Скачать {name}", + "rec.duration": "Длительность", + "rec.empty": "Записей пока нет. Включите запись справа и начните трансляцию.", + "rec.enabled": "Включено", + "rec.file": "Файл", + "rec.free": "Свободно", + "rec.haltedDefault": "Запись остановлена: на томе меньше свободного места, чем задано нижним порогом.", + "rec.haltedRestarts": "Рекордер запустится сам, как только место освободится.", + "rec.help.enabled": "Пишет каждый вход на диск прямо во время трансляции, сохраняя все звуковые дорожки раздельно и без перекодирования. Запись не зависит от того, что вы отправляете на платформы: её отключение не влияет на прямой эфир, а включение расходует место на диске, а не полосу отдачи.", + "rec.help.maxAgeHours": "Удаляет записи старше указанного числа часов независимо от общего размера. Полезно, когда нужны только последние день-два. 0 — хранить всё, пока не удалит другое ограничение.", + "rec.help.maxGb": "Верхняя граница общего размера записей, которые хранит polyemesis. При превышении самые старые удаляются, пока сумма снова не уложится. 0 — без ограничения размера; тогда рост ограничивают только возраст или нижний порог свободного места.", + "rec.help.minFreeGb": "Жёсткая остановка: запись прекращается, когда свободного места на томе становится меньше указанного, и возобновляется сама, как только место освободится. Это единственное ограничение, учитывающее файлы, которые polyemesis не создавал, поэтому именно оно действительно защищает общий диск от переполнения.", + "rec.help.segmentSeconds": "Сколько длится каждый файл, прежде чем рекордер начнёт следующий. Короткие сегменты ограничивают потери при сбое и упрощают перенос отдельных файлов, но их становится больше. Разрезы приходятся на ключевой кадр, поэтому реальная длина колеблется примерно на секунду.", + "rec.help.stemFiles": "Дополнительно пишет каждую звуковую дорожку отдельным файлом рядом с основным. Полезно, если вы монтируете потом и хотите получить ведущего, гостей и звук игры на разных каналах. Объём записи сессии увеличивается примерно во столько раз, сколько дорожек, поэтому по умолчанию выключено.", + "rec.help.stemFormat": "Оба варианта без потерь, так что качество не страдает ни в одном. FLAC примерно вдвое меньше и подходит, если только какой-нибудь инструмент в вашем процессе его не читает; WAV открывается чем угодно. Дорожки шире 8 каналов автоматически переходят на WAV.", + "rec.hideStems": "Скрыть отдельные дорожки", + "rec.keepForever": "0 = хранить всегда.", + "rec.loadFailed": "Не удалось загрузить записи.", + "rec.maxAgeHours": "Удалять через (часов)", + "rec.maxGb": "Хранить не более (ГБ)", + "rec.minFreeGb": "Остановить при свободном месте ниже (ГБ)", + "rec.minFreeNote": "Ограничения выше касаются только того, что записал polyemesis; всё остальное на томе всё равно может его заполнить. 0 = без нижнего порога.", + "rec.noSizeLimit": "0 = без ограничения размера.", + "rec.recording": "идёт запись", + "rec.retentionNote": "Очистка выполняется каждые 30 секунд: сначала самые старые, по возрасту, затем по общему размеру. Сегмент, который пишется сейчас, никогда не удаляется.", + "rec.saveFailed": "Не удалось сохранить настройки.", + "rec.savePolicy": "Сохранить правила хранения", + "rec.saved": "Настройки записи сохранены.", + "rec.segmentSeconds": "Длина сегмента (секунды)", + "rec.segments": "Сегменты", + "rec.settingsTitle": "Запись", + "rec.showStems": "Показать отдельные дорожки", + "rec.size": "Размер", + "rec.started": "Начата", + "rec.stemFiles": "Файлы отдельных дорожек", + "rec.stemFlac": "FLAC — без потерь, примерно вдвое меньше", + "rec.stemFormat": "Формат отдельных дорожек", + "rec.stemNote": "Пишется тем же процессом, что и основной файл, поэтому каждая отдельная дорожка остаётся выровненной по сэмплам с остальными. Дорожки шире 8 каналов сами переходят на WAV.", + "rec.stemWav": "WAV — без потерь, открывается везде", + "rec.stems": "Отдельные дорожки", + "rec.stemsTitle": "Файлов отдельных дорожек: {count}, выровнены по сэмплам с основным файлом", + "rec.stemsUsage": "Отдельные дорожки ({count} файлов)", + "rec.subtitle": "Полный многодорожечный архив: каждая входная звуковая дорожка сохранена без перекодирования.", + "rec.title": "Записи", + "rec.tracks": "Дорожки", + "rec.tracksTitle": "Сохранено звуковых дорожек: {count}", + "rec.used": "Занято", + "rec.volume": "Том", + "rend.aspectBlurpad": "Размытая заливка", + "rend.aspectBlurpadHint": "Заполняет остаток размытой копией самого кадра, обрезанной по размеру. Такое решение закрепилось для вертикальных трансляций.", + "rend.aspectCrop": "Обрезать по размеру", + "rend.aspectCropHint": "Обрезает по центру до целевой формы, затем масштабирует. Объекты сохраняют экранный размер; края кадра теряются.", + "rend.aspectHandling": "Обработка соотношения сторон", + "rend.aspectPad": "Леттербокс", + "rend.aspectPadHint": "Масштабирует весь кадр по размеру и заполняет остальное сплошным цветом. Ничего не теряется, но источник 16:9 на холсте 9:16 — это в основном полосы.", + "rend.aspectStretch": "Растянуть по размеру", + "rend.aspectStretchHint": "Масштабирует до целевого размера и допускает искажение картинки, если пропорции источника не совпадают. Так работал каждый вариант до появления остальных режимов.", + "rend.boxColour": "Цвет подложки", + "rend.boxOpacity": "Непрозрачность подложки (%)", + "rend.builtInFont": "Встроенный по умолчанию", + "rend.burnedText": "Текст, впечатанный в кадр", + "rend.chooseHardware": "Выберите аппаратный кодировщик или меньший размер вывода.", + "rend.colourBlack": "black", + "rend.colourWhite": "white", + "rend.costBeyond": "Программное кодирование этого в реальном времени не по силам большинству машин", + "rend.costBeyondDetail": "{label} — это {mpps} на одно кодирование. x264 при таком размере обычно проигрывает гонку реальному времени, а отстающий вариант теряет кадры для каждого назначения, которое его читает, — и узнаёте вы об этом посреди эфира.{machine} {escape}", + "rend.costBusy": "Ожидайте, что несколько ядер будут заняты", + "rend.costBusyDetail": "{label} — это {mpps} через {encoder}, то есть тяжёлое кодирование в реальном времени.{machine} Следите за скоростью кодировщика в эфире: ниже 1,0× он отстаёт, и назначения под ним начнут терять кадры. {escape}", + "rend.costMachineCores": " Эта машина сообщает о {cores} логических ядрах.", + "rend.costModest": "Умеренное программное кодирование", + "rend.costModestDetail": "{label} — это {mpps}, что по силам большинству машин.{machine} Это всё равно одно настоящее кодирование — общее для каждого назначения, которое его выбирает, но не бесплатное.", + "rend.costNoNumbers": "Стоимость пока нельзя оценить", + "rend.costNoNumbersDetail": "Источник ещё не прозондирован, поэтому за «как в источнике» нет никаких цифр. Запустите вход, и это заполнится.", + "rend.costOffloaded": "Передано на {encoder}", + "rend.costOffloadedDetail": "{label} ({mpps}) выполняется на аппаратном кодировщике GPU, поэтому почти не нагружает CPU. Качество и управление битрейтом различаются в зависимости от драйвера, и у каждого чипа свой потолок — проверьте результат до важного эфира.", + "rend.create": "Создать", + "rend.created": "Вариант создан. Выберите его в назначении, чтобы запустить кодирование.", + "rend.custom": "Свой…", + "rend.deintAll": "Каждый кадр", + "rend.deintAllHint": "Для источников, которые чересстрочные, но об этом не сообщают. Множество SDI-мостов и плат захвата помечают всё как прогрессивное независимо от того, что им подали, и на них «Только чересстрочные кадры» ничего не делает и выглядит как сломанная настройка.", + "rend.deintAuto": "Только чересстрочные кадры", + "rend.deintAutoHint": "Затрагивает только кадры, помеченные источником как чересстрочные, поэтому прогрессивные кадры проходят без изменений. Правильный выбор для любого смешанного материала.", + "rend.deintOff": "Выкл", + "rend.deintOffHint": "Подходит для прогрессивных источников, которые есть почти у всех. Деинтерлейсинг такого источника только размывает картинку без пользы.", + "rend.deinterlace": "Деинтерлейсинг", + "rend.deleteFailed": "Не удалось удалить вариант.", + "rend.deleteFallbackMany": "{count} назначений переходят на передачу без перекодирования и получат видео источника без изменений. Проверьте, что источник по-прежнему подходит под требования каждой площадки.", + "rend.deleteFallbackOne": "{count} назначение переходит на передачу без перекодирования и получит видео источника без изменений. Проверьте, что источник по-прежнему подходит под требования каждой площадки.", + "rend.deleteNoDestinations": "Этот вариант не выбран нигде, поэтому его удаление ничего не изменит ни для одного назначения.", + "rend.deleteRenditionTitle": "Удалить {name}?", + "rend.editRendition": "Изменить вариант", + "rend.empty": "Вариантов нет. Каждое назначение работает без перекодирования, и это ровно то, что нужно, пока какое-то из них не сможет принять источник — например, вход 4K60, который Twitch, Kick или X не примут. Добавьте вариант и направьте такие назначения на него.", + "rend.encoder": "Кодировщик", + "rend.encoderPreset": "Пресет кодировщика", + "rend.encoderRelatedFailed": "{name} не предлагается, потому что связанный кодировщик здесь не прошёл проверку.", + "rend.encoderTestFailed": "{name} не прошёл тестовое кодирование одного кадра на этой машине.", + "rend.encoderTestOk": "{name} закодировал тестовый кадр на этой машине.", + "rend.evenNote": "0 по оси сохраняет значение источника и соотношение сторон — задайте только высоту, чтобы масштабировать. Оба значения должны быть чётными: у H.264 и HEVC нет цветовой плоскости нечётного размера.", + "rend.font": "Шрифт", + "rend.frameRate": "Частота кадров", + "rend.gop": "GOP", + "rend.gpuUnopenable": "GPU присутствует, но этот процесс не может его открыть", + "rend.hardwareFallback": "аппаратный кодировщик", + "rend.hardwareScanFound": "Что обнаружило сканирование оборудования", + "rend.height": "Высота", + "rend.hevcWarning": "Этот кодировщик выдаёт HEVC. Большинство назначений RTMP принимают только H.264 — выбирайте его лишь для назначения, которое сообщило, что принимает HEVC.", + "rend.keyframeInterval": "Интервал ключевых кадров", + "rend.letterboxColour": "Цвет полей", + "rend.live": "В эфире", + "rend.loadFailed": "Не удалось загрузить варианты.", + "rend.marginX": "Отступ X (%)", + "rend.marginY": "Отступ Y (%)", + "rend.name": "Название", + "rend.namePlaceholder": "1080p60 для Twitch и Kick", + "rend.needsBothAWidthAnd": "Нужны и ширина, и высота — при одной свободной оси масштабирование и так сохраняет пропорции.", + "rend.needsBothAxes": "Нужны и ширина, и высота", + "rend.newRendition": "Новый вариант", + "rend.noEncoder": "Этот FFmpeg не сообщил ни об одном пригодном видеокодировщике.", + "rend.noHardwareEscape": "На этой машине не работает ни один аппаратный кодировщик, поэтому переносить нагрузку некуда — единственный рычаг здесь это меньший размер вывода. Повторите определение оборудования, если вы с тех пор установили драйвер или пробросили GPU в контейнер.", + "rend.notProbed": "На этой машине ещё ничего не кодировалось для проверки, поэтому этот список показывает, что содержит сборка FFmpeg, а не то, на что способно оборудование. Запустите повторное определение, чтобы выяснить.", + "rend.notRunning": "Не выполняется", + "rend.note": "Заметка", + "rend.noteDuplicateFps": "Источник идёт с частотой {srcFps} fps, поэтому {fps} fps только дублирует кадры.", + "rend.notePlaceholder": "Для чего нужен этот уровень.", + "rend.noteUpscale": "Источник — {width}×{height}. Увеличение нагружает CPU и не добавляет деталей.", + "rend.opacity": "Непрозрачность (%)", + "rend.percentNote": "Все значения задаются в процентах от кадра, поэтому одни и те же настройки верны и для уровня 16:9, и для 9:16. По центрированной оси отступы игнорируются.", + "rend.preset": "Пресет", + "rend.presetPlaceholder": "veryfast", + "rend.presetStartingPoint": "Отправная точка — актуальные ограничения уточняйте у площадки.", + "rend.redetectFailed": "Не удалось повторно определить оборудование.", + "rend.redetectTitle": "Повторить сканирование GPU и закодировать по одному тестовому кадру каждым кодировщиком. Занимает несколько секунд.", + "rend.redetectedNoHardware": "Оборудование определено заново. Ни один аппаратный кодировщик не прошёл тестовое кодирование на этой машине.", + "rend.resolution": "Разрешение", + "rend.restartEncode": "Перезапустить это кодирование и назначения, которые его читают", + "rend.restartFailed": "Не удалось перезапустить кодирование.", + "rend.restarting": "{name} перезапускается. Его назначения переподключаются вместе с ним.", + "rend.save": "Сохранить", + "rend.saveFailed": "Не удалось сохранить вариант.", + "rend.seedNote": "Они лишь заполняют форму начальными значениями — правьте что угодно перед сохранением. Ограничения платформ меняются и зависят от партнёрского статуса.", + "rend.signalError": "Ошибка", + "rend.signalIdle": "Простой", + "rend.sizePct": "Размер (% от высоты)", + "rend.softwareEncoderFallback": "программный кодировщик", + "rend.speed": "Скорость", + "rend.subtitle": "Одно общее кодирование видео, много назначений. Звук здесь никогда не затрагивается.", + "rend.target": "Цель", + "rend.textColour": "Цвет текста", + "rend.textPlaceholder": "МОЯ СТАНЦИЯ", + "rend.title": "Варианты трансляции", + "rend.unused": "Пока его не выбрало ни одно назначение, поэтому ничего не кодируется. Выберите его в назначении, чтобы запустить кодирование.", + "rend.uptime": "Время работы", + "rend.videoBitrate": "Битрейт видео", + "rend.waitingSnapshot": "Ожидание первого живого снимка.", + "rend.watermarkImage": "Изображение водяного знака", + "rend.watermarkPlaceholder": "overlays/logo.png", + "rend.width": "Ширина", + "rend.widthPct": "Ширина (% от кадра)", + "route.attack": "Атака", + "route.audio": "звук", + "route.audioAndVideoGoOut": "Звук и видео идут вместе.", + "route.audioLaterThanVideo": "Звук позже видео", + "route.audioRouting": "Маршрутизация звука", + "route.audioRoutingDesc": "Выберите в точности, какие входные дорожки получает каждое назначение.", + "route.autoLimitWhen2Tracks": "Авто (ограничивать при 2+ дорожках)", + "route.commentaryLanguage": "Язык комментария", + "route.couldNotApplyThePreset": "Не удалось применить пресет.", + "route.couldNotCompileThisRouting": "Не удалось скомпилировать эту маршрутизацию.", + "route.couldNotLoadDestinations": "Не удалось загрузить назначения.", + "route.couldNotSaveTheRouting": "Не удалось сохранить маршрутизацию.", + "route.couldNotSaveTrackRoles": "Не удалось сохранить роли дорожек.", + "route.custom": "Свой…", + "route.delayDirection": "Направление задержки", + "route.destinations": "Назначения", + "route.duckingNote": "Приглушать одну группу дорожек, когда звучит другая, — микрофон поверх музыкальной подложки. Выключено означает, что каждая выбранная дорожка суммируется со своим усилением, без изменений.", + "route.duckingTriggerNote": "Триггер не обязан входить в микс этого назначения — микрофон может приглушать здесь музыку, тогда как отправляется только музыка.", + "route.enableDucking": "Включить приглушение", + "route.everyTrackReaches": "Каждая выбранная дорожка доходит до этого назначения, включая музыку.", + "route.excludeMusic": "Исключить музыку", + "route.keepMusic": "Оставить музыку", + "route.keepMusicOut": "Не пропускать музыку в это назначение", + "route.labelTracks": "Пометить дорожки", + "route.langPlaceholder": "язык — например es", + "route.limiter04DbfsCeiling": "Лимитер (потолок −0,4 dBFS)", + "route.localOnlyNote": "Этот сервер пока не хранит роли дорожек, поэтому они остаются в этом браузере. Всё остальное на странице сохраняется как обычно.", + "route.loudnessTarget": "Целевая громкость", + "route.lufsBroadcast": "EBU R128 — эфир", + "route.measuredOverTheWholeProgramme": "Измеряется по всей программе, а не мгновенно: тихие фрагменты остаются тихими. Это те значения, на которые нацелены собственные нормализаторы площадок, поэтому попадание в одно из них и есть то, что не даёт площадке приглушить вас.", + "route.mixMatrix": "Матрица микса", + "route.musicPolicy": "Политика музыки", + "route.noTarget": "Без цели (не трогать уровни)", + "route.normalizeLoudnorm": "Громкость (EBU R128)", + "route.normalizeOff": "Выкл", + "route.off": "выкл", + "route.offset": "Смещение", + "route.on": "вкл", + "route.platformDefault": "По умолчанию для платформы", + "route.podcastSpokenWord": "Подкаст / речь", + "route.presetApplied": "Пресет применён. Проверьте его, затем сохраните.", + "route.range": "Диапазон", + "route.ratio": "Соотношение", + "route.release": "Восстановление", + "route.result": "Результат", + "route.role51": "5.1", + "route.roleClean": "чистый", + "route.roleMic": "микрофон", + "route.roleMusic": "музыка", + "route.rolesNote": "Роли описывают вход, а не это назначение, и сохраняются сами по себе. Ничего не фильтруется, пока назначение ниже об этом не попросит.", + "route.simple": "Простой", + "route.target": "Цель", + "route.targetRole": "цель", + "route.thisDestinationSPlatform": "Площадка этого назначения", + "route.threshold": "Порог", + "route.title": "Routing", + "route.triggerRole": "триггер", + "route.truePeak": "Истинный пик", + "route.video": "видео", + "route.videoLaterThanAudio": "Видео позже звука", + "route.withoutATargetTheMix": "Без целевого значения микс уходит с тем уровнем, который дал вход, — применяется только защита от клиппинга выше.", + "route.youtubeTwitchSpotify": "YouTube, Twitch, Spotify", + "set.acceptSelfSigned": "Принимать самоподписанный сертификат", + "set.accountDisconnected": "Аккаунт отключён.", + "set.adminPassword": "Пароль администратора", + "set.alertDelivery": "Доставка оповещений", + "set.alertDeliveryDesc": "Насколько настойчиво преследуется вебхук, который не отвечает, прежде чем от оповещения откажутся.", + "set.apiTokens": "Токены API", + "set.apiTokensDesc": "Долгоживущие учётные данные для скриптов, Prometheus и всего остального, что не может держать сессию. Отправляйте токен как Authorization: Bearer pmk_…. Токен может всё то же, что и администратор, кроме управления токенами — это остаётся за паролем, поэтому отзыв необратим.", + "set.app": "Приложение", + "set.attemptsBackoff": "Пауза между попытками автоматически растёт до потолка в 30 секунд, так что верхняя граница этого диапазона — это уже несколько минут преследования мёртвой конечной точки. Ограничение и есть суть: бесконечные повторы превращают один мёртвый вебхук в очередь, которая никогда не разгребается.", + "set.attemptsNote": "Включая первую попытку. 1 означает не повторять вовсе.", + "set.attemptsRetryable": "Повторяются только те сбои, которые имеет смысл повторять. Ответ 404 от вебхука, который кто-то удалил, окончателен, и ещё четыре попытки лишь задержат все оповещения в очереди за ним; ответ 502 от обратного прокси — ровно то, ради чего эти попытки и существуют.", + "set.audioMeters": "Индикаторы звука", + "set.brokerPasswordCleared": "Пароль брокера удалён.", + "set.brokerPlainNote": "mqtt:// не шифруется. MQTT передаёт имя пользователя и пароль в пакете CONNECT, поэтому оба идут по сети открытым текстом — нормально в доверенной локальной сети, что является обычным случаем для Home Assistant, и ненормально во всём, что вы не контролируете. Используйте mqtts://, если брокер его поддерживает.", + "set.brokerUrl": "URL брокера", + "set.brokerUrlNote": "mqtt://, mqtts://, ws:// или wss://. Пароль в URL отклоняется: URL попадает в строки журнала и в вывод ps, и это уже не отменить. Введите его в поле ниже, где он хранится в зашифрованном виде.", + "set.brokerUrlPlaceholder": "mqtts://broker.local:8883", + "set.chatBothApply": "Действуют оба, и побеждает более щедрое: сообщение удаляется, когда оно старше указанного числа часов и не входит в N новейших. Поэтому оживлённый канал хранит меньше времени, чем тихий, при тех же настройках.", + "set.chatBufferNote": "«Отправлять при подключении» — величина иной природы, чем две другие. Те ограничивают то, что хранится на диске; эта же — буфер в оперативной памяти, выделяемый на каждую подключённую панель.", + "set.chatHistory": "История чата", + "set.chatHistoryDesc": "Насколько далеко уходит история чата — а вместе с ней и то, сколько видит модератор, открывая чью-то карточку.", + "set.chatHours": "Хранить (часов)", + "set.chatKeep": "Всегда хранить (сообщений)", + "set.chatKeepNote": "Нижняя граница, независимо от возраста.", + "set.chatNotOnlyDisk": "Это не только настройка диска. Щелчок по имени в чате открывает то, что этот человек говорил раньше, и эта история берётся отсюда — ни одна платформа не публикует для неё API, поэтому polyemesis может показать лишь то, что сохранил. Поставьте слишком мало — и тот, кто создаёт проблемы уже неделю, будет выглядеть так, будто не сказал ни слова.", + "set.chatSendNote": "История без запроса.", + "set.chatSendOnConnect": "Отправлять при подключении (сообщений)", + "set.chatSendOnConnectShort": "Отправлять при подключении", + "set.chatSmall": "Чат занимает мало. Канал со средней интенсивностью десять сообщений в секунду хранит примерно 7 МБ в час; большинство и близко к этому не подходят.", + "set.clientId": "Идентификатор клиента", + "set.clientIdNote": "Должен быть уникален на брокере. Совпадение — причина номер один необъяснимого цикла переподключений: брокер при каждом подключении разрывает более старую сессию, и оба клиента переподключаются бесконечно.", + "set.clientIdPlaceholder": "выводится из имени экземпляра", + "set.clientSecret": "Секрет клиента", + "set.comingBack": "Возврат", + "set.confirmPassword": "Подтвердите новый пароль", + "set.connectedAccounts": "Подключённые аккаунты", + "set.copied": "Скопировано", + "set.copy": "Копировать", + "set.couldNotChangeThePassword": "Не удалось изменить пароль.", + "set.couldNotClearThePassword": "Не удалось удалить пароль.", + "set.couldNotCreateTheToken": "Не удалось создать токен.", + "set.couldNotReCheckThe": "Не удалось повторно проверить учётные данные.", + "set.couldNotReadTheTls": "Не удалось прочитать статус TLS.", + "set.couldNotRevokeTheToken": "Не удалось отозвать токен.", + "set.couldNotSaveTheCredentials": "Не удалось сохранить учётные данные.", + "set.couldNotSaveTheMqtt": "Не удалось сохранить настройки MQTT.", + "set.credsRemoved": "Учётные данные удалены.", + "set.currentPassword": "Текущий пароль", + "set.dataDirectory": "Каталог данных", + "set.deliveryAttempts": "Попытки доставки", + "set.dialsOnly": "Сервер подключается только к: {schemes}.", + "set.disconnect": "Отключить", + "set.disconnectDesc": "Назначения, использующие его, сохранят свои настройки, но перестанут обновлять ключ трансляции. Тот же аккаунт можно подключить снова позднее.", + "set.disconnectTitle": "Отключить этот аккаунт?", + "set.drawtextMissing": "В этой сборке нет фильтра drawtext, поэтому текстовые наложения недоступны. Нужен FFmpeg, собранный с libfreetype.", + "set.drawtextOk": "Эта сборка умеет наносить текст на видео.", + "set.enabled": "Включено", + "set.encoderUrl": "URL кодировщика", + "set.expires": "Истекает", + "set.failover": "Резервирование", + "set.failoverDesc": "Резервный вход и заставка на случай, когда живой источник перестаёт отдавать данные. По умолчанию выключено: в выключенном состоянии тракт байт в байт такой же, каким был до появления этой функции.", + "set.failoverRemuxNote": "Включение добавляет один этап ремультиплексирования между входом и всем, что ниже. Дёшево, но не бесплатно, поэтому это решение, а не значение по умолчанию.", + "set.fileSizeMb": "Размер файла (МБ)", + "set.filesKept": "Хранимых файлов", + "set.goingLive": "Выход в эфир", + "set.goingLiveDesc": "Что происходит, когда несколько назначений стартуют одновременно.", + "set.graceNote": "Сколько живой источник может ничего не отдавать до переключения уровня. Намеренно дольше переподключения — кодировщик, восстанавливающий соединение RTMP, это штатная работа, а не отказ.", + "set.gracePeriod": "Льготный период (секунды)", + "set.haDiscovery": "Обнаружение Home Assistant", + "set.height": "Высота", + "set.ingest": "Вход", + "set.ingestDesc": "SRT переносит до шести звуковых дорожек. RTMP переносит одну и существует как запасной вариант для кодировщиков, не умеющих SRT. Забор подключается к источнику сам, вместо того чтобы ждать его.", + "set.ingestUrlCopied": "URL входа скопирован.", + "set.instance": "Экземпляр", + "set.intervalNote": "Неизменившееся состояние повторно не публикуется, поэтому короткий интервал стоит сравнения, а не сообщения. Keep-alive определяет, как долго мёртвая связь остаётся незамеченной — и, следовательно, как быстро брокер сообщит подписчикам, что этот экземпляр исчез.", + "set.issuer": "Издатель", + "set.kbps": "кбит/с", + "set.keepAlive": "Keep-alive (с)", + "set.latencyMs": "Задержка (мс)", + "set.listeners": "Слушатели", + "set.listenersDesc": "Где сервер слушает — для всей установки. Один порт SRT обслуживает все источники: их различают по токену публикации, поэтому добавление источника никогда не означает открытия ещё одного порта. У RTMP такой маршрутизации нет, поэтому его порт обслуживает один источник.", + "set.logsBoundNote": "Каталог журналов ограничен произведением этих двух значений, поэтому сохранение журналов никогда не станет тем, что заполнит диск.", + "set.logsDesc": "Захваченный stderr FFmpeg всегда держится в памяти, но она умирает вместе с сервером — и теряются ровно те строки, которые объясняют, почему он умер. Сохранение зеркалит их в ротируемый файл в каталоге данных.", + "set.metersDesc": "Отдельный вспомогательный процесс измеряет каждый канал каждой дорожки. Меньший интервал обновляет чаще и нагружает процессор сильнее.", + "set.mode": "Режим", + "set.modePull": "Забор — подключиться к камере, потоку или файлу", + "set.modeRtmp": "RTMP — одна дорожка", + "set.modeSrt": "SRT — многодорожечный (рекомендуется)", + "set.mqtt": "Телеметрия MQTT", + "set.mqtt5Only": "Только MQTT 5.0.", + "set.mqtt5OnlyNote": "Брокер, зафиксированный на 3.1.1, вообще не установит соединение — это не режим с урезанными возможностями. Mosquitto 2.x, EMQX и дополнение Home Assistant говорят на 5.0.", + "set.mqttDesc": "Публикует состояние на брокер как retained-сообщения, поэтому потребитель, который не был подключён в момент изменения, всё равно получит актуальный ответ.", + "set.multiAccountNote": "Подключите одну и ту же платформу несколько раз, чтобы вещать на несколько каналов — каждый аккаунт станет отдельным назначением.", + "set.name": "Имя", + "set.newPassword": "Новый пароль", + "set.no": "нет", + "set.noEncryption": "Это соединение ничем не шифруется. Пароли и сессионные cookie идут по сети открытым текстом — задайте tls.mode: auto или привяжитесь к 127.0.0.1 за прокси.", + "set.noLibsrt": "В этой сборке FFmpeg нет поддержки SRT. Установите сборку, настроенную с --enable-libsrt, или используйте RTMP.", + "set.noTokens": "Токенов пока нет.", + "set.passphrase": "Кодовая фраза", + "set.passphraseNote": "Включает шифрование AES. Оставьте пустым, чтобы отключить. SRT отвергает фразы вне диапазона 10–79 символов.", + "set.passphrasePlaceholder": "необязательно — от 10 до 79 символов", + "set.password": "Пароль", + "set.passwordChanged": "Пароль изменён.", + "set.passwordMismatch": "Новые пароли не совпадают.", + "set.passwordNote": "Пароль не возвращается ни одним API, поэтому это поле пусто, даже когда пароль задан. Оставьте пустым, чтобы сохранить записанный пароль; очистите его кнопкой ниже.", + "set.platform": "Платформа", + "set.platformsDesc": "Платформы различаются тем, какую часть этого polyemesis может автоматизировать, и об этой разнице стоит знать до начала, а не час спустя. Само вещание везде работает одинаково — маршрутизация звука по назначениям, варианты, переподключение и индикаторы не зависят ни от одного из столбцов ниже.", + "set.playLoop": "Проигрывать петлю, когда ничего не идёт в эфир", + "set.playLoopNote": "Второй кандидат рядом с заставкой: вместо неподвижного изображения проигрываются файлы. Могут быть включены оба — порядок уровней и то, что победит, смотрите в описании резервирования.", + "set.pointObs": "Направьте OBS на этот адрес.", + "set.preview": "Предпросмотр", + "set.previewDesc": "HLS-монитор панели. Это единственное место, где polyemesis перекодирует видео, и он работает на собственной подписке ретранслятора, поэтому никогда не может повлиять на назначение.", + "set.previewIdleNote": "Кодировщик запускается, когда панель запрашивает плейлист, и снова останавливается через это время после последнего запроса, поэтому закрытая панель не тратит процессор. Изменение этого значения никогда не перезапускает предпросмотр, который кто-то смотрит.", + "set.processLogs": "Журналы процессов", + "set.proxyTerminatesTls": "Обратный прокси перед polyemesis завершает TLS, поэтому этот сервер намеренно отдаёт обычный HTTP.", + "set.publishInterval": "Интервал публикации (с)", + "set.reconnectCap": "Потолок паузы переподключения (с)", + "set.reconnectCapNote": "0 использует значение по умолчанию. Только источники HTTP и HLS.", + "set.redirectNote": "Должен совпадать точно, включая схему и порт. Если вы обращаетесь к polyemesis через обратный прокси, используйте его публичный URL.", + "set.redirectUri": "Redirect URI для внесения в список", + "set.removeCreds": "Удалить учётные данные", + "set.removeCredsDesc": "Каждый аккаунт, подключённый через это приложение, перестанет обновлять токен, и его придётся подключить заново. Секрет клиента отсюда не восстановить — придётся создать новое приложение на платформе.", + "set.removeCredsTitle": "Удалить эти учётные данные разработчика?", + "set.returnAfter": "Вернуться через (секунд стабильности)", + "set.returnAutomatic": "Автоматически — вернуться, как только основной стабилен", + "set.returnManual": "Вручную — оставаться на резерве, пока я не переключу", + "set.returnManualNote": "По умолчанию выбран ручной режим, потому что автоматический возврат может «дребезжать»: основной источник, восстанавливающийся рывками, будет перебрасывать программу туда-сюда прямо в эфире.", + "set.rtmpPort": "Порт RTMP", + "set.rtspNote": "TCP, если камера не вынуждает иное. RTSP поверх UDP через NAT выглядит подключённым и ничего не передаёт.", + "set.rtspTransport": "Транспорт RTSP", + "set.saveAndDrop": "Сохранить и оборвать живой поток", + "set.saveIngest": "Сохранить настройки входа", + "set.segmentSec": "Сегмент (с)", + "set.settingsSavedAffectedProcessesHave": "Настройки сохранены. Затронутые процессы перезапущены.", + "set.showSlate": "Показывать заставку, когда ничего не идёт в эфир", + "set.slateColour": "Цвет заставки", + "set.slateColourNote": "Одно слово, потому что значение попадает в граф фильтров, где запятая завершила бы аргумент. Используется, только если изображение не задано.", + "set.slateColourPlaceholder": "black", + "set.slateImage": "Изображение заставки", + "set.slateImageNote": "Путь внутри каталога данных. Оставьте пустым, чтобы вместо этого залить сплошным цветом — именно это и есть запасной вариант, ведь у цвета нет файла, который может не открыться.", + "set.slateImagePlaceholder": "slate/holding.png", + "set.sourceUrl": "URL источника", + "set.sourceUrlNote": "Один из {schemes}. Источник file:// — это путь ОТНОСИТЕЛЬНО каталога данных, и он не может содержать «..»: он заперт там так же, как загруженный файл.", + "set.sourceUrlPlaceholder": "rtsp://camera.local/stream1", + "set.srtPort": "Порт SRT", + "set.stagger": "Разносить подключения (мс)", + "set.staggerNote": "0 — выключено. Иначе выход в эфир означает, что каждое назначение открывает соединение, согласует TLS и начинает кодировать звук в один и тот же момент — на небольшой машине это момент, когда вероятнее всего теряются кадры, и это ровно тот момент, за которым вы наблюдаете, потому что только что вышли в эфир. Нескольких сотен миллисекунд достаточно.", + "set.staggerReconnectNote": "Переподключение никогда не задерживается. Назначение, отвалившееся в три часа ночи, возвращается немедленно, а не ждёт очереди за уже здоровыми процессами.", + "set.stepByStep": "Пошаговая настройка", + "set.stopAfterIdle": "Остановить после простоя (с)", + "set.streamKey": "Ключ трансляции", + "set.streamKeyNote": "Публикующие должны использовать именно этот путь, чтобы открытый порт не был открытой дверью.", + "set.subject": "Субъект", + "set.synthesiseNote": "Все крупные платформы отклоняют поток без звуковой дорожки. Если это включено, вход только с видео получает одну беззвучную стереодорожку и его назначения стартуют; если выключено — они отказывают с «routing profile selects no audio». Это применяется исключительно к входу, который был ОПРЕДЕЛЁН с нулём дорожек, поэтому никогда не может вытеснить звук, который вы действительно передаёте.", + "set.synthesiseSilent": "Создавать беззвучную дорожку", + "set.syntheticAudio": "Синтетический звук", + "set.syntheticDesc": "Что происходит, когда на входе есть видео и совсем нет звука.", + "set.tabIngest": "Вход", + "set.tabPipeline": "Тракт", + "set.tabPlatforms": "Учётные данные платформ", + "set.tabSecurity": "Безопасность", + "set.title": "Настройки", + "set.tlsAcme": "Let's Encrypt", + "set.tlsAuto": "Авто", + "set.tlsDesc": "Настраивается в config.yaml, а не здесь — TLS должен быть верным до того, как сервер начнёт слушать. Эта карточка сообщает, что из этого получилось.", + "set.tlsManual": "Ручной сертификат", + "set.tlsOff": "Не терминируется здесь", + "set.tlsSelfsigned": "Самоподписанный", + "set.tokenHashNote": "polyemesis хранит только хеш. Если вы его потеряете, отзовите этот токен и создайте другой.", + "set.tokenNamePlaceholder": "prometheus", + "set.tokenRevoked": "Токен {name} отозван.", + "set.topicPrefix": "Префикс топика", + "set.topicPrefixNote": "Разделители в префиксе сохраняются, поэтому home/av означает два уровня. Префикс, начинающийся с $, отклоняется: брокеры их резервируют, и подписчик, использующий #, никогда их не получит — телеметрия успешно опубликуется и окажется невидимой ровно в том представлении, из которого вы стали бы её отлаживать.", + "set.transportSecurity": "Безопасность транспорта", + "set.updateInterval": "Интервал обновления (мс)", + "set.username": "Имя пользователя", + "set.validFor": "Действителен для", + "set.whatPlatformsDo": "Что умеет каждая платформа", + "set.whyDevApp": "Зачем нужно собственное приложение разработчика", + "set.whyDevAppDesc": "polyemesis не может поставлять клиентские секреты OAuth — они оказались бы у каждого, у кого есть бинарник, и платформы их отозвали бы. Вы один раз регистрируете бесплатное приложение разработчика, вставляете сюда идентификатор и секрет клиента, и дальше polyemesis сам получает за вас ключи трансляции.", + "set.writeLogs": "Записывать журналы на диск", + "set.yes": "да", + "sources.addDescription": "Вторая программа с собственным входом, назначениями и вариантами — например, вертикальный холст рядом с горизонтальным. Она запускается на портах по умолчанию, которые стоит изменить, чтобы они не конфликтовали.", + "sources.addFailed": "Не удалось добавить источник", + "sources.addSource": "Добавить источник", + "sources.addTitle": "Добавить источник", + "sources.added": "«{name}» добавлен. Задайте порты входа ниже.", + "sources.app": "Приложение", + "sources.applyAndDrop": "Применить и оборвать поток", + "sources.copied": "{what} скопировано", + "sources.copyFailed": "Не удалось скопировать: {what}", + "sources.copyTokenAria": "Скопировать токен", + "sources.copyUrlAria": "Скопировать URL {proto}", + "sources.default": "по умолчанию", + "sources.defaultDescription": "Запросы, не указывающие источник, действуют на этот.", + "sources.deleteAria": "Удалить {name}", + "sources.deleteConfirm": "Удалить источник", + "sources.deleteDescription": "Его назначения и варианты будут удалены вместе с ним — они описывают, куда идёт эта программа, и без неё не имеют смысла. Записи сохраняются: файлы остаются на диске и по-прежнему воспроизводятся, они лишь перестают относиться к источнику.", + "sources.deleteFailed": "Не удалось удалить источник", + "sources.deleteThis": "Удалить этот источник", + "sources.deleteTitle": "Удалить «{name}»?", + "sources.deleted": "«{name}» и его назначения удалены", + "sources.destinations": "Назначения", + "sources.dirtyIdle": "Несохранённые изменения входа. Применение перезапустит вход этого источника.", + "sources.dirtyPublishing": "На этот источник публикует кодировщик. Применение перезапустит его вход и отключит всех зрителей.", + "sources.enableAria": "Включить {name}", + "sources.help.app": "Путь приложения RTMP — часть URL между хостом и ключом трансляции. Большинство кодировщиков ожидают «live». Меняйте только при наличии причины.", + "sources.help.ingest": "Как ваш кодировщик достигает этого источника. SRT выдерживает соединение с потерями, переотправляя пропавшее, и это правильный выбор по умолчанию через публичный интернет. RTMP старше и поддерживается повсеместно, но сильно деградирует на нестабильном канале. В режиме забора polyemesis сам забирает поток с указанного вами URL, вместо того чтобы ждать публикации.", + "sources.help.latencyMs": "Сколько SRT будет ждать запоздавший или переотправленный пакет, прежде чем отказаться от него. Более высокие значения переживают худшие соединения ценой задержки: около 2,5× времени кругового обхода — хорошая отправная точка, то есть при RTT 40 мс здесь нужно примерно 100 мс. Слишком низкое значение превращает короткий сбой в заметный артефакт.", + "sources.help.passphrase": "Шифрует соединение SRT, и сегодня именно это реально не пускает посторонних на этот вход. От 10 до 79 символов. Оставьте пустым — и публиковать сможет любой, кто дотянется до порта.", + "sources.help.streamKey": "Секрет, который кодировщик отправляет для публикации по RTMP. Это учётные данные, защищающие данный вход, поэтому обращайтесь с ними как с паролем: любой, у кого он есть, может вещать на ваши назначения.", + "sources.help.token": "Идентифицирует этот источник, когда несколько используют один порт входа. Управляет ли он также доступом, зависит от режима входа — примечание ниже говорит, что верно сейчас. Обновление выдаёт новый токен и сохраняет старый действующим пять минут, чтобы работающий кодировщик не был отключён посреди эфира.", + "sources.ingest": "Вход", + "sources.lastSourceTitle": "Последний источник удалить нельзя — установке нужен один вход", + "sources.latencyMs": "Задержка (мс)", + "sources.loadFailed": "Не удалось загрузить источники", + "sources.loading": "Загрузка источников…", + "sources.loss": "потери", + "sources.modePull": "Забор", + "sources.name": "Название", + "sources.namePlaceholder": "Вертикальный", + "sources.noEngine": "Для этого источника не запущен движок. Обычная причина — порт входа уже занят другим источником или другим процессом.", + "sources.notRunning": "не работает", + "sources.ownDescription": "Собственные назначения, варианты и записи.", + "sources.passphrase": "Кодовая фраза", + "sources.passphrasePlaceholder": "10–79 символов", + "sources.publishing": "публикует", + "sources.renditions": "Варианты трансляции", + "sources.retrans": "переотпр.", + "sources.rotate": "Обновить", + "sources.rotateConfirm": "Обновить токен", + "sources.rotateFailed": "Не удалось обновить токен", + "sources.rotateIdle": "Новый токен выдаётся немедленно. Старый продолжает работать пять минут, чтобы уже использующий его кодировщик не был отключён.", + "sources.rotateTitle": "Обновить токен для «{name}»?", + "sources.rotateWhilePublishing": "Сейчас на этот источник публикует кодировщик. Старый токен продолжит работать пять минут, поэтому соединение не оборвётся — но его нужно обновить до истечения этого срока.", + "sources.rtt": "RTT", + "sources.running": "работает", + "sources.saveFailed": "Не удалось сохранить", + "sources.streamKey": "Ключ трансляции", + "sources.subtitle": "Каждый — одна входящая программа. Назначения и варианты принадлежат источнику.", + "sources.title": "Источники", + "sources.token": "токен", + "sources.tokenEnforcedDetailRtmp": "Это ключ трансляции: публикуйте на указанный выше URL, используя его как ключ. Ротация выдаёт новый и сохраняет старый рабочим пять минут, так что можно переключиться без обрыва эфира.", + "sources.tokenEnforcedDetailSrt": "Укажите его в поле SRT streamid вашего кодировщика, чтобы публиковать в этот источник через общий порт. Ротация выдаёт новый и сохраняет старый рабочим пять минут, так что можно переключиться без обрыва эфира.", + "sources.tokenIsCredential": "Этот токен и есть учётные данные.", + "sources.tokenIssued": "Выдан новый токен", + "sources.tokenNotEnforced": "Этот токен сохранён, но сейчас не применяется.", + "sources.tokenNotEnforcedRtmp": "Источники разделяются по порту. Этот вход на самом деле защищает ключ трансляции выше. Включите однопортовый вход в Настройках, чтобы токен стал учётными данными.", + "sources.tokenNotEnforcedSrt": "Источники разделяются по порту. Этот вход на самом деле защищает кодовая фраза SRT выше. Включите однопортовый вход в Настройках, чтобы токен стал учётными данными.", + "state.disabled": "Отключено", + "state.failed": "Ошибка", + "state.live": "Прямой эфир", + "state.offline": "Офлайн", + "state.reconnecting": "Повторное подключение", + "state.starting": "Запуск", + "state.stopped": "Остановено" } diff --git a/ui/src/lib/i18n/tr.json b/ui/src/lib/i18n/tr.json index 537c39a0..622d0ec6 100644 --- a/ui/src/lib/i18n/tr.json +++ b/ui/src/lib/i18n/tr.json @@ -1,26 +1,227 @@ { - "nav.dashboard": "Kontrol Paneli", - "nav.sources": "Kaynaklar", - "nav.meters": "Ses seviye göstergeleri", - "nav.routing": "Routing", - "nav.renditions": "Çözünürlükler", - "nav.playout": "Playout", - "nav.library": "Kütüphane", - "nav.clips": "Klip", - "nav.chat": "Sohbet", - "nav.jobs": "İşlemler", - "nav.automation": "Otomasyon", - "nav.recordings": "Kayıtlar", - "nav.monitoring": "İzleme", - "nav.settings": "Ayarlar", - "chrome.toggleNav": "Navigasyonu aç/kapat", + "auth.accountCreated": "Yönetici hesabı oluşturuldu.", + "auth.confirmPassword": "Parolayı doğrula", + "auth.createAccount": "Hesap oluştur", + "auth.firstRun": "İlk çalıştırma kurulumu", + "auth.firstRunDesc": "Bu sunucu için yönetici kimlik bilgilerini seçin. Hesap kurtarma yoktur, bu yüzden onları güvenli bir yerde saklayın.", + "auth.password": "Parola", + "auth.passwordMismatch": "Parolalar eşleşmiyor.", + "auth.signIn": "Oturum aç", + "auth.signInDesc": "Yönetici kimlik bilgilerinizi girin.", + "auth.somethingWentWrong": "Bir şeyler ters gitti.", + "auth.tokenCreated": "API belirteci oluşturuldu", + "auth.tokenRevoked": "API belirteci iptal edildi", + "auth.username": "Kullanıcı adı", + "auto.action": "Eylem", + "auto.adminPasswordChanged": "Yönetici parolası değiştirildi", + "auto.chosenWeekdays": "Seçilen hafta günleri", + "auto.coalesced": "Birleştirilmiş", + "auto.coalescingWindow": "Birleştirme penceresi (saniye)", + "auto.couldNotChangeTheRule": "Kural değiştirilemedi.", + "auto.couldNotChangeTheSchedule": "Zamanlama değiştirilemedi.", + "auto.couldNotDeleteTheRule": "Kural silinemedi.", + "auto.couldNotDeleteTheSchedule": "Zamanlama silinemedi.", + "auto.couldNotLoadAutomationSettings": "Otomasyon ayarları yüklenemedi.", + "auto.couldNotSaveTheRule": "Kural kaydedilemedi.", + "auto.couldNotSaveTheSchedule": "Zamanlama kaydedilemedi.", + "auto.createRule": "Kural oluştur", + "auto.createSchedule": "Zamanlama oluştur", + "auto.days": "Günler", + "auto.delete": "Sil", + "auto.deleteRule": "Kuralı sil", + "auto.deleteRuleDesc": "Kural artık tetiklenmez. Webhook uç noktasına dokunulmaz ve kuralı yeniden oluşturabilirsiniz.", + "auto.deleteRuleTitle": "Bu uyarı kuralı silinsin mi?", + "auto.deleteSchedule": "Zamanlamayı sil", + "auto.deleteScheduleDesc": "Zamanlama artık çalışmaz. Halihazırda başlattığı hiçbir şey etkilenmez.", + "auto.deleteScheduleTitle": "Bu zamanlama silinsin mi?", + "auto.delivery": "Teslim", + "auto.destinationFallingBehindRealtime": "Hedef gerçek zamanın gerisine düşüyor", + "auto.destinationKeepingUpAgain": "Hedef yeniden yetişiyor", + "auto.destinationRecovered": "Hedef toparlandı", + "auto.destinationWentDown": "Hedef düştü", + "auto.destinations": "Hedefler", + "auto.dropped": "Düşürüldü", + "auto.edit": "Düzenle", + "auto.editRule": "Uyarı kuralını düzenle", + "auto.editSchedule": "Zamanlamayı düzenle", + "auto.endpoint": "Uç nokta", + "auto.evAudioClipping": "Ses kırpılması", + "auto.evClipCaptured": "Klip yakalandı", + "auto.evDiskLow": "Disk azalıyor", + "auto.evDiskRecovered": "Disk toparlandı", + "auto.evIngestLost": "Giriş kaybedildi", + "auto.evIngestRecovered": "Giriş geri geldi", + "auto.evSettingsChanged": "Ayarlar değiştirildi", + "auto.evSignedIn": "Oturum açıldı", + "auto.events": "Olaylar", + "auto.everyDay": "Her gün", + "auto.failed": "Başarısız", + "auto.failoverSwitchedSource": "Yük devretme kaynağı değiştirdi", + "auto.fired": "tetiklendi", + "auto.firesAt": "Tetiklenme saati", + "auto.lastDelivery": "Son teslim", + "auto.lastSweep": "Son tarama", + "auto.latenessAllowance": "Gecikme toleransı (saniye)", + "auto.loudnessBackInCompliance": "Ses yüksekliği yeniden uyumlu", + "auto.loudnessOutOfCompliance": "Ses yüksekliği uyumsuz", + "auto.minGap": "Mesajlar arasındaki en az aralık (saniye)", + "auto.minSeverity": "En düşük önem düzeyi", + "auto.missedNote": "Sunucu kapalı olduğu için kaçırılan bir tekrar, geç tetiklenmek yerine işlenmiş sayılıp atlanır — kendini dört saat geç başlatan bir yayın, hiç başlamamış olandan daha kötüdür.", + "auto.name": "Ad", + "auto.newRule": "Yeni uyarı kuralı", + "auto.newSchedule": "Yeni zamanlama", + "auto.next": "Sonraki", + "auto.nothingFired": "Henüz hiçbir şey tetiklenmedi. Zamanlayıcı her 20 saniyede bir tarama yapar.", + "auto.once": "Bir kez", + "auto.payloadFormat": "Payload biçimi", + "auto.pending": "Beklemede", + "auto.repeatedFailedSignIns": "Tekrarlanan başarısız oturum açma denemeleri", + "auto.repeats": "Yineleme", + "auto.retries": "Yeniden denemeler", + "auto.rule": "Kural", + "auto.ruleCreated": "Uyarı kuralı oluşturuldu. Ona bir test gönderin.", + "auto.ruleDeleted": "Uyarı kuralı silindi.", + "auto.rulePlaceholder": "Discord — yayın uyarıları", + "auto.ruleSaved": "Uyarı kuralı kaydedildi.", + "auto.saveRule": "Kuralı kaydet", + "auto.saveSchedule": "Zamanlamayı kaydet", + "auto.schedule": "Zamanlama", + "auto.scheduleCreated": "Zamanlama oluşturuldu.", + "auto.scheduleDeleted": "Zamanlama silindi.", + "auto.schedulePlaceholder": "Hafta içi akşam yayını", + "auto.scheduleSaved": "Zamanlama kaydedildi.", + "auto.schedules": "Zamanlamalar", + "auto.secretMasked": "Gizli anahtar yolun içinde yer aldığı için maskelenmiş gösterilir. Mevcut uç noktayı korumak için ona dokunmayın ya da değiştirmek için yeni bir URL yapıştırın.", + "auto.sendTest": "Test uyarısı gönder", + "auto.sendTestTitle": "Şimdi bir test uyarısı gönder", + "auto.sent": "Gönderildi", + "auto.skipped": "atlandı", + "auto.startDestinations": "Hedefleri başlat", + "auto.startPlaylist": "Çalma listesini başlat", + "auto.stopDestinations": "Hedefleri durdur", + "auto.stopPlaylist": "Çalma listesini durdur", + "auto.subscribedTo": "Abone olunan", + "auto.subtitle": "Bir şeyin bozulduğunu size bildiren webhook'lar ve yayını siz olmadan yürüten zamanlamalar.", + "auto.theEndpointDidNotAccept": "Uç nokta test iletisini kabul etmedi.", + "auto.time": "Saat", + "auto.timeZone": "Saat dilimi", + "auto.title": "Otomasyon", + "auto.tzPlaceholder": "UTC", + "auto.webhookHint": "Discord da Slack da kanal ayarları altında bir tane verir.", + "auto.webhookPlaceholder": "https://discord.com/api/webhooks/…", + "auto.webhookRules": "Webhook kuralları", + "auto.webhookUrl": "Webhook URL'si", + "auto.weekFri": "Cum", + "auto.weekMon": "Pzt", + "auto.weekSat": "Cmt", + "auto.weekSun": "Paz", + "auto.weekThu": "Per", + "auto.weekTue": "Sal", + "auto.weekWed": "Çar", + "auto.when": "Ne zaman", + "chatpage.allFiltered": "İletisi olan her platform filtrelenmiş durumda.", + "chatpage.couldNotDeleteThatMessage": "Bu ileti silinemedi.", + "chatpage.deleteNote": "Silmek, iletiyi onu yayınlayan platformda kaldırır. polyemesis'in silemediği bir platform bunu söyler ve bunun nerede yapılabileceğini belirtir — eylem asla tahmine dayanarak gizlenmez.", + "chatpage.droppedTitle": "Canlı olarak gösterildi, ancak kalıcı kayıt geride kaldı ve geçmiş bunları yitirdi.", + "chatpage.dupTitle": "Aynı ileti iki kez görüldü — kendi yankımız ya da platformun yeniden göndermesi.", + "chatpage.noAccount": "Sohbet çalışıyor, ancak bağlı bir platform hesabı yok. Ayarlar → Platform kimlik bilgileri altından bir tane bağlayın.", + "chatpage.notRunning": "Bu sunucuda sohbet çalışmıyor. Zaman çizelgesinin altındaki geçmiş, daha önce saklanmış olandır ve bir platform bağlanana dek gönderme kutusu devre dışıdır.", + "chatpage.received": "Alınan", + "chatpage.reload": "Yeniden yükle", + "chatpage.stored": "Saklanan", + "chatpage.storedTitle": "Hiçbir şey bağlı değil; bu saklanan geçmiştir.", + "chatpage.subtitle": "Tüm platformlar tek bir zaman çizelgesinde, tek bir gönderme kutusuyla.", + "chatpage.timeline": "Zaman çizelgesi", + "chatpage.title": "Sohbet", "chrome.ingest": "Ingest", + "chrome.language": "Dil", "chrome.liveCount": "{count} canlı", + "chrome.signOut": "Oturumu kapat", "chrome.socketOffline": "çevrimdışı", "chrome.socketOfflineHint": "Canlı güncellemeler bağlantısı kesildi; yeniden deneniyor", - "chrome.signOut": "Oturumu kapat", - "chrome.language": "Dil", + "chrome.toggleNav": "Navigasyonu aç/kapat", + "clipedit.back33": "33 ms geri", + "clipedit.clickLine": "Oraya atlamak için bir satıra tıklayın. Birkaçını birden seçtiğinizde giriş ve çıkış noktaları vurguladığınız sözcüklere taşınır.", + "clipedit.clipThisLine": "Bu satırdan klip al", + "clipedit.clipThisLineTitle": "Giriş ve çıkış noktalarını bu satırın çevresine ayarla", + "clipedit.copied": "Kopyalandı", + "clipedit.copiesEveryPacketSecondsBit": "Her paketi kopyalar. Saniyeler, bit birebir aynı ve başlangıç en yakın anahtar kareye geri oturur.", + "clipedit.couldNotOpenThatRecording": "Bu kayıt açılamadı.", + "clipedit.couldNotQueueTheExport": "Dışa aktarma kuyruğa alınamadı.", + "clipedit.cut": "Kesit", + "clipedit.downloadClip": "Klibi indir", + "clipedit.exportQueued": "Dışa aktarma kuyruğa alındı. Makine kaynak ayırabildiğinde çalışır.", + "clipedit.exports": "Dışa aktarmalar", + "clipedit.fast": "Hızlı", + "clipedit.filterTranscript": "Dökümü filtrele", + "clipedit.findSaid": "Söylenenleri bul", + "clipedit.forward33": "33 ms ileri", + "clipedit.in": "Giriş", + "clipedit.keepAllTracks": "Her ses parçasını koru", + "clipedit.keepsEveryAudioTrackIn": "Her ses parçasını, kodeki ne olursa olsun korur. Kaydedicinin yazdığı şey.", + "clipedit.markIn": "I — oynatma başlığında giriş işaretle", + "clipedit.markOut": "O — oynatma başlığında çıkış işaretle", + "clipedit.name": "Ad", + "clipedit.noKeyframes": "Anahtar kare konumları okunamadı, bu yüzden bu kesitin gerçekte nerede başlayacağını kimse söyleyemez. Yine de çalışır — FFmpeg'in kendi arama işlevi bir anahtar kareye iner — ancak başlangıç bir GOP kadar erken olabilir ve bu sayfa size ne kadar olduğunu gösteremez.", + "clipedit.noTranscript": "Bu kayıt için henüz hiçbir şeyin dökümü çıkarılmadı. Her mikrofon kendi parçasına kaydedildi, bu yüzden dökümünü çıkarmak size konuşmacı ayrıştırma modeli olmadan konuşmacı adlarını verir — sonra da söylenenlere göre klip alabilirsiniz.", + "clipedit.nothingExported": "Henüz hiçbir şey dışa aktarılmadı.", + "clipedit.out": "Çıkış", + "clipedit.pickAtLeastOneAudio": "En az bir ses parçası seçin ya da hepsini alın.", + "clipedit.playFaster": "L — oynat, hızlanmak için tekrar", + "clipedit.precise": "Hassas", + "clipedit.queueNote": "Dışa aktarma burada çalıştırılmaz, kuyruğa alınır: canlı yayına öncelik verir, böylece yayın sırasında alınan bir kesit, yayının ihtiyaç duyduğu kaynakları çalmak yerine bekler.", + "clipedit.reEncodesOnlyTheLeading": "Yalnızca baştaki eksik GOP'u yeniden kodlar, böylece başlangıç tam olur. Gerisi yine kopyalanır.", + "clipedit.reencoded": "Yeniden kodlandı", + "clipedit.shortcuts": "J / K / L mekik yapar, I ve O giriş ile çıkışı işaretler, oklar 33 ms, shift-oklar bir saniye kaydırır. İnce adım 30 fps'de bir karedir; bu kaydın gerçek hızı dizinlenmediğinden tahmin edilmek yerine milisaniye cinsinden adlandırılmıştır.", + "clipedit.shuttleBack": "J — geriye doğru mekik", + "clipedit.startMoved": "Başlangıç kayması", + "clipedit.starts": "Başlıyor", + "clipedit.stop": "K — durdur", + "clipedit.thatCutCouldNotBe": "Bu kesit planlanamadı.", + "clipedit.thatIsNotARecording": "Bu bir kayıt kimliği değil.", + "clipedit.thatRecordingIsNotAvailable": "Bu kayıt kullanılamıyor.", + "clipedit.title": "Klip düzenleyici", + "clipedit.whatASocialPlatformWill": "Bir sosyal platformun kabul edeceği şey. Bundan daha az kodek sağ çıkar.", + "clipedit.zoomIn": "Yakınlaştır", + "clipedit.zoomOut": "Uzaklaştır", + "clips.bitrate": "Bit hızı", + "clips.buffer": "Tampon", + "clips.bufferLiveNote": "Tampon anahtarı canlı bir açma-kapamadır, kaydedilen bir ayar değil: yeniden başlatmanın ardından yine kapalıdır.", + "clips.captured": "Yakalandı", + "clips.ceiling": "Tavan", + "clips.clipsKept": "Saklanan klipler", + "clips.couldNotCaptureAClip": "Klip yakalanamadı.", + "clips.couldNotChangeTheClip": "Klip tamponu değiştirilemedi.", + "clips.couldNotDeleteTheClip": "Klip silinemedi.", + "clips.couldNotLoadClips": "Klipler yüklenemedi.", + "clips.covers": "Kapsam", + "clips.customLength": "Saniye cinsinden özel klip uzunluğu", + "clips.delete": "Sil", + "clips.deleteDescription": "Klip genellikle tam da saklamak istediğiniz şeydir. Bu işlem dosyayı kaldırır; klibin alındığı kayda dokunulmaz.", + "clips.deleteTitle": "Bu klip silinsin mi?", + "clips.deleted": "Klip silindi.", + "clips.download": "İndir", + "clips.everythingHeld": "Tamponun tuttuğu her şey", + "clips.file": "Dosya", + "clips.held": "Tutulan", + "clips.inMemory": "Bellekte", + "clips.keepBuffer": "Kayan tampon tut", + "clips.keyframes": "Anahtar kareler", + "clips.length": "Uzunluk", + "clips.none": "yok", + "clips.off": "kapalı", + "clips.onDisk": "Diskte", + "clips.seen": "algılandı", + "clips.settings": "Ayarlar", + "clips.size": "Boyut", + "clips.subtitle": "Az önce yayınlananların kayan tamponu. Olay olduktan sonra yakalayın — önceden hiçbir şey hazırlamanız gerekmez.", + "clips.theClipDoesNotStart": "Klip bir anahtar karede başlamıyor.", + "clips.title": "Klipler", + "clips.waitingForStream": "yayın bekleniyor", + "clips.window": "Pencere", + "clips.windowSeconds": "Pencere (saniye)", "common.add": "Ekle", + "common.apply": "Uygula", "common.cancel": "İptal", "common.close": "Kapat", "common.confirm": "Onayla", @@ -28,110 +229,921 @@ "common.copy": "Kopyala", "common.delete": "Sil", "common.disabled": "Devre Dışı", + "common.discard": "Vazgeç", "common.edit": "Düzenle", "common.enabled": "Etkin", "common.loading": "Yükleniyor…", + "common.moreInfo": "Daha fazla bilgi", "common.refresh": "Yenile", "common.restart": "Yeniden Başlat", "common.retry": "Tekrar Dene", "common.save": "Kaydet", "common.start": "Başlat", "common.stop": "Durdur", - "state.live": "Canlı", - "state.reconnecting": "Yeniden bağlanılıyor", - "state.starting": "Başlatılıyor", - "state.failed": "Başarısız", - "state.stopped": "Durduruldu", - "state.offline": "Çevrimdışı", - "hooks.title": "Yaşam döngüsü webhook’ları", - "hooks.new": "Yeni webhook", - "hooks.empty": "Webhook yok. Bir tane ekleyin; yayın başladığı ya da durduğu anda veya bir hedef ayağa kalktığında ya da düştüğünde polyemesis imzalı bir JSON gövdesini POST eder — geçiş başına tek teslim, sırayla, bir betiğin işleyebilmesi için.", - "hooks.colWebhook": "Webhook", - "hooks.colEndpoint": "Uç nokta", - "hooks.colTriggers": "Abone olduğu", - "hooks.colEnabled": "Açık", - "hooks.signed": "imzalı", - "hooks.unsigned": "İMZASIZ", + "dash.audioTracks": "Ses parçaları", + "dash.bitrate": "Bit hızı", + "dash.broadcastSettings": "Yayın ayarları", + "dash.categoryPlaceholder": "Müzik", + "dash.clipboardUnavailable": "Bu köken üzerinde pano kullanılamıyor.", + "dash.connectPrompt": "Yayın başlığınızı, açıklamanızı ve kategorinizi tüm platformlarda tek seferde ayarlamak için Ayarlar → Platform kimlik bilgileri altından bir YouTube, Twitch, Facebook veya Kick hesabı bağlayın. Her biri farklı bir alt kümeyi kabul eder — form hangilerinin kabul edildiğini belirtir ve gönderebildiğini gönderir.", + "dash.copyIngestUrl": "Giriş URL'sini kopyala", + "dash.couldNotLoadTheDestination": "Hedef yüklenemedi.", + "dash.couldNotPushTheMetadata": "Meta veriler gönderilemedi.", + "dash.couldNotReorderTheDestinations": "Hedefler yeniden sıralanamadı.", + "dash.deleteDest": "Hedefi sil", + "dash.deleteDestDescription": "Bu işlem hedefi ve yönlendirme profilini kaldırır. Platforma dokunulmaz ve hedefi geri ekleyebilirsiniz — ancak onun için kurduğunuz ses yönlendirmesi kaybolur.", + "dash.descriptionPlaceholder": "Bu yayının ne olduğu.", + "dash.destDeleted": "Hedef silindi.", + "dash.disabled": "devre dışı", + "dash.everyConnectedAccount": "bağlı her hesap", + "dash.idle": "boşta", + "dash.ingestUrlCopied": "Giriş URL'si kopyalandı.", + "dash.keyRefreshed": "Yayın anahtarı platformdan yenilendi.", + "dash.leaveUnchanged": "Değiştirmeden bırak", + "dash.leaveUnchangedNote": "“Değiştirmeden bırak” hiçbir şey göndermez, böylece platform elindekini korur. Açık ya da Kapalı seçmek o değeri yazar.", + "dash.metaCategory": "Kategori", + "dash.metaDescription": "Açıklama", + "dash.metaTags": "Etiketler", + "dash.metaTitle": "Başlık", + "dash.noDestinations": "Henüz hedef yok. Bir tane ekleyin, sonra hangi ses parçalarını alacağını seçin.", + "dash.off": "Kapalı", + "dash.pipeline": "İşlem hattı", + "dash.pushToPlatforms": "Platformlara gönder", + "dash.pushing": "Gönderiliyor…", + "dash.reconnects": "Yeniden bağlanmalar", + "dash.relaySubscribers": "Aktarım aboneleri", + "dash.renditions": "Sürümler", + "dash.scheduledStart": "Planlanan başlangıç", + "dash.subtitle": "Bir kez al, paylaşılan video ve hedef başına sesle dağıt.", + "dash.tagsPlaceholder": "canlı, house, dj set", + "dash.theConnectedAccount": "bağlı hesap", + "dash.title": "Kontrol Paneli", + "dash.titlePlaceholder": "Cuma gecesi seti", + "dash.uptime": "Çalışma süresi", + "dash.youtubeStopsAcceptingTheseOnce": "Bir yayın “created” veya “ready” durumundan çıktığında YouTube bunları kabul etmeyi bırakır.", + "dest.audience": "Kitle", + "dest.audioBitrate": "Ses bit hızı", + "dest.audioCodec": "Ses kodeği", + "dest.channels": "Kanallar", + "dest.compliance": "Uygunluk", + "dest.connectedAccount": "Bağlı hesap", + "dest.contentLabels": "İçerik etiketleri", + "dest.copySource": "Kaynak videoyu kopyala", + "dest.createVariant": "Bu varyantı oluştur", + "dest.customiseSummary": "Bu hedef için özelleştir", + "dest.fbBackupCost": "Bu hedefin yükleme bant genişliğini iki katına çıkarır.", + "dest.fbBackupHelp": "Bu yayının ikinci bir kopyasını Facebook’un yedek girişine gönderir, böylece kopan bir bağlantı yayını düşürmez.", + "dest.fbBackupLabel": "Yedek yayın gönder", + "dest.fbBackupReconnect": "Açmak yayını bir kez yeniden bağlar — bu yüzden canlıya çıkmadan önce etkinleştirin.", + "dest.fbBoxIntro": "Kitle ile aynı oluşturma çağrısında gönderilir, dolayısıyla aynı kural geçerlidir: yayın başladığında bir kez uygulanır ve boşsa tamamen dokunulmadan bırakılır.", + "dest.fbBoxTitle": "Facebook çapraz paylaşımı ve bağış düğmesi", + "dest.fbCrosspostAdd": "Sayfa ekle", + "dest.fbCrosspostAlsoPost": "Ayrıca paylaş", + "dest.fbCrosspostHelp": "Facebook’un kendi konsolundaki sayısal Sayfa kimliği (Sayfa ayarları veya Sayfa olarak oturum açmışken graph.facebook.com/me/accounts) — burada arama yoktur, bu yüzden ad veya URL yerine kimliği yapıştırın. “Ayrıca paylaş”, yayını yalnızca o Sayfayla paylaşmak yerine o Sayfa olarak yayınlar; kapalıyken yalnızca sessiz paylaşım gerçekleşir.", + "dest.fbCrosspostLabel": "Sayfalara çapraz paylaşım", + "dest.fbCrosspostPageId": "Sayfa kimliği", + "dest.fbCrosspostRemove": "Bu Sayfayı kaldır", + "dest.fbDonateHelp": "Bu da Facebook’un bağış kampanyası konsolundan gelen anlaşılmaz bir kimliktir, aranacak bir kurum adı değil. Hiç bağış düğmesi eklenmemesi için boş bırakın.", + "dest.fbDonateLabel": "Bağış düğmesi hayır kurumu kimliği", + "dest.fbDonatePlaceholder": "Hayır kurumu kimliği", + "dest.fillFromPlatform": "Bu platformun yayınladığı değerlerle doldur", + "dest.friends": "Arkadaşlar", + "dest.friendsOfFriends": "Arkadaşların arkadaşları", + "dest.giveUpAfter": "Şu kadar denemeden sonra vazgeç", + "dest.leaveOnFacebook": "Facebook’ta olduğu gibi bırak", + "dest.leaveOnYouTube": "YouTube’da olduğu gibi bırak", + "dest.madeForKids": "Çocuklar için yapıldı (COPPA)", + "dest.muxQueue": "Çoklama kuyruğu (paket)", + "dest.muxQueuePair": "Bu ikisi bir çifttir: FFmpeg paket sınırını yalnızca kuyruk bayt eşiğini aştıktan sonra uygular, bu yüzden tek başına bir eşik hiçbir şey yapmaz ve reddedilir. Bir hedef araya ekleme hataları bildirirse ikisini de artırın — buradaki ses yolu değişken gecikmeye sahiptir, çünkü ses yüksekliği normalleştirmesi ileriyi okur.", + "dest.name": "Ad", + "dest.namePlaceholder": "YouTube — ana kanal", + "dest.notLinked": "Bağlı değil", + "dest.onlyMe": "Yalnızca ben", + "dest.platform": "Platform", + "dest.private": "Özel", + "dest.public": "Herkese açık", + "dest.reconnecting": "Yeniden bağlanma", + "dest.retryAfter": "Şu kadar sonra yeniden dene (saniye)", + "dest.sharedEncodeAria": "Paylaşılan video kodlaması", + "dest.socketTimeout": "Soket zaman aşımı (saniye)", + "dest.streamKeyLabel": "Yayın anahtarı", + "dest.transport": "Taşıma", + "dest.transportFile": "Yerel dosya", + "dest.transportRtmp": "RTMP / RTMPS", + "dest.unlisted": "Liste dışı", + "dest.urlFromDashboard": "Panodan alınan URL", + "dest.useSharedEncode": "Paylaşılan bir video kodlaması kullan", + "dest.varBitrate": "Video bit hızı (kbps)", + "dest.varFps": "Kare hızı", + "dest.varHeight": "Yükseklik", + "dest.varSourceHint": "kaynak", + "dest.varWidth": "Genişlik", + "dest.variantWarning": "Kaynağınızdan “{name}” temel alınarak ikinci bir kodlama başlatır. Bu ücretsiz bir varyasyon değildir ve o kodlamadaki diğer hedeflerin aldığı içeriği değiştirmez. Kaynağın değerini korumak için alanı boş bırakın.", + "dest.videoTreatment": "Video işleme", + "dest.visibility": "Görünürlük", + "hooks.allTriggers": "her şey", "hooks.attempts": "{count} deneme", + "hooks.attemptsLabel": "Denemeler", "hooks.attemptsOne": "{count} deneme", - "hooks.timeout": "{seconds}s zaman aşımı", - "hooks.allTriggers": "her şey", - "hooks.triggerCount": "{count} tetikleyici", - "hooks.triggerCountOne": "{count} tetikleyici", - "hooks.enableAria": "{name} etkinleştir", + "hooks.colEnabled": "Açık", + "hooks.colEndpoint": "Uç nokta", + "hooks.colTriggers": "Abone olduğu", + "hooks.colWebhook": "Webhook", + "hooks.create": "Oluştur", + "hooks.created": "Webhook oluşturuldu.", + "hooks.deleteBody": "Hemen teslim etmeyi bırakır. Uç noktaya dokunulmaz ve hook’u yeniden oluşturabilirsiniz — ama imzalama anahtarı geri getirilemez.", + "hooks.deleteConfirm": "Webhook’u sil", + "hooks.deleteFailed": "Hook silinemedi.", + "hooks.deleteTitle": "Bu webhook silinsin mi?", + "hooks.deleted": "Webhook silindi.", "hooks.deliveries": "Son teslimler", - "hooks.sendTest": "Test teslimi gönder", - "hooks.sendTestTitle": "Şimdi bir test teslimi gönder", - "hooks.secretTitle": "İmzalama anahtarı — şimdi kopyalayın, bir daha gösterilemez", - "hooks.secretCopied": "Kopyaladım", - "hooks.httpStatus": "HTTP {status}", - "hooks.noResponse": "yanıt yok", + "hooks.deliveriesFailed": "Son teslimler okunamadı.", + "hooks.deliveryTitle": "Teslim", + "hooks.dialogIntro": "Geçiş başına bir imzalı POST, sırayla. Bir uyarıdan farklı olarak hiçbir şey birleştirilmez veya bastırılmaz — bir betik her kenarı alır.", "hooks.durationMs": "{ms} ms", + "hooks.editTitle": "Webhook’u düzenle", + "hooks.empty": "Webhook yok. Bir tane ekleyin; yayın başladığı ya da durduğu anda veya bir hedef ayağa kalktığında ya da düştüğünde polyemesis imzalı bir JSON gövdesini POST eder — geçiş başına tek teslim, sırayla, bir betiğin işleyebilmesi için.", + "hooks.enableAria": "{name} etkinleştir", + "hooks.httpStatus": "HTTP {status}", + "hooks.loadFailed": "Webhook’lar yüklenemedi.", + "hooks.name": "Ad", + "hooks.new": "Yeni webhook", + "hooks.newTitle": "Yeni webhook", "hooks.noDeliveries": "Henüz hiçbir şey teslim edilmedi. Geçişler oldukça teslimler burada görünür.", - "hooks.deliveryTitle": "Teslim", - "hooks.statSent": "Gönderilen", - "hooks.statFailed": "Başarısız", + "hooks.noResponse": "yanıt yok", + "hooks.payloadNote": "Payload {version}. İmza {header} içinde, zaman damgası ve gövde üzerinden.", + "hooks.retriesHint": "Yeniden denemeler bu uç noktanın kendi kuyruğunu bloke eder, çünkü verilen söz sıralamadır. 4xx asla yeniden denenmez — isteğin yanlış olduğunu söyleyen bir uç nokta bunu yine söyleyecektir.", + "hooks.saveFailed": "Hook kaydedilemedi.", + "hooks.secretCopied": "Kopyaladım", + "hooks.secretTitle": "İmzalama anahtarı — şimdi kopyalayın, bir daha gösterilemez", + "hooks.sendTest": "Test teslimi gönder", + "hooks.sendTestTitle": "Şimdi bir test teslimi gönder", + "hooks.signed": "imzalı", + "hooks.statDropped": "Düşürülen", "hooks.statEndpoints": "Uç noktalar", + "hooks.statFailed": "Başarısız", "hooks.statQueued": "Kuyrukta", - "hooks.statDropped": "Düşürülen", "hooks.statRetries": "Yeniden denemeler", - "hooks.payloadNote": "Payload {version}. İmza {header} içinde, zaman damgası ve gövde üzerinden.", - "hooks.deleteTitle": "Bu webhook silinsin mi?", - "hooks.deleteBody": "Hemen teslim etmeyi bırakır. Uç noktaya dokunulmaz ve hook’u yeniden oluşturabilirsiniz — ama imzalama anahtarı geri getirilemez.", - "hooks.deleteConfirm": "Webhook’u sil", - "hooks.editTitle": "Webhook’u düzenle", - "hooks.newTitle": "Yeni webhook", - "hooks.dialogIntro": "Geçiş başına bir imzalı POST, sırayla. Bir uyarıdan farklı olarak hiçbir şey birleştirilmez veya bastırılmaz — bir betik her kenarı alır.", - "hooks.name": "Ad", - "hooks.url": "Uç nokta URL’si", - "hooks.urlMasked": "Kayıtlı URL asla tam olarak gösterilmez. Korumak için maskelenmiş değeri bırakın ya da değiştirmek için yeni bir URL yazın.", - "hooks.triggers": "Tetikleyiciler", - "hooks.triggersNoneHint": "Hiçbiri seçilmemişse her tetikleyici geçerlidir; gelecekteki bir sürümün ekleyecekleri de dâhil.", + "hooks.statSent": "Gönderilen", + "hooks.testFailed": "Uç nokta test teslimini kabul etmedi.", + "hooks.timeout": "{seconds}s zaman aşımı", "hooks.timeoutLabel": "Zaman aşımı (s)", - "hooks.attemptsLabel": "Denemeler", - "hooks.retriesHint": "Yeniden denemeler bu uç noktanın kendi kuyruğunu bloke eder, çünkü verilen söz sıralamadır. 4xx asla yeniden denenmez — isteğin yanlış olduğunu söyleyen bir uç nokta bunu yine söyleyecektir.", - "hooks.create": "Oluştur", - "hooks.triggerIngestPublished": "Yayın başladı", - "hooks.triggerIngestDisconnected": "Yayın durdu", - "hooks.triggerDestinationUp": "Hedef canlı", - "hooks.triggerDestinationDown": "Hedef durdu", - "hooks.loadFailed": "Webhook’lar yüklenemedi.", + "hooks.title": "Yaşam döngüsü webhook’ları", "hooks.toggleFailed": "Hook değiştirilemedi.", - "hooks.deleted": "Webhook silindi.", - "hooks.deleteFailed": "Hook silinemedi.", - "hooks.testFailed": "Uç nokta test teslimini kabul etmedi.", - "hooks.deliveriesFailed": "Son teslimler okunamadı.", + "hooks.triggerCount": "{count} tetikleyici", + "hooks.triggerCountOne": "{count} tetikleyici", + "hooks.triggerDestinationDown": "Hedef durdu", + "hooks.triggerDestinationUp": "Hedef canlı", + "hooks.triggerIngestDisconnected": "Yayın durdu", + "hooks.triggerIngestPublished": "Yayın başladı", + "hooks.triggers": "Tetikleyiciler", + "hooks.triggersNoneHint": "Hiçbiri seçilmemişse her tetikleyici geçerlidir; gelecekteki bir sürümün ekleyecekleri de dâhil.", + "hooks.unsigned": "İMZASIZ", "hooks.updated": "Webhook güncellendi.", - "hooks.created": "Webhook oluşturuldu.", - "hooks.saveFailed": "Hook kaydedilemedi.", - "playlist.title": "Çalma listesi", + "hooks.url": "Uç nokta URL’si", + "hooks.urlMasked": "Kayıtlı URL asla tam olarak gösterilmez. Korumak için maskelenmiş değeri bırakın ya da değiştirmek için yeni bir URL yazın.", + "jobs.alwaysKeepNewest": "Her zaman en yenileri sakla", + "jobs.automaticModel": "Otomatik (bu donanıma göre seçilir)", + "jobs.backToDefault": "Bu tür için varsayılan moda dön", + "jobs.batteryFloor": "Pil alt sınırı %", + "jobs.broadcastGoing": "Bir yayın sürüyor; ertelenen işler ona öncelik verir.", + "jobs.cancel": "İptal", + "jobs.cannotPause": "Bu türlerin duraklatılması gerekir ama duraklatılamazlar, bu yüzden onun yerine tamamlanıyorlar.", + "jobs.completed": "Tamamlandı", + "jobs.concurrency": "Eşzamanlılık", + "jobs.couldNotReadTheJob": "İşlem kuyruğu okunamadı.", + "jobs.couldNotSaveThePolicy": "Politika kaydedilemedi.", + "jobs.cpuCeiling": "CPU tavanı %", + "jobs.cpuResume": "CPU devam eşiği %", + "jobs.defaultMode": "Varsayılan mod", + "jobs.defaultModel": "Varsayılan model", + "jobs.deferred": "Ertelendi", + "jobs.failed": "Başarısız", + "jobs.finished": "Bitti", + "jobs.global": "Genel", + "jobs.gpuGateNote": "Bunun kapsamına yalnızca GPU kullandığı işaretlenmiş türler girer.", + "jobs.history": "Geçmiş", + "jobs.historyRetention": "Geçmiş saklama", + "jobs.holdDeferredWorkBackOn": "Şarjı tükenen bir dizüstünde bunun altında ertelenmiş işleri bekletir. 0 bunu devre dışı bırakır.", + "jobs.hostCpuLoad": "Ana makine CPU yükü", + "jobs.howManyJobsMayRun": "Aynı anda kaç işlemin çalışabileceği. Bir, çünkü ikinci bir transkod, kimsenin istemediği bir işlem hacmini kimsenin kabul etmediği bir riskle satın alır.", + "jobs.inQueue": "Kuyrukta", + "jobs.job": "İşlem", + "jobs.keepForDays": "Saklama süresi (gün)", + "jobs.machineSafety": "Makine güvenliği. Gerçek zamanlı işlerin de uyduğu tek sınır budur.", + "jobs.niceLevel": "Nice düzeyi", + "jobs.noFinished": "Henüz biten işlem yok.", + "jobs.noModels": "Modeller dizininde hiçbir model dosyası bulunamadı. Otomatik seçenek, seçtiği modeli indirir; burada belirli bir model adı vermek ancak o dosya var olduğunda işe yarar.", + "jobs.nothingNewStartsAboveThis": "Bunun üzerinde yeni hiçbir şey başlamaz. 0 bu kapıyı devre dışı bırakır.", + "jobs.nothingQueued": "Kuyrukta bir şey yok. Son işlem çalışmaları kütüphaneden gönderilir.", + "jobs.nothingWasOldEnoughTo": "Temizlenecek kadar eski bir şey yoktu. Önce Politika bölümünde saklama süresini düşürün.", + "jobs.pauseAll": "Tümünü duraklat", + "jobs.policy": "Politika", + "jobs.policySaved": "Kaynak politikası kaydedildi.", + "jobs.progress": "İlerleme", + "jobs.queuePaused": "Kuyruk duraklatıldı.", + "jobs.queueResumed": "Kuyruk sürdürüldü.", + "jobs.queued": "Kuyrukta", + "jobs.recording": "Kayıt", + "jobs.releasedItWillStartAs": "Serbest bırakıldı. Bir yuva boşalır boşalmaz başlayacak.", + "jobs.removeFromHistory": "Geçmişten kaldır", + "jobs.removeWindow": "Pencereyi kaldır", + "jobs.resumeAll": "Tümünü sürdür", + "jobs.retry": "Tekrar dene", + "jobs.retryTitle": "Yeni bir deneme bütçesiyle yeniden hazırla", + "jobs.runNow": "Şimdi çalıştır", + "jobs.runNowTitle": "Kaynak politikasını yok sayarak bunu şimdi çalıştır", + "jobs.running": "Çalışıyor", + "jobs.savedTheConcurrencyChangeTakes": "Kaydedildi. Eşzamanlılık değişikliği sunucu yeniden başlatıldığında geçerli olur — kuyruk bunu başlarken düzeltir.", + "jobs.state": "Durum", + "jobs.stopsEverythingRealtimeIncludedA": "Gerçek zamanlı olanlar dâhil her şeyi durdurur: hız kısan bir CPU yayını bozmaya çoktan başlamıştır. 0 bunu devre dışı bırakır.", + "jobs.subtitle": "Arka plan işleri: döküm çıkarma, ara kopyalar, küçük resimler ve arşiv sıkıştırma.", + "jobs.thatDidNotWork": "Bu işe yaramadı.", + "jobs.thermalCeiling": "Sıcaklık tavanı °C", + "jobs.thisManyFinishedJobsSurvive": "Bu kadar bitmiş işlem, yaşı ne olursa olsun saklanır.", + "jobs.title": "İşlemler", + "jobs.transcription": "Döküm çıkarma", + "jobs.unsavedChanges": "Kaydedilmemiş değişiklikler.", + "jobs.whereTheCeilingReleasesThe": "Tavanın bıraktığı yer. Aradaki fark, kapının salınmasını önleyen histerezdir.", + "jobs.whisperMissing": "whisper.cpp burada kurulu değil, bu yüzden bunun henüz bir etkisi yok. Yine de ayarlamaya değer: kurulur kurulmaz seçim geçerli olur.", + "jobs.why": "Neden", + "jobs.windowEnd": "Pencere bitişi", + "jobs.windowStart": "Pencere başlangıcı", + "jobs.yieldNote": "Ağır işler kuyruğa alınır ve canlı yayına öncelik verir. Düşen bir kare geri getirilemez; bir saat sonra gelen bir dökümün ise hiçbir maliyeti yoktur.", + "lib.anySpeaker": "Herhangi bir konuşmacı", + "lib.anyTag": "Herhangi bir etiket", + "lib.autoGrouped": "Otomatik gruplandı. Düzenlemek onu sizin yapar.", + "lib.autoGroupedNote": "Bu oturum otomatik olarak gruplandı. Kaydetmek onu sizin yapar ve gruplama geçişi onu yeniden yazmayı bırakır.", + "lib.bestMatch": "En iyi eşleşme", + "lib.clearSearch": "Aramayı temizle", + "lib.clip": "Klip", + "lib.clipTitle": "Bu kayıttan bir klip kes", + "lib.collapseSession": "Oturumu daralt", + "lib.couldNotLoadTheLibrary": "Kütüphane yüklenemedi.", + "lib.couldNotLoadTheSession": "Oturumun bölümleri yüklenemedi.", + "lib.couldNotQueueTheProxy": "Ara kopya kuyruğa alınamadı.", + "lib.couldNotRegroup": "Yeniden gruplanamadı.", + "lib.couldNotSaveTheSession": "Oturum kaydedilemedi.", + "lib.delete": "Sil", + "lib.deleteDescription": "Dosya; ara kopyaları, küçük resimleri ve dökümüyle birlikte diskten kaldırılır. Bu işlem geri alınamaz.", + "lib.deleteTitle": "Bu kayıt silinsin mi?", + "lib.downloadMaster": "Ana kaydı indir", + "lib.downloadMasterTitle": "Çok parçalı ana kaydı indir", + "lib.editSession": "Oturumu düzenle", + "lib.expandSession": "Oturumu genişlet", + "lib.from": "Başlangıç", + "lib.generateProxy": "Ara kopya oluştur", + "lib.generateProxyTitle": "Tarayıcıda oynatılabilir ara kopyayı oluştur", + "lib.mostRecent": "En yeni", + "lib.noProxyYet": "Henüz ara kopya yok — oluşturmak için açın", + "lib.noSegments": "Burada bölüm yok.", + "lib.noSessionMatch": "Bu filtrelere uyan oturum yok.", + "lib.nothingRecorded": "Henüz kayıt yok. Kaydedici bölümleri yazdıkça oturumlar görünür.", + "lib.oldestFirst": "Önce en eski", + "lib.playProxy": "Ara kopyayı oynat", + "lib.proxyAlreadyQueued": "Bir ara kopya zaten kuyrukta.", + "lib.proxyQueued": "Ara kopya kuyruğa alındı.", + "lib.recordingDeleted": "Kayıt silindi.", + "lib.regroupTitle": "Gruplamayı yeniden çalıştırır. Eklemeli ve idempotenttir: elle ayırdığınız bir gruplamayı asla birleştirmez.", + "lib.searchPlaceholder": "Her kayıtta, herkesin söylediği her şeyi arayın…", + "lib.searchTranscripts": "Dökümlerde ara", + "lib.searchable": "Aranabilir", + "lib.searchableOnly": "Dökümü olan, dolayısıyla aranabilen bölümler", + "lib.session": "Oturum", + "lib.sessionDescription": "Açıklama", + "lib.sessionSaved": "Oturum kaydedildi.", + "lib.sessionTags": "Etiketler", + "lib.sessionTitle": "Başlık", + "lib.subtitle": "Her yayın, aranabilir. Her mikrofon kendi parçasına kaydedildi, bu yüzden döküm kimin söylediğini zaten biliyor.", + "lib.tag": "Etiket", + "lib.tagsPlaceholder": "virgülle, ayrılmış", + "lib.title": "Kütüphane", + "lib.transcribe": "Dökümünü çıkar", + "lib.transcribeEachTrackOnIts": "Her parçanın dökümünü ayrı ayrı çıkarın — konuşmacı atamasını sağlayan budur", + "lib.ungrouped": "Gruplanmamış bölümler", + "meters.deviation": "Sapma", + "meters.idle": "boşta", + "meters.integrated": "Bütünleşik", + "meters.loudness": "Ses yüksekliği uyumu", + "meters.metering": "ölçülüyor", + "meters.momentary": "Anlık", + "meters.monitorOff": "İzleme kapalı. Her hedefin ne ilettiğini ölçmek için açın.", + "meters.noSignal": "bu parçada sinyal yok", + "meters.nothingToMeasure": "Henüz ölçülecek bir şey yok. Çalışan her hedef, aldığı miks için kendi EBU R128 çözümleyicisini alır.", + "meters.range": "Aralık", + "meters.shortTerm": "Kısa süreli", + "meters.subtitle": "Her giriş parçasının her kanalı, doğrudan aktarımdan.", + "meters.title": "Ses ölçerler", + "meters.truePeak": "Gerçek tepe", + "meters.waitingForStream": "yayın bekleniyor", + "mon.allProcesses": "Tüm süreçler", + "mon.bitrateChart": "Giriş bit hızı — son 30 dakika", + "mon.chooseDestination": "Bir hedef seçin…", + "mon.clearLog": "Günlüğü temizle", + "mon.clearedThisDestinationIsBack": "Temizlendi. Bu hedef yeniden üretilen komuta döndü.", + "mon.dryRun": "Kuru çalıştırma", + "mon.dryRunInvalid": "FFmpeg bir argümanı reddetti", + "mon.dryRunOk": "FFmpeg kabul etti", + "mon.extraInput": "Ek giriş argümanları", + "mon.extraOutput": "Ek çıkış argümanları", + "mon.ffmpegLog": "FFmpeg günlüğü", + "mon.fromRunningProcess": "çalışan süreçten", + "mon.fullCommand": "Tümüyle çözümlenmiş komut", + "mon.hostCpu": "Ana makine işlemcisi", + "mon.ingestLoss": "Giriş kaybı", + "mon.memory": "Bellek", + "mon.nothingProvenEitherWay": "Hiçbir yönde bir şey kanıtlanmadı", + "mon.pid": "PID", + "mon.processes": "Süreçler", + "mon.rebuilt": "yeniden oluşturuldu", + "mon.relayDrops": "Aktarım kayıpları", + "mon.relayIn": "Aktarım girişi", + "mon.resolveHint": "Uygulamadan önce tam olarak neyin çalışacağını görmek için komutu çözümleyin.", + "mon.restarts": "Yeniden başlatmalar", + "mon.subtitle": "Süreç sağlığı, ana makine kaynakları ve canlı FFmpeg günlüğü.", + "mon.title": "İzleme", + "mon.uptime": "Çalışma süresi", + "nav.automation": "Otomasyon", + "nav.chat": "Sohbet", + "nav.clips": "Klip", + "nav.dashboard": "Kontrol Paneli", + "nav.jobs": "İşlemler", + "nav.library": "Kütüphane", + "nav.meters": "Ses seviye göstergeleri", + "nav.monitoring": "İzleme", + "nav.playout": "Playout", + "nav.recordings": "Kayıtlar", + "nav.renditions": "Sürümler", + "nav.routing": "Routing", + "nav.settings": "Ayarlar", + "nav.sources": "Kaynaklar", + "play.acrossEveryRungTheBackstop": "Tüm basamaklar genelinde. Uzun bir yayının diski doldurmasını engelleyen son güvence.", + "play.allowCrossOrigin": "Çapraz kökene izin ver", + "play.allowCrossOriginPlayback": "Çapraz kökenli oynatmaya izin ver", + "play.audioBitrate": "Ses bit hızı (kbps)", + "play.audioTrack": "Ses parçası", + "play.basicPasswordNote": "Bir bağlantıyı izleyemeyen oynatıcılar (bir set üstü kutu, bir CDN) aynı gizli anahtarı HTTP basic parolası olarak gönderebilir.", + "play.boundsTheViewerTableViewers": "İzleyici tablosunu sınırlar. Sınırın ötesindeki izleyicilere hizmet verilir, yalnızca sayılmazlar.", + "play.carriesThePlaybackTokenAnyone": "Oynatma belirtecini taşır. Bu URL'ye sahip herkes izleyebilir.", + "play.clipboardUnavailable": "Pano kullanılamıyor; elle seçip kopyalayın", + "play.couldNotLoadPlayout": "Playout yüklenemedi", + "play.disabled": "Devre dışı", + "play.diskCap": "Disk sınırı (MB)", + "play.dvrWindow": "DVR penceresi (sn)", + "play.embedCode": "Gömme kodu", + "play.enable": "Playout'u etkinleştir", + "play.enabled": "Etkin", + "play.forObsACdnOr": "OBS, bir CDN veya HLS konuşan herhangi bir oynatıcı için.", + "play.format": "Biçim", + "play.hls": "HLS", + "play.hlsDash": "HLS ve DASH", + "play.hlsPlaylist": "HLS çalma listesi", + "play.idleTimeout": "İzleyici boşta kalma zaman aşımı (sn)", + "play.linkReset": "Yeni oynatma bağlantısı oluşturuldu. Önceki artık çalışmıyor.", + "play.linksProtected": "Genel seçeneğini açana kadar bu bağlantılar yalnızca oturum açmış bir yönetici için çalışır.", + "play.linksPublic": "Bu bağlantılar hesap olmadan çalışır.", + "play.liveWindow": "Canlı pencere (bölüm)", + "play.metaDescription": "Açıklama", + "play.metaTitle": "Başlık", + "play.mustExceedOneSegmentOr": "Bir bölümü aşmalı, yoksa izleyiciler yoklamalar arasında listeden düşer.", + "play.name": "Ad", + "play.noCredentialNote": "Hiçbir tür kimlik bilgisi gerekmez. Yalnızca tüm internetin görmesini istediğiniz bir yayın için uygundur.", + "play.noRungs": "Henüz basamak yok. Bir şey yayınlamak için bir tane ekleyin.", + "play.onDisk": "Diskte", + "play.oneStereoAacTrackPer": "Her basamak için bir stereo AAC parçası.", + "play.packaging": "Paketleme", + "play.passthrough": "Giriş (passthrough)", + "play.peak": "Tepe", + "play.playerPage": "Oynatıcı sayfası", + "play.posterNote": "Genel sayfada videonun üstünde gösterilir. Poster, otomatik olarak yenilenen yakın tarihli bir anahtar karedir.", + "play.private": "Özel.", + "play.protectedNote": "Oynatma varsayılan olarak korumalıdır. Onu açmak ayrı ve bilinçli bir adımdır.", + "play.protection": "Koruma", + "play.public": "Genel", + "play.resetFailed": "Sıfırlanamadı", + "play.restartNote": "Bunlardan herhangi birini değiştirmek çoklayıcıları yeniden başlatır ve izleyicilerin yayınını birkaç saniyeliğine keser.", + "play.rungNote": "Her basamak, sürümünün videosunu bit bit kopyalar, bu yüzden bir basamak eklemek bir çoklayıcıya mal olur, asla ikinci bir video kodlamasına değil. Basamaklar arasında yalnızca ses parçası farklıdır; hedef başına yönlendirmeye dokunulmaz.", + "play.segmentLength": "Bölüm uzunluğu (sn)", + "play.serveWithoutSession": "İzleyicilere oturum olmadan hizmet ver", + "play.sessionCap": "Oturum sınırı", + "play.sessions": "Oturumlar", + "play.share": "Paylaş", + "play.shorterIsLowerLatencyAnd": "Daha kısa olması daha az gecikme ve daha çok çalma listesi değişimi demektir.", + "play.subtitle": "Yayını izleyicilere doğrudan, genel HLS olarak sunun.", + "play.threeIsTheMinimumA": "Üç, bir oynatıcının üzerinde tampon yapabileceği en düşük değerdir.", + "play.title": "Playout", + "play.unlisted": "Listelenmemiş.", + "play.untitled": "Başlıksız yayın", + "play.videoSource": "Video kaynağı", + "play.watchingNow": "Şu anda izleyen", + "play.whoCanWatch": "Kimler izleyebilir", + "player.copied": "Kopyalandı", + "player.copy": "Kopyala", + "player.loading": "Yükleniyor…", + "player.missingToken": "İzlediğiniz bağlantıda oynatma belirteci eksik ya da belirteç değiştirilmiş. Bağlantıyı paylaşan kişiden güncel bir bağlantı isteyin.", + "player.notLive": "Şu anda canlı yayın yok", + "player.viewer": "izleyici", + "player.viewers": "izleyici", + "player.willStart": "Yayın başladığında bu sayfa kendiliğinden oynatmaya başlar.", + "playlist.choose": "Bir yükleme seçin", + "playlist.chooseAria": "Eklemek için bir yükleme seçin", "playlist.empty": "Kuyrukta henüz bir şey yok.", + "playlist.hint": "Media bölümünden yüklenir. Bu çalma listesinin adlandırdığı bir öğe sonradan yüklemesini kaybederse, yük devretme ona bir sonraki ulaştığında sessizce başarısız olmak yerine yukarıda ilgi gerektiren olarak görünür.", "playlist.itemsAria": "Çalma listesi öğeleri", - "playlist.stateReady": "Hazır", - "playlist.stateTranscoding": "Dönüştürülüyor", - "playlist.stateAttention": "İlgi gerekiyor", - "playlist.stateUnsaved": "Henüz kaydedilmedi", "playlist.missingUpload": "Bu yükleme artık bulunamıyor.", - "playlist.moveUp": "{name} yukarı taşı", "playlist.moveDown": "{name} aşağı taşı", - "playlist.remove": "{name} kaldır", - "playlist.choose": "Bir yükleme seçin", - "playlist.chooseAria": "Eklemek için bir yükleme seçin", + "playlist.moveUp": "{name} yukarı taşı", "playlist.noUploads": "Kullanılabilir yükleme yok", - "playlist.hint": "Media bölümünden yüklenir. Bu çalma listesinin adlandırdığı bir öğe sonradan yüklemesini kaybederse, yük devretme ona bir sonraki ulaştığında sessizce başarısız olmak yerine yukarıda ilgi gerektiren olarak görünür.", - "dest.fbBoxTitle": "Facebook çapraz paylaşımı ve bağış düğmesi", - "dest.fbBoxIntro": "Kitle ile aynı oluşturma çağrısında gönderilir, dolayısıyla aynı kural geçerlidir: yayın başladığında bir kez uygulanır ve boşsa tamamen dokunulmadan bırakılır.", - "dest.fbBackupLabel": "Yedek yayın gönder", - "dest.fbBackupHelp": "Bu yayının ikinci bir kopyasını Facebook’un yedek girişine gönderir, böylece kopan bir bağlantı yayını düşürmez.", - "dest.fbBackupCost": "Bu hedefin yükleme bant genişliğini iki katına çıkarır.", - "dest.fbBackupReconnect": "Açmak yayını bir kez yeniden bağlar — bu yüzden canlıya çıkmadan önce etkinleştirin.", - "dest.fbCrosspostLabel": "Sayfalara çapraz paylaşım", - "dest.fbCrosspostPageId": "Sayfa kimliği", - "dest.fbCrosspostAlsoPost": "Ayrıca paylaş", - "dest.fbCrosspostRemove": "Bu Sayfayı kaldır", - "dest.fbCrosspostAdd": "Sayfa ekle", - "dest.fbCrosspostHelp": "Facebook’un kendi konsolundaki sayısal Sayfa kimliği (Sayfa ayarları veya Sayfa olarak oturum açmışken graph.facebook.com/me/accounts) — burada arama yoktur, bu yüzden ad veya URL yerine kimliği yapıştırın. “Ayrıca paylaş”, yayını yalnızca o Sayfayla paylaşmak yerine o Sayfa olarak yayınlar; kapalıyken yalnızca sessiz paylaşım gerçekleşir.", - "dest.fbDonateLabel": "Bağış düğmesi hayır kurumu kimliği", - "dest.fbDonatePlaceholder": "Hayır kurumu kimliği", - "dest.fbDonateHelp": "Bu da Facebook’un bağış kampanyası konsolundan gelen anlaşılmaz bir kimliktir, aranacak bir kurum adı değil. Hiç bağış düğmesi eklenmemesi için boş bırakın." + "playlist.remove": "{name} kaldır", + "playlist.stateAttention": "İlgi gerekiyor", + "playlist.stateReady": "Hazır", + "playlist.stateTranscoding": "Dönüştürülüyor", + "playlist.stateUnsaved": "Henüz kaydedilmedi", + "playlist.title": "Çalma listesi", + "rec.delete": "Sil", + "rec.deleteDescription": "Dosya; ara kopyaları, küçük resimleri ve dökümüyle birlikte diskten kaldırılır. Bu işlem geri alınamaz.", + "rec.deleteFailed": "Kayıt silinemedi.", + "rec.deleteTitle": "Bu kayıt silinsin mi?", + "rec.deleted": "Kayıt silindi.", + "rec.disabled": "devre dışı", + "rec.disk": "Disk", + "rec.diskRecordings": "Kayıtlar", + "rec.download": "İndir", + "rec.downloadNamed": "{name} dosyasını indir", + "rec.duration": "Süre", + "rec.empty": "Henüz kayıt yok. Sağdan kaydı etkinleştirin ve bir yayın başlatın.", + "rec.enabled": "Etkin", + "rec.file": "Dosya", + "rec.free": "Boş", + "rec.haltedDefault": "Kayıt durduruldu: birim, boş alan alt sınırının altında.", + "rec.haltedRestarts": "Alan boşaldığı anda kaydedici kendiliğinden yeniden başlar.", + "rec.help.enabled": "Her girişi yayın sürerken diske yazar; tüm ses parçalarını ayrı ve kodlanmamış olarak tutar. Kayıt, platformlara gönderdiğinizden bağımsızdır: kapatmak canlı çıkışınızı etkilemez, açmak ise yükleme bant genişliği yerine disk alanına mal olur.", + "rec.help.maxAgeHours": "Toplam boyuttan bağımsız olarak bu saatten daha eski kayıtları siler. Yalnızca son bir iki güne ihtiyaç duyduğunuzda kullanışlıdır. Başka bir sınır kaldırana kadar her şeyi saklamak için 0 girin.", + "rec.help.maxGb": "polyemesis'in sakladığı kayıtların toplam boyutu için bir tavan. Aşıldığında toplam yeniden sığana kadar en eskiler silinir. Boyut sınırı olmaması için 0 girin; bu durumda büyümeyi sınırlayan tek şey yaş ya da boş alan alt sınırıdır.", + "rec.help.minFreeGb": "Kesin bir durdurma: birimdeki boş alan bu değerin altına düştüğünde kayıt durur ve alan boşalınca kendiliğinden sürer. Bu, polyemesis'in yazmadığı dosyaları da hesaba katan tek sınırdır; dolayısıyla paylaşılan bir diski dolmaktan gerçekten koruyan ayar budur.", + "rec.help.segmentSeconds": "Kaydedici yenisine başlamadan önce her dosyanın ne kadar süreceği. Daha kısa bölümler bir çökmenin size neye mal olacağını sınırlar ve tek tek dosyaları taşımayı kolaylaştırır, ama yönetilecek daha çok dosya bırakır. Kesmeler bir anahtar kareye denk gelir, bu yüzden gerçek uzunluk bir saniye kadar değişir.", + "rec.help.stemFiles": "Ayrıca her ses parçasını ana kaydın yanında kendi dosyası olarak yazar. Sonradan kurgu yaparken sunucuyu, konukları ve oyun sesini ayrı kanallarda istediğinizde işe yarar. Bir oturumun yazdığı veriyi kabaca parça sayısı kadar katlar; bu yüzden varsayılan olarak kapalıdır.", + "rec.help.stemFormat": "Her iki seçenek de kayıpsızdır, dolayısıyla hiçbiri kaliteden ödün vermez. FLAC yaklaşık yarı boyuttadır ve iş akışınızdaki bir araç onu okuyamıyorsa dışında tercih etmeniz gerekendir; WAV her şeyde açılır. 8 kanaldan geniş parçalar otomatik olarak WAV'a döner.", + "rec.hideStems": "Ayrı parçaları gizle", + "rec.keepForever": "0 = süresiz sakla.", + "rec.loadFailed": "Kayıtlar yüklenemedi.", + "rec.maxAgeHours": "Şu süre sonra sil (saat)", + "rec.maxGb": "En fazla sakla (GB)", + "rec.minFreeGb": "Şu boş alanın altında durdur (GB)", + "rec.minFreeNote": "Yukarıdaki sınırlar yalnızca polyemesis'in yazdıklarını sınırlar; birimdeki başka her şey yine de onu doldurabilir. 0 = alt sınır yok.", + "rec.noSizeLimit": "0 = boyut sınırı yok.", + "rec.recording": "kaydediyor", + "rec.retentionNote": "Saklama her 30 saniyede bir çalışır: önce en eskiler, önce yaşa sonra toplam boyuta göre. O sırada yazılmakta olan bölüm asla silinmez.", + "rec.saveFailed": "Ayarlar kaydedilemedi.", + "rec.savePolicy": "Saklama ilkesini kaydet", + "rec.saved": "Kayıt ayarları kaydedildi.", + "rec.segmentSeconds": "Bölüm uzunluğu (saniye)", + "rec.segments": "Bölümler", + "rec.settingsTitle": "Kayıt", + "rec.showStems": "Ayrı parçaları göster", + "rec.size": "Boyut", + "rec.started": "Başlangıç", + "rec.stemFiles": "Ayrı parça dosyaları", + "rec.stemFlac": "FLAC — kayıpsız, yaklaşık yarı boyut", + "rec.stemFormat": "Ayrı parça biçimi", + "rec.stemNote": "Ana kayıtla aynı süreçten yazılır, böylece her ayrı parça diğerleriyle örnek düzeyinde hizalı kalır. 8 kanaldan geniş parçalar kendiliğinden WAV'a döner.", + "rec.stemWav": "WAV — kayıpsız, her yerde açılır", + "rec.stems": "Ayrı parçalar", + "rec.stemsTitle": "{count} ayrı parça dosyası, ana kayıtla örnek düzeyinde hizalı", + "rec.stemsUsage": "Ayrı parçalar ({count} dosya)", + "rec.subtitle": "Eksiksiz çok parçalı arşiv: her giriş ses parçası kodlanmadan korunur.", + "rec.title": "Kayıtlar", + "rec.tracks": "Parçalar", + "rec.tracksTitle": "{count} ses parçası korundu", + "rec.used": "Kullanılan", + "rec.volume": "Birim", + "rend.aspectBlurpad": "Bulanık dolgu", + "rend.aspectBlurpadHint": "Kalan alanı, karenin kendisinin bulanıklaştırılmış ve dolduracak şekilde kırpılmış bir kopyasıyla doldurur. Dikey akışların üzerinde uzlaştığı yöntem budur.", + "rend.aspectCrop": "Kırparak doldur", + "rend.aspectCropHint": "Hedef biçime göre ortadan kırpar, sonra ölçekler. Özneler ekrandaki boyutunu korur; karenin kenarları gider.", + "rend.aspectHandling": "En boy oranı işleme", + "rend.aspectPad": "Letterbox", + "rend.aspectPadHint": "Tüm kareyi sığacak şekilde ölçekler ve kalanı düz bir renkle doldurur. Hiçbir şey kaybolmaz, ama 9:16 tuval üzerindeki 16:9 bir kaynak çoğunlukla banttan ibarettir.", + "rend.aspectStretch": "Esneterek sığdır", + "rend.aspectStretchHint": "Hedef boyuta ölçekler ve kaynak uyuşmuyorsa görüntünün bozulmasına izin verir. Diğer kipler yokken her sürümün yaptığı şey.", + "rend.boxColour": "Kutu rengi", + "rend.boxOpacity": "Kutu saydamsızlığı (%)", + "rend.builtInFont": "Yerleşik varsayılan", + "rend.burnedText": "Görüntüye işlenmiş metin", + "rend.chooseHardware": "Bir donanım kodlayıcı ya da daha küçük bir çıkış seçin.", + "rend.colourBlack": "black", + "rend.colourWhite": "white", + "rend.costBeyond": "Bunu gerçek zamanlı olarak yazılımla kodlamak çoğu makinenin gücünü aşar", + "rend.costBeyondDetail": "{label}, tek bir kodlama için {mpps}. Bu boyutta x264 genellikle gerçek zamanla yarışı kaybeder ve geride kalan bir sürüm, kendisini okuyan her hedef için kare düşürür — bunu da yayının ortasında öğrenirsiniz.{machine} {escape}", + "rend.costBusy": "Birkaç çekirdeğin meşgul olmasını bekleyin", + "rend.costBusyDetail": "{label}, {encoder} üzerinde {mpps}; bu ağır bir gerçek zamanlı kodlamadır.{machine} Canlıya çıktığında kodlayıcının hızını izleyin: 1,0× altında geride kalıyordur ve altındaki hedefler kare düşürmeye başlar. {escape}", + "rend.costMachineCores": " Bu makine {cores} mantıksal çekirdek bildiriyor.", + "rend.costModest": "Ölçülü bir yazılım kodlaması", + "rend.costModestDetail": "{label}, {mpps}; çoğu makinenin erişebileceği bir düzey.{machine} Yine de gerçek bir kodlamadır — onu seçen her hedef tarafından paylaşılır, ama bedava değildir.", + "rend.costNoNumbers": "Maliyet henüz tahmin edilemiyor", + "rend.costNoNumbersDetail": "Kaynağı hiçbir şey yoklamadı, bu yüzden “kaynağı koru” seçeneğinin arkasında sayı yok. Girişi başlatın, burası dolar.", + "rend.costOffloaded": "{encoder} üzerine devredildi", + "rend.costOffloadedDetail": "{label} ({mpps}) GPU'nun sabit işlevli kodlayıcısında çalışır, bu yüzden az CPU harcar. Kalite ve hız denetimi sürücüye göre değişir ve her yongada kendine ait bir tavan vardır — önemli bir yayından önce çıkışı denetleyin.", + "rend.create": "Oluştur", + "rend.created": "Sürüm oluşturuldu. Kodlamayı başlatmak için onu bir hedefte seçin.", + "rend.custom": "Özel…", + "rend.deintAll": "Her kare", + "rend.deintAllHint": "Taramalı olduğu hâlde bunu bildirmeyen kaynaklar için. Pek çok SDI köprüsü ve yakalama kartı, kendisine ne verilirse verilsin her şeyi ilerlemeli olarak işaretler; bunlarda “yalnızca taramalı kareler” hiçbir şey yapmaz ve bozuk bir ayar gibi görünür.", + "rend.deintAuto": "Yalnızca taramalı kareler", + "rend.deintAutoHint": "Yalnızca kaynağın taramalı olarak işaretlediği karelere dokunur, böylece ilerlemeli kareler el değmeden geçer. Karışık her şey için doğru seçim.", + "rend.deintOff": "Kapalı", + "rend.deintOffHint": "Neredeyse herkesin elindeki ilerlemeli kaynaklar için doğru olan. Böyle bir kaynakta taramayı ayırmak hiçbir kazanç sağlamadan görüntüyü yumuşatır.", + "rend.deinterlace": "Taramayı ayır", + "rend.deleteFailed": "Sürüm silinemedi.", + "rend.deleteFallbackMany": "{count} hedef passthrough'a geri düşer ve kendilerine kaynak videosu değiştirilmeden gönderilir. Kaynağın her platformun kabul ettiğine hâlâ uyduğunu kontrol edin.", + "rend.deleteFallbackOne": "{count} hedef passthrough'a geri düşer ve kendisine kaynak videosu değiştirilmeden gönderilir. Kaynağın her platformun kabul ettiğine hâlâ uyduğunu kontrol edin.", + "rend.deleteNoDestinations": "Bu sürümü hiçbir şey seçmiyor, dolayısıyla silmek hiçbir hedefi değiştirmez.", + "rend.deleteRenditionTitle": "{name} silinsin mi?", + "rend.editRendition": "Sürümü düzenle", + "rend.empty": "Sürüm yok. Her hedef passthrough üzerinde; içlerinden biri kaynağı kabul edemeyene kadar bu tam olarak doğrudur — örneğin Twitch, Kick ya da X'in kabul etmeyeceği bir 4K60 girişi. Bir sürüm ekleyin ve o hedefleri ona yönlendirin.", + "rend.encoder": "Kodlayıcı", + "rend.encoderPreset": "Kodlayıcı ön ayarı", + "rend.encoderRelatedFailed": "{name} sunulmadı, çünkü burada ilgili bir kodlayıcı başarısız oldu.", + "rend.encoderTestFailed": "{name}, bu makinede tek karelik deneme kodlamasında başarısız oldu.", + "rend.encoderTestOk": "{name}, bu makinede bir deneme karesi kodladı.", + "rend.evenNote": "Bir eksende 0, kaynağın değerini korur ve en boy oranını bozmaz — yeniden ölçeklemek için yalnızca yüksekliği ayarlayın. İkisi de çift sayı olmalıdır: H.264 ve HEVC'de tek boyutlu renk düzlemi yoktur.", + "rend.font": "Yazı tipi", + "rend.frameRate": "Kare hızı", + "rend.gop": "GOP", + "rend.gpuUnopenable": "Bir GPU var ancak bu süreç onu açamıyor", + "rend.hardwareFallback": "donanım", + "rend.hardwareScanFound": "Donanım taramasının bulduğu", + "rend.height": "Yükseklik", + "rend.hevcWarning": "Bu kodlayıcı HEVC üretir. Çoğu RTMP hedefi yalnızca H.264 kabul eder — bunu yalnızca HEVC aldığını size bildirmiş bir hedef için seçin.", + "rend.keyframeInterval": "Anahtar kare aralığı", + "rend.letterboxColour": "Letterbox rengi", + "rend.live": "Canlı", + "rend.loadFailed": "Sürümler yüklenemedi.", + "rend.marginX": "X kenar boşluğu (%)", + "rend.marginY": "Y kenar boşluğu (%)", + "rend.name": "Ad", + "rend.namePlaceholder": "Twitch ve Kick için 1080p60", + "rend.needsBothAWidthAnd": "Hem genişlik hem yükseklik gerekir — bir eksen boş bırakıldığında ölçekleme en boy oranını zaten korur.", + "rend.needsBothAxes": "Hem genişlik hem yükseklik gerekir", + "rend.newRendition": "Yeni sürüm", + "rend.noEncoder": "Bu FFmpeg kullanılabilir bir video kodlayıcı bildirmedi.", + "rend.noHardwareEscape": "Bu makinede hiçbir donanım kodlayıcı çalışmıyor, dolayısıyla bunu taşıyacak bir yer yok — buradaki tek kaldıraç daha küçük bir çıkıştır. O zamandan beri bir sürücü kurduysanız ya da kapsayıcıya bir GPU geçirdiyseniz donanımı yeniden algılayın.", + "rend.notProbed": "Bu makinede henüz hiçbir test kodlaması yapılmadı, bu yüzden bu liste donanımın yapabildiklerini değil, FFmpeg derlemesinin içerdiklerini gösterir. Öğrenmek için yeniden algılayın.", + "rend.notRunning": "Çalışmıyor", + "rend.note": "Not", + "rend.noteDuplicateFps": "Kaynak {srcFps} fps çalışıyor, dolayısıyla {fps} fps yalnızca kareleri çoğaltır.", + "rend.notePlaceholder": "Bu katmanın ne için olduğu.", + "rend.noteUpscale": "Kaynak {width}×{height}. Yukarı ölçeklemek CPU harcar ve ayrıntı eklemez.", + "rend.opacity": "Saydamsızlık (%)", + "rend.percentNote": "Her şey karenin yüzdesidir, bu yüzden aynı ayarlar hem 16:9 hem de 9:16 katmanında doğrudur. Ortalanmış bir eksende kenar boşlukları yok sayılır.", + "rend.preset": "Ön ayar", + "rend.presetPlaceholder": "veryfast", + "rend.presetStartingPoint": "Başlangıç noktası — güncel sınırları platformdan doğrulayın.", + "rend.redetectFailed": "Donanım yeniden algılanamadı.", + "rend.redetectTitle": "GPU taramasını yeniden çalıştırır ve her kodlayıcıyla bir test karesi kodlar. Birkaç saniye sürer.", + "rend.redetectedNoHardware": "Donanım yeniden algılandı. Bu makinede hiçbir donanım kodlayıcı deneme kodlamasını geçemedi.", + "rend.resolution": "Çözünürlük", + "rend.restartEncode": "Bu kodlamayı ve onu okuyan hedefleri yeniden başlat", + "rend.restartFailed": "Kodlama yeniden başlatılamadı.", + "rend.restarting": "{name} yeniden başlıyor. Hedefleri onunla birlikte yeniden bağlanır.", + "rend.save": "Kaydet", + "rend.saveFailed": "Sürüm kaydedilemedi.", + "rend.seedNote": "Bunlar formu yalnızca başlangıç değerleriyle doldurur — kaydetmeden önce her şeyi düzenleyebilirsiniz. Platform tavanları değişir ve iş ortağı durumuna göre farklılık gösterir.", + "rend.signalError": "Hata", + "rend.signalIdle": "Boşta", + "rend.sizePct": "Boyut (yüksekliğin %'si)", + "rend.softwareEncoderFallback": "bir yazılım kodlayıcısı", + "rend.speed": "Hız", + "rend.subtitle": "Tek bir paylaşılan video kodlaması, birçok hedef. Sese burada hiç dokunulmaz.", + "rend.target": "Hedef değer", + "rend.textColour": "Metin rengi", + "rend.textPlaceholder": "İSTASYONUM", + "rend.title": "Sürümler", + "rend.unused": "Bunu henüz hiçbir hedef seçmiyor, dolayısıyla hiçbir şey kodlanmıyor. Kodlamayı başlatmak için bir hedefte seçin.", + "rend.uptime": "Çalışma süresi", + "rend.videoBitrate": "Video bit hızı", + "rend.waitingSnapshot": "İlk canlı anlık görüntü bekleniyor.", + "rend.watermarkImage": "Filigran görseli", + "rend.watermarkPlaceholder": "overlays/logo.png", + "rend.width": "Genişlik", + "rend.widthPct": "Genişlik (karenin %'si)", + "route.attack": "Atak", + "route.audio": "ses", + "route.audioAndVideoGoOut": "Ses ve video birlikte çıkar.", + "route.audioLaterThanVideo": "Ses videodan sonra", + "route.audioRouting": "Ses yönlendirme", + "route.audioRoutingDesc": "Her hedefin tam olarak hangi giriş parçalarını alacağını seçin.", + "route.autoLimitWhen2Tracks": "Otomatik (2+ parçada sınırla)", + "route.commentaryLanguage": "Yorum dili", + "route.couldNotApplyThePreset": "Ön ayar uygulanamadı.", + "route.couldNotCompileThisRouting": "Bu yönlendirme derlenemedi.", + "route.couldNotLoadDestinations": "Hedefler yüklenemedi.", + "route.couldNotSaveTheRouting": "Yönlendirme kaydedilemedi.", + "route.couldNotSaveTrackRoles": "Parça rolleri kaydedilemedi.", + "route.custom": "Özel…", + "route.delayDirection": "Gecikme yönü", + "route.destinations": "Hedefler", + "route.duckingNote": "Bir grup parçayı, başka bir grup konuşurken kısın — müzik yatağının üzerindeki mikrofon gibi. Kapalı, seçili her parçanın kendi kazancıyla, değiştirilmeden toplanması demektir.", + "route.duckingTriggerNote": "Bir tetikleyicinin bu hedefin miksinde olması gerekmez — yalnızca müzik gönderilirken bile bir mikrofon buradaki müziği kısabilir.", + "route.enableDucking": "Ducking'i etkinleştir", + "route.everyTrackReaches": "Seçili her parça, müzik dâhil, bu hedefe ulaşır.", + "route.excludeMusic": "Müziği hariç tut", + "route.keepMusic": "Müziği koru", + "route.keepMusicOut": "Müziği bu hedefin dışında tut", + "route.labelTracks": "Parçaları etiketle", + "route.langPlaceholder": "lang — örn. es", + "route.limiter04DbfsCeiling": "Limiter (−0,4 dBFS tavan)", + "route.localOnlyNote": "Bu sunucu parça rollerini henüz saklamıyor, bu yüzden bunlar bu tarayıcıda kalır. Sayfadaki diğer her şey normal biçimde kaydedilir.", + "route.loudnessTarget": "Ses yüksekliği hedefi", + "route.lufsBroadcast": "EBU R128 yayıncılık", + "route.measuredOverTheWholeProgramme": "Anlık değil, tüm program boyunca ölçülür: sessiz bölümler sessiz kalır. Bunlar platformların kendi normalleştiricilerinin hedeflediği değerlerdir, dolayısıyla birini tutturmak platformun sesinizi kısmasını önleyen şeydir.", + "route.mixMatrix": "Miks matrisi", + "route.musicPolicy": "Müzik politikası", + "route.noTarget": "Hedef yok (seviyelere dokunma)", + "route.normalizeLoudnorm": "Ses yüksekliği (EBU R128)", + "route.normalizeOff": "Kapalı", + "route.off": "kapalı", + "route.offset": "Kaydırma", + "route.on": "açık", + "route.platformDefault": "Platform varsayılanı", + "route.podcastSpokenWord": "Podcast / konuşma", + "route.presetApplied": "Ön ayar uygulandı. Gözden geçirin, sonra kaydedin.", + "route.range": "Aralık", + "route.ratio": "Oran", + "route.release": "Bırakma", + "route.result": "Sonuç", + "route.role51": "5.1", + "route.roleClean": "temiz", + "route.roleMic": "mikrofon", + "route.roleMusic": "müzik", + "route.rolesNote": "Roller bu hedefi değil girişi tanımlar ve kendi başlarına kaydedilir. Aşağıdaki bir hedef istemedikçe hiçbir şey filtrelenmez.", + "route.simple": "Basit", + "route.target": "Hedef değer", + "route.targetRole": "kısılan", + "route.thisDestinationSPlatform": "Bu hedefin platformu", + "route.threshold": "Eşik", + "route.title": "Routing", + "route.triggerRole": "tetikleyici", + "route.truePeak": "Gerçek tepe", + "route.video": "video", + "route.videoLaterThanAudio": "Video sesten sonra", + "route.withoutATargetTheMix": "Hedef değer olmadan miks, girişin ürettiği seviyede çıkar; yalnızca yukarıdaki kırpılma koruması uygulanır.", + "route.youtubeTwitchSpotify": "YouTube, Twitch, Spotify", + "set.acceptSelfSigned": "Kendinden imzalı sertifikayı kabul et", + "set.accountDisconnected": "Hesap bağlantısı kesildi.", + "set.adminPassword": "Yönetici parolası", + "set.alertDelivery": "Uyarı teslimi", + "set.alertDeliveryDesc": "Yanıt vermeyen bir webhook'un, uyarıdan vazgeçilmeden önce ne kadar ısrarla kovalanacağı.", + "set.apiTokens": "API belirteçleri", + "set.apiTokensDesc": "Betikler, Prometheus ve oturum tutamayan her şey için uzun ömürlü kimlik bilgileri. Birini Authorization: Bearer pmk_… olarak gönderin. Bir belirteç, belirteçleri yönetmek dışında yöneticinin yapabildiği her şeyi yapabilir — o yetki parolanın arkasında kalır, dolayısıyla bir belirteci iptal etmek geri alınamaz.", + "set.app": "Uygulama", + "set.attemptsBackoff": "Denemeler arasındaki bekleme otomatik olarak 30 saniyelik bir tavana kadar uzar; dolayısıyla bu aralığın üst ucu, ölü bir uç noktayı kovalamakla geçen birkaç dakika demektir. Sınırlı olması işin özü: sonsuza dek yeniden denemek, tek bir ölü webhook'u hiç boşalmayan bir kuyruğa dönüştürür.", + "set.attemptsNote": "İlk deneme dâhil. 1, hiç yeniden deneme yapılmaması demektir.", + "set.attemptsRetryable": "Yalnızca yeniden denemeye değer hatalar yeniden denenir. Birinin sildiği bir webhook'tan gelen 404 kalıcıdır ve onu dört kez daha denemek yalnızca arkasında sıra bekleyen her uyarıyı geciktirir; ters vekil sunucudan gelen bir 502 ise tam olarak bu denemelerin var oluş nedenidir.", + "set.audioMeters": "Ses ölçerler", + "set.brokerPasswordCleared": "Broker parolası temizlendi.", + "set.brokerPlainNote": "mqtt:// şifrelenmemiştir. MQTT, kullanıcı adını ve parolayı CONNECT paketinde gönderir; dolayısıyla ikisi de ağda açık metin olarak gider — güvenilen bir yerel ağda sorun değil, ki bu olağan Home Assistant durumudur, ancak denetiminizde olmayan hiçbir yerde uygun değildir. Broker sunuyorsa mqtts:// kullanın.", + "set.brokerUrl": "Broker URL'si", + "set.brokerUrlNote": "mqtt://, mqtts://, ws:// veya wss://. URL içindeki parola reddedilir: bir URL günlük satırlarına ve ps çıktısına düşer, bunun geri dönüşü yoktur. Parolayı, beklemede şifrelendiği aşağıdaki alana girin.", + "set.brokerUrlPlaceholder": "mqtts://broker.local:8883", + "set.chatBothApply": "İkisi de geçerlidir ve daha cömert olan kazanır: bir ileti, hem saat sayısından eski hem de en yeni N iletinin dışında kaldığında silinir. Böylece yoğun bir kanal, aynı ayarlarla sakin bir kanaldan daha kısa süre saklar.", + "set.chatBufferNote": "“Bağlanınca gönder”, diğer ikisinden farklı türde bir sayıdır. Onlar diskte tutulanı sınırlar; bu ise bellekte tutulan ve bağlı her panel için ayrılan bir arabellektir.", + "set.chatHistory": "Sohbet geçmişi", + "set.chatHistoryDesc": "Sohbet geçmişinin ne kadar geriye gittiği — ve dolayısıyla bir moderatörün birinin kartını açtığında ne kadarını görebildiği.", + "set.chatHours": "Şu kadar sakla (saat)", + "set.chatKeep": "Her zaman sakla (ileti)", + "set.chatKeepNote": "Yaşları ne olursa olsun bir alt sınır.", + "set.chatNotOnlyDisk": "Bu yalnızca bir disk ayarı değildir. Sohbette bir ada tıklamak, o kişinin daha önce söylediklerini açar ve bu geçmiş buradan gelir — hiçbir platform bunun için API yayımlamaz, dolayısıyla polyemesis yalnızca sakladığını gösterebilir. Bunu çok kısa tutarsanız, bir haftadır sorun çıkaran biri hiç konuşmamış gibi görünür.", + "set.chatSendNote": "Sorgusuz geçmiş.", + "set.chatSendOnConnect": "Bağlanınca gönder (ileti)", + "set.chatSendOnConnectShort": "Bağlanınca gönder", + "set.chatSmall": "Sohbet küçüktür. Saniyede ortalama on ileti alan bir kanal saatte kabaca 7 MB depolar; çoğu bunun çok altındadır.", + "set.clientId": "İstemci kimliği", + "set.clientIdNote": "Broker üzerinde benzersiz olmalıdır. Çakışma, açıklanamayan yeniden bağlanma döngüsünün bir numaralı nedenidir: broker her bağlantıda daha eski oturumu düşürür ve iki istemci de sonsuza dek yeniden bağlanır.", + "set.clientIdPlaceholder": "örnek adından türetilir", + "set.clientSecret": "İstemci gizli anahtarı", + "set.comingBack": "Geri dönüş", + "set.confirmPassword": "Yeni parolayı doğrula", + "set.connectedAccounts": "Bağlı hesaplar", + "set.copied": "Kopyalandı", + "set.copy": "Kopyala", + "set.couldNotChangeThePassword": "Parola değiştirilemedi.", + "set.couldNotClearThePassword": "Parola temizlenemedi.", + "set.couldNotCreateTheToken": "Belirteç oluşturulamadı.", + "set.couldNotReCheckThe": "Kimlik bilgileri yeniden denetlenemedi.", + "set.couldNotReadTheTls": "TLS durumu okunamadı.", + "set.couldNotRevokeTheToken": "Belirteç iptal edilemedi.", + "set.couldNotSaveTheCredentials": "Kimlik bilgileri kaydedilemedi.", + "set.couldNotSaveTheMqtt": "MQTT ayarları kaydedilemedi.", + "set.credsRemoved": "Kimlik bilgileri kaldırıldı.", + "set.currentPassword": "Mevcut parola", + "set.dataDirectory": "Veri dizini", + "set.deliveryAttempts": "Teslim denemeleri", + "set.dialsOnly": "Sunucu yalnızca şunları arar: {schemes}.", + "set.disconnect": "Bağlantıyı kes", + "set.disconnectDesc": "Onu kullanan hedefler ayarlarını korur ancak yayın anahtarlarını yenilemeyi bırakır. Aynı hesabı sonradan yeniden bağlayabilirsiniz.", + "set.disconnectTitle": "Bu hesabın bağlantısı kesilsin mi?", + "set.drawtextMissing": "Bu derlemede drawtext filtresi yok, bu yüzden metin kaplamaları kullanılamaz. libfreetype ile derlenmiş bir FFmpeg gerekir.", + "set.drawtextOk": "Bu derleme video üzerine metin çizebilir.", + "set.enabled": "Etkin", + "set.encoderUrl": "Kodlayıcı URL'si", + "set.expires": "Bitiş", + "set.failover": "Yük devretme", + "set.failoverDesc": "Canlı kaynak veri göndermeyi bıraktığında devreye giren bir yedek giriş ve bir bekleme kartı. Varsayılan olarak kapalı: kapalıyken hat, bu özellik var olmadan önceki hâliyle bayt bayt aynıdır.", + "set.failoverRemuxNote": "Bunu açmak, giriş ile altındaki her şeyin arasına bir yeniden çoklama adımı ekler. Ucuz, ama bedava değil; işte bu yüzden varsayılan değil bir karardır.", + "set.fileSizeMb": "Dosya boyutu (MB)", + "set.filesKept": "Saklanan dosyalar", + "set.goingLive": "Canlıya geçiş", + "set.goingLiveDesc": "Birden çok hedef aynı anda başladığında ne olur.", + "set.graceNote": "Katman değişmeden önce canlı kaynağın ne kadar süre hiçbir şey göndermeyebileceği. Bilerek bir yeniden bağlanmadan uzun tutulur — RTMP bağlantısını yeniden kuran bir kodlayıcı normal çalışmadır, arıza değil.", + "set.gracePeriod": "Tolerans süresi (saniye)", + "set.haDiscovery": "Home Assistant keşfi", + "set.height": "Yükseklik", + "set.ingest": "Giriş", + "set.ingestDesc": "SRT altı ses parçasına kadar taşır. RTMP bir parça taşır ve SRT yapamayan kodlayıcılar için bir yedek olarak vardır. Çekme, bir kaynağı beklemek yerine onu arar.", + "set.ingestUrlCopied": "Giriş URL'si kopyalandı.", + "set.instance": "Örnek", + "set.intervalNote": "Değişmemiş durum yeniden yayımlanmaz; bu yüzden kısa bir aralık, bir ileti yerine bir karşılaştırmaya mal olur. Keep-alive, ölü bir bağlantının ne kadar süre fark edilmeden kalacağını — ve dolayısıyla broker'ın abonelere bu örneğin gittiğini ne kadar çabuk söyleyeceğini — sınırlar.", + "set.issuer": "Veren", + "set.kbps": "kbps", + "set.keepAlive": "Keep-alive (s)", + "set.latencyMs": "Gecikme (ms)", + "set.listeners": "Dinleyiciler", + "set.listenersDesc": "Sunucunun tüm kurulum için nereye bağlandığı. Tek bir SRT bağlantı noktası her kaynağa hizmet eder — kaynaklar yayın belirteçleriyle ayırt edilir, dolayısıyla kaynak eklemek asla bir bağlantı noktası daha açmak anlamına gelmez. RTMP'de böyle bir yönlendirme yoktur, bu yüzden onun bağlantı noktası tek bir kaynağa hizmet eder.", + "set.logsBoundNote": "Günlük dizini bu iki değerin çarpımıyla sınırlıdır; dolayısıyla kalıcılık asla diski dolduran şey olamaz.", + "set.logsDesc": "Yakalanan FFmpeg stderr çıktısı her zaman bellekte tutulur, ancak bellek sunucuyla birlikte ölür — ve tam olarak neden öldüğünü açıklayan satırlar kaybolur. Kalıcı kılmak, bunu veri dizini altındaki döner bir dosyaya yansıtır.", + "set.metersDesc": "Yardımcı bir süreç, her parçanın her kanalını ölçer. Daha düşük aralıklar daha hızlı güncellenir ve daha çok işlemci harcar.", + "set.mode": "Kip", + "set.modePull": "Çekme — bir kamerayı, akışı veya dosyayı ara", + "set.modeRtmp": "RTMP — tek parça", + "set.modeSrt": "SRT — çok parçalı (önerilir)", + "set.mqtt": "MQTT telemetrisi", + "set.mqtt5Only": "Yalnızca MQTT 5.0.", + "set.mqtt5OnlyNote": "3.1.1'e sabitlenmiş bir broker bağlantıyı hiç tamamlayamaz — bu bir düşük yetenek kipi değildir. Mosquitto 2.x, EMQX ve Home Assistant eklentisinin hepsi 5.0 konuşur.", + "set.mqttDesc": "Durumu bir broker'a saklanan iletiler olarak yayımlar; böylece bir şey değiştiğinde bağlı olmayan bir tüketici bile güncel yanıtı alır.", + "set.multiAccountNote": "Birden çok kanala yayın yapmak için aynı platformu birden fazla kez bağlayın — her hesap kendi hedefi olur.", + "set.name": "Ad", + "set.newPassword": "Yeni parola", + "set.no": "hayır", + "set.noEncryption": "Bu bağlantıyı hiçbir şey şifrelemiyor. Parolalar ve oturum çerezleri ağı düz metin olarak geçiyor — tls.mode: auto ayarlayın ya da bir vekil sunucunun arkasında 127.0.0.1'e bağlayın.", + "set.noLibsrt": "Bu FFmpeg derlemesinde SRT desteği yok. --enable-libsrt ile yapılandırılmış bir derleme kurun ya da RTMP kullanın.", + "set.noTokens": "Henüz belirteç yok.", + "set.passphrase": "Parola", + "set.passphraseNote": "AES şifrelemesini etkinleştirir. Hiçbiri için boş bırakın. SRT, 10–79 karakter dışındaki parolaları reddeder.", + "set.passphrasePlaceholder": "isteğe bağlı — 10 ile 79 karakter", + "set.password": "Parola", + "set.passwordChanged": "Parola değiştirildi.", + "set.passwordMismatch": "Yeni parolalar eşleşmiyor.", + "set.passwordNote": "Parola hiçbir API tarafından döndürülmez; bu yüzden bir parola ayarlanmış olsa bile bu kutu boştur. Saklanan parolayı korumak için boş bırakın; aşağıdaki düğmeyle temizleyin.", + "set.platform": "Platform", + "set.platformsDesc": "Platformlar, bunun ne kadarını polyemesis'in otomatikleştirebildiği bakımından ayrışır ve bu farkı bir saat sonra değil, başlamadan önce bilmek yararlıdır. Yayının kendisi her yerde aynı çalışır — hedef başına ses yönlendirme, sürümler, yeniden bağlanma ve ölçerler aşağıdaki sütunların hiçbirine bağlı değildir.", + "set.playLoop": "Hiçbir şey canlı değilken bir döngü oynat", + "set.playLoopNote": "Bekleme kartının yanında ikinci bir aday: sabit görüntü yerine dosyalar oynatılır. İkisi de açık olabilir — hangisinin kazandığı için yük devretme tasarımındaki katman sırasına bakın.", + "set.pointObs": "OBS'yi bu adrese yönlendirin.", + "set.preview": "Önizleme", + "set.previewDesc": "Panonun HLS izleyicisi. polyemesis'in videoyu yeniden kodladığı tek yer burasıdır ve kendi aktarma aboneliği üzerinde çalıştığından bir hedefi asla etkileyemez.", + "set.previewIdleNote": "Kodlayıcı, pano oynatma listesini istediğinde başlar ve son istekten bu süre sonra yeniden durur; böylece kapalı bir pano işlemci harcamaz. Bunu değiştirmek, birinin izlediği bir önizlemeyi asla yeniden başlatmaz.", + "set.processLogs": "Süreç günlükleri", + "set.proxyTerminatesTls": "polyemesis'in önündeki bir ters vekil sunucu TLS'i sonlandırıyor, bu yüzden bu sunucu bilerek düz HTTP sunuyor.", + "set.publishInterval": "Yayımlama aralığı (s)", + "set.reconnectCap": "Yeniden bağlanma bekleme tavanı (s)", + "set.reconnectCapNote": "0 varsayılanı kullanır. Yalnızca HTTP ve HLS kaynakları.", + "set.redirectNote": "Şema ve bağlantı noktası dâhil tam olarak eşleşmelidir. polyemesis'e ters vekil sunucu üzerinden erişiyorsanız o genel URL'yi kullanın.", + "set.redirectUri": "İzin verilecek yönlendirme URI'si", + "set.removeCreds": "Kimlik bilgilerini kaldır", + "set.removeCredsDesc": "Bu uygulama üzerinden bağlanan her hesap belirtecini yenilemeyi bırakır ve yeniden bağlanması gerekir. İstemci gizli anahtarı buradan kurtarılamaz — platformda yeni bir uygulama oluşturmanız gerekir.", + "set.removeCredsTitle": "Bu geliştirici kimlik bilgileri kaldırılsın mı?", + "set.returnAfter": "Şu kadar sonra dön (saniye kararlı)", + "set.returnAutomatic": "Otomatik — birincil kararlı hâle gelince dön", + "set.returnManual": "El ile — ben geçiş yapana dek yedekte kal", + "set.returnManualNote": "Varsayılan el ile olmasının nedeni, otomatik dönüşün gidip gelebilmesidir: kesik kesik toparlanan bir birincil kaynak, yayının ortasında programı ileri geri taşırdı.", + "set.rtmpPort": "RTMP bağlantı noktası", + "set.rtspNote": "Kamera başka türlü yapamıyorsa dışında TCP. NAT arkasında UDP üzerinden RTSP bağlı görünür ve hiçbir şey iletmez.", + "set.rtspTransport": "RTSP taşıması", + "set.saveAndDrop": "Kaydet ve canlı yayını düşür", + "set.saveIngest": "Giriş ayarlarını kaydet", + "set.segmentSec": "Bölüm (s)", + "set.settingsSavedAffectedProcessesHave": "Ayarlar kaydedildi. Etkilenen süreçler yeniden başlatıldı.", + "set.showSlate": "Hiçbir şey canlı değilken bekleme kartı göster", + "set.slateColour": "Bekleme kartı rengi", + "set.slateColourNote": "Tek sözcük, çünkü değer bir süzgeç grafiğine düşer ve orada virgül argümanı sonlandırır. Yalnızca görsel ayarlanmadığında kullanılır.", + "set.slateColourPlaceholder": "black", + "set.slateImage": "Bekleme kartı görseli", + "set.slateImageNote": "Veri dizini içindeki bir yol. Bunun yerine düz bir renk boyamak için boş bırakın — yedek çözümün bu olmasının nedeni tam da bir rengin açılamayacak bir dosyasının bulunmamasıdır.", + "set.slateImagePlaceholder": "slate/holding.png", + "set.sourceUrl": "Kaynak URL'si", + "set.sourceUrlNote": "Şunlardan biri: {schemes}. file:// kaynağı, veri dizinine GÖRECELİ bir yoldur ve “..” içeremez — tıpkı yüklenen bir dosya gibi orada sınırlandırılmıştır.", + "set.sourceUrlPlaceholder": "rtsp://camera.local/stream1", + "set.srtPort": "SRT bağlantı noktası", + "set.stagger": "Bağlantıları kademelendir (ms)", + "set.staggerNote": "0 kapalıdır. Aksi hâlde canlıya geçmek, her hedefin aynı anda bir bağlantı açması, TLS anlaşması yapması ve ses kodlamaya başlaması demektir — küçük bir makinede kare düşürmenin en olası olduğu an budur ve tam da izlediğiniz andır, çünkü daha yeni canlıya geçtiniz. Birkaç yüz milisaniye fazlasıyla yeter.", + "set.staggerReconnectNote": "Yeniden bağlanmayı asla geciktirmez. Gece 3'te düşen bir hedef, zaten sağlıklı süreçlerin arkasında sıra beklemek yerine hemen geri döner.", + "set.stepByStep": "Adım adım kurulum", + "set.stopAfterIdle": "Boşta kalınca durdur (s)", + "set.streamKey": "Yayın anahtarı", + "set.streamKeyNote": "Yayıncılar tam olarak bu yolu kullanmalıdır; böylece açık bir bağlantı noktası açık bir kapı olmaz.", + "set.subject": "Konu", + "set.synthesiseNote": "Bütün büyük platformlar ses parçası olmayan bir yayını reddeder. Bu açıkken, yalnızca videodan oluşan bir giriş sessiz bir stereo parça kazanır ve hedefleri başlar; kapalıyken “routing profile selects no audio” diyerek reddederler. Yalnızca sıfır parçayla ALGILANMIŞ bir giriş için geçerlidir, dolayısıyla gerçekten gönderdiğiniz sesin yerini asla alamaz.", + "set.synthesiseSilent": "Sessiz bir parça üret", + "set.syntheticAudio": "Yapay ses", + "set.syntheticDesc": "Giriş video taşıyıp hiç ses taşımadığında ne olur.", + "set.tabIngest": "Giriş", + "set.tabPipeline": "İşlem hattı", + "set.tabPlatforms": "Platform kimlik bilgileri", + "set.tabSecurity": "Güvenlik", + "set.title": "Ayarlar", + "set.tlsAcme": "Let's Encrypt", + "set.tlsAuto": "Otomatik", + "set.tlsDesc": "Burada değil config.yaml içinde yapılandırılır — sunucu dinlemeye başlamadan önce TLS doğru olmalıdır. Bu kart, bunun sonucunu bildirir.", + "set.tlsManual": "El ile sertifika", + "set.tlsOff": "Burada sonlandırılmıyor", + "set.tlsSelfsigned": "Kendinden imzalı", + "set.tokenHashNote": "polyemesis yalnızca bir özet saklar. Kaybederseniz bu belirteci iptal edip yenisini oluşturun.", + "set.tokenNamePlaceholder": "prometheus", + "set.tokenRevoked": "{name} iptal edildi.", + "set.topicPrefix": "Konu ön eki", + "set.topicPrefixNote": "Ön ekteki ayırıcılar korunur, dolayısıyla home/av iki düzey demektir. $ ile başlayan bir ön ek reddedilir: broker'lar bunları ayırır ve # kullanan bir abone bunları asla almaz — telemetri başarıyla yayımlanır ve tam da hata ararken bakacağınız görünümde görünmez olur.", + "set.transportSecurity": "Taşıma güvenliği", + "set.updateInterval": "Güncelleme aralığı (ms)", + "set.username": "Kullanıcı adı", + "set.validFor": "Geçerlilik", + "set.whatPlatformsDo": "Her platform ne yapabilir", + "set.whyDevApp": "Neden kendi geliştirici uygulamanız gerekir", + "set.whyDevAppDesc": "polyemesis OAuth istemci gizli anahtarlarını dağıtamaz — ikili dosyaya sahip olan herkes onlara sahip olurdu ve platformlar bunları iptal ederdi. Bir kez ücretsiz bir geliştirici uygulaması kaydeder, istemci kimliğini ve gizli anahtarını buraya yapıştırırsınız; polyemesis de ardından yayın anahtarlarınızı sizin için alır.", + "set.writeLogs": "Günlükleri diske yaz", + "set.yes": "evet", + "sources.addDescription": "Kendi girişi, hedefleri ve sürümleri olan ikinci bir yayın — örneğin yatay bir tuvalin yanında dikey bir tuval. Varsayılan bağlantı noktalarında başlar; ikisi çakışmasın diye bunları değiştirmek isteyeceksiniz.", + "sources.addFailed": "Kaynak eklenemedi", + "sources.addSource": "Kaynak ekle", + "sources.addTitle": "Kaynak ekle", + "sources.added": "“{name}” eklendi. Giriş bağlantı noktalarını aşağıda ayarlayın.", + "sources.app": "Uygulama", + "sources.applyAndDrop": "Uygula ve yayını kes", + "sources.copied": "{what} kopyalandı", + "sources.copyFailed": "{what} kopyalanamadı", + "sources.copyTokenAria": "Belirteci kopyala", + "sources.copyUrlAria": "{proto} URL'sini kopyala", + "sources.default": "varsayılan", + "sources.defaultDescription": "Kaynak belirtmeyen istekler bunun üzerinde çalışır.", + "sources.deleteAria": "{name} kaynağını sil", + "sources.deleteConfirm": "Kaynağı sil", + "sources.deleteDescription": "Hedefleri ve sürümleri de onunla birlikte gider — bu yayının nereye gittiğini tanımlarlar ve onsuz bir anlam taşımazlar. Kayıtlar korunur: dosyalar hâlâ diskte ve oynatılabilir durumdadır, yalnızca bir kaynağa atfedilmeleri sona erer.", + "sources.deleteFailed": "Kaynak silinemedi", + "sources.deleteThis": "Bu kaynağı sil", + "sources.deleteTitle": "“{name}” silinsin mi?", + "sources.deleted": "“{name}” ve hedefleri silindi", + "sources.destinations": "Hedefler", + "sources.dirtyIdle": "Kaydedilmemiş giriş değişiklikleri. Uygulamak bu kaynağın girişini yeniden başlatır.", + "sources.dirtyPublishing": "Bu kaynağa bir kodlayıcı yayın gönderiyor. Uygulamak girişini yeniden başlatır ve izleyen herkesin bağlantısını keser.", + "sources.enableAria": "{name} kaynağını etkinleştir", + "sources.help.app": "RTMP uygulama yolu — URL'nin ana bilgisayar ile yayın anahtarı arasındaki bölümü. Çoğu kodlayıcı “live” bekler. Yalnızca bir nedeniniz varsa değiştirin.", + "sources.help.ingest": "Kodlayıcınızın bu kaynağa nasıl ulaştığı. SRT, kaybolanı yeniden ileterek paket kayıplı bir bağlantıda ayakta kalır ve genel internet üzerinde doğru varsayılandır. RTMP daha eskidir ve her yerde desteklenir, ancak kararsız bir hatta ciddi biçimde bozulur. Çekme modunda polyemesis, kendisine yayın gönderilmesini beklemek yerine verdiğiniz bir URL'den akışı kendisi alır.", + "sources.help.latencyMs": "SRT'nin geciken veya yeniden iletilen bir paketten vazgeçmeden önce ne kadar bekleyeceği. Yüksek değerler gecikme pahasına daha kötü bağlantıları atlatır: gidiş-dönüş süresinin yaklaşık 2,5 katı iyi bir başlangıçtır, yani 40 ms RTT için buraya yaklaşık 100 ms uygundur. Çok düşük olursa kısa bir aksama gözle görülür bir bozulmaya dönüşür.", + "sources.help.passphrase": "SRT bağlantısını şifreler ve bugün bu girişi yabancılara karşı asıl koruyan şey budur. 10 ile 79 karakter arasında olmalıdır. Boş bırakırsanız bağlantı noktasına erişebilen herkes yayın gönderebilir.", + "sources.help.streamKey": "Bir kodlayıcının RTMP üzerinden yayın göndermek için ilettiği gizli anahtar. Bu girişi koruyan kimlik bilgisi budur; bu yüzden bir parola gibi davranın: elinde tutan herkes hedeflerinize yayın yapabilir.", + "sources.help.token": "Birden fazla kaynak tek bir giriş bağlantı noktasını paylaştığında bu kaynağı tanımlar. Ayrıca erişimi denetleyip denetlemediği giriş modunuza bağlıdır — alttaki not şu anda hangisinin geçerli olduğunu söyler. Yenileme yeni bir belirteç verir ve eskisini beş dakika geçerli tutar; böylece canlı bir kodlayıcı yayın ortasında kesilmez.", + "sources.ingest": "Giriş", + "sources.lastSourceTitle": "Son kaynak silinemez — bir kurulumun bir girişe ihtiyacı vardır", + "sources.latencyMs": "Gecikme (ms)", + "sources.loadFailed": "Kaynaklar yüklenemedi", + "sources.loading": "Kaynaklar yükleniyor…", + "sources.loss": "kayıp", + "sources.modePull": "Çekme", + "sources.name": "Ad", + "sources.namePlaceholder": "Dikey", + "sources.noEngine": "Bu kaynak için çalışan bir motor yok. Olağan neden, başka bir kaynak veya başka bir işlem tarafından zaten kullanılan bir giriş bağlantı noktasıdır.", + "sources.notRunning": "çalışmıyor", + "sources.ownDescription": "Kendi hedefleri, sürümleri ve kayıtları.", + "sources.passphrase": "Parola", + "sources.passphrasePlaceholder": "10–79 karakter", + "sources.publishing": "yayınlıyor", + "sources.renditions": "Sürümler", + "sources.retrans": "yeniden ilet.", + "sources.rotate": "Yenile", + "sources.rotateConfirm": "Belirteci yenile", + "sources.rotateFailed": "Belirteç yenilenemedi", + "sources.rotateIdle": "Yeni bir belirteç hemen verilir. Eskisi beş dakika daha çalışır, böylece onu kullanan bir kodlayıcının bağlantısı kesilmez.", + "sources.rotateTitle": "“{name}” için belirteç yenilensin mi?", + "sources.rotateWhilePublishing": "Şu anda bu kaynağa bir kodlayıcı yayın gönderiyor. Eski belirteç beş dakika daha çalışmayı sürdürür, bu yüzden bağlantı kesilmez — ancak bu süre dolmadan güncellenmesi gerekir.", + "sources.rtt": "RTT", + "sources.running": "çalışıyor", + "sources.saveFailed": "Kaydedilemedi", + "sources.streamKey": "Yayın anahtarı", + "sources.subtitle": "Her biri bir gelen yayın. Hedefler ve sürümler bir kaynağa aittir.", + "sources.title": "Kaynaklar", + "sources.token": "belirteç", + "sources.tokenEnforcedDetailRtmp": "Yayın anahtarıdır: yukarıdaki URL'ye bunu anahtar olarak kullanarak yayın yapın. Döndürme yenisini verir ve eskisini beş dakika çalışır tutar, böylece yayını kesmeden geçebilirsiniz.", + "sources.tokenEnforcedDetailSrt": "Paylaşılan porttan bu kaynağa yayın yapmak için kodlayıcınızın SRT streamid alanına yazın. Döndürme yenisini verir ve eskisini beş dakika çalışır tutar, böylece yayını kesmeden geçebilirsiniz.", + "sources.tokenIsCredential": "Bu belirteç kimlik bilgisidir.", + "sources.tokenIssued": "Yeni belirteç verildi", + "sources.tokenNotEnforced": "Bu belirteç saklanıyor ancak şu anda zorunlu tutulmuyor.", + "sources.tokenNotEnforcedRtmp": "Kaynaklar bağlantı noktasıyla ayrı tutulur. Bu girişi asıl koruyan, yukarıdaki yayın anahtarıdır. Belirtecin kimlik bilgisi olması için Ayarlar'dan tek bağlantı noktalı girişi açın.", + "sources.tokenNotEnforcedSrt": "Kaynaklar bağlantı noktasıyla ayrı tutulur. Bu girişi asıl koruyan, yukarıdaki SRT parolasıdır. Belirtecin kimlik bilgisi olması için Ayarlar'dan tek bağlantı noktalı girişi açın.", + "state.disabled": "Devre dışı", + "state.failed": "Başarısız", + "state.live": "Canlı", + "state.offline": "Çevrimdışı", + "state.reconnecting": "Yeniden bağlanılıyor", + "state.starting": "Başlatılıyor", + "state.stopped": "Durduruldu" } diff --git a/ui/src/lib/i18n/uk.json b/ui/src/lib/i18n/uk.json index 5b5c5a70..643d8aa6 100644 --- a/ui/src/lib/i18n/uk.json +++ b/ui/src/lib/i18n/uk.json @@ -1,26 +1,227 @@ { - "nav.dashboard": "Панель керування", - "nav.sources": "Джерела", - "nav.meters": "Індикатори звуку", - "nav.routing": "Routing", - "nav.renditions": "Версії", - "nav.playout": "Playout", - "nav.library": "Бібліотека", - "nav.clips": "Кліпи", - "nav.chat": "Чат", - "nav.jobs": "Завдання", - "nav.automation": "Автоматизація", - "nav.recordings": "Записи", - "nav.monitoring": "Моніторинг", - "nav.settings": "Налаштування", - "chrome.toggleNav": "Показати/сховати навігацію", + "auth.accountCreated": "Обліковий запис адміністратора створено.", + "auth.confirmPassword": "Підтвердьте пароль", + "auth.createAccount": "Створити обліковий запис", + "auth.firstRun": "Первинне налаштування", + "auth.firstRunDesc": "Виберіть облікові дані адміністратора для цього сервера. Відновлення облікового запису немає, тож зберігайте їх у надійному місці.", + "auth.password": "Пароль", + "auth.passwordMismatch": "Паролі не збігаються.", + "auth.signIn": "Увійти", + "auth.signInDesc": "Введіть облікові дані адміністратора.", + "auth.somethingWentWrong": "Щось пішло не так.", + "auth.tokenCreated": "Токен API створено", + "auth.tokenRevoked": "Токен API відкликано", + "auth.username": "Імʼя користувача", + "auto.action": "Дія", + "auto.adminPasswordChanged": "Пароль адміністратора змінено", + "auto.chosenWeekdays": "Вибрані дні тижня", + "auto.coalesced": "Обʼєднано", + "auto.coalescingWindow": "Вікно обʼєднання (секунди)", + "auto.couldNotChangeTheRule": "Не вдалося змінити правило.", + "auto.couldNotChangeTheSchedule": "Не вдалося змінити розклад.", + "auto.couldNotDeleteTheRule": "Не вдалося видалити правило.", + "auto.couldNotDeleteTheSchedule": "Не вдалося видалити розклад.", + "auto.couldNotLoadAutomationSettings": "Не вдалося завантажити налаштування автоматизації.", + "auto.couldNotSaveTheRule": "Не вдалося зберегти правило.", + "auto.couldNotSaveTheSchedule": "Не вдалося зберегти розклад.", + "auto.createRule": "Створити правило", + "auto.createSchedule": "Створити розклад", + "auto.days": "Дні", + "auto.delete": "Видалити", + "auto.deleteRule": "Видалити правило", + "auto.deleteRuleDesc": "Правило перестане спрацьовувати. Його кінцева точка webhook лишиться недоторканою, і правило можна створити знову.", + "auto.deleteRuleTitle": "Видалити це правило сповіщення?", + "auto.deleteSchedule": "Видалити розклад", + "auto.deleteScheduleDesc": "Розклад перестане виконуватися. Усе, що він уже запустив, лишиться без змін.", + "auto.deleteScheduleTitle": "Видалити цей розклад?", + "auto.delivery": "Доставка", + "auto.destinationFallingBehindRealtime": "Призначення відстає від реального часу", + "auto.destinationKeepingUpAgain": "Призначення знову встигає", + "auto.destinationRecovered": "Призначення відновлено", + "auto.destinationWentDown": "Призначення впало", + "auto.destinations": "Призначення", + "auto.dropped": "Відкинуто", + "auto.edit": "Редагувати", + "auto.editRule": "Редагувати правило сповіщення", + "auto.editSchedule": "Редагувати розклад", + "auto.endpoint": "Кінцева точка", + "auto.evAudioClipping": "Кліпінг звуку", + "auto.evClipCaptured": "Кліп записано", + "auto.evDiskLow": "Мало місця на диску", + "auto.evDiskRecovered": "Місце на диску відновлено", + "auto.evIngestLost": "Вхід втрачено", + "auto.evIngestRecovered": "Вхід відновлено", + "auto.evSettingsChanged": "Налаштування змінено", + "auto.evSignedIn": "Успішний вхід", + "auto.events": "Події", + "auto.everyDay": "Щодня", + "auto.failed": "Помилка", + "auto.failoverSwitchedSource": "Резервування перемкнуло джерело", + "auto.fired": "спрацювало", + "auto.firesAt": "Спрацьовує о", + "auto.lastDelivery": "Остання доставка", + "auto.lastSweep": "Останній обхід", + "auto.latenessAllowance": "Допустиме запізнення (секунди)", + "auto.loudnessBackInCompliance": "Гучність знову в нормі", + "auto.loudnessOutOfCompliance": "Гучність поза нормою", + "auto.minGap": "Мінімальний проміжок між повідомленнями (секунди)", + "auto.minSeverity": "Мінімальна важливість", + "auto.missedNote": "Спрацювання, пропущене через зупинку сервера, позначається як оброблене й пропускається, а не виконується із запізненням: трансляція, яка сама починається на чотири години пізніше, гірша за ту, що не почалася зовсім.", + "auto.name": "Назва", + "auto.newRule": "Нове правило сповіщення", + "auto.newSchedule": "Новий розклад", + "auto.next": "Наступне", + "auto.nothingFired": "Поки нічого не спрацювало. Планувальник виконує обхід кожні 20 секунд.", + "auto.once": "Одноразово", + "auto.payloadFormat": "Формат корисного навантаження", + "auto.pending": "В очікуванні", + "auto.repeatedFailedSignIns": "Повторні невдалі входи", + "auto.repeats": "Повторення", + "auto.retries": "Повторні спроби", + "auto.rule": "Правило", + "auto.ruleCreated": "Правило сповіщення створено. Надішліть на нього тест.", + "auto.ruleDeleted": "Правило сповіщення видалено.", + "auto.rulePlaceholder": "Discord — сповіщення ефіру", + "auto.ruleSaved": "Правило сповіщення збережено.", + "auto.saveRule": "Зберегти правило", + "auto.saveSchedule": "Зберегти розклад", + "auto.schedule": "Розклад", + "auto.scheduleCreated": "Розклад створено.", + "auto.scheduleDeleted": "Розклад видалено.", + "auto.schedulePlaceholder": "Будній вечірній ефір", + "auto.scheduleSaved": "Розклад збережено.", + "auto.schedules": "Розклади", + "auto.secretMasked": "Показано приховано, бо секрет міститься у шляху URL. Не чіпайте це поле, щоб зберегти поточну кінцеву точку, або вставте новий URL, щоб замінити її.", + "auto.sendTest": "Надіслати тестове сповіщення", + "auto.sendTestTitle": "Надіслати тестове сповіщення зараз", + "auto.sent": "Надіслано", + "auto.skipped": "пропущено", + "auto.startDestinations": "Запустити призначення", + "auto.startPlaylist": "Запустити плейлист", + "auto.stopDestinations": "Зупинити призначення", + "auto.stopPlaylist": "Зупинити плейлист", + "auto.subscribedTo": "Підписано на", + "auto.subtitle": "Webhook’и, які повідомляють про поломку, і розклади, що ведуть ефір без вас.", + "auto.theEndpointDidNotAccept": "Ендпоінт не прийняв тестове повідомлення.", + "auto.time": "Час", + "auto.timeZone": "Часовий пояс", + "auto.title": "Автоматизація", + "auto.tzPlaceholder": "UTC", + "auto.webhookHint": "І Discord, і Slack видають його в налаштуваннях каналу.", + "auto.webhookPlaceholder": "https://discord.com/api/webhooks/…", + "auto.webhookRules": "Правила webhook", + "auto.webhookUrl": "URL webhook", + "auto.weekFri": "Пт", + "auto.weekMon": "Пн", + "auto.weekSat": "Сб", + "auto.weekSun": "Нд", + "auto.weekThu": "Чт", + "auto.weekTue": "Вт", + "auto.weekWed": "Ср", + "auto.when": "Коли", + "chatpage.allFiltered": "Усі платформи з повідомленнями відфільтровано.", + "chatpage.couldNotDeleteThatMessage": "Не вдалося видалити це повідомлення.", + "chatpage.deleteNote": "Видалення прибирає повідомлення на платформі, яка його випустила. Платформа, на якій polyemesis видаляти не вміє, повідомляє про це й вказує, де це можна зробити, — дію ніколи не приховують навмання.", + "chatpage.droppedTitle": "Показані в ефірі, але збереження відстало, і історія їх втратила.", + "chatpage.dupTitle": "Те саме повідомлення побачено двічі — наше власне відлуння або повтор із платформи.", + "chatpage.noAccount": "Чат працює, але не підключено жодного облікового запису платформи. Підключіть його в розділі Налаштування → Облікові дані платформ.", + "chatpage.notRunning": "На цьому сервері чат не запущено. Історія під стрічкою — це те, що було збережено раніше, а поле надсилання лишатиметься недоступним, доки не буде підключено платформу.", + "chatpage.received": "Отримано", + "chatpage.reload": "Оновити", + "chatpage.stored": "Збережено", + "chatpage.storedTitle": "Нічого не підключено; це збережена історія.", + "chatpage.subtitle": "Усі платформи в одній стрічці, з одним полем надсилання.", + "chatpage.timeline": "Стрічка", + "chatpage.title": "Чат", "chrome.ingest": "Ingest", + "chrome.language": "Мова", "chrome.liveCount": "{count} у прямому ефірі", + "chrome.signOut": "Вийти", "chrome.socketOffline": "Офлайн", "chrome.socketOfflineHint": "З’єднання з сервером оновлень втрачено; повторна спроба", - "chrome.signOut": "Вийти", - "chrome.language": "Мова", + "chrome.toggleNav": "Показати/сховати навігацію", + "clipedit.back33": "Назад на 33 мс", + "clipedit.clickLine": "Клацніть рядок, щоб перейти до нього. Виділіть кілька рядків — точки входу та виходу перейдуть до виділених слів.", + "clipedit.clipThisLine": "Вирізати цей рядок", + "clipedit.clipThisLineTitle": "Поставити точки входу та виходу навколо цього рядка", + "clipedit.copied": "Скопійовано", + "clipedit.copiesEveryPacketSecondsBit": "Копіює кожен пакет. Секунди, біт у біт, а початок зсувається назад до найближчого ключового кадру.", + "clipedit.couldNotOpenThatRecording": "Не вдалося відкрити цей запис.", + "clipedit.couldNotQueueTheExport": "Не вдалося поставити експорт у чергу.", + "clipedit.cut": "Різання", + "clipedit.downloadClip": "Завантажити кліп", + "clipedit.exportQueued": "Експорт поставлено в чергу. Він виконається, коли машина матиме вільні ресурси.", + "clipedit.exports": "Експорти", + "clipedit.fast": "Швидко", + "clipedit.filterTranscript": "Фільтрувати розшифровку", + "clipedit.findSaid": "Знайти сказане", + "clipedit.forward33": "Вперед на 33 мс", + "clipedit.in": "Вхід", + "clipedit.keepAllTracks": "Зберегти всі звукові доріжки", + "clipedit.keepsEveryAudioTrackIn": "Зберігає всі звукові доріжки в будь-якому кодеку. Те, що пише рекордер.", + "clipedit.markIn": "I — поставити точку входу на позиції відтворення", + "clipedit.markOut": "O — поставити точку виходу на позиції відтворення", + "clipedit.name": "Назва", + "clipedit.noKeyframes": "Позиції ключових кадрів прочитати не вдалося, тож ніхто не скаже, де насправді почнеться це різання. Працювати воно буде — власна перемотка FFmpeg потрапляє на ключовий кадр, — проте початок може виявитися раніше на величину до одного GOP, і ця сторінка не може показати, наскільки саме.", + "clipedit.noTranscript": "Для цього запису поки нічого не розшифровано. Кожен мікрофон записано на власну доріжку, тож розшифровка дає імена мовців без моделі діаризації — а далі можна нарізати кліпи за сказаним.", + "clipedit.nothingExported": "Поки нічого не експортовано.", + "clipedit.out": "Вихід", + "clipedit.pickAtLeastOneAudio": "Виберіть хоча б одну звукову доріжку або візьміть усі.", + "clipedit.playFaster": "L — відтворення, повторно — швидше", + "clipedit.precise": "Точно", + "clipedit.queueNote": "Експорт ставиться в чергу, а не виконується тут: він поступається живому потоку, тож нарізка, зроблена посеред ефіру, чекає, а не забирає ресурси, потрібні потоку.", + "clipedit.reEncodesOnlyTheLeading": "Перекодовує лише початковий неповний GOP, щоб початок був точним. Решта так само копіюється.", + "clipedit.reencoded": "Перекодовано", + "clipedit.shortcuts": "J / K / L — перемотка, I та O ставлять точки входу й виходу, стрілки зсувають на 33 мс, а стрілки з shift — на секунду. Дрібний крок дорівнює одному кадру за 30 кадрів на секунду; реальна частота цього запису не проіндексована, тож крок названо в мілісекундах, а не вгадано.", + "clipedit.shuttleBack": "J — перемотка назад", + "clipedit.startMoved": "Початок зсунуто", + "clipedit.starts": "Починається", + "clipedit.stop": "K — стоп", + "clipedit.thatCutCouldNotBe": "Не вдалося спланувати цю нарізку.", + "clipedit.thatIsNotARecording": "Це не ідентифікатор запису.", + "clipedit.thatRecordingIsNotAvailable": "Цей запис недоступний.", + "clipedit.title": "Редактор кліпів", + "clipedit.whatASocialPlatformWill": "Те, що прийме соціальна платформа. Крізь нього проходить менше кодеків.", + "clipedit.zoomIn": "Наблизити", + "clipedit.zoomOut": "Віддалити", + "clips.bitrate": "Бітрейт", + "clips.buffer": "Буфер", + "clips.bufferLiveNote": "Перемикач буфера діє негайно і не зберігається: після перезапуску він знову вимкнений.", + "clips.captured": "Захоплено", + "clips.ceiling": "Стеля", + "clips.clipsKept": "Збережених кліпів", + "clips.couldNotCaptureAClip": "Не вдалося записати кліп.", + "clips.couldNotChangeTheClip": "Не вдалося змінити буфер кліпів.", + "clips.couldNotDeleteTheClip": "Не вдалося видалити кліп.", + "clips.couldNotLoadClips": "Не вдалося завантажити кліпи.", + "clips.covers": "Охоплює", + "clips.customLength": "Власна тривалість кліпу в секундах", + "clips.delete": "Видалити", + "clips.deleteDescription": "Кліп — зазвичай саме те, що ви хотіли зберегти. Це видалить файл; запис, з якого його взято, лишиться недоторканим.", + "clips.deleteTitle": "Видалити цей кліп?", + "clips.deleted": "Кліп видалено.", + "clips.download": "Завантажити", + "clips.everythingHeld": "Усе, що тримає буфер", + "clips.file": "Файл", + "clips.held": "Утримується", + "clips.inMemory": "У памʼяті", + "clips.keepBuffer": "Тримати ковзний буфер", + "clips.keyframes": "Ключові кадри", + "clips.length": "Тривалість", + "clips.none": "немає", + "clips.off": "вимк", + "clips.onDisk": "На диску", + "clips.seen": "виявлено", + "clips.settings": "Налаштування", + "clips.size": "Розмір", + "clips.subtitle": "Ковзний буфер того, що щойно вийшло в ефір. Захоплюйте заднім числом — нічого не треба готувати заздалегідь.", + "clips.theClipDoesNotStart": "Кліп починається не з ключового кадру.", + "clips.title": "Кліпи", + "clips.waitingForStream": "очікування потоку", + "clips.window": "Вікно", + "clips.windowSeconds": "Вікно (секунди)", "common.add": "Додати", + "common.apply": "Застосувати", "common.cancel": "Скасувати", "common.close": "Закрити", "common.confirm": "Підтвердити", @@ -28,110 +229,921 @@ "common.copy": "Копіювати", "common.delete": "Видалити", "common.disabled": "Відімкнено", + "common.discard": "Відхилити", "common.edit": "Редагувати", "common.enabled": "Увімкнено", "common.loading": "Завантаження…", + "common.moreInfo": "Докладніше", "common.refresh": "Оновити", "common.restart": "Перезапустити", "common.retry": "Спробувати ще раз", "common.save": "Зберегти", "common.start": "Почати", "common.stop": "Зупинити", - "state.live": "Прямий ефір", - "state.reconnecting": "Відновлення з’єднання", - "state.starting": "Запуск", - "state.failed": "Помилка", - "state.stopped": "Зупинено", - "state.offline": "Офлайн", - "hooks.title": "Webhook’и життєвого циклу", - "hooks.new": "Новий webhook", - "hooks.empty": "Webhook’ів немає. Додайте один, і polyemesis надішле POST із підписаним тілом JSON тієї миті, коли потік почнеться чи зупиниться або призначення підніметься чи впаде — одна доставка на перехід, по порядку, щоб скрипт міг відреагувати.", - "hooks.colWebhook": "Webhook", - "hooks.colEndpoint": "Ендпоінт", - "hooks.colTriggers": "Підписаний на", - "hooks.colEnabled": "Увімк.", - "hooks.signed": "підписаний", - "hooks.unsigned": "БЕЗ ПІДПИСУ", + "dash.audioTracks": "Звукові доріжки", + "dash.bitrate": "Бітрейт", + "dash.broadcastSettings": "Налаштування трансляції", + "dash.categoryPlaceholder": "Музика", + "dash.clipboardUnavailable": "Буфер обміну недоступний на цьому домені.", + "dash.connectPrompt": "Підключіть обліковий запис YouTube, Twitch, Facebook або Kick у розділі Налаштування → Облікові дані платформ, щоб задавати назву, опис і категорію трансляції одразу на всіх платформах. Кожна приймає свій набір полів — редактор вказує, які саме, і надсилає те, що може.", + "dash.copyIngestUrl": "Копіювати URL входу", + "dash.couldNotLoadTheDestination": "Не вдалося завантажити призначення.", + "dash.couldNotPushTheMetadata": "Не вдалося надіслати метадані.", + "dash.couldNotReorderTheDestinations": "Не вдалося змінити порядок призначень.", + "dash.deleteDest": "Видалити призначення", + "dash.deleteDestDescription": "Це видалить призначення та його профіль маршрутизації. Платформа лишиться недоторканою, і призначення можна додати знову — але налаштована для нього маршрутизація звуку буде втрачена.", + "dash.descriptionPlaceholder": "Про що ця трансляція.", + "dash.destDeleted": "Призначення видалено.", + "dash.disabled": "вимкнено", + "dash.everyConnectedAccount": "кожен підключений обліковий запис", + "dash.idle": "простій", + "dash.ingestUrlCopied": "URL входу скопійовано.", + "dash.keyRefreshed": "Ключ трансляції оновлено з платформи.", + "dash.leaveUnchanged": "Залишити без змін", + "dash.leaveUnchangedNote": "«Залишити без змін» не надсилає взагалі нічого, тож платформа зберігає те, що має. Вибір «Увімк» або «Вимк» записує це значення.", + "dash.metaCategory": "Категорія", + "dash.metaDescription": "Опис", + "dash.metaTags": "Теги", + "dash.metaTitle": "Назва", + "dash.noDestinations": "Призначень поки немає. Додайте одне, потім виберіть, які звукові доріжки воно отримує.", + "dash.off": "Вимк", + "dash.pipeline": "Конвеєр", + "dash.pushToPlatforms": "Надіслати на платформи", + "dash.pushing": "Надсилання…", + "dash.reconnects": "Перепідключення", + "dash.relaySubscribers": "Підписники ретранслятора", + "dash.renditions": "Варіанти", + "dash.scheduledStart": "Запланований початок", + "dash.subtitle": "Один вхід, роздача багатьом: спільне відео та власний звук для кожного призначення.", + "dash.tagsPlaceholder": "ефір, хаус, діджей-сет", + "dash.theConnectedAccount": "підключений обліковий запис", + "dash.title": "Панель керування", + "dash.titlePlaceholder": "Пʼятничний вечірній сет", + "dash.uptime": "Час роботи", + "dash.youtubeStopsAcceptingTheseOnce": "YouTube перестає приймати це, щойно трансляція виходить зі стану «created» або «ready».", + "dest.audience": "Аудиторія", + "dest.audioBitrate": "Бітрейт аудіо", + "dest.audioCodec": "Аудіокодек", + "dest.channels": "Канали", + "dest.compliance": "Відповідність вимогам", + "dest.connectedAccount": "Підключений акаунт", + "dest.contentLabels": "Мітки вмісту", + "dest.copySource": "Копіювати вихідне відео", + "dest.createVariant": "Створити цей варіант", + "dest.customiseSummary": "Налаштувати для цього призначення", + "dest.fbBackupCost": "Подвоює вихідну смугу цього призначення.", + "dest.fbBackupHelp": "Надсилає другу копію цього потоку на резервний вхід Facebook, щоб обірваний зв’язок не обірвав трансляцію.", + "dest.fbBackupLabel": "Публікувати резервний потік", + "dest.fbBackupReconnect": "Увімкнення один раз перепідключає потік — тож вмикайте його до виходу в ефір.", + "dest.fbBoxIntro": "Надсилається тим самим викликом створення, що й «Аудиторія», тож діє те саме правило: застосовується один раз на початку трансляції та повністю ігнорується, якщо порожньо.", + "dest.fbBoxTitle": "Кроспостинг і кнопка пожертви у Facebook", + "dest.fbCrosspostAdd": "Додати Сторінку", + "dest.fbCrosspostAlsoPost": "Також опублікувати", + "dest.fbCrosspostHelp": "Числовий ID Сторінки з власної консолі Facebook (налаштування Сторінки або graph.facebook.com/me/accounts, коли ви увійшли як Сторінка) — пошуку тут немає, тож вставляйте саме id, а не назву чи URL. «Також опублікувати» публікує від імені цієї Сторінки, а не просто ділиться з нею трансляцією; якщо вимкнено, відбувається лише тихий репост.", + "dest.fbCrosspostLabel": "Кроспостинг на Сторінки", + "dest.fbCrosspostPageId": "ID Сторінки", + "dest.fbCrosspostRemove": "Прибрати цю Сторінку", + "dest.fbDonateHelp": "Це теж непрозорий id з консолі зборів Facebook, а не назва організації для пошуку. Залиште порожнім, щоб не додавати кнопку пожертви взагалі.", + "dest.fbDonateLabel": "ID організації для кнопки пожертви", + "dest.fbDonatePlaceholder": "ID організації", + "dest.fillFromPlatform": "Заповнити тим, що публікує ця платформа", + "dest.friends": "Друзі", + "dest.friendsOfFriends": "Друзі друзів", + "dest.giveUpAfter": "Здатися після (спроб)", + "dest.leaveOnFacebook": "Залишити як є у Facebook", + "dest.leaveOnYouTube": "Залишити як є на YouTube", + "dest.madeForKids": "Створено для дітей (COPPA)", + "dest.muxQueue": "Черга мультиплексування (пакети)", + "dest.muxQueuePair": "Ці два параметри працюють у парі: FFmpeg застосовує ліміт пакетів лише після того, як черга перевищить поріг у байтах, тож сам лише поріг нічого не дає і буде відхилений. Збільште обидва, якщо призначення повідомляє про помилки чергування — звуковий тракт тут має змінну затримку, бо нормалізація гучності читає наперед.", + "dest.name": "Назва", + "dest.namePlaceholder": "YouTube — основний канал", + "dest.notLinked": "Не прив’язано", + "dest.onlyMe": "Тільки я", + "dest.platform": "Платформа", + "dest.private": "Приватне", + "dest.public": "Публічне", + "dest.reconnecting": "Перепідключення", + "dest.retryAfter": "Повтор через (секунди)", + "dest.sharedEncodeAria": "Спільне кодування відео", + "dest.socketTimeout": "Тайм-аут сокета (секунди)", + "dest.streamKeyLabel": "Ключ трансляції", + "dest.transport": "Транспорт", + "dest.transportFile": "Локальний файл", + "dest.transportRtmp": "RTMP / RTMPS", + "dest.unlisted": "За посиланням", + "dest.urlFromDashboard": "URL із панелі", + "dest.useSharedEncode": "Використати спільне кодування відео", + "dest.varBitrate": "Бітрейт відео (кбіт/с)", + "dest.varFps": "Частота кадрів", + "dest.varHeight": "Висота", + "dest.varSourceHint": "джерело", + "dest.varWidth": "Ширина", + "dest.variantWarning": "Запускає друге кодування з вашого джерела на основі «{name}». Це не безкоштовний варіант, і він не змінює того, що отримують інші призначення цього кодування. Залиште поле порожнім, щоб зберегти значення джерела.", + "dest.videoTreatment": "Обробка відео", + "dest.visibility": "Видимість", + "hooks.allTriggers": "усе", "hooks.attempts": "спроб: {count}", + "hooks.attemptsLabel": "Спроби", "hooks.attemptsOne": "{count} спроба", - "hooks.timeout": "тайм-аут {seconds}с", - "hooks.allTriggers": "усе", - "hooks.triggerCount": "тригерів: {count}", - "hooks.triggerCountOne": "{count} тригер", - "hooks.enableAria": "Увімкнути {name}", + "hooks.colEnabled": "Увімк.", + "hooks.colEndpoint": "Ендпоінт", + "hooks.colTriggers": "Підписаний на", + "hooks.colWebhook": "Webhook", + "hooks.create": "Створити", + "hooks.created": "Webhook створено.", + "hooks.deleteBody": "Він одразу припиняє доставляти. Ендпоінт не змінюється, і hook можна створити знову — але його ключ підпису відновити неможливо.", + "hooks.deleteConfirm": "Видалити webhook", + "hooks.deleteFailed": "Не вдалося видалити hook.", + "hooks.deleteTitle": "Видалити цей webhook?", + "hooks.deleted": "Webhook видалено.", "hooks.deliveries": "Останні доставки", - "hooks.sendTest": "Надіслати тестову доставку", - "hooks.sendTestTitle": "Надіслати тестову доставку зараз", - "hooks.secretTitle": "Ключ підпису — скопіюйте зараз, показати його ще раз не можна", - "hooks.secretCopied": "Я його скопіював", - "hooks.httpStatus": "HTTP {status}", - "hooks.noResponse": "немає відповіді", + "hooks.deliveriesFailed": "Не вдалося прочитати останні доставки.", + "hooks.deliveryTitle": "Доставка", + "hooks.dialogIntro": "Один підписаний POST на перехід, по порядку. На відміну від сповіщення, нічого не об’єднується й не придушується — скрипт отримує кожен фронт.", "hooks.durationMs": "{ms} мс", + "hooks.editTitle": "Редагувати webhook", + "hooks.empty": "Webhook’ів немає. Додайте один, і polyemesis надішле POST із підписаним тілом JSON тієї миті, коли потік почнеться чи зупиниться або призначення підніметься чи впаде — одна доставка на перехід, по порядку, щоб скрипт міг відреагувати.", + "hooks.enableAria": "Увімкнути {name}", + "hooks.httpStatus": "HTTP {status}", + "hooks.loadFailed": "Не вдалося завантажити webhook’и.", + "hooks.name": "Назва", + "hooks.new": "Новий webhook", + "hooks.newTitle": "Новий webhook", "hooks.noDeliveries": "Поки нічого не доставлено. Доставки з’являться тут у міру переходів.", - "hooks.deliveryTitle": "Доставка", - "hooks.statSent": "Надіслано", - "hooks.statFailed": "Помилок", + "hooks.noResponse": "немає відповіді", + "hooks.payloadNote": "Payload {version}. Підпис у {header}, за міткою часу й тілом.", + "hooks.retriesHint": "Повтори блокують власну чергу цього ендпоінта, бо обіцяно саме порядок. Код 4xx ніколи не повторюється — ендпоінт, який каже, що запит хибний, скаже це знову.", + "hooks.saveFailed": "Не вдалося зберегти hook.", + "hooks.secretCopied": "Я його скопіював", + "hooks.secretTitle": "Ключ підпису — скопіюйте зараз, показати його ще раз не можна", + "hooks.sendTest": "Надіслати тестову доставку", + "hooks.sendTestTitle": "Надіслати тестову доставку зараз", + "hooks.signed": "підписаний", + "hooks.statDropped": "Відкинуто", "hooks.statEndpoints": "Ендпоінтів", + "hooks.statFailed": "Помилок", "hooks.statQueued": "У черзі", - "hooks.statDropped": "Відкинуто", "hooks.statRetries": "Повторів", - "hooks.payloadNote": "Payload {version}. Підпис у {header}, за міткою часу й тілом.", - "hooks.deleteTitle": "Видалити цей webhook?", - "hooks.deleteBody": "Він одразу припиняє доставляти. Ендпоінт не змінюється, і hook можна створити знову — але його ключ підпису відновити неможливо.", - "hooks.deleteConfirm": "Видалити webhook", - "hooks.editTitle": "Редагувати webhook", - "hooks.newTitle": "Новий webhook", - "hooks.dialogIntro": "Один підписаний POST на перехід, по порядку. На відміну від сповіщення, нічого не об’єднується й не придушується — скрипт отримує кожен фронт.", - "hooks.name": "Назва", - "hooks.url": "URL ендпоінта", - "hooks.urlMasked": "Збережений URL ніколи не показується повністю. Залиште замасковане значення, щоб зберегти його, або введіть новий URL, щоб замінити.", - "hooks.triggers": "Тригери", - "hooks.triggersNoneHint": "Якщо не вибрано нічого, діють усі тригери, зокрема ті, які додасть майбутній випуск.", + "hooks.statSent": "Надіслано", + "hooks.testFailed": "Ендпоінт не прийняв тестову доставку.", + "hooks.timeout": "тайм-аут {seconds}с", "hooks.timeoutLabel": "Тайм-аут (с)", - "hooks.attemptsLabel": "Спроби", - "hooks.retriesHint": "Повтори блокують власну чергу цього ендпоінта, бо обіцяно саме порядок. Код 4xx ніколи не повторюється — ендпоінт, який каже, що запит хибний, скаже це знову.", - "hooks.create": "Створити", - "hooks.triggerIngestPublished": "Потік почався", - "hooks.triggerIngestDisconnected": "Потік зупинено", - "hooks.triggerDestinationUp": "Призначення в ефірі", - "hooks.triggerDestinationDown": "Призначення зупинено", - "hooks.loadFailed": "Не вдалося завантажити webhook’и.", + "hooks.title": "Webhook’и життєвого циклу", "hooks.toggleFailed": "Не вдалося змінити hook.", - "hooks.deleted": "Webhook видалено.", - "hooks.deleteFailed": "Не вдалося видалити hook.", - "hooks.testFailed": "Ендпоінт не прийняв тестову доставку.", - "hooks.deliveriesFailed": "Не вдалося прочитати останні доставки.", + "hooks.triggerCount": "тригерів: {count}", + "hooks.triggerCountOne": "{count} тригер", + "hooks.triggerDestinationDown": "Призначення зупинено", + "hooks.triggerDestinationUp": "Призначення в ефірі", + "hooks.triggerIngestDisconnected": "Потік зупинено", + "hooks.triggerIngestPublished": "Потік почався", + "hooks.triggers": "Тригери", + "hooks.triggersNoneHint": "Якщо не вибрано нічого, діють усі тригери, зокрема ті, які додасть майбутній випуск.", + "hooks.unsigned": "БЕЗ ПІДПИСУ", "hooks.updated": "Webhook оновлено.", - "hooks.created": "Webhook створено.", - "hooks.saveFailed": "Не вдалося зберегти hook.", - "playlist.title": "Плейлист", + "hooks.url": "URL ендпоінта", + "hooks.urlMasked": "Збережений URL ніколи не показується повністю. Залиште замасковане значення, щоб зберегти його, або введіть новий URL, щоб замінити.", + "jobs.alwaysKeepNewest": "Завжди зберігати найновіші", + "jobs.automaticModel": "Автоматично (за цим обладнанням)", + "jobs.backToDefault": "Повернутися до типового режиму для цього виду", + "jobs.batteryFloor": "Нижній поріг заряду, %", + "jobs.broadcastGoing": "Триває ефір; відкладена робота поступається йому.", + "jobs.cancel": "Скасувати", + "jobs.cannotPause": "Ці види слід було б призупинити, але це неможливо, тож вони доводяться до кінця.", + "jobs.completed": "Завершено", + "jobs.concurrency": "Паралельність", + "jobs.couldNotReadTheJob": "Не вдалося прочитати чергу завдань.", + "jobs.couldNotSaveThePolicy": "Не вдалося зберегти політику.", + "jobs.cpuCeiling": "Стеля CPU, %", + "jobs.cpuResume": "Відновлення за CPU, %", + "jobs.defaultMode": "Типовий режим", + "jobs.defaultModel": "Типова модель", + "jobs.deferred": "Відкладено", + "jobs.failed": "Помилка", + "jobs.finished": "Завершені", + "jobs.global": "Глобально", + "jobs.gpuGateNote": "Цим обмежуються лише види, позначені як такі, що використовують GPU.", + "jobs.history": "Історія", + "jobs.historyRetention": "Зберігання історії", + "jobs.holdDeferredWorkBackOn": "Притримувати відкладені завдання на ноутбуці, що розряджається, нижче цього значення. 0 вимикає.", + "jobs.hostCpuLoad": "Завантаження CPU хоста", + "jobs.howManyJobsMayRun": "Скільки завдань може виконуватися одночасно. Одне — бо друге перекодування дає продуктивність, якої ніхто не просив, ціною ризику, на який ніхто не погоджувався.", + "jobs.inQueue": "У черзі", + "jobs.job": "Завдання", + "jobs.keepForDays": "Зберігати днів", + "jobs.machineSafety": "Безпека машини. Це єдиний обмежувач, якому підпорядковується й робота в реальному часі.", + "jobs.niceLevel": "Рівень nice", + "jobs.noFinished": "Завершених завдань поки немає.", + "jobs.noModels": "У каталозі моделей не знайдено жодного файлу моделі. Автоматичний режим завантажить обрану ним модель; зазначення конкретної моделі тут спрацює лише після появи відповідного файлу.", + "jobs.nothingNewStartsAboveThis": "Вище цього значення нічого нового не запускається. 0 вимикає обмеження.", + "jobs.nothingQueued": "У черзі порожньо. Постобробка надсилається з бібліотеки.", + "jobs.nothingWasOldEnoughTo": "Нічого достатньо старого для очищення не знайшлося. Спочатку зменште термін зберігання в «Політиці».", + "jobs.pauseAll": "Призупинити всі", + "jobs.policy": "Політика", + "jobs.policySaved": "Політику ресурсів збережено.", + "jobs.progress": "Прогрес", + "jobs.queuePaused": "Чергу призупинено.", + "jobs.queueResumed": "Чергу відновлено.", + "jobs.queued": "У черзі", + "jobs.recording": "Запис", + "jobs.releasedItWillStartAs": "Звільнено. Завдання запуститься, щойно звільниться слот.", + "jobs.removeFromHistory": "Видалити з історії", + "jobs.removeWindow": "Видалити вікно", + "jobs.resumeAll": "Відновити всі", + "jobs.retry": "Спробувати ще раз", + "jobs.retryTitle": "Запустити знову зі свіжим запасом спроб", + "jobs.runNow": "Виконати зараз", + "jobs.runNowTitle": "Виконати це завдання зараз, ігноруючи політику ресурсів", + "jobs.running": "Виконується", + "jobs.savedTheConcurrencyChangeTakes": "Збережено. Зміна паралельності набуде чинності після перезапуску сервера — черга фіксує її під час старту.", + "jobs.state": "Стан", + "jobs.stopsEverythingRealtimeIncludedA": "Зупиняє все, включно з реальним часом: CPU у тротлінгу вже почав псувати потік. 0 вимикає.", + "jobs.subtitle": "Фонова робота: розшифровка, проксі, мініатюри та стиснення архіву.", + "jobs.thatDidNotWork": "Не вийшло.", + "jobs.thermalCeiling": "Теплова стеля, °C", + "jobs.thisManyFinishedJobsSurvive": "Стільки завершених завдань зберігається незалежно від їхнього віку.", + "jobs.title": "Завдання", + "jobs.transcription": "Розшифровка", + "jobs.unsavedChanges": "Незбережені зміни.", + "jobs.whereTheCeilingReleasesThe": "Значення, за якого стеля відпускає. Розрив між ними — це гістерезис, що не дає обмеженню коливатися.", + "jobs.whisperMissing": "whisper.cpp тут не встановлено, тож поки це ні на що не впливає. Задати все одно варто: вибір набуде чинності одразу після встановлення.", + "jobs.why": "Причина", + "jobs.windowEnd": "Кінець вікна", + "jobs.windowStart": "Початок вікна", + "jobs.yieldNote": "Важка робота ставиться в чергу й поступається живому потоку. Втрачений кадр не відновити; розшифровка, що прийшла на годину пізніше, не коштує нічого.", + "lib.anySpeaker": "Будь-який мовець", + "lib.anyTag": "Будь-який тег", + "lib.autoGrouped": "Згруповано автоматично. Редагування робить групування вашим.", + "lib.autoGroupedNote": "Цю сесію було згруповано автоматично. Збереження робить її вашою, і прохід групування перестане її переписувати.", + "lib.bestMatch": "Найкращий збіг", + "lib.clearSearch": "Очистити пошук", + "lib.clip": "Кліп", + "lib.clipTitle": "Вирізати кліп із цього запису", + "lib.collapseSession": "Згорнути сесію", + "lib.couldNotLoadTheLibrary": "Не вдалося завантажити бібліотеку.", + "lib.couldNotLoadTheSession": "Не вдалося завантажити сегменти сесії.", + "lib.couldNotQueueTheProxy": "Не вдалося поставити проксі в чергу.", + "lib.couldNotRegroup": "Не вдалося перегрупувати.", + "lib.couldNotSaveTheSession": "Не вдалося зберегти сесію.", + "lib.delete": "Видалити", + "lib.deleteDescription": "Файл буде видалено з диска разом із проксі, мініатюрами та розшифровкою. Цю дію не можна скасувати.", + "lib.deleteTitle": "Видалити цей запис?", + "lib.downloadMaster": "Завантажити основний файл", + "lib.downloadMasterTitle": "Завантажити багатодоріжковий основний файл", + "lib.editSession": "Редагувати сесію", + "lib.expandSession": "Розгорнути сесію", + "lib.from": "Від", + "lib.generateProxy": "Створити проксі", + "lib.generateProxyTitle": "Створити проксі, придатний для відтворення у браузері", + "lib.mostRecent": "Спочатку нові", + "lib.noProxyYet": "Проксі ще немає — відкрийте, щоб створити", + "lib.noSegments": "Тут немає сегментів.", + "lib.noSessionMatch": "Жодна сесія не відповідає цим фільтрам.", + "lib.nothingRecorded": "Поки нічого не записано. Сесії зʼявляються в міру того, як рекордер пише сегменти.", + "lib.oldestFirst": "Спочатку старі", + "lib.playProxy": "Відтворити проксі", + "lib.proxyAlreadyQueued": "Проксі вже в черзі.", + "lib.proxyQueued": "Проксі поставлено в чергу.", + "lib.recordingDeleted": "Запис видалено.", + "lib.regroupTitle": "Повторно виконує групування. Воно адитивне та ідемпотентне: розділене вручну групування воно ніколи не обʼєднає.", + "lib.searchPlaceholder": "Шукати в усьому сказаному, по всіх записах…", + "lib.searchTranscripts": "Пошук у розшифровках", + "lib.searchable": "Доступні для пошуку", + "lib.searchableOnly": "Сегменти з розшифровкою, доступні для пошуку", + "lib.session": "Сесія", + "lib.sessionDescription": "Опис", + "lib.sessionSaved": "Сесію збережено.", + "lib.sessionTags": "Теги", + "lib.sessionTitle": "Назва", + "lib.subtitle": "Кожен ефір — з пошуком. Кожен мікрофон записано на власну доріжку, тож розшифровка вже знає, хто це сказав.", + "lib.tag": "Тег", + "lib.tagsPlaceholder": "через, кому", + "lib.title": "Бібліотека", + "lib.transcribe": "Розшифрувати", + "lib.transcribeEachTrackOnIts": "Розшифровувати кожну доріжку окремо — саме це дає привʼязку до мовця", + "lib.ungrouped": "Незгруповані сегменти", + "meters.deviation": "Відхилення", + "meters.idle": "простій", + "meters.integrated": "Інтегральна", + "meters.loudness": "Відповідність гучності", + "meters.metering": "вимірювання", + "meters.momentary": "Миттєва", + "meters.monitorOff": "Монітор вимкнено. Увімкніть його, щоб виміряти, що віддає кожне призначення.", + "meters.noSignal": "немає сигналу на цій доріжці", + "meters.nothingToMeasure": "Поки нема чого вимірювати. Кожне працююче призначення отримує власний аналізатор EBU R128 для того міксу, який воно отримує.", + "meters.range": "Діапазон", + "meters.shortTerm": "Короткочасна", + "meters.subtitle": "Кожен канал кожної вхідної доріжки, прямо з ретранслятора.", + "meters.title": "Індикатори звуку", + "meters.truePeak": "Істинний пік", + "meters.waitingForStream": "очікування потоку", + "mon.allProcesses": "Усі процеси", + "mon.bitrateChart": "Бітрейт входу — останні 30 хвилин", + "mon.chooseDestination": "Виберіть призначення…", + "mon.clearLog": "Очистити журнал", + "mon.clearedThisDestinationIsBack": "Скинуто. Це призначення знову використовує згенеровану команду.", + "mon.dryRun": "Пробний запуск", + "mon.dryRunInvalid": "FFmpeg відхилив аргумент", + "mon.dryRunOk": "FFmpeg прийняв команду", + "mon.extraInput": "Додаткові аргументи входу", + "mon.extraOutput": "Додаткові аргументи виходу", + "mon.ffmpegLog": "Журнал FFmpeg", + "mon.fromRunningProcess": "з робочого процесу", + "mon.fullCommand": "Повністю розгорнута команда", + "mon.hostCpu": "Процесор хоста", + "mon.ingestLoss": "Втрати на вході", + "mon.memory": "Памʼять", + "mon.nothingProvenEitherWay": "Нічого не підтверджено", + "mon.pid": "PID", + "mon.processes": "Процеси", + "mon.rebuilt": "перезібрано", + "mon.relayDrops": "Втрати ретранслятора", + "mon.relayIn": "Вхід ретранслятора", + "mon.resolveHint": "Розгорніть команду, щоб перед застосуванням побачити точно, що буде виконано.", + "mon.restarts": "Перезапуски", + "mon.subtitle": "Стан процесів, ресурси хоста та живий журнал FFmpeg.", + "mon.title": "Моніторинг", + "mon.uptime": "Час роботи", + "nav.automation": "Автоматизація", + "nav.chat": "Чат", + "nav.clips": "Кліпи", + "nav.dashboard": "Панель керування", + "nav.jobs": "Завдання", + "nav.library": "Бібліотека", + "nav.meters": "Індикатори звуку", + "nav.monitoring": "Моніторинг", + "nav.playout": "Playout", + "nav.recordings": "Записи", + "nav.renditions": "Варіанти", + "nav.routing": "Routing", + "nav.settings": "Налаштування", + "nav.sources": "Джерела", + "play.acrossEveryRungTheBackstop": "По всіх щаблях. Обмежувач, що не дає довгій трансляції заповнити диск.", + "play.allowCrossOrigin": "Дозволити міжсайтові запити", + "play.allowCrossOriginPlayback": "Дозволити міжсайтове відтворення", + "play.audioBitrate": "Бітрейт звуку (кбіт/с)", + "play.audioTrack": "Звукова доріжка", + "play.basicPasswordNote": "Плеєри, які не вміють переходити за посиланням (телеприставка, CDN), можуть натомість надсилати той самий секрет як пароль HTTP basic.", + "play.boundsTheViewerTableViewers": "Обмежує таблицю глядачів. Глядачі понад це число обслуговуються, просто не враховуються.", + "play.carriesThePlaybackTokenAnyone": "Містить токен відтворення. Дивитися може будь-хто, хто має цей URL.", + "play.clipboardUnavailable": "Буфер обміну недоступний; виділіть і скопіюйте вручну", + "play.couldNotLoadPlayout": "Не вдалося завантажити Playout", + "play.disabled": "Вимкнено", + "play.diskCap": "Межа на диску (МБ)", + "play.dvrWindow": "Вікно DVR (с)", + "play.embedCode": "Код для вбудовування", + "play.enable": "Увімкнути playout", + "play.enabled": "Увімкнено", + "play.forObsACdnOr": "Для OBS, CDN або будь-якого плеєра, що розуміє HLS.", + "play.format": "Формат", + "play.hls": "HLS", + "play.hlsDash": "HLS та DASH", + "play.hlsPlaylist": "Плейлист HLS", + "play.idleTimeout": "Тайм-аут простою глядача (с)", + "play.linkReset": "Створено нове посилання відтворення. Попереднє більше не працює.", + "play.linksProtected": "Ці посилання працюють лише для адміністратора, який увійшов, доки ви не ввімкнете «Публічно».", + "play.linksPublic": "Ці посилання працюють без облікового запису.", + "play.liveWindow": "Живе вікно (сегменти)", + "play.metaDescription": "Опис", + "play.metaTitle": "Назва", + "play.mustExceedOneSegmentOr": "Має перевищувати довжину одного сегмента, інакше глядачі зникатимуть між опитуваннями.", + "play.name": "Назва", + "play.noCredentialNote": "Жодних облікових даних не потрібно. Підходить лише для потоку, який ви маєте намір показати всьому інтернету.", + "play.noRungs": "Щаблів поки немає. Додайте хоча б один, щоб щось публікувати.", + "play.onDisk": "На диску", + "play.oneStereoAacTrackPer": "По одній стереодоріжці AAC на щабель.", + "play.packaging": "Пакування", + "play.passthrough": "Вхід (без перекодування)", + "play.peak": "Пік", + "play.playerPage": "Сторінка плеєра", + "play.posterNote": "Показується над відео на публічній сторінці. Постер — це недавній ключовий кадр, оновлюється автоматично.", + "play.private": "Приватно.", + "play.protectedNote": "Відтворення типово захищене. Його відкриття — окремий свідомий крок.", + "play.protection": "Захист", + "play.public": "Публічно", + "play.resetFailed": "Не вдалося скинути", + "play.restartNote": "Зміна будь-якого з цих параметрів перезапускає мультиплексори, що перериває перегляд на кілька секунд.", + "play.rungNote": "Кожен щабель копіює відео свого варіанта біт у біт, тож додавання щабля коштує одного мультиплексора й ніколи — другого кодування відео. Від щабля до щабля відрізняється лише звукова доріжка; маршрутизація за призначеннями лишається недоторканою.", + "play.segmentLength": "Довжина сегмента (с)", + "play.serveWithoutSession": "Обслуговувати глядачів без сесії", + "play.sessionCap": "Межа кількості сесій", + "play.sessions": "Сесії", + "play.share": "Поділитися", + "play.shorterIsLowerLatencyAnd": "Коротше — менша затримка й частіше оновлюється плейлист.", + "play.subtitle": "Віддавайте потік глядачам напряму, як публічний HLS.", + "play.threeIsTheMinimumA": "Три — мінімум, на якому плеєр може буферизувати.", + "play.title": "Playout", + "play.unlisted": "За посиланням.", + "play.untitled": "Потік без назви", + "play.videoSource": "Джерело відео", + "play.watchingNow": "Дивляться зараз", + "play.whoCanWatch": "Хто може дивитися", + "player.copied": "Скопійовано", + "player.copy": "Копіювати", + "player.loading": "Завантаження…", + "player.missingToken": "У відкритому посиланні бракує токена відтворення або токен було змінено. Попросіть у того, хто його надіслав, актуальне посилання.", + "player.notLive": "Зараз ефіру немає", + "player.viewer": "глядач", + "player.viewers": "глядачів", + "player.willStart": "Ця сторінка почне відтворення сама, щойно трансляція розпочнеться.", + "playlist.choose": "Виберіть файл", + "playlist.chooseAria": "Виберіть файл, щоб додати", "playlist.empty": "У черзі поки порожньо.", + "playlist.hint": "Завантажуються в розділі Media. Елемент, названий цим плейлистом, який згодом втратить свій файл, з’явиться вище як такий, що потребує уваги, замість того щоб мовчки відмовити під час наступного перемикання.", "playlist.itemsAria": "Елементи плейлиста", - "playlist.stateReady": "Готово", - "playlist.stateTranscoding": "Перекодування", - "playlist.stateAttention": "Потребує уваги", - "playlist.stateUnsaved": "Ще не збережено", "playlist.missingUpload": "Цей файл більше не знайдено.", - "playlist.moveUp": "Перемістити {name} вгору", "playlist.moveDown": "Перемістити {name} вниз", - "playlist.remove": "Видалити {name}", - "playlist.choose": "Виберіть файл", - "playlist.chooseAria": "Виберіть файл, щоб додати", + "playlist.moveUp": "Перемістити {name} вгору", "playlist.noUploads": "Немає доступних файлів", - "playlist.hint": "Завантажуються в розділі Media. Елемент, названий цим плейлистом, який згодом втратить свій файл, з’явиться вище як такий, що потребує уваги, замість того щоб мовчки відмовити під час наступного перемикання.", - "dest.fbBoxTitle": "Кроспостинг і кнопка пожертви у Facebook", - "dest.fbBoxIntro": "Надсилається тим самим викликом створення, що й «Аудиторія», тож діє те саме правило: застосовується один раз на початку трансляції та повністю ігнорується, якщо порожньо.", - "dest.fbBackupLabel": "Публікувати резервний потік", - "dest.fbBackupHelp": "Надсилає другу копію цього потоку на резервний вхід Facebook, щоб обірваний зв’язок не обірвав трансляцію.", - "dest.fbBackupCost": "Подвоює вихідну смугу цього призначення.", - "dest.fbBackupReconnect": "Увімкнення один раз перепідключає потік — тож вмикайте його до виходу в ефір.", - "dest.fbCrosspostLabel": "Кроспостинг на Сторінки", - "dest.fbCrosspostPageId": "ID Сторінки", - "dest.fbCrosspostAlsoPost": "Також опублікувати", - "dest.fbCrosspostRemove": "Прибрати цю Сторінку", - "dest.fbCrosspostAdd": "Додати Сторінку", - "dest.fbCrosspostHelp": "Числовий ID Сторінки з власної консолі Facebook (налаштування Сторінки або graph.facebook.com/me/accounts, коли ви увійшли як Сторінка) — пошуку тут немає, тож вставляйте саме id, а не назву чи URL. «Також опублікувати» публікує від імені цієї Сторінки, а не просто ділиться з нею трансляцією; якщо вимкнено, відбувається лише тихий репост.", - "dest.fbDonateLabel": "ID організації для кнопки пожертви", - "dest.fbDonatePlaceholder": "ID організації", - "dest.fbDonateHelp": "Це теж непрозорий id з консолі зборів Facebook, а не назва організації для пошуку. Залиште порожнім, щоб не додавати кнопку пожертви взагалі." + "playlist.remove": "Видалити {name}", + "playlist.stateAttention": "Потребує уваги", + "playlist.stateReady": "Готово", + "playlist.stateTranscoding": "Перекодування", + "playlist.stateUnsaved": "Ще не збережено", + "playlist.title": "Плейлист", + "rec.delete": "Видалити", + "rec.deleteDescription": "Файл буде видалено з диска разом із проксі, мініатюрами та розшифровкою. Цю дію не можна скасувати.", + "rec.deleteFailed": "Не вдалося видалити запис.", + "rec.deleteTitle": "Видалити цей запис?", + "rec.deleted": "Запис видалено.", + "rec.disabled": "вимкнено", + "rec.disk": "Диск", + "rec.diskRecordings": "Записи", + "rec.download": "Завантажити", + "rec.downloadNamed": "Завантажити {name}", + "rec.duration": "Тривалість", + "rec.empty": "Записів ще немає. Увімкніть запис праворуч і розпочніть трансляцію.", + "rec.enabled": "Увімкнено", + "rec.file": "Файл", + "rec.free": "Вільно", + "rec.haltedDefault": "Запис зупинено: на томі менше вільного місця, ніж задано нижнім порогом.", + "rec.haltedRestarts": "Рекордер запуститься сам, щойно місце звільниться.", + "rec.help.enabled": "Записує кожен вхід на диск просто під час трансляції, зберігаючи всі звукові доріжки окремо й без перекодування. Запис не залежить від того, що ви надсилаєте на платформи: його вимкнення не впливає на прямий ефір, а ввімкнення витрачає місце на диску, а не смугу віддачі.", + "rec.help.maxAgeHours": "Видаляє записи, старші за вказану кількість годин, незалежно від загального розміру. Корисно, коли потрібні лише останні день-два. 0 — зберігати все, доки не видалить інше обмеження.", + "rec.help.maxGb": "Верхня межа загального розміру записів, які зберігає polyemesis. За її перевищення найстаріші видаляються, доки сума знову не вміститься. 0 — без обмеження розміру; тоді зростання обмежують лише вік або нижній поріг вільного місця.", + "rec.help.minFreeGb": "Жорстка зупинка: запис припиняється, коли вільного місця на томі стає менше за вказане, і відновлюється сам, щойно місце звільниться. Це єдине обмеження, що враховує файли, яких polyemesis не створював, тож саме воно справді захищає спільний диск від переповнення.", + "rec.help.segmentSeconds": "Скільки триває кожен файл, перш ніж рекордер почне наступний. Коротші сегменти обмежують втрати при збої та спрощують перенесення окремих файлів, але їх стає більше. Розрізи припадають на ключовий кадр, тому реальна довжина коливається приблизно на секунду.", + "rec.help.stemFiles": "Додатково записує кожну звукову доріжку окремим файлом поруч з основним. Корисно, якщо ви монтуєте потім і хочете мати ведучого, гостей і звук гри на різних каналах. Обсяг запису сесії зростає приблизно у стільки разів, скільки доріжок, тому типово вимкнено.", + "rec.help.stemFormat": "Обидва варіанти без втрат, тож якість не страждає в жодному. FLAC приблизно вдвічі менший і підходить, якщо тільки якийсь інструмент у вашому процесі його не читає; WAV відкривається будь-чим. Доріжки, ширші за 8 каналів, автоматично переходять на WAV.", + "rec.hideStems": "Сховати окремі доріжки", + "rec.keepForever": "0 = зберігати завжди.", + "rec.loadFailed": "Не вдалося завантажити записи.", + "rec.maxAgeHours": "Видаляти через (годин)", + "rec.maxGb": "Зберігати не більше (ГБ)", + "rec.minFreeGb": "Зупинити за вільним місцем нижче (ГБ)", + "rec.minFreeNote": "Обмеження вище стосуються лише того, що записав polyemesis; усе інше на томі все одно може його заповнити. 0 = без нижнього порогу.", + "rec.noSizeLimit": "0 = без обмеження розміру.", + "rec.recording": "триває запис", + "rec.retentionNote": "Очищення виконується кожні 30 секунд: спершу найстаріші, за віком, потім за загальним розміром. Сегмент, який записується зараз, ніколи не видаляється.", + "rec.saveFailed": "Не вдалося зберегти налаштування.", + "rec.savePolicy": "Зберегти правила зберігання", + "rec.saved": "Налаштування запису збережено.", + "rec.segmentSeconds": "Довжина сегмента (секунди)", + "rec.segments": "Сегменти", + "rec.settingsTitle": "Запис", + "rec.showStems": "Показати окремі доріжки", + "rec.size": "Розмір", + "rec.started": "Розпочато", + "rec.stemFiles": "Файли окремих доріжок", + "rec.stemFlac": "FLAC — без втрат, приблизно вдвічі менший", + "rec.stemFormat": "Формат окремих доріжок", + "rec.stemNote": "Записується тим самим процесом, що й основний файл, тож кожна окрема доріжка лишається вирівняною за семплами з рештою. Доріжки, ширші за 8 каналів, самі переходять на WAV.", + "rec.stemWav": "WAV — без втрат, відкривається будь-де", + "rec.stems": "Окремі доріжки", + "rec.stemsTitle": "Файлів окремих доріжок: {count}, вирівняні за семплами з основним файлом", + "rec.stemsUsage": "Окремі доріжки ({count} файлів)", + "rec.subtitle": "Повний багатодоріжковий архів: кожна вхідна звукова доріжка збережена без перекодування.", + "rec.title": "Записи", + "rec.tracks": "Доріжки", + "rec.tracksTitle": "Збережено звукових доріжок: {count}", + "rec.used": "Зайнято", + "rec.volume": "Том", + "rend.aspectBlurpad": "Розмите заповнення", + "rend.aspectBlurpadHint": "Заповнює решту розмитою копією самого кадру, обрізаною за розміром. Саме таке рішення закріпилося для вертикальних трансляцій.", + "rend.aspectCrop": "Обрізати за розміром", + "rend.aspectCropHint": "Обрізає по центру до цільової форми, потім масштабує. Обʼєкти зберігають екранний розмір; краї кадру втрачаються.", + "rend.aspectHandling": "Обробка співвідношення сторін", + "rend.aspectPad": "Леттербокс", + "rend.aspectPadHint": "Масштабує весь кадр за розміром і заповнює решту суцільним кольором. Нічого не втрачається, але джерело 16:9 на полотні 9:16 — це переважно смуги.", + "rend.aspectStretch": "Розтягнути за розміром", + "rend.aspectStretchHint": "Масштабує до цільового розміру й допускає спотворення картинки, якщо пропорції джерела не збігаються. Саме так працював кожен варіант до появи інших режимів.", + "rend.boxColour": "Колір підкладки", + "rend.boxOpacity": "Непрозорість підкладки (%)", + "rend.builtInFont": "Вбудований типовий", + "rend.burnedText": "Текст, вписаний у кадр", + "rend.chooseHardware": "Виберіть апаратний кодувальник або менший розмір виводу.", + "rend.colourBlack": "black", + "rend.colourWhite": "white", + "rend.costBeyond": "Програмне кодування цього в реальному часі не під силу більшості машин", + "rend.costBeyondDetail": "{label} — це {mpps} на одне кодування. x264 за такого розміру зазвичай програє перегони реальному часу, а варіант, що відстає, втрачає кадри для кожного призначення, яке його читає, — і дізнаєтеся ви про це посеред ефіру.{machine} {escape}", + "rend.costBusy": "Очікуйте, що кілька ядер будуть зайняті", + "rend.costBusyDetail": "{label} — це {mpps} через {encoder}, тобто важке кодування в реальному часі.{machine} Стежте за швидкістю кодувальника в ефірі: нижче 1,0× він відстає, і призначення під ним почнуть втрачати кадри. {escape}", + "rend.costMachineCores": " Ця машина повідомляє про {cores} логічних ядер.", + "rend.costModest": "Помірне програмне кодування", + "rend.costModestDetail": "{label} — це {mpps}, що під силу більшості машин.{machine} Це все одно одне справжнє кодування — спільне для кожного призначення, яке його вибирає, але не безкоштовне.", + "rend.costNoNumbers": "Вартість поки не можна оцінити", + "rend.costNoNumbersDetail": "Джерело ще не зондовано, тож за «як у джерелі» немає жодних цифр. Запустіть вхід, і це заповниться.", + "rend.costOffloaded": "Передано на {encoder}", + "rend.costOffloadedDetail": "{label} ({mpps}) виконується на апаратному кодувальнику GPU, тож майже не навантажує CPU. Якість і керування бітрейтом різняться залежно від драйвера, і кожен чип має власну стелю — перевірте результат до важливого ефіру.", + "rend.create": "Створити", + "rend.created": "Варіант створено. Виберіть його в призначенні, щоб запустити кодування.", + "rend.custom": "Власне…", + "rend.deintAll": "Кожен кадр", + "rend.deintAllHint": "Для джерел, які є черезрядковими, але не повідомляють про це. Багато SDI-мостів і плат захоплення позначають усе як прогресивне незалежно від того, що їм подали, і на них «Лише черезрядкові кадри» нічого не робить і має вигляд зламаного налаштування.", + "rend.deintAuto": "Лише черезрядкові кадри", + "rend.deintAutoHint": "Зачіпає лише кадри, позначені джерелом як черезрядкові, тож прогресивні кадри проходять без змін. Правильний вибір для будь-якого змішаного матеріалу.", + "rend.deintOff": "Вимк", + "rend.deintOffHint": "Підходить для прогресивних джерел, які є майже в усіх. Деінтерлейсинг такого джерела лише розмиває картинку без користі.", + "rend.deinterlace": "Деінтерлейсинг", + "rend.deleteFailed": "Не вдалося видалити варіант.", + "rend.deleteFallbackMany": "{count} призначень переходять на передавання без перекодування й отримають відео джерела без змін. Перевірте, що джерело досі відповідає вимогам кожного майданчика.", + "rend.deleteFallbackOne": "{count} призначення переходить на передавання без перекодування й отримає відео джерела без змін. Перевірте, що джерело досі відповідає вимогам кожного майданчика.", + "rend.deleteNoDestinations": "Цей варіант ніде не вибрано, тому його видалення нічого не змінить для жодного призначення.", + "rend.deleteRenditionTitle": "Видалити {name}?", + "rend.editRendition": "Редагувати варіант", + "rend.empty": "Варіантів немає. Кожне призначення працює без перекодування, і це саме те, що потрібно, доки якесь із них не зможе прийняти джерело — наприклад, вхід 4K60, який Twitch, Kick або X не приймуть. Додайте варіант і спрямуйте такі призначення на нього.", + "rend.encoder": "Кодувальник", + "rend.encoderPreset": "Пресет кодувальника", + "rend.encoderRelatedFailed": "{name} не пропонується, бо повʼязаний кодувальник тут не пройшов перевірку.", + "rend.encoderTestFailed": "{name} не пройшов тестове кодування одного кадру на цій машині.", + "rend.encoderTestOk": "{name} закодував тестовий кадр на цій машині.", + "rend.evenNote": "0 за віссю зберігає значення джерела та співвідношення сторін — задайте лише висоту, щоб масштабувати. Обидва значення мають бути парними: у H.264 та HEVC немає площини кольоровості непарного розміру.", + "rend.font": "Шрифт", + "rend.frameRate": "Частота кадрів", + "rend.gop": "GOP", + "rend.gpuUnopenable": "GPU присутній, але цей процес не може його відкрити", + "rend.hardwareFallback": "апаратний кодувальник", + "rend.hardwareScanFound": "Що виявило сканування обладнання", + "rend.height": "Висота", + "rend.hevcWarning": "Цей кодувальник видає HEVC. Більшість призначень RTMP приймають лише H.264 — вибирайте його тільки для призначення, яке повідомило, що приймає HEVC.", + "rend.keyframeInterval": "Інтервал ключових кадрів", + "rend.letterboxColour": "Колір полів", + "rend.live": "В ефірі", + "rend.loadFailed": "Не вдалося завантажити варіанти.", + "rend.marginX": "Відступ X (%)", + "rend.marginY": "Відступ Y (%)", + "rend.name": "Назва", + "rend.namePlaceholder": "1080p60 для Twitch та Kick", + "rend.needsBothAWidthAnd": "Потрібні і ширина, і висота — за однієї вільної осі масштабування й так зберігає пропорції.", + "rend.needsBothAxes": "Потрібні і ширина, і висота", + "rend.newRendition": "Новий варіант", + "rend.noEncoder": "Цей FFmpeg не повідомив про жоден придатний відеокодувальник.", + "rend.noHardwareEscape": "На цій машині не працює жоден апаратний кодувальник, тож переносити навантаження нікуди — єдиний важіль тут це менший розмір виводу. Повторіть визначення обладнання, якщо ви відтоді встановили драйвер або пробросили GPU у контейнер.", + "rend.notProbed": "На цій машині ще нічого не кодувалося для перевірки, тож цей список показує, що містить збірка FFmpeg, а не те, на що здатне обладнання. Запустіть повторне визначення, щоб зʼясувати.", + "rend.notRunning": "Не виконується", + "rend.note": "Нотатка", + "rend.noteDuplicateFps": "Джерело йде з частотою {srcFps} fps, тому {fps} fps лише дублює кадри.", + "rend.notePlaceholder": "Для чого потрібен цей рівень.", + "rend.noteUpscale": "Джерело — {width}×{height}. Збільшення навантажує CPU й не додає деталей.", + "rend.opacity": "Непрозорість (%)", + "rend.percentNote": "Усі значення задаються у відсотках від кадра, тож ті самі налаштування правильні і для рівня 16:9, і для 9:16. За центрованою віссю відступи ігноруються.", + "rend.preset": "Пресет", + "rend.presetPlaceholder": "veryfast", + "rend.presetStartingPoint": "Відправна точка — актуальні обмеження уточнюйте в майданчика.", + "rend.redetectFailed": "Не вдалося повторно визначити обладнання.", + "rend.redetectTitle": "Повторити сканування GPU та закодувати по одному тестовому кадру кожним кодувальником. Займає кілька секунд.", + "rend.redetectedNoHardware": "Обладнання визначено заново. Жоден апаратний кодувальник не пройшов тестове кодування на цій машині.", + "rend.resolution": "Роздільна здатність", + "rend.restartEncode": "Перезапустити це кодування та призначення, які його читають", + "rend.restartFailed": "Не вдалося перезапустити кодування.", + "rend.restarting": "{name} перезапускається. Його призначення перепідключаються разом із ним.", + "rend.save": "Зберегти", + "rend.saveFailed": "Не вдалося зберегти варіант.", + "rend.seedNote": "Вони лише заповнюють форму початковими значеннями — правте що завгодно перед збереженням. Обмеження платформ змінюються й залежать від партнерського статусу.", + "rend.signalError": "Помилка", + "rend.signalIdle": "Очікування", + "rend.sizePct": "Розмір (% від висоти)", + "rend.softwareEncoderFallback": "програмний кодувальник", + "rend.speed": "Швидкість", + "rend.subtitle": "Одне спільне кодування відео, багато призначень. Звук тут ніколи не зачіпається.", + "rend.target": "Ціль", + "rend.textColour": "Колір тексту", + "rend.textPlaceholder": "МОЯ СТАНЦІЯ", + "rend.title": "Варіанти", + "rend.unused": "Його поки не вибрало жодне призначення, тож нічого не кодується. Виберіть його в призначенні, щоб запустити кодування.", + "rend.uptime": "Час роботи", + "rend.videoBitrate": "Бітрейт відео", + "rend.waitingSnapshot": "Очікування першого живого знімка.", + "rend.watermarkImage": "Зображення водяного знака", + "rend.watermarkPlaceholder": "overlays/logo.png", + "rend.width": "Ширина", + "rend.widthPct": "Ширина (% від кадра)", + "route.attack": "Атака", + "route.audio": "звук", + "route.audioAndVideoGoOut": "Звук і відео йдуть разом.", + "route.audioLaterThanVideo": "Звук пізніше за відео", + "route.audioRouting": "Маршрутизація звуку", + "route.audioRoutingDesc": "Виберіть точно, які вхідні доріжки отримує кожне призначення.", + "route.autoLimitWhen2Tracks": "Авто (обмежувати за 2+ доріжок)", + "route.commentaryLanguage": "Мова коментаря", + "route.couldNotApplyThePreset": "Не вдалося застосувати пресет.", + "route.couldNotCompileThisRouting": "Не вдалося скомпілювати цю маршрутизацію.", + "route.couldNotLoadDestinations": "Не вдалося завантажити призначення.", + "route.couldNotSaveTheRouting": "Не вдалося зберегти маршрутизацію.", + "route.couldNotSaveTrackRoles": "Не вдалося зберегти ролі доріжок.", + "route.custom": "Власне…", + "route.delayDirection": "Напрямок затримки", + "route.destinations": "Призначення", + "route.duckingNote": "Приглушувати одну групу доріжок, коли звучить інша, — мікрофон поверх музичної підкладки. Вимкнено означає, що кожна вибрана доріжка сумується з власним підсиленням, без змін.", + "route.duckingTriggerNote": "Тригер не мусить входити в мікс цього призначення — мікрофон може приглушувати тут музику, тоді як надсилається лише музика.", + "route.enableDucking": "Увімкнути приглушення", + "route.everyTrackReaches": "Кожна вибрана доріжка доходить до цього призначення, включно з музикою.", + "route.excludeMusic": "Виключити музику", + "route.keepMusic": "Залишити музику", + "route.keepMusicOut": "Не пропускати музику в це призначення", + "route.labelTracks": "Позначити доріжки", + "route.langPlaceholder": "мова — наприклад es", + "route.limiter04DbfsCeiling": "Лімітер (стеля −0,4 dBFS)", + "route.localOnlyNote": "Цей сервер поки не зберігає ролі доріжок, тож вони лишаються в цьому браузері. Усе інше на сторінці зберігається як звичайно.", + "route.loudnessTarget": "Цільова гучність", + "route.lufsBroadcast": "EBU R128 — ефір", + "route.measuredOverTheWholeProgramme": "Вимірюється по всій програмі, а не миттєво: тихі фрагменти лишаються тихими. Це ті значення, на які націлені власні нормалізатори майданчиків, тому влучання в одне з них і є тим, що не дає майданчику приглушити вас.", + "route.mixMatrix": "Матриця міксу", + "route.musicPolicy": "Політика музики", + "route.noTarget": "Без цілі (не чіпати рівні)", + "route.normalizeLoudnorm": "Гучність (EBU R128)", + "route.normalizeOff": "Вимк", + "route.off": "вимк", + "route.offset": "Зсув", + "route.on": "увімк", + "route.platformDefault": "Типово для платформи", + "route.podcastSpokenWord": "Подкаст / розмовний жанр", + "route.presetApplied": "Пресет застосовано. Перевірте його, потім збережіть.", + "route.range": "Діапазон", + "route.ratio": "Співвідношення", + "route.release": "Відновлення", + "route.result": "Результат", + "route.role51": "5.1", + "route.roleClean": "чистий", + "route.roleMic": "мікрофон", + "route.roleMusic": "музика", + "route.rolesNote": "Ролі описують вхід, а не це призначення, і зберігаються самі по собі. Нічого не фільтрується, доки призначення нижче про це не попросить.", + "route.simple": "Простий", + "route.target": "Ціль", + "route.targetRole": "ціль", + "route.thisDestinationSPlatform": "Майданчик цього призначення", + "route.threshold": "Поріг", + "route.title": "Routing", + "route.triggerRole": "тригер", + "route.truePeak": "Істинний пік", + "route.video": "відео", + "route.videoLaterThanAudio": "Відео пізніше за звук", + "route.withoutATargetTheMix": "Без цільового значення мікс виходить із тим рівнем, який дав вхід, — застосовується лише захист від кліпінгу вище.", + "route.youtubeTwitchSpotify": "YouTube, Twitch, Spotify", + "set.acceptSelfSigned": "Приймати самопідписаний сертифікат", + "set.accountDisconnected": "Обліковий запис відʼєднано.", + "set.adminPassword": "Пароль адміністратора", + "set.alertDelivery": "Доставка сповіщень", + "set.alertDeliveryDesc": "Наскільки наполегливо переслідується вебхук, який не відповідає, перш ніж від сповіщення відмовляться.", + "set.apiTokens": "Токени API", + "set.apiTokensDesc": "Довговічні облікові дані для скриптів, Prometheus і всього іншого, що не може тримати сесію. Надсилайте токен як Authorization: Bearer pmk_…. Токен може все те саме, що й адміністратор, окрім керування токенами — це залишається за паролем, тож відкликання незворотне.", + "set.app": "Застосунок", + "set.attemptsBackoff": "Пауза між спробами автоматично зростає до стелі у 30 секунд, тож верхня межа цього діапазону — це вже кілька хвилин переслідування мертвої кінцевої точки. Обмеження і є суттю: нескінченні повтори перетворюють один мертвий вебхук на чергу, яка ніколи не розгрібається.", + "set.attemptsNote": "Разом із першою спробою. 1 означає не повторювати взагалі.", + "set.attemptsRetryable": "Повторюються лише ті збої, які варто повторювати. Відповідь 404 від вебхука, який хтось видалив, остаточна, і ще чотири спроби лише затримають усі сповіщення в черзі за ним; відповідь 502 від зворотного проксі — саме те, заради чого ці спроби й існують.", + "set.audioMeters": "Індикатори звуку", + "set.brokerPasswordCleared": "Пароль брокера видалено.", + "set.brokerPlainNote": "mqtt:// не шифрується. MQTT надсилає імʼя користувача й пароль у пакеті CONNECT, тож обидва йдуть мережею відкритим текстом — у довіреній локальній мережі це нормально, що є звичним випадком для Home Assistant, і ненормально в усьому, що ви не контролюєте. Використовуйте mqtts://, якщо брокер його підтримує.", + "set.brokerUrl": "URL брокера", + "set.brokerUrlNote": "mqtt://, mqtts://, ws:// або wss://. Пароль в URL відхиляється: URL потрапляє в рядки журналу та у вивід ps, і цього вже не скасувати. Введіть його в поле нижче, де він зберігається зашифрованим.", + "set.brokerUrlPlaceholder": "mqtts://broker.local:8883", + "set.chatBothApply": "Діють обидва, і перемагає щедріше: повідомлення зникає, коли воно старше за вказану кількість годин і не входить до N найновіших. Тому жвавий канал зберігає менший проміжок часу, ніж тихий, за тих самих налаштувань.", + "set.chatBufferNote": "«Надсилати при підключенні» — величина іншої природи, ніж дві інші. Ті обмежують те, що лишається на диску; ця ж — буфер у памʼяті, який виділяється на кожну підключену панель.", + "set.chatHistory": "Історія чату", + "set.chatHistoryDesc": "Наскільки далеко сягає історія чату — а разом із нею й те, скільки бачить модератор, відкриваючи чиюсь картку.", + "set.chatHours": "Зберігати (годин)", + "set.chatKeep": "Завжди зберігати (повідомлень)", + "set.chatKeepNote": "Нижня межа, незалежно від віку.", + "set.chatNotOnlyDisk": "Це не лише налаштування диска. Клацання на імені в чаті відкриває те, що ця людина казала раніше, і ця історія береться звідси — жодна платформа не публікує для неї API, тож polyemesis може показати лише те, що зберіг. Поставте замало — і той, хто створює проблеми вже тиждень, виглядатиме так, ніби не сказав ані слова.", + "set.chatSendNote": "Історія без запиту.", + "set.chatSendOnConnect": "Надсилати при підключенні (повідомлень)", + "set.chatSendOnConnectShort": "Надсилати при підключенні", + "set.chatSmall": "Чат займає мало. Канал із середньою інтенсивністю десять повідомлень на секунду зберігає приблизно 7 МБ на годину; більшість і близько до цього не доходять.", + "set.clientId": "Ідентифікатор клієнта", + "set.clientIdNote": "Має бути унікальним на брокері. Збіг — причина номер один незрозумілого циклу перепідключень: брокер за кожного підключення розриває старішу сесію, і обидва клієнти перепідключаються нескінченно.", + "set.clientIdPlaceholder": "виводиться з назви екземпляра", + "set.clientSecret": "Секрет клієнта", + "set.comingBack": "Повернення", + "set.confirmPassword": "Підтвердьте новий пароль", + "set.connectedAccounts": "Підключені облікові записи", + "set.copied": "Скопійовано", + "set.copy": "Копіювати", + "set.couldNotChangeThePassword": "Не вдалося змінити пароль.", + "set.couldNotClearThePassword": "Не вдалося видалити пароль.", + "set.couldNotCreateTheToken": "Не вдалося створити токен.", + "set.couldNotReCheckThe": "Не вдалося повторно перевірити облікові дані.", + "set.couldNotReadTheTls": "Не вдалося прочитати статус TLS.", + "set.couldNotRevokeTheToken": "Не вдалося відкликати токен.", + "set.couldNotSaveTheCredentials": "Не вдалося зберегти облікові дані.", + "set.couldNotSaveTheMqtt": "Не вдалося зберегти налаштування MQTT.", + "set.credsRemoved": "Облікові дані видалено.", + "set.currentPassword": "Поточний пароль", + "set.dataDirectory": "Каталог даних", + "set.deliveryAttempts": "Спроби доставки", + "set.dialsOnly": "Сервер підключається лише до: {schemes}.", + "set.disconnect": "Відʼєднати", + "set.disconnectDesc": "Призначення, що його використовують, збережуть свої налаштування, але перестануть оновлювати ключ трансляції. Той самий обліковий запис можна підключити знову пізніше.", + "set.disconnectTitle": "Відʼєднати цей обліковий запис?", + "set.drawtextMissing": "У цій збірці немає фільтра drawtext, тож текстові накладення недоступні. Потрібен FFmpeg, зібраний з libfreetype.", + "set.drawtextOk": "Ця збірка вміє наносити текст на відео.", + "set.enabled": "Увімкнено", + "set.encoderUrl": "URL кодувальника", + "set.expires": "Спливає", + "set.failover": "Резервування", + "set.failoverDesc": "Резервний вхід і заставка на випадок, коли живе джерело перестає віддавати дані. Типово вимкнено: у вимкненому стані тракт байт у байт такий самий, яким був до появи цієї функції.", + "set.failoverRemuxNote": "Увімкнення додає один етап ремультиплексування між входом і всім, що нижче. Дешево, але не безкоштовно, тож це рішення, а не типове значення.", + "set.fileSizeMb": "Розмір файлу (МБ)", + "set.filesKept": "Збережених файлів", + "set.goingLive": "Вихід в ефір", + "set.goingLiveDesc": "Що відбувається, коли кілька призначень стартують одночасно.", + "set.graceNote": "Скільки живе джерело може нічого не віддавати до перемикання рівня. Навмисно довше за перепідключення — кодувальник, який відновлює зʼєднання RTMP, це штатна робота, а не відмова.", + "set.gracePeriod": "Пільговий період (секунди)", + "set.haDiscovery": "Виявлення Home Assistant", + "set.height": "Висота", + "set.ingest": "Вхід", + "set.ingestDesc": "SRT переносить до шести звукових доріжок. RTMP переносить одну й існує як запасний варіант для кодувальників, що не вміють SRT. Забір підключається до джерела сам, замість чекати на нього.", + "set.ingestUrlCopied": "URL входу скопійовано.", + "set.instance": "Екземпляр", + "set.intervalNote": "Незмінений стан повторно не публікується, тож короткий інтервал коштує порівняння, а не повідомлення. Keep-alive визначає, як довго мертвий звʼязок лишається непоміченим — а отже, як швидко брокер повідомить підписникам, що цей екземпляр зник.", + "set.issuer": "Видавець", + "set.kbps": "кбіт/с", + "set.keepAlive": "Keep-alive (с)", + "set.latencyMs": "Затримка (мс)", + "set.listeners": "Слухачі", + "set.listenersDesc": "Де сервер слухає — для всієї інсталяції. Один порт SRT обслуговує всі джерела: їх розрізняють за токеном публікації, тож додавання джерела ніколи не означає відкриття ще одного порту. У RTMP такої маршрутизації немає, тож його порт обслуговує одне джерело.", + "set.logsBoundNote": "Каталог журналів обмежений добутком цих двох значень, тож збереження журналів ніколи не стане тим, що заповнить диск.", + "set.logsDesc": "Захоплений stderr FFmpeg завжди тримається в памʼяті, але вона гине разом із сервером — і втрачаються саме ті рядки, які пояснюють, чому він загинув. Збереження дзеркалить їх у ротований файл у каталозі даних.", + "set.metersDesc": "Окремий допоміжний процес вимірює кожен канал кожної доріжки. Менший інтервал оновлює частіше й навантажує процесор сильніше.", + "set.mode": "Режим", + "set.modePull": "Забір — підключитися до камери, потоку чи файлу", + "set.modeRtmp": "RTMP — одна доріжка", + "set.modeSrt": "SRT — багатодоріжковий (рекомендовано)", + "set.mqtt": "Телеметрія MQTT", + "set.mqtt5Only": "Лише MQTT 5.0.", + "set.mqtt5OnlyNote": "Брокер, зафіксований на 3.1.1, узагалі не встановить зʼєднання — це не режим зі зрізаними можливостями. Mosquitto 2.x, EMQX і додаток Home Assistant говорять 5.0.", + "set.mqttDesc": "Публікує стан на брокер як retained-повідомлення, тож споживач, який не був підключений у момент зміни, все одно отримає актуальну відповідь.", + "set.multiAccountNote": "Підключіть ту саму платформу кілька разів, щоб мовити на кілька каналів — кожен обліковий запис стане окремим призначенням.", + "set.name": "Назва", + "set.newPassword": "Новий пароль", + "set.no": "ні", + "set.noEncryption": "Це зʼєднання нічим не шифрується. Паролі та сесійні cookie йдуть мережею відкритим текстом — задайте tls.mode: auto або привʼяжіться до 127.0.0.1 за проксі.", + "set.noLibsrt": "У цій збірці FFmpeg немає підтримки SRT. Установіть збірку, налаштовану з --enable-libsrt, або використовуйте RTMP.", + "set.noTokens": "Токенів поки немає.", + "set.passphrase": "Кодова фраза", + "set.passphraseNote": "Вмикає шифрування AES. Залиште порожнім, щоб вимкнути. SRT відхиляє фрази поза діапазоном 10–79 символів.", + "set.passphrasePlaceholder": "необовʼязково — від 10 до 79 символів", + "set.password": "Пароль", + "set.passwordChanged": "Пароль змінено.", + "set.passwordMismatch": "Нові паролі не збігаються.", + "set.passwordNote": "Пароль не повертається жодним API, тож це поле порожнє навіть тоді, коли пароль задано. Залиште порожнім, щоб зберегти записаний пароль; очистіть його кнопкою нижче.", + "set.platform": "Платформа", + "set.platformsDesc": "Платформи різняться тим, яку частину цього polyemesis може автоматизувати, і про цю різницю варто знати до початку, а не за годину. Саме мовлення скрізь працює однаково — маршрутизація звуку за призначеннями, варіанти, перепідключення та індикатори не залежать від жодного зі стовпців нижче.", + "set.playLoop": "Програвати петлю, коли нічого не йде в ефір", + "set.playLoopNote": "Другий кандидат поруч із заставкою: замість нерухомого зображення програються файли. Можуть бути ввімкнені обидва — порядок рівнів і те, що переможе, дивіться в описі резервування.", + "set.pointObs": "Спрямуйте OBS на цю адресу.", + "set.preview": "Попередній перегляд", + "set.previewDesc": "HLS-монітор панелі. Це єдине місце, де polyemesis перекодовує відео, і він працює на власній підписці ретранслятора, тож ніколи не може вплинути на призначення.", + "set.previewIdleNote": "Кодувальник запускається, коли панель запитує плейлист, і знову зупиняється через цей час після останнього запиту, тож закрита панель не витрачає процесор. Зміна цього значення ніколи не перезапускає перегляд, який хтось дивиться.", + "set.processLogs": "Журнали процесів", + "set.proxyTerminatesTls": "Зворотний проксі перед polyemesis завершує TLS, тож цей сервер навмисно віддає звичайний HTTP.", + "set.publishInterval": "Інтервал публікації (с)", + "set.reconnectCap": "Стеля паузи перепідключення (с)", + "set.reconnectCapNote": "0 використовує типове значення. Лише джерела HTTP та HLS.", + "set.redirectNote": "Має збігатися точно, включно зі схемою та портом. Якщо ви звертаєтеся до polyemesis через зворотний проксі, використовуйте його публічний URL.", + "set.redirectUri": "Redirect URI для внесення до списку", + "set.removeCreds": "Видалити облікові дані", + "set.removeCredsDesc": "Кожен обліковий запис, підключений через цей застосунок, перестане оновлювати токен, і його доведеться підключити заново. Секрет клієнта звідси не відновити — доведеться створити новий застосунок на платформі.", + "set.removeCredsTitle": "Видалити ці облікові дані розробника?", + "set.returnAfter": "Повернутися через (секунд стабільності)", + "set.returnAutomatic": "Автоматично — повернутися, щойно основне стабільне", + "set.returnManual": "Вручну — лишатися на резерві, доки я не перемкну", + "set.returnManualNote": "Типово обрано ручний режим, бо автоматичне повернення може «деренчати»: основне джерело, яке відновлюється ривками, перекидатиме програму туди-сюди просто в ефірі.", + "set.rtmpPort": "Порт RTMP", + "set.rtspNote": "TCP, якщо камера не змушує інакше. RTSP поверх UDP через NAT виглядає підключеним і нічого не передає.", + "set.rtspTransport": "Транспорт RTSP", + "set.saveAndDrop": "Зберегти й обірвати живий потік", + "set.saveIngest": "Зберегти налаштування входу", + "set.segmentSec": "Сегмент (с)", + "set.settingsSavedAffectedProcessesHave": "Налаштування збережено. Задіяні процеси перезапущено.", + "set.showSlate": "Показувати заставку, коли нічого не йде в ефір", + "set.slateColour": "Колір заставки", + "set.slateColourNote": "Одне слово, бо значення потрапляє в граф фільтрів, де кома завершила б аргумент. Використовується, лише якщо зображення не задано.", + "set.slateColourPlaceholder": "black", + "set.slateImage": "Зображення заставки", + "set.slateImageNote": "Шлях усередині каталогу даних. Залиште порожнім, щоб натомість залити суцільним кольором — саме це і є запасним варіантом, адже в кольору немає файлу, який може не відкритися.", + "set.slateImagePlaceholder": "slate/holding.png", + "set.sourceUrl": "URL джерела", + "set.sourceUrlNote": "Один із {schemes}. Джерело file:// — це шлях ВІДНОСНО каталогу даних, і він не може містити «..»: воно замкнене там так само, як завантажений файл.", + "set.sourceUrlPlaceholder": "rtsp://camera.local/stream1", + "set.srtPort": "Порт SRT", + "set.stagger": "Рознести підключення (мс)", + "set.staggerNote": "0 — вимкнено. Інакше вихід в ефір означає, що кожне призначення відкриває зʼєднання, узгоджує TLS і починає кодувати звук в одну й ту саму мить — на невеликій машині це момент, коли найімовірніше губляться кадри, і це саме той момент, за яким ви спостерігаєте, бо щойно вийшли в ефір. Кількох сотень мілісекунд достатньо.", + "set.staggerReconnectNote": "Перепідключення ніколи не затримується. Призначення, яке відвалилося о третій ночі, повертається негайно, а не чекає черги за вже здоровими процесами.", + "set.stepByStep": "Покрокове налаштування", + "set.stopAfterIdle": "Зупинити після простою (с)", + "set.streamKey": "Ключ трансляції", + "set.streamKeyNote": "Ті, хто публікує, мають використовувати саме цей шлях, щоб відкритий порт не був відчиненими дверима.", + "set.subject": "Субʼєкт", + "set.synthesiseNote": "Усі великі платформи відхиляють потік без звукової доріжки. Якщо це ввімкнено, вхід лише з відео отримує одну беззвучну стереодоріжку і його призначення стартують; якщо вимкнено — вони відмовляють із «routing profile selects no audio». Це застосовується винятково до входу, який було ВИЗНАЧЕНО з нулем доріжок, тож воно ніколи не може витіснити звук, який ви справді передаєте.", + "set.synthesiseSilent": "Створювати беззвучну доріжку", + "set.syntheticAudio": "Синтетичний звук", + "set.syntheticDesc": "Що відбувається, коли на вході є відео й зовсім немає звуку.", + "set.tabIngest": "Вхід", + "set.tabPipeline": "Тракт", + "set.tabPlatforms": "Облікові дані платформ", + "set.tabSecurity": "Безпека", + "set.title": "Налаштування", + "set.tlsAcme": "Let's Encrypt", + "set.tlsAuto": "Авто", + "set.tlsDesc": "Налаштовується в config.yaml, а не тут — TLS має бути правильним до того, як сервер почне слухати. Ця картка повідомляє, що з цього вийшло.", + "set.tlsManual": "Ручний сертифікат", + "set.tlsOff": "Тут не термінується", + "set.tlsSelfsigned": "Самопідписаний", + "set.tokenHashNote": "polyemesis зберігає лише хеш. Якщо ви його втратите, відкличте цей токен і створіть інший.", + "set.tokenNamePlaceholder": "prometheus", + "set.tokenRevoked": "Токен {name} відкликано.", + "set.topicPrefix": "Префікс топіка", + "set.topicPrefixNote": "Роздільники в префіксі зберігаються, тож home/av означає два рівні. Префікс, що починається з $, відхиляється: брокери їх резервують, і підписник, який використовує #, ніколи їх не отримає — телеметрія успішно опублікується й виявиться невидимою саме в тому поданні, з якого ви б її налагоджували.", + "set.transportSecurity": "Безпека транспорту", + "set.updateInterval": "Інтервал оновлення (мс)", + "set.username": "Імʼя користувача", + "set.validFor": "Дійсний для", + "set.whatPlatformsDo": "Що вміє кожна платформа", + "set.whyDevApp": "Навіщо потрібен власний застосунок розробника", + "set.whyDevAppDesc": "polyemesis не може постачати клієнтські секрети OAuth — вони опинилися б у кожного, хто має бінарник, і платформи їх відкликали б. Ви один раз реєструєте безкоштовний застосунок розробника, вставляєте сюди ідентифікатор і секрет клієнта, і далі polyemesis сам отримує за вас ключі трансляції.", + "set.writeLogs": "Записувати журнали на диск", + "set.yes": "так", + "sources.addDescription": "Друга програма з власним входом, призначеннями та варіантами — наприклад, вертикальне полотно поруч із горизонтальним. Вона стартує на портах за замовчуванням, які варто змінити, щоб вони не конфліктували.", + "sources.addFailed": "Не вдалося додати джерело", + "sources.addSource": "Додати джерело", + "sources.addTitle": "Додати джерело", + "sources.added": "«{name}» додано. Задайте порти входу нижче.", + "sources.app": "Застосунок", + "sources.applyAndDrop": "Застосувати й обірвати потік", + "sources.copied": "{what} скопійовано", + "sources.copyFailed": "Не вдалося скопіювати: {what}", + "sources.copyTokenAria": "Скопіювати токен", + "sources.copyUrlAria": "Скопіювати URL {proto}", + "sources.default": "за замовчуванням", + "sources.defaultDescription": "Запити, які не вказують джерело, діють на це.", + "sources.deleteAria": "Видалити {name}", + "sources.deleteConfirm": "Видалити джерело", + "sources.deleteDescription": "Його призначення та варіанти зникнуть разом із ним — вони описують, куди йде ця програма, і без неї не мають сенсу. Записи зберігаються: файли залишаються на диску та відтворюються, вони лише перестають належати джерелу.", + "sources.deleteFailed": "Не вдалося видалити джерело", + "sources.deleteThis": "Видалити це джерело", + "sources.deleteTitle": "Видалити «{name}»?", + "sources.deleted": "«{name}» та його призначення видалено", + "sources.destinations": "Призначення", + "sources.dirtyIdle": "Незбережені зміни входу. Застосування перезапустить вхід цього джерела.", + "sources.dirtyPublishing": "На це джерело публікує кодувальник. Застосування перезапустить його вхід і відключить усіх глядачів.", + "sources.enableAria": "Увімкнути {name}", + "sources.help.app": "Шлях застосунку RTMP — частина URL між хостом і ключем трансляції. Більшість кодувальників очікують «live». Змінюйте лише за наявності причини.", + "sources.help.ingest": "Як ваш кодувальник дістається цього джерела. SRT витримує з'єднання з втратами, перевідправляючи втрачене, і це правильний вибір за замовчуванням через публічний інтернет. RTMP старіший і підтримується всюди, але сильно деградує на нестабільному каналі. У режимі забору polyemesis сам забирає потік із вказаного вами URL, замість чекати, доки на нього опублікують.", + "sources.help.latencyMs": "Скільки SRT чекатиме на запізнілий або перевідправлений пакет, перш ніж від нього відмовитися. Вищі значення переживають гірші з'єднання ціною затримки: приблизно 2,5× часу кругового обходу — гарна відправна точка, тож за RTT 40 мс тут потрібно близько 100 мс. Замало — і коротке зависання стає помітним дефектом.", + "sources.help.passphrase": "Шифрує з'єднання SRT, і сьогодні саме це реально не пускає сторонніх на цей вхід. Від 10 до 79 символів. Залиште порожнім — і публікувати зможе будь-хто, хто дістанеться порту.", + "sources.help.streamKey": "Секрет, який кодувальник надсилає для публікації через RTMP. Це облікові дані, що захищають цей вхід, тож поводьтеся з ними як із паролем: будь-хто, у кого він є, може мовити на ваші призначення.", + "sources.help.token": "Ідентифікує це джерело, коли кілька спільно використовують один порт входу. Чи керує він також доступом, залежить від режиму входу — примітка нижче каже, що є правдою зараз. Оновлення видає новий токен і зберігає старий чинним п'ять хвилин, щоб робочий кодувальник не був відключений посеред ефіру.", + "sources.ingest": "Вхід", + "sources.lastSourceTitle": "Останнє джерело видалити не можна — інсталяції потрібен один вхід", + "sources.latencyMs": "Затримка (мс)", + "sources.loadFailed": "Не вдалося завантажити джерела", + "sources.loading": "Завантаження джерел…", + "sources.loss": "втрати", + "sources.modePull": "Забір", + "sources.name": "Назва", + "sources.namePlaceholder": "Вертикальний", + "sources.noEngine": "Для цього джерела не запущено рушій. Звична причина — порт входу вже зайнятий іншим джерелом або іншим процесом.", + "sources.notRunning": "не працює", + "sources.ownDescription": "Власні призначення, варіанти та записи.", + "sources.passphrase": "Кодова фраза", + "sources.passphrasePlaceholder": "10–79 символів", + "sources.publishing": "публікує", + "sources.renditions": "Варіанти", + "sources.retrans": "перевідпр.", + "sources.rotate": "Оновити", + "sources.rotateConfirm": "Оновити токен", + "sources.rotateFailed": "Не вдалося оновити токен", + "sources.rotateIdle": "Новий токен видається негайно. Старий працює ще п'ять хвилин, щоб кодувальник, який уже його використовує, не був відключений.", + "sources.rotateTitle": "Оновити токен для «{name}»?", + "sources.rotateWhilePublishing": "Зараз на це джерело публікує кодувальник. Старий токен працюватиме ще п'ять хвилин, тож з'єднання не обірветься — але його треба оновити до кінця цього періоду.", + "sources.rtt": "RTT", + "sources.running": "працює", + "sources.saveFailed": "Не вдалося зберегти", + "sources.streamKey": "Ключ трансляції", + "sources.subtitle": "Кожне — одна вхідна програма. Призначення та варіанти належать джерелу.", + "sources.title": "Джерела", + "sources.token": "токен", + "sources.tokenEnforcedDetailRtmp": "Це ключ трансляції: публікуйте на вказану вище URL-адресу, використовуючи його як ключ. Ротація видає новий і зберігає старий робочим п'ять хвилин, тож можна перейти без обриву ефіру.", + "sources.tokenEnforcedDetailSrt": "Вкажіть його в полі SRT streamid вашого кодувальника, щоб публікувати в це джерело через спільний порт. Ротація видає новий і зберігає старий робочим п'ять хвилин, тож можна перейти без обриву ефіру.", + "sources.tokenIsCredential": "Цей токен і є обліковими даними.", + "sources.tokenIssued": "Видано новий токен", + "sources.tokenNotEnforced": "Цей токен збережено, але зараз він не застосовується.", + "sources.tokenNotEnforcedRtmp": "Джерела розділяються портом. Насправді цей вхід захищає ключ трансляції вище. Увімкніть однопортовий вхід у Налаштуваннях, щоб токен став обліковими даними.", + "sources.tokenNotEnforcedSrt": "Джерела розділяються портом. Насправді цей вхід захищає кодова фраза SRT вище. Увімкніть однопортовий вхід у Налаштуваннях, щоб токен став обліковими даними.", + "state.disabled": "Вимкнено", + "state.failed": "Помилка", + "state.live": "Прямий ефір", + "state.offline": "Офлайн", + "state.reconnecting": "Відновлення з’єднання", + "state.starting": "Запуск", + "state.stopped": "Зупинено" } diff --git a/ui/src/lib/i18n/zh-Hans.json b/ui/src/lib/i18n/zh-Hans.json index a0fc7a46..8a939378 100644 --- a/ui/src/lib/i18n/zh-Hans.json +++ b/ui/src/lib/i18n/zh-Hans.json @@ -1,26 +1,227 @@ { - "nav.dashboard": "控制面板", - "nav.sources": "源", - "nav.meters": "音频电平表", - "nav.routing": "Routing", - "nav.renditions": "渲染设置", - "nav.playout": "Playout", - "nav.library": "素材库", - "nav.clips": "片段", - "nav.chat": "聊天", - "nav.jobs": "任务", - "nav.automation": "自动化", - "nav.recordings": "录制内容", - "nav.monitoring": "监控", - "nav.settings": "设置", - "chrome.toggleNav": "切换导航", + "auth.accountCreated": "管理员账号已创建。", + "auth.confirmPassword": "确认密码", + "auth.createAccount": "创建账号", + "auth.firstRun": "首次运行设置", + "auth.firstRunDesc": "为此服务器选择管理员凭据。没有账号找回机制,请把它们保存在安全的地方。", + "auth.password": "密码", + "auth.passwordMismatch": "两次输入的密码不一致。", + "auth.signIn": "登录", + "auth.signInDesc": "请输入你的管理员凭据。", + "auth.somethingWentWrong": "出了点问题。", + "auth.tokenCreated": "API 令牌已创建", + "auth.tokenRevoked": "API 令牌已吊销", + "auth.username": "用户名", + "auto.action": "动作", + "auto.adminPasswordChanged": "管理员密码已更改", + "auto.chosenWeekdays": "选定的星期", + "auto.coalesced": "已合并", + "auto.coalescingWindow": "合并窗口(秒)", + "auto.couldNotChangeTheRule": "无法修改该规则。", + "auto.couldNotChangeTheSchedule": "无法修改该计划。", + "auto.couldNotDeleteTheRule": "无法删除该规则。", + "auto.couldNotDeleteTheSchedule": "无法删除该计划。", + "auto.couldNotLoadAutomationSettings": "无法加载自动化设置。", + "auto.couldNotSaveTheRule": "无法保存该规则。", + "auto.couldNotSaveTheSchedule": "无法保存该计划。", + "auto.createRule": "创建规则", + "auto.createSchedule": "创建计划", + "auto.days": "星期", + "auto.delete": "删除", + "auto.deleteRule": "删除规则", + "auto.deleteRuleDesc": "该规则将不再触发。它的 webhook 端点不受影响,之后可以重新创建这条规则。", + "auto.deleteRuleTitle": "要删除这条告警规则吗?", + "auto.deleteSchedule": "删除计划", + "auto.deleteScheduleDesc": "该计划将停止运行。它已经启动的内容不受影响。", + "auto.deleteScheduleTitle": "要删除这个计划吗?", + "auto.delivery": "投递", + "auto.destinationFallingBehindRealtime": "推流目标落后于实时", + "auto.destinationKeepingUpAgain": "推流目标重新跟上", + "auto.destinationRecovered": "推流目标已恢复", + "auto.destinationWentDown": "推流目标已断开", + "auto.destinations": "推流目标", + "auto.dropped": "已丢弃", + "auto.edit": "编辑", + "auto.editRule": "编辑告警规则", + "auto.editSchedule": "编辑计划", + "auto.endpoint": "端点", + "auto.evAudioClipping": "音频削波", + "auto.evClipCaptured": "片段已截取", + "auto.evDiskLow": "磁盘空间不足", + "auto.evDiskRecovered": "磁盘已恢复", + "auto.evIngestLost": "输入已丢失", + "auto.evIngestRecovered": "输入已恢复", + "auto.evSettingsChanged": "设置已更改", + "auto.evSignedIn": "已登录", + "auto.events": "事件", + "auto.everyDay": "每天", + "auto.failed": "失败", + "auto.failoverSwitchedSource": "故障切换已更换源", + "auto.fired": "已触发", + "auto.firesAt": "触发时间", + "auto.lastDelivery": "最近一次投递", + "auto.lastSweep": "最近一次扫描", + "auto.latenessAllowance": "允许迟到的时长(秒)", + "auto.loudnessBackInCompliance": "响度重新合规", + "auto.loudnessOutOfCompliance": "响度不合规", + "auto.minGap": "两条消息之间的最小间隔(秒)", + "auto.minSeverity": "最低严重级别", + "auto.missedNote": "因服务器停机而错过的一次执行会被标记为已处理并跳过,而不是延迟触发——一场自己迟到四小时才开始的直播,比根本没开始更糟。", + "auto.name": "名称", + "auto.newRule": "新建告警规则", + "auto.newSchedule": "新建计划", + "auto.next": "下次", + "auto.nothingFired": "还没有任何触发。调度器每 20 秒扫描一次。", + "auto.once": "一次", + "auto.payloadFormat": "负载格式", + "auto.pending": "待处理", + "auto.repeatedFailedSignIns": "多次登录失败", + "auto.repeats": "重复", + "auto.retries": "重试次数", + "auto.rule": "规则", + "auto.ruleCreated": "告警规则已创建。给它发一条测试消息。", + "auto.ruleDeleted": "告警规则已删除。", + "auto.rulePlaceholder": "Discord — 节目告警", + "auto.ruleSaved": "告警规则已保存。", + "auto.saveRule": "保存规则", + "auto.saveSchedule": "保存计划", + "auto.schedule": "计划", + "auto.scheduleCreated": "计划已创建。", + "auto.scheduleDeleted": "计划已删除。", + "auto.schedulePlaceholder": "工作日晚间节目", + "auto.scheduleSaved": "计划已保存。", + "auto.schedules": "计划", + "auto.secretMasked": "因为密钥就在路径里,所以这里以掩码显示。保持原样即可沿用当前端点,或粘贴一个新的 URL 来替换它。", + "auto.sendTest": "发送测试告警", + "auto.sendTestTitle": "立即发送一条测试告警", + "auto.sent": "已发送", + "auto.skipped": "已跳过", + "auto.startDestinations": "启动推流目标", + "auto.startPlaylist": "启动播放列表", + "auto.stopDestinations": "停止推流目标", + "auto.stopPlaylist": "停止播放列表", + "auto.subscribedTo": "订阅的事件", + "auto.subtitle": "在出问题时通知你的 Webhook,以及无需你在场就能把节目跑起来的计划。", + "auto.theEndpointDidNotAccept": "端点未接受这条测试消息。", + "auto.time": "时间", + "auto.timeZone": "时区", + "auto.title": "自动化", + "auto.tzPlaceholder": "UTC", + "auto.webhookHint": "Discord 和 Slack 都能在频道设置里给你一个。", + "auto.webhookPlaceholder": "https://discord.com/api/webhooks/…", + "auto.webhookRules": "Webhook 规则", + "auto.webhookUrl": "Webhook URL", + "auto.weekFri": "周五", + "auto.weekMon": "周一", + "auto.weekSat": "周六", + "auto.weekSun": "周日", + "auto.weekThu": "周四", + "auto.weekTue": "周二", + "auto.weekWed": "周三", + "auto.when": "发生时间", + "chatpage.allFiltered": "所有有消息的平台都被筛选掉了。", + "chatpage.couldNotDeleteThatMessage": "无法删除该消息。", + "chatpage.deleteNote": "删除会在发出该消息的平台上将其移除。polyemesis 无法删除的平台会明确说明,并指出可以在何处执行——这个操作绝不会凭猜测被隐藏。", + "chatpage.droppedTitle": "曾实时显示,但写入落后于接收,历史记录里丢失了它们。", + "chatpage.dupTitle": "同一条消息收到了两次——可能是我们自己的回显,也可能是平台重发。", + "chatpage.noAccount": "聊天正在运行,但尚未连接任何平台账号。请在“设置 → 平台凭据”中连接一个。", + "chatpage.notRunning": "此服务器上没有运行聊天。时间线下方的历史记录是此前存储的内容,在连接平台之前发送框将保持禁用。", + "chatpage.received": "已接收", + "chatpage.reload": "重新加载", + "chatpage.stored": "已存储", + "chatpage.storedTitle": "当前没有任何连接;这是已存储的历史记录。", + "chatpage.subtitle": "所有平台汇入一条时间线,共用一个发送框。", + "chatpage.timeline": "时间线", + "chatpage.title": "聊天", "chrome.ingest": "Ingest", + "chrome.language": "语言", "chrome.liveCount": "{count} 个直播", + "chrome.signOut": "注销", "chrome.socketOffline": "离线", "chrome.socketOfflineHint": "直播更新已断开;正在重试", - "chrome.signOut": "注销", - "chrome.language": "语言", + "chrome.toggleNav": "切换导航", + "clipedit.back33": "后退 33 毫秒", + "clipedit.clickLine": "点击某一行即可跳转到该处。跨多行选中,入点和出点会移动到你高亮的词语上。", + "clipedit.clipThisLine": "剪出这一行", + "clipedit.clipThisLineTitle": "把入点和出点设在这一行的前后", + "clipedit.copied": "直接复制", + "clipedit.copiesEveryPacketSecondsBit": "逐包复制。耗时以秒计,逐比特一致,起点会回退到最近的关键帧。", + "clipedit.couldNotOpenThatRecording": "无法打开该录像。", + "clipedit.couldNotQueueTheExport": "无法将导出加入队列。", + "clipedit.cut": "剪切", + "clipedit.downloadClip": "下载该片段", + "clipedit.exportQueued": "导出已排队。等机器有余力时才会运行。", + "clipedit.exports": "导出", + "clipedit.fast": "快速", + "clipedit.filterTranscript": "过滤转写文本", + "clipedit.findSaid": "查找说过的内容", + "clipedit.forward33": "前进 33 毫秒", + "clipedit.in": "入点", + "clipedit.keepAllTracks": "保留所有音轨", + "clipedit.keepsEveryAudioTrackIn": "保留所有音轨,任何编解码器都行。就是录制器写出的内容。", + "clipedit.markIn": "I — 在播放头处标记入点", + "clipedit.markOut": "O — 在播放头处标记出点", + "clipedit.name": "名称", + "clipedit.noKeyframes": "无法读取关键帧位置,因此谁也说不准这次剪切究竟从哪里开始。它仍然可用——FFmpeg 自己的定位会落在关键帧上——但起点可能提前至多一个 GOP,而本页面无法告诉你提前了多少。", + "clipedit.noTranscript": "这段录像还没有做过转写。每个话筒都录在各自的音轨上,因此转写它无需说话人分离模型就能得到说话人名字——之后你还可以按说过的内容来剪片段。", + "clipedit.nothingExported": "还没有导出任何内容。", + "clipedit.out": "出点", + "clipedit.pickAtLeastOneAudio": "请至少选择一条音轨,或者全部保留。", + "clipedit.playFaster": "L — 播放,再按一次加速", + "clipedit.precise": "精确", + "clipedit.queueNote": "导出会进入队列,而不是在这里直接运行:它让位于直播流,因此在直播过程中剪的片段会等待,而不是抢走直播所需的算力。", + "clipedit.reEncodesOnlyTheLeading": "只重新编码开头那段不完整的 GOP,使起点精确。其余部分仍然是直接复制。", + "clipedit.reencoded": "重新编码", + "clipedit.shortcuts": "J / K / L 用于往复播放,I 和 O 标记入点和出点,方向键微调 33 毫秒,Shift+方向键微调一秒。精细步长相当于 30 fps 下的一帧;这段录像的真实帧率没有建立索引,因此这里用毫秒来标注,而不是靠猜。", + "clipedit.shuttleBack": "J — 向后往复播放", + "clipedit.startMoved": "起点偏移", + "clipedit.starts": "起点", + "clipedit.stop": "K — 停止", + "clipedit.thatCutCouldNotBe": "无法为这次剪切制定方案。", + "clipedit.thatIsNotARecording": "这不是一个录像 id。", + "clipedit.thatRecordingIsNotAvailable": "该录像不可用。", + "clipedit.title": "片段编辑器", + "clipedit.whatASocialPlatformWill": "社交平台能够接受的规格。能通过它的编解码器更少。", + "clipedit.zoomIn": "放大", + "clipedit.zoomOut": "缩小", + "clips.bitrate": "码率", + "clips.buffer": "缓冲", + "clips.bufferLiveNote": "缓冲开关是即时开关,并非会保存的设置:重启之后它又会回到关闭状态。", + "clips.captured": "截取时间", + "clips.ceiling": "上限", + "clips.clipsKept": "保留片段数", + "clips.couldNotCaptureAClip": "无法截取片段。", + "clips.couldNotChangeTheClip": "无法修改片段缓冲。", + "clips.couldNotDeleteTheClip": "无法删除该片段。", + "clips.couldNotLoadClips": "无法加载片段。", + "clips.covers": "涵盖", + "clips.customLength": "自定义片段时长(秒)", + "clips.delete": "删除", + "clips.deleteDescription": "片段通常正是你想保留的成果。此操作会删除该文件;它所来自的录像不受影响。", + "clips.deleteTitle": "要删除这个片段吗?", + "clips.deleted": "片段已删除。", + "clips.download": "下载", + "clips.everythingHeld": "缓冲中保存的全部内容", + "clips.file": "文件", + "clips.held": "已保存", + "clips.inMemory": "内存中", + "clips.keepBuffer": "保持滚动缓冲", + "clips.keyframes": "关键帧", + "clips.length": "时长", + "clips.none": "无", + "clips.off": "关", + "clips.onDisk": "磁盘上", + "clips.seen": "已看到", + "clips.settings": "设置", + "clips.size": "大小", + "clips.subtitle": "刚刚播出内容的滚动缓冲。事后再截取即可——无需提前准备任何东西。", + "clips.theClipDoesNotStart": "该片段不是从关键帧开始的。", + "clips.title": "片段", + "clips.waitingForStream": "等待推流", + "clips.window": "窗口", + "clips.windowSeconds": "窗口(秒)", "common.add": "添加", + "common.apply": "应用", "common.cancel": "取消", "common.close": "关闭", "common.confirm": "确认", @@ -28,110 +229,921 @@ "common.copy": "复制", "common.delete": "删除", "common.disabled": "已禁用", + "common.discard": "放弃", "common.edit": "编辑", "common.enabled": "已启用", "common.loading": "加载中…", + "common.moreInfo": "更多信息", "common.refresh": "刷新", "common.restart": "重启", "common.retry": "重试", "common.save": "保存", "common.start": "开始", "common.stop": "停止", - "state.live": "直播中", - "state.reconnecting": "重新连接中", - "state.starting": "开始", - "state.failed": "失败", - "state.stopped": "已停止", - "state.offline": "离线", - "hooks.title": "生命周期 Webhook", - "hooks.new": "新建 Webhook", - "hooks.empty": "暂无 Webhook。添加一个后,只要推流开始或停止、或某个目标上线或掉线,polyemesis 就会 POST 一份签名的 JSON 主体——每次状态变化投递一次,按顺序送达,便于脚本处理。", - "hooks.colWebhook": "Webhook", - "hooks.colEndpoint": "端点", - "hooks.colTriggers": "订阅", - "hooks.colEnabled": "启用", - "hooks.signed": "已签名", - "hooks.unsigned": "未签名", + "dash.audioTracks": "音轨", + "dash.bitrate": "码率", + "dash.broadcastSettings": "直播设置", + "dash.categoryPlaceholder": "音乐", + "dash.clipboardUnavailable": "在此源站上无法使用剪贴板。", + "dash.connectPrompt": "在“设置 → 平台凭据”中连接 YouTube、Twitch、Facebook 或 Kick 账号,即可一次性在所有平台上设置直播标题、简介和分类。每个平台接受的字段并不相同——编辑器会标明是哪些,并推送它能推送的部分。", + "dash.copyIngestUrl": "复制输入 URL", + "dash.couldNotLoadTheDestination": "无法加载该推流目标。", + "dash.couldNotPushTheMetadata": "无法推送元数据。", + "dash.couldNotReorderTheDestinations": "无法重新排序推流目标。", + "dash.deleteDest": "删除推流目标", + "dash.deleteDestDescription": "此操作会删除该推流目标及其路由配置。平台本身不受影响,你也可以把它加回来——但为它设置的音频路由会一并消失。", + "dash.descriptionPlaceholder": "这场直播是什么。", + "dash.destDeleted": "推流目标已删除。", + "dash.disabled": "已禁用", + "dash.everyConnectedAccount": "所有已连接的账号", + "dash.idle": "空闲", + "dash.ingestUrlCopied": "输入 URL 已复制。", + "dash.keyRefreshed": "已从平台刷新推流密钥。", + "dash.leaveUnchanged": "保持不变", + "dash.leaveUnchangedNote": "“保持不变”完全不发送任何内容,因此平台会沿用它已有的值。选择“开”或“关”则会写入该值。", + "dash.metaCategory": "分类", + "dash.metaDescription": "简介", + "dash.metaTags": "标签", + "dash.metaTitle": "标题", + "dash.noDestinations": "还没有推流目标。先添加一个,再选择它接收哪些音轨。", + "dash.off": "关", + "dash.pipeline": "处理链路", + "dash.pushToPlatforms": "推送到平台", + "dash.pushing": "推送中…", + "dash.reconnects": "重连次数", + "dash.relaySubscribers": "中继订阅者", + "dash.renditions": "转码版本", + "dash.scheduledStart": "计划开始时间", + "dash.subtitle": "输入一次,用共享的视频和按目标区分的音频分发出去。", + "dash.tagsPlaceholder": "直播、浩室、dj set", + "dash.theConnectedAccount": "已连接的账号", + "dash.title": "面板", + "dash.titlePlaceholder": "周五夜场", + "dash.uptime": "运行时长", + "dash.youtubeStopsAcceptingTheseOnce": "一旦直播离开“created”或“ready”状态,YouTube 就不再接受这些内容。", + "dest.audience": "受众", + "dest.audioBitrate": "音频码率", + "dest.audioCodec": "音频编解码器", + "dest.channels": "声道", + "dest.compliance": "合规", + "dest.connectedAccount": "已连接账号", + "dest.contentLabels": "内容标签", + "dest.copySource": "复制源视频", + "dest.createVariant": "创建此变体", + "dest.customiseSummary": "为此目标单独调整", + "dest.fbBackupCost": "会使此目标的上行带宽翻倍。", + "dest.fbBackupHelp": "将此推流的第二份副本发送到 Facebook 的备用接收地址,这样连接断开也不会中断直播。", + "dest.fbBackupLabel": "发布备用推流", + "dest.fbBackupReconnect": "开启会让推流重新连接一次——因此请在开播前启用。", + "dest.fbBoxIntro": "与“受众”在同一次创建调用中发送,因此适用同一条规则:在直播开始时应用一次,若为空则完全不作改动。", + "dest.fbBoxTitle": "Facebook 转发与捐款按钮", + "dest.fbCrosspostAdd": "添加主页", + "dest.fbCrosspostAlsoPost": "同时发帖", + "dest.fbCrosspostHelp": "来自 Facebook 自家控制台的数字主页 ID(主页设置,或以主页身份登录时的 graph.facebook.com/me/accounts)——这里没有搜索功能,所以请粘贴 id,而不是名称或网址。“同时发帖”会以该主页的身份发布,而不只是把直播分享给它;不开启时,只会发生较低调的分享。", + "dest.fbCrosspostLabel": "转发到主页", + "dest.fbCrosspostPageId": "主页 ID", + "dest.fbCrosspostRemove": "移除此主页", + "dest.fbDonateHelp": "这同样是 Facebook 募捐控制台里的一个不透明 id,而不是可供搜索的机构名称。留空则完全不附加捐款按钮。", + "dest.fbDonateLabel": "捐款按钮的慈善机构 ID", + "dest.fbDonatePlaceholder": "慈善机构 ID", + "dest.fillFromPlatform": "用该平台公布的参数填充", + "dest.friends": "好友", + "dest.friendsOfFriends": "好友的好友", + "dest.giveUpAfter": "放弃前的重试次数", + "dest.leaveOnFacebook": "保持 Facebook 上的设置", + "dest.leaveOnYouTube": "保持 YouTube 上的设置", + "dest.madeForKids": "面向儿童 (COPPA)", + "dest.muxQueue": "复用队列 (数据包)", + "dest.muxQueuePair": "这两项是一对:FFmpeg 只有在队列超过字节阈值之后才会应用数据包上限,因此单独设置阈值不起作用,并会被拒绝。若某个目标报告交错错误,请同时调高两者——这里的音频路径延迟不固定,因为响度归一化会预读。", + "dest.name": "名称", + "dest.namePlaceholder": "YouTube — 主频道", + "dest.notLinked": "未关联", + "dest.onlyMe": "仅自己", + "dest.platform": "平台", + "dest.private": "私享", + "dest.public": "公开", + "dest.reconnecting": "重新连接", + "dest.retryAfter": "重试间隔 (秒)", + "dest.sharedEncodeAria": "共享视频编码", + "dest.socketTimeout": "套接字超时 (秒)", + "dest.streamKeyLabel": "推流密钥", + "dest.transport": "传输方式", + "dest.transportFile": "本地文件", + "dest.transportRtmp": "RTMP / RTMPS", + "dest.unlisted": "不公开列出", + "dest.urlFromDashboard": "来自控制台的 URL", + "dest.useSharedEncode": "使用共享视频编码", + "dest.varBitrate": "视频码率 (kbps)", + "dest.varFps": "帧率", + "dest.varHeight": "高度", + "dest.varSourceHint": "源", + "dest.varWidth": "宽度", + "dest.variantWarning": "以“{name}”为基础,从你的源开始第二路编码。这不是免费的变体,也不会改变该编码上其他目标收到的画面。留空则沿用源的取值。", + "dest.videoTreatment": "视频处理", + "dest.visibility": "可见性", + "hooks.allTriggers": "全部", "hooks.attempts": "{count} 次尝试", + "hooks.attemptsLabel": "尝试次数", "hooks.attemptsOne": "{count} 次尝试", - "hooks.timeout": "{seconds} 秒超时", - "hooks.allTriggers": "全部", - "hooks.triggerCount": "{count} 个触发器", - "hooks.triggerCountOne": "{count} 个触发器", - "hooks.enableAria": "启用 {name}", + "hooks.colEnabled": "启用", + "hooks.colEndpoint": "端点", + "hooks.colTriggers": "订阅", + "hooks.colWebhook": "Webhook", + "hooks.create": "创建", + "hooks.created": "已创建 Webhook。", + "hooks.deleteBody": "它会立即停止投递。端点不受影响,你可以重新创建这个钩子——但它的签名密钥无法恢复。", + "hooks.deleteConfirm": "删除 Webhook", + "hooks.deleteFailed": "无法删除该钩子。", + "hooks.deleteTitle": "删除此 Webhook?", + "hooks.deleted": "已删除 Webhook。", "hooks.deliveries": "最近投递", - "hooks.sendTest": "发送测试投递", - "hooks.sendTestTitle": "立即发送一次测试投递", - "hooks.secretTitle": "签名密钥——请立即复制,之后无法再次显示", - "hooks.secretCopied": "我已复制", - "hooks.httpStatus": "HTTP {status}", - "hooks.noResponse": "无响应", + "hooks.deliveriesFailed": "无法读取最近的投递。", + "hooks.deliveryTitle": "投递", + "hooks.dialogIntro": "每次状态变化发送一次签名 POST,按顺序送达。与告警不同,这里不做合并或防抖——脚本能收到每一个边沿。", "hooks.durationMs": "{ms} 毫秒", + "hooks.editTitle": "编辑 Webhook", + "hooks.empty": "暂无 Webhook。添加一个后,只要推流开始或停止、或某个目标上线或掉线,polyemesis 就会 POST 一份签名的 JSON 主体——每次状态变化投递一次,按顺序送达,便于脚本处理。", + "hooks.enableAria": "启用 {name}", + "hooks.httpStatus": "HTTP {status}", + "hooks.loadFailed": "无法加载 Webhook。", + "hooks.name": "名称", + "hooks.new": "新建 Webhook", + "hooks.newTitle": "新建 Webhook", "hooks.noDeliveries": "尚未投递任何内容。状态发生变化时,投递会显示在这里。", - "hooks.deliveryTitle": "投递", - "hooks.statSent": "已发送", - "hooks.statFailed": "失败", + "hooks.noResponse": "无响应", + "hooks.payloadNote": "载荷 {version}。签名位于 {header},覆盖时间戳与主体。", + "hooks.retriesHint": "顺序就是承诺,因此重试会阻塞该端点自己的队列。4xx 永不重试——一个说请求有误的端点还会再说一次。", + "hooks.saveFailed": "无法保存该钩子。", + "hooks.secretCopied": "我已复制", + "hooks.secretTitle": "签名密钥——请立即复制,之后无法再次显示", + "hooks.sendTest": "发送测试投递", + "hooks.sendTestTitle": "立即发送一次测试投递", + "hooks.signed": "已签名", + "hooks.statDropped": "已丢弃", "hooks.statEndpoints": "端点", + "hooks.statFailed": "失败", "hooks.statQueued": "排队中", - "hooks.statDropped": "已丢弃", "hooks.statRetries": "重试", - "hooks.payloadNote": "载荷 {version}。签名位于 {header},覆盖时间戳与主体。", - "hooks.deleteTitle": "删除此 Webhook?", - "hooks.deleteBody": "它会立即停止投递。端点不受影响,你可以重新创建这个钩子——但它的签名密钥无法恢复。", - "hooks.deleteConfirm": "删除 Webhook", - "hooks.editTitle": "编辑 Webhook", - "hooks.newTitle": "新建 Webhook", - "hooks.dialogIntro": "每次状态变化发送一次签名 POST,按顺序送达。与告警不同,这里不做合并或防抖——脚本能收到每一个边沿。", - "hooks.name": "名称", - "hooks.url": "端点 URL", - "hooks.urlMasked": "已保存的 URL 永远不会完整显示。保留掩码值即保持不变,或输入新的 URL 以替换它。", - "hooks.triggers": "触发器", - "hooks.triggersNoneHint": "不选择任何项即表示所有触发器,包括未来版本新增的触发器。", + "hooks.statSent": "已发送", + "hooks.testFailed": "端点未接受测试投递。", + "hooks.timeout": "{seconds} 秒超时", "hooks.timeoutLabel": "超时(秒)", - "hooks.attemptsLabel": "尝试次数", - "hooks.retriesHint": "顺序就是承诺,因此重试会阻塞该端点自己的队列。4xx 永不重试——一个说请求有误的端点还会再说一次。", - "hooks.create": "创建", - "hooks.triggerIngestPublished": "推流已开始", - "hooks.triggerIngestDisconnected": "推流已停止", - "hooks.triggerDestinationUp": "目标已直播", - "hooks.triggerDestinationDown": "目标已停止", - "hooks.loadFailed": "无法加载 Webhook。", + "hooks.title": "生命周期 Webhook", "hooks.toggleFailed": "无法修改该钩子。", - "hooks.deleted": "已删除 Webhook。", - "hooks.deleteFailed": "无法删除该钩子。", - "hooks.testFailed": "端点未接受测试投递。", - "hooks.deliveriesFailed": "无法读取最近的投递。", + "hooks.triggerCount": "{count} 个触发器", + "hooks.triggerCountOne": "{count} 个触发器", + "hooks.triggerDestinationDown": "目标已停止", + "hooks.triggerDestinationUp": "目标已直播", + "hooks.triggerIngestDisconnected": "推流已停止", + "hooks.triggerIngestPublished": "推流已开始", + "hooks.triggers": "触发器", + "hooks.triggersNoneHint": "不选择任何项即表示所有触发器,包括未来版本新增的触发器。", + "hooks.unsigned": "未签名", "hooks.updated": "已更新 Webhook。", - "hooks.created": "已创建 Webhook。", - "hooks.saveFailed": "无法保存该钩子。", - "playlist.title": "播放列表", + "hooks.url": "端点 URL", + "hooks.urlMasked": "已保存的 URL 永远不会完整显示。保留掩码值即保持不变,或输入新的 URL 以替换它。", + "jobs.alwaysKeepNewest": "始终保留最新的", + "jobs.automaticModel": "自动(根据本机硬件选择)", + "jobs.backToDefault": "恢复此类工作的默认模式", + "jobs.batteryFloor": "电量下限 %", + "jobs.broadcastGoing": "正在直播;延后的工作会为它让路。", + "jobs.cancel": "取消", + "jobs.cannotPause": "这些类型本应暂停但无法暂停,因此正在跑完。", + "jobs.completed": "已完成", + "jobs.concurrency": "并发数", + "jobs.couldNotReadTheJob": "无法读取任务队列。", + "jobs.couldNotSaveThePolicy": "无法保存策略。", + "jobs.cpuCeiling": "CPU 上限 %", + "jobs.cpuResume": "CPU 恢复阈值 %", + "jobs.defaultMode": "默认模式", + "jobs.defaultModel": "默认模型", + "jobs.deferred": "已延后", + "jobs.failed": "失败", + "jobs.finished": "已结束", + "jobs.global": "全局", + "jobs.gpuGateNote": "只有被标记为使用 GPU 的类型才受此限制。", + "jobs.history": "历史", + "jobs.historyRetention": "历史保留", + "jobs.holdDeferredWorkBackOn": "笔记本电脑在用电池供电且电量低于此值时,暂缓延后的工作。0 表示禁用。", + "jobs.hostCpuLoad": "主机 CPU 负载", + "jobs.howManyJobsMayRun": "同时可以运行多少个任务。默认是一个,因为第二个转码换来的是没人要求的吞吐量,代价却是没人愿意承担的风险。", + "jobs.inQueue": "队列中", + "jobs.job": "任务", + "jobs.keepForDays": "保留天数", + "jobs.machineSafety": "机器安全。这是实时工作也会遵守的唯一一道闸。", + "jobs.niceLevel": "nice 级别", + "jobs.noFinished": "还没有已结束的任务。", + "jobs.noModels": "在模型目录中没有找到任何模型文件。自动模式会下载它选中的那个;在这里指定某个具体模型,只有在该文件存在之后才有效。", + "jobs.nothingNewStartsAboveThis": "高于此值时不会启动新任务。0 表示禁用这道闸。", + "jobs.nothingQueued": "队列中没有内容。后期处理从素材库提交。", + "jobs.nothingWasOldEnoughTo": "没有内容旧到需要清除。请先在“策略”中调低保留时长。", + "jobs.pauseAll": "全部暂停", + "jobs.policy": "策略", + "jobs.policySaved": "资源策略已保存。", + "jobs.progress": "进度", + "jobs.queuePaused": "队列已暂停。", + "jobs.queueResumed": "队列已恢复。", + "jobs.queued": "排队中", + "jobs.recording": "录像", + "jobs.releasedItWillStartAs": "已放行。一旦有空位就会开始。", + "jobs.removeFromHistory": "从历史中移除", + "jobs.removeWindow": "移除时间窗", + "jobs.resumeAll": "全部恢复", + "jobs.retry": "重试", + "jobs.retryTitle": "以新的尝试次数额度重新准备", + "jobs.runNow": "立即运行", + "jobs.runNowTitle": "忽略资源策略,立即运行这一个", + "jobs.running": "运行中", + "jobs.savedTheConcurrencyChangeTakes": "已保存。并发数的更改要等服务器重启后才生效——队列在启动时就把这个值定死了。", + "jobs.state": "状态", + "jobs.stopsEverythingRealtimeIncludedA": "停止一切,包括实时工作:正在降频的 CPU 已经开始劣化直播流。0 表示禁用。", + "jobs.subtitle": "后台工作:转写、代理文件、缩略图和归档压缩。", + "jobs.thatDidNotWork": "这没有奏效。", + "jobs.thermalCeiling": "温度上限 °C", + "jobs.thisManyFinishedJobsSurvive": "无论多旧,这么多个已结束的任务都会保留下来。", + "jobs.title": "任务", + "jobs.transcription": "转写", + "jobs.unsavedChanges": "有未保存的更改。", + "jobs.whereTheCeilingReleasesThe": "上限在何处解除。这段差值就是防止闸门来回振荡的迟滞。", + "jobs.whisperMissing": "本机未安装 whisper.cpp,因此这一项暂时不起作用。但仍然值得设置:一旦装上,选择立即生效。", + "jobs.why": "原因", + "jobs.windowEnd": "时间窗结束", + "jobs.windowStart": "时间窗开始", + "jobs.yieldNote": "重负载工作会排队,并为直播流让路。丢掉的一帧无法找回;晚一小时拿到转写文本却没有任何代价。", + "lib.anySpeaker": "任意说话人", + "lib.anyTag": "任意标签", + "lib.autoGrouped": "自动分组。编辑之后它就归你所有。", + "lib.autoGroupedNote": "这场会话是自动分组的。保存后它就归你所有,分组流程也不会再改写它。", + "lib.bestMatch": "最佳匹配", + "lib.clearSearch": "清除搜索", + "lib.clip": "剪片段", + "lib.clipTitle": "从这段录像中剪出一个片段", + "lib.collapseSession": "折叠会话", + "lib.couldNotLoadTheLibrary": "无法加载素材库。", + "lib.couldNotLoadTheSession": "无法加载该会话的分段。", + "lib.couldNotQueueTheProxy": "无法将代理文件加入队列。", + "lib.couldNotRegroup": "无法重新分组。", + "lib.couldNotSaveTheSession": "无法保存该会话。", + "lib.delete": "删除", + "lib.deleteDescription": "该文件及其代理文件、缩略图和转写文本都会从磁盘上移除。此操作无法撤销。", + "lib.deleteTitle": "要删除这段录像吗?", + "lib.downloadMaster": "下载主文件", + "lib.downloadMasterTitle": "下载多轨主文件", + "lib.editSession": "编辑会话", + "lib.expandSession": "展开会话", + "lib.from": "起始日期", + "lib.generateProxy": "生成代理文件", + "lib.generateProxyTitle": "生成可在浏览器中播放的代理文件", + "lib.mostRecent": "最新优先", + "lib.noProxyYet": "还没有代理文件——打开即可生成一个", + "lib.noSegments": "这里没有分段。", + "lib.noSessionMatch": "没有会话符合这些筛选条件。", + "lib.nothingRecorded": "还没有任何录像。录制器写出分段后,会话就会出现。", + "lib.oldestFirst": "最早优先", + "lib.playProxy": "播放代理文件", + "lib.proxyAlreadyQueued": "已经有一个代理文件在排队。", + "lib.proxyQueued": "代理文件已排队。", + "lib.recordingDeleted": "录像已删除。", + "lib.regroupTitle": "重新执行分组。只增不减且幂等:绝不会合并你手动拆开的分组。", + "lib.searchPlaceholder": "在所有录像里搜索任何人说过的一切…", + "lib.searchTranscripts": "搜索转写文本", + "lib.searchable": "可搜索", + "lib.searchableOnly": "带有转写文本、因而可搜索的分段", + "lib.session": "会话", + "lib.sessionDescription": "简介", + "lib.sessionSaved": "会话已保存。", + "lib.sessionTags": "标签", + "lib.sessionTitle": "标题", + "lib.subtitle": "每一场直播都可搜索。每个话筒都录在各自的音轨上,因此转写文本本来就知道是谁说的。", + "lib.tag": "标签", + "lib.tagsPlaceholder": "用逗号, 分隔", + "lib.title": "素材库", + "lib.transcribe": "转写", + "lib.transcribeEachTrackOnIts": "分别转写每一条音轨——说话人归属正是这样得来的", + "lib.ungrouped": "未分组的分段", + "meters.deviation": "偏差", + "meters.idle": "空闲", + "meters.integrated": "整体", + "meters.loudness": "响度合规", + "meters.metering": "测量中", + "meters.momentary": "瞬时", + "meters.monitorOff": "监看已关闭。打开它才能测量每个推流目标实际送出的内容。", + "meters.noSignal": "此音轨无信号", + "meters.nothingToMeasure": "暂时没有可测量的内容。每个运行中的推流目标都会在它收到的混音上获得自己的 EBU R128 分析器。", + "meters.range": "范围", + "meters.shortTerm": "短时", + "meters.subtitle": "每一路输入音轨的每个声道,直接来自中继。", + "meters.title": "音频电平表", + "meters.truePeak": "真峰值", + "meters.waitingForStream": "等待推流", + "mon.allProcesses": "全部进程", + "mon.bitrateChart": "输入码率 — 最近 30 分钟", + "mon.chooseDestination": "选择一个推流目标…", + "mon.clearLog": "清空日志", + "mon.clearedThisDestinationIsBack": "已清除。此推流目标已恢复使用自动生成的命令。", + "mon.dryRun": "试运行", + "mon.dryRunInvalid": "FFmpeg 拒绝了某个参数", + "mon.dryRunOk": "FFmpeg 接受了它", + "mon.extraInput": "额外的输入参数", + "mon.extraOutput": "额外的输出参数", + "mon.ffmpegLog": "FFmpeg 日志", + "mon.fromRunningProcess": "来自运行中的进程", + "mon.fullCommand": "完整展开的命令", + "mon.hostCpu": "主机 CPU", + "mon.ingestLoss": "输入丢包", + "mon.memory": "内存", + "mon.nothingProvenEitherWay": "两边都尚无定论", + "mon.pid": "PID", + "mon.processes": "进程", + "mon.rebuilt": "已重建", + "mon.relayDrops": "中继丢弃", + "mon.relayIn": "中继输入", + "mon.resolveHint": "展开命令,在应用之前看清究竟会执行什么。", + "mon.restarts": "重启次数", + "mon.subtitle": "进程健康状况、主机资源,以及实时的 FFmpeg 日志。", + "mon.title": "监控", + "mon.uptime": "运行时长", + "nav.automation": "自动化", + "nav.chat": "聊天", + "nav.clips": "片段", + "nav.dashboard": "控制面板", + "nav.jobs": "任务", + "nav.library": "素材库", + "nav.meters": "音频电平表", + "nav.monitoring": "监控", + "nav.playout": "Playout", + "nav.recordings": "录制内容", + "nav.renditions": "转码版本", + "nav.routing": "Routing", + "nav.settings": "设置", + "nav.sources": "源", + "play.acrossEveryRungTheBackstop": "涵盖所有档位。这是防止长时间运行占满磁盘的最后一道防线。", + "play.allowCrossOrigin": "允许跨域", + "play.allowCrossOriginPlayback": "允许跨域播放", + "play.audioBitrate": "音频码率(kbps)", + "play.audioTrack": "音轨", + "play.basicPasswordNote": "无法跟随链接的播放器(机顶盒、CDN)可以改用 HTTP 基本认证密码发送同一个密钥。", + "play.boundsTheViewerTableViewers": "限制观看者表的大小。超出之后的观看者仍会得到服务,只是不计入统计。", + "play.carriesThePlaybackTokenAnyone": "其中带有播放令牌。任何拿到这个 URL 的人都能观看。", + "play.clipboardUnavailable": "剪贴板不可用;请手动选中并复制", + "play.couldNotLoadPlayout": "无法加载 Playout", + "play.disabled": "已禁用", + "play.diskCap": "磁盘上限(MB)", + "play.dvrWindow": "DVR 窗口(秒)", + "play.embedCode": "嵌入代码", + "play.enable": "启用 Playout", + "play.enabled": "已启用", + "play.forObsACdnOr": "适用于 OBS、CDN,或任何支持 HLS 的播放器。", + "play.format": "格式", + "play.hls": "HLS", + "play.hlsDash": "HLS 和 DASH", + "play.hlsPlaylist": "HLS 播放列表", + "play.idleTimeout": "观看者空闲超时(秒)", + "play.linkReset": "已生成新的播放链接。之前那个不再有效。", + "play.linksProtected": "在你打开“公开”之前,这些链接只对已登录的管理员有效。", + "play.linksPublic": "这些链接无需账号即可使用。", + "play.liveWindow": "直播窗口(分片数)", + "play.metaDescription": "简介", + "play.metaTitle": "标题", + "play.mustExceedOneSegmentOr": "必须大于一个分片,否则观看者会在两次轮询之间闪断消失。", + "play.name": "名称", + "play.noCredentialNote": "不需要任何形式的凭据。只适用于你打算让整个互联网都看到的直播。", + "play.noRungs": "还没有档位。添加一个才能发布任何内容。", + "play.onDisk": "磁盘占用", + "play.oneStereoAacTrackPer": "每个档位一条立体声 AAC 音轨。", + "play.packaging": "封装", + "play.passthrough": "输入(直通)", + "play.peak": "峰值", + "play.playerPage": "播放器页面", + "play.posterNote": "显示在公开页面视频的上方。海报取自最近的一个关键帧,会自动刷新。", + "play.private": "私有。", + "play.protectedNote": "播放默认受保护。开放它是单独的、需要刻意为之的一步。", + "play.protection": "保护", + "play.public": "公开", + "play.resetFailed": "无法重置", + "play.restartNote": "修改其中任何一项都会重启封装器,观看者会中断几秒。", + "play.rungNote": "每个档位都会逐比特复制其转码版本的视频,因此新增一个只多一个封装器,绝不会多出第二次视频编码。各档位之间只有音轨不同;按推流目标的路由不受影响。", + "play.segmentLength": "分片时长(秒)", + "play.serveWithoutSession": "无需会话即可向观看者提供服务", + "play.sessionCap": "会话上限", + "play.sessions": "会话", + "play.share": "分享", + "play.shorterIsLowerLatencyAnd": "越短延迟越低,播放列表的变动也越频繁。", + "play.subtitle": "以公开 HLS 的形式,把直播直接提供给观看者。", + "play.threeIsTheMinimumA": "三个是播放器能够用来缓冲的最小值。", + "play.title": "Playout", + "play.unlisted": "不公开列出。", + "play.untitled": "未命名直播", + "play.videoSource": "视频源", + "play.watchingNow": "当前观看人数", + "play.whoCanWatch": "谁可以观看", + "player.copied": "已复制", + "player.copy": "复制", + "player.loading": "加载中…", + "player.missingToken": "你打开的链接缺少播放令牌,或令牌已被更改。请向分享链接的人索取最新的链接。", + "player.notLive": "当前没有直播", + "player.viewer": "位观看者", + "player.viewers": "位观看者", + "player.willStart": "直播开始后,本页会自动开始播放。", + "playlist.choose": "选择一个上传文件", + "playlist.chooseAria": "选择要添加的上传文件", "playlist.empty": "队列中还没有内容。", + "playlist.hint": "在 Media 中上传。若此播放列表引用的条目之后丢失了对应文件,它会在上方显示为需要处理,而不是在下一次故障切换取用时悄然失败。", "playlist.itemsAria": "播放列表项", - "playlist.stateReady": "就绪", - "playlist.stateTranscoding": "转码中", - "playlist.stateAttention": "需要处理", - "playlist.stateUnsaved": "尚未保存", "playlist.missingUpload": "已找不到这个上传文件。", - "playlist.moveUp": "将 {name} 上移", "playlist.moveDown": "将 {name} 下移", - "playlist.remove": "移除 {name}", - "playlist.choose": "选择一个上传文件", - "playlist.chooseAria": "选择要添加的上传文件", + "playlist.moveUp": "将 {name} 上移", "playlist.noUploads": "没有可用的上传文件", - "playlist.hint": "在 Media 中上传。若此播放列表引用的条目之后丢失了对应文件,它会在上方显示为需要处理,而不是在下一次故障切换取用时悄然失败。", - "dest.fbBoxTitle": "Facebook 转发与捐款按钮", - "dest.fbBoxIntro": "与“受众”在同一次创建调用中发送,因此适用同一条规则:在直播开始时应用一次,若为空则完全不作改动。", - "dest.fbBackupLabel": "发布备用推流", - "dest.fbBackupHelp": "将此推流的第二份副本发送到 Facebook 的备用接收地址,这样连接断开也不会中断直播。", - "dest.fbBackupCost": "会使此目标的上行带宽翻倍。", - "dest.fbBackupReconnect": "开启会让推流重新连接一次——因此请在开播前启用。", - "dest.fbCrosspostLabel": "转发到主页", - "dest.fbCrosspostPageId": "主页 ID", - "dest.fbCrosspostAlsoPost": "同时发帖", - "dest.fbCrosspostRemove": "移除此主页", - "dest.fbCrosspostAdd": "添加主页", - "dest.fbCrosspostHelp": "来自 Facebook 自家控制台的数字主页 ID(主页设置,或以主页身份登录时的 graph.facebook.com/me/accounts)——这里没有搜索功能,所以请粘贴 id,而不是名称或网址。“同时发帖”会以该主页的身份发布,而不只是把直播分享给它;不开启时,只会发生较低调的分享。", - "dest.fbDonateLabel": "捐款按钮的慈善机构 ID", - "dest.fbDonatePlaceholder": "慈善机构 ID", - "dest.fbDonateHelp": "这同样是 Facebook 募捐控制台里的一个不透明 id,而不是可供搜索的机构名称。留空则完全不附加捐款按钮。" + "playlist.remove": "移除 {name}", + "playlist.stateAttention": "需要处理", + "playlist.stateReady": "就绪", + "playlist.stateTranscoding": "转码中", + "playlist.stateUnsaved": "尚未保存", + "playlist.title": "播放列表", + "rec.delete": "删除", + "rec.deleteDescription": "该文件及其代理文件、缩略图和转写文本都会从磁盘上移除。此操作无法撤销。", + "rec.deleteFailed": "无法删除录像。", + "rec.deleteTitle": "要删除这段录像吗?", + "rec.deleted": "录像已删除。", + "rec.disabled": "已禁用", + "rec.disk": "磁盘", + "rec.diskRecordings": "录像", + "rec.download": "下载", + "rec.downloadNamed": "下载 {name}", + "rec.duration": "时长", + "rec.empty": "还没有录像。请在右侧启用录制并开始推流。", + "rec.enabled": "启用", + "rec.file": "文件", + "rec.free": "可用", + "rec.haltedDefault": "录制已停止:该卷的可用空间低于下限。", + "rec.haltedRestarts": "一旦释放出空间,录制器会自行重新启动。", + "rec.help.enabled": "在推流的同时把每一路输入写入磁盘,所有音轨保持分离且不做编码。录制与你发往平台的内容互不相干:关闭它不会影响直播输出,开启它消耗的是磁盘而非上行带宽。", + "rec.help.maxAgeHours": "无论总大小如何,删除超过该小时数的录像。当你只需要最近一两天的内容时很有用。设为 0 表示一直保留,直到其他限制将其删除。", + "rec.help.maxGb": "polyemesis 保留的录像总大小上限。超出后会从最旧的开始删除,直到总量重新符合限制。设为 0 表示不限大小——那时只有保留时长或可用空间下限能约束增长。", + "rec.help.minFreeGb": "一道硬性刹车:当卷上的可用空间低于此值时录制停止,空间释放后自行恢复。这是唯一会把 polyemesis 未写入的文件也计算在内的限制,因此它才是真正能防止共享磁盘被占满的设置。", + "rec.help.segmentSeconds": "录制器开始新文件之前,每个文件的时长。分段越短,崩溃时的损失越小,单个文件也更好搬运,但要管理的文件更多。切分点落在关键帧上,因此实际长度会有一秒左右的浮动。", + "rec.help.stemFiles": "除主文件外,再把每条音轨各自写成一个文件。当你事后剪辑、希望主播、嘉宾和游戏声分处不同声道时很有用。它会让一场直播的写入量大致乘以音轨数量,因此默认关闭。", + "rec.help.stemFormat": "两种都是无损格式,都不损失音质。FLAC 体积约为一半,除非你流程里的某个工具读不了它,否则应当选它;WAV 则在任何软件中都能打开。超过 8 声道的音轨会自动改用 WAV。", + "rec.hideStems": "隐藏分轨", + "rec.keepForever": "0 = 永久保留。", + "rec.loadFailed": "无法加载录像。", + "rec.maxAgeHours": "多久后删除(小时)", + "rec.maxGb": "最多保留(GB)", + "rec.minFreeGb": "可用空间低于此值时停止(GB)", + "rec.minFreeNote": "上面的上限只约束 polyemesis 写入的内容;卷上的其他文件仍可能把它占满。0 = 不设下限。", + "rec.noSizeLimit": "0 = 不限大小。", + "rec.recording": "录制中", + "rec.retentionNote": "保留清理每 30 秒运行一次:先删最旧的,先按时间再按总大小。正在写入的分段永远不会被删除。", + "rec.saveFailed": "无法保存设置。", + "rec.savePolicy": "保存保留策略", + "rec.saved": "录制设置已保存。", + "rec.segmentSeconds": "分段长度(秒)", + "rec.segments": "分段", + "rec.settingsTitle": "录制", + "rec.showStems": "显示分轨", + "rec.size": "大小", + "rec.started": "开始时间", + "rec.stemFiles": "分轨文件", + "rec.stemFlac": "FLAC — 无损,体积约为一半", + "rec.stemFormat": "分轨格式", + "rec.stemNote": "与主文件由同一进程写出,因此每条分轨都与其他分轨保持逐样本对齐。超过 8 声道的音轨会自动回退为 WAV。", + "rec.stemWav": "WAV — 无损,随处可打开", + "rec.stems": "分轨", + "rec.stemsTitle": "{count} 个分轨文件,与主文件逐样本对齐", + "rec.stemsUsage": "分轨({count} 个文件)", + "rec.subtitle": "完整的多轨归档:每一路输入音轨都以未编码的形式保留。", + "rec.title": "录像", + "rec.tracks": "音轨", + "rec.tracksTitle": "已保留 {count} 条音轨", + "rec.used": "已用", + "rec.volume": "卷", + "rend.aspectBlurpad": "模糊填充", + "rend.aspectBlurpadHint": "用画面本身裁切铺满并做模糊处理的副本来填充剩余区域。这是竖屏内容已经形成的惯例。", + "rend.aspectCrop": "裁切铺满", + "rend.aspectCropHint": "先按目标形状居中裁切,再缩放。主体在屏幕上的大小保持不变;画面边缘会被舍弃。", + "rend.aspectHandling": "宽高比处理", + "rend.aspectPad": "黑边填充", + "rend.aspectPadHint": "把整个画面缩放到刚好放得下,其余部分用纯色填充。不会丢失任何内容,但把 16:9 的源放到 9:16 的画布上,画面大部分都是色条。", + "rend.aspectStretch": "拉伸适配", + "rend.aspectStretchHint": "缩放到目标尺寸,如果源的比例不一致就任由画面变形。在其他模式出现之前,每个转码版本都是这么做的。", + "rend.boxColour": "背景框颜色", + "rend.boxOpacity": "背景框不透明度(%)", + "rend.builtInFont": "内置默认", + "rend.burnedText": "烧录文字", + "rend.chooseHardware": "请选择一个硬件编码器,或者更小的输出尺寸。", + "rend.colourBlack": "black", + "rend.colourWhite": "white", + "rend.costBeyond": "用软件实时编码这个规格超出了多数机器的能力", + "rend.costBeyondDetail": "{label} 单次编码为 {mpps}。在这个尺寸下,x264 通常跑不赢实时,而一个落后的转码版本会让读取它的每个推流目标都丢帧——等你发现时已经在直播中途了。{machine} {escape}", + "rend.costBusy": "预计会占用多个核心", + "rend.costBusyDetail": "{label} 在 {encoder} 上为 {mpps},属于繁重的实时编码。{machine} 开播后请留意编码器的速度:低于 1.0× 说明它正在落后,其下的推流目标会开始丢帧。 {escape}", + "rend.costMachineCores": " 本机报告有 {cores} 个逻辑核心。", + "rend.costModest": "适中的软件编码", + "rend.costModestDetail": "{label} 为 {mpps},多数机器都能应付。{machine} 它仍然是一次真实的编码——选择它的每个推流目标共享这次编码,但并非免费。", + "rend.costNoNumbers": "暂时无法估算开销", + "rend.costNoNumbersDetail": "还没有任何东西探测过源,因此“保持源规格”背后没有数据支撑。启动输入后这里就会填上。", + "rend.costOffloaded": "已卸载到 {encoder}", + "rend.costOffloadedDetail": "{label}({mpps})运行在 GPU 的固定功能编码器上,因此几乎不占 CPU。画质和码率控制因驱动而异,每颗芯片也各有上限——重要的节目开始前请先检查输出。", + "rend.create": "创建", + "rend.created": "转码版本已创建。在某个推流目标上选择它即可开始编码。", + "rend.custom": "自定义…", + "rend.deintAll": "每一帧", + "rend.deintAllHint": "适用于本身是隔行、却没有标注的源。不少 SDI 桥接器和采集卡不管收到什么都把画面标成逐行,在这些设备上,“仅隔行帧”什么也不做,看起来就像设置坏了。", + "rend.deintAuto": "仅隔行帧", + "rend.deintAutoHint": "只处理源标记为隔行的帧,逐行帧原样通过。对于混合素材,这是正确的选择。", + "rend.deintOff": "关闭", + "rend.deintOffHint": "适用于几乎人人都在用的逐行源。对逐行源做反交错只会让画面变软,毫无收益。", + "rend.deinterlace": "反交错", + "rend.deleteFailed": "无法删除该转码版本。", + "rend.deleteFallbackMany": "{count} 个推流目标将回退到直通,会原样收到源视频。请确认源仍然符合各平台的接受范围。", + "rend.deleteFallbackOne": "{count} 个推流目标将回退到直通,会原样收到源视频。请确认源仍然符合各平台的接受范围。", + "rend.deleteNoDestinations": "没有任何东西选择这个转码版本,因此删除它不会改变任何推流目标。", + "rend.deleteRenditionTitle": "要删除 {name} 吗?", + "rend.editRendition": "编辑转码版本", + "rend.empty": "还没有转码版本。每个推流目标都在直通,这完全正确——直到其中某个无法接受源为止,比如 Twitch、Kick 或 X 不接受的 4K60 输入。这时添加一个转码版本,并把那些推流目标指向它。", + "rend.encoder": "编码器", + "rend.encoderPreset": "编码器预设", + "rend.encoderRelatedFailed": "{name} 未被列出,因为本机上一个与之相关的编码器失败了。", + "rend.encoderTestFailed": "{name} 在本机的单帧测试编码中失败。", + "rend.encoderTestOk": "{name} 在本机成功编码了一帧测试画面。", + "rend.evenNote": "某一轴填 0 表示沿用源的取值并保持宽高比——只设置高度即可缩放。两者都必须是偶数:H.264 和 HEVC 没有奇数尺寸的色度平面。", + "rend.font": "字体", + "rend.frameRate": "帧率", + "rend.gop": "GOP", + "rend.gpuUnopenable": "存在 GPU,但此进程无法打开它", + "rend.hardwareFallback": "硬件", + "rend.hardwareScanFound": "硬件扫描发现的结果", + "rend.height": "高度", + "rend.hevcWarning": "该编码器输出 HEVC。多数 RTMP 推流目标只接受 H.264——只有当某个推流目标明确告诉你它支持 HEVC 时才选它。", + "rend.keyframeInterval": "关键帧间隔", + "rend.letterboxColour": "黑边颜色", + "rend.live": "实时", + "rend.loadFailed": "无法加载转码版本。", + "rend.marginX": "X 边距(%)", + "rend.marginY": "Y 边距(%)", + "rend.name": "名称", + "rend.namePlaceholder": "面向 Twitch 和 Kick 的 1080p60", + "rend.needsBothAWidthAnd": "宽度和高度都要填写——只要留空一个轴,缩放本身就已经保持宽高比了。", + "rend.needsBothAxes": "需要同时填写宽度和高度", + "rend.newRendition": "新建转码版本", + "rend.noEncoder": "此 FFmpeg 报告没有可用的视频编码器。", + "rend.noHardwareEscape": "本机上没有任何硬件编码器可用,因此没有地方可以把它挪过去——这里唯一的办法就是更小的输出尺寸。如果之后安装了驱动,或把 GPU 透传进了容器,请重新检测硬件。", + "rend.notProbed": "本机还没有做过测试编码,因此这份列表反映的是 FFmpeg 构建包含的内容,而不是硬件实际能做到的。重新检测即可弄清楚。", + "rend.notRunning": "未运行", + "rend.note": "备注", + "rend.noteDuplicateFps": "源的帧率是 {srcFps} fps,因此 {fps} fps 只是在重复帧。", + "rend.notePlaceholder": "这一档是做什么用的。", + "rend.noteUpscale": "源是 {width}×{height}。放大会消耗 CPU,却不会增加细节。", + "rend.opacity": "不透明度(%)", + "rend.percentNote": "所有数值都是相对画面的百分比,因此同一套设置在 16:9 和 9:16 的档位上都成立。在居中的轴上会忽略边距。", + "rend.preset": "预设", + "rend.presetPlaceholder": "veryfast", + "rend.presetStartingPoint": "仅作起点——请向平台核实当前的限制。", + "rend.redetectFailed": "无法重新检测硬件。", + "rend.redetectTitle": "重新执行 GPU 扫描,并用每个编码器编码一帧测试画面。需要几秒钟。", + "rend.redetectedNoHardware": "硬件已重新检测。本机没有任何硬件编码器通过测试编码。", + "rend.resolution": "分辨率", + "rend.restartEncode": "重启这次编码以及读取它的推流目标", + "rend.restartFailed": "无法重启该编码。", + "rend.restarting": "{name} 正在重启。它的推流目标会随它一起重新连接。", + "rend.save": "保存", + "rend.saveFailed": "无法保存该转码版本。", + "rend.seedNote": "这些只是给表单填个初值——保存前可以随意修改。平台上限会变化,也会因合作等级而异。", + "rend.signalError": "错误", + "rend.signalIdle": "空闲", + "rend.sizePct": "大小(占高度的 %)", + "rend.softwareEncoderFallback": "软件编码器", + "rend.speed": "速度", + "rend.subtitle": "一次共享的视频编码,服务多个推流目标。这里从不改动音频。", + "rend.target": "目标码率", + "rend.textColour": "文字颜色", + "rend.textPlaceholder": "我的频道", + "rend.title": "转码版本", + "rend.unused": "还没有推流目标选择它,因此没有在编码。在某个推流目标上选中它即可开始编码。", + "rend.uptime": "运行时长", + "rend.videoBitrate": "视频码率", + "rend.waitingSnapshot": "正在等待第一张实时快照。", + "rend.watermarkImage": "水印图片", + "rend.watermarkPlaceholder": "overlays/logo.png", + "rend.width": "宽度", + "rend.widthPct": "宽度(占画面的 %)", + "route.attack": "启动时间", + "route.audio": "音频", + "route.audioAndVideoGoOut": "音频和视频同时发出。", + "route.audioLaterThanVideo": "音频晚于视频", + "route.audioRouting": "音频路由", + "route.audioRoutingDesc": "精确选择每个推流目标接收哪些输入音轨。", + "route.autoLimitWhen2Tracks": "自动(2 条及以上音轨时限幅)", + "route.commentaryLanguage": "解说语言", + "route.couldNotApplyThePreset": "无法应用该预设。", + "route.couldNotCompileThisRouting": "无法编译这套路由。", + "route.couldNotLoadDestinations": "无法加载推流目标。", + "route.couldNotSaveTheRouting": "无法保存该路由。", + "route.couldNotSaveTrackRoles": "无法保存音轨角色。", + "route.custom": "自定义…", + "route.delayDirection": "延迟方向", + "route.destinations": "推流目标", + "route.duckingNote": "只要另一组在发声,就把这一组音轨压下去——比如话筒压过垫底的音乐。关闭则表示每条选中的音轨都按各自的增益直接相加,不做改动。", + "route.duckingTriggerNote": "触发源不必出现在这个推流目标的混音里——话筒可以在这里压低音乐,而实际只发送音乐。", + "route.enableDucking": "启用闪避", + "route.everyTrackReaches": "每条选中的音轨都会送到这个推流目标,音乐也包括在内。", + "route.excludeMusic": "排除音乐", + "route.keepMusic": "保留音乐", + "route.keepMusicOut": "不要把音乐送到这个推流目标", + "route.labelTracks": "标注音轨", + "route.langPlaceholder": "语言代码 — 例如 es", + "route.limiter04DbfsCeiling": "限幅器(−0.4 dBFS 上限)", + "route.localOnlyNote": "此服务器尚不保存音轨角色,因此这些只留在本浏览器里。页面上的其他内容照常保存。", + "route.loudnessTarget": "响度目标", + "route.lufsBroadcast": "EBU R128 广播", + "route.measuredOverTheWholeProgramme": "按整个节目来测量,而不是逐时逐刻:安静的段落依然安静。这些正是各平台自家归一化器瞄准的数值,因此达到其中一个,平台才不会把你的音量压下去。", + "route.mixMatrix": "混音矩阵", + "route.musicPolicy": "音乐策略", + "route.noTarget": "无目标(不改动电平)", + "route.normalizeLoudnorm": "响度(EBU R128)", + "route.normalizeOff": "关闭", + "route.off": "关", + "route.offset": "偏移", + "route.on": "开", + "route.platformDefault": "平台默认", + "route.podcastSpokenWord": "播客 / 口播", + "route.presetApplied": "预设已应用。请先检查,再保存。", + "route.range": "范围", + "route.ratio": "压缩比", + "route.release": "释放时间", + "route.result": "结果", + "route.role51": "5.1", + "route.roleClean": "纯净", + "route.roleMic": "话筒", + "route.roleMusic": "音乐", + "route.rolesNote": "角色描述的是输入,而不是这个推流目标,并且会单独保存。在下方某个推流目标提出要求之前,不会过滤任何内容。", + "route.simple": "简易", + "route.target": "目标", + "route.targetRole": "目标", + "route.thisDestinationSPlatform": "此推流目标所在的平台", + "route.threshold": "阈值", + "route.title": "Routing", + "route.triggerRole": "触发", + "route.truePeak": "真峰值", + "route.video": "视频", + "route.videoLaterThanAudio": "视频晚于音频", + "route.withoutATargetTheMix": "没有目标时,混音会按输入本身的电平直接发出,只应用上面的削波保护。", + "route.youtubeTwitchSpotify": "YouTube、Twitch、Spotify", + "set.acceptSelfSigned": "接受自签名证书", + "set.accountDisconnected": "已断开账号连接。", + "set.adminPassword": "管理员密码", + "set.alertDelivery": "告警投递", + "set.alertDeliveryDesc": "在放弃某条告警之前,会以多大力度反复重试一个没有响应的 webhook。", + "set.apiTokens": "API 令牌", + "set.apiTokensDesc": "面向脚本、Prometheus 以及其他无法保持会话的调用方的长期凭据。以 Authorization: Bearer pmk_… 的形式发送。令牌能做管理员能做的一切,唯独不能管理令牌——那仍留在密码之后,因此吊销令牌是不可逆的。", + "set.app": "应用路径", + "set.attemptsBackoff": "两次尝试之间的等待会自动退避,上限为 30 秒,因此该区间的上端已经意味着花好几分钟去追一个失效的端点。有上限本身就是重点:无休止地重试会把一个失效的 webhook 变成永远排不完的队列。", + "set.attemptsNote": "含首次尝试。1 表示从不重试。", + "set.attemptsRetryable": "只有值得重试的失败才会重试。来自某人已删除的 webhook 的 404 是永久性的,再试四次只会拖慢排在它后面的每一条告警;而来自反向代理的 502 正是这些尝试存在的意义。", + "set.audioMeters": "音频电平表", + "set.brokerPasswordCleared": "Broker 密码已清除。", + "set.brokerPlainNote": "mqtt:// 未加密。MQTT 会把用户名和密码放在 CONNECT 报文中发送,因此两者都以明文穿过网络——在受信任的局域网里没问题,这也是 Home Assistant 的常见情形;但在你无法掌控的链路上就不合适。若 broker 支持,请使用 mqtts://。", + "set.brokerUrl": "Broker 地址", + "set.brokerUrlNote": "mqtt://、mqtts://、ws:// 或 wss://。地址中携带密码会被拒绝:URL 会进入日志行和 ps 输出,事后无法收回。请填在下面的字段里,那里会在存储时加密。", + "set.brokerUrlPlaceholder": "mqtts://broker.local:8883", + "set.chatBothApply": "两者同时生效,且更宽松的一方胜出:一条消息在既早于设定小时数、又落在最新 N 条之外时才会被清除。因此在相同设置下,热闹的频道保留的时间跨度比安静的频道更短。", + "set.chatBufferNote": "“连接时发送”与另外两项是不同性质的数字。那两项限制留在磁盘上的量;这一项是内存中的缓冲区,按每个已连接的面板分配。", + "set.chatHistory": "聊天记录", + "set.chatHistoryDesc": "聊天记录能回溯多久——以及随之而来,管理员打开某人的卡片时能看到多少内容。", + "set.chatHours": "保留时长(小时)", + "set.chatKeep": "始终保留(条消息)", + "set.chatKeepNote": "一个下限,无论消息多旧。", + "set.chatNotOnlyDisk": "这不只是一项磁盘设置。在聊天中点击一个名字会打开此人此前说过的话,而那份记录正来自这里——没有任何平台为此提供 API,所以 polyemesis 只能展示它保留下来的部分。设得太短,一个已经闹了一周的人看起来就像从没说过话。", + "set.chatSendNote": "无需查询的回溯记录。", + "set.chatSendOnConnect": "连接时发送(条消息)", + "set.chatSendOnConnectShort": "连接时发送", + "set.chatSmall": "聊天占用很小。平均每秒十条消息的频道每小时约存 7 MB;多数频道远达不到这个量。", + "set.clientId": "客户端 ID", + "set.clientIdNote": "在 broker 上必须唯一。冲突是无法解释的重连循环的头号原因:broker 每次连接都会踢掉较旧的会话,于是两个客户端永无止境地重连。", + "set.clientIdPlaceholder": "由实例名派生", + "set.clientSecret": "客户端密钥", + "set.comingBack": "切回", + "set.confirmPassword": "确认新密码", + "set.connectedAccounts": "已连接的账号", + "set.copied": "已复制", + "set.copy": "复制", + "set.couldNotChangeThePassword": "无法更改密码。", + "set.couldNotClearThePassword": "无法清除密码。", + "set.couldNotCreateTheToken": "无法创建令牌。", + "set.couldNotReCheckThe": "无法重新检查凭据。", + "set.couldNotReadTheTls": "无法读取 TLS 状态。", + "set.couldNotRevokeTheToken": "无法吊销令牌。", + "set.couldNotSaveTheCredentials": "无法保存凭据。", + "set.couldNotSaveTheMqtt": "无法保存 MQTT 设置。", + "set.credsRemoved": "已删除凭据。", + "set.currentPassword": "当前密码", + "set.dataDirectory": "数据目录", + "set.deliveryAttempts": "投递尝试次数", + "set.dialsOnly": "服务器仅会连接:{schemes}。", + "set.disconnect": "断开连接", + "set.disconnectDesc": "使用它的推流目标会保留各自的设置,但不再刷新推流密钥。之后可以重新连接同一个账号。", + "set.disconnectTitle": "要断开此账号吗?", + "set.drawtextMissing": "此构建没有 drawtext 滤镜,因此无法使用文字叠加。它需要一个带 libfreetype 编译的 FFmpeg。", + "set.drawtextOk": "此构建可以在视频上绘制文字。", + "set.enabled": "启用", + "set.encoderUrl": "编码器地址", + "set.expires": "到期", + "set.failover": "故障切换", + "set.failoverDesc": "当直播源停止供流时使用的备用输入和占位画面。默认关闭:关闭状态下,处理链路与该功能出现之前逐字节一致。", + "set.failoverRemuxNote": "开启它会在输入与其下所有环节之间多加一道重新封装。开销不大,但并非没有,这也是它是一项选择而非默认值的原因。", + "set.fileSizeMb": "文件大小 (MB)", + "set.filesKept": "保留文件数", + "set.goingLive": "开播", + "set.goingLiveDesc": "多个推流目标同时启动时会发生什么。", + "set.graceNote": "在层级切换之前,直播源可以多久不提供任何数据。刻意设得比一次重连更长——编码器重新建立 RTMP 连接属于正常运行,而不是故障。", + "set.gracePeriod": "宽限时间(秒)", + "set.haDiscovery": "Home Assistant 自动发现", + "set.height": "高度", + "set.ingest": "输入", + "set.ingestDesc": "SRT 最多可承载六条音轨。RTMP 承载一条,作为无法使用 SRT 的编码器的备选。拉流会主动连接一个源,而不是等待别人推流过来。", + "set.ingestUrlCopied": "已复制输入地址。", + "set.instance": "实例", + "set.intervalNote": "未发生变化的状态不会重复发布,因此较短的间隔付出的是一次比较而非一条消息。保活时间决定了链路失效后多久才被察觉——也就决定了 broker 多快告知订阅者此实例已经离线。", + "set.issuer": "颁发者", + "set.kbps": "kbps", + "set.keepAlive": "保活时间 (s)", + "set.latencyMs": "延迟 (ms)", + "set.listeners": "监听", + "set.listenersDesc": "服务器在何处绑定,对整个安装生效。一个 SRT 端口即可服务所有源——它们靠各自的发布令牌区分,因此新增源从不意味着再开一个端口。RTMP 没有这种路由能力,所以它的端口只服务一个源。", + "set.logsBoundNote": "日志目录的上限是这两个值的乘积,因此持久化日志绝不会成为塞满磁盘的原因。", + "set.logsDesc": "捕获到的 FFmpeg 标准错误始终保存在内存中,但内存会随服务器一同消失——恰恰丢掉了解释它为何崩溃的那几行。开启持久化后会镜像到数据目录下的一个轮转文件中。", + "set.metersDesc": "一个附属进程负责测量每条轨道的每个声道。间隔越小刷新越快,占用的 CPU 也越多。", + "set.mode": "模式", + "set.modePull": "拉流 — 连接摄像机、信号源或文件", + "set.modeRtmp": "RTMP — 单音轨", + "set.modeSrt": "SRT — 多音轨(推荐)", + "set.mqtt": "MQTT 遥测", + "set.mqtt5Only": "仅支持 MQTT 5.0。", + "set.mqtt5OnlyNote": "锁定在 3.1.1 的 broker 根本无法完成连接——这不是功能降级模式。Mosquitto 2.x、EMQX 以及 Home Assistant 加载项都支持 5.0。", + "set.mqttDesc": "以保留消息的形式把状态发布到 broker,这样即使某个消费者在状态变化时没有连接,之后也仍能拿到当前值。", + "set.multiAccountNote": "多次连接同一平台即可推向多个频道——每个账号都会成为独立的推流目标。", + "set.name": "名称", + "set.newPassword": "新密码", + "set.no": "否", + "set.noEncryption": "没有任何东西在加密这个连接。密码和会话 Cookie 以明文穿过网络——请设置 tls.mode: auto,或在反向代理后面绑定到 127.0.0.1。", + "set.noLibsrt": "此 FFmpeg 构建不支持 SRT。请安装以 --enable-libsrt 配置的构建,或改用 RTMP。", + "set.noTokens": "还没有令牌。", + "set.passphrase": "密码短语", + "set.passphraseNote": "启用 AES 加密。留空表示不加密。SRT 会拒绝长度不在 10–79 个字符之间的密码短语。", + "set.passphrasePlaceholder": "可选 — 10 到 79 个字符", + "set.password": "密码", + "set.passwordChanged": "密码已更改。", + "set.passwordMismatch": "两次输入的新密码不一致。", + "set.passwordNote": "任何 API 都不会返回密码,因此即便已经设置,这个框也是空的。留空可保留已保存的密码;用下方按钮可清除它。", + "set.platform": "平台", + "set.platformsDesc": "各平台在 polyemesis 能自动化多少这件事上并不相同,而这种差异值得在开始前就知道,而不是一小时后才发现。推流本身在各平台的表现是一致的——按目标的音频路由、转码版本、重连和电平表都不依赖下面任何一列。", + "set.playLoop": "无直播时播放循环片段", + "set.playLoopNote": "占位画面之外的第二种候选:播放文件而不是一张静止图。两者可以同时开启——谁优先请参见故障切换设计中的层级顺序。", + "set.pointObs": "把 OBS 指向此地址。", + "set.preview": "预览", + "set.previewDesc": "面板的 HLS 监看。这是 polyemesis 唯一会重新编码视频的地方,而且它运行在自己的中继订阅上,因此绝不会影响任何推流目标。", + "set.previewIdleNote": "编码器在面板请求播放列表时启动,并在最后一次请求之后经过这段时间再次停止,因此关闭面板不会消耗 CPU。修改此项绝不会重启某人正在观看的预览。", + "set.processLogs": "进程日志", + "set.proxyTerminatesTls": "polyemesis 前面的反向代理已经终结了 TLS,因此此服务器有意提供纯 HTTP。", + "set.publishInterval": "发布间隔 (s)", + "set.reconnectCap": "重连退避上限 (s)", + "set.reconnectCapNote": "0 表示使用默认值。仅适用于 HTTP 和 HLS 源。", + "set.redirectNote": "必须完全一致,包括协议和端口。如果你通过反向代理访问 polyemesis,请使用那个公开地址。", + "set.redirectUri": "需加入白名单的重定向 URI", + "set.removeCreds": "删除凭据", + "set.removeCredsDesc": "通过此应用连接的每个账号都将停止刷新其令牌,并需要重新连接。客户端密钥无法从这里恢复——你需要在平台上重新创建一个应用。", + "set.removeCredsTitle": "要删除这些开发者凭据吗?", + "set.returnAfter": "切回所需时长(稳定秒数)", + "set.returnAutomatic": "自动 — 主源稳定后即切回", + "set.returnManual": "手动 — 在我切换之前一直留在备用源", + "set.returnManualNote": "默认为手动,是因为自动切回可能来回抖动:一个断断续续恢复的主源会让节目在直播中途反复来回切换。", + "set.rtmpPort": "RTMP 端口", + "set.rtspNote": "除非摄像机不支持,否则请用 TCP。经过 NAT 的 UDP RTSP 看上去已连接,却什么也送不出来。", + "set.rtspTransport": "RTSP 传输方式", + "set.saveAndDrop": "保存并中断当前直播", + "set.saveIngest": "保存输入设置", + "set.segmentSec": "分片 (s)", + "set.settingsSavedAffectedProcessesHave": "设置已保存。受影响的进程已重启。", + "set.showSlate": "无直播时显示占位画面", + "set.slateColour": "占位画面颜色", + "set.slateColourNote": "只写一个词,因为该值会进入滤镜图,而在那里逗号会结束参数。仅在未设置图片时使用。", + "set.slateColourPlaceholder": "black", + "set.slateImage": "占位画面图片", + "set.slateImageNote": "数据目录内的一个路径。留空则改为填充纯色——正因为颜色不存在打不开的文件,它才是兜底方案。", + "set.slateImagePlaceholder": "slate/holding.png", + "set.sourceUrl": "源地址", + "set.sourceUrlNote": "{schemes} 之一。file:// 源是相对于数据目录的路径,且不能包含“..”——它被限制在其中,方式与上传的文件相同。", + "set.sourceUrlPlaceholder": "rtsp://camera.local/stream1", + "set.srtPort": "SRT 端口", + "set.stagger": "错开连接 (ms)", + "set.staggerNote": "0 表示关闭。否则开播意味着每个推流目标在同一瞬间建立连接、协商 TLS 并开始编码音频——在小型机器上这正是最容易丢帧的时刻,而且恰恰是你正盯着看的时刻,因为你刚刚开播。几百毫秒就足够了。", + "set.staggerReconnectNote": "它绝不会延迟重连。凌晨三点掉线的推流目标会立即恢复,而不是排在已经健康的进程后面等候。", + "set.stepByStep": "分步设置指引", + "set.stopAfterIdle": "空闲后停止 (s)", + "set.streamKey": "推流密钥", + "set.streamKeyNote": "推流方必须使用完全一致的路径,这样开放的端口才不等于敞开的大门。", + "set.subject": "使用者", + "set.synthesiseNote": "所有主流平台都会拒绝没有音轨的推流。开启后,只有视频的输入会获得一条静音立体声轨,其推流目标随之启动;关闭时它们会以“routing profile selects no audio”拒绝。它只对探测结果为零条音轨的输入生效,因此绝不会顶替你实际发送的音频。", + "set.synthesiseSilent": "合成一条静音轨", + "set.syntheticAudio": "合成音频", + "set.syntheticDesc": "当输入只有视频、完全没有音频时的处理方式。", + "set.tabIngest": "输入", + "set.tabPipeline": "处理链路", + "set.tabPlatforms": "平台凭据", + "set.tabSecurity": "安全", + "set.title": "设置", + "set.tlsAcme": "Let's Encrypt", + "set.tlsAuto": "自动", + "set.tlsDesc": "在 config.yaml 中配置,而不是这里——服务器开始监听之前 TLS 就必须正确。此卡片汇报的是配置的结果。", + "set.tlsManual": "手动证书", + "set.tlsOff": "不在此处终止", + "set.tlsSelfsigned": "自签名", + "set.tokenHashNote": "polyemesis 只保存哈希值。若你弄丢了它,请吊销此令牌并另建一个。", + "set.tokenNamePlaceholder": "prometheus", + "set.tokenRevoked": "已吊销 {name}。", + "set.topicPrefix": "主题前缀", + "set.topicPrefixNote": "前缀中的分隔符会被保留,因此 home/av 表示两级。以 $ 开头的前缀会被拒绝:broker 保留了这类前缀,使用 # 的订阅者永远收不到它们——遥测会发布成功,却恰恰在你用来排查的视图里看不见。", + "set.transportSecurity": "传输安全", + "set.updateInterval": "刷新间隔 (ms)", + "set.username": "用户名", + "set.validFor": "适用于", + "set.whatPlatformsDo": "各平台分别支持什么", + "set.whyDevApp": "为什么需要你自己的开发者应用", + "set.whyDevAppDesc": "polyemesis 无法内置 OAuth 客户端密钥——任何拿到二进制文件的人都会得到它们,平台随即会将其吊销。你只需注册一次免费的开发者应用,把客户端 ID 和密钥粘贴到这里,之后 polyemesis 就会替你获取推流密钥。", + "set.writeLogs": "将日志写入磁盘", + "set.yes": "是", + "sources.addDescription": "第二路节目,拥有独立的输入、推流目标和转码版本——例如与横屏画布并行的竖屏画布。它会以默认端口启动,建议修改端口以免两者冲突。", + "sources.addFailed": "无法添加源", + "sources.addSource": "添加源", + "sources.addTitle": "添加一个源", + "sources.added": "已添加“{name}”。请在下方设置它的输入端口。", + "sources.app": "应用路径", + "sources.applyAndDrop": "应用并中断推流", + "sources.copied": "已复制{what}", + "sources.copyFailed": "无法复制{what}", + "sources.copyTokenAria": "复制令牌", + "sources.copyUrlAria": "复制 {proto} 地址", + "sources.default": "默认", + "sources.defaultDescription": "未指定源的请求将作用于此源。", + "sources.deleteAria": "删除 {name}", + "sources.deleteConfirm": "删除源", + "sources.deleteDescription": "它的推流目标和转码版本会一并删除——它们描述这路节目的去向,离开节目便毫无意义。录像会保留:文件仍在磁盘上且可以播放,只是不再归属于某个源。", + "sources.deleteFailed": "无法删除源", + "sources.deleteThis": "删除此源", + "sources.deleteTitle": "要删除“{name}”吗?", + "sources.deleted": "已删除“{name}”及其推流目标", + "sources.destinations": "推流目标", + "sources.dirtyIdle": "存在未保存的输入更改。应用会重启此源的输入。", + "sources.dirtyPublishing": "有编码器正在向此源推流。应用会重启其输入,并断开所有正在观看的人。", + "sources.enableAria": "启用 {name}", + "sources.help.app": "RTMP 的应用路径——地址中位于主机与推流密钥之间的部分。多数编码器默认使用“live”。除非确有需要,否则不要修改。", + "sources.help.ingest": "编码器如何抵达此源。SRT 通过重传丢失的数据包来适应有丢包的链路,是公网传输的合适默认选项。RTMP 更为老旧但支持广泛,不过在不稳定链路上质量下降明显。选择拉流时,polyemesis 会主动从你提供的地址拉取,而不是等待别人推流过来。", + "sources.help.latencyMs": "SRT 在放弃一个迟到或重传的数据包之前会等待多久。数值越大越能扛住较差的链路,代价是延迟:约为往返时延的 2.5 倍是不错的起点,因此 40 ms 的 RTT 大约对应这里的 100 ms。设得过低,短暂的抖动就会变成肉眼可见的画面异常。", + "sources.help.passphrase": "加密 SRT 连接。就目前而言,真正把陌生人挡在这路输入之外的正是它。长度为 10 到 79 个字符。留空则任何能访问该端口的人都可以推流进来。", + "sources.help.streamKey": "编码器通过 RTMP 推流时发送的密钥。它是保护这路输入的凭据,请像对待密码一样对待它:任何持有它的人都能向你的推流目标广播。", + "sources.help.token": "当多个源共用一个输入端口时,用于标识此源。它是否同时充当访问控制取决于输入模式——下方的说明会指出当前的实际情况。更换会签发新令牌,旧令牌保持五分钟有效,因此正在直播的编码器不会在中途被断开。", + "sources.ingest": "输入", + "sources.lastSourceTitle": "最后一个源无法删除——安装至少需要一路输入", + "sources.latencyMs": "延迟 (ms)", + "sources.loadFailed": "无法加载源", + "sources.loading": "正在加载源…", + "sources.loss": "丢包", + "sources.modePull": "拉流", + "sources.name": "名称", + "sources.namePlaceholder": "竖屏", + "sources.noEngine": "此源没有正在运行的引擎。常见原因是输入端口已被其他源或其他进程占用。", + "sources.notRunning": "未运行", + "sources.ownDescription": "独立的推流目标、转码版本和录像。", + "sources.passphrase": "密码短语", + "sources.passphrasePlaceholder": "10–79 个字符", + "sources.publishing": "推流中", + "sources.renditions": "转码版本", + "sources.retrans": "重传", + "sources.rotate": "更换", + "sources.rotateConfirm": "更换令牌", + "sources.rotateFailed": "无法更换令牌", + "sources.rotateIdle": "新令牌会立即签发。旧令牌仍会在五分钟内有效,以免正在使用它的编码器被中断。", + "sources.rotateTitle": "要为“{name}”更换令牌吗?", + "sources.rotateWhilePublishing": "当前有编码器正在向此源推流。旧令牌仍会在五分钟内有效,因此不会被立即中断——但必须在该时间窗结束前完成更新。", + "sources.rtt": "RTT", + "sources.running": "运行中", + "sources.saveFailed": "无法保存", + "sources.streamKey": "推流密钥", + "sources.subtitle": "每个源对应一路输入节目。推流目标和转码版本都归属于某个源。", + "sources.title": "源", + "sources.token": "令牌", + "sources.tokenEnforcedDetailRtmp": "它就是推流密钥:使用它作为密钥推流到上面的地址。轮换会签发新的,旧的仍可用五分钟,因此可以在不中断直播的情况下切换。", + "sources.tokenEnforcedDetailSrt": "在编码器的 SRT streamid 中填入它,即可通过共享端口向此源推流。轮换会签发新的,旧的仍可用五分钟,因此可以在不中断直播的情况下切换。", + "sources.tokenIsCredential": "此令牌就是访问凭据。", + "sources.tokenIssued": "已签发新令牌", + "sources.tokenNotEnforced": "此令牌已保存,但当前并未被强制校验。", + "sources.tokenNotEnforcedRtmp": "各个源是靠端口彼此隔离的。真正保护这路输入的是上方的推流密钥。若要让令牌成为访问凭据,请在设置中启用单端口输入。", + "sources.tokenNotEnforcedSrt": "各个源是靠端口彼此隔离的。真正保护这路输入的是上方的 SRT 密码短语。若要让令牌成为访问凭据,请在设置中启用单端口输入。", + "state.disabled": "已禁用", + "state.failed": "失败", + "state.live": "直播中", + "state.offline": "离线", + "state.reconnecting": "重新连接中", + "state.starting": "开始", + "state.stopped": "已停止" } diff --git a/ui/src/lib/platformLinks.test.ts b/ui/src/lib/platformLinks.test.ts new file mode 100644 index 00000000..02b64ae2 --- /dev/null +++ b/ui/src/lib/platformLinks.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from "vitest"; + +import { platformLinkFor, platformNoun } from "./platformLinks"; +import type { ChatMessage, ChatPlatform } from "./types"; + +function msg(over: Partial & { platform: ChatPlatform }): ChatMessage { + return { + id: "m1", + text: "hello", + at: "2026-08-05T12:00:00Z", + ...over, + // After the spread: the author default has to merge with any override + // rather than be replaced by it, and `platform` is required. + platform: over.platform, + author: { id: "u1", name: "Viewer", ...(over.author ?? {}) }, + } as ChatMessage; +} + +describe("platformLinkFor", () => { + it("builds Twitch's moderator viewer card when the channel is known", () => { + const link = platformLinkFor( + msg({ platform: "twitch", channel: "SomeStreamer", author: { id: "u1", name: "BadActor" } }), + ); + // The viewer card is channel-scoped: both halves of the path matter, and + // both are lowercased because Twitch logins are. + expect(link?.url).toBe("https://www.twitch.tv/popout/somestreamer/viewercard/badactor"); + expect(link?.kind).toBe("moderator-card"); + }); + + // The distinction the whole module exists for. Only Twitch-with-a-channel is + // a moderation view; if anything else ever reports "moderator-card" the UI + // promises a moderator history and hands them a profile page. + it("marks every non-Twitch platform as a profile, not a moderator card", () => { + for (const platform of ["youtube", "kick", "facebook"] as const) { + const link = platformLinkFor(msg({ platform, channel: "chan" })); + expect(link, platform).not.toBeNull(); + expect(link?.kind, platform).toBe("profile"); + // And each must say why, or the caveat is decoration. + expect(link?.caveat, platform).not.toBe(""); + } + }); + + it("falls back to the Twitch channel page when no channel came with the message", () => { + const link = platformLinkFor(msg({ platform: "twitch", author: { id: "u1", name: "BadActor" } })); + expect(link?.url).toBe("https://www.twitch.tv/badactor"); + // Downgraded honestly rather than guessing a channel to keep the richer label. + expect(link?.kind).toBe("profile"); + }); + + it("uses the channel id for YouTube, not the display name", () => { + // Display names collide and change; the author id is the channel id and is + // the only thing that resolves to the right person. + const link = platformLinkFor( + msg({ platform: "youtube", author: { id: "UC123abc", name: "Some Name" } }), + ); + expect(link?.url).toBe("https://www.youtube.com/channel/UC123abc"); + }); + + it("returns null rather than a wrong link when the identifier is missing", () => { + // A link built from nothing lands on the platform's home page or a 404, and + // a moderator who clicks it concludes the feature is broken. No menu item + // is the better outcome. + expect(platformLinkFor(msg({ platform: "youtube", author: { id: "", name: "Anon" } }))).toBeNull(); + expect(platformLinkFor(msg({ platform: "facebook", author: { id: "", name: "Anon" } }))).toBeNull(); + expect(platformLinkFor(msg({ platform: "kick", author: { id: "u1", name: "" } }))).toBeNull(); + expect(platformLinkFor(msg({ platform: "twitch", author: { id: "u1", name: "" } }))).toBeNull(); + }); + + it("has no link shape to guess for a custom platform", () => { + expect(platformLinkFor(msg({ platform: "custom" }))).toBeNull(); + }); + + // Names arrive from strangers on the internet and go straight into a URL. + it("escapes names so a crafted one cannot reshape the URL", () => { + const link = platformLinkFor( + msg({ platform: "kick", author: { id: "u1", name: "evil/../../admin?x=1" } }), + ); + expect(link?.url.startsWith("https://kick.com/")).toBe(true); + // Path separators and query markers must be percent-encoded, or the link + // navigates somewhere other than the profile it claims to open. + expect(link?.url).not.toContain("/../"); + expect(link?.url).not.toContain("?"); + }); + + it("strips a leading @ that viewers often include in names", () => { + const link = platformLinkFor(msg({ platform: "kick", author: { id: "u1", name: "@Viewer" } })); + expect(link?.url).toBe("https://kick.com/viewer"); + }); +}); + +describe("platformNoun", () => { + it("uses each platform's own capitalisation", () => { + expect(platformNoun("youtube")).toBe("YouTube"); + expect(platformNoun("twitch")).toBe("Twitch"); + expect(platformNoun("kick")).toBe("Kick"); + expect(platformNoun("facebook")).toBe("Facebook"); + }); +}); diff --git a/ui/src/lib/platformLinks.ts b/ui/src/lib/platformLinks.ts new file mode 100644 index 00000000..85dfb2a1 --- /dev/null +++ b/ui/src/lib/platformLinks.ts @@ -0,0 +1,154 @@ +import type { ChatMessage, ChatPlatform } from "@/lib/types"; + +// Where a viewer can be looked at on the platform they were talking on. +// +// The honest summary, which the UI must not blur: only ONE of these four is a +// chat-history view. Twitch's popout viewer card is the window a Twitch +// moderator already knows — recent messages in this channel, plus the ban and +// timeout controls. The other three have no equivalent at any URL, so the best +// available link is the person's profile or channel, which shows what they +// publish and nothing about what they said in chat. +// +// That difference is the whole reason `kind` exists. A single "Open on +// " label would promise a moderator the same thing everywhere and +// deliver it only on Twitch, and the failure is silent: they click, get a +// profile page, and conclude the viewer has no history worth acting on. +// +// polyemesis's own user card is the cross-platform answer and stays the primary +// one; these links are the escape hatch for acting where the platform's own +// tools are richer. See internal/db/chat.go's ChatMessagesByAuthor for why no +// platform API can close this gap. + +export type PlatformLinkKind = + /** The platform's own moderation view of this person, with history. */ + | "moderator-card" + /** A profile or channel page. No chat history, no moderation controls. */ + | "profile"; + +export interface PlatformLink { + url: string; + kind: PlatformLinkKind; + /** Menu label. Says what the destination actually is. */ + label: string; + /** The caveat, for a title attribute. Empty when there is nothing to warn about. */ + caveat: string; +} + +/** Twitch display names are the login with capitalisation for most accounts, + * but an account with a localised display name (common on CJK channels) has a + * login that shares none of its characters. We store the display name, so the + * viewer card URL is right for the common case and wrong for that minority — + * which is why the caveat below says the name may not resolve rather than + * claiming the link always lands. */ +function twitchLogin(name: string): string { + return name.trim().toLowerCase().replace(/^@/, ""); +} + +/** The platform link for whoever sent this message, or null when we cannot + * build one that would land somewhere useful. */ +export function platformLinkFor(m: ChatMessage): PlatformLink | null { + const id = m.author.id?.trim() ?? ""; + const name = m.author.name?.trim() ?? ""; + + switch (m.platform) { + case "twitch": { + // The viewer card is scoped to a channel: it shows what this person said + // in THAT chat. Without the channel there is no card to open, so fall + // back to the profile rather than guessing a channel. + const channel = m.channel?.trim(); + const login = twitchLogin(name); + if (!login) return null; + if (channel) { + return { + url: `https://www.twitch.tv/popout/${encodeURIComponent( + twitchLogin(channel), + )}/viewercard/${encodeURIComponent(login)}`, + kind: "moderator-card", + label: "Open moderator card on Twitch", + caveat: + "Twitch's own viewer card for this channel: their recent messages here, plus ban and timeout. " + + "Opens logged in as whoever this browser is signed in to Twitch as. " + + "An account with a localised display name may not resolve.", + }; + } + return { + url: `https://www.twitch.tv/${encodeURIComponent(login)}`, + kind: "profile", + label: "Open channel on Twitch", + caveat: + "Their channel page, not a moderator view — this message arrived without a channel, " + + "so there is no viewer card to open.", + }; + } + + case "youtube": { + // YouTube author ids ARE channel ids, so this lands on the right person + // even when the display name is ambiguous or has changed. + if (!id) return null; + return { + url: `https://www.youtube.com/channel/${encodeURIComponent(id)}`, + kind: "profile", + label: "Open channel on YouTube", + caveat: + "YouTube publishes no per-viewer chat history, so this is their channel page. " + + "Moderate from the live chat in YouTube Studio.", + }; + } + + case "kick": { + if (!name) return null; + return { + url: `https://kick.com/${encodeURIComponent(twitchLogin(name))}`, + kind: "profile", + label: "Open profile on Kick", + caveat: + "Kick publishes no per-viewer chat history, so this is their profile page.", + }; + } + + case "facebook": { + // Facebook comment author ids are page-scoped: the id identifies the + // person only within the page they commented on, so this resolves for the + // page owner and can 404 for anyone else. + if (!id) return null; + return { + url: `https://www.facebook.com/${encodeURIComponent(id)}`, + kind: "profile", + label: "Open profile on Facebook", + caveat: + "Facebook publishes no per-viewer chat history, so this is their profile. " + + "Comment author ids are scoped to the page, so this may not resolve from another account.", + }; + } + + default: + // A custom or future platform: no URL shape we can honestly guess at. + return null; + } +} + +/** Opens a platform link in a real separate window rather than a tab, because + * the point is to review there while still watching chat here. + * + * `noopener` is not optional: without it the opened page gets a handle on this + * one through `window.opener` and can navigate it somewhere else, which on an + * operator console that is already authenticated is worth refusing outright. */ +export function openPlatformLink(link: PlatformLink): void { + window.open(link.url, `polyemesis-${link.kind}`, "noopener,noreferrer,width=420,height=720"); +} + +/** Human label for a platform, for menu copy. */ +export function platformNoun(p: ChatPlatform): string { + switch (p) { + case "twitch": + return "Twitch"; + case "youtube": + return "YouTube"; + case "kick": + return "Kick"; + case "facebook": + return "Facebook"; + default: + return p; + } +} diff --git a/ui/src/lib/preset-drift.test.ts b/ui/src/lib/preset-drift.test.ts new file mode 100644 index 00000000..cb4e4aa6 --- /dev/null +++ b/ui/src/lib/preset-drift.test.ts @@ -0,0 +1,64 @@ +/// +import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +/* The destination preset catalogue exists TWICE. + * + * internal/db/platforms.go is the source of truth and is served from + * GET /api/v1/platforms/presets; ui/src/components/DestinationDialog.tsx keeps + * a hand-written mirror so the picker works before any fetch resolves. That is + * a deliberate trade, and it was completely unguarded: adding a platform, or a + * field, to one side reached the other only if somebody remembered. + * + * This was not hypothetical. Researched per-platform encoder guidance was added + * to the Go catalogue and would never have appeared in the UI — no test, no + * type error, no warning. The mirror looked correct because it was correct for + * the fields it already had. + */ + +const ROOT = new URL("../../../", import.meta.url).pathname; + +function goPresetIDs(): string[] { + const src = readFileSync(join(ROOT, "internal/db/platforms.go"), "utf8"); + return [...src.matchAll(/ID:\s*"([a-z0-9-]+)",\s*Name:/g)].map((m) => m[1]); +} + +function tsPresetIDs(): string[] { + const src = readFileSync(join(ROOT, "ui/src/components/DestinationDialog.tsx"), "utf8"); + // The TS mirror's entries are `id: "youtube",` inside the PRESETS array. + const start = src.indexOf("const PRESETS:"); + const end = src.indexOf("\n];", start); + return [...src.slice(start, end).matchAll(/\bid:\s*"([a-z0-9-]+)"/g)].map((m) => m[1]); +} + +describe("the two destination preset catalogues", () => { + it("contain the same platforms", () => { + const go = goPresetIDs(); + const ts = tsPresetIDs(); + expect(go.length, "no presets parsed out of platforms.go — the regex has rotted").toBeGreaterThan(20); + expect(ts.length, "no presets parsed out of DestinationDialog.tsx").toBeGreaterThan(20); + + const missingFromUI = go.filter((id) => !ts.includes(id)); + const missingFromGo = ts.filter((id) => !go.includes(id)); + + expect(missingFromUI, `in platforms.go but not in the UI mirror: ${missingFromUI.join(", ")}`).toEqual([]); + expect(missingFromGo, `in the UI mirror but not in platforms.go: ${missingFromGo.join(", ")}`).toEqual([]); + }); + + it("agree on which platforms carry encoder guidance", () => { + // Guidance is researched, sourced and dated in Go. The UI does not need to + // duplicate the numbers, but it must not silently disagree about WHICH + // platforms have them — that is what decides whether the operator is + // offered a starting point at all. + const src = readFileSync(join(ROOT, "internal/db/platforms.go"), "utf8"); + const withGuidance = [...src.matchAll(/ID:\s*"([a-z0-9-]+)",\s*Name:[^\n]*\n\s*Video:/g)].map((m) => m[1]); + expect(withGuidance.length, "platforms.go has no VideoGuidance at all").toBeGreaterThan(5); + + // Every platform carrying guidance must exist in the UI mirror, or the + // guidance can never be shown. + const ts = tsPresetIDs(); + const orphaned = withGuidance.filter((id) => !ts.includes(id)); + expect(orphaned, `have guidance in Go but no UI preset to show it on: ${orphaned.join(", ")}`).toEqual([]); + }); +}); diff --git a/ui/src/lib/rendition-consequence.test.ts b/ui/src/lib/rendition-consequence.test.ts new file mode 100644 index 00000000..1204cabe --- /dev/null +++ b/ui/src/lib/rendition-consequence.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, it } from "vitest"; +import { + computeLeaving, + joinConsequence, + leaveConsequence, + type RenditionUsage, +} from "./rendition-consequence"; + +/** The four states of the consequence line. + * + * Two of them — what happens to the encode you are LEAVING — are the novel + * part of this design; nothing surveyed in docs/notes/video-treatment-ui.md + * tells an operator that at all. They are also the two that were untested, + * which is how "N other destinations stay on it" shipped without anyone + * confirming N is right. + */ + +const usage = (destinations: number, enabledDestinations: number): RenditionUsage => ({ + destinations, + enabledDestinations, +}); + +describe("joinConsequence", () => { + it("says joining is free when the encode is already running", () => { + const line = joinConsequence(usage(3, 2)); + expect(line).toContain("already encoding"); + expect(line).toContain("no new encode starts"); + // The count shown is total destinations, not enabled ones: the operator is + // being told how many things share this picture, not how many are live. + expect(line).toContain("3 destinations"); + }); + + it("says an idle encode will START, which is the expensive case", () => { + const line = joinConsequence(usage(4, 0)); + expect(line).toBe("Starts one shared encode when an enabled destination uses it."); + // The free-case wording must not leak into the paid case. These are the two + // sentences the whole shared model turns on. + expect(line).not.toContain("no new encode starts"); + }); + + it("does not claim an encode is running just because destinations exist", () => { + // Four destinations configured, none enabled: nothing is encoding. A check + // on `destinations` rather than `enabledDestinations` gets this backwards + // and promises a free join that will actually spawn a process. + expect(joinConsequence(usage(4, 0))).toContain("Starts one"); + }); + + it("counts one destination in the singular", () => { + expect(joinConsequence(usage(1, 1))).toContain("Feeds 1 destination ·"); + expect(joinConsequence(usage(1, 1))).not.toContain("1 destinations"); + }); +}); + +describe("leaveConsequence", () => { + it("warns that the encode stops when this was the last one on it", () => { + const line = leaveConsequence({ name: "720p30 backup", lastOut: true, others: 0 }); + expect(line).toContain("Stops the"); + expect(line).toContain("720p30 backup"); + }); + + it("reassures when others remain, and counts them", () => { + const line = leaveConsequence({ name: "1080p60", lastOut: false, others: 2 }); + expect(line).toBe("2 other enabled destinations stay on “1080p60”. Nothing else changes."); + expect(line).not.toContain("Stops"); + }); + + it("uses the singular for a single survivor", () => { + const line = leaveConsequence({ name: "1080p60", lastOut: false, others: 1 }); + expect(line).toContain("1 other enabled destination stay"); + expect(line).not.toContain("1 other enabled destinations"); + }); +}); + +describe("computeLeaving", () => { + const catalogue = { + 10: { name: "1080p60", destinations: 3, enabledDestinations: 3 }, + 11: { name: "720p30 backup", destinations: 1, enabledDestinations: 1 }, + 12: { name: "idle tier", destinations: 2, enabledDestinations: 0 }, + } as Record; + const usageOf = (id: number) => catalogue[id] ?? null; + + it("says nothing when the destination had no encode to begin with", () => { + expect(computeLeaving(null, "10", true, usageOf)).toBeNull(); + expect(computeLeaving(undefined, "10", true, usageOf)).toBeNull(); + // Rendition ids are truthy positives; 0 is not a real id and must not be + // treated as "had one". + expect(computeLeaving(0, "10", true, usageOf)).toBeNull(); + }); + + it("says nothing when the selection has not moved", () => { + // The dialog re-renders constantly; a leave warning for the encode you are + // still on is noise that teaches operators to ignore the line. + expect(computeLeaving(10, "10", true, usageOf)).toBeNull(); + }); + + it("says nothing about an encode it cannot find", () => { + // Better silent than confidently wrong about someone else's stream. + expect(computeLeaving(99, "10", true, usageOf)).toBeNull(); + }); + + it("reports last-one-out when this destination is the only enabled user", () => { + // enabledDestinations is 1 — and that 1 IS this destination, because + // nothing has been saved yet. Reaching zero would mean it had already been + // removed, which never happens while the dialog is open. + const leaving = computeLeaving(11, "", true, usageOf); + expect(leaving).toEqual({ name: "720p30 backup", lastOut: true, others: 0 }); + }); + + it("does not report last-one-out while others are still enabled", () => { + const leaving = computeLeaving(10, "", true, usageOf); + expect(leaving?.lastOut).toBe(false); + // Three enabled, minus this one, leaves two. + expect(leaving?.others).toBe(2); + }); + + it("never claims a DISABLED destination's departure stops an encode", () => { + // A disabled destination was never in the enabled count, so removing it + // changes nothing. Warning that an encode will stop when it will not is + // the failure that makes the whole line untrustworthy. + const leaving = computeLeaving(11, "", false, usageOf); + expect(leaving?.lastOut).toBe(false); + }); + + it("does not report a negative survivor count for an idle encode", () => { + // Zero enabled destinations, so 0 - 1 = -1 without the clamp: "-1 other + // enabled destinations stay on it". + const leaving = computeLeaving(12, "", true, usageOf); + expect(leaving?.others).toBe(0); + expect(leaveConsequence(leaving!)).not.toContain("-1"); + }); + + it("fires when moving to copy, not just between encodes", () => { + // Switching to passthrough is the selection most likely to strand an + // encode, and its form value is the empty string rather than an id. + expect(computeLeaving(11, "", true, usageOf)).not.toBeNull(); + }); +}); diff --git a/ui/src/lib/rendition-consequence.ts b/ui/src/lib/rendition-consequence.ts new file mode 100644 index 00000000..74d6cbf2 --- /dev/null +++ b/ui/src/lib/rendition-consequence.ts @@ -0,0 +1,94 @@ +/** What choosing — or abandoning — a shared video encode actually costs. + * + * A rendition is shared and ref-counted: the first enabled destination on it + * starts one encode, every later one joins that same encode for free, and it + * stops when the last enabled destination leaves. That arithmetic is the whole + * reason renditions exist, and it is invisible on a dropdown that shows only a + * name. `docs/notes/video-treatment-ui.md` has the full argument. + * + * These live outside the dialog because they are the part worth testing. In + * JSX they were four nested ternaries inside a template literal — reachable + * only by rendering the component with the right server fixtures, so in + * practice never checked at all. Off-by-one in `lastOut` is the expensive + * mistake here: it tells an operator their colleague's encode is about to stop + * when it is not, or stays silent when it is. + */ + +/** The usage counts the API already sends with every rendition. */ +export interface RenditionUsage { + /** Destinations configured to use this encode, enabled or not. */ + destinations: number; + /** Of those, the ones actually enabled — the count that decides whether an + * encode is running, because a disabled destination costs nothing. */ + enabledDestinations: number; +} + +/** What happens when this destination JOINS the selected encode. + * + * The distinction is free-vs-not: joining something already running adds no + * process, while being the first one on an idle encode starts one. */ +export function joinConsequence(usage: RenditionUsage): string { + if (usage.enabledDestinations > 0) { + const n = usage.destinations; + return `Feeds ${n} destination${n === 1 ? "" : "s"} · already encoding. This destination joins the running encode — no new encode starts.`; + } + return "Starts one shared encode when an enabled destination uses it."; +} + +/** The encode being left behind, and whether this departure stops it. */ +export interface Leaving { + name: string; + /** True when removing this destination takes the encode's enabled count to + * zero, so the encode stops. */ + lastOut: boolean; + /** Enabled destinations that remain on it afterwards. */ + others: number; +} + +/** Work out what the abandoned encode does once this destination has gone. + * + * Returns null when there is nothing to say: no previous encode, or the + * selection has not actually moved. + * + * @param previousRenditionId the encode this destination is saved with + * @param selectedRenditionId the encode now chosen, as the form's string value + * ("" for copy/passthrough) + * @param enabled whether this destination is itself enabled — a disabled one + * was never counted, so leaving cannot stop anything + * @param usageOf usage counts for a rendition id, or null if unknown + */ +export function computeLeaving( + previousRenditionId: number | null | undefined, + selectedRenditionId: string, + enabled: boolean, + usageOf: (id: number) => (RenditionUsage & { name: string }) | null, +): Leaving | null { + if (!previousRenditionId) return null; + if (String(previousRenditionId) === selectedRenditionId) return null; + + const was = usageOf(previousRenditionId); + if (!was) return null; + + // THE OFF-BY-ONE: this destination is still counted in enabledDestinations + // while the dialog is open, because nothing has been saved yet. So "last one + // out" is a count of one, not zero — the one being this destination itself. + // + // A disabled destination was never in that count, so it can never be the one + // whose departure stops the encode, however low the count is. + const lastOut = enabled && was.enabledDestinations <= 1; + + return { + name: was.name, + lastOut, + others: Math.max(0, was.enabledDestinations - 1), + }; +} + +/** How to say what `computeLeaving` worked out. */ +export function leaveConsequence(leaving: Leaving): string { + if (leaving.lastOut) { + return `Stops the “${leaving.name}” encode — no other enabled destination is on it.`; + } + const n = leaving.others; + return `${n} other enabled destination${n === 1 ? "" : "s"} stay on “${leaving.name}”. Nothing else changes.`; +} diff --git a/ui/src/lib/theme.test.ts b/ui/src/lib/theme.test.ts new file mode 100644 index 00000000..23e46f43 --- /dev/null +++ b/ui/src/lib/theme.test.ts @@ -0,0 +1,108 @@ +/// +import { describe, expect, it } from "vitest"; +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { join } from "node:path"; + +/* Colour classes that name a token which was never declared. + * + * Tailwind silently drops a utility whose colour does not exist — no build + * error, no console warning, just an element that inherits its parent. That is + * invisible in review and invisible in a screenshot unless you already know + * what colour it was supposed to be. + * + * It had already happened twice when this test was written. `text-ok` (no --ok + * token) marked a *healthy* backup feed, so the good state rendered as plain + * body text while the bad state was amber — the two were distinguished only by + * the presence of colour, and the good one was the invisible half. `danger` + * (no --danger token) coloured the banner warning that a stream is public to + * the internet, whose own comment reads "the one thing on this page that must + * never be subtle." + * + * Reading index.css rather than the built bundle keeps this a unit test: it + * needs no build step and fails on the commit that introduces the typo. + */ + +/* Read from disk, not through Vite. `import "../index.css?raw"` returns an + * empty string here — vitest stubs CSS imports by default — which made every + * assertion below pass against an empty token set. The "declares the signal + * tokens" case exists to catch exactly that, and did. + * + * tsconfig.app.json restricts `types` to vite/client, so the node import is + * declared per-file above rather than by widening the app's global types: + * browser code has no business seeing `process` or `fs`. */ +const SRC = new URL("../", import.meta.url).pathname; + +function declaredColours(): Set { + const css = readFileSync(join(SRC, "index.css"), "utf8"); + const names = new Set(); + for (const m of css.matchAll(/--color-([a-z0-9-]+)\s*:/g)) names.add(m[1]); + return names; +} + +function tsxFiles(dir: string): string[] { + return readdirSync(dir).flatMap((entry: string) => { + const p = join(dir, entry); + return statSync(p).isDirectory() ? tsxFiles(p) : p.endsWith(".tsx") ? [p] : []; + }); +} + +/* Tailwind's own palette. A utility naming one of these is valid without any + * token, so it is not evidence of a typo — whether raw palette colours *should* + * be used here is a separate question from whether they render. */ +const PALETTE = new Set([ + "inherit", "current", "transparent", "black", "white", + "slate", "gray", "zinc", "neutral", "stone", "red", "orange", "amber", "yellow", + "lime", "green", "emerald", "teal", "cyan", "sky", "blue", "indigo", "violet", + "purple", "fuchsia", "pink", "rose", +]); + +/* Non-colour utilities that share these prefixes, excluded by shape rather + * than by name: sizes (`border-2`, `text-sm`), edges (`border-t`, `divide-y`, + * `border-b-0`), alignment (`text-left`) and background behaviour + * (`bg-no-repeat`). An edge utility is the awkward case — `border-t` and + * `border-teal` differ only in length. */ +const NON_COLOUR = + /^(0|px|auto|none|solid|dashed|dotted|double|hidden|full|left|right|center|justify|start|end|top|bottom|middle|clip|ellipsis|balance|pretty|wrap|nowrap|xs|sm|base|md|lg|xl|\d?xl|inner|collapse|separate|no-repeat|repeat(-[xy])?|contain|cover|fixed|local|scroll|origin|clip-\w+|[tblrxyse](-\d+)?|\d+(\.\d+)?)$/; + +const PREFIX = "text|bg|border|ring|fill|stroke|outline|divide|accent|caret|placeholder|shadow|from|via|to"; + +describe("colour utilities name a declared token", () => { + const declared = declaredColours(); + + it("index.css declares the signal tokens the app is built on", () => { + // Guards the guard: if the parse silently returned nothing, every + // assertion below would pass vacuously. + for (const t of ["live", "warn", "down", "destructive", "primary", "muted"]) { + expect(declared, `--color-${t} should be declared`).toContain(t); + } + }); + + it("no utility refers to a token that does not exist", () => { + const bad: string[] = []; + + for (const file of tsxFiles(SRC)) { + const src = readFileSync(file, "utf8"); + // Only inside className/class attributes — prose in comments and UI copy + // produces matches like `text-splitting` otherwise. + for (const attr of src.matchAll(/class(?:Name)?=(?:"([^"]*)"|\{`([^`]*)`\}|\{"([^"]*)"\})/g)) { + const chunk = attr[1] ?? attr[2] ?? attr[3] ?? ""; + const line = src.slice(0, attr.index).split("\n").length; + + for (const u of chunk.matchAll( + new RegExp(`(? declared.has(c))) continue; + bad.push(`${file.slice(SRC.length)}:${line} ${u[0]}`); + } + } + } + + expect(bad, `these name no --color-* token, so Tailwind emits nothing:\n${bad.join("\n")}`) + .toEqual([]); + }); +}); diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts index 2063a252..f51dc8b3 100644 --- a/ui/src/lib/types.ts +++ b/ui/src/lib/types.ts @@ -1,7 +1,10 @@ /** Types mirroring the Go API. Kept hand-written and small rather than * generated, so the shapes the UI actually consumes stay obvious. */ -export const MAX_TRACKS = 6; +/** Mirrors routing.MaxTracks. The server also reports this as `maxTracks` in + * its capabilities response — prefer that where it is to hand, and treat this + * as the fallback for code that renders before capabilities have loaded. */ +export const MAX_TRACKS = 32; export type RoutingMode = "simple" | "matrix"; export type NormalizeMode = "auto" | "off" | "limiter" | "loudnorm"; @@ -384,6 +387,34 @@ export interface FontInfo { /** A rendition plus its usage. `enabledDestinations` is the ref count the * engine acts on: at zero there is no process and no CPU burnt. */ +/** A platform's OWN published encoder guidance, served from + * GET /platforms/presets. Advisory: it seeds a form and annotates a choice, + * and never gates anything. + * + * `source` and `checked` are not decoration. Once a bitrate is sitting in a + * form field an operator cannot tell a researched number from a guess, so the + * UI always shows where it came from and when it was last read. */ +export interface VideoGuidance { + width?: number; + height?: number; + fps?: number; + kbpsMin?: number; + kbpsMax?: number; + gopSeconds?: number; + note?: string; + source: string; + checked: string; +} + +/** One entry of the server's platform catalogue. Only the fields the dialog + * reads are typed here — the UI keeps its own preset list for the picker and + * consults this for the researched data it must not duplicate. */ +export interface PlatformPresetInfo { + id: string; + name: string; + video?: VideoGuidance; +} + export interface RenditionView { rendition: Rendition; destinations: number; @@ -1529,6 +1560,23 @@ export interface ChatUserCard { retentionNote: string; } +/** A chat search result set. + * + * Carries the same two honesty fields as ChatUserCard and for a sharper + * reason: search is the one place an operator can conclude something did NOT + * happen. "No results" here means "not in the scrollback we kept", never "never + * said", so `retentionNote` has to be rendered alongside an empty result and + * not only alongside a full one. */ +export interface ChatSearchResult { + query: string; + /** Echoed back so the UI can label a narrowed result set. */ + platform?: ChatPlatform; + messages: ChatMessage[]; + /** The limit was reached; older matches may exist beyond this page. */ + truncated: boolean; + retentionNote: string; +} + /** Messages that are gone. Carried by the "chatRetract" event. * * A list rather than one id because a timeout removes everything one author diff --git a/ui/src/pages/AuthScreen.tsx b/ui/src/pages/AuthScreen.tsx index 4ef43321..d42e9e32 100644 --- a/ui/src/pages/AuthScreen.tsx +++ b/ui/src/pages/AuthScreen.tsx @@ -6,6 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/lib/api"; +import { useT } from "@/lib/i18n"; const MIN_PASSWORD = 8; @@ -18,6 +19,7 @@ export function AuthScreen({ mode: "setup" | "login"; onDone: () => void; }) { + const t = useT(); const [username, setUsername] = useState("admin"); const [password, setPassword] = useState(""); const [confirm, setConfirm] = useState(""); @@ -35,7 +37,7 @@ export function AuthScreen({ return; } if (password !== confirm) { - toast.error("The passwords do not match."); + toast.error(t("auth.passwordMismatch")); return; } } @@ -44,13 +46,13 @@ export function AuthScreen({ try { if (isSetup) { await api.setup(username.trim() || "admin", password); - toast.success("Admin account created."); + toast.success(t("auth.accountCreated")); } else { await api.login(username.trim(), password); } onDone(); } catch (err) { - toast.error(err instanceof Error ? err.message : "Something went wrong."); + toast.error(err instanceof Error ? err.message : t("auth.somethingWentWrong")); setPassword(""); setConfirm(""); } finally { @@ -70,17 +72,16 @@ export function AuthScreen({ - {isSetup ? "First-run setup" : "Sign in"} + {isSetup ? t("auth.firstRun") : t("auth.signIn")} {isSetup - ? "Choose the administrator credentials for this server. There is no account recovery, so store them somewhere safe." - : "Enter your administrator credentials."} + ? t("auth.firstRunDesc") : t("auth.signInDesc")}
- +
- + - + {busy && } - {isSetup ? "Create account" : "Sign in"} + {isSetup ? t("auth.createAccount") : t("auth.signIn")} diff --git a/ui/src/pages/AutomationPage.tsx b/ui/src/pages/AutomationPage.tsx index aee8ad23..4a5f0be5 100644 --- a/ui/src/pages/AutomationPage.tsx +++ b/ui/src/pages/AutomationPage.tsx @@ -50,6 +50,7 @@ import { Stat } from "@/components/signature/Stat"; import { useLiveData } from "@/hooks/useLiveData"; import { autoApi } from "@/lib/autoApi"; import { timestamp } from "@/lib/format"; +import { useT, type Translator, type TranslationKey } from "@/lib/i18n"; export type AlertFormat = "json" | "discord" | "slack"; export type AlertSeverity = "info" | "warning" | "critical"; @@ -138,31 +139,37 @@ interface ScheduleRun { /** Event types are stored strings, so the catalogue comes from the server and * only the wording lives here. An unknown type still renders — as its own * name — rather than vanishing from the picker. */ -const EVENT_LABELS: Record = { - "destination.down": "Destination went down", - "destination.recovered": "Destination recovered", - "destination.falling_behind": "Destination falling behind realtime", - "destination.caught_up": "Destination keeping up again", - "ingest.lost": "Ingest lost", - "ingest.recovered": "Ingest recovered", - "failover.switched": "Failover switched source", - "audio.clipping": "Audio clipping", - "disk.low": "Disk running low", - "disk.recovered": "Disk recovered", - "loudness.out_of_compliance": "Loudness out of compliance", - "loudness.recovered": "Loudness back in compliance", - "auth.login.failed": "Repeated failed sign-ins", - "auth.login.succeeded": "Signed in", - "auth.password.changed": "Admin password changed", - "auth.token.created": "API token created", - "auth.token.revoked": "API token revoked", - "settings.changed": "Settings changed", - "clip.captured": "Clip captured", +const EVENT_LABELS: Record = { + "destination.down": "auto.destinationWentDown", + "destination.recovered": "auto.destinationRecovered", + "destination.falling_behind": "auto.destinationFallingBehindRealtime", + "destination.caught_up": "auto.destinationKeepingUpAgain", + "ingest.lost": "auto.evIngestLost", + "ingest.recovered": "auto.evIngestRecovered", + "failover.switched": "auto.failoverSwitchedSource", + "audio.clipping": "auto.evAudioClipping", + "disk.low": "auto.evDiskLow", + "disk.recovered": "auto.evDiskRecovered", + "loudness.out_of_compliance": "auto.loudnessOutOfCompliance", + "loudness.recovered": "auto.loudnessBackInCompliance", + "auth.login.failed": "auto.repeatedFailedSignIns", + "auth.login.succeeded": "auto.evSignedIn", + "auth.password.changed": "auto.adminPasswordChanged", + "auth.token.created": "auth.tokenCreated", + "auth.token.revoked": "auth.tokenRevoked", + "settings.changed": "auto.evSettingsChanged", + "clip.captured": "auto.evClipCaptured", }; -const eventLabel = (t: string) => EVENT_LABELS[t] ?? t; +/** The stored event name is the fallback: an unknown type renders as itself + * rather than vanishing from the picker. */ +const eventLabel = (t: Translator, name: string) => + name in EVENT_LABELS ? t(EVENT_LABELS[name]) : name; -const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; +const WEEKDAY_KEYS: TranslationKey[] = [ + "auto.weekSun", "auto.weekMon", "auto.weekTue", "auto.weekWed", + "auto.weekThu", "auto.weekFri", "auto.weekSat", +]; function hhmm(minutes: number): string { const m = Math.max(0, Math.min(1439, Math.round(minutes))); @@ -206,7 +213,7 @@ function describeSchedule(s: Schedule): string { case "daily": return `Every day at ${s.localTime || hhmm(s.atMinutes)} ${zone}`; case "weekly": { - const days = (s.days ?? []).map((d) => WEEKDAYS[d] ?? d).join(", "); + const days = (s.days ?? []).map((d) => WEEKDAY_KEYS[d] ?? d).join(", "); return `${days || "no days"} at ${s.localTime || hhmm(s.atMinutes)} ${zone}`; } default: @@ -225,6 +232,7 @@ const errText = (err: unknown, fallback: string) => * unattended stream wants "tell me when it breaks" and "start it at seven" * in the same sitting. */ export function AutomationPage() { + const t = useT(); const [rules, setRules] = useState([]); const [meta, setMeta] = useState(null); const [schedules, setSchedules] = useState([]); @@ -247,9 +255,9 @@ export function AutomationPage() { setSchedules(s ?? []); setRuns(runList ?? []); }) - .catch((err) => toast.error(errText(err, "Could not load automation settings."))) + .catch((err) => toast.error(errText(err, t("auto.couldNotLoadAutomationSettings")))) .finally(() => setLoading(false)); - }, []); + }, [t]); useEffect(load, [load]); @@ -267,8 +275,8 @@ export function AutomationPage() { return (
{loading ? ( @@ -389,6 +397,7 @@ function AlertRules({ onEdit: (r: AlertRule) => void; onNew: () => void; }) { + const t = useT(); const [testing, setTesting] = useState(null); const toggle = async (r: AlertRule, enabled: boolean) => { @@ -396,7 +405,7 @@ function AlertRules({ await autoApi.put(`/alerts/rules/${r.id}`, { enabled }); onReload(); } catch (err) { - toast.error(errText(err, "Could not change the rule.")); + toast.error(errText(err, t("auto.couldNotChangeTheRule"))); } }; @@ -405,10 +414,10 @@ function AlertRules({ const remove = async (r: AlertRule) => { try { await autoApi.del(`/alerts/rules/${r.id}`); - toast.success("Alert rule deleted."); + toast.success(t("auto.ruleDeleted")); onReload(); } catch (err) { - toast.error(errText(err, "Could not delete the rule.")); + toast.error(errText(err, t("auto.couldNotDeleteTheRule"))); } }; @@ -421,7 +430,7 @@ function AlertRules({ await autoApi.post(`/alerts/rules/${r.id}/test`, {}); toast.success(`Test alert delivered to ${r.name}. Check the channel.`); } catch (err) { - toast.error(errText(err, "The endpoint did not accept the test message.")); + toast.error(errText(err, t("auto.theEndpointDidNotAccept"))); } finally { setTesting(null); } @@ -433,7 +442,7 @@ function AlertRules({
- Webhook rules + {t("auto.webhookRules")} @@ -449,9 +458,9 @@ function AlertRules({ - Rule - Endpoint - Subscribed to + {t("auto.rule")} + {t("auto.endpoint")} + {t("auto.subscribedTo")} On @@ -495,8 +504,8 @@ function AlertRules({ size="icon-sm" onClick={() => test(r)} disabled={testing === r.id} - aria-label="Send test alert" - title="Send a test alert now" + aria-label={t("auto.sendTest")} + title={t("auto.sendTestTitle")} > {testing === r.id ? : } @@ -504,7 +513,7 @@ function AlertRules({ variant="ghost" size="icon-sm" onClick={() => onEdit(r)} - aria-label="Edit" + aria-label={t("auto.edit")} > @@ -512,7 +521,7 @@ function AlertRules({ variant="ghost" size="icon-sm" onClick={() => confirmDelete.ask(r)} - aria-label="Delete" + aria-label={t("auto.delete")} className="text-muted-foreground hover:text-down" > @@ -529,23 +538,23 @@ function AlertRules({ - Delivery + {t("auto.delivery")} - - - - + + + + - + {stats?.lastSent && ( @@ -562,9 +571,9 @@ function AlertRules({ open={confirmDelete.open} onOpenChange={confirmDelete.onOpenChange} subject={confirmDelete.target?.name ?? ""} - title="Delete this alert rule?" - description="The rule stops firing. Its webhook endpoint is untouched, and you can recreate the rule." - confirmLabel="Delete rule" + title={t("auto.deleteRuleTitle")} + description={t("auto.deleteRuleDesc")} + confirmLabel={t("auto.deleteRule")} onConfirm={async () => { if (confirmDelete.target) await remove(confirmDelete.target); }} @@ -584,6 +593,7 @@ function RuleDialog({ onClose: () => void; onSaved: () => void; }) { + const t = useT(); const [form, setForm] = useState(draft); const [saving, setSaving] = useState(false); const editing = draft.id > 0; @@ -612,10 +622,10 @@ function RuleDialog({ }; if (editing) await autoApi.put(`/alerts/rules/${form.id}`, body); else await autoApi.post("/alerts/rules", body); - toast.success(editing ? "Alert rule saved." : "Alert rule created. Send it a test."); + toast.success(editing ? t("auto.ruleSaved") : t("auto.ruleCreated")); onSaved(); } catch (err) { - toast.error(errText(err, "Could not save the rule.")); + toast.error(errText(err, t("auto.couldNotSaveTheRule"))); } finally { setSaving(false); } @@ -625,7 +635,7 @@ function RuleDialog({ !o && onClose()}> - {editing ? "Edit alert rule" : "New alert rule"} + {editing ? t("auto.editRule") : t("auto.newRule")} One endpoint and what it wants to hear about. Everything raised about the same subject inside the coalescing window arrives as a single message with a count. @@ -634,35 +644,34 @@ function RuleDialog({
- + setForm({ ...form, name: e.target.value })} />
- + setForm({ ...form, url: e.target.value })} /> {editing - ? "Shown masked because the secret lives in the path. Leave it alone to keep the current endpoint, or paste a new URL to replace it." - : "Discord and Slack both give you one under channel settings."} + ? t("auto.secretMasked") : t("auto.webhookHint")}
- +
- +
- + @@ -769,6 +778,7 @@ function Schedules({ onEdit: (s: Schedule) => void; onNew: () => void; }) { + const t = useT(); const { status } = useLiveData(); const destNames = useMemo(() => { const m = new Map(); @@ -792,7 +802,7 @@ function Schedules({ }); onReload(); } catch (err) { - toast.error(errText(err, "Could not change the schedule.")); + toast.error(errText(err, t("auto.couldNotChangeTheSchedule"))); } }; @@ -801,10 +811,10 @@ function Schedules({ const remove = async (s: Schedule) => { try { await autoApi.del(`/schedules/${s.id}`); - toast.success("Schedule deleted."); + toast.success(t("auto.scheduleDeleted")); onReload(); } catch (err) { - toast.error(errText(err, "Could not delete the schedule.")); + toast.error(errText(err, t("auto.couldNotDeleteTheSchedule"))); } }; @@ -824,7 +834,7 @@ function Schedules({
- Schedules + {t("auto.schedules")} @@ -839,9 +849,9 @@ function Schedules({
- Schedule - When - Next + {t("auto.schedule")} + {t("auto.when")} + {t("auto.next")} On @@ -883,7 +893,7 @@ function Schedules({ variant="ghost" size="icon-sm" onClick={() => onEdit(s)} - aria-label="Edit" + aria-label={t("auto.edit")} > @@ -891,7 +901,7 @@ function Schedules({ variant="ghost" size="icon-sm" onClick={() => confirmDelete.ask(s)} - aria-label="Delete" + aria-label={t("auto.delete")} className="text-muted-foreground hover:text-down" > @@ -908,12 +918,12 @@ function Schedules({ - Last sweep + {t("auto.lastSweep")} {runs.length === 0 ? (

- Nothing has fired yet. The scheduler sweeps every 20 seconds. + {t("auto.nothingFired")}

) : ( runs.map((r) => ( @@ -921,7 +931,7 @@ function Schedules({
{r.name} - {r.error ? "error" : r.fired ? "fired" : "skipped"} + {r.error ? "error" : r.fired ? t("auto.fired") : t("auto.skipped")}
@@ -932,9 +942,7 @@ function Schedules({ )) )}

- An occurrence missed because the server was down is marked handled and skipped rather - than fired late — a stream that starts itself four hours late is worse than one that - never started. + {t("auto.missedNote")}

@@ -942,9 +950,9 @@ function Schedules({ open={confirmDelete.open} onOpenChange={confirmDelete.onOpenChange} subject={confirmDelete.target?.name ?? ""} - title="Delete this schedule?" - description="The schedule stops running. Anything it already started is unaffected." - confirmLabel="Delete schedule" + title={t("auto.deleteScheduleTitle")} + description={t("auto.deleteScheduleDesc")} + confirmLabel={t("auto.deleteSchedule")} onConfirm={async () => { if (confirmDelete.target) await remove(confirmDelete.target); }} @@ -962,6 +970,7 @@ function ScheduleDialog({ onClose: () => void; onSaved: () => void; }) { + const t = useT(); const { status } = useLiveData(); const [form, setForm] = useState(draft); const [saving, setSaving] = useState(false); @@ -1001,10 +1010,10 @@ function ScheduleDialog({ }; if (editing) await autoApi.put(`/schedules/${form.id}`, body); else await autoApi.post("/schedules", body); - toast.success(editing ? "Schedule saved." : "Schedule created."); + toast.success(editing ? t("auto.scheduleSaved") : t("auto.scheduleCreated")); onSaved(); } catch (err) { - toast.error(errText(err, "Could not save the schedule.")); + toast.error(errText(err, t("auto.couldNotSaveTheSchedule"))); } finally { setSaving(false); } @@ -1014,7 +1023,7 @@ function ScheduleDialog({ !o && onClose()}> - {editing ? "Edit schedule" : "New schedule"} + {editing ? t("auto.editSchedule") : t("auto.newSchedule")} Wall-clock times carry their own zone, so a schedule does not move when the server's does. @@ -1023,18 +1032,18 @@ function ScheduleDialog({
- + setForm({ ...form, name: e.target.value })} />
- +
- +
@@ -1082,7 +1091,7 @@ function ScheduleDialog({ {form.kind === "once" ? (
- +
- +
- + setForm({ ...form, tz: e.target.value })} /> @@ -1130,9 +1139,9 @@ function ScheduleDialog({ {form.kind === "weekly" && (
- +
- {WEEKDAYS.map((label, i) => ( + {WEEKDAY_KEYS.map((label, i) => (
- File - Covers - Length - Size + {t("clips.file")} + {t("clips.covers")} + {t("clips.length")} + {t("clips.size")} @@ -276,7 +278,7 @@ export function ClipsPage() { rough start @@ -296,7 +298,7 @@ export function ClipsPage() {
-