Compare commits

...

20 Commits

Author SHA1 Message Date
KoalaDev aa84b63c77 docs: add v2.4.5 entry to CHANGELOG.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 17:38:36 +02:00
KoalaDev bd60a14754 feat(extension): send nothing to the server while alone in a room
When no other peer is present, heartbeats, force-sync, and episode auto-sync
are now fully suppressed (previously the keepAlive heartbeat, force-sync, and
episode lobby were still broadcast to an empty room):
- keepAlive heartbeat gated on a live peer count
- force-sync is a no-op when solo (no pause-then-timeout freeze, no traffic)
- episode auto-sync skips the lobby entirely and just plays the next episode

The solo state is recomputed live from the current peer list on every event
(never cached), so the instant another peer joins syncing resumes. On that
solo->not-solo transition the background also requests an immediate heartbeat
from the content script so the newcomer sees the current position without
waiting for the next interval.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 17:38:36 +02:00
KoalaDev a1bdcf4325 fix(extension): stop storing room/settings in chrome.storage.sync
Room ID, password, and username were resurrected from synced storage on a
fresh install (sync survives an uninstall in the user's Google account), which
made the extension silently auto-connect to a dead room and appear permanently
connected. getSettings() and all settings reads (popup, content, audio-options,
applyAudioSettingsToTab) are now local-only, and legacy keys are actively
purged from sync on install/update/startup. Only onboardingComplete and
dismissedHints remain in sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 17:38:36 +02:00
KoalaDev b846803062 Potential fix for code scanning alert no. 14: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-23 00:56:28 +02:00
KoalaDev 4bc7ad365d docs: add v2.4.4 entry to CHANGELOG.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:49:42 +02:00
GitHub Action 68b2205b0d chore(release): update versions to v2.4.4 [skip ci] 2026-06-22 22:46:03 +00:00
KoalaDev b450845522 fix(extension): harden reconnect, ping, and offline-queue flush
Address client-side rate-limit edge cases that could trip the server's
per-socket event budget or per-IP connection limit:

- Pace the offline event-queue flush after (re)connect in batches
  (10 per 3s) instead of one synchronous burst, so a reconnect after a
  long outage no longer dumps the whole backlog and gets kicked.
- Wrap socket.send() in try/catch and re-queue on failure (race with a
  server-side disconnect) instead of losing the event.
- Ping liveness: tolerate one missed PONG; only force a reconnect after
  2 consecutive misses (~20s) instead of a single hard 5s timeout.
- Reconnect backoff: tune to ~8 attempts/60s (under the connection limit)
  and add +/-20% jitter to de-synchronize reconnect herds.

All magic numbers extracted into named constants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:45:29 +02:00
KoalaDev 27023ea58e feat(server): raise event rate limit to 50 and name rate-limit constants
Raise the per-socket event budget from 30 to 50 per 10s to give legitimate
bursts (episode joins, force-sync, ACK flurries) more headroom. Extract the
previously inline connection/event/health/admin windows and limits into named
constants (CONNECTION_RATE_LIMIT, EVENT_RATE_LIMIT, *_WINDOW_MS). Tests now
derive their thresholds from the exported constants instead of hardcoding them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:45:19 +02:00
KoalaDev 1a7d23ce93 ci: add PR/push verification workflow and harden release
- Add .github/workflows/ci.yml running `npm run verify` (lint, tests,
  audits, builds) on every push to main and pull request, so regressions
  can't reach main or a release tag unchecked.
- release.yml: use `npm ci` instead of `npm install` for reproducible builds.
- package.json: add `test` script aliasing the verify suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:45:10 +02:00
KoalaDev d2c8ca1c1e Merge PR #11: extract tab management logic into independent module
3-way merge of Kaia-Alenia's tab-manager refactor (PR #11) onto current
main (v2.4.3). Only the tab-related service-worker listeners move out of
background.js into extension/modules/tab-manager.js via initTabManager({...})
dependency injection; all other recent main changes are preserved untouched.
Behavior-preserving; listeners still register synchronously at top level.

The unrelated website/assets/NewLogoIcon_64.webp change from the PR was
excluded to keep main clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 02:06:38 +02:00
Kaia-Alenia 1c404a7f11 refactor(extension): extract tab management logic into independent module 2026-06-19 14:11:47 -06:00
Timo e2bba04efd docs: update README language count to 15 (add zh, uk)
The localization section still listed 13 languages; Ukrainian (uk) and
Chinese Simplified (zh) were added in v2.4.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:49:34 +02:00
GitHub Action c7b29c277c chore(release): update versions to v2.4.3 [skip ci] 2026-06-19 14:39:06 +00:00
Timo e06965b95c docs: add v2.4.3 entry to CHANGELOG.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:38:49 +02:00
Timo 23d43741ce chore: replace Ko-Fi with Support links, drop uninstall token
Switch the Ko-Fi support links to https://support.koalastuff.net across the
README badge, the extension footer (popup.html/template.html, label
"Support KoalaSync") and remove the now-unused KOFI_URL constant. Stop
generating/sending the anonymous uninstall token: initUninstallURL() now
registers the feedback URL without the `t` parameter or storage write.
CHANGELOG updated to match.

Also guard the client CMD_ACK path: only ACK a command sender that is still
a known peer in the room, so we don't emit ACKs to peers that already left
(which the server now drops as absent-peer ACKs anyway).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:26:54 +02:00
Timo 027d2dbde5 fix(sync): coalesce play/pause bursts and quiet absent-peer ACK logs
Media players fire bursts of native play/pause events (source swaps, ABR,
ads, teardown). Each was relayed as a distinct command, spamming peers and
tripping the relay's event rate limit. Add leading+trailing coalescing in
the content script: emit the first event instantly (zero added latency) and,
on a 150ms window, collapse a burst to its final state. Echo-suppression and
seek-flush stay synchronous on event arrival; the trailing send is never
deduped against the leading one (a remote command may change shared state
mid-window — re-sending is the safe, idempotent choice).

Server: split the EVENT_ACK handler's logging so only a genuine different-room
ACK is logged as [SECURITY]; an ACK to a peer that already left is logged
quietly as [ACKDROP] (verbose-only), so real signals are no longer drowned out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:26:31 +02:00
Timo a0c16df664 feat(i18n): add Ukrainian & Chinese, fix zh/uk translation quality
Add two new languages (uk, zh / zh_CN) across the extension and website:
register them in SUPPORTED_LANGUAGES, the Chrome _locales map, the website
build/lang-init/app routing, hreflang/og/schema tags and language selectors.

Fix systematic machine-translation word-sense errors in the new zh/uk
locales (e.g. zh "Status"=地位→状态, "Leave Room"=留出空间→离开房间,
"Play"=玩→播放, "Seek"=寻找→跳转, audio Attack/Release/Knee; uk "Play"=Грати
→Відтворити, "Clear"=ЯСНО→Очистити, "Open"=ВІДЧИНЕНО→Відкрити, peers
"Однолітки"→"Учасники"), translate remaining English leftovers (Room ID,
Custom) and normalise terminology. All 15 locales pass test-locales.cjs.

Note: popup.html and website/template.html also carry the language-selector
additions here; their Ko-Fi→Support label text is part of the branding change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:26:20 +02:00
KoalaDev 8fd866f20a feat(website): add canonicals, hreflangs, meta descriptions, and localized schema descriptions 2026-06-19 12:10:08 +02:00
KoalaDev 8f72c238a2 docs: add v2.4.2 entry to CHANGELOG.md 2026-06-19 11:58:06 +02:00
GitHub Action 24ad7c3732 chore(release): update versions to v2.4.2 [skip ci] 2026-06-19 09:54:42 +00:00
61 changed files with 1524 additions and 372 deletions
+46
View File
@@ -0,0 +1,46 @@
name: CI
# Runs the full verification suite (lint, unit/integration tests, production
# audits, extension + website build) on every push to main and every PR, so a
# regression can never reach main or a release tag unchecked.
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
# Cancel superseded runs on the same ref to save CI minutes. Unlike the release
# workflow, an interrupted CI run has no side effects.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
cache-dependency-path: |
package-lock.json
server/package-lock.json
- name: Install root dependencies
run: npm ci
# The server test suite (test-server-ws/routes/ops) imports express,
# socket.io, and dotenv from server/node_modules, so install them too.
- name: Install server dependencies
run: npm ci
working-directory: server
- name: Run verification suite
run: npm run verify
+1 -1
View File
@@ -132,7 +132,7 @@ jobs:
- name: Build Extensions
run: |
npm install
npm ci
npm run build:extension
- name: Generate artifact attestation for extensions
+4 -4
View File
@@ -6,7 +6,7 @@
<p align="center">
<a href="https://github.com/Shik3i/KoalaSync/actions/workflows/release.yml"><img src="https://github.com/Shik3i/KoalaSync/actions/workflows/release.yml/badge.svg" alt="Release Status"></a>
<a href="https://github.com/Shik3i/KoalaSync/releases"><img src="https://img.shields.io/badge/Release-v2.4.1-blue?logo=github" alt="GitHub release"></a>
<a href="https://github.com/Shik3i/KoalaSync/releases"><img src="https://img.shields.io/badge/Release-v2.4.4-blue?logo=github" alt="GitHub release"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue" alt="License"></a>
<a href="https://addons.mozilla.org/de/firefox/addon/koalasync/"><img src="https://img.shields.io/badge/Firefox-Download-orange?logo=firefoxbrowser&logoColor=white" alt="Firefox Add-on"></a>
<a href="https://chromewebstore.google.com/detail/koalasync/obbnmkmlaaddodakcbdljknjpagklifc"><img src="https://img.shields.io/badge/Chrome-Download-blue?logo=googlechrome&logoColor=white" alt="Chrome Extension"></a>
@@ -14,7 +14,7 @@
<p align="center"><i>KoalaSync is a lightweight Browser Extension and Relay Server for synchronized video playback on almost any website with a video element—YouTube, Twitch, Netflix, Emby, Jellyfin, and beyond. Built with a focus on <b>Data Sovereignty</b> and <b>Performance</b>.</i></p>
<p align="center"><a href="docs/CHANGELOG.md"><b>New v2.4.1 Release!</b> — See what's changed</a></p>
<p align="center"><a href="docs/CHANGELOG.md"><b>New v2.4.4 Release!</b> — See what's changed</a></p>
### 🌟 Why KoalaSync?
@@ -59,7 +59,7 @@ The easiest and safest way to install KoalaSync is directly through the official
### 🌐 Localization & Translations
Both the official KoalaSync website and the **v2.0 Browser Extension** feature full dynamic localization:
- **Available Languages**: Support is included for 13 languages: English (`en`), German (`de`), French (`fr`), Spanish (`es`), Portuguese (Brazil) (`pt-BR`), Russian (`ru`), Italian (`it`), Polish (`pl`), Turkish (`tr`), Dutch (`nl`), Japanese (`ja`), Korean (`ko`), and European Portuguese (`pt`).
- **Available Languages**: Support is included for 15 languages: English (`en`), German (`de`), French (`fr`), Spanish (`es`), Portuguese (Brazil) (`pt-BR`), Russian (`ru`), Italian (`it`), Polish (`pl`), Turkish (`tr`), Dutch (`nl`), Japanese (`ja`), Korean (`ko`), Chinese (Simplified) (`zh`), Ukrainian (`uk`), and European Portuguese (`pt`).
- **Real-Time Extension Localization**: Inside the extension Settings panel, users can swap languages instantly. The entire interface, notifications, Empty States, and onboarding guides re-translate dynamically in real-time.
- **Contributing**: We welcome community translations for both the website and the extension! Please refer directly to the [TRANSLATION.md](docs/TRANSLATION.md) guide for step-by-step instructions on how to audit, refine, or add new languages.
@@ -138,7 +138,7 @@ gh attestation verify dist/koalasync-chrome.zip \
---
<div align="center">
<sub><a href="https://ko-fi.com/koaladev"><img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi"></a></sub>
<sub><a href="https://support.koalastuff.net"><img src="https://img.shields.io/badge/Support-KoalaSync-FF5E5B" alt="Support KoalaSync"></a></sub>
<sub><a href="https://gitgem.org/github/Shik3i/KoalaSync"><img src="https://gitgem.org/api/badge/github/Shik3i/KoalaSync.svg" alt="GitGem Badge" /></a></sub>
<sub>Built with ❤️ by <a href="https://github.com/Shik3i">Shik3i</a>. KoalaSync is Open Source under the <a href="LICENSE">MIT License</a>.</sub>
+36 -1
View File
@@ -4,6 +4,41 @@ All notable changes to the KoalaSync browser extension and relay server.
---
## [v2.4.5] — 2026-06-23
### Fixed
- **Room and settings are no longer stored in `chrome.storage.sync`** — Room ID, password, and username were being resurrected from synced storage on a fresh install (sync survives an uninstall in the user's Google account), which made the extension silently auto-connect to a dead room and appear permanently connected. `getSettings()` and all settings reads are now local-only, and legacy keys are actively purged from sync on install/update/startup. Only `onboardingComplete` and `dismissedHints` remain in sync.
- **No server traffic while alone in a room** — When you are the only peer, heartbeats, force-sync, and episode auto-sync are now fully suppressed (previously the keepAlive heartbeat, force-sync, and episode lobby were still broadcast to an empty room). The solo state is re-evaluated live on every event — never cached — so the instant another peer joins, syncing resumes immediately, including an instant state push so the newcomer sees your current position without waiting for the next heartbeat.
## [v2.4.4] — 2026-06-23
### Changed
- **Server: Event rate limit raised 30 → 50 per 10s**, and all connection/event/health rate-limit thresholds and windows extracted into named constants.
- **Extension: Reconnect backoff tuned and jittered** — capped at ~8 attempts/60s (under the per-IP connection limit) with ±20% jitter to de-synchronize reconnect herds after a server blip.
- **CI: Added a verification workflow** running lint, tests, audits, and builds on every push/PR; the release build now uses `npm ci`.
### Fixed
- **Extension: Offline event-queue flush is now paced** (small batches instead of one synchronous burst) so a reconnect after a long outage no longer trips the server event limit and gets disconnected on rejoin.
- **Extension: Ping liveness tolerates one missed PONG** — a reconnect is forced only after 2 consecutive misses (~20s) instead of a single 5s timeout, avoiding spurious drops under transient load.
- **Extension: `socket.send()` failures are caught and re-queued** instead of losing the event on a disconnect race.
## [v2.4.3] — 2026-06-19
### Added
- **Two new languages: Ukrainian (`uk`) and Chinese (`zh`, Simplified)** — added across the extension (UI strings + Chrome `_locales`) and the website (localized pages, hreflang/Open Graph/schema tags, language selector), bringing the total to 15 languages.
### Changed
- **Play/pause sync coalescing** — The content script now collapses rapid bursts of native play/pause events (source swaps, ABR/quality switches, ad transitions, page teardown) into a single relayed command: the first event is sent instantly and a short 150ms window absorbs the rest. This cuts redundant relay traffic and stops bursts from tripping the server's per-socket event rate limit.
### Fixed
- **zh/uk translation quality** — Corrected systematic machine-translation word-sense errors in the two new locales (e.g. "Play", "Status", "Leave Room", "Clear", "Open", "peers", and audio compressor terms) and translated the remaining English leftovers.
- **Relay logging** — An `EVENT_ACK` aimed at a peer that already left is now logged quietly instead of as a `[SECURITY]` cross-room event, so genuine cross-room attempts stand out in the logs.
## [v2.4.2] — 2026-06-19
### Changed
- **Extension: Optimized uninstall URL registration** — Extracted registration into a reusable, race-condition-protected `initUninstallURL()` helper. It registers the uninstall feedback URL with browser context on both extension installation/update and browser startup to prevent state loss, without storing or sending an installation token.
## [v2.4.1] — 2026-06-19
### Added
@@ -144,7 +179,7 @@ All notable changes to the KoalaSync browser extension and relay server.
- **Feature Hint System**: Generic `dismissedHints` array in sync storage for announcing new features. First hint highlights the Audio Options entry in Settings. Extensible for future features.
### Changed
- **Ko-Fi Support Links**: Static footer badges on the Settings and Status tabs linking to the developer's support page. README and website footer updated with Ko-Fi badge.
- **Support Links**: Static footer badges on the Settings and Status tabs linking to the developer's support page. README and website footer updated with a Support KoalaSync badge.
### Fixed
- **Portuguese (PT) locale**: Removed Italian contamination — "sincronizzazione" → "sincronização", "tempo reale" → "tempo real", "Link di Invito" → "Link de Convite", "Sair della Sala" → "Sair da Sala".
+60 -44
View File
@@ -1,78 +1,91 @@
# KoalaSync Translation & Localization Guide
Welcome to the **KoalaSync** translation guide! We rely on the open-source community to make KoalaSync accessible to users worldwide.
Welcome to the **KoalaSync** translation guide. We rely on the open-source community to make KoalaSync accessible to users worldwide.
KoalaSync is split into two independent translation areas. You can translate either one, or both:
1. **The Browser Extension** (`extension/locales/`): The core product that users interact with daily.
2. **The Website** (`website/locales/`): The landing page and invitation bridge.
---
## 📊 Supported Languages Dashboard
## Supported Languages Dashboard
We divide supported languages into two tiers: **Core Languages** (fully hand-crafted and audited by native speakers) and **Extended Languages** (auto-generated using translation models to expand initial coverage).
> [!TIP]
> **Help Us Improve!**
> We welcome community contributions to audit "Auto-Generated" translations and elevate them to "Verified" status.
> We welcome community contributions to audit `Auto-Generated` translations and elevate them to `100% Manually Verified` status.
| Language Code | Language Name | Verification Status | Rationale / Context |
| :--- | :--- | :--- | :--- |
| `en` | 🇬🇧 **English** | `100% Manually Verified` | Global default language (Verified by developer) |
| `de` | 🇩🇪 **German** | `100% Manually Verified` | Developer's native language |
| `fr` | 🇫🇷 **French** | `Auto-Generated` | Needs manual native review and polishing |
| `es` | 🇪🇸 **Spanish** | `100% Manually Verified` | Manual native review by Alenia Studios |
| `pt-BR` | 🇧🇷 **Portuguese (Brasil)** | `100% Manually Verified` | Manual native review by Alenia Studios |
| `ru` | 🇷🇺 **Russian** | `Auto-Generated` | Needs manual native review and polishing |
| `it` | 🇮🇹 **Italian** | `100% Manually Verified` | Manual native review by Alenia Studios |
| `pl` | 🇵🇱 **Polish** | `Auto-Generated` | Needs manual native review and polishing |
| `tr` | 🇹🇷 **Turkish** | `Auto-Generated` | Needs manual native review and polishing |
| `nl` | 🇳🇱 **Dutch** | `Auto-Generated` | Needs manual native review and polishing |
| `ja` | 🇯🇵 **Japanese** | `Auto-Generated` | Needs manual native review and polishing |
| `ko` | 🇰🇷 **Korean** | `Auto-Generated` | Needs manual native review and polishing |
| `pt` | 🇵🇹 **European Portuguese** | `100% Manually Verified` | Manual native review by Alenia Studios |
| `en` | **English** | `100% Manually Verified` | Global default language (verified by developer) |
| `de` | **German** | `100% Manually Verified` | Developer's native language |
| `fr` | **French** | `Auto-Generated` | Needs manual native review and polishing |
| `es` | **Spanish** | `100% Manually Verified` | Manual native review by Alenia Studios |
| `pt-BR` | **Portuguese (Brazil)** | `100% Manually Verified` | Manual native review by Alenia Studios |
| `ru` | **Russian** | `Auto-Generated` | Needs manual native review and polishing |
| `it` | **Italian** | `100% Manually Verified` | Manual native review by Alenia Studios |
| `pl` | **Polish** | `Auto-Generated` | Needs manual native review and polishing |
| `tr` | **Turkish** | `Auto-Generated` | Needs manual native review and polishing |
| `nl` | **Dutch** | `Auto-Generated` | Needs manual native review and polishing |
| `ja` | **Japanese** | `Auto-Generated` | Needs manual native review and polishing |
| `ko` | **Korean** | `Auto-Generated` | Needs manual native review and polishing |
| `pt` | **European Portuguese** | `100% Manually Verified` | Manual native review by Alenia Studios |
| `zh` | **Chinese (Simplified)** | `Auto-Generated` | Needs manual native review and polishing |
| `uk` | **Ukrainian** | `Auto-Generated` | Needs manual native review and polishing |
> [!WARNING]
> **Autogeneration Quality Rule**
> Any newly contributed languages must be marked as `"Auto-Generated"` in this table until fully reviewed and signed off by a native speaker in a pull request.
> Any newly contributed languages must be marked as `Auto-Generated` in this table until fully reviewed and signed off by a native speaker in a pull request.
---
## 🛠️ How to Translate KoalaSync
## How to Translate KoalaSync
Here is the exact step-by-step process for contributing translations.
### Step 1: Fork and Clone the Repository
If you are an external contributor, start with the standard Open Source workflow:
If you are an external contributor, start with the standard open-source workflow:
1. Click the "Fork" button on GitHub to create your own copy of the repository.
2. Clone your fork locally: `git clone https://github.com/YOUR-USERNAME/KoalaSync.git`
3. Create a branch: `git checkout -b translation/my-language`
### Step 2: Translate the Extension
The browser extension handles real-time syncing, settings, and popups.
1. Navigate to [`extension/locales/`](file:///Users/koala/Documents/Workspaces/KoalaSync/extension/locales/).
2. Edit an existing `[lang].json` or copy `en.json` to create a new one (e.g., `it.json`).
3. Translate all the string values. **Do not change the JSON keys.**
1. Navigate to `extension/locales/`.
2. Edit an existing `[lang].json` or copy `en.json` to create a new one (for example, `it.json`).
3. Translate all string values. **Do not change the JSON keys.**
### Step 3: Translate the Website
The website hosts the landing page and invitation bridge.
1. Navigate to [`website/locales/`](file:///Users/koala/Documents/Workspaces/KoalaSync/website/locales/).
1. Navigate to `website/locales/`.
2. Edit an existing `[lang].json` or copy `en.json` to create a new one.
3. Translate all the string values. **Do not change the JSON keys.**
4. If creating a **brand new language**, configure the metadata at the top of your JSON file:
```json
{
"LANG_CODE": "it",
"HTML_CLASS": "lang-it",
"CANONICAL_PATH": "it/",
"LANG_TOGGLE_URL": "../",
"LANG_TOGGLE_TEXT": "EN"
}
```
5. If creating a **brand new language**, register it in `website/build.cjs` by adding it to the `languages` array.
3. Translate all string values. **Do not change the JSON keys.**
4. If creating a brand new language, configure the metadata at the top of your JSON file:
```json
{
"LANG_CODE": "it",
"HTML_CLASS": "lang-it",
"CANONICAL_PATH": "it/",
"LANG_TOGGLE_URL": "../",
"LANG_TOGGLE_TEXT": "EN"
}
```
5. If creating a brand new language, register it in `website/build.cjs` by adding it to the `languages` array.
### Step 4: Verify Locally
Ensure your JSON files are valid and all keys match the English baseline. Open your terminal in the KoalaSync root folder and run:
```bash
# Tests the extension locales for missing keys or syntax errors
node scripts/test-locales.cjs
@@ -83,23 +96,26 @@ node scripts/test-website-locales.mjs
# Builds the website with your new translations
node website/build.cjs
```
*Note: If you receive any errors about missing keys or `TODO` placeholders, please fix them before submitting.*
If you receive any errors about missing keys or placeholder strings, fix them before submitting.
### Step 5: Commit and Pull Request
1. Open this `TRANSLATION.md` file and add/update your language in the **Supported Languages Dashboard** above. Mark it as `100% Manually Verified` if you are a native speaker.
1. Open this `TRANSLATION.md` file and add or update your language in the **Supported Languages Dashboard** above. Mark it as `100% Manually Verified` only if it has been reviewed by a native speaker.
2. Commit your changes: `git commit -m "Update Italian translations"`
3. Push to your fork: `git push origin translation/my-language`
4. Open a **Pull Request** on the main KoalaSync repository on GitHub.
4. Open a pull request on the main KoalaSync repository on GitHub.
---
## ⚖️ Strict Legal Exclusion Rule
## Strict Legal Exclusion Rule
Our legal pages have strict constraints to protect user privacy and avoid regulatory liabilities.
> [!IMPORTANT]
> **DO NOT TRANSLATE LEGAL DOCUMENTS**
> The legal notice ([impressum.html](file:///Users/koala/Documents/Workspaces/KoalaSync/website/impressum.html)) and privacy policy ([datenschutz.html](file:///Users/koala/Documents/Workspaces/KoalaSync/website/datenschutz.html)) **MUST remain exclusively in English and German**.
>
> * **Rationale:** Legal compliance under the European Union General Data Protection Regulation (GDPR) and the German Digital Services Act (DDG). Offering automated translations of legally binding notices introduces compliance risks due to potential mistranslations of liability limits.
> * **Technical Fallback:** Our system automatically falls back to **English** for legal pages if a user visits them in an unsupported language, so you do not need to worry about this.
> **Do Not Translate Legal Documents**
> The legal notice (`website/impressum.html`) and privacy policy (`website/datenschutz.html`) **MUST remain exclusively in English and German**.
>
> **Rationale:** Legal compliance under the European Union General Data Protection Regulation (GDPR) and the German Digital Services Act (DDG). Offering automated translations of legally binding notices introduces compliance risks due to potential mistranslations of liability limits.
>
> **Technical fallback:** Our system automatically falls back to English for legal pages if a user visits them in an unsupported language, so you do not need to translate them.
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "KoalaSync"
},
"appDesc": {
"message": "Синхронізуйте відтворення відео на YouTube, Netflix, Emby, Jellyfin і будь-якому сайті HTML5 у режимі реального часу з друзями."
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "KoalaSync"
},
"appDesc": {
"message": "与朋友实时同步YouTube、Netflix、Emby、Jellyfin和任何HTML5网站上的视频播放。"
}
}
+4 -8
View File
@@ -126,17 +126,13 @@ function setCustomParam(param, value) {
}
async function init() {
let audioData = (await chrome.storage.local.get(['audioSettings'])).audioSettings;
const syncData = await chrome.storage.sync.get(['audioSettings', 'locale']);
if (!audioData && syncData.audioSettings) {
audioData = syncData.audioSettings;
await chrome.storage.local.set({ audioSettings: audioData });
}
const lang = syncData.locale || getSystemLanguage();
// Local-only: audioSettings/locale are never read from storage.sync.
const { audioSettings, locale } = await chrome.storage.local.get(['audioSettings', 'locale']);
const lang = locale || getSystemLanguage();
await loadLocale(lang);
translateDOM();
currentSettings = mergeAudioSettings(audioData);
currentSettings = mergeAudioSettings(audioSettings);
render();
}
+194 -153
View File
@@ -2,6 +2,7 @@ import { EVENTS, PROTOCOL_VERSION, OFFICIAL_SERVER_URL, OFFICIAL_SERVER_TOKEN, E
import { generateUsername } from './shared/names.js';
import { loadLocale, getMessage, getSystemLanguage } from './i18n.js';
import { sameEpisode } from './episode-utils.js';
import { initTabManager } from './modules/tab-manager.js';
// --- Uninstall URL Initialization ---
let uninstallURLInitPromise = null;
@@ -19,24 +20,8 @@ async function initUninstallURL() {
if (UNINSTALL_URL && UNINSTALL_URL.trim() !== '') {
try {
if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) {
console.warn("Storage API not available, skipping uninstall URL token generation");
return;
}
const data = await chrome.storage.local.get("koalaByeToken");
let token = data?.koalaByeToken;
if (!token) {
token = (typeof self.crypto !== 'undefined' && typeof self.crypto.randomUUID === 'function')
? self.crypto.randomUUID()
: Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
await chrome.storage.local.set({ koalaByeToken: token });
}
const url = new URL(UNINSTALL_URL);
url.searchParams.set("browser", BROWSER_TYPE);
url.searchParams.set("t", token);
const runtimeAPI = typeof browser !== 'undefined' ? browser.runtime : chrome.runtime;
if (runtimeAPI && runtimeAPI.setUninstallURL) {
@@ -58,11 +43,13 @@ async function initUninstallURL() {
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install' || details.reason === 'update') {
initUninstallURL();
purgeLegacySyncKeys();
}
});
chrome.runtime.onStartup.addListener(() => {
initUninstallURL();
purgeLegacySyncKeys();
});
// --- State Management ---
@@ -78,6 +65,7 @@ let storageInitialized = false;
let pendingLogs = [];
let pendingHistory = [];
let eventQueue = [];
let flushTimer = null; // paces draining of eventQueue after (re)connect
let isNamespaceJoined = false;
let lastActionState = { action: null, senderId: null, timestamp: 0, acks: [] };
let localSeq = 0; // Monotonically increasing command sequence for this peer
@@ -90,6 +78,7 @@ let pingInterval = null;
let pingTimeout = null;
let pendingPingT = null;
let currentPingMs = null;
let missedPongs = 0;
// --- Keep-Alive Port Listener ---
chrome.runtime.onConnect.addListener((port) => {
@@ -213,8 +202,28 @@ let roomIdleSince = null;
let lastContentHeartbeatAt = null;
let connectIntent = false;
const MAX_RECONNECT_ATTEMPTS = 20;
const _RECONNECT_BASE_DELAY = 500;
const _RECONNECT_MAX_DELAY = 5000;
// Backoff tuned so that at most ~8 connection attempts land in any 60s window,
// keeping a single client comfortably under the server's per-IP connection
// budget (10/min) even before jitter. Cumulative (no jitter): 1, 2.8, 6, 11.9,
// 22.4, 34.4, 46.4, 58.4s → 8th attempt at ~58s.
const _RECONNECT_BASE_DELAY = 1000;
const _RECONNECT_MAX_DELAY = 12000;
const _RECONNECT_FACTOR = 1.8;
const _RECONNECT_GIVEUP_MS = 300000; // switch to slow mode after 5 min of fast retries
const _RECONNECT_SLOW_DELAY = 300000; // slow-mode interval: every 5 min
const _RECONNECT_JITTER = 0.2; // ±20% randomization to de-synchronize reconnect herds
// Paced queue flush: after a (re)connect we drain the offline event backlog in
// small batches instead of one synchronous burst, so we stay well under the
// server's per-socket event budget (50 / 10s) and leave headroom for the
// heartbeats/pings/commands that also count toward it. 10 per 3s ≈ 33/10s.
const FLUSH_BATCH_SIZE = 10;
const FLUSH_BATCH_INTERVAL_MS = 3000;
// Ping liveness: a single unanswered ping is tolerated (transient network
// blip); only MAX_MISSED_PONGS consecutive misses force a reconnect. With a
// 15s interval and 5s timeout that means ~20s to detect a genuinely dead link.
const PING_INTERVAL_MS = 15000;
const PING_TIMEOUT_MS = 5000;
const MAX_MISSED_PONGS = 2;
const ROOM_IDLE_AUTO_LEAVE_MS = 2 * 60 * 60 * 1000;
// Force Sync Coordination
@@ -279,29 +288,14 @@ async function getPeerId() {
}
async function getSettings() {
// Try local (per-device) first, fall back to sync for migration
let data = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username']);
let migrated = false;
if (!data.username) {
const syncData = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username']);
if (syncData.username || syncData.roomId) {
data = syncData;
migrated = true;
}
}
// Local-only by design. Room credentials (roomId/password) and identity
// (username) must NEVER come from storage.sync — syncing them across devices
// both leaks them and resurrects dead rooms on reinstall (a fresh install
// has empty local storage but sync survives in the user's Google account).
const data = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username']);
let username = data.username;
if (!username) {
username = generateUsername();
}
if (migrated) {
await chrome.storage.local.set({
serverUrl: data.serverUrl || '',
useCustomServer: data.useCustomServer || false,
roomId: data.roomId || '',
password: data.password || '',
username
});
} else if (!data.username) {
await chrome.storage.local.set({ username });
}
return {
@@ -313,6 +307,19 @@ async function getSettings() {
};
}
// Privacy + correctness: only onboardingComplete and dismissedHints belong in
// storage.sync. Everything else is per-device local storage. This actively
// removes legacy keys that older versions wrote to sync (and that would
// otherwise be redistributed across devices and resurrected on reinstall).
const LEGACY_SYNC_KEYS = [
'serverUrl', 'useCustomServer', 'roomId', 'password', 'username',
'filterNoise', 'autoSyncNextEpisode', 'forceSyncMode',
'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings'
];
function purgeLegacySyncKeys() {
chrome.storage.sync.remove(LEGACY_SYNC_KEYS).catch(() => {});
}
function addLog(message, type = 'info') {
const log = {
timestamp: new Date().toISOString(),
@@ -365,6 +372,7 @@ function forceDisconnect() {
roomIdleSince = null;
lastContentHeartbeatAt = null;
forceSyncAcks.clear();
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
eventQueue = [];
chrome.storage.session.set({
isForceSyncInitiator: false,
@@ -562,12 +570,7 @@ async function connect() {
protocolVersion: PROTOCOL_VERSION
});
}
while (eventQueue.length > 0) {
const queuedMsg = eventQueue.shift();
emit(queuedMsg.event, queuedMsg.data);
}
eventQueue = [];
chrome.storage.session.set({ eventQueue: [] });
flushEventQueue();
} else if (msg.startsWith('42')) {
try {
const payload = JSON.parse(msg.substring(2));
@@ -586,6 +589,7 @@ async function connect() {
isConnecting = false;
isNamespaceJoined = false;
stopPing();
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
if (!connectIntent && !currentRoom) {
isForceSyncInitiator = false;
@@ -704,17 +708,22 @@ function scheduleReconnect() {
const elapsed = Date.now() - reconnectStartTime;
reconnectAttempts++;
if (!reconnectFailed && (elapsed > 300000 || reconnectAttempts > MAX_RECONNECT_ATTEMPTS)) {
if (!reconnectFailed && (elapsed > _RECONNECT_GIVEUP_MS || reconnectAttempts > MAX_RECONNECT_ATTEMPTS)) {
reconnectFailed = true;
addLog('Switching to slow reconnect mode (every 5 minutes)', 'warn');
}
const delay = reconnectFailed
? 300000
: Math.min(_RECONNECT_BASE_DELAY * Math.pow(1.5, reconnectAttempts - 1), _RECONNECT_MAX_DELAY);
const baseDelay = reconnectFailed
? _RECONNECT_SLOW_DELAY
: Math.min(_RECONNECT_BASE_DELAY * Math.pow(_RECONNECT_FACTOR, reconnectAttempts - 1), _RECONNECT_MAX_DELAY);
// Jitter de-synchronizes herds: many clients dropped by the same server
// blip won't all reconnect on the same tick and exhaust the connection
// budget in lockstep. Applied in both fast and slow mode.
const jitterFactor = 1 - _RECONNECT_JITTER + Math.random() * 2 * _RECONNECT_JITTER;
const delay = Math.round(baseDelay * jitterFactor);
if (reconnectFailed) {
addLog(`Slow reconnect in 5min (attempt ${reconnectAttempts})`, 'info');
addLog(`Slow reconnect in ~5min (attempt ${reconnectAttempts})`, 'info');
} else {
addLog(`Reconnect in ${Math.round(delay)}ms (attempt ${reconnectAttempts})`, 'warn');
}
@@ -732,17 +741,58 @@ function scheduleReconnect() {
function emit(event, data) {
if (socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined) {
const msg = `42${JSON.stringify([event, data])}`;
socket.send(msg);
} else {
eventQueue.push({ event, data });
if (eventQueue.length > 50) {
eventQueue.shift();
addLog('Event queue cap reached, dropping oldest event', 'warn');
try {
socket.send(msg);
} catch (e) {
// The socket can close between the readyState check and send()
// (race with a server-side disconnect). Re-queue so the event is
// retried on the next successful (re)connect instead of being lost.
addLog(`Send failed, re-queueing ${event}: ${e.message}`, 'warn');
queueEvent(event, data);
}
chrome.storage.session.set({ eventQueue });
} else {
queueEvent(event, data);
}
}
function queueEvent(event, data) {
eventQueue.push({ event, data });
if (eventQueue.length > 50) {
eventQueue.shift();
addLog('Event queue cap reached, dropping oldest event', 'warn');
}
chrome.storage.session.set({ eventQueue });
}
/**
* Drain the offline event queue in paced batches. A reconnect after a long
* outage can leave up to 50 queued events; dumping them in one tick would
* exceed the server's per-socket event budget and get us disconnected right
* after rejoining. We send FLUSH_BATCH_SIZE events, then wait
* FLUSH_BATCH_INTERVAL_MS before the next batch. Remaining events drain across
* subsequent batches; if the connection drops mid-drain, the rest stay queued.
*/
function flushEventQueue() {
if (flushTimer) return; // a drain is already in progress
const drainBatch = () => {
flushTimer = null;
if (!socket || socket.readyState !== WebSocket.OPEN || !isNamespaceJoined) {
return; // lost the connection — leave the rest queued for next connect
}
let sent = 0;
while (eventQueue.length > 0 && sent < FLUSH_BATCH_SIZE) {
const queuedMsg = eventQueue.shift();
emit(queuedMsg.event, queuedMsg.data);
sent++;
}
chrome.storage.session.set({ eventQueue }).catch(() => {});
if (eventQueue.length > 0) {
flushTimer = setTimeout(drainBatch, FLUSH_BATCH_INTERVAL_MS);
}
};
drainBatch();
}
function addToHistory(action, senderId) {
const historyEntry = {
action,
@@ -766,16 +816,23 @@ function sendPing() {
emit(EVENTS.PING, { t });
if (pingTimeout) clearTimeout(pingTimeout);
pingTimeout = setTimeout(() => {
if (pendingPingT === t) {
addLog('Ping timeout reached, force disconnecting to trigger reconnect', 'warn');
pendingPingT = null;
pingTimeout = null;
if (pendingPingT !== t) return; // a PONG arrived in time
// This ping went unanswered. Tolerate transient blips: only force a
// reconnect after MAX_MISSED_PONGS consecutive misses, not the first.
pendingPingT = null;
missedPongs++;
if (missedPongs >= MAX_MISSED_PONGS) {
addLog(`${missedPongs} consecutive pings unanswered — force disconnecting to trigger reconnect`, 'warn');
missedPongs = 0;
forceDisconnect();
if (currentRoom || connectIntent) {
scheduleReconnect();
}
} else {
addLog(`Ping unanswered (${missedPongs}/${MAX_MISSED_PONGS}) — retrying next interval`, 'warn');
}
pingTimeout = null;
}, 5000);
}, PING_TIMEOUT_MS);
}
function startPing() {
@@ -783,7 +840,8 @@ function startPing() {
if (pingTimeout) { clearTimeout(pingTimeout); pingTimeout = null; }
currentPingMs = null;
pendingPingT = null;
pingInterval = setInterval(sendPing, 15000);
missedPongs = 0;
pingInterval = setInterval(sendPing, PING_INTERVAL_MS);
sendPing();
}
@@ -798,6 +856,7 @@ function stopPing() {
}
currentPingMs = null;
pendingPingT = null;
missedPongs = 0;
}
// --- Event Handlers ---
@@ -1022,13 +1081,21 @@ function handleServerEvent(event, data) {
if (!Array.isArray(currentRoom.peers)) currentRoom.peers = [];
if (data.status === 'joined') {
if (!currentRoom.peers.find(p => (p.peerId || p) === data.peerId)) {
const wasSolo = currentRoom.peers.filter(p => (p.peerId || p) !== peerId).length === 0;
delete lastSeqBySender[data.peerId];
_persistLastSeq();
currentRoom.peers.push(createPeerData(data));
if (storageInitialized) chrome.storage.session.set({ currentRoom });
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
// We were alone and now we're not — proactively push our
// current playback state so the newcomer syncs immediately
// instead of waiting up to a full heartbeat interval.
if (wasSolo && currentTabId) {
chrome.tabs.sendMessage(currentTabId, { type: 'REQUEST_HEARTBEAT' }).catch(() => {});
}
if (episodeLobby && episodeLobby.initiatorPeerId === peerId) {
emit(EVENTS.EPISODE_LOBBY, { peerId, expectedTitle: episodeLobby.expectedTitle });
}
@@ -1138,6 +1205,7 @@ function handleServerEvent(event, data) {
if (data && typeof data.t === 'number' && Number.isFinite(data.t)) {
if (pendingPingT === data.t) {
pendingPingT = null;
missedPongs = 0;
if (pingTimeout) {
clearTimeout(pingTimeout);
pingTimeout = null;
@@ -1390,14 +1458,18 @@ chrome.alarms.onAlarm.addListener(async (alarm) => {
await leaveRoomAfterIdleGrace('Left room after 2 hours without a selected video heartbeat.');
return;
}
// Heartbeat Logic: Always include identity metadata
const settings = await getSettings();
emit(EVENTS.PEER_STATUS, {
peerId,
status: 'heartbeat',
username: settings.username,
tabTitle: currentTabTitle
});
// Heartbeat — only broadcast when someone else is in the room.
// Recomputed live so a freshly joined peer is picked up immediately.
const otherCount = currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.filter(p => (typeof p === 'object' ? p.peerId : p) !== peerId).length : 0;
if (otherCount > 0) {
const settings = await getSettings();
emit(EVENTS.PEER_STATUS, {
peerId,
status: 'heartbeat',
username: settings.username,
tabTitle: currentTabTitle
});
}
}
}
});
@@ -1434,14 +1506,8 @@ function resetAudioProcessingInTab(tabId) {
async function applyAudioSettingsToTab(tabId) {
if (!tabId) return;
let data = (await chrome.storage.local.get(['audioSettings']));
if (!data.audioSettings) {
const syncData = await chrome.storage.sync.get(['audioSettings']);
if (syncData.audioSettings) {
data = syncData;
await chrome.storage.local.set({ audioSettings: syncData.audioSettings });
}
}
// Local-only: audioSettings are never read from storage.sync.
const data = await chrome.storage.local.get(['audioSettings']);
chrome.tabs.sendMessage(tabId, {
action: 'APPLY_AUDIO_SETTINGS',
settings: data.audioSettings
@@ -1652,6 +1718,19 @@ async function handleAsyncMessage(message, sender, sendResponse) {
});
} else if (message.type === 'CONTENT_EVENT') {
const processEvent = () => {
// Live solo check — recomputed from the current peer list on every
// event (the list is updated synchronously on PEER_STATUS join/leave),
// never cached, so the instant a peer joins we resume sending.
const otherCount = currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.filter(p => (typeof p === 'object' ? p.peerId : p) !== peerId).length : 0;
const hasOtherPeers = otherCount > 0;
// Force Sync only makes sense with other peers. Solo it is a no-op:
// skip the pause/seek + ACK-wait entirely (no freeze, no server traffic).
if (message.action === EVENTS.FORCE_SYNC_PREPARE && !hasOtherPeers) {
sendResponse({ status: 'ok_solo' });
return;
}
const timestamp = Date.now();
localSeq++;
chrome.storage.session.set({ localSeq });
@@ -1695,11 +1774,8 @@ async function handleAsyncMessage(message, sender, sendResponse) {
}, FORCE_SYNC_TIMEOUT);
}
addToHistory(message.action, 'You');
const isNonEssentialEvent = message.action === EVENTS.PLAY || message.action === EVENTS.PAUSE || message.action === EVENTS.SEEK;
const otherCount = currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.filter(p => (typeof p === 'object' ? p.peerId : p) !== peerId).length : 0;
const hasOtherPeers = otherCount > 0;
if (isNonEssentialEvent && !hasOtherPeers) {
sendResponse({ status: 'ok_solo' });
return;
@@ -1751,11 +1827,16 @@ async function handleAsyncMessage(message, sender, sendResponse) {
sendResponse({ status: 'ok' });
} else if (message.type === 'CMD_ACK') {
const commandSenderId = message.commandSenderId;
if (commandSenderId && commandSenderId !== peerId) {
emit(EVENTS.EVENT_ACK, {
senderId: peerId,
// Only ACK if the command sender is still a known peer in our room.
// If we've already seen their PEER_STATUS 'left', skip the ACK — it would
// only be dropped server-side as an absent-peer ACK anyway.
const senderStillPresent = currentRoom && Array.isArray(currentRoom.peers) &&
currentRoom.peers.some(p => (typeof p === 'object' ? p.peerId : p) === commandSenderId);
if (commandSenderId && commandSenderId !== peerId && senderStillPresent) {
emit(EVENTS.EVENT_ACK, {
senderId: peerId,
targetId: commandSenderId,
actionTimestamp: message.actionTimestamp
actionTimestamp: message.actionTimestamp
});
}
sendResponse({ status: 'ok' });
@@ -1854,6 +1935,16 @@ async function handleAsyncMessage(message, sender, sendResponse) {
return;
}
// Variant A: alone in the room → no one to wait for. Skip the lobby
// entirely so the next episode just plays through (no pause, no traffic).
// Live peer check, so the moment someone joins the next transition syncs.
const otherCount = currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.filter(p => (typeof p === 'object' ? p.peerId : p) !== peerId).length : 0;
if (otherCount === 0) {
addLog(`Episode change ("${newTitle}") — alone in room, playing through without a lobby.`, 'info');
sendResponse({ status: 'solo_no_lobby' });
return;
}
// If lobby already exists for this title, just mark self ready
if (episodeLobby && sameEpisode(episodeLobby.expectedTitle, newTitle)) {
if (!episodeLobby.readyPeers.includes(peerId)) {
@@ -1933,72 +2024,22 @@ async function handleAsyncMessage(message, sender, sendResponse) {
}
}
chrome.storage.onChanged.addListener(async (changes, area) => {
if (area !== 'local' || !changes.audioSettings) return;
await ensureState();
if (!currentTabId) return;
chrome.tabs.sendMessage(currentTabId, {
action: 'APPLY_AUDIO_SETTINGS',
settings: changes.audioSettings.newValue
}).catch(() => {});
});
// Tab removal listener
chrome.tabs.onRemoved.addListener(async (tabId) => {
await ensureState();
if (tabId === currentTabId) {
const wasInRoom = !!currentRoom;
currentTabId = null;
currentTabTitle = null;
lastContentHeartbeatAt = null;
roomIdleSince = Date.now();
chrome.storage.session.set({ currentTabId: null, currentTabTitle: null, roomIdleSince, lastContentHeartbeatAt });
updateBadgeStatus();
addLog('Target tab closed.', 'warn');
if (wasInRoom) {
const roomAtClose = currentRoom;
getSettings().then(settings => {
if (currentRoom !== roomAtClose) return;
emit(EVENTS.PEER_STATUS, {
peerId,
playbackState: 'paused',
currentTime: null,
mediaTitle: null,
username: settings.username,
tabTitle: null
});
if (currentRoom && Array.isArray(currentRoom.peers)) {
const me = currentRoom.peers.find(p => (p.peerId || p) === peerId);
if (me && typeof me === 'object') {
me.playbackState = 'paused';
me.currentTime = null;
me.mediaTitle = null;
me.tabTitle = null;
me.lastHeartbeat = Date.now();
if (storageInitialized) chrome.storage.session.set({ currentRoom });
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
}
}
}).catch(() => {});
}
}
});
// Re-inject on full page refresh
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, _tab) => {
await ensureState();
if (currentTabId && tabId === parseInt(currentTabId) && changeInfo.status === 'complete') {
chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
})
.then(() => applyAudioSettingsToTab(tabId))
.catch(() => {});
}
initTabManager({
getCurrentTabId: () => currentTabId,
setCurrentTabId: (val) => { currentTabId = val; },
setCurrentTabTitle: (val) => { currentTabTitle = val; },
setLastContentHeartbeatAt: (val) => { lastContentHeartbeatAt = val; },
setRoomIdleSince: (val) => { roomIdleSince = val; },
getCurrentRoom: () => currentRoom,
getPeerId: () => peerId,
getStorageInitialized: () => storageInitialized,
updateBadgeStatus,
addLog,
getSettings,
emit,
applyAudioSettingsToTab,
ensureState,
EVENTS
});
// Initial Connect — only if user has an active room configuration
+102 -34
View File
@@ -57,6 +57,29 @@
let seekDebounceTimer = null; // debounce timer for rapid seek events
let expectedSeekTime = null; // strictly track programmatic seeks
// --- Play/Pause Coalescing (leading + trailing) ---
// Media players (HLS/DASH, ad insertion, ABR/quality switches, source swaps,
// page teardown) fire bursts of native play/pause events within a few hundred
// ms. Relaying each as a distinct command spams peers and the relay.
//
// Strategy: emit the FIRST event immediately (leading edge → a deliberate
// single play/pause has zero added latency), then hold a short window. If
// more play/pause events arrive during the window it's a burst — we suppress
// the intermediate churn and, once it settles, emit the FINAL state on the
// trailing edge.
//
// We deliberately do NOT dedup the trailing send against the leading one. A
// remote play/pause may be applied mid-window (e.g. I pause, peer plays, I
// pause again within 150ms): the settled state then equals my last *sent*
// state yet is a genuine change versus the now-shared state — suppressing it
// would desync. Re-sending an unchanged state is a harmless no-op on peers,
// so re-sending is always the safe choice. Echo-suppression and seek-flush
// still run synchronously on event arrival (see reportEvent) — only the
// network emit is governed here.
const PLAY_PAUSE_COALESCE_MS = 150;
let playPauseCoalesceTimer = null; // non-null = a coalescing window is open
let pendingPlayPauseAction = null; // last play/pause seen during the window, awaiting trailing flush
// --- Episode Auto-Sync State ---
let lastKnownMediaTitle = null;
let episodeTransitionDebounce = null;
@@ -66,25 +89,8 @@
let _audioSettings = null;
let _audioProcessingAllowed = true;
// Cache the autoSyncNextEpisode setting
// Cache the autoSyncNextEpisode setting (local-only; never read from sync)
chrome.storage.local.get(['autoSyncNextEpisode', 'audioSettings'], (data) => {
if (data.autoSyncNextEpisode === undefined || data.audioSettings === undefined) {
chrome.storage.sync.get(['autoSyncNextEpisode', 'audioSettings'], (syncData) => {
const migrate = {};
if (data.autoSyncNextEpisode === undefined && syncData.autoSyncNextEpisode !== undefined) {
migrate.autoSyncNextEpisode = syncData.autoSyncNextEpisode;
}
if (data.audioSettings === undefined && syncData.audioSettings !== undefined) {
migrate.audioSettings = syncData.audioSettings;
}
if (Object.keys(migrate).length) chrome.storage.local.set(migrate);
_autoSyncEnabled = syncData.autoSyncNextEpisode !== false;
_audioSettings = mergeAudioSettings(syncData.audioSettings);
const v = findVideo();
if (v && _audioProcessingAllowed) applyAudioSettings(v, _audioSettings);
});
return;
}
_autoSyncEnabled = data.autoSyncNextEpisode !== false;
_audioSettings = mergeAudioSettings(data.audioSettings);
const video = findVideo();
@@ -532,6 +538,14 @@
return true;
}
// Background asks for an immediate state push (e.g. the first peer just
// joined while we were solo) so the newcomer syncs without waiting.
if (message.type === 'REQUEST_HEARTBEAT') {
sendHeartbeat();
sendResponse({ ok: true });
return true;
}
if (message.type === 'SERVER_COMMAND') {
const { action, payload } = message;
let actionCompleted = false;
@@ -768,6 +782,46 @@
});
// Detect native events
// Build the relay payload from the *current* media state and send it to
// background.js. Re-reads the video each call so deferred (coalesced) emits
// carry an up-to-date position. Safe with no/invalid video — it no-ops.
function sendContentEvent(action) {
const video = findVideo();
if (!video) return;
const current = video.currentTime;
if (!Number.isFinite(current)) return;
const mediaTitle = (navigator.mediaSession && navigator.mediaSession.metadata) ? navigator.mediaSession.metadata.title : null;
chrome.runtime.sendMessage({
type: 'CONTENT_EVENT',
action,
payload: {
currentTime: current,
targetTime: current,
mediaTitle: mediaTitle,
timestamp: Date.now()
}
}).catch(() => {});
// Trigger proactive heartbeat to push stabilized state
scheduleProactiveHeartbeat();
}
// Trailing-edge flush of a coalesced play/pause burst: emit the final settled
// state. No-ops only when no burst followed the leading edge. We do NOT skip
// when the state matches the leading send — a remote command may have changed
// the shared state mid-window, so re-sending is the safe (idempotent) choice.
function flushPlayPause() {
playPauseCoalesceTimer = null;
const finalAction = pendingPlayPauseAction;
pendingPlayPauseAction = null;
// No burst follow-up (only the leading edge fired), or invalid state.
if (finalAction !== EVENTS.PLAY && finalAction !== EVENTS.PAUSE) return;
sendContentEvent(finalAction);
}
function reportEvent(action) {
if (seekDebounceTimer && (action === EVENTS.PLAY || action === EVENTS.PAUSE)) {
clearTimeout(seekDebounceTimer);
@@ -786,10 +840,11 @@
const current = video.currentTime;
if (!Number.isFinite(current)) return;
const mediaTitle = (navigator.mediaSession && navigator.mediaSession.metadata) ? navigator.mediaSession.metadata.title : null;
const eventState = action === EVENTS.PLAY ? 'playing' : (action === EVENTS.PAUSE ? 'paused' : (action === EVENTS.SEEK ? 'seek' : null));
// Echo-suppression for remotely-applied commands. MUST stay synchronous
// on event arrival: the suppress timer is short-lived (300ms) and would
// expire if deferred, leaking an echo back to peers.
if (_suppressTimers[eventState]) {
_clearSuppress(eventState);
return;
@@ -798,20 +853,29 @@
// Suppress only SEEK during visibility grace period (tab re-focus ghost jump).
// Play/Pause pass through — user may want to immediately pause after tabbing back.
if (Date.now() < visibilityGraceUntil && action === EVENTS.SEEK) return;
chrome.runtime.sendMessage({
type: 'CONTENT_EVENT',
action,
payload: {
currentTime: current,
targetTime: current,
mediaTitle: mediaTitle,
timestamp: Date.now()
}
}).catch(() => {});
// Trigger proactive heartbeat to push stabilized state
scheduleProactiveHeartbeat();
// Coalesce play/pause bursts (source swaps, ABR, ads, teardown). The
// synchronous gates above have already run; only the network emit is
// governed here. Leading edge sends the first event instantly; further
// events within the window are collapsed to the final state by the
// trailing flush. A pending burst is REPLACED, never dropped — the last
// event in the window always wins.
if (action === EVENTS.PLAY || action === EVENTS.PAUSE) {
if (playPauseCoalesceTimer === null) {
// Window closed → fresh action. Emit now (zero added latency).
sendContentEvent(action);
pendingPlayPauseAction = null;
} else {
// Window open → part of a burst. Defer to the trailing flush.
pendingPlayPauseAction = action;
clearTimeout(playPauseCoalesceTimer);
}
playPauseCoalesceTimer = setTimeout(flushPlayPause, PLAY_PAUSE_COALESCE_MS);
return;
}
// SEEK (and any non play/pause action): emit immediately.
sendContentEvent(action);
}
// --- Tab Visibility Handling ---
@@ -843,6 +907,10 @@
if (lobbyPollTimer) { clearInterval(lobbyPollTimer); lobbyPollTimer = null; }
if (heartbeatTimeout) { clearTimeout(heartbeatTimeout); heartbeatTimeout = null; }
if (proactiveHeartbeatTimeout) { clearTimeout(proactiveHeartbeatTimeout); proactiveHeartbeatTimeout = null; }
// Drop any pending coalesced play/pause: we are tearing down and will
// disconnect — peers learn we are gone via PEER_STATUS 'left'. This also
// intentionally suppresses the teardown play/pause burst at the source.
if (playPauseCoalesceTimer) { clearTimeout(playPauseCoalesceTimer); playPauseCoalesceTimer = null; pendingPlayPauseAction = null; }
observer.disconnect();
});
window.addEventListener('pageshow', (event) => {
+1 -1
View File
@@ -1,5 +1,5 @@
// extension/i18n.js
export const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru', 'it', 'pl', 'tr', 'nl', 'ja', 'ko', 'pt'];
export const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es', 'it', 'nl', 'pl', 'pt', 'pt-BR', 'tr', 'ru', 'ja', 'ko', 'zh', 'uk'];
export const DEFAULT_LANGUAGE = 'en';
let activeDictionary = {};
+1 -1
View File
@@ -181,7 +181,7 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Zeitüberschreitung — nicht alle Teilnehmer haben die Episode geladen",
"LOBBY_CANCEL_USER": "Vom Benutzer abgebrochen",
"NOTIF_ERROR_TITLE": "KoalaSync-Fehler",
"FOOTER_SUPPORT": "☕ Kauf mir einen Kaffee",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ Bewerten",
"FOOTER_SUPPORT_PROMPT": "Gefällt dir KoalaSync? Hinterlasse eine Bewertung!",
"LABEL_AUDIO_PROCESSING": "Audio-Verarbeitung",
+1 -1
View File
@@ -181,7 +181,7 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Timeout — not all peers loaded the episode",
"LOBBY_CANCEL_USER": "Cancelled by user",
"NOTIF_ERROR_TITLE": "KoalaSync Error",
"FOOTER_SUPPORT": "☕ Buy me a coffee",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ Rate us",
"FOOTER_SUPPORT_PROMPT": "Enjoying KoalaSync? Leave a review!",
"LABEL_AUDIO_PROCESSING": "Audio Processing",
+1 -1
View File
@@ -181,7 +181,7 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Tiempo de espera agotado: no todos los participantes cargaron el episodio",
"LOBBY_CANCEL_USER": "Cancelado por el usuario",
"NOTIF_ERROR_TITLE": "Error de KoalaSync",
"FOOTER_SUPPORT": "☕ Invítame a un café",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ Valorar",
"FOOTER_SUPPORT_PROMPT": "¿Te gusta KoalaSync? ¡Deja una reseña!",
"LABEL_AUDIO_PROCESSING": "Procesamiento de audio",
+1 -1
View File
@@ -181,7 +181,7 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Délai dépassé — tous les membres n'ont pas chargé l'épisode",
"LOBBY_CANCEL_USER": "Annulé par l'utilisateur",
"NOTIF_ERROR_TITLE": "Erreur KoalaSync",
"FOOTER_SUPPORT": "☕ Offre-moi un café",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ Évaluer",
"FOOTER_SUPPORT_PROMPT": "Tu aimes KoalaSync? Laisse un avis!",
"LABEL_AUDIO_PROCESSING": "Traitement audio",
+1 -1
View File
@@ -181,7 +181,7 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Tempo scaduto: caricamento incompleto per alcuni partecipanti",
"LOBBY_CANCEL_USER": "Annullato dall'utente",
"NOTIF_ERROR_TITLE": "Errore KoalaSync",
"FOOTER_SUPPORT": "☕ Offrimi un caffè",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ Valutaci",
"FOOTER_SUPPORT_PROMPT": "Ti piace KoalaSync? Lascia una recensione!",
"LABEL_AUDIO_PROCESSING": "Elaborazione Audio",
+1 -1
View File
@@ -181,7 +181,7 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "タイムアウト — 一部のメンバーがエピソードを読み込めませんでした",
"LOBBY_CANCEL_USER": "ユーザーによってキャンセルされました",
"NOTIF_ERROR_TITLE": "KoalaSyncエラー",
"FOOTER_SUPPORT": "☕ コーヒーをおごってね",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ 評価する",
"FOOTER_SUPPORT_PROMPT": "KoalaSyncはいかが?レビューを書いてください!",
"LABEL_AUDIO_PROCESSING": "オーディオ処理",
+1 -1
View File
@@ -181,7 +181,7 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "시간 초과 — 모든 피어가 에피소드를 로드하지 못했습니다",
"LOBBY_CANCEL_USER": "사용자에 의해 취소됨",
"NOTIF_ERROR_TITLE": "KoalaSync 오류",
"FOOTER_SUPPORT": "☕ 커피 한 잔 사주세요",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ 평가하기",
"FOOTER_SUPPORT_PROMPT": "KoalaSync가 마음에 드세요? 리뷰를 남겨주세요!",
"LABEL_AUDIO_PROCESSING": "오디오 처리",
+1 -1
View File
@@ -181,7 +181,7 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Time-out — niet alle deelnemers hebben de aflevering geladen",
"LOBBY_CANCEL_USER": "Geannuleerd door gebruiker",
"NOTIF_ERROR_TITLE": "KoalaSync-fout",
"FOOTER_SUPPORT": "☕ Trakteer op een koffie",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ Beoordelen",
"FOOTER_SUPPORT_PROMPT": "Vind je KoalaSync leuk? Laat een review achter!",
"LABEL_AUDIO_PROCESSING": "Audioverwerking",
+1 -1
View File
@@ -181,7 +181,7 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Limit czasu — nie wszyscy uczestnicy załadowali odcinek",
"LOBBY_CANCEL_USER": "Anulowane przez użytkownika",
"NOTIF_ERROR_TITLE": "Błąd KoalaSync",
"FOOTER_SUPPORT": "☕ Postaw kawę",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ Oceń",
"FOOTER_SUPPORT_PROMPT": "Podoba Ci się KoalaSync? Zostaw recenzję!",
"LABEL_AUDIO_PROCESSING": "Przetwarzanie dźwięku",
+1 -1
View File
@@ -181,7 +181,7 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Tempo limite — nem todos carregaram o episódio",
"LOBBY_CANCEL_USER": "Cancelado pelo usuário",
"NOTIF_ERROR_TITLE": "Erro no KoalaSync",
"FOOTER_SUPPORT": "☕ Me pague um café",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ Avaliar",
"FOOTER_SUPPORT_PROMPT": "Gostou do KoalaSync? Deixe uma avaliação!",
"LABEL_AUDIO_PROCESSING": "Processamento de áudio",
+1 -1
View File
@@ -181,7 +181,7 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Tempo limite — alguns participantes não carregaram o episódio",
"LOBBY_CANCEL_USER": "Cancelado pelo utilizador",
"NOTIF_ERROR_TITLE": "Erro no KoalaSync",
"FOOTER_SUPPORT": "☕ Oferece-me um café",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ Avaliar",
"FOOTER_SUPPORT_PROMPT": "Gostas do KoalaSync? Deixa uma avaliação!",
"LABEL_AUDIO_PROCESSING": "Processamento de Áudio",
+1 -1
View File
@@ -181,7 +181,7 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Время ожидания истекло — не все участники загрузили серию",
"LOBBY_CANCEL_USER": "Отменено пользователем",
"NOTIF_ERROR_TITLE": "Ошибка KoalaSync",
"FOOTER_SUPPORT": "☕ Угости кофе",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ Оценить",
"FOOTER_SUPPORT_PROMPT": "Нравится KoalaSync? Оставьте отзыв!",
"LABEL_AUDIO_PROCESSING": "Обработка звука",
+1 -1
View File
@@ -181,7 +181,7 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Zaman aşımı — tüm bağlantılar bölümü yüklemedi",
"LOBBY_CANCEL_USER": "Kullanıcı tarafından iptal edildi",
"NOTIF_ERROR_TITLE": "KoalaSync Hatası",
"FOOTER_SUPPORT": "☕ Bana kahve ısmarla",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ Değerlendir",
"FOOTER_SUPPORT_PROMPT": "KoalaSync'i beğendin mi? Bir yorum bırak!",
"LABEL_AUDIO_PROCESSING": "Ses işleme",
+212
View File
@@ -0,0 +1,212 @@
{
"LANG_CODE": "uk",
"HTML_CLASS": "lang-uk",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Кімната",
"TAB_ROOM_TOOLTIP": "Налаштування кімнати та підключення",
"TAB_SYNC": "Синхронізувати",
"TAB_SYNC_TOOLTIP": "Елементи керування синхронізацією відео та віддалені дії",
"TAB_SETTINGS": "Налаштування",
"TAB_SETTINGS_TOOLTIP": "Параметри розширення",
"TAB_STATUS": "Статус",
"TAB_STATUS_TOOLTIP": "Розширена діагностика та журнали",
"BTN_CREATE_ROOM": "+ Створити нову кімнату",
"MANUAL_CONNECT_HEADER": "Ручне підключення / Додатково",
"LABEL_SERVER": "Сервер",
"BTN_SERVER_OFFICIAL": "Офіційний",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Використовуйте офіційний надійний сервер",
"BTN_SERVER_CUSTOM": "Власний",
"BTN_SERVER_CUSTOM_TOOLTIP": "Підключіться до власного серверу, розміщеного на власному хостингу",
"PLACEHOLDER_SERVER_URL": "wss://your-server:3000",
"LABEL_ROOM_ID": "ID кімнати",
"LABEL_ROOM_ID_TOOLTIP": "Унікальний ідентифікатор вашої кімнати синхронізації",
"PLACEHOLDER_ROOM_ID": "Введіть Room ID",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "Унікальний ідентифікатор кімнати, до якої ви хочете приєднатися",
"LABEL_PASSWORD": "Пароль (необов'язково)",
"LABEL_PASSWORD_TOOLTIP": "Додатковий пароль для обмеження доступу до кімнати",
"PLACEHOLDER_PASSWORD": "Пароль кімнати (необов'язково)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Пароль для кімнати (залиште порожнім, якщо немає)",
"BTN_JOIN_ROOM": "Приєднатися / Створити кімнату",
"BTN_JOIN_ROOM_TOOLTIP": "Підключіться до кімнати",
"LABEL_PUBLIC_ROOMS": "Громадські кімнати",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Список загальнодоступних кімнат на цьому сервері",
"BTN_REFRESH": "ОНОВИТИ",
"BTN_REFRESH_TOOLTIP": "Оновити список громадських кімнат",
"PUBLIC_ROOMS_REFRESHING": "Оновлення...",
"BTN_REFRESH_COOLDOWN": "ЧЕКАЙТЕ {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "Оновлення списку кімнат охолоджується. Повторіть спробу в {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Оновлення громадських кімнат. Наступне оновлення доступне в {seconds}s.",
"LABEL_ACTIVE_ROOM": "Активна кімната",
"LABEL_ACTIVE_ROOM_TOOLTIP": "Кімната, до якої ви зараз підключені",
"ACTIVE_ROOM_NONE": "ЖОДНОГО",
"ACTIVE_SERVER_OFFICIAL": "Офіційний сервер",
"LABEL_INVITE_LINK": "Посилання на запрошення",
"LABEL_INVITE_LINK_TOOLTIP": "Поділіться цим посиланням з друзями, щоб вони могли приєднатися",
"LABEL_PEERS_IN_ROOM": "Учасники в кімнаті",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Інші користувачі зараз підключені до цієї кімнати",
"NO_PEERS_CONNECTED": "Немає підключених однорангових пристроїв",
"BTN_LEAVE_ROOM": "Вийти з кімнати",
"LABEL_SELECT_VIDEO": "Виберіть Відео",
"LABEL_SELECT_VIDEO_TOOLTIP": "Виберіть вкладку браузера, яка містить відео для синхронізації",
"OPTION_SELECT_TAB": "-- Виберіть вкладку --",
"LABEL_REMOTE_CONTROL": "Пульт дистанційного керування",
"BTN_COPY_INVITE": "📋 Посилання на запрошення",
"BTN_COPY_INVITE_TOOLTIP": "Копіювати посилання для запрошення",
"BTN_PLAY": "▶ Відтворити",
"BTN_PLAY_TOOLTIP": "Надішліть усім команду Play",
"BTN_PAUSE": "⏸ Пауза",
"BTN_PAUSE_TOOLTIP": "Надішліть команду паузи всім",
"BTN_SYNC": "⚡ СИНХР",
"BTN_SYNC_TOOLTIP": "Змусити всіх користувачів синхронізуватися",
"OPTION_JUMP_TO_OTHERS": "Перейти до інших",
"OPTION_JUMP_TO_ME": "Перейти до мене",
"OPTION_JUMP_MODE_TOOLTIP": "Виберіть ціль синхронізації",
"LABEL_LAST_ACTIVITY": "Статус останньої дії",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Показує останню команду відтворення, паузи або перемотування",
"NO_RECENT_COMMANDS": "Немає останніх команд",
"LOBBY_HEADER": "ЕПІЗОДНЕ ЛОБІ",
"LOBBY_WAITING_FOR": "🎬 Чекаємо: \"{title}\"",
"LOBBY_WAITING_PEERS": "Очікування учасників...",
"BTN_SKIP_PLAY": "Все одно пропустити та відтворити",
"BTN_SKIP_PLAY_TOOLTIP": "Скасуйте лобі та все одно відтворюйте",
"LOBBY_CONNECT_FIRST": "Спочатку підключіться до кімнати",
"LOBBY_CONNECT_FIRST_DESC": "Вам потрібно приєднатися до кімнати за посиланням із запрошенням або створити нову, щоб синхронізувати відео.",
"BTN_CREATE_ROOM_ALT": "Створити нову кімнату",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Створіть нову випадкову кімнату та приєднайтеся до неї",
"LABEL_USERNAME": "Ваше ім'я користувача",
"LABEL_USERNAME_TOOLTIP": "Ім'я користувача допомагає іншим ідентифікувати вас.",
"PLACEHOLDER_USERNAME": "Анонімна коала",
"LABEL_HIDE_CLUTTER": "Приховати безладні вкладки",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Відфільтровує вкладки, не пов’язані з відео, і непов’язані домени, щоб зберегти список чистим",
"LABEL_AUTO_SYNC_NEXT": "Автоматична синхронізація наступного епізоду",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Автоматично призупиняється та чекає на всіх однорангових пристроїв, коли епізод зміниться, а потім синхронізація починається разом.",
"LABEL_AUTO_COPY_INVITE": "Автоматичне копіювання посилання для запрошення",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Автоматично копіює посилання запрошення в буфер обміну під час створення нової кімнати.",
"LABEL_NOTIFICATIONS": "Сповіщення браузера",
"LABEL_NOTIFICATIONS_TOOLTIP": "Показує рідні системні сповіщення, коли хтось приєднується/виходить або відтворює/призупиняє.",
"LABEL_LANGUAGE": "Мова програми",
"LABEL_LANGUAGE_TOOLTIP": "Виберіть бажану мову розширення",
"LABEL_TROUBLESHOOTING": "Усунення несправностей",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Інструменти для вирішення проблем з підключенням",
"BTN_REGEN_ID": "Регенерувати Peer ID",
"BTN_REGEN_ID_TOOLTIP": "Повторно згенеруйте свій внутрішній ідентифікатор і повторно підключіться",
"REGEN_ID_DESC": "Використовуйте це, якщо ви бачите помилку \"Дублікати ідентифікатора\".",
"REGEN_ID_OTHER_ISSUE": "Інша проблема? Відкрийте випуск GitHub",
"LABEL_CONN_STATUS": "Статус підключення",
"LABEL_CONN_STATUS_TOOLTIP": "Поточний стан підключення WebSocket",
"CONN_STATUS_DISCONNECTED": "Відключено",
"BTN_RETRY": "ПОВТОРИТИ",
"BTN_RETRY_TOOLTIP": "Спробуйте повторно підключитися до сервера",
"BTN_COPY_LOGS": "Копіювати журнали",
"BTN_COPY_LOGS_TOOLTIP": "Скопіюйте журнали в буфер обміну для спільного використання",
"LABEL_VIDEO_DEBUG": "Інформація про налагодження відео",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Технічні відомості про поточний вибраний елемент відео",
"VIDEO_DEBUG_EMPTY": "Вкладка не вибрана або відео не виявлено.",
"LABEL_HISTORY": "Повна історія дій",
"LABEL_HISTORY_TOOLTIP": "Хронологічний журнал усіх команд синхронізації в кімнаті",
"HISTORY_EMPTY": "Активності ще немає",
"LABEL_LOGS": "Журнали (останні 50)",
"LABEL_LOGS_TOOLTIP": "Журнали технічного підключення для налагодження",
"BTN_CLEAR": "Очистити",
"BTN_CLEAR_TOOLTIP": "Очистити журнал",
"LABEL_GITHUB": "Репозиторій GitHub",
"BTN_ONBOARDING_SKIP": "Пропустити",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Пропустити підручник",
"BTN_ONBOARDING_NEXT": "Далі",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Перейти до наступного кроку",
"ONBOARDING_1_TITLE": "Ласкаво просимо до KoalaSync!",
"ONBOARDING_1_TEXT": "Дивіться відео разом у ідеальній синхронізації — де б ви не були. Давайте зробимо короткий тур!",
"ONBOARDING_2_TITLE": "1. Створіть кімнату",
"ONBOARDING_2_TEXT": "Почніть тут. Створіть кімнату та поділіться посиланням запрошення з друзями.",
"ONBOARDING_3_TITLE": "2. Виберіть Відео",
"ONBOARDING_3_TEXT": "Перейдіть сюди, щоб вибрати відео, яке потрібно синхронізувати. Відтворення, пауза та пошук — усі залишаються синхронізованими.",
"ONBOARDING_4_TITLE": "3. Персоналізація",
"ONBOARDING_4_TEXT": "Виберіть цікаве ім’я користувача, щоб ваші друзі знали, хто ви.",
"ONBOARDING_5_TITLE": "Усе готово!",
"ONBOARDING_5_TEXT": "Час взяти попкорн. Приємного перегляду разом!",
"ERR_CONN_TIMEOUT": "Час очікування підключення минув. Спробуйте ще раз.",
"ERR_INVALID_SERVER_URL": "Недійсний формат URL-адреси сервера.",
"ERR_IDENTITY_NOT_LOADED": "Посвідчення особи ще не завантажено. Зачекайте хвилинку та повторіть спробу.",
"ERR_NO_PEERS_TIME": "Немає інших аналогів із відомим часом. Переключіться на «Перейти до мене».",
"ERR_NO_VIDEO_TAB": "Не вдалося підключитися до вкладки відео.",
"ERR_SELECT_VIDEO": "Спочатку виберіть відео!",
"TOAST_INVITE_COPIED": "Посилання на запрошення скопійовано!",
"TOAST_COPY_FAILED": "Не вдалося скопіювати в буфер обміну",
"TOAST_LOBBY_SKIPPED": "Лобі епізоду пропущено.",
"TOAST_LOBBY_SKIP_FAILED": "Не вдалося пропустити лобі.",
"TOAST_LOGS_COPIED": "Скопійовано!",
"TOAST_PEER_JOINED": "{name} приєднався до кімнати",
"TOAST_PEER_LEFT": "{name} вийшов з кімнати",
"TOAST_PEER_ACTION": "{name} {action}",
"STATUS_CONNECTED": "Підключено",
"STATUS_RECONNECTING": "Повторне підключення...",
"STATUS_CONNECTING": "Підключення...",
"STATUS_FAILED": "Не вдалося",
"STATUS_DISCONNECTED": "Відключено",
"BTN_STATE_JOINING": "🚀 Приєднуюсь...",
"BTN_STATE_RECONNECTING": "🔄 Повторне підключення...",
"BTN_STATE_PLAYING": "▶ Грає...",
"BTN_STATE_PAUSING": "⏸ Призупинення...",
"BTN_STATE_SYNCING_GROUP": "Синхронізація з групою ({time})...",
"BTN_STATE_SYNCING": "Синхронізація...",
"BTN_STATE_SYNCED": "✅ Синхронізовано!",
"NOTIF_PLAY": "почав відтворення",
"NOTIF_PAUSE": "призупинено відтворення",
"NOTIF_SEEK": "перемотав відео",
"NOTIF_FORCE_PREPARE": "почав примусову синхронізацію",
"NOTIF_FORCE_EXECUTE": "синхронізував усіх",
"DEBUG_NO_TAB": "Цільова вкладка не вибрана.",
"DEBUG_COMM_FAIL": "Не вдалося зв’язатися з відео вкладки.",
"EMPTY_PEERS_TITLE": "Учасників ще немає",
"EMPTY_PEERS_HINT": "Поділіться своїм запрошенням, щоб почати",
"EMPTY_HISTORY_TITLE": "Активності ще немає",
"EMPTY_HISTORY_HINT": "Відтворення, призупинення або перемотування, щоб побачити історію",
"EMPTY_LOGS_TITLE": "Без журналів",
"EMPTY_LOGS_HINT": "Тут відображатимуться події підключення",
"EMPTY_ROOMS_TITLE": "Немає активних кімнат",
"EMPTY_ROOMS_HINT": "Створіть кімнату або оновіть, щоб знайти загальнодоступні",
"LABEL_YOU": "Ви",
"ONBOARDING_DONE": "Готово!",
"LABEL_LOBBY_PEER_READY": "Готовий",
"LABEL_LOBBY_PEER_LOADING": "Завантаження...",
"LABEL_PASSWORD_PROTECTED": "Захищено паролем",
"LABEL_PEERS_COUNT": "{count} учасників",
"LABEL_CUSTOM_SERVER": "Спеціальний сервер",
"BTN_STATE_CREATING": "🚀 Створення кімнати...",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Помилка синхронізації епізоду",
"NOTIF_LOBBY_CANCEL_MSG": "Автоматичну синхронізацію скасовано: {reason}. Можливо, вам знадобиться виконати синхронізацію вручну.",
"LOBBY_CANCEL_TIMEOUT": "Час очікування",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Тайм-аут (відновлено)",
"LOBBY_CANCEL_PEERS_LEFT": "Усі інші учасники вийшли",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Тайм-аут — не всі вузли завантажили епізод",
"LOBBY_CANCEL_USER": "Скасовано користувачем",
"NOTIF_ERROR_TITLE": "Помилка KoalaSync",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ Оцініть нас",
"FOOTER_SUPPORT_PROMPT": "Подобається KoalaSync? Залиште відгук!",
"LABEL_AUDIO_PROCESSING": "Обробка аудіо",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Застосуйте звукові ефекти, такі як стиснення, до відтворення відео",
"AUDIO_OPEN_SETTINGS": "Відкрити",
"NEW_FEATURE_AUDIO": "Новинка: обробка звуку — спробуйте компресор!",
"AUDIO_BACK": "← Назад",
"AUDIO_PAGE_TITLE": "Параметри звуку",
"AUDIO_MASTER_TOGGLE": "Обробка аудіо",
"AUDIO_COMPRESSOR": "Компресор",
"AUDIO_COMPRESSOR_ENABLE": "Увімкнено",
"AUDIO_PRESET": "Попереднє налаштування",
"AUDIO_PRESET_RECOMMENDED": "Рекомендовано",
"AUDIO_PRESET_DYNAMIC_RANGE": "Динамічний діапазон",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Покращення голосу",
"AUDIO_PRESET_SMOOTH": "Плавний",
"AUDIO_PRESET_CUSTOM": "Власний",
"AUDIO_PARAM_THRESHOLD": "Поріг",
"AUDIO_PARAM_KNEE": "Коліно",
"AUDIO_PARAM_RATIO": "Співвідношення",
"AUDIO_PARAM_ATTACK": "Атака",
"AUDIO_PARAM_RELEASE": "Спад",
"AUDIO_EQUALIZER": "Еквалайзер",
"AUDIO_COMING_SOON": "Скоро буде",
"BTN_RESTART_TOUR": "Перезапустіть підручник",
"BTN_RESTART_TOUR_TOOLTIP": "Перезапустіть навчальний посібник із адаптації",
"HINT_SELECT_VIDEO": "Виберіть своє відео тут!"
}
+212
View File
@@ -0,0 +1,212 @@
{
"LANG_CODE": "zh",
"HTML_CLASS": "lang-zh",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "房间",
"TAB_ROOM_TOOLTIP": "房间设置和连接",
"TAB_SYNC": "同步",
"TAB_SYNC_TOOLTIP": "视频同步控制和远程操作",
"TAB_SETTINGS": "设置",
"TAB_SETTINGS_TOOLTIP": "扩展首选项",
"TAB_STATUS": "状态",
"TAB_STATUS_TOOLTIP": "高级诊断和日志",
"BTN_CREATE_ROOM": "+ 创建新房间",
"MANUAL_CONNECT_HEADER": "手动连接/高级",
"LABEL_SERVER": "服务器",
"BTN_SERVER_OFFICIAL": "官方的",
"BTN_SERVER_OFFICIAL_TOOLTIP": "使用官方可靠服务器",
"BTN_SERVER_CUSTOM": "自定义",
"BTN_SERVER_CUSTOM_TOOLTIP": "连接到您自己的自托管服务器",
"PLACEHOLDER_SERVER_URL": "wss://your-server:3000",
"LABEL_ROOM_ID": "房间 ID",
"LABEL_ROOM_ID_TOOLTIP": "您的同步室的唯一标识符",
"PLACEHOLDER_ROOM_ID": "输入Room ID",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "您要加入的房间的唯一ID",
"LABEL_PASSWORD": "密码(可选)",
"LABEL_PASSWORD_TOOLTIP": "可选密码以限制房间访问",
"PLACEHOLDER_PASSWORD": "房间密码(可选)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "房间密码(无则留空)",
"BTN_JOIN_ROOM": "加入/创建房间",
"BTN_JOIN_ROOM_TOOLTIP": "连接到房间",
"LABEL_PUBLIC_ROOMS": "公共房间",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "该服务器上公开可用的房间列表",
"BTN_REFRESH": "刷新",
"BTN_REFRESH_TOOLTIP": "刷新公共房间列表",
"PUBLIC_ROOMS_REFRESHING": "刷新中...",
"BTN_REFRESH_COOLDOWN": "等等{seconds}",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "房间列表刷新正在冷却。在 {seconds} 中重试。",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "公共房间列表刷新冷却中。下次刷新将在 {seconds} 后可用。",
"LABEL_ACTIVE_ROOM": "活动室",
"LABEL_ACTIVE_ROOM_TOOLTIP": "您当前连接的房间",
"ACTIVE_ROOM_NONE": "没有任何",
"ACTIVE_SERVER_OFFICIAL": "官方服务器",
"LABEL_INVITE_LINK": "邀请链接",
"LABEL_INVITE_LINK_TOOLTIP": "与朋友分享此链接,以便他们可以加入",
"LABEL_PEERS_IN_ROOM": "房间里的同伴",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "当前连接到该房间的其他用户",
"NO_PEERS_CONNECTED": "没有对等点连接",
"BTN_LEAVE_ROOM": "离开房间",
"LABEL_SELECT_VIDEO": "选择视频",
"LABEL_SELECT_VIDEO_TOOLTIP": "选择包含要同步的视频的浏览器选项卡",
"OPTION_SELECT_TAB": "-- 选择一个选项卡 --",
"LABEL_REMOTE_CONTROL": "遥控",
"BTN_COPY_INVITE": "📋 邀请链接",
"BTN_COPY_INVITE_TOOLTIP": "复制邀请链接",
"BTN_PLAY": "▶ 播放",
"BTN_PLAY_TOOLTIP": "向所有人发送播放命令",
"BTN_PAUSE": "⏸ 暂停",
"BTN_PAUSE_TOOLTIP": "向所有人发送暂停命令",
"BTN_SYNC": "⚡ 同步",
"BTN_SYNC_TOOLTIP": "强制所有用户同步",
"OPTION_JUMP_TO_OTHERS": "跳转到其他",
"OPTION_JUMP_TO_ME": "跳到我这里",
"OPTION_JUMP_MODE_TOOLTIP": "选择同步目标",
"LABEL_LAST_ACTIVITY": "上次活动状态",
"LABEL_LAST_ACTIVITY_TOOLTIP": "显示最近的播放、暂停或跳转命令",
"NO_RECENT_COMMANDS": "没有最近的命令",
"LOBBY_HEADER": "剧集大厅",
"LOBBY_WAITING_FOR": "🎬 等待:“{title}”",
"LOBBY_WAITING_PEERS": "等待同行...",
"BTN_SKIP_PLAY": "仍然跳过并播放",
"BTN_SKIP_PLAY_TOOLTIP": "取消大厅并继续播放",
"LOBBY_CONNECT_FIRST": "首先连接到房间",
"LOBBY_CONNECT_FIRST_DESC": "您需要通过邀请链接加入房间或创建一个新房间来同步视频。",
"BTN_CREATE_ROOM_ALT": "创建新房间",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "创建一个新的随机房间并加入它",
"LABEL_USERNAME": "您的用户名",
"LABEL_USERNAME_TOOLTIP": "用户名可以帮助其他人识别您。",
"PLACEHOLDER_USERNAME": "无名考拉",
"LABEL_HIDE_CLUTTER": "隐藏杂乱标签",
"LABEL_HIDE_CLUTTER_TOOLTIP": "过滤掉非视频选项卡和不相关的域以保持列表干净",
"LABEL_AUTO_SYNC_NEXT": "自动同步下一集",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "当情节发生变化时,自动暂停并等待所有对等点,然后一起开始同步。",
"LABEL_AUTO_COPY_INVITE": "自动复制邀请链接",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "创建新房间时,自动将邀请链接复制到剪贴板。",
"LABEL_NOTIFICATIONS": "浏览器通知",
"LABEL_NOTIFICATIONS_TOOLTIP": "当有人加入/离开或播放/暂停时显示本机系统通知。",
"LABEL_LANGUAGE": "应用程序语言",
"LABEL_LANGUAGE_TOOLTIP": "选择您喜欢的扩展语言",
"LABEL_TROUBLESHOOTING": "故障排除",
"LABEL_TROUBLESHOOTING_TOOLTIP": "用于修复连接问题的工具",
"BTN_REGEN_ID": "再生Peer ID",
"BTN_REGEN_ID_TOOLTIP": "重新生成您的内部 ID 并重新连接",
"REGEN_ID_DESC": "如果您看到“重复身份”错误,请使用此选项。",
"REGEN_ID_OTHER_ISSUE": "其他问题?打开 GitHub 问题",
"LABEL_CONN_STATUS": "连接状态",
"LABEL_CONN_STATUS_TOOLTIP": "当前WebSocket连接状态",
"CONN_STATUS_DISCONNECTED": "已断开连接",
"BTN_RETRY": "重试",
"BTN_RETRY_TOOLTIP": "尝试重新连接服务器",
"BTN_COPY_LOGS": "复制日志",
"BTN_COPY_LOGS_TOOLTIP": "将日志复制到剪贴板以便共享",
"LABEL_VIDEO_DEBUG": "视频调试信息",
"LABEL_VIDEO_DEBUG_TOOLTIP": "有关当前所选视频元素的技术详细信息",
"VIDEO_DEBUG_EMPTY": "未选择任何选项卡或检测到视频。",
"LABEL_HISTORY": "完整的行动历史",
"LABEL_HISTORY_TOOLTIP": "房间内所有同步命令的时间日志",
"HISTORY_EMPTY": "还没有活动",
"LABEL_LOGS": "日志(最后 50 条)",
"LABEL_LOGS_TOOLTIP": "用于调试的技术连接日志",
"BTN_CLEAR": "清除",
"BTN_CLEAR_TOOLTIP": "清除日志输出",
"LABEL_GITHUB": "GitHub 存储库",
"BTN_ONBOARDING_SKIP": "跳过",
"BTN_ONBOARDING_SKIP_TOOLTIP": "跳过教程",
"BTN_ONBOARDING_NEXT": "下一个",
"BTN_ONBOARDING_NEXT_TOOLTIP": "进入下一步",
"ONBOARDING_1_TITLE": "欢迎来到KoalaSync",
"ONBOARDING_1_TEXT": "无论您身在何处,都能完美同步地一起观看视频。让我们快速浏览一下吧!",
"ONBOARDING_2_TITLE": "1. 创建房间",
"ONBOARDING_2_TEXT": "从这里开始。创建一个房间并与您的朋友分享邀请链接。",
"ONBOARDING_3_TITLE": "2. 选择视频",
"ONBOARDING_3_TEXT": "导航此处选择您要同步的视频。播放、暂停和搜索——每个人都保持同步。",
"ONBOARDING_4_TITLE": "3. 个性化",
"ONBOARDING_4_TEXT": "选择一个有趣的用户名,以便您的朋友知道您是谁。",
"ONBOARDING_5_TITLE": "你都准备好了!",
"ONBOARDING_5_TEXT": "是时候去买点爆米花了。一起观赏吧!",
"ERR_CONN_TIMEOUT": "连接超时。请再试一次。",
"ERR_INVALID_SERVER_URL": "服务器 URL 格式无效。",
"ERR_IDENTITY_NOT_LOADED": "身份尚未加载。稍等片刻,然后重试。",
"ERR_NO_PEERS_TIME": "没有其他已知时间的同行。切换到“跳到我这里”。",
"ERR_NO_VIDEO_TAB": "无法连接到视频选项卡。",
"ERR_SELECT_VIDEO": "请先选择视频!",
"TOAST_INVITE_COPIED": "邀请链接已复制!",
"TOAST_COPY_FAILED": "无法复制到剪贴板",
"TOAST_LOBBY_SKIPPED": "剧集大厅已跳过。",
"TOAST_LOBBY_SKIP_FAILED": "无法跳过大厅。",
"TOAST_LOGS_COPIED": "复制了!",
"TOAST_PEER_JOINED": "{name} 加入房间",
"TOAST_PEER_LEFT": "{name} 离开房间",
"TOAST_PEER_ACTION": "{name} {action}",
"STATUS_CONNECTED": "已连接",
"STATUS_RECONNECTING": "正在重新连接...",
"STATUS_CONNECTING": "正在连接...",
"STATUS_FAILED": "失败的",
"STATUS_DISCONNECTED": "已断开连接",
"BTN_STATE_JOINING": "🚀 正在加入...",
"BTN_STATE_RECONNECTING": "🔄 正在重新连接...",
"BTN_STATE_PLAYING": "▶ 播放中...",
"BTN_STATE_PAUSING": "⏸ 暂停...",
"BTN_STATE_SYNCING_GROUP": "正在同步到群组 ({time})...",
"BTN_STATE_SYNCING": "正在同步...",
"BTN_STATE_SYNCED": "✅ 已同步!",
"NOTIF_PLAY": "开始播放",
"NOTIF_PAUSE": "暂停播放",
"NOTIF_SEEK": "跳转视频",
"NOTIF_FORCE_PREPARE": "开始强制同步",
"NOTIF_FORCE_EXECUTE": "同步所有人",
"DEBUG_NO_TAB": "未选择目标选项卡。",
"DEBUG_COMM_FAIL": "无法与标签视频通信。",
"EMPTY_PEERS_TITLE": "还没有同行",
"EMPTY_PEERS_HINT": "分享您的邀请链接以开始使用",
"EMPTY_HISTORY_TITLE": "还没有活动",
"EMPTY_HISTORY_HINT": "播放、暂停或跳转以查看历史",
"EMPTY_LOGS_TITLE": "无日志",
"EMPTY_LOGS_HINT": "连接事件将出现在这里",
"EMPTY_ROOMS_TITLE": "没有活动房间",
"EMPTY_ROOMS_HINT": "创建房间或刷新以查找公共房间",
"LABEL_YOU": "你",
"ONBOARDING_DONE": "完毕!",
"LABEL_LOBBY_PEER_READY": "准备好",
"LABEL_LOBBY_PEER_LOADING": "加载中...",
"LABEL_PASSWORD_PROTECTED": "密码保护",
"LABEL_PEERS_COUNT": "{count} 同行",
"LABEL_CUSTOM_SERVER": "定制服务器",
"BTN_STATE_CREATING": "🚀 正在创建房间...",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — 剧集同步失败",
"NOTIF_LOBBY_CANCEL_MSG": "自动同步取消:{reason}。您可能需要手动同步。",
"LOBBY_CANCEL_TIMEOUT": "暂停",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "超时(已恢复)",
"LOBBY_CANCEL_PEERS_LEFT": "所有其他同伴都离开了",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "超时——并非所有同行都加载了该剧集",
"LOBBY_CANCEL_USER": "已被用户取消",
"NOTIF_ERROR_TITLE": "KoalaSync 错误",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ 评价我们",
"FOOTER_SUPPORT_PROMPT": "喜欢 KoalaSync 吗?留下评论!",
"LABEL_AUDIO_PROCESSING": "音频处理",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "将压缩等音频效果应用于视频播放",
"AUDIO_OPEN_SETTINGS": "打开",
"NEW_FEATURE_AUDIO": "新功能:音频处理 - 尝试压缩器!",
"AUDIO_BACK": "← 返回",
"AUDIO_PAGE_TITLE": "音频设置",
"AUDIO_MASTER_TOGGLE": "音频处理",
"AUDIO_COMPRESSOR": "压缩器",
"AUDIO_COMPRESSOR_ENABLE": "启用",
"AUDIO_PRESET": "预设",
"AUDIO_PRESET_RECOMMENDED": "推荐",
"AUDIO_PRESET_DYNAMIC_RANGE": "动态范围",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "声音增强",
"AUDIO_PRESET_SMOOTH": "平滑",
"AUDIO_PRESET_CUSTOM": "自定义",
"AUDIO_PARAM_THRESHOLD": "阈值",
"AUDIO_PARAM_KNEE": "拐点",
"AUDIO_PARAM_RATIO": "比率",
"AUDIO_PARAM_ATTACK": "起音",
"AUDIO_PARAM_RELEASE": "释放",
"AUDIO_EQUALIZER": "均衡器",
"AUDIO_COMING_SOON": "即将推出",
"BTN_RESTART_TOUR": "重启教程",
"BTN_RESTART_TOUR_TOOLTIP": "重新启动入门教程",
"HINT_SELECT_VIDEO": "在这里选择您的视频!"
}
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 3,
"default_locale": "en",
"name": "KoalaSync",
"version": "2.4.1",
"version": "2.4.4",
"description": "Synchronize video playback on YouTube, Netflix, Emby, Jellyfin, and any HTML5 site in real-time with friends.",
"permissions": [
"storage",
+94
View File
@@ -0,0 +1,94 @@
export function initTabManager({
getCurrentTabId,
setCurrentTabId,
setCurrentTabTitle,
setLastContentHeartbeatAt,
setRoomIdleSince,
getCurrentRoom,
getPeerId,
getStorageInitialized,
updateBadgeStatus,
addLog,
getSettings,
emit,
applyAudioSettingsToTab,
ensureState,
EVENTS
}) {
chrome.storage.onChanged.addListener(async (changes, area) => {
if (area !== 'local' || !changes.audioSettings) return;
await ensureState();
const tabId = getCurrentTabId();
if (!tabId) return;
chrome.tabs.sendMessage(tabId, {
action: 'APPLY_AUDIO_SETTINGS',
settings: changes.audioSettings.newValue
}).catch(() => {});
});
chrome.tabs.onRemoved.addListener(async (tabId) => {
await ensureState();
if (tabId === getCurrentTabId()) {
const wasInRoom = !!getCurrentRoom();
setCurrentTabId(null);
setCurrentTabTitle(null);
setLastContentHeartbeatAt(null);
const now = Date.now();
setRoomIdleSince(now);
chrome.storage.session.set({
currentTabId: null,
currentTabTitle: null,
roomIdleSince: now,
lastContentHeartbeatAt: null
});
updateBadgeStatus();
addLog('Target tab closed.', 'warn');
if (wasInRoom) {
const roomAtClose = getCurrentRoom();
getSettings().then(settings => {
if (getCurrentRoom() !== roomAtClose) return;
emit(EVENTS.PEER_STATUS, {
peerId: getPeerId(),
playbackState: 'paused',
currentTime: null,
mediaTitle: null,
username: settings.username,
tabTitle: null
});
const room = getCurrentRoom();
if (room && Array.isArray(room.peers)) {
const me = room.peers.find(p => (p.peerId || p) === getPeerId());
if (me && typeof me === 'object') {
me.playbackState = 'paused';
me.currentTime = null;
me.mediaTitle = null;
me.tabTitle = null;
me.lastHeartbeat = Date.now();
if (getStorageInitialized()) {
chrome.storage.session.set({ currentRoom: room });
}
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: room.peers }).catch(() => {});
}
}
}).catch(() => {});
}
}
});
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, _tab) => {
await ensureState();
const curTabId = getCurrentTabId();
if (curTabId && tabId === parseInt(curTabId) && changeInfo.status === 'complete') {
chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
})
.then(() => applyAudioSettingsToTab(tabId))
.catch(() => {});
}
});
}
+4 -2
View File
@@ -584,6 +584,8 @@
<option value="nl">🇳🇱 Nederlands</option>
<option value="ja">🇯🇵 日本語</option>
<option value="ko">🇰🇷 한국어</option>
<option value="zh">🇨🇳 中文</option>
<option value="uk">🇺🇦 Українська</option>
<option value="pt">🇵🇹 Português (Portugal)</option>
</select>
</div>
@@ -600,7 +602,7 @@
<div style="font-size: 9px; color: var(--text-muted); opacity: 0.6; margin-bottom: 6px; line-height: 1.3;" data-i18n="FOOTER_SUPPORT_PROMPT">Enjoying KoalaSync? Leave a review!</div>
<a id="settingsReviewLink" href="#" target="_blank" data-i18n="FOOTER_REVIEW">★ Rate us</a>
<span> · </span>
<a id="settingsSupportLink" href="#" target="_blank" data-i18n="FOOTER_SUPPORT">☕ Buy me a coffee</a>
<a id="settingsSupportLink" href="#" target="_blank" data-i18n="FOOTER_SUPPORT">Support KoalaSync</a>
<span> · </span>
<span id="settingsVersion">v0.0.0</span>
</div>
@@ -641,7 +643,7 @@
<div style="font-size: 9px; color: var(--text-muted); opacity: 0.6; margin-bottom: 6px; line-height: 1.3;" data-i18n="FOOTER_SUPPORT_PROMPT">Enjoying KoalaSync? Leave a review!</div>
<a id="devReviewLink" href="#" target="_blank" data-i18n="FOOTER_REVIEW">★ Rate us</a>
<span> · </span>
<a id="devSupportLink" href="#" target="_blank" data-i18n="FOOTER_SUPPORT">☕ Buy me a coffee</a>
<a id="devSupportLink" href="#" target="_blank" data-i18n="FOOTER_SUPPORT">Support KoalaSync</a>
<span> · </span>
<span id="appVersion">v0.0.0</span>
</div>
+2 -12
View File
@@ -175,19 +175,9 @@ function setRoomRefreshCooldown() {
// --- Initialization ---
async function init() {
// Local-only by design — settings and room credentials never come from
// storage.sync (only onboardingComplete + dismissedHints live there).
const localData = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username', 'filterNoise', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings', 'activeTab']);
// Migrate preferences from sync → local for existing users
const oldSync = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username', 'filterNoise', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings']);
const toMigrate = {};
for (const key of ['serverUrl', 'useCustomServer', 'roomId', 'password', 'username', 'filterNoise', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings']) {
if (localData[key] === undefined && oldSync[key] !== undefined) {
toMigrate[key] = oldSync[key];
localData[key] = oldSync[key];
}
}
if (Object.keys(toMigrate).length) {
await chrome.storage.local.set(toMigrate);
}
let activeLang = localData.locale;
if (!activeLang) {
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "koalasync",
"version": "2.4.1",
"version": "2.4.4",
"description": "KoalaSync Build Scripts",
"private": true,
"type": "module",
@@ -8,6 +8,7 @@
"build:extension": "node scripts/build-extension.cjs",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "node scripts/verify-release.mjs",
"verify": "node scripts/verify-release.mjs"
},
"devDependencies": {
+2 -1
View File
@@ -128,7 +128,8 @@ const chromeLocalesDir = path.join(__dirname, '..', 'extension', '_locales');
const chromeLocaleMap = {
'en': 'en', 'de': 'de', 'fr': 'fr', 'es': 'es', 'it': 'it',
'ja': 'ja', 'ko': 'ko', 'nl': 'nl', 'pl': 'pl',
'pt-BR': 'pt_BR', 'pt': 'pt_PT', 'ru': 'ru', 'tr': 'tr'
'pt-BR': 'pt_BR', 'pt': 'pt_PT', 'ru': 'ru', 'tr': 'tr',
'zh': 'zh_CN', 'uk': 'uk'
};
// Read SUPPORTED_LANGUAGES from i18n.js
+9 -5
View File
@@ -15,7 +15,9 @@ import {
roomListCooldowns,
rateLimitDenied,
startRateLimitCleanup,
stopRateLimitCleanup
stopRateLimitCleanup,
CONNECTION_RATE_LIMIT,
EVENT_RATE_LIMIT
} from '../server/rate-limiter.js';
// Helper: mock io for cleanup
@@ -31,8 +33,9 @@ function reset() {
// --- checkConnectionRate ---
reset();
assert.equal(checkConnectionRate('1.1.1.1'), true, 'first connection allowed');
for (let i = 0; i < 9; i++) checkConnectionRate('1.1.1.1');
assert.equal(checkConnectionRate('1.1.1.1'), false, '11th connection blocked');
// Exhaust the rest of the budget (first call above counted as 1).
for (let i = 0; i < CONNECTION_RATE_LIMIT - 1; i++) checkConnectionRate('1.1.1.1');
assert.equal(checkConnectionRate('1.1.1.1'), false, `connection beyond ${CONNECTION_RATE_LIMIT}/window blocked`);
assert.equal(rateLimitDenied.connections, 1, 'denial counter incremented');
reset();
@@ -41,8 +44,9 @@ assert.equal(checkConnectionRate('2.2.2.2'), true, 'separate IP independent');
// --- checkEventRate ---
reset();
assert.equal(checkEventRate('sock1'), true, 'first event allowed');
for (let i = 0; i < 29; i++) checkEventRate('sock1');
assert.equal(checkEventRate('sock1'), false, '31st event blocked');
// Exhaust the rest of the budget (first call above counted as 1).
for (let i = 0; i < EVENT_RATE_LIMIT - 1; i++) checkEventRate('sock1');
assert.equal(checkEventRate('sock1'), false, `event beyond ${EVENT_RATE_LIMIT}/window blocked`);
assert.equal(rateLimitDenied.events, 1);
reset();
+9 -3
View File
@@ -150,7 +150,7 @@ const peerJoinLocks = new Map(); // peerId -> Promise (prevents race on same pee
function log(type, message, details = '') {
const debugLogging = process.env.DEBUG_LOGGING === '1';
const isVerbose = type === 'CONN' || type === 'ROOM' || type === 'DEDUPE' || type === 'CORS';
const isVerbose = type === 'CONN' || type === 'ROOM' || type === 'DEDUPE' || type === 'CORS' || type === 'ACKDROP';
if (!debugLogging && isVerbose) return;
const timestamp = new Date().toISOString();
@@ -569,12 +569,18 @@ io.on('connection', (socket) => {
// Security: Only relay ACK if both peers are in the same room
if (senderMapping && targetMapping && senderMapping.roomId === targetMapping.roomId) {
io.to(targetSocketId).emit(EVENTS.EVENT_ACK, {
io.to(targetSocketId).emit(EVENTS.EVENT_ACK, {
senderId: senderMapping.peerId,
actionTimestamp: data.actionTimestamp
});
} else {
} else if (senderMapping && targetMapping) {
// Both peers exist but live in different rooms — genuinely suspicious.
log('SECURITY', `Blocked cross-room ACK attempt from ${socket.id} to ${data.targetId}`);
} else {
// Benign + common: sender or target left/disconnected before the ACK
// arrived (a command was in-flight when they went). Not an attack —
// log quietly (verbose only) so it doesn't drown out real signals.
log('ACKDROP', `Dropped ACK from ${socket.id} to absent peer ${data.targetId}`);
}
});
+18 -10
View File
@@ -7,6 +7,14 @@ export const ROOM_LIST_COOLDOWN_MS = 10000;
export const HEALTH_RATE_LIMIT_PER_MINUTE = 10;
export const ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE = 5;
// --- Connection & event budgets (formerly inline magic numbers) ---
export const CONNECTION_RATE_LIMIT = 10; // max new connections per IP per window
export const CONNECTION_RATE_WINDOW_MS = 60000; // 1 minute
export const EVENT_RATE_LIMIT = 50; // max relayed events per socket per window
export const EVENT_RATE_WINDOW_MS = 10000; // 10 seconds
export const HEALTH_RATE_WINDOW_MS = 60000; // 1 minute
export const ADMIN_METRICS_AUTH_WINDOW_MS = 60000; // 1 minute
export const connectionCounts = new Map(); // ip -> { count, resetTime }
export const failedAuthAttempts = new Map(); // Map<IP+RoomID, {count, lastAttempt}>
export const eventCounts = new Map(); // socketId -> { count, resetTime }
@@ -72,30 +80,30 @@ export function recordAuthFailure(ip, roomId) {
export function checkConnectionRate(ip) {
const now = Date.now();
const entry = connectionCounts.get(ip) || { count: 0, resetTime: now + 60000 };
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 60000; }
const entry = connectionCounts.get(ip) || { count: 0, resetTime: now + CONNECTION_RATE_WINDOW_MS };
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + CONNECTION_RATE_WINDOW_MS; }
entry.count++;
connectionCounts.set(ip, entry);
if (entry.count <= 10) return true;
if (entry.count <= CONNECTION_RATE_LIMIT) return true;
rateLimitDenied.connections++;
return false;
}
export function checkEventRate(socketId) {
const now = Date.now();
const entry = eventCounts.get(socketId) || { count: 0, resetTime: now + 10000 };
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 10000; }
const entry = eventCounts.get(socketId) || { count: 0, resetTime: now + EVENT_RATE_WINDOW_MS };
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + EVENT_RATE_WINDOW_MS; }
entry.count++;
eventCounts.set(socketId, entry);
if (entry.count <= 30) return true;
if (entry.count <= EVENT_RATE_LIMIT) return true;
rateLimitDenied.events++;
return false;
}
export function checkHealthRate(ip) {
const now = Date.now();
const entry = healthCounts.get(ip) || { count: 0, resetTime: now + 60000 };
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 60000; }
const entry = healthCounts.get(ip) || { count: 0, resetTime: now + HEALTH_RATE_WINDOW_MS };
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + HEALTH_RATE_WINDOW_MS; }
entry.count++;
healthCounts.set(ip, entry);
if (entry.count <= HEALTH_RATE_LIMIT_PER_MINUTE) return true;
@@ -105,8 +113,8 @@ export function checkHealthRate(ip) {
export function checkAdminMetricsAuthRate(ip) {
const now = Date.now();
const entry = adminMetricsAuthCounts.get(ip) || { count: 0, resetTime: now + 60000 };
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 60000; }
const entry = adminMetricsAuthCounts.get(ip) || { count: 0, resetTime: now + ADMIN_METRICS_AUTH_WINDOW_MS };
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + ADMIN_METRICS_AUTH_WINDOW_MS; }
entry.count++;
adminMetricsAuthCounts.set(ip, entry);
if (entry.count <= ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE) return true;
+1 -2
View File
@@ -7,13 +7,12 @@
*/
export const PROTOCOL_VERSION = "1.0.0";
export const APP_VERSION = "2.4.1";
export const APP_VERSION = "2.4.4";
export const OFFICIAL_SERVER_URL = 'wss://syncserver.koalastuff.net';
export const OFFICIAL_LANDING_PAGE_URL = 'https://sync.koalastuff.net';
export const OFFICIAL_SERVER_TOKEN = '62170b705234c4f4807a9b22420bb93cf1a2aacfa4c5d3b47804482babb8eb50';
export const SUPPORT_URL = 'https://support.koalastuff.net';
export const KOFI_URL = 'https://ko-fi.com/koaladev';
export const GITHUB_URL = 'https://github.com/Shik3i/KoalaSync';
export function isFirefox() {
+4 -4
View File
@@ -480,11 +480,11 @@ document.addEventListener('DOMContentLoaded', () => {
const activeLang = safeGetLocalStorage('koala_lang') || (navigator.language.startsWith('de') ? 'de' : 'en');
const path = window.location.pathname;
const pathSegments = path.split('/');
const isSubdir = pathSegments.some(seg => ['de', 'fr', 'es', 'pt-BR', 'ru', 'it', 'pl', 'tr', 'nl', 'ja', 'ko', 'pt'].includes(seg));
const isSubdir = pathSegments.some(seg => ['de', 'fr', 'es', 'it', 'nl', 'pl', 'pt', 'pt-BR', 'tr', 'ru', 'ja', 'ko', 'zh', 'uk'].includes(seg));
// Only need to do this dynamic rewrite if we are NOT already inside a localized subdirectory
if (!isSubdir) {
const homeLinks = document.querySelectorAll('a[href="./"], a[href="de/"], a[href="fr/"], a[href="es/"], a[href="pt-BR/"], a[href="ru/"], a[href="it/"], a[href="pl/"], a[href="tr/"], a[href="nl/"], a[href="ja/"], a[href="ko/"], a[href="pt/"]');
const homeLinks = document.querySelectorAll('a[href="./"], a[href="de/"], a[href="fr/"], a[href="es/"], a[href="it/"], a[href="nl/"], a[href="pl/"], a[href="pt/"], a[href="pt-BR/"], a[href="tr/"], a[href="ru/"], a[href="ja/"], a[href="ko/"], a[href="zh/"], a[href="uk/"]');
homeLinks.forEach(link => {
link.href = (activeLang === 'en') ? './' : `${activeLang}/`;
});
@@ -535,7 +535,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (isIndex) {
// Static navigation: Route to correct subdirectory
const pathSegments = path.split('/');
const isSubdir = pathSegments.some(seg => ['de', 'fr', 'es', 'pt-BR', 'ru', 'it', 'pl', 'tr', 'nl', 'ja', 'ko', 'pt'].includes(seg));
const isSubdir = pathSegments.some(seg => ['de', 'fr', 'es', 'it', 'nl', 'pl', 'pt', 'pt-BR', 'tr', 'ru', 'ja', 'ko', 'zh', 'uk'].includes(seg));
let targetPath;
if (newLang === 'en') {
@@ -558,7 +558,7 @@ document.addEventListener('DOMContentLoaded', () => {
} else {
// Dynamic page: Toggle classes and update elements dynamically without navigating away
const html = document.documentElement;
html.classList.remove('lang-en', 'lang-de', 'lang-fr', 'lang-es', 'lang-pt-br', 'lang-ru', 'lang-it', 'lang-pl', 'lang-tr', 'lang-nl', 'lang-ja', 'lang-ko', 'lang-pt');
html.classList.remove('lang-en', 'lang-de', 'lang-fr', 'lang-es', 'lang-it', 'lang-nl', 'lang-pl', 'lang-pt', 'lang-pt-br', 'lang-tr', 'lang-ru', 'lang-ja', 'lang-ko', 'lang-zh', 'lang-uk');
// Fallback dynamic pages to 'en' if 'de' is not chosen (since fr/es markup is not present)
const activeDisplayLang = (newLang === 'de') ? 'de' : 'en';
+1 -1
View File
@@ -162,7 +162,7 @@ async function compile() {
}
const templateContent = fs.readFileSync(templatePath, 'utf8');
const localesDir = path.join(websiteDir, 'locales');
const languages = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru', 'it', 'pl', 'tr', 'nl', 'ja', 'ko', 'pt'];
const languages = ['en', 'de', 'fr', 'es', 'it', 'nl', 'pl', 'pt', 'pt-BR', 'tr', 'ru', 'ja', 'ko', 'zh', 'uk'];
// Read version for build-time injection (SEO: crawlers see real version)
const versionJson = JSON.parse(fs.readFileSync(path.join(websiteDir, 'version.json'), 'utf8'));
+5
View File
@@ -7,6 +7,11 @@
<link rel="preload" href="../style.min.css" as="style">
<link rel="stylesheet" href="../style.min.css">
<link rel="icon" type="image/webp" href="../assets/NewLogoIcon_64.webp">
<link rel="canonical" href="https://sync.koalastuff.net/de/datenschutz">
<link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/privacy">
<link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/datenschutz">
<link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/privacy">
<meta name="description" content="Datenschutzerklärung von KoalaSync - Wir nehmen Datenschutz ernst. Erfahre mehr über unseren Ansatz ohne Tracking, ohne Logs und ohne Cookies.">
<meta name="robots" content="index, follow">
<script type="application/ld+json">
+5
View File
@@ -7,6 +7,11 @@
<link rel="preload" href="../style.min.css" as="style">
<link rel="stylesheet" href="../style.min.css">
<link rel="icon" type="image/webp" href="../assets/NewLogoIcon_64.webp">
<link rel="canonical" href="https://sync.koalastuff.net/de/impressum">
<link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/imprint">
<link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/impressum">
<link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/imprint">
<meta name="description" content="Impressum (Legal Notice) von KoalaSync - Anbieterkennzeichnung und Kontaktinformationen für die Open-Source Video-Synchronisations-Erweiterung.">
<meta name="robots" content="index, follow">
<script type="application/ld+json">
+5
View File
@@ -7,6 +7,11 @@
<link rel="preload" href="style.min.css" as="style">
<link rel="stylesheet" href="style.min.css">
<link rel="icon" type="image/webp" href="assets/NewLogoIcon_64.webp">
<link rel="canonical" href="https://sync.koalastuff.net/imprint">
<link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/imprint">
<link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/impressum">
<link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/imprint">
<meta name="description" content="Legal Notice (Impressum) of KoalaSync - Contact information and operator details for the open-source video synchronization extension.">
<meta name="robots" content="index, follow">
<script type="application/ld+json">
+5 -1
View File
@@ -31,11 +31,15 @@
'tr': 'tr',
'nl': 'nl',
'ja': 'ja',
'ko': 'ko'
'ko': 'ko',
'zh': 'zh',
'uk': 'uk'
};
var getBrowserLang = function() {
var fullLang = (navigator.language || '').toLowerCase();
if (fullLang.indexOf('zh') === 0) return 'zh';
if (fullLang.indexOf('uk') === 0) return 'uk';
if (fullLang.indexOf('pt-br') === 0) return 'pt-br';
if (fullLang.indexOf('pt') === 0) return 'pt';
return fullLang.split('-')[0];
+3 -2
View File
@@ -7,6 +7,7 @@
"OG_LOCALE": "de_DE",
"META_TITLE": "KoalaSync | Netflix, YouTube & jedes Video mit Freunden synchronisieren",
"META_DESCRIPTION": "Schaue Netflix, YouTube, Twitch und jedes HTML5-Video synchron mit Freunden. Kostenlose, quelloffene Browser-Erweiterung. Keine Anmeldung erforderlich.",
"SCHEMA_APP_DESC": "Schaue Netflix, YouTube, Twitch und jedes HTML5-Video synchron mit Freunden. Kostenlose, quelloffene Browser-Erweiterung. Keine Anmeldung erforderlich.",
"OG_TITLE": "KoalaSync | Netflix, Emby, Jellyfin & fast jedes Video mit Freunden synchronisieren",
"OG_DESCRIPTION": "Schaue Netflix, Emby, Jellyfin, YouTube, Twitch und fast jedes HTML5-Video perfekt synchronisiert. Quelloffene, datenschutzfreundliche Browser-Erweiterung für Chrome und Firefox.",
"TWITTER_TITLE": "KoalaSync | Netflix, Emby, Jellyfin & fast jedes Video mit Freunden synchronisieren Browser-Erweiterung",
@@ -75,7 +76,7 @@
"COMP_FEAT_5_TELE": "Nur unterstützte Seiten",
"COMP_FEAT_6_NAME": "Lokalisierung / Übersetzung",
"COMP_FEAT_6_DESC": "Verfügbare Oberflächensprachen und Übersetzungsunterstützung.",
"COMP_FEAT_6_KOALA": "13 Sprachen (wachsend)",
"COMP_FEAT_6_KOALA": "15 Sprachen (wachsend)",
"COMP_FEAT_6_TELE": "Nur Englisch",
"COMP_FEAT_7_NAME": "Audio Compressor / Lautstärke Angleichen",
"COMP_FEAT_7_DESC": "Integrierte Audionormalisierung für leise Dialoge und laute Effekte",
@@ -175,5 +176,5 @@
"FOOTER_LEGAL_LINK": "de/impressum",
"FOOTER_PRIVACY_LINK": "de/datenschutz",
"FOOTER_DISCLAIMER": "KoalaSync ist nicht mit Netflix, Disney+, Amazon, YouTube, Twitch oder anderen Streaming-Plattformen verbunden, wird von diesen nicht unterstützt oder steht in keiner Beziehung zu diesen. Alle Markenrechte liegen bei ihren jeweiligen Inhabern.",
"FOOTER_SUPPORT": "Kauf mir einen Kaffee auf Ko-Fi"
"FOOTER_SUPPORT": "Support KoalaSync"
}
+3 -2
View File
@@ -7,6 +7,7 @@
"OG_LOCALE": "en_US",
"META_TITLE": "KoalaSync | Sync Netflix, YouTube & Any Video with Friends",
"META_DESCRIPTION": "Watch Netflix, YouTube, Twitch & any HTML5 video in perfect sync with friends. Free, open-source browser extension for Chrome and Firefox. No sign-up needed.",
"SCHEMA_APP_DESC": "Watch Netflix, YouTube, Twitch & any HTML5 video in perfect sync with friends. Free, open-source browser extension for Chrome and Firefox. No sign-up needed.",
"OG_TITLE": "KoalaSync | Sync Netflix, Emby, Jellyfin & Almost Any Video with Friends",
"OG_DESCRIPTION": "Watch Netflix, Emby, Jellyfin, YouTube, Twitch and almost any HTML5 video in perfect sync. Privacy-first, open-source browser extension for Chrome & Firefox.",
"TWITTER_TITLE": "KoalaSync | Sync Netflix, Emby, Jellyfin & Almost Any Video with Friends Browser Extension",
@@ -75,7 +76,7 @@
"COMP_FEAT_5_TELE": "Supported sites only",
"COMP_FEAT_6_NAME": "Localization / Translation",
"COMP_FEAT_6_DESC": "Available interface languages and translation support.",
"COMP_FEAT_6_KOALA": "13 Languages (and growing)",
"COMP_FEAT_6_KOALA": "15 Languages (and growing)",
"COMP_FEAT_6_TELE": "English only",
"COMP_FEAT_7_NAME": "Audio Compressor / Volume Leveling",
"COMP_FEAT_7_DESC": "Built-in audio normalization to balance quiet dialogue and loud effects",
@@ -175,5 +176,5 @@
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_DISCLAIMER": "KoalaSync is not affiliated with, endorsed by, or associated with Netflix, Disney+, Amazon, YouTube, Twitch, or any other streaming platform. All trademarks are property of their respective owners.",
"FOOTER_SUPPORT": "Buy me a coffee on Ko-Fi"
"FOOTER_SUPPORT": "Support KoalaSync"
}
+6 -5
View File
@@ -7,6 +7,7 @@
"OG_LOCALE": "es_ES",
"META_TITLE": "KoalaSync | Sincroniza Netflix, YouTube y cualquier video con amigos",
"META_DESCRIPTION": "Mira Netflix, YouTube, Twitch y cualquier video HTML5 en perfecta sincronización con amigos. Extensión de navegador gratuita, de código abierto y sin necesidad de registro.",
"SCHEMA_APP_DESC": "Mira Netflix, YouTube, Twitch y cualquier video HTML5 en perfecta sincronización con amigos. Extensión de navegador gratuita, de código abierto y sin necesidad de registro.",
"OG_TITLE": "KoalaSync | Sincroniza Netflix, Emby, Jellyfin y casi cualquier video con amigos",
"OG_DESCRIPTION": "Mira Netflix, Emby, Jellyfin, YouTube, Twitch y casi cualquier video HTML5 en perfecta sincronización. Extensión de navegador de código abierto y respetuosa con la privacidad para Chrome y Firefox.",
"TWITTER_TITLE": "KoalaSync | Sincroniza Netflix, Emby, Jellyfin y casi cualquier video con amigos Extensión de navegador",
@@ -75,7 +76,7 @@
"COMP_FEAT_5_TELE": "Solo sitios compatibles",
"COMP_FEAT_6_NAME": "Localización / Traducción",
"COMP_FEAT_6_DESC": "Idiomas de interfaz disponibles y soporte de traducción.",
"COMP_FEAT_6_KOALA": "13 idiomas (y sumando)",
"COMP_FEAT_6_KOALA": "15 idiomas (y sumando)",
"COMP_FEAT_6_TELE": "Solo inglés",
"COMP_FEAT_7_NAME": "Compresor de Audio / Nivelación de Volumen",
"COMP_FEAT_7_DESC": "Normalización de audio integrada para equilibrar diálogos bajos y efectos fuertes",
@@ -150,9 +151,9 @@
"MOCK_08": "Salir de la sala",
"MOCK_09": "Seleccionar video",
"MOCK_10": "Control remoto",
"MOCK_11": "Reproducir",
"MOCK_12": "Pausar",
"MOCK_13": "SINCRO",
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Reproducir",
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Pausar",
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> SINCRO",
"MOCK_14": "Ir a la posición de otros",
"MOCK_15": "Estado de la última actividad",
"MOCK_16": "Esperando a 1 participante (KoalaPC)...",
@@ -175,5 +176,5 @@
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_DISCLAIMER": "KoalaSync no está afiliado, respaldado ni asociado con Netflix, Disney+, Amazon, YouTube, Twitch ni ninguna otra plataforma de streaming. Todas las marcas comerciales pertenecen a sus respectivos dueños.",
"FOOTER_SUPPORT": "Invítame a un café en Ko-Fi"
"FOOTER_SUPPORT": "Support KoalaSync"
}
+7 -6
View File
@@ -7,6 +7,7 @@
"OG_LOCALE": "fr_FR",
"META_TITLE": "KoalaSync | Synchronisez Netflix, YouTube et n'importe quelle vidéo avec vos amis",
"META_DESCRIPTION": "Regardez Netflix, YouTube, Twitch et des vidéos HTML5 en synchro avec vos amis. Extension de navigateur gratuite et open-source. Aucune inscription requise.",
"SCHEMA_APP_DESC": "Regardez Netflix, YouTube, Twitch et des vidéos HTML5 en synchro avec vos amis. Extension de navigateur gratuite et open-source. Aucune inscription requise.",
"OG_TITLE": "KoalaSync | Synchronisez Netflix, Emby, Jellyfin et presque toutes les vidéos avec vos amis",
"OG_DESCRIPTION": "Regardez Netflix, Emby, Jellyfin, YouTube, Twitch et presque n'importe quelle vidéo HTML5 en parfaite synchronisation. Extension de navigateur open-source et respectueuse de la vie privée pour Chrome et Firefox.",
"TWITTER_TITLE": "KoalaSync | Synchronisez Netflix, Emby, Jellyfin et presque toutes les vidéos avec vos amis Extension de navigateur",
@@ -49,7 +50,7 @@
"FEATURE_7_TITLE": "Égalisation du volume / Compresseur audio",
"FEATURE_7_DESC": "Fini de jouer avec le bouton du volume. Le compresseur audio intégré booste les dialogues discrets et adoucit les explosions. Fonctionne sur Netflix, Prime Video, Disney+, YouTube et toute vidéo HTML5. Entièrement local — aucune donnée audio ne quitte votre ordinateur.",
"FEATURE_8_TITLE": "Open Source (MIT License)",
"FEATURE_8_DESC": "The entire codebase is publicly auditable on GitHub. No black boxes, no proprietary blobs — 100% transparency from the relay server to the browser extension.",
"FEATURE_8_DESC": "Tout le code source est publiquement auditable sur GitHub. Pas de boite noire, pas de blob proprietaire - transparence totale du serveur relais a l'extension de navigateur.",
"COMP_TITLE": "KoalaSync vs Teleparty",
"COMP_SUBTITLE": "Découvrez pourquoi un outil open-source, sans publicité et respectueux de la vie privée est le meilleur choix pour regarder ensemble.",
"COMP_COL_FEATURE": "Fonctionnalité",
@@ -75,7 +76,7 @@
"COMP_FEAT_5_TELE": "Sites pris en charge uniquement",
"COMP_FEAT_6_NAME": "Localisation / Traduction",
"COMP_FEAT_6_DESC": "Langues d'interface disponibles et support de traduction.",
"COMP_FEAT_6_KOALA": "13 langues (et plus à venir)",
"COMP_FEAT_6_KOALA": "15 langues (et plus à venir)",
"COMP_FEAT_6_TELE": "Anglais uniquement",
"COMP_FEAT_7_NAME": "Compresseur Audio / Égalisation du Volume",
"COMP_FEAT_7_DESC": "Normalisation audio intégrée pour équilibrer les dialogues silencieux et les effets sonores",
@@ -150,9 +151,9 @@
"MOCK_08": "Quitter",
"MOCK_09": "Choisir la vidéo",
"MOCK_10": "Télécommande",
"MOCK_11": "Lire",
"MOCK_12": "Pause",
"MOCK_13": "Sauter vers les autres",
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Lire",
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Pause",
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> SYNC",
"MOCK_14": "Dernière activité",
"MOCK_15": "En attente de 1 participant (KoalaPC)...",
"MOCK_16": "Passer & lire",
@@ -175,5 +176,5 @@
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_DISCLAIMER": "KoalaSync n'est pas affilié à Netflix, Disney+, Amazon, YouTube, Twitch ou toute autre plateforme de streaming. Toutes les marques déposées appartiennent à leurs détenteurs respectifs.",
"FOOTER_SUPPORT": "Offre-moi un café sur Ko-Fi"
"FOOTER_SUPPORT": "Support KoalaSync"
}
+6 -5
View File
@@ -7,6 +7,7 @@
"OG_LOCALE": "it_IT",
"META_TITLE": "KoalaSync | Sincronizza Netflix, YouTube e qualsiasi video con i tuoi amici",
"META_DESCRIPTION": "Guarda Netflix, YouTube, Twitch e qualsiasi video HTML5 in perfetta sincronia. Estensione gratuita, open source e senza registrazione.",
"SCHEMA_APP_DESC": "Guarda Netflix, YouTube, Twitch e qualsiasi video HTML5 in perfetta sincronia. Estensione gratuita, open source e senza registrazione.",
"OG_TITLE": "KoalaSync | Sincronizza Netflix, Emby, Jellyfin e video web con i tuoi amici",
"OG_DESCRIPTION": "Guarda video in perfetta sincronia con i tuoi amici. Estensione open source per Chrome e Firefox incentrata sulla privacy.",
"TWITTER_TITLE": "KoalaSync | Sincronizza video in streaming con i tuoi amici",
@@ -75,7 +76,7 @@
"COMP_FEAT_5_TELE": "Solo siti selezionati",
"COMP_FEAT_6_NAME": "Traduzione",
"COMP_FEAT_6_DESC": "Lingue disponibili per l'interfaccia.",
"COMP_FEAT_6_KOALA": "13 Lingue (in crescita)",
"COMP_FEAT_6_KOALA": "15 Lingue (in crescita)",
"COMP_FEAT_6_TELE": "Solo Inglese",
"COMP_FEAT_7_NAME": "Compressore Audio",
"COMP_FEAT_7_DESC": "Bilanciamento automatico del suono per voci più chiare.",
@@ -150,9 +151,9 @@
"MOCK_08": "Esci dalla stanza",
"MOCK_09": "Scegli Video",
"MOCK_10": "Telecomando",
"MOCK_11": "Play",
"MOCK_12": "Pausa",
"MOCK_13": "SINCRO",
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Riproduci",
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Pausa",
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> SYNC",
"MOCK_14": "Vai dagli altri",
"MOCK_15": "Ultima attività",
"MOCK_16": "In attesa di un amico (KoalaPC)...",
@@ -175,5 +176,5 @@
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_DISCLAIMER": "KoalaSync non è affiliato a Netflix, YouTube o altre piattaforme. I marchi appartengono ai rispettivi proprietari.",
"FOOTER_SUPPORT": "Offrimi un caffè su Ko-Fi"
"FOOTER_SUPPORT": "Support KoalaSync"
}
+3 -2
View File
@@ -7,6 +7,7 @@
"OG_LOCALE": "ja_JP",
"META_TITLE": "KoalaSync | Netflix、YouTube、あらゆる動画を友達と同期視聴",
"META_DESCRIPTION": "Netflix、YouTube、Twitch、およびあらゆるHTML5動画を友達と完全に同期して視聴。ChromeおよびFirefox用の無料のオープンソースブラウザ拡張機能。登録不要。",
"SCHEMA_APP_DESC": "Netflix、YouTube、Twitch、およびあらゆるHTML5動画を友達と完全に同期して視聴。ChromeおよびFirefox用の無料のオープンソースブラウザ拡張機能。登録不要。",
"OG_TITLE": "KoalaSync | Netflix、Emby、Jellyfin、ほぼすべての動画を友達と同期",
"OG_DESCRIPTION": "Netflix、Emby、Jellyfin、YouTube、Twitch、ほぼすべてのHTML5動画を完全に同期して視聴。ChromeおよびFirefox用のプライバシー優先のオープンソースブラウザ拡張機能。",
"TWITTER_TITLE": "KoalaSync | Netflix、Emby、Jellyfin、ほぼすべての動画を友達と同期 – ブラウザ拡張機能",
@@ -75,7 +76,7 @@
"COMP_FEAT_5_TELE": "サポートされているサイトのみ",
"COMP_FEAT_6_NAME": "ローカライズ / 翻訳",
"COMP_FEAT_6_DESC": "利用可能なインターフェース言語と翻訳サポート。",
"COMP_FEAT_6_KOALA": "13の言語(今後も追加予定)",
"COMP_FEAT_6_KOALA": "15の言語(今後も追加予定)",
"COMP_FEAT_6_TELE": "英語のみ",
"COMP_FEAT_7_NAME": "オーディオコンプレッサー / 音量均等化",
"COMP_FEAT_7_DESC": "静かな会話と大きな効果音のバランスを取る内蔵オーディオノーマライゼーション",
@@ -175,5 +176,5 @@
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_DISCLAIMER": "KoalaSyncは、Netflix、Disney+、Amazon、YouTube、Twitch、またはその他のストリーミングプラットフォームと提携、承認、または関連付けられているものではありません。すべての商標はそれぞれの所有者に帰属します。",
"FOOTER_SUPPORT": "Ko-Fiでコーヒーをおごる"
"FOOTER_SUPPORT": "Support KoalaSync"
}
+3 -2
View File
@@ -7,6 +7,7 @@
"OG_LOCALE": "ko_KR",
"META_TITLE": "KoalaSync | Netflix, YouTube 및 모든 비디오를 친구와 실시간 동기화 시청",
"META_DESCRIPTION": "Netflix, YouTube, Twitch 및 모든 HTML5 비디오를 친구들과 완벽하게 동기화하여 시청하세요. Chrome 및 Firefox용 무료 오픈 소스 브라우저 확장 프로그램. 가입 필요 없음.",
"SCHEMA_APP_DESC": "Netflix, YouTube, Twitch 및 모든 HTML5 비디오를 친구들과 완벽하게 동기화하여 시청하세요. Chrome 및 Firefox용 무료 오픈 소스 브라우저 확장 프로그램. 가입 필요 없음.",
"OG_TITLE": "KoalaSync | Netflix, Emby, Jellyfin 및 거의 모든 비디오를 친구와 동기화 시청",
"OG_DESCRIPTION": "Netflix, Emby, Jellyfin, YouTube, Twitch 및 거의 모든 HTML5 비디오를 완벽하게 동기화하여 시청하세요. Chrome 및 Firefox용 개인정보 보호 중심 오픈 소스 브라우저 확장 프로그램.",
"TWITTER_TITLE": "KoalaSync | Netflix, Emby, Jellyfin 및 거의 모든 비디오를 친구와 동기화 시청 – 브라우저 확장 프로그램",
@@ -75,7 +76,7 @@
"COMP_FEAT_5_TELE": "지원되는 사이트만 가능",
"COMP_FEAT_6_NAME": "로컬라이징 / 다국어 지원",
"COMP_FEAT_6_DESC": "제공되는 인터페이스 언어 및 번역 지원 여부.",
"COMP_FEAT_6_KOALA": "13개 언어 지원 (추가 중)",
"COMP_FEAT_6_KOALA": "15개 언어 지원 (추가 중)",
"COMP_FEAT_6_TELE": "영어만 지원",
"COMP_FEAT_7_NAME": "오디오 압축기 / 볼륨 레벨링",
"COMP_FEAT_7_DESC": "조용한 대화와 큰 효과음의 균형을 맞추는 내장 오디오 노멀라이제이션",
@@ -175,5 +176,5 @@
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_DISCLAIMER": "KoalaSync는 Netflix, Disney+, Amazon, YouTube, Twitch 또는 기타 스트리밍 플랫폼과 제휴, 보증 또는 연관되어 있지 않습니다. 모든 상표는 해당 소유자의 재산입니다.",
"FOOTER_SUPPORT": "Ko-Fi에서 커피 한 잔 사주세요"
"FOOTER_SUPPORT": "Support KoalaSync"
}
+4 -3
View File
@@ -7,6 +7,7 @@
"OG_LOCALE": "nl_NL",
"META_TITLE": "KoalaSync | Synchroniseer Netflix, YouTube & elke video met vrienden",
"META_DESCRIPTION": "Bekijk Netflix, YouTube, Twitch en elke HTML5-video in perfecte synchronisatie met vrienden. Gratis, open-source browserextensie voor Chrome en Firefox. Geen registratie nodig.",
"SCHEMA_APP_DESC": "Bekijk Netflix, YouTube, Twitch en elke HTML5-video in perfecte synchronisatie met vrienden. Gratis, open-source browserextensie voor Chrome en Firefox. Geen registratie nodig.",
"OG_TITLE": "KoalaSync | Synchroniseer Netflix, Emby, Jellyfin & bijna elke video met vrienden",
"OG_DESCRIPTION": "Bekijk Netflix, Emby, Jellyfin, YouTube, Twitch en bijna elke HTML5-video in perfecte synchronisatie. Privacy-first, open-source browserextensie voor Chrome & Firefox.",
"TWITTER_TITLE": "KoalaSync | Synchroniseer Netflix, Emby, Jellyfin & bijna elke video met vrienden Browserextensie",
@@ -49,7 +50,7 @@
"FEATURE_7_TITLE": "Volume egaliseren / Audiocompressor",
"FEATURE_7_DESC": "Niet langer aan de volumeknop zitten. De ingebouwde audiocompressor versterkt stille dialogen en dempt harde explosies. Werkt op Netflix, Prime Video, Disney+, YouTube en elke HTML5-video. Volledig lokaal — geen audiogegevens verlaten uw computer.",
"FEATURE_8_TITLE": "Open Source (MIT License)",
"FEATURE_8_DESC": "The entire codebase is publicly auditable on GitHub. No black boxes, no proprietary blobs 100% transparency from the relay server to the browser extension.",
"FEATURE_8_DESC": "De volledige codebase is openbaar controleerbaar op GitHub. Geen zwarte dozen, geen propri?taire blobs - 100% transparantie van de relayserver tot de browserextensie.",
"COMP_TITLE": "KoalaSync vs. Teleparty",
"COMP_SUBTITLE": "Ontdek waarom open-source, reclamevrij en privacy-first de superieure manier is om samen te kijken.",
"COMP_COL_FEATURE": "Functie",
@@ -75,7 +76,7 @@
"COMP_FEAT_5_TELE": "Alleen ondersteunde sites",
"COMP_FEAT_6_NAME": "Lokalisatie / Vertaling",
"COMP_FEAT_6_DESC": "Beschikbare interfacetalen en vertaalondersteuning.",
"COMP_FEAT_6_KOALA": "13 talen (en groeiend)",
"COMP_FEAT_6_KOALA": "15 talen (en groeiend)",
"COMP_FEAT_6_TELE": "Alleen Engels",
"COMP_FEAT_7_NAME": "Audiocompressor / Volume Egaliseren",
"COMP_FEAT_7_DESC": "Ingebouwde audionormalisatie om stille dialogen en luide effecten in balans te brengen",
@@ -175,5 +176,5 @@
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_DISCLAIMER": "KoalaSync is niet verbonden aan, goedgekeurd door of geassocieerd met Netflix, Disney+, Amazon, YouTube, Twitch of enig ander streamingplatform. Alle handelsmerken zijn eigendom van hun respectieve eigenaren.",
"FOOTER_SUPPORT": "Trakteer op een koffie via Ko-Fi"
"FOOTER_SUPPORT": "Support KoalaSync"
}
+3 -2
View File
@@ -7,6 +7,7 @@
"OG_LOCALE": "pl_PL",
"META_TITLE": "KoalaSync | Synchronizuj Netflix, YouTube i dowolne wideo ze znajomymi",
"META_DESCRIPTION": "Oglądaj Netflix, YouTube, Twitch i dowolne wideo HTML5 w idealnej synchronizacji ze znajomymi. Darmowe rozszerzenie o otwartym kodzie dla Chrome i Firefox. Bez rejestracji.",
"SCHEMA_APP_DESC": "Oglądaj Netflix, YouTube, Twitch i dowolne wideo HTML5 w idealnej synchronizacji ze znajomymi. Darmowe rozszerzenie o otwartym kodzie dla Chrome i Firefox. Bez rejestracji.",
"OG_TITLE": "KoalaSync | Synchronizuj Netflix, Emby, Jellyfin i prawie każde wideo ze znajomymi",
"OG_DESCRIPTION": "Oglądaj Netflix, Emby, Jellyfin, YouTube, Twitch i prawie każde wideo HTML5 w idealnej synchronizacji. Zorientowane na prywatność rozszerzenie o otwartym kodzie dla Chrome i Firefox.",
"TWITTER_TITLE": "KoalaSync | Synchronizuj Netflix, Emby, Jellyfin i prawie każde wideo ze znajomymi Rozszerzenie przeglądarki",
@@ -75,7 +76,7 @@
"COMP_FEAT_5_TELE": "Tylko obsługiwane strony",
"COMP_FEAT_6_NAME": "Lokalizacja / Tłumaczenie",
"COMP_FEAT_6_DESC": "Dostępne języki interfejsu i wsparcie tłumaczeń.",
"COMP_FEAT_6_KOALA": "13 języków (i rośnie)",
"COMP_FEAT_6_KOALA": "15 języków (i rośnie)",
"COMP_FEAT_6_TELE": "Tylko angielski",
"COMP_FEAT_7_NAME": "Kompresor Audio / Wyrównywanie Głośności",
"COMP_FEAT_7_DESC": "Wbudowana normalizacja dźwięku do równoważenia cichych dialogów i głośnych efektów",
@@ -175,5 +176,5 @@
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_DISCLAIMER": "KoalaSync nie jest powiązany, wspierany ani stowarzyszony z Netflix, Disney+, Amazon, YouTube, Twitch ani żadną inną platformą streamingową. Wszystkie znaki towarowe są własnością ich odpowiednich właścicieli.",
"FOOTER_SUPPORT": "Postaw kawę na Ko-Fi"
"FOOTER_SUPPORT": "Support KoalaSync"
}
+6 -5
View File
@@ -7,6 +7,7 @@
"OG_LOCALE": "pt_BR",
"META_TITLE": "KoalaSync | Sincronize Netflix, YouTube e qualquer vídeo com amigos",
"META_DESCRIPTION": "Assista Netflix, YouTube, Twitch e qualquer vídeo HTML5 em perfeita sincronia. Extensão de navegador gratuita e open source. Sem cadastro.",
"SCHEMA_APP_DESC": "Assista Netflix, YouTube, Twitch e qualquer vídeo HTML5 em perfeita sincronia. Extensão de navegador gratuita e open source. Sem cadastro.",
"OG_TITLE": "KoalaSync | Sincronize Netflix, Emby, Jellyfin e vídeos com amigos",
"OG_DESCRIPTION": "Assista a vídeos em perfeita sincronia com seus amigos. Extensão open source e focada em privacidade.",
"TWITTER_TITLE": "KoalaSync | Sincronize vídeos online com seus amigos",
@@ -75,7 +76,7 @@
"COMP_FEAT_5_TELE": "Apenas sites selecionados",
"COMP_FEAT_6_NAME": "Traduzido",
"COMP_FEAT_6_DESC": "Idiomas suportados.",
"COMP_FEAT_6_KOALA": "13 Idiomas",
"COMP_FEAT_6_KOALA": "15 Idiomas",
"COMP_FEAT_6_TELE": "Apenas Inglês",
"COMP_FEAT_7_NAME": "Compressor de Áudio",
"COMP_FEAT_7_DESC": "Melhora a clareza das vozes nos vídeos.",
@@ -150,9 +151,9 @@
"MOCK_08": "Sair da Sala",
"MOCK_09": "Selecionar Vídeo",
"MOCK_10": "Controle Remoto",
"MOCK_11": "Play",
"MOCK_12": "Pausa",
"MOCK_13": "SINCRO",
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Play",
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Pausa",
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> SINCRO",
"MOCK_14": "Ir para a posição dos outros",
"MOCK_15": "Última Atividade",
"MOCK_16": "Aguardando 1 amigo (KoalaPC)...",
@@ -175,5 +176,5 @@
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_DISCLAIMER": "KoalaSync não é afiliado à Netflix, Disney+, YouTube, Twitch, etc. Todas as marcas registradas são de propriedade de seus respectivos donos.",
"FOOTER_SUPPORT": "Me pague um café"
"FOOTER_SUPPORT": "Support KoalaSync"
}
+6 -5
View File
@@ -7,6 +7,7 @@
"OG_LOCALE": "pt_PT",
"META_TITLE": "KoalaSync | Sincronize Netflix, YouTube e vídeos com os amigos",
"META_DESCRIPTION": "Veja Netflix, YouTube, Twitch e qualquer vídeo HTML5 em sincronia perfeita. Extensão gratuita e de código aberto. Sem registo.",
"SCHEMA_APP_DESC": "Veja Netflix, YouTube, Twitch e qualquer vídeo HTML5 em sincronia perfeita. Extensão gratuita e de código aberto. Sem registo.",
"OG_TITLE": "KoalaSync | Sincronize Netflix, Emby, Jellyfin e vídeos com os amigos",
"OG_DESCRIPTION": "Veja vídeos em sincronia perfeita com os amigos. Extensão de código aberto e focada na privacidade.",
"TWITTER_TITLE": "KoalaSync | Sincronize vídeos em streaming com os amigos",
@@ -75,7 +76,7 @@
"COMP_FEAT_5_TELE": "Apenas sites selecionados",
"COMP_FEAT_6_NAME": "Traduzido",
"COMP_FEAT_6_DESC": "Idiomas suportados.",
"COMP_FEAT_6_KOALA": "13 Idiomas",
"COMP_FEAT_6_KOALA": "15 Idiomas",
"COMP_FEAT_6_TELE": "Apenas Inglês",
"COMP_FEAT_7_NAME": "Compressor de Áudio",
"COMP_FEAT_7_DESC": "Melhora a clareza das vozes nos vídeos.",
@@ -150,9 +151,9 @@
"MOCK_08": "Sair da Sala",
"MOCK_09": "Selecionar Vídeo",
"MOCK_10": "Controlo Remoto",
"MOCK_11": "Play",
"MOCK_12": "Pausa",
"MOCK_13": "SINCRO",
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Play",
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Pausa",
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> SINCRO",
"MOCK_14": "Ir para a posição dos outros",
"MOCK_15": "Última Atividade",
"MOCK_16": "À espera de 1 amigo (KoalaPC)...",
@@ -175,5 +176,5 @@
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_DISCLAIMER": "KoalaSync não é afiliado à Netflix, Disney+, YouTube, Twitch, etc. Todas as marcas são propriedade dos seus titulares.",
"FOOTER_SUPPORT": "Oferece-me um café"
"FOOTER_SUPPORT": "Support KoalaSync"
}
+6 -5
View File
@@ -7,6 +7,7 @@
"OG_LOCALE": "ru_RU",
"META_TITLE": "KoalaSync | Синхронизация Netflix, YouTube и любого видео с друзьями",
"META_DESCRIPTION": "Смотрите Netflix, YouTube, Twitch и любые HTML5-видео в синхронизации с друзьями. Бесплатное расширение браузера с открытым кодом. Регистрация не требуется.",
"SCHEMA_APP_DESC": "Смотрите Netflix, YouTube, Twitch и любые HTML5-видео в синхронизации с друзьями. Бесплатное расширение браузера с открытым кодом. Регистрация не требуется.",
"OG_TITLE": "KoalaSync | Синхронизация Netflix, Emby, Jellyfin и почти любого видео с друзьями",
"OG_DESCRIPTION": "Смотрите Netflix, Emby, Jellyfin, YouTube, Twitch и почти любое HTML5-видео в идеальной синхронизации. Открытое и ориентированное на конфиденциальность расширение для Chrome и Firefox.",
"TWITTER_TITLE": "KoalaSync | Синхронизация Netflix, Emby, Jellyfin и почти любого видео с друзьями – Расширение",
@@ -75,7 +76,7 @@
"COMP_FEAT_5_TELE": "Только поддерживаемые сайты",
"COMP_FEAT_6_NAME": "Локализация / Перевод",
"COMP_FEAT_6_DESC": "Доступные языки интерфейса и поддержка перевода.",
"COMP_FEAT_6_KOALA": "13 языков (и расширяется)",
"COMP_FEAT_6_KOALA": "15 языков (и расширяется)",
"COMP_FEAT_6_TELE": "Только английский",
"COMP_FEAT_7_NAME": "Аудиокомпрессор / Выравнивание Громкости",
"COMP_FEAT_7_DESC": "Встроенная нормализация звука для баланса тихих диалогов и громких эффектов",
@@ -150,9 +151,9 @@
"MOCK_08": "Покинуть комнату",
"MOCK_09": "Выбрать видео",
"MOCK_10": "Пульт управления",
"MOCK_11": "Воспроизвести",
"MOCK_12": "Пауза",
"MOCK_13": "Перейти к другим",
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Воспроизвести",
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Пауза",
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> SYNC",
"MOCK_14": "Последняя активность",
"MOCK_15": "Ожидание 1 участника (KoalaPC)...",
"MOCK_16": "Пропустить и играть",
@@ -175,5 +176,5 @@
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_DISCLAIMER": "KoalaSync не связан, не спонсируется и не аффилирован с Netflix, Disney+, Amazon, YouTube, Twitch или любой другой стриминговой платформой. Все товарные знаки являются собственностью их соответствующих владельцев.",
"FOOTER_SUPPORT": "Угости кофе на Ko-Fi"
"FOOTER_SUPPORT": "Support KoalaSync"
}
+3 -2
View File
@@ -7,6 +7,7 @@
"OG_LOCALE": "tr_TR",
"META_TITLE": "KoalaSync | Netflix, YouTube ve Herhangi Bir Videoyu Arkadaşlarınızla Eşitleyin",
"META_DESCRIPTION": "Netflix, YouTube, Twitch ve herhangi bir HTML5 videoyu arkadaşlarınızla mükemmel uyum içinde izleyin. Chrome ve Firefox için ücretsiz, açık kaynaklı tarayıcı eklentisi. Kayıt gerekmez.",
"SCHEMA_APP_DESC": "Netflix, YouTube, Twitch ve herhangi bir HTML5 videoyu arkadaşlarınızla mükemmel uyum içinde izleyin. Chrome ve Firefox için ücretsiz, açık kaynaklı tarayıcı eklentisi. Kayıt gerekmez.",
"OG_TITLE": "KoalaSync | Netflix, Emby, Jellyfin ve Neredeyse Her Videoyu Arkadaşlarınızla Eşitleyin",
"OG_DESCRIPTION": "Netflix, Emby, Jellyfin, YouTube, Twitch ve neredeyse tüm HTML5 videolarını mükemmel bir senkronizasyonla izleyin. Chrome ve Firefox için gizlilik öncelikli, açık kaynaklı tarayıcı eklentisi.",
"TWITTER_TITLE": "KoalaSync | Netflix, Emby, Jellyfin ve Neredeyse Her Videoyu Arkadaşlarla Eşitle Tarayıcı Eklentisi",
@@ -75,7 +76,7 @@
"COMP_FEAT_5_TELE": "Sadece desteklenen siteler",
"COMP_FEAT_6_NAME": "Yerelleştirme / Çeviri",
"COMP_FEAT_6_DESC": "Mevcut arayüz dilleri ve çeviri desteği.",
"COMP_FEAT_6_KOALA": "13 Dil (ve artıyor)",
"COMP_FEAT_6_KOALA": "15 Dil (ve artıyor)",
"COMP_FEAT_6_TELE": "Sadece İngilizce",
"COMP_FEAT_7_NAME": "Ses Kompresörü / Ses Seviyesi Dengeleme",
"COMP_FEAT_7_DESC": "Sessiz diyaloglar ve yüksek efektler arasında denge sağlayan yerleşik ses normalizasyonu",
@@ -175,5 +176,5 @@
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_DISCLAIMER": "KoalaSync; Netflix, Disney+, Amazon, YouTube, Twitch veya başka herhangi bir yayın platformuna bağlı değildir, bunlar tarafından desteklenmemektedir ve bunlarla ilişkili değildir. Tüm ticari markalar ilgili sahiplerinin mülkiyetindedir.",
"FOOTER_SUPPORT": "Ko-Fi'de bana kahve ısmarla"
"FOOTER_SUPPORT": "Support KoalaSync"
}
+180
View File
@@ -0,0 +1,180 @@
{
"LANG_CODE": "uk",
"HTML_CLASS": "lang-uk",
"CANONICAL_PATH": "uk/",
"LANG_TOGGLE_URL": "../",
"LANG_TOGGLE_TEXT": "EN",
"OG_LOCALE": "uk_UA",
"META_TITLE": "KoalaSync | Синхронізуйте Netflix, YouTube і будь-яке відео з друзями",
"META_DESCRIPTION": "Дивіться Netflix, YouTube, Twitch і будь-яке HTML5 відео в ідеальній синхронізації з друзями. Безкоштовне розширення для браузера з відкритим кодом для Chrome і Firefox. Реєстрація не потрібна.",
"SCHEMA_APP_DESC": "Дивіться Netflix, YouTube, Twitch і будь-яке HTML5 відео в ідеальній синхронізації з друзями. Безкоштовне розширення для браузера з відкритим кодом для Chrome і Firefox. Реєстрація не потрібна.",
"OG_TITLE": "KoalaSync | Синхронізація Netflix, Emby, Jellyfin і майже будь-якого відео з друзями",
"OG_DESCRIPTION": "Дивіться Netflix, Emby, Jellyfin, YouTube, Twitch і майже будь-яке відео HTML5 в ідеальній синхронізації. Розширення веб-переглядача з відкритим вихідним кодом для Chrome і Firefox, перш за все конфіденційність.",
"TWITTER_TITLE": "KoalaSync | Синхронізація Netflix, Emby, Jellyfin і майже будь-якого відео з друзями – розширення браузера",
"TWITTER_DESCRIPTION": "Дивіться Netflix, Emby, Jellyfin, YouTube, Twitch і майже будь-яке відео HTML5 в ідеальній синхронізації з друзями. Розширення веб-переглядача з відкритим вихідним кодом для Chrome і Firefox, перш за все конфіденційність.",
"NAV_FEATURES": "особливості",
"NAV_HOW_IT_WORKS": "Як це працює",
"HERO_TITLE": "Дивіться разом.<span class='hero-line2'>Ідеальна синхронізація.</span>",
"HERO_SUBTITLE": "Ваш віддалений кіновечір без лагів. Ні реєстрації, ні збору даних. Просто поділіться посиланням і дивіться разом.",
"HERO_MASCOT_ALT": "Мила коала стоїть і дивиться вниз на кнопки завантаження",
"ADD_TO_CHROME": "Додати до Chrome",
"ADD_TO_FIREFOX": "Додати до Firefox",
"COMPAT_HEADING": "Працює на ваших улюблених платформах",
"COMPAT_MORE": "і багато іншого",
"COMPAT_TOOLTIP": "Працює майже на будь-якому сайті з елементом відео",
"USE_CASES_TITLE": "Ідеально підходить для будь-якого випадку",
"USE_CASES_SUBTITLE": "Незалежно від того, поблизу чи далеко, KoalaSync об’єднує людей навколо їхніх улюблених відео.",
"USE_CASE_1_ALT": "Дві милі коали сидять разом і діляться відерцем попкорну",
"USE_CASE_1_TITLE": "Вечір кіно з друзями",
"USE_CASE_1_DESC": "Синхронізуйте свої фільми в режимі реального часу та спілкуйтеся через Discord, Zoom або улюблену програму для голосових викликів. Таке відчуття, ніби сидиш в одній кімнаті.",
"USE_CASE_2_ALT": "Професор-коала презентує на екрані двом віддаленим студентам-коалам",
"USE_CASE_2_TITLE": "Дистанційне навчання",
"USE_CASE_2_DESC": "Аналізуйте онлайн-уроки, лекції чи навчальні потоки для розробників разом з однокласниками чи колегами. Зробіть паузу та обговоріть складні кроки в ідеальній гармонії.",
"USE_CASE_3_ALT": "Мила пара коал дивиться разом віддалено; один сидить за комп’ютером, а інший у ліжку з ноутбуком, а між ними літають серця",
"USE_CASE_3_TITLE": "Відносини на відстані",
"USE_CASE_3_DESC": "Подолайте відстань і насолоджуйтесь побаченнями. Переживайте кожен поворот сюжету, смійтеся над тими самими жартами та діліться емоційними моментами в ту саму мілісекунду.",
"WHY_TITLE": "Чому KoalaSync?",
"WHY_SUBTITLE": "Створено для надійної синхронізації, конфіденційності та простого налаштування.",
"FEATURE_1_TITLE": "Повний контроль/миттєва синхронізація",
"FEATURE_1_DESC": "Один робить паузу, всі зупиняються. Один шукає, всі йдуть. Наш двофазний протокол синхронізації координує відтворення в режимі реального часу для всіх учасників.",
"FEATURE_2_TITLE": "Нескінченне споглядання",
"FEATURE_2_DESC": "Автоматичне відтворення синхронізовано. KoalaSync автоматично визначає переходи епізодів і утримує відтворення, доки всі однорангові пристрої успішно не завантажать наступне відео.",
"FEATURE_3_TITLE": "Нуль облікових записів / чиста конфіденційність",
"FEATURE_3_DESC": "Ні реєстрації, ні відстеження, ні слідів постійності. Сервер повністю працює в ефемерному RAM і очищає вашу кімнату, коли ви залишаєте її.",
"FEATURE_4_TITLE": "Універсальна підтримка HTML5",
"FEATURE_4_DESC": "Працює на YouTube, Twitch, Netflix, Jellyfin, Emby і майже будь-якій стандартній веб-сторінці, що містить відеоелемент HTML5. Також сумісний із користувальницькими налаштуваннями, розміщеними на власному хості. На наші сервери ніколи не надсилаються відеодані — лише метадані синхронізації.",
"FEATURE_5_TITLE": "Саморозміщення та готовність до Docker",
"FEATURE_5_DESC": "Наші офіційні сервери безкоштовні та швидкі, але ви можете отримати повне право власності, якщо хочете. Запустіть власний приватний сервер ретрансляції за лічені секунди через Docker.",
"FEATURE_6_TITLE": "Миттєві запрошення / Приєднання в 1 клік",
"FEATURE_6_DESC": "Немає IP-адрес або паролів для обміну. Поділіться створеним посиланням із запрошенням із друзями, щоб вони могли автоматично приєднатися до вашої кімнати одним клацанням миші.",
"FEATURE_7_TITLE": "Вирівнювання гучності / Аудіокомпресор",
"FEATURE_7_DESC": "Більше не потрібно натискати на ручку гучності. Вбудований аудіокомпресор посилює тихий діалог і приборкує гучні вибухи. Працює на Netflix, Prime Video, Disney+, YouTube і будь-якому відео HTML5. Повністю локально — аудіодані не залишають ваш комп’ютер.",
"FEATURE_8_TITLE": "Відкритий код (ліцензія MIT)",
"FEATURE_8_DESC": "Вся кодова база доступна для публічного аудиту на GitHub. Жодних чорних скриньок, жодних власних блобів — 100% прозорість від сервера ретрансляції до розширення браузера.",
"COMP_TITLE": "KoalaSync проти Teleparty",
"COMP_SUBTITLE": "Дізнайтеся, чому відкритий вихідний код, без реклами та конфіденційність – це найкращий спосіб дивитися разом.",
"COMP_COL_FEATURE": "Особливість",
"COMP_FEAT_1_NAME": "Вартість / Paywalls",
"COMP_FEAT_1_DESC": "Преміум-підписки, приховані комісії або заблоковані функції.",
"COMP_FEAT_1_KOALA": "100% безкоштовно",
"COMP_FEAT_1_TELE": "Платні рівні / преміум-блокування",
"COMP_FEAT_2_NAME": "Ліцензія/Код підлягає аудиту",
"COMP_FEAT_2_DESC": "Чи є вихідний код відкритим і доступним для вільного аудиту.",
"COMP_FEAT_2_KOALA": "Відкритий код (MIT)",
"COMP_FEAT_2_TELE": "Власний",
"COMP_FEAT_3_NAME": "Самостійне розміщення",
"COMP_FEAT_3_DESC": "Можливість розгорнути власний приватний сервер ретрансляції.",
"COMP_FEAT_3_KOALA": "Так (готовий до Docker)",
"COMP_FEAT_3_TELE": "немає",
"COMP_FEAT_4_NAME": "Конфіденційність і відстеження",
"COMP_FEAT_4_DESC": "Вимоги до реєстрації, відстеження файлів cookie та аналітика.",
"COMP_FEAT_4_KOALA": "Нульова стійкість (лише для RAM)",
"COMP_FEAT_4_TELE": "Google Analytics і файли cookie",
"COMP_FEAT_5_NAME": "Сумісність сайту",
"COMP_FEAT_5_DESC": "Підтримувані веб-сайти та сумісність із плеєрами.",
"COMP_FEAT_5_KOALA": "Майже будь-яке відео HTML5",
"COMP_FEAT_5_TELE": "Лише підтримувані сайти",
"COMP_FEAT_6_NAME": "Локалізація / Переклад",
"COMP_FEAT_6_DESC": "Доступні мови інтерфейсу та підтримка перекладу.",
"COMP_FEAT_6_KOALA": "15 мов (і зростає)",
"COMP_FEAT_6_TELE": "лише англійською мовою",
"COMP_FEAT_7_NAME": "Аудіокомпресор / Вирівнювання гучності",
"COMP_FEAT_7_DESC": "Вбудована нормалізація аудіо для балансування тихих діалогів і гучних ефектів",
"COMP_FEAT_7_KOALA": "Так (4 попередні налаштування, сухий/мокрий контроль)",
"COMP_FEAT_7_TELE": "немає",
"COMP_FOOTNOTE_1": "Стан для порівняння: травень 2026 року.",
"COMP_FOOTNOTE_2": "Офіційні ціни на Teleparty Premium і відомості про підтримувані мережі:",
"COMP_FOOTNOTE_3": "Політика конфіденційності Teleparty і збір даних відстеження:",
"COMP_FOOTNOTE_4": "Працює на веб-сайтах, які дозволяють ін’єкції сценаріїв у стандартні відеотеги HTML5. Веб-сайти з дуже суворими політиками безпеки вмісту (CSP), захистом від копіювання DRM або сильно затуманеними оболонками програвачів (як-от складні тіньові DOM) можуть обмежувати автоматизований контроль або впровадження.",
"STEPS_TITLE": "Початок роботи",
"STEP_1_TITLE": "Встановити розширення",
"STEP_1_DESC": "Додайте KoalaSync до свого браузера з веб-магазину Chrome, доповнень Firefox або завантажте найновіший розробник ZIP із GitHub.",
"STEP_1_ILLUS_DESC": "Синхронізатор відео насамперед із конфіденційністю",
"STEP_1_ILLUS_DL_CHROME": "Завантажити для Chrome",
"STEP_1_ILLUS_DL_FIREFOX": "Завантажити для Firefox",
"STEP_1_ILLUS_ACTIVE": "Розширення активне",
"STEP_1_ILLUS_READY": "Готові дивитися разом!",
"STEP_2_TITLE": "Створіть кімнату",
"STEP_2_DESC": "Відкрийте спливаюче вікно розширення та натисніть «+ Створити нову кімнату». KoalaSync автоматично генерує безпечний Room ID і пароль, приєднується до нього та копіює посилання для запрошення в буфер обміну.",
"STEP_2_ILLUS_ROOM": "Кімната",
"STEP_2_ILLUS_SYNC": "Синхронізувати",
"STEP_2_ILLUS_SETTINGS": "Налаштування",
"STEP_2_ILLUS_CREATE": "+ Створити нову кімнату",
"STEP_2_ILLUS_MANUAL": "Ручне підключення / Додатково",
"STEP_2_ILLUS_COPIED": "Посилання на запрошення скопійовано!",
"STEP_3_TITLE": "Поділитися та синхронізувати",
"STEP_3_DESC": "Надішліть запрошення своїм друзям. Коли вони приєднаються, виберіть вкладку відео та насолоджуйтеся синхронізованим відтворенням.",
"STEP_3_ILLUS_IN_SYNC": "СИНХРОНІЗОВАНО",
"SELF_TITLE": "Для самостійних господарів",
"SELF_SUBTITLE": "Не довіряєте нашому серверу? Підтримуйте повний суверенітет даних. Розгорніть власний приватний сервер ретрансляції за лічені хвилини.",
"SELF_MASCOT_ALT": "Мила коала сидить за ноутбуком і розгортає контейнер Docker для самостійного розміщення",
"SELF_COPY_CODE": "Копіювати код",
"SELF_GITHUB_PACKAGES": "Переглянути всі теги зображень на пакетах GitHub",
"BOTTOM_TITLE": "Не переконали? Подивіться самі.",
"BOTTOM_SUBTITLE": "KoalaSync є на 100% відкритим кодом, без реклами та відстеження. Перевірте нашу кодову базу на GitHub або безпосередньо встановіть розширення для браузера.",
"BOTTOM_MASCOT_ALT": "Мила коала тримає сторінку репозиторію GitHub поруч із талісманом GitHub Octocat",
"FAQ_TITLE": "Часті запитання",
"FAQ_SUBTITLE": "Все, що вам потрібно знати про спільний перегляд відео в ідеальній синхронізації.",
"FAQ_Q1": "Чи можу я дивитися Netflix, Emby або Jellyfin разом із друзями?",
"FAQ_A1": "так! KoalaSync працює на Netflix, YouTube, Twitch і будь-якому веб-сайті з відеопрогравачем HTML5, включаючи Emby, Jellyfin і медіасервери, розміщені на власному хості. Установіть розширення, створіть кімнату, поділіться посиланням для запрошення, і всі дивитимуться в повній синхронізації.",
"FAQ_Q2": "Чи є KoalaSync безкоштовною альтернативою Teleparty?",
"FAQ_A2": "100% безкоштовне та з відкритим кодом за ліцензією MIT. Без преміум-рівнів, без заблокованих функцій, без реклами — все включено. На відміну від Teleparty, ретрансляційний сервер KoalaSync призначений лише для RAM і не вимагає збору даних або облікових записів користувачів.",
"FAQ_Q3": "Чи працює KoalaSync із Emby, Jellyfin і власними медіа-серверами?",
"FAQ_A3": "так KoalaSync виявляє будь-який стандартний відеоелемент HTML5, що означає, що він ідеально працює з Emby, Jellyfin, Plex та будь-якими спеціальними налаштуваннями потокового передавання. Ви також можете самостійно розмістити сервер ретрансляції KoalaSync через Docker для повного суверенітету даних.",
"FAQ_Q4": "Які браузери та платформи підтримують KoalaSync?",
"FAQ_A4": "Google Chrome, Mozilla Firefox, Microsoft Edge, Opera, Brave та всі браузери на основі Chromium. Встановіть його безпосередньо з веб-магазину Chrome або додатків Firefox — ручне налаштування не потрібне.",
"FAQ_Q5": "Чи всім моїм друзям потрібно встановити розширення?",
"FAQ_A5": "Так, кожному учаснику потрібно встановити KoalaSync — але немає реєстрації чи створення облікового запису. Просто клацніть посилання запрошення, установіть розширення, якщо ви ще цього не зробили, і ви автоматично приєднаєтесь до кімнати за лічені секунди.",
"FAQ_Q6": "Чи мої дані в безпеці? Що робити, якщо я не довіряю офіційному серверу?",
"FAQ_A6": "Повністю безпечний. Сервер ретрансляції KoalaSync призначений лише для RAM — усі дані сеансу назавжди видаляються, коли ви залишаєте кімнату. Ні бази даних, ні реєстрації, ні аналітики, ні облікових записів. А якщо ви віддаєте перевагу повному контролю: ви можете самостійно розмістити свій приватний сервер ретрансляції через Docker за лічені хвилини. Уся кодова база є відкритим кодом і доступна дя перевірки на GitHub.",
"FAQ_Q7": "Чи можу я виправити тихий діалог і гучні вибухи під час перегляду відео в браузері?",
"FAQ_A7": "Так. KoalaSync 2.2.0 містить вбудований аудіокомпресор, який зменшує різницю між тихими та гучними частинами відео. Він працює локально через Web Audio API - аудіодані ніколи не залишають ваш комп'ютер. Відкрийте спливне вікно, створіть кімнату, перейдіть до «Налаштування» → «Обробка аудіо», увімкніть компресор і виберіть пресет. Працює з Netflix, Prime Video, Disney+, YouTube і будь-яким HTML5-відеосайтом.",
"HOWTO_TITLE": "Як синхронізувати відео з друзями за допомогою KoalaSync",
"HOWTO_DESC": "Дивіться будь-яке відео в ідеальній синхронізації з друзями на YouTube, Netflix, Twitch тощо. Без реєстрації, без відстеження.",
"HOWTO_STEP_1_NAME": "Встановіть розширення для браузера",
"HOWTO_STEP_1_TEXT": "Додайте KoalaSync до свого браузера з Chrome Web Store або Firefox Add-ons. Він працює на будь-якому веб-сайті з елементом відео.",
"HOWTO_STEP_2_NAME": "Створіть кімнату",
"HOWTO_STEP_2_TEXT": "Відкрийте спливаюче вікно розширення та натисніть «Створити нову кімнату». Посилання для запрошення автоматично копіюється в буфер обміну.",
"HOWTO_STEP_3_NAME": "Поділіться та синхронізуйте",
"HOWTO_STEP_3_TEXT": "Надішліть запрошення своїм друзям. Виберіть вкладку відео та насолоджуйтеся синхронізованим відтворенням у реальному часі.",
"FOOTER_MIT": "Відкритий код за ліцензією MIT.",
"FOOTER_RAM": "На наших серверах дані не зберігаються. Чисте реле на основі RAM.",
"FOOTER_LEGAL": "Юридична інформація",
"FOOTER_PRIVACY": "Політика конфіденційності",
"MOCK_01": "Активна кімната",
"MOCK_02": "Офіційний сервер",
"MOCK_03": "Посилання на запрошення",
"MOCK_04": "Однолітки в кімнаті",
"MOCK_05": "ВИ",
"MOCK_06": "Граючи",
"MOCK_07": "Одноліток",
"MOCK_08": "Вийти з кімнати",
"MOCK_09": "Виберіть Відео",
"MOCK_10": "Пульт дистанційного керування",
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Грати",
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Пауза",
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> СИНХ",
"MOCK_14": "Перейти до інших",
"MOCK_15": "Статус останньої дії",
"MOCK_16": "Очікування 1 партнера (KoalaPC)...",
"MOCK_17": "Все одно пропустіть і грайте",
"MOCK_18": "Ваше ім'я користувача",
"MOCK_19": "Приховати безладні вкладки",
"MOCK_20": "Автоматична синхронізація наступного епізоду",
"MOCK_21": "Автоматичне копіювання посилання для запрошення",
"MOCK_22": "Сповіщення браузера",
"MOCK_23": "Усунення несправностей",
"MOCK_24": "Регенерувати Peer ID",
"MOCK_25": "Використовуйте це, якщо ви бачите помилку \"Дублікати ідентифікатора\".",
"MOCK_26": "Статус підключення",
"MOCK_27": "Підключено",
"MOCK_28": "Копіювати журнали",
"MOCK_29": "Інформація про налагодження відео",
"MOCK_30": "Повна історія дій",
"MOCK_31": "Журнали (останні 50)",
"MOCK_32": "ЯСНО",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_DISCLAIMER": "KoalaSync не пов’язаний із Netflix, Disney+, Amazon, YouTube, Twitch або будь-якою іншою потоковою платформою, не схвалений і не пов’язаний з ними. Усі торгові марки є власністю відповідних власників.",
"FOOTER_SUPPORT": "Support KoalaSync"
}
+180
View File
@@ -0,0 +1,180 @@
{
"LANG_CODE": "zh",
"HTML_CLASS": "lang-zh",
"CANONICAL_PATH": "zh/",
"LANG_TOGGLE_URL": "../",
"LANG_TOGGLE_TEXT": "EN",
"OG_LOCALE": "zh_CN",
"META_TITLE": "KoalaSync | 与朋友同步 Netflix、YouTube 和任何视频",
"META_DESCRIPTION": "与朋友完美同步观看 Netflix、YouTube、Twitch 和任何 HTML5 视频。适用于 Chrome 和 Firefox 的免费开源浏览器扩展。无需注册。",
"SCHEMA_APP_DESC": "与朋友完美同步观看 Netflix、YouTube、Twitch 和任何 HTML5 视频。适用于 Chrome 和 Firefox 的免费开源浏览器扩展。无需注册。",
"OG_TITLE": "KoalaSync |与朋友同步 Netflix、Emby、Jellyfin 和几乎任何视频",
"OG_DESCRIPTION": "完美同步观看 Netflix、Emby、Jellyfin、YouTube、Twitch 和几乎任何 HTML5 视频。适用于 Chrome 和 Firefox 的隐私第一开源浏览器扩展。",
"TWITTER_TITLE": "KoalaSync |与朋友同步 Netflix、Emby、Jellyfin 和几乎所有视频 浏览器扩展",
"TWITTER_DESCRIPTION": "与朋友完美同步观看 Netflix、Emby、Jellyfin、YouTube、Twitch 和几乎任何 HTML5 视频。适用于 Chrome 和 Firefox 的隐私第一开源浏览器扩展。",
"NAV_FEATURES": "特征",
"NAV_HOW_IT_WORKS": "它是如何运作的",
"HERO_TITLE": "一起观看.<span class='hero-line2'> 完美同步.</span>",
"HERO_SUBTITLE": "您的远程电影之夜没有延迟。无需注册,无需收集数据。只需分享链接即可一起观看。",
"HERO_MASCOT_ALT": "一只可爱的考拉站着,低头看着下载按钮",
"ADD_TO_CHROME": "添加到Chrome",
"ADD_TO_FIREFOX": "添加到Firefox",
"COMPAT_HEADING": "适用于您最喜欢的平台",
"COMPAT_MORE": "还有更多",
"COMPAT_TOOLTIP": "适用于几乎所有具有视频元素的网站",
"USE_CASES_TITLE": "适合任何场合",
"USE_CASES_SUBTITLE": "无论远近,KoalaSync 将人们聚集在他们最喜欢的视频周围。",
"USE_CASE_1_ALT": "两只可爱的考拉坐在一起分享一桶爆米花",
"USE_CASE_1_TITLE": "与朋友的电影之夜",
"USE_CASE_1_DESC": "实时同步您的电影并通过 Discord、Zoom 或您最喜欢的语音通话应用程序进行交谈。感觉就像坐在同一个房间里一样。",
"USE_CASE_2_ALT": "一位考拉教授在屏幕上向两只远程学生考拉进行演示",
"USE_CASE_2_TITLE": "远程学习",
"USE_CASE_2_DESC": "与同学或同事一起分析在线教程、讲座或开发人员培训流。暂停并完美和谐地讨论复杂的步骤。",
"USE_CASE_3_ALT": "一对可爱的考拉情侣一起远程观看;一个坐在电脑前,另一个拿着笔记本电脑躺在床上,两人的心在飞舞",
"USE_CASE_3_TITLE": "远距离关系",
"USE_CASE_3_DESC": "拉近距离,享受约会之夜。体验每一个情节转折,嘲笑同样的笑话,并在同一毫秒内分享情感时刻。",
"WHY_TITLE": "为什么是KoalaSync",
"WHY_SUBTITLE": "专为可靠的同步、隐私优先的设计和轻松的设置而打造。",
"FEATURE_1_TITLE": "完全控制/即时同步",
"FEATURE_1_DESC": "一个人停顿,所有人都停顿。一追寻,众人追随。我们的两阶段同步协议协调所有参与者的实时播放。",
"FEATURE_2_TITLE": "无尽的狂欢",
"FEATURE_2_DESC": "自动同步播放。 KoalaSync 自动检测剧集转换并保持播放,直到所有对等方都成功加载下一个视频。",
"FEATURE_3_TITLE": "零账户/纯粹隐私",
"FEATURE_3_DESC": "没有注册,没有跟踪,没有持久性足迹。服务器完全在临时 RAM 中运行,并在您离开时清理您的房间。",
"FEATURE_4_TITLE": "通用 HTML5 支持",
"FEATURE_4_DESC": "适用于 YouTube、Twitch、Netflix、Jellyfin、Emby 以及几乎任何包含 HTML5 视频元素的标准网页。还与自定义自托管设置兼容。不会将任何视频数据发送到我们的服务器,仅发送同步元数据。",
"FEATURE_5_TITLE": "自托管和 Docker 就绪",
"FEATURE_5_DESC": "我们的官方服务器是免费且快速的,但如果您愿意,您可以完全拥有所有权。通过 Docker 在几秒钟内启动您自己的私有中继服务器。",
"FEATURE_6_TITLE": "即时邀请/一键加入",
"FEATURE_6_DESC": "无需交换 IP 地址或密码。与您的朋友分享生成的邀请链接,让他们只需单击一下即可自动加入您的房间。",
"FEATURE_7_TITLE": "音量调节/音频压缩器",
"FEATURE_7_DESC": "不再需要调节音量旋钮。内置音频压缩器可增强安静的对话并抑制大声的爆炸声。适用于 Netflix、Prime Video、Disney+、YouTube 和任何 HTML5 视频。完全本地化——音频数据不会离开您的计算机。",
"FEATURE_8_TITLE": "开源(MIT 许可证)",
"FEATURE_8_DESC": "整个代码库可在 GitHub 上公开审核。没有黑匣子,没有专有 blob——从中继服务器到浏览器扩展 100% 透明。",
"COMP_TITLE": "KoalaSync 与 Teleparty",
"COMP_SUBTITLE": "了解为什么开源、无广告和隐私优先是一起观看的最佳方式。",
"COMP_COL_FEATURE": "特征",
"COMP_FEAT_1_NAME": "成本/付费专区",
"COMP_FEAT_1_DESC": "高级订阅、隐藏费用或锁定功能。",
"COMP_FEAT_1_KOALA": "100% 免费",
"COMP_FEAT_1_TELE": "付费等级/高级锁",
"COMP_FEAT_2_NAME": "许可证/代码可审核",
"COMP_FEAT_2_DESC": "源代码是否开放并可自由审核。",
"COMP_FEAT_2_KOALA": "开源(MIT",
"COMP_FEAT_2_TELE": "所有权",
"COMP_FEAT_3_NAME": "自托管",
"COMP_FEAT_3_DESC": "能够部署您自己的私有中继服务器。",
"COMP_FEAT_3_KOALA": "是(Docker-就绪)",
"COMP_FEAT_3_TELE": "不",
"COMP_FEAT_4_NAME": "隐私和追踪",
"COMP_FEAT_4_DESC": "注册要求、跟踪 cookie 和分析。",
"COMP_FEAT_4_KOALA": "零持久性(仅限 RAM",
"COMP_FEAT_4_TELE": "谷歌分析和 Cookie",
"COMP_FEAT_5_NAME": "站点兼容性",
"COMP_FEAT_5_DESC": "支持的网站和播放器兼容性。",
"COMP_FEAT_5_KOALA": "几乎任何 HTML5 视频",
"COMP_FEAT_5_TELE": "仅支持的网站",
"COMP_FEAT_6_NAME": "本地化/翻译",
"COMP_FEAT_6_DESC": "可用的界面语言和翻译支持。",
"COMP_FEAT_6_KOALA": "15 种语言(并且还在不断增加)",
"COMP_FEAT_6_TELE": "仅英语",
"COMP_FEAT_7_NAME": "音频压缩器/音量调节",
"COMP_FEAT_7_DESC": "内置音频标准化,以平衡安静的对话和响亮的效果",
"COMP_FEAT_7_KOALA": "有(4 个预设,干/湿控制)",
"COMP_FEAT_7_TELE": "不",
"COMP_FOOTNOTE_1": "比较状态:2026 年 5 月。",
"COMP_FOOTNOTE_2": "官方 Teleparty Premium 定价和支持的网络详细信息:",
"COMP_FOOTNOTE_3": "Teleparty 隐私政策和跟踪数据收集:",
"COMP_FOOTNOTE_4": "适用于允许将脚本注入标准 HTML5 视频标签的网站。具有高度严格的内容安全策略 (CSP)、DRM 复制保护或严重混淆的播放器包装器(如复杂的影子 DOM)的网站可能会限制自动控制或注入。",
"STEPS_TITLE": "入门",
"STEP_1_TITLE": "安装扩展",
"STEP_1_DESC": "从 Chrome Web Store、Firefox 附加组件将 KoalaSync 添加到您的浏览器,或从 GitHub 下载最新的开发人员 ZIP。",
"STEP_1_ILLUS_DESC": "隐私第一的视频同步器",
"STEP_1_ILLUS_DL_CHROME": "下载 Chrome",
"STEP_1_ILLUS_DL_FIREFOX": "下载 Firefox",
"STEP_1_ILLUS_ACTIVE": "分机激活",
"STEP_1_ILLUS_READY": "准备好一起观看吧!",
"STEP_2_TITLE": "创建房间",
"STEP_2_DESC": "打开扩展弹出窗口并单击“+ 创建新房间”。 KoalaSync 自动生成安全的 Room ID 和密码,加入它,并将邀请链接复制到剪贴板。",
"STEP_2_ILLUS_ROOM": "房间",
"STEP_2_ILLUS_SYNC": "同步",
"STEP_2_ILLUS_SETTINGS": "设置",
"STEP_2_ILLUS_CREATE": "+ 创建新房间",
"STEP_2_ILLUS_MANUAL": "手动连接/高级",
"STEP_2_ILLUS_COPIED": "邀请链接已复制!",
"STEP_3_TITLE": "分享与同步",
"STEP_3_DESC": "将邀请链接发送给您的朋友。他们加入后,选择您的视频选项卡并享受同步播放。",
"STEP_3_ILLUS_IN_SYNC": "同步",
"SELF_TITLE": "对于自托管者",
"SELF_SUBTITLE": "不信任我们的服务器?维护完整的数据主权。只需几分钟即可部署您自己的私有中继服务器。",
"SELF_MASCOT_ALT": "一只可爱的考拉坐在笔记本电脑前,部署 Docker 容器进行自托管",
"SELF_COPY_CODE": "复制代码",
"SELF_GITHUB_PACKAGES": "查看 GitHub 套餐上的所有图像标签",
"BOTTOM_TITLE": "不相信?你自己看看吧。",
"BOTTOM_SUBTITLE": "KoalaSync 100%开源、无广告、无跟踪。在 GitHub 上审核我们的代码库或直接安装浏览器扩展。",
"BOTTOM_MASCOT_ALT": "一只可爱的考拉在 GitHub 吉祥物 Octocat 旁边拿着 GitHub 存储库页面",
"FAQ_TITLE": "常见问题解答",
"FAQ_SUBTITLE": "关于完美同步地一起观看视频所需了解的一切。",
"FAQ_Q1": "我可以与朋友同步观看 Netflix、Emby 或 Jellyfin 吗?",
"FAQ_A1": "是的! KoalaSync 适用于 Netflix、YouTube、Twitch 以及任何具有 HTML5 视频播放器的网站 - 包括 Emby、Jellyfin 和自托管媒体服务器。安装扩展程序、创建房间、共享邀请链接,每个人都可以完美同步观看。",
"FAQ_Q2": "KoalaSync 是 Teleparty 的免费替代品吗?",
"FAQ_A2": "在 MIT 许可证下 100% 免费和开源。没有高级套餐、没有锁定功能、没有广告——一切都包含在内。与 Teleparty 不同,KoalaSync 的中继服务器仅是 RAM,需要零数据收集或用户帐户。",
"FAQ_Q3": "KoalaSync 是否可以与 Emby、Jellyfin 和自托管媒体服务器配合使用?",
"FAQ_A3": "是的。 KoalaSync 可检测任何标准 HTML5 视频元素,这意味着它可以与 Emby、Jellyfin、Plex 和任何自定义自托管流设置完美配合。您还可以通过 Docker 自托管 KoalaSync 中继服务器,以实现完整的数据主权。",
"FAQ_Q4": "哪些浏览器和平台支持KoalaSync",
"FAQ_A4": "Google Chrome、Mozilla Firefox、Microsoft Edge、Opera、Brave 以及所有基于 Chromium 的浏览器。直接从 Chrome Web Store 或 Firefox 附加组件安装 - 无需手动配置。",
"FAQ_Q5": "我所有的朋友都需要安装该扩展吗?",
"FAQ_A5": "是的,每个参与者都需要安装 KoalaSync - 但无需注册或创建帐户。只需单击邀请链接,安装扩展程序(如果尚未安装),您将在几秒钟内自动加入房间。",
"FAQ_Q6": "我的数据安全吗?如果我不信任官方服务器怎么办?",
"FAQ_A6": "完全安全。 KoalaSync 的中继服务器仅限 RAM — 所有会话数据在您离开房间时都会永久删除。没有数据库,没有日志记录,没有分析,没有帐户。如果您喜欢完全控制:您可以在几分钟内通过 Docker 自行托管您自己的私人中继服务器。整个代码库是开源的,并且可以在 GitHub 上进行审核。",
"FAQ_Q7": "我可以修复在浏览器中观看视频时安静的对话和大声的爆炸声吗?",
"FAQ_A7": "是的。 KoalaSync 2.2.0 包含一个内置音频压缩器,可减少视频安静部分和大声部分之间的差距。它通过 Web Audio API 在本地运行 — 任何音频数据都不会离开您的计算机。打开弹出窗口,创建一个房间,转到“设置”→“音频处理”,打开压缩器,然后选择一个预设。适用于 Netflix、Prime Video、Disney+、YouTube 和任何 HTML5 视频网站。",
"HOWTO_TITLE": "如何使用KoalaSync与好友同步视频",
"HOWTO_DESC": "与 YouTube、Netflix、Twitch 等平台上的朋友完美同步观看任何视频。无需注册,无需跟踪。",
"HOWTO_STEP_1_NAME": "安装浏览器扩展",
"HOWTO_STEP_1_TEXT": "从 Chrome Web Store 或 Firefox 附加组件将 KoalaSync 添加到您的浏览器。它适用于任何带有视频元素的网站。",
"HOWTO_STEP_2_NAME": "创建一个房间",
"HOWTO_STEP_2_TEXT": "打开扩展弹出窗口并单击“创建新房间”。邀请链接会自动复制到您的剪贴板。",
"HOWTO_STEP_3_NAME": "分享和同步",
"HOWTO_STEP_3_TEXT": "将邀请链接发送给您的朋友。选择一个视频选项卡并实时享受同步播放。",
"FOOTER_MIT": "根据 MIT 许可证开源。",
"FOOTER_RAM": "我们的服务器上不存储任何数据。纯RAM型继电器。",
"FOOTER_LEGAL": "法律声明",
"FOOTER_PRIVACY": "隐私政策",
"MOCK_01": "活动室",
"MOCK_02": "官方服务器",
"MOCK_03": "邀请链接",
"MOCK_04": "房间里的同伴",
"MOCK_05": "你",
"MOCK_06": "演奏",
"MOCK_07": "同行",
"MOCK_08": "留出空间",
"MOCK_09": "选择视频",
"MOCK_10": "遥控",
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> 玩",
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> 暂停",
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> 同步",
"MOCK_14": "跳转到其他",
"MOCK_15": "上次活动状态",
"MOCK_16": "正在等待 1 个同伴 (KoalaPC)...",
"MOCK_17": "仍然跳过并播放",
"MOCK_18": "您的用户名",
"MOCK_19": "隐藏杂乱标签",
"MOCK_20": "自动同步下一集",
"MOCK_21": "自动复制邀请链接",
"MOCK_22": "浏览器通知",
"MOCK_23": "故障排除",
"MOCK_24": "再生Peer ID",
"MOCK_25": "如果您看到“重复身份”错误,请使用此选项。",
"MOCK_26": "连接状态",
"MOCK_27": "已连接",
"MOCK_28": "复制日志",
"MOCK_29": "视频调试信息",
"MOCK_30": "完整的行动历史",
"MOCK_31": "日志(最后 50 条)",
"MOCK_32": "清除",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_DISCLAIMER": "KoalaSync 不隶属于 Netflix、Disney+、Amazon、YouTube、Twitch 或任何其他流媒体平台,也不受其认可或关联。所有商标均为其各自所有者的财产。",
"FOOTER_SUPPORT": "Support KoalaSync"
}
+5
View File
@@ -7,6 +7,11 @@
<link rel="preload" href="style.min.css" as="style">
<link rel="stylesheet" href="style.min.css">
<link rel="icon" type="image/webp" href="assets/NewLogoIcon_64.webp">
<link rel="canonical" href="https://sync.koalastuff.net/privacy">
<link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/privacy">
<link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/datenschutz">
<link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/privacy">
<meta name="description" content="Privacy Policy of KoalaSync - We take data security and transparency seriously. Learn about our zero-tracking, zero-log, and zero-cookie approach.">
<meta name="robots" content="index, follow">
<script type="application/ld+json">
+17 -17
View File
@@ -3,31 +3,31 @@
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://sync.koalastuff.net/imprint</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://sync.koalastuff.net/privacy</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://sync.koalastuff.net/de/impressum</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://sync.koalastuff.net/de/datenschutz</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://sync.koalastuff.net/</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -47,7 +47,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/de/</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -67,7 +67,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/fr/</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -87,7 +87,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/es/</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -107,7 +107,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/pt-BR/</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -127,7 +127,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/ru/</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -147,7 +147,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/it/</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -167,7 +167,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/pl/</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -187,7 +187,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/tr/</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -207,7 +207,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/nl/</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -227,7 +227,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/ja/</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -247,7 +247,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/ko/</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -267,7 +267,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/pt/</loc>
<lastmod>2026-06-19</lastmod>
<lastmod>2026-06-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
+13 -5
View File
@@ -25,6 +25,8 @@
<link rel="alternate" hreflang="nl" href="https://sync.koalastuff.net/nl/">
<link rel="alternate" hreflang="ja" href="https://sync.koalastuff.net/ja/">
<link rel="alternate" hreflang="ko" href="https://sync.koalastuff.net/ko/">
<link rel="alternate" hreflang="zh" href="https://sync.koalastuff.net/zh/">
<link rel="alternate" hreflang="uk" href="https://sync.koalastuff.net/uk/">
<link rel="alternate" hreflang="pt" href="https://sync.koalastuff.net/pt/">
<link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/">
<meta name="robots" content="index, follow">
@@ -48,6 +50,8 @@
<meta property="og:locale:alternate" content="nl_NL">
<meta property="og:locale:alternate" content="ja_JP">
<meta property="og:locale:alternate" content="ko_KR">
<meta property="og:locale:alternate" content="zh_CN">
<meta property="og:locale:alternate" content="uk_UA">
<meta property="og:locale:alternate" content="pt_PT">
<meta property="og:site_name" content="KoalaSync">
@@ -85,7 +89,7 @@
"@type": "WebSite",
"name": "KoalaSync",
"url": "https://sync.koalastuff.net/",
"inLanguage": ["en", "de", "fr", "es", "pt-BR", "ru", "it", "pl", "tr", "nl", "ja", "ko", "pt"]
"inLanguage": ["en", "de", "fr", "es", "it", "nl", "pl", "pt", "pt-BR", "tr", "ru", "ja", "ko", "zh", "uk"]
}
</script>
<script type="application/ld+json" id="schema-software">
@@ -101,8 +105,8 @@
"price": "0",
"priceCurrency": "EUR"
},
"description": "Watch Netflix, YouTube, Twitch, Jellyfin, Emby and almost any HTML5 video in perfect sync with friends. Open source, privacy-first, free.",
"softwareVersion": "2.4.1",
"description": "{{SCHEMA_APP_DESC}}",
"softwareVersion": "2.4.4",
"license": "https://opensource.org/licenses/MIT",
"sameAs": "https://github.com/Shik3i/KoalaSync",
"image": "https://sync.koalastuff.net/assets/NewLogoIcon.webp",
@@ -231,6 +235,8 @@
<a href="https://sync.koalastuff.net/nl/" hreflang="nl">Nederlands</a>
<a href="https://sync.koalastuff.net/ja/" hreflang="ja">日本語</a>
<a href="https://sync.koalastuff.net/ko/" hreflang="ko">한국어</a>
<a href="https://sync.koalastuff.net/zh/" hreflang="zh">中文</a>
<a href="https://sync.koalastuff.net/uk/" hreflang="uk">Українська</a>
<a href="https://sync.koalastuff.net/pt/" hreflang="pt">Português (Portugal)</a>
</nav>
</div>
@@ -249,6 +255,8 @@
<option value="nl" {{SELECTED_NL}}>🇳🇱 Nederlands</option>
<option value="ja" {{SELECTED_JA}}>🇯🇵 日本語</option>
<option value="ko" {{SELECTED_KO}}>🇰🇷 한국어</option>
<option value="zh" {{SELECTED_ZH}}>🇨🇳 中文</option>
<option value="uk" {{SELECTED_UK}}>🇺🇦 Українська</option>
<option value="pt" {{SELECTED_PT}}>🇵🇹 Português (Portugal)</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="chevron-icon"><polyline points="6 9 12 15 18 9"/></svg>
@@ -1257,9 +1265,9 @@
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="{{ASSET_PATH}}{{FOOTER_LEGAL_LINK}}" style="color: var(--text-muted); text-decoration: none;"><span>{{FOOTER_LEGAL}}</span></a>
<a href="{{ASSET_PATH}}{{FOOTER_PRIVACY_LINK}}" style="color: var(--text-muted); text-decoration: none;"><span>{{FOOTER_PRIVACY}}</span></a>
<a href="https://ko-fi.com/koaladev" target="_blank" rel="noopener" title="{{FOOTER_SUPPORT}}" style="color: #fff; text-decoration: none; display: inline-flex; align-items: center; gap: 6px; background: #ff5e5b; border: 1px solid rgba(255,255,255,0.18); border-radius: 999px; padding: 3px 9px; font-size: 0.72rem; font-weight: 700; line-height: 1;">
<a href="https://support.koalastuff.net" target="_blank" rel="noopener" title="{{FOOTER_SUPPORT}}" style="color: #fff; text-decoration: none; display: inline-flex; align-items: center; gap: 6px; background: #ff5e5b; border: 1px solid rgba(255,255,255,0.18); border-radius: 999px; padding: 3px 9px; font-size: 0.72rem; font-weight: 700; line-height: 1;">
<span aria-hidden="true"></span>
<span>Ko-Fi</span>
<span>{{FOOTER_SUPPORT}}</span>
</a>
<a href="https://mastodon.social/@koalastuff" rel="me noopener" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
+2 -2
View File
@@ -1,4 +1,4 @@
{
"version": "2.4.1",
"date": "2026-06-19T08:50:27Z"
"version": "2.4.4",
"date": "2026-06-22T22:46:03Z"
}