Compare commits
478 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 869c64171b | |||
| 6ccaf45f8e | |||
| 1b7be23b29 | |||
| 9ec0e9dd4e | |||
| 9ac8c28442 | |||
| 076157fef1 | |||
| 236da46f5d | |||
| 2fbcbc4f83 | |||
| 8fe1c6dfbc | |||
| 51da169d69 | |||
| a9f5c0b9bf | |||
| 705fbc146b | |||
| 09d35f3e48 | |||
| 32994aaaa5 | |||
| bea0f6925c | |||
| 71fa6e7315 | |||
| 54b806d270 | |||
| 066d2c9407 | |||
| ce522e2ab7 | |||
| 9cbeb661d6 | |||
| beda924b65 | |||
| 01762b0c2f | |||
| 0af22998c8 | |||
| 52265e84eb | |||
| 57cd071d58 | |||
| 8a21fbe07f | |||
| 7781d418ba | |||
| c022f6f490 | |||
| 9fe83f89b2 | |||
| e51e8ae436 | |||
| e74abd00f7 | |||
| dab7368537 | |||
| 463f871958 | |||
| 1f5196e1e6 | |||
| 003741a63f | |||
| e644ab33cd | |||
| 9006ebbc67 | |||
| e40017f053 | |||
| fa7eea54d8 | |||
| 04d2360814 | |||
| 29aba936ce | |||
| 4bae88e107 | |||
| 3e4b97b11a | |||
| e555906553 | |||
| 4f1335242c | |||
| e683466bf0 | |||
| 788a3331d3 | |||
| 2d9b7bc099 | |||
| 717d37c693 | |||
| 1f48e9df47 | |||
| adab684ed0 | |||
| e23f9ab226 | |||
| 77203fe8fd | |||
| 5379778a20 | |||
| 9fc58dca4b | |||
| 17ac5c0a6f | |||
| 1ec2396c41 | |||
| 7c65fe80cb | |||
| a326087ca5 | |||
| de518ac205 | |||
| ce3cf0ef89 | |||
| 9626948f29 | |||
| dfa0a3d03e | |||
| 02c41776b8 | |||
| b32e2fa8cc | |||
| 9963da2ebc | |||
| 9feafab617 | |||
| d26b95985d | |||
| bb030a9737 | |||
| 95ff460856 | |||
| 96e2207cf3 | |||
| 2928393b4c | |||
| 5863991bfb | |||
| 1eca3fa5bb | |||
| 58af258fd3 | |||
| 69f8f4cfd7 | |||
| af9cf34f0b | |||
| b1ea280f0b | |||
| 9006570bd1 | |||
| f9dc9d4017 | |||
| 7b37d783b5 | |||
| 9f4be58172 | |||
| a968f708b3 | |||
| 2995afe970 | |||
| 68027a21d5 | |||
| 4aa6f6d7a6 | |||
| 7602bfdb21 | |||
| 3ccb967a45 | |||
| fe970de39a | |||
| e795339924 | |||
| 06b5ffbd0b | |||
| 24cac52674 | |||
| 6276ee134c | |||
| d847389eef | |||
| aa84b63c77 | |||
| bd60a14754 | |||
| a1bdcf4325 | |||
| b846803062 | |||
| 4bc7ad365d | |||
| 68b2205b0d | |||
| b450845522 | |||
| 27023ea58e | |||
| 1a7d23ce93 | |||
| d2c8ca1c1e | |||
| 1c404a7f11 | |||
| e2bba04efd | |||
| c7b29c277c | |||
| e06965b95c | |||
| 23d43741ce | |||
| 027d2dbde5 | |||
| a0c16df664 | |||
| 8fd866f20a | |||
| 8f72c238a2 | |||
| 24ad7c3732 | |||
| 32fd677a31 | |||
| fb22700268 | |||
| 59b675ce17 | |||
| 8a9293280b | |||
| 450f715874 | |||
| b4c00f36a0 | |||
| 36f494fa55 | |||
| b5b3885d19 | |||
| 373f2d127a | |||
| 3ebe80aab2 | |||
| e7b59d61ac | |||
| 71f7ac425a | |||
| 394b9ba474 | |||
| 526736b045 | |||
| 86112057b4 | |||
| c56fcfd281 | |||
| 5c43fc6236 | |||
| 101761e984 | |||
| 3bc68a5713 | |||
| 6c96dd6344 | |||
| d7bb8dc97c | |||
| d38a840114 | |||
| e7d2d76ba3 | |||
| 5277cd0f21 | |||
| 4d7cbfe347 | |||
| 91aa76e842 | |||
| 5f0a189451 | |||
| 312d4f2cf7 | |||
| 884feb982d | |||
| b2da17ab62 | |||
| b518685e2c | |||
| b7a7a14a35 | |||
| 9ee22d985f | |||
| 2070e701de | |||
| dd8eefe3f9 | |||
| cc97e0d371 | |||
| 7571f1986d | |||
| bb1a88c085 | |||
| 07863bdad6 | |||
| 5e6306432f | |||
| fa5dfc1cf1 | |||
| b88b8bbe47 | |||
| 8aab8ce2f5 | |||
| 95b4608236 | |||
| 6d42ee7d4c | |||
| 3f8cf33dc9 | |||
| f12bb0a532 | |||
| 1685b6a327 | |||
| ec8f56a85d | |||
| 38dc923a7c | |||
| 4ca1ef22d2 | |||
| 27e57862c0 | |||
| c391068706 | |||
| d76e9195c4 | |||
| 6652a06840 | |||
| b1a89cff41 | |||
| e4a77a3ef4 | |||
| 5adcce2074 | |||
| a1398ed0e4 | |||
| ed80856803 | |||
| 102031e0d2 | |||
| 503c7d6dc4 | |||
| 77793c8c6e | |||
| 4fbf309e5a | |||
| aa61a24351 | |||
| 2ee5c83ee6 | |||
| ca1cfdb382 | |||
| ef7b1f2e5f | |||
| 10fdaa23fc | |||
| 6ba5e1b10b | |||
| 6e234fb8fd | |||
| 9f553b4f8f | |||
| 131afadc1d | |||
| c62da66b06 | |||
| 34f0c2b265 | |||
| af59b4c64c | |||
| af8184420c | |||
| 45e1e3defe | |||
| e67b0c564d | |||
| f54a38b656 | |||
| 5216fde641 | |||
| 39fe2cbd11 | |||
| 03d5dbda66 | |||
| 0c0cbbc090 | |||
| 0f4ad39fb4 | |||
| cfbb070c57 | |||
| 0caf19cf18 | |||
| ff31f56911 | |||
| 54d03df0af | |||
| 4f1016fa87 | |||
| 3e7afd7592 | |||
| 6c6d4dc2a2 | |||
| 498d5bd91b | |||
| bfd1177a14 | |||
| 15f4f5a12e | |||
| fa39be7d40 | |||
| 5fd00f0bda | |||
| dca9c6ed07 | |||
| d8634744ea | |||
| 2031f76bee | |||
| 3757308117 | |||
| e4fae56509 | |||
| 766bc1760c | |||
| 4949b04d07 | |||
| 6ba81d66a4 | |||
| d5ed1083bc | |||
| 78d576c7f6 | |||
| 08ce689f4c | |||
| bf48d1889b | |||
| 39788e5bd9 | |||
| c134e1bfae | |||
| cad4b4a6db | |||
| 9e2abadf5a | |||
| f5db39b77b | |||
| 85d05b85c9 | |||
| 6b438f2d37 | |||
| 7bba36c505 | |||
| f93937f5f3 | |||
| da12dc07a7 | |||
| 198064b720 | |||
| def0ad6ded | |||
| 026bbc65b2 | |||
| 854be35474 | |||
| 3506eb0331 | |||
| 0a34d804fb | |||
| a1e882cfa7 | |||
| c88f6fb121 | |||
| fe047dea2e | |||
| 464f5f466a | |||
| 9f00645f58 | |||
| f95095f2cd | |||
| be91d07c56 | |||
| a6bc7cae48 | |||
| d0c4b3740d | |||
| 67a7f6e663 | |||
| 7499eafe4e | |||
| 3fe8ca18e4 | |||
| f0ccc0c082 | |||
| d59fc4777d | |||
| 3ad2459558 | |||
| 839f4d0761 | |||
| 8838bf20b7 | |||
| 9d849a2996 | |||
| 6d5661ea45 | |||
| 886550408a | |||
| 28694c22bb | |||
| a2a56f2b17 | |||
| 742876e415 | |||
| bcd956a8db | |||
| 1e329c0c52 | |||
| a7481d42a2 | |||
| f9577aace9 | |||
| ad33720053 | |||
| fa3341cc8e | |||
| b526e9287b | |||
| a6be6b2670 | |||
| b51e66d824 | |||
| e034882975 | |||
| e53a93829e | |||
| e3dbf2b8a2 | |||
| 595ea297f5 | |||
| a948780745 | |||
| 8c899a6469 | |||
| 4f4cdcd8c0 | |||
| 602be3a724 | |||
| bb7bd21102 | |||
| 543bfe074d | |||
| 81c50eff16 | |||
| bf0fb5741f | |||
| 6d2355f404 | |||
| 732d585273 | |||
| 666808f876 | |||
| 3ec9812265 | |||
| 64f3c5eefd | |||
| 3fe074b308 | |||
| a06cca8b9d | |||
| c4fc3ab53b | |||
| 3f3fea58ed | |||
| 014169f84e | |||
| 9c61abee03 | |||
| 2f11c60307 | |||
| 27301f8746 | |||
| 030b839b12 | |||
| 5c805bcafa | |||
| eb83d9148e | |||
| 290360387f | |||
| c56e404c13 | |||
| e519ea2302 | |||
| 5a75bae8d0 | |||
| fc3f4f38ff | |||
| f69da7542e | |||
| aba05060b9 | |||
| 0b986e6e13 | |||
| b23ce8ee7d | |||
| 3195bd0089 | |||
| 1c1778d265 | |||
| 0262d9c1ad | |||
| f23c329709 | |||
| 78a165d368 | |||
| 63b5ef0ffd | |||
| a26b07cb5c | |||
| a445759dd7 | |||
| 1e883d8ee4 | |||
| be574e0e25 | |||
| ebf3178e32 | |||
| ba96cf2765 | |||
| d026ed891a | |||
| d6665fe0f5 | |||
| 79025f4d18 | |||
| a1f921407c | |||
| f2669ed769 | |||
| 7a6ec8087e | |||
| 8cc622bda0 | |||
| 2b5da0dbb7 | |||
| 1a7ff6b3a7 | |||
| b1b858d771 | |||
| 608f742f83 | |||
| b2ff24e155 | |||
| 9285e1041e | |||
| 3ac110f8f5 | |||
| cc8c58fac5 | |||
| 53768980d0 | |||
| 1b04db009a | |||
| a2720da5f8 | |||
| d2c380d6fc | |||
| 1a36b138c0 | |||
| 47ead27344 | |||
| 72180ba817 | |||
| 9eab699e2a | |||
| c95e72b713 | |||
| 14be38fe11 | |||
| 1bb2123da2 | |||
| e29c6666b6 | |||
| b08e8ba06b | |||
| 30a4057c99 | |||
| fd47eb82b9 | |||
| 3fcafbd081 | |||
| d430501b82 | |||
| d738352c90 | |||
| dbf1b3e81b | |||
| 5a3f1c7019 | |||
| 57b0dd1632 | |||
| b6f7c1ccdb | |||
| 4bada9533a | |||
| df32385fa6 | |||
| ce9778245d | |||
| 5d6c0cd1fb | |||
| bbba50f643 | |||
| 56293209f8 | |||
| c55691a535 | |||
| ce3b17f55f | |||
| 5e8e9f61c2 | |||
| 59eb1a6092 | |||
| 00c9ff8cfc | |||
| 748ccaa835 | |||
| 25b23d083d | |||
| 74042c3c78 | |||
| fb1451022b | |||
| 54ffbfa4bd | |||
| 0db0b5eea2 | |||
| d58041bd4e | |||
| 1f1b99a1fe | |||
| c608d2d9e9 | |||
| 06203eb5cb | |||
| ed599a96ab | |||
| 5d1707a7d7 | |||
| f4343ca6b1 | |||
| 3644a7b8ac | |||
| c621c851af | |||
| 1113fb8bb5 | |||
| c852826091 | |||
| cfef79ec1d | |||
| 8fbada8b03 | |||
| 2261a8133a | |||
| d5241b19e8 | |||
| f7096edd30 | |||
| e2b76b05a1 | |||
| 93acd0b44c | |||
| ce7b5c47f2 | |||
| 92fb8e2d00 | |||
| 43cde9bbef | |||
| 56955027f9 | |||
| 5ef059a94f | |||
| 98b4fc5fb4 | |||
| 762d6425be | |||
| 1fba2fb69c | |||
| eca259281a | |||
| afd28be2e6 | |||
| cc0265c836 | |||
| 6ebff9ab4c | |||
| 35e779c1ff | |||
| 09f0e04891 | |||
| 9eff53ba46 | |||
| bde2f7ea55 | |||
| 901861269a | |||
| ba91e2744c | |||
| b7a44024ab | |||
| 3c49bfe54c | |||
| d2ea7c7423 | |||
| db11812bd6 | |||
| 6db8fdbf75 | |||
| d23c37f87f | |||
| acd428d4f7 | |||
| c0a6f0adc2 | |||
| 42b73bb97f | |||
| dc36bfdded | |||
| 06db850387 | |||
| 0de92b5b61 | |||
| 8ff8e7beb6 | |||
| 8317099072 | |||
| 04c63dcf68 | |||
| fd23ccc23a | |||
| 4d5caeda9e | |||
| f1f41e5cac | |||
| 473eacda22 | |||
| 42029f86bf | |||
| 4c4a2638d7 | |||
| 5b57970c4c | |||
| 61492f953b | |||
| da6a1cc643 | |||
| 62fdffa5ee | |||
| fb13978c9d | |||
| 82c09a5328 | |||
| 2fbeafeb3f | |||
| 80f8c821cb | |||
| 7d3965a9fd | |||
| 1aca6c37d4 | |||
| aa740592dd | |||
| 6f8bcf8478 | |||
| 807a620fe9 | |||
| 6e138f51d3 | |||
| ed50e354ab | |||
| 48bd503b5c | |||
| 35351bdacd | |||
| a0063d42b1 | |||
| 8b3f9e1242 | |||
| eb5515fc1b | |||
| c621685aae | |||
| b98cfc9ca1 | |||
| 6f08a9d7c4 | |||
| 440ed2db47 | |||
| ed24a4c263 | |||
| 284b82a910 | |||
| f59d30569e | |||
| f23c7eb3c8 | |||
| 9a4dd41555 | |||
| 2067f76ced | |||
| 3b65af1bbb | |||
| bb316340a7 | |||
| a3af397fe9 | |||
| 0ac2b49d89 | |||
| c9f93dc4ba | |||
| 4909a86a13 | |||
| d66c68be5d | |||
| 23d4b7068e | |||
| f40d4e8de4 | |||
| e996275c2a | |||
| dd37045afc | |||
| 92bec29215 | |||
| 552afac26a | |||
| 3c671bcfab | |||
| 18ed8953db | |||
| 967fe8872e | |||
| 0e93e31bd0 |
@@ -0,0 +1,2 @@
|
||||
# These are supported funding model platforms
|
||||
ko_fi: koaladev
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: 🐛 Bug Report
|
||||
about: Create a report to help us improve KoalaSync
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
> **⚠️ Required:** Before submitting, open the KoalaSync **Status** tab in the extension popup and click **"Copy Logs"**. Paste the full output below — it contains essential system info, connection state, and debug data needed to diagnose your issue.
|
||||
|
||||
<details>
|
||||
<summary><b>📋 Copy Logs Output</b></summary>
|
||||
|
||||
<!-- Paste the copied logs here -->
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Describe the Bug
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
### To Reproduce
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
### Expected Behavior
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
### Actual Behavior
|
||||
|
||||
A clear and concise description of what actually happened.
|
||||
|
||||
### Screenshots / Screen Recordings
|
||||
|
||||
If applicable, add screenshots or recordings to help explain your problem.
|
||||
|
||||
### Environment
|
||||
|
||||
- **Browser:** (e.g. Chrome 125, Firefox 128)
|
||||
- **Extension Version:** (visible at the bottom of the Settings tab)
|
||||
- **OS:** (e.g. Windows 11, macOS 14.5)
|
||||
- **Self-Hosted Server?:** (yes / no — if yes, provide server version)
|
||||
- **Website/Platform:** (e.g. YouTube, Netflix, Twitch, Jellyfin, Emby)
|
||||
|
||||
### Additional Context
|
||||
|
||||
Add any other context about the problem here (e.g. network setup, VPN usage, multiple monitors, etc.).
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: 🚀 Feature Request
|
||||
about: Suggest a new feature or enhancement for KoalaSync
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Problem Description
|
||||
|
||||
A clear and concise description of the problem or limitation you're encountering. What's missing or what could be improved?
|
||||
|
||||
*As a user, I want to ... so that ...*
|
||||
|
||||
### Proposed Solution
|
||||
|
||||
Describe the feature you'd like to see. How should it work? Be as specific as possible.
|
||||
|
||||
### Use Cases & Benefits
|
||||
|
||||
- **When would you use this?** (e.g., specific websites, workflows, setups)
|
||||
- **What value does it add?** (e.g., saves time, enables new functionality, improves UX)
|
||||
|
||||
### Alternatives Considered
|
||||
|
||||
What workarounds or alternative approaches have you tried? Are there other ways to achieve a similar result?
|
||||
|
||||
### Additional Context
|
||||
|
||||
Add any other context, sketches, mockups, or references (e.g., links to similar features in other projects).
|
||||
@@ -0,0 +1,45 @@
|
||||
<!--
|
||||
Thanks for contributing to KoalaSync!
|
||||
|
||||
Please read the CONTRIBUTING.md and CODE_OF_CONDUCT.md before submitting.
|
||||
By submitting this PR, you agree to abide by our code of conduct.
|
||||
|
||||
Use conventional commits for the title: feat:, fix:, docs:, refactor:, chore:
|
||||
-->
|
||||
|
||||
### Description
|
||||
|
||||
<!-- What does this PR do and why? Link to relevant issue or motivation. -->
|
||||
|
||||
Closes #
|
||||
|
||||
### Type of Change
|
||||
|
||||
- [ ] Bug fix (non-breaking change that fixes an issue)
|
||||
- [ ] New feature (non-breaking change that adds functionality)
|
||||
- [ ] Breaking change (fix or feature that alters existing behavior)
|
||||
- [ ] Refactoring (no functional changes)
|
||||
- [ ] Documentation update
|
||||
- [ ] Build, dependencies, or CI
|
||||
|
||||
### How Has This Been Tested?
|
||||
|
||||
<!-- Describe the tests you ran and the environments (browsers, OS, etc.) -->
|
||||
|
||||
- [ ] Tested on Chrome
|
||||
- [ ] Tested on Firefox
|
||||
- [ ] `npm run lint` passes with zero errors and zero warnings
|
||||
- [ ] `node -c` passes on all modified `.js` files
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] My code follows the project's style guidelines
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have added/updated tests if needed
|
||||
- [ ] I have updated documentation if needed (`docs/`, README, etc.)
|
||||
- [ ] Protocol changes: I ran `node scripts/build-extension.cjs` and updated relevant docs
|
||||
- [ ] No new warnings, secrets, or hardcoded credentials introduced
|
||||
|
||||
### Additional Context
|
||||
|
||||
<!-- Screenshots, migration notes, performance data, etc. -->
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Beta Server Image
|
||||
|
||||
# Publishes the relay server as a Docker image under NON-production tags so a
|
||||
# feature branch can be deployed to a staging/backup server and used as a custom
|
||||
# server, without ever touching the ':latest' tag the official relay tracks.
|
||||
#
|
||||
# Tags produced (on ghcr.io/<owner>/<repo>):
|
||||
# - beta moving channel pointer to the newest build
|
||||
# - <branch-slug> e.g. feature-host-control-mode
|
||||
# - sha-<short-commit> immutable, pin to an exact build
|
||||
# - <custom> only on manual run, e.g. hostcontrol01
|
||||
# Never ':latest' (flavor: latest=false).
|
||||
#
|
||||
# Remove this workflow once the feature is merged & released the normal way.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- feature/host-control-mode
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Extra immutable tag for this build (e.g. hostcontrol01). Optional.'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
concurrency:
|
||||
group: beta-image-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-beta-image:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
# Never publish ':latest' from a beta build.
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=raw,value=beta
|
||||
type=ref,event=branch
|
||||
type=sha,prefix=sha-
|
||||
type=raw,value=${{ github.event.inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '' }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: server/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -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
|
||||
@@ -5,21 +5,29 @@ on:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
# A release run must never be interrupted (it commits back to main and publishes
|
||||
# artifacts). Only dedupe accidental re-pushes of the same tag.
|
||||
concurrency:
|
||||
group: release-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release-server:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -27,7 +35,7 @@ jobs:
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
@@ -35,7 +43,8 @@ jobs:
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: server/Dockerfile
|
||||
@@ -43,14 +52,32 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# Reuse layers across releases to speed up the multi-arch build.
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest@v4
|
||||
with:
|
||||
subject-name: ghcr.io/${{ github.repository }}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
release-extension:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Extract version from tag
|
||||
id: version
|
||||
@@ -67,7 +94,7 @@ jobs:
|
||||
echo " ✓ manifest.base.json -> $VERSION"
|
||||
|
||||
# 2. shared/constants.js — APP_VERSION
|
||||
sed -i "s/export const APP_VERSION = '.*'/export const APP_VERSION = '$VERSION'/" shared/constants.js
|
||||
sed -i "s/export const APP_VERSION = [\"'].*[\"']/export const APP_VERSION = \"$VERSION\"/" shared/constants.js
|
||||
echo " ✓ shared/constants.js -> $VERSION"
|
||||
|
||||
# 3. package.json
|
||||
@@ -78,13 +105,26 @@ jobs:
|
||||
jq -n --arg v "$VERSION" --arg d "$DATE" '{version: $v, date: $d}' > website/version.json
|
||||
echo " ✓ website/version.json -> version $VERSION, date $DATE"
|
||||
|
||||
# 5. website/template.html — SoftwareApplication schema
|
||||
sed -i "s/\"softwareVersion\": \".*\"/\"softwareVersion\": \"$VERSION\"/" website/template.html
|
||||
echo " ✓ website/template.html -> softwareVersion $VERSION"
|
||||
|
||||
# 6. README.md — version badge & banner
|
||||
sed -i "s|Release-v[0-9]\+\.[0-9]\+\.[0-9]\+-blue|Release-v$VERSION-blue|g" README.md
|
||||
sed -i "s/New v[0-9]\+\.[0-9]\+\.[0-9]\+ Release/New v$VERSION Release/g" README.md
|
||||
echo " ✓ README.md -> v$VERSION"
|
||||
|
||||
# 7. website/sitemap.xml — lastmod dates
|
||||
sed -i "s/<lastmod>[0-9-]*<\/lastmod>/<lastmod>$(date +%Y-%m-%d)<\/lastmod>/g" website/sitemap.xml
|
||||
echo " ✓ website/sitemap.xml -> lastmod $(date +%Y-%m-%d)"
|
||||
|
||||
echo "Version injection complete."
|
||||
|
||||
- name: Commit and push version updates back to main
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add extension/manifest.base.json shared/constants.js package.json website/version.json
|
||||
git add extension/manifest.base.json shared/constants.js package.json website/version.json website/template.html README.md website/sitemap.xml
|
||||
git commit -m "chore(release): update versions to $GITHUB_REF_NAME [skip ci]" || echo "No changes to commit"
|
||||
git push origin HEAD:main
|
||||
env:
|
||||
@@ -92,11 +132,26 @@ jobs:
|
||||
|
||||
- name: Build Extensions
|
||||
run: |
|
||||
npm install
|
||||
npm ci
|
||||
npm run build:extension
|
||||
|
||||
- name: Generate artifact attestation for extensions
|
||||
uses: actions/attest@v4
|
||||
with:
|
||||
subject-path: dist/koalasync-*.zip
|
||||
|
||||
- name: Build Website
|
||||
run: node website/build.cjs
|
||||
|
||||
- name: Upload Website Artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: website-www
|
||||
path: website/www/
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
files: |
|
||||
dist/koalasync-chrome.zip
|
||||
|
||||
@@ -27,6 +27,7 @@ Thumbs.db
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
.claude/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
@@ -40,5 +41,8 @@ coverage/
|
||||
# the root 'shared/' remains the Single Source of Truth.
|
||||
extension/shared/
|
||||
|
||||
# Auto-generated website build output
|
||||
website/www/
|
||||
|
||||
# Temporary scratch files
|
||||
scratch/
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the project maintainer at **koaladev@koalamail.rocks**.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All project maintainers are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Project maintainers will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from project maintainers, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
@@ -1,58 +1,168 @@
|
||||
# Contributing to KoalaSync
|
||||
|
||||
Thank you for your interest in contributing to KoalaSync! We welcome all contributions, from bug reports to new features.
|
||||
Thanks for your interest in improving KoalaSync. All contributions are welcome — from bug reports and translations to core protocol changes.
|
||||
|
||||
## Development Workflow
|
||||
Please note that by participating in this project, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
### 1. Prerequisites
|
||||
- Node.js (v18+)
|
||||
- Docker (for local server testing)
|
||||
---
|
||||
|
||||
### 2. Setup
|
||||
1. Clone the repository.
|
||||
2. Run `npm install` in the root directory to install build dependencies.
|
||||
3. Run the build script to synchronize protocol constants and generate browser bundles:
|
||||
```bash
|
||||
node scripts/build-extension.js
|
||||
```
|
||||
## Ways to Contribute
|
||||
|
||||
### 3. Testing Locally
|
||||
1. Load `dist/chrome/` as an "Unpacked Extension" in Chrome (`chrome://extensions/` → Developer Mode → Load Unpacked).
|
||||
2. For Firefox, load `dist/firefox/` via `about:debugging` → "Load Temporary Add-on".
|
||||
3. Start the relay server: `docker-compose up --build`.
|
||||
4. Use **two different browser profiles** (or Chrome + Firefox) to test multi-peer synchronization.
|
||||
5. Use the extension's **Dev tab** to verify real-time video element metadata (`readyState`, `currentTime`, `paused`).
|
||||
| Area | Description |
|
||||
|------|-------------|
|
||||
| **Bug Reports** | Found a bug? Open an issue with repro steps (see template below). |
|
||||
| **Code** | Fix bugs, add features, or improve the extension / server / website. |
|
||||
| **Translations** | Help localize the extension and website into more languages. See [TRANSLATION.md](docs/TRANSLATION.md). |
|
||||
| **Documentation** | Improve docs, fix typos, or add missing examples. |
|
||||
| **Security** | Found a vulnerability? See [SECURITY.md](SECURITY.md) — do NOT open a public issue. |
|
||||
|
||||
### 4. Protocol Synchronization
|
||||
KoalaSync uses a "Single Source of Truth" for protocol constants in `shared/constants.js`.
|
||||
- **CRITICAL**: If you modify the constants, you MUST run the build script:
|
||||
```bash
|
||||
node scripts/build-extension.js
|
||||
```
|
||||
This will automatically synchronize the changes to the extension and generate the browser-specific bundles in the `dist/` folder.
|
||||
---
|
||||
|
||||
### 5. Code Standards
|
||||
- **Vanilla JS**: The extension must remain dependency-free. Do not add npm packages to the `extension/` directory.
|
||||
- **Privacy**: Do not add external requests (CDNs, fonts, analytics, etc.).
|
||||
- **Comments**: Maintain the existing documentation style, especially for complex sync logic.
|
||||
- **Room IDs**: Room IDs are restricted to `[a-zA-Z0-9-]` (alphanumeric + hyphens only). Ensure any UI that generates room IDs follows this constraint.
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Node.js** v18+
|
||||
- **Docker** (for local relay server testing)
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Shik3i/KoalaSync.git
|
||||
cd KoalaSync
|
||||
npm install
|
||||
node scripts/build-extension.cjs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
| Directory | Purpose |
|
||||
|-----------|---------|
|
||||
| `extension/` | Browser extension (Manifest V3, Chrome & Firefox) |
|
||||
| `server/` | Node.js + Socket.IO relay server (Dockerized) |
|
||||
| `website/` | Landing page, invitation bridge, and marketing site |
|
||||
| `shared/` | Protocol constants — single source of truth |
|
||||
| `scripts/` | Build and sync utilities |
|
||||
| `docs/` | Architecture, sync protocol, and deep-dive guides |
|
||||
|
||||
---
|
||||
|
||||
## Testing Locally
|
||||
|
||||
### Extension
|
||||
|
||||
1. Load `dist/chrome/` as an unpacked extension in Chrome (`chrome://extensions/` → Developer Mode → **Load unpacked**).
|
||||
2. For Firefox: load `dist/firefox/` via `about:debugging` → **Load Temporary Add-on**.
|
||||
3. Start the relay server: `docker compose up --build`.
|
||||
4. Use **two different browser profiles** (or Chrome + Firefox) to test multi-peer sync.
|
||||
5. Use the extension's **Dev tab** to inspect real-time video element state (`readyState`, `currentTime`, `paused`).
|
||||
|
||||
### Website
|
||||
|
||||
```bash
|
||||
node website/build.cjs # Compile static site → www/
|
||||
python3 -m http.server 8080 -d website/www # Serve locally
|
||||
```
|
||||
|
||||
Then open `http://localhost:8080`. For multi-language testing: `http://localhost:8080/de/`.
|
||||
|
||||
---
|
||||
|
||||
## Protocol Constants
|
||||
|
||||
KoalaSync uses a **single source of truth** for all protocol constants in `shared/constants.js`.
|
||||
|
||||
### 6. Version Numbers
|
||||
> [!IMPORTANT]
|
||||
> **Do NOT manually bump version numbers.** The CI pipeline automatically injects the version from the git tag into `manifest.base.json`, `shared/constants.js`, and `package.json` during release builds. Manually changing version numbers in a PR will cause conflicts.
|
||||
> After modifying `shared/constants.js`, you **must** run the build script to sync changes to the extension:
|
||||
> ```bash
|
||||
> node scripts/build-extension.cjs
|
||||
> ```
|
||||
> This automatically injects constants into `content.js` and regenerates browser bundles in `dist/`.
|
||||
|
||||
## Pull Request Process
|
||||
1. Create a new branch for your feature or bugfix.
|
||||
2. Ensure your code is tested locally (Chrome and Firefox).
|
||||
3. Update relevant documentation (e.g., `docs/ARCHITECTURE.md` if you change the protocol).
|
||||
4. Submit your PR with a clear description of the changes.
|
||||
---
|
||||
|
||||
## Bug Reports
|
||||
When reporting a bug, please include:
|
||||
- **Browser**: Chrome / Firefox / Edge + version number.
|
||||
- **Extension Version**: Visible in the popup's Dev tab.
|
||||
- **Dev Tab Output**: Copy the connection status, logs, and video debug info from the Dev tab.
|
||||
- **Steps to Reproduce**: A clear sequence of actions that triggers the issue.
|
||||
## Code Standards
|
||||
|
||||
- **Vanilla JS**: The extension must remain dependency-free. No npm packages in `extension/`.
|
||||
- **Privacy-first**: Zero external requests — no CDNs, fonts, analytics, or trackers. All assets self-hosted.
|
||||
- **System font stack**: `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, ...` — never `@import` external fonts.
|
||||
- **Room IDs**: Restricted to `[a-zA-Z0-9-]` (alphanumeric + hyphens only). Enforced server-side.
|
||||
- **Comments**: Document complex sync logic. The codebase uses inline comments for protocol reasoning.
|
||||
|
||||
---
|
||||
|
||||
## Version Numbers
|
||||
|
||||
> [!CAUTION]
|
||||
> **Never manually bump version numbers.** The CI pipeline injects the version from the git tag into `manifest.base.json`, `shared/constants.js`, and `package.json` during release builds. Manual bumps cause conflicts.
|
||||
|
||||
---
|
||||
|
||||
## Open Source Workflow & Pull Requests
|
||||
|
||||
If you are new to open-source contributions, follow these steps to propose your changes:
|
||||
|
||||
1. **Fork the Repository**: Click the "Fork" button at the top right of this repository to create your own copy of KoalaSync.
|
||||
2. **Clone your Fork**: `git clone https://github.com/YOUR-USERNAME/KoalaSync.git`
|
||||
3. **Create a Branch**: `git checkout -b my-new-feature` (e.g. `feature/dark-mode` or `fix/translation-de`)
|
||||
4. **Make your Changes**: Edit the files, then verify them locally.
|
||||
- *Extension/Server changes*: Test on Chrome/Firefox and check `npm run lint`.
|
||||
- *Website/Translation changes*: Run `node website/build.cjs` and check the output in `www/`.
|
||||
5. **Commit and Push**: `git commit -m "Add my feature"` and `git push origin my-new-feature`
|
||||
6. **Open a Pull Request (PR)**: Go to the original KoalaSync repository on GitHub and click "New Pull Request".
|
||||
|
||||
### PR Code Requirements
|
||||
- **Lint**: Ensure `npm run lint` passes with zero errors and warnings.
|
||||
- **Syntax**: Run `node -c` on every modified `.js` file.
|
||||
- **Protocol changes**: Update relevant documentation in `docs/`.
|
||||
|
||||
---
|
||||
|
||||
## Bug Report Template
|
||||
|
||||
When filing a bug, the easiest way is to use the **Copy Logs** button in the extension's **Status** tab. It copies a fully formatted Markdown report to your clipboard containing:
|
||||
|
||||
- System info (version, protocol, peer ID, browser)
|
||||
- Connection status (server, room, peers, reconnect state)
|
||||
- Video debug info (playback state, readyState, network state, dimensions, error codes, shadow DOM detection, platform)
|
||||
- Action history (last 20 events)
|
||||
- Log entries (last 50)
|
||||
|
||||
Simply paste the clipboard contents into your GitHub issue and add:
|
||||
|
||||
| Field | Example |
|
||||
|-------|---------|
|
||||
| **Steps to Reproduce** | 1. Create room → 2. Join from second browser → 3. Play video |
|
||||
| **Expected Behavior** | Both peers play simultaneously |
|
||||
| **Actual Behavior** | Peer B remains paused |
|
||||
|
||||
If you cannot access the Status tab, include as much of the following manually:
|
||||
|
||||
| Field | Example |
|
||||
|-------|---------|
|
||||
| **Browser** | Chrome 125, Firefox 128 |
|
||||
| **Extension Version** | v1.9.3 (visible at bottom of Settings tab) |
|
||||
| **Website/Platform** | Netflix, YouTube, Twitch, Jellyfin, etc.
|
||||
|
||||
---
|
||||
|
||||
## Translation Contributions (Translators Welcome!)
|
||||
|
||||
We welcome native speakers to help translate KoalaSync! You **do not** need deep programming knowledge to contribute translations.
|
||||
|
||||
KoalaSync supports multiple languages. To add or improve translations:
|
||||
1. Read the **[Translation Guide](docs/TRANSLATION.md)** first. It explains how our localization system works.
|
||||
2. Edit the `.json` files in `website/locales/` (for the website) and `extension/locales/` (for the extension).
|
||||
3. Test your translations locally by running:
|
||||
- `node scripts/test-locales.cjs` (for extension)
|
||||
- `node scripts/test-website-locales.mjs` (for website)
|
||||
- `node website/build.cjs` (to build the site)
|
||||
4. Follow the **Open Source Workflow** above (Fork -> Branch -> Edit -> PR) to submit your translations.
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
If you find a security vulnerability, please do not open a public issue. Instead, refer to our [SECURITY.md](SECURITY.md) for responsible disclosure instructions.
|
||||
|
||||
If you discover a security vulnerability, **do not open a public issue**. Report it privately as described in [SECURITY.md](SECURITY.md).
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
# KoalaSync - Production Caddy Configuration Example
|
||||
# Replace domains and paths with your actual setup.
|
||||
|
||||
# 1. Marketing Website & Invitation Bridge
|
||||
sync.koalastuff.net {
|
||||
root * /var/www/koalasync/website
|
||||
file_server
|
||||
encode zstd gzip
|
||||
|
||||
# Static Caching for high-performance PageSpeed (1 year with validation)
|
||||
@static {
|
||||
file
|
||||
path *.ico *.css *.js *.png *.svg *.webp
|
||||
}
|
||||
header @static Cache-Control "public, max-age=31536000, must-revalidate"
|
||||
|
||||
# Security Headers & Content Security Policy (CSP)
|
||||
header {
|
||||
# Strict Content Security Policy (restricts scripts and connections to self, forbids frames)
|
||||
Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';"
|
||||
# Prevent FLoC tracking
|
||||
Permissions-Policy interest-cohort=()
|
||||
# Security best practices
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options nosniff
|
||||
X-Frame-Options DENY
|
||||
Referrer-Policy no-referrer-when-downgrade
|
||||
}
|
||||
}
|
||||
|
||||
# 2. Relay Server (Socket.IO / WebSocket)
|
||||
syncserver.koalastuff.net {
|
||||
reverse_proxy localhost:3000 {
|
||||
# Ensure WebSocket support is explicitly handled if needed
|
||||
# (Caddy usually handles this automatically)
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
|
||||
# Security Headers for the relay
|
||||
header {
|
||||
X-Content-Type-Options nosniff
|
||||
X-Frame-Options DENY
|
||||
Referrer-Policy no-referrer
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,25 @@
|
||||
# <img src="extension/icons/icon128.png" width="32" valign="middle"> KoalaSync
|
||||
<p align="center">
|
||||
<img src="website/assets/PlatformJuggler_New.webp" width="280" alt="KoalaSync Mascot">
|
||||
</p>
|
||||
|
||||
<h1 align="center">KoalaSync</h1>
|
||||
|
||||
<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/github/v/release/Shik3i/KoalaSync" alt="GitHub release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Shik3i/KoalaSync?color=blue" alt="License"></a>
|
||||
<img src="https://img.shields.io/badge/Browser-Chrome%20|%20Firefox-blueviolet" alt="Cross Browser">
|
||||
<a href="https://github.com/Shik3i/KoalaSync/releases"><img src="https://img.shields.io/badge/Release-v2.5.2-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>
|
||||
</p>
|
||||
|
||||
KoalaSync is a premium, lightweight Browser Extension and Relay Server for synchronized video playback across any website—YouTube, Twitch, Netflix, and custom HTML5 players. Built with a focus on **Data Sovereignty** and **Extreme Performance**.
|
||||
<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.5.2 Release!</b> — See what's changed</a></p>
|
||||
|
||||
### 🌟 Why KoalaSync?
|
||||
|
||||
* **🛡️ Security-First**: Volatile RAM-based relay with built-in brute-force protection and zero-persistence architecture.
|
||||
* **📡 Direct Logic**: Custom wire protocol implementation for frame-perfect synchronization.
|
||||
* **🛡️ Security-First**: Volatile RAM-based relay with built-in brute-force protection and zero-persistence architecture. We keep no logs of your sessions or synchronizations. *We don't track you. We only track our server* (relying on the [aggregated, anonymous, non-personal metrics](https://syncserver.koalastuff.net/health) provided under `/health`).
|
||||
* **📡 Direct Logic**: Manual Socket.IO wire implementation for reliable synchronization.
|
||||
* **🛠️ Clean Build**: Dependency-free extension runtime with no library overhead.
|
||||
* **🌐 Universal**: Works on any website with a `<video>` tag.
|
||||
|
||||
@@ -24,12 +31,44 @@ KoalaSync is a premium, lightweight Browser Extension and Relay Server for synch
|
||||
- **Episode Auto-Sync**: Perfectly sync series binges. All peers wait until everyone has loaded the next episode before starting together.
|
||||
- **Smart Matching**: Automatically highlights tabs containing matching video titles.
|
||||
- **Dual Heartbeat Architecture**: Robust session tracking that prevents ghost rooms and stale connections.
|
||||
- **Zero-Latency Relay**: Custom wire protocol implementation for maximum performance.
|
||||
- **Efficient Relay**: Minimal overhead WebSocket message forwarding.
|
||||
- **Seamless Invitations**: Smart links that automatically configure server and room credentials for your friends.
|
||||
- **Smart Audio Compressor**: Tired of constantly riding the volume? Automatically balance out whispering dialogue and deafening explosions with simple presets, or fully customize the audio to your liking.
|
||||
|
||||
---
|
||||
|
||||
### 📂 Repository Structure
|
||||
### 🚀 Quick Start
|
||||
|
||||
#### For Users (Installation & Usage)
|
||||
The easiest and safest way to install KoalaSync is directly through the official browser stores:
|
||||
|
||||
<p>
|
||||
<a href="https://chromewebstore.google.com/detail/koalasync/obbnmkmlaaddodakcbdljknjpagklifc"><img src="https://img.shields.io/badge/Chrome-Download-blue?logo=googlechrome&logoColor=white&style=for-the-badge" alt="Chrome Extension"></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&style=for-the-badge" alt="Firefox Add-on"></a>
|
||||
</p>
|
||||
|
||||
*(For manual offline installation: Download the latest `.zip` from the [Releases](https://github.com/Shik3i/KoalaSync/releases) page and load it as an "Unpacked Extension" in Developer Mode).*
|
||||
|
||||
**How to use:**
|
||||
1. **Create a Room:** Click the Koala icon in your browser and hit `+ Create New Room`.
|
||||
2. **Invite Friends:** Share the auto-copied invite link. Once they click it, they automatically join.
|
||||
3. **Pick a Video:** Navigate to the Sync Tab, select the tab playing your video, and grab some popcorn! 🍿
|
||||
|
||||
---
|
||||
|
||||
### 🌐 Localization & Translations
|
||||
|
||||
Both the official KoalaSync website and the **v2.0 Browser Extension** feature full dynamic localization:
|
||||
- **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.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 🛠️ For Developers & Self-Hosters
|
||||
|
||||
#### 📂 Repository Structure
|
||||
|
||||
- `extension/`: Browser Extension (Chrome & Firefox).
|
||||
- `server/`: Node.js + Socket.IO Relay Server (Containerized).
|
||||
@@ -38,20 +77,11 @@ KoalaSync is a premium, lightweight Browser Extension and Relay Server for synch
|
||||
- `scripts/`: Automated build and synchronization utilities.
|
||||
- `docs/`: Technical deep-dives ([Architecture](docs/ARCHITECTURE.md), [Sync Guide](docs/SYNC_GUIDE.md)).
|
||||
|
||||
---
|
||||
|
||||
### 🚀 Quick Start
|
||||
|
||||
#### For Users (Installation)
|
||||
The easiest way to install KoalaSync is to download the pre-compiled version from the [Releases](https://github.com/Shik3i/KoalaSync/releases) page.
|
||||
1. Download the latest `koalasync-chrome.zip` or `koalasync-firefox.zip`.
|
||||
2. Extract the file and load it as an "Unpacked Extension" in your browser's Developer Mode.
|
||||
|
||||
#### For Developers (Building)
|
||||
#### Building from Source
|
||||
To build the extension from source and synchronize protocol constants:
|
||||
```bash
|
||||
npm install
|
||||
node scripts/build-extension.js
|
||||
npm run build:extension
|
||||
```
|
||||
The compiled artifacts will be available in the `dist/` directory.
|
||||
|
||||
@@ -62,28 +92,55 @@ Deploy your own private relay server using our official image:
|
||||
docker pull ghcr.io/shik3i/koalasync:latest
|
||||
|
||||
# Or use our example compose file
|
||||
cp docker-compose.example.yml docker-compose.yml
|
||||
cp examples/docker-compose.caddy.example.yml docker-compose.yml
|
||||
docker-compose up -d
|
||||
```
|
||||
The server will be available at `ws://localhost:3000`. See [docker-compose.example.yml](docker-compose.example.yml) for advanced configuration.
|
||||
The server will be available at `ws://localhost:3000`. See [Docker network compose](examples/docker-compose.caddy.example.yml) or [Static IP compose](examples/docker-compose.ip.example.yml) for ready-to-use Docker Compose files.
|
||||
|
||||
To connect your extension to a self-hosted server, open the popup → **Room** tab → select **Custom Server** → enter your server's WebSocket URL (e.g., `ws://localhost:3000`).
|
||||
|
||||
> **⚠️ Note**: `ws://` only works for `localhost`. If you deploy to a real domain, you **must** use `wss://` (e.g., `wss://sync.yourdomain.com`). This requires a TLS-terminating reverse proxy (e.g., Caddy, Nginx, or Traefik) in front of the relay server. See [Caddyfile.example](Caddyfile.example) for a production-ready template.
|
||||
> **⚠️ Note**: `ws://` only works for `localhost`. If you deploy to a real domain, you **must** use `wss://` (e.g., `wss://sync.yourdomain.com`). This requires a TLS-terminating reverse proxy (e.g., Caddy, Nginx, or Traefik) in front of the relay server. See [Caddyfile.example](examples/Caddyfile.example) for a production-ready template.
|
||||
|
||||
To verify your relay is reachable from outside, visit `https://your-domain.com` in a browser — it should return `{"status":"online","service":"KoalaSync Relay"}`.
|
||||
|
||||
#### Supply Chain Security (v2.2.2+)
|
||||
|
||||
All official release artifacts (Docker images and extension binaries) are published with signed [artifact attestations](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations) to prove they were built from this repository's source code.
|
||||
|
||||
**Verify a Docker image:**
|
||||
```bash
|
||||
gh attestation verify oci://ghcr.io/shik3i/koalasync:latest \
|
||||
-R Shik3i/KoalaSync
|
||||
```
|
||||
|
||||
**Verify an extension binary:**
|
||||
```bash
|
||||
gh attestation verify dist/koalasync-chrome.zip \
|
||||
-R Shik3i/KoalaSync
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📖 Documentation & Links
|
||||
|
||||
- **[PRIVACY.md](PRIVACY.md)**: Data Handling and Privacy Policy.
|
||||
- **[CHANGELOG.md](docs/CHANGELOG.md)**: Full version history for the extension and relay server.
|
||||
- **[TESTED_SERVICES.md](docs/TESTED_SERVICES.md)**: Detailed compatibility matrix of tested streaming platforms and known limitations.
|
||||
- **[TRANSLATION.md](docs/TRANSLATION.md)**: Translation and localization guide for contributors.
|
||||
- **[PRIVACY.md](docs/PRIVACY.md)**: Data Handling and Privacy Policy.
|
||||
- **[CONTRIBUTING.md](CONTRIBUTING.md)**: How to help make KoalaSync better.
|
||||
- **[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)**: Our community standards and expectations.
|
||||
- **[HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)**: Step-by-step walkthrough of the complete user flow.
|
||||
- **[ARCHITECTURE.md](docs/ARCHITECTURE.md)**: Deep-dive into the two-phase sync and heartbeat logic.
|
||||
- **[PROTOCOL.md](docs/PROTOCOL.md)**: WebSocket protocol specification and event reference.
|
||||
- **[ROADMAP.md](docs/ROADMAP.md)**: Planned features, backlog, and rejected ideas.
|
||||
- **[SECURITY.md](SECURITY.md)**: Disclosure policy and security practices.
|
||||
- **[Caddyfile.example](Caddyfile.example)**: Production Caddy configuration for website and relay.
|
||||
- **[Caddyfile.example](examples/Caddyfile.example)**: Production Caddy configuration for website and relay.
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,102 @@
|
||||
# Security Policy
|
||||
|
||||
KoalaSync is built on a **zero-persistence, privacy-first** architecture. We take security seriously and appreciate responsible disclosure of vulnerabilities.
|
||||
|
||||
---
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We take the security of our users and their data very seriously. We actively support and patch the latest stable releases of KoalaSync.
|
||||
Only the latest stable release receives security patches.
|
||||
|
||||
| Version | Supported |
|
||||
| -------------- | ------------------ |
|
||||
| Latest Release | :white_check_mark: |
|
||||
| Older Versions | :x: |
|
||||
| Version | Supported |
|
||||
|---------|-----------|
|
||||
| Latest release | :white_check_mark: Active |
|
||||
| Older versions | :x: Unsupported |
|
||||
|
||||
Users on older versions are encouraged to update. The server enforces a minimum client version via `MIN_VERSION`.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
The following components are within scope for security reports:
|
||||
|
||||
| Component | Examples |
|
||||
|-----------|----------|
|
||||
| **Relay Server** (`server/`) | Authentication bypass, rate-limit evasion, room hijacking, DoS vectors |
|
||||
| **Browser Extension** (`extension/`) | XSS via content scripts, privilege escalation, data exfiltration, tab snooping |
|
||||
| **WebSocket Protocol** | Message injection, replay attacks, man-in-the-middle (WSS bypass) |
|
||||
| **Website** (`website/`) | XSS, CSP bypass, invitation-hash leaks |
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Theoretical attacks requiring physical device access
|
||||
- Social engineering or phishing
|
||||
- Denial of service via resource exhaustion on self-hosted instances
|
||||
- Vulnerabilities in third-party browser extensions or websites
|
||||
|
||||
---
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability within KoalaSync (e.g., related to the Node.js relay server, WebSocket wire protocol, or the Chrome/Firefox browser extension), please **DO NOT** report it by creating a public GitHub issue.
|
||||
> [!CAUTION]
|
||||
> **Do NOT open a public GitHub issue for security vulnerabilities.** Public disclosure before a patch is available puts users at risk.
|
||||
|
||||
Publicly disclosing a vulnerability before a patch is available puts our users at risk. Instead, please send an email privately to the project administrator at:
|
||||
**koalasync_admin@koalamail.rocks**
|
||||
Instead, email the project maintainer privately:
|
||||
|
||||
### What to expect
|
||||
1. **Acknowledgment**: You should receive an acknowledgment of your report within 48 hours.
|
||||
2. **Investigation**: We will investigate the issue, confirm its severity, and work on a patch.
|
||||
3. **Resolution**: We will notify you when the patch is deployed to the Chrome Web Store, Mozilla Add-on Store, and our GitHub Docker releases.
|
||||
4. **Disclosure**: Once the fix is confirmed and users have had time to update, we will publicly acknowledge your contribution in our release notes (unless you prefer to remain anonymous).
|
||||
**`koalasync_admin@koalamail.rocks`**
|
||||
|
||||
Encrypt sensitive findings with our PGP key (available on request).
|
||||
|
||||
### What to Include
|
||||
|
||||
- **Affected component**: Server / Extension / Website / Protocol
|
||||
- **Steps to reproduce**: Clear, minimal steps to trigger the vulnerability
|
||||
- **Impact**: What an attacker could achieve (data access, privilege escalation, etc.)
|
||||
- **Environment**: Browser version, extension version, server configuration
|
||||
- **Suggested fix** (optional): If you have ideas for a patch
|
||||
|
||||
### What to Expect
|
||||
|
||||
| Timeline | Action |
|
||||
|----------|--------|
|
||||
| **Within 48 hours** | Acknowledgment of your report |
|
||||
| **Within 7 days** | Initial assessment and severity confirmation |
|
||||
| **As needed** | Collaborative discussion for clarification |
|
||||
| **After patch** | Notification that the fix is deployed |
|
||||
| **After rollout** | Public acknowledgment in release notes (or anonymity if preferred) |
|
||||
|
||||
---
|
||||
|
||||
## Architecture & Threat Model
|
||||
|
||||
KoalaSync's security is grounded in its architecture:
|
||||
|
||||
- **RAM-only relay**: No database, no persistent logs. All session data evaporates on disconnect.
|
||||
- **Keyed SHA-256 room password hashes**: Plaintext passwords are never stored. Room passwords are held only as in-memory HMAC-SHA256 hashes for the short room lifetime, with brute-force protection: 5 attempts → 15-minute IP lockout.
|
||||
- **Rate limiting**: Connection rate (IP-based, 60s window), health endpoint rate (10 requests/minute/IP), wrong admin-metrics bearer attempts (5 requests/minute/IP), and event rate (per-socket, 10s window). Health-style JSON responses are cached server-side for 60 seconds and refreshed lazily on request.
|
||||
- **Reverse proxy boundary**: The relay trusts one proxy hop for client IP detection. In production, keep the Node server reachable only through Caddy or another trusted reverse proxy.
|
||||
- **URL-hash credential isolation**: Invitation credentials live in the URL fragment (`#join:...`) — never sent to the web server.
|
||||
- **Strict CSP**: `default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'`.
|
||||
- **No third-party requests**: Zero CDNs, fonts, analytics, or external scripts.
|
||||
|
||||
If you find a way to bypass any of these protections, we want to know about it.
|
||||
|
||||
> [!NOTE]
|
||||
> Some frequently-reported "issues" are **intentional and out of scope** for our
|
||||
> threat model (ephemeral, account-less rooms of invited participants) — e.g. an
|
||||
> unauthenticated `peerId` or non-constant-time room-password compare. Before
|
||||
> reporting, please read **[`docs/KNOWN_LIMITATIONS.md`](docs/KNOWN_LIMITATIONS.md)**.
|
||||
|
||||
---
|
||||
|
||||
## Responsible Disclosure
|
||||
|
||||
We follow the principle of **coordinated vulnerability disclosure**:
|
||||
|
||||
1. You report privately.
|
||||
2. We investigate and develop a patch.
|
||||
3. We deploy to the Chrome Web Store, Firefox Add-ons, and Docker registry.
|
||||
4. We credit you publicly (unless you prefer to remain anonymous).
|
||||
|
||||
We do not pursue legal action against researchers who act in good faith and follow this disclosure process.
|
||||
|
||||
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 889 KiB |
@@ -0,0 +1,66 @@
|
||||
# KoalaSync — Marketing Copy Kit
|
||||
|
||||
Ready-to-paste copy for product listings, launch pages, directory submissions, and anywhere else you keep re-typing the same pitch. Three lengths, one consistent message. Pick the one that fits the field limit.
|
||||
|
||||
---
|
||||
|
||||
## 1. One-Sentence Pitch
|
||||
|
||||
> KoalaSync is a privacy-first browser extension that synchronizes video playback across almost any website so you can watch with friends in real time — no accounts, no tracking, and your video never passes through anyone's server but the original site's.
|
||||
|
||||
**Shorter alternative** (for tight tagline fields):
|
||||
|
||||
> Private, universal watch parties on any website — no accounts, no tracking, no media proxying.
|
||||
|
||||
---
|
||||
|
||||
## 2. Three-Sentence Overview
|
||||
|
||||
> KoalaSync is a lightweight browser extension that keeps you and your friends perfectly in sync on YouTube, Netflix, Twitch, Prime Video, Jellyfin, Emby, and almost any other site with an HTML5 video player — press play once and everyone stays together. It's built privacy-first: no accounts, no telemetry, and the official relay server runs entirely in volatile RAM with zero persistence, so nothing about your sessions is ever stored. Open source under the MIT license and fully self-hostable with a single Docker command, KoalaSync is a transparent watch-party tool that works everywhere and respects your data sovereignty.
|
||||
|
||||
---
|
||||
|
||||
## 3. Full Description
|
||||
|
||||
### Watch together — on any site, on your terms.
|
||||
|
||||
Counting down "3, 2, 1, play" over voice chat doesn't scale past two people. KoalaSync fixes that with a tiny browser extension that synchronizes play, pause, and seeking across everyone in the room, on almost any website with a `<video>` element. Create a room, share a link, press play — that's it.
|
||||
|
||||
### What makes KoalaSync different
|
||||
|
||||
Most watch-party tools fall into one of two traps: they only work on a short allowlist of sites (site-specific extensions that need a separate build for every platform), or they route your video through their own player and servers. KoalaSync was built around three principles that break that mold.
|
||||
|
||||
**Universal by design.** If the site has an HTML5 `<video>` element, KoalaSync can usually sync it. YouTube, Netflix, Twitch, Prime Video, Disney+, Jellyfin, Emby, and countless niche sites work out of the box — no per-site integration to wait for, no extension swap when your friends want to switch services.
|
||||
|
||||
**Your video never touches our servers.** KoalaSync only relays tiny timing messages — play, pause, seek position, readiness — over a hand-rolled WebSocket protocol. The actual video keeps streaming directly from the original site to each viewer's browser. KoalaSync never proxies, transcodes, uploads, or redistributes a single frame, which also means there is no legal gray zone around redistribution.
|
||||
|
||||
**Privacy is the default, not an upgrade.** No accounts, no emails, no telemetry, no analytics, no behavior profiling. The official relay server runs entirely in volatile RAM and keeps zero persistent state — when the room closes, the data is gone. Pick a nickname or let KoalaSync generate one for you and you're in.
|
||||
|
||||
### Built for people who actually want to read the code
|
||||
|
||||
KoalaSync is MIT-licensed open source, built by a solo developer. Audit it, fork it, change it. The extension is dependency-free with a direct Socket.IO wire implementation — no opaque libraries, no framework bloat, no surprise third-party SDKs. Want full sovereignty? Self-host your own relay with a single Docker command and keep all watch-party coordination traffic inside your own infrastructure. The official public relay is there when you don't care, self-hosting is there when you do.
|
||||
|
||||
### Little touches you'll notice
|
||||
|
||||
- **Episode Auto-Sync** pauses the room when someone loads the next episode and resumes only when everyone is ready — no spoilers, no one left behind on the previous cliffhanger.
|
||||
- **Smart Audio Compressor** tames the modern "whisper dialogue, deafening explosion" mix with one click. Three presets or full manual control over threshold, ratio, attack, and release.
|
||||
- **One-click invite links** auto-configure the server and room for your friends — they just click the link and they're in. No fumbling with server URLs or room IDs.
|
||||
- **Dual-heartbeat architecture** kills ghost rooms and stale connections before they desync your session.
|
||||
- **15 languages** fully translated and switchable in real time from the settings panel — English, German, French, Spanish, Portuguese (Brazil + European), Russian, Italian, Polish, Turkish, Dutch, Japanese, Korean, Chinese, Ukrainian.
|
||||
|
||||
### Install and start in under a minute
|
||||
|
||||
Install KoalaSync from the Chrome Web Store or Firefox Add-ons, click "Create Room," share the invite link, and pick a video. The official relay is ready out of the box — no setup required unless you want to self-host.
|
||||
|
||||
- Website: https://sync.koalastuff.net
|
||||
- GitHub: https://github.com/Shik3i/KoalaSync
|
||||
|
||||
---
|
||||
|
||||
## Bonus: Taglines (for hero headlines, social bios, meta descriptions)
|
||||
|
||||
- Watch together. Anywhere. Privately.
|
||||
- The watch-party tool that works on every site and tracks none of them.
|
||||
- Sync play, pause, and seek on any video — no accounts, no logs, no lock-in.
|
||||
- Self-hostable, open-source watch parties for the post-"3, 2, 1, play" era.
|
||||
- Press play once. Stay together anywhere.
|
||||
|
Before Width: | Height: | Size: 462 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 482 KiB |
|
After Width: | Height: | Size: 692 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 99 KiB |
@@ -0,0 +1,57 @@
|
||||
KoalaSync: Private Watch Parties for Emby, Jellyfin, Plex, Netflix & YouTube
|
||||
|
||||
Tired of counting down "3, 2, 1, Play" over voice chat? KoalaSync keeps you and your friends perfectly in sync. Whether you are streaming from your own self-hosted media server like Emby, Jellyfin or Plex, or watching on a major platform like Netflix, Prime Video or YouTube — KoalaSync is designed for smooth, browser-based watch parties.
|
||||
|
||||
|
||||
✨ CORE FEATURES
|
||||
No account required. No tracking. Just create a room, invite your friends, and start watching together.
|
||||
|
||||
• Real-Time Video Sync: Play, pause, seek, and watch together with fast synchronized playback across everyone in your room.
|
||||
• No Account Needed: Create a room and share the invite link. No emails, no passwords, no sign-ups. Pick a nickname or let KoalaSync generate one for you.
|
||||
• Works Almost Everywhere: If the website uses a standard HTML5 video player, KoalaSync can usually sync it. Perfect for streaming sites, self-hosted media servers, and other websites.
|
||||
• Smart Binge-Watching: When a new episode loads, KoalaSync automatically pauses the lobby until everyone is ready. No spoilers, no one left behind.
|
||||
• Smart Audio Compressor: Tired of quiet dialogue and suddenly loud action scenes? Balance whispering, explosions, and music with a single click while you watch.
|
||||
• One-Click Invites: Send a smart invite link to your friends. When they open it, KoalaSync automatically configures the room so they can join instantly.
|
||||
• 13 Languages: Enjoy a native experience with a fully translated user interface.
|
||||
|
||||
|
||||
|
||||
🛡️ PRIVACY & SECURITY
|
||||
KoalaSync is built for private watch parties without unnecessary data collection.
|
||||
|
||||
• No Tracking: Zero analytics, zero telemetry, and absolutely no behavior profiling.
|
||||
• Anonymous by Design: No accounts needed. Rooms can be joined with a simple nickname.
|
||||
• Ready Out of the Box: Install KoalaSync and start watching immediately using the official public relay server. No technical setup required.
|
||||
• RAM-Only Public Server: The official relay server operates entirely in volatile RAM. No databases, no stored watch history, no persistent room data. Room data exists only temporarily and disappears when the room closes.
|
||||
• Self-Hostable: Want full control? You can run your own private KoalaSync relay server via Docker in seconds. Self-hosting is optional and never required.
|
||||
|
||||
|
||||
|
||||
🚀 HOW IT WORKS
|
||||
1. Install KoalaSync.
|
||||
2. Click "Create Room" to start a private watch party.
|
||||
3. Share the invite link with your friends.
|
||||
4. Open your favorite streaming site or media server.
|
||||
5. Select the active video tab.
|
||||
6. Press play — everyone stays perfectly in sync.
|
||||
|
||||
|
||||
|
||||
⚙️ UNDER THE HOOD
|
||||
KoalaSync is lightweight, transparent, and built with privacy in mind.
|
||||
|
||||
• On-Demand Relay: Playback state is synchronized through a custom WebSocket-based relay server. No persistent connection — the relay is only active while you're in a room. No background traffic, no idle connections.
|
||||
• No Media Streaming: KoalaSync does not stream, proxy, upload, download, or redistribute any video content. Everyone watches from their own browser on the original website.
|
||||
• Temporary Room State Only: The relay server only coordinates room state such as play, pause, seek position, active target, nickname, and readiness status.
|
||||
• Docker Self-Hosting: The relay server can be self-hosted with Docker if you prefer to run your own private instance.
|
||||
• Open Architecture: The project is designed to be inspectable, forkable, and easy to review.
|
||||
|
||||
|
||||
|
||||
💻 OPEN SOURCE
|
||||
KoalaSync was built by a solo developer who needed a fast, secure way to watch movies with friends. The code is fully transparent under the MIT license: audit it, fork it, improve it, or self-host your own relay server.
|
||||
|
||||
Found a bug or have a feature idea? Open an issue on GitHub. Contributions and code reviews are always welcome.
|
||||
|
||||
• Website: https://sync.koalastuff.net
|
||||
• GitHub: https://github.com/Shik3i/KoalaSync
|
||||
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -1,25 +0,0 @@
|
||||
services:
|
||||
koala-sync:
|
||||
image: ghcr.io/shik3i/koalasync:latest
|
||||
container_name: KoalaSync
|
||||
restart: always
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
- PORT=3000
|
||||
- MIN_VERSION=1.0.0
|
||||
- MAX_ROOMS=100
|
||||
- MAX_PEERS_PER_ROOM=50
|
||||
# KoalaSync uses in-memory storage for the relay,
|
||||
# so no persistent database volume is required.
|
||||
pids_limit: 2048
|
||||
# Example for custom network (e.g., Unraid/Macvlan)
|
||||
# networks:
|
||||
# custom_network:
|
||||
# ipv4_address: 192.168.1.XXX
|
||||
|
||||
# networks:
|
||||
# custom_network:
|
||||
# external: true
|
||||
# name: br0
|
||||
@@ -5,6 +5,9 @@ Welcome to the KoalaSync project. This file is the primary entry point for any d
|
||||
> [!IMPORTANT]
|
||||
> **Privacy & Data Sovereignty**: KoalaSync follows a strict **Zero-External-Requests Policy**: The extension and website must not make requests to any third-party domains (Google Fonts, CDNs, etc.). All assets (fonts, icons, scripts) must be self-hosted or use system defaults.
|
||||
> - **Font Stack**: Use a modern system font stack (e.g., -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif) to maintain a premium look without external dependencies. Prohibit the use of `@import` or `<link>` for external font services.
|
||||
>
|
||||
> [!IMPORTANT]
|
||||
> **Caveman Communication Protocol**: To conserve context window and tokens, AI agents MUST communicate with the user in **caveman language** (broken, short German/English hybrid caveman style, e.g., "Ich Antigravity. Ich machen Git pull. Code fertig.") at all times. This rule is highly strict and must NOT be broken under any circumstances unless the user explicitly requests to drop it.
|
||||
|
||||
---
|
||||
|
||||
@@ -18,25 +21,27 @@ KoalaSync is a specialized tool for **synchronized video playback** across multi
|
||||
- `extension/`: Browser Extension (Chrome & Firefox, Manifest V3). Contains background service worker, content scripts, and popup UI.
|
||||
- `server/`: Node.js Relay Server using Socket.IO (WebSocket-only).
|
||||
- `website/`: **Landing Page** & Invitation Bridge (Marketing, Tutorials, and Downloads).
|
||||
- **`build.cjs`**: Zero-dependency static site compiler. Translates `template.html` + `locales/*.json` → `www/`. Also minifies CSS/JS automatically.
|
||||
- **`www/` is auto-generated**: Never edit files in `www/` directly. Always edit source files (`template.html`, `style.css`, `app.js`, `lang-init.js`, `locales/*.json`) and run `node website/build.cjs` to regenerate. CSS/JS are output as `.min.*` files — a built-in cleanup step removes stale artifacts on each build.
|
||||
- `shared/`: **Single Source of Truth** for protocol constants and event names.
|
||||
- `scripts/`: Development utilities (e.g., `build-extension.js`).
|
||||
- `scripts/`: Development utilities (e.g., `build-extension.cjs`).
|
||||
- `docker-compose.yml`: Root-level orchestration for the relay server.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Single Source of Truth**: `shared/constants.js` and `shared/blacklist.js` are the master files. They must be synchronized to the `extension/shared/` directory using `node scripts/build-extension.js`.
|
||||
> **Single Source of Truth**: `shared/constants.js` and `shared/blacklist.js` are the master files. They must be synchronized to the `extension/shared/` directory using `node scripts/build-extension.cjs`.
|
||||
> - **Extension Modules** (`background.js`, `popup.js`) import directly from `./shared/constants.js`.
|
||||
> - **Content Scripts** (`content.js`) use a **marker-injected synchronous copy** of the constants. The build script automatically replaces the marked blocks — no manual mirroring needed.
|
||||
|
||||
## 3. Mandatory Reading
|
||||
Before touching any code, you MUST read the following documents in order:
|
||||
1. [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) – Detailed communication flows, Dual Heartbeat, and two-phase sync protocol.
|
||||
2. [extension/README.md](extension/README.md) – Extension components, tab structure, and loading process.
|
||||
3. [docs/SYNC_GUIDE.md](docs/SYNC_GUIDE.md) – Protocol constants and synchronization requirements.
|
||||
1. [ARCHITECTURE.md](ARCHITECTURE.md) – Detailed communication flows, Dual Heartbeat, and two-phase sync protocol.
|
||||
2. [extension/README.md](../extension/README.md) – Extension components, tab structure, and loading process.
|
||||
3. [SYNC_GUIDE.md](SYNC_GUIDE.md) – Protocol constants and synchronization requirements.
|
||||
|
||||
## 4. The "Vanilla JS Mirror" Pattern
|
||||
To avoid boot-time race conditions in Manifest V3 without a bundler, the following architectural trade-off is enforced:
|
||||
- **Synchronous Execution**: `content.js` MUST execute synchronously to catch early media events.
|
||||
- **Automated Injection**: The build script (`node scripts/build-extension.js`) automatically injects `EVENTS` and `HEARTBEAT_INTERVAL` into `content.js` using marker-based replacement (see `scripts/README.md` for marker details).
|
||||
- **Automated Injection**: The build script (`node scripts/build-extension.cjs`) automatically injects `EVENTS` and `HEARTBEAT_INTERVAL` into `content.js` using marker-based replacement (see `../scripts/README.md` for marker details).
|
||||
- **Maintenance**: After modifying `shared/constants.js`, simply run the build script. No manual mirroring is required.
|
||||
|
||||
## 5. File Responsibility Map
|
||||
@@ -68,7 +73,7 @@ The following features are critical and must not be removed or fundamentally alt
|
||||
- **Two-Phase Force Sync**: The `Prepare` → `ACK` → `Execute` flow ensures all peers are buffered before playback resumes.
|
||||
- **Episode Auto-Sync**: Ensures series binges stay perfectly synced. A lobby initiates during title transitions, freezing peers until everyone is ready.
|
||||
- **Dual Heartbeat**:
|
||||
- **Background Heartbeat (30s)**: Ensures session persistence even without a video element.
|
||||
- **Background Heartbeat (1m)**: Ensures session persistence even without a video element.
|
||||
- **Content Heartbeat (15s)**: Transmits current video metadata (time, title).
|
||||
- **Dead Peer Pruning**: Server "Reaper" disconnects peers after 5 minutes of total silence (no heartbeats or events).
|
||||
- **Deduplication**: Server kills old sockets if a user re-joins with the same `peerId` to prevent ghosts.
|
||||
@@ -85,6 +90,7 @@ The following features are critical and must not be removed or fundamentally alt
|
||||
- **Server Transport**: Restricted to `websocket` only. Polling is disabled.
|
||||
- **Docker Context**: The Docker build must run from the **Repo Root**.
|
||||
- **Manifest Settings**: `run_at` must remain `document_idle`, and `all_frames` must remain `false`.
|
||||
- **Strict Backward & Forward Compatibility (Store Delay Rule)**: Browser extensions are distributed through stores (e.g., Chrome Web Store, Firefox Add-ons) which can take up to 2 weeks to approve updates. Therefore, the server MUST NOT reject older extension clients unless a critical protocol version bump is explicitly authorized, and new extension versions MUST remain fully operational when connected to older servers (e.g., by silently falling back if a new feature is not supported). This is a core architectural requirement.
|
||||
|
||||
## 9. Security & Deployment
|
||||
- **Tokens**: Security tokens are intentionally managed via `shared/constants.js` and server `.env`.
|
||||
@@ -94,29 +100,55 @@ The following features are critical and must not be removed or fundamentally alt
|
||||
|
||||
## 10. Common Workflows
|
||||
|
||||
## CRITICAL: Git & Release Rules
|
||||
|
||||
- **NEVER** push, commit, tag, or release without explicit user instruction.
|
||||
- **NEVER** retag or force-push without explicit instruction.
|
||||
- **NEVER** create tags for documentation-only or README changes.
|
||||
- **NEVER** run `git push`, `git tag`, `git commit` unless the user says "push", "commit", or "tag".
|
||||
- Only the user decides when to commit, push, tag, or release.
|
||||
- Ask before any git write operation.
|
||||
|
||||
### ⚠️ Pre-Session Git Sync (MANDATORY)
|
||||
Before starting any task, committing, or pushing, you **MUST** run `git pull --rebase` to ensure your local branch is up-to-date with `origin/main`. CI pipelines and other agents may push commits concurrently. Skipping this step will cause merge conflicts and rejected pushes.
|
||||
|
||||
### Releasing a New Version (CRITICAL WORKFLOW FOR AI AGENTS)
|
||||
> [!CAUTION]
|
||||
> **AI AGENTS MUST FOLLOW THIS EXACT SEQUENCE WHEN RELEASING A NEW VERSION OR TAGGING.**
|
||||
> The CI pipeline automatically injects the version from the git tag into `manifest.base.json`, `shared/constants.js`, and `package.json`. You do NOT need to manually bump version numbers.
|
||||
>
|
||||
> **🚫 NO MANUAL VERSION BUMPING**: You MUST **NEVER** manually modify the version strings in `package.json`, `extension/manifest.base.json`, or `website/version.json`. The GitHub Actions CI pipeline automatically extracts the version from the git tag (e.g. `v2.0.5` -> `2.0.5`), injects it into all target files, and commits the updates back to `main` with `[skip ci]`. Manual bumps will cause merge conflicts and build failures.
|
||||
> - **Website Versioning**: **NEVER** manually modify the version fallback strings in `website/index.html`. The website dynamically fetches the latest version and release date from `website/version.json` at runtime using `website/app.js`. Manual bumps in the HTML file are completely redundant and should be avoided.
|
||||
1. **MANDATORY SYNTAX & LINT CHECKS**: Before staging, committing, or pushing any changes, you **MUST** run both checks on every modified JavaScript file:
|
||||
- **Syntax Validation**: Run `node -c` on every single modified JavaScript file (e.g., `node -c extension/background.js` and `node -c extension/content.js`). **NEVER** commit or push code that fails this check.
|
||||
- **ESLint Validation**: Run `npm run lint` (or `npx eslint .`). The output must show **zero errors and zero warnings**. ESLint is configured to catch undefined variables, unused vars, unreachable code, and other semantic issues. **NEVER** commit or push code that fails this check.
|
||||
2. Commit all verified code changes and push to `main`.
|
||||
3. Create and push a new tag. **MANDATORY**: Tags MUST start with a `v` (e.g., `v1.4.0`). The GitHub Actions release workflow is strictly configured to ignore any tags without the `v` prefix.
|
||||
- **🚫 TAG IMMUTABILITY**: Once a tag is pushed to `origin`, it is **PERMANENT**. You MUST **NEVER** reuse, move, or force-push an existing tag — not even to "fix" a mistake. If a release is missing a fix, increment the version and create a **new** tag (e.g., `v1.7.0` → `v1.7.1`). Tags are immutable identifiers; moving them breaks CI pipelines, corrupts the release history, and causes unreproducible builds.
|
||||
- **🚫 WHEN NOT TO TAG**: Do NOT create a release tag for changes that do NOT affect the shipped extension or server artifacts. Website text changes, documentation updates (`.md` files), and landing page content do NOT require a version tag. Tags trigger the full CI pipeline (Docker build, extension packaging, GitHub Release) — running this for a typo fix wastes CI resources and creates meaningless releases. Only tag when extension code (`extension/`), server code (`server/`), or shared protocol constants (`shared/`) have changed.
|
||||
4. The CI will extract the version from the tag (e.g., `v1.4.0` → `1.4.0`), inject it into all source files, build the extension artifacts, publish the Docker image, and create a GitHub Release.
|
||||
5. Verify the release builds on GitHub Actions.
|
||||
|
||||
### 🚫 Force Push Policy
|
||||
> [!CAUTION]
|
||||
> **Force pushing (`git push --force` or `git push -f`) is FORBIDDEN without explicit user confirmation.**
|
||||
> - If a push is rejected due to a non-fast-forward conflict, you **MUST** run `git pull --rebase` first.
|
||||
> - If a force push is absolutely required (e.g., squashed history, amended commits), you **MUST** ask the user for explicit permission with a clear explanation of why it's necessary. Never force-push autonomously.
|
||||
> - This applies to both branches (`main`) and **tags** (see Tag Immutability above). Force-pushing tags is doubly destructive. Never do it.
|
||||
|
||||
### Adding a Protocol Event
|
||||
1. Add the event name to `shared/constants.js`.
|
||||
2. Run the build script (`node scripts/build-extension.js`).
|
||||
2. Run the build script (`node scripts/build-extension.cjs`).
|
||||
3. Implement the handler in `server/index.js` and `background.js`.
|
||||
|
||||
### Making Website Changes
|
||||
1. Edit source files in `website/` (`template.html`, `style.css`, `app.js`, `lang-init.js`, or `locales/*.json`).
|
||||
2. Run the compiler: `node website/build.cjs`. This generates the multilingual pages in `www/` and minifies CSS/JS.
|
||||
3. Verify the output: `node --check website/www/app.js && node --check website/www/lang-init.js`.
|
||||
4. Test locally: `npx serve website/www` or `python3 -m http.server 8080 -d website/www`.
|
||||
5. Commit both source changes and the updated `www/` output.
|
||||
|
||||
### Testing Locally
|
||||
1. Run the build script: `node scripts/build-extension.js`.
|
||||
1. Run the build script: `node scripts/build-extension.cjs`.
|
||||
2. Load `dist/chrome/` as an "Unpacked Extension" in Chrome (or `dist/firefox/` in Firefox).
|
||||
3. Start the server from the root: `docker-compose up --build`.
|
||||
4. Use **different browser profiles** or vendors to test multi-peer logic.
|
||||
@@ -2,27 +2,29 @@
|
||||
|
||||
This document describes the communication flows and internal logic of the KoalaSync system.
|
||||
|
||||
## 1. Extension Startup & Connection
|
||||
- **Initialization**: On startup, `background.js` reads settings (Server URL, Username, Last Room) from `chrome.storage.sync`.
|
||||
- **WebSocket Handshake**:
|
||||
## 1. Extension Connection (Lazy Connect)
|
||||
- **Initialization**: On startup, `background.js` reads settings (Server URL, Username, Last Room) from `chrome.storage.sync`. No WebSocket connection is established at this point.
|
||||
- **On-Demand Connection**: The extension only connects when needed — either the user opens the popup with saved room credentials, or when actively in a room. When not in a room, no connection exists. This improves privacy (IP not exposed while idle) and reduces battery/network usage.
|
||||
- **WebSocket Handshake (when connecting)**:
|
||||
1. Background creates a `new WebSocket` to `/socket.io/?EIO=4&transport=websocket&version=1.0.0`.
|
||||
2. Server performs security checks:
|
||||
- **IP Rate Limit**: Checks if the IP has exceeded connection limits.
|
||||
- **Protocol Version**: Client must match the server's protocol (currently `1.0.0`).
|
||||
3. Server responds with Engine.IO handshake (`0`) and the client joins the namespace (`40`).
|
||||
- **Room Join**: Background emits `JOIN_ROOM` containing `roomId`, `password`, `peerId`, and `username`.
|
||||
- **Deduplication**: If a user joins with a `peerId` that already has an active socket, the server kills the old socket to prevent "Ghost Peers".
|
||||
- **Deduplication**: If a user joins with a `peerId` that already has an active socket, the server kills the old socket to prevent "Ghost Peers". Deduplication re-validates after acquiring the room creation lock to avoid kicking the wrong socket during concurrent joins.
|
||||
|
||||
## 2. Media Event Synchronization
|
||||
When a user interacts with a video:
|
||||
1. **Detection**: `content.js` listens to native events (`play`, `pause`, `seeked`) on the `<video>` element.
|
||||
2. **Prevention of Loops**: Uses an `expectedEvents` Set to distinguish between user actions and programmatic actions. Expected events are consumed on match and expire via timeout.
|
||||
1. **Detection**: `content.js` listens to native events (`play`, `pause`, `seeked`) on the `<video>` element, including videos inside Shadow DOM (YouTube, Netflix, etc.).
|
||||
2. **Prevention of Loops**: Uses an `expectedEvents` Set to distinguish between user actions and programmatic actions. Expected events are consumed on match and expire via timeout. Timeout IDs are cleaned up immediately to prevent memory leaks.
|
||||
3. **Reporting**: `content.js` sends a `CONTENT_EVENT` to `background.js`.
|
||||
4. **Relay**: The Server forwards the event to all other peers in the room.
|
||||
5. **Execution**: Remote peers receive the command and call `video.play()`, `video.pause()`, or `video.currentTime = targetTime`.
|
||||
5. **Execution**: Remote peers receive the command via `SERVER_COMMAND` (which includes the original `senderId` for correct ACK routing) and call `video.play()`, `video.pause()`, or `video.currentTime = targetTime`.
|
||||
6. **ACK Routing**: `content.js` echoes the `commandSenderId` back in `CMD_ACK`, ensuring the `EVENT_ACK` is routed to the correct initiating peer even when multiple commands arrive concurrently.
|
||||
|
||||
## 3. Two-Phase Force Sync
|
||||
Ensures all peers are frame-perfect and buffered before resuming:
|
||||
Ensures all peers are buffered and synchronized before resuming:
|
||||
1. **Prepare**: Initiator sends `FORCE_SYNC_PREPARE` with the target timestamp.
|
||||
2. **Buffer**: Peers seek and pause. Once buffered (`readyState >= 3`), they send a `FORCE_SYNC_ACK`. (Note: `content.js` limits polling to 8000ms).
|
||||
3. **Execute**: Once the Initiator collects ACKs (or after an 8.5s timeout), they send `FORCE_SYNC_EXECUTE`.
|
||||
@@ -44,6 +46,7 @@ To maintain a clean room state and eliminate "Ghost Peers":
|
||||
- **Video Heartbeat (Content)**: Every 15 seconds, `content.js` sends current playback metadata (time, title, state) if a video is found.
|
||||
- **Server Pruning**: The server runs a "Reaper" every 2 minutes. If a peer has sent **zero** activity (no events and no heartbeats) for 5 minutes, they are forcefully disconnected.
|
||||
- **Immediate Cleanup**: Rooms are deleted instantly when the last peer leaves or disconnects.
|
||||
- **Reconnect Strategy (while in room)**: Aggressive backoff — 500ms base, 1.5x multiplier, capped at 5s. Max 20 attempts before marking as failed. Events are queued during disconnect and flushed after namespace rejoin. When not in a room, no reconnection occurs.
|
||||
|
||||
> [!CAUTION]
|
||||
> **Identity Rule**: Differentiate between `peerId` and `socket.id`. Use `socket.id` exclusively for ephemeral transport routing on the server. Use `peerId` exclusively for identity, state management, and room tracking across the stack.
|
||||
@@ -54,8 +57,13 @@ KoalaSync uses a megaphone routing approach to minimize server logic:
|
||||
- **Storm Prevention**: When dispatching state updates in response to a new user joining (e.g., an active lobby state), ensure ONLY the initiator (or a designated leader) calls `emit()` to prevent $O(N)$ broadcast storms.
|
||||
|
||||
## 7. Security & Stability
|
||||
- **Service Worker Lifecycle**: Uses `chrome.alarms` to prevent the Manifest V3 service worker from suspending while in an active room.
|
||||
- **Rate Limiting**: Server-side per-socket and per-IP rate limits to prevent sync-spamming or DoS.
|
||||
- **Service Worker Lifecycle**: Uses `chrome.alarms` (30s interval) to prevent the Manifest V3 service worker from suspending while in an active room. On wake, runtime state is restored from `chrome.storage.session` via `ensureState()`.
|
||||
- **Reconnect Visualization**: Badge shows "..." (orange) during reconnect. Popup displays "Reconnecting..." with attempt counter.
|
||||
- **Rate Limiting**: Server-side per-socket and per-IP rate limits to prevent sync-spamming or simple DoS. Public health endpoints are limited to 10 requests/minute/IP and cached server-side for 60 seconds, wrong admin-metrics bearer attempts to 5 requests/minute/IP, and room discovery to one request every 10 seconds per socket. Real client IP is taken from the trusted reverse proxy hop, so the Node port must stay private behind Caddy or another trusted proxy.
|
||||
- **Room Creation Lock**: Per-room mutex prevents race conditions when multiple peers join a new room simultaneously.
|
||||
- **CORS**: Allows `chrome-extension://` origins for WebSocket fallback compatibility.
|
||||
- **Message Buffer**: `maxHttpBufferSize` set to 4KB to accommodate large `JOIN_ROOM` payloads.
|
||||
- **Process Guards**: `uncaughtException` and `unhandledRejection` handlers prevent silent server crashes.
|
||||
- **Noise Filtering**: Uses a curated blacklist of domains (Search Engines, Social Media) to declutter the "Target Tab" selector in the popup.
|
||||
- **Diagnostics**: A "Dev" tab provides real-time access to the underlying `<video>` state (`readyState`, `paused`, `currentTime`) for easier troubleshooting.
|
||||
|
||||
@@ -63,5 +71,5 @@ KoalaSync uses a megaphone routing approach to minimize server logic:
|
||||
To maintain a "Single Source of Truth" across the server and extension without using a bundler:
|
||||
- **Relay Server & Extension Modules**: `background.js` and `popup.js` import constants directly from `shared/constants.js`.
|
||||
- **Content Scripts**: To ensure zero-latency execution, `content.js` uses a synchronized copy of `EVENTS` and constants.
|
||||
- **Automation**: The `node scripts/build-extension.js` script automatically injects these constants into `content.js` during the build process, eliminating the risk of manual mirror mismatch.
|
||||
- **Automation**: The `npm run build:extension` script automatically injects `EVENTS`, `HEARTBEAT_INTERVAL`, and `episode-utils.js` functions into `content.js` during the build process, eliminating the risk of manual mirror mismatch.
|
||||
- **Verification**: Any protocol change is automatically propagated across the stack by running the build script.
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
# KoalaSync Changelog
|
||||
|
||||
All notable changes to the KoalaSync browser extension and relay server.
|
||||
|
||||
---
|
||||
|
||||
## Unreleased
|
||||
|
||||
---
|
||||
|
||||
## [v2.5.3] — 2026-07-02
|
||||
|
||||
### Fixed
|
||||
- **Extension: Disney+ force sync and seek reliability** — The v2.5.2 Disney+ page-API integration leaked blob-relative `<video>` time into force sync, seeks, and heartbeats when the page-API bridge had no fresh data, which presented as "force sync does nothing on Disney+". Time/duration accessors now refuse to return native values on Disney+ (returning null/0) so stale bridge data degrades to a clean no-op instead of broadcasting garbage. `FORCE_SYNC_PREPARE` and `SEEK` payloads are now validated as finite before being relayed, and the popup's force-sync flow fails cleanly with a clear error rather than sending NaN through.
|
||||
- **Extension: Force-sync no longer routed twice** — A popup-initiated `FORCE_SYNC_PREPARE` was being delivered to the content script twice (once by the generic popup route and once by the force-sync-specific route), causing a double seek. The generic route is now scoped to play/pause/seek only.
|
||||
- **Extension: Disney+ Host Control Mode classification** — `hcmIsLive` previously read the native `<video>` duration to detect live streams, which on Disney+ is blob-relative garbage and falsely classified every stream as live (disabling snap-back and the desync dialog). The native-duration live signal is now skipped on Disney+ while YouTube/Twitch live detection via `Infinity` duration is preserved.
|
||||
- **Extension: Disney+ episode auto-sync and lobby readiness** — Episode-transition detection and the episode-lobby "ready" poll now read the playhead through the gated time accessor, so they no longer rely on blob-relative `currentTime` on Disney+.
|
||||
- **Extension: Force-sync median no longer skewed by peers without a known time** — A peer broadcasting `currentTime: null` (e.g. a Disney+ peer whose page-API bridge was not yet ready, or a freshly joined peer) was coerced to `0` by the jump-to-others median calculation, dragging the sync target toward the start of the video. Null and empty peer times are now excluded before the median.
|
||||
- **Extension: Force-sync `jump-to-me` retry on Disney+** — When the content script responds but the page-API bridge has not yet delivered a finite time (typical during the first ~250 ms after a Disney+ player loads), the popup now retries once without redundantly re-injecting the content script.
|
||||
- **Extension: Empty-string seek payload rejection** — The internal seek-time coercion no longer treats `''` as `0`; an empty-string `targetTime`/`currentTime` is rejected as invalid.
|
||||
|
||||
---
|
||||
|
||||
## [v2.5.2] — 2026-07-02
|
||||
|
||||
### Added
|
||||
- **Extension: Privacy title controls** - Advanced users can now disable sending browser tab titles separately from media titles. Media titles can still be sent in full, reduced to detected episode identifiers such as `S01E04`, or hidden entirely. Defaults remain full titles for backwards compatibility.
|
||||
- **Relay: Cleaner restart handling** — Connected clients are now disconnected explicitly during relay shutdown so reconnects recover more predictably.
|
||||
- **Relay: Stronger abuse protection** — Rapid room-leave spam is now rate-limited.
|
||||
- **Extension: Hidden remote seek diagnostics** — KoalaDev can use the hidden Dev tab to simulate remote seeks and inspect precise native/page-API timing while debugging playback integrations.
|
||||
|
||||
### Changed
|
||||
- **Extension: Shared page-API seek bridge** — Netflix and Disney+ now use a common page-level seek bridge so private player APIs can be invoked from the page context while the default HTML5 path stays unchanged.
|
||||
- **Build: Release build timestamp** — Extension builds now inject a build timestamp into the hidden Dev tab for easier local package verification.
|
||||
|
||||
### Fixed
|
||||
- **Extension: Disney+ precise sync** — Disney+ now reads time and seeks through the real page media-player API, and the temporary DOM timeline/button scraping fallback has been removed.
|
||||
- **Extension: Netflix seek reliability** — Netflix seeking keeps using the page player API with a safer session lookup path.
|
||||
- **Extension: Tab-title counter cleanup** — Leading browser notification counters such as `(14)` or `[7]` are removed from shared tab titles and matching logic without changing the existing privacy controls.
|
||||
- **Extension: Tab navigation reinjection** — Reinjecting the content script after selected-tab navigation now uses the same page-API-aware injection path.
|
||||
|
||||
---
|
||||
|
||||
## [v2.5.0] — 2026-06-29
|
||||
|
||||
### Added
|
||||
- **Extension + Relay: Host Control Mode** — Room owners can now switch a room between open playback control and host-controlled playback. In host-only mode, guests stay synchronized but their local play, pause, and seek actions are not rebroadcast to the room.
|
||||
- **Backward-compatible Host Control rollout** — The extension only shows Host Control when the connected relay supports it, so users on older self-hosted servers do not see controls that cannot work yet.
|
||||
- **Extension: Clear host and guest states** — The popup shows the current control mode, host status, peer roles, and localized guest guidance so participants understand when playback is controlled by the host.
|
||||
- **Website: FAQ clarification for streaming access** — The landing page and FAQ structured data now state clearly that KoalaSync does not stream, host, share, or bypass access to video content. Every participant watches locally and needs their own access to services such as Netflix.
|
||||
|
||||
### Changed
|
||||
- **Playback sync now follows the room's control setting** — When Host Control is enabled, only the host can drive room-wide playback changes; guests can still watch in sync without accidentally changing playback for everyone.
|
||||
|
||||
---
|
||||
|
||||
## [v2.4.6] — 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
|
||||
- **Extension: Onboarding tour now has a closing step** — The first-run tour ends on a dedicated "You're all set!" card (the `ONBOARDING_5` copy that already existed in all 13 locales but was never shown). The tour no longer stops abruptly on the username step.
|
||||
- **Extension: One-click invite from the empty peer list** — The "No peers yet" state now shows a **📋 Invite Link** button that copies the invite link to the clipboard, so users can share it without hunting for the field.
|
||||
|
||||
### Changed
|
||||
- **Extension: Cleaner onboarding welcome** — Step 1 is now a centered welcome card instead of spotlighting the logo title. Added a guard so target-less tour steps center cleanly.
|
||||
- **Website: Mobile comparison table** — The KoalaSync vs Teleparty table stacks into per-feature cards on phones instead of forcing horizontal scrolling; feature descriptions are shown again on mobile.
|
||||
|
||||
### Fixed
|
||||
- **Extension: Onboarding step counter/progress placeholders** — Static `Step 1 of 3` / 33% fallbacks in `popup.html` corrected to match the actual 5-step tour (`Step 1 of 5` / 20%).
|
||||
- **Website: Mobile navigation restored** — The header hamburger menu was hidden by a `display:none !important` rule, leaving the nav links unreachable on phones. Re-enabled, with spacing kept comfortable down to ~320px.
|
||||
- **Website: Hero alignment on mobile** — A fixed-width extension mockup forced the hero grid column wider than the container, shifting all hero content off-center (larger left margin than right). The mockup is now responsive (`width:100%/max-width` + `minmax(0,1fr)` grid track).
|
||||
- **Website: Reveal-animation fallback** — Added a `<noscript>` style fallback and `IntersectionObserver` feature guards so scroll-revealed content can never stay invisible if JavaScript is disabled or unsupported.
|
||||
|
||||
## [v2.4.0] — 2026-06-16
|
||||
|
||||
### Added
|
||||
- **Extension: Lazy WebSocket connection** — The extension no longer maintains a permanent WebSocket connection to the relay server. Instead, the connection is established only when actively in a room or when the popup is opened with a saved room configuration. This improves privacy (IP is not exposed while idle), reduces battery/network usage, and prevents the server from tracking online status of inactive users. Automatic reconnect is guaranteed while in a room — zero behavior change during active sync sessions. See `connectIntent` flag in `background.js`.
|
||||
- **Extension: Episode title regex unification** — `extractEpisodeId()` had inconsistent regex patterns between `background.js` and `content.js`. The content script correctly matched Crunchyroll-style separators (`S01/E01`) while the service worker's stricter pattern (`[\s\-\.]*`) silently rejected them, causing episode lobby sync failures. Now unified to `[^a-zA-Z0-9]*` via shared `episode-utils.js`.
|
||||
- **Unit tests: `rate-limiter` and `episode-utils`** — 12 test groups for rate-limit functions and 30+ assertions for episode title parsing, covering all 6 separator types (dash, dot, slash, colon, comma, space). Run automatically via `npm run verify`.
|
||||
|
||||
### Changed
|
||||
- **Server: Rate limiter extracted to `rate-limiter.js`** — 6 rate-limit functions, all rate-limit Maps, and cleanup intervals moved from `index.js` (149 lines). `index.js` now imports via facade pattern with re-exports for backward compatibility.
|
||||
- **Extension: Episode utilities extracted to `episode-utils.js`** — `extractEpisodeId()` and `sameEpisode()` deduplicated from `background.js` and `content.js`. The shared module is imported as an ES module by the service worker and injected into the content script IIFE by the build script.
|
||||
- **Build: `"type": "module"` in root `package.json`** — All scripts standardized to ESM (`.mjs`) or explicitly CommonJS (`.cjs`). Eliminated Node.js `MODULE_TYPELESS_PACKAGE_JSON` warnings.
|
||||
- **Build: 4 CJS scripts renamed to `.cjs`** — `build-extension.js`, `test-content-video-finder.js`, `test-locales.js`, `website/build.js`.
|
||||
|
||||
### Fixed
|
||||
- **Server: npm audit resolved** — `ws` package vulnerability (CVE-2024-37890) fixed. Zero vulnerabilities in production dependencies.
|
||||
- **Pop-up: Connection status flicker fixed** — Removed hardcoded `disconnected` state on every pop-up open. Status now reflects actual background state from the first frame.
|
||||
- **Pop-up: Join button timeout improved** — No longer blindly re-enables after 15s. Polls connection status and extends window if still connecting.
|
||||
- **Pop-up: Validation failure state cleanup** — Custom server URL validation errors now properly reset `isProcessingConnection` and `joinBtnTimeout`.
|
||||
- **Extension: `WEB_JOIN_REQUEST` channel leak fixed** — Missing `sendResponse()` call when already in the target room.
|
||||
- **Extension: `LEAVE_ROOM` now clears `roomId` from storage** — Prevents phantom auto-reconnect on browser restart after explicit leave.
|
||||
- **Extension: Reconnect attempt counters reset on leave** — Prevents stale `reconnecting` status display after intentional disconnect.
|
||||
|
||||
## [v2.3.2] — 2026-06-16
|
||||
|
||||
### Changed
|
||||
- **Extension: Refined Spanish, Italian, and Portuguese translations**: Complete manual review and improvement of all Spanish (`es`), Italian (`it`), Portuguese — Brazil (`pt-BR`), and Portuguese — Portugal (`pt`) locale files for both the extension UI and the landing website. Thanks to [@Kaia-Alenia](https://github.com/Kaia-Alenia) for the native-quality translations.
|
||||
|
||||
### Fixed
|
||||
- **Extension: Locale typos and corrupted characters fixed**: Repaired a Korean refresh button label (`Refreschi` → `새로고침`), a corrupted Korean connection status string (`연kel` → `연결`), a Korean character contaminating a Japanese string (`의` → `の`), and a Dutch typo (`cmmuniceren` → `communiceren`).
|
||||
- **Server: Admin token length leak fixed (timing side-channel)**: `isAdminMetricsAuthorized()` returned early when the provided buffer had a different length than the expected token, leaking the token length via response timing. Now `crypto.timingSafeEqual` runs in constant time on every attempt regardless of length match. Reported by [@Kaia-Alenia](https://github.com/Kaia-Alenia).
|
||||
|
||||
---
|
||||
|
||||
## [v2.3.1] — 2026-06-15
|
||||
|
||||
### Fixed
|
||||
- **Server: Concurrent peer join race condition and teardown error handling**
|
||||
|
||||
### Changed
|
||||
- **Server: Smart unhandled rejection handling (exits after 5/min instead of 1)**
|
||||
- **Server: Optimized admin health metrics allocation**
|
||||
|
||||
---
|
||||
|
||||
## [v2.3.0] — 2026-06-14
|
||||
|
||||
### Added
|
||||
- **Extension: New Interactive Onboarding Tour**: A fully redesigned, interactive step-by-step onboarding experience.
|
||||
- **Extension: Auto-Switch to Sync Tab**: The UI now intelligently switches to the Sync tab when you join a room to guide video selection.
|
||||
- **Extension: Uninstall URL Integration**: Prepared an uninstall URL setup that works natively across Chrome and Firefox, cleanly attaching browser context for analytics.
|
||||
|
||||
### Fixed
|
||||
- **Extension: Infinite Seek Loop Prevention**: Replaced the fragile time-based seek suppression with an exact target-time verification mechanism, entirely eliminating infinite seek loops on slow buffers.
|
||||
- **Extension: Zombie Connections Resolved**: Implemented a forced disconnect upon ping timeouts, ensuring the extension reliably auto-reconnects when the WebSocket hangs in a half-open state.
|
||||
- **Extension: Room Switching Architecture**: Joining a new room while already connected now explicitly severs the old connection first, preventing state cross-contamination.
|
||||
- **Extension: Join/Leave Race Conditions**: Added UI locks to prevent users from accidentally sending conflicting connection commands via rapid double-clicking.
|
||||
- **Extension: Same-Room Invite Bypass**: Clicking an invite link for the room you are currently in no longer triggers a redundant reconnect, instead instantly confirming the join.
|
||||
- **Extension: Audio settings now propagate immediately to video tabs**: Changes made in the audio options page are now instantly applied to the active video tab. Previously, settings saved to `chrome.storage.local` were not picked up by the background listener, which only watched `chrome.storage.sync`.
|
||||
- **Extension: Audio compressor now logs enable/disable state and resume failures**: The compressor reports when it is activated or bypassed, and warns if the `AudioContext` cannot be resumed (e.g. browser autoplay policy requires a user gesture on the page first).
|
||||
- **Extension: Video heartbeat no longer sent when alone in a room**: The full media metadata `PEER_STATUS` is now only emitted when other peers are present. The session keepalive (background heartbeat) continues to run unaffected, preventing the server reaper from disconnecting idle peers.
|
||||
- **Server: Increased `failedAuthAttempts` eviction threshold from 50k to 200k**: Reduces frequency of expensive batch evictions under high auth-failure volumes, smoothing heap usage.
|
||||
|
||||
---
|
||||
|
||||
## [v2.2.4] — 2026-06-10
|
||||
|
||||
### Fixed
|
||||
- **Extension: Error notifications now respect `browserNotifications` setting**: Server error events (e.g. "Server is restarting") no longer trigger a browser notification when the user has disabled notifications in the extension settings.
|
||||
- **Server: Misleading reconnect message corrected**: The graceful shutdown message no longer tells users to manually reconnect — the extension handles this automatically.
|
||||
|
||||
---
|
||||
|
||||
## [v2.2.3] — 2026-06-10
|
||||
|
||||
### Added
|
||||
- **Artifact Attestations (Supply Chain Security)**: All release artifacts (Docker images, extension ZIPs) are now published with signed [SLSA provenance attestations](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations) via `actions/attest@v4`. Anyone can verify that an artifact was built from this repository using `gh attestation verify`.
|
||||
- **Admin health: `rateLimits.denied` counters**: New rolling counters track actual rate-limit denials (429 responses), separate from `rateLimits.trackedClients` which reports unique IPs in the tracking window.
|
||||
- **Docker HEALTHCHECK**: Container health is now checked every 30s via `GET /health`.
|
||||
- **`npm start` script**: Server can now be started with `npm start`.
|
||||
|
||||
### Fixed
|
||||
- **Server: `activeLobby` no longer silently overwritten**: If a second peer sends `EPISODE_LOBBY` while a lobby is already active, the request is now ignored instead of destroying the first peer's lobby.
|
||||
- **CORS log sanitization**: Rejected origin headers are sanitized (`\r\n` stripped) to prevent log injection.
|
||||
- **Extension: pagehide resource leak**: `keepAlivePort`, `lobbyPollTimer`, heartbeats, and `MutationObserver` are now properly cleaned up when a tab is hidden or enters bfcache.
|
||||
- **Extension: unhandled storage rejections**: `chrome.storage.session.set()` calls in the disconnect handler now have `.catch(() => {})`.
|
||||
- **`'Pixel'` duplicate in name generator**: Second occurrence replaced with `'Nitro'` for better name diversity.
|
||||
- **`'opposum'` typo**: Corrected to `'opossum'` in the emoji map and added `'Opossum'` to `USERNAME_NOUNS`.
|
||||
- **Test reliability**: `test-server-routes.mjs` now sets `ADMIN_METRICS_TOKEN` before importing the server module, fixing standalone test execution.
|
||||
- **`MAX_PEERS_PER_ROOM` konsistent**: `.env` auf 25 gesetzt (wie `.env.example`).
|
||||
- **pt-BR.json duplicate removed**: Duplicate `FOOTER_DISCLAIMER` key removed from website locale.
|
||||
|
||||
### Changed
|
||||
- **Admin health: `rateLimitEntries` renamed to `rateLimits.trackedClients`**: The field now accurately describes that it tracks unique clients in the rate-limit window, not denial counts. Update your json_exporter/Grafana config accordingly.
|
||||
- **README restructured**: Sections reordered by progressive technical depth. New "Supply Chain Security" subsection under "For Developers & Self-Hosters" with verification commands.
|
||||
|
||||
---
|
||||
|
||||
## [v2.2.2] — 2026-06-09
|
||||
|
||||
### Added
|
||||
- **Chrome Web Store i18n Support**: Added `default_locale: "en"` to manifest and created `_locales/*/messages.json` for all 13 supported languages. This unlocks the language selection dropdown in the Chrome Web Store dashboard, allowing translated store listings (title, description) per locale. The extension's own UI translations (`locales/*.json` + `i18n.js`) remain unchanged.
|
||||
- **Locale test coverage**: Extended `scripts/test-locales.js` to validate all `_locales/*/messages.json` files (correct format, required keys, no duplicates) and verify `default_locale` is set in the manifest.
|
||||
|
||||
### Fixed
|
||||
- **Copy Logs button alignment**: Removed stray `margin-top: 8px` inherited from `.secondary` class that pushed the button 8px down in the connection status row.
|
||||
|
||||
---
|
||||
|
||||
## [v2.2.1] — 2026-06-09
|
||||
|
||||
### Added
|
||||
- **Server Ping Display**: Measures round-trip latency to the relay server via application-level ping/pong events. The extension sends `PING { t }` every 15 seconds; the server responds with `PONG { t }`. Round-trip time is calculated client-side and displayed in the Status tab, color-coded (<50ms green, 50–150ms yellow, >150ms red). No ping value is shown when disconnected or if the server does not respond within 5 seconds.
|
||||
- **Peer Ping Response (Future-Proof)**: The extension can now respond to incoming `PING { t, sender }` events from other peers by sending back `PONG { t, target: sender }`. The relay server forwards `PING` to the target peer and routes `PONG` back to the original sender. Both client and server validate that peers are in the same room before forwarding/routing. Peer-to-peer ping initiation will be activated in a future extension update without requiring a server restart.
|
||||
|
||||
---
|
||||
|
||||
## [v2.2.0] — 2026-06-08
|
||||
|
||||
### Added
|
||||
- **Web Audio API Compressor**: Built-in audio dynamic range compression with four presets (Recommended, Dynamic Range, Vocal Enhancement, Smooth) and fully customizable sliders (threshold, ratio, knee, attack, release). Uses dry/wet crossfade (40ms linear ramp) to avoid clicks. Configured via the new Audio Options page accessible from the Settings tab.
|
||||
- **Audio Options Page** (`audio-options.html`): Dedicated settings page with master toggle, compressor preset selector, real-time custom sliders, and equalizer placeholder. Dark theme matching the popup design.
|
||||
- **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
|
||||
- **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".
|
||||
- **Korean locale**: Fixed broken character in `HOWTO_STEP_2_TEXT` (`클rip보드` → `클립보드`).
|
||||
- **Website COMP_FEAT_6_KOALA**: Normalized from inconsistent "6 Languages" to "13 Languages" across all locale files (en, de, es, fr, pt-BR, ru).
|
||||
- **Debug report showing wrong logs**: Fixed `logs.slice(-50)` and `history.slice(-20)` in the "Copy Debug Report" feature. Since `addLog()` and `addToHistory()` use `unshift` (inserting entries at index 0), the arrays are ordered newest-first. `slice(-N)` took the N **oldest** entries instead of the N **newest**. Changed to `slice(0, N).reverse()` to correctly include the most recent logs and display them chronologically.
|
||||
|
||||
---
|
||||
|
||||
## [v2.1.2] — 2026-06-06
|
||||
|
||||
### Fixed
|
||||
- **Episode guard regex**: Fixed `isDifferentEpisode()` not detecting episode changes when the MediaSession title uses `Sxx:Exx` format (colon separator, as used by Jellyfin/Emby). The regex character class `[\s\-\.]` was replaced with `[^a-zA-Z0-9]` to match **any** non-alphanumeric separator between season and episode numbers, preventing play/pause/seek commands from a different episode leaking through and incorrectly manipulating a peer's playback.
|
||||
- **Per-device storage isolation**: Migrated `username`, `roomId`, `password`, `serverUrl`, and `useCustomServer` from `chrome.storage.sync` (synced across Google account) to `chrome.storage.local` (per-device). This prevents the extension from automatically joining the same room with the same name on multiple devices. Existing user data is migrated silently on first run; all preferences (`filterNoise`, `autoSyncNextEpisode`, etc.) remain synced.
|
||||
|
||||
### Changed
|
||||
- Added one-time migration fallback in `getSettings()` and popup `init()` to copy existing user settings from `storage.sync` to `storage.local` on first launch after the update.
|
||||
|
||||
---
|
||||
|
||||
## [v2.1.0] — 2026-06-04
|
||||
|
||||
### Added
|
||||
- Added full translation support for 7 new languages to both the browser extension popup settings and landing website: Italian (`it`), Polish (`pl`), Turkish (`tr`), Dutch (`nl`), Japanese (`ja`), Korean (`ko`), and European Portuguese (`pt`).
|
||||
- Implemented robust, centralized browser system language detection mapping `pt-BR` to Brazilian Portuguese and other `pt` locales (like `pt-PT`) automatically to European Portuguese.
|
||||
- Added flag emojis to language selector dropdowns in both the extension popup and landing/utility web pages for quicker visual identification.
|
||||
- Added 181 translation keys parity validation suite checks for the new languages.
|
||||
|
||||
### Fixed & Hardened (Extension Audit)
|
||||
- Guarded all website `localStorage` interactions to prevent initialization/join flow script failures on privacy-hardened or cookie-blocked browser configurations.
|
||||
- Added robust validation null-guards to `chrome.runtime.onMessage` listeners across all extension scripts (`bridge.js`, `content.js`, `background.js`, `popup.js`) to reject unexpected runtime messages.
|
||||
- Guarded CustomEvent payload destructuring in `bridge.js` to ensure stability when receiving third-party page events.
|
||||
- Wrapped `video.currentTime` seeking adjustments during forced sync in content scripts with exception handling to absorb uninitialized video state DOMExceptions.
|
||||
- Added payload validation guards on incoming Socket.IO events within the background script's event handlers to secure against malformed server updates.
|
||||
- Prevented noisy browser console exceptions from context invalidation in target tabs by catching promise rejections on extension message dispatches.
|
||||
|
||||
### Performance
|
||||
- Implemented in-memory language dictionary caching in the background script to completely avoid redundant extension package filesystem reads during translations.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## [v2.0.8] — 2026-06-03
|
||||
|
||||
### Fixed
|
||||
- Fixed a bug where switching language inside the extension popup overwrote dynamic fields (such as active room ID, connection status, active server details, and video debug info) with default localized placeholder texts.
|
||||
- Fixed a version reporting mismatch where the copied logs (debug reports) and connection handshake parameters incorrectly reported the hardcoded `1.9.0` version instead of the actual installed manifest version.
|
||||
|
||||
---
|
||||
|
||||
## [v2.0.7] — 2026-06-03
|
||||
|
||||
### Added
|
||||
- Added a `DEBUG_LOGGING` environment variable to the relay server (defaulting to `"0"` / disabled) to prevent console spam from verbose connection (`CONN`), room activity (`ROOM`, `DEDUPE`), and `CORS` events under load. Critical logs like `SERVER`, `SECURITY`, `AUTH`, and `ERROR` remain enabled at all times.
|
||||
|
||||
---
|
||||
|
||||
## [v2.0.6] — 2026-06-03
|
||||
|
||||
### Performance & Security Hardening
|
||||
- Optimized failed authentication attempts cache eviction algorithm to $O(1)$ by exploiting Javascript `Map` insertion-order properties. This completely removes the previous array copying and sorting bottleneck, neutralizing a potential main-thread blocking DoS vector under heavy brute-force password traffic.
|
||||
|
||||
---
|
||||
|
||||
## [v2.0.5] — 2026-06-03
|
||||
|
||||
### Security & Hardening
|
||||
- Hardened extension room idle auto-leave detection to correctly recognize when the target tab's video heartbeat goes stale (e.g., after tab navigation or media closure).
|
||||
- Exported cleaner graceful shutdown and lifecycle methods (`stopServerForTests`) from the relay server to prevent socket leaks and port-binding conflicts during verify checks.
|
||||
|
||||
### Added
|
||||
- Added a validation step in `test-locales.js` to ensure the supported language list in `extension/i18n.js` is perfectly synchronized with the actual JSON translation files in the locales directory.
|
||||
- Added a robust route verification test suite (`scripts/test-server-routes.mjs`) covering rate limit throttling, caching headers, and admin metrics access control.
|
||||
|
||||
---
|
||||
|
||||
## [v2.0.4] — 2026-06-03
|
||||
|
||||
### Security & Hardening
|
||||
- Hardened relay health endpoints against simple flood traffic: `GET /` and `GET /health` are now limited to 10 requests per minute per client IP.
|
||||
- Added lazy 60-second server-side caching for `GET /`, basic `/health`, and admin `/health` JSON responses to reduce repeated health-check work under noisy polling.
|
||||
- Added stricter brute-force throttling for invalid admin metrics bearer attempts.
|
||||
- Added startup warning for short `ADMIN_METRICS_TOKEN` values and documented that production Node ports must stay private behind Caddy or another trusted reverse proxy.
|
||||
- Lowered the default maximum peers per room to 25.
|
||||
|
||||
### Added
|
||||
- Optional privacy-preserving admin metrics on `/health` when `ADMIN_METRICS_TOKEN` is configured and a valid bearer token is supplied. Metrics are aggregate-only and exclude room IDs, peer IDs, usernames, IP addresses, media titles, passwords, and other user-level data.
|
||||
|
||||
### Changed
|
||||
- Removed `bcryptjs`; temporary room passwords continue to use keyed SHA-256/HMAC hashing as documented.
|
||||
- Public room discovery is now rate-limited server-side to one refresh every 10 seconds per socket, with the extension refresh button locked for 11 seconds.
|
||||
|
||||
### Fixed
|
||||
- Improved Shadow DOM video detection so real embedded players are not hidden by smaller light-DOM preview or placeholder videos.
|
||||
- Fixed join-button timeout cleanup after join status responses.
|
||||
|
||||
---
|
||||
|
||||
## [v2.0.2] — 2026-06-02
|
||||
|
||||
### Fixed
|
||||
- Peer identity spoofing in relay server: client-supplied `peerId` could be used to impersonate other peers in PEER_STATUS events. Server now always stamps `peerId` with the authenticated sender's identity.
|
||||
- Amazon domain detection: replaced broad `includes('amazon.')` substring check with boundary-safe regex that correctly matches all Amazon storefronts (`amazon.com`, `amazon.de`, `amazon.co.uk`, etc.) while rejecting lookalike domains.
|
||||
|
||||
---
|
||||
|
||||
## [v2.0.1] — 2026-06-01
|
||||
|
||||
### Fixed
|
||||
- Video detection on Prime Video: `findVideo()` now scores all video elements by size, duration, and mute state instead of picking the first one. Fixes 0×0 placeholder being selected over the actual player.
|
||||
- History entries in debug report showing `?` instead of action names.
|
||||
- Prime Video status in compatibility matrix updated to reflect partial support.
|
||||
|
||||
### Added
|
||||
- Multi-video overview table in Copy Debug Report when a page has more than one `<video>` element. Shows resolution, mute state, playback state, readyState, duration, and marks the currently targeted video.
|
||||
|
||||
---
|
||||
|
||||
## [v2.0.0] — 2026-06-01
|
||||
|
||||
### 🌍 Multi-Language Extension (Biggest Feature!)
|
||||
- **6-Language UI**: The browser extension is now fully translated into **English, German, French, Spanish, Portuguese (Brazilian), and Russian**. Switch languages instantly in Settings without reload.
|
||||
- **Real-Time i18n**: Every label, button, tooltip, toast notification, empty state, and onboarding guide updates dynamically when the language changes.
|
||||
|
||||
### New Features
|
||||
- **Copy Debug Report (Markdown)**: The *Copy Logs* button in the Status tab now copies a fully formatted Markdown debug report — system info, connection status, video diagnostics, action history, and logs. One click, paste into a GitHub issue, all debugging data ready.
|
||||
- **Platform Auto-Detection**: The Dev tab now identifies streaming platforms (YouTube, Netflix, Twitch, Prime Video, Disney+, HBO Max, Vimeo, Dailymotion) and displays the detected platform.
|
||||
- **Enhanced Video Debug Info**: 20+ new fields in the Status tab including network state, buffered ranges, dimensions (with 0×0 warning), media error codes, shadow DOM status, seeking/ended/loop flags, volume, playback speed, and data attributes.
|
||||
- **No-Video Diagnostic Mode**: When no video is found, the Status tab shows platform, page title, video count, shadow DOM presence, and MediaSession data to help troubleshoot.
|
||||
|
||||
### Changed
|
||||
- **New TwoPointZero Branding**: Updated extension icons (16/32/48/96/128px).
|
||||
- **Larger Popup Logo**: Extension popup icon increased to 48px.
|
||||
- **Prime Video Unblocked**: Removed `amazon.` from the tab blacklist so Amazon/Prime Video tabs appear in the video selector.
|
||||
- **Improved Debug Report**: Full User-Agent string for accurate browser identification, UTC timestamp, connection details including server URL and room info.
|
||||
- **Smart Disconnect**: Improved disconnect handling when leaving rooms.
|
||||
- **Human-Readable Room IDs**: Expanded word lists for friendlier room names.
|
||||
- **Custom Server Support**: WEB_JOIN_REQUEST and join button for custom server invite flows.
|
||||
- **Reconnection Strategy**: Custom server reconnection improvements.
|
||||
- **Episode-Aware Sync**: Command sequencing with smarter episode transition detection and echo suppression for smoother series binges.
|
||||
- **Sync Status Refinements**: YouTube and Twitch sync behavior improved.
|
||||
- **No External Dependencies**: Extension remains dependency-free with no library overhead.
|
||||
|
||||
### Fixed
|
||||
- Hardcoded strings, missing translation keys, and Service Worker notification race conditions.
|
||||
|
||||
---
|
||||
|
||||
## Versioning Policy
|
||||
|
||||
- **MAJOR** (x.0.0): Breaking protocol changes, architecture rewrites, or major feature milestones.
|
||||
- **MINOR** (0.x.0): New features, significant enhancements, new translations, or UI redesigns.
|
||||
- **PATCH** (0.0.x): Bug fixes, minor improvements, and documentation updates. PATCH releases may not receive individual changelog entries if bundled with a MINOR release.
|
||||
@@ -16,9 +16,9 @@ This guide walks through the complete user flow of KoalaSync, from creating a ro
|
||||
|
||||
## Step 2: Connecting to the Relay Server
|
||||
|
||||
When you open the extension popup, the background service worker connects to the relay server:
|
||||
When you open the extension popup (with saved room credentials) or when a saved room configuration exists from a previous session, the background service worker connects to the relay server:
|
||||
|
||||
1. **WebSocket Handshake**: `background.js` opens a WebSocket to `wss://syncserver.koalastuff.net/socket.io/?EIO=4&transport=websocket`.
|
||||
1. **WebSocket Handshake** (on demand): `background.js` opens a WebSocket to `wss://syncserver.koalastuff.net/socket.io/?EIO=4&transport=websocket` only when needed (popup opened or active room).
|
||||
2. **Security Checks** (server-side):
|
||||
- The server checks the client's **IP rate limit** (max 10 connections per 60 seconds).
|
||||
- The server validates the **authentication token** (hardcoded in `shared/constants.js`) to verify this is a legitimate KoalaSync client.
|
||||
@@ -51,7 +51,7 @@ Click **"Create Room"** in the popup's Room tab:
|
||||
|
||||
4. **Server-side processing**:
|
||||
- All fields are **sanitized**: `roomId` is stripped of invalid characters and clamped to 64 chars; `peerId` clamped to 16 chars; `password` clamped to 128 chars; `username` clamped to 30 chars.
|
||||
- The server **hashes the password** with bcrypt and stores the hash in RAM (the plaintext is never stored).
|
||||
- The server **hashes the password** with a keyed SHA-256 HMAC and stores only that hash in RAM (the plaintext is never stored).
|
||||
- A new room object is created in memory with the peer's data.
|
||||
- The server responds with `ROOM_DATA` containing the list of peers in the room.
|
||||
|
||||
@@ -89,7 +89,7 @@ When your friend opens the link in their browser:
|
||||
- After 500ms, the page dispatches a `KOALASYNC_JOIN_REQUEST` custom DOM event with `{ roomId, password, useCustomServer, serverUrl }`.
|
||||
- `bridge.js` catches this event and forwards it to `background.js` via `chrome.runtime.sendMessage`.
|
||||
- `background.js` stores the credentials in `chrome.storage.sync` and emits `JOIN_ROOM` to the server.
|
||||
- The server validates the password against the stored bcrypt hash.
|
||||
- The server validates the password against the stored keyed SHA-256 HMAC hash.
|
||||
- On success, the server responds with `ROOM_DATA` and broadcasts `PEER_STATUS { status: 'joined' }` to all existing peers.
|
||||
- The join page updates to show "✅ Successfully joined!".
|
||||
|
||||
@@ -143,7 +143,7 @@ If videos drift out of sync, either user can click **"Force Sync"**:
|
||||
|
||||
### Phase 2 — Execute
|
||||
6. Once all ACKs are received (or after 8.5 seconds), the initiator emits `FORCE_SYNC_EXECUTE`.
|
||||
7. All peers call `video.play()` simultaneously, achieving frame-perfect sync.
|
||||
7. All peers call `video.play()` simultaneously, achieving synchronized playback.
|
||||
|
||||
> **Why two phases?** Without buffering confirmation, peers with slower connections would start playing before they've loaded the target timestamp, causing immediate desync.
|
||||
|
||||
@@ -155,8 +155,8 @@ While in a room, two heartbeats keep the session alive:
|
||||
|
||||
| Heartbeat | Interval | Source | Purpose |
|
||||
|:----------|:---------|:-------|:--------|
|
||||
| **Background** | 1 minute | `background.js` | Signals "I'm still connected" and handles 5-min auto-reconnect fallback |
|
||||
| **Content** | 15 seconds | `content.js` | Sends video metadata: `currentTime`, `mediaTitle`, `playbackState`, `volume`, `muted` |
|
||||
| **Background** | 30 seconds | `background.js` | While connected, signals "I'm still connected" and triggers automatic reconnect (500ms base, max 5s). No heartbeats fire when idle (lazy connect). |
|
||||
| **Content** | 15 seconds | `content.js` | Sends video metadata: `currentTime`, privacy-filtered `mediaTitle`, `playbackState`, `volume`, `muted` |
|
||||
|
||||
- **Server Reaper**: Every 2 minutes, the server checks for peers with no activity for 5+ minutes and disconnects them ("dead peer pruning").
|
||||
- **Room Cleanup**: Empty rooms are deleted immediately. Inactive rooms are pruned after 2 hours.
|
||||
@@ -184,7 +184,7 @@ When a user clicks **"Leave"** or closes their browser:
|
||||
When watching a series and an episode ends:
|
||||
|
||||
1. `content.js` monitors the [Media Session API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API) for title changes.
|
||||
2. When a new title is detected, the peer broadcasts `EPISODE_LOBBY` with the expected new title.
|
||||
2. When a new title is detected, the peer broadcasts `EPISODE_LOBBY` with the expected title after applying the local media title privacy setting. In episode-only mode this is an identifier such as `S01E04`; when media titles are not sent, the client does not create a new episode lobby.
|
||||
3. All peers' videos freeze. The UI shows an "Episode Lobby" card with peer readiness status.
|
||||
4. Each peer's `content.js` polls for the new title to appear in the `<video>` element's metadata.
|
||||
5. Once a peer detects the matching title, they send `EPISODE_READY`.
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# Known Limitations & Accepted Non-Issues (NOFIX)
|
||||
|
||||
This file records design decisions that are **intentional** and **out of scope** for
|
||||
KoalaSync's threat model. They are frequently re-discovered by reviewers, linters, and
|
||||
AI agents and re-reported as "bugs" or "vulnerabilities." They are not. Please read the
|
||||
threat model below before flagging any of them.
|
||||
|
||||
> **For reviewers & agents:** Do **not** re-report the NOFIX items below. They are
|
||||
> deliberate. If you think the *threat model itself* should change (e.g. KoalaSync
|
||||
> starts handling accounts, payments, or sensitive data), raise **that** discussion
|
||||
> first — these items only stand because the threat model holds.
|
||||
|
||||
---
|
||||
|
||||
## Threat model
|
||||
|
||||
KoalaSync is a **privacy-preserving watch-party sync tool**, not a security product.
|
||||
|
||||
- **Rooms are ephemeral.** They exist for a few hours and are auto-reaped. There are no
|
||||
accounts, no persistent storage, no money, and no sensitive content on the relay.
|
||||
- **The relay is a dumb, stateless message bus.** It forwards play/pause/seek between
|
||||
peers who *chose* to watch together and joined via an invite link shared out-of-band.
|
||||
- **Participants are invited.** Anyone in a room was let in. The social contract is
|
||||
"we're watching a video together," not "mutually distrusting parties."
|
||||
|
||||
### What we DO defend against
|
||||
- **Accidental disruption** — the entire point of Host Control Mode.
|
||||
- **Spam / DoS** that degrades the relay for everyone — rate limits, 4 KB payload cap,
|
||||
server-side gating, lazy-cached health responses.
|
||||
- **Resource exhaustion / memory leaks** — bounded maps, periodic cleanup, room/peer reaping.
|
||||
- **Crashes from malformed input** — strict sanitization and clamping of every field.
|
||||
- **Genuine boundary breaches** — admin-metrics auth (constant-time), CORS, WSS upgrade,
|
||||
invite-hash isolation, strict CSP. Reports here are very welcome (see `SECURITY.md`).
|
||||
|
||||
### What we explicitly DO NOT defend against
|
||||
A **determined participant who modifies their own client to misbehave inside a room they
|
||||
were invited to.** The worst they achieve is sending playback commands or seizing the
|
||||
"host" role in a temporary room they could already disrupt by other means. That is a
|
||||
**social** problem, solved socially: kick them, or start a new room. Engineering real
|
||||
identity/auth to prevent it would destroy the account-less, frictionless, privacy-first
|
||||
design — a bad trade for an ad-hoc movie night.
|
||||
|
||||
---
|
||||
|
||||
## NOFIX entries
|
||||
|
||||
### NOFIX-1 — `peerId` is unauthenticated; a crafted client can impersonate or seize the host
|
||||
**Flag:** `peerId` is client-asserted and broadcast to every peer (in `ROOM_DATA` /
|
||||
`PEER_STATUS`). A modified client can join with the host's `peerId`, dedupe-kick the real
|
||||
host, and become host — controlling or locking `host-only` mode.
|
||||
|
||||
**Why NOFIX:** Requires a *modified client* + an *invited* participant + a `peerId` that is
|
||||
only meaningful inside that one *temporary* room. The payoff is sending play/pause or
|
||||
locking a room the attacker is already in — pure trolling, instantly reversible (kick /
|
||||
new room). Cryptographic per-user identity is wildly disproportionate for an ad-hoc,
|
||||
account-less, ephemeral watch party. **Out of threat model.**
|
||||
Do **not** "fix" with accounts, signed peerIds, or per-user tokens — that breaks the
|
||||
core design.
|
||||
|
||||
### NOFIX-2 — Room-password comparison is not constant-time
|
||||
**Flag:** room password hashes are compared with `!==` (`server/index.js`), so the compare
|
||||
is theoretically timing-attackable.
|
||||
|
||||
**Why NOFIX:** The compared value is an **HMAC-SHA256 hash that never leaves the server** —
|
||||
an attacker cannot observe it to mount a timing attack. Even a hypothetical success only
|
||||
lets someone join a *temporary* room to send playback commands. Not worth defending.
|
||||
(The admin-metrics bearer token — a real boundary — **does** use `crypto.timingSafeEqual`.
|
||||
That is the line we actually guard.)
|
||||
|
||||
### NOFIX-3 — `OFFICIAL_SERVER_TOKEN` is public in the repo
|
||||
**Flag:** the connection token in `shared/constants.js` is committed, so anyone can connect.
|
||||
|
||||
**Why NOFIX:** It is a **coarse filter** to keep random scanners off the relay, **not
|
||||
authentication**. The relay is a public message bus by design; rate limits and per-room
|
||||
behavior are the real protections.
|
||||
|
||||
### NOFIX-4 — Room IDs are enumerable via `GET_ROOMS`
|
||||
**Flag:** any connected client can list all room IDs (and whether each has a password).
|
||||
|
||||
**Why NOFIX:** This is the intended **"Public Rooms"** feature. Rooms wanting privacy set a
|
||||
password; listing the IDs of password-less rooms only lets someone join a watch party —
|
||||
the same as being handed the invite link.
|
||||
|
||||
### NOFIX-5 — A pause/seek can only be reverted, not prevented
|
||||
**Flag:** in `host-only` mode a guest's pause still fires locally before the extension can
|
||||
react, so there is a brief flicker before snap-back.
|
||||
|
||||
**Why NOFIX:** A content script cannot intercept a `<video>` event before the element
|
||||
acts. Reacting (snap-back) is the only option and is by design; the ~½s flicker is
|
||||
acceptable. Not a bug.
|
||||
|
||||
---
|
||||
|
||||
## Not NOFIX — just deferred (may be revisited)
|
||||
|
||||
These are *not* accepted-forever; they are scoped out of v1 and tracked separately
|
||||
(see the host-control-mode design docs in `docs/`):
|
||||
|
||||
- **Host grace on a long disconnect (EC-10).** A brief reconnect/second-tab keeps the host
|
||||
(handled), but a long real disconnect still falls back to `everyone`. A ~30s host-reserve
|
||||
grace could be added later.
|
||||
- **Intent-classifier / snap-back tuning.** Thresholds are first-pass; real-device testing
|
||||
may adjust them.
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
**KoalaSync does not collect, store, or sell any personal data.**
|
||||
|
||||
*We don't track you. We only track our server* (relying exclusively on aggregated, anonymous, and non-personal system metrics to monitor performance and stability).
|
||||
|
||||
KoalaSync is designed with a **Security-First & Volatile** architecture. This means we prioritize keeping your data out of persistent storage, though certain technical data must be processed temporarily to ensure service stability and security.
|
||||
|
||||
## 1. Data Processing (In-Memory Only)
|
||||
KoalaSync does not use a database. All active session data exists only in the server's RAM and is purged immediately when no longer needed.
|
||||
- **Session Data**: To synchronize playback, the server must temporarily hold your `peerId`, `username`, and the `title` of the video you are watching. Additionally, playback metadata (`mediaTitle`, `playbackState`, `currentTime`, `volume`, `muted`) is held per peer for the duration of the session. All of this is deleted as soon as you leave the room.
|
||||
- **Room Passwords**: If you set a room password, it is stored only as a secure **bcrypt hash** in RAM. The server never sees or stores your plaintext password.
|
||||
- **Session Data**: To synchronize playback, the server must temporarily hold your `peerId` and `username`. By default, KoalaSync also shares the selected tab title and media title with the room so peers can identify matching videos and coordinate episode transitions. Privacy Settings let you disable sending the tab title separately, and choose whether media titles are sent in full, reduced to a detected episode identifier (for example `S01E04`), or not sent. Playback metadata (`playbackState`, `currentTime`, `volume`, `muted`) is held per peer for the duration of the session. All of this is deleted as soon as you leave the room.
|
||||
- **Room Passwords**: If you set a room password, it is stored only as an in-memory **keyed SHA-256 HMAC hash**. The server receives the plaintext password only during join validation, never stores it, and keeps only the hash for the short room lifetime.
|
||||
- **Routing Maps**: The server maintains ephemeral lookup tables (`socketToRoom`, `peerToSocket`) to route messages between peers. These contain only transport identifiers and are purged on disconnect.
|
||||
|
||||
### Data Retention
|
||||
@@ -15,8 +17,7 @@ KoalaSync does not use a database. All active session data exists only in the se
|
||||
|:----------|:------------------|:---------------------|
|
||||
| Session data (peerId, username, video metadata) | Duration of session | User leaves room or disconnects |
|
||||
| Room state | 2 hours max | Last peer leaves, or inactivity timeout |
|
||||
| Failed auth lockout records | 15 minutes | Automatic expiry |
|
||||
| Auth failure records | 1 hour | Periodic cleanup |
|
||||
| Auth failure records (lockout after 5 failed attempts) | 15 minutes | Periodic cleanup |
|
||||
| Connection rate-limit counters | 60 seconds | Automatic expiry |
|
||||
| Event rate-limit counters | 10 seconds | Automatic expiry + periodic cleanup |
|
||||
|
||||
@@ -32,7 +33,7 @@ The browser extension requires the following permissions:
|
||||
- `storage`: To remember your local preferences (username, server URL, room settings).
|
||||
- `tabs` & `scripting`: To detect and control video elements on the pages you choose to sync.
|
||||
- `<all_urls>` (host permission): Required to detect `<video>` elements on any website the user chooses to synchronize. The extension only activates on the specific tab the user has actively selected — it does not scan, monitor, or interact with any other tabs or pages.
|
||||
- `alarms`: To keep the background service worker alive during active sync sessions.
|
||||
- `alarms`: To keep the background service worker alive during active sync sessions (the extension only stays connected while you are in a room).
|
||||
- `notifications`: To display sync status updates (e.g., "Peer joined", "Force Sync initiated").
|
||||
- **No History Access**: We do not read, store, or transmit your browsing history. We only interact with the specific tab you have actively selected for synchronization.
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
# WebSocket Protocol Reference
|
||||
|
||||
This document describes the relay behavior implemented by `server/index.js` and
|
||||
the event names defined in `shared/constants.js`.
|
||||
|
||||
## Transport
|
||||
|
||||
- The relay uses Socket.IO v4 events over WebSocket.
|
||||
- Long-polling is disabled (`transports: ['websocket']`, `allowUpgrades: false`).
|
||||
- Messages are Socket.IO event packets whose payload is an event name plus an
|
||||
object payload.
|
||||
- The relay caps incoming Socket.IO message size at 4 KB.
|
||||
|
||||
## Connection Handshake
|
||||
|
||||
The Socket.IO handshake must include:
|
||||
|
||||
- `token`: must match `OFFICIAL_SERVER_TOKEN`.
|
||||
- `version`: optional app version. If present, it must be a valid semver-like
|
||||
string and not older than `MIN_VERSION` (default `1.0.0`).
|
||||
|
||||
If the token is invalid, the relay emits `error` and disconnects the socket.
|
||||
If `version` is invalid or too old, the relay emits `error` and disconnects the
|
||||
socket.
|
||||
|
||||
After the socket is connected, `join_room` must include `protocolVersion`.
|
||||
It must equal `PROTOCOL_VERSION` exactly. A mismatch emits `error` and rejects
|
||||
the join attempt; it does not currently disconnect the socket.
|
||||
|
||||
## Room Join
|
||||
|
||||
### `join_room` (client -> server)
|
||||
|
||||
Payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"roomId": "string, sanitized to [A-Za-z0-9-], max 64",
|
||||
"peerId": "string, max 16",
|
||||
"username": "string, max 30",
|
||||
"password": "string, max 128, optional",
|
||||
"tabTitle": "string, max 100, optional",
|
||||
"mediaTitle": "string, max 100, optional",
|
||||
"protocolVersion": "string, max 16"
|
||||
}
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- Creates the room if it does not exist and capacity allows it.
|
||||
- The first peer becomes `hostPeerId`.
|
||||
- Rooms may have an optional password hash.
|
||||
- Joining with a duplicate `peerId` disconnects the previous socket for that peer.
|
||||
- Joining the same room with the same socket and peer is ignored as a no-op.
|
||||
- Switching rooms removes the socket from the old room first.
|
||||
|
||||
On success, the joining socket receives `room_data`.
|
||||
Other room members receive `peer_status` with `status: "joined"`.
|
||||
|
||||
### `room_data` (server -> client)
|
||||
|
||||
Payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"roomId": "string",
|
||||
"peers": ["peer state objects"],
|
||||
"activeLobby": "object or null",
|
||||
"hostPeerId": "string or null",
|
||||
"controlMode": "everyone | host-only",
|
||||
"controllers": ["peerId"],
|
||||
"capabilities": ["host-control", "co-host"]
|
||||
}
|
||||
```
|
||||
|
||||
`room_data` is sent to the joining socket. It is not the general broadcast used
|
||||
for every later room update.
|
||||
|
||||
## Room Leave
|
||||
|
||||
### `leave_room` (client -> server)
|
||||
|
||||
Payload: none.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Rate-limited to 10 events per socket per minute.
|
||||
- If the socket is mapped to a room, the relay removes it from that room.
|
||||
- Remaining room members receive `peer_status` with `status: "left"` when the
|
||||
peer is no longer represented by another socket.
|
||||
- Empty rooms are deleted.
|
||||
- If the host leaves and peers remain, the relay assigns the next peer as host,
|
||||
falls back to `controlMode: "everyone"`, resets controllers to the new host,
|
||||
and broadcasts `control_mode`.
|
||||
|
||||
Exceeding the `leave_room` limit is logged and the socket is disconnected.
|
||||
|
||||
## Relayed Room Events
|
||||
|
||||
The relay accepts and sanitizes these events, then emits the same event to other
|
||||
peers in the room:
|
||||
|
||||
- `play`
|
||||
- `pause`
|
||||
- `seek`
|
||||
- `peer_status`
|
||||
- `force_sync_prepare`
|
||||
- `force_sync_ack`
|
||||
- `force_sync_execute`
|
||||
- `episode_lobby`
|
||||
- `episode_ready`
|
||||
- `episode_lobby_cancel`
|
||||
|
||||
Relayed payload fields are sanitized and may include:
|
||||
|
||||
```json
|
||||
{
|
||||
"senderId": "peerId of sender",
|
||||
"seq": "number",
|
||||
"currentTime": "number 0..86400 or null",
|
||||
"targetTime": "number 0..86400",
|
||||
"playbackState": "playing | paused",
|
||||
"username": "string, max 30",
|
||||
"tabTitle": "string, max 100 or null",
|
||||
"mediaTitle": "string, max 100 or null",
|
||||
"volume": "number 0..1",
|
||||
"muted": "boolean",
|
||||
"desynced": "boolean",
|
||||
"peerId": "sender peerId",
|
||||
"status": "string, max 16",
|
||||
"expectedTitle": "string, max 100",
|
||||
"title": "string, max 100",
|
||||
"actionTimestamp": "number"
|
||||
}
|
||||
```
|
||||
|
||||
Undefined fields are removed before relay. Raw client payloads are not forwarded.
|
||||
|
||||
## Media Control
|
||||
|
||||
### `play`, `pause`, `seek`
|
||||
|
||||
These are room-moving actions. In `host-only` mode, the relay drops them unless
|
||||
the sender is a controller.
|
||||
|
||||
Common payload fields:
|
||||
|
||||
- `currentTime` for `play`/`pause`.
|
||||
- `targetTime` for `seek`.
|
||||
- `seq` and `actionTimestamp` when the extension needs stale-command or ACK
|
||||
handling.
|
||||
|
||||
The content script applies additional client-side filtering for noisy native
|
||||
player events before it sends these events.
|
||||
|
||||
## Peer Status
|
||||
|
||||
### `peer_status`
|
||||
|
||||
Used for heartbeats and peer state updates. The extension sends it every
|
||||
`HEARTBEAT_INTERVAL` while syncing is active.
|
||||
|
||||
Typical fields:
|
||||
|
||||
- `peerId`
|
||||
- `username`
|
||||
- `tabTitle`
|
||||
- `mediaTitle`
|
||||
- `playbackState`
|
||||
- `currentTime`
|
||||
- `volume`
|
||||
- `muted`
|
||||
- `desynced`
|
||||
- `status`
|
||||
|
||||
The relay stores sanitized peer state and relays the sanitized update to other
|
||||
peers.
|
||||
|
||||
## Force Sync
|
||||
|
||||
Force sync coordination is implemented primarily in the extension. The relay
|
||||
sanitizes and relays the events.
|
||||
|
||||
### `force_sync_prepare`
|
||||
|
||||
Payload includes `targetTime`. The initiator waits for ACKs or for
|
||||
`FORCE_SYNC_TIMEOUT` before sending `force_sync_execute`.
|
||||
|
||||
In `host-only` mode, only controllers may initiate it.
|
||||
|
||||
### `force_sync_ack`
|
||||
|
||||
The extension sends ACKs with peer identity and sequence data. The relay relays
|
||||
them with the same sanitized relay envelope as other room events, including
|
||||
`senderId`.
|
||||
|
||||
### `force_sync_execute`
|
||||
|
||||
Payload includes `targetTime`. In `host-only` mode, only controllers may send it.
|
||||
The relay also allows a matching initiator's execute event after that initiator
|
||||
started the prepare step, even if their controller state changed before execute.
|
||||
|
||||
## Episode Lobby
|
||||
|
||||
Episode lobby coordination is implemented primarily in the extension. The relay
|
||||
tracks enough state to include `activeLobby` in `room_data` for later joiners.
|
||||
|
||||
### `episode_lobby`
|
||||
|
||||
Payload uses `expectedTitle`. The relay creates `activeLobby` when this field is
|
||||
present and no lobby is already active.
|
||||
|
||||
In `host-only` mode, only controllers may initiate it.
|
||||
|
||||
### `episode_ready`
|
||||
|
||||
Payload may include `title`. The relay adds the sender to the active lobby's
|
||||
ready list when a lobby exists.
|
||||
|
||||
### `episode_lobby_cancel`
|
||||
|
||||
Clears the active lobby and is relayed to peers. In `host-only` mode, only
|
||||
controllers may initiate it.
|
||||
|
||||
## Host Control Mode
|
||||
|
||||
### `set_control_mode` (client -> server)
|
||||
|
||||
Payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"controlMode": "everyone | host-only"
|
||||
}
|
||||
```
|
||||
|
||||
Only the room host may change the mode. Non-host attempts are ignored and the
|
||||
sender receives the current `control_mode` snapshot.
|
||||
|
||||
Mode changes are debounced per room with `CONTROL_MODE_MIN_INTERVAL_MS` (500 ms).
|
||||
|
||||
### `set_peer_role` (client -> server)
|
||||
|
||||
Payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"peerId": "string, max 16",
|
||||
"controller": "boolean"
|
||||
}
|
||||
```
|
||||
|
||||
Only the room host may promote or demote controllers. The host cannot demote
|
||||
themself. Role changes use the same 500 ms per-room debounce as mode changes.
|
||||
|
||||
### `control_mode` (server -> client)
|
||||
|
||||
Payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"controlMode": "everyone | host-only",
|
||||
"hostPeerId": "string or null",
|
||||
"controllers": ["peerId"]
|
||||
}
|
||||
```
|
||||
|
||||
Sent when mode or controller state changes, when host migration changes room
|
||||
authority, and when unauthorized role/mode attempts need to resync the sender.
|
||||
|
||||
## Room List
|
||||
|
||||
### `get_rooms` (client -> server)
|
||||
|
||||
Payload: none.
|
||||
|
||||
No admin token is required for this Socket.IO event.
|
||||
|
||||
Limits:
|
||||
|
||||
- Counts against the per-socket event limit.
|
||||
- Also has a 10 second per-socket cooldown.
|
||||
|
||||
### `room_list` (server -> client)
|
||||
|
||||
Payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"rooms": [
|
||||
{
|
||||
"id": "room id",
|
||||
"peerCount": 2,
|
||||
"hasPassword": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Ping, Pong, and ACK
|
||||
|
||||
### `ping`
|
||||
|
||||
Payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"t": 1234567890,
|
||||
"target": "peerId, optional"
|
||||
}
|
||||
```
|
||||
|
||||
If `target` is omitted, the relay responds to the sender with `pong`.
|
||||
If `target` is another peer in the same room, the relay sends `ping` to that peer
|
||||
with `{ "t": ..., "sender": "senderPeerId" }`.
|
||||
|
||||
### `pong`
|
||||
|
||||
Payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"t": 1234567890,
|
||||
"target": "peerId, optional"
|
||||
}
|
||||
```
|
||||
|
||||
If `target` is a peer in the same room, the relay sends `pong` to that peer with
|
||||
`{ "t": ... }`.
|
||||
|
||||
### `event_ack`
|
||||
|
||||
Client payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"targetId": "peerId",
|
||||
"actionTimestamp": 1234567890
|
||||
}
|
||||
```
|
||||
|
||||
If sender and target are still in the same room, the relay emits:
|
||||
|
||||
```json
|
||||
{
|
||||
"senderId": "sender peerId",
|
||||
"actionTimestamp": 1234567890
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limits
|
||||
|
||||
- Connections: 10 per IP per minute; excess connections are disconnected.
|
||||
- Relayed/events: 50 per socket per 10 seconds; excess disconnects the socket.
|
||||
- `get_rooms`: 10 second cooldown per socket plus the event limit.
|
||||
- `leave_room`: 10 per socket per minute; excess disconnects the socket.
|
||||
- Invalid room passwords: tracked per IP and room. Five recent failures block
|
||||
more password attempts for that room until the failure window ages out.
|
||||
- HTTP health and admin-metrics endpoints have their own rate limits outside this
|
||||
Socket.IO protocol.
|
||||
|
||||
## Capabilities
|
||||
|
||||
`room_data.capabilities` advertises server-backed features:
|
||||
|
||||
- `host-control`
|
||||
- `co-host`
|
||||
|
||||
Clients should treat a missing or unknown capabilities list as unsupported.
|
||||
@@ -5,3 +5,4 @@ This directory contains deep-dives into the KoalaSync protocol and architecture.
|
||||
- [HOW_IT_WORKS.md](HOW_IT_WORKS.md): Step-by-step walkthrough of every user flow, from room creation to synchronized playback. Ideal for store reviewers and manual testers.
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md): Communication flows, Dual Heartbeat, and Sync logic.
|
||||
- [SYNC_GUIDE.md](SYNC_GUIDE.md): Protocol constants and sync requirements.
|
||||
- [TRANSLATION.md](TRANSLATION.md): Translation and localization guide for the extension and website.
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# KoalaSync Roadmap
|
||||
|
||||
> Feature priorities, planned work, backlog, and rejected ideas for KoalaSync.
|
||||
|
||||
---
|
||||
|
||||
## Status Legend
|
||||
|
||||
| Badge | Meaning |
|
||||
|---|---|
|
||||
| 🚧 In Progress | Currently being developed |
|
||||
| 📋 Planned | Prioritized for an upcoming phase |
|
||||
| 💡 Backlog | Under evaluation, not yet prioritized |
|
||||
| ❌ Rejected | Declined (with rationale) |
|
||||
| ✅ Completed | Shipped |
|
||||
|
||||
---
|
||||
|
||||
## 🚧 In Progress
|
||||
|
||||
*Currently being worked on.*
|
||||
|
||||
| Feature | Priority | Area |
|
||||
|---|---|---|
|
||||
| *(none yet)* | | |
|
||||
|
||||
---
|
||||
|
||||
## 📋 Planned
|
||||
|
||||
*Prioritized for upcoming phases.*
|
||||
|
||||
### 2. Invite link with target URL for auto-redirect
|
||||
|
||||
- **Priority:** P2
|
||||
- **Category:** UX / Ease of Sharing
|
||||
- **Background:** The invite link currently only contains the room ID. The invited person has to manually open the page. Ideally, the link would include the shared tab's URL so the invitee gets redirected to the right page and the tab is auto-selected (auto-matching via tab title already exists).
|
||||
- **Known challenges:**
|
||||
- Many streaming sites (e.g., Emby, Jellyfin) don't have unique URLs per content — once inside the player, the URL stays the same.
|
||||
- Dozens of such edge cases exist; a generic solution is difficult.
|
||||
- Would likely need site-specific extractor logic (similar to the existing sync service adapters).
|
||||
- **Possible approaches:**
|
||||
- Fallback: if no unique URL can be determined, only pass the tab title.
|
||||
- Site-specific URL extraction for known services.
|
||||
|
||||
---
|
||||
|
||||
## 💡 Backlog
|
||||
|
||||
*Ideas and feature requests under evaluation.*
|
||||
|
||||
### In-room chat overlay (like TeleParty)
|
||||
|
||||
- **Priority:** P3
|
||||
- **Category:** Social / Communication
|
||||
- **Background:** A collapsible chat panel to the right of the video (or as an overlay) allowing text-based communication with everyone in the room.
|
||||
- **Why backlog (still uncertain):**
|
||||
- **Use case:** No strong personal need — with chat, latency matters less than with voice; async communication tolerates a few seconds of delay.
|
||||
- **Complexity:** Relatively large feature (UI + message persistence + possibly history).
|
||||
- **Legal/moderation:** Unclear what moderation requirements would apply if users can exchange chat messages. Could be relevant depending on jurisdiction.
|
||||
- **Status:** Under evaluation, may come later.
|
||||
|
||||
### Cross-frame video detection and control
|
||||
|
||||
- **Priority:** P3
|
||||
- **Category:** Compatibility / Embedded Players
|
||||
- **Background:** KoalaSync currently injects on demand into the selected tab's top frame. This works for normal top-frame players, including current Emby/Jellyfin usage, but does not cover cases where the real `<video>` lives inside a cross-origin iframe or an `about:blank`/`srcdoc` player frame.
|
||||
- **Possible approach:** Add an opt-in frame bridge where child frames announce detected videos to the top frame, and the top frame routes remote play/pause/seek commands to the active child video.
|
||||
- **Status:** Future compatibility work, not needed for current Emby behavior.
|
||||
|
||||
### Local extension E2E smoke tests
|
||||
|
||||
- **Priority:** P2
|
||||
- **Category:** Testing / Release Confidence
|
||||
- **Background:** The release verification covers unit tests, server integration, syntax, lint, audits, and builds, but it does not currently run a real browser extension flow. A small local E2E smoke suite would catch regressions in content-script injection, tab navigation reinjection, remote seek handling, and iframe player support.
|
||||
- **Possible approach:** Add a separate local-only Playwright smoke command that loads the unpacked extension, opens two controlled video pages, and verifies play/pause/seek through the actual extension path. Keep it outside `npm run verify` until it is stable enough for CI.
|
||||
- **Status:** Backlog, recommended before larger content-script or frame-bridge changes.
|
||||
|
||||
---
|
||||
|
||||
## ❌ Rejected
|
||||
|
||||
*Declined features with rationale — keeps decisions documented so they don't get re-debated.*
|
||||
|
||||
| Feature | Reason |
|
||||
|---|---|
|
||||
| *(none yet)* | |
|
||||
|
||||
@@ -17,13 +17,15 @@ You MUST run the build script in any of the following scenarios:
|
||||
|
||||
Run the Node.js build script from the repository root:
|
||||
```bash
|
||||
node scripts/build-extension.js
|
||||
node scripts/build-extension.cjs
|
||||
# or simply:
|
||||
npm run build:extension
|
||||
```
|
||||
|
||||
## What does it do?
|
||||
The build script performs the following actions:
|
||||
1. Synchronizes protocol constants by copying `shared/constants.js`, `shared/blacklist.js`, and `shared/README.md` into `extension/shared/`.
|
||||
2. Injects `EVENTS` and `HEARTBEAT_INTERVAL` into `content.js` via marker-based replacement.
|
||||
2. Injects `EVENTS`, `HEARTBEAT_INTERVAL`, and `episode-utils.js` functions (`extractEpisodeId`, `sameEpisode`) into `content.js` via marker-based replacement.
|
||||
3. Compiles browser-specific manifest files.
|
||||
4. Packages the final ready-to-publish extension artifacts for Chrome and Firefox into the `dist/` directory.
|
||||
|
||||
@@ -31,7 +33,7 @@ The build script performs the following actions:
|
||||
The system enforces a strict `protocolVersion` check during the `JOIN_ROOM` handshake.
|
||||
- The version is defined in `shared/constants.js`.
|
||||
- If the extension and server versions mismatch, the server will reject the connection with an `Incompatible protocol version` error.
|
||||
- **Always run the build script** after bumping the version number to ensure both components are updated.
|
||||
- **Never manually bump version numbers**. The CI pipeline automatically injects the version from the git tag into `manifest.base.json`, `shared/constants.js`, and `package.json` during release builds. Run the build script to synchronize other constant updates.
|
||||
|
||||
> [!CAUTION]
|
||||
> **NEVER** edit the files inside `extension/shared/` directly. They will be overwritten the next time the build script is run. Always edit the files in the root `shared/` directory and then run the build script.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Tested Streaming Services
|
||||
|
||||
This document tracks which streaming platforms and media servers have been tested with KoalaSync.
|
||||
|
||||
| Service | Sync Works | Media Title | Episode Auto-Sync | Notes |
|
||||
|---------|:----------:|:-----------:|:-----------------:|-------|
|
||||
| **YouTube** | ✅ Full | ✅ Full | ❌ | Individual videos, not episodes — no episode auto-sync. |
|
||||
| **Twitch** | ✅ Full | ✅ Full | ❌ | Individual streams/VODs, not episodes — no episode auto-sync. |
|
||||
| **Netflix** | ✅ Full | ❌ | ❌ | No media title exposed. |
|
||||
| **Emby** | ✅ Full | ✅ Full | ✅ Full | Best-in-class support. |
|
||||
| **Jellyfin** | ✅ Full | ✅ Full | ✅ Full | — |
|
||||
| **Plex** | Not tested | Not tested | Not tested | — |
|
||||
| **Disney+** | ✅ Full | ⚠️ Partial | ❌ | Series title only (e.g. "The Simpsons"), no episode info. |
|
||||
| **Prime Video** | ✅ Full | ✅ Full | ❌ | — |
|
||||
| **HBO Max / Max** | Not tested | Not tested | Not tested | — |
|
||||
| **Crunchyroll** | Not tested | Not tested | Not tested | — |
|
||||
| **Vimeo** | Not tested | Not tested | Not tested | — |
|
||||
| **Dailymotion** | Not tested | Not tested | Not tested | — |
|
||||
| **ARD / ZDF Mediathek** | Not tested | Not tested | Not tested | — |
|
||||
|
||||
## Legend
|
||||
|
||||
| Symbol | Meaning |
|
||||
|--------|---------|
|
||||
| ✅ Full | Works without limitations. |
|
||||
| ⚠️ Partial | Works with caveats (see Notes). |
|
||||
| ❌ N/A | Not applicable or not supported. |
|
||||
|
||||
## How to Contribute
|
||||
|
||||
Tested a service that's not listed? Found different behavior than documented?
|
||||
|
||||
1. Test KoalaSync on the service with two browser profiles
|
||||
2. Use the extension's **Dev tab** to check `readyState`, `currentTime`, and media title
|
||||
3. Open a GitHub issue or PR updating this table
|
||||
|
||||
## Technical Background
|
||||
|
||||
KoalaSync works on any website with a **standard HTML5 `<video>` element** that allows script injection.
|
||||
|
||||
Limited functionality on certain platforms is typically caused by:
|
||||
- **DRM/Copy Protection** (e.g., Widevine on Netflix) which restricts access to media metadata like title and playback state
|
||||
- **Shadow DOM encapsulation** that hides video elements from content scripts
|
||||
- **Strict Content Security Policies** (CSP) that block script injection
|
||||
|
||||
Websites with heavily obfuscated custom players (e.g., complex Shadow DOM, iframe isolation) may require platform-specific workarounds in `content.js`.
|
||||
@@ -0,0 +1,121 @@
|
||||
# KoalaSync Translation & Localization Guide
|
||||
|
||||
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
|
||||
|
||||
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 `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 (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.
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
|
||||
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/`.
|
||||
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/`.
|
||||
2. Edit an existing `[lang].json` or copy `en.json` to create a new one.
|
||||
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
|
||||
|
||||
# Tests the website locales for missing keys or syntax errors
|
||||
node scripts/test-website-locales.mjs
|
||||
|
||||
# Builds the website with your new translations
|
||||
node website/build.cjs
|
||||
```
|
||||
|
||||
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 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.
|
||||
|
||||
---
|
||||
|
||||
## 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 (`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.
|
||||
@@ -0,0 +1,51 @@
|
||||
# DevOps Release Workflow
|
||||
|
||||
This document describes the deployment and release process for KoalaSync.
|
||||
|
||||
## Tag-Based Releases
|
||||
|
||||
KoalaSync uses a fully automated release pipeline triggered by Git tags.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **DO NOT** manually bump the version numbers in any files (such as `package.json`, `manifest.base.json`, `shared/constants.js`, etc.) before creating a release.
|
||||
> Bumping versions manually is redundant, leads to conflicts, and is completely handled by the CI/CD pipeline.
|
||||
|
||||
### How it Works
|
||||
|
||||
When you push a Git tag matching `v*` (e.g., `v2.5.1`), the GitHub Actions release workflow (`.github/workflows/release.yml`) is triggered. The workflow performs the following actions:
|
||||
|
||||
1. **Extracts the version** from the tag (e.g., `2.5.1` from `v2.5.1`).
|
||||
2. **Injects the version** automatically into the following files:
|
||||
- `extension/manifest.base.json`
|
||||
- `shared/constants.js` (updates `APP_VERSION`)
|
||||
- `package.json`
|
||||
- `website/version.json`
|
||||
- `website/template.html` (updates `softwareVersion` schema)
|
||||
- `README.md` (updates badge and announcement banner)
|
||||
- `website/sitemap.xml` (updates `lastmod` dates)
|
||||
3. **Commits and pushes** these version updates back to the `main` branch automatically with the commit message `chore(release): update versions to vX.X.X [skip ci]`.
|
||||
4. **Builds the extension** for both Chrome and Firefox and publishes the zipped archives.
|
||||
5. **Builds the website** and uploads website artifacts.
|
||||
6. **Builds and publishes** the Docker image for the relay server to the GitHub Container Registry (`ghcr.io`).
|
||||
|
||||
---
|
||||
|
||||
## Steps to Deploy a New Release
|
||||
|
||||
To release a new version (e.g., `v2.5.1`), follow these steps:
|
||||
|
||||
1. Make sure your local repository is synced on `main`:
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
```
|
||||
2. Create a local Git tag:
|
||||
```bash
|
||||
git tag v2.5.1
|
||||
```
|
||||
3. Push the tag to GitHub:
|
||||
```bash
|
||||
git push origin v2.5.1
|
||||
```
|
||||
|
||||
The release pipeline will take care of the rest! You can monitor the progress under the **Actions** tab of the GitHub repository.
|
||||
@@ -0,0 +1,151 @@
|
||||
# Host Control Mode
|
||||
|
||||
This document describes the Host Control Mode implementation in the relay and
|
||||
extension. It only covers behavior implemented in the current codebase.
|
||||
|
||||
## Modes
|
||||
|
||||
### `everyone`
|
||||
|
||||
- Default room mode.
|
||||
- Any peer may send room-moving playback events.
|
||||
|
||||
### `host-only`
|
||||
|
||||
- Only controllers may send room-moving playback events.
|
||||
- The host is always a controller.
|
||||
- The host can promote additional peers to controllers.
|
||||
- Guests can keep watching locally in solo/desynced mode, but their local actions
|
||||
still do not drive the shared room.
|
||||
|
||||
Room-moving events are:
|
||||
|
||||
- `play`
|
||||
- `pause`
|
||||
- `seek`
|
||||
- `force_sync_prepare`
|
||||
- `force_sync_execute`
|
||||
- `episode_lobby`
|
||||
- `episode_lobby_cancel`
|
||||
|
||||
Heartbeats, force-sync ACKs, episode-ready events, ping/pong, and command ACKs
|
||||
remain allowed for guests.
|
||||
|
||||
## Server State
|
||||
|
||||
Rooms store Host Control state in memory:
|
||||
|
||||
```js
|
||||
{
|
||||
hostPeerId,
|
||||
controlMode,
|
||||
controllers,
|
||||
lastControlModeChangeAt,
|
||||
lastRoleChangeAt,
|
||||
forceSyncInitiator
|
||||
}
|
||||
```
|
||||
|
||||
`controllers` is a `Set` on the server and is serialized as an array in
|
||||
`room_data` and `control_mode`.
|
||||
|
||||
State is not persisted across relay restarts.
|
||||
|
||||
## Authority Rules
|
||||
|
||||
### Changing mode
|
||||
|
||||
Only `hostPeerId` may send `set_control_mode`.
|
||||
|
||||
Valid values:
|
||||
|
||||
- `everyone`
|
||||
- `host-only`
|
||||
|
||||
Invalid values are ignored. Non-host attempts are ignored and the sender receives
|
||||
the current `control_mode` snapshot so optimistic UI can revert.
|
||||
|
||||
Mode changes are debounced per room for 500 ms.
|
||||
|
||||
### Promoting and demoting controllers
|
||||
|
||||
Only `hostPeerId` may send `set_peer_role`.
|
||||
|
||||
The host cannot demote themself. No-op role changes are ignored. Role changes are
|
||||
debounced per room for 500 ms.
|
||||
|
||||
### Host leaving
|
||||
|
||||
When the host leaves and peers remain:
|
||||
|
||||
- the next peer becomes `hostPeerId`;
|
||||
- `controlMode` falls back to `everyone`;
|
||||
- `controllers` is reset to the new host;
|
||||
- the relay broadcasts `control_mode`.
|
||||
|
||||
When a non-host controller leaves, the relay removes that peer from
|
||||
`controllers` and broadcasts `control_mode`.
|
||||
|
||||
## Enforcement
|
||||
|
||||
The implementation has two enforcement points:
|
||||
|
||||
- The extension background script blocks local guest attempts in `host-only` and
|
||||
sends `HOST_BLOCKED` to the content script for local UX.
|
||||
- The relay drops room-moving events from non-controllers in `host-only`, so old
|
||||
or modified clients cannot drive the room.
|
||||
|
||||
The relay is the authority for room-wide effects.
|
||||
|
||||
## Guest UX
|
||||
|
||||
When a guest action is blocked locally, the content script classifies it:
|
||||
|
||||
- deliberate user action: show the host-control dialog;
|
||||
- likely involuntary player action (buffering, tab refocus, no recent gesture):
|
||||
silently snap back when safe;
|
||||
- live/DVR stream: degrade without forcing snap-back.
|
||||
|
||||
The dialog offers:
|
||||
|
||||
- stay in sync: resync to the host;
|
||||
- watch on my own: enter solo/desynced mode.
|
||||
|
||||
In solo/desynced mode:
|
||||
|
||||
- the guest can control their local video;
|
||||
- host room commands are ignored locally, except force-sync preparation is ACKed
|
||||
so the host's flow can continue;
|
||||
- the guest can resync to the host.
|
||||
|
||||
The extension reports `desynced` in peer status so the host UI can show that a
|
||||
guest is watching solo.
|
||||
|
||||
## Force Sync Edge Case
|
||||
|
||||
The relay tracks `forceSyncInitiator` after a controller sends
|
||||
`force_sync_prepare`.
|
||||
|
||||
This allows that same initiator's `force_sync_execute` through even if their
|
||||
controller role changes before execute arrives. Without this, a demotion in the
|
||||
middle of a force-sync flow could leave peers waiting after prepare.
|
||||
|
||||
The relay clears `forceSyncInitiator` after execute or when the initiator leaves.
|
||||
|
||||
## Capabilities
|
||||
|
||||
The relay advertises Host Control support in `room_data.capabilities`:
|
||||
|
||||
- `host-control`
|
||||
- `co-host`
|
||||
|
||||
The extension hides or disables matching UI when capabilities are missing.
|
||||
|
||||
## Related Events
|
||||
|
||||
See [PROTOCOL.md](PROTOCOL.md) for payloads and relay behavior for:
|
||||
|
||||
- `set_control_mode`
|
||||
- `control_mode`
|
||||
- `set_peer_role`
|
||||
- host-only gated relay events
|
||||
@@ -1,6 +1,6 @@
|
||||
export default [
|
||||
{
|
||||
ignores: ["dist/**", "node_modules/**", "scratch/**"]
|
||||
ignores: ["coverage/**", "dist/**", "node_modules/**", "scratch/**"]
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
@@ -8,6 +8,7 @@ export default [
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
chrome: "readonly",
|
||||
browser: "readonly",
|
||||
window: "readonly",
|
||||
document: "readonly",
|
||||
navigator: "readonly",
|
||||
@@ -20,6 +21,7 @@ export default [
|
||||
fetch: "readonly",
|
||||
CustomEvent: "readonly",
|
||||
MutationObserver: "readonly",
|
||||
IntersectionObserver: "readonly",
|
||||
Uint32Array: "readonly",
|
||||
Set: "readonly",
|
||||
Map: "readonly",
|
||||
@@ -33,8 +35,12 @@ export default [
|
||||
Date: "readonly",
|
||||
Error: "readonly",
|
||||
URL: "readonly",
|
||||
URLSearchParams: "readonly",
|
||||
WebSocket: "readonly",
|
||||
self: "readonly"
|
||||
history: "readonly",
|
||||
location: "readonly",
|
||||
self: "readonly",
|
||||
process: "readonly",
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
@@ -52,7 +58,7 @@ export default [
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["server/**/*.js", "scripts/**/*.js"],
|
||||
files: ["server/**/*.js", "scripts/**/*.js", "scripts/**/*.cjs", "website/build.cjs"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
require: "readonly",
|
||||
@@ -0,0 +1,83 @@
|
||||
# ==============================================================================
|
||||
# KoalaSync - Production Caddy Configuration Example
|
||||
# ==============================================================================
|
||||
# This file provides examples of both a lightweight "Simple" configuration
|
||||
# and a production-hardened "Advanced" configuration.
|
||||
# Replace domains, reverse proxy locations, and directories with your actual setup.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# OPTION A: Simple Configuration
|
||||
# ------------------------------------------------------------------------------
|
||||
# Minimal configuration that serves the static website, enables gzip compression,
|
||||
# supports extension-less Clean URLs, and reverse proxies the relay server.
|
||||
|
||||
# sync.koalastuff.net {
|
||||
# root * /var/www/koalasync/website/www
|
||||
# encode zstd gzip
|
||||
#
|
||||
# # Clean URLs support (resolves /join to join.html, etc.)
|
||||
# try_files {path} {path}.html {path}/
|
||||
# file_server
|
||||
# }
|
||||
#
|
||||
# syncserver.koalastuff.net {
|
||||
# reverse_proxy localhost:3000
|
||||
# }
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# OPTION B: Advanced Configuration (Production-Hardened)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Highly secure, optimized configuration using advanced HTTP security headers,
|
||||
# aggressive static assets caching, server signature concealment, and strict
|
||||
# hardware permission access policies.
|
||||
|
||||
(security_headers) {
|
||||
header {
|
||||
# Enable HTTP Strict Transport Security (HSTS)
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
# Prevent clickjacking attacks (Sameorigin)
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
# Prevent MIME-sniffing
|
||||
X-Content-Type-Options "nosniff"
|
||||
# Enable browser XSS protection
|
||||
X-XSS-Protection "1; mode=block"
|
||||
# Control referrer information
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
# Hide Caddy server stamp signature
|
||||
-Server
|
||||
}
|
||||
}
|
||||
|
||||
sync.koalastuff.net {
|
||||
encode zstd gzip
|
||||
root * /var/www/koalasync/website/www
|
||||
|
||||
# Clean URLs: Resolves paths without .html in the URL
|
||||
try_files {path} {path}.html {path}/
|
||||
file_server
|
||||
|
||||
# Static Caching for high-performance PageSpeed (1 year with validation)
|
||||
@static {
|
||||
file
|
||||
path *.ico *.css *.js *.png *.svg *.webp *.avif
|
||||
}
|
||||
header @static Cache-Control "public, max-age=31536000, must-revalidate"
|
||||
|
||||
# Security Headers & Content Security Policy (CSP)
|
||||
import security_headers
|
||||
header {
|
||||
# CSP hardened with base-uri and form-action limits
|
||||
Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"
|
||||
|
||||
# Modern Permissions Policy (blocks browser hardware access for enhanced privacy)
|
||||
Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()"
|
||||
}
|
||||
}
|
||||
|
||||
syncserver.koalastuff.net {
|
||||
import security_headers
|
||||
encode zstd gzip
|
||||
reverse_proxy KoalaSync:3000
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
services: # Top-level key defining all containers in this Compose file
|
||||
koala-sync: # Name of the KoalaSync service
|
||||
image: ghcr.io/shik3i/koalasync:latest # Pulls the latest KoalaSync image from GitHub Container Registry
|
||||
container_name: KoalaSync # Sets a fixed container name instead of an auto-generated one
|
||||
restart: always # Always restart the container if it stops or if Docker starts
|
||||
environment: # Environment variables passed into the container
|
||||
- TZ=Europe/Berlin # Sets the timezone inside the container
|
||||
- PORT=3000 # Port KoalaSync listens on inside the container
|
||||
- MIN_VERSION=1.0.0 # Minimum client version allowed to connect
|
||||
- MAX_ROOMS=100 # Maximum number of rooms that can exist
|
||||
- MAX_PEERS_PER_ROOM=25 # Maximum number of peers allowed per room
|
||||
- ADMIN_METRICS_TOKEN= # Optional: 32+ char random token for aggregate-only /health metrics
|
||||
pids_limit: 2048 # Limits the container to 2048 process IDs for safety
|
||||
networks: # Attaches the service to the networks listed below
|
||||
- caddy_net # Joins the pre-existing Caddy network for reverse proxying
|
||||
networks: # Top-level networks definition
|
||||
caddy_net: # Network name as referenced by the service
|
||||
external: true # Marks the network as managed outside of Compose (created by Caddy)
|
||||
@@ -0,0 +1,22 @@
|
||||
services: # Top-level key defining all containers in this Compose file
|
||||
koala-sync: # Name of the KoalaSync service
|
||||
image: ghcr.io/shik3i/koalasync:latest # Pulls the latest KoalaSync image from GitHub Container Registry
|
||||
container_name: KoalaSync # Sets a fixed container name instead of an auto-generated one
|
||||
restart: always # Always restart the container if it stops or if Docker starts
|
||||
ports: # Exposes the container port to the host
|
||||
- "3000:3000" # Maps host port 3000 to container port 3000
|
||||
environment: # Environment variables passed into the container
|
||||
- TZ=Europe/Berlin # Sets the timezone inside the container
|
||||
- PORT=3000 # Port KoalaSync listens on inside the container
|
||||
- MIN_VERSION=1.0.0 # Minimum client version allowed to connect
|
||||
- MAX_ROOMS=100 # Maximum number of rooms that can exist
|
||||
- MAX_PEERS_PER_ROOM=25 # Maximum number of peers allowed per room
|
||||
- ADMIN_METRICS_TOKEN= # Optional: 32+ char random token for aggregate-only /health metrics
|
||||
pids_limit: 2048 # Limits the container to 2048 process IDs for safety
|
||||
networks: # Attaches the service to the networks listed below
|
||||
bond0_network: # Network name as referenced by the service
|
||||
ipv4_address: 192.168.1.XXX # Static IPv4 address for the KoalaSync container
|
||||
networks: # Top-level networks definition
|
||||
bond0_network: # Network name inside Compose
|
||||
external: true # Marks the network as managed outside of Compose
|
||||
name: bond0 # Name of the pre-existing network on the Docker host
|
||||
@@ -0,0 +1,104 @@
|
||||
# Prometheus Community JSON Exporter Configuration Example
|
||||
# File: examples/json_exporter.example.yml
|
||||
#
|
||||
# Use this configuration to map KoalaSync admin health metrics (JSON)
|
||||
# to native Prometheus metrics.
|
||||
#
|
||||
# Usage:
|
||||
# 1. Rename this file to json_exporter.yml
|
||||
# 2. Replace "YOUR_ADMIN_METRICS_TOKEN" with your actual ADMIN_METRICS_TOKEN env value
|
||||
# 3. Mount it to the json-exporter docker container: /config.yml
|
||||
|
||||
modules:
|
||||
koalasync:
|
||||
http_client_config:
|
||||
bearer_token: "YOUR_ADMIN_METRICS_TOKEN"
|
||||
metrics:
|
||||
- name: koalasync_uptime_seconds
|
||||
path: '{.uptime}'
|
||||
help: "Uptime of the KoalaSync relay server in seconds"
|
||||
|
||||
- name: koalasync_rooms
|
||||
path: '{.rooms}'
|
||||
help: "Total active rooms"
|
||||
|
||||
- name: koalasync_connections
|
||||
path: '{.connections}'
|
||||
help: "Total active socket connections (sockets)"
|
||||
|
||||
- name: koalasync_peers
|
||||
path: '{.peers}'
|
||||
help: "Total connected peers across all rooms"
|
||||
|
||||
- name: koalasync_rooms_with_lobby
|
||||
path: '{.roomsWithLobby}'
|
||||
help: "Number of rooms waiting in an episode lobby"
|
||||
|
||||
- name: koalasync_avg_peers_per_room
|
||||
path: '{.avgPeersPerRoom}'
|
||||
help: "Average number of peers per room"
|
||||
|
||||
- name: koalasync_max_peers_in_room
|
||||
path: '{.maxPeersInRoom}'
|
||||
help: "Maximum number of peers in a single room"
|
||||
|
||||
- name: koalasync_memory_rss_bytes
|
||||
path: '{.memory.rss}'
|
||||
help: "Resident Set Size (RSS) memory usage in bytes"
|
||||
|
||||
- name: koalasync_memory_heap_used_bytes
|
||||
path: '{.memory.heapUsed}'
|
||||
help: "V8 engine heap used in bytes"
|
||||
|
||||
- name: koalasync_memory_heap_total_bytes
|
||||
path: '{.memory.heapTotal}'
|
||||
help: "V8 engine heap total in bytes"
|
||||
|
||||
# Rate limiter tracking — unique clients currently in each tracking window
|
||||
# (not rate-limit denials; these include legitimate traffic too)
|
||||
|
||||
- name: koalasync_rate_limit_connections
|
||||
path: '{.rateLimits.trackedClients.connections}'
|
||||
help: "Unique clients tracked in the connection rate limiter window"
|
||||
|
||||
- name: koalasync_rate_limit_events
|
||||
path: '{.rateLimits.trackedClients.events}'
|
||||
help: "Unique sockets tracked in the event rate limiter window"
|
||||
|
||||
- name: koalasync_rate_limit_health
|
||||
path: '{.rateLimits.trackedClients.health}'
|
||||
help: "Unique IPs tracked in the health endpoint rate limiter window"
|
||||
|
||||
- name: koalasync_rate_limit_admin_metrics_auth
|
||||
path: '{.rateLimits.trackedClients.adminMetricsAuth}'
|
||||
help: "Unique IPs tracked in the admin metrics auth rate limiter window"
|
||||
|
||||
- name: koalasync_rate_limit_auth_failures
|
||||
path: '{.rateLimits.trackedClients.authFailures}'
|
||||
help: "Unique IPs tracked in the authentication failures cache"
|
||||
|
||||
- name: koalasync_rate_limit_room_list
|
||||
path: '{.rateLimits.trackedClients.roomList}'
|
||||
help: "Unique sockets in the room list cooldown cache"
|
||||
|
||||
# Actual rate-limit denials — incremented only when a 429 is served
|
||||
|
||||
- name: koalasync_rate_limit_denied_connections
|
||||
path: '{.rateLimits.denied.connections}'
|
||||
help: "Total connection attempts denied by rate limiter"
|
||||
|
||||
- name: koalasync_rate_limit_denied_events
|
||||
path: '{.rateLimits.denied.events}'
|
||||
help: "Total socket events denied by rate limiter"
|
||||
|
||||
- name: koalasync_rate_limit_denied_health
|
||||
path: '{.rateLimits.denied.health}'
|
||||
help: "Total health endpoint requests denied by rate limiter"
|
||||
|
||||
- name: koalasync_rate_limit_denied_admin_metrics_auth
|
||||
path: '{.rateLimits.denied.adminMetricsAuth}'
|
||||
help: "Total admin metrics auth attempts denied by rate limiter"
|
||||
|
||||
- name: koalasync_rate_limit_denied_room_list
|
||||
path: '{.rateLimits.denied.roomList}'
|
||||
help: "Total room list refresh requests denied by rate limiter"
|
||||
@@ -6,13 +6,14 @@ A Manifest V3 Browser Extension (Chrome & Firefox) for synchronized video playba
|
||||
- **Manifest V3**: Optimized Service Worker architecture with session persistence.
|
||||
- **Pure Vanilla JS**: No external dependencies or heavy libraries.
|
||||
- **Smart Peer IDs**: Hexadecimal IDs combined with customizable Usernames for easy identification.
|
||||
- **Dual Heartbeat**: Advanced session tracking (Background) and video synchronization (Content) to prevent ghost sessions.
|
||||
- **On-Demand Connection**: The service worker only maintains a WebSocket connection while you're in a room. No persistent background connections — privacy-first architecture. Based on `connectIntent` flag that gates all reconnect attempts.
|
||||
- **Live Diagnostics**: Built-in "Dev" tab for real-time video state debugging (ReadyState, CurrentTime, etc.).
|
||||
- **Dynamic i18n (Multi-Language)**: Fully localized in 13 languages (`en`, `de`, `fr`, `es`, `it`, `pl`, `tr`, `nl`, `ja`, `ko`, `pt-BR`, `pt`, `ru`) with auto-detected fallback and dynamic on-the-fly language selectors.
|
||||
|
||||
## Tab Overview
|
||||
1. **Room**: Manage connections, view active peers, and share invitation links.
|
||||
2. **Sync**: Control video playback (Play/Pause/Force Sync) and view recent activity.
|
||||
3. **Settings**: Customize your Username and toggle domain-based Noise Filtering.
|
||||
3. **Settings**: Customize your Username, toggle domain-based Noise Filtering, and switch the App Language.
|
||||
4. **Dev**: Monitor connection status and view real-time video element metadata for debugging.
|
||||
|
||||
## Privacy & Permissions
|
||||
@@ -20,12 +21,12 @@ KoalaSync requires `<all_urls>` permission to detect and interact with video ele
|
||||
- **No Browsing History**: We do not track or store your browsing history.
|
||||
- **State Management**: Sensitive data (Room Passwords) is stored locally using `chrome.storage`.
|
||||
- **Zero Telemetry**: No analytics or external tracking scripts.
|
||||
- **Zero Runtime Dependencies**: The extension is built with pure Vanilla JS and contains no external libraries or tracking scripts, ensuring maximum performance and privacy.
|
||||
- **Zero Runtime Dependencies**: The extension is built with pure Vanilla JS and contains no external libraries or tracking scripts, ensuring performance and privacy.
|
||||
|
||||
## Installation
|
||||
1. **Prepare Extension**: From the repository root, run:
|
||||
```bash
|
||||
node scripts/build-extension.js
|
||||
npm run build:extension
|
||||
```
|
||||
2. Open Chrome and go to `chrome://extensions/`.
|
||||
3. Enable **Developer mode** (top right).
|
||||
@@ -34,6 +35,18 @@ KoalaSync requires `<all_urls>` permission to detect and interact with video ele
|
||||
## Development
|
||||
If you modify `shared/constants.js`, you must synchronize the changes by running the build script from the root:
|
||||
```bash
|
||||
node scripts/build-extension.js
|
||||
npm run build:extension
|
||||
```
|
||||
This ensures that the `extension/shared` folder is updated with the latest protocol constants.
|
||||
|
||||
## Module Structure
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `background.js` | Service worker: message routing, tab listeners, startup |
|
||||
| `content.js` | Video detection, audio processing, episode transition (IIFE) |
|
||||
| `popup.js` | Popup UI: join/create, tabs, status, settings |
|
||||
| `bridge.js` | Landing page bridge (injected into sync.koalastuff.net) |
|
||||
| `episode-utils.js` | Shared `extractEpisodeId()` / `sameEpisode()` — used by background.js, injected into content.js at build time |
|
||||
| `i18n.js` | Translation loader |
|
||||
| `shared/` | Constants, blacklist, name generator |
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "Synchronisiere die Videowiedergabe auf YouTube, Netflix, Emby, Jellyfin und jeder HTML5-Seite in Echtzeit mit Freunden."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "Synchronize video playback on YouTube, Netflix, Emby, Jellyfin, and any HTML5 site in real-time with friends."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "Mira videos junto a tus amigos en perfecta sincronización. Compatible con Netflix, YouTube, Twitch, Prime Video, Disney+ y cualquier reproductor HTML5."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "Synchronisez la lecture vidéo sur YouTube, Netflix, Emby, Jellyfin et tout site HTML5 en temps réel avec vos amis."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "Sincronizza la riproduzione video su YouTube, Netflix, Emby, Jellyfin e qualsiasi sito HTML5 in tempo reale con gli amici."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "YouTube、Netflix、Emby、Jellyfin、その他HTML5サイトでの動画再生を友達とリアルタイムで同期します。"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "YouTube, Netflix, Emby, Jellyfin 및 모든 HTML5 사이트에서 친구들과 실시간으로 비디오 재생을 동기화하세요."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "Synchroniseer video-afspelen op YouTube, Netflix, Emby, Jellyfin en elke HTML5-site in realtime met vrienden."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "Synchronizuj odtwarzanie wideo na YouTube, Netflix, Emby, Jellyfin i dowolnej stronie HTML5 w czasie rzeczywistym ze znajomymi."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "Assista a vídeos junto com seus amigos em sincronia perfeita. Compatível com Netflix, YouTube, Twitch, Prime Video, Disney+ e qualquer player HTML5."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "Veja vídeos em conjunto com os seus amigos em sincronia perfeita. Compatível com Netflix, YouTube, Twitch, Prime Video, Disney+ e qualquer leitor HTML5."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "Синхронизируйте воспроизведение видео на YouTube, Netflix, Emby, Jellyfin и любых HTML5-сайтах в реальном времени с друзьями."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "YouTube, Netflix, Emby, Jellyfin ve herhangi bir HTML5 sitesinde video oynatmayı arkadaşlarınızla gerçek zamanlı olarak senkronize edin."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "Синхронізуйте відтворення відео на YouTube, Netflix, Emby, Jellyfin і будь-якому сайті HTML5 у режимі реального часу з друзями."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "与朋友实时同步YouTube、Netflix、Emby、Jellyfin和任何HTML5网站上的视频播放。"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
:root {
|
||||
--bg: #0f172a;
|
||||
--card: #1e293b;
|
||||
--panel: #172033;
|
||||
--accent: #6366f1;
|
||||
--accent-hover: #818cf8;
|
||||
--text: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
--border: #334155;
|
||||
--radius: 8px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-shell {
|
||||
width: min(760px, calc(100vw - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.back-link:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 4px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--accent-hover);
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.muted-panel {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.section-heading,
|
||||
.master-toggle,
|
||||
.inline-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.master-toggle,
|
||||
.inline-toggle {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.preset-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
|
||||
gap: 10px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
.preset-card {
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.preset-card:hover {
|
||||
border-color: rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
.preset-card:has(input:checked) {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.35);
|
||||
}
|
||||
|
||||
.preset-card input {
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.custom-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: rgba(15, 23, 42, 0.58);
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
border-radius: var(--radius);
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.control-row {
|
||||
display: grid;
|
||||
grid-template-columns: 110px minmax(160px, 1fr) 78px 30px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.control-row label {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
width: 100%;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
input[type="number"]:focus {
|
||||
border-color: var(--accent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.unit {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 38px;
|
||||
height: 22px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
inset: 0;
|
||||
background-color: #334155;
|
||||
transition: .3s;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: #94a3b8;
|
||||
transition: .3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: var(--accent);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(16px);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.page-header,
|
||||
.section-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.control-row {
|
||||
grid-template-columns: 1fr 74px 28px;
|
||||
}
|
||||
|
||||
.control-row label {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>KoalaSync Audio Settings</title>
|
||||
<link rel="stylesheet" href="audio-options.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="page-shell">
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<a id="backLink" href="#" class="back-link" data-i18n="AUDIO_BACK">← Back</a>
|
||||
<p class="eyebrow">KoalaSync</p>
|
||||
<h1 data-i18n="AUDIO_PAGE_TITLE">Audio Settings</h1>
|
||||
</div>
|
||||
<label class="master-toggle">
|
||||
<span data-i18n="AUDIO_MASTER_TOGGLE">Audio Processing</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="audioEnabled">
|
||||
<span class="slider"></span>
|
||||
</span>
|
||||
</label>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="section-heading">
|
||||
<h2 data-i18n="AUDIO_COMPRESSOR">Compressor</h2>
|
||||
<label class="inline-toggle">
|
||||
<span data-i18n="AUDIO_COMPRESSOR_ENABLE">Enabled</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="compressorEnabled">
|
||||
<span class="slider"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="preset-group" role="radiogroup" aria-label="Compressor preset">
|
||||
<label class="preset-card">
|
||||
<input type="radio" name="preset" value="recommended">
|
||||
<span data-i18n="AUDIO_PRESET_RECOMMENDED">Recommended</span>
|
||||
</label>
|
||||
<label class="preset-card">
|
||||
<input type="radio" name="preset" value="dynamicRange">
|
||||
<span data-i18n="AUDIO_PRESET_DYNAMIC_RANGE">Dynamic Range</span>
|
||||
</label>
|
||||
<label class="preset-card">
|
||||
<input type="radio" name="preset" value="vocalEnhancement">
|
||||
<span data-i18n="AUDIO_PRESET_VOCAL_ENHANCEMENT">Vocal Enhancement</span>
|
||||
</label>
|
||||
<label class="preset-card">
|
||||
<input type="radio" name="preset" value="smooth">
|
||||
<span data-i18n="AUDIO_PRESET_SMOOTH">Smooth</span>
|
||||
</label>
|
||||
<label class="preset-card">
|
||||
<input type="radio" name="preset" value="custom">
|
||||
<span data-i18n="AUDIO_PRESET_CUSTOM">Custom</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="custom-grid" id="customControls">
|
||||
<div class="control-row" data-param="threshold">
|
||||
<label data-i18n="AUDIO_PARAM_THRESHOLD">Threshold</label>
|
||||
<input type="range" min="-60" max="0" step="1">
|
||||
<input type="number" min="-60" max="0" step="1">
|
||||
<span class="unit">dB</span>
|
||||
</div>
|
||||
<div class="control-row" data-param="knee">
|
||||
<label data-i18n="AUDIO_PARAM_KNEE">Knee</label>
|
||||
<input type="range" min="0" max="40" step="1">
|
||||
<input type="number" min="0" max="40" step="1">
|
||||
<span class="unit">dB</span>
|
||||
</div>
|
||||
<div class="control-row" data-param="ratio">
|
||||
<label data-i18n="AUDIO_PARAM_RATIO">Ratio</label>
|
||||
<input type="range" min="1" max="20" step="0.5">
|
||||
<input type="number" min="1" max="20" step="0.5">
|
||||
<span class="unit">:1</span>
|
||||
</div>
|
||||
<div class="control-row" data-param="attack">
|
||||
<label data-i18n="AUDIO_PARAM_ATTACK">Attack</label>
|
||||
<input type="range" min="0" max="1" step="0.001">
|
||||
<input type="number" min="0" max="1000" step="1" data-ms-input="true">
|
||||
<span class="unit">ms</span>
|
||||
</div>
|
||||
<div class="control-row" data-param="release">
|
||||
<label data-i18n="AUDIO_PARAM_RELEASE">Release</label>
|
||||
<input type="range" min="0" max="1" step="0.005">
|
||||
<input type="number" min="0" max="1000" step="5" data-ms-input="true">
|
||||
<span class="unit">ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel muted-panel">
|
||||
<div class="section-heading">
|
||||
<h2 data-i18n="AUDIO_EQUALIZER">Equalizer</h2>
|
||||
</div>
|
||||
<p data-i18n="AUDIO_COMING_SOON">Coming soon</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="audio-options.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,192 @@
|
||||
import { loadLocale, translateDOM, getSystemLanguage } from './i18n.js';
|
||||
|
||||
const PRESETS = {
|
||||
recommended: { threshold: -24, ratio: 8, attack: 0.010, release: 0.300, knee: 15 },
|
||||
dynamicRange: { threshold: -18, ratio: 4, attack: 0.020, release: 0.200, knee: 10 },
|
||||
vocalEnhancement: { threshold: -12, ratio: 3, attack: 0.015, release: 0.150, knee: 5 },
|
||||
smooth: { threshold: -30, ratio: 1.5, attack: 0.030, release: 0.250, knee: 20 },
|
||||
custom: { threshold: -24, ratio: 12, attack: 0.003, release: 0.250, knee: 30 }
|
||||
};
|
||||
|
||||
const DEFAULT_AUDIO_SETTINGS = {
|
||||
enabled: false,
|
||||
compressor: {
|
||||
enabled: false,
|
||||
preset: 'recommended',
|
||||
customParams: { ...PRESETS.custom }
|
||||
}
|
||||
};
|
||||
const PARAM_LIMITS = {
|
||||
threshold: { min: -60, max: 0 },
|
||||
knee: { min: 0, max: 40 },
|
||||
ratio: { min: 1, max: 20 },
|
||||
attack: { min: 0, max: 1 },
|
||||
release: { min: 0, max: 1 }
|
||||
};
|
||||
|
||||
const elements = {
|
||||
audioEnabled: document.getElementById('audioEnabled'),
|
||||
compressorEnabled: document.getElementById('compressorEnabled'),
|
||||
presetInputs: Array.from(document.querySelectorAll('input[name="preset"]')),
|
||||
controlRows: Array.from(document.querySelectorAll('.control-row')),
|
||||
backLink: document.getElementById('backLink')
|
||||
};
|
||||
|
||||
let saveTimer = null;
|
||||
let isRendering = false;
|
||||
|
||||
function cloneDefaultSettings() {
|
||||
return JSON.parse(JSON.stringify(DEFAULT_AUDIO_SETTINGS));
|
||||
}
|
||||
|
||||
let currentSettings = cloneDefaultSettings();
|
||||
|
||||
function mergeAudioSettings(settings = {}) {
|
||||
const safeSettings = settings && typeof settings === 'object' ? settings : {};
|
||||
const defaults = cloneDefaultSettings();
|
||||
return {
|
||||
...defaults,
|
||||
...safeSettings,
|
||||
compressor: {
|
||||
...defaults.compressor,
|
||||
...(safeSettings.compressor || {}),
|
||||
customParams: {
|
||||
...defaults.compressor.customParams,
|
||||
...(safeSettings.compressor?.customParams || {})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function debounceSave() {
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
saveTimer = setTimeout(() => {
|
||||
chrome.storage.local.set({ audioSettings: currentSettings });
|
||||
}, 40);
|
||||
}
|
||||
|
||||
function getParamValue(param, value, isMsInput = false) {
|
||||
const parsed = Number(value);
|
||||
const candidate = Number.isFinite(parsed)
|
||||
? (isMsInput ? parsed / 1000 : parsed)
|
||||
: currentSettings.compressor.customParams[param];
|
||||
const limits = PARAM_LIMITS[param];
|
||||
if (!limits) return candidate;
|
||||
return Math.min(limits.max, Math.max(limits.min, candidate));
|
||||
}
|
||||
|
||||
function formatNumber(value, param, isMsInput = false) {
|
||||
if (isMsInput) return Math.round(value * 1000);
|
||||
if (param === 'ratio') return Number(value).toFixed(1).replace(/\.0$/, '');
|
||||
return value;
|
||||
}
|
||||
|
||||
function render() {
|
||||
isRendering = true;
|
||||
elements.audioEnabled.checked = currentSettings.enabled === true;
|
||||
elements.compressorEnabled.checked = currentSettings.compressor.enabled === true;
|
||||
|
||||
const selectedPreset = currentSettings.compressor.preset || 'recommended';
|
||||
elements.presetInputs.forEach(input => {
|
||||
input.checked = input.value === selectedPreset;
|
||||
});
|
||||
|
||||
const params = selectedPreset === 'custom'
|
||||
? currentSettings.compressor.customParams
|
||||
: PRESETS[selectedPreset] || PRESETS.recommended;
|
||||
|
||||
elements.controlRows.forEach(row => {
|
||||
const param = row.dataset.param;
|
||||
const range = row.querySelector('input[type="range"]');
|
||||
const number = row.querySelector('input[type="number"]');
|
||||
const value = params[param];
|
||||
range.value = value;
|
||||
number.value = formatNumber(value, param, number.dataset.msInput === 'true');
|
||||
});
|
||||
isRendering = false;
|
||||
}
|
||||
|
||||
function setPreset(preset) {
|
||||
currentSettings.compressor.preset = preset;
|
||||
if (preset === 'custom') {
|
||||
currentSettings.compressor.customParams = {
|
||||
...PRESETS.custom,
|
||||
...currentSettings.compressor.customParams
|
||||
};
|
||||
}
|
||||
render();
|
||||
debounceSave();
|
||||
}
|
||||
|
||||
function setCustomParam(param, value) {
|
||||
currentSettings.compressor.preset = 'custom';
|
||||
currentSettings.compressor.customParams[param] = getParamValue(param, value);
|
||||
render();
|
||||
debounceSave();
|
||||
}
|
||||
|
||||
async function init() {
|
||||
// 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(audioSettings);
|
||||
render();
|
||||
}
|
||||
|
||||
elements.audioEnabled.addEventListener('change', () => {
|
||||
currentSettings.enabled = elements.audioEnabled.checked;
|
||||
if (currentSettings.enabled && !currentSettings.compressor.enabled) {
|
||||
currentSettings.compressor.enabled = true;
|
||||
}
|
||||
render();
|
||||
debounceSave();
|
||||
});
|
||||
|
||||
elements.compressorEnabled.addEventListener('change', () => {
|
||||
currentSettings.compressor.enabled = elements.compressorEnabled.checked;
|
||||
if (currentSettings.compressor.enabled) currentSettings.enabled = true;
|
||||
render();
|
||||
debounceSave();
|
||||
});
|
||||
|
||||
elements.presetInputs.forEach(input => {
|
||||
input.addEventListener('change', () => {
|
||||
if (input.checked) setPreset(input.value);
|
||||
});
|
||||
});
|
||||
|
||||
elements.controlRows.forEach(row => {
|
||||
const param = row.dataset.param;
|
||||
const range = row.querySelector('input[type="range"]');
|
||||
const number = row.querySelector('input[type="number"]');
|
||||
|
||||
range.addEventListener('input', () => {
|
||||
if (isRendering) return;
|
||||
setCustomParam(param, getParamValue(param, range.value));
|
||||
});
|
||||
|
||||
number.addEventListener('input', () => {
|
||||
if (isRendering) return;
|
||||
setCustomParam(param, getParamValue(param, number.value, number.dataset.msInput === 'true'));
|
||||
});
|
||||
});
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area !== 'local' || !changes.audioSettings) return;
|
||||
currentSettings = mergeAudioSettings(changes.audioSettings.newValue);
|
||||
render();
|
||||
});
|
||||
|
||||
if (elements.backLink) {
|
||||
elements.backLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
|
||||
init().catch(err => {
|
||||
console.error('[AudioOptions] Failed to initialize:', err);
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
/* global cloneInto */
|
||||
/**
|
||||
* KoalaSync Bridge Script
|
||||
* Injected into sync.koalastuff.net to facilitate communication between
|
||||
@@ -9,6 +10,7 @@ document.documentElement.dataset.koalasyncInstalled = 'true';
|
||||
|
||||
// 2. Listen for Join Requests from the Website
|
||||
window.addEventListener('KOALASYNC_JOIN_REQUEST', (e) => {
|
||||
if (!e || !e.detail) return;
|
||||
const { roomId, password, useCustomServer, serverUrl } = e.detail;
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'WEB_JOIN_REQUEST',
|
||||
@@ -16,18 +18,22 @@ window.addEventListener('KOALASYNC_JOIN_REQUEST', (e) => {
|
||||
password,
|
||||
useCustomServer,
|
||||
serverUrl
|
||||
});
|
||||
}).catch(() => {});
|
||||
});
|
||||
|
||||
// 3. Listen for Status Updates from the Extension and relay to Website
|
||||
chrome.runtime.onMessage.addListener((msg) => {
|
||||
if (!msg) return;
|
||||
if (msg.type === 'JOIN_STATUS') {
|
||||
const event = new CustomEvent('KOALASYNC_STATUS', {
|
||||
detail: {
|
||||
success: msg.success,
|
||||
message: msg.message
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
const detail = { success: msg.success, message: msg.message };
|
||||
// Firefox MV3 content scripts run in an isolated world. When dispatching
|
||||
// a CustomEvent with a detail object, Firefox wraps it in an XrayWrapper
|
||||
// that the page's JavaScript cannot destructure (Permission denied).
|
||||
// cloneInto() exposes the object to the page's context correctly.
|
||||
// Chrome doesn't have this issue — cloneInto() is undefined there.
|
||||
const safeDetail = typeof cloneInto === 'function'
|
||||
? cloneInto(detail, document.defaultView)
|
||||
: detail;
|
||||
window.dispatchEvent(new CustomEvent('KOALASYNC_STATUS', { detail: safeDetail }));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* KoalaSync Episode Title Utilities
|
||||
* Single source of truth — synced to content.js by build-extension.js.
|
||||
* Keep in sync with the injection block in content.js!
|
||||
*/
|
||||
|
||||
export function extractEpisodeId(title) {
|
||||
if (!title || typeof title !== 'string') return null;
|
||||
const se = title.match(/S(?:eason\s*)?(\d+)[^a-zA-Z0-9]*E(?:pisode\s*)?(\d+)/i);
|
||||
if (se) return `S${String(se[1]).padStart(2, '0')}E${String(se[2]).padStart(2, '0')}`;
|
||||
const ep = title.match(/(?:Episode|Folge|Ep\.?|#)\s*(\d+)/i);
|
||||
if (ep) return `EP${String(ep[1]).padStart(3, '0')}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function sameEpisode(titleA, titleB) {
|
||||
if (!titleA && !titleB) return true;
|
||||
if (!titleA || !titleB) return false;
|
||||
const idA = extractEpisodeId(titleA);
|
||||
const idB = extractEpisodeId(titleB);
|
||||
if (idA && idB) return idA === idB;
|
||||
if (idA || idB) return false;
|
||||
return titleA === titleB;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// extension/i18n.js
|
||||
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 = {};
|
||||
const dictionaryCache = {};
|
||||
let currentLanguage = null;
|
||||
|
||||
/**
|
||||
* Resolves, loads, and merges the target language with the English baseline fallback.
|
||||
* @param {string} langCode - Target language (e.g. 'de')
|
||||
*/
|
||||
export async function loadLocale(langCode) {
|
||||
const resolvedLang = SUPPORTED_LANGUAGES.includes(langCode) ? langCode : DEFAULT_LANGUAGE;
|
||||
|
||||
if (currentLanguage === resolvedLang && Object.keys(activeDictionary).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dictionaryCache[resolvedLang]) {
|
||||
activeDictionary = dictionaryCache[resolvedLang];
|
||||
currentLanguage = resolvedLang;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Load Baseline English
|
||||
let enDict;
|
||||
if (dictionaryCache[DEFAULT_LANGUAGE]) {
|
||||
enDict = dictionaryCache[DEFAULT_LANGUAGE];
|
||||
} else {
|
||||
const enResponse = await fetch(chrome.runtime.getURL(`locales/${DEFAULT_LANGUAGE}.json`));
|
||||
enDict = await enResponse.json();
|
||||
dictionaryCache[DEFAULT_LANGUAGE] = enDict;
|
||||
}
|
||||
|
||||
if (resolvedLang === DEFAULT_LANGUAGE) {
|
||||
activeDictionary = enDict;
|
||||
currentLanguage = resolvedLang;
|
||||
return;
|
||||
}
|
||||
|
||||
// Load Target Locale
|
||||
const targetResponse = await fetch(chrome.runtime.getURL(`locales/${resolvedLang}.json`));
|
||||
const targetDict = await targetResponse.json();
|
||||
|
||||
// Airtight Fallback Merge: target overrides en, missing elements fallback to en
|
||||
const mergedDict = Object.assign({}, enDict, targetDict);
|
||||
dictionaryCache[resolvedLang] = mergedDict;
|
||||
activeDictionary = mergedDict;
|
||||
currentLanguage = resolvedLang;
|
||||
} catch (err) {
|
||||
console.error('[i18n] Failed to load locale. Defaulting to English:', err);
|
||||
// Fallback directly to static English if fetching fails
|
||||
try {
|
||||
let enDict;
|
||||
if (dictionaryCache[DEFAULT_LANGUAGE]) {
|
||||
enDict = dictionaryCache[DEFAULT_LANGUAGE];
|
||||
} else {
|
||||
const enResponse = await fetch(chrome.runtime.getURL(`locales/${DEFAULT_LANGUAGE}.json`));
|
||||
enDict = await enResponse.json();
|
||||
dictionaryCache[DEFAULT_LANGUAGE] = enDict;
|
||||
}
|
||||
activeDictionary = enDict;
|
||||
currentLanguage = DEFAULT_LANGUAGE;
|
||||
} catch (_) {
|
||||
activeDictionary = {};
|
||||
currentLanguage = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translated string for a given key. Supports optional value interpolation.
|
||||
* @param {string} key - Dictionary key
|
||||
* @param {object} [placeholders] - Key-value map for replacements (e.g., { name: 'Alice' })
|
||||
* @returns {string} Translated string or the key itself
|
||||
*/
|
||||
export function getMessage(key, placeholders = null) {
|
||||
let msg = activeDictionary[key] !== undefined ? String(activeDictionary[key]) : key;
|
||||
if (placeholders && typeof placeholders === 'object') {
|
||||
for (const [k, v] of Object.entries(placeholders)) {
|
||||
msg = msg.replace(new RegExp(`{${k}}`, 'g'), v);
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs dynamic DOM replacements for elements carrying data-i18n attributes.
|
||||
*/
|
||||
export function translateDOM() {
|
||||
// 1. Text Content
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n');
|
||||
const translated = getMessage(key);
|
||||
|
||||
// Special case: Preserve logo image elements inside headers (like h1 logo)
|
||||
const img = el.querySelector('img');
|
||||
if (img) {
|
||||
el.innerHTML = '';
|
||||
el.appendChild(img);
|
||||
el.appendChild(document.createTextNode(' ' + translated));
|
||||
} else {
|
||||
el.textContent = translated;
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Tooltips (titles)
|
||||
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n-title');
|
||||
el.setAttribute('title', getMessage(key));
|
||||
});
|
||||
|
||||
// 3. Placeholders
|
||||
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n-placeholder');
|
||||
el.setAttribute('placeholder', getMessage(key));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects and maps the user's system language to the best supported locale.
|
||||
* @returns {string} Supported language code
|
||||
*/
|
||||
export function getSystemLanguage() {
|
||||
const uiLang = (typeof chrome !== 'undefined' && chrome.i18n && chrome.i18n.getUILanguage)
|
||||
? chrome.i18n.getUILanguage()
|
||||
: '';
|
||||
const fullLang = (navigator.language || uiLang || '').toLowerCase();
|
||||
if (fullLang.startsWith('pt-br')) {
|
||||
return 'pt-BR';
|
||||
}
|
||||
const baseLang = fullLang.split('-')[0];
|
||||
return SUPPORTED_LANGUAGES.includes(baseLang) ? baseLang : DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 889 B After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"LANG_CODE": "de",
|
||||
"HTML_CLASS": "lang-de",
|
||||
"APP_TITLE": "KoalaSync",
|
||||
"TAB_ROOM": "Raum",
|
||||
"TAB_ROOM_TOOLTIP": "Raum-Einstellungen und Verbindung",
|
||||
"TAB_SYNC": "Sync",
|
||||
"TAB_SYNC_TOOLTIP": "Video-Synchronisations-Steuerung und Aktionen",
|
||||
"TAB_SETTINGS": "Optionen",
|
||||
"TAB_SETTINGS_TOOLTIP": "Erweiterungseinstellungen",
|
||||
"TAB_STATUS": "Status",
|
||||
"TAB_STATUS_TOOLTIP": "Erweiterte Diagnose & Protokolle",
|
||||
"BTN_CREATE_ROOM": "+ Neuer Raum",
|
||||
"MANUAL_CONNECT_HEADER": "Manuell verbinden / Erweitert",
|
||||
"LABEL_SERVER": "Server",
|
||||
"BTN_SERVER_OFFICIAL": "Offiziell",
|
||||
"BTN_SERVER_OFFICIAL_TOOLTIP": "Verwende den offiziellen, zuverlässigen Server",
|
||||
"BTN_SERVER_CUSTOM": "Eigener",
|
||||
"BTN_SERVER_CUSTOM_TOOLTIP": "Verbinde dich mit deinem eigenen, selbstgehosteten Server",
|
||||
"PLACEHOLDER_SERVER_URL": "wss://dein-server:3000",
|
||||
"LABEL_ROOM_ID": "Raum-ID",
|
||||
"LABEL_ROOM_ID_TOOLTIP": "Der eindeutige Identifikator für deinen Sync-Raum",
|
||||
"PLACEHOLDER_ROOM_ID": "Raum-ID eingeben",
|
||||
"PLACEHOLDER_ROOM_ID_TOOLTIP": "Die eindeutige ID des Raums, dem du beitreten möchtest",
|
||||
"LABEL_PASSWORD": "Passwort (Optional)",
|
||||
"LABEL_PASSWORD_TOOLTIP": "Optionales Passwort, um den Raumzugang zu beschränken",
|
||||
"PLACEHOLDER_PASSWORD": "Raum-Passwort (optional)",
|
||||
"PLACEHOLDER_PASSWORD_TOOLTIP": "Passwort für den Raum (leer lassen, wenn keines vorhanden)",
|
||||
"BTN_JOIN_ROOM": "Raum beitreten / erstellen",
|
||||
"BTN_JOIN_ROOM_TOOLTIP": "Mit dem Raum verbinden",
|
||||
"LABEL_PUBLIC_ROOMS": "Öffentliche Räume",
|
||||
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Liste der öffentlich verfügbaren Räume auf diesem Server",
|
||||
"BTN_REFRESH": "AKTUALISIEREN",
|
||||
"BTN_REFRESH_TOOLTIP": "Die Liste der öffentlichen Räume aktualisieren",
|
||||
"PUBLIC_ROOMS_REFRESHING": "Aktualisiere...",
|
||||
"BTN_REFRESH_COOLDOWN": "WARTE {seconds}s",
|
||||
"BTN_REFRESH_COOLDOWN_TOOLTIP": "Die Raumliste kühlt ab. Versuche es in {seconds}s erneut.",
|
||||
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Aktualisiere öffentliche Räume. Nächste Aktualisierung in {seconds}s verfügbar.",
|
||||
"LABEL_ACTIVE_ROOM": "Aktiver Raum",
|
||||
"LABEL_ACTIVE_ROOM_TOOLTIP": "Der Raum, mit dem du gerade verbunden bist",
|
||||
"ACTIVE_ROOM_NONE": "KEINER",
|
||||
"ACTIVE_SERVER_OFFICIAL": "Offizieller Server",
|
||||
"LABEL_INVITE_LINK": "Einladungslink",
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Teile diesen Link mit Freunden, damit sie beitreten können",
|
||||
"LABEL_PEERS_IN_ROOM": "Teilnehmer im Raum",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Andere Benutzer, die derzeit mit diesem Raum verbunden sind",
|
||||
"LABEL_HOST_CONTROL": "Host-Steuerung",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Festlegen, wer die Wiedergabe in diesem Raum steuern darf",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Nur ich darf die Wiedergabe steuern",
|
||||
"NOTICE_HOST_CONTROLS": "Der Host steuert die Wiedergabe für alle.",
|
||||
"NOTICE_COHOST_HINT": "Tipp: Gib einzelnen Teilnehmern unten in der Teilnehmerliste die Steuerung.",
|
||||
"BADGE_HOST": "Host",
|
||||
"BADGE_GUEST": "Gast",
|
||||
"BADGE_CONTROLLER": "Controller",
|
||||
"BTN_GIVE_CONTROL": "Steuerung geben",
|
||||
"BTN_REVOKE_CONTROL": "Entziehen",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Schaut alleine — ignoriert die Befehle des Hosts",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · Der Host steuert diesen Raum",
|
||||
"HCM_DIALOG_BODY": "Nur der Host kann die Wiedergabe in diesem Raum steuern. Weiter gemeinsam schauen oder allein weiterschauen?",
|
||||
"HCM_DIALOG_STAY": "Synchron bleiben",
|
||||
"HCM_DIALOG_SOLO": "Allein weiterschauen",
|
||||
"HCM_BADGE_SOLO": "Du schaust allein",
|
||||
"HCM_BADGE_RESYNC": "Neu sync",
|
||||
"NO_PEERS_CONNECTED": "Keine Teilnehmer verbunden",
|
||||
"BTN_LEAVE_ROOM": "Raum verlassen",
|
||||
"LABEL_SELECT_VIDEO": "Video auswählen",
|
||||
"LABEL_SELECT_VIDEO_TOOLTIP": "Wähle den Browser-Tab aus, der das zu synchronisierende Video enthält",
|
||||
"OPTION_SELECT_TAB": "-- Wähle einen Tab --",
|
||||
"LABEL_REMOTE_CONTROL": "Fernsteuerung",
|
||||
"BTN_COPY_INVITE": "📋 Einladungslink",
|
||||
"BTN_COPY_INVITE_TOOLTIP": "Einladungslink kopieren",
|
||||
"BTN_PLAY": "▶ Abspielen",
|
||||
"BTN_PLAY_TOOLTIP": "Sende einen Play-Befehl an alle",
|
||||
"BTN_PAUSE": "⏸ Pause",
|
||||
"BTN_PAUSE_TOOLTIP": "Sende einen Pause-Befehl an alle",
|
||||
"BTN_SYNC": "⚡ SYNC",
|
||||
"BTN_SYNC_TOOLTIP": "Erzwinge die Synchronisation aller Teilnehmer",
|
||||
"OPTION_JUMP_TO_OTHERS": "Zu anderen springen",
|
||||
"OPTION_JUMP_TO_ME": "Zu mir springen",
|
||||
"OPTION_JUMP_MODE_TOOLTIP": "Synchronisationsziel auswählen",
|
||||
"LABEL_LAST_ACTIVITY": "Letzter Aktivitätsstatus",
|
||||
"LABEL_LAST_ACTIVITY_TOOLTIP": "Zeigt den neuesten Play-, Pause- oder Seek-Befehl",
|
||||
"NO_RECENT_COMMANDS": "Keine aktuellen Befehle",
|
||||
"LOBBY_HEADER": "EPISODEN-LOBBY",
|
||||
"LOBBY_WAITING_FOR": "🎬 Warte auf: \"{title}\"",
|
||||
"LOBBY_WAITING_PEERS": "Warte auf Teilnehmer...",
|
||||
"BTN_SKIP_PLAY": "Überspringen & Abspielen",
|
||||
"BTN_SKIP_PLAY_TOOLTIP": "Lobby abbrechen und trotzdem abspielen",
|
||||
"LOBBY_CONNECT_FIRST": "Zuerst Raum beitreten",
|
||||
"LOBBY_CONNECT_FIRST_DESC": "Du musst über einen Einladungslink beitreten oder einen neuen Raum erstellen, um Videos zu synchronisieren.",
|
||||
"BTN_CREATE_ROOM_ALT": "Neuen Raum erstellen",
|
||||
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Einen neuen zufälligen Raum erstellen und beitreten",
|
||||
"LABEL_USERNAME": "Dein Benutzername",
|
||||
"LABEL_USERNAME_TOOLTIP": "Der Benutzername hilft anderen, dich zu identifizieren.",
|
||||
"PLACEHOLDER_USERNAME": "Anonymer Koala",
|
||||
"LABEL_HIDE_CLUTTER": "Aufgeräumte Tab-Liste",
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtert Nicht-Video-Tabs und irrelevante Domains heraus, um die Liste sauber zu halten",
|
||||
"LABEL_AUTO_SYNC_NEXT": "Nächste Episode auto-syncen",
|
||||
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pausiert automatisch und wartet auf alle Teilnehmer bei Episodenwechsel, startet dann synchron.",
|
||||
"LABEL_AUTO_COPY_INVITE": "Einladungslink auto-kopieren",
|
||||
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Kopiert den Einladungslink automatisch beim Erstellen eines Raums.",
|
||||
"LABEL_NOTIFICATIONS": "Browser-Benachrichtigungen",
|
||||
"LABEL_NOTIFICATIONS_TOOLTIP": "Zeigt Systembenachrichtigungen, wenn jemand beitritt/verlässt oder abspielt/pausiert.",
|
||||
"LABEL_LANGUAGE": "Sprache",
|
||||
"LABEL_LANGUAGE_TOOLTIP": "Wähle deine bevorzugte Sprache für die Erweiterung aus",
|
||||
"LABEL_TROUBLESHOOTING": "Fehlerbehebung",
|
||||
"LABEL_TROUBLESHOOTING_TOOLTIP": "Werkzeuge zur Behebung von Verbindungsproblemen",
|
||||
"BTN_REGEN_ID": "Peer-ID neu generieren",
|
||||
"BTN_REGEN_ID_TOOLTIP": "Generiere deine interne ID neu und verbinde dich erneut",
|
||||
"REGEN_ID_DESC": "Verwende dies, wenn du Fehler wegen doppelter Identität siehst.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Anderes Problem? Öffne ein GitHub Issue",
|
||||
"TOAST_ID_REGENERATED": "Identität neu generiert — Verbindung wird wiederhergestellt…",
|
||||
"LABEL_CONN_STATUS": "Verbindungsstatus",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Aktueller WebSocket-Verbindungsstatus",
|
||||
"CONN_STATUS_DISCONNECTED": "Getrennt",
|
||||
"BTN_RETRY": "WIEDERHOLEN",
|
||||
"BTN_RETRY_TOOLTIP": "Versuchen, die Verbindung zum Server wiederherzustellen",
|
||||
"BTN_COPY_LOGS": "Logs kopieren",
|
||||
"BTN_COPY_LOGS_TOOLTIP": "Logs zum Teilen in die Zwischenablage kopieren",
|
||||
"LABEL_VIDEO_DEBUG": "Video-Debug-Info",
|
||||
"LABEL_VIDEO_DEBUG_TOOLTIP": "Technische Details zum aktuell ausgewählten Video-Element",
|
||||
"VIDEO_DEBUG_EMPTY": "Kein Tab ausgewählt oder kein Video erkannt.",
|
||||
"LABEL_HISTORY": "Aktionsverlauf",
|
||||
"LABEL_HISTORY_TOOLTIP": "Chronologisches Protokoll aller Sync-Befehle im Raum",
|
||||
"HISTORY_EMPTY": "Noch keine Aktivität",
|
||||
"LABEL_LOGS": "Logs (Letzte 50)",
|
||||
"LABEL_LOGS_TOOLTIP": "Technische Verbindungsprotokolle zur Fehlersuche",
|
||||
"BTN_CLEAR": "LEEREN",
|
||||
"BTN_CLEAR_TOOLTIP": "Protokollausgabe leeren",
|
||||
"LABEL_GITHUB": "GitHub-Repository",
|
||||
"BTN_ONBOARDING_SKIP": "Überspringen",
|
||||
"BTN_ONBOARDING_SKIP_TOOLTIP": "Einführung überspringen",
|
||||
"BTN_ONBOARDING_NEXT": "Weiter",
|
||||
"BTN_ONBOARDING_NEXT_TOOLTIP": "Zum nächsten Schritt gehen",
|
||||
"ONBOARDING_1_TITLE": "Willkommen bei KoalaSync!",
|
||||
"ONBOARDING_1_TEXT": "Schau Videos in perfekter Synchronität zusammen — egal wo ihr seid. Machen wir eine kurze Tour!",
|
||||
"ONBOARDING_2_TITLE": "1. Raum erstellen",
|
||||
"ONBOARDING_2_TEXT": "Hier geht's los. Erstelle einen Raum und teile den Einladungslink mit deinen Freunden.",
|
||||
"ONBOARDING_3_TITLE": "2. Video auswählen",
|
||||
"ONBOARDING_3_TEXT": "Wähle hier das Video aus, das du synchronisieren willst. Abspielen, Pause, Spulen — alle bleiben synchron.",
|
||||
"ONBOARDING_4_TITLE": "3. Personalisieren",
|
||||
"ONBOARDING_4_TEXT": "Wähle einen lustigen Benutzernamen, damit deine Freunde wissen, wer du bist.",
|
||||
"ONBOARDING_5_TITLE": "Alles startklar!",
|
||||
"ONBOARDING_5_TEXT": "Zeit, das Popcorn zu holen. Viel Spaß beim gemeinsamen Schauen!",
|
||||
"ERR_CONN_TIMEOUT": "Verbindung abgelaufen. Bitte versuche es erneut.",
|
||||
"ERR_INVALID_SERVER_URL": "Ungültiges Server-URL-Format.",
|
||||
"ERR_IDENTITY_NOT_LOADED": "Identität noch nicht geladen. Warte kurz und versuche es erneut.",
|
||||
"ERR_NO_PEERS_TIME": "Keine anderen Teilnehmer mit bekannter Zeit. Wechsle zu 'Zu mir springen'.",
|
||||
"ERR_NO_VIDEO_TAB": "Verbindung zum Video-Tab fehlgeschlagen.",
|
||||
"ERR_SELECT_VIDEO": "Bitte wähle zuerst ein Video aus!",
|
||||
"TOAST_INVITE_COPIED": "Einladungslink kopiert!",
|
||||
"TOAST_COPY_FAILED": "Kopieren in Zwischenablage fehlgeschlagen",
|
||||
"TOAST_LOBBY_SKIPPED": "Episoden-Lobby übersprungen.",
|
||||
"TOAST_LOBBY_SKIP_FAILED": "Überspringen der Lobby fehlgeschlagen.",
|
||||
"TOAST_LOGS_COPIED": "Kopiert!",
|
||||
"TOAST_PEER_JOINED": "{name} ist dem Raum beigetreten",
|
||||
"TOAST_PEER_LEFT": "{name} hat den Raum verlassen",
|
||||
"TOAST_PEER_ACTION": "{name} hat {action}",
|
||||
"STATUS_CONNECTED": "Verbunden",
|
||||
"STATUS_RECONNECTING": "Verbinde erneut...",
|
||||
"STATUS_CONNECTING": "Verbinde...",
|
||||
"STATUS_FAILED": "Fehlgeschlagen",
|
||||
"STATUS_DISCONNECTED": "Getrennt",
|
||||
"STATUS_IDLE": "Bereit zum Verbinden",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync ist bereit. Tritt einem Raum bei oder erstelle einen, um die Verbindung herzustellen und die Synchronisierung zu starten.",
|
||||
"BTN_STATE_JOINING": "🚀 Trete bei...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Verbinde erneut...",
|
||||
"BTN_STATE_PLAYING": "▶ Spiele ab...",
|
||||
"BTN_STATE_PAUSING": "⏸ Pausiere...",
|
||||
"BTN_STATE_SYNCING_GROUP": "Synce zur Gruppe ({time})...",
|
||||
"BTN_STATE_SYNCING": "Synchronisiere...",
|
||||
"BTN_STATE_SYNCED": "✅ Synchronisiert!",
|
||||
"NOTIF_PLAY": "die Wiedergabe gestartet",
|
||||
"NOTIF_PAUSE": "die Wiedergabe pausiert",
|
||||
"NOTIF_SEEK": "im Video gespult",
|
||||
"NOTIF_FORCE_PREPARE": "eine erzwungene Synchronisation gestartet",
|
||||
"NOTIF_FORCE_EXECUTE": "alle Teilnehmer synchronisiert",
|
||||
"DEBUG_NO_TAB": "Kein Ziel-Tab ausgewählt.",
|
||||
"DEBUG_COMM_FAIL": "Kommunikation mit dem Tab-Video fehlgeschlagen.",
|
||||
"EMPTY_PEERS_TITLE": "Noch keine Teilnehmer",
|
||||
"EMPTY_PEERS_HINT": "Teile deinen Einladungslink, um loszulegen",
|
||||
"EMPTY_HISTORY_TITLE": "Noch keine Aktivität",
|
||||
"EMPTY_HISTORY_HINT": "Abspielen, Pausieren oder Spulen, um Verlauf zu sehen",
|
||||
"EMPTY_LOGS_TITLE": "Keine Logs",
|
||||
"EMPTY_LOGS_HINT": "Verbindungsereignisse werden hier angezeigt",
|
||||
"EMPTY_ROOMS_TITLE": "Keine aktiven Räume",
|
||||
"EMPTY_ROOMS_HINT": "Raum erstellen oder aktualisieren, um öffentliche zu finden",
|
||||
"LABEL_YOU": "Du",
|
||||
"ONBOARDING_DONE": "Fertig!",
|
||||
"LABEL_LOBBY_PEER_READY": "Bereit",
|
||||
"LABEL_LOBBY_PEER_LOADING": "Lädt...",
|
||||
"LABEL_PASSWORD_PROTECTED": "Passwortgeschützt",
|
||||
"LABEL_PEERS_COUNT": "{count} Teilnehmer",
|
||||
"LABEL_CUSTOM_SERVER": "Eigener Server",
|
||||
"BTN_STATE_CREATING": "🚀 Erstelle Raum...",
|
||||
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Episoden-Synchronisation fehlgeschlagen",
|
||||
"NOTIF_LOBBY_CANCEL_MSG": "Auto-Sync abgebrochen: {reason}. Du musst eventuell manuell synchronisieren.",
|
||||
"LOBBY_CANCEL_TIMEOUT": "Zeitüberschreitung",
|
||||
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Zeitüberschreitung (wiederhergestellt)",
|
||||
"LOBBY_CANCEL_PEERS_LEFT": "Alle anderen Teilnehmer haben den Raum verlassen",
|
||||
"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": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Bewerten",
|
||||
"FOOTER_SUPPORT_PROMPT": "Gefällt dir KoalaSync? Hinterlasse eine Bewertung!",
|
||||
"LABEL_AUDIO_PROCESSING": "Audio-Verarbeitung",
|
||||
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Wendet Audioeffekte wie Kompression auf die Videowiedergabe an",
|
||||
"AUDIO_OPEN_SETTINGS": "Öffnen",
|
||||
"NEW_FEATURE_AUDIO": "Neu: Audio-Verarbeitung — probiere den Kompressor aus!",
|
||||
"AUDIO_BACK": "← Zurück",
|
||||
"AUDIO_PAGE_TITLE": "Audio-Einstellungen",
|
||||
"AUDIO_MASTER_TOGGLE": "Audio-Verarbeitung",
|
||||
"AUDIO_COMPRESSOR": "Kompressor",
|
||||
"AUDIO_COMPRESSOR_ENABLE": "Aktiviert",
|
||||
"AUDIO_PRESET": "Preset",
|
||||
"AUDIO_PRESET_RECOMMENDED": "Empfohlen",
|
||||
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamikumfang",
|
||||
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Sprache verbessern",
|
||||
"AUDIO_PRESET_SMOOTH": "Sanft",
|
||||
"AUDIO_PRESET_CUSTOM": "Benutzerdefiniert",
|
||||
"AUDIO_PARAM_THRESHOLD": "Schwellwert",
|
||||
"AUDIO_PARAM_KNEE": "Knee",
|
||||
"AUDIO_PARAM_RATIO": "Ratio",
|
||||
"AUDIO_PARAM_ATTACK": "Attack",
|
||||
"AUDIO_PARAM_RELEASE": "Release",
|
||||
"AUDIO_EQUALIZER": "Equalizer",
|
||||
"AUDIO_COMING_SOON": "Demnächst",
|
||||
"BTN_RESTART_TOUR": "Einführung neu starten",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Startet die Einführung für neue Benutzer erneut",
|
||||
"HINT_SELECT_VIDEO": "Wähle hier dein Video aus!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Voll senden",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Nur Episoden-ID senden",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Nicht senden",
|
||||
"LABEL_PRIVACY_SETTINGS": "Privatsphäre-Einstellungen",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Lege fest, welche Tab- und Medientitel an den Raum gesendet werden.",
|
||||
"LABEL_SEND_TAB_TITLE": "Tab-Titel senden",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Sendet den Titel des ausgewählten Browser-Tabs an den Raum.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Medientitel",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Lege fest, wie Medientitel an den Raum gesendet werden."
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"LANG_CODE": "en",
|
||||
"HTML_CLASS": "lang-en",
|
||||
"APP_TITLE": "KoalaSync",
|
||||
"TAB_ROOM": "Room",
|
||||
"TAB_ROOM_TOOLTIP": "Room settings and connection",
|
||||
"TAB_SYNC": "Sync",
|
||||
"TAB_SYNC_TOOLTIP": "Video sync controls and remote actions",
|
||||
"TAB_SETTINGS": "Settings",
|
||||
"TAB_SETTINGS_TOOLTIP": "Extension preferences",
|
||||
"TAB_STATUS": "Status",
|
||||
"TAB_STATUS_TOOLTIP": "Advanced Diagnostics & Logs",
|
||||
"BTN_CREATE_ROOM": "+ Create New Room",
|
||||
"MANUAL_CONNECT_HEADER": "Manual Connect / Advanced",
|
||||
"LABEL_SERVER": "Server",
|
||||
"BTN_SERVER_OFFICIAL": "Official",
|
||||
"BTN_SERVER_OFFICIAL_TOOLTIP": "Use the official reliable server",
|
||||
"BTN_SERVER_CUSTOM": "Custom",
|
||||
"BTN_SERVER_CUSTOM_TOOLTIP": "Connect to your own self-hosted server",
|
||||
"PLACEHOLDER_SERVER_URL": "wss://your-server:3000",
|
||||
"LABEL_ROOM_ID": "Room ID",
|
||||
"LABEL_ROOM_ID_TOOLTIP": "The unique identifier for your sync room",
|
||||
"PLACEHOLDER_ROOM_ID": "Enter Room ID",
|
||||
"PLACEHOLDER_ROOM_ID_TOOLTIP": "The unique ID of the room you want to join",
|
||||
"LABEL_PASSWORD": "Password (Optional)",
|
||||
"LABEL_PASSWORD_TOOLTIP": "Optional password to restrict room access",
|
||||
"PLACEHOLDER_PASSWORD": "Room Password (optional)",
|
||||
"PLACEHOLDER_PASSWORD_TOOLTIP": "Password for the room (leave empty if none)",
|
||||
"BTN_JOIN_ROOM": "Join / Create Room",
|
||||
"BTN_JOIN_ROOM_TOOLTIP": "Connect to the room",
|
||||
"LABEL_PUBLIC_ROOMS": "Public Rooms",
|
||||
"LABEL_PUBLIC_ROOMS_TOOLTIP": "List of publicly available rooms on this server",
|
||||
"BTN_REFRESH": "REFRESH",
|
||||
"BTN_REFRESH_TOOLTIP": "Refresh the list of public rooms",
|
||||
"PUBLIC_ROOMS_REFRESHING": "Refreshing...",
|
||||
"BTN_REFRESH_COOLDOWN": "WAIT {seconds}s",
|
||||
"BTN_REFRESH_COOLDOWN_TOOLTIP": "Room list refresh is cooling down. Try again in {seconds}s.",
|
||||
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Refreshing public rooms. Next refresh available in {seconds}s.",
|
||||
"LABEL_ACTIVE_ROOM": "Active Room",
|
||||
"LABEL_ACTIVE_ROOM_TOOLTIP": "The room you are currently connected to",
|
||||
"ACTIVE_ROOM_NONE": "NONE",
|
||||
"ACTIVE_SERVER_OFFICIAL": "Official Server",
|
||||
"LABEL_INVITE_LINK": "Invite Link",
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Share this link with friends so they can join",
|
||||
"LABEL_PEERS_IN_ROOM": "Peers in Room",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Other users currently connected to this room",
|
||||
"LABEL_HOST_CONTROL": "Host Control",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Restrict who can control playback in this room",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Only I can control playback",
|
||||
"NOTICE_HOST_CONTROLS": "The host controls playback for everyone.",
|
||||
"NOTICE_COHOST_HINT": "Tip: grant control to individual viewers in the participant list below.",
|
||||
"BADGE_HOST": "Host",
|
||||
"BADGE_GUEST": "Guest",
|
||||
"BADGE_CONTROLLER": "Controller",
|
||||
"BTN_GIVE_CONTROL": "Give control",
|
||||
"BTN_REVOKE_CONTROL": "Revoke",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Watching on their own — host's commands are ignored",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · Host controls this room",
|
||||
"HCM_DIALOG_BODY": "Only the host can control playback in this room. Keep watching together, or watch on your own?",
|
||||
"HCM_DIALOG_STAY": "Stay in sync",
|
||||
"HCM_DIALOG_SOLO": "Watch on my own",
|
||||
"HCM_BADGE_SOLO": "Watching on your own",
|
||||
"HCM_BADGE_RESYNC": "Resync",
|
||||
"NO_PEERS_CONNECTED": "No peers connected",
|
||||
"BTN_LEAVE_ROOM": "Leave Room",
|
||||
"LABEL_SELECT_VIDEO": "Select Video",
|
||||
"LABEL_SELECT_VIDEO_TOOLTIP": "Choose the browser tab containing the video to sync",
|
||||
"OPTION_SELECT_TAB": "-- Select a Tab --",
|
||||
"LABEL_REMOTE_CONTROL": "Remote Control",
|
||||
"BTN_COPY_INVITE": "📋 Invite Link",
|
||||
"BTN_COPY_INVITE_TOOLTIP": "Copy Invite Link",
|
||||
"BTN_PLAY": "▶ Play",
|
||||
"BTN_PLAY_TOOLTIP": "Send a Play command to everyone",
|
||||
"BTN_PAUSE": "⏸ Pause",
|
||||
"BTN_PAUSE_TOOLTIP": "Send a Pause command to everyone",
|
||||
"BTN_SYNC": "⚡ SYNC",
|
||||
"BTN_SYNC_TOOLTIP": "Force all users to sync up",
|
||||
"OPTION_JUMP_TO_OTHERS": "Jump to Others",
|
||||
"OPTION_JUMP_TO_ME": "Jump to Me",
|
||||
"OPTION_JUMP_MODE_TOOLTIP": "Choose sync target",
|
||||
"LABEL_LAST_ACTIVITY": "Last Activity Status",
|
||||
"LABEL_LAST_ACTIVITY_TOOLTIP": "Shows the most recent play, pause, or seek command",
|
||||
"NO_RECENT_COMMANDS": "No recent commands",
|
||||
"LOBBY_HEADER": "EPISODE LOBBY",
|
||||
"LOBBY_WAITING_FOR": "🎬 Waiting for: \"{title}\"",
|
||||
"LOBBY_WAITING_PEERS": "Waiting for peers...",
|
||||
"BTN_SKIP_PLAY": "Skip & Play anyway",
|
||||
"BTN_SKIP_PLAY_TOOLTIP": "Cancel lobby and play anyway",
|
||||
"LOBBY_CONNECT_FIRST": "Connect to a room first",
|
||||
"LOBBY_CONNECT_FIRST_DESC": "You need to join a room via an invite link or create a new one to sync videos.",
|
||||
"BTN_CREATE_ROOM_ALT": "Create New Room",
|
||||
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Create a new random room and join it",
|
||||
"LABEL_USERNAME": "Your Username",
|
||||
"LABEL_USERNAME_TOOLTIP": "Username helps others identify you.",
|
||||
"PLACEHOLDER_USERNAME": "Anonymous Koala",
|
||||
"LABEL_HIDE_CLUTTER": "Hide Clutter Tabs",
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filters out non-video tabs and unrelated domains to keep the list clean",
|
||||
"LABEL_AUTO_SYNC_NEXT": "Auto-Sync Next Episode",
|
||||
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pauses automatically and waits for all peers when an episode changes, then sync-starts together.",
|
||||
"LABEL_AUTO_COPY_INVITE": "Auto-Copy Invite Link",
|
||||
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Automatically copies the invite link to your clipboard when creating a new room.",
|
||||
"LABEL_NOTIFICATIONS": "Browser Notifications",
|
||||
"LABEL_NOTIFICATIONS_TOOLTIP": "Shows native system notifications when someone joins/leaves or plays/pauses.",
|
||||
"LABEL_LANGUAGE": "App Language",
|
||||
"LABEL_LANGUAGE_TOOLTIP": "Choose your preferred extension language",
|
||||
"LABEL_TROUBLESHOOTING": "Troubleshooting",
|
||||
"LABEL_TROUBLESHOOTING_TOOLTIP": "Tools for fixing connection issues",
|
||||
"BTN_REGEN_ID": "Regenerate Peer ID",
|
||||
"BTN_REGEN_ID_TOOLTIP": "Regenerate your internal ID and reconnect",
|
||||
"REGEN_ID_DESC": "Use this if you see \"Duplicate Identity\" errors.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Other issue? Open a GitHub Issue",
|
||||
"TOAST_ID_REGENERATED": "Identity regenerated — reconnecting…",
|
||||
"LABEL_CONN_STATUS": "Connection Status",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Current WebSocket connection state",
|
||||
"CONN_STATUS_DISCONNECTED": "Disconnected",
|
||||
"BTN_RETRY": "RETRY",
|
||||
"BTN_RETRY_TOOLTIP": "Attempt to reconnect to the server",
|
||||
"BTN_COPY_LOGS": "Copy Logs",
|
||||
"BTN_COPY_LOGS_TOOLTIP": "Copy logs to clipboard for sharing",
|
||||
"LABEL_VIDEO_DEBUG": "Video Debug Info",
|
||||
"LABEL_VIDEO_DEBUG_TOOLTIP": "Technical details about the currently selected video element",
|
||||
"VIDEO_DEBUG_EMPTY": "No tab selected or video detected.",
|
||||
"LABEL_HISTORY": "Full Action History",
|
||||
"LABEL_HISTORY_TOOLTIP": "Chronological log of all sync commands in the room",
|
||||
"HISTORY_EMPTY": "No activity yet",
|
||||
"LABEL_LOGS": "Logs (Last 50)",
|
||||
"LABEL_LOGS_TOOLTIP": "Technical connection logs for debugging",
|
||||
"BTN_CLEAR": "CLEAR",
|
||||
"BTN_CLEAR_TOOLTIP": "Clear log output",
|
||||
"LABEL_GITHUB": "GitHub Repository",
|
||||
"BTN_ONBOARDING_SKIP": "Skip",
|
||||
"BTN_ONBOARDING_SKIP_TOOLTIP": "Skip the tutorial",
|
||||
"BTN_ONBOARDING_NEXT": "Next",
|
||||
"BTN_ONBOARDING_NEXT_TOOLTIP": "Go to next step",
|
||||
"ONBOARDING_1_TITLE": "Welcome to KoalaSync!",
|
||||
"ONBOARDING_1_TEXT": "Watch videos together in perfect sync — no matter where you are. Let's take a quick tour!",
|
||||
"ONBOARDING_2_TITLE": "1. Create a Room",
|
||||
"ONBOARDING_2_TEXT": "Start here. Create a room and share the invite link with your friends.",
|
||||
"ONBOARDING_3_TITLE": "2. Select Video",
|
||||
"ONBOARDING_3_TEXT": "Navigate here to select the video you want to sync. Play, pause, and seek — everyone stays in sync.",
|
||||
"ONBOARDING_4_TITLE": "3. Personalize",
|
||||
"ONBOARDING_4_TEXT": "Pick a fun username so your friends know who you are.",
|
||||
"ONBOARDING_5_TITLE": "You're all set!",
|
||||
"ONBOARDING_5_TEXT": "Time to grab some popcorn. Enjoy watching together!",
|
||||
"ERR_CONN_TIMEOUT": "Connection timed out. Please try again.",
|
||||
"ERR_INVALID_SERVER_URL": "Invalid Server URL format.",
|
||||
"ERR_IDENTITY_NOT_LOADED": "Identity not yet loaded. Wait a moment and try again.",
|
||||
"ERR_NO_PEERS_TIME": "No other peers with a known time. Switch to 'Jump to Me'.",
|
||||
"ERR_NO_VIDEO_TAB": "Could not connect to video tab.",
|
||||
"ERR_SELECT_VIDEO": "Please select a video first!",
|
||||
"TOAST_INVITE_COPIED": "Invite link copied!",
|
||||
"TOAST_COPY_FAILED": "Failed to copy to clipboard",
|
||||
"TOAST_LOBBY_SKIPPED": "Episode Lobby skipped.",
|
||||
"TOAST_LOBBY_SKIP_FAILED": "Failed to skip lobby.",
|
||||
"TOAST_LOGS_COPIED": "Copied!",
|
||||
"TOAST_PEER_JOINED": "{name} joined the room",
|
||||
"TOAST_PEER_LEFT": "{name} left the room",
|
||||
"TOAST_PEER_ACTION": "{name} {action}",
|
||||
"STATUS_CONNECTED": "Connected",
|
||||
"STATUS_RECONNECTING": "Reconnecting...",
|
||||
"STATUS_CONNECTING": "Connecting...",
|
||||
"STATUS_FAILED": "Failed",
|
||||
"STATUS_DISCONNECTED": "Disconnected",
|
||||
"STATUS_IDLE": "Ready to connect",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync is ready. Join or create a room to connect and start syncing.",
|
||||
"BTN_STATE_JOINING": "🚀 Joining...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Reconnecting...",
|
||||
"BTN_STATE_PLAYING": "▶ Playing...",
|
||||
"BTN_STATE_PAUSING": "⏸ Pausing...",
|
||||
"BTN_STATE_SYNCING_GROUP": "Syncing to group ({time})...",
|
||||
"BTN_STATE_SYNCING": "Syncing...",
|
||||
"BTN_STATE_SYNCED": "✅ Synced!",
|
||||
"NOTIF_PLAY": "started playback",
|
||||
"NOTIF_PAUSE": "paused playback",
|
||||
"NOTIF_SEEK": "seeked the video",
|
||||
"NOTIF_FORCE_PREPARE": "started force sync",
|
||||
"NOTIF_FORCE_EXECUTE": "synchronized everyone",
|
||||
"DEBUG_NO_TAB": "No target tab selected.",
|
||||
"DEBUG_COMM_FAIL": "Could not communicate with tab video.",
|
||||
"EMPTY_PEERS_TITLE": "No peers yet",
|
||||
"EMPTY_PEERS_HINT": "Share your invite link to get started",
|
||||
"EMPTY_HISTORY_TITLE": "No activity yet",
|
||||
"EMPTY_HISTORY_HINT": "Play, pause, or seek to see history",
|
||||
"EMPTY_LOGS_TITLE": "No logs",
|
||||
"EMPTY_LOGS_HINT": "Connection events will appear here",
|
||||
"EMPTY_ROOMS_TITLE": "No active rooms",
|
||||
"EMPTY_ROOMS_HINT": "Create a room or refresh to find public ones",
|
||||
"LABEL_YOU": "You",
|
||||
"ONBOARDING_DONE": "Done!",
|
||||
"LABEL_LOBBY_PEER_READY": "Ready",
|
||||
"LABEL_LOBBY_PEER_LOADING": "Loading...",
|
||||
"LABEL_PASSWORD_PROTECTED": "Password Protected",
|
||||
"LABEL_PEERS_COUNT": "{count} peers",
|
||||
"LABEL_CUSTOM_SERVER": "Custom Server",
|
||||
"BTN_STATE_CREATING": "🚀 Creating Room...",
|
||||
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Episode Sync Failed",
|
||||
"NOTIF_LOBBY_CANCEL_MSG": "Auto-sync cancelled: {reason}. You may need to manually sync.",
|
||||
"LOBBY_CANCEL_TIMEOUT": "Timeout",
|
||||
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Timeout (recovered)",
|
||||
"LOBBY_CANCEL_PEERS_LEFT": "All other peers left",
|
||||
"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": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Rate us",
|
||||
"FOOTER_SUPPORT_PROMPT": "Enjoying KoalaSync? Leave a review!",
|
||||
"LABEL_AUDIO_PROCESSING": "Audio Processing",
|
||||
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Apply audio effects like compression to video playback",
|
||||
"AUDIO_OPEN_SETTINGS": "Open",
|
||||
"NEW_FEATURE_AUDIO": "New: Audio Processing — try the compressor!",
|
||||
"AUDIO_BACK": "← Back",
|
||||
"AUDIO_PAGE_TITLE": "Audio Settings",
|
||||
"AUDIO_MASTER_TOGGLE": "Audio Processing",
|
||||
"AUDIO_COMPRESSOR": "Compressor",
|
||||
"AUDIO_COMPRESSOR_ENABLE": "Enabled",
|
||||
"AUDIO_PRESET": "Preset",
|
||||
"AUDIO_PRESET_RECOMMENDED": "Recommended",
|
||||
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamic Range",
|
||||
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Vocal Enhancement",
|
||||
"AUDIO_PRESET_SMOOTH": "Smooth",
|
||||
"AUDIO_PRESET_CUSTOM": "Custom",
|
||||
"AUDIO_PARAM_THRESHOLD": "Threshold",
|
||||
"AUDIO_PARAM_KNEE": "Knee",
|
||||
"AUDIO_PARAM_RATIO": "Ratio",
|
||||
"AUDIO_PARAM_ATTACK": "Attack",
|
||||
"AUDIO_PARAM_RELEASE": "Release",
|
||||
"AUDIO_EQUALIZER": "Equalizer",
|
||||
"AUDIO_COMING_SOON": "Coming soon",
|
||||
"BTN_RESTART_TOUR": "Restart Tutorial",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Restart the onboarding tutorial",
|
||||
"HINT_SELECT_VIDEO": "Select your video here!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Send full title",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Send episode ID",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Do not send",
|
||||
"LABEL_PRIVACY_SETTINGS": "Privacy Settings",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Choose which tab and media titles are sent to the room.",
|
||||
"LABEL_SEND_TAB_TITLE": "Send tab title",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Send the selected browser tab title to the room.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Media title",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Choose how media titles are sent to the room."
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"LANG_CODE": "es",
|
||||
"HTML_CLASS": "lang-es",
|
||||
"APP_TITLE": "KoalaSync",
|
||||
"TAB_ROOM": "Sala",
|
||||
"TAB_ROOM_TOOLTIP": "Configuración de sala y conexión",
|
||||
"TAB_SYNC": "Sincro",
|
||||
"TAB_SYNC_TOOLTIP": "Controles de sincronización de video y acciones",
|
||||
"TAB_SETTINGS": "Configuración",
|
||||
"TAB_SETTINGS_TOOLTIP": "Preferencias de la extensión",
|
||||
"TAB_STATUS": "Estado",
|
||||
"TAB_STATUS_TOOLTIP": "Diagnósticos avanzados y registros",
|
||||
"BTN_CREATE_ROOM": "+ Crear nueva sala",
|
||||
"MANUAL_CONNECT_HEADER": "Conexión manual / Avanzado",
|
||||
"LABEL_SERVER": "Servidor",
|
||||
"BTN_SERVER_OFFICIAL": "Oficial",
|
||||
"BTN_SERVER_OFFICIAL_TOOLTIP": "Usar el servidor oficial confiable",
|
||||
"BTN_SERVER_CUSTOM": "Personalizado",
|
||||
"BTN_SERVER_CUSTOM_TOOLTIP": "Conectarse a su propio servidor auto-alojado",
|
||||
"PLACEHOLDER_SERVER_URL": "wss://tu-servidor:3000",
|
||||
"LABEL_ROOM_ID": "ID de sala",
|
||||
"LABEL_ROOM_ID_TOOLTIP": "El identificador único para tu sala de sincronización",
|
||||
"PLACEHOLDER_ROOM_ID": "Ingresa el ID de la sala",
|
||||
"PLACEHOLDER_ROOM_ID_TOOLTIP": "El ID único de la sala a la que deseas unirte",
|
||||
"LABEL_PASSWORD": "Contraseña (Opcional)",
|
||||
"LABEL_PASSWORD_TOOLTIP": "Contraseña opcional para restringir el acceso a la sala",
|
||||
"PLACEHOLDER_PASSWORD": "Contraseña de la sala (opcional)",
|
||||
"PLACEHOLDER_PASSWORD_TOOLTIP": "Contraseña para la sala (dejar vacío si no hay)",
|
||||
"BTN_JOIN_ROOM": "Unirse / Crear sala",
|
||||
"BTN_JOIN_ROOM_TOOLTIP": "Conectarse a la sala",
|
||||
"LABEL_PUBLIC_ROOMS": "Salas públicas",
|
||||
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Lista de salas públicas disponibles en este servidor",
|
||||
"BTN_REFRESH": "ACTUALIZAR",
|
||||
"BTN_REFRESH_TOOLTIP": "Actualizar la lista de salas públicas",
|
||||
"PUBLIC_ROOMS_REFRESHING": "Actualizando...",
|
||||
"BTN_REFRESH_COOLDOWN": "ESPERA {seconds}s",
|
||||
"BTN_REFRESH_COOLDOWN_TOOLTIP": "La lista de salas está en espera. Inténtalo de nuevo en {seconds}s.",
|
||||
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Actualizando salas públicas. Próxima actualización en {seconds}s.",
|
||||
"LABEL_ACTIVE_ROOM": "Sala activa",
|
||||
"LABEL_ACTIVE_ROOM_TOOLTIP": "La sala a la que estás conectado actualmente",
|
||||
"ACTIVE_ROOM_NONE": "NINGUNA",
|
||||
"ACTIVE_SERVER_OFFICIAL": "Servidor oficial",
|
||||
"LABEL_INVITE_LINK": "Enlace de invitación",
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Comparte este enlace con amigos para que se unan",
|
||||
"LABEL_PEERS_IN_ROOM": "Participantes en la sala",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Otros usuarios conectados actualmente a esta sala",
|
||||
"LABEL_HOST_CONTROL": "Control del anfitrión",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Restringir quién puede controlar la reproducción en esta sala",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Solo yo puedo controlar la reproducción",
|
||||
"NOTICE_HOST_CONTROLS": "El anfitrión controla la reproducción para todos.",
|
||||
"NOTICE_COHOST_HINT": "Consejo: concede el control a usuarios concretos en la lista de participantes de abajo.",
|
||||
"BADGE_HOST": "Anfitrión",
|
||||
"BADGE_GUEST": "Invitado",
|
||||
"BADGE_CONTROLLER": "Controlador",
|
||||
"BTN_GIVE_CONTROL": "Dar control",
|
||||
"BTN_REVOKE_CONTROL": "Quitar",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Viendo por su cuenta — ignora los comandos del anfitrión",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · El anfitrión controla esta sala",
|
||||
"HCM_DIALOG_BODY": "Solo el anfitrión puede controlar la reproducción en esta sala. ¿Seguir viendo juntos o ver por tu cuenta?",
|
||||
"HCM_DIALOG_STAY": "Seguir sincronizado",
|
||||
"HCM_DIALOG_SOLO": "Ver por mi cuenta",
|
||||
"HCM_BADGE_SOLO": "Viendo por tu cuenta",
|
||||
"HCM_BADGE_RESYNC": "Resincronizar",
|
||||
"NO_PEERS_CONNECTED": "Sin participantes conectados",
|
||||
"BTN_LEAVE_ROOM": "Salir de la sala",
|
||||
"LABEL_SELECT_VIDEO": "Seleccionar video",
|
||||
"LABEL_SELECT_VIDEO_TOOLTIP": "Elige la pestaña del navegador que contiene el video a sincronizar",
|
||||
"OPTION_SELECT_TAB": "-- Selecciona una pestaña --",
|
||||
"LABEL_REMOTE_CONTROL": "Control remoto",
|
||||
"BTN_COPY_INVITE": "📋 Enlace de invitación",
|
||||
"BTN_COPY_INVITE_TOOLTIP": "Copiar enlace de invitación",
|
||||
"BTN_PLAY": "▶ Reproducir",
|
||||
"BTN_PLAY_TOOLTIP": "Enviar un comando de reproducción a todos",
|
||||
"BTN_PAUSE": "⏸ Pausar",
|
||||
"BTN_PAUSE_TOOLTIP": "Enviar un comando de pausa a todos",
|
||||
"BTN_SYNC": "⚡ SYNC",
|
||||
"BTN_SYNC_TOOLTIP": "Forzar la sincronización de todos los usuarios",
|
||||
"OPTION_JUMP_TO_OTHERS": "Ir a la posición de otros",
|
||||
"OPTION_JUMP_TO_ME": "Traerlos a mi posición",
|
||||
"OPTION_JUMP_MODE_TOOLTIP": "Elegir el objetivo de sincronización",
|
||||
"LABEL_LAST_ACTIVITY": "Última actividad",
|
||||
"LABEL_LAST_ACTIVITY_TOOLTIP": "Muestra el comando más reciente de reproducción, pausa o salto en el tiempo",
|
||||
"NO_RECENT_COMMANDS": "Sin comandos recientes",
|
||||
"LOBBY_HEADER": "SALA DE ESPERA DE EPISODIO",
|
||||
"LOBBY_WAITING_FOR": "🎬 Esperando a: \"{title}\"",
|
||||
"LOBBY_WAITING_PEERS": "Esperando participantes...",
|
||||
"BTN_SKIP_PLAY": "Omitir y reproducir ya",
|
||||
"BTN_SKIP_PLAY_TOOLTIP": "Cancelar la sala de espera y reproducir de todos modos",
|
||||
"LOBBY_CONNECT_FIRST": "Únete a una sala primero",
|
||||
"LOBBY_CONNECT_FIRST_DESC": "Necesitas unirte a una sala a través de un enlace de invitación o crear una nueva para sincronizar videos.",
|
||||
"BTN_CREATE_ROOM_ALT": "Crear nueva sala",
|
||||
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Crear una nueva sala aleatoria y unirse",
|
||||
"LABEL_USERNAME": "Tu nombre de usuario",
|
||||
"LABEL_USERNAME_TOOLTIP": "El nombre de usuario ayuda a otros a identificarte.",
|
||||
"PLACEHOLDER_USERNAME": "Koala anónimo",
|
||||
"LABEL_HIDE_CLUTTER": "Ocultar pestañas sin video",
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtra pestañas que no son de video y dominios no relacionados para mantener limpia la lista",
|
||||
"LABEL_AUTO_SYNC_NEXT": "Sincro auto del siguiente episodio",
|
||||
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pausa automáticamente y espera a todos al cambiar de episodio, luego inicia de forma sincrónica.",
|
||||
"LABEL_AUTO_COPY_INVITE": "Auto-copiar enlace",
|
||||
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Copia automáticamente el enlace de invitación a tu portapapeles al crear una nueva sala.",
|
||||
"LABEL_NOTIFICATIONS": "Notificaciones del navegador",
|
||||
"LABEL_NOTIFICATIONS_TOOLTIP": "Muestra notificaciones del sistema cuando alguien se une/sale o reproduce/pausa.",
|
||||
"LABEL_LANGUAGE": "Idioma de la aplicación",
|
||||
"LABEL_LANGUAGE_TOOLTIP": "Elige tu idioma preferido para la extensión",
|
||||
"LABEL_TROUBLESHOOTING": "Resolución de problemas",
|
||||
"LABEL_TROUBLESHOOTING_TOOLTIP": "Herramientas para solucionar problemas de conexión",
|
||||
"BTN_REGEN_ID": "Regenerar ID de usuario",
|
||||
"BTN_REGEN_ID_TOOLTIP": "Regenerar tu ID interno y volver a conectarte",
|
||||
"REGEN_ID_DESC": "Usa esto si ves errores de 'Identidad duplicada'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "¿Tienes otro problema? Abre un reporte en GitHub",
|
||||
"TOAST_ID_REGENERATED": "Identidad regenerada — reconectando…",
|
||||
"LABEL_CONN_STATUS": "Estado de la conexión",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Estado actual de la conexión WebSocket",
|
||||
"CONN_STATUS_DISCONNECTED": "Desconectado",
|
||||
"BTN_RETRY": "REINTENTAR",
|
||||
"BTN_RETRY_TOOLTIP": "Intentar volver a conectarse al servidor",
|
||||
"BTN_COPY_LOGS": "Copiar registros",
|
||||
"BTN_COPY_LOGS_TOOLTIP": "Copiar registros al portapapeles para compartir",
|
||||
"LABEL_VIDEO_DEBUG": "Información de depuración de video",
|
||||
"LABEL_VIDEO_DEBUG_TOOLTIP": "Detalles técnicos sobre el elemento de video seleccionado actualmente",
|
||||
"VIDEO_DEBUG_EMPTY": "No hay pestaña seleccionada o no se detectó ningún video.",
|
||||
"LABEL_HISTORY": "Historial completo",
|
||||
"LABEL_HISTORY_TOOLTIP": "Registro cronológico de todos los comandos de sincronización en la sala",
|
||||
"HISTORY_EMPTY": "Sin actividad aún",
|
||||
"LABEL_LOGS": "Logs (Últimos 50)",
|
||||
"LABEL_LOGS_TOOLTIP": "Registros técnicos de conexión para depuración",
|
||||
"BTN_CLEAR": "LIMPIAR",
|
||||
"BTN_CLEAR_TOOLTIP": "Limpiar salida de registros",
|
||||
"LABEL_GITHUB": "Repositorio de GitHub",
|
||||
"BTN_ONBOARDING_SKIP": "Omitir",
|
||||
"BTN_ONBOARDING_SKIP_TOOLTIP": "Omitir el tutorial",
|
||||
"BTN_ONBOARDING_NEXT": "Siguiente",
|
||||
"BTN_ONBOARDING_NEXT_TOOLTIP": "Ir al siguiente paso",
|
||||
"ONBOARDING_1_TITLE": "¡Bienvenido a KoalaSync!",
|
||||
"ONBOARDING_1_TEXT": "Mira videos junto a tus amigos en perfecta sincronización, sin importar dónde estén. ¡Hagamos un recorrido rápido!",
|
||||
"ONBOARDING_2_TITLE": "1. Crear una sala",
|
||||
"ONBOARDING_2_TEXT": "Comienza aquí. Crea una sala y comparte el enlace de invitación con tus amigos.",
|
||||
"ONBOARDING_3_TITLE": "2. Seleccionar video",
|
||||
"ONBOARDING_3_TEXT": "Navega aquí para seleccionar el video que deseas sincronizar. Reproduce, pausa y busca: todos permanecen sincronizados.",
|
||||
"ONBOARDING_4_TITLE": "3. Personalizar",
|
||||
"ONBOARDING_4_TEXT": "Elige un nombre de usuario divertido para que tus amigos sepan quién eres.",
|
||||
"ONBOARDING_5_TITLE": "¡Todo listo!",
|
||||
"ONBOARDING_5_TEXT": "Hora de preparar las palomitas. ¡Disfruta viendo con tus amigos!",
|
||||
"ERR_CONN_TIMEOUT": "Tiempo de conexión agotado. Inténtalo de nuevo.",
|
||||
"ERR_INVALID_SERVER_URL": "Formato de URL de servidor no válido.",
|
||||
"ERR_IDENTITY_NOT_LOADED": "Identidad no cargada aún. Espera un momento e inténtalo de nuevo.",
|
||||
"ERR_NO_PEERS_TIME": "No hay otros participantes con posición conocida. Cambia a 'Traerlos a mi posición'.",
|
||||
"ERR_NO_VIDEO_TAB": "No se pudo conectar a la pestaña de video.",
|
||||
"ERR_SELECT_VIDEO": "¡Selecciona un video primero!",
|
||||
"TOAST_INVITE_COPIED": "¡Enlace de invitación copiado!",
|
||||
"TOAST_COPY_FAILED": "Error al copiar al portapapeles",
|
||||
"TOAST_LOBBY_SKIPPED": "Sala de espera de episodio omitida.",
|
||||
"TOAST_LOBBY_SKIP_FAILED": "Error al omitir la sala de espera.",
|
||||
"TOAST_LOGS_COPIED": "¡Copiado!",
|
||||
"TOAST_PEER_JOINED": "{name} se ha unido a la sala",
|
||||
"TOAST_PEER_LEFT": "{name} ha salido de la sala",
|
||||
"TOAST_PEER_ACTION": "{name} ha {action}",
|
||||
"STATUS_CONNECTED": "Conectado",
|
||||
"STATUS_RECONNECTING": "Reconectando...",
|
||||
"STATUS_CONNECTING": "Conectando...",
|
||||
"STATUS_FAILED": "Error",
|
||||
"STATUS_DISCONNECTED": "Desconectado",
|
||||
"STATUS_IDLE": "Listo para conectar",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync está listo. Únete o crea una sala para conectarte y empezar a sincronizar.",
|
||||
"BTN_STATE_JOINING": "🚀 Uniéndose...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Reconectando...",
|
||||
"BTN_STATE_PLAYING": "▶ Reproduciendo...",
|
||||
"BTN_STATE_PAUSING": "⏸ Pausando...",
|
||||
"BTN_STATE_SYNCING_GROUP": "Sincronizando al grupo ({time})...",
|
||||
"BTN_STATE_SYNCING": "Sincronizando...",
|
||||
"BTN_STATE_SYNCED": "✅ ¡Sincronizado!",
|
||||
"NOTIF_PLAY": "ha iniciado la reproducción",
|
||||
"NOTIF_PAUSE": "ha pausado la reproducción",
|
||||
"NOTIF_SEEK": "ha cambiado la posición del video",
|
||||
"NOTIF_FORCE_PREPARE": "ha iniciado una sincronización forzada",
|
||||
"NOTIF_FORCE_EXECUTE": "ha sincronizado a todos",
|
||||
"DEBUG_NO_TAB": "No hay pestaña objetivo seleccionada.",
|
||||
"DEBUG_COMM_FAIL": "No se pudo comunicar con el video de la pestaña.",
|
||||
"EMPTY_PEERS_TITLE": "Sin participantes aún",
|
||||
"EMPTY_PEERS_HINT": "Comparte tu enlace de invitación para comenzar",
|
||||
"EMPTY_HISTORY_TITLE": "Sin actividad aún",
|
||||
"EMPTY_HISTORY_HINT": "Reproduce, pausa o salta en el tiempo para ver el historial",
|
||||
"EMPTY_LOGS_TITLE": "Sin registros",
|
||||
"EMPTY_LOGS_HINT": "Los eventos de conexión aparecerán aquí",
|
||||
"EMPTY_ROOMS_TITLE": "Sin salas activas",
|
||||
"EMPTY_ROOMS_HINT": "Crea una sala o actualiza para buscar salas públicas",
|
||||
"LABEL_YOU": "Tú",
|
||||
"ONBOARDING_DONE": "¡Hecho!",
|
||||
"LABEL_LOBBY_PEER_READY": "Listo",
|
||||
"LABEL_LOBBY_PEER_LOADING": "Cargando...",
|
||||
"LABEL_PASSWORD_PROTECTED": "Protegido con contraseña",
|
||||
"LABEL_PEERS_COUNT": "{count} participantes",
|
||||
"LABEL_CUSTOM_SERVER": "Servidor personalizado",
|
||||
"BTN_STATE_CREATING": "🚀 Creando sala...",
|
||||
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Error al sincronizar episodio",
|
||||
"NOTIF_LOBBY_CANCEL_MSG": "Sincronización automática cancelada: {reason}. Es posible que debas sincronizar manualmente.",
|
||||
"LOBBY_CANCEL_TIMEOUT": "Tiempo de espera agotado",
|
||||
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Tiempo de espera agotado (recuperado)",
|
||||
"LOBBY_CANCEL_PEERS_LEFT": "Todos los demás participantes se han ido",
|
||||
"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": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Valorar",
|
||||
"FOOTER_SUPPORT_PROMPT": "¿Te gusta KoalaSync? ¡Deja una reseña!",
|
||||
"LABEL_AUDIO_PROCESSING": "Procesamiento de audio",
|
||||
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Aplica efectos de audio como compresión a la reproducción de video",
|
||||
"AUDIO_OPEN_SETTINGS": "Abrir",
|
||||
"NEW_FEATURE_AUDIO": "Nuevo: Procesamiento de audio — ¡prueba el compresor!",
|
||||
"AUDIO_BACK": "← Volver",
|
||||
"AUDIO_PAGE_TITLE": "Configuración de audio",
|
||||
"AUDIO_MASTER_TOGGLE": "Procesamiento de audio",
|
||||
"AUDIO_COMPRESSOR": "Compresor",
|
||||
"AUDIO_COMPRESSOR_ENABLE": "Activado",
|
||||
"AUDIO_PRESET": "Preajuste",
|
||||
"AUDIO_PRESET_RECOMMENDED": "Recomendado",
|
||||
"AUDIO_PRESET_DYNAMIC_RANGE": "Rango dinámico",
|
||||
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Mejora de voz",
|
||||
"AUDIO_PRESET_SMOOTH": "Suave",
|
||||
"AUDIO_PRESET_CUSTOM": "Personalizado",
|
||||
"AUDIO_PARAM_THRESHOLD": "Umbral (Threshold)",
|
||||
"AUDIO_PARAM_KNEE": "Codo (Knee)",
|
||||
"AUDIO_PARAM_RATIO": "Relación (Ratio)",
|
||||
"AUDIO_PARAM_ATTACK": "Ataque (Attack)",
|
||||
"AUDIO_PARAM_RELEASE": "Liberación (Release)",
|
||||
"AUDIO_EQUALIZER": "Ecualizador",
|
||||
"AUDIO_COMING_SOON": "Próximamente",
|
||||
"BTN_RESTART_TOUR": "Reiniciar tutorial",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Reiniciar el tutorial de inicio",
|
||||
"HINT_SELECT_VIDEO": "¡Selecciona tu video aquí!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Enviar título completo",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Enviar ID de episodio",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "No enviar",
|
||||
"LABEL_PRIVACY_SETTINGS": "Ajustes de privacidad",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Elige qué títulos de pestaña y multimedia se envían a la sala.",
|
||||
"LABEL_SEND_TAB_TITLE": "Enviar título de pestaña",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Envía a la sala el título de la pestaña del navegador seleccionada.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Título multimedia",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Elige cómo se envían los títulos multimedia a la sala."
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"LANG_CODE": "fr",
|
||||
"HTML_CLASS": "lang-fr",
|
||||
"APP_TITLE": "KoalaSync",
|
||||
"TAB_ROOM": "Salon",
|
||||
"TAB_ROOM_TOOLTIP": "Paramètres de salon et connexion",
|
||||
"TAB_SYNC": "Synchro",
|
||||
"TAB_SYNC_TOOLTIP": "Contrôles de synchronisation vidéo et actions",
|
||||
"TAB_SETTINGS": "Options",
|
||||
"TAB_SETTINGS_TOOLTIP": "Préférences de l'extension",
|
||||
"TAB_STATUS": "Statut",
|
||||
"TAB_STATUS_TOOLTIP": "Diagnostics avancés & Journaux",
|
||||
"BTN_CREATE_ROOM": "+ Créer un nouveau salon",
|
||||
"MANUAL_CONNECT_HEADER": "Connexion manuelle / Avancé",
|
||||
"LABEL_SERVER": "Serveur",
|
||||
"BTN_SERVER_OFFICIAL": "Officiel",
|
||||
"BTN_SERVER_OFFICIAL_TOOLTIP": "Utiliser le serveur officiel fiable",
|
||||
"BTN_SERVER_CUSTOM": "Perso",
|
||||
"BTN_SERVER_CUSTOM_TOOLTIP": "Se connecter à votre propre serveur auto-hébergé",
|
||||
"PLACEHOLDER_SERVER_URL": "wss://votre-serveur:3000",
|
||||
"LABEL_ROOM_ID": "ID du salon",
|
||||
"LABEL_ROOM_ID_TOOLTIP": "L'identifiant unique de votre salon de synchronisation",
|
||||
"PLACEHOLDER_ROOM_ID": "Entrer l'ID du salon",
|
||||
"PLACEHOLDER_ROOM_ID_TOOLTIP": "L'ID unique du salon que vous souhaitez rejoindre",
|
||||
"LABEL_PASSWORD": "Mot de passe (Optionnel)",
|
||||
"LABEL_PASSWORD_TOOLTIP": "Mot de passe optionnel pour restreindre l'accès au salon",
|
||||
"PLACEHOLDER_PASSWORD": "Mot de passe du salon (optionnel)",
|
||||
"PLACEHOLDER_PASSWORD_TOOLTIP": "Mot de passe du salon (laisser vide si aucun)",
|
||||
"BTN_JOIN_ROOM": "Rejoindre / Créer le salon",
|
||||
"BTN_JOIN_ROOM_TOOLTIP": "Se connecter au salon",
|
||||
"LABEL_PUBLIC_ROOMS": "Salons publics",
|
||||
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Liste des salons publics disponibles sur ce serveur",
|
||||
"BTN_REFRESH": "ACTUALISER",
|
||||
"BTN_REFRESH_TOOLTIP": "Actualiser la liste des salons publics",
|
||||
"PUBLIC_ROOMS_REFRESHING": "Actualisation...",
|
||||
"BTN_REFRESH_COOLDOWN": "ATTENDRE {seconds}s",
|
||||
"BTN_REFRESH_COOLDOWN_TOOLTIP": "La liste des salons est en pause. Réessayez dans {seconds}s.",
|
||||
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Actualisation des salons publics. Prochaine actualisation dans {seconds}s.",
|
||||
"LABEL_ACTIVE_ROOM": "Salon actif",
|
||||
"LABEL_ACTIVE_ROOM_TOOLTIP": "Le salon auquel vous êtes actuellement connecté",
|
||||
"ACTIVE_ROOM_NONE": "AUCUN",
|
||||
"ACTIVE_SERVER_OFFICIAL": "Serveur officiel",
|
||||
"LABEL_INVITE_LINK": "Lien d'invitation",
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Partagez ce lien avec vos amis pour qu'ils vous rejoignent",
|
||||
"LABEL_PEERS_IN_ROOM": "Membres dans le salon",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Autres utilisateurs connectés à ce salon",
|
||||
"LABEL_HOST_CONTROL": "Contrôle de l'hôte",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Définir qui peut contrôler la lecture dans ce salon",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Moi seul peux contrôler la lecture",
|
||||
"NOTICE_HOST_CONTROLS": "L'hôte contrôle la lecture pour tout le monde.",
|
||||
"NOTICE_COHOST_HINT": "Astuce : donnez le contrôle à des participants précis dans la liste ci-dessous.",
|
||||
"BADGE_HOST": "Hôte",
|
||||
"BADGE_GUEST": "Invité",
|
||||
"BADGE_CONTROLLER": "Contrôleur",
|
||||
"BTN_GIVE_CONTROL": "Donner le contrôle",
|
||||
"BTN_REVOKE_CONTROL": "Retirer",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Regarde de son côté — ignore les commandes de l'hôte",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · L'hôte contrôle ce salon",
|
||||
"HCM_DIALOG_BODY": "Seul l'hôte peut contrôler la lecture dans ce salon. Continuer à regarder ensemble, ou regarder de votre côté ?",
|
||||
"HCM_DIALOG_STAY": "Rester synchronisé",
|
||||
"HCM_DIALOG_SOLO": "Regarder de mon côté",
|
||||
"HCM_BADGE_SOLO": "Vous regardez seul",
|
||||
"HCM_BADGE_RESYNC": "Resynchroniser",
|
||||
"NO_PEERS_CONNECTED": "Aucun membre connecté",
|
||||
"BTN_LEAVE_ROOM": "Quitter le salon",
|
||||
"LABEL_SELECT_VIDEO": "Choisir une vidéo",
|
||||
"LABEL_SELECT_VIDEO_TOOLTIP": "Choisissez l'onglet du navigateur contenant la vidéo à synchroniser",
|
||||
"OPTION_SELECT_TAB": "-- Choisir un onglet --",
|
||||
"LABEL_REMOTE_CONTROL": "Contrôle à distance",
|
||||
"BTN_COPY_INVITE": "📋 Lien d'invitation",
|
||||
"BTN_COPY_INVITE_TOOLTIP": "Copier le lien d'invitation",
|
||||
"BTN_PLAY": "▶ Lecture",
|
||||
"BTN_PLAY_TOOLTIP": "Envoyer une commande Lecture à tout le monde",
|
||||
"BTN_PAUSE": "⏸ Pause",
|
||||
"BTN_PAUSE_TOOLTIP": "Envoyer une commande Pause à tout le monde",
|
||||
"BTN_SYNC": "⚡ SYNC",
|
||||
"BTN_SYNC_TOOLTIP": "Forcer tous les utilisateurs à se synchroniser",
|
||||
"OPTION_JUMP_TO_OTHERS": "Rejoindre les autres",
|
||||
"OPTION_JUMP_TO_ME": "Les amener à moi",
|
||||
"OPTION_JUMP_MODE_TOOLTIP": "Choisir la cible de synchronisation",
|
||||
"LABEL_LAST_ACTIVITY": "Dernière activité",
|
||||
"LABEL_LAST_ACTIVITY_TOOLTIP": "Affiche la dernière commande de lecture, pause ou recherche",
|
||||
"NO_RECENT_COMMANDS": "Aucune commande récente",
|
||||
"LOBBY_HEADER": "LOBBY D'ÉPISODE",
|
||||
"LOBBY_WAITING_FOR": "🎬 En attente de : \"{title}\"",
|
||||
"LOBBY_WAITING_PEERS": "En attente des membres...",
|
||||
"BTN_SKIP_PLAY": "Ignorer & Lancer",
|
||||
"BTN_SKIP_PLAY_TOOLTIP": "Annuler le lobby et lancer la lecture quand même",
|
||||
"LOBBY_CONNECT_FIRST": "Rejoignez d'abord un salon",
|
||||
"LOBBY_CONNECT_FIRST_DESC": "Vous devez rejoindre un salon via un lien d'invitation ou en créer un nouveau pour synchroniser des vidéos.",
|
||||
"BTN_CREATE_ROOM_ALT": "Créer un nouveau salon",
|
||||
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Créer un salon aléatoire et le rejoindre",
|
||||
"LABEL_USERNAME": "Votre pseudo",
|
||||
"LABEL_USERNAME_TOOLTIP": "Votre pseudo permet aux autres de vous identifier.",
|
||||
"PLACEHOLDER_USERNAME": "Koala anonyme",
|
||||
"LABEL_HIDE_CLUTTER": "Liste d'onglets épurée",
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtre les onglets non-vidéo et les domaines non pertinents pour garder la liste propre",
|
||||
"LABEL_AUTO_SYNC_NEXT": "Synchro auto l'épisode suivant",
|
||||
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Met en pause et attend tous les membres lors d'un changement d'épisode, puis démarre de manière synchrone.",
|
||||
"LABEL_AUTO_COPY_INVITE": "Copie auto du lien",
|
||||
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Copie automatiquement le lien d'invitation dans votre presse-papiers lors de la création d'un salon.",
|
||||
"LABEL_NOTIFICATIONS": "Notifications de navigateur",
|
||||
"LABEL_NOTIFICATIONS_TOOLTIP": "Affiche des notifications système lorsqu'un membre arrive/part ou lance/met en pause la lecture.",
|
||||
"LABEL_LANGUAGE": "Langue de l'application",
|
||||
"LABEL_LANGUAGE_TOOLTIP": "Choisissez la langue de l'application",
|
||||
"LABEL_TROUBLESHOOTING": "Dépannage",
|
||||
"LABEL_TROUBLESHOOTING_TOOLTIP": "Outils pour résoudre les problèmes de connexion",
|
||||
"BTN_REGEN_ID": "Régénérer l'identifiant",
|
||||
"BTN_REGEN_ID_TOOLTIP": "Régénérer votre identifiant interne et vous reconnecter",
|
||||
"REGEN_ID_DESC": "Utilisez cette option si vous rencontrez des erreurs de 'Double identité'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Autre problème? Ouvrez un Issue GitHub",
|
||||
"TOAST_ID_REGENERATED": "Identité régénérée — reconnexion…",
|
||||
"LABEL_CONN_STATUS": "Statut de la connexion",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Statut actuel de la connexion WebSocket",
|
||||
"CONN_STATUS_DISCONNECTED": "Déconnecté",
|
||||
"BTN_RETRY": "RÉESSAYER",
|
||||
"BTN_RETRY_TOOLTIP": "Tenter de se reconnecter au serveur",
|
||||
"BTN_COPY_LOGS": "Copier les journaux",
|
||||
"BTN_COPY_LOGS_TOOLTIP": "Copier les journaux dans le presse-papiers",
|
||||
"LABEL_VIDEO_DEBUG": "Infos de débogage vidéo",
|
||||
"LABEL_VIDEO_DEBUG_TOOLTIP": "Détails techniques de l'élément vidéo actuellement sélectionné",
|
||||
"VIDEO_DEBUG_EMPTY": "Aucun onglet sélectionné ou aucune vidéo détectée.",
|
||||
"LABEL_HISTORY": "Historique complet",
|
||||
"LABEL_HISTORY_TOOLTIP": "Historique chronologique de toutes les commandes de synchronisation dans le salon",
|
||||
"HISTORY_EMPTY": "Aucune activité pour le moment",
|
||||
"LABEL_LOGS": "Journaux (50 derniers)",
|
||||
"LABEL_LOGS_TOOLTIP": "Journaux techniques pour le débogage de la connexion",
|
||||
"BTN_CLEAR": "EFFACER",
|
||||
"BTN_CLEAR_TOOLTIP": "Effacer la sortie des journaux",
|
||||
"LABEL_GITHUB": "Dépôt GitHub",
|
||||
"BTN_ONBOARDING_SKIP": "Passer",
|
||||
"BTN_ONBOARDING_SKIP_TOOLTIP": "Passer le didacticiel",
|
||||
"BTN_ONBOARDING_NEXT": "Suivant",
|
||||
"BTN_ONBOARDING_NEXT_TOOLTIP": "Étape suivante",
|
||||
"ONBOARDING_1_TITLE": "Bienvenue sur KoalaSync !",
|
||||
"ONBOARDING_1_TEXT": "Regardez des vidéos en parfaite synchronisation — où que vous soyez. Faisons un petit tour !",
|
||||
"ONBOARDING_2_TITLE": "1. Créer un salon",
|
||||
"ONBOARDING_2_TEXT": "Commencez ici. Créez un salon et partagez le lien d'invitation avec vos amis.",
|
||||
"ONBOARDING_3_TITLE": "2. Choisir une vidéo",
|
||||
"ONBOARDING_3_TEXT": "Naviguez ici pour sélectionner la vidéo à synchroniser. Lecture, pause, recherche — tout le monde reste synchrone.",
|
||||
"ONBOARDING_4_TITLE": "3. Personnaliser",
|
||||
"ONBOARDING_4_TEXT": "Choisissez un pseudo sympa pour que vos amis sachent qui vous êtes.",
|
||||
"ONBOARDING_5_TITLE": "Vous êtes prêt !",
|
||||
"ONBOARDING_5_TEXT": "Il est temps de préparer le pop-corn. Bon visionnage !",
|
||||
"ERR_CONN_TIMEOUT": "Délai de connexion dépassé. Veuillez réessayer.",
|
||||
"ERR_INVALID_SERVER_URL": "Format d'adresse de serveur non valide.",
|
||||
"ERR_IDENTITY_NOT_LOADED": "Identifiant non encore chargé. Veuillez patienter et réessayer.",
|
||||
"ERR_NO_PEERS_TIME": "Aucun autre membre avec une position connue. Basculez sur 'Les amener à moi'.",
|
||||
"ERR_NO_VIDEO_TAB": "Impossible de se connecter à l'onglet vidéo.",
|
||||
"ERR_SELECT_VIDEO": "Veuillez d'abord sélectionner une vidéo !",
|
||||
"TOAST_INVITE_COPIED": "Lien d'invitation copié !",
|
||||
"TOAST_COPY_FAILED": "Échec de la copie dans le presse-papiers",
|
||||
"TOAST_LOBBY_SKIPPED": "Lobby d'épisode ignoré.",
|
||||
"TOAST_LOBBY_SKIP_FAILED": "Échec de l'annulation du lobby.",
|
||||
"TOAST_LOGS_COPIED": "Copié !",
|
||||
"TOAST_PEER_JOINED": "{name} a rejoint le salon",
|
||||
"TOAST_PEER_LEFT": "{name} a quitté le salon",
|
||||
"TOAST_PEER_ACTION": "{name} a {action}",
|
||||
"STATUS_CONNECTED": "Connecté",
|
||||
"STATUS_RECONNECTING": "Reconnexion...",
|
||||
"STATUS_CONNECTING": "Connexion...",
|
||||
"STATUS_FAILED": "Échec",
|
||||
"STATUS_DISCONNECTED": "Déconnecté",
|
||||
"STATUS_IDLE": "Prêt à se connecter",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync est prêt. Rejoignez ou créez un salon pour vous connecter et commencer la synchronisation.",
|
||||
"BTN_STATE_JOINING": "🚀 Connexion...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Reconnexion...",
|
||||
"BTN_STATE_PLAYING": "▶ Lecture...",
|
||||
"BTN_STATE_PAUSING": "⏸ Pause...",
|
||||
"BTN_STATE_SYNCING_GROUP": "Synchro au groupe ({time})...",
|
||||
"BTN_STATE_SYNCING": "Synchronisation...",
|
||||
"BTN_STATE_SYNCED": "✅ Synchronisé !",
|
||||
"NOTIF_PLAY": "lancé la lecture",
|
||||
"NOTIF_PAUSE": "mis la lecture en pause",
|
||||
"NOTIF_SEEK": "déplacé la position dans la vidéo",
|
||||
"NOTIF_FORCE_PREPARE": "lancé une synchronisation forcée",
|
||||
"NOTIF_FORCE_EXECUTE": "synchronisé tout le monde",
|
||||
"DEBUG_NO_TAB": "Aucun onglet cible sélectionné.",
|
||||
"DEBUG_COMM_FAIL": "Impossible de communiquer avec l'onglet vidéo.",
|
||||
"EMPTY_PEERS_TITLE": "Aucun membre pour l'instant",
|
||||
"EMPTY_PEERS_HINT": "Partagez votre lien d'invitation pour commencer",
|
||||
"EMPTY_HISTORY_TITLE": "Aucune activité pour l'instant",
|
||||
"EMPTY_HISTORY_HINT": "Lancez, mettez en pause ou déplacez la position pour voir l'historique",
|
||||
"EMPTY_LOGS_TITLE": "Aucun journal",
|
||||
"EMPTY_LOGS_HINT": "Les événements de connexion s'afficheront ici",
|
||||
"EMPTY_ROOMS_TITLE": "Aucun salon actif",
|
||||
"EMPTY_ROOMS_HINT": "Créez un salon ou actualisez pour trouver des salons publics",
|
||||
"LABEL_YOU": "Vous",
|
||||
"ONBOARDING_DONE": "Terminé !",
|
||||
"LABEL_LOBBY_PEER_READY": "Prêt",
|
||||
"LABEL_LOBBY_PEER_LOADING": "Chargement...",
|
||||
"LABEL_PASSWORD_PROTECTED": "Protégé par mot de passe",
|
||||
"LABEL_PEERS_COUNT": "{count} membres",
|
||||
"LABEL_CUSTOM_SERVER": "Serveur personnalisé",
|
||||
"BTN_STATE_CREATING": "🚀 Création de la salle...",
|
||||
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Échec de la synchronisation de l'épisode",
|
||||
"NOTIF_LOBBY_CANCEL_MSG": "Synchronisation automatique annulée : {reason}. Vous devrez peut-être synchroniser manuellement.",
|
||||
"LOBBY_CANCEL_TIMEOUT": "Délai d'attente dépassé",
|
||||
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Délai dépassé (récupéré)",
|
||||
"LOBBY_CANCEL_PEERS_LEFT": "Tous les autres membres sont partis",
|
||||
"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": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Évaluer",
|
||||
"FOOTER_SUPPORT_PROMPT": "Tu aimes KoalaSync? Laisse un avis!",
|
||||
"LABEL_AUDIO_PROCESSING": "Traitement audio",
|
||||
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Applique des effets audio comme la compression à la lecture vidéo",
|
||||
"AUDIO_OPEN_SETTINGS": "Ouvrir",
|
||||
"NEW_FEATURE_AUDIO": "Nouveau : Traitement audio — essayez le compresseur !",
|
||||
"AUDIO_BACK": "← Retour",
|
||||
"AUDIO_PAGE_TITLE": "Paramètres audio",
|
||||
"AUDIO_MASTER_TOGGLE": "Traitement audio",
|
||||
"AUDIO_COMPRESSOR": "Compresseur",
|
||||
"AUDIO_COMPRESSOR_ENABLE": "Activé",
|
||||
"AUDIO_PRESET": "Préréglage",
|
||||
"AUDIO_PRESET_RECOMMENDED": "Recommandé",
|
||||
"AUDIO_PRESET_DYNAMIC_RANGE": "Plage dynamique",
|
||||
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Amélioration vocale",
|
||||
"AUDIO_PRESET_SMOOTH": "Douce",
|
||||
"AUDIO_PRESET_CUSTOM": "Personnalisé",
|
||||
"AUDIO_PARAM_THRESHOLD": "Seuil",
|
||||
"AUDIO_PARAM_KNEE": "Knee",
|
||||
"AUDIO_PARAM_RATIO": "Ratio",
|
||||
"AUDIO_PARAM_ATTACK": "Attack",
|
||||
"AUDIO_PARAM_RELEASE": "Release",
|
||||
"AUDIO_EQUALIZER": "Égaliseur",
|
||||
"AUDIO_COMING_SOON": "Bientôt disponible",
|
||||
"BTN_RESTART_TOUR": "Redémarrer le tutoriel",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Recommencer le tutoriel d'intégration",
|
||||
"HINT_SELECT_VIDEO": "Sélectionnez votre vidéo ici !",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Envoyer le titre complet",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Envoyer l’identifiant d’épisode",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Ne pas envoyer",
|
||||
"LABEL_PRIVACY_SETTINGS": "Paramètres de confidentialité",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Choisissez quels titres d’onglet et de média sont envoyés au salon.",
|
||||
"LABEL_SEND_TAB_TITLE": "Envoyer le titre de l’onglet",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Envoie au salon le titre de l’onglet de navigateur sélectionné.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Titre du média",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Choisissez comment les titres des médias sont envoyés au salon."
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"LANG_CODE": "it",
|
||||
"HTML_CLASS": "lang-it",
|
||||
"APP_TITLE": "KoalaSync",
|
||||
"TAB_ROOM": "Stanza",
|
||||
"TAB_ROOM_TOOLTIP": "Impostazioni della stanza y connessione",
|
||||
"TAB_SYNC": "Sincro",
|
||||
"TAB_SYNC_TOOLTIP": "Controlli di sincronizzazione video e azioni remote",
|
||||
"TAB_SETTINGS": "Impostazioni",
|
||||
"TAB_SETTINGS_TOOLTIP": "Preferenze dell'estensione",
|
||||
"TAB_STATUS": "Stato",
|
||||
"TAB_STATUS_TOOLTIP": "Diagnostica avanzata e log",
|
||||
"BTN_CREATE_ROOM": "+ Crea Nuova Stanza",
|
||||
"MANUAL_CONNECT_HEADER": "Connessione Manuale / Avanzata",
|
||||
"LABEL_SERVER": "Server",
|
||||
"BTN_SERVER_OFFICIAL": "Ufficiale",
|
||||
"BTN_SERVER_OFFICIAL_TOOLTIP": "Usa il server ufficiale affidabile",
|
||||
"BTN_SERVER_CUSTOM": "Personalizzato",
|
||||
"BTN_SERVER_CUSTOM_TOOLTIP": "Connettiti al tuo server privato",
|
||||
"PLACEHOLDER_SERVER_URL": "wss://tuo-server:3000",
|
||||
"LABEL_ROOM_ID": "ID Stanza",
|
||||
"LABEL_ROOM_ID_TOOLTIP": "L'identificatore univoco per la tua stanza",
|
||||
"PLACEHOLDER_ROOM_ID": "Inserisci ID Stanza",
|
||||
"PLACEHOLDER_ROOM_ID_TOOLTIP": "L'ID della stanza a cui vuoi unirti",
|
||||
"LABEL_PASSWORD": "Password (Opzionale)",
|
||||
"LABEL_PASSWORD_TOOLTIP": "Password per limitare l'accesso alla stanza",
|
||||
"PLACEHOLDER_PASSWORD": "Password della stanza (opzionale)",
|
||||
"PLACEHOLDER_PASSWORD_TOOLTIP": "Lascia vuoto se non c'è una password",
|
||||
"BTN_JOIN_ROOM": "Entra / Crea Stanza",
|
||||
"BTN_JOIN_ROOM_TOOLTIP": "Entra nella stanza",
|
||||
"LABEL_PUBLIC_ROOMS": "Stanze Pubbliche",
|
||||
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Elenco delle stanze pubbliche su questo server",
|
||||
"BTN_REFRESH": "AGGIORNA",
|
||||
"BTN_REFRESH_TOOLTIP": "Aggiorna l'elenco delle stanze",
|
||||
"PUBLIC_ROOMS_REFRESHING": "Aggiornamento...",
|
||||
"BTN_REFRESH_COOLDOWN": "ATTENDI {seconds}s",
|
||||
"BTN_REFRESH_COOLDOWN_TOOLTIP": "Riprova tra {seconds}s.",
|
||||
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Aggiornamento in corso. Disponibile tra {seconds}s.",
|
||||
"LABEL_ACTIVE_ROOM": "Stanza Attiva",
|
||||
"LABEL_ACTIVE_ROOM_TOOLTIP": "La stanza a cui sei connesso",
|
||||
"ACTIVE_ROOM_NONE": "NESSUNA",
|
||||
"ACTIVE_SERVER_OFFICIAL": "Server Ufficiale",
|
||||
"LABEL_INVITE_LINK": "Link di Invito",
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Condividi questo link per invitare i tuoi amici",
|
||||
"LABEL_PEERS_IN_ROOM": "Partecipanti",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Utenti connessi a questa stanza",
|
||||
"LABEL_HOST_CONTROL": "Controllo host",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Limita chi può controllare la riproduzione in questa stanza",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Solo io posso controllare la riproduzione",
|
||||
"NOTICE_HOST_CONTROLS": "L'host controlla la riproduzione per tutti.",
|
||||
"NOTICE_COHOST_HINT": "Suggerimento: concedi il controllo ai singoli partecipanti nell'elenco qui sotto.",
|
||||
"BADGE_HOST": "Host",
|
||||
"BADGE_GUEST": "Ospite",
|
||||
"BADGE_CONTROLLER": "Controller",
|
||||
"BTN_GIVE_CONTROL": "Dai il controllo",
|
||||
"BTN_REVOKE_CONTROL": "Revoca",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Guarda per conto proprio — ignora i comandi dell'host",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · L'host controlla questa stanza",
|
||||
"HCM_DIALOG_BODY": "Solo l'host può controllare la riproduzione in questa stanza. Continuare a guardare insieme o guardare per conto tuo?",
|
||||
"HCM_DIALOG_STAY": "Resta sincronizzato",
|
||||
"HCM_DIALOG_SOLO": "Guarda per conto mio",
|
||||
"HCM_BADGE_SOLO": "Stai guardando da solo",
|
||||
"HCM_BADGE_RESYNC": "Risincronizza",
|
||||
"NO_PEERS_CONNECTED": "Nessun partecipante connesso",
|
||||
"BTN_LEAVE_ROOM": "Lascia Stanza",
|
||||
"LABEL_SELECT_VIDEO": "Seleziona Video",
|
||||
"LABEL_SELECT_VIDEO_TOOLTIP": "Scegli la scheda con il video da sincronizzare",
|
||||
"OPTION_SELECT_TAB": "-- Seleziona una Scheda --",
|
||||
"LABEL_REMOTE_CONTROL": "Telecomando",
|
||||
"BTN_COPY_INVITE": "📋 Link di Invito",
|
||||
"BTN_COPY_INVITE_TOOLTIP": "Copia il link di invito",
|
||||
"BTN_PLAY": "▶ Riproduci",
|
||||
"BTN_PLAY_TOOLTIP": "Avvia la riproduzione per tutti",
|
||||
"BTN_PAUSE": "⏸ Pausa",
|
||||
"BTN_PAUSE_TOOLTIP": "Metti in pausa per tutti",
|
||||
"BTN_SYNC": "⚡ SINCRO",
|
||||
"BTN_SYNC_TOOLTIP": "Forza la sincronizzazione per tutti",
|
||||
"OPTION_JUMP_TO_OTHERS": "Vai alla posizione degli altri",
|
||||
"OPTION_JUMP_TO_ME": "Portali alla mia posizione",
|
||||
"OPTION_JUMP_MODE_TOOLTIP": "Scegli l'obiettivo di sincronizzazione",
|
||||
"LABEL_LAST_ACTIVITY": "Ultima Attività",
|
||||
"LABEL_LAST_ACTIVITY_TOOLTIP": "Mostra l'ultimo comando inviato",
|
||||
"NO_RECENT_COMMANDS": "Nessun comando recente",
|
||||
"LOBBY_HEADER": "SALA D'ATTESA EPISODIO",
|
||||
"LOBBY_WAITING_FOR": "🎬 In attesa di: \"{title}\"",
|
||||
"LOBBY_WAITING_PEERS": "In attesa dei partecipanti...",
|
||||
"BTN_SKIP_PLAY": "Salta e riproduci ora",
|
||||
"BTN_SKIP_PLAY_TOOLTIP": "Annulla l'attesa e riproduci comunque",
|
||||
"LOBBY_CONNECT_FIRST": "Entra prima in una stanza",
|
||||
"LOBBY_CONNECT_FIRST_DESC": "Devi unirti a una stanza o crearne una nuova per sincronizzare i video.",
|
||||
"BTN_CREATE_ROOM_ALT": "Crea Nuova Stanza",
|
||||
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Crea una stanza casuale ed entra",
|
||||
"LABEL_USERNAME": "Tuo Nome Utente",
|
||||
"LABEL_USERNAME_TOOLTIP": "Il tuo nome visibile agli altri.",
|
||||
"PLACEHOLDER_USERNAME": "Koala Anonimo",
|
||||
"LABEL_HIDE_CLUTTER": "Nascondi schede senza video",
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Mantiene pulito l'elenco filtrando le schede non pertinenti",
|
||||
"LABEL_AUTO_SYNC_NEXT": "Sincro auto prossimo episodio",
|
||||
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Attende tutti i partecipanti prima di avviare il prossimo episodio.",
|
||||
"LABEL_AUTO_COPY_INVITE": "Copia automatica invito",
|
||||
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Copia il link negli appunti quando crei una nuova stanza.",
|
||||
"LABEL_NOTIFICATIONS": "Notifiche del Browser",
|
||||
"LABEL_NOTIFICATIONS_TOOLTIP": "Mostra notifiche quando qualcuno entra, esce o cambia stato.",
|
||||
"LABEL_LANGUAGE": "Lingua Applicazione",
|
||||
"LABEL_LANGUAGE_TOOLTIP": "Scegli la lingua dell'estensione",
|
||||
"LABEL_TROUBLESHOOTING": "Risoluzione Problemi",
|
||||
"LABEL_TROUBLESHOOTING_TOOLTIP": "Strumenti per risolvere problemi di connessione",
|
||||
"BTN_REGEN_ID": "Rigenera ID Utente",
|
||||
"BTN_REGEN_ID_TOOLTIP": "Genera un nuovo ID interno e riconnettiti",
|
||||
"REGEN_ID_DESC": "Usa questa opzione se riscontri errori di 'Identità Duplicata'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Hai altri problemi? Apri una segnalazione su GitHub",
|
||||
"TOAST_ID_REGENERATED": "Identità rigenerata — riconnessione…",
|
||||
"LABEL_CONN_STATUS": "Stato Connessione",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Stato attuale della connessione",
|
||||
"CONN_STATUS_DISCONNECTED": "Disconnesso",
|
||||
"BTN_RETRY": "RIPROVA",
|
||||
"BTN_RETRY_TOOLTIP": "Tenta di riconnettersi al server",
|
||||
"BTN_COPY_LOGS": "Copia Log",
|
||||
"BTN_COPY_LOGS_TOOLTIP": "Copia i log negli appunti",
|
||||
"LABEL_VIDEO_DEBUG": "Debug Video",
|
||||
"LABEL_VIDEO_DEBUG_TOOLTIP": "Dettagli tecnici sul video selezionato",
|
||||
"VIDEO_DEBUG_EMPTY": "Nessuna scheda selezionata o video rilevato.",
|
||||
"LABEL_HISTORY": "Cronologia Completa",
|
||||
"LABEL_HISTORY_TOOLTIP": "Registro di tutti i comandi inviati nella stanza",
|
||||
"HISTORY_EMPTY": "Ancora nessuna attività",
|
||||
"LABEL_LOGS": "Log (Ultimi 50)",
|
||||
"LABEL_LOGS_TOOLTIP": "Log tecnici per il debug",
|
||||
"BTN_CLEAR": "PULISCI",
|
||||
"BTN_CLEAR_TOOLTIP": "Cancella i log",
|
||||
"LABEL_GITHUB": "Repository GitHub",
|
||||
"BTN_ONBOARDING_SKIP": "Salta",
|
||||
"BTN_ONBOARDING_SKIP_TOOLTIP": "Salta il tutorial",
|
||||
"BTN_ONBOARDING_NEXT": "Avanti",
|
||||
"BTN_ONBOARDING_NEXT_TOOLTIP": "Vai al passaggio successivo",
|
||||
"ONBOARDING_1_TITLE": "Benvenuto in KoalaSync!",
|
||||
"ONBOARDING_1_TEXT": "Guarda i video insieme ai tuoi amici in perfetta sincronia. Facciamo un breve tour!",
|
||||
"ONBOARDING_2_TITLE": "1. Crea una Stanza",
|
||||
"ONBOARDING_2_TEXT": "Inizia da qui. Crea una stanza e condividi il link con i tuoi amici.",
|
||||
"ONBOARDING_3_TITLE": "2. Seleziona Video",
|
||||
"ONBOARDING_3_TEXT": "Scegli il video che vuoi sincronizzare. Play, pausa o salta: tutti vedranno lo stesso.",
|
||||
"ONBOARDING_4_TITLE": "3. Personalizza",
|
||||
"ONBOARDING_4_TEXT": "Scegli un nome utente per farti riconoscere dai tuoi amici.",
|
||||
"ONBOARDING_5_TITLE": "Tutto pronto!",
|
||||
"ONBOARDING_5_TEXT": "È ora di prendere i popcorn. Buona visione!",
|
||||
"ERR_CONN_TIMEOUT": "Connessione scaduta. Riprova.",
|
||||
"ERR_INVALID_SERVER_URL": "Formato URL del server non valido.",
|
||||
"ERR_IDENTITY_NOT_LOADED": "Identità non ancora caricata. Attendi un momento.",
|
||||
"ERR_NO_PEERS_TIME": "Nessun altro partecipante con posizione nota. Passa a 'Portali alla mia posizione'.",
|
||||
"ERR_NO_VIDEO_TAB": "Impossibile connettersi alla scheda del video.",
|
||||
"ERR_SELECT_VIDEO": "Seleziona prima un video!",
|
||||
"TOAST_INVITE_COPIED": "Link di invito copiato!",
|
||||
"TOAST_COPY_FAILED": "Impossibile copiare negli appunti",
|
||||
"TOAST_LOBBY_SKIPPED": "Sala d'attesa saltata.",
|
||||
"TOAST_LOBBY_SKIP_FAILED": "Impossibile saltare la sala d'attesa.",
|
||||
"TOAST_LOGS_COPIED": "Copiato!",
|
||||
"TOAST_PEER_JOINED": "{name} è entrato nella stanza",
|
||||
"TOAST_PEER_LEFT": "{name} ha lasciato la stanza",
|
||||
"TOAST_PEER_ACTION": "{name} {action}",
|
||||
"STATUS_CONNECTED": "Connesso",
|
||||
"STATUS_RECONNECTING": "Riconnessione...",
|
||||
"STATUS_CONNECTING": "Connessione in corso...",
|
||||
"STATUS_FAILED": "Errore",
|
||||
"STATUS_DISCONNECTED": "Disconnesso",
|
||||
"STATUS_IDLE": "Pronto a connettersi",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync è pronto. Entra o crea una stanza per connetterti e avviare la sincronizzazione.",
|
||||
"BTN_STATE_JOINING": "🚀 Entrando...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Riconnessione...",
|
||||
"BTN_STATE_PLAYING": "▶ In riproduzione...",
|
||||
"BTN_STATE_PAUSING": "⏸ In pausa...",
|
||||
"BTN_STATE_SYNCING_GROUP": "Sincronizzazione di gruppo ({time})...",
|
||||
"BTN_STATE_SYNCING": "Sincronizzazione...",
|
||||
"BTN_STATE_SYNCED": "✅ Sincronizzato!",
|
||||
"NOTIF_PLAY": "ha avviato la riproduzione",
|
||||
"NOTIF_PAUSE": "ha messo in pausa",
|
||||
"NOTIF_SEEK": "ha cambiato posizione",
|
||||
"NOTIF_FORCE_PREPARE": "ha avviato una sincronizzazione forzata",
|
||||
"NOTIF_FORCE_EXECUTE": "ha sincronizzato tutti",
|
||||
"DEBUG_NO_TAB": "Nessuna scheda selezionata.",
|
||||
"DEBUG_COMM_FAIL": "Errore di comunicazione con il video.",
|
||||
"EMPTY_PEERS_TITLE": "Nessun partecipante",
|
||||
"EMPTY_PEERS_HINT": "Condividi il tuo link per iniziare",
|
||||
"EMPTY_HISTORY_TITLE": "Nessuna attività",
|
||||
"EMPTY_HISTORY_HINT": "Usa i comandi per vedere la cronologia",
|
||||
"EMPTY_LOGS_TITLE": "Nessun log",
|
||||
"EMPTY_LOGS_HINT": "Gli eventi appariranno qui",
|
||||
"EMPTY_ROOMS_TITLE": "Nessuna stanza attiva",
|
||||
"EMPTY_ROOMS_HINT": "Crea una stanza o aggiorna l'elenco",
|
||||
"LABEL_YOU": "Tu",
|
||||
"ONBOARDING_DONE": "Fatto!",
|
||||
"LABEL_LOBBY_PEER_READY": "Pronto",
|
||||
"LABEL_LOBBY_PEER_LOADING": "Caricamento...",
|
||||
"LABEL_PASSWORD_PROTECTED": "Protetto da Password",
|
||||
"LABEL_PEERS_COUNT": "{count} partecipanti",
|
||||
"LABEL_CUSTOM_SERVER": "Server Personalizzato",
|
||||
"BTN_STATE_CREATING": "🚀 Creazione Stanza...",
|
||||
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Sincro Episodio Fallita",
|
||||
"NOTIF_LOBBY_CANCEL_MSG": "Sincro auto annullata: {reason}. Usa la sincronizzazione manuale.",
|
||||
"LOBBY_CANCEL_TIMEOUT": "Tempo scaduto",
|
||||
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Tempo scaduto (ripristinato)",
|
||||
"LOBBY_CANCEL_PEERS_LEFT": "Tutti gli altri partecipanti sono usciti",
|
||||
"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": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Valutaci",
|
||||
"FOOTER_SUPPORT_PROMPT": "Ti piace KoalaSync? Lascia una recensione!",
|
||||
"LABEL_AUDIO_PROCESSING": "Elaborazione Audio",
|
||||
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Applica effetti sonori (come il compressore) ai video",
|
||||
"AUDIO_OPEN_SETTINGS": "Apri",
|
||||
"NEW_FEATURE_AUDIO": "Novità: Elaborazione Audio — prova il compressore!",
|
||||
"AUDIO_BACK": "← Indietro",
|
||||
"AUDIO_PAGE_TITLE": "Impostazioni Audio",
|
||||
"AUDIO_MASTER_TOGGLE": "Elaborazione Audio",
|
||||
"AUDIO_COMPRESSOR": "Compressore",
|
||||
"AUDIO_COMPRESSOR_ENABLE": "Attivo",
|
||||
"AUDIO_PRESET": "Preimpostazione",
|
||||
"AUDIO_PRESET_RECOMMENDED": "Consigliato",
|
||||
"AUDIO_PRESET_DYNAMIC_RANGE": "Gamma Dinamica",
|
||||
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Miglioramento Vocale",
|
||||
"AUDIO_PRESET_SMOOTH": "Morbido",
|
||||
"AUDIO_PRESET_CUSTOM": "Personalizzato",
|
||||
"AUDIO_PARAM_THRESHOLD": "Soglia (Threshold)",
|
||||
"AUDIO_PARAM_KNEE": "Ginocchio (Knee)",
|
||||
"AUDIO_PARAM_RATIO": "Rapporto (Ratio)",
|
||||
"AUDIO_PARAM_ATTACK": "Attacco (Attack)",
|
||||
"AUDIO_PARAM_RELEASE": "Rilascio (Release)",
|
||||
"AUDIO_EQUALIZER": "Equalizzatore",
|
||||
"AUDIO_COMING_SOON": "Prossimamente",
|
||||
"BTN_RESTART_TOUR": "Riavvia Tutorial",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Riavvia il tutorial introduttivo",
|
||||
"HINT_SELECT_VIDEO": "Scegli il tuo video qui!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Invia titolo completo",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Invia ID episodio",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Non inviare",
|
||||
"LABEL_PRIVACY_SETTINGS": "Impostazioni privacy",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Scegli quali titoli di scheda e contenuto multimediale vengono inviati alla stanza.",
|
||||
"LABEL_SEND_TAB_TITLE": "Invia titolo scheda",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Invia alla stanza il titolo della scheda del browser selezionata.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Titolo multimediale",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Scegli come inviare alla stanza i titoli multimediali."
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"LANG_CODE": "ja",
|
||||
"HTML_CLASS": "lang-ja",
|
||||
"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": "ルーム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": "現在このルームに接続している他のユーザー",
|
||||
"LABEL_HOST_CONTROL": "ホスト操作",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "このルームで再生を操作できる人を制限します",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "再生を操作できるのは自分だけ",
|
||||
"NOTICE_HOST_CONTROLS": "ホストが全員の再生を操作します。",
|
||||
"NOTICE_COHOST_HINT": "ヒント: 下の参加者リストで個別の参加者に操作権限を付与できます。",
|
||||
"BADGE_HOST": "ホスト",
|
||||
"BADGE_GUEST": "ゲスト",
|
||||
"BADGE_CONTROLLER": "操作権あり",
|
||||
"BTN_GIVE_CONTROL": "操作権を付与",
|
||||
"BTN_REVOKE_CONTROL": "取り消す",
|
||||
"BADGE_DESYNCED": "ソロ",
|
||||
"TOOLTIP_PEER_DESYNCED": "単独で視聴中 — ホストのコマンドを無視しています",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · ホストがこのルームを操作中",
|
||||
"HCM_DIALOG_BODY": "このルームではホストのみが再生を操作できます。一緒に視聴を続けますか、それとも自分だけで視聴しますか?",
|
||||
"HCM_DIALOG_STAY": "同期を維持",
|
||||
"HCM_DIALOG_SOLO": "自分だけで視聴",
|
||||
"HCM_BADGE_SOLO": "自分だけで視聴中",
|
||||
"HCM_BADGE_RESYNC": "再同期",
|
||||
"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": "ピアIDの再生成",
|
||||
"BTN_REGEN_ID_TOOLTIP": "内部IDを再生成して再接続します",
|
||||
"REGEN_ID_DESC": "「Duplicate Identity」(ID重複)エラーが表示される場合に使用します。",
|
||||
"REGEN_ID_OTHER_ISSUE": "他の問題?GitHub Issueを開く",
|
||||
"TOAST_ID_REGENERATED": "IDを再生成しました — 再接続中…",
|
||||
"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": "IDがまだロードされていません。しばらく待ってから再試行してください。",
|
||||
"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": "切断されました",
|
||||
"STATUS_IDLE": "接続準備完了",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSyncの準備ができました。ルームに参加するか作成して接続し、同期を開始してください。",
|
||||
"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": "Knee",
|
||||
"AUDIO_PARAM_RATIO": "Ratio",
|
||||
"AUDIO_PARAM_ATTACK": "Attack",
|
||||
"AUDIO_PARAM_RELEASE": "Release",
|
||||
"AUDIO_EQUALIZER": "イコライザー",
|
||||
"AUDIO_COMING_SOON": "近日公開",
|
||||
"BTN_RESTART_TOUR": "チュートリアルを再起動",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "オンボーディングチュートリアルを再起動する",
|
||||
"HINT_SELECT_VIDEO": "ここで動画を選択してください!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "完全なタイトルを送信",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "エピソードIDを送信",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "送信しない",
|
||||
"LABEL_PRIVACY_SETTINGS": "プライバシー設定",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "ルームに送信するタブタイトルとメディアタイトルを選択します。",
|
||||
"LABEL_SEND_TAB_TITLE": "タブタイトルを送信",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "選択中のブラウザータブのタイトルをルームに送信します。",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "メディアタイトル",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "メディアタイトルをルームに送信する方法を選択します。"
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"LANG_CODE": "ko",
|
||||
"HTML_CLASS": "lang-ko",
|
||||
"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": "방 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": "현재 이 방에 연결되어 있는 다른 사용자들",
|
||||
"LABEL_HOST_CONTROL": "호스트 제어",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "이 방에서 재생을 제어할 수 있는 사람 제한",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "나만 재생을 제어할 수 있음",
|
||||
"NOTICE_HOST_CONTROLS": "호스트가 모두의 재생을 제어합니다.",
|
||||
"NOTICE_COHOST_HINT": "팁: 아래 참가자 목록에서 개별 참가자에게 제어 권한을 부여하세요.",
|
||||
"BADGE_HOST": "호스트",
|
||||
"BADGE_GUEST": "게스트",
|
||||
"BADGE_CONTROLLER": "제어자",
|
||||
"BTN_GIVE_CONTROL": "제어 권한 부여",
|
||||
"BTN_REVOKE_CONTROL": "취소",
|
||||
"BADGE_DESYNCED": "단독",
|
||||
"TOOLTIP_PEER_DESYNCED": "혼자 시청 중 — 호스트 명령을 무시합니다",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · 호스트가 이 방을 제어합니다",
|
||||
"HCM_DIALOG_BODY": "이 방에서는 호스트만 재생을 제어할 수 있습니다. 계속 함께 보시겠어요, 아니면 혼자 보시겠어요?",
|
||||
"HCM_DIALOG_STAY": "동기화 유지",
|
||||
"HCM_DIALOG_SOLO": "혼자 보기",
|
||||
"HCM_BADGE_SOLO": "혼자 시청 중",
|
||||
"HCM_BADGE_RESYNC": "다시 동기화",
|
||||
"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": "피어 ID 재생성",
|
||||
"BTN_REGEN_ID_TOOLTIP": "내부 ID를 재생성하고 다시 연결합니다",
|
||||
"REGEN_ID_DESC": "\"중복된 ID\" 오류가 발생할 경우에 사용하세요.",
|
||||
"REGEN_ID_OTHER_ISSUE": "다른 문제? GitHub Issue 열기",
|
||||
"TOAST_ID_REGENERATED": "ID가 재생성되었습니다 — 다시 연결 중…",
|
||||
"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": "ID가 아직 로드되지 않았습니다. 잠시 후 다시 시도해 주세요.",
|
||||
"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": "연결 끊김",
|
||||
"STATUS_IDLE": "연결 준비 완료",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync가 준비되었습니다. 방에 참여하거나 생성하여 연결하고 동기화를 시작하세요.",
|
||||
"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": "Knee",
|
||||
"AUDIO_PARAM_RATIO": "Ratio",
|
||||
"AUDIO_PARAM_ATTACK": "Attack",
|
||||
"AUDIO_PARAM_RELEASE": "Release",
|
||||
"AUDIO_EQUALIZER": "이퀄라이저",
|
||||
"AUDIO_COMING_SOON": "출시 예정",
|
||||
"BTN_RESTART_TOUR": "튜토리얼 다시 시작",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "온보딩 튜토리얼을 다시 시작합니다",
|
||||
"HINT_SELECT_VIDEO": "여기에서 비디오를 선택하세요!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "전체 제목 보내기",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "에피소드 ID 보내기",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "보내지 않음",
|
||||
"LABEL_PRIVACY_SETTINGS": "개인정보 설정",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "방에 보낼 탭 제목과 미디어 제목을 선택합니다.",
|
||||
"LABEL_SEND_TAB_TITLE": "탭 제목 보내기",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "선택한 브라우저 탭 제목을 방에 보냅니다.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "미디어 제목",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "미디어 제목을 방에 보내는 방식을 선택합니다."
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"LANG_CODE": "nl",
|
||||
"HTML_CLASS": "lang-nl",
|
||||
"APP_TITLE": "KoalaSync",
|
||||
"TAB_ROOM": "Kamer",
|
||||
"TAB_ROOM_TOOLTIP": "Kamerinstellingen en verbinding",
|
||||
"TAB_SYNC": "Sync",
|
||||
"TAB_SYNC_TOOLTIP": "Videobeheer en externe acties",
|
||||
"TAB_SETTINGS": "Instellingen",
|
||||
"TAB_SETTINGS_TOOLTIP": "Extensievoorkeuren",
|
||||
"TAB_STATUS": "Status",
|
||||
"TAB_STATUS_TOOLTIP": "Geavanceerde diagnostische gegevens & logs",
|
||||
"BTN_CREATE_ROOM": "+ Nieuwe kamer maken",
|
||||
"MANUAL_CONNECT_HEADER": "Handmatig verbinden / Geavanceerd",
|
||||
"LABEL_SERVER": "Server",
|
||||
"BTN_SERVER_OFFICIAL": "Officieel",
|
||||
"BTN_SERVER_OFFICIAL_TOOLTIP": "Gebruik de officiële betrouwbare server",
|
||||
"BTN_SERVER_CUSTOM": "Aangepast",
|
||||
"BTN_SERVER_CUSTOM_TOOLTIP": "Verbind met uw eigen gehoste server",
|
||||
"PLACEHOLDER_SERVER_URL": "wss://uw-server:3000",
|
||||
"LABEL_ROOM_ID": "Kamer-ID",
|
||||
"LABEL_ROOM_ID_TOOLTIP": "De unieke identificatie voor uw synchronisatiekamer",
|
||||
"PLACEHOLDER_ROOM_ID": "Voer kamer-ID in",
|
||||
"PLACEHOLDER_ROOM_ID_TOOLTIP": "Het unieke ID van de kamer waar u aan wilt deelnemen",
|
||||
"LABEL_PASSWORD": "Wachtwoord (optioneel)",
|
||||
"LABEL_PASSWORD_TOOLTIP": "Optioneel wachtwoord om toegang tot de kamer te beperken",
|
||||
"PLACEHOLDER_PASSWORD": "Kamerwachtwoord (optioneel)",
|
||||
"PLACEHOLDER_PASSWORD_TOOLTIP": "Wachtwoord voor de kamer (leeg laten indien geen)",
|
||||
"BTN_JOIN_ROOM": "Deelnemen / Kamer maken",
|
||||
"BTN_JOIN_ROOM_TOOLTIP": "Verbinden met de kamer",
|
||||
"LABEL_PUBLIC_ROOMS": "Openbare kamers",
|
||||
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Lijst met openbaar beschikbare kamers op deze server",
|
||||
"BTN_REFRESH": "VERNIEUWEN",
|
||||
"BTN_REFRESH_TOOLTIP": "Vernieuw de lijst met openbare kamers",
|
||||
"PUBLIC_ROOMS_REFRESHING": "Vernieuwen...",
|
||||
"BTN_REFRESH_COOLDOWN": "WACHT {seconds}s",
|
||||
"BTN_REFRESH_COOLDOWN_TOOLTIP": "Vernieuwen van kamerlijst is aan het afkoelen. Probeer opnieuw in {seconds}s.",
|
||||
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Openbare kamers vernieuwen. Volgende verversing beschikbaar in {seconds}s.",
|
||||
"LABEL_ACTIVE_ROOM": "Actieve kamer",
|
||||
"LABEL_ACTIVE_ROOM_TOOLTIP": "De kamer waar u momenteel mee bent verbonden",
|
||||
"ACTIVE_ROOM_NONE": "GEEN",
|
||||
"ACTIVE_SERVER_OFFICIAL": "Officiële server",
|
||||
"LABEL_INVITE_LINK": "Uitnodigingslink",
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Deel deze link met vrienden zodat ze kunnen deelnemen",
|
||||
"LABEL_PEERS_IN_ROOM": "Deelnemers in kamer",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Andere gebruikers die momenteel zijn verbonden met deze kamer",
|
||||
"LABEL_HOST_CONTROL": "Hostbeheer",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Bepaal wie de weergave in deze kamer mag bedienen",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Alleen ik mag de weergave bedienen",
|
||||
"NOTICE_HOST_CONTROLS": "De host bedient de weergave voor iedereen.",
|
||||
"NOTICE_COHOST_HINT": "Tip: geef afzonderlijke deelnemers de besturing in de lijst hieronder.",
|
||||
"BADGE_HOST": "Host",
|
||||
"BADGE_GUEST": "Gast",
|
||||
"BADGE_CONTROLLER": "Bediener",
|
||||
"BTN_GIVE_CONTROL": "Bediening geven",
|
||||
"BTN_REVOKE_CONTROL": "Intrekken",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Zelfstandig aan het kijken — negeert de commando's van de host",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · De host bedient deze kamer",
|
||||
"HCM_DIALOG_BODY": "Alleen de host kan de weergave in deze kamer bedienen. Samen blijven kijken of zelf verder kijken?",
|
||||
"HCM_DIALOG_STAY": "Gesynchroniseerd blijven",
|
||||
"HCM_DIALOG_SOLO": "Zelf verder kijken",
|
||||
"HCM_BADGE_SOLO": "Je kijkt zelfstandig",
|
||||
"HCM_BADGE_RESYNC": "Opnieuw sync",
|
||||
"NO_PEERS_CONNECTED": "Geen deelnemers verbonden",
|
||||
"BTN_LEAVE_ROOM": "Kamer verlaten",
|
||||
"LABEL_SELECT_VIDEO": "Selecteer video",
|
||||
"LABEL_SELECT_VIDEO_TOOLTIP": "Kies het browsertabblad met de video die u wilt synchroniseren",
|
||||
"OPTION_SELECT_TAB": "-- Selecteer een tabblad --",
|
||||
"LABEL_REMOTE_CONTROL": "Afstandsbediening",
|
||||
"BTN_COPY_INVITE": "📋 Uitnodigingslink",
|
||||
"BTN_COPY_INVITE_TOOLTIP": "Uitnodigingslink kopiëren",
|
||||
"BTN_PLAY": "▶ Afspelen",
|
||||
"BTN_PLAY_TOOLTIP": "Stuur een afspeelopdracht naar iedereen",
|
||||
"BTN_PAUSE": "⏸ Pauzeren",
|
||||
"BTN_PAUSE_TOOLTIP": "Stuur een pauzeeropdracht naar iedereen",
|
||||
"BTN_SYNC": "⚡ SYNC",
|
||||
"BTN_SYNC_TOOLTIP": "Dwing alle gebruikers om te synchroniseren",
|
||||
"OPTION_JUMP_TO_OTHERS": "Spring naar anderen",
|
||||
"OPTION_JUMP_TO_ME": "Spring naar mij",
|
||||
"OPTION_JUMP_MODE_TOOLTIP": "Kies synchronisatiedoel",
|
||||
"LABEL_LAST_ACTIVITY": "Laatste activiteitsstatus",
|
||||
"LABEL_LAST_ACTIVITY_TOOLTIP": "Toont de meest recente afspeel-, pauzeer- of zoekopdracht",
|
||||
"NO_RECENT_COMMANDS": "Geen recente commando's",
|
||||
"LOBBY_HEADER": "EPISODE LOBBY",
|
||||
"LOBBY_WAITING_FOR": "🎬 Wachten op: \"{title}\"",
|
||||
"LOBBY_WAITING_PEERS": "Wachten op deelnemers...",
|
||||
"BTN_SKIP_PLAY": "Overslaan & toch afspelen",
|
||||
"BTN_SKIP_PLAY_TOOLTIP": "Lobby annuleren en toch afspelen",
|
||||
"LOBBY_CONNECT_FIRST": "Verbind eerst met een kamer",
|
||||
"LOBBY_CONNECT_FIRST_DESC": "U moet deelnemen aan een kamer via een uitnodigingslink of een nieuwe maken om video's te synchroniseren.",
|
||||
"BTN_CREATE_ROOM_ALT": "Nieuwe kamer maken",
|
||||
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Maak een nieuwe willekeurige kamer en neem deel",
|
||||
"LABEL_USERNAME": "Uw gebruikersnaam",
|
||||
"LABEL_USERNAME_TOOLTIP": "Gebruikersnaam helpt anderen u te identificeren.",
|
||||
"PLACEHOLDER_USERNAME": "Anonieme Koala",
|
||||
"LABEL_HIDE_CLUTTER": "Overbodige tabbladen verbergen",
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtert tabbladen zonder video's en niet-gerelateerde domeinen om de lijst schoon te houden",
|
||||
"LABEL_AUTO_SYNC_NEXT": "Volgende aflevering automatisch syncen",
|
||||
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pauzeert automatisch en wacht op alle deelnemers wanneer een aflevering verandert, en start dan gezamenlijk gesynchroniseerd.",
|
||||
"LABEL_AUTO_COPY_INVITE": "Uitnodigingslink automatisch kopiëren",
|
||||
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Kopieert de uitnodigingslink automatisch naar uw klembord bij het maken van een nieuwe kamer.",
|
||||
"LABEL_NOTIFICATIONS": "Browsernotificaties",
|
||||
"LABEL_NOTIFICATIONS_TOOLTIP": "Toont systeemnotificaties wanneer iemand deelneemt/verlaat of afspeelt/pauzeert.",
|
||||
"LABEL_LANGUAGE": "Taal app",
|
||||
"LABEL_LANGUAGE_TOOLTIP": "Kies uw favoriete taal voor de extensie",
|
||||
"LABEL_TROUBLESHOOTING": "Problemen oplossen",
|
||||
"LABEL_TROUBLESHOOTING_TOOLTIP": "Hulpmiddelen voor het oplossen van verbindingsproblemen",
|
||||
"BTN_REGEN_ID": "Peer-ID opnieuw genereren",
|
||||
"BTN_REGEN_ID_TOOLTIP": "Genereer uw interne ID opnieuw en maak opnieuw verbinding",
|
||||
"REGEN_ID_DESC": "Gebruik dit als u fouten ziet over 'Duplicate Identity'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Ander probleem? Open een GitHub Issue",
|
||||
"TOAST_ID_REGENERATED": "Identiteit opnieuw gegenereerd — opnieuw verbinden…",
|
||||
"LABEL_CONN_STATUS": "Verbindingsstatus",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Huidige WebSocket-verbindingsstatus",
|
||||
"CONN_STATUS_DISCONNECTED": "Verbinding verbroken",
|
||||
"BTN_RETRY": "OPNIEUW PROBEREN",
|
||||
"BTN_RETRY_TOOLTIP": "Proberen opnieuw verbinding te maken met de server",
|
||||
"BTN_COPY_LOGS": "Logs kopiëren",
|
||||
"BTN_COPY_LOGS_TOOLTIP": "Kopieer logboeken naar het klembord om te delen",
|
||||
"LABEL_VIDEO_DEBUG": "Video-debuginfo",
|
||||
"LABEL_VIDEO_DEBUG_TOOLTIP": "Technische details over het momenteel geselecteerde video-element",
|
||||
"VIDEO_DEBUG_EMPTY": "Geen tabblad geselecteerd of video gedetecteerd.",
|
||||
"LABEL_HISTORY": "Volledige actiegeschiedenis",
|
||||
"LABEL_HISTORY_TOOLTIP": "Chronologisch logboek van alle synchronisatieopdrachten in de kamer",
|
||||
"HISTORY_EMPTY": "Nog geen activiteit",
|
||||
"LABEL_LOGS": "Logs (Laatste 50)",
|
||||
"LABEL_LOGS_TOOLTIP": "Technische verbindingslogboeken voor debuggen",
|
||||
"BTN_CLEAR": "WISSEN",
|
||||
"BTN_CLEAR_TOOLTIP": "Logboekuitvoer wissen",
|
||||
"LABEL_GITHUB": "GitHub-repository",
|
||||
"BTN_ONBOARDING_SKIP": "Overslaan",
|
||||
"BTN_ONBOARDING_SKIP_TOOLTIP": "Sla de tutorial over",
|
||||
"BTN_ONBOARDING_NEXT": "Volgende",
|
||||
"BTN_ONBOARDING_NEXT_TOOLTIP": "Ga naar de volgende stap",
|
||||
"ONBOARDING_1_TITLE": "Welkom bij KoalaSync!",
|
||||
"ONBOARDING_1_TEXT": "Bekijk video's samen in perfecte synchronisatie — waar u ook bent. Laten we een snelle rondleiding nemen!",
|
||||
"ONBOARDING_2_TITLE": "1. Kamer maken",
|
||||
"ONBOARDING_2_TEXT": "Begin hier. Maak een kamer en deel de uitnodigingslink met uw vrienden.",
|
||||
"ONBOARDING_3_TITLE": "2. Selecteer video",
|
||||
"ONBOARDING_3_TEXT": "Navigeer hiernaartoe om de video te selecteren die u wilt synchroniseren. Spelen, pauzeren en zoeken — iedereen blijft synchroon.",
|
||||
"ONBOARDING_4_TITLE": "3. Personaliseren",
|
||||
"ONBOARDING_4_TEXT": "Kies een leuke gebruikersnaam zodat uw vrienden weten wie u bent.",
|
||||
"ONBOARDING_5_TITLE": "U bent er helemaal klaar voor!",
|
||||
"ONBOARDING_5_TEXT": "Tijd voor popcorn. Veel plezier met samen kijken!",
|
||||
"ERR_CONN_TIMEOUT": "Verbinding time-out. Probeer het opnieuw.",
|
||||
"ERR_INVALID_SERVER_URL": "Ongeldig server URL-formaat.",
|
||||
"ERR_IDENTITY_NOT_LOADED": "Identiteit nog niet geladen. Wacht een moment en probeer het opnieuw.",
|
||||
"ERR_NO_PEERS_TIME": "Geen andere deelnemers met een bekende tijd. Schakel over naar 'Spring naar mij'.",
|
||||
"ERR_NO_VIDEO_TAB": "Kon geen verbinding maken met videotabblad.",
|
||||
"ERR_SELECT_VIDEO": "Selecteer eerst een video!",
|
||||
"TOAST_INVITE_COPIED": "Uitnodigingslink gekopieerd!",
|
||||
"TOAST_COPY_FAILED": "Kopiëren naar klembord mislukt",
|
||||
"TOAST_LOBBY_SKIPPED": "Episode Lobby overgeslagen.",
|
||||
"TOAST_LOBBY_SKIP_FAILED": "Lobby overslaan mislukt.",
|
||||
"TOAST_LOGS_COPIED": "Gekopieerd!",
|
||||
"TOAST_PEER_JOINED": "{name} neemt deel aan de kamer",
|
||||
"TOAST_PEER_LEFT": "{name} heeft de kamer verlaten",
|
||||
"TOAST_PEER_ACTION": "{name} {action}",
|
||||
"STATUS_CONNECTED": "Verbonden",
|
||||
"STATUS_RECONNECTING": "Opnieuw verbinden...",
|
||||
"STATUS_CONNECTING": "Verbinden...",
|
||||
"STATUS_FAILED": "Mislukt",
|
||||
"STATUS_DISCONNECTED": "Verbinding verbroken",
|
||||
"STATUS_IDLE": "Gereed om te verbinden",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync is gereed. Neem deel aan een kamer of maak er een aan om te verbinden en te synchroniseren.",
|
||||
"BTN_STATE_JOINING": "🚀 Deelnemen...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Opnieuw verbinden...",
|
||||
"BTN_STATE_PLAYING": "▶ Afspelen...",
|
||||
"BTN_STATE_PAUSING": "⏸ Pauzeren...",
|
||||
"BTN_STATE_SYNCING_GROUP": "Synchroniseren met groep ({time})...",
|
||||
"BTN_STATE_SYNCING": "Synchroniseren...",
|
||||
"BTN_STATE_SYNCED": "✅ Gesynchroniseerd!",
|
||||
"NOTIF_PLAY": "is begonnen met afspelen",
|
||||
"NOTIF_PAUSE": "heeft afspelen gepauzeerd",
|
||||
"NOTIF_SEEK": "heeft de video gespoeld",
|
||||
"NOTIF_FORCE_PREPARE": "is een geforceerde sync gestart",
|
||||
"NOTIF_FORCE_EXECUTE": "heeft iedereen gesynchroniseerd",
|
||||
"DEBUG_NO_TAB": "Geen doeltabblad geselecteerd.",
|
||||
"DEBUG_COMM_FAIL": "Kon niet communiceren met de videotab.",
|
||||
"EMPTY_PEERS_TITLE": "Nog geen deelnemers",
|
||||
"EMPTY_PEERS_HINT": "Deel uw uitnodigingslink om te beginnen",
|
||||
"EMPTY_HISTORY_TITLE": "Nog geen activiteit",
|
||||
"EMPTY_HISTORY_HINT": "Speel af, pauzeer of spoel om geschiedenis te zien",
|
||||
"EMPTY_LOGS_TITLE": "Geen logs",
|
||||
"EMPTY_LOGS_HINT": "Verbindingsgebeurtenissen verschijnen hier",
|
||||
"EMPTY_ROOMS_TITLE": "Geen actieve kamers",
|
||||
"EMPTY_ROOMS_HINT": "Maak een kamer of vernieuw om openbare te vinden",
|
||||
"LABEL_YOU": "Jij",
|
||||
"ONBOARDING_DONE": "Klaar!",
|
||||
"LABEL_LOBBY_PEER_READY": "Gereed",
|
||||
"LABEL_LOBBY_PEER_LOADING": "Laden...",
|
||||
"LABEL_PASSWORD_PROTECTED": "Wachtwoord beveiligd",
|
||||
"LABEL_PEERS_COUNT": "{count} deelnemers",
|
||||
"LABEL_CUSTOM_SERVER": "Aangepaste server",
|
||||
"BTN_STATE_CREATING": "🚀 Kamer maken...",
|
||||
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Episode Sync mislukt",
|
||||
"NOTIF_LOBBY_CANCEL_MSG": "Auto-sync geannuleerd: {reason}. Mogelijk moet u handmatig synchroniseren.",
|
||||
"LOBBY_CANCEL_TIMEOUT": "Time-out",
|
||||
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Time-out (hersteld)",
|
||||
"LOBBY_CANCEL_PEERS_LEFT": "Alle andere deelnemers zijn vertrokken",
|
||||
"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": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Beoordelen",
|
||||
"FOOTER_SUPPORT_PROMPT": "Vind je KoalaSync leuk? Laat een review achter!",
|
||||
"LABEL_AUDIO_PROCESSING": "Audioverwerking",
|
||||
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Past audio-effecten zoals compressie toe op video-afspelen",
|
||||
"AUDIO_OPEN_SETTINGS": "Openen",
|
||||
"NEW_FEATURE_AUDIO": "Nieuw: Audioverwerking — probeer de compressor!",
|
||||
"AUDIO_BACK": "← Terug",
|
||||
"AUDIO_PAGE_TITLE": "Audio-instellingen",
|
||||
"AUDIO_MASTER_TOGGLE": "Audioverwerking",
|
||||
"AUDIO_COMPRESSOR": "Compressor",
|
||||
"AUDIO_COMPRESSOR_ENABLE": "Ingeschakeld",
|
||||
"AUDIO_PRESET": "Voorinstelling",
|
||||
"AUDIO_PRESET_RECOMMENDED": "Aanbevolen",
|
||||
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamisch bereik",
|
||||
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Stemverbetering",
|
||||
"AUDIO_PRESET_SMOOTH": "Zacht",
|
||||
"AUDIO_PRESET_CUSTOM": "Aangepast",
|
||||
"AUDIO_PARAM_THRESHOLD": "Drempel",
|
||||
"AUDIO_PARAM_KNEE": "Knee",
|
||||
"AUDIO_PARAM_RATIO": "Ratio",
|
||||
"AUDIO_PARAM_ATTACK": "Attack",
|
||||
"AUDIO_PARAM_RELEASE": "Release",
|
||||
"AUDIO_EQUALIZER": "Equalizer",
|
||||
"AUDIO_COMING_SOON": "Binnenkort beschikbaar",
|
||||
"BTN_RESTART_TOUR": "Tutorial opnieuw starten",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Start de introductie-tutorial opnieuw",
|
||||
"HINT_SELECT_VIDEO": "Selecteer hier je video!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Volledige titel verzenden",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Afleverings-ID verzenden",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Niet verzenden",
|
||||
"LABEL_PRIVACY_SETTINGS": "Privacy-instellingen",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Kies welke tab- en mediatitels naar de kamer worden gestuurd.",
|
||||
"LABEL_SEND_TAB_TITLE": "Tabtitel verzenden",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Stuurt de titel van het geselecteerde browsertabblad naar de kamer.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Mediatitel",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Kies hoe mediatitels naar de kamer worden gestuurd."
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"LANG_CODE": "pl",
|
||||
"HTML_CLASS": "lang-pl",
|
||||
"APP_TITLE": "KoalaSync",
|
||||
"TAB_ROOM": "Pokój",
|
||||
"TAB_ROOM_TOOLTIP": "Ustawienia pokoju i połączenie",
|
||||
"TAB_SYNC": "Sync",
|
||||
"TAB_SYNC_TOOLTIP": "Sterowanie synchronizacją wideo i zdalne akcje",
|
||||
"TAB_SETTINGS": "Ustawienia",
|
||||
"TAB_SETTINGS_TOOLTIP": "Preferencje rozszerzenia",
|
||||
"TAB_STATUS": "Status",
|
||||
"TAB_STATUS_TOOLTIP": "Zaawansowana diagnostyka i logi",
|
||||
"BTN_CREATE_ROOM": "+ Utwórz nowy pokój",
|
||||
"MANUAL_CONNECT_HEADER": "Połączenie ręczne / Zaawansowane",
|
||||
"LABEL_SERVER": "Serwer",
|
||||
"BTN_SERVER_OFFICIAL": "Oficjalny",
|
||||
"BTN_SERVER_OFFICIAL_TOOLTIP": "Użyj oficjalnego, niezawodnego serwera",
|
||||
"BTN_SERVER_CUSTOM": "Własny",
|
||||
"BTN_SERVER_CUSTOM_TOOLTIP": "Połącz się z własnym serwerem",
|
||||
"PLACEHOLDER_SERVER_URL": "wss://twoj-serwer:3000",
|
||||
"LABEL_ROOM_ID": "ID pokoju",
|
||||
"LABEL_ROOM_ID_TOOLTIP": "Unikalny identyfikator pokoju synchronizacji",
|
||||
"PLACEHOLDER_ROOM_ID": "Wpisz ID pokoju",
|
||||
"PLACEHOLDER_ROOM_ID_TOOLTIP": "Unikalny identyfikator pokoju, do którego chcesz dołączyć",
|
||||
"LABEL_PASSWORD": "Hasło (opcjonalnie)",
|
||||
"LABEL_PASSWORD_TOOLTIP": "Opcjonalne hasło ograniczające dostęp do pokoju",
|
||||
"PLACEHOLDER_PASSWORD": "Hasło pokoju (opcjonalnie)",
|
||||
"PLACEHOLDER_PASSWORD_TOOLTIP": "Hasło do pokoju (pozostaw puste, jeśli brak)",
|
||||
"BTN_JOIN_ROOM": "Dołącz / Utwórz pokój",
|
||||
"BTN_JOIN_ROOM_TOOLTIP": "Połącz z pokojem",
|
||||
"LABEL_PUBLIC_ROOMS": "Pokoje publiczne",
|
||||
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Lista publicznie dostępnych pokoi na tym serwerze",
|
||||
"BTN_REFRESH": "ODŚWIEŻ",
|
||||
"BTN_REFRESH_TOOLTIP": "Odśwież listę pokoi publicznych",
|
||||
"PUBLIC_ROOMS_REFRESHING": "Odświeżanie...",
|
||||
"BTN_REFRESH_COOLDOWN": "CZEKAJ {seconds}s",
|
||||
"BTN_REFRESH_COOLDOWN_TOOLTIP": "Odświeżanie listy pokoi stygnie. Spróbuj ponownie za {seconds}s.",
|
||||
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Odświeżanie pokoi publicznych. Następne odświeżenie za {seconds}s.",
|
||||
"LABEL_ACTIVE_ROOM": "Aktywny pokój",
|
||||
"LABEL_ACTIVE_ROOM_TOOLTIP": "Pokój, z którym jesteś aktualnie połączony",
|
||||
"ACTIVE_ROOM_NONE": "BRAK",
|
||||
"ACTIVE_SERVER_OFFICIAL": "Oficjalny serwer",
|
||||
"LABEL_INVITE_LINK": "Link do zaproszenia",
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Udostępnij ten link znajomym, aby mogli dołączyć",
|
||||
"LABEL_PEERS_IN_ROOM": "Uczestnicy w pokoju",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Inni użytkownicy aktualnie połączeni z tym pokojem",
|
||||
"LABEL_HOST_CONTROL": "Kontrola hosta",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Określ, kto może sterować odtwarzaniem w tym pokoju",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Tylko ja mogę sterować odtwarzaniem",
|
||||
"NOTICE_HOST_CONTROLS": "Host steruje odtwarzaniem dla wszystkich.",
|
||||
"NOTICE_COHOST_HINT": "Wskazówka: przyznaj sterowanie wybranym uczestnikom na liście poniżej.",
|
||||
"BADGE_HOST": "Host",
|
||||
"BADGE_GUEST": "Gość",
|
||||
"BADGE_CONTROLLER": "Sterujący",
|
||||
"BTN_GIVE_CONTROL": "Daj sterowanie",
|
||||
"BTN_REVOKE_CONTROL": "Odbierz",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Ogląda samodzielnie — ignoruje polecenia hosta",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · Host steruje tym pokojem",
|
||||
"HCM_DIALOG_BODY": "Tylko host może sterować odtwarzaniem w tym pokoju. Oglądać dalej razem czy oglądać samodzielnie?",
|
||||
"HCM_DIALOG_STAY": "Pozostań zsynchronizowany",
|
||||
"HCM_DIALOG_SOLO": "Oglądaj samodzielnie",
|
||||
"HCM_BADGE_SOLO": "Oglądasz samodzielnie",
|
||||
"HCM_BADGE_RESYNC": "Zsynchronizuj",
|
||||
"NO_PEERS_CONNECTED": "Brak połączonych uczestników",
|
||||
"BTN_LEAVE_ROOM": "Opuść pokój",
|
||||
"LABEL_SELECT_VIDEO": "Wybierz wideo",
|
||||
"LABEL_SELECT_VIDEO_TOOLTIP": "Wybierz kartę przeglądarki zawierającą wideo do synchronizacji",
|
||||
"OPTION_SELECT_TAB": "-- Wybierz kartę --",
|
||||
"LABEL_REMOTE_CONTROL": "Zdalne sterowanie",
|
||||
"BTN_COPY_INVITE": "📋 Link do zaproszenia",
|
||||
"BTN_COPY_INVITE_TOOLTIP": "Kopiuj link zaproszenia",
|
||||
"BTN_PLAY": "▶ Odtwórz",
|
||||
"BTN_PLAY_TOOLTIP": "Wyślij polecenie odtwarzania do wszystkich",
|
||||
"BTN_PAUSE": "⏸ Pauza",
|
||||
"BTN_PAUSE_TOOLTIP": "Wyślij polecenie pauzy do wszystkich",
|
||||
"BTN_SYNC": "⚡ SYNC",
|
||||
"BTN_SYNC_TOOLTIP": "Wymuś synchronizację wszystkich użytkowników",
|
||||
"OPTION_JUMP_TO_OTHERS": "Skocz do innych",
|
||||
"OPTION_JUMP_TO_ME": "Skocz do mnie",
|
||||
"OPTION_JUMP_MODE_TOOLTIP": "Wybierz cel synchronizacji",
|
||||
"LABEL_LAST_ACTIVITY": "Status ostatniej aktywności",
|
||||
"LABEL_LAST_ACTIVITY_TOOLTIP": "Pokazuje najnowsze polecenie odtwarzania, pauzy lub przewijania",
|
||||
"NO_RECENT_COMMANDS": "Brak ostatnich poleceń",
|
||||
"LOBBY_HEADER": "LOBBY ODCINKA",
|
||||
"LOBBY_WAITING_FOR": "🎬 Czekam na: \"{title}\"",
|
||||
"LOBBY_WAITING_PEERS": "Oczekiwanie na uczestników...",
|
||||
"BTN_SKIP_PLAY": "Pomiń i odtwórz mimo to",
|
||||
"BTN_SKIP_PLAY_TOOLTIP": "Anuluj lobby i odtwórz mimo to",
|
||||
"LOBBY_CONNECT_FIRST": "Najpierw połącz się z pokojem",
|
||||
"LOBBY_CONNECT_FIRST_DESC": "Musisz dołączyć do pokoju przez link zaproszenia lub utworzyć nowy, aby synchronizować wideo.",
|
||||
"BTN_CREATE_ROOM_ALT": "Utwórz nowy pokój",
|
||||
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Utwórz nowy losowy pokój i dołącz",
|
||||
"LABEL_USERNAME": "Twoja nazwa użytkownika",
|
||||
"LABEL_USERNAME_TOOLTIP": "Nazwa użytkownika pomaga innym Cię zidentyfikować.",
|
||||
"PLACEHOLDER_USERNAME": "Anonimowy Koala",
|
||||
"LABEL_HIDE_CLUTTER": "Ukryj zbędne karty",
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtruje karty bez wideo i niepowiązane domeny, aby zachować porządek",
|
||||
"LABEL_AUTO_SYNC_NEXT": "Auto-sync następnego odcinka",
|
||||
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Automatycznie wstrzymuje i czeka na wszystkich uczestników po zmianie odcinka, a następnie uruchamia się wspólnie.",
|
||||
"LABEL_AUTO_COPY_INVITE": "Auto-kopiowanie zaproszenia",
|
||||
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Automatycznie kopiuje link zaproszenia do schowka po utworzeniu nowego pokoju.",
|
||||
"LABEL_NOTIFICATIONS": "Powiadomienia przeglądarki",
|
||||
"LABEL_NOTIFICATIONS_TOOLTIP": "Pokazuje natywne powiadomienia systemowe, gdy ktoś dołącza/odchodzi lub odtwarza/wstrzymuje.",
|
||||
"LABEL_LANGUAGE": "Język aplikacji",
|
||||
"LABEL_LANGUAGE_TOOLTIP": "Wybierz preferowany język rozszerzenia",
|
||||
"LABEL_TROUBLESHOOTING": "Rozwiązywanie problemów",
|
||||
"LABEL_TROUBLESHOOTING_TOOLTIP": "Narzędzia do rozwiązywania problemów z połączeniem",
|
||||
"BTN_REGEN_ID": "Wygeneruj ponownie ID",
|
||||
"BTN_REGEN_ID_TOOLTIP": "Wygeneruj ponownie swój identyfikator i połącz się ponownie",
|
||||
"REGEN_ID_DESC": "Użyj tego, jeśli widzisz błędy o zduplikowanej tożsamości.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Inny problem? Otwórz Issue na GitHub",
|
||||
"TOAST_ID_REGENERATED": "Tożsamość wygenerowana ponownie — ponowne łączenie…",
|
||||
"LABEL_CONN_STATUS": "Status połączenia",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Bieżący stan połączenia WebSocket",
|
||||
"CONN_STATUS_DISCONNECTED": "Rozłączono",
|
||||
"BTN_RETRY": "PONÓW",
|
||||
"BTN_RETRY_TOOLTIP": "Próba ponownego połączenia z serwerem",
|
||||
"BTN_COPY_LOGS": "Kopiuj logi",
|
||||
"BTN_COPY_LOGS_TOOLTIP": "Kopiuj logi do schowka, aby je udostępnić",
|
||||
"LABEL_VIDEO_DEBUG": "Info debugowania wideo",
|
||||
"LABEL_VIDEO_DEBUG_TOOLTIP": "Szczegóły techniczne wybranego elementu wideo",
|
||||
"VIDEO_DEBUG_EMPTY": "Nie wybrano karty ani nie wykryto wideo.",
|
||||
"LABEL_HISTORY": "Pełna historia akcji",
|
||||
"LABEL_HISTORY_TOOLTIP": "Chronologiczny rejestr wszystkich poleceń synchronizacji w pokoju",
|
||||
"HISTORY_EMPTY": "Brak aktywności",
|
||||
"LABEL_LOGS": "Logi (ostatnie 50)",
|
||||
"LABEL_LOGS_TOOLTIP": "Logi połączenia technicznego do debugowania",
|
||||
"BTN_CLEAR": "WYCZYŚĆ",
|
||||
"BTN_CLEAR_TOOLTIP": "Wyczyść dane wyjściowe logów",
|
||||
"LABEL_GITHUB": "Repozytorium GitHub",
|
||||
"BTN_ONBOARDING_SKIP": "Pomiń",
|
||||
"BTN_ONBOARDING_SKIP_TOOLTIP": "Pomiń samouczek",
|
||||
"BTN_ONBOARDING_NEXT": "Dalej",
|
||||
"BTN_ONBOARDING_NEXT_TOOLTIP": "Przejdź do następnego kroku",
|
||||
"ONBOARDING_1_TITLE": "Witaj w KoalaSync!",
|
||||
"ONBOARDING_1_TEXT": "Oglądaj filmy w idealnej synchronizacji — bez względu na to, gdzie jesteś. Zróbmy szybką wycieczkę!",
|
||||
"ONBOARDING_2_TITLE": "1. Utwórz pokój",
|
||||
"ONBOARDING_2_TEXT": "Zacznij tutaj. Utwórz pokój i udostępnij link zaproszenia znajomym.",
|
||||
"ONBOARDING_3_TITLE": "2. Wybierz wideo",
|
||||
"ONBOARDING_3_TEXT": "Przejdź tutaj, aby wybrać wideo, które chcesz synchronizować. Odtwarzaj, wstrzymuj i przewijaj — każdy pozostaje zsynchronizowany.",
|
||||
"ONBOARDING_4_TITLE": "3. Personalizacja",
|
||||
"ONBOARDING_4_TEXT": "Wybierz fajną nazwę użytkownika, aby znajomi wiedzieli, kim jesteś.",
|
||||
"ONBOARDING_5_TITLE": "Wszystko gotowe!",
|
||||
"ONBOARDING_5_TEXT": "Czas na popcorn. Miłego wspólnego oglądania!",
|
||||
"ERR_CONN_TIMEOUT": "Przekroczono limit czasu połączenia. Spróbuj ponownie.",
|
||||
"ERR_INVALID_SERVER_URL": "Nieprawidłowy format URL serwera.",
|
||||
"ERR_IDENTITY_NOT_LOADED": "Tożsamość nie została jeszcze załadowana. Odczekaj chwilę i spróbuj ponownie.",
|
||||
"ERR_NO_PEERS_TIME": "Brak innych uczestników ze znanym czasem. Przełącz na 'Skocz do mnie'.",
|
||||
"ERR_NO_VIDEO_TAB": "Nie można połączyć się z kartą wideo.",
|
||||
"ERR_SELECT_VIDEO": "Najpierw wybierz wideo!",
|
||||
"TOAST_INVITE_COPIED": "Link zaproszenia skopiowany!",
|
||||
"TOAST_COPY_FAILED": "Nie udało się skopiować do schowka",
|
||||
"TOAST_LOBBY_SKIPPED": "Pominięto lobby odcinka.",
|
||||
"TOAST_LOBBY_SKIP_FAILED": "Nie udało się pominąć lobby.",
|
||||
"TOAST_LOGS_COPIED": "Skopiowano!",
|
||||
"TOAST_PEER_JOINED": "{name} dołączył do pokoju",
|
||||
"TOAST_PEER_LEFT": "{name} opuścił pokój",
|
||||
"TOAST_PEER_ACTION": "{name} {action}",
|
||||
"STATUS_CONNECTED": "Połączono",
|
||||
"STATUS_RECONNECTING": "Ponowne łączenie...",
|
||||
"STATUS_CONNECTING": "Łączenie...",
|
||||
"STATUS_FAILED": "Nieudane",
|
||||
"STATUS_DISCONNECTED": "Rozłączono",
|
||||
"STATUS_IDLE": "Gotowy do połączenia",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync jest gotowy. Dołącz do pokoju lub utwórz go, aby się połączyć i rozpocząć synchronizację.",
|
||||
"BTN_STATE_JOINING": "🚀 Dołączanie...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Ponowne łączenie...",
|
||||
"BTN_STATE_PLAYING": "▶ Odtwarzanie...",
|
||||
"BTN_STATE_PAUSING": "⏸ Wstrzymywanie...",
|
||||
"BTN_STATE_SYNCING_GROUP": "Synchronizacja do grupy ({time})...",
|
||||
"BTN_STATE_SYNCING": "Synchronizacja...",
|
||||
"BTN_STATE_SYNCED": "✅ Zsynchronizowano!",
|
||||
"NOTIF_PLAY": "rozpoczął odtwarzanie",
|
||||
"NOTIF_PAUSE": "wstrzymał odtwarzanie",
|
||||
"NOTIF_SEEK": "przewinął wideo",
|
||||
"NOTIF_FORCE_PREPARE": "wymusił synchronizację",
|
||||
"NOTIF_FORCE_EXECUTE": "zsynchronizował wszystkich",
|
||||
"DEBUG_NO_TAB": "Nie wybrano karty docelowej.",
|
||||
"DEBUG_COMM_FAIL": "Nie można skomunikować się z wideo w karcie.",
|
||||
"EMPTY_PEERS_TITLE": "Brak uczestników",
|
||||
"EMPTY_PEERS_HINT": "Udostępnij link zaproszenia, aby rozpocząć",
|
||||
"EMPTY_HISTORY_TITLE": "Brak aktywności",
|
||||
"EMPTY_HISTORY_HINT": "Odtwórz, wstrzymaj lub przewiń, aby zobaczyć historię",
|
||||
"EMPTY_LOGS_TITLE": "Brak logów",
|
||||
"EMPTY_LOGS_HINT": "Zdarzenia połączenia pojawią się tutaj",
|
||||
"EMPTY_ROOMS_TITLE": "Brak aktywnych pokoi",
|
||||
"EMPTY_ROOMS_HINT": "Utwórz pokój lub odśwież, aby znaleźć publiczne",
|
||||
"LABEL_YOU": "Ty",
|
||||
"ONBOARDING_DONE": "Gotowe!",
|
||||
"LABEL_LOBBY_PEER_READY": "Gotowy",
|
||||
"LABEL_LOBBY_PEER_LOADING": "Ładowanie...",
|
||||
"LABEL_PASSWORD_PROTECTED": "Chronione hasłem",
|
||||
"LABEL_PEERS_COUNT": "{count} uczestników",
|
||||
"LABEL_CUSTOM_SERVER": "Własny serwer",
|
||||
"BTN_STATE_CREATING": "🚀 Tworzenie pokoju...",
|
||||
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Synchronizacja odcinka nieudana",
|
||||
"NOTIF_LOBBY_CANCEL_MSG": "Auto-sync anulowane: {reason}. Może być konieczna ręczna synchronizacja.",
|
||||
"LOBBY_CANCEL_TIMEOUT": "Limit czasu",
|
||||
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Limit czasu (przywrócono)",
|
||||
"LOBBY_CANCEL_PEERS_LEFT": "Wszyscy inni uczestnicy wyszli",
|
||||
"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": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Oceń",
|
||||
"FOOTER_SUPPORT_PROMPT": "Podoba Ci się KoalaSync? Zostaw recenzję!",
|
||||
"LABEL_AUDIO_PROCESSING": "Przetwarzanie dźwięku",
|
||||
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Stosuje efekty dźwiękowe, takie jak kompresja, do odtwarzania wideo",
|
||||
"AUDIO_OPEN_SETTINGS": "Otwórz",
|
||||
"NEW_FEATURE_AUDIO": "Nowość: Przetwarzanie dźwięku — wypróbuj kompresor!",
|
||||
"AUDIO_BACK": "← Wstecz",
|
||||
"AUDIO_PAGE_TITLE": "Ustawienia dźwięku",
|
||||
"AUDIO_MASTER_TOGGLE": "Przetwarzanie dźwięku",
|
||||
"AUDIO_COMPRESSOR": "Kompresor",
|
||||
"AUDIO_COMPRESSOR_ENABLE": "Włączony",
|
||||
"AUDIO_PRESET": "Preset",
|
||||
"AUDIO_PRESET_RECOMMENDED": "Zalecany",
|
||||
"AUDIO_PRESET_DYNAMIC_RANGE": "Zakres dynamiki",
|
||||
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Wzmocnienie głosu",
|
||||
"AUDIO_PRESET_SMOOTH": "Płynny",
|
||||
"AUDIO_PRESET_CUSTOM": "Niestandardowy",
|
||||
"AUDIO_PARAM_THRESHOLD": "Próg",
|
||||
"AUDIO_PARAM_KNEE": "Knee",
|
||||
"AUDIO_PARAM_RATIO": "Ratio",
|
||||
"AUDIO_PARAM_ATTACK": "Attack",
|
||||
"AUDIO_PARAM_RELEASE": "Release",
|
||||
"AUDIO_EQUALIZER": "Korektor",
|
||||
"AUDIO_COMING_SOON": "Wkrótce",
|
||||
"BTN_RESTART_TOUR": "Uruchom samouczek ponownie",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Uruchom ponownie samouczek powitalny",
|
||||
"HINT_SELECT_VIDEO": "Wybierz swoje wideo tutaj!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Wysyłaj pełny tytuł",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Wysyłaj ID odcinka",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Nie wysyłaj",
|
||||
"LABEL_PRIVACY_SETTINGS": "Ustawienia prywatności",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Wybierz, które tytuły kart i multimediów są wysyłane do pokoju.",
|
||||
"LABEL_SEND_TAB_TITLE": "Wysyłaj tytuł karty",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Wysyła do pokoju tytuł wybranej karty przeglądarki.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Tytuł multimediów",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Wybierz, jak tytuły multimediów są wysyłane do pokoju."
|
||||
}
|
||||